File Coverage

blib/lib/Parse/SAMGov/Entity.pm
Criterion Covered Total %
statement 304 355 85.6
branch 178 256 69.5
condition 21 29 72.4
subroutine 19 19 100.0
pod 1 4 25.0
total 523 663 78.8


line stmt bran cond sub pod time code
1             package Parse::SAMGov::Entity;
2             $Parse::SAMGov::Entity::VERSION = '0.202';
3 5     5   310833 use strict;
  5         29  
  5         216  
4 5     5   25 use warnings;
  5         10  
  5         303  
5 5     5   85 use 5.010;
  5         17  
6 5     5   1264 use Data::Dumper;
  5         16077  
  5         411  
7 5     5   2042 use Parse::SAMGov::Mo;
  5         11  
  5         40  
8 5     5   3320 use URI;
  5         28888  
  5         174  
9 5     5   5011 use DateTime;
  5         2979543  
  5         359  
10 5     5   3895 use DateTime::Format::Strptime;
  5         307573  
  5         28  
11 5     5   3273 use Parse::SAMGov::Entity::Address;
  5         16  
  5         173  
12 5     5   2153 use Parse::SAMGov::Entity::PointOfContact;
  5         47  
  5         502  
13 5     5   49 use Carp;
  5         9  
  5         3464  
14              
15             #ABSTRACT: Object to denote each Entity in SAM
16              
17             use overload fallback => 1,
18             '""' => sub {
19 8     8   17784 my $self = $_[0];
20 8         21 my $str = '';
21 8 100       34 $str .= $self->name if $self->name;
22 8 100       30 $str .= ' dba ' . $self->dba_name if $self->dba_name;
23 8 100       32 $str .= "\nUEI: " . $self->UEI if $self->UEI;
24 8 100       29 $str .= "\nDUNS: " . $self->DUNS if $self->DUNS;
25 8 50 66     29 $str .= '+' . $self->DUNSplus4 if $self->DUNS and $self->DUNSplus4 ne '0000';
26 8 100       27 $str .= '\nEntity EFT Indicator: ' . $self->EEFTI if defined $self->EEFTI;
27 8 50       28 $str .= "\nCAGE: " . $self->CAGE if $self->CAGE;
28 8 100       31 $str .= "\nDODAAC: " . $self->DODAAC if $self->DODAAC;
29 8 50       26 $str .= "\nStatus: " . $self->extract_code if $self->extract_code;
30 8 100       62 $str .= "\nUpdated: Yes" if $self->updated;
31 8 50       56 $str .= "\nRegistration Purpose: " . $self->regn_purpose if $self->regn_purpose;
32 8 100       26 $str .= "\nRegistration Date: " . $self->regn_date->ymd('-') if $self->regn_date;
33 8 100       238 $str .= "\nExpiry Date: " . $self->expiry_date->ymd('-') if $self->expiry_date;
34 8 100       102 $str .= "\nLast Update Date: " . $self->lastupdate_date->ymd('-') if $self->lastupdate_date;
35 8 100       116 $str .= "\nActivation Date: " . $self->activation_date->ymd('-') if $self->activation_date;
36 8 100       113 $str .= "\nCompany Division: " . $self->company_division if $self->company_division;
37 8 50       28 $str .= "\nDivision No.: " . $self->division_no if $self->division_no;
38 8 100       25 $str .= "\nPhysical Address: " . $self->physical_address if $self->physical_address;
39 8 100       29 $str .= "\nBusiness Start Date: " . $self->start_date->ymd('-') if $self->start_date;
40 8 100       137 $str .= sprintf "\nFiscal Year End: %02d-%02d", $self->fiscalyear_date->month, $self->fiscalyear_date->day
41             if $self->fiscalyear_date;
42 8 100       72 $str .= "\nCorporate URL: " . $self->url if $self->url;
43 8         84 $str .= "\nBusiness Types: [" . join(',', @{$self->biztype}) . "]";
  8         30  
44 8         33 $str .= "\nNAICS Codes: [" . join(',', keys %{$self->NAICS}) . "]";
  8         24  
45 8 100       31 $str .= "\nSmall Business: " . ($self->is_smallbiz() ? 'Yes' : 'No');
46             {
47 8         16 local $Data::Dumper::Indent = 1;
  8         36  
48 8         20 local $Data::Dumper::Terse = 1;
49 8         22 $str .= "\nNAICS Details: " . Dumper($self->NAICS);
50             }
51 8         1517 $str .= "\nPSC Codes: [" . join(',', @{$self->PSC}) . "]";
  8         40  
52 8 100       27 $str .= "\nMailing Address: " . $self->mailing_address if $self->mailing_address;
53 8 100       29 $str .= "\nGovt Business POC: " . $self->POC_gov if $self->POC_gov;
54 8 100       37 $str .= "\nGovt Business POC (alternate): " . $self->POC_gov_alt if $self->POC_gov_alt;
55 8 100       54 $str .= "\nPast Performance POC: " . $self->POC_pastperf if $self->POC_pastperf;
56 8 100       28 $str .= "\nPast Performance POC (alternate): " . $self->POC_pastperf_alt if $self->POC_pastperf_alt;
57 8 100       46 $str .= "\nElectronic POC: " . $self->POC_elec if $self->POC_elec;
58 8 100       36 $str .= "\nElectronic POC (alternate): " . $self->POC_elec_alt if $self->POC_elec_alt;
59 8 50       35 $str .= "\nDelinquent Federal Debt: " . ($self->delinquent_fed_debt ? 'Yes' : 'No');
60 8 50       23 $str .= "\nExclusion Status: " . $self->exclusion_status if $self->exclusion_status;
61             {
62 8         24 local $Data::Dumper::Indent = 0;
63 8         16 local $Data::Dumper::Terse = 1;
64 8         25 $str .= "\nSBA Business Type: " . Dumper($self->SBA);
65             }
66             {
67 8         36 local $Data::Dumper::Indent = 0;
  8         554  
  8         17  
68 8         14 local $Data::Dumper::Terse = 1;
69 8         33 $str .= "\nDisaster Response Type: " . Dumper($self->disaster_response);
70             }
71 8 100       383 $str .= "\nIs Private Listing: " . ($self->is_private ? 'Yes' : 'No');
72 8 100       23 $str .= "\nD&B Open Data Flag: " . $self->dnb_open_data if defined $self->dnb_open_data;
73 8         134 return $str;
74 5     5   39 };
  5         1496  
  5         410  
