active_shipping 0.12.4 → 0.12.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/lib/active_shipping.rb +2 -1
  3. data/lib/active_shipping/shipping/base.rb +2 -2
  4. data/lib/active_shipping/shipping/carrier.rb +16 -13
  5. data/lib/active_shipping/shipping/carriers/benchmark_carrier.rb +3 -4
  6. data/lib/active_shipping/shipping/carriers/bogus_carrier.rb +1 -3
  7. data/lib/active_shipping/shipping/carriers/canada_post.rb +33 -44
  8. data/lib/active_shipping/shipping/carriers/canada_post_pws.rb +72 -81
  9. data/lib/active_shipping/shipping/carriers/fedex.rb +118 -109
  10. data/lib/active_shipping/shipping/carriers/kunaki.rb +33 -32
  11. data/lib/active_shipping/shipping/carriers/new_zealand_post.rb +9 -16
  12. data/lib/active_shipping/shipping/carriers/shipwire.rb +36 -35
  13. data/lib/active_shipping/shipping/carriers/stamps.rb +39 -51
  14. data/lib/active_shipping/shipping/carriers/ups.rb +280 -116
  15. data/lib/active_shipping/shipping/carriers/ups.rb.orig +456 -0
  16. data/lib/active_shipping/shipping/carriers/usps.rb +145 -100
  17. data/lib/active_shipping/shipping/carriers/usps.rb.orig +616 -0
  18. data/lib/active_shipping/shipping/errors.rb +1 -1
  19. data/lib/active_shipping/shipping/label_response.rb +25 -0
  20. data/lib/active_shipping/shipping/location.rb +18 -16
  21. data/lib/active_shipping/shipping/package.rb +51 -54
  22. data/lib/active_shipping/shipping/rate_estimate.rb +10 -12
  23. data/lib/active_shipping/shipping/rate_response.rb +3 -7
  24. data/lib/active_shipping/shipping/response.rb +6 -9
  25. data/lib/active_shipping/shipping/shipment_event.rb +2 -4
  26. data/lib/active_shipping/shipping/shipment_packer.rb +32 -17
  27. data/lib/active_shipping/shipping/shipping_response.rb +2 -4
  28. data/lib/active_shipping/shipping/tracking_response.rb +3 -5
  29. data/lib/active_shipping/version.rb +1 -1
  30. data/lib/vendor/quantified/lib/quantified/attribute.rb +79 -80
  31. data/lib/vendor/quantified/lib/quantified/length.rb +5 -5
  32. data/lib/vendor/quantified/lib/quantified/mass.rb +4 -4
  33. data/lib/vendor/quantified/test/length_test.rb +19 -15
  34. data/lib/vendor/quantified/test/mass_test.rb +14 -14
  35. data/lib/vendor/quantified/test/test_helper.rb +1 -2
  36. data/lib/vendor/test_helper.rb +0 -1
  37. data/lib/vendor/xml_node/benchmark/bench_generation.rb +2 -4
  38. data/lib/vendor/xml_node/lib/xml_node.rb +54 -55
  39. data/lib/vendor/xml_node/test/test_generating.rb +23 -28
  40. data/lib/vendor/xml_node/test/test_parsing.rb +5 -8
  41. metadata +6 -25
  42. checksums.yaml.gz.sig +0 -1
  43. data.tar.gz.sig +0 -0
  44. metadata.gz.sig +0 -0
@@ -14,10 +14,12 @@ module ActiveMerchant
14
14
 
15
15
  RESOURCES = {
16
16
  :rates => 'ups.app/xml/Rate',
17
- :track => 'ups.app/xml/Track'
17
+ :track => 'ups.app/xml/Track',
18
+ :ship_confirm => 'ups.app/xml/ShipConfirm',
19
+ :ship_accept => 'ups.app/xml/ShipAccept'
18
20
  }
19
21
 
20
- PICKUP_CODES = HashWithIndifferentAccess.new({
22
+ PICKUP_CODES = HashWithIndifferentAccess.new(
21
23
  :daily_pickup => "01",
22
24
  :customer_counter => "03",
23
25
  :one_time_pickup => "06",
@@ -25,18 +27,18 @@ module ActiveMerchant
25
27
  :suggested_retail_rates => "11",
26
28
  :letter_center => "19",
27
29
  :air_service_center => "20"
28
- })
30
+ )
29
31
 
