File Coverage

blib/lib/Net/Async/Webservice/DHL.pm
Criterion Covered Total %
statement 34 36 94.4
branch n/a
condition n/a
subroutine 12 12 100.0
pod n/a
total 46 48 95.8


line stmt bran cond sub pod time code
1             package Net::Async::Webservice::DHL;
2             $Net::Async::Webservice::DHL::VERSION = '1.2.1';
3             {
4             $Net::Async::Webservice::DHL::DIST = 'Net-Async-Webservice-DHL';
5             }
6 4     4   75036 use Moo;
  4         13489  
  4         24  
7 4     4   2663 use Types::Standard qw(Str Bool Object Dict Num Optional ArrayRef HashRef Undef Optional);
  4         55895  
  4         44  
8 4     4   8318 use Types::URI qw(Uri);
  4         443356  
  4         62  
9             use Types::DateTime
10 4         72 DateTime => { -as => 'DateTimeT' },
11 4     4   3937 Format => { -as => 'DTFormat' };
  4         463874  
12 4     4   2855 use Net::Async::Webservice::DHL::Types qw(Address RouteType RegionCode CountryCode);
  4         11  
  4         39  
13 4     4   4534 use Net::Async::Webservice::DHL::Exception;
  4         18  
  4         246  
14 4     4   3294 use Type::Params qw(compile);
  4         46667  
  4         50  
15 4     4   1042 use Error::TypeTiny;
  4         7  
  4         84  
16 4     4   20 use Try::Tiny;
  4         6  
  4         327  
17 4     4   2886 use List::AllUtils 'pairwise';
  4         13083  
  4         612  
18 4     4   2341 use HTTP::Request;
  4         125606  
  4         157  
19 4     4   6865 use XML::Compile::Cache;
  0            
  0            
