| line | stmt | bran | cond | sub | pod | time | code | 
| 1 |  |  |  |  |  |  | package WebService::DPD::API; | 
| 2 | 1 |  |  | 1 |  | 29218 | use strict; | 
|  | 1 |  |  |  |  | 3 |  | 
|  | 1 |  |  |  |  | 36 |  | 
| 3 | 1 |  |  | 1 |  | 5 | use warnings; | 
|  | 1 |  |  |  |  | 3 |  | 
|  | 1 |  |  |  |  | 34 |  | 
| 4 | 1 |  |  | 1 |  | 7 | use Carp; | 
|  | 1 |  |  |  |  | 8 |  | 
|  | 1 |  |  |  |  | 108 |  | 
| 5 | 1 |  |  | 1 |  | 3226 | use Moo; | 
|  | 1 |  |  |  |  | 23907 |  | 
|  | 1 |  |  |  |  | 7 |  | 
| 6 | 1 |  |  | 1 |  | 2912 | use LWP::UserAgent; | 
|  | 1 |  |  |  |  | 90895 |  | 
|  | 1 |  |  |  |  | 33 |  | 
| 7 | 1 |  |  | 1 |  | 821 | use HTTP::Request::Common; | 
|  | 1 |  |  |  |  | 2057 |  | 
|  | 1 |  |  |  |  | 95 |  | 
| 8 | 1 |  |  | 1 |  | 8 | use URI::Escape; | 
|  | 1 |  |  |  |  | 3 |  | 
|  | 1 |  |  |  |  | 67 |  | 
| 9 | 1 |  |  | 1 |  | 2819 | use Data::Dumper; | 
|  | 1 |  |  |  |  | 8257 |  | 
|  | 1 |  |  |  |  | 71 |  | 
| 10 | 1 |  |  | 1 |  | 1567 | use JSON; | 
|  | 1 |  |  |  |  | 15049 |  | 
|  | 1 |  |  |  |  | 8 |  | 
| 11 | 1 |  |  | 1 |  | 2671 | use MIME::Base64; | 
|  | 1 |  |  |  |  | 890 |  | 
|  | 1 |  |  |  |  | 75 |  | 
| 12 | 1 |  |  | 1 |  | 728 | use namespace::clean; | 
|  | 1 |  |  |  |  | 19128 |  | 
|  | 1 |  |  |  |  | 8 |  | 
| 13 |  |  |  |  |  |  |  | 
| 14 |  |  |  |  |  |  | # ABSTRACT: communicates with DPD API | 
| 15 |  |  |  |  |  |  |  | 
| 16 |  |  |  |  |  |  | our $VERSION = 'v0.0002'; | 
| 17 |  |  |  |  |  |  |  | 
| 18 |  |  |  |  |  |  |  | 
| 19 |  |  |  |  |  |  | =head1 NAME | 
| 20 |  |  |  |  |  |  |  | 
| 21 |  |  |  |  |  |  | WebService::DPD::API | 
| 22 |  |  |  |  |  |  |  | 
| 23 |  |  |  |  |  |  | =head1 SYNOPSIS | 
| 24 |  |  |  |  |  |  |  | 
| 25 |  |  |  |  |  |  |  | 
| 26 |  |  |  |  |  |  | $dpd = WebService::DPD::API->new( | 
| 27 |  |  |  |  |  |  | username => $username, | 
| 28 |  |  |  |  |  |  | password => $password, | 
| 29 |  |  |  |  |  |  | geoClient => "account/$customer_id", | 
| 30 |  |  |  |  |  |  | ); | 
| 31 |  |  |  |  |  |  | =cut | 
| 32 |  |  |  |  |  |  |  | 
| 33 |  |  |  |  |  |  | =head1 DESCRIPTION | 
| 34 |  |  |  |  |  |  |  | 
| 35 |  |  |  |  |  |  | This module provides a simple wrapper around DPD delivery service API. This is a work in progress and contains incomplete test code, methods are likely to be refactored, you have been warned. | 
| 36 |  |  |  |  |  |  |  | 
| 37 |  |  |  |  |  |  |  | 
| 38 |  |  |  |  |  |  | =head1 METHODS | 
| 39 |  |  |  |  |  |  |  | 
| 40 |  |  |  |  |  |  | =cut | 
| 41 |  |  |  |  |  |  |  | 
| 42 |  |  |  |  |  |  |  | 
| 43 |  |  |  |  |  |  | has username => ( | 
| 44 |  |  |  |  |  |  | is => 'ro', | 
| 45 |  |  |  |  |  |  | required => 1, | 
| 46 |  |  |  |  |  |  | ); | 
| 47 |  |  |  |  |  |  |  | 
| 48 |  |  |  |  |  |  | has password => ( | 
| 49 |  |  |  |  |  |  | is => 'ro', | 
| 50 |  |  |  |  |  |  | required => 1, | 
| 51 |  |  |  |  |  |  | ); | 
| 52 |  |  |  |  |  |  |  | 
| 53 |  |  |  |  |  |  | has url => ( is => 'ro', | 
| 54 |  |  |  |  |  |  | default => 'https://api.dpd.co.uk' | 
| 55 |  |  |  |  |  |  | ); | 
| 56 |  |  |  |  |  |  |  | 
| 57 |  |  |  |  |  |  | has host => ( is => 'ro', | 
| 58 |  |  |  |  |  |  | lazy => 1, | 
| 59 |  |  |  |  |  |  | default => sub { | 
| 60 |  |  |  |  |  |  | my $self=shift; | 
| 61 |  |  |  |  |  |  | my $url = $self->url; | 
| 62 |  |  |  |  |  |  | $url =~ s/^https{0,1}.:\/\///; | 
| 63 |  |  |  |  |  |  | return $url; }, | 
| 64 |  |  |  |  |  |  | ); | 
| 65 |  |  |  |  |  |  |  | 
| 66 |  |  |  |  |  |  | has ua => ( | 
| 67 |  |  |  |  |  |  | is => 'rw', | 
| 68 |  |  |  |  |  |  | ); | 
| 69 |  |  |  |  |  |  |  | 
| 70 |  |  |  |  |  |  | has geoSession => ( | 
| 71 |  |  |  |  |  |  | is => 'rw', | 
| 72 |  |  |  |  |  |  | ); | 
| 73 |  |  |  |  |  |  |  | 
| 74 |  |  |  |  |  |  | has geoClient => ( | 
| 75 |  |  |  |  |  |  | is => 'ro', | 
| 76 |  |  |  |  |  |  | default => 'thirdparty/pryanet', | 
| 77 |  |  |  |  |  |  | ); | 
| 78 |  |  |  |  |  |  |  | 
| 79 |  |  |  |  |  |  | has debug => ( | 
| 80 |  |  |  |  |  |  | is => 'rw', | 
| 81 |  |  |  |  |  |  | default => 0, | 
| 82 |  |  |  |  |  |  | ); | 
| 83 |  |  |  |  |  |  |  | 
| 84 |  |  |  |  |  |  | has errstr => ( | 
| 85 |  |  |  |  |  |  | is => 'rw', | 
| 86 |  |  |  |  |  |  | default => '', | 
| 87 |  |  |  |  |  |  | ); | 
| 88 |  |  |  |  |  |  |  | 
| 89 |  |  |  |  |  |  | sub BUILD | 
| 90 |  |  |  |  |  |  | { | 
| 91 | 1 |  |  | 1 | 0 | 3718 | my $self = shift; | 
| 92 | 1 |  |  |  |  | 14 | $self->ua( LWP::UserAgent->new ); | 
| 93 | 1 |  |  |  |  | 4010 | $self->ua->agent("Perl_WebService::DPD::API/$VERSION"); | 
| 94 | 1 |  |  |  |  | 90 | $self->ua->cookie_jar({}); | 
| 95 |  |  |  |  |  |  | } | 
| 96 |  |  |  |  |  |  |  | 
| 97 |  |  |  |  |  |  |  | 
| 98 |  |  |  |  |  |  |  | 
| 99 |  |  |  |  |  |  |  | 
| 100 |  |  |  |  |  |  | =head2 login | 
| 101 |  |  |  |  |  |  |  | 
| 102 |  |  |  |  |  |  | Authenticates and establishes api session used by following methods | 
| 103 |  |  |  |  |  |  |  | 
| 104 |  |  |  |  |  |  | $dpd->login; | 
| 105 |  |  |  |  |  |  |  | 
| 106 |  |  |  |  |  |  | =cut | 
| 107 |  |  |  |  |  |  | sub login | 
| 108 |  |  |  |  |  |  | { | 
| 109 | 1 |  |  | 1 | 1 | 2 | my $self = shift; | 
| 110 | 1 |  |  |  |  | 29 | my $result =  $self->send_request( { | 
| 111 |  |  |  |  |  |  | path	=> '/user/?action=login', | 
| 112 |  |  |  |  |  |  | type	=> 'POST', | 
| 113 |  |  |  |  |  |  | header	=>  { | 
| 114 |  |  |  |  |  |  | Authorization => 'Basic ' . encode_base64($self->username . ':' . $self->password, ''), | 
| 115 |  |  |  |  |  |  | }, | 
| 116 |  |  |  |  |  |  | } ); | 
| 117 | 1 |  |  |  |  | 10 | $self->geoSession( $result->{geoSession} ); | 
| 118 | 1 |  |  |  |  | 9 | return $result; | 
| 119 |  |  |  |  |  |  | } | 
| 120 |  |  |  |  |  |  |  | 
| 121 |  |  |  |  |  |  | =head2 get_country( $code ) | 
| 122 |  |  |  |  |  |  |  | 
| 123 |  |  |  |  |  |  | Retrieves the country details for a provided country code and can be used to determine if a country requires a postcode or if liability is allowed etc. | 
| 124 |  |  |  |  |  |  |  | 
| 125 |  |  |  |  |  |  | $country = $dpd->get_country( 'GB' ); | 
| 126 |  |  |  |  |  |  |  | 
| 127 |  |  |  |  |  |  | =cut | 
| 128 |  |  |  |  |  |  | sub get_country | 
| 129 |  |  |  |  |  |  | { | 
| 130 | 1 |  |  | 1 | 1 | 8 | my ( $self, $code ) = @_; | 
| 131 | 1 | 50 | 0 |  |  | 4 | $self->errstr( "No country code" ) and return unless $code; | 
| 132 | 1 |  |  |  |  | 8 | return $self->send_request ( { | 
| 133 |  |  |  |  |  |  | path => '/shipping/country/' . $code, | 
| 134 |  |  |  |  |  |  | } ); | 
| 135 |  |  |  |  |  |  | } | 
| 136 |  |  |  |  |  |  |  | 
| 137 |  |  |  |  |  |  | =head2 get_services( \%shipping_information ) | 
| 138 |  |  |  |  |  |  |  | 
| 139 |  |  |  |  |  |  | Retrieves list of services available for provided shipping information. | 
| 140 |  |  |  |  |  |  |  | 
| 141 |  |  |  |  |  |  | my $address = { | 
| 142 |  |  |  |  |  |  | countryCode     => 'GB', | 
| 143 |  |  |  |  |  |  | county          => 'West Midlands', | 
| 144 |  |  |  |  |  |  | locality        => 'Birmingham', | 
| 145 |  |  |  |  |  |  | organisation    => 'GeoPost', | 
| 146 |  |  |  |  |  |  | postcode        => 'B661BY', | 
| 147 |  |  |  |  |  |  | property        => 'GeoPost UK', | 
| 148 |  |  |  |  |  |  | street          => 'Roebuck Ln', | 
| 149 |  |  |  |  |  |  | town            => 'Smethwick', | 
| 150 |  |  |  |  |  |  | }; | 
| 151 |  |  |  |  |  |  |  | 
| 152 |  |  |  |  |  |  | my $shipping = { | 
| 153 |  |  |  |  |  |  | collectionDetails   => { | 
| 154 |  |  |  |  |  |  | address => $address, | 
| 155 |  |  |  |  |  |  | }, | 
| 156 |  |  |  |  |  |  | deliveryDetails     => { | 
| 157 |  |  |  |  |  |  | address => $address, | 
| 158 |  |  |  |  |  |  | }, | 
| 159 |  |  |  |  |  |  | deliveryDirection   => 1, # 1 for outbound 2 for inbound | 
| 160 |  |  |  |  |  |  | numberOfParcels     => 1, | 
| 161 |  |  |  |  |  |  | totalWeight         => 5, | 
| 162 |  |  |  |  |  |  | shipmentType        => 0, # 1 or 2 if a collection on delivery or swap it service is required | 
| 163 |  |  |  |  |  |  | }; | 
| 164 |  |  |  |  |  |  |  | 
| 165 |  |  |  |  |  |  | my $services = $dpd->get_services( $shipping ); | 
| 166 |  |  |  |  |  |  |  | 
| 167 |  |  |  |  |  |  |  | 
| 168 |  |  |  |  |  |  | =cut | 
| 169 |  |  |  |  |  |  | sub get_services | 
| 170 |  |  |  |  |  |  | { | 
| 171 | 1 |  |  | 1 | 1 | 1478 | my ( $self, $shipping ) = @_; | 
| 172 | 1 | 50 | 0 |  |  | 5 | $self->errstr( "No shipping information" ) and return unless $shipping; | 
| 173 | 1 |  |  |  |  | 5 | return $self->send_request ( { | 
| 174 |  |  |  |  |  |  | path => '/shipping/network/?' . $self->_to_query_params($shipping), | 
| 175 |  |  |  |  |  |  | } ); | 
| 176 |  |  |  |  |  |  | } | 
| 177 |  |  |  |  |  |  |  | 
| 178 |  |  |  |  |  |  | =head2 get_service( geoServiceCode ) | 
| 179 |  |  |  |  |  |  |  | 
| 180 |  |  |  |  |  |  | Retrieves the supported countries for a geoServiceCode | 
| 181 |  |  |  |  |  |  |  | 
| 182 |  |  |  |  |  |  | $service = $dpd->get_service(812); | 
| 183 |  |  |  |  |  |  |  | 
| 184 |  |  |  |  |  |  | =cut | 
| 185 |  |  |  |  |  |  | sub get_service | 
| 186 |  |  |  |  |  |  | { | 
| 187 | 1 |  |  | 1 | 1 | 1141 | my ( $self, $geoServiceCode ) = @_; | 
| 188 | 1 | 50 | 0 |  |  | 5 | $self->errstr( "No geoServiceCode" ) and return unless $geoServiceCode; | 
| 189 | 1 |  |  |  |  | 8 | return $self->send_request ( { | 
| 190 |  |  |  |  |  |  | path => "/shipping/network/$geoServiceCode/", | 
| 191 |  |  |  |  |  |  | } ); | 
| 192 |  |  |  |  |  |  | } | 
| 193 |  |  |  |  |  |  |  | 
| 194 |  |  |  |  |  |  | =head2 create_shipment( \%data ) | 
| 195 |  |  |  |  |  |  |  | 
| 196 |  |  |  |  |  |  | Creates a shipment object | 
| 197 |  |  |  |  |  |  |  | 
| 198 |  |  |  |  |  |  | my $shipment_data = { | 
| 199 |  |  |  |  |  |  | jobId => 'null', | 
| 200 |  |  |  |  |  |  | collectionOnDelivery =>  "false", | 
| 201 |  |  |  |  |  |  | invoice =>  "null", | 
| 202 |  |  |  |  |  |  | collectionDate =>  $date, | 
| 203 |  |  |  |  |  |  | consolidate =>  "false", | 
| 204 |  |  |  |  |  |  | consignment => [ | 
| 205 |  |  |  |  |  |  | { | 
| 206 |  |  |  |  |  |  | collectionDetails => { | 
| 207 |  |  |  |  |  |  | contactDetails => { | 
| 208 |  |  |  |  |  |  | contactName => "Mr David Smith", | 
| 209 |  |  |  |  |  |  | telephone => "0121 500 2500" | 
| 210 |  |  |  |  |  |  | }, | 
| 211 |  |  |  |  |  |  | address => $address, | 
| 212 |  |  |  |  |  |  | }, | 
| 213 |  |  |  |  |  |  | deliveryDetails => { | 
| 214 |  |  |  |  |  |  | contactDetails => { | 
| 215 |  |  |  |  |  |  | contactName => "Mr David Smith", | 
| 216 |  |  |  |  |  |  | telephone => "0121 500 2500" | 
| 217 |  |  |  |  |  |  | }, | 
| 218 |  |  |  |  |  |  | notificationDetails => { | 
| 219 |  |  |  |  |  |  | mobile => "07921 123456", | 
| 220 |  |  |  |  |  |  | email => 'david.smith@acme.com', | 
| 221 |  |  |  |  |  |  | }, | 
| 222 |  |  |  |  |  |  | address => { | 
| 223 |  |  |  |  |  |  | organisation => "ACME Ltd", | 
| 224 |  |  |  |  |  |  | property => "Miles Industrial Estate", | 
| 225 |  |  |  |  |  |  | street => "42 Bridge Road", | 
| 226 |  |  |  |  |  |  | locality => "", | 
| 227 |  |  |  |  |  |  | town => "Birmingham", | 
| 228 |  |  |  |  |  |  | county => "West Midlands", | 
| 229 |  |  |  |  |  |  | postcode => "B1 1AA", | 
| 230 |  |  |  |  |  |  | countryCode => "GB", | 
| 231 |  |  |  |  |  |  | } | 
| 232 |  |  |  |  |  |  | }, | 
| 233 |  |  |  |  |  |  | networkCode => "1^12", | 
| 234 |  |  |  |  |  |  | numberOfParcels => '1', | 
| 235 |  |  |  |  |  |  | totalWeight => '5', | 
| 236 |  |  |  |  |  |  | shippingRef1 => "Catalogue Batch 1", | 
| 237 |  |  |  |  |  |  | shippingRef2 => "Invoice 231", | 
| 238 |  |  |  |  |  |  | shippingRef3 => "", | 
| 239 |  |  |  |  |  |  | customsValue => '0', | 
| 240 |  |  |  |  |  |  | deliveryInstructions => "Please deliver to industrial gate A", | 
| 241 |  |  |  |  |  |  | parcelDescription => "", | 
| 242 |  |  |  |  |  |  | liabilityValue => '0', | 
| 243 |  |  |  |  |  |  | liability => "false", | 
| 244 |  |  |  |  |  |  | parcels => [], | 
| 245 |  |  |  |  |  |  | consignmentNumber => "null", | 
| 246 |  |  |  |  |  |  | consignmentRef =>  "null", | 
| 247 |  |  |  |  |  |  | } | 
| 248 |  |  |  |  |  |  | ] | 
| 249 |  |  |  |  |  |  | }; | 
| 250 |  |  |  |  |  |  |  | 
| 251 |  |  |  |  |  |  |  | 
| 252 |  |  |  |  |  |  | $shipment = $dpd->create_shipment( $shipment_data_example ); | 
| 253 |  |  |  |  |  |  |  | 
| 254 |  |  |  |  |  |  | =cut | 
| 255 |  |  |  |  |  |  | sub create_shipment | 
| 256 |  |  |  |  |  |  | { | 
| 257 | 1 |  |  | 1 | 1 | 2328 | my ( $self, $data ) = @_; | 
| 258 | 1 | 50 | 0 |  |  | 6 | $self->errstr( "No data" ) and return unless $data; | 
| 259 | 1 |  |  |  |  | 12 | return $self->send_request ( { | 
| 260 |  |  |  |  |  |  | type => 'POST', | 
| 261 |  |  |  |  |  |  | path => "/shipping/shipment", | 
| 262 |  |  |  |  |  |  | data => $data, | 
| 263 |  |  |  |  |  |  | } ); | 
| 264 |  |  |  |  |  |  | } | 
| 265 |  |  |  |  |  |  |  | 
| 266 |  |  |  |  |  |  | =head2 list_countries | 
| 267 |  |  |  |  |  |  |  | 
| 268 |  |  |  |  |  |  | Provides a full list of available shipping countries | 
| 269 |  |  |  |  |  |  |  | 
| 270 |  |  |  |  |  |  | $countries = $dpd->list_countries; | 
| 271 |  |  |  |  |  |  |  | 
| 272 |  |  |  |  |  |  | =cut | 
| 273 |  |  |  |  |  |  |  | 
| 274 |  |  |  |  |  |  | sub list_countries | 
| 275 |  |  |  |  |  |  | { | 
| 276 | 1 |  |  | 1 | 1 | 1186 | my $self = shift; | 
| 277 | 1 |  |  |  |  | 6 | return $self->send_request ( { | 
| 278 |  |  |  |  |  |  | path => '/shipping/country', | 
| 279 |  |  |  |  |  |  | } ); | 
| 280 |  |  |  |  |  |  | } | 
| 281 |  |  |  |  |  |  |  | 
| 282 |  |  |  |  |  |  | =head2 get_labels( $shipment_id, $format ) | 
| 283 |  |  |  |  |  |  |  | 
| 284 |  |  |  |  |  |  | Get label for given shipment id, available in multiple formats | 
| 285 |  |  |  |  |  |  |  | 
| 286 |  |  |  |  |  |  | $label = $dpd->get_labels( $shipment_id, 'application/pdf' ); | 
| 287 |  |  |  |  |  |  |  | 
| 288 |  |  |  |  |  |  | =cut | 
| 289 |  |  |  |  |  |  | sub get_labels | 
| 290 |  |  |  |  |  |  | { | 
| 291 | 1 |  |  | 1 | 1 | 1260 | my ( $self, $id, $format ) = @_; | 
| 292 | 1 | 50 | 50 |  |  | 11 | $self->errstr( "No shipment ID/format provided" ) and return unless ( $id and $format ); | 
|  |  |  | 33 |  |  |  |  | 
| 293 | 0 |  |  |  |  | 0 | return $self->send_request ( { | 
| 294 |  |  |  |  |  |  | path	=> "/shipping/shipment/$id/label/", | 
| 295 |  |  |  |  |  |  | header	=> { | 
| 296 |  |  |  |  |  |  | Accept => $format, | 
| 297 |  |  |  |  |  |  | }, | 
| 298 |  |  |  |  |  |  | raw_result => 1, | 
| 299 |  |  |  |  |  |  | } ); | 
| 300 |  |  |  |  |  |  |  | 
| 301 |  |  |  |  |  |  | } | 
| 302 |  |  |  |  |  |  |  | 
| 303 |  |  |  |  |  |  |  | 
| 304 |  |  |  |  |  |  | =head1 FUTURE METHODS | 
| 305 |  |  |  |  |  |  |  | 
| 306 |  |  |  |  |  |  | These methods are implemented as documented in the DPD API specification.  Although at the time of writing their functionality has not been publically implemented within the API. | 
| 307 |  |  |  |  |  |  |  | 
| 308 |  |  |  |  |  |  | =cut | 
| 309 |  |  |  |  |  |  |  | 
| 310 |  |  |  |  |  |  |  | 
| 311 |  |  |  |  |  |  | =head2 request_job_id | 
| 312 |  |  |  |  |  |  |  | 
| 313 |  |  |  |  |  |  | Get a job id to group shipments | 
| 314 |  |  |  |  |  |  |  | 
| 315 |  |  |  |  |  |  | $job_id = $dpd->request_jobid; | 
| 316 |  |  |  |  |  |  |  | 
| 317 |  |  |  |  |  |  | =cut | 
| 318 |  |  |  |  |  |  | sub request_jobid | 
| 319 |  |  |  |  |  |  | { | 
| 320 | 0 |  |  | 0 | 0 | 0 | my ( $self ) = @_; | 
| 321 | 0 |  |  |  |  | 0 | return $self->send_request( { | 
| 322 |  |  |  |  |  |  | type	=> 'GET', | 
| 323 |  |  |  |  |  |  | path	=> '/shipping/job/', | 
| 324 |  |  |  |  |  |  | header	=> { | 
| 325 |  |  |  |  |  |  | Accept => 'application/json', | 
| 326 |  |  |  |  |  |  | } | 
| 327 |  |  |  |  |  |  | } ); | 
| 328 |  |  |  |  |  |  | } | 
| 329 |  |  |  |  |  |  |  | 
| 330 |  |  |  |  |  |  | =head2  get_labels_for_job( $id, $format ) | 
| 331 |  |  |  |  |  |  |  | 
| 332 |  |  |  |  |  |  | Retrieves all labels for a given job id | 
| 333 |  |  |  |  |  |  |  | 
| 334 |  |  |  |  |  |  | $labels = $dpd->get_labels_for_job( $id, $format ); | 
| 335 |  |  |  |  |  |  |  | 
| 336 |  |  |  |  |  |  | =cut | 
| 337 |  |  |  |  |  |  | sub get_labels_for_job | 
| 338 |  |  |  |  |  |  | { | 
| 339 | 0 |  |  | 0 | 1 | 0 | my ( $self, $id, $format ) = @_; | 
| 340 | 0 | 0 | 0 |  |  | 0 | $self->errstr( "No id provided" ) and return unless $id; | 
| 341 | 0 | 0 | 0 |  |  | 0 | $self->errstr( "No format provided" ) and return unless $format; | 
| 342 | 0 |  |  |  |  | 0 | return $self->send_request( { | 
| 343 |  |  |  |  |  |  | path	=> "/shipping/job/$id/label", | 
| 344 |  |  |  |  |  |  | header	=> { | 
| 345 |  |  |  |  |  |  | Accept => $format, | 
| 346 |  |  |  |  |  |  | } | 
| 347 |  |  |  |  |  |  | } ); | 
| 348 |  |  |  |  |  |  | } | 
| 349 |  |  |  |  |  |  |  | 
| 350 |  |  |  |  |  |  |  | 
| 351 |  |  |  |  |  |  | =head2 get_shipments( \%search_params ) | 
| 352 |  |  |  |  |  |  |  | 
| 353 |  |  |  |  |  |  | Retrieves a full list of shipments meeting the search criteria and/or collection date. If no URL parameters are set the default settings brings back the first 100 shipments found. | 
| 354 |  |  |  |  |  |  |  | 
| 355 |  |  |  |  |  |  | $shipments = $self->get_shipments( { | 
| 356 |  |  |  |  |  |  | collectionDate => $date, | 
| 357 |  |  |  |  |  |  | searchCriterea => 'foo', | 
| 358 |  |  |  |  |  |  | searchPage     => 1, | 
| 359 |  |  |  |  |  |  | searchPageSize => 20, | 
| 360 |  |  |  |  |  |  | useTemplate    => false, | 
| 361 |  |  |  |  |  |  | }); | 
| 362 |  |  |  |  |  |  | =cut | 
| 363 |  |  |  |  |  |  | sub get_shipments | 
| 364 |  |  |  |  |  |  | { | 
| 365 | 0 |  |  | 0 | 1 | 0 | my ( $self, $params ) = @_; | 
| 366 | 0 |  |  |  |  | 0 | my $path = '/shipping/shipment/'; | 
| 367 | 0 | 0 |  |  |  | 0 | $path .= '?' . $self->_to_query_params($params) if $params; | 
| 368 | 0 |  |  |  |  | 0 | return $self->send_request( { | 
| 369 |  |  |  |  |  |  | path => $path, | 
| 370 |  |  |  |  |  |  | } ); | 
| 371 |  |  |  |  |  |  |  | 
| 372 |  |  |  |  |  |  | } | 
| 373 |  |  |  |  |  |  |  | 
| 374 |  |  |  |  |  |  | =head2 get_shipment( $id ) | 
| 375 |  |  |  |  |  |  |  | 
| 376 |  |  |  |  |  |  | Retrieves all shipment information associated with a shipment id | 
| 377 |  |  |  |  |  |  |  | 
| 378 |  |  |  |  |  |  | $shipment = $dpd->get_shipment( $id ); | 
| 379 |  |  |  |  |  |  |  | 
| 380 |  |  |  |  |  |  | =cut | 
| 381 |  |  |  |  |  |  | sub get_shipment | 
| 382 |  |  |  |  |  |  | { | 
| 383 | 0 |  |  | 0 | 1 | 0 | my ( $self, $id ) = @_; | 
| 384 | 0 | 0 | 0 |  |  | 0 | $self->errstr( "No id provided" ) and return unless $id; | 
| 385 | 0 |  |  |  |  | 0 | return $self->send_request( { | 
| 386 |  |  |  |  |  |  | path => "/shipping/shipment/$id/", | 
| 387 |  |  |  |  |  |  | } ); | 
| 388 |  |  |  |  |  |  | } | 
| 389 |  |  |  |  |  |  |  | 
| 390 |  |  |  |  |  |  | =head2 get_international_invoice( $shipment_id ) | 
| 391 |  |  |  |  |  |  |  | 
| 392 |  |  |  |  |  |  | Creates and returns an international invoice associated with the given shipment id. | 
| 393 |  |  |  |  |  |  |  | 
| 394 |  |  |  |  |  |  | $invoice = $dpd->get_international_invoice( $shipment_id ); | 
| 395 |  |  |  |  |  |  |  | 
| 396 |  |  |  |  |  |  | =cut | 
| 397 |  |  |  |  |  |  | sub get_international_invoice | 
| 398 |  |  |  |  |  |  | { | 
| 399 | 0 |  |  | 0 | 1 | 0 | my ( $self, $id ) = @_; | 
| 400 | 0 | 0 | 0 |  |  | 0 | $self->errstr( "No shipment ID provided" ) and return unless $id; | 
| 401 | 0 |  |  |  |  | 0 | return $self->send_request( { | 
| 402 |  |  |  |  |  |  | path	=> "/shipping/shipment/$id/invoice/", | 
| 403 |  |  |  |  |  |  | header	=> { | 
| 404 |  |  |  |  |  |  | Accept => 'text/html', | 
| 405 |  |  |  |  |  |  | }, | 
| 406 |  |  |  |  |  |  | raw_result => 1, | 
| 407 |  |  |  |  |  |  | } ); | 
| 408 |  |  |  |  |  |  | } | 
| 409 |  |  |  |  |  |  |  | 
| 410 |  |  |  |  |  |  | =head2 get_unprinted_labels( $date, $format ) | 
| 411 |  |  |  |  |  |  |  | 
| 412 |  |  |  |  |  |  | Retrieves all labels that have not already been printed for a particular collection date. | 
| 413 |  |  |  |  |  |  |  | 
| 414 |  |  |  |  |  |  | $labels = $dpd->get_unprinted_labels( $date, $format ); | 
| 415 |  |  |  |  |  |  |  | 
| 416 |  |  |  |  |  |  | =cut | 
| 417 |  |  |  |  |  |  | sub get_unprinted_labels | 
| 418 |  |  |  |  |  |  | { | 
| 419 | 0 |  |  | 0 | 1 | 0 | my ( $self, $date, $format ) = @_; | 
| 420 | 0 | 0 | 0 |  |  | 0 | $self->errstr( "No date" ) and return unless $date; | 
| 421 | 0 |  |  |  |  | 0 | return $self->send_request( { | 
| 422 |  |  |  |  |  |  | path	=> "/shipping/shipment/_/label/?collectionDate=$date", | 
| 423 |  |  |  |  |  |  | header	=> { | 
| 424 |  |  |  |  |  |  | Accept => $format, | 
| 425 |  |  |  |  |  |  | } | 
| 426 |  |  |  |  |  |  | } ); | 
| 427 |  |  |  |  |  |  | } | 
| 428 |  |  |  |  |  |  |  | 
| 429 |  |  |  |  |  |  | =head2 delete_shipment( $id ) | 
| 430 |  |  |  |  |  |  |  | 
| 431 |  |  |  |  |  |  | Delete a shipment | 
| 432 |  |  |  |  |  |  |  | 
| 433 |  |  |  |  |  |  | $dpd->delete_shipment( $id ); | 
| 434 |  |  |  |  |  |  |  | 
| 435 |  |  |  |  |  |  | =cut | 
| 436 |  |  |  |  |  |  | sub delete_shipment | 
| 437 |  |  |  |  |  |  | { | 
| 438 | 0 |  |  | 0 | 1 | 0 | my ( $self, $id ) = @_; | 
| 439 | 0 | 0 | 0 |  |  | 0 | $self->errstr( "No id provided" ) and return unless $id; | 
| 440 | 0 |  |  |  |  | 0 | return $self->send_request( { | 
| 441 |  |  |  |  |  |  | type	=> 'DELETE', | 
| 442 |  |  |  |  |  |  | path	=> "/shipping/shipment/$id/", | 
| 443 |  |  |  |  |  |  | } ); | 
| 444 |  |  |  |  |  |  | } | 
| 445 |  |  |  |  |  |  |  | 
| 446 |  |  |  |  |  |  | =head2 change_collection_date( $id, $date ) | 
| 447 |  |  |  |  |  |  |  | 
| 448 |  |  |  |  |  |  | Update collection date for a shipment | 
| 449 |  |  |  |  |  |  |  | 
| 450 |  |  |  |  |  |  | $dpd->change_collection_date( $id, $date ); | 
| 451 |  |  |  |  |  |  |  | 
| 452 |  |  |  |  |  |  | =cut | 
| 453 |  |  |  |  |  |  | sub change_collection_date | 
| 454 |  |  |  |  |  |  | { | 
| 455 | 0 |  |  | 0 | 1 | 0 | my ( $self, $id, $date ) = @_; | 
| 456 | 0 | 0 | 0 |  |  | 0 | $self->errstr( "No id provided" ) and return unless $id; | 
| 457 | 0 | 0 | 0 |  |  | 0 | $self->errstr( "No date provided" ) and return unless $date; | 
| 458 | 0 |  |  |  |  | 0 | return $self->send_request( { | 
| 459 |  |  |  |  |  |  | type	=> 'PUT', | 
| 460 |  |  |  |  |  |  | path	=> "/shipping/shipment/$id/?action=ChangeCollectionDate", | 
| 461 |  |  |  |  |  |  | data	=> { | 
| 462 |  |  |  |  |  |  | collectionDate => $date, | 
| 463 |  |  |  |  |  |  | } | 
| 464 |  |  |  |  |  |  | } ); | 
| 465 |  |  |  |  |  |  | } | 
| 466 |  |  |  |  |  |  |  | 
| 467 |  |  |  |  |  |  | =head2 void_shipment | 
| 468 |  |  |  |  |  |  |  | 
| 469 |  |  |  |  |  |  | Update status of shipment to void. | 
| 470 |  |  |  |  |  |  |  | 
| 471 |  |  |  |  |  |  | $dpd->void_shipment( $id ); | 
| 472 |  |  |  |  |  |  |  | 
| 473 |  |  |  |  |  |  | =cut | 
| 474 |  |  |  |  |  |  | sub void_shipment | 
| 475 |  |  |  |  |  |  | { | 
| 476 | 0 |  |  | 0 | 1 | 0 | my ( $self, $id ) = @_; | 
| 477 | 0 | 0 | 0 |  |  | 0 | $self->errstr( "No id provided" ) and return unless $id; | 
| 478 | 0 |  |  |  |  | 0 | return $self->send_request( { | 
| 479 |  |  |  |  |  |  | type	=> 'PUT', | 
| 480 |  |  |  |  |  |  | path	=> "/shipping/shipment/$id/?action=Void", | 
| 481 |  |  |  |  |  |  | data	=> { | 
| 482 |  |  |  |  |  |  | isVoided => 'true', | 
| 483 |  |  |  |  |  |  | }, | 
| 484 |  |  |  |  |  |  | } ); | 
| 485 |  |  |  |  |  |  | } | 
| 486 |  |  |  |  |  |  |  | 
| 487 |  |  |  |  |  |  | =head2 create_manifest | 
| 488 |  |  |  |  |  |  |  | 
| 489 |  |  |  |  |  |  | Tag all non manifested shipments for a collection date with a new generated manifest id. | 
| 490 |  |  |  |  |  |  |  | 
| 491 |  |  |  |  |  |  | $manifest = $dpd->create_manifest( $date ); | 
| 492 |  |  |  |  |  |  |  | 
| 493 |  |  |  |  |  |  | =cut | 
| 494 |  |  |  |  |  |  | sub create_manifest | 
| 495 |  |  |  |  |  |  | { | 
| 496 | 0 |  |  | 0 | 1 | 0 | my ( $self, $date ) = @_; | 
| 497 | 0 | 0 | 0 |  |  | 0 | $self->errstr( "No date provided" ) and return unless $date; | 
| 498 | 0 |  |  |  |  | 0 | return $self->send_request( { | 
| 499 |  |  |  |  |  |  | type	=> 'POST', | 
| 500 |  |  |  |  |  |  | path	=> '/shipping/manifest/', | 
| 501 |  |  |  |  |  |  | data	=> { | 
| 502 |  |  |  |  |  |  | collectionDate => $date, | 
| 503 |  |  |  |  |  |  | }, | 
| 504 |  |  |  |  |  |  | } ); | 
| 505 |  |  |  |  |  |  | } | 
| 506 |  |  |  |  |  |  |  | 
| 507 |  |  |  |  |  |  | =head2 get_manifest_by_date( $date ) | 
| 508 |  |  |  |  |  |  |  | 
| 509 |  |  |  |  |  |  | Retrieves all the manifests and the core manifest information for a particular collection date. | 
| 510 |  |  |  |  |  |  |  | 
| 511 |  |  |  |  |  |  | $manifests = $dpd->get_manifest_by_date( $date ); | 
| 512 |  |  |  |  |  |  |  | 
| 513 |  |  |  |  |  |  | =cut | 
| 514 |  |  |  |  |  |  | sub get_manifest_by_date | 
| 515 |  |  |  |  |  |  | { | 
| 516 | 0 |  |  | 0 | 1 | 0 | my ( $self, $date ) = @_; | 
| 517 | 0 |  |  |  |  | 0 | return $self->send_request( { | 
| 518 |  |  |  |  |  |  | path	=> "/shipping/manifest/?collectionDate=$date", | 
| 519 |  |  |  |  |  |  | } ); | 
| 520 |  |  |  |  |  |  | } | 
| 521 |  |  |  |  |  |  |  | 
| 522 |  |  |  |  |  |  | =head2 get_manifest_by_id( $id ) | 
| 523 |  |  |  |  |  |  |  | 
| 524 |  |  |  |  |  |  | Get printable manifest by its associated manifest id | 
| 525 |  |  |  |  |  |  |  | 
| 526 |  |  |  |  |  |  | $manifest = get_manifest_by_id( $id ); | 
| 527 |  |  |  |  |  |  | =cut | 
| 528 |  |  |  |  |  |  | sub get_manifest_by_id | 
| 529 |  |  |  |  |  |  | { | 
| 530 | 0 |  |  | 0 | 1 | 0 | my ( $self, $id ) = @_; | 
| 531 | 0 | 0 | 0 |  |  | 0 | $self->errstr( "No id provided" ) and return unless $id; | 
| 532 | 0 |  |  |  |  | 0 | return $self->send_request( { | 
| 533 |  |  |  |  |  |  | path	=> "/shipping/manifest/$id", | 
| 534 |  |  |  |  |  |  | header	=> { | 
| 535 |  |  |  |  |  |  | Accept => 'text/html', | 
| 536 |  |  |  |  |  |  | }, | 
| 537 |  |  |  |  |  |  | } ); | 
| 538 |  |  |  |  |  |  | } | 
| 539 |  |  |  |  |  |  |  | 
| 540 |  |  |  |  |  |  |  | 
| 541 |  |  |  |  |  |  | =head1 INTERNAL METHODS | 
| 542 |  |  |  |  |  |  |  | 
| 543 |  |  |  |  |  |  | =cut | 
| 544 |  |  |  |  |  |  |  | 
| 545 |  |  |  |  |  |  | =head2 _to_query_params | 
| 546 |  |  |  |  |  |  |  | 
| 547 |  |  |  |  |  |  | Recursiverly converts hash of hashes into query string for http request | 
| 548 |  |  |  |  |  |  |  | 
| 549 |  |  |  |  |  |  | =cut | 
| 550 |  |  |  |  |  |  | sub _to_query_params | 
| 551 |  |  |  |  |  |  | { | 
| 552 | 1 |  |  | 1 |  | 2 | my ( $self, $data ) = @_; | 
| 553 | 1 |  |  |  |  | 3 | my @params; | 
| 554 |  |  |  |  |  |  | my $sub; | 
| 555 |  |  |  |  |  |  | $sub = sub { | 
| 556 | 5 |  |  | 5 |  | 6 | my ( $name, $data ) = @_; | 
| 557 | 5 |  |  |  |  | 12 | for ( keys %$data ) | 
| 558 |  |  |  |  |  |  | { | 
| 559 | 24 | 100 |  |  |  | 42 | if ( ref $data->{$_} eq 'HASH' ) | 
| 560 |  |  |  |  |  |  | { | 
| 561 | 4 |  |  |  |  | 14 | $sub->( "$name.$_", $data->{$_} ); | 
| 562 |  |  |  |  |  |  | } | 
| 563 |  |  |  |  |  |  | else | 
| 564 |  |  |  |  |  |  | { | 
| 565 | 20 |  |  |  |  | 87 | push @params, { key => "$name.$_", value => $data->{$_} }; | 
| 566 |  |  |  |  |  |  | } | 
| 567 |  |  |  |  |  |  | } | 
| 568 | 1 |  |  |  |  | 7 | }; | 
| 569 | 1 |  |  |  |  | 2 | $sub->( '', $data); | 
| 570 | 1 |  |  |  |  | 2 | my $query; | 
| 571 | 1 |  |  |  |  | 2 | for ( @params ) | 
| 572 |  |  |  |  |  |  | { | 
| 573 | 20 |  |  |  |  | 240 | $_->{key} =~ s/^\.//; | 
| 574 | 20 |  |  |  |  | 54 | $query .= $_->{key} . '='.  uri_escape( $_->{value} ) . '&'; | 
| 575 |  |  |  |  |  |  | } | 
| 576 | 1 |  |  |  |  | 10 | $query =~ s/&$//; | 
| 577 | 1 |  |  |  |  | 7 | return $query; | 
| 578 |  |  |  |  |  |  | } | 
| 579 |  |  |  |  |  |  |  | 
| 580 |  |  |  |  |  |  | =head2 send_request( \%args ) | 
| 581 |  |  |  |  |  |  |  | 
| 582 |  |  |  |  |  |  | Constructs and sends authenticated HTTP API request | 
| 583 |  |  |  |  |  |  |  | 
| 584 |  |  |  |  |  |  | $result = $dpd->send_request( { | 
| 585 |  |  |  |  |  |  | type    => 'POST',                    # HTTP request type defaults to GET | 
| 586 |  |  |  |  |  |  | path    => "/path/to/service",        # Path to service | 
| 587 |  |  |  |  |  |  | data    => {                          # hashref of data for POST/PUT requests, converted to JSON for sending | 
| 588 |  |  |  |  |  |  | key1 => 'value1', | 
| 589 |  |  |  |  |  |  | key2 => 'value2', | 
| 590 |  |  |  |  |  |  | }, | 
| 591 |  |  |  |  |  |  | content_type => 'appilcation/json',   # defaults to application/json | 
| 592 |  |  |  |  |  |  | header  => {                          # hashref of additional headers | 
| 593 |  |  |  |  |  |  | Accept => $format, | 
| 594 |  |  |  |  |  |  | } | 
| 595 |  |  |  |  |  |  |  | 
| 596 |  |  |  |  |  |  | } ); | 
| 597 |  |  |  |  |  |  |  | 
| 598 |  |  |  |  |  |  | =cut | 
| 599 |  |  |  |  |  |  | sub send_request | 
| 600 |  |  |  |  |  |  | { | 
| 601 | 6 |  |  | 6 | 1 | 17 | my ( $self, $args ) = @_; | 
| 602 | 6 |  | 100 |  |  | 34 | my $type = $args->{type} || 'GET'; | 
| 603 | 6 |  |  |  |  | 69 | my $req = HTTP::Request->new($type => $self->url . $args->{path} ); | 
| 604 |  |  |  |  |  |  | #Required headers | 
| 605 | 6 |  |  |  |  | 12021 | $req->header( Host => $self->host ); | 
| 606 | 6 |  |  |  |  | 423 | $req->protocol('HTTP/1.1'); | 
| 607 | 6 |  |  |  |  | 72 | $req->header( GEOClient =>  $self->geoClient ); | 
| 608 | 6 | 50 |  |  |  | 300 | $req->header( GEOSession =>  $self->geoSession ) if $self->geoSession; | 
| 609 |  |  |  |  |  |  |  | 
| 610 |  |  |  |  |  |  | #Per request overridable | 
| 611 | 6 |  | 50 |  |  | 49 | $req->content_type( $args->{content_type} || 'application/json' ); | 
| 612 | 6 |  | 50 |  |  | 139 | $req->header( Accept => $args->{header}->{Accept} || 'application/json' ); | 
| 613 |  |  |  |  |  |  |  | 
| 614 |  |  |  |  |  |  | #Custom headers | 
| 615 | 6 |  |  |  |  | 223 | for ( keys %{ $args->{header} } ) | 
|  | 6 |  |  |  |  | 28 |  | 
| 616 |  |  |  |  |  |  | { | 
| 617 | 1 |  |  |  |  | 6 | $req->header( $_ => $args->{header}->{$_} ); | 
| 618 |  |  |  |  |  |  | } | 
| 619 |  |  |  |  |  |  |  | 
| 620 | 6 | 100 | 66 |  |  | 77 | if ( $args->{data} and $type =~ /^(POST|PUT)$/ ) | 
| 621 |  |  |  |  |  |  | { | 
| 622 | 1 |  |  |  |  | 10 | my $content = to_json( $args->{data} ); | 
| 623 |  |  |  |  |  |  | #hacky translation to correct representation of null and boolean values | 
| 624 | 1 |  |  |  |  | 168 | $content =~ s/"null"/null/gi; | 
| 625 | 1 |  |  |  |  | 94 | $content =~ s/"false"/false/gi; | 
| 626 | 1 |  |  |  |  | 131 | $content =~ s/"true"/true/gi; | 
| 627 | 1 |  |  |  |  | 12 | $req->content( $content ); | 
| 628 |  |  |  |  |  |  | } | 
| 629 |  |  |  |  |  |  |  | 
| 630 |  |  |  |  |  |  | #Send request | 
| 631 | 6 | 50 |  |  |  | 69 | warn $req->as_string if $self->debug; | 
| 632 | 6 |  |  |  |  | 54 | my $response = $self->ua->request($req); | 
| 633 | 6 | 50 |  |  |  | 12008 | warn $response->as_string if $self->debug; | 
| 634 | 6 | 50 |  |  |  | 17 | if ( $response->code == 200 ) | 
| 635 |  |  |  |  |  |  | { | 
| 636 | 0 |  |  |  |  | 0 | my $result; | 
| 637 |  |  |  |  |  |  | #FIXME assumes JSON | 
| 638 | 0 |  |  |  |  | 0 | eval{ $result = JSON->new->utf8->decode($response->content) }; | 
|  | 0 |  |  |  |  | 0 |  | 
| 639 | 0 | 0 | 0 |  |  | 0 | $self->errstr("Server response was invalid\n") and return if $@ and ! $args->{raw_result}; | 
|  |  |  | 0 |  |  |  |  | 
| 640 | 0 | 0 |  |  |  | 0 | if ( $result->{error} ) | 
| 641 |  |  |  |  |  |  | { | 
| 642 | 0 | 0 |  |  |  | 0 | my $error = ref $result->{error} eq 'ARRAY' ? $result->{error}->[0] : $result->{error}; | 
| 643 | 0 |  | 0 |  |  | 0 | my $error_type = $error->{errorType} || ''; | 
| 644 | 0 |  | 0 |  |  | 0 | my $error_obj = $error->{obj} || ''; | 
| 645 | 0 |  | 0 |  |  | 0 | my $error_code = $error->{errorCode} || ''; | 
| 646 | 0 |  | 0 |  |  | 0 | my $error_message = $error->{errorMessage} || ''; | 
| 647 | 0 |  |  |  |  | 0 | $self->errstr( "$error_type error : $error_obj :  $error_code : $error_message\n" ); | 
| 648 | 0 |  |  |  |  | 0 | return; | 
| 649 |  |  |  |  |  |  | } | 
| 650 | 0 |  |  |  |  | 0 | $result->{response} = $response; | 
| 651 | 0 | 0 |  |  |  | 0 | if ( $args->{raw_result} ) | 
| 652 |  |  |  |  |  |  | { | 
| 653 | 0 |  |  |  |  | 0 | $result->{data} = $response->content; | 
| 654 |  |  |  |  |  |  | } | 
| 655 | 0 |  |  |  |  | 0 | return $result->{data}; | 
| 656 |  |  |  |  |  |  | } | 
| 657 |  |  |  |  |  |  | else | 
| 658 |  |  |  |  |  |  | { | 
| 659 | 6 |  |  |  |  | 83 | $self->errstr('API communication error: ' . $args->{path} . ': ' . $response->status_line . "\n\n\n\n"); | 
| 660 | 6 |  |  |  |  | 140 | return; | 
| 661 |  |  |  |  |  |  | } | 
| 662 |  |  |  |  |  |  | } | 
| 663 |  |  |  |  |  |  |  | 
| 664 |  |  |  |  |  |  | 1; | 
| 665 |  |  |  |  |  |  |  | 
| 666 |  |  |  |  |  |  | =head1 SOURCE CODE | 
| 667 |  |  |  |  |  |  |  | 
| 668 |  |  |  |  |  |  | The source code for this module is held in a public git repository on Github : https://github.com/pryanet/WebService-DPD-API | 
| 669 |  |  |  |  |  |  |  | 
| 670 |  |  |  |  |  |  | =head1 LICENSE AND COPYRIGHT | 
| 671 |  |  |  |  |  |  |  | 
| 672 |  |  |  |  |  |  | Copyright (c) 2014 Richard Newsham, Pryanet Ltd | 
| 673 |  |  |  |  |  |  |  | 
| 674 |  |  |  |  |  |  | This library is free software; you can redistribute it and/or | 
| 675 |  |  |  |  |  |  | modify it under the same terms as Perl itself. | 
| 676 |  |  |  |  |  |  |  | 
| 677 |  |  |  |  |  |  | =head1 BUGS AND LIMITATIONS | 
| 678 |  |  |  |  |  |  |  | 
| 679 |  |  |  |  |  |  | See rt.cpan.org for current bugs, if any. | 
| 680 |  |  |  |  |  |  |  | 
| 681 |  |  |  |  |  |  | =head1 INCOMPATIBILITIES | 
| 682 |  |  |  |  |  |  |  | 
| 683 |  |  |  |  |  |  | None known. | 
| 684 |  |  |  |  |  |  |  | 
| 685 |  |  |  |  |  |  | =head1 DEPENDENCIES | 
| 686 |  |  |  |  |  |  |  | 
| 687 |  |  |  |  |  |  | Carp | 
| 688 |  |  |  |  |  |  | Moo | 
| 689 |  |  |  |  |  |  | LWP::UserAgent | 
| 690 |  |  |  |  |  |  | HTTP::Request::Common | 
| 691 |  |  |  |  |  |  | URI::Escape | 
| 692 |  |  |  |  |  |  | Data::Dumper | 
| 693 |  |  |  |  |  |  | JSON | 
| 694 |  |  |  |  |  |  | MIME::Base64 | 
| 695 |  |  |  |  |  |  | namespace::clean | 
| 696 |  |  |  |  |  |  |  | 
| 697 |  |  |  |  |  |  | =cut |