File Coverage

blib/lib/API/INSEE/Sirene.pm
Criterion Covered Total %
statement 139 259 53.6
branch 26 68 38.2
condition 5 22 22.7
subroutine 30 39 76.9
pod 17 18 94.4
total 217 406 53.4


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