30
- CUSTOMER_CLASSIFICATIONS = HashWithIndifferentAccess.new({
32
+ CUSTOMER_CLASSIFICATIONS = HashWithIndifferentAccess.new(
31
33
  :wholesale => "01",
32
34
  :occasional => "03",
33
35
  :retail => "04"
34
- })
36
+ )
35
37
 
36
38
  # these are the defaults described in the UPS API docs,
37
39
  # but they don't seem to apply them under all circumstances,
38
40
  # so we need to take matters into our own hands
39
- DEFAULT_CUSTOMER_CLASSIFICATIONS = Hash.new do |hash,key|
41
+ DEFAULT_CUSTOMER_CLASSIFICATIONS = Hash.new do |hash, key|
40
42
  hash[key] = case key.to_sym
41
43
  when :daily_pickup then :wholesale
42
44
  when :customer_counter then :retail
@@ -86,24 +88,26 @@ module ActiveMerchant
86
88
  "07" => "UPS Express"
87
89
  }
88
90
 
89
- TRACKING_STATUS_CODES = HashWithIndifferentAccess.new({
91
+ TRACKING_STATUS_CODES = HashWithIndifferentAccess.new(
90
92
  'I' => :in_transit,
91
93
  'D' => :delivered,
92
94
  'X' => :exception,
93
95
  'P' => :pickup,
94
96
  'M' => :manifest_pickup
95
- })
97
+ )
96
98
 
97
99
  # From http://en.wikipedia.org/w/index.php?title=European_Union&oldid=174718707 (Current as of November 30, 2007)
98
- EU_COUNTRY_CODES = ["GB", "AT", "BE", "BG", "CY", "CZ", "DK", "EE", "FI", "FR", "DE", "GR", "HU", "IE", "IT", "LV", "LT", "LU", "MT", "NL", "PL", "PT", "RO", "SK", "SI", "ES", "SE"]
100
+ EU_COUNTRY_CODES = %w(GB AT BE BG CY CZ DK EE FI FR DE GR HU IE IT LV LT LU MT NL PL PT RO SK SI ES SE)
99
101
 
100
- US_TERRITORIES_TREATED_AS_COUNTRIES = ["AS", "FM", "GU", "MH", "MP", "PW", "PR", "VI"]
102
+ US_TERRITORIES_TREATED_AS_COUNTRIES = %w(AS FM GU MH MP PW PR VI)
103
+
104
+ IMPERIAL_COUNTRIES = %w(US LR MM)
101
105
 
102
106
  def requirements
103
107
  [:key, :login, :password]
104
108
  end
105
109
 
106
- def find_rates(origin, destination, packages, options={})
110
+ def find_rates(origin, destination, packages, options = {})
107
111
  origin, destination = upsified_location(origin), upsified_location(destination)
108
112
  options = @options.merge(options)
109
113
  packages = Array(packages)
@@ -113,7 +117,7 @@ module ActiveMerchant
113
117
  parse_rate_response(origin, destination, packages, response, options)
114
118
  end
115
119
 
116
- def find_tracking_info(tracking_number, options={})
120
+ def find_tracking_info(tracking_number, options = {})
117
121
  options = @options.update(options)
118
122
  access_request = build_access_request
119
123
  tracking_request = build_tracking_request(tracking_number, options)
@@ -121,6 +125,46 @@ module ActiveMerchant
121
125
  parse_tracking_response(response, options)
122
126
  end
123
127
 
