File Coverage

blib/lib/API/INSEE/Sirene.pm
Criterion Covered Total %
statement 136 259 52.5
branch 26 70 37.1
condition 5 12 41.6
subroutine 29 38 76.3
pod 17 18 94.4
total 213 397 53.6


line stmt bran cond sub pod time code
1             package API::INSEE::Sirene;
2              
3 1     1   101286 use strict;
  1         2  
  1         32  
4 1     1   5 use warnings;
  1         2  
  1         35  
5              
6 1     1   6 use Carp 'croak';
  1         2  
  1         130  
7 1     1   781 use JSON;
  1         11922  
  1         8  
8 1     1   827 use HTTP::Request::Common qw/ GET POST /;
  1         2066  
  1         90  
9 1     1   669 use HTTP::Status ':constants';
  1         4985  
  1         459  
10 1     1   10 use List::Util 'any';
  1         2  
  1         114  
11 1     1   889 use LWP::UserAgent;
  1         23913  
  1         54  
12 1     1   799 use POSIX 'strftime';
  1         7437  
  1         23  
13 1     1   2484 use Switch;
  1         30288  
  1         8  
14              
15             our $VERSION = 4.04;
16              
17             use constant {
18 1         3139 API_AUTH_URL => 'https://api.insee.fr/token',
19             API_BASE_URL => 'https://api.insee.fr/entreprises/sirene/V3',
20             DEFAULT_MAX_RESULTS => 20, # from documentation
21             DEFAULT_TIMEOUT => 20,
22             HARD_MAX_RESULTS => 1_000, # from documentation
23             MAX_SIREN_LENGTH => 9,
24             MAX_SIRET_LENGTH => 14,
25             MIN_LENGTH => 3,
26 1     1   180931 };
  1         2  
