File Coverage

blib/lib/Net/Async/Webservice/UPS.pm
Criterion Covered Total %
statement 4 6 66.6
branch n/a
condition n/a
subroutine 2 2 100.0
pod n/a
total 6 8 75.0


line stmt bran cond sub pod time code
1             package Net::Async::Webservice::UPS;
2             $Net::Async::Webservice::UPS::VERSION = '1.1.4';
3             {
4             $Net::Async::Webservice::UPS::DIST = 'Net-Async-Webservice-UPS';
5             }
6 5     5   162155 use Moo;
  5         25271  
  5         29  
7 5     5   10060 use XML::Simple;
  0            
  0            
8             use Types::Standard 1.000003 qw(Str Int Bool Object Dict Optional ArrayRef HashRef Undef);
9             use Types::URI qw(Uri);
10             use Type::Params qw(compile);
11             use Error::TypeTiny;
12             use Net::Async::Webservice::UPS::Types qw(:types to_Service);
13             use Net::Async::Webservice::UPS::Exception;
14             use Try::Tiny;
15             use List::AllUtils 'pairwise';
16             use HTTP::Request;
17             use Encode;
18             use namespace::autoclean;
19             use Net::Async::Webservice::UPS::Rate;
20             use Net::Async::Webservice::UPS::Address;
21             use Net::Async::Webservice::UPS::Service;
22             use Net::Async::Webservice::UPS::Response::Rate;
23             use Net::Async::Webservice::UPS::Response::Address;
24             use Net::Async::Webservice::UPS::Response::ShipmentConfirm;
25             use Net::Async::Webservice::UPS::Response::ShipmentAccept;
26             use Net::Async::Webservice::UPS::Response::QV;
27             use MIME::Base64;
28             use Future;
29             use 5.010;
30              
31             # ABSTRACT: UPS API client, non-blocking
32              
33              
34             my %code_for_pickup_type = (
35             DAILY_PICKUP => '01',
36             DAILY => '01',
37             CUSTOMER_COUNTER => '03',
38             ONE_TIME_PICKUP => '06',
39             ONE_TIME => '06',
40             ON_CALL_AIR => '07',
41             SUGGESTED_RETAIL => '11',
42             SUGGESTED_RETAIL_RATES => '11',
43             LETTER_CENTER => '19',
44             AIR_SERVICE_CENTER => '20'
45             );
46              
47             my %code_for_customer_classification = (
48             WHOLESALE => '01',
49             OCCASIONAL => '03',
50             RETAIL => '04'
51             );
52              
53             my %base_urls = (
54             live => 'https://onlinetools.ups.com/ups.app/xml',
55             test => 'https://wwwcie.ups.com/ups.app/xml',
56             );
57              
58              
59             has live_mode => (
60             is => 'rw',
61             isa => Bool,
62             trigger => 1,
63             default => sub { 0 },
64             );
65              
66              
67             has base_url => (
68             is => 'lazy',
69             isa => Uri,
70             clearer => '_clear_base_url',
71             coerce => Uri->coercion,
72             );
73              
74             sub _trigger_live_mode {
75             my ($self) = @_;
76              
77             $self->_clear_base_url;
78             }
79             sub _build_base_url {
80             my ($self) = @_;
81              
82             return $base_urls{$self->live_mode ? 'live' : 'test'};
83             }
84              
85              
86             has user_id => (
87             is => 'ro',
88             isa => Str,
89             required => 1,
90             );
91             has password => (
92             is => 'ro',
93             isa => Str,
94             required => 1,
95             );
96             has access_key => (
97             is => 'ro',
98             isa => Str,
99             required => 1,
100             );
101              
102              
103             has account_number => (
104             is => 'ro',
105             isa => Str,
106             );
107              
108              
109             has customer_classification => (
110             is => 'rw',
111             isa => CustomerClassification,
112             );
113              
114              
115             has pickup_type => (
116             is => 'rw',
117             isa => PickupType,
118             default => sub { 'ONE_TIME' },
119             );
120              
121              
122             has cache => (
123             is => 'ro',
124             isa => Cache|Undef,
125             );
126              
127              
128             sub does_caching {
129             my ($self) = @_;
130             return defined $self->cache;
131             }
132              
133              
134             sub _build_ssl_options {
135             eval "require IO::Socket::SSL; require IO::Socket::SSL::Utils; require Mozilla::CA;"
136             or return {};
137              
138             my $cert = IO::Socket::SSL::Utils::PEM_string2cert(<<'PEM');
139             -----BEGIN CERTIFICATE-----
140             MIICPDCCAaUCEHC65B0Q2Sk0tjjKewPMur8wDQYJKoZIhvcNAQECBQAwXzELMAkGA1UEBhMCVVMx
141             FzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFzcyAzIFB1YmxpYyBQcmltYXJ5
142             IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTk2MDEyOTAwMDAwMFoXDTI4MDgwMTIzNTk1OVow
143             XzELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFzcyAz
144             IFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIGfMA0GCSqGSIb3DQEBAQUA
145             A4GNADCBiQKBgQDJXFme8huKARS0EN8EQNvjV69qRUCPhAwL0TPZ2RHP7gJYHyX3KqhEBarsAx94
146             f56TuZoAqiN91qyFomNFx3InzPRMxnVx0jnvT0Lwdd8KkMaOIG+YD/isI19wKTakyYbnsZogy1Ol
147             hec9vn2a/iRFM9x2Fe0PonFkTGUugWhFpwIDAQABMA0GCSqGSIb3DQEBAgUAA4GBALtMEivPLCYA
148             TxQT3ab7/AoRhIzzKBxnki98tsX63/Dolbwdj2wsqFHMc9ikwFPwTtYmwHYBV4GSXiHx0bH/59Ah
149             WM1pF+NEHJwZRDmJXNycAA9WjQKZ7aKQRUzkuxCkPfAyAw7xzvjoyVGM5mKf5p/AfbdynMk2Omuf
150             Tqj/ZA1k
151             -----END CERTIFICATE-----
152             PEM
153             return {
154             SSL_verify_mode => IO::Socket::SSL::SSL_VERIFY_PEER(),
155             SSL_ca => [ $cert ],
156             SSL_ca_file => Mozilla::CA::SSL_ca_file(),
157             };
158             }
159              
160              
161             with 'Net::Async::Webservice::Common::WithUserAgent';
162             with 'Net::Async::Webservice::Common::WithConfigFile';
163              
164             around BUILDARGS => sub {
165             my ($orig,$class,@args) = @_;
166              
167             my $ret = $class->$orig(@args);
168              
169             if ($ret->{cache_life}) {
170             require CHI;
171             if (not $ret->{cache_root}) {
172             require File::Spec;
173             $ret->{cache_root} =
174             File::Spec->catdir(File::Spec->tmpdir,'naws_ups'),
175             }
176             $ret->{cache} = CHI->new(
177             driver => 'File',
178             root_dir => $ret->{cache_root},
179             depth => 5,
180             expires_in => $ret->{cache_life} . ' min',
181             );
182             }
183              
184             return $ret;
185             };
186              
187              
188             sub transaction_reference {
189             my ($self,$args) = @_;
190             no warnings 'once';
191             return {
192             CustomerContext => ($args->{customer_context} // "Net::Async::Webservice::UPS"),
193             XpciVersion => "".($Net::Async::Webservice::UPS::VERSION||0),
194             };
195             }
196              
197              
198             sub access_as_xml {
199             my $self = shift;
200             return XMLout({
201             AccessRequest => {
202             AccessLicenseNumber => $self->access_key,
203             Password => $self->password,
204             UserId => $self->user_id,
205             }
206             }, NoAttr=>1, KeepRoot=>1, XMLDecl=>1);
207             }
208              
209              
210             sub request_rate {
211             state $argcheck = compile(Object, Dict[
212             from => Address|Shipper,
213             to => Address,
214             packages => PackageList,
215             limit_to => Optional[ArrayRef[Str]],
216             exclude => Optional[ArrayRef[Str]],
217             mode => Optional[RequestMode],
218             service => Optional[Service],
219             customer_context => Optional[Str],
220             ]);
221             my ($self,$args) = $argcheck->(@_);
222             $args->{mode} ||= 'rate';
223             $args->{service} ||= to_Service('GROUND');
224             if ($args->{from}->isa('Net::Async::Webservice::UPS::Address')) {
225             $args->{from} ||= $self->_shipper_from_address($args->{from});
226             }
227              
228             if ( $args->{exclude} && $args->{limit_to} ) {
229             Error::TypeTiny::croak("You cannot use both 'limit_to' and 'exclude' at the same time");
230             }
231              
232             my $packages = $args->{packages};
233              
234             unless (scalar(@$packages)) {
235             Error::TypeTiny::croak("request_rate() was given an empty list of packages");
236             }
237              
238             my $cache_key;
239             if ($self->does_caching) {
240             $cache_key = $self->generate_cache_key(
241             'rate',
242             [ $args->{from},$args->{to},@$packages, ],
243             {
244             mode => $args->{mode},
245             service => $args->{service}->code,
246             pickup_type => $self->pickup_type,
247             customer_classification => $self->customer_classification,
248             },
249             );
250             if (my $cached_services = $self->cache->get($cache_key)) {
251             return Future->wrap($cached_services);
252             }
253             }
254              
255             my %request = (
256             RatingServiceSelectionRequest => {
257             Request => {
258             RequestAction => 'Rate',
259             RequestOption => $args->{mode},
260             TransactionReference => $self->transaction_reference($args),
261             },
262             PickupType => {
263             Code => $code_for_pickup_type{$self->pickup_type},
264             },
265             Shipment => {
266             Service => { Code => $args->{service}->code },
267             Package => [map { $_->as_hash() } @$packages],
268             Shipper => $args->{from}->as_hash('AV'),
269             ShipTo => $args->{to}->as_hash('AV'),
270             },
271             ( $self->customer_classification ? (
272             CustomerClassification => { Code => $code_for_customer_classification{$self->customer_classification} }
273             ) : () ),
274             }
275             );
276              
277             # default to "all allowed"
278             my %ok_labels = map { $_ => 1 } @{ServiceLabel->values};
279             if ($args->{limit_to}) {
280             # deny all, allow requested
281             %ok_labels = map { $_ => 0 } @{ServiceLabel->values};
282             $ok_labels{$_} = 1 for @{$args->{limit_to}};
283             }
284             elsif ($args->{exclude}) {
285             # deny requested
286             $ok_labels{$_} = 0 for @{$args->{exclude}};
287             }
288              
289             $self->xml_request({
290             data => \%request,
291             url_suffix => '/Rate',
292             XMLin => {
293             ForceArray => [ 'RatedPackage', 'RatedShipment' ],
294             },
295             })->transform(
296             done => sub {
297             my ($response) = @_;
298              
299             my @services;
300             for my $rated_shipment (@{$response->{RatedShipment}}) {
301             my $code = $rated_shipment->{Service}{Code};
302             my $label = Net::Async::Webservice::UPS::Service::label_for_code($code);
303             next if not $ok_labels{$label};
304              
305             push @services, my $service = Net::Async::Webservice::UPS::Service->new({
306             code => $code,
307             label => $label,
308             total_charges => $rated_shipment->{TotalCharges}{MonetaryValue},
309             # TODO check this logic
310             ( ref($rated_shipment->{GuaranteedDaysToDelivery})
311             ? ()
312             : ( guaranteed_days => $rated_shipment->{GuaranteedDaysToDelivery} ) ),
313             rated_packages => $packages,
314             # TODO check this pairwise
315             rates => [ pairwise {
316             Net::Async::Webservice::UPS::Rate->new({
317             %$a,
318             rated_package => $b,
319             from => $args->{from},
320             to => $args->{to},
321             });
322             } @{$rated_shipment->{RatedPackage}},@$packages ],
323             });
324              
325             # fixup service-rate-service refs
326             $_->_set_service($service) for @{$service->rates};
327             }
328             @services = sort { $a->total_charges <=> $b->total_charges } @services;
329              
330             my $ret = Net::Async::Webservice::UPS::Response::Rate->new({
331             %$response,
332             services => \@services,
333             });
334              
335             $self->cache->set($cache_key,$ret) if $self->does_caching;
336              
337             return $ret;
338             },
339             );
340             }
341              
342              
343             sub validate_address {
344             state $argcheck = compile(
345             Object,
346             Address, Optional[Tolerance],
347             );
348             my ($self,$address,$tolerance) = $argcheck->(@_);
349              
350             $tolerance //= 0.05;
351              
352             my %data = (
353             AddressValidationRequest => {
354             Request => {
355             RequestAction => "AV",
356             TransactionReference => $self->transaction_reference(),
357             },
358             %{$address->as_hash('AV')},
359             },
360             );
361              
362             my $cache_key;
363             if ($self->does_caching) {
364             $cache_key = $self->generate_cache_key(
365             'AV',
366             [ $address ],
367             { tolerance => $tolerance },
368             );
369             if (my $cached_services = $self->cache->get($cache_key)) {
370             return Future->wrap($cached_services);
371             }
372             }
373              
374             $self->xml_request({
375             data => \%data,
376             url_suffix => '/AV',
377             XMLin => {
378             ForceArray => [ 'AddressValidationResult' ],
379             },
380             })->transform(
381             done => sub {
382             my ($response) = @_;
383              
384             my @addresses;
385             for my $address (@{$response->{AddressValidationResult}}) {
386             next if $address->{Quality} < (1 - $tolerance);
387             for my $possible_postal_code ($address->{PostalCodeLowEnd} .. $address->{PostalCodeHighEnd}) {
388             $address->{Address}{PostalCode} = $possible_postal_code;
389             push @addresses, Net::Async::Webservice::UPS::Address->new($address);
390             }
391             }
392              
393              
394             my $ret = Net::Async::Webservice::UPS::Response::Address->new({
395             %$response,
396             addresses => \@addresses,
397             });
398              
399             $self->cache->set($cache_key,$ret) if $self->does_caching;
400             return $ret;
401             },
402             );
403             }
404              
405              
406             sub validate_street_address {
407             state $argcheck = compile(
408             Object,
409             Address,
410             );
411             my ($self,$address) = $argcheck->(@_);
412              
413             my %data = (
414             AddressValidationRequest => {
415             Request => {
416             RequestAction => 'XAV',
417             RequestOption => '3',
418             TransactionReference => $self->transaction_reference(),
419             },
420             %{$address->as_hash('XAV')},
421             },
422             );
423              
424             my $cache_key;
425             if ($self->does_caching) {
426             $cache_key = $self->generate_cache_key(
427             'XAV',
428             [ $address ],
429             );
430             if (my $cached_services = $self->cache->get($cache_key)) {
431             return Future->wrap($cached_services);
432             }
433             }
434              
435             $self->xml_request({
436             data => \%data,
437             url_suffix => '/XAV',
438             XMLin => {
439             ForceArray => [ 'AddressValidationResponse','AddressLine', 'AddressKeyFormat' ],
440             },
441             })->then(
442             sub {
443             my ($response) = @_;
444              
445              
446             if ($response->{NoCandidatesIndicator}) {
447             return Future->new->fail(Net::Async::Webservice::UPS::Exception::UPSError->new({
448             error => {
449             ErrorDescription => 'The Address Matching System is not able to match an address from any other one in the database',
450             ErrorCode => 'NoCandidates',
451             },
452             }),'ups');
453             }
454             if ($response->{AmbiguousAddressIndicator}) {
455             return Future->new->fail(Net::Async::Webservice::UPS::Exception::UPSError->new({
456             error => {
457             ErrorDescription => 'The Address Matching System is not able to explicitly differentiate an address from any other one in the database',
458             ErrorCode => 'AmbiguousAddress',
459             },
460             }),'ups');
461             }
462              
463             my $quality = 0;
464             if ($response->{ValidAddressIndicator}) {
465             $quality = 1;
466             }
467              
468             my @addresses;
469             for my $ak (@{$response->{AddressKeyFormat}}) {
470             push @addresses, Net::Async::Webservice::UPS::Address->new({
471             AddressKeyFormat => $ak,
472             Quality => $quality,
473             });
474             }
475              
476             my $ret = Net::Async::Webservice::UPS::Response::Address->new({
477             %$response,
478             addresses => \@addresses,
479             });
480              
481             $self->cache->set($cache_key,$ret) if $self->does_caching;
482             return Future->wrap($ret);
483             },
484             );
485             }
486              
487              
488             sub ship_confirm {
489             state $argcheck = compile(Object, Dict[
490             from => Contact,
491             to => Contact,
492             shipper => Optional[Shipper],
493             service => Optional[Service],
494             description => Str,
495             payment => Payment,
496             label => Optional[Label],
497             packages => PackageList,
498             return_service => Optional[ReturnService],
499             customer_context => Optional[Str],
500             delivery_confirmation => Optional[Int],
501             ]);
502             my ($self,$args) = $argcheck->(@_);
503              
504             $args->{service} //= to_Service('GROUND');
505             $args->{shipper} //= $self->_shipper_from_contact($args->{from});
506              
507             my $packages = $args->{packages};
508              
509             unless (scalar(@$packages)) {
510             Error::TypeTiny::croak("ship_confirm() was given an empty list of packages");
511             }
512             my $package_data = [map { $_->as_hash() } @$packages];
513             if ($args->{delivery_confirmation}) {
514             for my $p (@$package_data) {
515             $p->{PackageServiceOptions}{DeliveryConfirmation}{DCISType} =
516             $args->{delivery_confirmation};
517             }
518             }
519              
520             my %data = (
521             ShipmentConfirmRequest => {
522             Request => {
523             TransactionReference => $self->transaction_reference($args),
524             RequestAction => 'ShipConfirm',
525             RequestOption => 'validate', # this makes the request
526             # fail if there are
527             # address problems
528             },
529             Shipment => {
530             Service => { Code => $args->{service}->code },
531             Description => $args->{description},
532             ( $args->{return_service} ? (
533             ReturnService => { Code => $args->{return_service}->code }
534             ) : () ),
535             Shipper => $args->{shipper}->as_hash,
536             ( $args->{from} ? ( ShipFrom => $args->{from}->as_hash ) : () ),
537             ShipTo => $args->{to}->as_hash,
538             PaymentInformation => $args->{payment}->as_hash,
539             Package => $package_data,
540             },
541             ( $args->{label} ? ( LabelSpecification => $args->{label}->as_hash ) : () ),
542             }
543             );
544              
545             $self->xml_request({
546             data => \%data,
547             url_suffix => '/ShipConfirm',
548             })->transform(
549             done => sub {
550             my ($response) = @_;
551              
552             return Net::Async::Webservice::UPS::Response::ShipmentConfirm->new({
553             %$response,
554             packages => $packages,
555             });
556             },
557             );
558             }
559              
560              
561             sub ship_accept {
562             state $argcheck = compile( Object, Dict[
563             confirm => ShipmentConfirm,
564             customer_context => Optional[Str],
565             ]);
566             my ($self,$args) = $argcheck->(@_);
567              
568             my %data = (
569             ShipmentAcceptRequest => {
570             Request => {
571             TransactionReference => $self->transaction_reference($args),
572             RequestAction => 'ShipAccept',
573             },
574             ShipmentDigest => $args->{confirm}->shipment_digest,
575             },
576             );
577              
578             my $packages = $args->{confirm}->packages;
579              
580             $self->xml_request({
581             data => \%data,
582             url_suffix => '/ShipAccept',
583             XMLin => {
584             ForceArray => [ 'PackageResults' ],
585             },
586             })->transform(
587             done => sub {
588             my ($response) = @_;
589              
590             return Net::Async::Webservice::UPS::Response::ShipmentAccept->new({
591             packages => $packages,
592             %$response,
593             });
594             },
595             );
596             }
597              
598              
599             sub qv_events {
600             state $argcheck = compile( Object, Dict[
601             subscriptions => Optional[ArrayRef[QVSubscription]],
602             bookmark => Optional[Str],
603             customer_context => Optional[Str],
604             ]);
605              
606             my ($self,$args) = $argcheck->(@_);
607              
608             my %data = (
609             QuantumViewRequest => {
610             Request => {
611             TransactionReference => $self->transaction_reference($args),
612             RequestAction => 'QVEvents',
613             },
614             ( $args->{subscriptions} ? ( SubscriptionRequest => [
615             map { $_->as_hash } @{$args->{subscriptions}}
616             ] ) : () ),
617             ($args->{bookmark} ? (Bookmark => $args->{bookmark}) : () ),
618             },
619             );
620              
621             $self->xml_request({
622             data => \%data,
623             url_suffix => '/QVEvents',
624             })->transform(
625             done => sub {
626             my ($response) = @_;
627             return Net::Async::Webservice::UPS::Response::QV->new($response);
628             }
629             );
630             }
631              
632              
633             sub xml_request {
634             state $argcheck = compile(
635             Object,
636             Dict[
637             data => HashRef,
638             url_suffix => Str,
639             XMLout => Optional[HashRef],
640             XMLin => Optional[HashRef],
641             ],
642             );
643             my ($self, $args) = $argcheck->(@_);
644              
645             # default XML::Simple args
646             my $xmlargs = {
647             NoAttr => 1,
648             KeyAttr => [],
649             };
650              
651             my $request =
652             $self->access_as_xml .
653             XMLout(
654             $args->{data},
655             %{ $xmlargs },
656             XMLDecl => 1,
657             KeepRoot => 1,
658             %{ $args->{XMLout}||{} },
659             );
660              
661             return $self->post( $self->base_url . $args->{url_suffix}, $request )->then(
662             sub {
663             my ($response_string) = @_;
664              
665             my $response = XMLin(
666             $response_string,
667             %{ $xmlargs },
668             %{ $args->{XMLin} },
669             );
670              
671             if ($response->{Response}{ResponseStatusCode}==0) {
672             return Future->new->fail(
673             Net::Async::Webservice::UPS::Exception::UPSError->new({
674             error => $response->{Response}{Error}
675             }),
676             'ups',
677             );
678             }
679             return Future->wrap($response);
680             },
681             );
682             }
683              
684              
685             with 'Net::Async::Webservice::Common::WithRequestWrapper';
686              
687              
688             sub generate_cache_key {
689             state $argcheck = compile(Object, Str, ArrayRef[Cacheable],Optional[HashRef]);
690             my ($self,$kind,$things,$args) = $argcheck->(@_);
691              
692             return join ':',
693             $kind,
694             ( map { $_->cache_id } @$things ),
695             ( map {
696             sprintf '%s:%s',
697             $_,
698             ( defined($args->{$_}) ? '"'.$args->{$_}.'"' : 'undef' )
699             } sort keys %{$args || {}}
700             );
701             }
702              
703             sub _shipper_from_address {
704             my ($self,$addr) = @_;
705              
706             require Net::Async::Webservice::UPS::Shipper;
707              
708             return Net::Async::Webservice::UPS::Shipper->new({
709             address => $addr,
710             ( $self->account_number ? ( account_number => $self->account_number ) : () ),
711             });
712             }
713              
714             sub _shipper_from_contact {
715             my ($self,$contact) = @_;
716              
717             return $contact if $contact->isa('Net::Async::Webservice::UPS::Shipper');
718              
719             require Net::Async::Webservice::UPS::Shipper;
720              
721             return Net::Async::Webservice::UPS::Shipper->new({
722             %$contact, # ugly!
723             ( $self->account_number ? ( account_number => $self->account_number ) : () ),
724             });
725             }
726              
727             1;
728              
729             __END__
730              
731             =pod
732              
733             =encoding UTF-8
734              
735             =head1 NAME
736              
737             Net::Async::Webservice::UPS - UPS API client, non-blocking
738              
739             =head1 VERSION
740              
741             version 1.1.4
742              
743             =head1 SYNOPSIS
744              
745             use IO::Async::Loop;
746             use Net::Async::Webservice::UPS;
747              
748             my $loop = IO::Async::Loop->new;
749              
750             my $ups = Net::Async::Webservice::UPS->new({
751             config_file => $ENV{HOME}.'/.naws_ups.conf',
752             loop => $loop,
753             });
754              
755             $ups->validate_address($postcode)->then(sub {
756             my ($response) = @_;
757             say $_->postal_code for @{$response->addresses};
758             return Future->wrap();
759             });
760              
761             $loop->run;
762              
763             Alternatively:
764              
765             use Net::Async::Webservice::UPS;
766              
767             my $ups = Net::Async::Webservice::UPS->new({
768             config_file => $ENV{HOME}.'/.naws_ups.conf',
769             user_agent => LWP::UserAgent->new,
770             });
771              
772             my $response = $ups->validate_address($postcode)->get;
773              
774             say $_->postal_code for @{$response->addresses};
775              
776             =head1 DESCRIPTION
777              
778             This class implements some of the methods of the UPS API, using
779             L<Net::Async::HTTP> as a user agent I<by default> (you can still pass
780             something like L<LWP::UserAgent> and it will work). All methods that
781             perform API calls return L<Future>s (if using a synchronous user
782             agent, all the Futures will be returned already completed).
783              
784             B<NOTE>: I've kept many names and codes from the original L<Net::UPS>,
785             so the API of this distribution may look a bit strange. It should make
786             it simpler to migrate from L<Net::UPS>, though.
787              
788             =head1 ATTRIBUTES
789              
790             =head2 C<live_mode>
791              
792             Boolean, defaults to false. When set to true, the live API endpoint
793             will be used, otherwise the test one will. Flipping this attribute
794             will reset L</base_url>, so you generally don't want to touch this if
795             you're using some custom API endpoint.
796              
797             =head2 C<base_url>
798              
799             A L<URI> object, coercible from a string. The base URL to use to send
800             API requests to (actual requests will be C<POST>ed to an actual URL
801             built from this by appending the appropriate service path). Defaults
802             to the standard UPS endpoints:
803              
804             =over 4
805              
806             =item *
807              
808             C<https://onlinetools.ups.com/ups.app/xml> for live
809              
810             =item *
811              
812             C<https://wwwcie.ups.com/ups.app/xml> for testing
813              
814             =back
815              
816             See also L</live_mode>.
817              
818             =head2 C<user_id>
819              
820             =head2 C<password>
821              
822             =head2 C<access_key>
823              
824             Strings, required. Authentication credentials.
825              
826             =head2 C<account_number>
827              
828             String. Used in some requests as "shipper number".
829              
830             =head2 C<customer_classification>
831              
832             String, usually one of C<WHOLESALE>, C<OCCASIONAL>, C<RETAIL>. Used
833             when requesting rates.
834              
835             =head2 C<pickup_type>
836              
837             String, defaults to C<ONE_TIME>. Used when requesting rates.
838              
839             =head2 C<cache>
840              
841             Responses are cached if this is set. You can pass your own cache
842             object (that implements the C<get> and C<set> methods like L<CHI>
843             does), or use the C<cache_life> and C<cache_root> constructor
844             parameters to get a L<CHI> instance based on L<CHI::Driver::File>.
845              
846             =head2 C<user_agent>
847              
848             A user agent object, looking either like L<Net::Async::HTTP> (has
849             C<do_request> and C<POST>) or like L<LWP::UserAgent> (has C<request>
850             and C<post>). You can pass the C<loop> constructor parameter to get a
851             default L<Net::Async::HTTP> instance.
852              
853             =head2 C<ssl_options>
854              
855             Optional hashref, its contents will be passed to C<user_agent>'s
856             C<do_request> method.
857              
858             If L<IO::Socket::SSL> and L<Mozilla::CA> are installed, the default
859             value sets full TLS validation, and makes sure that the Verisign
860             certificate currently (as of 2015-02-03) used by the UPS servers is
861             recognised (see L<UPS SSL/TLS notes>).
862              
863             =head1 METHODS
864              
865             =head2 C<does_caching>
866              
867             Returns a true value if caching is enabled.
868              
869             =head2 C<new>
870              
871             Async:
872              
873             my $ups = Net::Async::Webservice::UPS->new({
874             loop => $loop,
875             config_file => $file_name,
876             cache_life => 5,
877             });
878              
879             Sync:
880              
881             my $ups = Net::Async::Webservice::UPS->new({
882             user_agent => LWP::UserAgent->new,
883             config_file => $file_name,
884             cache_life => 5,
885             });
886              
887             In addition to passing all the various attributes values, you can use
888             a few shortcuts.
889              
890             =over 4
891              
892             =item C<loop>
893              
894             a L<IO::Async::Loop>; a locally-constructed L<Net::Async::HTTP> will be registered to it and set as L</user_agent>
895              
896             =item C<config_file>
897              
898             a path name; will be parsed with L<Config::Any>, and the values used as if they had been passed in to the constructor
899              
900             =item C<cache_life>
901              
902             lifetime, in I<minutes>, of cache entries; a L</cache> will be built automatically if this is set (using L<CHI> with the C<File> driver)
903              
904             =item C<cache_root>
905              
906             where to store the cache files for the default cache object, defaults to C<naws_ups> under your system's temporary directory
907              
908             =back
909              
910             A few more examples:
911              
912             =over 4
913              
914             =item *
915              
916             no config file, no cache, async:
917              
918             ->new({
919             user_id=>$user,password=>$pw,access_key=>$ak,
920             loop=>$loop,
921             }),
922              
923             =item *
924              
925             no config file, no cache, custom user agent (sync or async):
926              
927             ->new({
928             user_id=>$user,password=>$pw,access_key=>$ak,
929             user_agent=>$ua,
930             }),
931              
932             it's your job to register the custom user agent to the event loop, if
933             you're using an async agent
934              
935             =item *
936              
937             config file, async, custom cache:
938              
939             ->new({
940             loop=>$loop,
941             cache=>CHI->new(...),
942             }),
943              
944             =back
945              
946             =head2 C<transaction_reference>
947              
948             Constant data used to fill something in requests. I don't know what
949             it's for, I just copied it from L<Net::UPS>.
950              
951             =head2 C<access_as_xml>
952              
953             Returns a XML document with the credentials.
954              
955             =head2 C<request_rate>
956              
957             $ups->request_rate({
958             from => $address_a,
959             to => $address_b,
960             packages => [ $package_1, $package_2 ],
961             }) ==> (Net::Async::Webservice::UPS::Response::Rate)
962              
963             C<from> and C<to> are instances of
964             L<Net::Async::Webservice::UPS::Address>, or postcode strings that will
965             be coerced to addresses.
966              
967             C<packages> is an arrayref of L<Net::Async::Webservice::UPS::Package>
968             (or a single package, will be coerced to a 1-element array ref).
969              
970             I<NOTE>: the C<id> field of the packages I<used to be modified>. It no
971             longer is.
972              
973             Optional parameters:
974              
975             =over 4
976              
977             =item C<limit_to>
978              
979             only accept some services (see L<Net::Async::Webservice::UPS::Types/ServiceLabel>)
980              
981             =item C<exclude>
982              
983             exclude some services (see L<Net::Async::Webservice::UPS::Types/ServiceLabel>)
984              
985             =item C<mode>
986              
987             defaults to C<rate>, could be C<shop>
988              
989             =item C<service>
990              
991             defaults to C<GROUND>, see L<Net::Async::Webservice::UPS::Service>
992              
993             =item C<customer_context>
994              
995             optional string for reference purposes
996              
997             =back
998              
999             The L<Future> returned will yield an instance of
1000             L<Net::Async::Webservice::UPS::Response::Rate>, or fail with an
1001             exception.
1002              
1003             Identical requests can be cached.
1004              
1005             =head2 C<validate_address>
1006              
1007             $ups->validate_address($address)
1008             ==> (Net::Async::Webservice::UPS::Response::Address)
1009              
1010             $ups->validate_address($address,$tolerance)
1011             ==> (Net::Async::Webservice::UPS::Response::Address)
1012              
1013             C<$address> is an instance of L<Net::Async::Webservice::UPS::Address>,
1014             or a postcode string that will be coerced to an address.
1015              
1016             Optional parameter: a tolerance (float, between 0 and 1). Returned
1017             addresses with quality below 1 minus tolerance will be filtered out.
1018              
1019             The L<Future> returned will yield an instance of
1020             L<Net::Async::Webservice::UPS::Response::Address>, or fail with an
1021             exception.
1022              
1023             Identical requests can be cached.
1024              
1025             =head2 C<validate_street_address>
1026              
1027             $ups->validate_street_address($address)
1028             ==> (Net::Async::Webservice::UPS::Response::Address)
1029              
1030             C<$address> is an instance of L<Net::Async::Webservice::UPS::Address>,
1031             or a postcode string that will be coerced to an address.
1032              
1033             The L<Future> returned will yield an instance of
1034             L<Net::Async::Webservice::UPS::Response::Address>, or fail with an
1035             exception.
1036              
1037             Identical requests can be cached.
1038              
1039             =head2 C<ship_confirm>
1040              
1041             $ups->ship_confirm({
1042             from => $source_contact,
1043             to => $destination_contact,
1044             description => 'something',
1045             payment => $payment_method,
1046             packages => \@packages,
1047             }) ==> $shipconfirm_response
1048              
1049             Performs a C<ShipConfirm> request to UPS. The parameters are:
1050              
1051             =over 4
1052              
1053             =item C<from>
1054              
1055             required, instance of L<Net::Async::Webservice::UPS::Contact>, where the shipments starts from
1056              
1057             =item C<to>
1058              
1059             required, instance of L<Net::Async::Webservice::UPS::Contact>, where the shipments has to be delivered to
1060              
1061             =item C<shipper>
1062              
1063             optional, instance of L<Net::Async::Webservice::UPS::Shipper>, who is requesting the shipment; if not specified, it's taken to be the same as the C<from> with the L</account_number> of this UPS object
1064              
1065             =item C<service>
1066              
1067             the shipping service to use, see L<Net::Async::Webservice::UPS::Types/Service>, defaults to C<GROUND>
1068              
1069             =item C<description>
1070              
1071             required string, description of the shipment
1072              
1073             =item C<payment>
1074              
1075             required instance of L<Net::Async::Webservice::UPS::Payment>, how to pay for this shipment
1076              
1077             =item C<label>
1078              
1079             optional instance of L<Net::Async::Webservice::UPS::Label>, what kind of label to request
1080              
1081             =item C<packages>
1082              
1083             an arrayref of L<Net::Async::Webservice::UPS::Package> (or a single package, will be coerced to a 1-element array ref), the packages to ship
1084              
1085             =item C<return_service>
1086              
1087             optional, instance of L<Net::Async::Webservice::UPS::ReturnService>, what kind of return service to request
1088              
1089             =item C<delivery_confirmation>
1090              
1091             optional, 1 means "signature required", 2 mean "adult signature required"
1092              
1093             =item C<customer_context>
1094              
1095             optional string for reference purposes
1096              
1097             =back
1098              
1099             Returns a L<Future> yielding an instance of
1100             L<Net::Async::Webservice::UPS::Response::ShipmentConfirm>.
1101              
1102             B<NOTE>: the API of this call may change in the future, let me know if
1103             features you need are missing or badly understood!
1104              
1105             =head2 C<ship_accept>
1106              
1107             $ups->ship_accept({
1108             confirm => $shipconfirm_response,
1109             }) ==> $shipaccept_response
1110              
1111             Performs a C<ShipAccept> request to UPS. The parameters are:
1112              
1113             =over 4
1114              
1115             =item C<confirm>
1116              
1117             required, instance of L<Net::Async::Webservice::UPS::Response::ShipmentConfirm>,as returned by L</ship_confirm>
1118              
1119             =item C<customer_context>
1120              
1121             optional string for reference purposes
1122              
1123             =back
1124              
1125             Returns a L<Future> yielding an instance of
1126             L<Net::Async::Webservice::UPS::Response::ShipmentAccept>.
1127              
1128             =head2 C<qv_events>
1129              
1130             $ups->qv_events({
1131             subscriptions => [ Net::Async::Webservice::UPS::QVSubscription->new(
1132             name => 'MySubscription',
1133             ) ],
1134             }) ==> $qv_response
1135              
1136             Performs a C<QVEvennts> request to UPS. The parameters are:
1137              
1138             =over 4
1139              
1140             =item C<subscriptions>
1141              
1142             optional, array of L<Net::Async::Webservice::UPS::QVSubscription>, specifying what you want to retrieve
1143              
1144             =item C<bookmark>
1145              
1146             optional, string retrieved from a previous call, used for pagination (see L<Net::Async::Webservice::UPS::Response::QV>)
1147              
1148             =item C<customer_context>
1149              
1150             optional string for reference purposes
1151              
1152             =back
1153              
1154             Returns a L<Future> yielding an instance of
1155             L<Net::Async::Webservice::UPS::Response::QV>.
1156              
1157             =head2 C<xml_request>
1158              
1159             $ups->xml_request({
1160             url_suffix => $string,
1161             data => \%request_data,
1162             XMLout => \%xml_simple_out_options,
1163             XMLin => \%xml_simple_in_options,
1164             }) ==> ($parsed_response);
1165              
1166             This method is mostly internal, you shouldn't need to call it.
1167              
1168             It builds a request XML document by concatenating the output of
1169             L</access_as_xml> with whatever L<XML::Simple> produces from the given
1170             C<data> and C<XMLout> options.
1171              
1172             It then posts (possibly asynchronously) this to the URL obtained
1173             concatenating L</base_url> with C<url_suffix> (see the L</post>
1174             method). If the request is successful, it parses the body (with
1175             L<XML::Simple> using the C<XMLin> options) and completes the returned
1176             future with the result.
1177              
1178             If the parsed response contains a non-zero
1179             C</Response/ResponseStatusCode>, the returned future will fail with a
1180             L<Net::Async::Webservice::UPS::Exception::UPSError> instance.
1181              
1182             =head2 C<post>
1183              
1184             $ups->post($url_suffix,$body) ==> ($decoded_content)
1185              
1186             Posts the given C<$body> to the URL obtained concatenating
1187             L</base_url> with C<$url_suffix>. If the request is successful, it
1188             completes the returned future with the decoded content of the
1189             response, otherwise it fails the future with a
1190             L<Net::Async::Webservice::Common::Exception::HTTPError> instance.
1191              
1192             =head2 C<generate_cache_key>
1193              
1194             Generates a cache key (a string) identifying a request. Two requests
1195             with the same cache key should return the same response.
1196              
1197             =for Pod::Coverage BUILDARGS
1198              
1199             =head1 UPS SSL/TLS notes
1200              
1201             In December 2014, UPS notified all its users that it would stop
1202             supporting SSLv3 in March 2015. This library has no problems with
1203             that, since LWP has supported TLS for years.
1204              
1205             Another, unrelated, issue cropped up at rougly the same time, to
1206             confuse the situation: L<Mozilla::CA>, which is used to get the root
1207             certificates to verify connections, dropped a top-level Verisign
1208             certificate that Verisign stopped using in 2010, but the UPS servers'
1209             certificate was signed with it, so LWP stopped recognising the
1210             servers' certificate. Net::Async::Webservice::UPS 1.1.3 works around
1211             the problem by always including the root certificate in the default
1212             L</ssl_options>. If you use custom options, you may want to check that
1213             you're including the correct certificate. See also
1214             https://rt.cpan.org/Ticket/Display.html?id=101908
1215              
1216             =head1 AUTHORS
1217              
1218             =over 4
1219              
1220             =item *
1221              
1222             Gianni Ceccarelli <gianni.ceccarelli@net-a-porter.com>
1223              
1224             =item *
1225              
1226             Sherzod B. Ruzmetov <sherzodr@cpan.org>
1227              
1228             =back
1229              
1230             =head1 COPYRIGHT AND LICENSE
1231              
1232             This software is copyright (c) 2015 by Gianni Ceccarelli <gianni.ceccarelli@net-a-porter.com>.
1233              
1234             This is free software; you can redistribute it and/or modify it under
1235             the same terms as the Perl 5 programming language system itself.
1236              
1237             =cut