128
+ def create_shipment(origin, destination, packages, options = {})
129
+ options = @options.merge(options)
130
+ packages = Array(packages)
131
+ access_request = build_access_request
132
+
133
+ begin
134
+
135
+ # STEP 1: Confirm. Validation step, important for verifying price.
136
+ confirm_request = build_shipment_request(origin, destination, packages, options)
137
+ logger.debug(confirm_request) if logger
138
+
139
+ confirm_response = commit(:ship_confirm, save_request(access_request + confirm_request), (options[:test] || false))
140
+ logger.debug(confirm_response) if logger
141
+
142
+ # ... now, get the digest, it's needed to get the label. In theory,
143
+ # one could make decisions based on the price or some such to avoid
144
+ # surprises. This also has *no* error handling yet.
145
+ xml = parse_ship_confirm(confirm_response)
146
+ success = response_success?(xml)
147
+ message = response_message(xml)
148
+ digest = response_digest(xml)
149
+ raise message unless success
150
+
151
+ # STEP 2: Accept. Use shipment digest in first response to get the actual label.
152
+ accept_request = build_accept_request(digest, options)
153
+ logger.debug(accept_request) if logger
154
+
155
+ accept_response = commit(:ship_accept, save_request(access_request + accept_request), (options[:test] || false))
156
+ logger.debug(accept_response) if logger
157
+
158
+ # ...finally, build a map from the response that contains
159
+ # the label data and tracking information.
160
+ parse_ship_accept(accept_response)
161
+
162
+ rescue RuntimeError => e
163
+ raise "Could not obtain shipping label. #{e.message}."
164
+
165
+ end
166
+ end
167
+
124
168
  protected
125
169
 
126
170
  def upsified_location(location)
@@ -144,7 +188,7 @@ module ActiveMerchant
144
188
  xml_request.to_s
145
189
  end
146
190
 
147
- def build_rate_request(origin, destination, packages, options={})
191
+ def build_rate_request(origin, destination, packages, options = {})
148
192
  packages = Array(packages)
149
193
  xml_request = XmlNode.new('RatingServiceSelectionRequest') do |root_node|
150
194
  root_node << XmlNode.new('Request') do |request|
@@ -183,45 +227,8 @@ module ActiveMerchant
183
227
  # * Shipment/DocumentsOnly element
184
228
 
185
229
  packages.each do |package|
186
- imperial = ['US','LR','MM'].include?(origin.country_code(:alpha2))
187
-
188
- shipment << XmlNode.new("Package") do |package_node|
189
-
190
- # not implemented: * Shipment/Package/PackagingType element
191
- # * Shipment/Package/Description element
192
-
193
- package_node << XmlNode.new("PackagingType") do |packaging_type|
194
- packaging_type << XmlNode.new("Code", '02')
195
- end
196
-
197
- package_node << XmlNode.new("Dimensions") do |dimensions|
198
- dimensions << XmlNode.new("UnitOfMeasurement") do |units|
199
- units << XmlNode.new("Code", imperial ? 'IN' : 'CM')
200
- end
201
- [:length,:width,:height].each do |axis|
202
- value = ((imperial ? package.inches(axis) : package.cm(axis)).to_f*1000).round/1000.0 # 3 decimals
203
- dimensions << XmlNode.new(axis.to_s.capitalize, [value,0.1].max)
204
- end
205
- end
206
-
207
- package_node << XmlNode.new("PackageWeight") do |package_weight|
208
- package_weight << XmlNode.new("UnitOfMeasurement") do |units|
209
- units << XmlNode.new("Code", imperial ? 'LBS' : 'KGS')
210
- end
211
- if package.value.present? && package.currency.present?
212
- add_insured_node( package_node, currency:package.currency, value:(package.value.to_i/100) )
213
- end
214
-
215
- value = ((imperial ? package.lbs : package.kgs).to_f*1000).round/1000.0 # 3 decimals
216
- package_weight << XmlNode.new("Weight", [value,0.1].max)
217
- end
218
-
219
- # not implemented: * Shipment/Package/LargePackageIndicator element
220
- # * Shipment/Package/ReferenceNumber element
221
- # * Shipment/Package/PackageServiceOptions element
222
- # * Shipment/Package/AdditionalHandling element
223
- end
224
-
230
+ options[:imperial] ||= IMPERIAL_COUNTRIES.include?(origin.country_code(:alpha2))
231
+ shipment << build_package_node(package, options)
225
232
  end
226
233
 
227
234
  # not implemented: * Shipment/ShipmentServiceOptions element
