File Coverage

blib/lib/Interchange6/Schema/Result/Zone.pm
Criterion Covered Total %
statement 137 139 100.0
branch 84 86 100.0
condition n/a
subroutine 15 15 100.0
pod 9 9 100.0
total 245 249 100.0


line stmt bran cond sub pod time code
1 2     2   1420 use utf8;
  2         5  
  2         15  
2              
3             package Interchange6::Schema::Result::Zone;
4              
5             =head1 NAME
6              
7             Interchange6::Schema::Result::Zone
8              
9             =cut
10              
11 2     2   89 use DateTime;
  2         28  
  2         66  
12 2     2   12 use Scalar::Util qw(blessed);
  2         4  
  2         147  
13              
14 2         20 use Interchange6::Schema::Candy -components =>
15 2     2   13 [qw(InflateColumn::DateTime TimeStamp)];
  2         5  
16              
17             =head1 DESCRIPTION
18              
19             In the context of zones the term 'state' refers to state, province or other principal subdivision of a country as defined in L<ISO 3116-2|http://en.wikipedia.org/wiki/ISO_3166-2>. Countries to be added to a zone must already exist in L<Interchange6::Schema::Result::Country> and states in L<Interchange6::Schema::Result::State>.
20              
21             Zones can contain any of the following:
22              
23             =over 4
24              
25             =item * No countries and no states
26              
27             An empty zone must be created before countries/states are added but otherwise is probably not useful.
28              
29             =item * Multiple countries
30              
31             For example to create a trading group like the European Union.
32              
33             =item * A single country
34              
35              
36             =item * A single country with a single state
37              
38             For example Quebec in Canada which has GST + QST
39              
40             =item * A single country with multiple states
41              
42             For example a group containing all Canadian provinces that charge only GST.
43              
44             =back
45              
46             The following combinations are NOT allowed:
47              
48             =over 4
49              
50             =item * Multiple countries with one or more states
51              
52             =item * One or more states with no country
53              
54             =back
55              
56             Countries and states should be added to and removed from the zone using these methods which are described further below:
57              
58             =over 4
59              
60             =item * add_countries
61              
62             =item * remove_countries
63              
64             =item * add_states
65              
66             =item * remove_states
67              
68             =back
69              
70             B<NOTE:> avoid using other methods from L<DBIx::Class::Relationship::Base> since you may inadvertently end up with an invalid zone.
71              
72             =head1 ACCESSORS
73              
74             =head2 zones_id
75              
76             Primary Key.
77              
78             =cut
79              
80             primary_column zones_id => {
81             data_type => "integer",
82             is_auto_increment => 1,
83             sequence => "zones_id_seq"
84             };
85              
86             =head2 zone
87              
88             For example for storing the UPS/USPS zone code or a simple name for the zone.
89              
90             Unique constraint.
91              
92             =cut
93              
94             unique_column zone => { data_type => "varchar", size => 255 };
95              
96             =head2 created
97              
98             Date and time when this record was created returned as L<DateTime> object.
99             Value is auto-set on insert.
100              
101             =cut
102              
103             column created => { data_type => "datetime", set_on_create => 1 };
104              
105             =head2 last_modified
106              
107             Date and time when this record was last modified returned as L<DateTime> object.
108             Value is auto-set on insert and update.
109              
110             =cut
111              
112             column last_modified => {
113             data_type => "datetime",
114             set_on_create => 1,
115             set_on_update => 1,
116             };
117              
118             =head1 RELATIONS
119              
120             =head2 zone_countries
121              
122             Type: has_many
123              
124             Related object: L<Interchange6::Schema::Result::ZoneCountry>
125              
126             =cut
127              
128             has_many
129             zone_countries => "Interchange6::Schema::Result::ZoneCountry",
130             "zones_id",
131             { cascade_copy => 0, cascade_delete => 0 };
132              
133             =head2 countries
134              
135             Type: many_to_many
136              
137             Accessor to related country results ordered by name.
138              
139             =cut
140              
141             many_to_many
142             countries => "zone_countries",
143             "country",
144             { order_by => 'country.name' };
145              
146             =head2 zone_states
147              
148             Type: has_many
149              
150             Related object: L<Interchange6::Schema::Result::ZoneState>
151              
152             =cut
153              
154             has_many
155             zone_states => "Interchange6::Schema::Result::ZoneState",
156             "zones_id",
157             { cascade_copy => 0, cascade_delete => 0 };
158              
159             =head2 states
160              
161             Type: many_to_many
162              
163             Accessor to related state results ordered by name.
164              
165             =cut
166              
167             many_to_many
168             states => "zone_states",
169             "state", { order_by => 'state.name' };
170              
171             =head2 shipment_destinations
172              
173             C<has_many> relationship with
174             L<Interchange6::Schema::Result::ShipmentDestination>
175              
176             =cut
177              
178             has_many
179             shipment_destinations => "Interchange6::Schema::Result::ShipmentDestination",
180             "zones_id";
181              
182             =head2 shipment_methods
183              
184             C<many_to_many> relationship to shipment_method. Currently it ignores
185             the C<active> field in shipment_destinations.
186              
187             =cut
188              
189             many_to_many shipment_methods => "shipment_destinations", "shipment_method";
190              
191             =head1 METHODS
192              
193             =head2 new
194              
195             Overloaded method. We allow a form of multi-create here so you can do something like:
196              
197             $schema->resultset('Zone')->create({
198             zone => 'some states of the USA',
199             countries => [ 'US' ],
200             states => [ 'CA', 'PA' ],
201             });
202              
203             If there is only a single country or state the value can be a scalar instead of a hashref.
204              
205             =cut
206              
207             sub new {
208 959     959 1 159290 my ( $class, $attrs ) = @_;
209              
210 959         2205 my ( $countries, $states, $new );
211              
212 959 100       4464 if ( $attrs->{countries} ) {
    100          
213 4 100       27 if ( ref( $attrs->{countries} ) eq 'ARRAY' ) {
214 1         4 push @$countries, @{ $attrs->{countries} };
  1         4  
215             }
216             else {
217 3         17 push @$countries, $attrs->{countries};
218             }
219 4         17 delete $attrs->{countries};
220              
221 4 100       17 if ( $attrs->{states} ) {
222 3 100       14 if ( ref( $attrs->{states} ) eq 'ARRAY' ) {
223 1         4 push @$states, @{ $attrs->{states} };
  1         4  
224             }
225             else {
226 2         7 push @$states, $attrs->{states};
227             }
228 3         11 delete $attrs->{states};
229             }
230             }
231             elsif ( $attrs->{states} ) {
232 1         14 die "Cannot create Zone with states but without countries";
233             }
234              
235 958         4064 $new = $class->next::method($attrs);
236 958 100       2003046 $new->add_countries($countries) if $countries;
237 958 100       2859 $new->add_states($states) if $states;
238              
239 957         3170 return $new;
240             }
241              
242             =head2 add_countries
243              
244             Argument is one of:
245              
246             =over 4
247              
248             =item an L<Interchange6::Schema::Result::Country> object
249              
250             =item a country ISO code
251              
252             =item an arrayref of the above (can include a mixture of both)
253              
254             =back
255              
256             Returns the zone object on success.
257              
258             =cut
259              
260             # add/remove_countries can be passed all sorts of junk but we need Country obj
261              
262             sub _get_country_obj {
263              
264 34     34   112 my ( $self, $country ) = @_;
265              
266 34 100       290 if ( !defined $country ) {
    100          
    100          
267 2         28 $self->throw_exception("Country must be defined");
268             }
269             elsif ( blessed($country) ) {
270              
271 16         69 my $class = ref($country);
272              
273 16 100       133 $self->throw_exception("Country cannot be a $class")
274             unless $country->isa('Interchange6::Schema::Result::Country');
275              
276             }
277             elsif ( $country =~ m/^[a-z]{2}$/i ) {
278              
279 12         99 my $result = $self->result_source->schema->resultset("Country")
280             ->find( { country_iso_code => uc($country) } );
281              
282 12 100       41467 $self->throw_exception("No country found for code: $country")
283             unless defined $result;
284              
285 10         190 $country = $result;
286             }
287             else {
288 4         27 $self->throw_exception("Bad country: $country");
289             }
290              
291 24         143 return $country;
292             }
293              
294             sub add_countries {
295 22     22 1 68552 my ( $self, $arg ) = ( shift, shift );
296              
297 22         100 my $schema = $self->result_source->schema;
298              
299 22 100       369 if ( $self->state_count > 0 ) {
    100          
300 1         10603 $self->throw_exception(
301             "Cannot add countries to zone containing states");
302             }
303             elsif ( ref($arg) ne "ARRAY" ) {
304              
305             # we need an arrayref
306 13         134016 $arg = [$arg];
307             }
308              
309             # use a transaction when adding countries so that all succeed or all fail
310              
311 21         83474 my $guard = $schema->txn_scope_guard;
312              
313 21         11122 foreach my $country (@$arg) {
314              
315 23         17396 $country = $self->_get_country_obj($country);
316              
317 16 100       85 if ( $self->has_country($country) ) {
318 1         7209 $self->throw_exception(
319             "Zone already includes country: " . $country->name );
320             }
321              
322 15         538 $self->add_to_countries($country);
323             }
324              
325 13         79634 $guard->commit;
326              
327 13         3523 return $self;
328             }
329              
330             =head2 has_country
331              
332             Argument can be Interchange6::Schema::Result::Country, country name or iso code. Returns 1 if zone includes that country else 0;
333              
334             =cut
335              
336             sub has_country {
337 34     34 1 68718 my ( $self, $country ) = ( shift, shift );
338 34         91 my $rset;
339              
340             # first try Country object
341              
342 34 100       168 if ( blessed($country) ) {
343 26 100       192 if ( $country->isa('Interchange6::Schema::Result::Country') ) {
344              
345 25         117 $rset = $self->countries->search(
346             { "country.country_iso_code" => $country->country_iso_code, } );
347 25 100       82646 return 1 if $rset->count == 1;
348              
349             }
350             else {
351 1         9 return 0;
352             }
353             }
354             else {
355              
356             # maybe an ISO code?
357              
358 8 100       48 if ( $country =~ /^[a-z]{2}$/i ) {
359              
360 6         28 $rset = $self->countries->search(
361             { "country.country_iso_code" => uc($country) } );
362              
363 6 100       16610 return 1 if $rset->count == 1;
364             }
365             else {
366              
367             # finally try country name
368              
369 2         12 $rset = $self->countries->search( { "country.name" => $country } );
370              
371 2 100       5563 return 1 if $rset->count == 1;
372             }
373             }
374              
375             # failed to find the country
376 19         138999 return 0;
377             }
378              
379             =head2 country_count
380              
381             Takes no args. Returns the number of countries in the zone.
382              
383             =cut
384              
385             sub country_count {
386 52     52 1 44612 my $self = shift;
387 52         291 return $self->countries->count;
388             }
389              
390             =head2 remove_countries
391              
392             Argument is either a L<Interchange6::Schema::Result::Country> object or an arrayref of the same.
393              
394             Throws an exception on failure.
395              
396             =cut
397              
398             sub remove_countries {
399 11     11 1 48040 my ( $self, $arg ) = ( shift, shift );
400              
401 11         61 my $schema = $self->result_source->schema;
402              
403 11 100       188 if ( $self->state_count > 0 ) {
    100          
404              
405 1         10223 $self->throw_exception("States must be removed before countries");
406              
407             }
408             elsif ( ref($arg) ne "ARRAY" ) {
409              
410             # convert to arrayref
411 5         50217 $arg = [$arg];
412             }
413              
414             # use a transaction when removing countries so that all succeed or all fail
415              
416 10         50106 my $guard = $schema->txn_scope_guard;
417              
418 10         5441 foreach my $country (@$arg) {
419              
420 11         3067 $country = $self->_get_country_obj($country);
421              
422 8 100       38 unless ( $self->has_country($country) ) {
423 1         57 $self->throw_exception(
424             "Country does not exist in zone: " . $country->name );
425             }
426              
427 7         51393 $self->remove_from_countries($country);
428             }
429              
430 6         17871 $guard->commit;
431              
432 6         1816 return $self;
433             }
434              
435             =head2 add_states
436              
437             Argument is one of:
438              
439             =over 4
440              
441             =item an L<Interchange6::Schema::Result::State> object
442              
443             =item a state ISO code
444              
445             =item an arrayref of the above (can include a mixture of both)
446              
447             =back
448              
449             Returns the zone object on success.
450              
451             =cut
452              
453             # add/remove_states can be passed all sorts of junk but we need State obj
454              
455             sub _get_state_obj {
456              
457 28     28   139 my ( $self, $state ) = @_;
458              
459             # let Devel::Cover watch this for us since it should never happen
460             # uncoverable branch true
461 28 50       263 if ( !defined $state ) {
    100          
    100          
462             # uncoverable statement
463 0         0 $self->throw_exception("State must be defined");
464             }
465             elsif ( blessed($state) ) {
466              
467 21         64 my $class = ref($state);
468              
469 21 100       211 $self->throw_exception("State cannot be a $class")
470             unless $state->isa('Interchange6::Schema::Result::State');
471              
472             }
473             elsif ( $state =~ m/^[a-z]{2}$/i ) {
474              
475 5 100       31 if ( $self->country_count == 1 ) {
476              
477 4         36443 my $result =
478             $self->result_source->schema->resultset("State")->single(
479             {
480             country_iso_code => {
481             -in => $self->countries->get_column('country_iso_code')
482             ->as_query
483             },
484             state_iso_code => uc($state),
485             }
486             );
487              
488 4 100       48249 $self->throw_exception("No state found for code: $state")
489             unless defined $result;
490              
491 3         252 $state = $result;
492              
493             }
494             else {
495             # We should have elsif/else as part of main if statement but
496             # since we have an uncoverable branch and Devel::Cover is a bit
497             # brain dead we have to split this up so. :(
498             # uncoverable branch false
499 1 50       9063 if ( $self->country_count == 0 ) {
500              
501 1         9074 $self->throw_exception(
502             "Cannot resolve state_iso_code for zone with no country");
503             }
504             else {
505             # uncoverable statement
506 0         0 $self->throw_exception(
507             "Cannot resolve state_iso_code for zone with > 1 country");
508             }
509             }
510             }
511             else {
512 2         49 $self->throw_exception("Bad state: $state");
513             }
514              
515 21         72 return $state;
516             }
517              
518             sub add_states {
519 15     15 1 39778 my ( $self, $arg ) = ( shift, shift );
520              
521 15         89 my $schema = $self->result_source->schema;
522              
523 15 100       264 if ( $self->country_count > 1 ) {
    100          
524              
525 1         9528 $self->throw_exception(
526             "Cannot add state to zone with multiple countries");
527             }
528             elsif ( ref($arg) ne "ARRAY" ) {
529              
530             # we need an arayref
531 7         69345 $arg = [$arg];
532             }
533              
534             # use a transaction when adding states so that all succeed or all fail
535              
536 14         71568 my $guard = $schema->txn_scope_guard;
537              
538 14         7490 foreach my $state (@$arg) {
539              
540 24         38544 $state = $self->_get_state_obj($state);
541              
542 19 100       141 if ( $self->country_count == 0 ) {
543              
544             # add the country first
545              
546 1         9351 $self->add_countries( $state->country );
547             }
548             else {
549              
550             # make sure state is in the existing country
551              
552 18         166578 my $country = $self->countries->single;
553              
554 18 100       147423 unless ( $country->country_iso_code eq
555             $state->country->country_iso_code )
556             {
557 2         4185 $self->throw_exception( "State "
558             . $state->name
559             . " is not in country "
560             . $country->name );
561             }
562             }
563              
564 17 100       35216 if ( $self->has_state($state) ) {
565 1         7619 $self->throw_exception(
566             "Zone already includes state: " . $state->name );
567             }
568              
569             # try to add the state
570              
571 16         550 $self->add_to_states($state);
572             }
573              
574 6         22799 $guard->commit;
575              
576 6         1981 return $self;
577             }
578              
579             =head2 has_state
580              
581             Argument can be Interchange6::Schema::Result::State, state name or iso code. Returns 1 if zone includes that state else 0;
582              
583             =cut
584              
585             sub has_state {
586 26     26 1 64448 my ( $self, $state ) = ( shift, shift );
587 26         74 my $rset;
588              
589             # first try State object
590              
591 26 100       147 if ( blessed($state) ) {
592 19 100       161 if ( $state->isa('Interchange6::Schema::Result::State') ) {
593              
594 18         99 $rset = $self->states->search(
595             {
596             "state.country_iso_code" => $state->country_iso_code,
597             "state.state_iso_code" => $state->state_iso_code
598             }
599             );
600 18 100       54387 return 1 if $rset->count == 1;
601              
602             }
603             else {
604 1         8 return 0;
605             }
606             }
607             else {
608              
609             # maybe an ISO code?
610              
611 7 100       57 if ( $state =~ /^[a-z]{2}$/i ) {
612              
613 5         34 $rset = $self->states->search( { state_iso_code => uc($state) } );
614              
615 5 100       14502 return 1 if $rset->count == 1;
616             }
617             else {
618              
619             # finally try state name
620              
621 2         11 $rset = $self->states->search( { name => $state } );
622              
623 2 100       5568 return 1 if $rset->count == 1;
624              
625             }
626             }
627              
628             # failed to find the state
629 19         147321 return 0;
630             }
631              
632             =head2 state_count
633              
634             Takes no args. Returns the number of states in the zone.
635              
636             =cut
637              
638             sub state_count {
639 43     43 1 189681 my $self = shift;
640 43         230 return $self->states->search( {} )->count;
641             }
642              
643             =head2 remove_states
644              
645             Argument is either a L<Interchange6::Schema::Result::State> object or an arrayref of the same.
646              
647             Returns the Zone object or undef on failure. Errors are available via errors method inherited from L<Interchange6::Schema::Role::Errors>.
648              
649             =cut
650              
651             sub remove_states {
652 4     4 1 16479 my ( $self, $arg ) = ( shift, shift );
653              
654 4         24 my $schema = $self->result_source->schema;
655              
656 4 100       89 if ( ref($arg) ne "ARRAY" ) {
657              
658             # we need an arrayref
659 3         15 $arg = [$arg];
660             }
661              
662             # use a transaction when removing states so that all succeed or all fail
663              
664 4         27 my $guard = $schema->txn_scope_guard;
665              
666 4         2262 foreach my $state (@$arg) {
667              
668 4         19 $state = $self->_get_state_obj($state);
669              
670 2         14 $self->remove_from_states($state);
671             }
672              
673 2         6099 $guard->commit;
674              
675 2         516 return $self;
676             }
677              
678             1;