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