File Coverage

blib/lib/Parse/SAMGov/Entity.pm
Criterion Covered Total %
statement 200 222 90.0
branch 118 156 75.6
condition 13 14 92.8
subroutine 16 16 100.0
pod 0 1 0.0
total 347 409 84.8


line stmt bran cond sub pod time code
1             package Parse::SAMGov::Entity;
2             $Parse::SAMGov::Entity::VERSION = '0.104';
3 3     3   460 use strict;
  3         4  
  3         63  
4 3     3   9 use warnings;
  3         3  
  3         48  
5 3     3   38 use 5.010;
  3         6  
6 3     3   933 use Data::Dumper;
  3         7768  
  3         122  
7 3     3   923 use Parse::SAMGov::Mo;
  3         5  
  3         10  
8 3     3   1484 use URI;
  3         8371  
  3         61  
9 3     3   2324 use DateTime;
  3         212067  
  3         97  
10 3     3   1525 use DateTime::Format::Strptime;
  3         32318  
  3         13  
11 3     3   1300 use Parse::SAMGov::Entity::Address;
  3         6  
  3         75  
12 3     3   1238 use Parse::SAMGov::Entity::PointOfContact;
  3         5  
  3         76  
13 3     3   11 use Carp;
  3         3  
  3         1623  
14              
15             #ABSTRACT: Object to denote each Entity in SAM
16              
17             use overload fallback => 1,
18             '""' => sub {
19 6     6   7638 my $self = $_[0];
20 6         6 my $str = '';
21 6 100       14 $str .= $self->name if $self->name;
22 6 100       12 $str .= ' dba ' . $self->dba_name if $self->dba_name;
23 6 50       12 $str .= "\nDUNS: " . $self->DUNS if $self->DUNS;
24 6 50       11 $str .= '+' . $self->DUNSplus4 if $self->DUNSplus4 ne '0000';
25 6 50       11 $str .= "\nCAGE: " . $self->CAGE if $self->CAGE;
26 6 100       9 $str .= "\nDODAAC: " . $self->DODAAC if $self->DODAAC;
27 6 50       12 $str .= "\nStatus: " . $self->extract_code if $self->extract_code;
28 6 100       11 $str .= "\nUpdated: Yes" if $self->updated;
29 6 50       7 $str .= "\nRegistration Purpose: " . $self->regn_purpose if $self->regn_purpose;
30 6 100       9 $str .= "\nRegistration Date: " . $self->regn_date->ymd('-') if $self->regn_date;
31 6 100       38 $str .= "\nExpiry Date: " . $self->expiry_date->ymd('-') if $self->expiry_date;
32 6 100       34 $str .= "\nLast Update Date: " . $self->lastupdate_date->ymd('-') if $self->lastupdate_date;
33 6 100       32 $str .= "\nActivation Date: " . $self->activation_date->ymd('-') if $self->activation_date;
34 6 50       32 $str .= "\nCompany Division: " . $self->company_division if $self->company_division;
35 6 50       7 $str .= "\nDivision No.: " . $self->division_no if $self->division_no;
36 6 100       24 $str .= "\nPhysical Address: " . $self->physical_address if $self->physical_address;
37 6 100       13 $str .= "\nBusiness Start Date: " . $self->start_date->ymd('-') if $self->start_date;
38 6 100       32 $str .= sprintf "\nFiscal Year End: %02d-%02d", $self->fiscalyear_date->month, $self->fiscalyear_date->day
39             if $self->fiscalyear_date;
40 6 100       26 $str .= "\nCorporate URL: " . $self->url if $self->url;
41 6         27 $str .= "\nBusiness Types: [" . join(',', @{$self->biztype}) . "]";
  6         10  
42 6         6 my $pnaics;
43 6         3 foreach my $k (keys %{$self->NAICS}) {
  6         10  
44 9 100       13 $pnaics = $k if $self->NAICS->{$k}->{is_primary};
45 9 100       14 last if $pnaics;
46             }
47 6         4 $str .= "\nNAICS Codes: [" . join(',', keys %{$self->NAICS}) . "]";
  6         10  
48 6 100       11 if ($pnaics) {
49 5 100       6 $str .= "\nSmall Business: " . ($self->NAICS->{$pnaics}->{small_biz} ? 'Yes' : 'No');
50             }
51             {
52 6         5 local $Data::Dumper::Indent = 1;
  6         6  
53 6         6 local $Data::Dumper::Terse = 1;
54 6         7 $str .= "\nNAICS Details: " . Dumper($self->NAICS);
55             }
56 6         315 $str .= "\nPSC Codes: [" . join(',', @{$self->PSC}) . "]";
  6         11  
57 6 100       10 $str .= "\nMailing Address: " . $self->mailing_address if $self->mailing_address;
58 6 100       11 $str .= "\nGovt Business POC: " . $self->POC_gov if $self->POC_gov;
59 6 100       11 $str .= "\nGovt Business POC (alternate): " . $self->POC_gov_alt if $self->POC_gov_alt;
60 6 100       12 $str .= "\nPast Performance POC: " . $self->POC_pastperf if $self->POC_pastperf;
61 6 100       10 $str .= "\nPast Performance POC (alternate): " . $self->POC_pastperf_alt if $self->POC_pastperf_alt;
62 6 100       11 $str .= "\nElectronic POC: " . $self->POC_elec if $self->POC_elec;
63 6 100       12 $str .= "\nElectronic POC (alternate): " . $self->POC_elec_alt if $self->POC_elec_alt;
64 6 50       11 $str .= "\nDelinquent Federal Debt: " . ($self->delinquent_fed_debt ? 'Yes' : 'No');
65 6 50       11 $str .= "\nExclusion Status: " . $self->exclusion_status if $self->exclusion_status;
66             {
67 6         7 local $Data::Dumper::Indent = 0;
68 6         4 local $Data::Dumper::Terse = 1;
69 6         10 $str .= "\nSBA Business Type: " . Dumper($self->SBA);
70             }
71             {
72 6         5 local $Data::Dumper::Indent = 0;
  6         189  
  6         6  
73 6         4 local $Data::Dumper::Terse = 1;
74 6         14 $str .= "\nDisaster Response Type: " . Dumper($self->disaster_response);
75             }
76 6 100       147 $str .= "\nIs Private Listing: " . ($self->is_private ? 'Yes' : 'No');
77 6         41 return $str;
78 3     3   13 };
  3         3  
  3         29  
