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,1060 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ module ReactiveShipping
4
+ class UPS < Carrier
5
+ self.retry_safe = true
6
+
7
+ cattr_accessor :default_options
8
+ cattr_reader :name
9
+ @@name = "UPS"
10
+
11
+ TEST_URL = 'https://wwwcie.ups.com'
12
+ LIVE_URL = 'https://onlinetools.ups.com'
13
+
14
+ RESOURCES = {
15
+ :rates => 'ups.app/xml/Rate',
16
+ :track => 'ups.app/xml/Track',
17
+ :ship_confirm => 'ups.app/xml/ShipConfirm',
18
+ :ship_accept => 'ups.app/xml/ShipAccept',
19
+ :delivery_dates => 'ups.app/xml/TimeInTransit',
20
+ :void => 'ups.app/xml/Void'
21
+ }
22
+
23
+ PICKUP_CODES = HashWithIndifferentAccess.new(
24
+ :daily_pickup => "01",
25
+ :customer_counter => "03",
26
+ :one_time_pickup => "06",
27
+ :on_call_air => "07",
28
+ :suggested_retail_rates => "11",
29
+ :letter_center => "19",
30
+ :air_service_center => "20"
31
+ )
32
+
33
+ CUSTOMER_CLASSIFICATIONS = HashWithIndifferentAccess.new(
34
+ :wholesale => "01",
35
+ :occasional => "03",
36
+ :retail => "04"
37
+ )
38
+
39
+ # these are the defaults described in the UPS API docs,
40
+ # but they don't seem to apply them under all circumstances,
41
+ # so we need to take matters into our own hands
42
+ DEFAULT_CUSTOMER_CLASSIFICATIONS = Hash.new do |hash, key|
43
+ hash[key] = case key.to_sym
44
+ when :daily_pickup then :wholesale
45
+ when :customer_counter then :retail
46
+ else
47
+ :occasional
48
+ end
49
+ end
50
+
51
+ DEFAULT_SERVICES = {
52
+ "01" => "UPS Next Day Air",
53
+ "02" => "UPS Second Day Air",
54
+ "03" => "UPS Ground",
55
+ "07" => "UPS Worldwide Express",
56
+ "08" => "UPS Worldwide Expedited",
57
+ "11" => "UPS Standard",
58
+ "12" => "UPS Three-Day Select",
59
+ "13" => "UPS Next Day Air Saver",
60
+ "14" => "UPS Next Day Air Early A.M.",
61
+ "54" => "UPS Worldwide Express Plus",
62
+ "59" => "UPS Second Day Air A.M.",
63
+ "65" => "UPS Saver",
64
+ "82" => "UPS Today Standard",
65
+ "83" => "UPS Today Dedicated Courier",
66
+ "84" => "UPS Today Intercity",
67
+ "85" => "UPS Today Express",
68
+ "86" => "UPS Today Express Saver",
69
+ "92" => "UPS SurePost (USPS) < 1lb",
70
+ "93" => "UPS SurePost (USPS) > 1lb",
71
+ "94" => "UPS SurePost (USPS) BPM",
72
+ "95" => "UPS SurePost (USPS) Media",
73
+ }
74
+
75
+ CANADA_ORIGIN_SERVICES = {
76
+ "01" => "UPS Express",
77
+ "02" => "UPS Expedited",
78
+ "14" => "UPS Express Early A.M."
79
+ }
80
+
81
+ MEXICO_ORIGIN_SERVICES = {
82
+ "07" => "UPS Express",
83
+ "08" => "UPS Expedited",
84
+ "54" => "UPS Express Plus"
85
+ }
86
+
87
+ EU_ORIGIN_SERVICES = {
88
+ "07" => "UPS Express",
89
+ "08" => "UPS Expedited"
90
+ }
91
+
92
+ OTHER_NON_US_ORIGIN_SERVICES = {
93
+ "07" => "UPS Express"
94
+ }
95
+
96
+ RETURN_SERVICE_CODES = {
97
+ "2" => "UPS Print and Mail (PNM)",
98
+ "3" => "UPS Return Service 1-Attempt (RS1)",
99
+ "5" => "UPS Return Service 3-Attempt (RS3)",
100
+ "8" => "UPS Electronic Return Label (ERL)",
101
+ "9" => "UPS Print Return Label (PRL)",
102
+ "10" => "UPS Exchange Print Return Label",
103
+ "11" => "UPS Pack & Collect Service 1-Attempt Box 1",
104
+ "12" => "UPS Pack & Collect Service 1-Attempt Box 2",
105
+ "13" => "UPS Pack & Collect Service 1-Attempt Box 3",
106
+ "14" => "UPS Pack & Collect Service 1-Attempt Box 4",
107
+ "15" => "UPS Pack & Collect Service 1-Attempt Box 5",
108
+ "16" => "UPS Pack & Collect Service 3-Attempt Box 1",
109
+ "17" => "UPS Pack & Collect Service 3-Attempt Box 2",
110
+ "18" => "UPS Pack & Collect Service 3-Attempt Box 3",
111
+ "19" => "UPS Pack & Collect Service 3-Attempt Box 4",
112
+ "20" => "UPS Pack & Collect Service 3-Attempt Box 5",
113
+ }
114
+
115
+ TRACKING_STATUS_CODES = HashWithIndifferentAccess.new(
116
+ 'I' => :in_transit,
117
+ 'D' => :delivered,
118
+ 'X' => :exception,
119
+ 'P' => :pickup,
120
+ 'M' => :manifest_pickup
121
+ )
122
+
123
+ # From http://en.wikipedia.org/w/index.php?title=European_Union&oldid=174718707 (Current as of November 30, 2007)
124
+ EU_COUNTRY_CODES = %w(GB AT BE BG CY CZ DK EE FI FR DE GR HU IE IT LV LT LU MT NL PL PT RO SK SI ES SE)
125
+
126
+ US_TERRITORIES_TREATED_AS_COUNTRIES = %w(AS FM GU MH MP PW PR VI)
127
+
128
+ IMPERIAL_COUNTRIES = %w(US LR MM)
129
+
130
+ DEFAULT_SERVICE_NAME_TO_CODE = Hash[UPS::DEFAULT_SERVICES.to_a.map(&:reverse)]
131
+ DEFAULT_SERVICE_NAME_TO_CODE['UPS 2nd Day Air'] = "02"
132
+ DEFAULT_SERVICE_NAME_TO_CODE['UPS 3 Day Select'] = "12"
133
+ DEFAULT_SERVICE_NAME_TO_CODE['UPS Next Day Air Early'] = "14"
134
+
135
+ SHIPMENT_DELIVERY_CONFIRMATION_CODES = {
136
+ delivery_confirmation_signature_required: 1,
137
+ delivery_confirmation_adult_signature_required: 2
138
+ }
139
+
140
+ PACKAGE_DELIVERY_CONFIRMATION_CODES = {
141
+ delivery_confirmation: 1,
142
+ delivery_confirmation_signature_required: 2,
143
+ delivery_confirmation_adult_signature_required: 3,
144
+ usps_delivery_confirmation: 4
145
+ }
146
+
147
+ def requirements
148
+ [:key, :login, :password]
149
+ end
150
+
151
+ def find_rates(origin, destination, packages, options = {})
152
+ origin, destination = upsified_location(origin), upsified_location(destination)
153
+ options = @options.merge(options)
154
+ packages = Array(packages)
155
+ access_request = build_access_request
156
+ rate_request = build_rate_request(origin, destination, packages, options)
157
+ response = commit(:rates, save_request(access_request + rate_request), options[:test])
158
+ parse_rate_response(origin, destination, packages, response, options)
159
+ end
160
+
161
+ # Retrieves tracking information for a previous shipment
162
+ #
163
+ # @note Override with whatever you need to get a shipping label
164
+ #
165
+ # @param tracking_number [String] The unique identifier of the shipment to track.
166
+ # @param options [Hash] Carrier-specific parameters.
167
+ # @option options [Boolean] :mail_innovations Set this to true to track a Mail Innovations Package
168
+ # @return [ReactiveShipping::TrackingResponse] The response from the carrier. This
169
+ # response should a list of shipment tracking events if successful.
170
+ def find_tracking_info(tracking_number, options = {})
171
+ options = @options.merge(options)
172
+ access_request = build_access_request
173
+ tracking_request = build_tracking_request(tracking_number, options)
174
+ response = commit(:track, save_request(access_request + tracking_request), options[:test])
175
+ parse_tracking_response(response, options)
176
+ end
177
+
178
+ def create_shipment(origin, destination, packages, options = {})
179
+ options = @options.merge(options)
180
+ packages = Array(packages)
181
+ access_request = build_access_request
182
+
183
+ # STEP 1: Confirm. Validation step, important for verifying price.
184
+ confirm_request = build_shipment_request(origin, destination, packages, options)
185
+ logger.debug(confirm_request) if logger
186
+
187
+ confirm_response = commit(:ship_confirm, save_request(access_request + confirm_request), (options[:test] || false))
188
+ logger.debug(confirm_response) if logger
189
+
190
+ # ... now, get the digest, it's needed to get the label. In theory,
191
+ # one could make decisions based on the price or some such to avoid
192
+ # surprises. This also has *no* error handling yet.
193
+ xml = parse_ship_confirm(confirm_response)
194
+ success = response_success?(xml)
195
+ message = response_message(xml)
196
+ raise message unless success
197
+ digest = response_digest(xml)
198
+
199
+ # STEP 2: Accept. Use shipment digest in first response to get the actual label.
200
+ accept_request = build_accept_request(digest, options)
201
+ logger.debug(accept_request) if logger
202
+
203
+ accept_response = commit(:ship_accept, save_request(access_request + accept_request), (options[:test] || false))
204
+ logger.debug(accept_response) if logger
205
+
206
+ # ...finally, build a map from the response that contains
207
+ # the label data and tracking information.
208
+ parse_ship_accept(accept_response)
209
+ end
210
+
211
+ def get_delivery_date_estimates(origin, destination, packages, pickup_date=Date.current, options = {})
212
+ origin, destination = upsified_location(origin), upsified_location(destination)
213
+ options = @options.merge(options)
214
+ packages = Array(packages)
215
+ access_request = build_access_request
216
+ dates_request = build_delivery_dates_request(origin, destination, packages, pickup_date, options)
217
+ response = commit(:delivery_dates, save_request(access_request + dates_request), (options[:test] || false))
218
+ parse_delivery_dates_response(origin, destination, packages, response, options)
219
+ end
220
+
221
+ def void_shipment(tracking, options={})
222
+ options = @options.merge(options)
223
+ access_request = build_access_request
224
+ void_request = build_void_request(tracking)
225
+ response = commit(:void, save_request(access_request + void_request), (options[:test] || false))
226
+ parse_void_response(response, options)
227
+ end
228
+
229
+ def maximum_address_field_length
230
+ # http://www.ups.com/worldshiphelp/WS12/ENU/AppHelp/CONNECT/Shipment_Data_Field_Descriptions.htm
231
+ 35
232
+ end
233
+
234
+ protected
235
+
236
+ def upsified_location(location)
237
+ if location.country_code == 'US' && US_TERRITORIES_TREATED_AS_COUNTRIES.include?(location.state)
238
+ atts = {:country => location.state}
239
+ [:zip, :city, :address1, :address2, :address3, :phone, :fax, :address_type].each do |att|
240
+ atts[att] = location.send(att)
241
+ end
242
+ Location.new(atts)
243
+ else
244
+ location
245
+ end
246
+ end
247
+
248
+ def build_access_request
249
+ xml_builder = Nokogiri::XML::Builder.new do |xml|
250
+ xml.AccessRequest do
251
+ xml.AccessLicenseNumber(@options[:key])
252
+ xml.UserId(@options[:login])
253
+ xml.Password(@options[:password])
254
+ end
255
+ end
256
+ xml_builder.to_xml
257
+ end
258
+
259
+ def build_rate_request(origin, destination, packages, options = {})
260
+ xml_builder = Nokogiri::XML::Builder.new do |xml|
261
+ xml.RatingServiceSelectionRequest do
262
+ xml.Request do
263
+ xml.RequestAction('Rate')
264
+ xml.RequestOption((options[:service].nil?) ? 'Shop' : 'Rate')
265
+ end
266
+
267
+ pickup_type = options[:pickup_type] || :daily_pickup
268
+
269
+ xml.PickupType do
270
+ xml.Code(PICKUP_CODES[pickup_type])
271
+ # not implemented: PickupType/PickupDetails element
272
+ end
273
+
274
+ cc = options[:customer_classification] || DEFAULT_CUSTOMER_CLASSIFICATIONS[pickup_type]
275
+ xml.CustomerClassification do
276
+ xml.Code(CUSTOMER_CLASSIFICATIONS[cc])
277
+ end
278
+
279
+ xml.Shipment do
280
+ # not implemented: Shipment/Description element
281
+ build_location_node(xml, 'Shipper', (options[:shipper] || origin), options)
282
+ build_location_node(xml, 'ShipTo', destination, options)
283
+ build_location_node(xml, 'ShipFrom', origin, options) if options[:shipper] && options[:shipper] != origin
284
+
285
+ # not implemented: * Shipment/ShipmentWeight element
286
+ # * Shipment/ReferenceNumber element
287
+ # * Shipment/Service element
288
+ # * Shipment/PickupDate element
289
+ # * Shipment/ScheduledDeliveryDate element
290
+ # * Shipment/ScheduledDeliveryTime element
291
+ # * Shipment/AlternateDeliveryTime element
292
+ # * Shipment/DocumentsOnly element
293
+
294
+ unless options[:service].nil?
295
+ xml.Service do
296
+ xml.Code(options[:service])
297
+ end
298
+ end
299
+
300
+ Array(packages).each do |package|
301
+ options[:imperial] ||= IMPERIAL_COUNTRIES.include?(origin.country_code(:alpha2))
302
+ build_package_node(xml, package, options)
303
+ end
304
+
305
+ # not implemented: * Shipment/ShipmentServiceOptions element
306
+ if options[:negotiated_rates]
307
+ xml.RateInformation do
308
+ xml.NegotiatedRatesIndicator
309
+ end
310
+ end
311
+ end
312
+ end
313
+ end
314
+ xml_builder.to_xml
315
+ end
316
+
317
+ # Build XML node to request a shipping label for the given packages.
318
+ #
319
+ # options:
320
+ # * origin_account: who will pay for the shipping label
321
+ # * customer_context: a "guid like substance" -- according to UPS
322
+ # * shipper: who is sending the package and where it should be returned
323
+ # if it is undeliverable.
324
+ # * ship_from: where the package is picked up.
325
+ # * service_code: default to '03'
326
+ # * saturday_delivery: any truthy value causes this element to exist
327
+ # * optional_processing: 'validate' (blank) or 'nonvalidate' or blank
328
+ # * paperless_invoice: set to truthy if using paperless invoice to ship internationally
329
+ # * terms_of_shipment: used with paperless invoice to specify who pays duties and taxes
330
+ # * reference_numbers: Array of hashes with :value => a reference number value and optionally :code => reference number type
331
+ # * prepay: if truthy the shipper will be bill immediatly. Otherwise the shipper is billed when the label is used.
332
+ # * negotiated_rates: if truthy negotiated rates will be requested from ups. Only valid if shipper account has negotiated rates.
333
+ # * delivery_confirmation: Can be set to any key from SHIPMENT_DELIVERY_CONFIRMATION_CODES. Can also be set on package level via package.options
334
+ def build_shipment_request(origin, destination, packages, options={})
335
+ packages = Array(packages)
336
+ shipper = options[:shipper] || origin
337
+ options[:international] = origin.country.name != destination.country.name
338
+ options[:imperial] ||= IMPERIAL_COUNTRIES.include?(shipper.country_code(:alpha2))
339
+ options[:return] = options[:return_service_code].present?
340
+ options[:reason_for_export] ||= ("RETURN" if options[:return])
341
+
342
+ if allow_package_level_reference_numbers(origin, destination)
343
+ if options[:reference_numbers]
344
+ packages.each do |package|
345
+ package.options[:reference_numbers] = options[:reference_numbers]
346
+ end
347
+ end
348
+ options[:reference_numbers] = []
349
+ end
350
+
351
+ handle_delivery_confirmation_options(origin, destination, packages, options)
352
+
353
+ xml_builder = Nokogiri::XML::Builder.new do |xml|
354
+ xml.ShipmentConfirmRequest do
355
+ xml.Request do
356
+ xml.RequestAction('ShipConfirm')
357
+ # Required element cotnrols level of address validation.
358
+ xml.RequestOption(options[:optional_processing] || 'validate')
359
+ # Optional element to identify transactions between client and server.
360
+ if options[:customer_context]
361
+ xml.TransactionReference do
362
+ xml.CustomerContext(options[:customer_context])
363
+ end
364
+ end
365
+ end
366
+
367
+ xml.Shipment do
368
+ xml.Service do
369
+ xml.Code(options[:service_code] || '03')
370
+ end
371
+
372
+ build_location_node(xml, 'ShipTo', destination, options)
373
+ build_location_node(xml, 'ShipFrom', origin, options)
374
+ # Required element. The company whose account is responsible for the label(s).
375
+ build_location_node(xml, 'Shipper', shipper, options)
376
+
377
+ if options[:saturday_delivery]
378
+ xml.ShipmentServiceOptions do
379
+ xml.SaturdayDelivery
380
+ end
381
+ end
382
+
383
+ if options[:origin_account]
384
+ xml.RateInformation do
385
+ xml.NegotiatedRatesIndicator
386
+ end
387
+ end
388
+
389
+ Array(options[:reference_numbers]).each do |reference_num_info|
390
+ xml.ReferenceNumber do
391
+ xml.Code(reference_num_info[:code] || "")
392
+ xml.Value(reference_num_info[:value])
393
+ end
394
+ end
395
+
396
+ if options[:prepay]
397
+ xml.PaymentInformation do
398
+ xml.Prepaid do
399
+ xml.BillShipper do
400
+ xml.AccountNumber(options[:origin_account])
401
+ end
402
+ end
403
+ end
404
+ elsif options[:bill_third_party]
405
+ xml.PaymentInformation do
406
+ xml.BillThirdParty do
407
+ xml.BillThirdPartyShipper do
408
+ xml.AccountNumber(options[:billing_account])
409
+ xml.ThirdParty do
410
+ xml.Address do
411
+ xml.PostalCode(options[:billing_zip])
412
+ xml.CountryCode(options[:billing_country])
413
+ end
414
+ end
415
+ end
416
+ end
417
+ end
418
+ else
419
+ xml.ItemizedPaymentInformation do
420
+ xml.ShipmentCharge do
421
+ # Type '01' means 'Transportation'
422
+ # This node specifies who will be billed for transportation.
423
+ xml.Type('01')
424
+ xml.BillShipper do
425
+ xml.AccountNumber(options[:origin_account])
426
+ end
427
+ end
428
+ if options[:terms_of_shipment] == 'DDP'
429
+ # DDP stands for delivery duty paid and means the shipper will cover duties and taxes
430
+ # Otherwise UPS will charge the receiver
431
+ xml.ShipmentCharge do
432
+ xml.Type('02') # Type '02' means 'Duties and Taxes'
433
+ xml.BillShipper do
434
+ xml.AccountNumber(options[:origin_account])
435
+ end
436
+ end
437
+ end
438
+ end
439
+ end
440
+
441
+ if options[:international]
442
+ unless options[:return]
443
+ build_location_node(xml, 'SoldTo', options[:sold_to] || destination, options)
444
+ end
445
+
446
+ if origin.country_code(:alpha2) == 'US' && ['CA', 'PR'].include?(destination.country_code(:alpha2))
447
+ # Required for shipments from the US to Puerto Rico or Canada
448
+ xml.InvoiceLineTotal do
449
+ total_value = packages.inject(0) {|sum, package| sum + (package.value || 0)}
450
+ xml.MonetaryValue(total_value)
451
+ end
452
+ end
453
+
454
+ contents_description = packages.map {|p| p.options[:description]}.compact.join(',')
455
+ unless contents_description.empty?
456
+ xml.Description(contents_description)
457
+ end
458
+ end
459
+
460
+ if options[:return]
461
+ xml.ReturnService do
462
+ xml.Code(options[:return_service_code])
463
+ end
464
+ end
465
+
466
+ xml.ShipmentServiceOptions do
467
+ if delivery_confirmation = options[:delivery_confirmation]
468
+ xml.DeliveryConfirmation do
469
+ xml.DCISType(SHIPMENT_DELIVERY_CONFIRMATION_CODES[delivery_confirmation])
470
+ end
471
+ end
472
+
473
+ if options[:international]
474
+ build_international_forms(xml, origin, destination, packages, options)
475
+ end
476
+ end
477
+
478
+ # A request may specify multiple packages.
479
+ packages.each do |package|
480
+ build_package_node(xml, package, options)
481
+ end
482
+ end
483
+
484
+ # Supported label formats:
485
+ # GIF, EPL, ZPL, STARPL and SPL
486
+ label_format = options[:label_format] ? options[:label_format].upcase : 'GIF'
487
+ label_size = options[:label_size] ? options[:label_size] : [4, 6]
488
+
489
+ xml.LabelSpecification do
490
+ xml.LabelStockSize do
491
+ xml.Height(label_size[0])
492
+ xml.Width(label_size[1])
493
+ end
494
+
495
+ xml.LabelPrintMethod do
496
+ xml.Code(label_format)
497
+ end
498
+
499
+ # API requires these only if returning a GIF formated label
500
+ if label_format == 'GIF'
501
+ xml.HTTPUserAgent('Mozilla/4.5')
502
+ xml.LabelImageFormat(label_format) do
503
+ xml.Code(label_format)
504
+ end
505
+ end
506
+ end
507
+ end
508
+ end
509
+ xml_builder.to_xml
510
+ end
511
+
512
+ def build_delivery_dates_request(origin, destination, packages, pickup_date, options={})
513
+ xml_builder = Nokogiri::XML::Builder.new do |xml|
514
+
515
+ xml.TimeInTransitRequest do
516
+ xml.Request do
517
+ xml.RequestAction('TimeInTransit')
518
+ end
519
+
520
+ build_address_artifact_format_location(xml, 'TransitFrom', origin)
521
+ build_address_artifact_format_location(xml, 'TransitTo', destination)
522
+
523
+ xml.ShipmentWeight do
524
+ xml.UnitOfMeasurement do
525
+ xml.Code(options[:imperial] ? 'LBS' : 'KGS')
526
+ end
527
+
528
+ value = packages.inject(0) do |sum, package|
529
+ sum + (options[:imperial] ? package.lbs.to_f : package.kgs.to_f )
530
+ end
531
+
532
+ xml.Weight([value.round(3), 0.1].max)
533
+ end
534
+
535
+ xml.InvoiceLineTotal do
536
+ xml.CurrencyCode('USD')
537
+ total_value = packages.inject(0) {|sum, package| sum + package.value}
538
+ xml.MonetaryValue(total_value)
539
+ end
540
+
541
+ xml.PickupDate(pickup_date.strftime('%Y%m%d'))
542
+ end
543
+ end
544
+
545
+ xml_builder.to_xml
546
+ end
547
+
548
+ def build_void_request(tracking)
549
+ xml_builder = Nokogiri::XML::Builder.new do |xml|
550
+ xml.VoidShipmentRequest do
551
+ xml.Request do
552
+ xml.RequestAction('Void')
553
+ end
554
+ xml.ShipmentIdentificationNumber(tracking)
555
+ end
556
+ end
557
+ xml_builder.to_xml
558
+ end
559
+
560
+ def build_international_forms(xml, origin, destination, packages, options)
561
+ if options[:paperless_invoice]
562
+ xml.InternationalForms do
563
+ xml.FormType('01') # 01 is "Invoice"
564
+ xml.InvoiceDate(options[:invoice_date] || Date.today.strftime('%Y%m%d'))
565
+ xml.ReasonForExport(options[:reason_for_export] || 'SALE')
566
+ xml.CurrencyCode(options[:currency_code] || 'USD')
567
+
568
+ if options[:terms_of_shipment]
569
+ xml.TermsOfShipment(options[:terms_of_shipment])
570
+ end
571
+
572
+ packages.each do |package|
573
+ xml.Product do |xml|
574
+ xml.Description(package.options[:description])
575
+ xml.CommodityCode(package.options[:commodity_code])
576
+ xml.OriginCountryCode(origin.country_code(:alpha2))
577
+ xml.Unit do |xml|
578
+ xml.Value(package.value / (package.options[:item_count] || 1))
579
+ xml.Number((package.options[:item_count] || 1))
580
+ xml.UnitOfMeasurement do |xml|
581
+ # NMB = number. You can specify units in barrels, boxes, etc. Codes are in the api docs.
582
+ xml.Code(package.options[:unit_of_item_count] || 'NMB')
583
+ end
584
+ end
585
+ end
586
+ end
587
+ end
588
+ end
589
+ end
590
+
591
+ def build_accept_request(digest, options = {})
592
+ xml_builder = Nokogiri::XML::Builder.new do |xml|
593
+ xml.ShipmentAcceptRequest do
594
+ xml.Request do
595
+ xml.RequestAction('ShipAccept')
596
+ end
597
+ xml.ShipmentDigest(digest)
598
+ end
599
+ end
600
+ xml_builder.to_xml
601
+ end
602
+
603
+ def build_tracking_request(tracking_number, options = {})
604
+ xml_builder = Nokogiri::XML::Builder.new do |xml|
605
+ xml.TrackRequest do
606
+ xml.TrackingOption(options[:tracking_option]) if options[:tracking_option]
607
+ xml.Request do
608
+ xml.RequestAction('Track')
609
+ xml.RequestOption('1')
610
+ end
611
+ xml.TrackingNumber(tracking_number.to_s)
612
+ xml.TrackingOption('03') if options[:mail_innovations]
613
+ end
614
+ end
615
+ xml_builder.to_xml
616
+ end
617
+
618
+ def build_location_node(xml, name, location, options = {})
619
+ # not implemented: * Shipment/Shipper/Name element
620
+ # * Shipment/(ShipTo|ShipFrom)/CompanyName element
621
+ # * Shipment/(Shipper|ShipTo|ShipFrom)/AttentionName element
622
+ # * Shipment/(Shipper|ShipTo|ShipFrom)/TaxIdentificationNumber element
623
+ xml.public_send(name) do
624
+ if shipper_name = (location.name || location.company_name || options[:origin_name])
625
+ xml.Name(shipper_name)
626
+ end
627
+ xml.PhoneNumber(location.phone.gsub(/[^\d]/, '')) unless location.phone.blank?
628
+ xml.FaxNumber(location.fax.gsub(/[^\d]/, '')) unless location.fax.blank?
629
+
630
+ if name == 'Shipper' and (origin_account = options[:origin_account] || @options[:origin_account])
631
+ xml.ShipperNumber(origin_account)
632
+ elsif name == 'ShipTo' and (destination_account = options[:destination_account] || @options[:destination_account])
633
+ xml.ShipperAssignedIdentificationNumber(destination_account)
634
+ end
635
+
636
+ if name = (location.company_name || location.name || options[:origin_name])
637
+ xml.CompanyName(name)
638
+ end
639
+
640
+ if phone = location.phone
641
+ xml.PhoneNumber(phone)
642
+ end
643
+
644
+ if attn = location.name
645
+ xml.AttentionName(attn)
646
+ end
647
+
648
+ xml.Address do
649
+ xml.AddressLine1(location.address1) unless location.address1.blank?
650
+ xml.AddressLine2(location.address2) unless location.address2.blank?
651
+ xml.AddressLine3(location.address3) unless location.address3.blank?
652
+ xml.City(location.city) unless location.city.blank?
653
+ xml.StateProvinceCode(location.province) unless location.province.blank?
654
+ # StateProvinceCode required for negotiated rates but not otherwise, for some reason
655
+ xml.PostalCode(location.postal_code) unless location.postal_code.blank?
656
+ xml.CountryCode(location.country_code(:alpha2)) unless location.country_code(:alpha2).blank?
657
+ xml.ResidentialAddressIndicator(true) unless location.commercial? # the default should be that UPS returns residential rates for destinations that it doesn't know about
658
+ # not implemented: Shipment/(Shipper|ShipTo|ShipFrom)/Address/ResidentialAddressIndicator element
659
+ end
660
+ end
661
+ end
662
+
663
+ def build_address_artifact_format_location(xml, name, location)
664
+ xml.public_send(name) do
665
+ xml.AddressArtifactFormat do
666
+ xml.PoliticalDivision2(location.city)
667
+ xml.PoliticalDivision1(location.province)
668
+ xml.CountryCode(location.country_code(:alpha2))
669
+ xml.PostcodePrimaryLow(location.postal_code)
670
+ xml.ResidentialAddressIndicator(true) unless location.commercial?
671
+ end
672
+ end
673
+ end
674
+
675
+ def build_package_node(xml, package, options = {})
676
+ xml.Package do
677
+ # not implemented: * Shipment/Package/PackagingType element
678
+
679
+ #return requires description
680
+ if options[:return]
681
+ contents_description = package.options[:description]
682
+ xml.Description(contents_description) if contents_description
683
+ end
684
+
685
+ xml.PackagingType do
686
+ xml.Code('02')
687
+ end
688
+
689
+ xml.Dimensions do
690
+ xml.UnitOfMeasurement do
691
+ xml.Code(options[:imperial] ? 'IN' : 'CM')
692
+ end
693
+ [:length, :width, :height].each do |axis|
694
+ value = ((options[:imperial] ? package.inches(axis) : package.cm(axis)).to_f * 1000).round / 1000.0 # 3 decimals
695
+ xml.public_send(axis.to_s.capitalize, [value, 0.1].max)
696
+ end
697
+ end
698
+
699
+ xml.PackageWeight do
700
+ if (options[:service] || options[:service_code]) == DEFAULT_SERVICE_NAME_TO_CODE["UPS SurePost (USPS) < 1lb"]
701
+ # SurePost < 1lb uses OZS, not LBS
702
+ code = options[:imperial] ? 'OZS' : 'KGS'
703
+ weight = options[:imperial] ? package.oz : package.kgs
704
+ else
705
+ code = options[:imperial] ? 'LBS' : 'KGS'
706
+ weight = options[:imperial] ? package.lbs : package.kgs
707
+ end
708
+ xml.UnitOfMeasurement do
709
+ xml.Code(code)
710
+ end
711
+
712
+ value = ((weight).to_f * 1000).round / 1000.0 # 3 decimals
713
+ xml.Weight([value, 0.1].max)
714
+ end
715
+
716
+
717
+ Array(package.options[:reference_numbers]).each do |reference_number_info|
718
+ xml.ReferenceNumber do
719
+ xml.Code(reference_number_info[:code] || "")
720
+ xml.Value(reference_number_info[:value])
721
+ end
722
+ end
723
+
724
+ xml.PackageServiceOptions do
725
+ if delivery_confirmation = package.options[:delivery_confirmation]
726
+ xml.DeliveryConfirmation do
727
+ xml.DCISType(PACKAGE_DELIVERY_CONFIRMATION_CODES[delivery_confirmation])
728
+ end
729
+ end
730
+
731
+ if dry_ice = package.options[:dry_ice]
732
+ xml.DryIce do
733
+ xml.RegulationSet(dry_ice[:regulation_set] || 'CFR')
734
+ xml.DryIceWeight do
735
+ xml.UnitOfMeasurement do
736
+ xml.Code(options[:imperial] ? 'LBS' : 'KGS')
737
+ end
738
+ # Cannot be more than package weight.
739
+ # Should be more than 0.0.
740
+ # Valid characters are 0-9 and .(Decimal point).
741
+ # Limit to 1 digit after the decimal. The maximum length
742
+ # of the field is 5 including . and can hold up
743
+ # to 1 decimal place.
744
+ xml.Weight(dry_ice[:weight])
745
+ end
746
+ end
747
+ end
748
+ end
749
+
750
+ # not implemented: * Shipment/Package/LargePackageIndicator element
751
+ # * Shipment/Package/AdditionalHandling element
752
+ end
753
+ end
754
+
755
+ def build_document(xml, expected_root_tag)
756
+ document = Nokogiri.XML(xml)
757
+ if document.root.nil? || document.root.name != expected_root_tag
758
+ raise ReactiveShipping::ResponseContentError.new(StandardError.new('Invalid document'), xml)
759
+ end
760
+ document
761
+ rescue Nokogiri::XML::SyntaxError => e
762
+ raise ReactiveShipping::ResponseContentError.new(e, xml)
763
+ end
764
+
765
+ def parse_rate_response(origin, destination, packages, response, options = {})
766
+ xml = build_document(response, 'RatingServiceSelectionResponse')
767
+ success = response_success?(xml)
768
+ message = response_message(xml)
769
+
770
+ if success
771
+ rate_estimates = xml.root.css('> RatedShipment').map do |rated_shipment|
772
+ service_code = rated_shipment.at('Service/Code').text
773
+ days_to_delivery = rated_shipment.at('GuaranteedDaysToDelivery').text.to_i
774
+ days_to_delivery = nil if days_to_delivery == 0
775
+ RateEstimate.new(origin, destination, @@name, service_name_for(origin, service_code),
776
+ :total_price => rated_shipment.at('TotalCharges/MonetaryValue').text.to_f,
777
+ :insurance_price => rated_shipment.at('ServiceOptionsCharges/MonetaryValue').text.to_f,
778
+ :currency => rated_shipment.at('TotalCharges/CurrencyCode').text,
779
+ :service_code => service_code,
780
+ :packages => packages,
781
+ :delivery_range => [timestamp_from_business_day(days_to_delivery)],
782
+ :negotiated_rate => rated_shipment.at('NegotiatedRates/NetSummaryCharges/GrandTotal/MonetaryValue').try(:text).to_f
783
+ )
784
+ end
785
+ end
786
+ RateResponse.new(success, message, Hash.from_xml(response).values.first, :rates => rate_estimates, :xml => response, :request => last_request)
787
+ end
788
+
789
+ def parse_tracking_response(response, options = {})
790
+ xml = build_document(response, 'TrackResponse')
791
+ success = response_success?(xml)
792
+ message = response_message(xml)
793
+
794
+ if success
795
+ delivery_signature = nil
796
+ exception_event, scheduled_delivery_date, actual_delivery_date = nil
797
+ delivered, exception = false
798
+ shipment_events = []
799
+
800
+ first_shipment = xml.root.at('Shipment')
801
+ first_package = first_shipment.at('Package')
802
+ tracking_number = first_shipment.at_xpath('ShipmentIdentificationNumber | Package/TrackingNumber').text
803
+
804
+ # Build status hash
805
+ status_nodes = first_package.css('Activity > Status > StatusType')
806
+
807
+ # Prefer a delivery node
808
+ status_node = status_nodes.detect { |x| x.at('Code').text == 'D' }
809
+ status_node ||= status_nodes.first
810
+
811
+ status_code = status_node.at('Code').text
812
+ status_description = status_node.at('Description').text
813
+ status = TRACKING_STATUS_CODES[status_code]
814
+
815
+ if status_description =~ /out.*delivery/i
816
+ status = :out_for_delivery
817
+ end
818
+
819
+ origin, destination = %w(Shipper ShipTo).map do |location|
820
+ location_from_address_node(first_shipment.at("#{location}/Address"))
821
+ end
822
+
823
+ # Get scheduled delivery date
824
+ unless status == :delivered
825
+ scheduled_delivery_date_node = first_shipment.at('ScheduledDeliveryDate')
826
+ scheduled_delivery_date_node ||= first_shipment.at('RescheduledDeliveryDate')
827
+
828
+ if scheduled_delivery_date_node
829
+ scheduled_delivery_date = parse_ups_datetime(
830
+ :date => scheduled_delivery_date_node,
831
+ :time => nil
832
+ )
833
+ end
834
+ end
835
+
836
+ activities = first_package.css('> Activity')
837
+ unless activities.empty?
838
+ shipment_events = activities.map do |activity|
839
+ description = activity.at('Status/StatusType/Description').text
840
+ type_code = activity.at('Status/StatusType/Code').text
841
+ zoneless_time = parse_ups_datetime(:time => activity.at('Time'), :date => activity.at('Date'))
842
+ location = location_from_address_node(activity.at('ActivityLocation/Address'))
843
+ ShipmentEvent.new(description, zoneless_time, location, nil, type_code)
844
+ end
845
+
846
+ shipment_events = shipment_events.sort_by(&:time)
847
+
848
+ # UPS will sometimes archive a shipment, stripping all shipment activity except for the delivery
849
+ # event (see test/fixtures/xml/delivered_shipment_without_events_tracking_response.xml for an example).
850
+ # This adds an origin event to the shipment activity in such cases.
851
+ if origin && !(shipment_events.count == 1 && status == :delivered)
852
+ first_event = shipment_events[0]
853
+ origin_event = ShipmentEvent.new(first_event.name, first_event.time, origin, first_event.message, first_event.type_code)
854
+
855
+ if within_same_area?(origin, first_event.location)
856
+ shipment_events[0] = origin_event
857
+ else
858
+ shipment_events.unshift(origin_event)
859
+ end
860
+ end
861
+
862
+ # Has the shipment been delivered?
863
+ if status == :delivered
864
+ delivered_activity = activities.first
865
+ delivery_signature = delivered_activity.at('ActivityLocation/SignedForByName').try(:text)
866
+ if delivered_activity.at('Status/StatusType/Code').text == 'D'
867
+ actual_delivery_date = parse_ups_datetime(:date => delivered_activity.at('Date'), :time => delivered_activity.at('Time'))
868
+ end
869
+ unless destination
870
+ destination = shipment_events[-1].location
871
+ end
872
+ shipment_events[-1] = ShipmentEvent.new(shipment_events.last.name, shipment_events.last.time, destination, shipment_events.last.message, shipment_events.last.type_code)
873
+ end
874
+ end
875
+
876
+ end
877
+ TrackingResponse.new(success, message, Hash.from_xml(response).values.first,
878
+ :carrier => @@name,
879
+ :xml => response,
880
+ :request => last_request,
881
+ :status => status,
882
+ :status_code => status_code,
883
+ :status_description => status_description,
884
+ :delivery_signature => delivery_signature,
885
+ :scheduled_delivery_date => scheduled_delivery_date,
886
+ :actual_delivery_date => actual_delivery_date,
887
+ :shipment_events => shipment_events,
888
+ :delivered => delivered,
889
+ :exception => exception,
890
+ :exception_event => exception_event,
891
+ :origin => origin,
892
+ :destination => destination,
893
+ :tracking_number => tracking_number)
894
+ end
895
+
896
+ def parse_delivery_dates_response(origin, destination, packages, response, options={})
897
+ xml = build_document(response, 'TimeInTransitResponse')
898
+ success = response_success?(xml)
899
+ message = response_message(xml)
900
+ delivery_estimates = []
901
+
902
+ if success
903
+ xml.css('ServiceSummary').each do |service_summary|
904
+ # Translate the Time in Transit Codes to the service codes used elsewhere
905
+ service_name = service_summary.at('Service/Description').text
906
+ service_code = UPS::DEFAULT_SERVICE_NAME_TO_CODE[service_name]
907
+ date = Date.strptime(service_summary.at('EstimatedArrival/Date').text, '%Y-%m-%d')
908
+ business_transit_days = service_summary.at('EstimatedArrival/BusinessTransitDays').text.to_i
909
+ delivery_estimates << DeliveryDateEstimate.new(origin, destination, self.class.class_variable_get(:@@name),
910
+ service_name,
911
+ :service_code => service_code,
912
+ :guaranteed => service_summary.at('Guaranteed/Code').text == 'Y',
913
+ :date => date,
914
+ :business_transit_days => business_transit_days)
915
+ end
916
+ end
917
+ response = DeliveryDateEstimatesResponse.new(success, message, Hash.from_xml(response).values.first, :delivery_estimates => delivery_estimates, :xml => response, :request => last_request)
918
+ end
919
+
920
+ def parse_void_response(response, options={})
921
+ xml = build_document(response, 'VoidShipmentResponse')
922
+ success = response_success?(xml)
923
+ message = response_message(xml)
924
+ if success
925
+ true
926
+ else
927
+ raise ResponseError.new("Void shipment failed with message: #{message}")
928
+ end
929
+ end
930
+
931
+ def location_from_address_node(address)
932
+ return nil unless address
933
+ country = address.at('CountryCode').try(:text)
934
+ country = 'US' if country == 'ZZ' # Sometimes returned by SUREPOST in the US
935
+ Location.new(
936
+ :country => country,
937
+ :postal_code => address.at('PostalCode').try(:text),
938
+ :province => address.at('StateProvinceCode').try(:text),
939
+ :city => address.at('City').try(:text),
940
+ :address1 => address.at('AddressLine1').try(:text),
941
+ :address2 => address.at('AddressLine2').try(:text),
942
+ :address3 => address.at('AddressLine3').try(:text)
943
+ )
944
+ end
945
+
946
+ def parse_ups_datetime(options = {})
947
+ time, date = options[:time].try(:text), options[:date].text
948
+ if time.nil?
949
+ hour, minute, second = 0
950
+ else
951
+ hour, minute, second = time.scan(/\d{2}/)
952
+ end
953
+ year, month, day = date[0..3], date[4..5], date[6..7]
954
+
955
+ Time.utc(year, month, day, hour, minute, second)
956
+ end
957
+
958
+ def response_success?(document)
959
+ document.root.at('Response/ResponseStatusCode').text == '1'
960
+ end
961
+
962
+ def response_message(document)
963
+ status = document.root.at_xpath('Response/ResponseStatusDescription').try(:text)
964
+ desc = document.root.at_xpath('Response/Error/ErrorDescription').try(:text)
965
+ [status, desc].select(&:present?).join(": ").presence || "UPS could not process the request."
966
+ end
967
+
968
+ def response_digest(xml)
969
+ xml.root.at('ShipmentDigest').text
970
+ end
971
+
972
+ def parse_ship_confirm(response)
973
+ build_document(response, 'ShipmentConfirmResponse')
974
+ end
975
+
976
+ def parse_ship_accept(response)
977
+ xml = build_document(response, 'ShipmentAcceptResponse')
978
+ success = response_success?(xml)
979
+ message = response_message(xml)
980
+
981
+ response_info = Hash.from_xml(response).values.first
982
+ packages = response_info["ShipmentResults"]["PackageResults"]
983
+ packages = [packages] if Hash === packages
984
+ labels = packages.map do |package|
985
+ Label.new(package["TrackingNumber"], Base64.decode64(package["LabelImage"]["GraphicImage"]))
986
+ end
987
+
988
+ LabelResponse.new(success, message, response_info, {labels: labels})
989
+ end
990
+
991
+ def commit(action, request, test = false)
992
+ ssl_post("#{test ? TEST_URL : LIVE_URL}/#{RESOURCES[action]}", request)
993
+ end
994
+
995
+ def within_same_area?(origin, location)
996
+ return false unless location
997
+ matching_country_codes = origin.country_code(:alpha2) == location.country_code(:alpha2)
998
+ matching_or_blank_city = location.city.blank? || location.city == origin.city
999
+ matching_country_codes && matching_or_blank_city
1000
+ end
1001
+
1002
+ def service_name_for(origin, code)
1003
+ origin = origin.country_code(:alpha2)
1004
+
1005
+ name = case origin
1006
+ when "CA" then CANADA_ORIGIN_SERVICES[code]
1007
+ when "MX" then MEXICO_ORIGIN_SERVICES[code]
1008
+ when *EU_COUNTRY_CODES then EU_ORIGIN_SERVICES[code]
1009
+ end
1010
+
1011
+ name ||= OTHER_NON_US_ORIGIN_SERVICES[code] unless name == 'US'
1012
+ name || DEFAULT_SERVICES[code]
1013
+ end
1014
+
1015
+ def allow_package_level_reference_numbers(origin, destination)
1016
+ # if the package is US -> US or PR -> PR the only type of reference numbers that are allowed are package-level
1017
+ # Otherwise the only type of reference numbers that are allowed are shipment-level
1018
+ [['US','US'],['PR', 'PR']].include?([origin,destination].map(&:country_code))
1019
+ end
1020
+
1021
+ def handle_delivery_confirmation_options(origin, destination, packages, options)
1022
+ if package_level_delivery_confirmation?(origin, destination)
1023
+ handle_package_level_delivery_confirmation(origin, destination, packages, options)
1024
+ else
1025
+ handle_shipment_level_delivery_confirmation(origin, destination, packages, options)
1026
+ end
1027
+ end
1028
+
1029
+ def handle_package_level_delivery_confirmation(origin, destination, packages, options)
1030
+ packages.each do |package|
1031
+ # Transfer shipment-level option to package with no specified delivery_confirmation
1032
+ package.options[:delivery_confirmation] = options[:delivery_confirmation] unless package.options[:delivery_confirmation]
1033
+
1034
+ # Assert that option is valid
1035
+ if package.options[:delivery_confirmation] && !PACKAGE_DELIVERY_CONFIRMATION_CODES[package.options[:delivery_confirmation]]
1036
+ raise "Invalid delivery_confirmation option on package: '#{package.options[:delivery_confirmation]}'. Use a key from PACKAGE_DELIVERY_CONFIRMATION_CODES"
1037
+ end
1038
+ end
1039
+ options.delete(:delivery_confirmation)
1040
+ end
1041
+
1042
+ def handle_shipment_level_delivery_confirmation(origin, destination, packages, options)
1043
+ if packages.any? { |p| p.options[:delivery_confirmation] }
1044
+ raise "origin/destination pair does not support package level delivery_confirmation options"
1045
+ end
1046
+
1047
+ if options[:delivery_confirmation] && !SHIPMENT_DELIVERY_CONFIRMATION_CODES[options[:delivery_confirmation]]
1048
+ raise "Invalid delivery_confirmation option: '#{options[:delivery_confirmation]}'. Use a key from SHIPMENT_DELIVERY_CONFIRMATION_CODES"
1049
+ end
1050
+ end
1051
+
1052
+ # For certain origin/destination pairs, UPS allows each package in a shipment to have a specified delivery_confirmation option
1053
+ # otherwise the delivery_confirmation option must be specified on the entire shipment.
1054
+ # See Appendix P of UPS Shipping Package XML Developers Guide for the rules on which the logic below is based.
1055
+ def package_level_delivery_confirmation?(origin, destination)
1056
+ origin.country_code == destination.country_code ||
1057
+ [['US','PR'], ['PR','US']].include?([origin,destination].map(&:country_code))
1058
+ end
1059
+ end
1060
+ end