27              
28             my $EMPTY = q//;
29              
30             my $historized_fields = {
31             siren => [ qw/
32             dateFin dateDebut
33             etatAdministratifUniteLegale changementEtatAdministratifUniteLegale
34             nomUniteLegale changementNomUniteLegale nomUsageUniteLegale changementNomUsageUniteLegale
35             denominationUniteLegale changementDenominationUniteLegale denominationUsuelle1UniteLegale
36             denominationUsuelle2UniteLegale denominationUsuelle3UniteLegale changementDenominationUsuelleUniteLegale
37             categorieJuridiqueUniteLegale changementCategorieJuridiqueUniteLegale activitePrincipaleUniteLegale
38             nomenclatureActivitePrincipaleUniteLegale changementActivitePrincipaleUniteLegale nicSiegeUniteLegale
39             changementNicSiegeUniteLegale economieSocialeSolidaireUniteLegale changementEconomieSocialeSolidaireUniteLegale
40             caractereEmployeurUniteLegale changementCaractereEmployeurUniteLegale
41             / ],
42             siret => [ qw/
43             etatAdministratifEtablissement changementEtatAdministratifEtablissement
44             enseigne1Etablissement enseigne2Etablissement enseigne3Etablissement changementEnseigneEtablissement
45             denominationUsuelleEtablissement changementDenominationUsuelleEtablissement
46             activitePrincipaleEtablissement nomenclatureActivitePrincipaleEtablissement changementActivitePrincipaleEtablissement
47             caractereEmployeurEtablissement changementCaractereEmployeurEtablissement
48             / ],
49             };
50              
51             my $useful_fields_legal_unit = [
52             qw/
53             siren
54             dateCreationUniteLegale
55             sigleUniteLegale
56             categorieEntreprise
57             denominationUniteLegale denominationUsuelle1UniteLegale nomUniteLegale
58             categorieJuridiqueUniteLegale
59             activitePrincipaleUniteLegale nomenclatureActivitePrincipaleUniteLegale
60             nicSiegeUniteLegale
61             /
62             ];
63              
64             my $useful_fields_establishment = [
65             qw/
66             siren siret
67             denominationUsuelleEtablissement denominationUniteLegale denominationUsuelle1UniteLegale nomUniteLegale
68             activitePrincipaleUniteLegale
69             numeroVoieEtablissement typeVoieEtablissement libelleVoieEtablissement
70             codePostalEtablissement libelleCommuneEtablissement
71             /
72             ];
73              
74             my $useful_fields_aliases = {
75             nicSiege => 'nicSiegeUniteLegale',
76             nom => [ 'denominationUniteLegale', 'nomUniteLegale' ],
77             dateCreation => 'dateCreationUniteLegale',
78             sigle => 'sigleUniteLegale',
79             categorieJuridique => 'categorieJuridiqueUniteLegale',
80             nomenclatureActivitePrincipale => 'nomenclatureActivitePrincipaleUniteLegale',
81             activitePrincipale => 'activitePrincipaleUniteLegale',
82             numvoie => 'numeroVoieEtablissement',
83             typevoie => 'typeVoieEtablissement',
84             nomvoie => 'libelleVoieEtablissement',
85             codePostal => 'codePostalEtablissement',
86             nomCommune => 'libelleCommuneEtablissement',
87             adresseComplete => [
88             'numeroVoieEtablissement',
89             'typeVoieEtablissement', 'libelleVoieEtablissement',
90             'codePostalEtablissement', 'libelleCommuneEtablissement'
91             ],
92             };
93              
94             sub new {
95 1     1 0 102 my $class = shift;
96 1         5 my ($credentials, $timeout, $max_results, $proxy) = @_;
97              
98 1         11 my $self = bless {
99             credentials => $credentials,
100             user_agent => undef,
101             token_expiration => undef,
102             max_results => undef,
103             debug_mode => 0,
104             current_endpoint => undef,
105             }, $class;
106              
107 1         6 $self->_initUserAgent();
108 1         56 $self->setProxy($proxy);
109 1         21641 $self->setMaxResults($max_results);
110 1         6 $self->setTimeout($timeout);
111              
112 1         27 return $self;
113             }
114              
115             sub setCredentials {
116 0     0 1 0 my ($self, $credentials) = @_;
117              
118 0         0 $self->{'credentials'} = $credentials;
119             }
120              
121             sub setMaxResults {
122 1     1 1 3 my ($self, $max_results) = @_;
123              
124 1   50     10 $max_results //= DEFAULT_MAX_RESULTS;
125 1 50       5 $self->{'max_results'} = $max_results > HARD_MAX_RESULTS ? HARD_MAX_RESULTS : $max_results;
126             }
127              
128             sub setDebugMode {
129 1     1 1 308 my ($self, $debug_value) = @_;
130              
131 1         6 $self->{'debug_mode'} = $debug_value;
132             }
133              
134             sub setProxy {
135 1     1 1 3 my ($self, $proxy) = @_;
136              
137 1 50       9 defined $proxy ? $self->{'user_agent'}->proxy([ 'http', 'https' ], $proxy) : $self->{'user_agent'}->env_proxy;
138             }
139              
140             sub setTimeout {
141 1     1 1 3 my ($self, $timeout) = @_;
142              
143 1   50     8 $timeout //= DEFAULT_TIMEOUT;
144 1         8 $self->{'user_agent'}->timeout($timeout);
145             }
146              
147             sub setCurrentEndpoint {
148 5     5 1 24 my ($self, $endpoint) = @_;
149              
150 5         16 $self->{'current_endpoint'} = $endpoint;
151             }
152              
153             sub _dumpRequest {
154 4     4   12 my ($self, $request, $response) = @_;
155              
156 4         15 my $dump = sprintf "Sent request:\n%s\n", $request->as_string;
157 4 50       584 $dump .= sprintf "Received response:\n%s\n", $response->as_string if defined $response;
158              
159 4         50 return $dump;
160             }
161              
162             sub _initUserAgent {
163 1     1   2 my $self = shift;
164              
165 1         25 $self->{'user_agent'} = LWP::UserAgent->new(protocols_allowed => [ 'http', 'https' ]);
166              
167 1         3355 $self->{'user_agent'}->agent("Perl API::INSEE::Sirene V$VERSION");
168 1         129 $self->{'user_agent'}->default_header('Accept' => 'application/json');
169             }
170              
171             sub _getToken {
172 0     0   0 my $self = shift;
173              
174 0 0       0 croak 'Please provide your credentials.' if !defined $self->{'credentials'};
175              
176 0         0 my $request = POST API_AUTH_URL,
177             Authorization => "Basic $self->{'credentials'}",
178             Content => [ grant_type => 'client_credentials' ];
179              
180 0         0 my $response = $self->{'user_agent'}->request($request);
181 0         0 my $json_obj;
182              
183 0 0       0 if ($response->content_type =~ m/^application\/json/) {
184 0         0 $json_obj = decode_json($response->content);
185             }
186             else {
187 0         0 return 1, $self->_dumpRequest($request, $response); # the API may return xml intead of json...
188             }
189              
190 0         0 switch ($response->code) {
  0         0  
  0         0  
191 0 0       0 case HTTP_OK {
  0         0  
192 0         0 $self->{'token_expiration'} = time + $json_obj->{'expires_in'};
193 0         0 $self->{'user_agent'}->default_header( Authorization => "Bearer $json_obj->{'access_token'}" );
194 0         0 return 0;
195 0         0 }
  0         0  
  0         0  
  0         0  
196 0 0       0 case HTTP_UNAUTHORIZED { # wrong credentials
  0         0  
197 0         0 return 1 , $json_obj->{'error_description'};
198 0         0 }
  0         0  
  0         0  
  0         0  
199             else { # oh dear we are in trouble
200 0         0 return 1, $self->_dumpRequest($request, $response);
201             }
202 0         0 }
203             }
204              
205             sub _sendRequest {
206 4     4   12 my ($self, $parameters) = @_;
207              
208 4         8 my $request;
209 4 100       15 if (!exists $parameters->{'q'}) {
210 2         5 my @url_parameters;
211              
212 2         4 foreach my $key (keys %{ $parameters }) {
  2         11  
213 4         17 push @url_parameters, join '=', $key, $parameters->{$key};
214             }
215              
216 2         12 my $endpoint = join '?', $self->{'current_endpoint'}, join '&', @url_parameters;
217 2         17 $request = GET join '/', API_BASE_URL, $endpoint;
218             }
219             else {
220             $request = POST join('/', API_BASE_URL, $self->{'current_endpoint'}),
221 2         12 Content => [ %{ $parameters } ];
  2         18  
222             }
223              
224 4 50       11000 if ($self->{'debug_mode'}) { # Requests will not be sent in debug mode
225 4         20 return 0, $self->_dumpRequest($request);
226             }
227              
228 0 0 0     0 if (!defined $self->{'token_expiration'} || $self->{'token_expiration'} < time) {
229 0         0 my ($err, $msg) = $self->_getToken();
230 0 0       0 croak $msg if $err;
231             }
232              
233 0         0 my $response = $self->{'user_agent'}->request($request);
234              
235 0         0 switch ($response->code) {
  0         0  
  0         0  
236 0 0       0 case [ HTTP_OK, HTTP_NOT_FOUND ] {
  0         0  
237 0         0 return 0, $response->content;
238 0         0 }
  0         0  
  0         0  
  0         0  
239 0 0       0 case HTTP_MOVED_PERMANENTLY { # duplicated legal unit/ establishment
  0         0  
240 0         0 return 1, sprintf "%s\n%s", $response->message, $response->header('Location');
241 0         0 }
  0         0  
  0         0  
  0         0  
242 0 0       0 case [
243             HTTP_REQUEST_URI_TOO_LARGE, HTTP_TOO_MANY_REQUESTS,
244             HTTP_UNAUTHORIZED, HTTP_FORBIDDEN,
245             HTTP_SERVICE_UNAVAILABLE
246 0         0 ] {
247             # There is no syntax error in request, the http message should be sufficient to understand the problem
248 0         0 return 1, $response->message;
249 0         0 }
  0         0  
  0         0  
  0         0  
250             else { # case HTTP_BAD_REQUEST || HTTP_INTERNAL_SERVER_ERROR
251 0         0 return 1, $self->_dumpRequest($request, $response);
252             }
253 0         0 }
254             }
255              
256             sub _buildParameters {
257 4     4   16 my ($self, $usefull_fields, $desired_fields, $criteria) = @_;
258              
259             # Parameters names come from the documentation
260             my $parameters = {
261             date => strftime('%Y-%m-%d', localtime),
262 4         401 nombre => $self->{'max_results'},
263             };
264 4 50 33     35 $parameters->{'champs'} = $self->_buildFields($usefull_fields, $desired_fields) if (defined $desired_fields && $desired_fields ne 'all');
265 4 100       21 $parameters->{'q'} = sprintf('(%s)', $criteria) if defined $criteria;
266              
267 4         11 return $parameters;
268             }
269              
270             sub _buildFields {
271 0     0   0 my ($self, $usefull_fields, $desired_fields) = @_;
272              
273 0 0       0 if (defined $desired_fields) {
274 0         0 return $self->_mapAliases($desired_fields);
275             }
276             else {
277 0         0 return join ',', @{ $usefull_fields };
  0         0  
278             }
279             }
280              
281             sub _mapAliases {
282 0     0   0 my ($self, $desired_fields) = @_;
283              
284 0 0       0 my @desired_fields = ref $desired_fields eq 'ARRAY' ? @{ $desired_fields } : $desired_fields;
  0         0  
285              
286 0         0 foreach my $desired_field (@desired_fields) {
287 0 0       0 if (exists $useful_fields_aliases->{$desired_field}) {
288 0 0       0 if (ref $useful_fields_aliases->{$desired_field} eq 'ARRAY') {
289 0         0 $desired_field = join ',', @{ $useful_fields_aliases->{$desired_field} };
  0         0  
290             }
291             else {
292 0         0 $desired_field = $useful_fields_aliases->{$desired_field};
293             }
294             }
295             }
296              
297 0         0 return join ',', @desired_fields;
298             }
299              
300             sub getCustomCriteria {
301 13     13 1 3315 my ($self, $field_name, $value, $search_mode) = @_;
302              
303 13 50       40 croak 'No endpoint specified.' if !defined $self->{'current_endpoint'};
304              
305 13   100     45 $search_mode //= 'aproximate';
306 13 100       39 if (exists $useful_fields_aliases->{$field_name}) {
307 1 50       6 if (ref $useful_fields_aliases->{$field_name} eq 'ARRAY') {
308 0         0 croak "Can't use the alias $field_name in custom criteria";
309             }
310 1         3 $field_name = $useful_fields_aliases->{$field_name};
311             }
312              
313 13 100       32 if ($search_mode eq 'aproximate') {
314 6         13 my @criteria;
315 6         28 my @words = split /[ \/-]/, $value;
316              
317 6         17 foreach my $word (@words) {
318 6         22 $word =~ s/&/%26/ig;
319 6         32 $word = sprintf '(%s:"%s"~ OR %s:*%s*)', $field_name, $word, $field_name, $word;
320 6 100   86   33 $word = "periode$word" if any { $_ eq $field_name } @{ $historized_fields->{$self->{'current_endpoint'}} };
  86         116  
  6         37  
321              
322              
323 6         28 push @criteria, $word;
324             }
325              
326 6         34 return join ' AND ', @criteria;
327             }
328              
329 7         9 my $criteria;
330 7         21 $value =~ s/&/%26/ig;
331              
332 7 100       24 if ($search_mode eq 'exact') {
    50          
333 5         21 $criteria = sprintf '%s:%s', $field_name, $value;
334             }
335             elsif ($search_mode eq 'begin') {
336 2         10 $criteria = sprintf '%s:%s*', $field_name, $value;
337             }
338              
339 7 100   87   29 $criteria = "periode($criteria)" if any { $_ eq $field_name } @{ $historized_fields->{$self->{'current_endpoint'}} };
  87         119  
  7         26  
340              
341 7         33 return $criteria;
342             }
343              
344             sub searchByCustomCriteria {
345 2     2 1 7 my ($self, $criteria, $desired_fields) = @_;
346              
347 2         4 my $parameters;
348 2         5 switch ($self->{'current_endpoint'}) {
  2         6  
  2         17  
349 2 50       51 case 'siren' { $parameters = $self->_buildParameters($useful_fields_legal_unit, $desired_fields, $criteria) }
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
350 2 50       39 case 'siret' { $parameters = $self->_buildParameters($useful_fields_establishment, $desired_fields, $criteria) }
  2         24  
  2         9  
  2         19  
  0         0  
  0         0  
  0         0  
351 0         0 else { croak 'Bad endpoint specified.' }
352 0         0 }
353              
354 2         9 return $self->_sendRequest($parameters);
355             }
356              
357             sub getLegalUnitBySIREN {
358 1     1 1 392 my ($self, $siren_number, $desired_fields) = @_;
359              
360 1 50       4 return 1, "Invalid SIREN $siren_number -> Must be a ${ \MAX_SIREN_LENGTH } digits number."
  0         0  
361 1         61 if $siren_number !~ m/^\d{${ \MAX_SIREN_LENGTH }}$/;
362              
363 1         11 $self->setCurrentEndpoint("siren/$siren_number");
364 1         7 my $parameters = $self->_buildParameters($useful_fields_legal_unit, $desired_fields);
365              
366 1         6 return $self->_sendRequest($parameters);
367             }
368              
369             sub searchLegalUnitBySIREN {
370 0     0 1 0 my ($self, $siren_number, $desired_fields) = @_;
371              
372 0 0       0 return 1, "Invalid SIREN $siren_number -> Must be a ${ \MIN_LENGTH } digits min and ${ \MAX_SIREN_LENGTH } digits number max."
  0         0  
  0         0  
373 0         0 if $siren_number !~ m/^\d{${ \MIN_LENGTH },${ \MAX_SIREN_LENGTH }}$/;
  0         0  
374              
375 0         0 $self->setCurrentEndpoint('siren');
376 0         0 my $criteria = $self->getCustomCriteria('siren', $siren_number, 'begin');
377              
378 0         0 return $self->searchByCustomCriteria($criteria, $desired_fields);
379             }
380              
381             sub getEstablishmentBySIRET {
382 1     1 1 1731 my ($self, $siret_number, $desired_fields) = @_;
383              
384 1 50       3 return 1, "Invalid SIRET $siret_number -> Must be a ${ \MAX_SIRET_LENGTH } digits number."
  0         0  
385 1         31 if $siret_number !~ m/^\d{${ \MAX_SIRET_LENGTH }}$/;
386              
387 1         9 $self->setCurrentEndpoint("siret/$siret_number");
388 1         6 my $parameters = $self->_buildParameters($useful_fields_establishment, $desired_fields);
389              
390 1         5 return $self->_sendRequest($parameters);
391             }
392              
393             sub getEstablishmentsBySIREN {
394 0     0 1 0 my ($self, $siren_number, $desired_fields) = @_;
395              
396 0 0       0 return (1, "Invalid SIREN $siren_number -> Must be a ${ \MAX_SIREN_LENGTH } digits number.")
  0         0  
397 0         0 if $siren_number !~ m/^\d{${ \MAX_SIREN_LENGTH }}$/;
398              
399 0         0 $self->setCurrentEndpoint('siret');
400 0         0 my $criteria = $self->getCustomCriteria('siren', $siren_number);
401              
402 0         0 return $self->searchByCustomCriteria($criteria, $desired_fields);
403             }
404              
405             sub searchEstablishmentBySIRET {
406 0     0 1 0 my ($self, $siret_number, $desired_fields) = @_;
407              
408 0 0       0 return 1, "Invalid SIRET $siret_number -> Must be a ${ \MIN_LENGTH } digits min and a ${ \MAX_SIRET_LENGTH } digits number max."
  0         0  
  0         0  
409 0         0 if $siret_number !~ m/^\d{${ \MIN_LENGTH },${ \MAX_SIRET_LENGTH }}$/;
  0         0  
410              
411 0         0 $self->setCurrentEndpoint('siret');
412 0         0 my $criteria = $self->getCustomCriteria('siret', $siret_number);
413              
414 0         0 return $self->searchByCustomCriteria($criteria, $desired_fields);
415             }
416              
417             sub getLegalUnitsByName {
418 0     0 1 0 my ($self, $name, $desired_fields) = @_;
419              
420 0         0 $self->setCurrentEndpoint('siren');
421 0         0 my $criteria = $self->getCustomCriteria('denominationUniteLegale', $name);
422              
423 0         0 return $self->searchByCustomCriteria($criteria, $desired_fields);
424             }
425              
426             sub getEstablishmentsByName {
427 1     1 1 1915 my ($self, $name, $desired_fields) = @_;
428              
429 1         13 $self->setCurrentEndpoint('siret');
430 1         5 my $criteria = $self->getCustomCriteria('denominationUniteLegale', $name);
431              
432 1         7 return $self->searchByCustomCriteria($criteria, $desired_fields);
433             }
434              
435             sub getLegalUnitsByUsualName {
436 0     0 1 0 my ($self, $name, $desired_fields) = @_;
437              
438              
439 0         0 $self->setCurrentEndpoint('siren');
440 0         0 my $criteria = $self->getCustomCriteria('denominationUsuelle1UniteLegale', $name);
441              
442 0         0 return $self->searchByCustomCriteria($criteria, $desired_fields);
443             }
444              
445             sub getEstablishmentsByUsualName {
446 1     1 1 1550 my ($self, $name, $desired_fields) = @_;
447              
448 1         5 $self->setCurrentEndpoint('siret');
449 1         6 my $criteria = $self->getCustomCriteria('denominationUsuelle1UniteLegale', $name);
450              
451 1         6 return $self->searchByCustomCriteria($criteria, $desired_fields);
452             }
453              
454             1;
455              
456             __END__