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,708 @@
1
+ module ReactiveShipping
2
+ # After getting an API login from USPS (looks like '123YOURNAME456'),
3
+ # run the following test:
4
+ #
5
+ # usps = USPS.new(:login => '123YOURNAME456', :test => true)
6
+ # usps.valid_credentials?
7
+ #
8
+ # This will send a test request to the USPS test servers, which they ask you
9
+ # to do before they put your API key in production mode.
10
+ class USPS < Carrier
11
+ EventDetails = Struct.new(:description, :time, :zoneless_time, :location, :event_code)
12
+ ONLY_PREFIX_EVENTS = ['DELIVERED','OUT FOR DELIVERY']
13
+ self.retry_safe = true
14
+ self.ssl_version = :TLSv1_2
15
+
16
+ cattr_reader :name
17
+ @@name = "USPS"
18
+
19
+ LIVE_DOMAIN = 'production.shippingapis.com'
20
+ LIVE_RESOURCE = 'ShippingAPI.dll'
21
+
22
+ TEST_DOMAINS = { # indexed by security; e.g. TEST_DOMAINS[USE_SSL[:rates]]
23
+ true => 'secure.shippingapis.com',
24
+ false => 'stg-production.shippingapis.com'
25
+ }
26
+
27
+ API_CODES = {
28
+ :us_rates => 'RateV4',
29
+ :world_rates => 'IntlRateV2',
30
+ :test => 'CarrierPickupAvailability',
31
+ :track => 'TrackV2'
32
+ }
33
+ USE_SSL = {
34
+ :us_rates => false,
35
+ :world_rates => false,
36
+ :test => true,
37
+ :track => false
38
+ }
39
+
40
+ CONTAINERS = {
41
+ rectangular: 'RECTANGULAR',
42
+ variable: 'VARIABLE',
43
+ box: 'FLAT RATE BOX',
44
+ box_large: 'LG FLAT RATE BOX',
45
+ box_medium: 'MD FLAT RATE BOX',
46
+ box_small: 'SM FLAT RATE BOX',
47
+ envelope: 'FLAT RATE ENVELOPE',
48
+ envelope_legal: 'LEGAL FLAT RATE ENVELOPE',
49
+ envelope_padded: 'PADDED FLAT RATE ENVELOPE',
50
+ envelope_gift_card: 'GIFT CARD FLAT RATE ENVELOPE',
51
+ envelope_window: 'WINDOW FLAT RATE ENVELOPE',
52
+ envelope_small: 'SM FLAT RATE ENVELOPE',
53
+ package_service: 'PACKAGE SERVICE'
54
+ }
55
+
56
+ MAIL_TYPES = {
57
+ :package => 'Package',
58
+ :postcard => 'Postcards or aerogrammes',
59
+ :matter_for_the_blind => 'Matter for the blind',
60
+ :envelope => 'Envelope'
61
+ }
62
+
63
+ PACKAGE_PROPERTIES = {
64
+ 'ZipOrigination' => :origin_zip,
65
+ 'ZipDestination' => :destination_zip,
66
+ 'Pounds' => :pounds,
67
+ 'Ounces' => :ounces,
68
+ 'Container' => :container,
69
+ 'Size' => :size,
70
+ 'Machinable' => :machinable,
71
+ 'Zone' => :zone,
72
+ 'Postage' => :postage,
73
+ 'Restrictions' => :restrictions
74
+ }
75
+ POSTAGE_PROPERTIES = {
76
+ 'MailService' => :service,
77
+ 'Rate' => :rate
78
+ }
79
+ US_SERVICES = {
80
+ :first_class => 'FIRST CLASS',
81
+ :priority => 'PRIORITY',
82
+ :express => 'EXPRESS',
83
+ :bpm => 'BPM',
84
+ :parcel => 'PARCEL',
85
+ :media => 'MEDIA',
86
+ :library => 'LIBRARY',
87
+ :online => 'ONLINE',
88
+ :plus => 'PLUS',
89
+ :all => 'ALL'
90
+ }
91
+ DEFAULT_SERVICE = Hash.new(:all).update(
92
+ :base => :online,
93
+ :plus => :plus
94
+ )
95
+ DOMESTIC_RATE_FIELD = Hash.new('Rate').update(
96
+ :base => 'CommercialRate',
97
+ :plus => 'CommercialPlusRate'
98
+ )
99
+ INTERNATIONAL_RATE_FIELD = Hash.new('Postage').update(
100
+ :base => 'CommercialPostage',
101
+ :plus => 'CommercialPlusPostage'
102
+ )
103
+ COMMERCIAL_FLAG_NAME = {
104
+ :base => 'CommercialFlag',
105
+ :plus => 'CommercialPlusFlag'
106
+ }
107
+ FIRST_CLASS_MAIL_TYPES = {
108
+ :letter => 'LETTER',
109
+ :flat => 'FLAT',
110
+ :parcel => 'PARCEL',
111
+ :post_card => 'POSTCARD',
112
+ :package_service => 'PACKAGESERVICE'
113
+ }
114
+
115
+ ATTEMPTED_DELIVERY_CODES = %w(02 53 54 55 56 H0)
116
+
117
+ # Array of U.S. possessions according to USPS: https://www.usps.com/ship/official-abbreviations.htm
118
+ US_POSSESSIONS = %w(AS FM GU MH MP PW PR VI)
119
+
120
+ # Country names:
121
+ # http://pe.usps.gov/text/Imm/immctry.htm
122
+ COUNTRY_NAME_CONVERSIONS = {
123
+ "BA" => "Bosnia-Herzegovina",
124
+ "CD" => "Congo, Democratic Republic of the",
125
+ "CG" => "Congo (Brazzaville),Republic of the",
126
+ "CI" => "Côte d'Ivoire (Ivory Coast)",
127
+ "CK" => "Cook Islands (New Zealand)",
128
+ "FK" => "Falkland Islands",
129
+ "GB" => "Great Britain and Northern Ireland",
130
+ "GE" => "Georgia, Republic of",
131
+ "IR" => "Iran",
132
+ "KN" => "Saint Kitts (St. Christopher and Nevis)",
133
+ "KP" => "North Korea (Korea, Democratic People's Republic of)",
134
+ "KR" => "South Korea (Korea, Republic of)",
135
+ "LA" => "Laos",
136
+ "LY" => "Libya",
137
+ "MC" => "Monaco (France)",
138
+ "MD" => "Moldova",
139
+ "MK" => "Macedonia, Republic of",
140
+ "MM" => "Burma",
141
+ "PN" => "Pitcairn Island",
142
+ "RU" => "Russia",
143
+ "SK" => "Slovak Republic",
144
+ "TK" => "Tokelau (Union) Group (Western Samoa)",
145
+ "TW" => "Taiwan",
146
+ "TZ" => "Tanzania",
147
+ "VA" => "Vatican City",
148
+ "VG" => "British Virgin Islands",
149
+ "VN" => "Vietnam",
150
+ "WF" => "Wallis and Futuna Islands",
151
+ "WS" => "Western Samoa"
152
+ }
153
+
154
+ TRACKING_ODD_COUNTRY_NAMES = {
155
+ 'TAIWAN' => 'TW',
156
+ 'MACEDONIA THE FORMER YUGOSLAV REPUBLIC OF'=> 'MK',
157
+ 'MICRONESIA FEDERATED STATES OF' => 'FM',
158
+ 'MOLDOVA REPUBLIC OF' => 'MD',
159
+ }
160
+
161
+ RESPONSE_ERROR_MESSAGES = [
162
+ /There is no record of that mail item/,
163
+ /This Information has not been included in this Test Server\./,
164
+ /Delivery status information is not available/
165
+ ]
166
+
167
+ ESCAPING_AND_SYMBOLS = /&lt;\S*&gt;/
168
+ LEADING_USPS = /^USPS /
169
+ TRAILING_ASTERISKS = /\*+$/
170
+ SERVICE_NAME_SUBSTITUTIONS = /#{ESCAPING_AND_SYMBOLS}|#{LEADING_USPS}|#{TRAILING_ASTERISKS}/
171
+
172
+ def find_tracking_info(tracking_number, options = {})
173
+ options = @options.merge(options)
174
+ tracking_request = build_tracking_request(tracking_number, options)
175
+ response = commit(:track, tracking_request, options[:test] || false)
176
+ parse_tracking_response(response).first
177
+ end
178
+
179
+ def batch_find_tracking_info(tracking_infos, options = {})
180
+ options = @options.update(options)
181
+ tracking_request = build_tracking_batch_request(tracking_infos, options)
182
+ response = commit(:track, tracking_request, options[:test] || false)
183
+ parse_tracking_response(response, fault_tolerant: true)
184
+ end
185
+
186
+ def self.size_code_for(package)
187
+ if package.inches(:max) <= 12
188
+ 'REGULAR'
189
+ else
190
+ 'LARGE'
191
+ end
192
+ end
193
+
194
+ # from info at http://www.usps.com/businessmail101/mailcharacteristics/parcels.htm
195
+ #
196
+ # package.options[:books] -- 25 lb. limit instead of 35 for books or other printed matter.
197
+ # Defaults to false.
198
+ def self.package_machinable?(package, options = {})
199
+ at_least_minimum = package.inches(:length) >= 6.0 &&
200
+ package.inches(:width) >= 3.0 &&
201
+ package.inches(:height) >= 0.25 &&
202
+ package.ounces >= 6.0
203
+ at_most_maximum = package.inches(:length) <= 34.0 &&
204
+ package.inches(:width) <= 17.0 &&
205
+ package.inches(:height) <= 17.0 &&
206
+ package.pounds <= (package.options[:books] ? 25.0 : 35.0)
207
+ at_least_minimum && at_most_maximum
208
+ end
209
+
210
+ def requirements
211
+ [:login]
212
+ end
213
+
214
+ def find_rates(origin, destination, packages, options = {})
215
+ options = @options.merge(options)
216
+
217
+ origin = Location.from(origin)
218
+ destination = Location.from(destination)
219
+ packages = Array(packages)
220
+
221
+ domestic_codes = US_POSSESSIONS + ['US', nil]
222
+ if domestic_codes.include?(destination.country_code(:alpha2))
223
+ us_rates(origin, destination, packages, options)
224
+ else
225
+ world_rates(origin, destination, packages, options)
226
+ end
227
+ end
228
+
229
+ def valid_credentials?
230
+ # Cannot test with find_rates because USPS doesn't allow that in test mode
231
+ test_mode? ? canned_address_verification_works? : super
232
+ end
233
+
234
+ def maximum_weight
235
+ Measured::Weight.new(70, :pounds)
236
+ end
237
+
238
+ def extract_event_details(node)
239
+ description = node.at('Event').text.upcase
240
+
241
+ if prefix = ONLY_PREFIX_EVENTS.find { |p| description.start_with?(p) }
242
+ description = prefix
243
+ end
244
+
245
+ time = if node.at('EventDate').text.present?
246
+ timestamp = "#{node.at('EventDate').text}, #{node.at('EventTime').text}"
247
+ Time.parse(timestamp)
248
+ else
249
+ # Epoch time, because we need to sort properly by time
250
+ Time.at(0)
251
+ end
252
+
253
+ event_code = node.at('EventCode').text
254
+ city = node.at('EventCity').try(:text)
255
+ state = node.at('EventState').try(:text)
256
+ zip_code = node.at('EventZIPCode').try(:text)
257
+
258
+ country_node = node.at('EventCountry')
259
+ country = country_node ? country_node.text : ''
260
+ country = 'UNITED STATES' if country.empty?
261
+ # USPS returns upcased country names which ActiveUtils doesn't recognize without translation
262
+ country = find_country_code_case_insensitive(country)
263
+
264
+ zoneless_time = Time.utc(time.year, time.month, time.mday, time.hour, time.min, time.sec)
265
+ location = Location.new(city: city, state: state, postal_code: zip_code, country: country)
266
+ EventDetails.new(description, time, zoneless_time, location, event_code)
267
+ end
268
+
269
+ def maximum_address_field_length
270
+ # https://www.usps.com/business/web-tools-apis/address-information-api.pdf
271
+ 38
272
+ end
273
+
274
+ protected
275
+
276
+ def build_tracking_request(tracking_number, options = {})
277
+ build_tracking_batch_request([{
278
+ number: tracking_number,
279
+ destination_zip: options[:destination_zip],
280
+ mailing_date: options[:mailing_date]
281
+ }], options)
282
+ end
283
+
284
+ def build_tracking_batch_request(tracking_infos, options)
285
+ xml_builder = Nokogiri::XML::Builder.new do |xml|
286
+ xml.TrackFieldRequest('USERID' => options[:login]) do
287
+ xml.Revision { xml.text('1') }
288
+ xml.ClientIp { xml.text(options[:client_ip] || '127.0.0.1') }
289
+ xml.SourceId { xml.text(options[:source_id] || 'reactive_shipping') }
290
+ tracking_infos.each do |info|
291
+ xml.TrackID('ID' => info[:number]) do
292
+ xml.DestinationZipCode { xml.text(strip_zip(info[:destination_zip]))} if info[:destination_zip]
293
+ if info[:mailing_date]
294
+ formatted_date = info[:mailing_date].strftime('%Y-%m-%d')
295
+ xml.MailingDate { xml.text(formatted_date)}
296
+ end
297
+ end
298
+ end
299
+ end
300
+ end
301
+ xml_builder.to_xml
302
+ end
303
+
304
+ def us_rates(origin, destination, packages, options = {})
305
+ request = build_us_rate_request(packages, origin.zip, destination.zip, options)
306
+ # never use test mode; rate requests just won't work on test servers
307
+ parse_rate_response(origin, destination, packages, commit(:us_rates, request, false), options)
308
+ end
309
+
310
+ def world_rates(origin, destination, packages, options = {})
311
+ request = build_world_rate_request(origin, packages, destination, options)
312
+ # never use test mode; rate requests just won't work on test servers
313
+ parse_rate_response(origin, destination, packages, commit(:world_rates, request, false), options)
314
+ end
315
+
316
+ # Once the address verification API is implemented, remove this and have valid_credentials? build the request using that instead.
317
+ def canned_address_verification_works?
318
+ return false unless @options[:login]
319
+ request = <<-EOF
320
+ <?xml version="1.0" encoding="UTF-8"?>
321
+ <CarrierPickupAvailabilityRequest USERID="#{URI.encode(@options[:login])}">
322
+ <FirmName>Shopifolk</FirmName>
323
+ <SuiteOrApt>Suite 0</SuiteOrApt>
324
+ <Address2>18 Fair Ave</Address2>
325
+ <Urbanization />
326
+ <City>San Francisco</City>
327
+ <State>CA</State>
328
+ <ZIP5>94110</ZIP5>
329
+ <ZIP4>9411</ZIP4>
330
+ </CarrierPickupAvailabilityRequest>
331
+ EOF
332
+ xml = Nokogiri.XML(commit(:test, request, true)) { |config| config.strict }
333
+ xml.at('/CarrierPickupAvailabilityResponse/City').try(:text) == 'SAN FRANCISCO' && xml.at('/CarrierPickupAvailabilityResponse/Address2').try(:text) == '18 FAIR AVE'
334
+ end
335
+
336
+ # options[:service] -- One of [:first_class, :priority, :express, :bpm, :parcel,
337
+ # :media, :library, :online, :plus, :all]. defaults to :all.
338
+ # options[:books] -- Either true or false. Packages of books or other printed matter
339
+ # have a lower weight limit to be considered machinable.
340
+ # package.options[:container] -- Can be :rectangular, :variable, or a flat rate container
341
+ # defined in CONTAINERS.
342
+ # package.options[:machinable] -- Either true or false. Overrides the detection of
343
+ # "machinability" entirely.
344
+ def build_us_rate_request(packages, origin_zip, destination_zip, options = {})
345
+ xml_builder = Nokogiri::XML::Builder.new do |xml|
346
+ xml.RateV4Request('USERID' => @options[:login]) do
347
+ Array(packages).each_with_index do |package, id|
348
+ xml.Package('ID' => id) do
349
+ commercial_type = commercial_type(options)
350
+ default_service = DEFAULT_SERVICE[commercial_type]
351
+ service = options.fetch(:service, default_service).to_sym
352
+
353
+ if commercial_type && service != default_service
354
+ raise ArgumentError, "Commercial #{commercial_type} rates are only provided with the #{default_service.inspect} service."
355
+ end
356
+
357
+ xml.Service(US_SERVICES[service])
358
+ xml.FirstClassMailType(FIRST_CLASS_MAIL_TYPES[options[:first_class_mail_type].try(:to_sym)])
359
+ xml.ZipOrigination(strip_zip(origin_zip))
360
+ xml.ZipDestination(strip_zip(destination_zip))
361
+ xml.Pounds(0)
362
+ xml.Ounces("%0.1f" % [package.ounces, 1].max)
363
+ size_code = USPS.size_code_for(package)
364
+ container = CONTAINERS[package.options[:container]]
365
+ container ||= (package.cylinder? ? 'NONRECTANGULAR' : 'RECTANGULAR') if size_code == 'LARGE'
366
+ xml.Container(container)
367
+ xml.Size(size_code)
368
+ xml.Width("%0.2f" % package.inches(:width))
369
+ xml.Length("%0.2f" % package.inches(:length))
370
+ xml.Height("%0.2f" % package.inches(:height))
371
+ xml.Girth("%0.2f" % package.inches(:girth))
372
+ is_machinable = if package.options.has_key?(:machinable)
373
+ package.options[:machinable] ? true : false
374
+ else
375
+ USPS.package_machinable?(package)
376
+ end
377
+ xml.Machinable(is_machinable.to_s.upcase)
378
+ end
379
+ end
380
+ end
381
+ end
382
+ save_request(xml_builder.to_xml)
383
+ end
384
+
385
+ # important difference with international rate requests:
386
+ # * services are not given in the request
387
+ # * package sizes are not given in the request
388
+ # * services are returned in the response along with restrictions of size
389
+ # * the size restrictions are returned AS AN ENGLISH SENTENCE (!?)
390
+ #
391
+ #
392
+ # package.options[:mail_type] -- one of [:package, :postcard, :matter_for_the_blind, :envelope].
393
+ # Defaults to :package.
394
+ def build_world_rate_request(origin, packages, destination, options)
395
+ country = COUNTRY_NAME_CONVERSIONS[destination.country.code(:alpha2).value] || destination.country.name
396
+ xml_builder = Nokogiri::XML::Builder.new do |xml|
397
+ xml.IntlRateV2Request('USERID' => @options[:login]) do
398
+ xml.Revision(2)
399
+ Array(packages).each_with_index do |package, id|
400
+ xml.Package('ID' => id) do
401
+ xml.Pounds(0)
402
+ xml.Ounces([package.ounces, 1].max.ceil) # takes an integer for some reason, must be rounded UP
403
+ xml.MailType(MAIL_TYPES[package.options[:mail_type]] || 'Package')
404
+ xml.GXG do
405
+ xml.POBoxFlag(destination.po_box? ? 'Y' : 'N')
406
+ xml.GiftFlag(package.gift? ? 'Y' : 'N')
407
+ end
408
+
409
+ value = if package.value && package.value > 0 && package.currency && package.currency != 'USD'
410
+ 0.0
411
+ else
412
+ (package.value || 0) / 100.0
413
+ end
414
+ xml.ValueOfContents(value)
415
+
416
+ xml.Country(country)
417
+ xml.Container(package.cylinder? ? 'NONRECTANGULAR' : 'RECTANGULAR')
418
+ xml.Size(USPS.size_code_for(package))
419
+ xml.Width("%0.2f" % [package.inches(:width), 0.01].max)
420
+ xml.Length("%0.2f" % [package.inches(:length), 0.01].max)
421
+ xml.Height("%0.2f" % [package.inches(:height), 0.01].max)
422
+ xml.Girth("%0.2f" % [package.inches(:girth), 0.01].max)
423
+ xml.OriginZip(strip_zip(origin.zip))
424
+ if commercial_type = commercial_type(options)
425
+ xml.public_send(COMMERCIAL_FLAG_NAME.fetch(commercial_type), 'Y')
426
+ end
427
+ if destination.zip.present?
428
+ xml.AcceptanceDateTime((options[:acceptance_time] || Time.now.utc).iso8601)
429
+ xml.DestinationPostalCode(destination.zip)
430
+ end
431
+ end
432
+ end
433
+ end
434
+ end
435
+ save_request(xml_builder.to_xml)
436
+ end
437
+
438
+ def parse_rate_response(origin, destination, packages, response, options = {})
439
+ success = true
440
+ message = ''
441
+ rate_hash = {}
442
+
443
+ xml = Nokogiri.XML(response)
444
+
445
+ if error = xml.at_xpath('/Error | //ServiceErrors/ServiceError')
446
+ success = false
447
+ message = error.at('Description').text
448
+ else
449
+ xml.root.xpath('Package').each do |package|
450
+ if package.at('Error')
451
+ success = false
452
+ message = package.at('Error/Description').text
453
+ break
454
+ end
455
+ end
456
+
457
+ if success
458
+ rate_hash = rates_from_response_node(xml, packages, options)
459
+ unless rate_hash
460
+ success = false
461
+ message = "Unknown root node in XML response: '#{xml.root.name}'"
462
+ end
463
+ end
464
+
465
+ end
466
+
467
+ if success
468
+ rate_estimates = rate_hash.keys.map do |service_name|
469
+ RateEstimate.new(origin, destination, @@name, "USPS #{service_name}",
470
+ :package_rates => rate_hash[service_name][:package_rates],
471
+ :service_code => rate_hash[service_name][:service_code],
472
+ :currency => 'USD')
473
+ end
474
+ rate_estimates.reject! { |e| e.package_count != packages.length }
475
+ rate_estimates = rate_estimates.sort_by(&:total_price)
476
+ end
477
+
478
+ RateResponse.new(success, message, Hash.from_xml(response), :rates => rate_estimates, :xml => response, :request => last_request)
479
+ end
480
+
481
+ def rates_from_response_node(response_node, packages, options = {})
482
+ rate_hash = {}
483
+ return false unless (root_node = response_node.at_xpath('/IntlRateV2Response | /RateV4Response'))
484
+
485
+ commercial_type = commercial_type(options)
486
+ service_node, service_code_node, service_name_node, rate_node = if root_node.name == 'RateV4Response'
487
+ %w(Postage CLASSID MailService) << DOMESTIC_RATE_FIELD[commercial_type]
488
+ else
489
+ %w(Service ID SvcDescription) << INTERNATIONAL_RATE_FIELD[commercial_type]
490
+ end
491
+
492
+ root_node.xpath('Package').each do |package_node|
493
+ this_package = packages[package_node['ID'].to_i]
494
+
495
+ package_node.xpath(service_node).each do |service_response_node|
496
+ service_name = service_response_node.at(service_name_node).text
497
+
498
+ service_name.gsub!(SERVICE_NAME_SUBSTITUTIONS, '')
499
+
500
+ # aggregate specific package rates into a service-centric RateEstimate
501
+ # first package with a given service name will initialize these;
502
+ # later packages with same service will add to them
503
+ this_service = rate_hash[service_name] ||= {}
504
+ this_service[:service_code] ||= service_response_node.attributes[service_code_node].value
505
+ package_rates = this_service[:package_rates] ||= []
506
+ this_package_rate = {:package => this_package,
507
+ :rate => Package.cents_from(rate_value(rate_node, service_response_node, commercial_type))}
508
+
509
+ package_rates << this_package_rate if package_valid_for_service(this_package, service_response_node)
510
+ end
511
+ end
512
+ rate_hash
513
+ end
514
+
515
+ def package_valid_for_service(package, service_node)
516
+ return true if service_node.at('MaxWeight').nil?
517
+ max_weight = service_node.at('MaxWeight').text.to_f
518
+ name = service_node.at_xpath('SvcDescription | MailService').text.downcase
519
+
520
+ if name =~ /flat.rate.box/ # domestic or international flat rate box
521
+ # flat rate dimensions from http://www.usps.com/shipping/flatrate.htm
522
+ return (package_valid_for_max_dimensions(package,
523
+ :weight => max_weight, # domestic apparently has no weight restriction
524
+ :length => 11.0,
525
+ :width => 8.5,
526
+ :height => 5.5) or
527
+ package_valid_for_max_dimensions(package,
528
+ :weight => max_weight,
529
+ :length => 13.625,
530
+ :width => 11.875,
531
+ :height => 3.375))
532
+ elsif name =~ /flat.rate.envelope/
533
+ return package_valid_for_max_dimensions(package,
534
+ :weight => max_weight,
535
+ :length => 12.5,
536
+ :width => 9.5,
537
+ :height => 0.75)
538
+ elsif service_node.at('MailService') # domestic non-flat rates
539
+ return true
540
+ else # international non-flat rates
541
+ # Some sample english that this is required to parse:
542
+ #
543
+ # 'Max. length 46", width 35", height 46" and max. length plus girth 108"'
544
+ # 'Max. length 24", Max. length, height, depth combined 36"'
545
+ #
546
+ sentence = CGI.unescapeHTML(service_node.at('MaxDimensions').text)
547
+ tokens = sentence.downcase.split(/[^\d]*"/).reject(&:empty?)
548
+ max_dimensions = {:weight => max_weight}
549
+ single_axis_values = []
550
+ tokens.each do |token|
551
+ axis_sum = [/length/, /width/, /height/, /depth/].sum { |regex| (token =~ regex) ? 1 : 0 }
552
+ unless axis_sum == 0
553
+ value = token[/\d+$/].to_f
554
+ if axis_sum == 3
555
+ max_dimensions[:length_plus_width_plus_height] = value
556
+ elsif token =~ /girth/ and axis_sum == 1
557
+ max_dimensions[:length_plus_girth] = value
558
+ else
559
+ single_axis_values << value
560
+ end
561
+ end
562
+ end
563
+ single_axis_values.sort!.reverse!
564
+ [:length, :width, :height].each_with_index do |axis, i|
565
+ max_dimensions[axis] = single_axis_values[i] if single_axis_values[i]
566
+ end
567
+ package_valid_for_max_dimensions(package, max_dimensions)
568
+ end
569
+ end
570
+
571
+ def package_valid_for_max_dimensions(package, dimensions)
572
+ ((not ([:length, :width, :height].map { |dim| dimensions[dim].nil? || dimensions[dim].to_f >= package.inches(dim).to_f }.include?(false))) and
573
+ (dimensions[:weight].nil? || dimensions[:weight] >= package.pounds) and
574
+ (dimensions[:length_plus_girth].nil? or
575
+ dimensions[:length_plus_girth].to_f >=
576
+ package.inches(:length) + package.inches(:girth)) and
577
+ (dimensions[:length_plus_width_plus_height].nil? or
578
+ dimensions[:length_plus_width_plus_height].to_f >=
579
+ package.inches(:length) + package.inches(:width) + package.inches(:height)))
580
+ end
581
+
582
+ def parse_tracking_response(response, options = {})
583
+ xml = Nokogiri.XML(response)
584
+
585
+ if has_error?(xml)
586
+ message = error_description_node(xml).text
587
+ # actually raises instead of returning by nature of TrackingResponse#initialize
588
+ return TrackingResponse.new(false, message, Hash.from_xml(response),
589
+ carrier: @@name, xml: response, request: last_request)
590
+ end
591
+
592
+ # Responses are always returned in the order originally given.
593
+ if options[:fault_tolerant]
594
+ xml.root.xpath('TrackInfo').map do |info|
595
+ # Don't let one failure wreck the whole batch
596
+ begin
597
+ parse_tracking_info(response, info)
598
+ rescue ResponseError => e
599
+ e.response
600
+ end
601
+ end
602
+ else
603
+ xml.root.xpath('TrackInfo').map { |info| parse_tracking_info(response, info) }
604
+ end
605
+ end
606
+
607
+
608
+ def parse_tracking_info(response, node)
609
+ success = !has_error?(node)
610
+ message = response_message(node)
611
+
612
+ if success
613
+ destination = nil
614
+ shipment_events = []
615
+ tracking_details = node.xpath('TrackDetail')
616
+ tracking_details << node.at('TrackSummary')
617
+
618
+ tracking_number = node.attributes['ID'].value
619
+ prediction_node = node.at('PredictedDeliveryDate') || node.at('ExpectedDeliveryDate')
620
+ scheduled_delivery = prediction_node ? Time.parse(prediction_node.text) : nil
621
+
622
+ tracking_details.each do |event|
623
+ details = extract_event_details(event)
624
+ if details.location
625
+ shipment_events << ShipmentEvent.new(details.description, details.zoneless_time,
626
+ details.location, details.description, details.event_code)
627
+ end
628
+ end
629
+
630
+ shipment_events = shipment_events.sort_by(&:time)
631
+
632
+ attempted_delivery_date = shipment_events.detect{ |shipment_event| ATTEMPTED_DELIVERY_CODES.include?(shipment_event.type_code) }.try(:time)
633
+
634
+ if last_shipment = shipment_events.last
635
+ status = last_shipment.status
636
+ actual_delivery_date = last_shipment.time if last_shipment.delivered?
637
+ end
638
+ end
639
+
640
+ TrackingResponse.new(success, message, Hash.from_xml(response),
641
+ :carrier => @@name,
642
+ :xml => response,
643
+ :request => last_request,
644
+ :shipment_events => shipment_events,
645
+ :destination => destination,
646
+ :tracking_number => tracking_number,
647
+ :status => status,
648
+ :actual_delivery_date => actual_delivery_date,
649
+ :attempted_delivery_date => attempted_delivery_date,
650
+ :scheduled_delivery_date => scheduled_delivery
651
+ )
652
+ end
653
+
654
+ def error_description_node(node)
655
+ node.xpath('Error/Description')
656
+ end
657
+
658
+ def response_status_node(node)
659
+ node.at('StatusSummary') || error_description_node(node)
660
+ end
661
+
662
+ def has_error?(node)
663
+ node.xpath('Error').length > 0
664
+ end
665
+
666
+ def response_message(document)
667
+ response_status_node(document).text
668
+ end
669
+
670
+ def find_country_code_case_insensitive(name)
671
+ upcase_name = name.upcase.gsub(' ', ', ')
672
+ if special = TRACKING_ODD_COUNTRY_NAMES[upcase_name]
673
+ return special
674
+ end
675
+ country = ActiveUtils::Country::COUNTRIES.detect { |c| c[:name].upcase == upcase_name }
676
+ raise ReactiveShipping::Error, "No country found for #{name}" unless country
677
+ country[:alpha2]
678
+ end
679
+
680
+ def commit(action, request, test = false)
681
+ ssl_get(request_url(action, request, test))
682
+ end
683
+
684
+ def request_url(action, request, test)
685
+ scheme = USE_SSL[action] ? 'https://' : 'http://'
686
+ host = test ? TEST_DOMAINS[USE_SSL[action]] : LIVE_DOMAIN
687
+ "#{scheme}#{host}/#{LIVE_RESOURCE}?API=#{API_CODES[action]}&XML=#{URI.encode(request)}"
688
+ end
689
+
690
+ def strip_zip(zip)
691
+ zip.to_s.scan(/\d{5}/).first || zip
692
+ end
693
+
694
+ private
695
+
696
+ def rate_value(rate_node, service_response_node, commercial_type)
697
+ service_response_node.at(rate_node).try(:text).to_f
698
+ end
699
+
700
+ def commercial_type(options)
701
+ if options[:commercial_plus] == true
702
+ :plus
703
+ elsif options[:commercial_base] == true
704
+ :base
705
+ end
706
+ end
707
+ end
708
+ end