@@ -235,7 +242,110 @@ module ActiveMerchant
235
242
  xml_request.to_s
236
243
  end
237
244
 
238
- def build_tracking_request(tracking_number, options={})
245
+ # Build XML node to request a shipping label for the given packages.
246
+ #
247
+ # options:
248
+ # * origin_account: who will pay for the shipping label
249
+ # * customer_context: a "guid like substance" -- according to UPS
250
+ # * shipper: who is sending the package and where it should be returned
251
+ # if it is undeliverable.
252
+ # * ship_from: where the package is picked up.
253
+ # * service_code: default to '14'
254
+ # * service_descriptor: default to 'Next Day Air Early AM'
255
+ # * saturday_delivery: any truthy value causes this element to exist
256
+ # * optional_processing: 'validate' (blank) or 'nonvalidate' or blank
257
+ #
258
+ def build_shipment_request(origin, destination, packages, options = {})
259
+ # There are a lot of unimplemented elements, documenting all of them
260
+ # wouldprobably be unhelpful.
261
+
262
+ xml_request = XmlNode.new('ShipmentConfirmRequest') do |root_node|
263
+ root_node << XmlNode.new('Request') do |request|
264
+ # Required element and the text must be "ShipConfirm"
265
+ request << XmlNode.new('RequestAction', 'ShipConfirm')
266
+ # Required element cotnrols level of address validation.
267
+ request << XmlNode.new('RequestOption', options[:optional_processing] || 'validate')
268
+ # Optional element to identify transactions between client and server.
269
+ if options[:customer_context]
270
+ request << XmlNode.new('TransactionReference') do |refer|
271
+ refer << XmlNode.new('CustomerContext', options[:customer_context])
272
+ end
273
+ end
274
+ end
275
+ root_node << XmlNode.new('Shipment') do |shipment|
276
+ # Required element.
277
+ shipment << XmlNode.new('Service') do |service|
278
+ service << XmlNode.new('Code', options[:service_code] || '14')
279
+ service << XmlNode.new('Description', options[:service_description] || 'Next Day Air Early AM')
280
+ end
281
+ # Required element. The delivery destination.
282
+ shipment << build_location_node('ShipTo', destination, {})
283
+ # Required element. The company whose account is responsible for the label(s).
284
+ shipment << build_location_node('Shipper', options[:shipper] || origin, {})
285
+ # Required if pickup is different different from shipper's address.
286
+ if options[:ship_from]
287
+ shipment << build_location_node('ShipFrom', options[:ship_from], {})
288
+ end
289
+ # Optional.
290
+ if options[:saturday_delivery]
291
+ shipment << XmlNode.new('ShipmentServiceOptions') do |opts|
292
+ opts << XmlNode.new('SaturdayDelivery')
293
+ end
294
+ end
295
+ # Optional.
296
+ if options[:origin_account]
297
+ shipment << XmlNode.new('RateInformation') do |rate|
298
+ rate << XmlNode.new('NegotiatedRatesIndicator')
299
+ end
300
+ end
301
+ # Optional.
302
+ if options[:shipment] && options[:shipment][:reference_number]
303
+ shipment << XmlNode.new("ReferenceNumber") do |ref_node|
304
+ ref_node << XmlNode.new("Code", options[:shipment][:reference_number][:code] || "")
305
+ ref_node << XmlNode.new("Value", options[:shipment][:reference_number][:value])
306
+ end
307
+ end
308
+ # Conditionally required. Either this element or an ItemizedPaymentInformation
309
+ # is needed. However, only PaymentInformation is not implemented.
310
+ shipment << XmlNode.new('PaymentInformation') do |payment|
311
+ payment << XmlNode.new('Prepaid') do |prepay|
312
+ prepay << XmlNode.new('BillShipper') do |bill|
313
+ bill << XmlNode.new('AccountNumber', options[:origin_account])
314
+ end
315
+ end
316
+ end
317
+ # A request may specify multiple packages.
318
+ options[:imperial] ||= IMPERIAL_COUNTRIES.include?(origin.country_code(:alpha2))
319
+ packages.each do |package|
320
+ shipment << build_package_node(package, options)
321
+ end
322
+ end
323
+ # I don't know all of the options that UPS supports for labels
324
+ # so I'm going with something very simple for now.
325
+ root_node << XmlNode.new('LabelSpecification') do |specification|
326
+ specification << XmlNode.new('LabelPrintMethod') do |print_method|
327
+ print_method << XmlNode.new('Code', 'GIF')
328
+ end
329
+ specification << XmlNode.new('HTTPUserAgent', 'Mozilla/4.5') # hmmm
330
+ specification << XmlNode.new('LabelImageFormat', 'GIF') do |image_format|
331
+ image_format << XmlNode.new('Code', 'GIF')
332
+ end
333
+ end
334
+ end
335
+ xml_request.to_s
336
+ end
337
+
338
+ def build_accept_request(digest, options = {})
339
+ xml_request = XmlNode.new('ShipmentAcceptRequest') do |root_node|
340
+ root_node << XmlNode.new('Request') do |request|
341
+ request << XmlNode.new('RequestAction', 'ShipAccept')
342
+ end
343
+ root_node << XmlNode.new('ShipmentDigest', digest)
344
+ end
345
+ xml_request.to_s
346
+ end
347
+
348
+ def build_tracking_request(tracking_number, options = {})
239
349
  xml_request = XmlNode.new('TrackRequest') do |root_node|
