File Coverage

blib/lib/Shipment/UPS.pm
Criterion Covered Total %
statement 10 12 83.3
branch n/a
condition n/a
subroutine 4 4 100.0
pod n/a
total 14 16 87.5


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