active_shipping 0.12.6 → 1.0.0.pre1

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.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +2 -0
  3. data.tar.gz.sig +0 -0
  4. data/{CHANGELOG → CHANGELOG.md} +6 -2
  5. data/CONTRIBUTING.md +32 -0
  6. data/{README.markdown → README.md} +45 -61
  7. data/lib/active_shipping.rb +20 -28
  8. data/lib/active_shipping/carrier.rb +82 -0
  9. data/lib/active_shipping/carriers.rb +33 -0
  10. data/lib/active_shipping/carriers/benchmark_carrier.rb +31 -0
  11. data/lib/active_shipping/carriers/bogus_carrier.rb +12 -0
  12. data/lib/active_shipping/carriers/canada_post.rb +253 -0
  13. data/lib/active_shipping/carriers/canada_post_pws.rb +870 -0
  14. data/lib/active_shipping/carriers/fedex.rb +579 -0
  15. data/lib/active_shipping/carriers/kunaki.rb +164 -0
  16. data/lib/active_shipping/carriers/new_zealand_post.rb +262 -0
  17. data/lib/active_shipping/carriers/shipwire.rb +181 -0
  18. data/lib/active_shipping/carriers/stamps.rb +861 -0
  19. data/lib/active_shipping/carriers/ups.rb +648 -0
  20. data/lib/active_shipping/carriers/usps.rb +642 -0
  21. data/lib/active_shipping/errors.rb +7 -0
  22. data/lib/active_shipping/label_response.rb +23 -0
  23. data/lib/active_shipping/location.rb +149 -0
  24. data/lib/active_shipping/package.rb +241 -0
  25. data/lib/active_shipping/rate_estimate.rb +64 -0
  26. data/lib/active_shipping/rate_response.rb +13 -0
  27. data/lib/active_shipping/response.rb +41 -0
  28. data/lib/active_shipping/shipment_event.rb +17 -0
  29. data/lib/active_shipping/shipment_packer.rb +73 -0
  30. data/lib/active_shipping/shipping_response.rb +12 -0
  31. data/lib/active_shipping/tracking_response.rb +52 -0
  32. data/lib/active_shipping/version.rb +1 -1
  33. data/lib/vendor/quantified/test/length_test.rb +2 -2
  34. data/lib/vendor/xml_node/test/test_parsing.rb +1 -1
  35. metadata +58 -36
  36. metadata.gz.sig +0 -0
  37. data/lib/active_shipping/shipping/base.rb +0 -13
  38. data/lib/active_shipping/shipping/carrier.rb +0 -84
  39. data/lib/active_shipping/shipping/carriers.rb +0 -23
  40. data/lib/active_shipping/shipping/carriers/benchmark_carrier.rb +0 -33
  41. data/lib/active_shipping/shipping/carriers/bogus_carrier.rb +0 -14
  42. data/lib/active_shipping/shipping/carriers/canada_post.rb +0 -257
  43. data/lib/active_shipping/shipping/carriers/canada_post_pws.rb +0 -874
  44. data/lib/active_shipping/shipping/carriers/fedex.rb +0 -581
  45. data/lib/active_shipping/shipping/carriers/kunaki.rb +0 -166
  46. data/lib/active_shipping/shipping/carriers/new_zealand_post.rb +0 -262
  47. data/lib/active_shipping/shipping/carriers/shipwire.rb +0 -184
  48. data/lib/active_shipping/shipping/carriers/stamps.rb +0 -864
  49. data/lib/active_shipping/shipping/carriers/ups.rb +0 -650
  50. data/lib/active_shipping/shipping/carriers/usps.rb +0 -649
  51. data/lib/active_shipping/shipping/errors.rb +0 -9
  52. data/lib/active_shipping/shipping/label_response.rb +0 -25
  53. data/lib/active_shipping/shipping/location.rb +0 -152
  54. data/lib/active_shipping/shipping/package.rb +0 -243
  55. data/lib/active_shipping/shipping/rate_estimate.rb +0 -66
  56. data/lib/active_shipping/shipping/rate_response.rb +0 -15
  57. data/lib/active_shipping/shipping/response.rb +0 -43
  58. data/lib/active_shipping/shipping/shipment_event.rb +0 -19
  59. data/lib/active_shipping/shipping/shipment_packer.rb +0 -75
  60. data/lib/active_shipping/shipping/shipping_response.rb +0 -14
  61. data/lib/active_shipping/shipping/tracking_response.rb +0 -54
