File Coverage

blib/lib/Shipment/UPS.pm
Criterion Covered Total %
statement 22 24 91.6
branch n/a
condition n/a
subroutine 8 8 100.0
pod n/a
total 30 32 93.7


line stmt bran cond sub pod time code
1             package Shipment::UPS;
2             $Shipment::UPS::VERSION = '2.00';
3 3     3   119175 use strict;
  3         8  
  3         163  
4 3     3   22 use warnings;
  3         6  
  3         126  
5              
6              
7 3     3   2409 use Try::Tiny;
  3         10297  
  3         259  
8 3     3   21962 use Shipment::SOAP::WSDL;
  3         14  
  3         142  
9 3     3   58 use Moo;
  3         1027  
  3         24  
10 3     3   3118 use MooX::Types::MooseLike::Base qw(:all);
  3         30130  
  3         1589  
11 3     3   63 use namespace::clean;
  3         17554  
  3         27  
12              
13             extends 'Shipment::Base';
14              
15              
16             has 'username' => (
17             is => 'rw',
18             isa => Str,
19             );
20              
21             has 'password' => (
22             is => 'rw',
23             isa => Str,
24             );
25              
26             has 'key' => (
27             is => 'rw',
28             isa => Str,
29             );
30              
31              
32             has 'proxy_domain' => (
33             is => 'rw',
34             isa => Enum [
35             qw(
36             wwwcie.ups.com
37             onlinetools.ups.com
38             )
39             ],
40             default => 'wwwcie.ups.com',
41             );
42              
43              
44             has 'negotiated_rates' => (
45             is => 'rw',
46             isa => Bool,
47             default => 0,
48             );
49              
50              
51             has 'residential_address' => (
52             is => 'rw',
53             isa => Bool,
54             default => 0,
55             );
56              
57              
58             has 'address_validation' => (
59             is => 'rw',
60             isa => Bool,
61             default => 1,
62             );
63              
64              
65             has 'label_height' => (
66             is => 'rw',
67             isa => Enum [qw( 6 8 )],
68             default => 6,
69             );
70              
71              
72             has 'control_log_receipt' => (
73             is => 'rw',
74             isa => InstanceOf ['Shipment::Label'],
75             );
76              
77              
78             has 'carbon_neutral' => (
79             is => 'rw',
80             isa => Bool,
81             default => undef,
82             );
83              
84              
85             my %service_map = (
86             '01' => 'UPS Next Day Air',
87             '02' => 'UPS Second Day Air',
88             '03' => 'UPS Ground',
89             '07' => 'UPS Worldwide Express',
90             '08' => 'UPS Worldwide Expedited',
91             '11' => 'UPS Standard',
92             '12' => 'UPS Three-Day Select',
93             '13' => 'UPS Next Day Air Saver',
94             '14' => 'UPS Next Day Air Early A.M.',
95             '54' => 'UPS Worldwide Express Plus',
96             '59' => 'UPS Second Day Air A.M.',
97             '65' => 'UPS Saver',
98             '82' => 'UPS Today Standard',
99             '83' => 'UPS Today Dedicated Courier',
100             '85' => 'UPS Today Express',
101             '86' => 'UPS Today Express Saver',
102             '93' => 'UPS SurePost 1 lb or Greater',
103             'CA' => {
104             '01' => 'UPS Express',
105             '13' => 'UPS Express Saver',
106             '65' => 'UPS Worldwide Express Saver',
107             '02' => 'UPS Expedited',
108             },
109             );
110              
111             ## Rating code to Shipping code map for cases when they differ
112             my %service_code_map = (
113             'CA' => {
114             '07' => '01',
115             '13' => '65',
116             '02' => '08',
117             },
118             );
119              
120              
121             my %bill_type_map = (
122             'sender' => 'BillShipper',
123             'recipient' => 'BillReceiver',
124             'third_party' => 'BillThirdParty',
125             );
126              
127             my %signature_type_map = (
128             'default' => '1',
129             'required' => '2',
130             'not_required' => undef,
131             'adult' => '3',
132             );
133              
134             my %package_type_map = (
135             'custom' => '02',
136             'envelope' => '01',
137             'tube' => '03',
138             'box' => '21',
139             'pack' => '04',
140             '25kg_box' => '24',
141             '10kg_box' => '25',
142             'pallet' => '30',
143             'small_express_box' => '2a',
144             'medium_express_box' => '2b',
145             'large_express_box' => '2c',
146             );
147              
148             my %units_type_map = (
149             'lb' => 'LBS',
150             'kg' => 'KGS',
151             'in' => 'IN',
152             'cm' => 'CM',
153             );
154              
155              
156             has '+package_type' => (
157             isa => Enum [
158             qw( custom envelope tube box pack 25kg_box 10kg_box pallet small_express_box medium_express_box large_express_box )
159             ]
160             );
161              
162             my %printer_type_map = (
163             'pdf' => '',
164             'thermal' => 'EPL',
165             'image' => 'GIF',
166             'ZPL' => 'ZPL',
167             'SPL' => 'SPL',
168             'STARPL' => 'STARPL',
169             );
170              
171             my %label_content_type_map = (
172             'thermal' => 'text/ups-epl',
173             'image' => 'image/gif',
174             'ZPL' => 'text/ups-zpl',
175             'SPL' => 'text/ups-spl',
176             'STARPL' => 'text/ups-starpl',
177             );
178              
179              
180             # FIXME: check whether this is needed:
181             #enum 'PrinterOptions' => [qw( thermal image ZPL SPL STARPL )];
182              
183             has '+printer_type' => (default => 'image',);
184              
185              
186             has '+currency' => (default => 'USD',);
187              
188              
189             has 'surepost' => (
190             is => 'rw',
191             isa => Bool,
192             default => undef,
193             );
194              
195              
196             sub _build_services {
197             my $self = shift;
198              
199 3     3   4446 use Shipment::Package;
  0            
  0            
200             use Shipment::Service;
201             use Shipment::UPS::WSDL::RateInterfaces::RateService::RatePort;
202              
203             my $interface =
204             Shipment::UPS::WSDL::RateInterfaces::RateService::RatePort->new(
205             {proxy_domain => $self->proxy_domain,});
206             my $response;
207              
208             my $options;
209             $options->{DeliveryConfirmation}->{DCISType} =
210             $signature_type_map{$self->signature_type}
211             if defined $signature_type_map{$self->signature_type};
212             $options->{DeclaredValue}->{CurrencyCode} = $self->currency;
213              
214             my $rating_options;
215             $rating_options->{NegotiatedRatesIndicator} = 1 if $self->negotiated_rates;
216              
217             my $shipment_options;
218             $shipment_options->{UPScarbonneutralIndicator} = ''
219             if $self->carbon_neutral;
220              
221             my @pieces;
222             foreach (@{$self->packages}) {
223             $options->{DeclaredValue}->{MonetaryValue} = $_->insured_value->value;
224              
225             ## SurePost doesn't accept service options
226             $options = undef if $self->surepost;
227              
228             push @pieces,
229             { PackagingType => {
230             Code => $package_type_map{$self->package_type}
231             || $self->package_type,
232             },
233             Dimensions => {
234             UnitOfMeasurement => {
235             Code => $units_type_map{$self->dim_unit}
236             || $self->dim_unit,
237             },
238             Length => $_->length,
239             Width => $_->width,
240             Height => $_->height,
241             },
242             PackageWeight => {
243             UnitOfMeasurement => {
244             Code => $units_type_map{$self->weight_unit}
245             || $self->weight_unit,
246             },
247             Weight => $_->weight,
248             },
249             PackageServiceOptions => $options,
250             };
251             }
252              
253             my @from_addresslines = (
254             $self->from_address->address1,
255             $self->from_address->address2,
256             $self->from_address->address3
257             );
258             my @to_addresslines = (
259             $self->to_address->address1,
260             $self->to_address->address2,
261             $self->to_address->address3
262             );
263              
264             my $shipto = {
265             Address => {
266             AddressLine => \@to_addresslines,
267             City => $self->to_address()->city,
268             StateProvinceCode => $self->to_address()->province_code,
269             PostalCode => $self->to_address()->postal_code,
270             CountryCode => $self->to_address()->country_code,
271             },
272             };
273             $shipto->{Address}->{ResidentialAddressIndicator} = 1
274             if $self->{residential_address};
275             $shipto->{Phone}{Number} = $self->to_address->phone
276             if $self->to_address->phone;
277              
278             my %services;
279             try {
280             $response = $interface->ProcessRate(
281             { Request => {RequestOption => 'Shop',},
282             Shipment => {
283             Shipper => {
284             ShipperNumber => $self->account,
285             Address => {
286             AddressLine => \@from_addresslines,
287             City => $self->from_address()->city,
288             StateProvinceCode =>
289             $self->from_address()->province_code,
290             PostalCode => $self->from_address()->postal_code,
291             CountryCode => $self->from_address()->country_code,
292             },
293             },
294             ShipTo => $shipto,
295             ShipmentRatingOptions => $rating_options,
296             Package => \@pieces,
297             ShipmentServiceOptions => $shipment_options,
298             },
299             },
300             { UsernameToken => {
301             Username => $self->username,
302             Password => $self->password,
303             },
304             ServiceAccessToken => {AccessLicenseNumber => $self->key,},
305             },
306             );
307              
308             #warn $response;
309              
310             foreach my $service (@{$response->get_RatedShipment()}) {
311             my $rate = $service->get_TotalCharges->get_MonetaryValue;
312             my $currency = $service->get_TotalCharges->get_CurrencyCode;
313             if ($self->negotiated_rates) {
314             if ($service->get_NegotiatedRateCharges) {
315             $rate =
316             $service->get_NegotiatedRateCharges->get_TotalCharge
317             ->get_MonetaryValue;
318             $currency =
319             $service->get_NegotiatedRateCharges->get_TotalCharge
320             ->get_CurrencyCode;
321             }
322             }
323             $services{$service->get_Service()->get_Code()->get_value} =
324             Shipment::Service->new(
325             id => $service->get_Service()->get_Code()->get_value,
326             name => (
327             $service_map{$self->from_address()->country_code}
328             ->{$service->get_Service()->get_Code()->get_value}
329             || $service_map{$service->get_Service()->get_Code()
330             ->get_value}
331             ),
332             cost => Data::Currency->new($rate, $currency),
333             );
334             }
335             $services{ground} = $services{'03'} || $services{'11'} || undef;
336             $services{express} =
337             $services{'02'} || $services{'13'} || $services{'65'} || undef;
338             $services{priority} = $services{'01'} || undef;
339             foreach (qw/ground express priority/) {
340             delete $services{$_} if !$services{$_};
341             }
342              
343             $self->notice('');
344             if ($response->get_Response->get_Alert) {
345             foreach my $alert (@{$response->get_Response->get_Alert}) {
346             warn "Notice: " . $alert->get_Description->get_value;
347             $self->add_notice($alert->get_Description->get_value . "\n");
348             }
349             }
350              
351             }
352             catch {
353             #warn $_;
354             try {
355             warn "Error: "
356             . $response->get_detail()->get_Errors()->get_ErrorDetail()
357             ->get_PrimaryErrorCode()->get_Description;
358             $self->error(
359             $response->get_detail()->get_Errors()->get_ErrorDetail()
360             ->get_PrimaryErrorCode()->get_Description->get_value);
361             }
362             catch {
363             #warn $_;
364             warn "Error: " . $response->get_faultstring;
365             $self->error($response->get_faultstring->get_value);
366             };
367             };
368              
369             if ($self->surepost) {
370             if ($self->error) {
371             $self->add_notice(
372             'All services other than SurePost failed due to error: '
373             . $self->error
374             . "\n");
375             $self->error('');
376             }
377             $services{93} = Shipment::Service->new(
378             id => '93',
379             name => $service_map{93},
380             );
381             $services{surepost} = $services{93};
382             }
383              
384             \%services;
385             }
386              
387              
388             sub rate {
389             my ($self, $service_id) = @_;
390              
391             try {
392             $service_id = $self->services->{$service_id}->id;
393             }
394             catch {
395             #warn $_;
396             warn "service ($service_id) not available";
397             $self->error("service ($service_id) not available");
398             $service_id = '';
399             };
400             return unless $service_id;
401              
402             my $options;
403             $options->{DeliveryConfirmation}->{DCISType} =
404             $signature_type_map{$self->signature_type}
405             if defined $signature_type_map{$self->signature_type};
406             $options->{DeclaredValue}->{CurrencyCode} = $self->currency;
407              
408             my $rating_options;
409             $rating_options->{NegotiatedRatesIndicator} = 1 if $self->negotiated_rates;
410              
411             my $shipment_options;
412             $shipment_options->{UPScarbonneutralIndicator} = ''
413             if $self->carbon_neutral;
414              
415             my @pieces;
416             foreach (@{$self->packages}) {
417             $options->{DeclaredValue}->{MonetaryValue} = $_->insured_value->value;
418              
419             ## SurePost doesn't accept service options
420             $options = undef if $self->surepost && $service_id eq '93';
421              
422             push @pieces,
423             { PackagingType => {
424             Code => $package_type_map{$self->package_type}
425             || $self->package_type,
426             },
427             Dimensions => {
428             UnitOfMeasurement => {
429             Code => $units_type_map{$self->dim_unit}
430             || $self->dim_unit,
431             },
432             Length => $_->length,
433             Width => $_->width,
434             Height => $_->height,
435             },
436             PackageWeight => {
437             UnitOfMeasurement => {
438             Code => $units_type_map{$self->weight_unit}
439             || $self->weight_unit,
440             },
441             Weight => $_->weight,
442             },
443             PackageServiceOptions => $options,
444             };
445             }
446              
447             my @from_addresslines = (
448             $self->from_address->address1,
449             $self->from_address->address2,
450             $self->from_address->address3
451             );
452             my @to_addresslines = (
453             $self->to_address->address1,
454             $self->to_address->address2,
455             $self->to_address->address3
456             );
457              
458              
459             my $shipto = {
460             Address => {
461             AddressLine => \@to_addresslines,
462             City => $self->to_address()->city,
463             StateProvinceCode => $self->to_address()->province_code,
464             PostalCode => $self->to_address()->postal_code,
465             CountryCode => $self->to_address()->country_code,
466             },
467             };
468             $shipto->{Address}->{ResidentialAddressIndicator} = 1
469             if $self->{residential_address};
470             $shipto->{Phone}{Number} = $self->to_address->phone
471             if $self->to_address->phone;
472              
473             use Shipment::UPS::WSDL::RateInterfaces::RateService::RatePort;
474              
475             my $interface =
476             Shipment::UPS::WSDL::RateInterfaces::RateService::RatePort->new(
477             {proxy_domain => $self->proxy_domain,});
478              
479             my $response;
480             try {
481              
482             $response = $interface->ProcessRate(
483             { Request => {RequestOption => 'Rate',},
484             Shipment => {
485             Shipper => {
486             ShipperNumber => $self->account,
487             Address => {
488             AddressLine => \@from_addresslines,
489             City => $self->from_address->city,
490             StateProvinceCode =>
491             $self->from_address->province_code,
492             PostalCode => $self->from_address->postal_code,
493             CountryCode => $self->from_address->country_code,
494             },
495             },
496             ShipTo => $shipto,
497             ShipmentRatingOptions => $rating_options,
498             Service => {Code => $service_id,},
499             Package => \@pieces,
500             ShipmentServiceOptions => $shipment_options,
501             },
502             },
503             { UsernameToken => {
504             Username => $self->username,
505             Password => $self->password,
506             },
507             ServiceAccessToken => {AccessLicenseNumber => $self->key,},
508             },
509             );
510              
511             #warn $response;
512              
513             use Data::Currency;
514             use Shipment::Service;
515             my $rate =
516             $response->get_RatedShipment->get_TotalCharges->get_MonetaryValue;
517             my $currency =
518             $response->get_RatedShipment->get_TotalCharges->get_CurrencyCode;
519             if ($self->negotiated_rates) {
520             if ($response->get_RatedShipment->get_NegotiatedRateCharges) {
521             $rate = $response->get_RatedShipment->get_NegotiatedRateCharges
522             ->get_TotalCharge->get_MonetaryValue;
523             $currency =
524             $response->get_RatedShipment->get_NegotiatedRateCharges
525             ->get_TotalCharge->get_CurrencyCode;
526             }
527             }
528             $self->service(
529             new Shipment::Service(
530             id => $service_id,
531             name => (
532             $service_map{$self->from_address()->country_code}->{
533             $response->get_RatedShipment->get_Service->get_Code
534             ->get_value
535             }
536             || $service_map{
537             $response->get_RatedShipment->get_Service->get_Code
538             ->get_value
539             }
540             ),
541             cost => Data::Currency->new($rate, $currency),
542             )
543             );
544              
545             $self->notice('');
546             if ($response->get_Response->get_Alert) {
547             foreach my $alert (@{$response->get_Response->get_Alert}) {
548             warn $alert->get_Description->get_value;
549             $self->add_notice($alert->get_Description->get_value . "\n");
550             }
551             }
552             }
553             catch {
554             #warn $_;
555             try {
556             warn $response->get_detail()->get_Errors()->get_ErrorDetail()
557             ->get_PrimaryErrorCode()->get_Description;
558             $self->error(
559             $response->get_detail()->get_Errors()->get_ErrorDetail()
560             ->get_PrimaryErrorCode()->get_Description->get_value);
561             }
562             catch {
563             #warn $_;
564             warn $response->get_faultstring;
565             $self->error($response->get_faultstring->get_value);
566             };
567             };
568              
569             }
570              
571              
572             sub ship {
573             my ($self, $service_id) = @_;
574              
575             try {
576             $service_id = $self->services->{$service_id}->id;
577             }
578             catch {
579             #warn $_;
580             warn "service ($service_id) not available";
581             $self->error("service ($service_id) not available");
582             $service_id = '';
583             };
584             return unless $service_id;
585              
586             my $package_options;
587             $package_options->{DeliveryConfirmation}->{DCISType} =
588             $signature_type_map{$self->signature_type}
589             if defined $signature_type_map{$self->signature_type};
590             $package_options->{DeclaredValue}->{CurrencyCode} = $self->currency;
591              
592             my $shipment_options;
593             if ($self->to_address->email) {
594             $shipment_options->{Notification}->{NotificationCode} = '6';
595             $shipment_options->{Notification}->{EMail}->{EMailAddress} =
596             $self->to_address->email;
597             $shipment_options->{Notification}->{EMail}->{SubjectCode} = '03';
598             }
599             $shipment_options->{UPScarbonneutralIndicator} = ''
600             if $self->carbon_neutral;
601              
602             my $rating_options;
603             $rating_options->{NegotiatedRatesIndicator} = 1 if $self->negotiated_rates;
604              
605             my @pieces;
606             my $reference_index = 1;
607             foreach (@{$self->packages}) {
608             $package_options->{DeclaredValue}->{MonetaryValue} =
609             $_->insured_value->value;
610              
611             ## SurePost doesn't accept service options
612             $package_options = undef if $self->surepost && $service_id eq '93';
613              
614             my @references;
615             if ( $self->references
616             && $self->from_address->country_code =~ /(US|PR)/
617             && $self->to_address->country_code =~ /(US|PR)/
618             && $self->from_address->country_code eq
619             $self->to_address->country_code)
620             {
621             foreach ($self->get_reference(0), $self->get_reference(1)) {
622             next if !$_;
623             push @references,
624             { Code => $reference_index,
625             Value => $_,
626             };
627             $reference_index++;
628             }
629             }
630             push @pieces,
631             { Packaging => {
632             Code => $package_type_map{$self->package_type}
633             || $self->package_type,
634             },
635             Dimensions => {
636             UnitOfMeasurement => {
637             Code => $units_type_map{$self->dim_unit}
638             || $self->dim_unit,
639             },
640             Length => $_->length,
641             Width => $_->width,
642             Height => $_->height,
643             },
644             PackageWeight => {
645             UnitOfMeasurement => {
646             Code => $units_type_map{$self->weight_unit}
647             || $self->weight_unit,
648             },
649             Weight => $_->weight,
650             },
651             ReferenceNumber => \@references,
652             PackageServiceOptions => $package_options,
653             };
654             }
655              
656             my $payment_option;
657             $payment_option->{Type} = '01';
658             $payment_option->{$bill_type_map{$self->bill_type}}->{AccountNumber} =
659             $self->bill_account;
660             $payment_option->{$bill_type_map{$self->bill_type}}->{Address}
661             ->{PostalCode} = $self->bill_address->postal_code
662             if $self->bill_type =~ /(recipient|third_party)/;
663             $payment_option->{$bill_type_map{$self->bill_type}}->{Address}
664             ->{CountryCode} = $self->bill_address->country_code
665             if $self->bill_type eq 'third_party';
666              
667             my @from_addresslines = (
668             $self->from_address->address1,
669             $self->from_address->address2,
670             $self->from_address->address3
671             );
672             my @to_addresslines = (
673             $self->to_address->address1,
674             $self->to_address->address2,
675             $self->to_address->address3
676             );
677              
678             my $shipto = {
679             Name => $self->to_address->company,
680             AttentionName => $self->to_address->name,
681             Address => {
682             AddressLine => \@to_addresslines,
683             City => $self->to_address->city,
684             StateProvinceCode => $self->to_address->province_code,
685             PostalCode => $self->to_address->postal_code,
686             CountryCode => $self->to_address->country_code,
687             },
688             };
689             $shipto->{Address}->{ResidentialAddressIndicator} = 1
690             if $self->{residential_address};
691             $shipto->{Phone}{Number} = $self->to_address->phone
692             if $self->to_address->phone;
693              
694             use Shipment::UPS::WSDL::ShipInterfaces::ShipService::ShipPort;
695              
696             my $interface =
697             Shipment::UPS::WSDL::ShipInterfaces::ShipService::ShipPort->new(
698             {proxy_domain => $self->proxy_domain,});
699              
700             my $response;
701             try {
702             $response = $interface->ProcessShipment(
703             { Request => {
704             RequestOption => ($self->address_validation)
705             ? 'validate'
706             : 'nonvalidate',
707             },
708             Shipment => {
709             Shipper => {
710             Name => $self->from_address->company,
711             AttentionName => $self->from_address->name,
712             ShipperNumber => $self->account,
713             Address => {
714             AddressLine => \@from_addresslines,
715             City => $self->from_address->city,
716             StateProvinceCode =>
717             $self->from_address->province_code,
718             PostalCode => $self->from_address->postal_code,
719             CountryCode => $self->from_address->country_code,
720             },
721             },
722             ShipTo => $shipto,
723             ShipmentRatingOptions => $rating_options,
724             Service => {
725             Code => (
726             $service_code_map{$self->from_address
727             ->country_code}->{$service_id}
728             || $service_id
729             ),
730             },
731             Package => \@pieces,
732             PaymentInformation => {ShipmentCharge => $payment_option,},
733             ShipmentServiceOptions => $shipment_options,
734             },
735             LabelSpecification => {
736             LabelImageFormat =>
737             {Code => $printer_type_map{$self->printer_type},},
738             LabelStockSize => {
739             Height => $self->label_height,
740             Width => 4,
741             },
742             },
743             },
744             { UsernameToken => {
745             Username => $self->username,
746             Password => $self->password,
747             },
748             ServiceAccessToken => {AccessLicenseNumber => $self->key,},
749             },
750             );
751              
752             #warn $response;
753              
754             $self->tracking_id($response->get_ShipmentResults()
755             ->get_ShipmentIdentificationNumber()->get_value);
756             use Data::Currency;
757             use Shipment::Service;
758             my $rate = $response->get_ShipmentResults->get_ShipmentCharges
759             ->get_TotalCharges->get_MonetaryValue;
760             my $currency = $response->get_ShipmentResults->get_ShipmentCharges
761             ->get_TotalCharges->get_CurrencyCode;
762             if ($self->negotiated_rates) {
763             if ($response->get_ShipmentResults->get_NegotiatedRateCharges) {
764             $rate =
765             $response->get_ShipmentResults->get_NegotiatedRateCharges
766             ->get_TotalCharge->get_MonetaryValue;
767             $currency =
768             $response->get_ShipmentResults->get_NegotiatedRateCharges
769             ->get_TotalCharge->get_CurrencyCode;
770             }
771             }
772             $self->service(
773             new Shipment::Service(
774             id => $service_id,
775             name => $self->services->{$service_id}->name,
776             cost => Data::Currency->new($rate, $currency),
777             )
778             );
779              
780             use Shipment::Label;
781             use MIME::Base64;
782             my $package_index = 0;
783             foreach (@{$response->get_ShipmentResults()->get_PackageResults()}) {
784             $self->get_package($package_index)
785             ->tracking_id($_->get_TrackingNumber()->get_value);
786              
787             ## For EPL labels, force Top Orientation by inserting the ZT command at the beginning of the file.
788             ## This is needed for cases when the printer defaults to the incorrect orientation.
789             my $data = "ZT\n"
790             if $printer_type_map{$self->printer_type} eq 'EPL';
791             $data .= decode_base64(
792             $_->get_ShippingLabel()->get_GraphicImage->get_value);
793              
794             $self->get_package($package_index)->label(
795             Shipment::Label->new(
796             { tracking_id => $_->get_TrackingNumber()->get_value,
797             content_type =>
798             $label_content_type_map{$self->printer_type},
799             data => $data,
800             file_name => $_->get_TrackingNumber()->get_value . '.'
801             . lc $printer_type_map{$self->printer_type},
802             },
803             )
804             );
805             $package_index++;
806             }
807              
808             if ($response->get_ShipmentResults()->get_ControlLogReceipt) {
809              
810             ## For EPL labels, force Top Orientation by inserting the ZT command at the beginning of the file.
811             ## This is needed for cases when the printer defaults to the incorrect orientation.
812             my $data = "ZT\n"
813             if $printer_type_map{$self->printer_type} eq 'EPL';
814             $data
815             .= decode_base64(
816             $response->get_ShipmentResults()->get_ControlLogReceipt()
817             ->get_GraphicImage->get_value);
818              
819             $self->control_log_receipt(
820             Shipment::Label->new(
821             { content_type =>
822             $label_content_type_map{$self->printer_type},
823             data => $data,
824             file_name => 'control_log_receipt.'
825             . lc $printer_type_map{$self->printer_type},
826             }
827             )
828             );
829             }
830              
831             $self->notice('');
832             if ($response->get_Response->get_Alert) {
833             foreach my $alert (@{$response->get_Response->get_Alert}) {
834             warn $alert->get_Description->get_value;
835             $self->add_notice($alert->get_Description->get_value . "\n");
836             }
837             }
838              
839             }
840             catch {
841             #warn $_;
842             try {
843             warn $response->get_detail()->get_Errors()->get_ErrorDetail()
844             ->get_PrimaryErrorCode()->get_Description;
845             $self->error(
846             $response->get_detail()->get_Errors()->get_ErrorDetail()
847             ->get_PrimaryErrorCode()->get_Description->get_value);
848             }
849             catch {
850             #warn $_;
851             warn $response->get_faultstring;
852             $self->error($response->get_faultstring->get_value);
853             };
854             };
855              
856             }
857              
858              
859             sub return {
860             my ($self, $service_id) = @_;
861              
862             try {
863             $service_id = $self->services->{$service_id}->id;
864             }
865             catch {
866             #warn $_;
867             warn "service ($service_id) not available";
868             $self->error("service ($service_id) not available");
869             $service_id = '';
870             };
871             return unless $service_id;
872              
873             my $package_options;
874             $package_options->{DeclaredValue}->{CurrencyCode} = $self->currency;
875              
876             my @pieces;
877             foreach (@{$self->packages}) {
878             $package_options->{DeclaredValue}->{MonetaryValue} =
879             $_->insured_value->value;
880             push @pieces,
881             { Description => 'n/a',
882             Packaging => {
883             Code => $package_type_map{$self->package_type}
884             || $self->package_type,
885             },
886             Dimensions => {
887             UnitOfMeasurement => {
888             Code => $units_type_map{$self->dim_unit}
889             || $self->dim_unit,
890             },
891             Length => $_->length,
892             Width => $_->width,
893             Height => $_->height,
894             },
895             PackageWeight => {
896             UnitOfMeasurement => {
897             Code => $units_type_map{$self->weight_unit}
898             || $self->weight_unit,
899             },
900             Weight => $_->weight,
901             },
902             PackageServiceOptions => $package_options,
903             };
904             }
905              
906             my $payment_option;
907             $payment_option->{Type} = '01';
908             $payment_option->{$bill_type_map{$self->bill_type}}->{AccountNumber} =
909             $self->bill_account;
910             $payment_option->{$bill_type_map{$self->bill_type}}->{Address}
911             ->{PostalCode} = $self->bill_address->postal_code
912             if $self->bill_type =~ /(recipient|third_party)/;
913             $payment_option->{$bill_type_map{$self->bill_type}}->{Address}
914             ->{CountryCode} = $self->bill_address->country_code
915             if $self->bill_type eq 'third_party';
916              
917             my @from_addresslines = (
918             $self->from_address->address1,
919             $self->from_address->address2,
920             $self->from_address->address3
921             );
922             my @to_addresslines = (
923             $self->to_address->address1,
924             $self->to_address->address2,
925             $self->to_address->address3
926             );
927              
928             use Shipment::UPS::WSDL::ShipInterfaces::ShipService::ShipPort;
929              
930             my $interface =
931             Shipment::UPS::WSDL::ShipInterfaces::ShipService::ShipPort->new(
932             {proxy_domain => $self->proxy_domain,});
933              
934             my $response;
935             try {
936             $response = $interface->ProcessShipment(
937             { Request => {
938             RequestOption => ($self->address_validation)
939             ? 'validate'
940             : 'nonvalidate',
941             },
942             Shipment => {
943             ReturnService => {Code => 9,},
944             Shipper => {
945             Name => $self->from_address->company,
946             AttentionName => $self->from_address->name,
947             ShipperNumber => $self->account,
948             Address => {
949             AddressLine => \@from_addresslines,
950             City => $self->from_address->city,
951             StateProvinceCode =>
952             $self->from_address->province_code,
953             PostalCode => $self->from_address->postal_code,
954             CountryCode => $self->from_address->country_code,
955             },
956             },
957             ShipFrom => {
958             Name => $self->to_address->company,
959             AttentionName => $self->to_address->name,
960             Address => {
961             AddressLine => \@to_addresslines,
962             City => $self->to_address->city,
963             StateProvinceCode =>
964             $self->to_address->province_code,
965             PostalCode => $self->to_address->postal_code,
966             CountryCode => $self->to_address->country_code,
967             },
968             EmailAddress => $self->from_address->email,
969             },
970             ShipTo => {
971             Name => $self->from_address->company,
972             AttentionName => $self->from_address->name,
973             Address => {
974             AddressLine => \@from_addresslines,
975             City => $self->from_address->city,
976             StateProvinceCode =>
977             $self->from_address->province_code,
978             PostalCode => $self->from_address->postal_code,
979             CountryCode => $self->from_address->country_code,
980             },
981             EmailAddress => $self->to_address->email,
982             },
983             Service => {Code => $service_id,},
984             Package => \@pieces,
985             PaymentInformation => {ShipmentCharge => $payment_option,},
986             },
987             LabelSpecification => {
988             LabelImageFormat =>
989             {Code => $printer_type_map{$self->printer_type},},
990             LabelStockSize => {
991             Height => $self->label_height,
992             Width => 4,
993             },
994             },
995             },
996             { UsernameToken => {
997             Username => $self->username,
998             Password => $self->password,
999             },
1000             ServiceAccessToken => {AccessLicenseNumber => $self->key,},
1001             },
1002             );
1003              
1004             #warn $response;
1005              
1006             $self->tracking_id($response->get_ShipmentResults()
1007             ->get_ShipmentIdentificationNumber()->get_value);
1008             use Data::Currency;
1009             use Shipment::Service;
1010             $self->service(
1011             new Shipment::Service(
1012             id => $service_id,
1013             name => $self->services->{$service_id}->name,
1014             cost => Data::Currency->new(
1015             $response->get_ShipmentResults()
1016             ->get_ShipmentCharges->get_TotalCharges()
1017             ->get_MonetaryValue,
1018             $response->get_ShipmentResults()->get_ShipmentCharges()
1019             ->get_TotalCharges()->get_CurrencyCode
1020             ),
1021             )
1022             );
1023              
1024             use Shipment::Label;
1025             use MIME::Base64;
1026             my $package_index = 0;
1027             foreach (@{$response->get_ShipmentResults()->get_PackageResults()}) {
1028              
1029             ## For EPL labels, force Top Orientation by inserting the ZT command at the beginning of the file.
1030             ## This is needed for cases when the printer defaults to the incorrect orientation.
1031             my $data = "ZT\n"
1032             if $printer_type_map{$self->printer_type} eq 'EPL';
1033             $data .= decode_base64(
1034             $_->get_ShippingLabel()->get_GraphicImage->get_value);
1035              
1036             $self->get_package($package_index)
1037             ->tracking_id($_->get_TrackingNumber()->get_value);
1038             $self->get_package($package_index)->label(
1039             Shipment::Label->new(
1040             { tracking_id => $_->get_TrackingNumber()->get_value,
1041             content_type =>
1042             $label_content_type_map{$self->printer_type},
1043             data => $data,
1044             file_name => $_->get_TrackingNumber()->get_value . '.'
1045             . lc $printer_type_map{$self->printer_type},
1046             },
1047             )
1048             );
1049             $package_index++;
1050             }
1051              
1052             if ($response->get_ShipmentResults()->get_ControlLogReceipt) {
1053              
1054             ## For EPL labels, force Top Orientation by inserting the ZT command at the beginning of the file.
1055             ## This is needed for cases when the printer defaults to the incorrect orientation.
1056             my $data = "ZT\n"
1057             if $printer_type_map{$self->printer_type} eq 'EPL';
1058             $data
1059             .= decode_base64(
1060             $response->get_ShipmentResults()->get_ControlLogReceipt()
1061             ->get_GraphicImage->get_value);
1062              
1063             $self->control_log_receipt(
1064             Shipment::Label->new(
1065             { content_type =>
1066             $label_content_type_map{$self->printer_type},
1067             data => $data,
1068             file_name => 'control_log_receipt.'
1069             . lc $printer_type_map{$self->printer_type},
1070             }
1071             )
1072             );
1073             }
1074              
1075             $self->notice('');
1076             if ($response->get_Response->get_Alert) {
1077             foreach my $alert (@{$response->get_Response->get_Alert}) {
1078             warn $alert->get_Description->get_value;
1079             $self->add_notice($alert->get_Description->get_value . "\n");
1080             }
1081             }
1082              
1083             }
1084             catch {
1085             #warn $_;
1086             try {
1087             warn $response->get_detail()->get_Errors()->get_ErrorDetail()
1088             ->get_PrimaryErrorCode()->get_Description;
1089             $self->error(
1090             $response->get_detail()->get_Errors()->get_ErrorDetail()
1091             ->get_PrimaryErrorCode()->get_Description->get_value);
1092             }
1093             catch {
1094             #warn $_;
1095             warn $response->get_faultstring;
1096             $self->error($response->get_faultstring->get_value);
1097             };
1098             };
1099              
1100             }
1101              
1102              
1103             sub cancel {
1104             my $self = shift;
1105              
1106             if (!$self->tracking_id) {
1107             $self->error('no tracking id provided');
1108             return;
1109             }
1110              
1111             my $void->{ShipmentIdentificationNumber} = $self->tracking_id;
1112              
1113             my @tracking_ids;
1114             foreach ($self->all_packages) {
1115             push @tracking_ids, $_->tracking_id;
1116             }
1117             if ($#tracking_ids) {
1118             $void->{TrackingNumber} = \@tracking_ids;
1119             }
1120              
1121             use Shipment::UPS::WSDL::ShipInterfaces::VoidService::VoidPort;
1122             my $interface =
1123             Shipment::UPS::WSDL::ShipInterfaces::VoidService::VoidPort->new(
1124             {proxy_domain => $self->proxy_domain,});
1125              
1126             my $response;
1127             my $success;
1128              
1129             try {
1130             $response = $interface->ProcessVoid(
1131             { Request => {RequestOption => '',},
1132             VoidShipment => $void,
1133             },
1134             { UsernameToken => {
1135             Username => $self->username,
1136             Password => $self->password,
1137             },
1138             ServiceAccessToken => {AccessLicenseNumber => $self->key,},
1139             },
1140             );
1141              
1142             #warn $response;
1143              
1144             $success =
1145             $response->get_SummaryResult->get_Status->get_Description->get_value;
1146              
1147             $self->notice('');
1148             if ($response->get_Response->get_Alert) {
1149             foreach my $alert (@{$response->get_Response->get_Alert}) {
1150             warn $alert->get_Description->get_value;
1151             $self->add_notice($alert->get_Description->get_value . "\n");
1152             }
1153             }
1154              
1155             }
1156             catch {
1157             #warn $_;
1158             try {
1159             warn $response->get_detail()->get_Errors()->get_ErrorDetail()
1160             ->get_PrimaryErrorCode()->get_Description;
1161             $self->error(
1162             $response->get_detail()->get_Errors()->get_ErrorDetail()
1163             ->get_PrimaryErrorCode()->get_Description->get_value);
1164             }
1165             catch {
1166             #warn $_;
1167             warn $response->get_faultstring;
1168             $self->error($response->get_faultstring->get_value);
1169             };
1170             };
1171              
1172             return $success;
1173              
1174             }
1175              
1176              
1177             1;
1178              
1179             __END__
1180              
1181             =pod
1182              
1183             =encoding UTF-8
1184              
1185             =head1 NAME
1186              
1187             Shipment::UPS
1188              
1189             =head1 VERSION
1190              
1191             version 2.00
1192              
1193             =head1 SYNOPSIS
1194              
1195             use Shipment::UPS;
1196             use Shipment::Address;
1197             use Shipment::Package;
1198              
1199             my $shipment = Shipment::UPS->new(
1200             from_address => Shipment::Address->new( ... ),
1201             to_address => Shipment::Address->new( ... ),
1202             packages => [ Shipment::Package->new( ... ), ],
1203             );
1204              
1205             foreach my $service ( $shipment->all_services ) {
1206             print $service->id . " (" . $service->cost . ")\n";
1207             }
1208              
1209             $shipment->rate( 'express' );
1210             print $shipment->service->cost . "\n";
1211              
1212             $shipment->ship( 'ground' );
1213             $shipment->get_package(0)->label->save;
1214              
1215             =head1 NAME
1216              
1217             Shipment::UPS - Interface to UPS Shipping Web Services
1218              
1219             =head1 ABOUT
1220              
1221             This class provides an interface to the UPS Online Tools. You must sign up for a developer access key in order to make use of this module.
1222              
1223             https://www.ups.com/upsdeveloperkit
1224              
1225             It is an extension of L<Shipment::Base>.
1226              
1227             It makes extensive use of SOAP::WSDL in order to create/decode xml requests and responses. The Shipment::UPS::WSDL interface was created primarily using the wsdl2perl.pl script from SOAP::WSDL.
1228              
1229             =head1 Class Attributes
1230              
1231             =head2 username, password, key
1232              
1233             Credentials required to access UPS Online Tools.
1234              
1235             =head2 proxy_domain
1236              
1237             This determines whether you will use the UPS Customer Integration Environment (for development) or the production (live) environment
1238             * wwwcie.ups.com (development)
1239             * onlinetools.ups.com (production)
1240              
1241             =head2 negotiated_rates
1242              
1243             Turn negotiated rates on or off.
1244              
1245             The Shipper Account/UserID used must be qualified to receive negotiated rates. You will most likely need to contact UPS to have set this up.
1246              
1247             If the Shipper Account/UserID is not qualified, the published rates will be used instead and a notice set.
1248              
1249             Default is off.
1250              
1251             =head2 residential_address
1252              
1253             Flag the ship to address as residential.
1254              
1255             Default is false.
1256              
1257             =head2 address_validation
1258              
1259             Turn address validation on or off. When on, ship will fail if the address does not pass UPS address validation
1260              
1261             Default is on.
1262              
1263             =head2 label_height
1264              
1265             The label height. Can be either 6" or 8". The label width is fixed at 4".
1266              
1267             =head2 control_log_receipt
1268              
1269             In certain cases (i.e. for shipments with declared value over $999), UPS will return a control log receipt which must be printed off along with the label.
1270              
1271             type: Shipment::Label
1272              
1273             =head2 carbon_neutral
1274              
1275             Set the Carbon Neutral Indicator - http://www.ups.com/content/us/en/resources/ship/carbonneutral/shipping.html
1276              
1277             type: Bool
1278              
1279             =head1 Type Maps
1280              
1281             =head2 service_map
1282              
1283             UPS returns service codes without descriptions. This is mapped here so that we can display 'UPS Ground' instead of '03'.
1284              
1285             =head2 Shipment::Base type maps
1286              
1287             Shipment::Base provides abstract types which need to be mapped to UPS codes (i.e. bill_type of "sender" maps to UPS "BillShipper")
1288              
1289             =head2 custom package types
1290              
1291             UPS provides package types in addition to the defaults in Shipment::Base
1292             * 25kg_box
1293             * 10kg_box
1294             * pallet
1295             * small_express_box
1296             * medium_express_box
1297             * large_express_box
1298              
1299             =head2 custom printer types
1300              
1301             UPS does not offer a pdf option for labels, so the default printer type is image (gif).
1302              
1303             UPS does offer additional thermal options:
1304             * ZPL
1305             * SPL
1306             * STARPL
1307              
1308             =head2 default currency
1309              
1310             The default currency is USD
1311              
1312             =head2 surepost
1313              
1314             Enable UPS SurePost
1315              
1316             =head1 Class Methods
1317              
1318             =head2 _build_services
1319              
1320             This calls ProcessRate from the Rating API with RequestOption => 'Shop'
1321              
1322             Each RatedShipment that is returned is added to services
1323              
1324             The following service mapping is used:
1325             * ground => 03 (UPS Ground) or 11 (UPS Standard)
1326             * express => 02 (UPS Second Day Air)
1327             * priority => 01 (UPS Next Day Air)
1328              
1329             This method ignores what is in $self->packages and uses a single package weighing 1 pound for rating. The idea is to list what services are available, but for accurate rate comparisons, the rate method should be used.
1330              
1331             =head2 rate
1332              
1333             This calls ProcessRate from the Rating API with RequestOption => 'Rate'
1334              
1335             =head2 ship
1336              
1337             This method calls ProcessShipment from the Shipping API
1338              
1339             =head2 return
1340              
1341             This method calls ProcessShipment from the Shipping API with
1342             ReturnService => Code => 9
1343             which provides the return label to be printed off.
1344              
1345             This method has only been implemented for the purpose of obtaining certification with UPS. It has not been fully tested and does not offer some core options (such as the ability to email the return label).
1346              
1347             It assumes that you are first creating an outgoing shipment and creating the return shipment at the same time. Because of this, it uses the "to_address" as the origin and the "from_address" as the destination.
1348              
1349             =head2 cancel
1350              
1351             This method calls ProcessVoid from the Shipping API
1352              
1353             It uses $self->tracking_id for the shipment identification number in order
1354             to void a single package shipment.
1355              
1356             It will use all package tracking id's
1357             to void one or more packages within a multi-package shipment.
1358              
1359             returns "Voided" if successful
1360              
1361             =head1 AUTHOR
1362              
1363             Andrew Baerg @ <andrew at pullingshots dot ca>
1364              
1365             http://pullingshots.ca/
1366              
1367             =head1 BUGS
1368              
1369             Please contact me directly.
1370              
1371             =head1 COPYRIGHT
1372              
1373             Copyright (C) 2010 Andrew J Baerg, All Rights Reserved
1374              
1375             =head1 NO WARRANTY
1376              
1377             Absolutely, positively NO WARRANTY, neither express or implied, is
1378             offered with this software. You use this software at your own risk. In
1379             case of loss, no person or entity owes you anything whatsoever. You
1380             have been warned.
1381              
1382             =head1 LICENSE
1383              
1384             This program is free software; you can redistribute it and/or modify it
1385             under the same terms as Perl itself.
1386              
1387             =head1 AUTHOR
1388              
1389             Andrew Baerg <baergaj@cpan.org>
1390              
1391             =head1 COPYRIGHT AND LICENSE
1392              
1393             This software is copyright (c) 2013 by Andrew Baerg.
1394              
1395             This is free software; you can redistribute it and/or modify it under
1396             the same terms as the Perl 5 programming language system itself.
1397              
1398             =cut