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,581 +0,0 @@
1
- # FedEx module by Jimmy Baker
2
- # http://github.com/jimmyebaker
3
-
4
- require 'date'
5
- module ActiveMerchant
6
- module Shipping
7
- # :key is your developer API key
8
- # :password is your API password
9
- # :account is your FedEx account number
10
- # :login is your meter number
11
- class FedEx < Carrier
12
- self.retry_safe = true
13
-
14
- cattr_reader :name
15
- @@name = "FedEx"
16
-
17
- TEST_URL = 'https://gatewaybeta.fedex.com:443/xml'
18
- LIVE_URL = 'https://gateway.fedex.com:443/xml'
19
-
20
- CARRIER_CODES = {
21
- "fedex_ground" => "FDXG",
22
- "fedex_express" => "FDXE"
23
- }
24
-
25
- DELIVERY_ADDRESS_NODE_NAMES = %w(DestinationAddress ActualDeliveryAddress)
26
- SHIPPER_ADDRESS_NODE_NAMES = %w(ShipperAddress)
27
-
28
- SERVICE_TYPES = {
29
- "PRIORITY_OVERNIGHT" => "FedEx Priority Overnight",
30
- "PRIORITY_OVERNIGHT_SATURDAY_DELIVERY" => "FedEx Priority Overnight Saturday Delivery",
31
- "FEDEX_2_DAY" => "FedEx 2 Day",
32
- "FEDEX_2_DAY_SATURDAY_DELIVERY" => "FedEx 2 Day Saturday Delivery",
33
- "STANDARD_OVERNIGHT" => "FedEx Standard Overnight",
34
- "FIRST_OVERNIGHT" => "FedEx First Overnight",
35
- "FIRST_OVERNIGHT_SATURDAY_DELIVERY" => "FedEx First Overnight Saturday Delivery",
36
- "FEDEX_EXPRESS_SAVER" => "FedEx Express Saver",
37
- "FEDEX_1_DAY_FREIGHT" => "FedEx 1 Day Freight",
38
- "FEDEX_1_DAY_FREIGHT_SATURDAY_DELIVERY" => "FedEx 1 Day Freight Saturday Delivery",
39
- "FEDEX_2_DAY_FREIGHT" => "FedEx 2 Day Freight",
40
- "FEDEX_2_DAY_FREIGHT_SATURDAY_DELIVERY" => "FedEx 2 Day Freight Saturday Delivery",
41
- "FEDEX_3_DAY_FREIGHT" => "FedEx 3 Day Freight",
42
- "FEDEX_3_DAY_FREIGHT_SATURDAY_DELIVERY" => "FedEx 3 Day Freight Saturday Delivery",
43
- "INTERNATIONAL_PRIORITY" => "FedEx International Priority",
44
- "INTERNATIONAL_PRIORITY_SATURDAY_DELIVERY" => "FedEx International Priority Saturday Delivery",
45
- "INTERNATIONAL_ECONOMY" => "FedEx International Economy",
46
- "INTERNATIONAL_FIRST" => "FedEx International First",
47
- "INTERNATIONAL_PRIORITY_FREIGHT" => "FedEx International Priority Freight",
48
- "INTERNATIONAL_ECONOMY_FREIGHT" => "FedEx International Economy Freight",
49
- "GROUND_HOME_DELIVERY" => "FedEx Ground Home Delivery",
50
- "FEDEX_GROUND" => "FedEx Ground",
51
- "INTERNATIONAL_GROUND" => "FedEx International Ground",
52
- "SMART_POST" => "FedEx SmartPost",
53
- "FEDEX_FREIGHT_PRIORITY" => "FedEx Freight Priority",
54
- "FEDEX_FREIGHT_ECONOMY" => "FedEx Freight Economy"
55
- }
56
-
57
- PACKAGE_TYPES = {
58
- "fedex_envelope" => "FEDEX_ENVELOPE",
59
- "fedex_pak" => "FEDEX_PAK",
60
- "fedex_box" => "FEDEX_BOX",
61
- "fedex_tube" => "FEDEX_TUBE",
62
- "fedex_10_kg_box" => "FEDEX_10KG_BOX",
63
- "fedex_25_kg_box" => "FEDEX_25KG_BOX",
64
- "your_packaging" => "YOUR_PACKAGING"
65
- }
66
-
67
- DROPOFF_TYPES = {
68
- 'regular_pickup' => 'REGULAR_PICKUP',
69
- 'request_courier' => 'REQUEST_COURIER',
70
- 'dropbox' => 'DROP_BOX',
71
- 'business_service_center' => 'BUSINESS_SERVICE_CENTER',
72
- 'station' => 'STATION'
73
- }
74
-
75
- PAYMENT_TYPES = {
76
- 'sender' => 'SENDER',
77
- 'recipient' => 'RECIPIENT',
78
- 'third_party' => 'THIRDPARTY',
79
- 'collect' => 'COLLECT'
80
- }
81
-
82
- PACKAGE_IDENTIFIER_TYPES = {
83
- 'tracking_number' => 'TRACKING_NUMBER_OR_DOORTAG',
84
- 'door_tag' => 'TRACKING_NUMBER_OR_DOORTAG',
85
- 'rma' => 'RMA',
86
- 'ground_shipment_id' => 'GROUND_SHIPMENT_ID',
87
- 'ground_invoice_number' => 'GROUND_INVOICE_NUMBER',
88
- 'ground_customer_reference' => 'GROUND_CUSTOMER_REFERENCE',
89
- 'ground_po' => 'GROUND_PO',
90
- 'express_reference' => 'EXPRESS_REFERENCE',
91
- 'express_mps_master' => 'EXPRESS_MPS_MASTER'
92
- }
93
-
94
- TRANSIT_TIMES = %w(UNKNOWN ONE_DAY TWO_DAYS THREE_DAYS FOUR_DAYS FIVE_DAYS SIX_DAYS SEVEN_DAYS EIGHT_DAYS NINE_DAYS TEN_DAYS ELEVEN_DAYS TWELVE_DAYS THIRTEEN_DAYS FOURTEEN_DAYS FIFTEEN_DAYS SIXTEEN_DAYS SEVENTEEN_DAYS EIGHTEEN_DAYS)
95
-
96
- # FedEx tracking codes as described in the FedEx Tracking Service WSDL Guide
97
- # All delays also have been marked as exceptions
98
- TRACKING_STATUS_CODES = HashWithIndifferentAccess.new(
99
- 'AA' => :at_airport,
100
- 'AD' => :at_delivery,
101
- 'AF' => :at_fedex_facility,
102
- 'AR' => :at_fedex_facility,
103
- 'AP' => :at_pickup,
104
- 'CA' => :canceled,
105
- 'CH' => :location_changed,
106
- 'DE' => :exception,
107
- 'DL' => :delivered,
108
- 'DP' => :departed_fedex_location,
109
- 'DR' => :vehicle_furnished_not_used,
110
- 'DS' => :vehicle_dispatched,
111
- 'DY' => :exception,
112
- 'EA' => :exception,
113
- 'ED' => :enroute_to_delivery,
114
- 'EO' => :enroute_to_origin_airport,
115
- 'EP' => :enroute_to_pickup,
116
- 'FD' => :at_fedex_destination,
117
- 'HL' => :held_at_location,
118
- 'IT' => :in_transit,
119
- 'LO' => :left_origin,
120
- 'OC' => :order_created,
121
- 'OD' => :out_for_delivery,
122
- 'PF' => :plane_in_flight,
123
- 'PL' => :plane_landed,
124
- 'PU' => :picked_up,
125
- 'RS' => :return_to_shipper,
126
- 'SE' => :exception,
127
- 'SF' => :at_sort_facility,
128
- 'SP' => :split_status,
129
- 'TR' => :transfer
130
- )
131
-
132
- def self.service_name_for_code(service_code)
133
- SERVICE_TYPES[service_code] || "FedEx #{service_code.titleize.sub(/Fedex /, '')}"
134
- end
135
-
136
- def requirements
137
- [:key, :password, :account, :login]
138
- end
139
-
140
- def find_rates(origin, destination, packages, options = {})
141
- options = @options.update(options)
142
- packages = Array(packages)
143
-
144
- rate_request = build_rate_request(origin, destination, packages, options)
145
-
146
- xml = commit(save_request(rate_request), (options[:test] || false))
147
- response = remove_version_prefix(xml)
148
-
149
- parse_rate_response(origin, destination, packages, response, options)
150
- end
151
-
152
- def find_tracking_info(tracking_number, options = {})
153
- options = @options.update(options)
154
-
155
- tracking_request = build_tracking_request(tracking_number, options)
156
- xml = commit(save_request(tracking_request), (options[:test] || false))
157
- response = remove_version_prefix(xml)
158
- parse_tracking_response(response, options)
159
- end
160
-
161
- protected
162
-
163
- def build_rate_request(origin, destination, packages, options = {})
164
- imperial = %w(US LR MM).include?(origin.country_code(:alpha2))
165
-
166
- xml_request = XmlNode.new('RateRequest', 'xmlns' => 'http://fedex.com/ws/rate/v13') do |root_node|
167
- root_node << build_request_header
168
- root_node << build_version_node
169
-
170
- # Returns delivery dates
171
- root_node << XmlNode.new('ReturnTransitAndCommit', true)
172
- # Returns saturday delivery shipping options when available
173
- root_node << XmlNode.new('VariableOptions', 'SATURDAY_DELIVERY')
174
-
175
- root_node << XmlNode.new('RequestedShipment') do |rs|
176
- rs << XmlNode.new('ShipTimestamp', ship_timestamp(options[:turn_around_time]))
177
-
178
- freight = has_freight?(options)
179
-
180
- unless freight
181
- # fedex api wants this up here otherwise request returns an error
182
- rs << XmlNode.new('DropoffType', options[:dropoff_type] || 'REGULAR_PICKUP')
183
- rs << XmlNode.new('PackagingType', options[:packaging_type] || 'YOUR_PACKAGING')
184
- end
185
-
186
- rs << build_location_node('Shipper', (options[:shipper] || origin))
187
- rs << build_location_node('Recipient', destination)
188
- if options[:shipper] and options[:shipper] != origin
189
- rs << build_location_node('Origin', origin)
190
- end
191
-
192
- if freight
193
- # build xml for freight rate requests
194
- freight_options = options[:freight]
195
- rs << build_shipping_charges_payment_node(freight_options)
196
- rs << build_freight_shipment_detail_node(freight_options, packages, imperial)
197
- rs << build_rate_request_types_node
198
- else
199
- # build xml for non-freight rate requests
200
- rs << XmlNode.new('SmartPostDetail') do |spd|
201
- spd << XmlNode.new('Indicia', options[:smart_post_indicia] || 'PARCEL_SELECT')
202
- spd << XmlNode.new('HubId', options[:smart_post_hub_id] || 5902) # default to LA
203
- end
204
-
205
- rs << build_rate_request_types_node
206
- rs << XmlNode.new('PackageCount', packages.size)
207
- rs << build_packages_nodes(packages, imperial)
208
-
209
- end
210
- end
211
- end
212
- xml_request.to_s
213
- end
214
-
215
- def build_packages_nodes(packages, imperial)
216
- packages.map do |pkg|
217
- XmlNode.new('RequestedPackageLineItems') do |rps|
218
- rps << XmlNode.new('GroupPackageCount', 1)
219
- rps << build_package_weight_node(pkg, imperial)
220
- rps << build_package_dimensions_node(pkg, imperial)
221
- end
222
- end
223
- end
224
-
225
- def build_shipping_charges_payment_node(freight_options)
226
- XmlNode.new('ShippingChargesPayment') do |shipping_charges_payment|
227
- shipping_charges_payment << XmlNode.new('PaymentType', freight_options[:payment_type])
228
- shipping_charges_payment << XmlNode.new('Payor') do |payor|
229
- payor << XmlNode.new('ResponsibleParty') do |responsible_party|
230
- # TODO: case of different freight account numbers?
231
- responsible_party << XmlNode.new('AccountNumber', freight_options[:account])
232
- end
233
- end
234
- end
235
- end
236
-
237
- def build_freight_shipment_detail_node(freight_options, packages, imperial)
238
- XmlNode.new('FreightShipmentDetail') do |freight_shipment_detail|
239
- # TODO: case of different freight account numbers?
240
- freight_shipment_detail << XmlNode.new('FedExFreightAccountNumber', freight_options[:account])
241
- freight_shipment_detail << build_location_node('FedExFreightBillingContactAndAddress', freight_options[:billing_location])
242
- freight_shipment_detail << XmlNode.new('Role', freight_options[:role])
243
-
244
- packages.each do |pkg|
245
- freight_shipment_detail << XmlNode.new('LineItems') do |line_items|
246
- line_items << XmlNode.new('FreightClass', freight_options[:freight_class])
247
- line_items << XmlNode.new('Packaging', freight_options[:packaging])
248
- line_items << build_package_weight_node(pkg, imperial)
249
- line_items << build_package_dimensions_node(pkg, imperial)
250
- end
251
- end
252
- end
253
- end
254
-
255
- def has_freight?(options)
256
- options[:freight] && options[:freight].present?
257
- end
258
-
259
- def build_package_weight_node(pkg, imperial)
260
- XmlNode.new('Weight') do |tw|
261
- tw << XmlNode.new('Units', imperial ? 'LB' : 'KG')
262
- tw << XmlNode.new('Value', [((imperial ? pkg.lbs : pkg.kgs).to_f * 1000).round / 1000.0, 0.1].max)
263
- end
264
- end
265
-
266
- def build_version_node
267
- XmlNode.new('Version') do |version_node|
268
- version_node << XmlNode.new('ServiceId', 'crs')
269
- version_node << XmlNode.new('Major', '13')
270
- version_node << XmlNode.new('Intermediate', '0')
271
- version_node << XmlNode.new('Minor', '0')
272
- end
273
- end
274
-
275
- def build_package_dimensions_node(pkg, imperial)
276
- XmlNode.new('Dimensions') do |dimensions|
277
- [:length, :width, :height].each do |axis|
278
- value = ((imperial ? pkg.inches(axis) : pkg.cm(axis)).to_f * 1000).round / 1000.0 # 3 decimals
279
- dimensions << XmlNode.new(axis.to_s.capitalize, value.ceil)
280
- end
281
- dimensions << XmlNode.new('Units', imperial ? 'IN' : 'CM')
282
- end
283
- end
284
-
285
- def build_rate_request_types_node(type = 'ACCOUNT')
286
- XmlNode.new('RateRequestTypes', type)
287
- end
288
-
289
- def build_tracking_request(tracking_number, options = {})
290
- xml_request = XmlNode.new('TrackRequest', 'xmlns' => 'http://fedex.com/ws/track/v3') do |root_node|
291
- root_node << build_request_header
292
-
293
- # Version
294
- root_node << XmlNode.new('Version') do |version_node|
295
- version_node << XmlNode.new('ServiceId', 'trck')
296
- version_node << XmlNode.new('Major', '3')
297
- version_node << XmlNode.new('Intermediate', '0')
298
- version_node << XmlNode.new('Minor', '0')
299
- end
300
-
301
- root_node << XmlNode.new('PackageIdentifier') do |package_node|
302
- package_node << XmlNode.new('Value', tracking_number)
303
- package_node << XmlNode.new('Type', PACKAGE_IDENTIFIER_TYPES[options['package_identifier_type'] || 'tracking_number'])
304
- end
305
-
306
- root_node << XmlNode.new('ShipDateRangeBegin', options['ship_date_range_begin']) if options['ship_date_range_begin']
307
- root_node << XmlNode.new('ShipDateRangeEnd', options['ship_date_range_end']) if options['ship_date_range_end']
308
- root_node << XmlNode.new('IncludeDetailedScans', 1)
309
- end
310
- xml_request.to_s
311
- end
312
-
313
- def build_request_header
314
- web_authentication_detail = XmlNode.new('WebAuthenticationDetail') do |wad|
315
- wad << XmlNode.new('UserCredential') do |uc|
316
- uc << XmlNode.new('Key', @options[:key])
317
- uc << XmlNode.new('Password', @options[:password])
318
- end
319
- end
320
-
321
- client_detail = XmlNode.new('ClientDetail') do |cd|
322
- cd << XmlNode.new('AccountNumber', @options[:account])
323
- cd << XmlNode.new('MeterNumber', @options[:login])
324
- end
325
-
326
- trasaction_detail = XmlNode.new('TransactionDetail') do |td|
327
- td << XmlNode.new('CustomerTransactionId', @options[:transaction_id] || 'ActiveShipping') # TODO: Need to do something better with this..
328
- end
329
-
330
- [web_authentication_detail, client_detail, trasaction_detail]
331
- end
332
-
333
- def build_location_node(name, location)
334
- XmlNode.new(name) do |xml_node|
335
- xml_node << XmlNode.new('Address') do |address_node|
336
- address_node << XmlNode.new('StreetLines', location.address1) if location.address1
337
- address_node << XmlNode.new('StreetLines', location.address2) if location.address2
338
- address_node << XmlNode.new('City', location.city) if location.city
339
- address_node << XmlNode.new('PostalCode', location.postal_code)
340
- address_node << XmlNode.new("CountryCode", location.country_code(:alpha2))
341
-
342
- address_node << XmlNode.new("Residential", true) unless location.commercial?
343
- end
344
- end
345
- end
346
-
347
- def parse_rate_response(origin, destination, packages, response, options)
348
- rate_estimates = []
349
-
350
- xml = build_document(response)
351
- root_node = xml.elements['RateReply']
352
-
353
- success = response_success?(xml)
354
- message = response_message(xml)
355
-
356
- raise ActiveMerchant::Shipping::ResponseContentError.new(StandardError.new('Invalid document'), xml) unless root_node
357
-
358
- root_node.elements.each('RateReplyDetails') do |rated_shipment|
359
- service_code = rated_shipment.get_text('ServiceType').to_s
360
- is_saturday_delivery = rated_shipment.get_text('AppliedOptions').to_s == 'SATURDAY_DELIVERY'
361
- service_type = is_saturday_delivery ? "#{service_code}_SATURDAY_DELIVERY" : service_code
362
-
363
- transit_time = rated_shipment.get_text('TransitTime').to_s if service_code == "FEDEX_GROUND"
364
- max_transit_time = rated_shipment.get_text('MaximumTransitTime').to_s if service_code == "FEDEX_GROUND"
365
-
366
- delivery_timestamp = rated_shipment.get_text('DeliveryTimestamp').to_s
367
-
368
- delivery_range = delivery_range_from(transit_time, max_transit_time, delivery_timestamp, options)
369
-
370
- currency = rated_shipment.get_text('RatedShipmentDetails/ShipmentRateDetail/TotalNetCharge/Currency').to_s
371
- rate_estimates << RateEstimate.new(origin, destination, @@name,
372
- self.class.service_name_for_code(service_type),
373
- :service_code => service_code,
374
- :total_price => rated_shipment.get_text('RatedShipmentDetails/ShipmentRateDetail/TotalNetCharge/Amount').to_s.to_f,
375
- :currency => currency,
376
- :packages => packages,
377
- :delivery_range => delivery_range)
378
- end
379
-
380
- if rate_estimates.empty?
381
- success = false
382
- message = "No shipping rates could be found for the destination address" if message.blank?
383
- end
384
-
385
- RateResponse.new(success, message, Hash.from_xml(response), :rates => rate_estimates, :xml => response, :request => last_request, :log_xml => options[:log_xml])
386
- end
387
-
388
- def delivery_range_from(transit_time, max_transit_time, delivery_timestamp, options)
389
- delivery_range = [delivery_timestamp, delivery_timestamp]
390
-
391
- # if there's no delivery timestamp but we do have a transit time, use it
392
- if delivery_timestamp.blank? && transit_time.present?
393
- transit_range = parse_transit_times([transit_time, max_transit_time.presence || transit_time])
394
- delivery_range = transit_range.map { |days| business_days_from(ship_date(options[:turn_around_time]), days) }
395
- end
396
-
397
- delivery_range
398
- end
399
-
400
- def business_days_from(date, days)
401
- future_date = date
402
- count = 0
403
-
404
- while count < days
405
- future_date += 1.day
406
- count += 1 if business_day?(future_date)
407
- end
408
-
409
- future_date
410
- end
411
-
412
- def business_day?(date)
413
- (1..5).include?(date.wday)
414
- end
415
-
416
- def parse_tracking_response(response, options)
417
- xml = build_document(response)
418
- root_node = xml.elements['TrackReply']
419
-
420
- success = response_success?(xml)
421
- message = response_message(xml)
422
-
423
- if success
424
- origin = nil
425
- delivery_signature = nil
426
- shipment_events = []
427
-
428
- tracking_details = root_node.elements['TrackDetails']
429
- tracking_number = tracking_details.get_text('TrackingNumber').to_s
430
- status_code = tracking_details.get_text('StatusCode').to_s
431
- status_description = tracking_details.get_text('StatusDescription').to_s
432
- status = TRACKING_STATUS_CODES[status_code]
433
-
434
- if status_code == 'DL' && tracking_details.get_text('SignatureProofOfDeliveryAvailable').to_s == 'true'
435
- delivery_signature = tracking_details.get_text('DeliverySignatureName').to_s
436
- end
437
-
438
- origin_node = tracking_details.elements['OriginLocationAddress']
439
-
440
- if origin_node
441
- origin = Location.new(
442
- :country => origin_node.get_text('CountryCode').to_s,
443
- :province => origin_node.get_text('StateOrProvinceCode').to_s,
444
- :city => origin_node.get_text('City').to_s
445
- )
446
- end
447
-
448
- destination = extract_address(tracking_details, DELIVERY_ADDRESS_NODE_NAMES)
449
- shipper_address = extract_address(tracking_details, SHIPPER_ADDRESS_NODE_NAMES)
450
-
451
- ship_time = extract_timestamp(tracking_details, 'ShipTimestamp')
452
- actual_delivery_time = extract_timestamp(tracking_details, 'ActualDeliveryTimestamp')
453
- scheduled_delivery_time = extract_timestamp(tracking_details, 'EstimatedDeliveryTimestamp')
454
-
455
- tracking_details.elements.each('Events') do |event|
456
- address = event.elements['Address']
457
-
458
- city = address.get_text('City').to_s
459
- state = address.get_text('StateOrProvinceCode').to_s
460
- zip_code = address.get_text('PostalCode').to_s
461
- country = address.get_text('CountryCode').to_s
462
- next if country.blank?
463
-
464
- location = Location.new(:city => city, :state => state, :postal_code => zip_code, :country => country)
465
- description = event.get_text('EventDescription').to_s
466
-
467
- time = Time.parse("#{event.get_text('Timestamp').to_s}")
468
- zoneless_time = time.utc
469
-
470
- shipment_events << ShipmentEvent.new(description, zoneless_time, location)
471
- end
472
- shipment_events = shipment_events.sort_by(&:time)
473
-
474
- end
475
-
476
- TrackingResponse.new(success, message, Hash.from_xml(response),
477
- :carrier => @@name,
478
- :xml => response,
479
- :request => last_request,
480
- :status => status,
481
- :status_code => status_code,
482
- :status_description => status_description,
483
- :ship_time => ship_time,
484
- :scheduled_delivery_date => scheduled_delivery_time,
485
- :actual_delivery_date => actual_delivery_time,
486
- :delivery_signature => delivery_signature,
487
- :shipment_events => shipment_events,
488
- :shipper_address => (shipper_address.nil? || shipper_address.unknown?) ? nil : shipper_address,
489
- :origin => origin,
490
- :destination => destination,
491
- :tracking_number => tracking_number
492
- )
493
- end
494
-
495
- def ship_timestamp(delay_in_hours)
496
- delay_in_hours ||= 0
497
- Time.now + delay_in_hours.hours
498
- end
499
-
500
- def ship_date(delay_in_hours)
501
- delay_in_hours ||= 0
502
- (Time.now + delay_in_hours.hours).to_date
503
- end
504
-
505
- def response_status_node(document)
506
- document.elements['/*/Notifications/']
507
- end
508
-
509
- def response_success?(document)
510
- response_node = response_status_node(document)
511
- return false if response_node.nil?
512
-
513
- %w(SUCCESS WARNING NOTE).include? response_node.get_text('Severity').to_s
514
- end
515
-
516
- def response_message(document)
517
- response_node = response_status_node(document)
518
- return "" if response_node.nil?
519
-
520
- "#{response_node.get_text('Severity')} - #{response_node.get_text('Code')}: #{response_node.get_text('Message')}"
521
- end
522
-
523
- def commit(request, test = false)
524
- ssl_post(test ? TEST_URL : LIVE_URL, request.gsub("\n", ''))
525
- end
526
-
527
- def parse_transit_times(times)
528
- results = []
529
- times.each do |day_count|
530
- days = TRANSIT_TIMES.index(day_count.to_s.chomp)
531
- results << days.to_i
532
- end
533
- results
534
- end
535
-
536
- def extract_address(document, possible_node_names)
537
- node = nil
538
- possible_node_names.each do |name|
539
- node ||= document.elements[name]
540
- break if node
541
- end
542
-
543
- args = if node && node.elements['CountryCode']
544
- {
545
- :country => node.get_text('CountryCode').to_s,
546
- :province => node.get_text('StateOrProvinceCode').to_s,
547
- :city => node.get_text('City').to_s
548
- }
549
- else
550
- {
551
- :country => ActiveMerchant::Country.new(:alpha2 => 'ZZ', :name => 'Unknown or Invalid Territory', :alpha3 => 'ZZZ', :numeric => '999'),
552
- :province => 'unknown',
553
- :city => 'unknown'
554
- }
555
- end
556
-
557
- Location.new(args)
558
- end
559
-
560
- def extract_timestamp(document, node_name)
561
- if timestamp_node = document.elements[node_name]
562
- Time.parse(timestamp_node.to_s).utc
563
- end
564
- end
565
-
566
- def remove_version_prefix(xml)
567
- if xml =~ /xmlns:v[0-9]/
568
- xml.gsub(/<(\/)?.*?\:(.*?)>/, '<\1\2>')
569
- else
570
- xml
571
- end
572
- end
573
-
574
- def build_document(xml)
575
- REXML::Document.new(xml)
576
- rescue REXML::ParseException => e
577
- raise ActiveMerchant::Shipping::ResponseContentError.new(e, xml)
578
- end
579
- end
580
- end
581
- end