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.105';
3 3     3   484 use strict;
  3         3  
  3         65  
4 3     3   9 use warnings;
  3         3  
  3         53  
5 3     3   40 use 5.010;
  3         5  
6 3     3   961 use Data::Dumper;
  3         7935  
  3         142  
7 3     3   968 use Parse::SAMGov::Mo;
  3         5  
  3         11  
8 3     3   1237 use URI;
  3         8426  
  3         63  
9 3     3   2200 use DateTime;
  3         252829  
  3         144  
10 3     3   1609 use DateTime::Format::Strptime;
  3         28324  
  3         10  
11 3     3   1293 use Parse::SAMGov::Entity::Address;
  3         4  
  3         61  
12 3     3   1112 use Parse::SAMGov::Entity::PointOfContact;
  3         6  
  3         83  
13 3     3   12 use Carp;
  3         3  
  3         1663  
14              
15             #ABSTRACT: Object to denote each Entity in SAM
16              
17             use overload fallback => 1,
18             '""' => sub {
19 6     6   2449 my $self = $_[0];
20 6         7 my $str = '';
21 6 100       12 $str .= $self->name if $self->name;
22 6 100       9 $str .= ' dba ' . $self->dba_name if $self->dba_name;
23 6 50       12 $str .= "\nDUNS: " . $self->DUNS if $self->DUNS;
24 6 50       12 $str .= '+' . $self->DUNSplus4 if $self->DUNSplus4 ne '0000';
25 6 50       10 $str .= "\nCAGE: " . $self->CAGE if $self->CAGE;
26 6 100       12 $str .= "\nDODAAC: " . $self->DODAAC if $self->DODAAC;
27 6 50       11 $str .= "\nStatus: " . $self->extract_code if $self->extract_code;
28 6 100       9 $str .= "\nUpdated: Yes" if $self->updated;
29 6 50       10 $str .= "\nRegistration Purpose: " . $self->regn_purpose if $self->regn_purpose;
30 6 100       10 $str .= "\nRegistration Date: " . $self->regn_date->ymd('-') if $self->regn_date;
31 6 100       37 $str .= "\nExpiry Date: " . $self->expiry_date->ymd('-') if $self->expiry_date;
32 6 100       33 $str .= "\nLast Update Date: " . $self->lastupdate_date->ymd('-') if $self->lastupdate_date;
33 6 100       41 $str .= "\nActivation Date: " . $self->activation_date->ymd('-') if $self->activation_date;
34 6 50       36 $str .= "\nCompany Division: " . $self->company_division if $self->company_division;
35 6 50       10 $str .= "\nDivision No.: " . $self->division_no if $self->division_no;
36 6 100       22 $str .= "\nPhysical Address: " . $self->physical_address if $self->physical_address;
37 6 100       12 $str .= "\nBusiness Start Date: " . $self->start_date->ymd('-') if $self->start_date;
38 6 100       38 $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         29 $str .= "\nBusiness Types: [" . join(',', @{$self->biztype}) . "]";
  6         10  
42 6         4 my $pnaics;
43 6         4 foreach my $k (keys %{$self->NAICS}) {
  6         10  
44 8 100       11 $pnaics = $k if $self->NAICS->{$k}->{is_primary};
45 8 100       14 last if $pnaics;
46             }
47 6         8 $str .= "\nNAICS Codes: [" . join(',', keys %{$self->NAICS}) . "]";
  6         8  
48 6 100       10 if ($pnaics) {
49 5 100       8 $str .= "\nSmall Business: " . ($self->NAICS->{$pnaics}->{small_biz} ? 'Yes' : 'No');
50             }
51             {
52 6         4 local $Data::Dumper::Indent = 1;
  6         7  
53 6         3 local $Data::Dumper::Terse = 1;
54 6         10 $str .= "\nNAICS Details: " . Dumper($self->NAICS);
55             }
56 6         311 $str .= "\nPSC Codes: [" . join(',', @{$self->PSC}) . "]";
  6         13  
57 6 100       12 $str .= "\nMailing Address: " . $self->mailing_address if $self->mailing_address;
58 6 100       10 $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       11 $str .= "\nPast Performance POC (alternate): " . $self->POC_pastperf_alt if $self->POC_pastperf_alt;
62 6 100       10 $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       10 $str .= "\nDelinquent Federal Debt: " . ($self->delinquent_fed_debt ? 'Yes' : 'No');
65 6 50       9 $str .= "\nExclusion Status: " . $self->exclusion_status if $self->exclusion_status;
66             {
67 6         8 local $Data::Dumper::Indent = 0;
68 6         4 local $Data::Dumper::Terse = 1;
69 6         11 $str .= "\nSBA Business Type: " . Dumper($self->SBA);
70             }
71             {
72 6         4 local $Data::Dumper::Indent = 0;
  6         191  
  6         6  
73 6         4 local $Data::Dumper::Terse = 1;
74 6         13 $str .= "\nDisaster Response Type: " . Dumper($self->disaster_response);
75             }
76 6 100       164 $str .= "\nIs Private Listing: " . ($self->is_private ? 'Yes' : 'No');
77 6         30 return $str;
78 3     3   15 };
  3         4  
  3         50  
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   132 if (@_) {
100 78         62 my $d = shift;
101 78 100       163 if (length($d) == 4) {
102 10         20 my $y = DateTime->now->year;
103 10         1464 $d = "$y$d";
104             }
105 78         71 state $Strp =
106             DateTime::Format::Strptime->new(pattern => '%Y%m%d',
107             time_zone => 'America/New_York',);
108 78         19873 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   407 my $s = shift;
174 540         534 $s =~ s/^\s+//g;
175 540         482 $s =~ s/\s+$//g;
176 540         1269 return $s;
177             }
178              
179             sub load {
180 12     12 0 9 my $self = shift;
181 12 50       22 return unless (scalar(@_) == 150);
182 12         25 $self->DUNS(shift);
183 12   50     52 $self->DUNSplus4(shift || '0000');
184 12         22 $self->CAGE(shift);
185 12         21 $self->DODAAC(shift);
186 12         19 $self->updated(0);
187 12         10 my $code = shift;
188 12 100       51 if ($code =~ /A|2|3/x) {
    50          
189 10         19 $self->extract_code('active');
190 10 100       24 $self->updated(1) if $code eq '3';
191             } elsif ($code =~ /E|1|4/x) {
192 2         5 $self->extract_code('expired');
193 2 50       5 $self->updated(1) if $code eq '1';
194             }
195 12         24 $self->regn_purpose(shift);
196 12         21 $self->regn_date(shift);
197 12         25 $self->expiry_date(shift);
198 12         28 $self->lastupdate_date(shift);
199 12         23 $self->activation_date(shift);
200 12         20 $self->name(_trim(shift));
201 12         18 $self->dba_name(_trim(shift));
202 12         16 $self->company_division(_trim(shift));
203 12         15 $self->division_no(_trim(shift));
204 12         25 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         64 $self->start_date(shift);
215 12         25 $self->fiscalyear_date(shift);
216 12         21 $self->url(_trim(shift));
217 12         31 $self->entity_structure(shift);
218 12         21 $self->incorporation_state(shift);
219 12         22 $self->incorporation_country(shift);
220 12   100     18 my $count = int(_trim(shift) || 0);
221 12 100       22 if ($count > 0) {
222 10         27 my @biztypes = grep { length($_) > 0 } split /~/, shift;
  34         51  
223 10         32 $self->biztype([@biztypes]);
224             } else {
225 2         2 shift; # ignore
226             }
227 12         15 my $pnaics = _trim(shift);
228 12         26 $self->NAICS->{$pnaics} = {};
229 12 100 100     14 $count = int(_trim(shift) || 0) + (length($pnaics) ? 1 : 0);
230 12 100       16 if ($count > 0) {
231 10         15 my @naics = grep { length($_) > 0 } split /~/, shift;
  16         23  
232 10         13 foreach my $c (@naics) {
233 16 50       50 if ($c =~ /(\d+)(Y|N|E)/) {
234 16 100       25 $self->NAICS->{$1} = {} unless ref $self->NAICS->{$1} eq 'HASH';
235 16 100       1217 $self->NAICS->{$1}->{is_primary} = 1 if $pnaics eq $1;
236 16 100       32 $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       34 $self->NAICS->{$1}->{exception} = {} if $2 eq 'E';
239             }
240             }
241             } else {
242 2         1 shift; # ignore
243             }
244 12   100     14 $count = int(_trim(shift) || 0);
245 12 100       18 if ($count > 0) {
246 2         3 my @psc = grep { length ($_) > 0 } split /~/, shift;
  2         5  
247 2         5 $self->PSC([@psc]);
248             } else {
249 10         6 shift; # ignore
250             }
251 12 100       28 $self->creditcard((shift eq 'Y') ? 1 : 0);
252 12         13 $code = shift; # re-use variable
253 12 50       16 $self->correspondence_type('mail') if $code eq 'M';
254 12 50       15 $self->correspondence_type('fax') if $code eq 'F';
255 12 50       12 $self->correspondence_type('email') if $code eq 'E';
256 12         25 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         23 $self->mailing_address($maddr);
265 12         21 for my $i (0..5) {
266 72         102 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       161 $self->POC_gov($poc) if $i == 0;
283 72 100       108 $self->POC_gov_alt($poc) if $i == 1;
284 72 100       98 $self->POC_pastperf($poc) if $i == 2;
285 72 100       159 $self->POC_pastperf_alt($poc) if $i == 3;
286 72 100       103 $self->POC_elec($poc) if $i == 4;
287 72 100       123 $self->POC_elec_alt($poc) if $i == 5;
288             }
289 12   100     19 $count = int(_trim(shift) || 0);
290 12 100       18 if ($count > 0) {
291 2         4 my @naics = grep { length($_) > 0 } split /~/, shift;
  4         7  
292 2         3 foreach my $c (@naics) {
293 4 50       11 if ($c =~ /(\d+)([YN ]*)/) {
294 4         9 my @es = split //, $2;
295 4 50       5 if (@es) {
296 4 50       7 $self->NAICS->{$1}->{exception} = {} unless ref $self->NAICS->{$1}->{exception} eq 'HASH';
297 4 50       9 $self->NAICS->{$1}->{exception}->{small_biz} = 1 if $es[0] eq 'Y';
298 4 50       10 $self->NAICS->{$1}->{exception}->{small_biz} = 0 if $es[0] eq 'N';
299             }
300             }
301             }
302             } else {
303 10         9 shift; # ignore
304             }
305 12         11 $code = shift;
306 12 50       16 $self->delinquent_fed_debt(1) if $code eq 'Y';
307 12 100       23 $self->delinquent_fed_debt(0) if $code eq 'N';
308 12         12 $self->exclusion_status(_trim(shift));
309 12   100     15 $count = int(_trim(shift) || 0);
310 12 50       19 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         7 shift; # ignore
322             }
323 12 100       26 $self->is_private(length(shift) ? 1 : 0);
324 12   100     14 $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         10 shift; # ignore
346             }
347 12         11 my $eof = shift;
348 12 50       19 carp "Invalid end of record '$eof' seen. Expected '!end'" if $eof ne '!end';
349 12         108 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.105
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__