active_shipping 0.12.6 → 1.0.0.pre1

Sign up to get free protection for your applications and to get access to all the features.
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