reactive_shipping 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (247) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +15 -0
  3. data/.travis.yml +33 -0
  4. data/.yardopts +13 -0
  5. data/CHANGELOG.md +225 -0
  6. data/CONTRIBUTING.md +23 -0
  7. data/Gemfile +3 -0
  8. data/MIT-LICENSE +21 -0
  9. data/README.md +158 -0
  10. data/Rakefile +35 -0
  11. data/dev.yml +17 -0
  12. data/gemfiles/activesupport42.gemfile +5 -0
  13. data/gemfiles/activesupport50.gemfile +6 -0
  14. data/gemfiles/activesupport51.gemfile +5 -0
  15. data/gemfiles/activesupport52.gemfile +5 -0
  16. data/gemfiles/activesupport_master.gemfile +5 -0
  17. data/lib/certs/eParcel.dtd +111 -0
  18. data/lib/reactive_shipping.rb +26 -0
  19. data/lib/reactive_shipping/address_validation_response.rb +30 -0
  20. data/lib/reactive_shipping/carrier.rb +184 -0
  21. data/lib/reactive_shipping/carriers.rb +35 -0
  22. data/lib/reactive_shipping/carriers/australia_post.rb +248 -0
  23. data/lib/reactive_shipping/carriers/benchmark_carrier.rb +31 -0
  24. data/lib/reactive_shipping/carriers/bogus_carrier.rb +12 -0
  25. data/lib/reactive_shipping/carriers/canada_post.rb +263 -0
  26. data/lib/reactive_shipping/carriers/canada_post_pws.rb +908 -0
  27. data/lib/reactive_shipping/carriers/fedex.rb +797 -0
  28. data/lib/reactive_shipping/carriers/kunaki.rb +155 -0
  29. data/lib/reactive_shipping/carriers/new_zealand_post.rb +260 -0
  30. data/lib/reactive_shipping/carriers/shipwire.rb +178 -0
  31. data/lib/reactive_shipping/carriers/stamps.rb +860 -0
  32. data/lib/reactive_shipping/carriers/ups.rb +1060 -0
  33. data/lib/reactive_shipping/carriers/usps.rb +708 -0
  34. data/lib/reactive_shipping/carriers/usps_returns.rb +86 -0
  35. data/lib/reactive_shipping/delivery_date_estimate.rb +20 -0
  36. data/lib/reactive_shipping/delivery_date_estimates_response.rb +11 -0
  37. data/lib/reactive_shipping/errors.rb +35 -0
  38. data/lib/reactive_shipping/external_return_label_request.rb +417 -0
  39. data/lib/reactive_shipping/external_return_label_response.rb +26 -0
  40. data/lib/reactive_shipping/label.rb +10 -0
  41. data/lib/reactive_shipping/label_response.rb +10 -0
  42. data/lib/reactive_shipping/location.rb +166 -0
  43. data/lib/reactive_shipping/package.rb +165 -0
  44. data/lib/reactive_shipping/package_item.rb +60 -0
  45. data/lib/reactive_shipping/rate_estimate.rb +197 -0
  46. data/lib/reactive_shipping/rate_response.rb +33 -0
  47. data/lib/reactive_shipping/response.rb +44 -0
  48. data/lib/reactive_shipping/shipment_event.rb +22 -0
  49. data/lib/reactive_shipping/shipment_packer.rb +108 -0
  50. data/lib/reactive_shipping/shipping_response.rb +34 -0
  51. data/lib/reactive_shipping/tracking_response.rb +120 -0
  52. data/lib/reactive_shipping/version.rb +3 -0
  53. data/reactive_shipping.gemspec +38 -0
  54. data/shipit.rubygems.yml +1 -0
  55. data/test/console.rb +39 -0
  56. data/test/credentials.yml +76 -0
  57. data/test/fixtures/files/label1.pdf +0 -0
  58. data/test/fixtures/files/ups-shipping-label.gif +0 -0
  59. data/test/fixtures/json/australia_post/calculate_domestic.json +13 -0
  60. data/test/fixtures/json/australia_post/calculate_domestic_2.json +19 -0
  61. data/test/fixtures/json/australia_post/calculate_international.json +12 -0
  62. data/test/fixtures/json/australia_post/calculate_international_2.json +15 -0
  63. data/test/fixtures/json/australia_post/error_message.json +5 -0
  64. data/test/fixtures/json/australia_post/service_domestic.json +117 -0
  65. data/test/fixtures/json/australia_post/service_domestic_2.json +117 -0
  66. data/test/fixtures/json/australia_post/service_international.json +76 -0
  67. data/test/fixtures/json/australia_post/service_international_2.json +59 -0
  68. data/test/fixtures/json/newzealandpost/domestic_book.json +1 -0
  69. data/test/fixtures/json/newzealandpost/domestic_default.json +1 -0
  70. data/test/fixtures/json/newzealandpost/domestic_error.json +1 -0
  71. data/test/fixtures/json/newzealandpost/domestic_poster.json +1 -0
  72. data/test/fixtures/json/newzealandpost/domestic_small_half_pound.json +1 -0
  73. data/test/fixtures/json/newzealandpost/international_book.json +1 -0
  74. data/test/fixtures/json/newzealandpost/international_new_zealand_wii.json +1 -0
  75. data/test/fixtures/json/newzealandpost/international_small_half_pound.json +1 -0
  76. data/test/fixtures/json/newzealandpost/international_wii.json +1 -0
  77. data/test/fixtures/xml/canadapost/example_request.xml +25 -0
  78. data/test/fixtures/xml/canadapost/example_response.xml +130 -0
  79. data/test/fixtures/xml/canadapost/example_response_error.xml +16 -0
  80. data/test/fixtures/xml/canadapost/example_response_french.xml +122 -0
  81. data/test/fixtures/xml/canadapost/example_response_with_nil_value.xml +164 -0
  82. data/test/fixtures/xml/canadapost/example_response_with_postal_outlet.xml +155 -0
  83. data/test/fixtures/xml/canadapost/example_response_with_postal_outlet_french.xml +274 -0
  84. data/test/fixtures/xml/canadapost/example_response_with_strange_delivery_date.xml +130 -0
  85. data/test/fixtures/xml/canadapost_pws/dnc_tracking_details_en.xml +112 -0
  86. data/test/fixtures/xml/canadapost_pws/merchant_details_error.xml +7 -0
  87. data/test/fixtures/xml/canadapost_pws/merchant_details_response.xml +7 -0
  88. data/test/fixtures/xml/canadapost_pws/option_response.xml +13 -0
  89. data/test/fixtures/xml/canadapost_pws/option_response_no_conflicts.xml +7 -0
  90. data/test/fixtures/xml/canadapost_pws/rates_info.xml +190 -0
  91. data/test/fixtures/xml/canadapost_pws/rates_info_error.xml +7 -0
  92. data/test/fixtures/xml/canadapost_pws/receipt_response.xml +42 -0
  93. data/test/fixtures/xml/canadapost_pws/receipt_response_no_priced_options.xml +36 -0
  94. data/test/fixtures/xml/canadapost_pws/register_token_error.xml +7 -0
  95. data/test/fixtures/xml/canadapost_pws/register_token_response.xml +3 -0
  96. data/test/fixtures/xml/canadapost_pws/service_options_response.xml +42 -0
  97. data/test/fixtures/xml/canadapost_pws/services_error.xml +6 -0
  98. data/test/fixtures/xml/canadapost_pws/services_response.xml +32 -0
  99. data/test/fixtures/xml/canadapost_pws/shipment_domestic.xml +69 -0
  100. data/test/fixtures/xml/canadapost_pws/shipment_response.xml +20 -0
  101. data/test/fixtures/xml/canadapost_pws/shipment_us.xml +69 -0
  102. data/test/fixtures/xml/canadapost_pws/tracking_details_en.xml +152 -0
  103. data/test/fixtures/xml/canadapost_pws/tracking_details_en_error.xml +7 -0
  104. data/test/fixtures/xml/canadapost_pws/tracking_details_en_undelivered.xml +116 -0
  105. data/test/fixtures/xml/canadapost_pws/tracking_details_fr.xml +156 -0
  106. data/test/fixtures/xml/canadapost_pws/tracking_details_no_expected_delivery_date.xml +40 -0
  107. data/test/fixtures/xml/fedex/create_shipment_response.xml +2 -0
  108. data/test/fixtures/xml/fedex/freight_rate_request.xml +82 -0
  109. data/test/fixtures/xml/fedex/freight_rate_response.xml +506 -0
  110. data/test/fixtures/xml/fedex/invalid_fedex_reply.xml +27 -0
  111. data/test/fixtures/xml/fedex/ottawa_to_beverly_hills_commercial_rate_request.xml +79 -0
  112. data/test/fixtures/xml/fedex/ottawa_to_beverly_hills_no_saturday_rate_request.xml +79 -0
  113. data/test/fixtures/xml/fedex/ottawa_to_beverly_hills_rate_request.xml +80 -0
  114. data/test/fixtures/xml/fedex/ottawa_to_beverly_hills_rate_response.xml +214 -0
  115. data/test/fixtures/xml/fedex/raterequest_reply.xml +213 -0
  116. data/test/fixtures/xml/fedex/raterequest_response_with_ground_home_delivery.xml +206 -0
  117. data/test/fixtures/xml/fedex/reply_without_notifications.xml +185 -0
  118. data/test/fixtures/xml/fedex/tracking_request.xml +29 -0
  119. data/test/fixtures/xml/fedex/tracking_response_bad_tracking_number.xml +20 -0
  120. data/test/fixtures/xml/fedex/tracking_response_delivered_at_door.xml +254 -0
  121. data/test/fixtures/xml/fedex/tracking_response_delivered_at_facility.xml +403 -0
  122. data/test/fixtures/xml/fedex/tracking_response_delivered_with_signature.xml +269 -0
  123. data/test/fixtures/xml/fedex/tracking_response_empty_status_detail.xml +84 -0
  124. data/test/fixtures/xml/fedex/tracking_response_failure_code_9045.xml +52 -0
  125. data/test/fixtures/xml/fedex/tracking_response_failure_code_9080.xml +51 -0
  126. data/test/fixtures/xml/fedex/tracking_response_in_transit.xml +127 -0
  127. data/test/fixtures/xml/fedex/tracking_response_invalid_tracking_number.xml +52 -0
  128. data/test/fixtures/xml/fedex/tracking_response_missing_status_code.xml +89 -0
  129. data/test/fixtures/xml/fedex/tracking_response_multiple_results.xml +100 -0
  130. data/test/fixtures/xml/fedex/tracking_response_not_found.xml +52 -0
  131. data/test/fixtures/xml/fedex/tracking_response_shipment_exception.xml +209 -0
  132. data/test/fixtures/xml/fedex/tracking_response_unable_to_process.xml +32 -0
  133. data/test/fixtures/xml/fedex/tracking_response_with_blank_state.xml +107 -0
  134. data/test/fixtures/xml/fedex/unknown_fedex_document_reply.xml +3 -0
  135. data/test/fixtures/xml/kunaki/invalid_state_response.xml +3 -0
  136. data/test/fixtures/xml/kunaki/no_valid_items_response.xml +3 -0
  137. data/test/fixtures/xml/kunaki/successful_rates_response.xml +3 -0
  138. data/test/fixtures/xml/kunaki/unsuccessful_rates_response.xml +9 -0
  139. data/test/fixtures/xml/shipwire/international_rates_response.xml +17 -0
  140. data/test/fixtures/xml/shipwire/new_carrier_rate_response.xml +18 -0
  141. data/test/fixtures/xml/shipwire/no_rates_response.xml +7 -0
  142. data/test/fixtures/xml/shipwire/rates_response.xml +36 -0
  143. data/test/fixtures/xml/shipwire/rates_response_no_estimate.xml +14 -0
  144. data/test/fixtures/xml/stamps/authenticate_user_request.xml +15 -0
  145. data/test/fixtures/xml/stamps/authenticate_user_response.xml +10 -0
  146. data/test/fixtures/xml/stamps/cleanse_address_request.xml +19 -0
  147. data/test/fixtures/xml/stamps/cleanse_address_response.xml +27 -0
  148. data/test/fixtures/xml/stamps/create_indicium_request.xml +69 -0
  149. data/test/fixtures/xml/stamps/create_indicium_response.xml +40 -0
  150. data/test/fixtures/xml/stamps/expired_authenticator_response.xml +15 -0
  151. data/test/fixtures/xml/stamps/get_account_info_request.xml +11 -0
  152. data/test/fixtures/xml/stamps/get_account_info_response.xml +36 -0
  153. data/test/fixtures/xml/stamps/get_purchase_status_request.xml +12 -0
  154. data/test/fixtures/xml/stamps/get_purchase_status_response.xml +16 -0
  155. data/test/fixtures/xml/stamps/get_rates_request.xml +19 -0
  156. data/test/fixtures/xml/stamps/get_rates_response.xml +351 -0
  157. data/test/fixtures/xml/stamps/purchase_postage_request.xml +13 -0
  158. data/test/fixtures/xml/stamps/purchase_postage_response.xml +17 -0
  159. data/test/fixtures/xml/stamps/track_shipment_request.xml +12 -0
  160. data/test/fixtures/xml/stamps/track_shipment_response.xml +45 -0
  161. data/test/fixtures/xml/ups/access_request.xml +6 -0
  162. data/test/fixtures/xml/ups/delivered_shipment_with_refund.xml +290 -0
  163. data/test/fixtures/xml/ups/delivered_shipment_without_events_tracking_response.xml +62 -0
  164. data/test/fixtures/xml/ups/delivery_dates_response.xml +140 -0
  165. data/test/fixtures/xml/ups/example_tracking_response.xml +53 -0
  166. data/test/fixtures/xml/ups/in_transit_shipment.xml +183 -0
  167. data/test/fixtures/xml/ups/out_for_delivery_shipment.xml +165 -0
  168. data/test/fixtures/xml/ups/package_exceeds_maximum_length.xml +12 -0
  169. data/test/fixtures/xml/ups/rate_single_service.xml +54 -0
  170. data/test/fixtures/xml/ups/rescheduled_shipment.xml +204 -0
  171. data/test/fixtures/xml/ups/shipment_accept_response.xml +42 -0
  172. data/test/fixtures/xml/ups/shipment_confirm_response.xml +33 -0
  173. data/test/fixtures/xml/ups/shipment_from_tiger_direct.xml +222 -0
  174. data/test/fixtures/xml/ups/test_real_home_as_residential_destination_response.xml +290 -0
  175. data/test/fixtures/xml/ups/test_real_home_as_residential_destination_response_with_insured.xml +289 -0
  176. data/test/fixtures/xml/ups/test_real_home_as_residential_destination_with_origin_account_response.xml +311 -0
  177. data/test/fixtures/xml/ups/tracking_request.xml +9 -0
  178. data/test/fixtures/xml/ups/triple_accept_response.xml +72 -0
  179. data/test/fixtures/xml/ups/triple_confirm_response.xml +32 -0
  180. data/test/fixtures/xml/ups/void_shipment_response.xml +11 -0
  181. data/test/fixtures/xml/usps/api_error_rate_response.xml +53 -0
  182. data/test/fixtures/xml/usps/beverly_hills_to_new_york_book_commercial_base_rate_response.xml +2 -0
  183. data/test/fixtures/xml/usps/beverly_hills_to_new_york_book_commercial_plus_rate_response.xml +258 -0
  184. data/test/fixtures/xml/usps/beverly_hills_to_new_york_book_rate_response.xml +108 -0
  185. data/test/fixtures/xml/usps/beverly_hills_to_ottawa_american_wii_commercial_base_rate_response.xml +84 -0
  186. data/test/fixtures/xml/usps/beverly_hills_to_ottawa_american_wii_commercial_plus_rate_response.xml +212 -0
  187. data/test/fixtures/xml/usps/beverly_hills_to_ottawa_american_wii_rate_response.xml +230 -0
  188. data/test/fixtures/xml/usps/first_class_packages_with_invalid_mail_type_response.xml +12 -0
  189. data/test/fixtures/xml/usps/first_class_packages_with_mail_type_response.xml +16 -0
  190. data/test/fixtures/xml/usps/first_class_packages_without_mail_type_response.xml +12 -0
  191. data/test/fixtures/xml/usps/invalid_xml_response.xml +10 -0
  192. data/test/fixtures/xml/usps/invalid_xml_tracking_response_error.xml +2 -0
  193. data/test/fixtures/xml/usps/tracking_request.xml +10 -0
  194. data/test/fixtures/xml/usps/tracking_request_batch.xml +12 -0
  195. data/test/fixtures/xml/usps/tracking_response.xml +162 -0
  196. data/test/fixtures/xml/usps/tracking_response_alt.xml +53 -0
  197. data/test/fixtures/xml/usps/tracking_response_batch.xml +231 -0
  198. data/test/fixtures/xml/usps/tracking_response_failure.xml +11 -0
  199. data/test/fixtures/xml/usps/tracking_response_not_available.xml +12 -0
  200. data/test/fixtures/xml/usps/tracking_response_test_error.xml +8 -0
  201. data/test/fixtures/xml/usps/us_rate_request.xml +18 -0
  202. data/test/fixtures/xml/usps/us_rate_request_large.xml +18 -0
  203. data/test/fixtures/xml/usps/world_rate_request_only_country.xml +22 -0
  204. data/test/fixtures/xml/usps/world_rate_request_with_value.xml +24 -0
  205. data/test/fixtures/xml/usps/world_rate_request_without_value.xml +24 -0
  206. data/test/fixtures/xml/usps_returns/external_return_label_response.xml +2 -0
  207. data/test/fixtures/xml/usps_returns/external_return_label_response_failure.xml +10 -0
  208. data/test/remote/australia_post_test.rb +140 -0
  209. data/test/remote/canada_post_pws_platform_test.rb +259 -0
  210. data/test/remote/canada_post_pws_test.rb +169 -0
  211. data/test/remote/canada_post_test.rb +55 -0
  212. data/test/remote/fedex_test.rb +400 -0
  213. data/test/remote/kunaki_test.rb +37 -0
  214. data/test/remote/new_zealand_post_test.rb +149 -0
  215. data/test/remote/shipwire_test.rb +84 -0
  216. data/test/remote/stamps_test.rb +396 -0
  217. data/test/remote/usps_returns_test.rb +72 -0
  218. data/test/remote/usps_test.rb +243 -0
  219. data/test/test_helper.rb +296 -0
  220. data/test/unit/carrier_test.rb +130 -0
  221. data/test/unit/carriers/australia_post_test.rb +181 -0
  222. data/test/unit/carriers/benchmark_test.rb +18 -0
  223. data/test/unit/carriers/canada_post_pws_rating_test.rb +379 -0
  224. data/test/unit/carriers/canada_post_pws_register_test.rb +76 -0
  225. data/test/unit/carriers/canada_post_pws_shipping_test.rb +258 -0
  226. data/test/unit/carriers/canada_post_pws_test.rb +59 -0
  227. data/test/unit/carriers/canada_post_pws_tracking_test.rb +154 -0
  228. data/test/unit/carriers/canada_post_test.rb +148 -0
  229. data/test/unit/carriers/fedex_test.rb +693 -0
  230. data/test/unit/carriers/kunaki_test.rb +56 -0
  231. data/test/unit/carriers/new_zealand_post_test.rb +177 -0
  232. data/test/unit/carriers/shipwire_test.rb +188 -0
  233. data/test/unit/carriers/stamps_test.rb +245 -0
  234. data/test/unit/carriers/ups_test.rb +580 -0
  235. data/test/unit/carriers/usps_returns_test.rb +45 -0
  236. data/test/unit/carriers/usps_test.rb +633 -0
  237. data/test/unit/carriers_test.rb +16 -0
  238. data/test/unit/external_return_label_request_test.rb +258 -0
  239. data/test/unit/location_test.rb +234 -0
  240. data/test/unit/package_item_test.rb +232 -0
  241. data/test/unit/package_test.rb +404 -0
  242. data/test/unit/rate_estimate_test.rb +93 -0
  243. data/test/unit/response_test.rb +38 -0
  244. data/test/unit/shipment_event_test.rb +20 -0
  245. data/test/unit/shipment_packer_test.rb +212 -0
  246. data/test/unit/tracking_response_test.rb +41 -0
  247. metadata +684 -0
