File Coverage

blib/lib/Business/Shipping.pm
Criterion Covered Total %
statement 17 85 20.0
branch 1 32 3.1
condition 1 6 16.6
subroutine 6 16 37.5
pod 6 8 75.0
total 31 147 21.0


line stmt bran cond sub pod time code
1             # Copyright 2003-2011 Daniel Browning . All rights reserved.
2             # This program is free software; you may redistribute it and/or modify it
3             # under the same terms as Perl itself. See LICENSE for more info.
4              
5             package Business::Shipping;
6              
7             =head1 NAME
8              
9             Business::Shipping - Rates and tracking for UPS and USPS
10              
11             =cut
12              
13 13     13   456544 use Any::Moose;
  13         616252  
  13         95  
14 13     13   8527 use Carp;
  13         31  
  13         975  
15 13     13   13636 use Business::Shipping::Logging;
  13         53  
  13         2643  
16 13     13   17423 use Business::Shipping::Util 'unique';
  13         52  
  13         962  
17              
18             =head1 VERSION
19              
20             Version 3.1.0
21              
22             =cut
23              
24 13     13   75 use version; our $VERSION = qv('3.1.0');
  13         28  
  13         61  
25              
26             =head1 SYNOPSIS
27              
28             =head2 Rate request example
29              
30             use Business::Shipping;
31            
32             my $rate_request = Business::Shipping->rate_request(
33             shipper => 'UPS_Offline',
34             service => 'Ground Residential',
35             from_zip => '98683',
36             to_zip => '98270',
37             weight => 5.00,
38             );
39            
40             $rate_request->execute() or die $rate_request->user_error();
41            
42             print $rate_request->rate();
43              
44             =head1 FEATURES
45              
46             Business::Shipping currently supports three shippers:
47              
48             =head2 UPS_Offline: United Parcel Service
49              
50             =over 4
51              
52             =item * Shipment rate estimation using offline tables.
53              
54             As of January, 2007, UPS has only released the data tables in binary Excel
55             format. These have not been converted to the text files necessary for use in
56             this module. A script is distributed with the module for automatically
57             updating the fuel surcharge every month.
58              
59             =back
60              
61             =head2 UPS_Online: United Parcel Service using UPS OnLine Tools
62              
63             =over 4
64              
65             =item * Shipment rate estimation
66              
67             =item * Shipment tracking.
68              
69             =item * Rate Shopping.
70              
71             Gets rates for all the services in one request:
72              
73             my $rr_shop = Business::Shipping->rate_request(
74             service => 'shop',
75             shipper => 'UPS_Online',
76             from_zip => '98682',
77             to_zip => '98270',
78             weight => 5.00,
79             user_id => '',
80             password => '',
81             access_key => '',
82             );
83            
84             $rr_shop->execute() or die $rr_shop->user_error();
85            
86             foreach my $shipper ( @$results ) {
87             print "Shipper: $shipper->{name}\n\n";
88             foreach my $rate ( @{ $shipper->{ rates } } ) {
89             print " Service: $rate->{name}\n";
90             print " Charges: $rate->{charges_formatted}\n";
91             print " Delivery: $rate->{deliv_date_formatted}\n"
92             if $rate->{ deliv_date_formatted };
93             print "\n";
94             }
95             }
96              
97             =item * C.O.D. (Cash On Delivery)
98              
99             Add these options to your rate request for C.O.D.:
100              
101             cod: enable C.O.D.
102              
103             cod_funds_code: The code that indicates the type of funds that will be used for
104             the COD payment. Required if CODCode is 1, 2, or 3. Valid Values: 0 = All Funds
105             Allowed. 8 = cashier's check or money order, no cash allowed.
106              
107             cod_value: The COD value for the package. Required if COD option is present.
108             Valid values: 0.01 - 50000.00
109              
110             cod_code: The code associated with the type of COD. Values: 1 = Regular COD,
111             2 = Express COD, 3 = Tagless COD
112            
113             For example:
114              
115             cod => 1,
116             cod_value => 400.00,
117             cod_funds_code => 0,
118              
119             =back
120              
121             =head2 USPS_Online: United States Postal Service
122              
123             =over 4
124              
125             =item * Shipment rate estimation using USPS Online WebTools.
126              
127             =item * Shipment tracking
128              
129             =back
130              
131             =head1 INSTALLATION
132              
133             perl -MCPAN -e 'install Bundle::Business::Shipping'
134              
135             See INSTALL.
136              
137             =head1 REQUIRED MODULES
138              
139             Any::Moose (any)
140             Config::IniFiles (any)
141             Log::Log4perl (any)
142              
143             See INSTALL.
144              
145             =head1 OPTIONAL MODULES
146              
147             For UPS offline rate estimation:
148              
149             Business::Shipping::DataFiles (any)
150              
151             The following modules are used by online rate estimation and tracking. See
152             INSTALL.
153              
154             CHI (0.39)
155             Crypt::SSLeay (any)
156             LWP::UserAgent (any)
157             XML::DOM (any)
158             XML::Simple (2.05)
159              
160             =head1 GETTING STARTED
161              
162             Be careful to read, understand, and comply with the terms of use for the
163             shipping service that you will use.
164              
165             =head2 UPS_Offline: For United Parcel Service (UPS) offline rate requests
166              
167             No signup required. C has all of rate tables,
168             which are usually updated only once per year.
169              
170             We recommend that you run the following script to update your fuel surcharge
171             every first monday of the month.
172              
173             bin/Business-Shipping-UPS_Offline-update-fuel-surcharge.pl
174              
175             =head2 UPS_Online: For United Parcel Service (UPS) Online XML: Free signup
176              
177             =over 4
178              
179             =item * Read the legal terms and conditions:
180             L
181              
182             =item * L
183              
184             =item * After receiving a User Id and Password from UPS, login, then select
185             "Get Access Key", then "Get XML Access Key".
186              
187             =item * Read more about UPS Online Tools:
188             L
189              
190             =back
191              
192             =head2 USPS_Online: For United States Postal Service (USPS): Free signup
193              
194             =over 4
195              
196             =item * L
197              
198             =item * (More info at L)
199              
200             =item * The online signup will result in a testing-only account (only a small
201             sample of queries will work).
202              
203             =item * To activate the "production" use of your USPS account, you must follow
204             the USPS documentation. As of Sept 16 2004, that means contacting the
205             USPS Internet Customer Care Center by e-mail
206             (C) or phone: 1-800-344-7779.
207              
208             =back
209              
210             =head1 ERROR/DEBUG HANDLING
211              
212             Log4perl is used for logging error, debug, etc. messages. For simple
213             manipulation of the current log level, use the Business::Shipping->log_level()
214             class method (below). For more advanced logging/debugging options, see
215             config/log4perl.conf.
216              
217             =head1 Preloading Modules
218              
219             To preload all modules, call Business::Shipping with this syntax:
220              
221             use Business::Shipping { preload => 'All' };
222              
223             To preload the modules for just one shipper:
224              
225             use Business::Shipping { preload => 'USPS_Online' };
226            
227             Without preloading, some modules will be loaded at runtime. Normally, runtime
228             loading is the best mode of operation. However, there are some circumstances
229             when preloading is advantagous. For example:
230              
231             =over 4
232              
233             =item * For mod_perl, to load the modules only once at startup to reduce memory
234             utilization.
235              
236             =item * For compatibilty with some security modules (e.g. Safe).
237              
238             =item * To move the delay that would normally occur with the first request into
239             startup time. That way, it takes longer to start up, but the first user
240             will not experience any delay.
241              
242             =back
243              
244             =head1 METHODS
245              
246             =cut
247              
248             has 'tx_type' => (is => 'rw', isa => 'Str');
249             has 'shipper' => (is => 'rw', isa => 'Str');
250             has '_user_error_msg' => (is => 'rw', isa => 'Str');
251              
252             __PACKAGE__->meta()->make_immutable();
253              
254             $Business::Shipping::RuntimeLoad = 1;
255              
256             sub import {
257 21     21   2117 my ($class_name, $record) = @_;
258              
259 21 50 33     1516303 return unless defined $record and ref($record) eq 'HASH';
260              
261 0           while (my ($key, $val) = each %$record) {
262 0 0         if (lc $key eq 'preload') {
263              
264             # Required modules lists
265             # ======================
266             # Each of these modules does a compile-time require of all
267             # the modules that it needs. If, in the future, any of these
268             # modules switch to a run-time require, then update this list with
269             # the modules that may be run-time required.
270 0           my $module_list = {
271             'USPS_Online' =>
272             ['Business::Shipping::USPS_Online::Tracking',],
273             'UPS_Online' => ['Business::Shipping::UPS_Online::Tracking',],
274             'UPS_Offline' => [],
275             };
276              
277 0           my @to_load;
278 0           while (my ($shipper, $mod_list) = each %$module_list) {
279 0 0 0       if (lc $val eq lc $shipper or lc $val eq 'all') {
280 0           my $rate_req_mod
281             = 'Business::Shipping::' . $shipper . '::RateRequest';
282 0           push @to_load, (@$mod_list, $rate_req_mod);
283             }
284             }
285              
286 0 0         if (@to_load) {
287 0           $Business::Shipping::RuntimeLoad = 0;
288             }
289 0           my @unique_to_load = Business::Shipping::Util::unique(@to_load);
290 0           foreach my $module (@unique_to_load) {
291 0           eval "use $module;";
292 0 0         die $@ if $@;
293             }
294             }
295             }
296             }
297              
298             =head2 $obj->init()
299              
300             Generic attribute setter.
301              
302             =cut
303              
304             sub init {
305 0     0 1   my ($self, %args) = @_;
306              
307 0           foreach my $arg (keys %args) {
308 0 0         if ($self->can($arg)) {
309 0           $self->$arg($args{$arg});
310             }
311             }
312              
313 0           return;
314             }
315              
316             =head2 $obj->user_error()
317              
318             Log and store errors that should be visibile to the user.
319              
320             =cut
321              
322             sub user_error {
323 0     0 1   my ($self, $msg) = @_;
324              
325 0 0         if (defined $msg) {
326 0           $self->_user_error_msg($msg);
327 0           error($msg);
328             }
329              
330 0           return $self->_user_error_msg;
331             }
332              
333             =head2 $obj->validate()
334              
335             Confirms that the object is valid. Checks that required attributes are set.
336              
337             =cut
338              
339             sub validate {
340 0     0 1   trace '()';
341 0           my ($self) = shift;
342              
343 0           my @required = $self->get_grouped_attrs('Required');
344 0           my @optional = $self->get_grouped_attrs('Optional');
345              
346 0           info("required = " . join(', ', @required));
347 0           trace("optional = " . join(', ', @optional));
348              
349 0           my @missing;
350 0           foreach my $required_field (@required) {
351 0 0         if (!$self->$required_field()) {
352 0           push @missing, $required_field;
353             }
354             }
355              
356 0 0         if (@missing) {
357 0           my $user_error = "Missing required argument(s): " . join ", ",
358             @missing;
359 0           $self->user_error($user_error);
360 0           $self->invalid(1);
361 0           return 0;
362             }
363             else {
364 0           return 1;
365             }
366             }
367              
368             =head2 $self->get_grouped_attrs( $attribute_name )
369              
370             =cut
371              
372             # attr_name = Attribute Name.
373             sub get_grouped_attrs {
374 0     0 1   my ($self, $attr_name) = @_;
375 0           my @results = $self->$attr_name();
376              
377             #print "get_grouped_attrs( $attr_name ): " . join( ', ', @results ) . "\n";
378 0           return @results;
379             }
380              
381             =head2 $obj->rate_request()
382              
383             This method is used to request shipping rate information from online providers
384             or offline tables. A hash is accepted as input. The acceptable values are
385             determined by the shipper class, but the following are common to all:
386              
387             =over 4
388              
389             =item * shipper
390              
391             The name of the shipper to use. Must correspond to a module by the name of:
392             C. For example, C.
393              
394             =item * service
395              
396             A valid service name for the provider. See the corresponding module
397             documentation for a list of services compatible with the shipper.
398              
399             =item * from_zip
400              
401             The origin zipcode.
402              
403             =item * from_state
404              
405             The origin state in two-letter code format or full-name format. Required for
406             UPS_Offline.
407              
408             =item * to_zip
409              
410             The destination zipcode.
411              
412             =item * to_country
413              
414             The destination country. Required for international shipments only.
415              
416             =item * weight
417              
418             Weight of the shipment, in pounds, as a decimal number.
419              
420             =back
421              
422             There are some additional common values:
423              
424             =over 4
425              
426             =item * user_id
427              
428             A user_id, if required by the provider. USPS_Online and UPS_Online require
429             this, while UPS_Offline does not.
430              
431             =item * password
432              
433             A password, if required by the provider. USPS_Online and UPS_Online require
434             this, while UPS_Offline does not.
435              
436             =back
437              
438             =cut
439              
440             sub rate_request {
441 0     0 1   my $class = shift;
442 0           my (%opt) = @_;
443 0           my $shipper = $opt{shipper};
444              
445 0 0         Carp::croak 'shipper required' unless $opt{shipper};
446              
447 0           $shipper = _compat_shipper_name($shipper);
448              
449 0           my $rr = Business::Shipping->_new_subclass($shipper . '::RateRequest');
450 0 0         logdie "New $shipper::RateRequest object was undefined."
451             if not defined $rr;
452              
453 0           $rr->init(%opt);
454              
455 0           return $rr;
456             }
457              
458             # _compat_shipper_name
459             #
460             # Shipper name backwards-compatibility
461             #
462             # 1. Really old: "UPS" or "USPS" (implies Online::)
463             # 2. Semi-old: "Online::UPS", "Offline::UPS", or "Online::USPS"
464             # 3. Current: "UPS_Online", "UPS_Offline", or "USPS_Online"
465             sub _compat_shipper_name {
466 0     0     my ($shipper) = @_;
467              
468 0           my %old_to_new = (
469             'Online::UPS' => 'UPS_Online',
470             'Offline::UPS' => 'UPS_Offline',
471             'Online::USPS' => 'USPS_Online',
472             'UPS' => 'UPS_Online',
473             'USPS' => 'USPS_Online'
474             );
475 0 0         $shipper = $old_to_new{$shipper} if $old_to_new{$shipper};
476              
477 0           return $shipper;
478             }
479              
480             =head2 Business::Shipping->log_level()
481              
482             Simple alternative to editing the config/log4perl.conf file. Sets the log
483             level for all Business::Shipping objects.
484              
485             Takes a scalar that can be 'trace', 'debug', 'info', 'warn', 'error', or
486             'fatal'.
487              
488             =cut
489              
490             *log_level = *Business::Shipping::Logging::log_level;
491              
492             #=head2 Business::Shipping->_new_subclass()
493             #
494             #Private Method.
495             #
496             #Generates an object of a given subclass dynamically. Will dynamically 'use'
497             #the corresponding module, unless runtime module loading has been disabled via
498             #the 'preload' option.
499             #
500             #=cut
501              
502             sub _new_subclass {
503 0     0     my ($class, $subclass, %opt) = @_;
504              
505 0 0         croak("Error before _new_subclass was called: $@") if $@;
506              
507 0           my $new_class = $class . '::' . $subclass;
508              
509 0 0         if ($Business::Shipping::RuntimeLoad) {
510 0           eval "use $new_class";
511             }
512              
513 0 0         croak("Error when trying to use $new_class: \n\t$@") if $@;
514              
515 0           my $new_sub_object = eval "$new_class->new()";
516 0 0         croak("Failed to create new $new_class object. Error: $@") if $@;
517              
518 0           return $new_sub_object;
519             }
520              
521 0     0 0   sub Optional { return qw/ tx_type /; }
522 0     0 1   sub Required { return (); }
523 0     0 0   sub Unique { return (); }
524              
525             1;
526              
527             __END__