240
350
  root_node << XmlNode.new('Request') do |request|
241
351
  request << XmlNode.new('RequestAction', 'Track')
@@ -246,28 +356,44 @@ module ActiveMerchant
246
356
  xml_request.to_s
247
357
  end
248
358
 
249
- def build_location_node(name,location,options={})
359
+ def build_location_node(name, location, options = {})
250
360
  # not implemented: * Shipment/Shipper/Name element
251
361
  # * Shipment/(ShipTo|ShipFrom)/CompanyName element
252
362
  # * Shipment/(Shipper|ShipTo|ShipFrom)/AttentionName element
253
363
  # * Shipment/(Shipper|ShipTo|ShipFrom)/TaxIdentificationNumber element
254
- location_node = XmlNode.new(name) do |location_node|
255
- location_node << XmlNode.new('PhoneNumber', location.phone.gsub(/[^\d]/,'')) unless location.phone.blank?
256
- location_node << XmlNode.new('FaxNumber', location.fax.gsub(/[^\d]/,'')) unless location.fax.blank?
364
+ XmlNode.new(name) do |location_node|
365
+ # You must specify the shipper name when creating labels.
366
+ if shipper_name = (options[:origin_name] || @options[:origin_name])
367
+ location_node << XmlNode.new('Name', shipper_name)
368
+ end
369
+ location_node << XmlNode.new('PhoneNumber', location.phone.gsub(/[^\d]/, '')) unless location.phone.blank?
370
+ location_node << XmlNode.new('FaxNumber', location.fax.gsub(/[^\d]/, '')) unless location.fax.blank?
257
371
 
258
- if name == 'Shipper' and (origin_account = @options[:origin_account] || options[:origin_account])
372
+ if name == 'Shipper' and (origin_account = options[:origin_account] || @options[:origin_account])
259
373
  location_node << XmlNode.new('ShipperNumber', origin_account)
260
- elsif name == 'ShipTo' and (destination_account = @options[:destination_account] || options[:destination_account])
374
+ elsif name == 'ShipTo' and (destination_account = options[:destination_account] || @options[:destination_account])
261
375
  location_node << XmlNode.new('ShipperAssignedIdentificationNumber', destination_account)
262
376
  end
263
377
 
378
+ if name = location.company_name || location.name
379
+ location_node << XmlNode.new('CompanyName', name)
380
+ end
381
+
382
+ if phone = location.phone
383
+ location_node << XmlNode.new('PhoneNumber', phone)
384
+ end
385
+
386
+ if attn = location.name
387
+ location_node << XmlNode.new('AttentionName', attn)
388
+ end
389
+
264
390
  location_node << XmlNode.new('Address') do |address|
265
391
  address << XmlNode.new("AddressLine1", location.address1) unless location.address1.blank?
