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,86 @@
1
+ module ReactiveShipping
2
+
3
+ class USPSReturns < Carrier
4
+
5
+ self.retry_safe = true
6
+
7
+ cattr_reader :name
8
+ @@name = "USPS Returns"
9
+
10
+ LIVE_DOMAIN = 'returns.usps.com'
11
+ LIVE_RESOURCE = 'Services/ExternalCreateReturnLabel.svc/ExternalCreateReturnLabel'
12
+
13
+ TEST_DOMAIN = 'returns.usps.com'
14
+ TEST_RESOURCE = 'Services/ExternalCreateReturnLabel.svc/ExternalCreateReturnLabel'
15
+
16
+ API_CODES = {
17
+ :external_return_label_request => 'externalReturnLabelRequest'
18
+ }
19
+
20
+ USE_SSL = {
21
+ :external_return_label_request => true
22
+ }
23
+
24
+ def requirements
25
+ []
26
+ end
27
+
28
+ def external_return_label_request(label, options = {})
29
+ response = commit(:external_return_label_request, label.to_xml, (options[:test] || false))
30
+ parse_external_return_label_response(response)
31
+ end
32
+
33
+ protected
34
+
35
+ def parse_external_return_label_response(response)
36
+ tracking_number, postal_routing, return_label, message = '', '', '', '', ''
37
+ xml = Nokogiri::XML(response)
38
+ error = external_return_label_errors(xml)
39
+ if error.is_a?(Hash) && error.size > 0
40
+ message << "#{error[:error][:code]}: #{error[:error][:message]}"
41
+ else
42
+ tracking_number = xml.at('TrackingNumber').try(:text)
43
+ postal_routing = xml.at('PostalRouting').try(:text)
44
+ return_label = xml.at('ReturnLabel').try(:text)
45
+ end
46
+
47
+ ExternalReturnLabelResponse.new(message.length == 0, message, Hash.from_xml(response),
48
+ :xml => response,
49
+ :carrier => @@name,
50
+ :request => last_request,
51
+ :return_label => return_label,
52
+ :postal_routing => postal_routing,
53
+ :tracking_number => tracking_number
54
+ )
55
+ end
56
+
57
+ def external_return_label_errors(document)
58
+ return {} unless document.respond_to?(:elements)
59
+ res = {}
60
+ if node = document.at('*/errors')
61
+ if node.at('ExternalReturnLabelError')
62
+ if message = node.at('ExternalReturnLabelError/InternalErrorDescription').try(:text)
63
+ code = node.at('ExternalReturnLabelError/InternalErrorNumber').try(:text) || ''
64
+ res = {:error => {:code => code, :message => message}}
65
+ elsif message = node.at('ExternalReturnLabelError/ExternalErrorDescription').try(:text)
66
+ code = node.at('ExternalReturnLabelError/ExternalErrorNumber').try(:text) || ''
67
+ res = {:error => {:code => code, :message => message}}
68
+ end
69
+ end
70
+ end
71
+ res
72
+ end
73
+
74
+ def commit(action, request, test = false)
75
+ ssl_get(request_url(action, request, test))
76
+ end
77
+
78
+ def request_url(action, request, test)
79
+ scheme = USE_SSL[action] ? 'https://' : 'http://'
80
+ host = test ? TEST_DOMAIN : LIVE_DOMAIN
81
+ resource = test ? TEST_RESOURCE : LIVE_RESOURCE
82
+ "#{scheme}#{host}/#{resource}?#{API_CODES[action]}=#{URI.encode(request)}"
83
+ end
84
+
85
+ end
86
+ end
@@ -0,0 +1,20 @@
1
+ module ReactiveShipping
2
+ class DeliveryDateEstimate
3
+ attr_reader :origin
4
+ attr_reader :destination
5
+ attr_reader :carrier
6
+ attr_reader :service_name
7
+ attr_reader :service_code
8
+ attr_reader :date
9
+ attr_reader :guaranteed
10
+ attr_reader :business_transit_days
11
+
12
+ def initialize(origin, destination, carrier, service_name, options={})
13
+ @origin, @destination, @carrier, @service_name = origin, destination, carrier, service_name
14
+ @service_code = options[:service_code]
15
+ @date = options[:date]
16
+ @guaranteed = options[:guaranteed]
17
+ @business_transit_days = options[:business_transit_days]
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,11 @@
1
+ module ReactiveShipping
2
+ class DeliveryDateEstimatesResponse < Response
3
+ attr_reader :delivery_estimates
4
+
5
+ def initialize(success, message, params = {}, options = {})
6
+ @delivery_estimates = Array(options[:delivery_estimates])
7
+ super
8
+ end
9
+
10
+ end
11
+ end
@@ -0,0 +1,35 @@
1
+ module ReactiveShipping
2
+ class Error < ActiveUtils::ActiveUtilsError
3
+ end
4
+
5
+ class ResponseError < ReactiveShipping::Error
6
+ attr_reader :response
7
+
8
+ def initialize(response = nil)
9
+ if response.is_a? Response
10
+ super(response.message)
11
+ @response = response
12
+ else
13
+ super(response)
14
+ end
15
+ end
16
+ end
17
+
18
+ class ResponseContentError < ReactiveShipping::Error
19
+ def initialize(exception, content_body = nil)
20
+ super([exception.message, content_body].compact.join(" \n\n"))
21
+ end
22
+ end
23
+
24
+ class ShipmentNotFound < ReactiveShipping::Error
25
+ end
26
+
27
+ class USPSValidationError < StandardError
28
+ end
29
+
30
+ class USPSMissingRequiredTagError < StandardError
31
+ def initialize(tag, prop)
32
+ super("Missing required tag #{tag} set by property #{prop}")
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,417 @@
1
+ module ReactiveShipping
2
+ class ExternalReturnLabelRequest
3
+ CAP_STRING_LEN = 100
4
+
5
+ USPS_EMAIL_REGEX = /^([a-zA-Z0-9_\-\.]+)@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.)|(([a-zA-Z0-9\-]+\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})(\]?)$/
6
+
7
+ LABEL_FORMAT = {
8
+ 'Instructions' => 'null',
9
+ 'No Instructions' => 'NOI',
10
+ 'Double Label' => 'TWO'
11
+ }
12
+
13
+ SERVICE_TYPE_CODE = [
14
+ '044', '019', '596', '020', '597','022', '024', '017', '018'
15
+ ]
16
+
17
+ CALL_CENTER_OR_SELF_SERVICE = ['CallCenter', 'Customer']
18
+
19
+ LABEL_DEFINITION = ['4X6', 'Zebra-4X6', '4X4', '3X6']
20
+
21
+ IMAGE_TYPE = ['PDF', 'TIF']
22
+
23
+ attr_reader :customer_name,
24
+ :customer_address1,
25
+ :customer_address2,
26
+ :customer_city,
27
+ :customer_state,
28
+ :customer_zipcode,
29
+ :customer_urbanization,
30
+ :company_name,
31
+ :attention,
32
+ :label_format,
33
+ :label_definition,
34
+ :service_type_code,
35
+ :merchandise_description,
36
+ :insurance_amount,
37
+ :address_override_notification,
38
+ :packaging_information,
39
+ :packaging_information2,
40
+ :call_center_or_self_service,
41
+ :image_type,
42
+ :address_validation,
43
+ :sender_name,
44
+ :sender_email,
45
+ :recipient_name,
46
+ :recipient_email,
47
+ :recipient_bcc,
48
+ :merchant_account_id,
49
+ :mid
50
+
51
+ def initialize(options = {})
52
+ options.each do |pair|
53
+ self.public_send("#{pair[0]}=".to_sym, pair[1]) if self.respond_to?("#{pair[0]}=".to_sym)
54
+ end
55
+
56
+ verify_or_raise_required
57
+ end
58
+
59
+ def self.from_hash(options={})
60
+ self.new(options)
61
+ end
62
+
63
+ # Sent by the system containing the returns label attachment and message.
64
+ def recipient_bcc=(v)
65
+ @recipient_bcc = validate_email(v, __method__)
66
+ end
67
+
68
+ # Sent by the system containing the returns label attachment and message.
69
+ # <em>Optional</em>.
70
+ def recipient_email=(v)
71
+ @recipient_email = validate_email(v, __method__)
72
+ end
73
+
74
+ # The name in an email sent by the system containing the returns label attachment.
75
+ # <em>Optional</em>.
76
+ def recipient_name=(v)
77
+ @recipient_name = nil
78
+ if (v = sanitize(v)) && v.length > 0
79
+ @recipient_name = v
80
+ else
81
+ raise USPSValidationError, "'#{v}' is not a valid string in #{__method__}"
82
+ end
83
+ end
84
+
85
+ # The From address in an email sent by the system containing the returns
86
+ # label attachment and message, Defaults to DONOTREPLY@USPSReturns.com
87
+ # if a recipient email is entered and a sender email is not.
88
+ # <em>Optional</em>.
89
+ def sender_email=(v)
90
+ @sender_email = validate_email(v, __method__)
91
+ end
92
+
93
+ # The From name in an email sent by the system containing the returns
94
+ # label attachment. Defaults to “Merchant Returns” if a recipient name
95
+ # is entered and a sender name is not.
96
+ # <em>Optional</em>.
97
+ def sender_name=(v)
98
+ @sender_name = nil
99
+ if (v = sanitize(v)) && v.length > 0
100
+ @sender_name = v
101
+ else
102
+ raise USPSValidationError, "'#{v}' is not a valid string in #{__method__}"
103
+ end
104
+ end
105
+
106
+ # Used to override the validation of the customer address.
107
+ # If true, the address will be validated against WebTools.
108
+ # If false, the system will bypass the validation.
109
+ # <em>Optional</em>.
110
+ def address_validation=(v)
111
+ @address_validation = to_bool(v, true)
112
+ end
113
+
114
+ # Used to select the format of the return label.
115
+ # <em>Optional</em>.
116
+ # * PDF <em>Default</em>.
117
+ # * TIF
118
+ def image_type=(v)
119
+ @image_type = validate_set_inclusion(v.to_s.upcase, IMAGE_TYPE, __method__)
120
+ end
121
+
122
+ # Used to determine if the returns label request is coming from a
123
+ # merchant call center agent or an end customer.
124
+ # <b>Required</b>.
125
+ # [CallCenter]
126
+ # [Customer]
127
+ def call_center_or_self_service=(v)
128
+ @call_center_or_self_service = validate_set_inclusion(v, CALL_CENTER_OR_SELF_SERVICE, __method__)
129
+ end
130
+
131
+ # Package information can be one of three types: RMA, Invoice or
132
+ # Order number. This will appear on the second label generated when
133
+ # the LabelFormat “TWO” is selected.
134
+ # <em>Optional</em>.
135
+ def packaging_information2=(v)
136
+ @packaging_information2 = validate_string_length(v, 15, __method__)
137
+ end
138
+
139
+ # Package information can be one of three types: RMA, Invoice or
140
+ # Order number. This will appear on the generated label.
141
+ # <em>Optional</em>.
142
+ def packaging_information=(v)
143
+ @packaging_information = validate_string_length(v, 15, __method__)
144
+ end
145
+
146
+ # Override address if more address information
147
+ # is needed or system cannot find address. If
148
+ # the address_override_notification value is
149
+ # true then any address error being passed from
150
+ # WebTools would be bypassed and a successful
151
+ # response will be sent.
152
+ # <b>Required</b>.
153
+ def address_override_notification=(v)
154
+ @address_validation = to_bool(v)
155
+ end
156
+
157
+ # Insured amount of package.
158
+ def insurance_amount=(v)
159
+ @insurance_amount = nil
160
+ if (1..200).include?(v.to_f)
161
+ @insurance_amount = v
162
+ else
163
+ raise USPSValidationError, "#{__method__} must be a numerical value between 1 and 200, found value '#{v}'."
164
+ end
165
+ end
166
+
167
+ # Description of the merchandise.
168
+ # <em>Optional</em>.
169
+ def merchandise_description=(v)
170
+ @merchandise_description = validate_string_length(v, 255, __method__)
171
+ end
172
+
173
+ # Service type of the label as specified in the merchant profile setup.
174
+ # <b>Required</b>.
175
+ # [044] (Parcel Return Service)
176
+ # [019] (Priority Mail Returns service)
177
+ # [596] (Priority Mail Returns service, Insurance <= $200)
178
+ # [020] (First-Class Package Return service)
179
+ # [597] (First-Class Package Return service, Insurance <= $200)
180
+ # [022] (Ground Return Service)
181
+ # [024] (PRS – Full Network)
182
+ # [017] (PRS – Full Network, Insurance <=$200)
183
+ # [018] (PRS – Full Network, Insurance >$200)
184
+ def service_type_code=(v)
185
+ @service_type_code = validate_set_inclusion(v, SERVICE_TYPE_CODE, __method__)
186
+ end
187
+
188
+ # Size of the label.
189
+ # <b>Required</b>.
190
+ # * 4X6
191
+ # * Zebra-4X6
192
+ # * 4X4
193
+ # * 3X6
194
+ def label_definition=(v)
195
+ @label_definition = validate_set_inclusion(v, LABEL_DEFINITION, __method__)
196
+ end
197
+
198
+ def label_format
199
+ @label_format && LABEL_FORMAT[@label_format]
200
+ end
201
+
202
+ # Format in which the label(s) will be printed.
203
+ # * null (“Instructions”)
204
+ # * NOI (“No Instructions”)
205
+ # * TWO (“Double Label”)
206
+ def label_format=(v)
207
+ @label_format = validate_set_inclusion(v, LABEL_FORMAT.keys, __method__)
208
+ end
209
+
210
+ # The intended recipient of the returned package (e.g. Returns Department).
211
+ # <em>Optional</em>.
212
+ def attention=(v)
213
+ @attention = validate_string_length(v, 38, __method__)
214
+ end
215
+
216
+ # The name of the company to which the package is being returned.
217
+ # <em>Optional</em>.
218
+ def company_name=(v)
219
+ @company_name = validate_string_length(v, 38, __method__)
220
+ end
221
+
222
+ # <b>Required</b>.
223
+ def merchant_account_id=(v)
224
+ @merchant_account_id = nil
225
+ if v.to_i > 0
226
+ @merchant_account_id = v
227
+ else
228
+ raise USPSValidationError, "#{__method__} must be a valid positive integer, found value '#{v}'."
229
+ end
230
+ end
231
+
232
+ # <b>Required</b>.
233
+ def mid=(v)
234
+ @mid = nil
235
+ if v.to_s =~ /^\d{6,9}$/
236
+ @mid = v
237
+ else
238
+ raise USPSValidationError, "#{__method__} must be a valid integer between 6 and 9 digits in length, found value '#{v}'."
239
+ end
240
+ end
241
+
242
+ # Urbanization of customer returning the package (only applicable to Puerto Rico addresses).
243
+ # <em>Optional</em>.
244
+ def customer_urbanization=(v)
245
+ @customer_urbanization = validate_string_length(v, 32, __method__)
246
+ end
247
+
248
+ # Name of customer returning package.
249
+ # <b>Required</b>.
250
+ def customer_name=(v)
251
+ @customer_name = validate_range(v, 1, 32, __method__)
252
+ end
253
+
254
+ # Address of the customer returning the package.
255
+ # <b>Required</b>.
256
+ def customer_address1=(v)
257
+ @customer_address1 = validate_range(v, 1, 32, __method__)
258
+ end
259
+
260
+ # Secondary address unit designator / number of customer
261
+ # returning the package. (such as an apartment or
262
+ # suite number, e.g. APT 202, STE 100)
263
+ def customer_address2=(v)
264
+ @customer_address2 = validate_range(v, 0, 32, __method__)
265
+ end
266
+
267
+ # City of customer returning the package.
268
+ # <b>Required</b>.
269
+ def customer_city=(v)
270
+ @customer_city = validate_range(v, 1, 32, __method__)
271
+ end
272
+
273
+ # State of customer returning the package.
274
+ # <b>Required</b>.
275
+ def customer_state=(v)
276
+ @customer_state = nil
277
+ if (v = sanitize(v)) && v =~ /^[a-zA-Z]{2}$/
278
+ @customer_state = v
279
+ else
280
+ raise USPSValidationError, "#{__method__} must be a String 2 chars in length, found value '#{v}'."
281
+ end
282
+ end
283
+
284
+ # Zipcode of customer returning the package.
285
+ # According to the USPS documentation, Zipcode is optional
286
+ # unless <tt>address_override_notification</tt> is true
287
+ # and <tt>address_validation</tt> is set to false.
288
+ # It's probably just easier to require Zipcodes.
289
+ # <b>Required</b>.
290
+ def customer_zipcode=(v)
291
+ @customer_zipcode = nil
292
+ if (v = sanitize(v))
293
+ v = v[0..4]
294
+ if v =~ /^\d{5}$/
295
+ @customer_zipcode = v
296
+ end
297
+ else
298
+ raise USPSValidationError, "#{__method__} must be a 5 digit number, found value '#{v}'."
299
+ end
300
+ end
301
+
302
+ def verify_or_raise_required
303
+ %w(customer_name customer_address1 customer_city customer_state
304
+ customer_zipcode label_format label_definition service_type_code
305
+ call_center_or_self_service).each do |attr|
306
+ raise USPSMissingRequiredTagError.new(attr.camelize, attr) unless send(attr.to_sym)
307
+ end
308
+ # Safer than using inflection acroynms
309
+ raise USPSMissingRequiredTagError.new("MID", "mid") unless mid
310
+ raise USPSMissingRequiredTagError.new("MerchantAccountID", "merchant_account_id") unless merchant_account_id
311
+ end
312
+
313
+ def to_xml
314
+ xml_builder = Nokogiri::XML::Builder.new do |xml|
315
+ xml.ExternalReturnLabelRequest do
316
+ xml.CustomerName { xml.text(customer_name) }
317
+ xml.CustomerAddress1 { xml.text(customer_address1) }
318
+ xml.CustomerAddress2 { xml.text(customer_address2) } if customer_address2
319
+ xml.CustomerCity { xml.text(customer_city) }
320
+ xml.CustomerState { xml.text(customer_state) }
321
+ xml.CustomerZipCode { xml.text(customer_zipcode) } if customer_zipcode
322
+ xml.CustomerUrbanization { xml.text(customer_urbanization) } if customer_urbanization
323
+
324
+ xml.MerchantAccountID { xml.text(merchant_account_id) }
325
+ xml.MID { xml.text(mid) }
326
+
327
+ xml.SenderName { xml.text(sender_name) } if sender_name
328
+ xml.SenderEmail { xml.text(sender_email) } if sender_email
329
+
330
+ xml.RecipientName { xml.text(recipient_name) } if recipient_name
331
+ xml.RecipientEmail { xml.text(recipient_email) } if recipient_email
332
+ xml.RecipientBcc { xml.text(recipient_bcc) } if recipient_bcc
333
+
334
+ xml.LabelFormat { xml.text(label_format) } if label_format
335
+ xml.LabelDefinition { xml.text(label_definition) } if label_definition
336
+ xml.ServiceTypeCode { xml.text(service_type_code) } if service_type_code
337
+
338
+ xml.CompanyName { xml.text(company_name) } if company_name
339
+ xml.Attention { xml.text(attention) } if attention
340
+
341
+ xml.CallCenterOrSelfService { xml.text(call_center_or_self_service) }
342
+
343
+ xml.MerchandiseDescription { xml.text(merchandise_description) } if merchandise_description
344
+ xml.InsuranceAmount { xml.text(insurance_amount) } if insurance_amount
345
+
346
+ xml.AddressOverrideNotification { xml.text(!!address_override_notification) }
347
+
348
+ xml.PackageInformation { xml.text(packaging_information) } if packaging_information
349
+ xml.PackageInformation2 { xml.text(packaging_information2) } if packaging_information2
350
+
351
+ xml.ImageType { xml.text(image_type) } if image_type
352
+ xml.AddressValidation { xml.text(!!address_validation) }
353
+
354
+ end
355
+ end
356
+ xml_builder.to_xml
357
+ end
358
+
359
+ private
360
+
361
+ def to_bool(v, default = false)
362
+ v = v.to_s
363
+ if v =~ (/(true|yes|1)$/i)
364
+ true
365
+ elsif v =~ (/(false|no|0)$/i)
366
+ false
367
+ else
368
+ default
369
+ end
370
+ end
371
+
372
+ def sanitize(v)
373
+ if v.is_a?(String)
374
+ v.strip!
375
+ v[0..CAP_STRING_LEN - 1]
376
+ else
377
+ nil
378
+ end
379
+ end
380
+
381
+ def validate_range(v, min, max, meth)
382
+ if (v = sanitize(v).to_s) && ((min.to_i)..(max.to_i)).include?(v.length)
383
+ if v.length == 0
384
+ nil
385
+ else
386
+ v
387
+ end
388
+ else
389
+ raise USPSValidationError, "#{meth} must be a String between #{min.to_i} and #{max.to_i} chars in length, found value '#{v}'."
390
+ end
391
+ end
392
+
393
+ def validate_string_length(s, max_len, meth)
394
+ if (s = sanitize(s)) && s.length <= max_len.to_i
395
+ s
396
+ else
397
+ raise USPSValidationError, "#{meth} must be a String no more than #{max_len} chars in length, found value '#{s}'."
398
+ end
399
+ end
400
+
401
+ def validate_set_inclusion(v, set, meth)
402
+ if set.respond_to?(:include?) && set.include?(v)
403
+ v
404
+ else
405
+ raise USPSValidationError, "#{v} is not valid in #{meth}, try any of the following: #{(set.respond_to?(:join) && set.join(',')) || ''}"
406
+ end
407
+ end
408
+
409
+ def validate_email(v, meth)
410
+ if (v = sanitize(v)) && v =~ USPS_EMAIL_REGEX
411
+ v
412
+ else
413
+ raise USPSValidationError, "'#{v}' is not a valid e-mail in #{meth}"
414
+ end
415
+ end
416
+ end
417
+ end