75              
76              
77             has 'UEI';
78             has 'DUNS';
79             has DUNSplus4 => default => sub { '0000' };
80             has 'EEFTI';
81             has 'CAGE';
82             has 'DODAAC';
83              
84              
85             has 'extract_code';
86             has 'updated';
87             has 'regn_purpose' => coerce => sub {
88             my $p = $_[0];
89             return 'Federal Assistance Awards' if $p eq 'Z1';
90             return 'All Awards' if $p eq 'Z2';
91             return 'IGT-Only' if $p eq 'Z3';
92             return 'Federal Assistance Awards & IGT' if $p eq 'Z4';
93             return 'All Awards & IGT' if $p eq 'Z5';
94             };
95              
96             sub _parse_yyyymmdd {
97 102 50   102   231 if (@_) {
98 102         168 my $d = shift;
99 102 100       219 if (length($d) == 4) {
100 14         106 my $y = DateTime->now->year;
101 14         4947 $d = "$y$d";
102             }
103 102         224 state $Strp =
104             DateTime::Format::Strptime->new(pattern => '%Y%m%d',
105             time_zone => 'America/New_York',);
106 102         83030 return $Strp->parse_datetime($d);
107             }
108 0         0 return;
109             }
110              
111              
112             has 'regn_date' => coerce => sub { _parse_yyyymmdd $_[0] };
113             has 'expiry_date' => coerce => sub { _parse_yyyymmdd $_[0] };
114             has 'lastupdate_date' => coerce => sub { _parse_yyyymmdd $_[0] };
115             has 'activation_date' => coerce => sub { _parse_yyyymmdd $_[0] };
116              
117              
118             has 'name';
119             has 'dba_name';
120             has 'company_division';
121             has 'division_no';
122             has 'physical_address' => default => sub { return Parse::SAMGov::Entity::Address->new; };
123              
124              
125             has 'start_date' => coerce => sub { _parse_yyyymmdd $_[0] };
126             has 'fiscalyear_date' => coerce => sub { _parse_yyyymmdd $_[0] };
127             has 'url' => coerce => sub { URI->new($_[0]) };
128             has 'entity_structure';
129             has entity_structure_descriptions => default => sub {
130             {
131             '2J' => 'Sole Proprietorship',
132             '2K' => 'Partnership or Limited Liability Partnership',
133             '2L' => 'Corporate Entity (Not Tax Exempt)',
134             '8H' => 'Corporate Entity (Tax Exempt)',
135             '2A' => 'U.S. Government Entity',
136             'CY' => 'Country - Foreign Government',
137             'X6' => 'International Organization',
138             'ZZ' => 'Other',
139             }
140             };
141              
142              
143              
144             has 'incorporation_state';
145             has 'incorporation_country';
146              
147              
148              
149             has 'biztype' => default => sub { [] };
150             has 'NAICS' => default => sub { {} };
151             has 'PSC' => default => sub { [] };
152             has 'creditcard';
153             has 'correspondence_type';
154             has 'mailing_address' => default => sub { return Parse::SAMGov::Entity::Address->new; };
155             has 'POC_gov' => default => sub { return
156             Parse::SAMGov::Entity::PointOfContact->new; };
157             has 'POC_gov_alt' => default => sub {
158             Parse::SAMGov::Entity::PointOfContact->new; };
159             has 'POC_pastperf' => default => sub {
160             Parse::SAMGov::Entity::PointOfContact->new; };
161             has 'POC_pastperf_alt' => default => sub {
162             Parse::SAMGov::Entity::PointOfContact->new; };
163             has 'POC_elec' => default => sub {
164             Parse::SAMGov::Entity::PointOfContact->new; };
165             has 'POC_elec_alt' => default => sub {
166             Parse::SAMGov::Entity::PointOfContact->new; };
167             has 'delinquent_fed_debt';
168             has 'exclusion_status';
169             has 'is_private';
170             has 'dnb_open_data';
171             has 'disaster_response' => default => sub { {} };
172             has 'SBA' => default => sub { {} };
173              
174             has 'SBA_descriptions' => default => sub {
175             {
176             A4 => 'SBA Certified Small Disadvantaged Business',
177             A6 => 'SBA Certified 8A Program Participant',
178             JT => 'SBA Certified 8A Joint Venture',
179             XX => 'SBA Certified HUBZone Firm',
180             A9 => 'SBA Certified Women-Owned Small Business',
181             A0 => 'SBA Certified Economically Disadvantaged Women-Owned Small Business',
182             }
183             };
184              
185             sub is_smallbiz {
186 8     8 1 17 my $self = shift;
187 8         16 my $res = 0;
188 8         18 foreach my $k (keys %{$self->NAICS}) {
  8         19  
189 34 100       60 $res = 1 if $self->NAICS->{$k}->{small_biz};
190 34 100       59 $res = 1 if $self->NAICS->{$k}->{exception}->{small_biz};
191 34 100       67 last if $res;
192             }
193 8         36 return $res;
194             }
195              
196             sub _trim {
197             # from Mojo::Util::trim
198 720     720   1062 my $s = shift;
199 720         1443 $s =~ s/^\s+//g;
200 720         1336 $s =~ s/\s+$//g;
201 720         2411 return $s;
202             }
203              
204             sub load {
205 16     16 0 33 my $self = shift;
206 16         38 my $ncols = scalar(@_);
207 16 100       95 return $self->load_v1(@_) if $ncols eq 150;
208 4 50       31 return $self->load_v2(@_) if $ncols eq 142;
209 0         0 carp "Unknown version of data file found with $ncols columns";
210 0         0 return undef;
211             }
212              
213             sub load_v1 {
214 12     12 0 25 my $self = shift;
215 12 50       35 return unless (scalar(@_) == 150);
216 12         49 $self->DUNS(shift);
217 12   50     78 $self->DUNSplus4(shift || '0000');
218 12         40 $self->CAGE(shift);
219 12         38 $self->DODAAC(shift);
220 12         48 $self->updated(0);
221 12         23 my $code = shift;
222 12 100       128 if ($code =~ /A|2|3/x) {
    50          
223 10         31 $self->extract_code('active');
224 10 100       68 $self->updated(1) if $code eq '3';
225             } elsif ($code =~ /E|1|4/x) {
226 2         10 $self->extract_code('expired');
227 2 50       12 $self->updated(1) if $code eq '1';
228             }
229 12         44 $self->regn_purpose(shift);
230 12         54 $self->regn_date(shift);
231 12         51 $self->expiry_date(shift);
232 12         57 $self->lastupdate_date(shift);
233 12         90 $self->activation_date(shift);
234 12         42 $self->name(_trim(shift));
235 12         41 $self->dba_name(_trim(shift));
236 12         28 $self->company_division(_trim(shift));
237 12         28 $self->division_no(_trim(shift));
238 12         51 my $paddr = Parse::SAMGov::Entity::Address->new(
239             # the order of shifting matters
240             address => _trim(join(' ', shift, shift)),
241             city => shift,
242             state => shift,
243             zip => sprintf("%s-%s", shift, shift),
244             country => shift,
245             district => shift,
246             );
247 12         48 $self->physical_address($paddr);
248 12         37 $self->start_date(shift);
249 12         55 $self->fiscalyear_date(shift);
250 12         36 $self->url(_trim(shift));
251 12         61 $self->entity_structure(shift);
252 12         48 $self->incorporation_state(shift);
253 12         46 $self->incorporation_country(shift);
254 12   100     31 my $count = int(_trim(shift) || 0);
255 12 100       39 if ($count > 0) {
256 10         52 my @biztypes = grep { length($_) > 0 } split /~/, shift;
  34         84  
257 10         54 $self->biztype([@biztypes]);
258             } else {
259 2         4 shift; # ignore
260             }
261 12         31 my $pnaics = _trim(shift);
262 12 100       74 $self->NAICS->{$pnaics} = { is_primary => 1 } if length($pnaics);
263 12 100 100     29 $count = int(_trim(shift) || 0) + (length($pnaics) ? 1 : 0);
264 12 100       38 if ($count > 0) {
265 10         36 my @naics = grep { length($_) > 0 } split /~/, shift;
  16         42  
266 10         25 foreach my $c (@naics) {
267 16 50       107 if ($c =~ /(\d+)(Y|N|E)/) {
268 16 100       46 $self->NAICS->{$1} = {} unless ref $self->NAICS->{$1} eq 'HASH';
269 16 100       68 $self->NAICS->{$1}->{is_primary} = 1 if $pnaics eq $1;
270 16 100       64 $self->NAICS->{$1}->{small_biz} = 1 if $2 eq 'Y';
271 16 100       113 $self->NAICS->{$1}->{small_biz} = 0 if $2 eq 'N';
272 16 100       61 $self->NAICS->{$1}->{exception} = {} if $2 eq 'E';
273             }
274             }
275             } else {
276 2         2 shift; # ignore
277             }
278 12   100     34 $count = int(_trim(shift) || 0);
279 12 100       33 if ($count > 0) {
280 2         8 my @psc = grep { length ($_) > 0 } split /~/, shift;
  2         8  
281 2         13 $self->PSC([@psc]);
282             } else {
283 10         14 shift; # ignore
284             }
285 12 100       65 $self->creditcard((shift eq 'Y') ? 1 : 0);
286 12         18 $code = shift; # re-use variable
287 12 50       41 $self->correspondence_type('mail') if $code eq 'M';
288 12 50       28 $self->correspondence_type('fax') if $code eq 'F';
289 12 50       27 $self->correspondence_type('email') if $code eq 'E';
290 12         62 my $maddr = Parse::SAMGov::Entity::Address->new(
291             # the order of shifting matters
292             address => _trim(join(' ', shift, shift)),
293             city => shift,
294             zip => sprintf("%s-%s", shift, shift),
295             country => shift,
296             state => shift,
297             );
298 12         51 $self->mailing_address($maddr);
299 12         38 for my $i (0..5) {
300 72         175 my $poc = Parse::SAMGov::Entity::PointOfContact->new(
301             first => _trim(shift),
302             middle => _trim(shift),
303             last => _trim(shift),
304             title => _trim(shift),
305             address => _trim(join(' ', shift, shift)),
306             city => shift,
307             zip => sprintf("%s-%s", shift, shift),
308             country => shift,
309             state => shift,
310             phone => shift,
311             phone_ext => shift,
312             phone_nonUS => shift,
313             fax => shift,
314             email => shift,
315             );
316 72 100       302 $self->POC_gov($poc) if $i == 0;
317 72 100       183 $self->POC_gov_alt($poc) if $i == 1;
318 72 100       170 $self->POC_pastperf($poc) if $i == 2;
319 72 100       178 $self->POC_pastperf_alt($poc) if $i == 3;
320 72 100       214 $self->POC_elec($poc) if $i == 4;
321 72 100       227 $self->POC_elec_alt($poc) if $i == 5;
322             }
323 12   100     37 $count = int(_trim(shift) || 0);
324 12 100       43 if ($count > 0) {
325 2         11 my @naics = grep { length($_) > 0 } split /~/, shift;
  4         17  
326 2         8 foreach my $c (@naics) {
327 4 50       28 if ($c =~ /(\d+)([YN ]*)/) {
328 4         20 my @es = split //, $2;
329 4 50       13 if (@es) {
330 4 50       14 $self->NAICS->{$1}->{exception} = {} unless ref $self->NAICS->{$1}->{exception} eq 'HASH';
331 4 50       18 $self->NAICS->{$1}->{exception}->{small_biz} = 1 if $es[0] eq 'Y';
332 4 50       21 $self->NAICS->{$1}->{exception}->{small_biz} = 0 if $es[0] eq 'N';
333             }
334             }
335             }
336             } else {
337 10         18 shift; # ignore
338             }
339 12         25 $code = shift;
340 12 50       38 $self->delinquent_fed_debt(1) if $code eq 'Y';
341 12 100       52 $self->delinquent_fed_debt(0) if $code eq 'N';
342 12         34 $self->exclusion_status(_trim(shift));
343 12   100     26 $count = int(_trim(shift) || 0);
344 12 50       34 if ($count > 0) {
345 0         0 my @sba = grep { length($_) > 0 } split /~/, shift;
  0         0  
346 0         0 foreach my $c (@sba) {
347 0 0       0 if ($c =~ /(\w{2})(\d{8})/) {
348 0         0 my $t = $1;
349 0 0       0 $self->SBA->{$t} = {} unless ref $self->SBA->{$t} eq 'HASH';
350 0         0 $self->SBA->{$t}->{description} = $self->SBA_descriptions->{$t};
351 0         0 $self->SBA->{$t}->{expiration} = _parse_yyyymmdd($2);
352             }
353             }
354             } else {
355 12         22 shift; # ignore
356             }
357 12 100       56 $self->is_private(length(shift) ? 1 : 0);
358 12   100     24 $count = int(_trim(shift) || 0);
359 12 50       32 if ($count > 0) {
360 0         0 my @dres = grep { length($_) > 0 } split /~/, shift;
  0         0  
361 0         0 my $h = {};
362 0         0 my %desc = (
363             ANY => 'Any area',
364             CTY => 'County',
365             STA => 'State',
366             MSA => 'Metropolitan Service Area',
367             );
368 0         0 foreach my $c (@dres) {
369 0 0       0 if ($c =~ /(\w{3})(\w*)/) {
370 0 0       0 $h->{$1} = {} unless ref $h->{$1} eq 'HASH';
371 0         0 $h->{$1}->{description} = $desc{$1};
372 0 0       0 $h->{$1}->{areas} = [] unless ref $h->{$1}->{areas} eq 'HASH';
373 0         0 my $a = _trim($2);
374 0 0       0 push @{$h->{$1}->{areas}}, $a if length $a;
  0         0  
375             }
376             }
377 0         0 $self->disaster_response($h);
378             } else {
379 12         19 shift; # ignore
380             }
381 12         24 my $eof = shift;
382 12 50       38 carp "Invalid end of record '$eof' seen. Expected '!end'" if $eof ne '!end';
383 12         236 return 1;
384             }
385              
386             sub load_v2 {
387 4     4 0 6 my $self = shift;
388 4 50       12 return unless (scalar(@_) == 142);
389 4         17 $self->UEI(shift);## UEI SAM 12 chars
390 4         13 $self->DUNS(shift);## DUNS UEI 9 chars
391 4         15 $self->EEFTI(shift);## 4 chars
392 4         13 $self->CAGE(shift);## 5 chars
393 4         13 $self->DODAAC(shift);## 9 chars
394 4         13 $self->updated(0);
395 4         9 my $code = shift;
396 4 50       24 if ($code =~ /A|2|3/x) {
    0          
397 4         13 $self->extract_code('active');
398 4 50       16 $self->updated(1) if $code eq '3';
399             } elsif ($code =~ /E|1|4/x) {
400 0         0 $self->extract_code('expired');
401 0 0       0 $self->updated(1) if $code eq '1';
402             }
403 4         15 $self->regn_purpose(shift);
404 4         13 $self->regn_date(shift);
405 4         19 $self->expiry_date(shift);
406 4         18 $self->lastupdate_date(shift);
407 4         18 $self->activation_date(shift);
408 4         15 $self->name(_trim(shift));
409 4         9 $self->dba_name(_trim(shift));
410 4         9 $self->company_division(_trim(shift));## entity division
411 4         8 $self->division_no(_trim(shift)); ## entity division number
412 4         18 my $paddr = Parse::SAMGov::Entity::Address->new(
413             # the order of shifting matters
414             address => _trim(join(' ', shift, shift)),
415             city => shift,
416             state => shift,
417             zip => sprintf("%s-%s", shift, shift),
418             country => shift,
419             district => shift,
420             );
421 4         17 $self->physical_address($paddr);
422 4         14 $self->dnb_open_data(shift);# D&B Open Data flag
423 4         26 $self->start_date(shift);
424 4         16 $self->fiscalyear_date(shift);
425 4         12 $self->url(_trim(shift));
426 4         17 $self->entity_structure(shift);
427 4         17 $self->incorporation_state(shift);
428 4         11 $self->incorporation_country(shift);
429 4   50     11 my $count = int(_trim(shift) || 0);
430 4 50       13 if ($count > 0) {
431 4         15 my @biztypes = grep { length($_) > 0 } split /~/, shift;
  6         43  
432 4         17 $self->biztype([@biztypes]);
433             } else {
434 0         0 shift; # ignore
435             }
436 4         17 my $pnaics = _trim(shift);
437 4 50       25 $self->NAICS->{$pnaics} = { is_primary => 1 } if length ($pnaics);
438 4 50 50     9 $count = int(_trim(shift) || 0) + (length($pnaics) ? 1 : 0);
439 4 50       9 if ($count > 0) {
440 4         39 my @naics = grep { length($_) > 0 } split /~/, shift;
  62         131  
441 4         15 foreach my $c (@naics) {
442 62 100       228 if ($c =~ /(\d+)(Y|N|E)/) {
443 60 100       122 $self->NAICS->{$1} = {} unless ref $self->NAICS->{$1} eq 'HASH';
444 60 100       169 $self->NAICS->{$1}->{is_primary} = 1 if $pnaics eq $1;
445 60 50       116 $self->NAICS->{$1}->{small_biz} = 1 if $2 eq 'Y';
446 60 100       165 $self->NAICS->{$1}->{small_biz} = 0 if $2 eq 'N';
447 60 100       148 $self->NAICS->{$1}->{exception} = {} if $2 eq 'E';
448             }
449             }
450             } else {
451 0         0 shift; # ignore
452             }
453 4   50     11 $count = int(_trim(shift) || 0);
454 4 50       12 if ($count > 0) {
455 4         18 my @psc = grep { length ($_) > 0 } split /~/, shift;
  24         46  
456 4         19 $self->PSC([@psc]);
457             } else {
458 0         0 shift; # ignore
459             }
460 4 50       18 $self->creditcard((shift eq 'Y') ? 1 : 0);
461 4         8 $code = shift; # re-use variable
462 4 50       36 $self->correspondence_type('mail') if $code eq 'M';
463 4 50       11 $self->correspondence_type('fax') if $code eq 'F';
464 4 50       7 $self->correspondence_type('email') if $code eq 'E';
465 4         18 my $maddr = Parse::SAMGov::Entity::Address->new(
466             # the order of shifting matters
467             address => _trim(join(' ', shift, shift)),
468             city => shift,
469             zip => sprintf("%s-%s", shift, shift),
470             country => shift,
471             state => shift,
472             );
473 4         17 $self->mailing_address($maddr);
474 4         14 for my $i (0..5) {
475             ### V2 has no email/phone/fax
476 24         55 my $poc = Parse::SAMGov::Entity::PointOfContact->new(
477             first => _trim(shift),
478             middle => _trim(shift),
479             last => _trim(shift),
480             title => _trim(shift),
481             address => _trim(join(' ', shift, shift)),
482             city => shift,
483             zip => sprintf("%s-%s", shift, shift),
484             country => shift,
485             state => shift,
486             );
487 24 100       72 $self->POC_gov($poc) if $i == 0;
488 24 100       72 $self->POC_gov_alt($poc) if $i == 1;
489 24 100       50 $self->POC_pastperf($poc) if $i == 2;
490 24 100       48 $self->POC_pastperf_alt($poc) if $i == 3;
491 24 100       56 $self->POC_elec($poc) if $i == 4;
492 24 100       64 $self->POC_elec_alt($poc) if $i == 5;
493             }
494 4   50     11 $count = int(_trim(shift) || 0);
495 4 50       12 if ($count > 0) {
496 4         15 my @naics = grep { length($_) > 0 } split /~/, shift;
  12         27  
497 4         9 foreach my $c (@naics) {
498 12 50       65 if ($c =~ /(\d+)([YN ]*)/) {
499 12         45 my @es = split //, $2;
500 12 50       23 if (@es) {
501 12 50       30 $self->NAICS->{$1}->{exception} = {} unless ref $self->NAICS->{$1}->{exception} eq 'HASH';
502 12 50       22 $self->NAICS->{$1}->{exception}->{small_biz} = 1 if $es[0] eq 'Y';
503 12 50       28 $self->NAICS->{$1}->{exception}->{small_biz} = 0 if $es[0] eq 'N';
504             }
505             }
506             }
507             } else {
508 0         0 shift; # ignore
509             }
510 4         8 $code = shift;
511 4 50       9 $self->delinquent_fed_debt(1) if $code eq 'Y';
512 4 50       34 $self->delinquent_fed_debt(0) if $code eq 'N';
513 4         11 $self->exclusion_status(_trim(shift));
514 4   50     9 $count = int(_trim(shift) || 0);
515 4 50       11 if ($count > 0) {
516 0         0 my @sba = grep { length($_) > 0 } split /~/, shift;
  0         0  
517 0         0 foreach my $c (@sba) {
518 0 0       0 if ($c =~ /(\w{2})(\d{8})/) {
519 0         0 my $t = $1;
520 0 0       0 $self->SBA->{$t} = {} unless ref $self->SBA->{$t} eq 'HASH';
521 0         0 $self->SBA->{$t}->{description} = $self->SBA_descriptions->{$t};
522 0         0 $self->SBA->{$t}->{expiration} = _parse_yyyymmdd($2);
523             }
524             }
525             } else {
526 4         7 shift; # ignore
527             }
528 4 50       19 $self->is_private(length(shift) ? 1 : 0);
529 4   50     9 $count = int(_trim(shift) || 0);
530 4 50       10 if ($count > 0) {
531 0         0 my @dres = grep { length($_) > 0 } split /~/, shift;
  0         0  
532 0         0 my $h = {};
533 0         0 my %desc = (
534             ANY => 'Any area',
535             CTY => 'County',
536             STA => 'State',
537             MSA => 'Metropolitan Service Area',
538             );
539 0         0 foreach my $c (@dres) {
540 0 0       0 if ($c =~ /(\w{3})(\w*)/) {
541 0 0       0 $h->{$1} = {} unless ref $h->{$1} eq 'HASH';
542 0         0 $h->{$1}->{description} = $desc{$1};
543 0 0       0 $h->{$1}->{areas} = [] unless ref $h->{$1}->{areas} eq 'HASH';
544 0         0 my $a = _trim($2);
545 0 0       0 push @{$h->{$1}->{areas}}, $a if length $a;
  0         0  
546             }
547             }
548 0         0 $self->disaster_response($h);
549             } else {
550 4         6 shift; # ignore
551             }
552             ## field 122-141 are Flex Fields with length 0
553 4         11 for (122 .. 141) {
554 80         95 shift;#ignore
555             }
556 4         8 my $eof = shift;
557 4 50       11 carp "Invalid end of record '$eof' seen. Expected '!end'" if $eof ne '!end';
558 4         54 return 1;
559             }
560              
561             1;
562              
563             =pod
564              
565             =encoding UTF-8
566              
567             =head1 NAME
568              
569             Parse::SAMGov::Entity - Object to denote each Entity in SAM
570              
571             =head1 VERSION
572              
573             version 0.202
574              
575             =head1 SYNOPSIS
576              
577             ### for V2 files
578             my $e_v2 = Parse::SAMGov::Entity->new(UEI => 12345);
579             say $e_v2; #... stringification supported ...
580             ### for V1 files
581             my $e_v1 = Parse::SAMGov::Entity->new(DUNS => 12345);
582             say $e_v1; #... stringification supported ...
583              
584             =head1 METHODS
585              
586             =head2 UEI
587              
588             This holds the SAM Unique Entity Identifier (UEI) and is 12 characters long. This number is only valid for V2 files on or after 2022.
589              
590             =head2 DUNS
591              
592             This holds the unique identifier of the entity, currently the Data
593             Universal Numbering System (DUNS) number. This has a maximum length of 9 characters.
594             This number can be gotten from Dun & Bradstreet. This is only valid for V1 files on or before 2021.
595              
596             =head2 DUNSplus4
597              
598             This holds the DUNS+4 value which is of 4 characters. If an entity doesn't have
599             this value set, it will be set as '0000'. This is only valid for V1 files on or before 2021.
600              
601             =head2 EEFTI
602              
603             The Entity EFT Indicator is an entity-selected Electronics Funds Transfer (EFT) Identifier used to distinguish more than one remittance location for payment. An entity can only provide an Entity EFT Indicator if they provide an additional set of EFT information. If the entity does not need to provide additional EFT information, the registration will show a value of null. CAGE codes are assigned at the Entity EFT Indicator level.
604              
605             =head2 CAGE
606              
607             This holds the CAGE code of the Entity.
608              
609             =head2 DODAAC
610              
611             This holds the DODAAC code of the entity.
612              
613             =head2 extract_code
614              
615             This denotes whether the SAM entry is active or expired
616             during extraction of the data.
617              
618             =head2 updated
619              
620             This denotes whether the SAM entry has been updated recently. Has a boolean
621             value of 1 if updated and 0 or undef otherwise.
622              
623             =head2 regn_purpose
624              
625             This denotes whether the purpose of registration is Federal Assistance Awards,
626             All Awards, IGT-only, Federal Assistance Awards & IGT or All Awards & IGT.
627              
628             =head2 regn_date
629              
630             Registration date of the entity with the input in YYYYMMDD format and it returns
631             a DateTime object.
632              
633             =head2 expiry_date
634              
635             Expiration date of the registration of the entity. The input is in YYYYMMDD
636             format and it returns a DateTime object.
637              
638             =head2 lastupdate_date
639              
640             Last update date of the registration of the entity. The input is in YYYYMMDD
641             format and it returns a DateTime object.
642              
643             =head2 activation_date
644              
645             Activation date of the registration of the entity. The input is in YYYYMMDD
646             format and it returns a DateTime object.
647              
648             =head2 name
649              
650             The legal business name of the entity.
651              
652             =head2 dba_name
653              
654             The Doing Business As (DBA) name of the entity.
655              
656             =head2 company_division
657              
658             The company division (V1) listed in the entity. Same as Entity Division in V2.
659              
660             =head2 division_no
661              
662             The divison number of the company division (V1) or entity division (V2).
663              
664             =head2 physical_address
665              
666             This is the physical address of the entity represented as a
667             Parse::SAMGov::Entity::Address object.
668              
669             =head2 start_date
670              
671             This denotes the business start date. It takes as input the date in YYYYMMDD
672             format and returns a DateTime object.
673              
674             =head2 fiscalyear_date
675              
676             This denotes the current fiscal year end close date in YYYYMMDD format and
677             returns a DateTime object.
678              
679             =head2 url
680              
681             The corporate URL is denoted in this method. Returns a URI object and takes a
682             string value.
683              
684             =head2 entity_structure
685              
686             Get/Set the entity structure of the entity. This is a 2-letter code
687              
688             =head2 entity_structure_descriptions
689              
690             Describe the 2-letter code for entity structure (V2).
691              
692             =head2 incorporation_state
693              
694             Get/Set the two-character abbreviation of the state of incorporation.
695              
696             =head2 incorporation_country
697              
698             Get/Set the three-character abbreviation of the country of incorporation.
699              
700             =head2 biztype
701              
702             Get/Set the various business types that the entity holds. Requires an array
703             reference. The full list of business type codes can be retrieved from the SAM
704             Functional Data Dictionary.
705              
706             =head2 is_smallbiz
707              
708             Returns 1 or 0 if the business is defined as a small business or not.
709              
710             =head2 NAICS
711              
712             Get/Set the NAICS codes for the entity. This is a hash reference with the keys
713             being the numeric NAICS codes and the values being a hash reference with the
714             following keys:
715              
716             {
717             124567 => {
718             small_biz => 1,
719             exceptions => {
720             small_biz => 0,
721             # ... undocumented others ...
722             },
723             },
724             # ...
725             }
726             whether it is a small
727             business (value is 1) or not (value is 0) or has an exception (value is 2).
728              
729             =head2 PSC
730              
731             Get/Set the PSC codes for the entity. This requires an array reference.
732              
733             =head2 creditcard
734              
735             This denotes whether the entity uses a credit card.
736              
737             =head2 correspondence_type
738              
739             This denotes whether the entity prefers correspondence by mail, fax or email.
740             Returns a string of value 'mail', 'fax' or 'email'.
741              
742             =head2 mailing_address
743              
744             The mailing address of the entity as a L<Parse::SAMGov::Entity::Address> object.
745              
746             =head2 POC_gov
747              
748             This denotes the Government business Point of Contact for the entity and holds an
749             L<Parse::SAMGov::Entity::PointOfContact> object.
750              
751             =head2 POC_gov_alt
752              
753             This denotes the alternative Government business Point of Contact for the entity and
754             holds an L<Parse::SAMGov::Entity::PointOfContact> object.
755              
756             =head2 POC_pastperf
757              
758             This denotes the Past Performance Point of Contact for the entity and
759             holds an L<Parse::SAMGov::Entity::PointOfContact> object.
760              
761             =head2 POC_pastperf_alt
762              
763             This denotes the alternative Past Performance Point of Contact for the entity and
764             holds an L<Parse::SAMGov::Entity::PointOfContact> object.
765              
766             =head2 POC_elec
767              
768             This denotes the electronic business Point of Contact for the entity and
769             holds an L<Parse::SAMGov::Entity::PointOfContact> object.
770              
771             =head2 POC_elec_alt
772              
773             This denotes the alternative electronic business Point of Contact for the entity and
774             holds an L<Parse::SAMGov::Entity::PointOfContact> object.
775              
776             =head2 delinquent_fed_debt
777              
778             Get/Set the delinquent federal debt flag. Also known as Debt Subject to Offset Flag in V2.
779              
780             =head2 exclusion_status
781              
782             Get/Set the exclusion status flag.
783              
784             =head2 is_private
785              
786             This flag denotes whether the listing is private or not.
787              
788             =head2 dnb_open_data
789              
790             This flag denotes whether this is a D&B Open Data or not. V2 only.
791              
792             =head2 SBA
793              
794             This holds a hash-ref of Small Business Administration codes such as Hubzone,
795             8(a) certifications and the expiration dates. The structure looks like below:
796              
797             {
798             A4 => { description => 'SBA Certified Small Disadvantaged Busines',
799             expiration => '2016-12-01', #... this is a DateTime object...
800             },
801             }
802              
803             =head2 disaster_response
804              
805             This holds an array ref of disaster response (FEMA) codes that the entity falls
806             under, if applicable.
807              
808             =head1 AUTHOR
809              
810             Vikas N Kumar <vikas@cpan.org>
811              
812             =head1 COPYRIGHT AND LICENSE
813              
814             This software is copyright (c) 2023 by Selective Intellect LLC.
815              
816             This is free software; you can redistribute it and/or modify it under
817             the same terms as the Perl 5 programming language system itself.
818              
819             =cut
820              
821             __END__
822             ### COPYRIGHT: Selective Intellect LLC.
823             ### AUTHOR: Vikas N Kumar <vikas@cpan.org>