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,248 @@
1
+ module ReactiveShipping
2
+ class AustraliaPost < Carrier
3
+ cattr_reader :name
4
+ @@name = 'Australia Post'
5
+
6
+ HOST = 'digitalapi.auspost.com.au'
7
+
8
+ PARCEL_ENDPOINTS = {
9
+ service: {
10
+ domestic: '/postage/parcel/domestic/service.json',
11
+ international: '/postage/parcel/international/service.json'
12
+ },
13
+ calculate: {
14
+ domestic: '/postage/parcel/domestic/calculate.json',
15
+ international: '/postage/parcel/international/calculate.json'
16
+ }
17
+ }.freeze
18
+
19
+ def requirements
20
+ [:api_key]
21
+ end
22
+
23
+ def find_rates(origin, destination, packages, options = {})
24
+ packages = Array(packages)
25
+
26
+ service_requests = packages.map do |package|
27
+ service_request = ServiceRequest.new(origin, destination, package, options)
28
+
29
+ service_request.parse(commit(service_request.url))
30
+ service_request
31
+ end
32
+
33
+ combined_response = CombinedResponse.new(origin, destination, packages, service_requests)
34
+
35
+ RateResponse.new(true, 'success', combined_response.params, combined_response.options)
36
+ end
37
+
38
+ def calculate_rates(origin, destination, packages, service_code, options = {})
39
+ packages = Array(packages)
40
+
41
+ calculate_requests = packages.map do |package|
42
+ calculate_request = CalculateRequest.new(origin, destination, package, service_code, options)
43
+
44
+ calculate_request.parse(commit(calculate_request.url))
45
+ calculate_request
46
+ end
47
+
48
+ combined_response = CombinedResponse.new(origin, destination, packages, calculate_requests)
49
+
50
+ RateResponse.new(true, 'success', combined_response.params, combined_response.options)
51
+ end
52
+
53
+ private
54
+
55
+ def commit(request_url)
56
+ ssl_get(request_url, headers)
57
+
58
+ rescue ActiveUtils::ResponseError, ReactiveShipping::ResponseError => e
59
+ data = JSON.parse(e.response.body)
60
+ error_message = data['error'] && data['error']['errorMessage'] ? data['error']['errorMessage'] : 'unknown'
61
+
62
+ RateResponse.new(false, error_message, data)
63
+ end
64
+
65
+ def headers
66
+ {
67
+ 'Content-type' => 'application/json',
68
+ 'auth-key' => @options[:api_key]
69
+ }
70
+ end
71
+
72
+ class CombinedResponse
73
+
74
+ def initialize(origin, destination, packages, requests)
75
+ @requests = requests
76
+ @origin = origin
77
+ @destination = destination
78
+ @packages = packages
79
+ end
80
+
81
+ def options
82
+ {
83
+ rates: rates,
84
+ raw_responses: @requests.map(&:raw_response),
85
+ request: @requests.map(&:url)
86
+ }
87
+ end
88
+
89
+ def params
90
+ {
91
+ responses: @requests.map(&:response)
92
+ }
93
+ end
94
+
95
+ private
96
+
97
+ def rate_options(rates)
98
+ {
99
+ service_name: rates.first[:service_name],
100
+ service_code: rates.first[:service_code],
101
+ total_price: rates.sum { |rate| rate[:total_price] },
102
+ currency: 'AUD',
103
+ delivery_time_text: rates.first[:delivery_time_text]
104
+ }
105
+ end
106
+
107
+ def rates
108
+ rates = @requests.map(&:rates).flatten
109
+
110
+ rates.group_by { |rate| rate[:service_name] }.map do |service_name, service_rates|
111
+ next unless service_rates.size == @packages.size
112
+
113
+ AustraliaPostRateEstimate.new(@origin, @destination, AustraliaPost.name, service_name, rate_options(service_rates))
114
+ end.compact
115
+ end
116
+
117
+ end
118
+
119
+ class AustraliaPostRequest
120
+ attr_reader :raw_response
121
+ attr_reader :response
122
+ attr_reader :rates
123
+
124
+ def initialize(origin, destination, package, options)
125
+ @origin = Location.from(origin)
126
+ @destination = Location.from(destination)
127
+ @package = package
128
+ @rates = []
129
+ @options = options
130
+ end
131
+
132
+ def url
133
+ endpoint = domestic_destination? ? @endpoints[:domestic] : @endpoints[:international]
134
+ params = domestic_destination? ? domestic_params : international_params
135
+
136
+ URI::HTTPS.build(host: HOST, path: endpoint, query: params.to_query).to_s
137
+ end
138
+
139
+ def parse(data)
140
+ @raw_response = data
141
+ @response = JSON.parse(data)
142
+ end
143
+
144
+ protected
145
+
146
+ def domestic_destination?
147
+ @destination.country_code == 'AU'
148
+ end
149
+
150
+ def domestic_params
151
+ {
152
+ length: @package.cm(:length),
153
+ width: @package.cm(:width),
154
+ height: @package.cm(:height),
155
+ weight: @package.kilograms.to_f.round(2),
156
+ from_postcode: @origin.postal_code,
157
+ to_postcode: @destination.postal_code
158
+ }
159
+ end
160
+
161
+ def international_params
162
+ {
163
+ weight: @package.kilograms.to_f.round(2),
164
+ country_code: @destination.country_code
165
+ }
166
+ end
167
+
168
+ end
169
+
170
+ class ServiceRequest < AustraliaPostRequest
171
+
172
+ def initialize(origin, destination, package, options)
173
+ super
174
+ @endpoints = PARCEL_ENDPOINTS[:service]
175
+ end
176
+
177
+ def parse(data)
178
+ super
179
+
180
+ @rates = response['services']['service'].map do |service|
181
+ {
182
+ service_name: service['name'],
183
+ service_code: service['code'],
184
+ total_price: service['price'].to_f,
185
+ currency: 'AUD'
186
+ }
187
+ end
188
+ end
189
+
190
+ end
191
+
192
+ class CalculateRequest < AustraliaPostRequest
193
+ attr_reader :service_code
194
+
195
+ def initialize(origin, destination, package, service_code, options)
196
+ super(origin, destination, package, options)
197
+
198
+ @service_code = service_code
199
+ @endpoints = PARCEL_ENDPOINTS[:calculate]
200
+ end
201
+
202
+ def parse(data)
203
+ super
204
+ postage_result = response['postage_result']
205
+
206
+ @rates = [{
207
+ service_name: postage_result['service'],
208
+ service_code: service_code,
209
+ total_price: postage_result['total_cost'].to_f,
210
+ currency: 'AUD',
211
+ delivery_time_text: postage_result['delivery_time']
212
+ }]
213
+ end
214
+
215
+ private
216
+
217
+ def calculate_params
218
+ {
219
+ service_code: @service_code,
220
+ option_code: @options[:option_code],
221
+ suboption_code: @options[:suboption_code],
222
+ extra_cover: @options[:extra_cover]
223
+ }.
224
+ # INFO: equivalent of .compact
225
+ select { |_, value| !value.nil? }
226
+ end
227
+
228
+ def domestic_params
229
+ super.merge(calculate_params)
230
+ end
231
+
232
+ def international_params
233
+ super.merge(calculate_params)
234
+ end
235
+
236
+ end
237
+
238
+ class AustraliaPostRateEstimate < RateEstimate
239
+ attr_reader :delivery_time_text
240
+
241
+ def initialize(origin, destination, carrier, service_name, options = {})
242
+ super
243
+ @delivery_time_text = options[:delivery_time_text]
244
+ end
245
+
246
+ end
247
+ end
248
+ end
@@ -0,0 +1,31 @@
1
+ # The time a 3rd-party shipping provider takes to respond to a request varies greatly.
2
+ # This class simulates these unpredictable delays in shipping rate retrieval so that
3
+ # load-testing tools run into situations that more accurately reflect the real world.
4
+
5
+ module ReactiveShipping
6
+ class BenchmarkCarrier < Carrier
7
+ cattr_reader :name
8
+ @@name = "Benchmark Carrier"
9
+
10
+ def find_rates(origin, destination, packages, options = {})
11
+ origin = Location.from(origin)
12
+ destination = Location.from(destination)
13
+ packages = Array(packages)
14
+
15
+ delay_time = generate_simulated_lag
16
+
17
+ bogus_estimate = RateEstimate.new(
18
+ origin, destination, @@name,
19
+ "Free Benchmark Shipping", :total_price => 0, :currency => 'USD',
20
+ :packages => packages, :delivery_range => [Time.now.utc.strftime("%Y-%d-%m"), Time.now.utc.strftime("%Y-%d-%m")]
21
+ )
22
+ RateResponse.new(true, "Success (delayed #{delay_time} seconds)", {:rate => 'free'}, :rates => [bogus_estimate], :xml => "<rate>free</rate>")
23
+ end
24
+
25
+ private
26
+
27
+ def generate_simulated_lag(max_delay = 30)
28
+ sleep Random.rand * max_delay
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,12 @@
1
+ module ReactiveShipping
2
+ class BogusCarrier < Carrier
3
+ cattr_reader :name
4
+ @@name = "Bogus Carrier"
5
+
6
+ def find_rates(origin, destination, packages, options = {})
7
+ origin = Location.from(origin)
8
+ destination = Location.from(destination)
9
+ packages = Array(packages)
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,263 @@
1
+ module ReactiveShipping
2
+ class CanadaPost < Carrier
3
+ # NOTE!
4
+ # A Merchant CPC Id must be assigned to you by Canada Post
5
+ # CPC_DEMO_XML is just a public domain account for testing
6
+
7
+ class CanadaPostRateResponse < RateResponse
8
+ attr_reader :boxes, :postal_outlets
9
+
10
+ def initialize(success, message, params = {}, options = {})
11
+ @boxes = options[:boxes]
12
+ @postal_outlets = options[:postal_outlets]
13
+ super
14
+ end
15
+ end
16
+
17
+ cattr_reader :name, :name_french
18
+ @@name = "Canada Post"
19
+ @@name_french = "Postes Canada"
20
+
21
+ Box = Struct.new(:name, :weight, :expediter_weight, :length, :width, :height, :packedItems)
22
+ PackedItem = Struct.new(:quantity, :description)
23
+ PostalOutlet = Struct.new(:sequence_no, :distance, :name, :business_name, :postal_address, :business_hours)
24
+
25
+ URL = "http://sellonline.canadapost.ca:30000"
26
+ DTD_NAME = 'eparcel'
27
+ DTD_URI = "http://sellonline.canadapost.ca/DevelopersResources/protocolV3/eParcel.dtd"
28
+
29
+ RESPONSE_CODES = {
30
+ '1' => "All calculation was done",
31
+ '2' => "Default shipping rates are returned due to a problem during the processing of the request.",
32
+ '-2' => "Missing argument when calling module",
33
+ '-5' => "No Item to ship",
34
+ '-6' => "Illegal Item weight",
35
+ '-7' => "Illegal item dimension",
36
+ '-12' => "Can't open IM config file",
37
+ '-13' => "Can't create log files",
38
+ '-15' => "Invalid config file format",
39
+ '-102' => "Invalid socket connection",
40
+ '-106' => "Can't connect to server",
41
+ '-1000' => "Unknow request type sent by client",
42
+ '-1002' => "MAS Timed out",
43
+ '-1004' => "Socket communication break",
44
+ '-1005' => "Did not receive required data on socket.",
45
+ '-2000' => "Unable to estabish socket connection with RSSS",
46
+ '-2001' => "Merchant Id not found on server",
47
+ '-2002' => "One or more parameter was not sent by the IM to the MAS",
48
+ '-2003' => "Did not receive required data on socket.",
49
+ '-2004' => "The request contains to many items to process it.",
50
+ '-2005' => "The request received on socket is larger than the maximum allowed.",
51
+ '-3000' => "Origin Postal Code is illegal",
52
+ '-3001' => "Destination Postal Code/State Name/ Country is illegal",
53
+ '-3002' => "Parcel too large to be shipped with CPC",
54
+ '-3003' => "Parcel too small to be shipped with CPC",
55
+ '-3004' => "Parcel too heavy to be shipped with CPC",
56
+ '-3005' => "Internal error code returned by the rating DLL",
57
+ '-3006' => "The pick up time format is invalid or not defined.",
58
+ '-4000' => "Volumetric internal error",
59
+ '-4001' => "Volumetric time out calculation error.",
60
+ '-4002' => "No bins provided to the volumetric engine.",
61
+ '-4003' => "No items provided to the volumetric engine.",
62
+ '-4004' => "Item is too large to be packed",
63
+ '-4005' => "Number of item more than maximum allowed",
64
+ '-5000' => "XML Parsing error",
65
+ '-5001' => "XML Tag not found",
66
+ '-5002' => "Node Value Number format error",
67
+ '-5003' => "Node value is empty",
68
+ '-5004' => "Unable to create/parse XML Document",
69
+ '-6000' => "Unable to open the database",
70
+ '-6001' => "Unable to read from the database",
71
+ '-6002' => "Unable to write to the database",
72
+ '-50000' => "Internal problem - Please contact Sell Online Help Desk"
73
+ }
74
+
75
+ NON_ISO_COUNTRY_NAMES = {
76
+ 'Russian Federation' => 'Russia'
77
+ }
78
+
79
+ def requirements
80
+ [:login]
81
+ end
82
+
83
+ def find_rates(origin, destination, line_items = [], options = {})
84
+ rate_request = build_rate_request(origin, destination, line_items, options)
85
+ commit(rate_request, origin, destination, options)
86
+ end
87
+
88
+ def maximum_weight
89
+ Measured::Weight.new(30, :kg)
90
+ end
91
+
92
+ def maximum_address_field_length
93
+ # https://www.canadapost.ca/cpo/mc/business/productsservices/developers/services/shippingmanifest/createshipment.jsf
94
+ 44
95
+ end
96
+
97
+ def self.default_location
98
+ {
99
+ :country => 'CA',
100
+ :province => 'ON',
101
+ :city => 'Ottawa',
102
+ :address1 => '61A York St',
103
+ :postal_code => 'K1N5T2'
104
+ }
105
+ end
106
+
107
+ protected
108
+
109
+ def commit(request, origin, destination, options = {})
110
+ parse_rate_response(ssl_post(URL, request), origin, destination, options)
111
+ end
112
+
113
+ private
114
+
115
+ def generate_xml(&block)
116
+ builder = Nokogiri::XML::Builder.new do |xml|
117
+ xml.doc.create_internal_subset(DTD_NAME, nil, DTD_URI)
118
+ yield(xml)
119
+ end
120
+ builder.to_xml
121
+ end
122
+
123
+ def build_rate_request(origin, destination, line_items = [], options = {})
124
+ line_items = [line_items] unless line_items.is_a?(Array)
125
+ origin = origin.is_a?(Location) ? origin : Location.new(origin)
126
+ destination = destination.is_a?(Location) ? destination : Location.new(destination)
127
+
128
+ generate_xml do |xml|
129
+ xml.eparcel do
130
+ xml.language(@options[:french] ? 'fr' : 'en')
131
+ xml.ratesAndServicesRequest do
132
+ xml.merchantCPCID(@options[:login])
133
+ xml.fromPostalCode(origin.postal_code)
134
+ xml.turnAroundTime(options[:turn_around_time]) if options[:turn_around_time]
135
+ xml.itemsPrice(dollar_amount(line_items.map(&:value).compact.sum))
136
+
137
+ build_line_items(xml, line_items)
138
+
139
+ xml.city(destination.city)
140
+ xml.provOrState(destination.province)
141
+ xml.country(handle_non_iso_country_names(destination.country))
142
+ xml.postalCode(destination.postal_code)
143
+ end
144
+ end
145
+ end
146
+ end
147
+
148
+ def parse_rate_response(response, origin, destination, options = {})
149
+ xml = Nokogiri.XML(response)
150
+ success = response_success?(xml)
151
+ message = response_message(xml)
152
+
153
+ rate_estimates = []
154
+ boxes = []
155
+ if success
156
+ xml.xpath('eparcel/ratesAndServicesResponse/product').each do |product|
157
+ service_name = (@options[:french] ? @@name_french : @@name) + " " + product.at('name').text
158
+ service_code = product['id']
159
+
160
+ rate_estimates << RateEstimate.new(origin, destination, @@name, service_name,
161
+ :service_code => service_code,
162
+ :total_price => product.at('rate').text,
163
+ :currency => 'CAD',
164
+ :shipping_date => product.at('shippingDate').text,
165
+ :delivery_range => [product.at('deliveryDate').text] * 2
166
+ )
167
+ end
168
+
169
+ boxes = xml.xpath('eparcel/ratesAndServicesResponse/packing/box').map do |box|
170
+ b = Box.new
171
+ b.packedItems = []
172
+ b.name = box.at('name').text
173
+ b.weight = box.at('weight').text.to_f
174
+ b.expediter_weight = box.at('expediterWeight').text.to_f
175
+ b.length = box.at('length').text.to_f
176
+ b.width = box.at('width').text.to_f
177
+ b.height = box.at('height').text.to_f
178
+ b.packedItems = box.xpath('packedItem').map do |item|
179
+ p = PackedItem.new
180
+ p.quantity = item.at('quantity').text.to_i
181
+ p.description = item.at('description').text
182
+ p
183
+ end
184
+ b
185
+ end
186
+
187
+ postal_outlets = xml.xpath('eparcel/ratesAndServicesResponse/nearestPostalOutlet').map do |outlet|
188
+ postal_outlet = PostalOutlet.new
189
+ postal_outlet.sequence_no = outlet.at('postalOutletSequenceNo').text
190
+ postal_outlet.distance = outlet.at('distance').text
191
+ postal_outlet.name = outlet.at('outletName').text
192
+ postal_outlet.business_name = outlet.at('businessName').text
193
+
194
+ postal_outlet.postal_address = Location.new(
195
+ :address1 => outlet.at('postalAddress/addressLine').text,
196
+ :postal_code => outlet.at('postalAddress/postal_code').text,
197
+ :city => outlet.at('postalAddress/municipality').text,
198
+ :province => outlet.at('postalAddress/province').text,
199
+ :country => 'Canada',
200
+ :phone_number => outlet.at('phoneNumber').text
201
+ )
202
+
203
+ postal_outlet.business_hours = outlet.elements.collect('businessHours') do |hour|
204
+ { :day_of_week => hour.at('dayOfWeek').text, :time => hour.at('time').text }
205
+ end
206
+
207
+ postal_outlet
208
+ end
209
+ end
210
+
211
+ CanadaPostRateResponse.new(success, message, Hash.from_xml(response), :rates => rate_estimates, :xml => response, :boxes => boxes, :postal_outlets => postal_outlets)
212
+ end
213
+
214
+ def response_success?(xml)
215
+ return false unless xml.at('eparcel/error').nil?
216
+
217
+ value = xml.at('eparcel/ratesAndServicesResponse/statusCode').text
218
+ value == '1' || value == '2'
219
+ end
220
+
221
+ def response_message(xml)
222
+ if response_success?(xml)
223
+ xml.at('eparcel/ratesAndServicesResponse/statusMessage').text
224
+ else
225
+ xml.at('eparcel/error/statusMessage').text
226
+ end
227
+ end
228
+
229
+ # <!-- List of items in the shopping -->
230
+ # <!-- cart -->
231
+ # <!-- Each item is defined by : -->
232
+ # <!-- - quantity (mandatory) -->
233
+ # <!-- - size (mandatory) -->
234
+ # <!-- - weight (mandatory) -->
235
+ # <!-- - description (mandatory) -->
236
+ # <!-- - ready to ship (optional) -->
237
+
238
+ def build_line_items(xml, line_items)
239
+ xml.lineItems do
240
+ line_items.each do |line_item|
241
+ xml.item do
242
+ xml.quantity(1)
243
+ xml.weight(line_item.kilograms)
244
+ xml.length(line_item.cm(:length).to_s)
245
+ xml.width(line_item.cm(:width).to_s)
246
+ xml.height(line_item.cm(:height).to_s)
247
+ xml.description(line_item.options[:description] || ' ')
248
+ xml.readyToShip(line_item.options[:ready_to_ship] || nil)
249
+ # By setting the 'readyToShip' tag to true, Sell Online will not pack this item in the boxes defined in the merchant profile.
250
+ end
251
+ end
252
+ end
253
+ end
254
+
255
+ def dollar_amount(cents)
256
+ "%0.2f" % (cents / 100.0)
257
+ end
258
+
259
+ def handle_non_iso_country_names(country)
260
+ NON_ISO_COUNTRY_NAMES[country.to_s] || country
261
+ end
262
+ end
263
+ end