266
392
  address << XmlNode.new("AddressLine2", location.address2) unless location.address2.blank?
267
393
  address << XmlNode.new("AddressLine3", location.address3) unless location.address3.blank?
268
394
  address << XmlNode.new("City", location.city) unless location.city.blank?
269
395
  address << XmlNode.new("StateProvinceCode", location.province) unless location.province.blank?
270
- # StateProvinceCode required for negotiated rates but not otherwise, for some reason
396
+ # StateProvinceCode required for negotiated rates but not otherwise, for some reason
271
397
  address << XmlNode.new("PostalCode", location.postal_code) unless location.postal_code.blank?
272
398
  address << XmlNode.new("CountryCode", location.country_code(:alpha2)) unless location.country_code(:alpha2).blank?
273
399
  address << XmlNode.new("ResidentialAddressIndicator", true) unless location.commercial? # the default should be that UPS returns residential rates for destinations that it doesn't know about
@@ -276,24 +402,51 @@ module ActiveMerchant
276
402
  end
277
403
  end
278
404
 
279
- def add_insured_node(*args)
280
- params, package_node = args.extract_options!, args[0]
281
- currency, value = params[:currency], params[:value].to_i
282
- package_node << XmlNode.new("PackageServiceOptions") do |package_service_options|
283
- package_service_options << XmlNode.new("DeclaredValue") do |declared_value|
284
- declared_value << XmlNode.new("CurrencyCode", currency)
285
- declared_value << XmlNode.new("MonetaryValue", (value.to_i))
405
+ def build_package_node(package, options = {})
406
+ XmlNode.new("Package") do |package_node|
407
+
408
+ # not implemented: * Shipment/Package/PackagingType element
409
+ # * Shipment/Package/Description element
410
+
411
+ package_node << XmlNode.new("PackagingType") do |packaging_type|
412
+ packaging_type << XmlNode.new("Code", '02')
286
413
  end
287
- package_service_options << XmlNode.new("InsuredValue") do |declared_value|
288
- declared_value << XmlNode.new("CurrencyCode", currency)
289
- declared_value << XmlNode.new("MonetaryValue", (value.to_i))
414
+
415
+ package_node << XmlNode.new("Dimensions") do |dimensions|
416
+ dimensions << XmlNode.new("UnitOfMeasurement") do |units|
417
+ units << XmlNode.new("Code", options[:imperial] ? 'IN' : 'CM')
418
+ end
419
+ [:length, :width, :height].each do |axis|
420
+ value = ((options[:imperial] ? package.inches(axis) : package.cm(axis)).to_f * 1000).round / 1000.0 # 3 decimals
421
+ dimensions << XmlNode.new(axis.to_s.capitalize, [value, 0.1].max)
422
+ end
290
423
  end
424
+
425
+ package_node << XmlNode.new("PackageWeight") do |package_weight|
426
+ package_weight << XmlNode.new("UnitOfMeasurement") do |units|
427
+ units << XmlNode.new("Code", options[:imperial] ? 'LBS' : 'KGS')
428
+ end
429
+
430
+ value = ((options[:imperial] ? package.lbs : package.kgs).to_f * 1000).round / 1000.0 # 3 decimals
431
+ package_weight << XmlNode.new("Weight", [value, 0.1].max)
432
+ end
433
+
434
+ if options[:package] && options[:package][:reference_number]
435
+ package_node << XmlNode.new("ReferenceNumber") do |ref_node|
436
+ ref_node << XmlNode.new("Code", options[:package][:reference_number][:code] || "")
437
+ ref_node << XmlNode.new("Value", options[:package][:reference_number][:value])
438
+ end
439
+ end
440
+
441
+ package_node
442
+
443
+ # not implemented: * Shipment/Package/LargePackageIndicator element
444
+ # * Shipment/Package/PackageServiceOptions element
445
+ # * Shipment/Package/AdditionalHandling element
291
446
  end
292
447
  end
293
448
 
294
- def parse_rate_response(origin, destination, packages, response, options={})
295
- rates = []
296
-
449
+ def parse_rate_response(origin, destination, packages, response, options = {})
297
450
  xml = REXML::Document.new(response)
