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