@@ -0,0 +1,797 @@
1
+ module ReactiveShipping
2
+
3
+ # FedEx carrier implementation.
4
+ #
5
+ # FedEx module by Jimmy Baker (http://github.com/jimmyebaker)
6
+ # Documentation can be found here: http://images.fedex.com/us/developer/product/WebServices/MyWebHelp/PropDevGuide.pdf
7
+ class FedEx < Carrier
8
+ self.retry_safe = true
9
+
10
+ cattr_reader :name
11
+ @@name = "FedEx"
12
+
13
+ TEST_URL = 'https://gatewaybeta.fedex.com:443/xml'
14
+ LIVE_URL = 'https://gateway.fedex.com:443/xml'
15
+
16
+ CARRIER_CODES = {
17
+ "fedex_ground" => "FDXG",
18
+ "fedex_express" => "FDXE"
19
+ }
20
+
21
+ DELIVERY_ADDRESS_NODE_NAMES = %w(DestinationAddress ActualDeliveryAddress)
22
+ SHIPPER_ADDRESS_NODE_NAMES = %w(ShipperAddress)
23
+
24
+ SERVICE_TYPES = {
25
+ "PRIORITY_OVERNIGHT" => "FedEx Priority Overnight",
26
+ "PRIORITY_OVERNIGHT_SATURDAY_DELIVERY" => "FedEx Priority Overnight Saturday Delivery",
27
+ "FEDEX_2_DAY" => "FedEx 2 Day",
28
+ "FEDEX_2_DAY_SATURDAY_DELIVERY" => "FedEx 2 Day Saturday Delivery",
29
+ "STANDARD_OVERNIGHT" => "FedEx Standard Overnight",
30
+ "FIRST_OVERNIGHT" => "FedEx First Overnight",
31
+ "FIRST_OVERNIGHT_SATURDAY_DELIVERY" => "FedEx First Overnight Saturday Delivery",
32
+ "FEDEX_EXPRESS_SAVER" => "FedEx Express Saver",
33
+ "FEDEX_1_DAY_FREIGHT" => "FedEx 1 Day Freight",
34
+ "FEDEX_1_DAY_FREIGHT_SATURDAY_DELIVERY" => "FedEx 1 Day Freight Saturday Delivery",
35
+ "FEDEX_2_DAY_FREIGHT" => "FedEx 2 Day Freight",
36
+ "FEDEX_2_DAY_FREIGHT_SATURDAY_DELIVERY" => "FedEx 2 Day Freight Saturday Delivery",
37
+ "FEDEX_3_DAY_FREIGHT" => "FedEx 3 Day Freight",
38
+ "FEDEX_3_DAY_FREIGHT_SATURDAY_DELIVERY" => "FedEx 3 Day Freight Saturday Delivery",
39
+ "INTERNATIONAL_PRIORITY" => "FedEx International Priority",
40
+ "INTERNATIONAL_PRIORITY_SATURDAY_DELIVERY" => "FedEx International Priority Saturday Delivery",
41
+ "INTERNATIONAL_ECONOMY" => "FedEx International Economy",
42
+ "INTERNATIONAL_FIRST" => "FedEx International First",
43
+ "INTERNATIONAL_PRIORITY_FREIGHT" => "FedEx International Priority Freight",
44
+ "INTERNATIONAL_ECONOMY_FREIGHT" => "FedEx International Economy Freight",
45
+ "GROUND_HOME_DELIVERY" => "FedEx Ground Home Delivery",
46
+ "FEDEX_GROUND" => "FedEx Ground",
47
+ "INTERNATIONAL_GROUND" => "FedEx International Ground",
48
+ "SMART_POST" => "FedEx SmartPost",
49
+ "FEDEX_FREIGHT_PRIORITY" => "FedEx Freight Priority",
50
+ "FEDEX_FREIGHT_ECONOMY" => "FedEx Freight Economy"
51
+ }
52
+
53
+ PACKAGE_TYPES = {
54
+ "fedex_envelope" => "FEDEX_ENVELOPE",
55
+ "fedex_pak" => "FEDEX_PAK",
56
+ "fedex_box" => "FEDEX_BOX",
57
+ "fedex_tube" => "FEDEX_TUBE",
58
+ "fedex_10_kg_box" => "FEDEX_10KG_BOX",
59
+ "fedex_25_kg_box" => "FEDEX_25KG_BOX",
60
+ "your_packaging" => "YOUR_PACKAGING"
61
+ }
62
+
63
+ DROPOFF_TYPES = {
64
+ 'regular_pickup' => 'REGULAR_PICKUP',
65
+ 'request_courier' => 'REQUEST_COURIER',
66
+ 'dropbox' => 'DROP_BOX',
67
+ 'business_service_center' => 'BUSINESS_SERVICE_CENTER',
68
+ 'station' => 'STATION'
69
+ }
70
+
71
+ SIGNATURE_OPTION_CODES = {
72
+ adult: 'ADULT', # 21 years plus
73
+ direct: 'DIRECT', # A person at the delivery address
74
+ indirect: 'INDIRECT', # A person at the delivery address, or a neighbor, or a signed note for fedex on the door
75
+ none_required: 'NO_SIGNATURE_REQUIRED',
76
+ default_for_service: 'SERVICE_DEFAULT'
77
+ }
78
+
79
+ PAYMENT_TYPES = {
80
+ 'sender' => 'SENDER',
81
+ 'recipient' => 'RECIPIENT',
82
+ 'third_party' => 'THIRDPARTY',
83
+ 'collect' => 'COLLECT'
84
+ }
85
+
86
+ PACKAGE_IDENTIFIER_TYPES = {
87
+ 'tracking_number' => 'TRACKING_NUMBER_OR_DOORTAG',
88
+ 'door_tag' => 'TRACKING_NUMBER_OR_DOORTAG',
89
+ 'rma' => 'RMA',
90
+ 'ground_shipment_id' => 'GROUND_SHIPMENT_ID',
91
+ 'ground_invoice_number' => 'GROUND_INVOICE_NUMBER',
92
+ 'ground_customer_reference' => 'GROUND_CUSTOMER_REFERENCE',
93
+ 'ground_po' => 'GROUND_PO',
94
+ 'express_reference' => 'EXPRESS_REFERENCE',
95
+ 'express_mps_master' => 'EXPRESS_MPS_MASTER',
96
+ 'shipper_reference' => 'SHIPPER_REFERENCE',
97
+ }
98
+
99
+ 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)
100
+
101
+ # FedEx tracking codes as described in the FedEx Tracking Service WSDL Guide
102
+ # All delays also have been marked as exceptions
103
+ TRACKING_STATUS_CODES = HashWithIndifferentAccess.new(
104
+ 'AA' => :at_airport,
105
+ 'AD' => :at_delivery,
106
+ 'AF' => :at_fedex_facility,
107
+ 'AR' => :at_fedex_facility,
108
+ 'AP' => :at_pickup,
109
+ 'CA' => :canceled,
110
+ 'CH' => :location_changed,
111
+ 'DE' => :exception,
112
+ 'DL' => :delivered,
113
+ 'DP' => :departed_fedex_location,
114
+ 'DR' => :vehicle_furnished_not_used,
115
+ 'DS' => :vehicle_dispatched,
116
+ 'DY' => :exception,
117
+ 'EA' => :exception,
118
+ 'ED' => :enroute_to_delivery,
119
+ 'EO' => :enroute_to_origin_airport,
120
+ 'EP' => :enroute_to_pickup,
121
+ 'FD' => :at_fedex_destination,
122
+ 'HL' => :held_at_location,
123
+ 'IT' => :in_transit,
124
+ 'LO' => :left_origin,
125
+ 'OC' => :order_created,
126
+ 'OD' => :out_for_delivery,
127
+ 'PF' => :plane_in_flight,
128
+ 'PL' => :plane_landed,
129
+ 'PU' => :picked_up,
130
+ 'RS' => :return_to_shipper,
131
+ 'SE' => :exception,
132
+ 'SF' => :at_sort_facility,
133
+ 'SP' => :split_status,
134
+ 'TR' => :transfer
135
+ )
136
+
137
+ DEFAULT_LABEL_STOCK_TYPE = 'PAPER_7X4.75'
138
+
139
+ # Available return formats for image data when creating labels
140
+ LABEL_FORMATS = %w(DPL EPL2 PDF ZPLII PNG).freeze
141
+
142
+ TRANSIENT_TRACK_RESPONSE_CODES = %w(9035 9040 9041 9045 9050 9055 9060 9065 9070 9075 9085 9086 9090).freeze
143
+
144
+ UNRECOVERABLE_TRACK_RESPONSE_CODES = %w(9080 9081 9082 9095 9100).freeze
145
+
146
+ def self.service_name_for_code(service_code)
147
+ SERVICE_TYPES[service_code] || "FedEx #{service_code.titleize.sub(/Fedex /, '')}"
148
+ end
149
+
150
+ def requirements
151
+ [:key, :password, :account, :login]
152
+ end
153
+
154
+ def find_rates(origin, destination, packages, options = {})
155
+ options = @options.merge(options)
156
+ packages = Array(packages)
157
+
158
+ rate_request = build_rate_request(origin, destination, packages, options)
159
+
160
+ xml = commit(save_request(rate_request), (options[:test] || false))
161
+
162
+ parse_rate_response(origin, destination, packages, xml, options)
163
+ end
164
+
165
+ def find_tracking_info(tracking_number, options = {})
166
+ options = @options.merge(options)
167
+
168
+ tracking_request = build_tracking_request(tracking_number, options)
169
+ xml = commit(save_request(tracking_request), (options[:test] || false))
170
+ parse_tracking_response(xml, options)
171
+ end
172
+
173
+
174
+ # Get Shipping labels
175
+ def create_shipment(origin, destination, packages, options = {})
176
+ options = @options.merge(options)
177
+ packages = Array(packages)
178
+ raise Error, "Multiple packages are not supported yet." if packages.length > 1
179
+
180
+ request = build_shipment_request(origin, destination, packages, options)
181
+ logger.debug(request) if logger
182
+
183
+ response = commit(save_request(request), (options[:test] || false))
184
+ parse_ship_response(response)
185
+ end
186
+
187
+ def maximum_address_field_length
188
+ # See Fedex Developper Guide
189
+ 35
190
+ end
191
+
192
+ protected
193
+
194
+ def build_shipment_request(origin, destination, packages, options = {})
195
+ imperial = location_uses_imperial(origin)
196
+
197
+ xml_builder = Nokogiri::XML::Builder.new do |xml|
198
+ xml.ProcessShipmentRequest(xmlns: 'http://fedex.com/ws/ship/v13') do
199
+ build_request_header(xml)
200
+ build_version_node(xml, 'ship', 13, 0 ,0)
201
+
202
+ xml.RequestedShipment do
203
+ xml.ShipTimestamp(ship_timestamp(options[:turn_around_time]).iso8601(0))
204
+ xml.DropoffType('REGULAR_PICKUP')
205
+ xml.ServiceType(options[:service_type] || 'FEDEX_GROUND')
206
+ xml.PackagingType('YOUR_PACKAGING')
207
+
208
+ xml.Shipper do
209
+ build_contact_address_nodes(xml, options[:shipper] || origin)
210
+ end
211
+
212
+ xml.Recipient do
213
+ build_contact_address_nodes(xml, destination)
214
+ end
215
+
216
+ xml.Origin do
217
+ build_contact_address_nodes(xml, origin)
218
+ end
219
+
220
+ xml.ShippingChargesPayment do
221
+ xml.PaymentType('SENDER')
222
+ xml.Payor do
223
+ build_shipment_responsible_party_node(xml, options[:shipper] || origin)
224
+ end
225
+ end
226
+
227
+ xml.LabelSpecification do
228
+ xml.LabelFormatType('COMMON2D')
229
+ xml.ImageType(options[:label_format] || 'PNG')
230
+ xml.LabelStockType(options[:label_stock_type] || DEFAULT_LABEL_STOCK_TYPE)
231
+ end
232
+
233
+ xml.RateRequestTypes('ACCOUNT')
234
+
235
+ xml.PackageCount(packages.size)
236
+ packages.each do |package|
237
+ xml.RequestedPackageLineItems do
238
+ xml.GroupPackageCount(1)
239
+ build_package_weight_node(xml, package, imperial)
240
+ build_package_dimensions_node(xml, package, imperial)
241
+
242
+ # Reference Numbers
243
+ reference_numbers = Array(package.options[:reference_numbers])
244
+ if reference_numbers.size > 0
245
+ reference_numbers.each do |reference_number_info|
246
+ xml.CustomerReferences do
247
+ xml.CustomerReferenceType(reference_number_info[:type] || "CUSTOMER_REFERENCE")
248
+ xml.Value(reference_number_info[:value])
249
+ end
250
+ end
251
+ end
252
+
253
+ xml.SpecialServicesRequested do
254
+ xml.SpecialServiceTypes("SIGNATURE_OPTION")
255
+ xml.SignatureOptionDetail do
256
+ xml.OptionType(SIGNATURE_OPTION_CODES[package.options[:signature_option] || :default_for_service])
257
+ end
258
+ end
259
+ end
260
+ end
261
+ end
262
+ end
263
+ end
264
+ xml_builder.to_xml
265
+ end
266
+
267
+ def build_contact_address_nodes(xml, location)
268
+ xml.Contact do
269
+ xml.PersonName(location.name)
270
+ xml.CompanyName(location.company)
271
+ xml.PhoneNumber(location.phone)
272
+ end
273
+ xml.Address do
274
+ xml.StreetLines(location.address1) if location.address1
275
+ xml.StreetLines(location.address2) if location.address2
276
+ xml.City(location.city) if location.city
277
+ xml.StateOrProvinceCode(location.state)
278
+ xml.PostalCode(location.postal_code)
279
+ xml.CountryCode(location.country_code(:alpha2))
280
+ xml.Residential('true') if location.residential?
281
+ end
282
+ end
283
+
284
+ def build_shipment_responsible_party_node(xml, origin)
285
+ xml.ResponsibleParty do
286
+ xml.AccountNumber(@options[:account])
287
+ xml.Contact do
288
+ xml.PersonName(origin.name)
289
+ xml.CompanyName(origin.company)
290
+ xml.PhoneNumber(origin.phone)
291
+ end
292
+ end
293
+ end
294
+
295
+ def build_rate_request(origin, destination, packages, options = {})
296
+ imperial = location_uses_imperial(origin)
297
+
298
+ xml_builder = Nokogiri::XML::Builder.new do |xml|
299
+ xml.RateRequest(xmlns: 'http://fedex.com/ws/rate/v13') do
300
+ build_request_header(xml)
301
+ build_version_node(xml, 'crs', 13, 0 ,0)
302
+
303
+ # Returns delivery dates
304
+ xml.ReturnTransitAndCommit(true)
305
+
306
+ # Returns saturday delivery shipping options when available
307
+ xml.VariableOptions('SATURDAY_DELIVERY') if options[:saturday_delivery]
308
+
309
+ xml.RequestedShipment do
310
+ if options[:pickup_date]
311
+ xml.ShipTimestamp(options[:pickup_date].to_time.iso8601(0))
312
+ else
313
+ xml.ShipTimestamp(ship_timestamp(options[:turn_around_time]).iso8601(0))
314
+ end
315
+
316
+ freight = has_freight?(options)
317
+
318
+ unless freight
319
+ # fedex api wants this up here otherwise request returns an error
320
+ xml.DropoffType(options[:dropoff_type] || 'REGULAR_PICKUP')
321
+ xml.PackagingType(options[:packaging_type] || 'YOUR_PACKAGING')
322
+ end
323
+
324
+ build_location_node(xml, 'Shipper', options[:shipper] || origin)
325
+ build_location_node(xml, 'Recipient', destination)
326
+ if options[:shipper] && options[:shipper] != origin
327
+ build_location_node(xml, 'Origin', origin)
328
+ end
329
+
330
+ if freight
331
+ freight_options = options[:freight]
332
+ build_shipping_charges_payment_node(xml, freight_options)
333
+ build_freight_shipment_detail_node(xml, freight_options, packages, imperial)
334
+ build_rate_request_types_node(xml)
335
+ else
336
+ xml.SmartPostDetail do
337
+ xml.Indicia(options[:smart_post_indicia] || 'PARCEL_SELECT')
338
+ xml.HubId(options[:smart_post_hub_id] || 5902) # default to LA
339
+ end
340
+
341
+ build_rate_request_types_node(xml)
342
+ xml.PackageCount(packages.size)
343
+ build_packages_nodes(xml, packages, imperial)
344
+ end
345
+ end
346
+ end
347
+ end
348
+ xml_builder.to_xml
349
+ end
350
+
351
+ def build_packages_nodes(xml, packages, imperial)
352
+ packages.map do |pkg|
353
+ xml.RequestedPackageLineItems do
354
+ xml.GroupPackageCount(1)
355
+ build_package_weight_node(xml, pkg, imperial)
356
+ build_package_dimensions_node(xml, pkg, imperial)
357
+ end
358
+ end
359
+ end
360
+
361
+ def build_shipping_charges_payment_node(xml, freight_options)
362
+ xml.ShippingChargesPayment do
363
+ xml.PaymentType(freight_options[:payment_type])
364
+ xml.Payor do
365
+ xml.ResponsibleParty do
366
+ # TODO: case of different freight account numbers?
367
+ xml.AccountNumber(freight_options[:account])
368
+ end
369
+ end
370
+ end
371
+ end
372
+
373
+ def build_freight_shipment_detail_node(xml, freight_options, packages, imperial)
374
+ xml.FreightShipmentDetail do
375
+ # TODO: case of different freight account numbers?
376
+ xml.FedExFreightAccountNumber(freight_options[:account])
377
+ build_location_node(xml, 'FedExFreightBillingContactAndAddress', freight_options[:billing_location])
378
+ xml.Role(freight_options[:role])
379
+
380
+ packages.each do |pkg|
381
+ xml.LineItems do
382
+ xml.FreightClass(freight_options[:freight_class])
383
+ xml.Packaging(freight_options[:packaging])
384
+ build_package_weight_node(xml, pkg, imperial)
385
+ build_package_dimensions_node(xml, pkg, imperial)
386
+ end
387
+ end
388
+ end
389
+ end
390
+
391
+ def has_freight?(options)
392
+ options[:freight] && options[:freight].present?
393
+ end
394
+
395
+ def build_package_weight_node(xml, pkg, imperial)
396
+ xml.Weight do
397
+ xml.Units(imperial ? 'LB' : 'KG')
398
+ xml.Value([((imperial ? pkg.lbs : pkg.kgs).to_f * 1000).round / 1000.0, 0.1].max)
399
+ end
400
+ end
401
+
402
+ def build_package_dimensions_node(xml, pkg, imperial)
403
+ xml.Dimensions do
404
+ [:length, :width, :height].each do |axis|
405
+ value = ((imperial ? pkg.inches(axis) : pkg.cm(axis)).to_f * 1000).round / 1000.0 # 3 decimals
406
+ xml.public_send(axis.to_s.capitalize, value.ceil)
407
+ end
408
+ xml.Units(imperial ? 'IN' : 'CM')
409
+ end
410
+ end
411
+
412
+ def build_rate_request_types_node(xml, type = 'ACCOUNT')
413
+ xml.RateRequestTypes(type)
414
+ end
415
+
416
+ def build_tracking_request(tracking_number, options = {})
417
+ xml_builder = Nokogiri::XML::Builder.new do |xml|
418
+ xml.TrackRequest(xmlns: 'http://fedex.com/ws/track/v7') do
419
+ build_request_header(xml)
420
+ build_version_node(xml, 'trck', 7, 0, 0)
421
+
422
+ xml.SelectionDetails do
423
+ xml.PackageIdentifier do
424
+ xml.Type(PACKAGE_IDENTIFIER_TYPES[options[:package_identifier_type] || 'tracking_number'])
425
+ xml.Value(tracking_number)
426
+ end
427
+
428
+ xml.ShipDateRangeBegin(options[:ship_date_range_begin]) if options[:ship_date_range_begin]
429
+ xml.ShipDateRangeEnd(options[:ship_date_range_end]) if options[:ship_date_range_end]
430
+ xml.TrackingNumberUniqueIdentifier(options[:unique_identifier]) if options[:unique_identifier]
431
+ end
432
+
433
+ xml.ProcessingOptions('INCLUDE_DETAILED_SCANS')
434
+ end
435
+ end
436
+ xml_builder.to_xml
437
+ end
438
+
439
+ def build_request_header(xml)
440
+ xml.WebAuthenticationDetail do
441
+ xml.UserCredential do
442
+ xml.Key(@options[:key])
443
+ xml.Password(@options[:password])
444
+ end
445
+ end
446
+
447
+ xml.ClientDetail do
448
+ xml.AccountNumber(@options[:account])
449
+ xml.MeterNumber(@options[:login])
450
+ end
451
+
452
+ xml.TransactionDetail do
453
+ xml.CustomerTransactionId(@options[:transaction_id] || 'ReactiveShipping') # TODO: Need to do something better with this...
454
+ end
455
+ end
456
+
457
+ def build_version_node(xml, service_id, major, intermediate, minor)
458
+ xml.Version do
459
+ xml.ServiceId(service_id)
460
+ xml.Major(major)
461
+ xml.Intermediate(intermediate)
462
+ xml.Minor(minor)
463
+ end
464
+ end
465
+
466
+ def build_location_node(xml, name, location)
467
+ xml.public_send(name) do
468
+ xml.Address do
469
+ xml.StreetLines(location.address1) if location.address1
470
+ xml.StreetLines(location.address2) if location.address2
471
+ xml.City(location.city) if location.city
472
+ xml.PostalCode(location.postal_code)
473
+ xml.CountryCode(location.country_code(:alpha2))
474
+ xml.Residential(true) unless location.commercial?
475
+ end
476
+ end
477
+ end
478
+
479
+ def parse_rate_response(origin, destination, packages, response, options)
480
+ xml = build_document(response, 'RateReply')
481
+
482
+ success = response_success?(xml)
483
+ message = response_message(xml)
484
+
485
+ if success
486
+ missing_xml_field = false
487
+ rate_estimates = xml.root.css('> RateReplyDetails').map do |rated_shipment|
488
+ begin
489
+ service_code = rated_shipment.at('ServiceType').text
490
+ is_saturday_delivery = rated_shipment.at('AppliedOptions').try(:text) == 'SATURDAY_DELIVERY'
491
+ service_type = is_saturday_delivery ? "#{service_code}_SATURDAY_DELIVERY" : service_code
492
+
493
+ transit_time = rated_shipment.at('TransitTime').text if ["FEDEX_GROUND", "GROUND_HOME_DELIVERY"].include?(service_code)
494
+ max_transit_time = rated_shipment.at('MaximumTransitTime').try(:text) if service_code == "FEDEX_GROUND"
495
+
496
+ delivery_timestamp = rated_shipment.at('DeliveryTimestamp').try(:text)
497
+ delivery_range = delivery_range_from(transit_time, max_transit_time, delivery_timestamp, (service_code == "GROUND_HOME_DELIVERY"), options)
498
+
499
+ currency = rated_shipment.at('RatedShipmentDetails/ShipmentRateDetail/TotalNetCharge/Currency').text
500
+
501
+ RateEstimate.new(origin, destination, @@name,
502
+ self.class.service_name_for_code(service_type),
503
+ :service_code => service_code,
504
+ :total_price => rated_shipment.at('RatedShipmentDetails/ShipmentRateDetail/TotalNetCharge/Amount').text.to_f,
505
+ :currency => currency,
506
+ :packages => packages,
507
+ :delivery_range => delivery_range)
508
+ rescue NoMethodError
509
+ missing_xml_field = true
510
+ nil
511
+ end
512
+ end
513
+
514
+ rate_estimates = rate_estimates.compact
515
+ logger.warn("[FedexParseRateError] Some fields where missing in the response: #{response}") if logger && missing_xml_field
516
+
517
+ if rate_estimates.empty?
518
+ success = false
519
+ if missing_xml_field
520
+ message = "The response from the carrier contained errors and could not be treated"
521
+ else
522
+ message = "No shipping rates could be found for the destination address" if message.blank?
523
+ end
524
+ end
525
+
526
+ else
527
+ rate_estimates = []
528
+ end
529
+
530
+ RateResponse.new(success, message, Hash.from_xml(response), :rates => rate_estimates, :xml => response, :request => last_request, :log_xml => options[:log_xml])
531
+ end
532
+
533
+ def delivery_range_from(transit_time, max_transit_time, delivery_timestamp, is_home_delivery, options)
534
+ delivery_range = [delivery_timestamp, delivery_timestamp]
535
+
536
+ # if there's no delivery timestamp but we do have a transit time, use it
537
+ if delivery_timestamp.blank? && transit_time.present?
538
+ transit_range = parse_transit_times([transit_time, max_transit_time.presence || transit_time])
539
+ pickup_date = options[:pickup_date] || ship_date(options[:turn_around_time])
540
+
541
+ delivery_range = transit_range.map { |days| business_days_from(pickup_date, days, is_home_delivery) }
542
+ end
543
+
544
+ delivery_range
545
+ end
546
+
547
+ def parse_ship_response(response)
548
+ xml = build_document(response, 'ProcessShipmentReply')
549
+ success = response_success?(xml)
550
+ message = response_message(xml)
551
+
552
+ response_info = Hash.from_xml(response)
553
+ tracking_number = xml.css("CompletedPackageDetails TrackingIds TrackingNumber").text
554
+ base_64_image = xml.css("Label Image").text
555
+
556
+ labels = [Label.new(tracking_number, Base64.decode64(base_64_image))]
557
+ LabelResponse.new(success, message, response_info, {labels: labels})
558
+ end
559
+
560
+ def business_days_from(date, days, is_home_delivery=false)
561
+ future_date = date
562
+ count = 0
563
+
564
+ while count < days
565
+ future_date += 1.day
566
+ if is_home_delivery
567
+ count += 1 if home_delivery_business_day?(future_date)
568
+ else
569
+ count += 1 if business_day?(future_date)
570
+ end
571
+ end
572
+
573
+ future_date
574
+ end
575
+
576
+ #Transit times for FedEx® Ground do not include Saturdays, Sundays, or holidays.
577
+ def business_day?(date)
578
+ (1..5).include?(date.wday)
579
+ end
580
+
581
+ #Transit times for FedEx® Home Delivery, do not include Sundays, Mondays, or holidays.
582
+ def home_delivery_business_day?(date)
583
+ (2..6).include?(date.wday)
584
+ end
585
+
586
+ def parse_tracking_response(response, options)
587
+ xml = build_document(response, 'TrackReply')
588
+
589
+ success = response_success?(xml)
590
+ message = response_message(xml)
591
+
592
+ if success
593
+ tracking_details_root = xml.at('CompletedTrackDetails')
594
+ success = response_success?(tracking_details_root)
595
+ message = response_message(tracking_details_root)
596
+ end
597
+
598
+ if success
599
+ delivery_signature = nil
600
+ shipment_events = []
601
+
602
+ all_tracking_details = xml.root.xpath('CompletedTrackDetails/TrackDetails')
603
+ tracking_details = case all_tracking_details.length
604
+ when 1
605
+ all_tracking_details.first
606
+ when 0
607
+ message = "The response did not contain tracking details"
608
+ return TrackingResponse.new(
609
+ false,
610
+ message,
611
+ Hash.from_xml(response),
612
+ carrier: @@name,
613
+ xml: response,
614
+ request: last_request
615
+ )
616
+ else
617
+ all_unique_identifiers = xml.root.xpath('CompletedTrackDetails/TrackDetails/TrackingNumberUniqueIdentifier').map(&:text)
618
+ message = "Multiple matches were found. Specify a unqiue identifier: #{all_unique_identifiers.join(', ')}"
619
+ return TrackingResponse.new(
620
+ false,
621
+ message,
622
+ Hash.from_xml(response),
623
+ carrier: @@name,
624
+ xml: response,
625
+ request: last_request
626
+ )
627
+ end
628
+
629
+ first_notification = tracking_details.at('Notification')
630
+ severity = first_notification.at('Severity').text
631
+ if severity == 'ERROR' || severity == 'FAILURE'
632
+ message = first_notification.try(:text)
633
+ code = first_notification.at('Code').try(:text)
634
+ case code
635
+ when *TRANSIENT_TRACK_RESPONSE_CODES
636
+ raise ReactiveShipping::ShipmentNotFound, first_notification.at('Message').text
637
+ else
638
+ raise ReactiveShipping::ResponseContentError, StandardError.new(first_notification.at('Message').text)
639
+ end
640
+ end
641
+
642
+ tracking_number = tracking_details.at('TrackingNumber').text
643
+ status_detail = tracking_details.at('StatusDetail')
644
+ if status_detail.blank?
645
+ status_code, status, status_description, delivery_signature = nil
646
+ else
647
+ status_code = status_detail.at('Code').try(:text)
648
+ status_description = status_detail.at('AncillaryDetails/ReasonDescription').try(:text) || status_detail.at('Description').try(:text)
649
+
650
+ status = TRACKING_STATUS_CODES[status_code]
651
+
652
+ if status_code == 'DL' && tracking_details.at('AvailableImages').try(:text) == 'SIGNATURE_PROOF_OF_DELIVERY'
653
+ delivery_signature = tracking_details.at('DeliverySignatureName').try(:text)
654
+ end
655
+ end
656
+
657
+ origin = if origin_node = tracking_details.at('OriginLocationAddress')
658
+ Location.new(
659
+ country: origin_node.at('CountryCode').text,
660
+ province: origin_node.at('StateOrProvinceCode').text,
661
+ city: origin_node.at('City').text
662
+ )
663
+ end
664
+
665
+ destination = extract_address(tracking_details, DELIVERY_ADDRESS_NODE_NAMES)
666
+ shipper_address = extract_address(tracking_details, SHIPPER_ADDRESS_NODE_NAMES)
667
+
668
+ ship_time = extract_timestamp(tracking_details, 'ShipTimestamp')
669
+ actual_delivery_time = extract_timestamp(tracking_details, 'ActualDeliveryTimestamp')
670
+ scheduled_delivery_time = extract_timestamp(tracking_details, 'EstimatedDeliveryTimestamp')
671
+
672
+ tracking_details.xpath('Events').each do |event|
673
+ address = event.at('Address')
674
+ next if address.nil? || address.at('CountryCode').nil?
675
+
676
+ city = address.at('City').try(:text)
677
+ state = address.at('StateOrProvinceCode').try(:text)
678
+ zip_code = address.at('PostalCode').try(:text)
679
+ country = address.at('CountryCode').try(:text)
680
+
681
+ location = Location.new(:city => city, :state => state, :postal_code => zip_code, :country => country)
682
+ description = event.at('EventDescription').text
683
+ type_code = event.at('EventType').text
684
+
685
+ time = Time.parse(event.at('Timestamp').text)
686
+ zoneless_time = time.utc
687
+
688
+ shipment_events << ShipmentEvent.new(description, zoneless_time, location, description, type_code)
689
+ end
690
+
691
+ shipment_events = shipment_events.sort_by(&:time)
692
+ end
693
+
694
+ TrackingResponse.new(
695
+ success,
696
+ message,
697
+ Hash.from_xml(response),
698
+ carrier: @@name,
699
+ xml: response,
700
+ request: last_request,
701
+ status: status,
702
+ status_code: status_code,
703
+ status_description: status_description,
704
+ ship_time: ship_time,
705
+ scheduled_delivery_date: scheduled_delivery_time,
706
+ actual_delivery_date: actual_delivery_time,
707
+ delivery_signature: delivery_signature,
708
+ shipment_events: shipment_events,
709
+ shipper_address: (shipper_address.nil? || shipper_address.unknown?) ? nil : shipper_address,
710
+ origin: origin,
711
+ destination: destination,
712
+ tracking_number: tracking_number
713
+ )
714
+ end
715
+
716
+ def ship_timestamp(delay_in_hours)
717
+ delay_in_hours ||= 0
718
+ Time.now + delay_in_hours.hours
719
+ end
720
+
721
+ def ship_date(delay_in_hours)
722
+ delay_in_hours ||= 0
723
+ (Time.now + delay_in_hours.hours).to_date
724
+ end
725
+
726
+ def response_success?(document)
727
+ highest_severity = document.at('HighestSeverity')
728
+ return false if highest_severity.nil?
729
+ %w(SUCCESS WARNING NOTE).include?(highest_severity.text)
730
+ end
731
+
732
+ def response_message(document)
733
+ notifications = document.at('Notifications')
734
+ return "" if notifications.nil?
735
+
736
+ "#{notifications.at('Severity').text} - #{notifications.at('Code').text}: #{notifications.at('Message').text}"
737
+ end
738
+
739
+ def commit(request, test = false)
740
+ ssl_post(test ? TEST_URL : LIVE_URL, request.gsub("\n", ''))
741
+ end
742
+
743
+ def parse_transit_times(times)
744
+ results = []
745
+ times.each do |day_count|
746
+ days = TRANSIT_TIMES.index(day_count.to_s.chomp)
747
+ results << days.to_i
748
+ end
749
+ results
750
+ end
751
+
752
+ def extract_address(document, possible_node_names)
753
+ node = nil
754
+ args = {}
755
+ possible_node_names.each do |name|
756
+ node = document.at(name)
757
+ break if node
758
+ end
759
+
760
+ if node
761
+ args[:country] =
762
+ node.at('CountryCode').try(:text) ||
763
+ ActiveUtils::Country.new(:alpha2 => 'ZZ', :name => 'Unknown or Invalid Territory', :alpha3 => 'ZZZ', :numeric => '999')
764
+
765
+ args[:province] = node.at('StateOrProvinceCode').try(:text) || 'unknown'
766
+ args[:city] = node.at('City').try(:text) || 'unknown'
767
+ end
768
+
769
+ Location.new(args)
770
+ end
771
+
772
+ def extract_timestamp(document, node_name)
773
+ if timestamp_node = document.at(node_name)
774
+ if timestamp_node.text =~ /\A(\d{4}-\d{2}-\d{2})T00:00:00\Z/
775
+ Date.parse($1)
776
+ else
777
+ Time.parse(timestamp_node.text)
778
+ end
779
+ end
780
+ end
781
+
782
+ def build_document(xml, expected_root_tag)
783
+ document = Nokogiri.XML(xml) { |config| config.strict }
784
+ document.remove_namespaces!
785
+ if document.root.nil? || document.root.name != expected_root_tag
786
+ raise ReactiveShipping::ResponseContentError.new(StandardError.new('Invalid document'), xml)
787
+ end
788
+ document
789
+ rescue Nokogiri::XML::SyntaxError => e
790
+ raise ReactiveShipping::ResponseContentError.new(e, xml)
791
+ end
792
+
793
+ def location_uses_imperial(location)
794
+ %w(US LR MM).include?(location.country_code(:alpha2))
795
+ end
796
+ end
797
+ end