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