298
451
  success = response_success?(xml)
299
452
  message = response_message(xml)
@@ -306,31 +459,29 @@ module ActiveMerchant
306
459
  days_to_delivery = rated_shipment.get_text('GuaranteedDaysToDelivery').to_s.to_i
307
460
  days_to_delivery = nil if days_to_delivery == 0
308
461
  rate_estimates << RateEstimate.new(origin, destination, @@name,
309
- service_name_for(origin, service_code),
310
- :total_price => rated_shipment.get_text('TotalCharges/MonetaryValue').to_s.to_f,
311
- :insurance_price => rated_shipment.get_text('ServiceOptionsCharges/MonetaryValue').to_s.to_f,
312
- :currency => rated_shipment.get_text('TotalCharges/CurrencyCode').to_s,
313
- :service_code => service_code,
314
- :packages => packages,
315
- :delivery_range => [timestamp_from_business_day(days_to_delivery)],
316
- :negotiated_rate => rated_shipment.get_text('NegotiatedRates/NetSummaryCharges/GrandTotal/MonetaryValue').to_s.to_f)
462
+ service_name_for(origin, service_code),
463
+ :total_price => rated_shipment.get_text('TotalCharges/MonetaryValue').to_s.to_f,
464
+ :insurance_price => rated_shipment.get_text('ServiceOptionsCharges/MonetaryValue').to_s.to_f,
465
+ :currency => rated_shipment.get_text('TotalCharges/CurrencyCode').to_s,
466
+ :service_code => service_code,
467
+ :packages => packages,
468
+ :delivery_range => [timestamp_from_business_day(days_to_delivery)],
469
+ :negotiated_rate => rated_shipment.get_text('NegotiatedRates/NetSummaryCharges/GrandTotal/MonetaryValue').to_s.to_f)
317
470
  end
318
471
  end
319
472
  RateResponse.new(success, message, Hash.from_xml(response).values.first, :rates => rate_estimates, :xml => response, :request => last_request)
320
473
  end
321
474
 
322
- def parse_tracking_response(response, options={})
475
+ def parse_tracking_response(response, options = {})
323
476
  xml = REXML::Document.new(response)
324
477
  success = response_success?(xml)
325
478
  message = response_message(xml)
326
479
 
327
480
  if success
328
- tracking_number, origin, destination, status_code, status_description, delivery_signature = nil
481
+ delivery_signature = nil
482
+ exception_event, scheduled_delivery_date, actual_delivery_date = nil
329
483
  delivered, exception = false
330
- exception_event = nil
331
484
  shipment_events = []
332
- status = {}
333
- scheduled_delivery_date = nil
334
485
 
335
486
  first_shipment = xml.elements['/*/Shipment']
336
487
  first_package = first_shipment.elements['Package']
@@ -347,34 +498,27 @@ module ActiveMerchant
347
498
  status_description = status_node.get_text('Description').to_s
348
499
  status = TRACKING_STATUS_CODES[status_code]
349
500
 
350
-
351
501
  if status_description =~ /out.*delivery/i
352
502
  status = :out_for_delivery
353
503
  end
354
504
 
355
- origin, destination = %w{Shipper ShipTo}.map do |location|
505
+ origin, destination = %w(Shipper ShipTo).map do |location|
356
506
  location_from_address_node(first_shipment.elements["#{location}/Address"])
357
507
  end
358
508
 
359
509
  # Get scheduled delivery date
360
510
  unless status == :delivered
361
- scheduled_delivery_date = parse_ups_datetime({
511
+ scheduled_delivery_date = parse_ups_datetime(
362
512
  :date => first_shipment.get_text('ScheduledDeliveryDate'),
363
513
  :time => nil
364
- })
514
+ )
365
515
  end
366
516
 
367
517
  activities = first_package.get_elements('Activity')
368
518
  unless activities.empty?
369
519
  shipment_events = activities.map do |activity|
370
520
  description = activity.get_text('Status/StatusType/Description').to_s
