active_shipping 1.0.0.pre1 → 1.0.0.pre2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data.tar.gz.sig +0 -0
- data/CONTRIBUTING.md +43 -30
- data/README.md +4 -5
- data/lib/active_shipping.rb +3 -0
- data/lib/active_shipping/carrier.rb +86 -16
- data/lib/active_shipping/carriers/canada_post.rb +70 -65
- data/lib/active_shipping/carriers/kunaki.rb +22 -31
- data/lib/active_shipping/carriers/ups.rb +224 -205
- data/lib/active_shipping/version.rb +1 -1
- metadata +29 -31
- metadata.gz.sig +0 -0
@@ -1,5 +1,3 @@
|
|
1
|
-
require 'builder'
|
2
|
-
|
3
1
|
module ActiveShipping
|
4
2
|
class Kunaki < Carrier
|
5
3
|
self.retry_safe = true
|
@@ -77,31 +75,30 @@ module ActiveShipping
|
|
77
75
|
private
|
78
76
|
|
79
77
|
def build_request(destination, options)
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
end
|
78
|
+
country = COUNTRIES[destination.country_code]
|
79
|
+
state_province = %w(US CA).include?(destination.country_code.to_s) ? destination.state : ''
|
80
|
+
|
81
|
+
builder = Nokogiri::XML::Builder.new do |xml|
|
82
|
+
xml.ShippingOptions do
|
83
|
+
xml.AddressInfo do
|
84
|
+
xml.Country(country)
|
85
|
+
xml.State_Province(state_province)
|
86
|
+
xml.PostalCode(destination.zip)
|
87
|
+
end
|
91
88
|
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
89
|
+
options[:items].each do |item|
|
90
|
+
xml.Product do
|
91
|
+
xml.ProductId(item[:sku])
|
92
|
+
xml.Quantity(item[:quantity])
|
93
|
+
end
|
96
94
|
end
|
97
95
|
end
|
98
96
|
end
|
99
|
-
|
97
|
+
builder.to_xml
|
100
98
|
end
|
101
99
|
|
102
100
|
def commit(origin, destination, options)
|
103
101
|
request = build_request(destination, options)
|
104
|
-
|
105
102
|
response = parse( ssl_post(URL, request, "Content-Type" => "text/xml") )
|
106
103
|
|
107
104
|
RateResponse.new(success?(response), message_from(response), response,
|
@@ -126,15 +123,15 @@ module ActiveShipping
|
|
126
123
|
response = {}
|
127
124
|
response["Options"] = []
|
128
125
|
|
129
|
-
document =
|
126
|
+
document = Nokogiri.XML(sanitize(xml))
|
130
127
|
|
131
|
-
response["ErrorCode"] =
|
132
|
-
response["ErrorText"] =
|
128
|
+
response["ErrorCode"] = document.at('/Response/ErrorCode').text
|
129
|
+
response["ErrorText"] = document.at('/Response/ErrorText').text
|
133
130
|
|
134
|
-
document.
|
131
|
+
document.xpath("Response/Option").each do |node|
|
135
132
|
rate = {}
|
136
|
-
rate["Description"] =
|
137
|
-
rate["Price"] =
|
133
|
+
rate["Description"] = node.at("Description").text
|
134
|
+
rate["Price"] = node.at("Price").text
|
138
135
|
response["Options"] << rate
|
139
136
|
end
|
140
137
|
response
|
@@ -147,12 +144,6 @@ module ActiveShipping
|
|
147
144
|
result
|
148
145
|
end
|
149
146
|
|
150
|
-
def parse_child_text(parent, name)
|
151
|
-
if element = parent.elements[name]
|
152
|
-
element.text
|
153
|
-
end
|
154
|
-
end
|
155
|
-
|
156
147
|
def success?(response)
|
157
148
|
response["ErrorCode"] == "0"
|
158
149
|
end
|
@@ -179,66 +179,68 @@ module ActiveShipping
|
|
179
179
|
end
|
180
180
|
|
181
181
|
def build_access_request
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
182
|
+
xml_builder = Nokogiri::XML::Builder.new do |xml|
|
183
|
+
xml.AccessRequest do
|
184
|
+
xml.AccessLicenseNumber(@options[:key])
|
185
|
+
xml.UserId(@options[:password])
|
186
|
+
xml.Password(@options[:password])
|
187
|
+
end
|
186
188
|
end
|
187
|
-
|
189
|
+
xml_builder.to_xml
|
188
190
|
end
|
189
191
|
|
190
192
|
def build_rate_request(origin, destination, packages, options = {})
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
pickup_type = options[:pickup_type] || :daily_pickup
|
193
|
+
xml_builder = Nokogiri::XML::Builder.new do |xml|
|
194
|
+
xml.RatingServiceSelectionRequest do
|
195
|
+
xml.Request do
|
196
|
+
xml.RequestAction('Rate')
|
197
|
+
xml.RequestOption('Shop')
|
198
|
+
# not implemented: 'Rate' RequestOption to specify a single service query
|
199
|
+
# xml.RequestOption((options[:service].nil? or options[:service] == :all) ? 'Shop' : 'Rate')
|
200
|
+
end
|
201
201
|
|
202
|
-
|
203
|
-
pickup_type_node << XmlNode.new('Code', PICKUP_CODES[pickup_type])
|
204
|
-
# not implemented: PickupType/PickupDetails element
|
205
|
-
end
|
206
|
-
cc = options[:customer_classification] || DEFAULT_CUSTOMER_CLASSIFICATIONS[pickup_type]
|
207
|
-
root_node << XmlNode.new('CustomerClassification') do |cc_node|
|
208
|
-
cc_node << XmlNode.new('Code', CUSTOMER_CLASSIFICATIONS[cc])
|
209
|
-
end
|
202
|
+
pickup_type = options[:pickup_type] || :daily_pickup
|
210
203
|
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
shipment << build_location_node('ShipTo', destination, options)
|
215
|
-
if options[:shipper] and options[:shipper] != origin
|
216
|
-
shipment << build_location_node('ShipFrom', origin, options)
|
204
|
+
xml.PickupType do
|
205
|
+
xml.Code(PICKUP_CODES[pickup_type])
|
206
|
+
# not implemented: PickupType/PickupDetails element
|
217
207
|
end
|
218
208
|
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
# * Shipment/PickupDate element
|
223
|
-
# * Shipment/ScheduledDeliveryDate element
|
224
|
-
# * Shipment/ScheduledDeliveryTime element
|
225
|
-
# * Shipment/AlternateDeliveryTime element
|
226
|
-
# * Shipment/DocumentsOnly element
|
227
|
-
|
228
|
-
packages.each do |package|
|
229
|
-
options[:imperial] ||= IMPERIAL_COUNTRIES.include?(origin.country_code(:alpha2))
|
230
|
-
shipment << build_package_node(package, options)
|
209
|
+
cc = options[:customer_classification] || DEFAULT_CUSTOMER_CLASSIFICATIONS[pickup_type]
|
210
|
+
xml.CustomerClassification do
|
211
|
+
xml.Code(CUSTOMER_CLASSIFICATIONS[cc])
|
231
212
|
end
|
232
213
|
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
214
|
+
xml.Shipment do
|
215
|
+
# not implemented: Shipment/Description element
|
216
|
+
build_location_node(xml, 'Shipper', (options[:shipper] || origin), options)
|
217
|
+
build_location_node(xml, 'ShipTo', destination, options)
|
218
|
+
build_location_node(xml, 'ShipFrom', origin, options) if options[:shipper] && options[:shipper] != origin
|
219
|
+
|
220
|
+
# not implemented: * Shipment/ShipmentWeight element
|
221
|
+
# * Shipment/ReferenceNumber element
|
222
|
+
# * Shipment/Service element
|
223
|
+
# * Shipment/PickupDate element
|
224
|
+
# * Shipment/ScheduledDeliveryDate element
|
225
|
+
# * Shipment/ScheduledDeliveryTime element
|
226
|
+
# * Shipment/AlternateDeliveryTime element
|
227
|
+
# * Shipment/DocumentsOnly element
|
228
|
+
|
229
|
+
Array(packages).each do |package|
|
230
|
+
options[:imperial] ||= IMPERIAL_COUNTRIES.include?(origin.country_code(:alpha2))
|
231
|
+
build_package_node(xml, package, options)
|
232
|
+
end
|
233
|
+
|
234
|
+
# not implemented: * Shipment/ShipmentServiceOptions element
|
235
|
+
if options[:origin_account]
|
236
|
+
xml.RateInformation do
|
237
|
+
xml.NegotiatedRatesIndicator
|
238
|
+
end
|
237
239
|
end
|
238
240
|
end
|
239
241
|
end
|
240
242
|
end
|
241
|
-
|
243
|
+
xml_builder.to_xml
|
242
244
|
end
|
243
245
|
|
244
246
|
# Build XML node to request a shipping label for the given packages.
|
@@ -257,222 +259,239 @@ module ActiveShipping
|
|
257
259
|
def build_shipment_request(origin, destination, packages, options = {})
|
258
260
|
# There are a lot of unimplemented elements, documenting all of them
|
259
261
|
# wouldprobably be unhelpful.
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
262
|
+
xml_builder = Nokogiri::XML::Builder.new do |xml|
|
263
|
+
xml.ShipmentConfirmRequest do
|
264
|
+
xml.Request do
|
265
|
+
# Required element and the text must be "ShipConfirm"
|
266
|
+
xml.RequestAction('ShipConfirm')
|
267
|
+
# Required element cotnrols level of address validation.
|
268
|
+
xml.RequestOption(options[:optional_processing] || 'validate')
|
269
|
+
# Optional element to identify transactions between client and server.
|
270
|
+
if options[:customer_context]
|
271
|
+
xml.TransactionReference do
|
272
|
+
xml.CustomerContext(options[:customer_context])
|
273
|
+
end
|
271
274
|
end
|
272
275
|
end
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
end
|
280
|
-
# Required element. The delivery destination.
|
281
|
-
shipment << build_location_node('ShipTo', destination, {})
|
282
|
-
# Required element. The company whose account is responsible for the label(s).
|
283
|
-
shipment << build_location_node('Shipper', options[:shipper] || origin, {})
|
284
|
-
# Required if pickup is different different from shipper's address.
|
285
|
-
if options[:ship_from]
|
286
|
-
shipment << build_location_node('ShipFrom', options[:ship_from], {})
|
287
|
-
end
|
288
|
-
# Optional.
|
289
|
-
if options[:saturday_delivery]
|
290
|
-
shipment << XmlNode.new('ShipmentServiceOptions') do |opts|
|
291
|
-
opts << XmlNode.new('SaturdayDelivery')
|
276
|
+
|
277
|
+
xml.Shipment do
|
278
|
+
# Required element.
|
279
|
+
xml.Service do
|
280
|
+
xml.Code(options[:service_code] || '14')
|
281
|
+
xml.Description(options[:service_description] || 'Next Day Air Early AM')
|
292
282
|
end
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
283
|
+
|
284
|
+
# Required element. The delivery destination.
|
285
|
+
build_location_node(xml, 'ShipTo', destination, {})
|
286
|
+
# Required element. The company whose account is responsible for the label(s).
|
287
|
+
build_location_node(xml, 'Shipper', options[:shipper] || origin, {})
|
288
|
+
# Required if pickup is different different from shipper's address.
|
289
|
+
build_location_node(xml, 'ShipFrom', options[:ship_from], {}) if options[:ship_from]
|
290
|
+
|
291
|
+
# Optional.
|
292
|
+
if options[:saturday_delivery]
|
293
|
+
xml.ShipmentServiceOptions do
|
294
|
+
xml.SaturdayDelivery
|
295
|
+
end
|
298
296
|
end
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
297
|
+
|
298
|
+
# Optional.
|
299
|
+
if options[:origin_account]
|
300
|
+
xml.RateInformation do
|
301
|
+
xml.NegotiatedRatesIndicator
|
302
|
+
end
|
305
303
|
end
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
bill << XmlNode.new('AccountNumber', options[:origin_account])
|
304
|
+
|
305
|
+
# Optional.
|
306
|
+
if options[:shipment] && options[:shipment][:reference_number]
|
307
|
+
xml.ReferenceNumber do
|
308
|
+
xml.Code(options[:shipment][:reference_number][:code] || "")
|
309
|
+
xml.Value(options[:shipment][:reference_number][:value])
|
313
310
|
end
|
314
311
|
end
|
312
|
+
|
313
|
+
# Conditionally required. Either this element or an ItemizedPaymentInformation
|
314
|
+
# is needed. However, only PaymentInformation is not implemented.
|
315
|
+
xml.PaymentInformation do
|
316
|
+
xml.Prepaid do
|
317
|
+
xml.BillShipper do
|
318
|
+
xml.AccountNumber(options[:origin_account])
|
319
|
+
end
|
320
|
+
end
|
321
|
+
end
|
322
|
+
|
323
|
+
# A request may specify multiple packages.
|
324
|
+
options[:imperial] ||= IMPERIAL_COUNTRIES.include?(origin.country_code(:alpha2))
|
325
|
+
Array(packages).each do |package|
|
326
|
+
build_package_node(xml, package, options)
|
327
|
+
end
|
315
328
|
end
|
316
|
-
|
317
|
-
options
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
end
|
328
|
-
specification << XmlNode.new('HTTPUserAgent', 'Mozilla/4.5') # hmmm
|
329
|
-
specification << XmlNode.new('LabelImageFormat', 'GIF') do |image_format|
|
330
|
-
image_format << XmlNode.new('Code', 'GIF')
|
329
|
+
|
330
|
+
# I don't know all of the options that UPS supports for labels
|
331
|
+
# so I'm going with something very simple for now.
|
332
|
+
xml.LabelSpecification do
|
333
|
+
xml.LabelPrintMethod do
|
334
|
+
xml.Code('GIF')
|
335
|
+
end
|
336
|
+
xml.HTTPUserAgent('Mozilla/4.5') # hmmm
|
337
|
+
xml.LabelImageFormat('GIF') do
|
338
|
+
xml.Code('GIF')
|
339
|
+
end
|
331
340
|
end
|
332
341
|
end
|
333
342
|
end
|
334
|
-
|
343
|
+
xml_builder.to_xml
|
335
344
|
end
|
336
345
|
|
337
346
|
def build_accept_request(digest, options = {})
|
338
|
-
|
339
|
-
|
340
|
-
|
347
|
+
xml_builder = Nokogiri::XML::Builder.new do |xml|
|
348
|
+
xml.ShipmentAcceptRequest do
|
349
|
+
xml.Request do
|
350
|
+
xml.RequestAction('ShipAccept')
|
351
|
+
end
|
352
|
+
xml.ShipmentDigest(digest)
|
341
353
|
end
|
342
|
-
root_node << XmlNode.new('ShipmentDigest', digest)
|
343
354
|
end
|
344
|
-
|
355
|
+
xml_builder.to_xml
|
345
356
|
end
|
346
357
|
|
347
358
|
def build_tracking_request(tracking_number, options = {})
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
359
|
+
xml_builder = Nokogiri::XML::Builder.new do |xml|
|
360
|
+
xml.TrackRequest do
|
361
|
+
xml.Request do
|
362
|
+
xml.RequestAction('Track')
|
363
|
+
xml.RequestOption('1')
|
364
|
+
end
|
365
|
+
xml.TrackingNumber(tracking_number.to_s)
|
352
366
|
end
|
353
|
-
root_node << XmlNode.new('TrackingNumber', tracking_number.to_s)
|
354
367
|
end
|
355
|
-
|
368
|
+
xml_builder.to_xml
|
356
369
|
end
|
357
370
|
|
358
|
-
def build_location_node(name, location, options = {})
|
371
|
+
def build_location_node(xml, name, location, options = {})
|
359
372
|
# not implemented: * Shipment/Shipper/Name element
|
360
373
|
# * Shipment/(ShipTo|ShipFrom)/CompanyName element
|
361
374
|
# * Shipment/(Shipper|ShipTo|ShipFrom)/AttentionName element
|
362
375
|
# * Shipment/(Shipper|ShipTo|ShipFrom)/TaxIdentificationNumber element
|
363
|
-
|
376
|
+
xml.public_send(name) do
|
364
377
|
# You must specify the shipper name when creating labels.
|
365
378
|
if shipper_name = (options[:origin_name] || @options[:origin_name])
|
366
|
-
|
379
|
+
xml.Name(shipper_name)
|
367
380
|
end
|
368
|
-
|
369
|
-
|
381
|
+
xml.PhoneNumber(location.phone.gsub(/[^\d]/, '')) unless location.phone.blank?
|
382
|
+
xml.FaxNumber(location.fax.gsub(/[^\d]/, '')) unless location.fax.blank?
|
370
383
|
|
371
384
|
if name == 'Shipper' and (origin_account = options[:origin_account] || @options[:origin_account])
|
372
|
-
|
385
|
+
xml.ShipperNumber(origin_account)
|
373
386
|
elsif name == 'ShipTo' and (destination_account = options[:destination_account] || @options[:destination_account])
|
374
|
-
|
387
|
+
xml.ShipperAssignedIdentificationNumber(destination_account)
|
375
388
|
end
|
376
389
|
|
377
390
|
if name = location.company_name || location.name
|
378
|
-
|
391
|
+
xml.CompanyName(name)
|
379
392
|
end
|
380
393
|
|
381
394
|
if phone = location.phone
|
382
|
-
|
395
|
+
xml.PhoneNumber(phone)
|
383
396
|
end
|
384
397
|
|
385
398
|
if attn = location.name
|
386
|
-
|
399
|
+
xml.AttentionName(attn)
|
387
400
|
end
|
388
401
|
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
402
|
+
xml.Address do
|
403
|
+
xml.AddressLine1(location.address1) unless location.address1.blank?
|
404
|
+
xml.AddressLine2(location.address2) unless location.address2.blank?
|
405
|
+
xml.AddressLine3(location.address3) unless location.address3.blank?
|
406
|
+
xml.City(location.city) unless location.city.blank?
|
407
|
+
xml.StateProvinceCode(location.province) unless location.province.blank?
|
395
408
|
# StateProvinceCode required for negotiated rates but not otherwise, for some reason
|
396
|
-
|
397
|
-
|
398
|
-
|
409
|
+
xml.PostalCode(location.postal_code) unless location.postal_code.blank?
|
410
|
+
xml.CountryCode(location.country_code(:alpha2)) unless location.country_code(:alpha2).blank?
|
411
|
+
xml.ResidentialAddressIndicator(true) unless location.commercial? # the default should be that UPS returns residential rates for destinations that it doesn't know about
|
399
412
|
# not implemented: Shipment/(Shipper|ShipTo|ShipFrom)/Address/ResidentialAddressIndicator element
|
400
413
|
end
|
401
414
|
end
|
402
415
|
end
|
403
416
|
|
404
|
-
def build_package_node(package, options = {})
|
405
|
-
|
417
|
+
def build_package_node(xml, package, options = {})
|
418
|
+
xml.Package do
|
406
419
|
|
407
420
|
# not implemented: * Shipment/Package/PackagingType element
|
408
421
|
# * Shipment/Package/Description element
|
409
422
|
|
410
|
-
|
411
|
-
|
423
|
+
xml.PackagingType do
|
424
|
+
xml.Code('02')
|
412
425
|
end
|
413
426
|
|
414
|
-
|
415
|
-
|
416
|
-
|
427
|
+
xml.Dimensions do
|
428
|
+
xml.UnitOfMeasurement do
|
429
|
+
xml.Code(options[:imperial] ? 'IN' : 'CM')
|
417
430
|
end
|
418
431
|
[:length, :width, :height].each do |axis|
|
419
432
|
value = ((options[:imperial] ? package.inches(axis) : package.cm(axis)).to_f * 1000).round / 1000.0 # 3 decimals
|
420
|
-
|
433
|
+
xml.public_send(axis.to_s.capitalize, [value, 0.1].max)
|
421
434
|
end
|
422
435
|
end
|
423
436
|
|
424
|
-
|
425
|
-
|
426
|
-
|
437
|
+
xml.PackageWeight do
|
438
|
+
xml.UnitOfMeasurement do
|
439
|
+
xml.Code(options[:imperial] ? 'LBS' : 'KGS')
|
427
440
|
end
|
428
441
|
|
429
442
|
value = ((options[:imperial] ? package.lbs : package.kgs).to_f * 1000).round / 1000.0 # 3 decimals
|
430
|
-
|
443
|
+
xml.Weight([value, 0.1].max)
|
431
444
|
end
|
432
445
|
|
433
446
|
if options[:package] && options[:package][:reference_number]
|
434
|
-
|
435
|
-
|
436
|
-
|
447
|
+
xml.ReferenceNumber do
|
448
|
+
xml.Code(options[:package][:reference_number][:code] || "")
|
449
|
+
xml.Value(options[:package][:reference_number][:value])
|
437
450
|
end
|
438
451
|
end
|
439
452
|
|
440
|
-
package_node
|
441
|
-
|
442
453
|
# not implemented: * Shipment/Package/LargePackageIndicator element
|
443
454
|
# * Shipment/Package/PackageServiceOptions element
|
444
455
|
# * Shipment/Package/AdditionalHandling element
|
445
456
|
end
|
446
457
|
end
|
447
458
|
|
459
|
+
def build_document(xml, expected_root_tag)
|
460
|
+
document = Nokogiri.XML(xml)
|
461
|
+
if document.root.nil? || document.root.name != expected_root_tag
|
462
|
+
raise ActiveShipping::ResponseContentError.new(StandardError.new('Invalid document'), xml)
|
463
|
+
end
|
464
|
+
document
|
465
|
+
rescue Nokogiri::XML::SyntaxError => e
|
466
|
+
raise ActiveShipping::ResponseContentError.new(e, xml)
|
467
|
+
end
|
468
|
+
|
448
469
|
def parse_rate_response(origin, destination, packages, response, options = {})
|
449
|
-
xml =
|
470
|
+
xml = build_document(response, 'RatingServiceSelectionResponse')
|
450
471
|
success = response_success?(xml)
|
451
472
|
message = response_message(xml)
|
452
473
|
|
453
474
|
if success
|
454
|
-
rate_estimates =
|
455
|
-
|
456
|
-
|
457
|
-
service_code = rated_shipment.get_text('Service/Code').to_s
|
458
|
-
days_to_delivery = rated_shipment.get_text('GuaranteedDaysToDelivery').to_s.to_i
|
475
|
+
rate_estimates = xml.root.css('> RatedShipment').map do |rated_shipment|
|
476
|
+
service_code = rated_shipment.at('Service/Code').text
|
477
|
+
days_to_delivery = rated_shipment.at('GuaranteedDaysToDelivery').text.to_i
|
459
478
|
days_to_delivery = nil if days_to_delivery == 0
|
460
|
-
|
461
|
-
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
|
467
|
-
|
468
|
-
|
479
|
+
RateEstimate.new(origin, destination, @@name, service_name_for(origin, service_code),
|
480
|
+
:total_price => rated_shipment.at('TotalCharges/MonetaryValue').text.to_f,
|
481
|
+
:insurance_price => rated_shipment.at('ServiceOptionsCharges/MonetaryValue').text.to_f,
|
482
|
+
:currency => rated_shipment.at('TotalCharges/CurrencyCode').text,
|
483
|
+
:service_code => service_code,
|
484
|
+
:packages => packages,
|
485
|
+
:delivery_range => [timestamp_from_business_day(days_to_delivery)],
|
486
|
+
:negotiated_rate => rated_shipment.at('NegotiatedRates/NetSummaryCharges/GrandTotal/MonetaryValue').try(:text).to_f
|
487
|
+
)
|
469
488
|
end
|
470
489
|
end
|
471
490
|
RateResponse.new(success, message, Hash.from_xml(response).values.first, :rates => rate_estimates, :xml => response, :request => last_request)
|
472
491
|
end
|
473
492
|
|
474
493
|
def parse_tracking_response(response, options = {})
|
475
|
-
xml
|
494
|
+
xml = build_document(response, 'TrackResponse')
|
476
495
|
success = response_success?(xml)
|
477
496
|
message = response_message(xml)
|
478
497
|
|
@@ -482,19 +501,19 @@ module ActiveShipping
|
|
482
501
|
delivered, exception = false
|
483
502
|
shipment_events = []
|
484
503
|
|
485
|
-
first_shipment = xml.
|
486
|
-
first_package = first_shipment.
|
487
|
-
tracking_number = first_shipment.
|
504
|
+
first_shipment = xml.root.at('Shipment')
|
505
|
+
first_package = first_shipment.at('Package')
|
506
|
+
tracking_number = first_shipment.at_xpath('ShipmentIdentificationNumber | Package/TrackingNumber').text
|
488
507
|
|
489
508
|
# Build status hash
|
490
|
-
status_nodes = first_package.
|
509
|
+
status_nodes = first_package.css('Activity > Status > StatusType')
|
491
510
|
|
492
511
|
# Prefer a delivery node
|
493
|
-
status_node = status_nodes.detect { |x| x.
|
512
|
+
status_node = status_nodes.detect { |x| x.at('Code').text == 'D' }
|
494
513
|
status_node ||= status_nodes.first
|
495
514
|
|
496
|
-
status_code = status_node.
|
497
|
-
status_description = status_node.
|
515
|
+
status_code = status_node.at('Code').text
|
516
|
+
status_description = status_node.at('Description').text
|
498
517
|
status = TRACKING_STATUS_CODES[status_code]
|
499
518
|
|
500
519
|
if status_description =~ /out.*delivery/i
|
@@ -502,23 +521,23 @@ module ActiveShipping
|
|
502
521
|
end
|
503
522
|
|
504
523
|
origin, destination = %w(Shipper ShipTo).map do |location|
|
505
|
-
location_from_address_node(first_shipment.
|
524
|
+
location_from_address_node(first_shipment.at("#{location}/Address"))
|
506
525
|
end
|
507
526
|
|
508
527
|
# Get scheduled delivery date
|
509
528
|
unless status == :delivered
|
510
529
|
scheduled_delivery_date = parse_ups_datetime(
|
511
|
-
:date => first_shipment.
|
530
|
+
:date => first_shipment.at('ScheduledDeliveryDate'),
|
512
531
|
:time => nil
|
513
532
|
)
|
514
533
|
end
|
515
534
|
|
516
|
-
activities = first_package.
|
535
|
+
activities = first_package.css('> Activity')
|
517
536
|
unless activities.empty?
|
518
537
|
shipment_events = activities.map do |activity|
|
519
|
-
description = activity.
|
520
|
-
zoneless_time = parse_ups_datetime(:time => activity.
|
521
|
-
location = location_from_address_node(activity.
|
538
|
+
description = activity.at('Status/StatusType/Description').text
|
539
|
+
zoneless_time = parse_ups_datetime(:time => activity.at('Time'), :date => activity.at('Date'))
|
540
|
+
location = location_from_address_node(activity.at('ActivityLocation/Address'))
|
522
541
|
ShipmentEvent.new(description, zoneless_time, location)
|
523
542
|
end
|
524
543
|
|
@@ -541,9 +560,9 @@ module ActiveShipping
|
|
541
560
|
# Has the shipment been delivered?
|
542
561
|
if status == :delivered
|
543
562
|
delivered_activity = activities.first
|
544
|
-
delivery_signature = delivered_activity.
|
545
|
-
if delivered_activity.
|
546
|
-
actual_delivery_date = parse_ups_datetime(:date => delivered_activity.
|
563
|
+
delivery_signature = delivered_activity.at('ActivityLocation/SignedForByName').try(:text)
|
564
|
+
if delivered_activity.at('Status/StatusType/Code').text == 'D'
|
565
|
+
actual_delivery_date = parse_ups_datetime(:date => delivered_activity.at('Date'), :time => delivered_activity.at('Time'))
|
547
566
|
end
|
548
567
|
unless destination
|
549
568
|
destination = shipment_events[-1].location
|
@@ -575,18 +594,18 @@ module ActiveShipping
|
|
575
594
|
def location_from_address_node(address)
|
576
595
|
return nil unless address
|
577
596
|
Location.new(
|
578
|
-
|
579
|
-
|
580
|
-
|
581
|
-
|
582
|
-
|
583
|
-
|
584
|
-
|
585
|
-
|
597
|
+
:country => address.at('CountryCode').try(:text),
|
598
|
+
:postal_code => address.at('PostalCode').try(:text),
|
599
|
+
:province => address.at('StateProvinceCode').try(:text),
|
600
|
+
:city => address.at('City').try(:text),
|
601
|
+
:address1 => address.at('AddressLine1').try(:text),
|
602
|
+
:address2 => address.at('AddressLine2').try(:text),
|
603
|
+
:address3 => address.at('AddressLine3').try(:text)
|
604
|
+
)
|
586
605
|
end
|
587
606
|
|
588
607
|
def parse_ups_datetime(options = {})
|
589
|
-
time, date = options[:time].
|
608
|
+
time, date = options[:time].try(:text), options[:date].text
|
590
609
|
if time.nil?
|
591
610
|
hour, minute, second = 0
|
592
611
|
else
|
@@ -597,24 +616,24 @@ module ActiveShipping
|
|
597
616
|
Time.utc(year, month, day, hour, minute, second)
|
598
617
|
end
|
599
618
|
|
600
|
-
def response_success?(
|
601
|
-
|
619
|
+
def response_success?(document)
|
620
|
+
document.root.at('Response/ResponseStatusCode').text == '1'
|
602
621
|
end
|
603
622
|
|
604
|
-
def response_message(
|
605
|
-
|
623
|
+
def response_message(document)
|
624
|
+
document.root.at_xpath('Response/Error/ErrorDescription | Response/ResponseStatusDescription').text
|
606
625
|
end
|
607
626
|
|
608
627
|
def response_digest(xml)
|
609
|
-
xml.
|
628
|
+
xml.root.at('ShipmentDigest').text
|
610
629
|
end
|
611
630
|
|
612
631
|
def parse_ship_confirm(response)
|
613
|
-
|
632
|
+
build_document(response, 'ShipmentConfirmResponse')
|
614
633
|
end
|
615
634
|
|
616
635
|
def parse_ship_accept(response)
|
617
|
-
xml
|
636
|
+
xml = build_document(response, 'ShipmentAcceptResponse')
|
618
637
|
success = response_success?(xml)
|
619
638
|
message = response_message(xml)
|
620
639
|
|