reactive_shipping 3.0.0

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 (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