371
- zoneless_time = if (time = activity.get_text('Time')) &&
372
- (date = activity.get_text('Date'))
373
- time, date = time.to_s, date.to_s
374
- hour, minute, second = time.scan(/\d{2}/)
375
- year, month, day = date[0..3], date[4..5], date[6..7]
376
- Time.utc(year, month, day, hour, minute, second)
377
- end
521
+ zoneless_time = parse_ups_datetime(:time => activity.get_text('Time'), :date => activity.get_text('Date'))
378
522
  location = location_from_address_node(activity.elements['ActivityLocation/Address'])
379
523
  ShipmentEvent.new(description, zoneless_time, location)
380
524
  end
@@ -397,8 +541,12 @@ module ActiveMerchant
397
541
 
398
542
  # Has the shipment been delivered?
399
543
  if status == :delivered
400
- delivery_signature = activities.first.get_text('ActivityLocation/SignedForByName').to_s
401
- if !destination
544
+ delivered_activity = activities.first
545
+ delivery_signature = delivered_activity.get_text('ActivityLocation/SignedForByName').to_s
546
+ if delivered_activity.get_text('Status/StatusType/Code') == 'D'
547
+ actual_delivery_date = parse_ups_datetime(:date => delivered_activity.get_text('Date'), :time => delivered_activity.get_text('Time'))
548
+ end
549
+ unless destination
402
550
  destination = shipment_events[-1].location
403
551
  end
404
552
  shipment_events[-1] = ShipmentEvent.new(shipment_events.last.name, shipment_events.last.time, destination)
@@ -407,21 +555,22 @@ module ActiveMerchant
407
555
 
408
556
  end
409
557
  TrackingResponse.new(success, message, Hash.from_xml(response).values.first,
410
- :carrier => @@name,
411
- :xml => response,
412
- :request => last_request,
413
- :status => status,
414
- :status_code => status_code,
415
- :status_description => status_description,
416
- :delivery_signature => delivery_signature,
417
- :scheduled_delivery_date => scheduled_delivery_date,
418
- :shipment_events => shipment_events,
419
- :delivered => delivered,
420
- :exception => exception,
421
- :exception_event => exception_event,
422
- :origin => origin,
423
- :destination => destination,
424
- :tracking_number => tracking_number)
558
+ :carrier => @@name,
559
+ :xml => response,
560
+ :request => last_request,
561
+ :status => status,
562
+ :status_code => status_code,
563
+ :status_description => status_description,
564
+ :delivery_signature => delivery_signature,
565
+ :scheduled_delivery_date => scheduled_delivery_date,
566
+ :actual_delivery_date => actual_delivery_date,
567
+ :shipment_events => shipment_events,
568
+ :delivered => delivered,
569
+ :exception => exception,
570
+ :exception_event => exception_event,
571
+ :origin => origin,
572
+ :destination => destination,
573
+ :tracking_number => tracking_number)
425
574
  end
426
575
 
427
576
  def location_from_address_node(address)
@@ -457,6 +606,22 @@ module ActiveMerchant
457
606
  xml.get_text('/*/Response/Error/ErrorDescription | /*/Response/ResponseStatusDescription').to_s
458
607
  end
459
608
 
609
+ def response_digest(xml)
610
+ xml.get_text('/*/ShipmentDigest').to_s
611
+ end
612
+
613
+ def parse_ship_confirm(response)
614
+ REXML::Document.new(response)
615
+ end
616
+
617
+ def parse_ship_accept(response)
618
+ xml = REXML::Document.new(response)
619
+ success = response_success?(xml)
620
+ message = response_message(xml)
621
+
622
+ LabelResponse.new(success, message, Hash.from_xml(response).values.first)
623
+ end
624
+
460
625
  def commit(action, request, test = false)
461
626
  ssl_post("#{test ? TEST_URL : LIVE_URL}/#{RESOURCES[action]}", request)
462
627
  end
@@ -478,9 +643,8 @@ module ActiveMerchant
478
643
  end
479
644
 
480
645
  name ||= OTHER_NON_US_ORIGIN_SERVICES[code] unless name == 'US'
481
- name ||= DEFAULT_SERVICES[code]
646
+ name || DEFAULT_SERVICES[code]
482
647
  end
483
-
484
648
  end
485
649
  end
486
650
  end