20             use XML::Compile::Util 'type_of_node';
21             use XML::LibXML;
22             use Encode;
23             use namespace::autoclean;
24             use Future;
25             use DateTime;
26             use File::ShareDir::ProjectDistDir 'dist_dir', strict => 1;
27             use 5.010;
28              
29             # ABSTRACT: DHL API client, non-blocking
30              
31              
32             my %base_urls = (
33             live => 'https://xmlpi-ea.dhl.com/XMLShippingServlet',
34             test => 'https://xmlpitest-ea.dhl.com/XMLShippingServlet',
35             );
36              
37              
38             has live_mode => (
39             is => 'rw',
40             isa => Bool,
41             trigger => 1,
42             default => sub { 0 },
43             );
44              
45              
46             has base_url => (
47             is => 'lazy',
48             isa => Str,
49             clearer => '_clear_base_url',
50             );
51              
52             sub _trigger_live_mode {
53             my ($self) = @_;
54              
55             $self->_clear_base_url;
56             }
57             sub _build_base_url {
58             my ($self) = @_;
59              
60             return $base_urls{$self->live_mode ? 'live' : 'test'};
61             }
62              
63              
64             has username => (
65             is => 'ro',
66             isa => Str,
67             required => 1,
68             );
69             has password => (
70             is => 'ro',
71             isa => Str,
72             required => 1,
73             );
74              
75              
76             with 'Net::Async::Webservice::Common::WithUserAgent';
77              
78             has _xml_cache => (
79             is => 'lazy',
80             );
81              
82             sub _build__xml_cache {
83             my ($self) = @_;
84              
85             my $dir = dist_dir('Net-Async-Webservice-DHL');
86             my $c = XML::Compile::Cache->new(
87             schema_dirs => [ $dir ],
88             opts_rw => {
89             elements_qualified => 'TOP',
90             },
91             );
92             for my $f (qw(datatypes datatypes_global
93             DCT-req DCTRequestdatatypes
94             DCT-Response DCTResponsedatatypes
95             routing-global-req routing-global-res
96             routing-err-res err-res)) {
97             $c->importDefinitions("$f.xsd");
98             }
99             $c->declare('WRITER' => '{http://www.dhl.com}DCTRequest');
100             $c->declare('READER' => '{http://www.dhl.com}DCTResponse');
101              
102             $c->declare('WRITER' => '{http://www.dhl.com}RouteRequest');
103             $c->declare('READER' => '{http://www.dhl.com}RouteResponse');
104             $c->declare('READER' => '{http://www.dhl.com}RoutingErrorResponse');
105              
106             $c->declare('READER' => '{http://www.dhl.com}ErrorResponse');
107              
108             $c->compileAll;
109              
110             return $c;
111             }
112              
113              
114             with 'Net::Async::Webservice::Common::WithConfigFile';
115              
116              
117             sub _mr {
118             if ($_[0]->{message_reference}) {
119             return ( message_reference => $_[0]->{message_reference} );
120             }
121             return;
122             }
123              
124             sub get_capability {
125             state $argcheck = compile(
126             Object,
127             Dict[
128             from => Address,
129             to => Address,
130             is_dutiable => Bool,
131             currency_code => Str,
132             shipment_value => Num,
133             product_code => Optional[Str],
134             date => Optional[DateTimeT->plus_coercions(DTFormat['ISO8601'])],
135             message_reference => Optional[Str],
136             ],
137             );
138             my ($self,$args) = $argcheck->(@_);
139              
140             $args->{date} = $args->{date}
141             ? $args->{date}->clone->set_time_zone('UTC')
142             : DateTime->now(time_zone => 'UTC');
143              
144             my $req = {
145             From => $args->{from}->as_hash('capability'),
146             To => $args->{to}->as_hash('capability'),
147             BkgDetails => {
148             PaymentCountryCode => $args->{to}->country_code,
149             Date => $args->{date}->ymd,
150             ReadyTime => 'PT' . $args->{date}->hour . 'H' . $args->{date}->minute . 'M',
151             DimensionUnit => 'CM',
152             WeightUnit => 'KG',
153             IsDutiable => ($args->{is_dutiable} ? 'Y' : 'N'),
154             NetworkTypeCode => 'AL',
155             ( defined $args->{product_code} ? (
156             QtdShp => {
157             GlobalProductCode => $args->{product_code},
158             QtdShpExChrg => {
159             SpecialServiceType => 'OSINFO',
160             },
161             },
162             ) : () ),
163             },
164             Dutiable => {
165             DeclaredCurrency => $args->{currency_code},
166             DeclaredValue => $args->{shipment_value},
167             },
168             };
169              
170             return $self->xml_request({
171             data => $req,
172             request_method => 'GetCapability',
173             _mr($args),
174             })->then(
175             sub {
176             my ($response) = @_;
177             return Future->wrap($response);
178             },
179             );
180             }
181              
182              
183             sub route_request {
184             state $argcheck = compile(
185             Object,
186             Dict[
187             region_code => RegionCode,
188             routing_type => RouteType,
189             address => Address,
190             origin_country_code => CountryCode,
191             message_reference => Optional[Str],
192             ],
193             );
194             my ($self,$args) = $argcheck->(@_);
195              
196             my $req = {
197             RequestType => $args->{routing_type},
198             RegionCode => $args->{region_code},
199             OriginCountryCode => $args->{origin_country_code},
200             %{$args->{address}->as_hash('route')},
201             schemaVersion => '1.0',
202             };
203              
204             return $self->xml_request({
205             data => $req,
206             request_method => 'RouteRequest',
207             _mr($args),
208             })->then(
209             sub {
210             my ($response) = @_;
211             return Future->wrap($response);
212             },
213             );
214             }
215              
216              
217             my %request_type_map = (
218             GetCapability => ['GetCapability','DCTRequest','DCTResponse'],
219             RouteRequest => ['','RouteRequest','RouteResponse'],
220             );
221              
222             sub xml_request {
223             state $argcheck = compile(
224             Object,
225             Dict[
226             data => HashRef,
227             request_method => Str,
228             message_time => Optional[DateTimeT->plus_coercions(DTFormat['ISO8601'])],
229             message_reference => Optional[Str],
230             ],
231             );
232             my ($self, $args) = $argcheck->(@_);
233              
234             my ($top_level_elemel,$req_type,$res_type) =
235             @{ $request_type_map{$args->{request_method}} || [] };
236              
237             $args->{message_time} = $args->{message_time}
238             ? $args->{message_time}->clone->set_time_zone('UTC')
239             : DateTime->now(time_zone => 'UTC');
240              
241             my $doc = XML::LibXML::Document->new('1.0','utf-8');
242              
243             my $writer = $self->_xml_cache->writer("{http://www.dhl.com}$req_type");
244              
245             my $req = {
246             Request => {
247             ServiceHeader => {
248             MessageTime => $args->{message_time}->iso8601,
249             SiteID => $self->username,
250             Password => $self->password,
251             MessageReference => (sprintf '% 28s',($args->{message_reference} // time())),
252             },
253             },
254             %{$args->{data}},
255             };
256              
257             if ($top_level_elemel) {
258             $req = { $top_level_elemel => $req };
259             }
260              
261             my $docElem = $writer->($doc,$req);
262             $doc->setDocumentElement($docElem);
263              
264             my $request = $doc->toString(1);
265              
266             return $self->post( $self->base_url, $request )->then(
267             sub {
268             my ($response_string) = @_;
269              
270             my $response_doc = XML::LibXML->load_xml(
271             string=>\$response_string,
272             load_ext_dtd => 0,
273             expand_xincludes => 0,
274             no_network => 1,
275             );
276              
277             my $type = type_of_node $response_doc->documentElement;
278              
279             my $reader = $self->_xml_cache->reader($type);
280             my $response = $reader->($response_doc);
281              
282             if ($response_doc->documentElement->nodeName =~ /Error/) {
283             return Future->new->fail(
284             Net::Async::Webservice::DHL::Exception::DHLError->new({
285             error => $response->{Response}{Status}
286             }),
287             'dhl',
288             );
289             }
290             else {
291             return Future->wrap($response);
292             }
293             }
294             );
295             }
296              
297              
298             with 'Net::Async::Webservice::Common::WithRequestWrapper';
299              
300             1;
301              
302             __END__
303              
304             =pod
305              
306             =encoding UTF-8
307              
308             =head1 NAME
309              
310             Net::Async::Webservice::DHL - DHL API client, non-blocking
311              
312             =head1 VERSION
313              
314             version 1.2.1
315              
316             =head1 SYNOPSIS
317              
318             use IO::Async::Loop;
319             use Net::Async::Webservice::DHL;
320             use Data::Printer;
321              
322             my $loop = IO::Async::Loop->new;
323              
324             my $dhl = Net::Async::Webservice::DHL->new({
325             config_file => $ENV{HOME}.'/.naws_dhl.conf',
326             loop => $loop,
327             });
328              
329             $dhl->get_capability({
330             from => $address_a,
331             to => $address_b,
332             is_dutiable => 0,
333             currency_code => 'GBP',
334             shipment_value => 100,
335             })->then(sub {
336             my ($response) = @_;
337             p $response;
338             return Future->wrap();
339             });
340              
341             $loop->run;
342              
343             Alternatively:
344              
345             use Net::Async::Webservice::DHL;
346             use Data::Printer;
347              
348             my $ups = Net::Async::Webservice::DHL->new({
349             config_file => $ENV{HOME}.'/.naws_dhl.conf',
350             user_agent => LWP::UserAgent->new,
351             });
352              
353             my $response = $dhl->get_capability({
354             from => $address_a,
355             to => $address_b,
356             is_dutiable => 0,
357             currency_code => 'GBP',
358             shipment_value => 100,
359             })->get;
360              
361             p $response;
362              
363             =head1 DESCRIPTION
364              
365             This class implements some of the methods of the DHL XML-PI API, using
366             L<Net::Async::HTTP> as a user agent I<by default> (you can still pass
367             something like L<LWP::UserAgent> and it will work). All methods that
368             perform API calls return L<Future>s (if using a synchronous user
369             agent, all the Futures will be returned already completed).
370              
371             =head1 ATTRIBUTES
372              
373             =head2 C<live_mode>
374              
375             Boolean, defaults to false. When set to true, the live API endpoint
376             will be used, otherwise the test one will. Flipping this attribute
377             will reset L</base_url>, so you generally don't want to touch this if
378             you're using some custom API endpoint.
379              
380             =head2 C<base_url>
381              
382             A L<URI> object, coercible from a string. The base URL to use to send
383             API requests to. Defaults to the standard DHL endpoints:
384              
385             =over 4
386              
387             =item *
388              
389             C<https://xmlpi-ea.dhl.com/XMLShippingServlet> for live
390              
391             =item *
392              
393             C<https://xmlpitest-ea.dhl.com/XMLShippingServlet> for testing
394              
395             =back
396              
397             See also L</live_mode>.
398              
399             =head2 C<username>
400              
401             =head2 C<password>
402              
403             Strings, required. Authentication credentials.
404              
405             =head2 C<user_agent>
406              
407             A user agent object, looking either like L<Net::Async::HTTP> (has
408             C<do_request> and C<POST>) or like L<LWP::UserAgent> (has C<request>
409             and C<post>). You can pass the C<loop> constructor parameter to get a
410             default L<Net::Async::HTTP> instance.
411              
412             =head1 METHODS
413              
414             =head2 C<new>
415              
416             Async:
417              
418             my $dhl = Net::Async::Webservice::DHL->new({
419             loop => $loop,
420             config_file => $file_name,
421             });
422              
423             Sync:
424              
425             my $dhl = Net::Async::Webservice::DHL->new({
426             user_agent => LWP::UserAgent->new,
427             config_file => $file_name,
428             });
429              
430             In addition to passing all the various attributes values, you can use
431             a few shortcuts.
432              
433             =over 4
434              
435             =item C<loop>
436              
437             a L<IO::Async::Loop>; a locally-constructed L<Net::Async::HTTP> will be registered to it and set as L</user_agent>
438              
439             =item C<config_file>
440              
441             a path name; will be parsed with L<Config::Any>, and the values used as if they had been passed in to the constructor
442              
443             =back
444              
445             =head2 C<get_capability>
446              
447             $dhl->get_capability({
448             from => $address_a,
449             to => $address_b,
450             is_dutiable => 0,
451             currency_code => 'GBP',
452             shipment_value => 100,
453             }) ==> ($hashref)
454              
455             C<from> and C<to> are instances of
456             L<Net::Async::Webservice::DHL::Address>, C<is_dutiable> is a boolean.
457              
458             Optional parameters:
459              
460             =over 4
461              
462             =item C<date>
463              
464             the date/time for the booking, defaults to I<now>; it will converted to UTC time zone
465              
466             =item C<product_code>
467              
468             a DHL product code
469              
470             =item C<message_reference>
471              
472             a string, to uniquely identify individual messages
473              
474             =back
475              
476             Performs a C<GetCapability> request. Lots of values in the request are
477             not filled in, this should be used essentially to check for address
478             validity and little more. I'm not sure how to read the response,
479             either.
480              
481             The L<Future> returned will yield a hashref containing the
482             "interesting" bits of the XML response (as judged by
483             L<XML::Compile::Schema>), or fail with an exception.
484              
485             =head2 C<route_request>
486              
487             $dhl->route_request({
488             region_code => $dhl_region_code,
489             routing_type => 'O', # or 'D'
490             address => $address,
491             origin_country_code => $country_code,
492             }) ==> ($hashref)
493              
494             C<address> is an instance of L<Net::Async::Webservice::DHL::Address>.
495             C<type> is C<O> for origin routing, or C<D> for destination
496             routing. C<origin_country_code> is the "country code of origin"
497             according to the DHL spec.
498              
499             Optional parameters:
500              
501             =over 4
502              
503             =item C<message_reference>
504              
505             a string, to uniquely identify individual messages
506              
507             =back
508              
509             Performs a C<RouteRequest> request.
510              
511             The L<Future> returned will yield a hashref containing the
512             "interesting" bits of the XML response (as judged by
513             L<XML::Compile::Schema>), or fail with an exception.
514              
515             =head2 C<xml_request>
516              
517             $dhl->xml_request({
518             request_method => $string,
519             data => \%request_data,
520             }) ==> ($parsed_response);
521              
522             This method is mostly internal, you shouldn't need to call it.
523              
524             It builds a request XML document by passing the given C<data> to an
525             L<XML::Compile> writer built on the DHL schema.
526              
527             It then posts (possibly asynchronously) this to the L</base_url> (see
528             the L</post> method). If the request is successful, it parses the body
529             with a L<XML::Compile> reader, either the one for the response or the
530             one for C<ErrorResponse>, depending on the document element. If it's a
531             valid response, the Future is completed with the hashref returned by
532             the reader. If it's C<ErrorResponse>, teh Future is failed with a
533             L<Net::Async::Webservice::DHL::Exception::DHLError> contaning the
534             response status.
535              
536             =head2 C<post>
537              
538             $dhl->post($body) ==> ($decoded_content)
539              
540             Posts the given C<$body> to the L</base_url>. If the request is
541             successful, it completes the returned future with the decoded content
542             of the response, otherwise it fails the future with a
543             L<Net::Async::Webservice::Common::Exception::HTTPError> instance.
544              
545             =for Pod::Coverage BUILDARGS
546              
547             =head1 AUTHOR
548              
549             Gianni Ceccarelli <gianni.ceccarelli@net-a-porter.com>
550              
551             =head1 COPYRIGHT AND LICENSE
552              
553             This software is copyright (c) 2014 by Net-a-porter.
554              
555             This is free software; you can redistribute it and/or modify it under
556             the same terms as the Perl 5 programming language system itself.
557              
558             =cut