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