@@ -1,650 +0,0 @@
1
- # -*- encoding: utf-8 -*-
2
-
3
- module ActiveMerchant
4
- module Shipping
5
- class UPS < Carrier
6
- self.retry_safe = true
7
-
8
- cattr_accessor :default_options
9
- cattr_reader :name
10
- @@name = "UPS"
11
-
12
- TEST_URL = 'https://wwwcie.ups.com'
13
- LIVE_URL = 'https://onlinetools.ups.com'
14
-
15
- RESOURCES = {
16
- :rates => 'ups.app/xml/Rate',
17
- :track => 'ups.app/xml/Track',
18
- :ship_confirm => 'ups.app/xml/ShipConfirm',
19
- :ship_accept => 'ups.app/xml/ShipAccept'
20
- }
21
-
22
- PICKUP_CODES = HashWithIndifferentAccess.new(
23
- :daily_pickup => "01",
24
- :customer_counter => "03",
25
- :one_time_pickup => "06",
26
- :on_call_air => "07",
27
- :suggested_retail_rates => "11",
28
- :letter_center => "19",
29
- :air_service_center => "20"
30
- )
31
-
32
- CUSTOMER_CLASSIFICATIONS = HashWithIndifferentAccess.new(
33
- :wholesale => "01",
34
- :occasional => "03",
35
- :retail => "04"
36
- )
37
-
38
- # these are the defaults described in the UPS API docs,
39
- # but they don't seem to apply them under all circumstances,
40
- # so we need to take matters into our own hands
41
- DEFAULT_CUSTOMER_CLASSIFICATIONS = Hash.new do |hash, key|
42
- hash[key] = case key.to_sym
43
- when :daily_pickup then :wholesale
44
- when :customer_counter then :retail
45
- else
46
- :occasional
47
- end
48
- end
49
-
50
- DEFAULT_SERVICES = {
51
- "01" => "UPS Next Day Air",
52
- "02" => "UPS Second Day Air",
53
- "03" => "UPS Ground",
54
- "07" => "UPS Worldwide Express",
55
- "08" => "UPS Worldwide Expedited",
56
- "11" => "UPS Standard",
57
- "12" => "UPS Three-Day Select",
58
- "13" => "UPS Next Day Air Saver",
59
- "14" => "UPS Next Day Air Early A.M.",
60
- "54" => "UPS Worldwide Express Plus",
61
- "59" => "UPS Second Day Air A.M.",
62
- "65" => "UPS Saver",
63
- "82" => "UPS Today Standard",
64
- "83" => "UPS Today Dedicated Courier",
65
- "84" => "UPS Today Intercity",
66
- "85" => "UPS Today Express",
67
- "86" => "UPS Today Express Saver"
68
- }
69
-
70
- CANADA_ORIGIN_SERVICES = {
71
- "01" => "UPS Express",
72
- "02" => "UPS Expedited",
73
- "14" => "UPS Express Early A.M."
74
- }
75
-
76
- MEXICO_ORIGIN_SERVICES = {
77
- "07" => "UPS Express",
78
- "08" => "UPS Expedited",
79
- "54" => "UPS Express Plus"
80
- }
81
-
82
- EU_ORIGIN_SERVICES = {
83
- "07" => "UPS Express",
84
- "08" => "UPS Expedited"
85
- }
86
-
87
- OTHER_NON_US_ORIGIN_SERVICES = {
88
- "07" => "UPS Express"
89
- }
90
-
91
- TRACKING_STATUS_CODES = HashWithIndifferentAccess.new(
92
- 'I' => :in_transit,
93
- 'D' => :delivered,
94
- 'X' => :exception,
95
- 'P' => :pickup,
96
- 'M' => :manifest_pickup
97
- )
98
-
99
- # From http://en.wikipedia.org/w/index.php?title=European_Union&oldid=174718707 (Current as of November 30, 2007)
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)
101
-
102
- US_TERRITORIES_TREATED_AS_COUNTRIES = %w(AS FM GU MH MP PW PR VI)
103
-
104
- IMPERIAL_COUNTRIES = %w(US LR MM)
105
-
106
- def requirements
107
- [:key, :login, :password]
108
- end
109
-
110
- def find_rates(origin, destination, packages, options = {})
111
- origin, destination = upsified_location(origin), upsified_location(destination)
112
- options = @options.merge(options)
113
- packages = Array(packages)
114
- access_request = build_access_request
115
- rate_request = build_rate_request(origin, destination, packages, options)
116
- response = commit(:rates, save_request(access_request + rate_request), (options[:test] || false))
117
- parse_rate_response(origin, destination, packages, response, options)
118
- end
119
-
120
- def find_tracking_info(tracking_number, options = {})
121
- options = @options.update(options)
122
- access_request = build_access_request
123
- tracking_request = build_tracking_request(tracking_number, options)
124
- response = commit(:track, save_request(access_request + tracking_request), (options[:test] || false))
125
- parse_tracking_response(response, options)
126
- end
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
-
168
- protected
169
-
170
- def upsified_location(location)
171
- if location.country_code == 'US' && US_TERRITORIES_TREATED_AS_COUNTRIES.include?(location.state)
172
- atts = {:country => location.state}
173
- [:zip, :city, :address1, :address2, :address3, :phone, :fax, :address_type].each do |att|
174
- atts[att] = location.send(att)
175
- end
176
- Location.new(atts)
177
- else
178
- location
179
- end
180
- end
181
-
182
- def build_access_request
183
- xml_request = XmlNode.new('AccessRequest') do |access_request|
184
- access_request << XmlNode.new('AccessLicenseNumber', @options[:key])
185
- access_request << XmlNode.new('UserId', @options[:login])
186
- access_request << XmlNode.new('Password', @options[:password])
187
- end
188
- xml_request.to_s
189
- end
190
-
191
- def build_rate_request(origin, destination, packages, options = {})
192
- packages = Array(packages)
193
- xml_request = XmlNode.new('RatingServiceSelectionRequest') do |root_node|
194
- root_node << XmlNode.new('Request') do |request|
195
- request << XmlNode.new('RequestAction', 'Rate')
196
- request << XmlNode.new('RequestOption', 'Shop')
197
- # not implemented: 'Rate' RequestOption to specify a single service query
198
- # request << XmlNode.new('RequestOption', ((options[:service].nil? or options[:service] == :all) ? 'Shop' : 'Rate'))
199
- end
200
-
201
- pickup_type = options[:pickup_type] || :daily_pickup
202
-
203
- root_node << XmlNode.new('PickupType') do |pickup_type_node|
204
- pickup_type_node << XmlNode.new('Code', PICKUP_CODES[pickup_type])
205
- # not implemented: PickupType/PickupDetails element
206
- end
207
- cc = options[:customer_classification] || DEFAULT_CUSTOMER_CLASSIFICATIONS[pickup_type]
208
- root_node << XmlNode.new('CustomerClassification') do |cc_node|
209
- cc_node << XmlNode.new('Code', CUSTOMER_CLASSIFICATIONS[cc])
210
- end
211
-
212
- root_node << XmlNode.new('Shipment') do |shipment|
213
- # not implemented: Shipment/Description element
214
- shipment << build_location_node('Shipper', (options[:shipper] || origin), options)
215
- shipment << build_location_node('ShipTo', destination, options)
216
- if options[:shipper] and options[:shipper] != origin
217
- shipment << build_location_node('ShipFrom', origin, options)
218
- end
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
- packages.each do |package|
230
- options[:imperial] ||= IMPERIAL_COUNTRIES.include?(origin.country_code(:alpha2))
231
- shipment << build_package_node(package, options)
232
- end
233
-
234
- # not implemented: * Shipment/ShipmentServiceOptions element
235
- if options[:origin_account]
236
- shipment << XmlNode.new("RateInformation") do |rate_info_node|
237
- rate_info_node << XmlNode.new("NegotiatedRatesIndicator")
238
- end
239
- end
240
- end
241
- end
242
- xml_request.to_s
243
- end
244
-
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 = {})
349
- xml_request = XmlNode.new('TrackRequest') do |root_node|
350
- root_node << XmlNode.new('Request') do |request|
351
- request << XmlNode.new('RequestAction', 'Track')
352
- request << XmlNode.new('RequestOption', '1')
353
- end
354
- root_node << XmlNode.new('TrackingNumber', tracking_number.to_s)
355
- end
356
- xml_request.to_s
357
- end
358
-
359
- def build_location_node(name, location, options = {})
360
- # not implemented: * Shipment/Shipper/Name element
361
- # * Shipment/(ShipTo|ShipFrom)/CompanyName element
362
- # * Shipment/(Shipper|ShipTo|ShipFrom)/AttentionName element
363
- # * Shipment/(Shipper|ShipTo|ShipFrom)/TaxIdentificationNumber element
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?
371
-
372
- if name == 'Shipper' and (origin_account = options[:origin_account] || @options[:origin_account])
373
- location_node << XmlNode.new('ShipperNumber', origin_account)
374
- elsif name == 'ShipTo' and (destination_account = options[:destination_account] || @options[:destination_account])
375
- location_node << XmlNode.new('ShipperAssignedIdentificationNumber', destination_account)
376
- end
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
-
390
- location_node << XmlNode.new('Address') do |address|
391
- address << XmlNode.new("AddressLine1", location.address1) unless location.address1.blank?
392
- address << XmlNode.new("AddressLine2", location.address2) unless location.address2.blank?
393
- address << XmlNode.new("AddressLine3", location.address3) unless location.address3.blank?
394
- address << XmlNode.new("City", location.city) unless location.city.blank?
395
- address << XmlNode.new("StateProvinceCode", location.province) unless location.province.blank?
396
- # StateProvinceCode required for negotiated rates but not otherwise, for some reason
397
- address << XmlNode.new("PostalCode", location.postal_code) unless location.postal_code.blank?
398
- address << XmlNode.new("CountryCode", location.country_code(:alpha2)) unless location.country_code(:alpha2).blank?
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
400
- # not implemented: Shipment/(Shipper|ShipTo|ShipFrom)/Address/ResidentialAddressIndicator element
401
- end
402
- end
403
- end
404
-
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')
413
- end
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
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
446
- end
447
- end
448
-
449
- def parse_rate_response(origin, destination, packages, response, options = {})
450
- xml = REXML::Document.new(response)
451
- success = response_success?(xml)
452
- message = response_message(xml)
453
-
454
- if success
455
- rate_estimates = []
456
-
457
- xml.elements.each('/*/RatedShipment') do |rated_shipment|
458
- service_code = rated_shipment.get_text('Service/Code').to_s
459
- days_to_delivery = rated_shipment.get_text('GuaranteedDaysToDelivery').to_s.to_i
460
- days_to_delivery = nil if days_to_delivery == 0
461
- rate_estimates << RateEstimate.new(origin, destination, @@name,
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)
470
- end
471
- end
472
- RateResponse.new(success, message, Hash.from_xml(response).values.first, :rates => rate_estimates, :xml => response, :request => last_request)
473
- end
474
-
475
- def parse_tracking_response(response, options = {})
476
- xml = REXML::Document.new(response)
477
- success = response_success?(xml)
478
- message = response_message(xml)
479
-
480
- if success
481
- delivery_signature = nil
482
- exception_event, scheduled_delivery_date, actual_delivery_date = nil
483
- delivered, exception = false
484
- shipment_events = []
485
-
486
- first_shipment = xml.elements['/*/Shipment']
487
- first_package = first_shipment.elements['Package']
488
- tracking_number = first_shipment.get_text('ShipmentIdentificationNumber | Package/TrackingNumber').to_s
489
-
490
- # Build status hash
491
- status_nodes = first_package.elements.to_a('Activity/Status/StatusType')
492
-
493
- # Prefer a delivery node
494
- status_node = status_nodes.detect { |x| x.get_text('Code').to_s == 'D' }
495
- status_node ||= status_nodes.first
496
-
497
- status_code = status_node.get_text('Code').to_s
498
- status_description = status_node.get_text('Description').to_s
499
- status = TRACKING_STATUS_CODES[status_code]
500
-
501
- if status_description =~ /out.*delivery/i
502
- status = :out_for_delivery
503
- end
504
-
505
- origin, destination = %w(Shipper ShipTo).map do |location|
506
- location_from_address_node(first_shipment.elements["#{location}/Address"])
507
- end
508
-
509
- # Get scheduled delivery date
510
- unless status == :delivered
511
- scheduled_delivery_date = parse_ups_datetime(
512
- :date => first_shipment.get_text('ScheduledDeliveryDate'),
513
- :time => nil
514
- )
515
- end
516
-
517
- activities = first_package.get_elements('Activity')
518
- unless activities.empty?
519
- shipment_events = activities.map do |activity|
520
- description = activity.get_text('Status/StatusType/Description').to_s
521
- zoneless_time = parse_ups_datetime(:time => activity.get_text('Time'), :date => activity.get_text('Date'))
522
- location = location_from_address_node(activity.elements['ActivityLocation/Address'])
523
- ShipmentEvent.new(description, zoneless_time, location)
524
- end
525
-
526
- shipment_events = shipment_events.sort_by(&:time)
527
-
528
- # UPS will sometimes archive a shipment, stripping all shipment activity except for the delivery
529
- # event (see test/fixtures/xml/delivered_shipment_without_events_tracking_response.xml for an example).
530
- # This adds an origin event to the shipment activity in such cases.
531
- if origin && !(shipment_events.count == 1 && status == :delivered)
532
- first_event = shipment_events[0]
533
- origin_event = ShipmentEvent.new(first_event.name, first_event.time, origin)
534
-
535
- if within_same_area?(origin, first_event.location)
536
- shipment_events[0] = origin_event
537
- else
538
- shipment_events.unshift(origin_event)
539
- end
540
- end
541
-
542
- # Has the shipment been delivered?
543
- if status == :delivered
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
550
- destination = shipment_events[-1].location
551
- end
552
- shipment_events[-1] = ShipmentEvent.new(shipment_events.last.name, shipment_events.last.time, destination)
553
- end
554
- end
555
-
556
- end
557
- TrackingResponse.new(success, message, Hash.from_xml(response).values.first,
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)
574
- end
575
-
576
- def location_from_address_node(address)
577
- return nil unless address
578
- Location.new(
579
- :country => node_text_or_nil(address.elements['CountryCode']),
580
- :postal_code => node_text_or_nil(address.elements['PostalCode']),
581
- :province => node_text_or_nil(address.elements['StateProvinceCode']),
582
- :city => node_text_or_nil(address.elements['City']),
583
- :address1 => node_text_or_nil(address.elements['AddressLine1']),
584
- :address2 => node_text_or_nil(address.elements['AddressLine2']),
585
- :address3 => node_text_or_nil(address.elements['AddressLine3'])
586
- )
587
- end
588
-
589
- def parse_ups_datetime(options = {})
590
- time, date = options[:time].to_s, options[:date].to_s
591
- if time.nil?
592
- hour, minute, second = 0
593
- else
594
- hour, minute, second = time.scan(/\d{2}/)
595
- end
596
- year, month, day = date[0..3], date[4..5], date[6..7]
597
-
598
- Time.utc(year, month, day, hour, minute, second)
599
- end
600
-
601
- def response_success?(xml)
602
- xml.get_text('/*/Response/ResponseStatusCode').to_s == '1'
603
- end
604
-
605
- def response_message(xml)
606
- xml.get_text('/*/Response/Error/ErrorDescription | /*/Response/ResponseStatusDescription').to_s
607
- end
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
-
625
- def commit(action, request, test = false)
626
- ssl_post("#{test ? TEST_URL : LIVE_URL}/#{RESOURCES[action]}", request)
627
- end
628
-
629
- def within_same_area?(origin, location)
630
- return false unless location
631
- matching_country_codes = origin.country_code(:alpha2) == location.country_code(:alpha2)
632
- matching_or_blank_city = location.city.blank? || location.city == origin.city
633
- matching_country_codes && matching_or_blank_city
634
- end
635
-
636
- def service_name_for(origin, code)
637
- origin = origin.country_code(:alpha2)
638
-
639
- name = case origin
640
- when "CA" then CANADA_ORIGIN_SERVICES[code]
641
- when "MX" then MEXICO_ORIGIN_SERVICES[code]
642
- when *EU_COUNTRY_CODES then EU_ORIGIN_SERVICES[code]
643
- end
644
-
645
- name ||= OTHER_NON_US_ORIGIN_SERVICES[code] unless name == 'US'
646
- name || DEFAULT_SERVICES[code]
647
- end
648
- end
649
- end
650
- end