79              
80              
81             has 'DUNS';
82             has DUNSplus4 => default => sub { '0000' };
83             has 'CAGE';
84             has 'DODAAC';
85              
86              
87             has 'extract_code';
88             has 'updated';
89             has 'regn_purpose' => coerce => sub {
90             my $p = $_[0];
91             return 'Federal Assistance Awards' if $p eq 'Z1';
92             return 'All Awards' if $p eq 'Z2';
93             return 'IGT-Only' if $p eq 'Z3';
94             return 'Federal Assistance Awards & IGT' if $p eq 'Z4';
95             return 'All Awards & IGT' if $p eq 'Z5';
96             };
97              
98             sub _parse_yyyymmdd {
99 78 50   78   118 if (@_) {
100 78         63 my $d = shift;
101 78 100       120 if (length($d) == 4) {
102 10         22 my $y = DateTime->now->year;
103 10         1419 $d = "$y$d";
104             }
105 78         67 state $Strp =
106             DateTime::Format::Strptime->new(pattern => '%Y%m%d',
107             time_zone => 'America/New_York',);
108 78         18387 return $Strp->parse_datetime($d);
109             }
110 0         0 return;
111             }
112              
113              
114             has 'regn_date' => coerce => sub { _parse_yyyymmdd $_[0] };
115             has 'expiry_date' => coerce => sub { _parse_yyyymmdd $_[0] };
116             has 'lastupdate_date' => coerce => sub { _parse_yyyymmdd $_[0] };
117             has 'activation_date' => coerce => sub { _parse_yyyymmdd $_[0] };
118              
119              
120             has 'name';
121             has 'dba_name';
122             has 'company_division';
123             has 'division_no';
124             has 'physical_address' => default => sub { return Parse::SAMGov::Entity::Address->new; };
125              
126              
127             has 'start_date' => coerce => sub { _parse_yyyymmdd $_[0] };
128             has 'fiscalyear_date' => coerce => sub { _parse_yyyymmdd $_[0] };
129             has 'url' => coerce => sub { URI->new($_[0]) };
130             has 'entity_structure';
131              
132              
133             has 'incorporation_state';
134             has 'incorporation_country';
135              
136              
137              
138             has 'biztype' => default => sub { [] };
139             has 'NAICS' => default => sub { {} };
140             has 'PSC' => default => sub { [] };
141             has 'creditcard';
142             has 'correspondence_type';
143             has 'mailing_address' => default => sub { return Parse::SAMGov::Entity::Address->new; };
144             has 'POC_gov' => default => sub { return
145             Parse::SAMGov::Entity::PointOfContact->new; };
146             has 'POC_gov_alt' => default => sub {
147             Parse::SAMGov::Entity::PointOfContact->new; };
148             has 'POC_pastperf' => default => sub {
149             Parse::SAMGov::Entity::PointOfContact->new; };
150             has 'POC_pastperf_alt' => default => sub {
151             Parse::SAMGov::Entity::PointOfContact->new; };
152             has 'POC_elec' => default => sub {
153             Parse::SAMGov::Entity::PointOfContact->new; };
154             has 'POC_elec_alt' => default => sub {
155             Parse::SAMGov::Entity::PointOfContact->new; };
156             has 'delinquent_fed_debt';
157             has 'exclusion_status';
158             has 'is_private';
159             has 'disaster_response' => default => sub { {} };
160             has 'SBA' => default => sub { {} };
161              
162             has 'SBA_descriptions' => default => sub {
163             {
164             A4 => 'SBA Certified Small Disadvantaged Business',
165             A6 => 'SBA Certified 8A Program Participant',
166             JT => 'SBA Certified 8A Joint Venture',
167             XX => 'SBA Certified HUBZone Firm',
168             }
169             };
170              
171             sub _trim {
172             # from Mojo::Util::trim
173 540     540   400 my $s = shift;
174 540         481 $s =~ s/^\s+//g;
175 540         504 $s =~ s/\s+$//g;
176 540         1074 return $s;
177             }
178              
179             sub load {
180 12     12 0 7 my $self = shift;
181 12 50       22 return unless (scalar(@_) == 150);
182 12         24 $self->DUNS(shift);
183 12   50     50 $self->DUNSplus4(shift || '0000');
184 12         21 $self->CAGE(shift);
185 12         19 $self->DODAAC(shift);
186 12         19 $self->updated(0);
187 12         9 my $code = shift;
188 12 100       51 if ($code =~ /A|2|3/x) {
    50          
189 10         15 $self->extract_code('active');
190 10 100       22 $self->updated(1) if $code eq '3';
191             } elsif ($code =~ /E|1|4/x) {
192 2         5 $self->extract_code('expired');
193 2 50       6 $self->updated(1) if $code eq '1';
194             }
195 12         20 $self->regn_purpose(shift);
196 12         16 $self->regn_date(shift);
197 12         25 $self->expiry_date(shift);
198 12         26 $self->lastupdate_date(shift);
199 12         24 $self->activation_date(shift);
200 12         18 $self->name(_trim(shift));
201 12         13 $self->dba_name(_trim(shift));
202 12         16 $self->company_division(_trim(shift));
203 12         15 $self->division_no(_trim(shift));
204 12         28 my $paddr = Parse::SAMGov::Entity::Address->new(
205             # the order of shifting matters
206             address => _trim(join(' ', shift, shift)),
207             city => shift,
208             state => shift,
209             zip => sprintf("%s-%s", shift, shift),
210             country => shift,
211             district => shift,
212             );
213 12         24 $self->physical_address($paddr);
214 12         55 $self->start_date(shift);
215 12         28 $self->fiscalyear_date(shift);
216 12         18 $self->url(_trim(shift));
217 12         26 $self->entity_structure(shift);
218 12         17 $self->incorporation_state(shift);
219 12         21 $self->incorporation_country(shift);
220 12   100     13 my $count = int(_trim(shift) || 0);
221 12 100       18 if ($count > 0) {
222 10         21 my @biztypes = grep { length($_) > 0 } split /~/, shift;
  34         43  
223 10         25 $self->biztype([@biztypes]);
224             } else {
225 2         2 shift; # ignore
226             }
227 12         13 my $pnaics = _trim(shift);
228 12         25 $self->NAICS->{$pnaics} = {};
229 12 100 100     13 $count = int(_trim(shift) || 0) + (length($pnaics) ? 1 : 0);
230 12 100       15 if ($count > 0) {
231 10         18 my @naics = grep { length($_) > 0 } split /~/, shift;
  16         21  
232 10         15 foreach my $c (@naics) {
233 16 50       47 if ($c =~ /(\d+)(Y|N|E)/) {
234 16 100       27 $self->NAICS->{$1} = {} unless ref $self->NAICS->{$1} eq 'HASH';
235 16 100       41 $self->NAICS->{$1}->{is_primary} = 1 if $pnaics eq $1;
236 16 100       30 $self->NAICS->{$1}->{small_biz} = 1 if $2 eq 'Y';
237 16 100       29 $self->NAICS->{$1}->{small_biz} = 0 if $2 eq 'N';
238 16 100       36 $self->NAICS->{$1}->{exception} = {} if $2 eq 'E';
239             }
240             }
241             } else {
242 2         2 shift; # ignore
243             }
244 12   100     13 $count = int(_trim(shift) || 0);
245 12 100       18 if ($count > 0) {
246 2         3 my @psc = grep { length ($_) > 0 } split /~/, shift;
  2         4  
247 2         6 $self->PSC([@psc]);
248             } else {
249 10         8 shift; # ignore
250             }
251 12 100       30 $self->creditcard((shift eq 'Y') ? 1 : 0);
252 12         11 $code = shift; # re-use variable
253 12 50       14 $self->correspondence_type('mail') if $code eq 'M';
254 12 50       15 $self->correspondence_type('fax') if $code eq 'F';
255 12 50       14 $self->correspondence_type('email') if $code eq 'E';
256 12         26 my $maddr = Parse::SAMGov::Entity::Address->new(
257             # the order of shifting matters
258             address => _trim(join(' ', shift, shift)),
259             city => shift,
260             zip => sprintf("%s-%s", shift, shift),
261             country => shift,
262             state => shift,
263             );
264 12         24 $self->mailing_address($maddr);
265 12         17 for my $i (0..5) {
266 72         95 my $poc = Parse::SAMGov::Entity::PointOfContact->new(
267             first => _trim(shift),
268             middle => _trim(shift),
269             last => _trim(shift),
270             title => _trim(shift),
271             address => _trim(join(' ', shift, shift)),
272             city => shift,
273             zip => sprintf("%s-%s", shift, shift),
274             country => shift,
275             state => shift,
276             phone => shift,
277             phone_ext => shift,
278             phone_nonUS => shift,
279             fax => shift,
280             email => shift,
281             );
282 72 100       150 $self->POC_gov($poc) if $i == 0;
283 72 100       102 $self->POC_gov_alt($poc) if $i == 1;
284 72 100       106 $self->POC_pastperf($poc) if $i == 2;
285 72 100       94 $self->POC_pastperf_alt($poc) if $i == 3;
286 72 100       100 $self->POC_elec($poc) if $i == 4;
287 72 100       118 $self->POC_elec_alt($poc) if $i == 5;
288             }
289 12   100     17 $count = int(_trim(shift) || 0);
290 12 100       16 if ($count > 0) {
291 2         6 my @naics = grep { length($_) > 0 } split /~/, shift;
  4         6  
292 2         3 foreach my $c (@naics) {
293 4 50       13 if ($c =~ /(\d+)([YN ]*)/) {
294 4         7 my @es = split //, $2;
295 4 50       6 if (@es) {
296 4 50       8 $self->NAICS->{$1}->{exception} = {} unless ref $self->NAICS->{$1}->{exception} eq 'HASH';
297 4 50       12 $self->NAICS->{$1}->{exception}->{small_biz} = 1 if $es[0] eq 'Y';
298 4 50       11 $self->NAICS->{$1}->{exception}->{small_biz} = 0 if $es[0] eq 'N';
299             }
300             }
301             }
302             } else {
303 10         10 shift; # ignore
304             }
305 12         12 $code = shift;
306 12 50       16 $self->delinquent_fed_debt(1) if $code eq 'Y';
307 12 100       24 $self->delinquent_fed_debt(0) if $code eq 'N';
308 12         14 $self->exclusion_status(_trim(shift));
309 12   100     15 $count = int(_trim(shift) || 0);
310 12 50       16 if ($count > 0) {
311 0         0 my @sba = grep { length($_) > 0 } split /~/, shift;
  0         0  
312 0         0 foreach my $c (@sba) {
313 0 0       0 if ($c =~ /(\w{2})(\d{8})/) {
314 0         0 my $t = $1;
315 0 0       0 $self->SBA->{$t} = {} unless ref $self->SBA->{$t} eq 'HASH';
316 0         0 $self->SBA->{$t}->{description} = $self->SBA_descriptions->{$t};
317 0         0 $self->SBA->{$t}->{expiration} = _parse_yyyymmdd($2);
318             }
319             }
320             } else {
321 12         11 shift; # ignore
322             }
323 12 100       27 $self->is_private(length(shift) ? 1 : 0);
324 12   100     13 $count = int(_trim(shift) || 0);
325 12 50       16 if ($count > 0) {
326 0         0 my @dres = grep { length($_) > 0 } split /~/, shift;
  0         0  
327 0         0 my $h = {};
328 0         0 my %desc = (
329             ANY => 'Any area',
330             CTY => 'County',
331             STA => 'State',
332             MSA => 'Metropolitan Service Area',
333             );
334 0         0 foreach my $c (@dres) {
335 0 0       0 if ($c =~ /(\w{3})(\w*)/) {
336 0 0       0 $h->{$1} = {} unless ref $h->{$1} eq 'HASH';
337 0         0 $h->{$1}->{description} = $desc{$1};
338 0 0       0 $h->{$1}->{areas} = [] unless ref $h->{$1}->{areas} eq 'HASH';
339 0         0 my $a = _trim($2);
340 0 0       0 push @{$h->{$1}->{areas}}, $a if length $a;
  0         0  
341             }
342             }
343 0         0 $self->disaster_response($h);
344             } else {
345 12         9 shift; # ignore
346             }
347 12         6 my $eof = shift;
348 12 50       21 carp "Invalid end of record '$eof' seen. Expected '!end'" if $eof ne '!end';
349 12         100 return 1;
350             }
351              
352             1;
353              
354             =pod
355              
356             =encoding UTF-8
357              
358             =head1 NAME
359              
360             Parse::SAMGov::Entity - Object to denote each Entity in SAM
361              
362             =head1 VERSION
363              
364             version 0.104
365              
366             =head1 SYNOPSIS
367              
368             my $e = Parse::SAMGov::Entity->new(DUNS => 12345);
369             say $e; #... stringification supported ...
370              
371             =head1 METHODS
372              
373             =head2 DUNS
374              
375             This holds the unique identifier of the entity, currently the Data
376             Universal Numbering System (DUNS) number. This has a maximum length of 9 characters.
377             This number can be gotten from Dun & Bradstreet.
378              
379             =head2 DUNSplus4
380              
381             This holds the DUNS+4 value which is of 4 characters. If an entity doesn't have
382             this value set, it will be set as '0000'.
383              
384             =head2 CAGE
385              
386             This holds the CAGE code of the Entity.
387              
388             =head2 DODAAC
389              
390             This holds the DODAAC code of the entity.
391              
392             =head2 extract_code
393              
394             This denotes whether the SAM entry is active or expired
395             during extraction of the data.
396              
397             =head2 updated
398              
399             This denotes whether the SAM entry has been updated recently. Has a boolean
400             value of 1 if updated and 0 or undef otherwise.
401              
402             =head2 regn_purpose
403              
404             This denotes whether the purpose of registration is Federal Assistance Awards,
405             All Awards, IGT-only, Federal Assistance Awards & IGT or All Awards & IGT.
406              
407             =head2 regn_date
408              
409             Registration date of the entity with the input in YYYYMMDD format and it returns
410             a DateTime object.
411              
412             =head2 expiry_date
413              
414             Expiration date of the registration of the entity. The input is in YYYYMMDD
415             format and it returns a DateTime object.
416              
417             =head2 lastupdate_date
418              
419             Last update date of the registration of the entity. The input is in YYYYMMDD
420             format and it returns a DateTime object.
421              
422             =head2 activation_date
423              
424             Activation date of the registration of the entity. The input is in YYYYMMDD
425             format and it returns a DateTime object.
426              
427             =head2 name
428              
429             The legal business name of the entity.
430              
431             =head2 dba_name
432              
433             The Doing Business As (DBA) name of the entity.
434              
435             =head2 company_division
436              
437             The company division listed in the entity.
438              
439             =head2 division_no
440              
441             The divison number of the company division.
442              
443             =head2 physical_address
444              
445             This is the physical address of the entity represented as a
446             Parse::SAMGov::Entity::Address object.
447              
448             =head2 start_date
449              
450             This denotes the business start date. It takes as input the date in YYYYMMDD
451             format and returns a DateTime object.
452              
453             =head2 fiscalyear_date
454              
455             This denotes the current fiscal year end close date in YYYYMMDD format and
456             returns a DateTime object.
457              
458             =head2 url
459              
460             The corporate URL is denoted in this method. Returns a URI object and takes a
461             string value.
462              
463             =head2 entity_structure
464              
465             Get/Set the entity structure of the entity.
466              
467             =head2 incorporation_state
468              
469             Get/Set the two-character abbreviation of the state of incorporation.
470              
471             =head2 incorporation_country
472              
473             Get/Set the three-character abbreviation of the country of incorporation.
474              
475             =head2 biztype
476              
477             Get/Set the various business types that the entity holds. Requires an array
478             reference. The full list of business type codes can be retrieved from the SAM
479             Functional Data Dictionary.
480              
481             =head2 NAICS
482              
483             Get/Set the NAICS codes for the entity. This is a hash reference with the keys
484             being the numeric NAICS codes and the values being a hash reference with the
485             following keys:
486              
487             {
488             124567 => {
489             small_biz => 1,
490             exceptions => {
491             small_biz => 0,
492             # ... undocumented others ...
493             },
494             },
495             # ...
496             }
497             whether it is a small
498             business (value is 1) or not (value is 0) or has an exception (value is 2).
499              
500             =head2 PSC
501              
502             Get/Set the PSC codes for the entity. This requires an array reference.
503              
504             =head2 creditcard
505              
506             This denotes whether the entity uses a credit card.
507              
508             =head2 correspondence_type
509              
510             This denotes whether the entity prefers correspondence by mail, fax or email.
511             Returns a string of value 'mail', 'fax' or 'email'.
512              
513             =head2 mailing_address
514              
515             The mailing address of the entity as a L object.
516              
517             =head2 POC_gov
518              
519             This denotes the Government business Point of Contact for the entity and holds an
520             L object.
521              
522             =head2 POC_gov_alt
523              
524             This denotes the alternative Government business Point of Contact for the entity and
525             holds an L object.
526              
527             =head2 POC_pastperf
528              
529             This denotes the Past Performance Point of Contact for the entity and
530             holds an L object.
531              
532             =head2 POC_pastperf_alt
533              
534             This denotes the alternative Past Performance Point of Contact for the entity and
535             holds an L object.
536              
537             =head2 POC_elec
538              
539             This denotes the electronic business Point of Contact for the entity and
540             holds an L object.
541              
542             =head2 POC_elec_alt
543              
544             This denotes the alternative electronic business Point of Contact for the entity and
545             holds an L object.
546              
547             =head2 delinquent_fed_debt
548              
549             Get/Set the delinquent federal debt flag.
550              
551             =head2 exclusion_status
552              
553             Get/Set the exclusion status flag.
554              
555             =head2 is_private
556              
557             This flag denotes whether the listing is private or not.
558              
559             =head2 SBA
560              
561             This holds a hash-ref of Small Business Administration codes such as Hubzone,
562             8(a) certifications and the expiration dates. The structure looks like below:
563              
564             {
565             A4 => { description => 'SBA Certified Small Disadvantaged Busines',
566             expiration => '2016-12-01', #... this is a DateTime object...
567             },
568             }
569              
570             =head2 disaster_response
571              
572             This holds an array ref of disaster response (FEMA) codes that the entity falls
573             under, if applicable.
574              
575             =head1 AUTHOR
576              
577             Vikas N Kumar
578              
579             =head1 COPYRIGHT AND LICENSE
580              
581             This software is copyright (c) 2016 by Selective Intellect LLC.
582              
583             This is free software; you can redistribute it and/or modify it under
584             the same terms as the Perl 5 programming language system itself.
585              
586             =cut
587              
588             __END__