File Coverage

blib/lib/API/INSEE/Sirene.pm
Criterion Covered Total %
statement 136 256 53.1
branch 26 68 38.2
condition 5 22 22.7
subroutine 29 38 76.3
pod 17 18 94.4
total 213 402 52.9


line stmt bran cond sub pod time code
1             package API::INSEE::Sirene;
2              
3 1     1   88556 use strict;
  1         2  
  1         29  
4 1     1   5 use warnings;
  1         2  
  1         25  
5              
6 1     1   5 use Carp 'croak';
  1         2  
  1         40  
7 1     1   722 use JSON;
  1         10913  
  1         5  
8 1     1   632 use HTTP::Request::Common qw/ GET POST /;
  1         1949  
  1         64  
9 1     1   461 use HTTP::Status ':constants';
  1         4698  
  1         375  
10 1     1   8 use List::Util 'any';
  1         1  
  1         87  
11 1     1   674 use LWP::UserAgent;
  1         21341  
  1         35  
12 1     1   544 use POSIX 'strftime';
  1         6311  
  1         15  
13 1     1   2053 use Switch;
  1         32583  
  1         8  
14              
15             our $VERSION = 4.03;
16              
17             use constant {
18 1         2906 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_LENGHT => 9,
24             MAX_SIRET_LENGHT => 14,
25             MIN_LENGHT => 3,
26 1     1   171821 };
  1         4  
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 85 my $class = shift;
96 1         4 my ($credentials, $timeout, $max_results, $proxy) = @_;
97              
98 1         7 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         5 $self->_initUserAgent();
108 1         50 $self->setProxy($proxy);
109 1         19746 $self->setMaxResults($max_results);
110 1         4 $self->setTimeout($timeout);
111              
112 1         18 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 4 my ($self, $max_results) = @_;
123              
124 1   50     8 $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 276 my ($self, $debug_value) = @_;
130              
131 1         3 $self->{'debug_mode'} = $debug_value;
132             }
133              
134             sub setProxy {
135 1     1 1 3 my ($self, $proxy) = @_;
136              
137 1 50       7 defined $proxy ? $self->{'user_agent'}->proxy([ 'http', 'https' ], $proxy) : $self->{'user_agent'}->env_proxy;
138             }
139              
140             sub setTimeout {
141 1     1 1 2 my ($self, $timeout) = @_;
142              
143 1   50     6 $timeout //= DEFAULT_TIMEOUT;
144 1         6 $self->{'user_agent'}->timeout($timeout);
145             }
146              
147             sub setCurrentEndpoint {
148 5     5 1 15 my ($self, $endpoint) = @_;
149              
150 5         13 $self->{'current_endpoint'} = $endpoint;
151             }
152              
153             sub _dumpRequest {
154 4     4   10 my ($self, $request, $response) = @_;
155              
156 4         14 my $dump = sprintf "Sent request:\n%s\n", $request->as_string;
157 4 50       480 $dump .= sprintf "Received response:\n%s\n", $response->as_string if defined $response;
158              
159 4         39 return $dump;
160             }
161              
162             sub _initUserAgent {
163 1     1   2 my $self = shift;
164              
165 1         17 $self->{'user_agent'} = LWP::UserAgent->new(protocols_allowed => [ 'http', 'https' ]);
166              
167 1         3024 $self->{'user_agent'}->agent("Perl API::INSEE::Sirene V$VERSION");
168 1         74 $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 = decode_json($response->content);
182              
183 0         0 switch ($response->code) {
  0         0  
  0         0  
184 0 0       0 case HTTP_OK {
  0         0  
185 0         0 $self->{'token_expiration'} = time + $json_obj->{'expires_in'};
186 0         0 $self->{'user_agent'}->default_header( Authorization => "Bearer $json_obj->{'access_token'}" );
187 0         0 return 0;
188 0         0 }
  0         0  
  0         0  
  0         0  
189 0 0       0 case HTTP_UNAUTHORIZED { # wrong credentials
  0         0  
190 0         0 return 1 , $json_obj->{'error_description'};
191 0         0 }
  0         0  
  0         0  
  0         0  
192             else { # oh dear we are in trouble
193 0         0 return 1, $self->_dumpRequest($request, $response);
194             }
195 0         0 }
196             }
197              
198             sub _sendRequest {
199 4     4   10 my ($self, $parameters) = @_;
200              
201 4         7 my $request;
202 4 100       11 if (!exists $parameters->{'q'}) {
203 2         3 my @url_parameters;
204              
205 2         5 foreach my $key (keys %{ $parameters }) {
  2         8  
206 4         14 push @url_parameters, join '=', $key, $parameters->{$key};
207             }
208              
209 2         9 my $endpoint = join '?', $self->{'current_endpoint'}, join '&', @url_parameters;
210 2         12 $request = GET join '/', API_BASE_URL, $endpoint;
211             }
212             else {
213             $request = POST join('/', API_BASE_URL, $self->{'current_endpoint'}),
214 2         8 Content => [ %{ $parameters } ];
  2         13  
215             }
216              
217 4 50       9507 if ($self->{'debug_mode'}) { # Requests will not be sent in debug mode
218 4         15 return 0, $self->_dumpRequest($request);
219             }
220              
221 0 0 0     0 if (!defined $self->{'token_expiration'} || $self->{'token_expiration'} < time) {
222 0         0 my ($err, $msg) = $self->_getToken();
223 0 0       0 croak $msg if $err;
224             }
225              
226 0         0 my $response = $self->{'user_agent'}->request($request);
227              
228 0         0 switch ($response->code) {
  0         0  
  0         0  
229 0 0 0     0 case HTTP_OK
230 0         0 || HTTP_NOT_FOUND {
231 0         0 return 0, $response->content;
232 0         0 }
  0         0  
  0         0  
  0         0  
233 0 0       0 case HTTP_MOVED_PERMANENTLY { # duplicated legal unit/ establishment
  0         0  
234 0         0 return 1, sprintf "%s\n%s", $response->message, $response->header('Location');
235 0         0 }
  0         0  
  0         0  
  0         0  
236 0 0 0     0 case HTTP_REQUEST_URI_TOO_LARGE
      0        
      0        
      0        
237             || HTTP_TOO_MANY_REQUESTS
238             || HTTP_UNAUTHORIZED
239             || HTTP_FORBIDDEN
240 0         0 || HTTP_SERVICE_UNAVAILABLE {
241             # There is no syntax error in request, the http message should be sufficient to understand the problem
242 0         0 return 1, $response->message;
243 0         0 }
  0         0  
  0         0  
  0         0  
244             else { # case HTTP_BAD_REQUEST || HTTP_INTERNAL_SERVER_ERROR
245 0         0 return 1, $self->_dumpRequest($request, $response);
246             }
247 0         0 }
248             }
249              
250             sub _buildParameters {
251 4     4   11 my ($self, $usefull_fields, $desired_fields, $criteria) = @_;
252              
253             # Parameters names come from the documentation
254             my $parameters = {
255             date => strftime('%Y-%m-%d', localtime),
256 4         267 nombre => $self->{'max_results'},
257             };
258 4 50 33     22 $parameters->{'champs'} = $self->_buildFields($usefull_fields, $desired_fields) if (defined $desired_fields && $desired_fields ne 'all');
259 4 100       17 $parameters->{'q'} = sprintf('(%s)', $criteria) if defined $criteria;
260              
261 4         11 return $parameters;
262             }
263              
264             sub _buildFields {
265 0     0   0 my ($self, $usefull_fields, $desired_fields) = @_;
266              
267 0 0       0 if (defined $desired_fields) {
268 0         0 return $self->_mapAliases($desired_fields);
269             }
270             else {
271 0         0 return join ',', @{ $usefull_fields };
  0         0  
272             }
273             }
274              
275             sub _mapAliases {
276 0     0   0 my ($self, $desired_fields) = @_;
277              
278 0 0       0 my @desired_fields = ref $desired_fields eq 'ARRAY' ? @{ $desired_fields } : $desired_fields;
  0         0  
279              
280 0         0 foreach my $desired_field (@desired_fields) {
281 0 0       0 if (exists $useful_fields_aliases->{$desired_field}) {
282 0 0       0 if (ref $useful_fields_aliases->{$desired_field} eq 'ARRAY') {
283 0         0 $desired_field = join ',', @{ $useful_fields_aliases->{$desired_field} };
  0         0  
284             }
285             else {
286 0         0 $desired_field = $useful_fields_aliases->{$desired_field};
287             }
288             }
289             }
290              
291 0         0 return join ',', @desired_fields;
292             }
293              
294             sub getCustomCriteria {
295 13     13 1 3086 my ($self, $field_name, $value, $search_mode) = @_;
296              
297 13 50       40 croak 'No endpoint specified.' if !defined $self->{'current_endpoint'};
298              
299 13   100     40 $search_mode //= 'aproximate';
300 13 100       31 if (exists $useful_fields_aliases->{$field_name}) {
301 1 50       5 if (ref $useful_fields_aliases->{$field_name} eq 'ARRAY') {
302 0         0 croak "Can't use the alias $field_name in custom criteria";
303             }
304 1         3 $field_name = $useful_fields_aliases->{$field_name};
305             }
306              
307 13 100       78 if ($search_mode eq 'aproximate') {
308 6         9 my @criteria;
309 6         26 my @words = split /[ \/-]/, $value;
310              
311 6         13 foreach my $word (@words) {
312 6         17 $word =~ s/&/%26/ig;
313 6         27 $word = sprintf '(%s:"%s"~ OR %s:*%s*)', $field_name, $word, $field_name, $word;
314 6 100   86   25 $word = "periode$word" if any { $_ eq $field_name } @{ $historized_fields->{$self->{'current_endpoint'}} };
  86         123  
  6         27  
315              
316              
317 6         24 push @criteria, $word;
318             }
319              
320 6         28 return join ' AND ', @criteria;
321             }
322              
323 7         14 my $criteria;
324 7         19 $value =~ s/&/%26/ig;
325              
326 7 100       25 if ($search_mode eq 'exact') {
    50          
327 5         20 $criteria = sprintf '%s:%s', $field_name, $value;
328             }
329             elsif ($search_mode eq 'begin') {
330 2         9 $criteria = sprintf '%s:%s*', $field_name, $value;
331             }
332              
333 7 100   87   41 $criteria = "periode($criteria)" if any { $_ eq $field_name } @{ $historized_fields->{$self->{'current_endpoint'}} };
  87         127  
  7         29  
334              
335 7         35 return $criteria;
336             }
337              
338             sub searchByCustomCriteria {
339 2     2 1 7 my ($self, $criteria, $desired_fields) = @_;
340              
341 2         3 my $parameters;
342 2         4 switch ($self->{'current_endpoint'}) {
  2         4  
  2         10  
343 2 50       38 case 'siren' { $parameters = $self->_buildParameters($useful_fields_legal_unit, $desired_fields, $criteria) }
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
344 2 50       34 case 'siret' { $parameters = $self->_buildParameters($useful_fields_establishment, $desired_fields, $criteria) }
  2         24  
  2         4  
  2         18  
  0         0  
  0         0  
  0         0  
345 0         0 else { croak 'Bad endpoint specified.' }
346 0         0 }
347              
348 2         7 return $self->_sendRequest($parameters);
349             }
350              
351             sub getLegalUnitBySIREN {
352 1     1 1 390 my ($self, $siren_number, $desired_fields) = @_;
353              
354 1 50       3 return 1, "Invalid SIREN $siren_number -> Must be a ${ \MAX_SIREN_LENGHT } digits number."
  0         0  
355 1         34 if $siren_number !~ m/^\d{${ \MAX_SIREN_LENGHT }}$/;
356              
357 1         7 $self->setCurrentEndpoint("siren/$siren_number");
358 1         4 my $parameters = $self->_buildParameters($useful_fields_legal_unit, $desired_fields);
359              
360 1         4 return $self->_sendRequest($parameters);
361             }
362              
363             sub searchLegalUnitBySIREN {
364 0     0 1 0 my ($self, $siren_number, $desired_fields) = @_;
365              
366 0 0       0 return 1, "Invalid SIREN $siren_number -> Must be a ${ \MIN_LENGHT } digits min and ${ \MAX_SIREN_LENGHT } digits number max."
  0         0  
  0         0  
367 0         0 if $siren_number !~ m/^\d{${ \MIN_LENGHT },${ \MAX_SIREN_LENGHT }}$/;
  0         0  
368              
369 0         0 $self->setCurrentEndpoint('siren');
370 0         0 my $criteria = $self->getCustomCriteria('siren', $siren_number, 'begin');
371              
372 0         0 return $self->searchByCustomCriteria($criteria, $desired_fields);
373             }
374              
375             sub getEstablishmentBySIRET {
376 1     1 1 1449 my ($self, $siret_number, $desired_fields) = @_;
377              
378 1 50       3 return 1, "Invalid SIRET $siret_number -> Must be a ${ \MAX_SIRET_LENGHT } digits number."
  0         0  
379 1         23 if $siret_number !~ m/^\d{${ \MAX_SIRET_LENGHT }}$/;
380              
381 1         6 $self->setCurrentEndpoint("siret/$siret_number");
382 1         4 my $parameters = $self->_buildParameters($useful_fields_establishment, $desired_fields);
383              
384 1         5 return $self->_sendRequest($parameters);
385             }
386              
387             sub getEstablishmentsBySIREN {
388 0     0 1 0 my ($self, $siren_number, $desired_fields) = @_;
389              
390 0 0       0 return (1, "Invalid SIREN $siren_number -> Must be a ${ \MAX_SIREN_LENGHT } digits number.")
  0         0  
391 0         0 if $siren_number !~ m/^\d{${ \MAX_SIREN_LENGHT }}$/;
392              
393 0         0 $self->setCurrentEndpoint('siret');
394 0         0 my $criteria = $self->getCustomCriteria('siren', $siren_number);
395              
396 0         0 return $self->searchByCustomCriteria($criteria, $desired_fields);
397             }
398              
399             sub searchEstablishmentBySIRET {
400 0     0 1 0 my ($self, $siret_number, $desired_fields) = @_;
401              
402 0 0       0 return 1, "Invalid SIRET $siret_number -> Must be a ${ \MIN_LENGHT } digits min and a ${ \MAX_SIRET_LENGHT } digits number max."
  0         0  
  0         0  
403 0         0 if $siret_number !~ m/^\d{${ \MIN_LENGHT },${ \MAX_SIRET_LENGHT }}$/;
  0         0  
404              
405 0         0 $self->setCurrentEndpoint('siret');
406 0         0 my $criteria = $self->getCustomCriteria('siret', $siret_number);
407              
408 0         0 return $self->searchByCustomCriteria($criteria, $desired_fields);
409             }
410              
411             sub getLegalUnitsByName {
412 0     0 1 0 my ($self, $name, $desired_fields) = @_;
413              
414 0         0 $self->setCurrentEndpoint('siren');
415 0         0 my $criteria = $self->getCustomCriteria('denominationUniteLegale', $name);
416              
417 0         0 return $self->searchByCustomCriteria($criteria, $desired_fields);
418             }
419              
420             sub getEstablishmentsByName {
421 1     1 1 1810 my ($self, $name, $desired_fields) = @_;
422              
423 1         10 $self->setCurrentEndpoint('siret');
424 1         4 my $criteria = $self->getCustomCriteria('denominationUniteLegale', $name);
425              
426 1         4 return $self->searchByCustomCriteria($criteria, $desired_fields);
427             }
428              
429             sub getLegalUnitsByUsualName {
430 0     0 1 0 my ($self, $name, $desired_fields) = @_;
431              
432              
433 0         0 $self->setCurrentEndpoint('siren');
434 0         0 my $criteria = $self->getCustomCriteria('denominationUsuelle1UniteLegale', $name);
435              
436 0         0 return $self->searchByCustomCriteria($criteria, $desired_fields);
437             }
438              
439             sub getEstablishmentsByUsualName {
440 1     1 1 1440 my ($self, $name, $desired_fields) = @_;
441              
442 1         4 $self->setCurrentEndpoint('siret');
443 1         3 my $criteria = $self->getCustomCriteria('denominationUsuelle1UniteLegale', $name);
444              
445 1         4 return $self->searchByCustomCriteria($criteria, $desired_fields);
446             }
447              
448             1;
449              
450             __END__