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,908 @@
1
+ module ReactiveShipping
2
+ class CanadaPostPWS < Carrier
3
+
4
+ cattr_reader :name
5
+ @@name = "Canada Post PWS"
6
+
7
+ SHIPPING_SERVICES = {
8
+ "DOM.RP" => "Regular Parcel",
9
+ "DOM.EP" => "Expedited Parcel",
10
+ "DOM.XP" => "Xpresspost",
11
+ "DOM.XP.CERT" => "Xpresspost Certified",
12
+ "DOM.PC" => "Priority",
13
+ "DOM.LIB" => "Library Books",
14
+
15
+ "USA.EP" => "Expedited Parcel USA",
16
+ "USA.PW.ENV" => "Priority Worldwide Envelope USA",
17
+ "USA.PW.PAK" => "Priority Worldwide pak USA",
18
+ "USA.PW.PARCEL" => "Priority Worldwide Parcel USA",
19
+ "USA.SP.AIR" => "Small Packet USA Air",
20
+ "USA.SP.SURF" => "Small Packet USA Surface",
21
+ "USA.XP" => "Xpresspost USA",
22
+
23
+ "INT.XP" => "Xpresspost International",
24
+ "INT.IP.AIR" => "International Parcel Air",
25
+ "INT.IP.SURF" => "International Parcel Surface",
26
+ "INT.PW.ENV" => "Priority Worldwide Envelope Int'l",
27
+ "INT.PW.PAK" => "Priority Worldwide pak Int'l",
28
+ "INT.PW.PARCEL" => "Priority Worldwide parcel Int'l",
29
+ "INT.SP.AIR" => "Small Packet International Air",
30
+ "INT.SP.SURF" => "Small Packet International Surface"
31
+ }
32
+
33
+ ENDPOINT = "https://soa-gw.canadapost.ca/" # production
34
+
35
+ SHIPMENT_MIMETYPE = "application/vnd.cpc.ncshipment+xml"
36
+ RATE_MIMETYPE = "application/vnd.cpc.ship.rate+xml"
37
+ TRACK_MIMETYPE = "application/vnd.cpc.track+xml"
38
+ REGISTER_MIMETYPE = "application/vnd.cpc.registration+xml"
39
+
40
+ LANGUAGE = {
41
+ 'en' => 'en-CA',
42
+ 'fr' => 'fr-CA'
43
+ }
44
+
45
+ SHIPPING_OPTIONS = [:d2po, :d2po_office_id, :cov, :cov_amount, :cod, :cod_amount, :cod_includes_shipping,
46
+ :cod_method_of_payment, :so, :dc, :dns, :pa18, :pa19, :hfp, :lad,
47
+ :rase, :rts, :aban]
48
+
49
+ RATES_OPTIONS = [:cov, :cov_amount, :cod, :so, :dc, :dns, :pa18, :pa19, :hfp, :lad]
50
+
51
+ MAX_WEIGHT = 30 # kg
52
+
53
+ attr_accessor :language, :endpoint, :logger, :platform_id, :customer_number
54
+
55
+ def initialize(options = {})
56
+ @language = LANGUAGE[options[:language]] || LANGUAGE['en']
57
+ @endpoint = options[:endpoint] || ENDPOINT
58
+ @platform_id = options[:platform_id]
59
+ @customer_number = options[:customer_number]
60
+ super(options)
61
+ end
62
+
63
+ def requirements
64
+ [:api_key, :secret]
65
+ end
66
+
67
+ def self.default_location
68
+ {
69
+ :country => 'CA',
70
+ :province => 'ON',
71
+ :city => 'Ottawa',
72
+ :address1 => '61A York St',
73
+ :postal_code => 'K1N5T2'
74
+ }
75
+ end
76
+
77
+ def find_rates(origin, destination, line_items = [], options = {}, package = nil, services = [])
78
+ url = endpoint + "rs/ship/price"
79
+ request = build_rates_request(origin, destination, line_items, options, package, services)
80
+ response = ssl_post(url, request, headers(options, RATE_MIMETYPE, RATE_MIMETYPE))
81
+ parse_rates_response(response, origin, destination, !!options[:exclude_tax])
82
+ rescue ActiveUtils::ResponseError, ReactiveShipping::ResponseError => e
83
+ error_response(e.response.body, CPPWSRateResponse)
84
+ end
85
+
86
+ def find_tracking_info(pin, options = {})
87
+ response = ssl_get(tracking_url(pin), headers(options, TRACK_MIMETYPE))
88
+ parse_tracking_response(response)
89
+ rescue ActiveUtils::ResponseError, ReactiveShipping::ResponseError => e
90
+ if e.response
91
+ error_response(e.response.body, CPPWSTrackingResponse)
92
+ else
93
+ CPPWSTrackingResponse.new(false, e.message, {}, :carrier => @@name)
94
+ end
95
+ rescue InvalidPinFormatError
96
+ CPPWSTrackingResponse.new(false, "Invalid Pin Format", {}, :carrier => @@name)
97
+ end
98
+
99
+ # line_items should be a list of PackageItem's
100
+ def create_shipment(origin, destination, package, line_items = [], options = {})
101
+ request_body = build_shipment_request(origin, destination, package, line_items, options)
102
+ response = ssl_post(create_shipment_url(options), request_body, headers(options, SHIPMENT_MIMETYPE, SHIPMENT_MIMETYPE))
103
+ parse_shipment_response(response)
104
+ rescue ActiveUtils::ResponseError, ReactiveShipping::ResponseError => e
105
+ error_response(e.response.body, CPPWSShippingResponse)
106
+ rescue MissingCustomerNumberError
107
+ CPPWSShippingResponse.new(false, "Missing Customer Number", {}, :carrier => @@name)
108
+ end
109
+
110
+ def retrieve_shipment(shipping_id, options = {})
111
+ response = ssl_post(shipment_url(shipping_id, options), nil, headers(options, SHIPMENT_MIMETYPE, SHIPMENT_MIMETYPE))
112
+ parse_shipment_response(response)
113
+ end
114
+
115
+ def find_shipment_receipt(shipping_id, options = {})
116
+ response = ssl_get(shipment_receipt_url(shipping_id, options), headers(options, SHIPMENT_MIMETYPE, SHIPMENT_MIMETYPE))
117
+ parse_shipment_receipt_response(response)
118
+ end
119
+
120
+ def retrieve_shipping_label(shipping_response, options = {})
121
+ raise MissingShippingNumberError unless shipping_response && shipping_response.shipping_id
122
+ ssl_get(shipping_response.label_url, headers(options, "application/pdf"))
123
+ end
124
+
125
+ def register_merchant(options = {})
126
+ url = endpoint + "ot/token"
127
+ response = ssl_post(url, nil, headers({}, REGISTER_MIMETYPE, REGISTER_MIMETYPE).merge("Content-Length" => "0"))
128
+ parse_register_token_response(response)
129
+ rescue ActiveUtils::ResponseError, ReactiveShipping::ResponseError => e
130
+ error_response(e.response.body, CPPWSRegisterResponse)
131
+ end
132
+
133
+ def retrieve_merchant_details(options = {})
134
+ raise MissingTokenIdError unless token_id = options[:token_id]
135
+ url = endpoint + "ot/token/#{token_id}"
136
+ response = ssl_get(url, headers({}, REGISTER_MIMETYPE, REGISTER_MIMETYPE))
137
+ parse_merchant_details_response(response)
138
+ rescue ActiveUtils::ResponseError, ReactiveShipping::ResponseError => e
139
+ error_response(e.response.body, CPPWSMerchantDetailsResponse)
140
+ rescue Exception => e
141
+ raise ResponseError.new(e.message)
142
+ end
143
+
144
+ def find_services(country = nil, options = {})
145
+ response = ssl_get(services_url(country), headers(options, RATE_MIMETYPE))
146
+ parse_services_response(response)
147
+ rescue ActiveUtils::ResponseError, ReactiveShipping::ResponseError => e
148
+ error_response(e.response.body, CPPWSRateResponse)
149
+ end
150
+
151
+ def find_service_options(service_code, country, options = {})
152
+ response = ssl_get(services_url(country, service_code), headers(options, RATE_MIMETYPE))
153
+ parse_service_options_response(response)
154
+ rescue ActiveUtils::ResponseError, ReactiveShipping::ResponseError => e
155
+ error_response(e.response.body, CPPWSRateResponse)
156
+ end
157
+
158
+ def find_option_details(option_code, options = {})
159
+ url = endpoint + "rs/ship/option/#{option_code}"
160
+ response = ssl_get(url, headers(options, RATE_MIMETYPE))
161
+ parse_option_response(response)
162
+ rescue ActiveUtils::ResponseError, ReactiveShipping::ResponseError => e
163
+ error_response(e.response.body, CPPWSRateResponse)
164
+ end
165
+
166
+ def maximum_weight
167
+ Measured::Weight.new(MAX_WEIGHT, :kg)
168
+ end
169
+
170
+ def maximum_address_field_length
171
+ # https://www.canadapost.ca/cpo/mc/business/productsservices/developers/services/shippingmanifest/createshipment.jsf
172
+ 44
173
+ end
174
+
175
+ # service discovery
176
+
177
+ def parse_services_response(response)
178
+ doc = Nokogiri.XML(response)
179
+ doc.remove_namespaces!
180
+ service_nodes = doc.xpath('services/service')
181
+ service_nodes.inject({}) do |result, node|
182
+ service_code = node.at("service-code").text
183
+ service_name = node.at("service-name").text
184
+ service_link = node.at("link").attributes['href'].value
185
+ service_link_media_type = node.at("link").attributes['media-type'].value
186
+ result[service_code] = {
187
+ :name => service_name,
188
+ :link => service_link,
189
+ :link_media_type => service_link_media_type
190
+ }
191
+ result
192
+ end
193
+ end
194
+
195
+ def parse_service_options_response(response)
196
+ doc = Nokogiri.XML(response)
197
+ doc.remove_namespaces!
198
+
199
+ service_code = doc.root.at("service-code").text
200
+ service_name = doc.root.at("service-name").text
201
+
202
+ option_nodes = doc.root.xpath('options/option')
203
+ options = option_nodes.map do |node|
204
+ option = {
205
+ :code => node.at("option-code").text,
206
+ :name => node.at("option-name").text,
207
+ :required => node.at("mandatory").text != "false",
208
+ :qualifier_required => node.at("qualifier-required").text != "false",
209
+ }
210
+ option[:qualifier_max] = node.at("qualifier-max").text.to_i if node.at("qualifier-max")
211
+ option
212
+ end
213
+
214
+ restrictions_node = doc.root.at('restrictions')
215
+ dimensions_node = restrictions_node.at('dimensional-restrictions')
216
+ restrictions = {
217
+ :min_weight => restrictions_node.at("weight-restriction").attributes['min'].value.to_i,
218
+ :max_weight => restrictions_node.at("weight-restriction").attributes['max'].value.to_i,
219
+ :min_length => dimensions_node.at("length").attributes['min'].value.to_f,
220
+ :max_length => dimensions_node.at("length").attributes['max'].value.to_f,
221
+ :min_height => dimensions_node.at("height").attributes['min'].value.to_f,
222
+ :max_height => dimensions_node.at("height").attributes['max'].value.to_f,
223
+ :min_width => dimensions_node.at("width").attributes['min'].value.to_f,
224
+ :max_width => dimensions_node.at("width").attributes['max'].value.to_f
225
+ }
226
+
227
+ {
228
+ :service_code => service_code,
229
+ :service_name => service_name,
230
+ :options => options,
231
+ :restrictions => restrictions
232
+ }
233
+ end
234
+
235
+ def parse_option_response(response)
236
+ doc = Nokogiri.XML(response)
237
+ doc.remove_namespaces!
238
+
239
+ conflicts = doc.root.xpath('conflicting-options/option-code').map(&:text)
240
+ prereqs = doc.root.xpath('prerequisite-options/option-code').map(&:text)
241
+ option = {
242
+ :code => doc.root.at('option-code').text,
243
+ :name => doc.root.at('option-name').text,
244
+ :class => doc.root.at('option-class').text,
245
+ :prints_on_label => doc.root.at('prints-on-label').text != "false",
246
+ :qualifier_required => doc.root.at('qualifier-required').text != "false",
247
+ }
248
+ option[:conflicting_options] = conflicts if conflicts
249
+ option[:prerequisite_options] = prereqs if prereqs
250
+
251
+ option[:qualifier_max] = doc.root.at("qualifier-max").text.to_i if doc.root.at("qualifier-max")
252
+ option
253
+ end
254
+
255
+ # rating
256
+
257
+ def build_rates_request(origin, destination, line_items = [], options = {}, package = nil, services = [])
258
+ line_items = Array(line_items)
259
+
260
+ builder = Nokogiri::XML::Builder.new do |xml|
261
+ xml.public_send('mailing-scenario', :xmlns => "http://www.canadapost.ca/ws/ship/rate") do
262
+ customer_number_node(xml, options)
263
+ contract_id_node(xml, options)
264
+ quote_type_node(xml, options)
265
+ expected_mailing_date_node(xml, shipping_date(options)) if options[:shipping_date]
266
+ shipping_options_node(xml, RATES_OPTIONS, options)
267
+ parcel_node(xml, line_items, package)
268
+ origin_node(xml, origin)
269
+ destination_node(xml, destination)
270
+ services_node(xml, services) unless services.blank?
271
+ end
272
+ end
273
+ builder.to_xml
274
+ end
275
+
276
+ def parse_rates_response(response, origin, destination, exclude_tax)
277
+ doc = Nokogiri.XML(response)
278
+ doc.remove_namespaces!
279
+ raise ReactiveShipping::ResponseError, "No Quotes" unless doc.at('price-quotes')
280
+
281
+ rates = doc.root.xpath('price-quote').map do |node|
282
+ service_name = node.at("service-name").text
283
+ service_code = node.at("service-code").text
284
+ total_price = price_from_node(node, exclude_tax)
285
+ expected_date = expected_date_from_node(node)
286
+ options = {
287
+ :service_code => service_code,
288
+ :total_price => total_price,
289
+ :currency => 'CAD',
290
+ :delivery_range => [expected_date, expected_date]
291
+ }
292
+ RateEstimate.new(origin, destination, @@name, service_name, options)
293
+ end
294
+ CPPWSRateResponse.new(true, "", {}, :rates => rates)
295
+ end
296
+
297
+ def price_from_node(node, exclude_tax)
298
+ price = node.at('price-details/due').text
299
+ return price unless exclude_tax
300
+ children = node.at('price-details/taxes').children
301
+ tax_total_cents = children.sum { |node| node.elem? ? Package.cents_from(node.text) : 0 }
302
+ Package.cents_from(price) - tax_total_cents
303
+ end
304
+
305
+ # tracking
306
+
307
+ def parse_tracking_response(response)
308
+ doc = Nokogiri.XML(response)
309
+ doc.remove_namespaces!
310
+ raise ReactiveShipping::ResponseError, "No Tracking" unless doc.at('tracking-detail')
311
+
312
+ events = doc.root.xpath('significant-events/occurrence')
313
+
314
+ shipment_events = build_tracking_events(events)
315
+ change_date = doc.root.at('changed-expected-date').text
316
+ expected_date = doc.root.at('expected-delivery-date').text
317
+ dest_postal_code = doc.root.at('destination-postal-id').text
318
+ destination = Location.new(:postal_code => dest_postal_code)
319
+ origin = Location.new(origin_hash_for(doc.root))
320
+ options = {
321
+ :carrier => @@name,
322
+ :service_name => doc.root.at('service-name').text,
323
+ :expected_date => expected_date.blank? ? nil : Date.parse(expected_date),
324
+ :changed_date => change_date.blank? ? nil : Date.parse(change_date),
325
+ :change_reason => doc.root.at('changed-expected-delivery-reason').text.strip,
326
+ :destination_postal_code => doc.root.at('destination-postal-id').text,
327
+ :shipment_events => shipment_events,
328
+ :tracking_number => doc.root.at('pin').text,
329
+ :origin => origin,
330
+ :destination => destination,
331
+ :customer_number => doc.root.at('mailed-by-customer-number').text
332
+ }
333
+
334
+ CPPWSTrackingResponse.new(true, "", {}, options)
335
+ end
336
+
337
+ def build_tracking_events(events)
338
+ events.map do |event|
339
+ date = event.at('event-date').text
340
+ time = event.at('event-time').text
341
+ zone = event.at('event-time-zone').text
342
+ timestamp = DateTime.parse("#{date} #{time} #{zone}")
343
+ time = Time.utc(timestamp.utc.year, timestamp.utc.month, timestamp.utc.day, timestamp.utc.hour, timestamp.utc.min, timestamp.utc.sec)
344
+ message = event.at('event-description').text
345
+ location = [event.at('event-retail-name'), event.at('event-site'), event.at('event-province')].
346
+ reject { |e| e.nil? || e.text.empty? }.join(", ")
347
+ name = event.at('event-identifier').text
348
+ ShipmentEvent.new(name, time, location, message)
349
+ end
350
+ end
351
+
352
+ # shipping
353
+
354
+ # options
355
+ # :service => 'DOM.EP'
356
+ # :notification_email
357
+ # :packing_instructions
358
+ # :show_postage_rate
359
+ # :cod, :cod_amount, :insurance, :insurance_amount, :signature_required, :pa18, :pa19, :hfp, :dns, :lad
360
+ #
361
+ def build_shipment_request(origin, destination, package, line_items = [], options = {})
362
+ builder = Nokogiri::XML::Builder.new do |xml|
363
+ xml.public_send('non-contract-shipment', :xmlns => "http://www.canadapost.ca/ws/ncshipment") do
364
+ xml.public_send('delivery-spec') do
365
+ shipment_service_code_node(xml, options)
366
+ shipment_sender_node(xml, origin, options)
367
+ shipment_destination_node(xml, destination, options)
368
+ shipment_options_node(xml, options)
369
+ shipment_parcel_node(xml, package)
370
+ shipment_notification_node(xml, options)
371
+ shipment_preferences_node(xml, options)
372
+ references_node(xml, options) # optional > user defined custom notes
373
+ shipment_customs_node(xml, destination, line_items, options)
374
+ # COD Remittance defaults to sender
375
+ end
376
+ end
377
+ end
378
+ builder.to_xml
379
+ end
380
+
381
+ def shipment_service_code_node(xml, options)
382
+ xml.public_send('service-code', options[:service])
383
+ end
384
+
385
+ def shipment_sender_node(xml, sender, options)
386
+ location = location_from_hash(sender)
387
+ xml.public_send('sender') do
388
+ xml.public_send('name', location.name)
389
+ xml.public_send('company', location.company) if location.company.present?
390
+ xml.public_send('contact-phone', location.phone)
391
+ xml.public_send('address-details') do
392
+ xml.public_send('address-line-1', location.address1)
393
+ xml.public_send('address-line-2', location.address2_and_3) unless location.address2_and_3.blank?
394
+ xml.public_send('city', location.city)
395
+ xml.public_send('prov-state', location.province)
396
+ # xml.public_send('country-code', location.country_code)
397
+ xml.public_send('postal-zip-code', get_sanitized_postal_code(location))
398
+ end
399
+ end
400
+ end
401
+
402
+ def shipment_destination_node(xml, destination, options)
403
+ location = location_from_hash(destination)
404
+ xml.public_send('destination') do
405
+ xml.public_send('name', location.name)
406
+ xml.public_send('company', location.company) if location.company.present?
407
+ xml.public_send('client-voice-number', location.phone)
408
+ xml.public_send('address-details') do
409
+ xml.public_send('address-line-1', location.address1)
410
+ xml.public_send('address-line-2', location.address2_and_3) unless location.address2_and_3.blank?
411
+ xml.public_send('city', location.city)
412
+ xml.public_send('prov-state', location.province) unless location.province.blank?
413
+ xml.public_send('country-code', location.country_code)
414
+ xml.public_send('postal-zip-code', get_sanitized_postal_code(location))
415
+ end
416
+ end
417
+ end
418
+
419
+ def shipment_options_node(xml, options)
420
+ shipping_options_node(xml, SHIPPING_OPTIONS, options)
421
+ end
422
+
423
+ def shipment_notification_node(xml, options)
424
+ return unless options[:notification_email]
425
+ xml.public_send('notification') do
426
+ xml.public_send('email', options[:notification_email])
427
+ xml.public_send('on-shipment', true)
428
+ xml.public_send('on-exception', true)
429
+ xml.public_send('on-delivery', true)
430
+ end
431
+ end
432
+
433
+ def shipment_preferences_node(xml, options)
434
+ xml.public_send('preferences') do
435
+ xml.public_send('show-packing-instructions', options[:packing_instructions] || true)
436
+ xml.public_send('show-postage-rate', options[:show_postage_rate] || false)
437
+ xml.public_send('show-insured-value', true)
438
+ end
439
+ end
440
+
441
+ def references_node(xml, options)
442
+ # custom values
443
+ # xml.public_send('references') do
444
+ # end
445
+ end
446
+
447
+ def shipment_customs_node(xml, destination, line_items, options)
448
+ return unless location_from_hash(destination).country_code != 'CA'
449
+
450
+ xml.public_send('customs') do
451
+ currency = options[:currency] || "CAD"
452
+ xml.public_send('currency', currency)
453
+ xml.public_send('conversion-from-cad', options[:conversion_from_cad].to_s) if currency != 'CAD' && options[:conversion_from_cad]
454
+ xml.public_send('reason-for-export', 'SOG') # SOG - Sale of Goods
455
+ xml.public_send('other-reason', options[:customs_other_reason]) if options[:customs_reason_for_export] && options[:customs_other_reason]
456
+ xml.public_send('additional-customs-info', options[:customs_addition_info]) if options[:customs_addition_info]
457
+ xml.public_send('sku-list') do
458
+ line_items.each do |line_item|
459
+ kg = '%#2.3f' % [sanitize_weight_kg(line_item.kg)]
460
+ xml.public_send('item') do
461
+ xml.public_send('hs-tariff-code', line_item.hs_code) if line_item.hs_code && !line_item.hs_code.empty?
462
+ xml.public_send('sku', line_item.sku) if line_item.sku && !line_item.sku.empty?
463
+ xml.public_send('customs-description', line_item.name.slice(0, 44))
464
+ xml.public_send('unit-weight', kg)
465
+ xml.public_send('customs-value-per-unit', '%.2f' % sanitize_price_from_cents(line_item.value))
466
+ xml.public_send('customs-number-of-units', line_item.quantity)
467
+ xml.public_send('country-of-origin', line_item.options[:country_of_origin]) if line_item.options && line_item.options[:country_of_origin] && !line_item.options[:country_of_origin].empty?
468
+ xml.public_send('province-of-origin', line_item.options[:province_of_origin]) if line_item.options && line_item.options[:province_of_origin] && !line_item.options[:province_of_origin].empty?
469
+ end
470
+ end
471
+ end
472
+
473
+ end
474
+ end
475
+
476
+ def shipment_parcel_node(xml, package, options = {})
477
+ weight = sanitize_weight_kg(package.kilograms.to_f)
478
+ xml.public_send('parcel-characteristics') do
479
+ xml.public_send('weight', "%#2.3f" % weight)
480
+ pkg_dim = package.cm
481
+ if pkg_dim && !pkg_dim.select { |x| x != 0 }.empty?
482
+ xml.public_send('dimensions') do
483
+ xml.public_send('length', '%.1f' % ((pkg_dim[2] * 10).round / 10.0)) if pkg_dim.size >= 3
484
+ xml.public_send('width', '%.1f' % ((pkg_dim[1] * 10).round / 10.0)) if pkg_dim.size >= 2
485
+ xml.public_send('height', '%.1f' % ((pkg_dim[0] * 10).round / 10.0)) if pkg_dim.size >= 1
486
+ end
487
+ xml.public_send('document', false)
488
+ else
489
+ xml.public_send('document', true)
490
+ end
491
+
492
+ xml.public_send('mailing-tube', package.tube?)
493
+ xml.public_send('unpackaged', package.unpackaged?)
494
+ end
495
+ end
496
+
497
+ def parse_shipment_response(response)
498
+ doc = Nokogiri.XML(response)
499
+ doc.remove_namespaces!
500
+ raise ReactiveShipping::ResponseError, "No Shipping" unless doc.at('non-contract-shipment-info')
501
+ options = {
502
+ :shipping_id => doc.root.at('shipment-id').text,
503
+ :details_url => doc.root.at_xpath("links/link[@rel='details']")['href'],
504
+ :label_url => doc.root.at_xpath("links/link[@rel='label']")['href'],
505
+ :receipt_url => doc.root.at_xpath("links/link[@rel='receipt']")['href'],
506
+ }
507
+ options[:tracking_number] = doc.root.at('tracking-pin').text if doc.root.at('tracking-pin')
508
+
509
+ CPPWSShippingResponse.new(true, "", {}, options)
510
+ end
511
+
512
+ def parse_register_token_response(response)
513
+ doc = Nokogiri.XML(response)
514
+ doc.remove_namespaces!
515
+ raise ReactiveShipping::ResponseError, "No Registration Token" unless doc.at('token')
516
+ options = {
517
+ :token_id => doc.root.at('token-id').text
518
+ }
519
+ CPPWSRegisterResponse.new(true, "", {}, options)
520
+ end
521
+
522
+ def parse_merchant_details_response(response)
523
+ doc = Nokogiri.XML(response)
524
+ doc.remove_namespaces!
525
+ raise "No Merchant Info" unless doc.at('merchant-info')
526
+ raise "No Merchant Info" if doc.root.at('customer-number').blank?
527
+ options = {
528
+ :customer_number => doc.root.at('customer-number').text,
529
+ :contract_number => doc.root.at('contract-number').text,
530
+ :username => doc.root.at('merchant-username').text,
531
+ :password => doc.root.at('merchant-password').text,
532
+ :has_default_credit_card => doc.root.at('has-default-credit-card').text == 'true'
533
+ }
534
+ CPPWSMerchantDetailsResponse.new(true, "", {}, options)
535
+ end
536
+
537
+ def parse_shipment_receipt_response(response)
538
+ doc = Nokogiri.XML(response)
539
+ doc.remove_namespaces!
540
+ root = doc.at('non-contract-shipment-receipt')
541
+ cc_details_node = root.at('cc-receipt-details')
542
+ service_standard_node = root.at('service-standard')
543
+ receipt = {
544
+ :final_shipping_point => root.at("final-shipping-point").text,
545
+ :shipping_point_name => root.at("shipping-point-name").text,
546
+ :service_code => root.at("service-code").text,
547
+ :rated_weight => root.at("rated-weight").text.to_f,
548
+ :base_amount => root.at("base-amount").text.to_f,
549
+ :pre_tax_amount => root.at("pre-tax-amount").text.to_f,
550
+ :gst_amount => root.at("gst-amount").text.to_f,
551
+ :pst_amount => root.at("pst-amount").text.to_f,
552
+ :hst_amount => root.at("hst-amount").text.to_f,
553
+ :charge_amount => cc_details_node.at("charge-amount").text.to_f,
554
+ :currency => cc_details_node.at("currency").text,
555
+ :expected_transit_days => service_standard_node.at("expected-transit-time").text.to_i,
556
+ :expected_delivery_date => service_standard_node.at("expected-delivery-date").text
557
+ }
558
+ option_nodes = root.xpath('priced-options/priced-option')
559
+
560
+ receipt[:priced_options] = if option_nodes.length > 0
561
+ option_nodes.inject({}) do |result, node|
562
+ result[node.at("option-code").text] = node.at("option-price").text.to_f
563
+ result
564
+ end
565
+ else
566
+ {}
567
+ end
568
+
569
+ receipt
570
+ end
571
+
572
+ def error_response(response, response_klass)
573
+ doc = Nokogiri.XML(response)
574
+ doc.remove_namespaces!
575
+ messages = doc.xpath('messages/message')
576
+ message = messages.map { |m| m.at('description').text }.join(", ")
577
+ code = messages.map { |m| m.at('code').text }.join(", ")
578
+ response_klass.new(false, message, {}, :carrier => @@name, :code => code)
579
+ end
580
+
581
+ def log(msg)
582
+ logger.debug(msg) if logger
583
+ end
584
+
585
+ private
586
+
587
+ def tracking_url(pin)
588
+ case pin.length
589
+ when 12, 13, 16
590
+ "#{endpoint}vis/track/pin/#{pin}/detail"
591
+ when 15
592
+ "#{endpoint}vis/track/dnc/#{pin}/detail"
593
+ else
594
+ raise InvalidPinFormatError
595
+ end
596
+ end
597
+
598
+ def create_shipment_url(options)
599
+ raise MissingCustomerNumberError unless customer_number = options[:customer_number]
600
+ if @platform_id.present?
601
+ endpoint + "rs/#{customer_number}-#{@platform_id}/ncshipment"
602
+ else
603
+ endpoint + "rs/#{customer_number}/ncshipment"
604
+ end
605
+ end
606
+
607
+ def shipment_url(shipping_id, options = {})
608
+ raise MissingCustomerNumberError unless customer_number = options[:customer_number]
609
+ if @platform_id.present?
610
+ endpoint + "rs/#{customer_number}-#{@platform_id}/ncshipment/#{shipping_id}"
611
+ else
612
+ endpoint + "rs/#{customer_number}/ncshipment/#{shipping_id}"
613
+ end
614
+ end
615
+
616
+ def shipment_receipt_url(shipping_id, options = {})
617
+ raise MissingCustomerNumberError unless customer_number = options[:customer_number]
618
+ if @platform_id.present?
619
+ endpoint + "rs/#{customer_number}-#{@platform_id}/ncshipment/#{shipping_id}/receipt"
620
+ else
621
+ endpoint + "rs/#{customer_number}/ncshipment/#{shipping_id}/receipt"
622
+ end
623
+ end
624
+
625
+ def services_url(country = nil, service_code = nil)
626
+ url = endpoint + "rs/ship/service"
627
+ url += "/#{service_code}" if service_code
628
+ url += "?country=#{country}" if country
629
+ url
630
+ end
631
+
632
+ def customer_credentials_valid?(credentials)
633
+ (credentials.keys & [:customer_api_key, :customer_secret]).any?
634
+ end
635
+
636
+ def encoded_authorization(customer_credentials = {})
637
+ if customer_credentials_valid?(customer_credentials)
638
+ "Basic %s" % Base64.encode64("#{customer_credentials[:customer_api_key]}:#{customer_credentials[:customer_secret]}")
639
+ else
640
+ "Basic %s" % Base64.encode64("#{@options[:api_key]}:#{@options[:secret]}")
641
+ end
642
+ end
643
+
644
+ def headers(customer_credentials, accept = nil, content_type = nil)
645
+ headers = {
646
+ 'Authorization' => encoded_authorization(customer_credentials),
647
+ 'Accept-Language' => language
648
+ }
649
+ headers['Accept'] = accept if accept
650
+ headers['Content-Type'] = content_type if content_type
651
+ headers['Platform-ID'] = platform_id if platform_id && customer_credentials_valid?(customer_credentials)
652
+ headers
653
+ end
654
+
655
+ def customer_number_node(xml, options)
656
+ xml.public_send("customer-number", options[:customer_number] || customer_number)
657
+ end
658
+
659
+ def contract_id_node(xml, options)
660
+ xml.public_send("contract-id", options[:contract_id]) if options[:contract_id]
661
+ end
662
+
663
+ def quote_type_node(xml, options)
664
+ xml.public_send("quote-type", 'commercial')
665
+ end
666
+
667
+ def expected_mailing_date_node(xml, date_as_string)
668
+ xml.public_send("expected-mailing-date", date_as_string)
669
+ end
670
+
671
+ def parcel_node(xml, line_items, package = nil, options = {})
672
+ weight = sanitize_weight_kg(package && !package.kilograms.zero? ? package.kilograms.to_f : line_items.sum(&:kilograms).to_f)
673
+ xml.public_send('parcel-characteristics') do
674
+ xml.public_send('weight', "%#2.3f" % weight)
675
+ if package
676
+ pkg_dim = package.cm
677
+ if pkg_dim && !pkg_dim.select { |x| x != 0 }.empty?
678
+ xml.public_send('dimensions') do
679
+ xml.public_send('length', '%.1f' % ((pkg_dim[2] * 10).round / 10.0)) if pkg_dim.size >= 3
680
+ xml.public_send('width', '%.1f' % ((pkg_dim[1] * 10).round / 10.0)) if pkg_dim.size >= 2
681
+ xml.public_send('height', '%.1f' % ((pkg_dim[0] * 10).round / 10.0)) if pkg_dim.size >= 1
682
+ end
683
+ end
684
+ end
685
+ xml.public_send('mailing-tube', line_items.any?(&:tube?))
686
+ xml.public_send('oversized', true) if line_items.any?(&:oversized?)
687
+ xml.public_send('unpackaged', line_items.any?(&:unpackaged?))
688
+ end
689
+ end
690
+
691
+ def origin_node(xml, location)
692
+ origin = location_from_hash(location)
693
+ xml.public_send("origin-postal-code", get_sanitized_postal_code(origin))
694
+ end
695
+
696
+ def destination_node(xml, location)
697
+ destination = location_from_hash(location)
698
+ postal_code = get_sanitized_postal_code(destination)
699
+ case destination.country_code
700
+ when 'CA'
701
+ xml.public_send('destination') do
702
+ xml.public_send('domestic') do
703
+ xml.public_send('postal-code', postal_code)
704
+ end
705
+ end
706
+
707
+ when 'US'
708
+ xml.public_send('destination') do
709
+ xml.public_send('united-states') do
710
+ xml.public_send('zip-code', postal_code)
711
+ end
712
+ end
713
+
714
+ else
715
+ xml.public_send('destination') do
716
+ xml.public_send('international') do
717
+ xml.public_send('country-code', destination.country_code)
718
+ end
719
+ end
720
+ end
721
+ end
722
+
723
+ def services_node(xml, services)
724
+ xml.public_send('services') do
725
+ services.each { |code| xml.public_send('service-code', code) }
726
+ end
727
+ end
728
+
729
+ def shipping_options_node(xml, available_options, options = {})
730
+ return if (options.symbolize_keys.keys & available_options).empty?
731
+ xml.public_send('options') do
732
+
733
+ if options[:cod] && options[:cod_amount]
734
+ xml.public_send('option') do
735
+ xml.public_send('option-code', 'COD')
736
+ xml.public_send('option-amount', options[:cod_amount])
737
+ xml.public_send('option-qualifier-1', options[:cod_includes_shipping]) unless options[:cod_includes_shipping].blank?
738
+ xml.public_send('option-qualifier-2', options[:cod_method_of_payment]) unless options[:cod_method_of_payment].blank?
739
+ end
740
+ end
741
+
742
+ if options[:cov]
743
+ xml.public_send('option') do
744
+ xml.public_send('option-code', 'COV')
745
+ xml.public_send('option-amount', options[:cov_amount]) unless options[:cov_amount].blank?
746
+ end
747
+ end
748
+
749
+ if options[:d2po]
750
+ xml.public_send('option') do
751
+ xml.public_send('option-code', 'D2PO')
752
+ xml.public_send('option-qualifier-2'. options[:d2po_office_id]) unless options[:d2po_office_id].blank?
753
+ end
754
+ end
755
+
756
+ [:so, :dc, :pa18, :pa19, :hfp, :dns, :lad, :rase, :rts, :aban].each do |code|
757
+ if options[code]
758
+ xml.public_send('option') do
759
+ xml.public_send('option-code', code.to_s.upcase)
760
+ end
761
+ end
762
+ end
763
+ end
764
+ end
765
+
766
+ def expected_date_from_node(node)
767
+ if service = node.at('service-standard/expected-delivery-date')
768
+ expected_date = service.text
769
+ else
770
+ expected_date = nil
771
+ end
772
+ expected_date
773
+ end
774
+
775
+ def shipping_date(options)
776
+ DateTime.strptime((options[:shipping_date] || Time.now).to_s, "%Y-%m-%d")
777
+ end
778
+
779
+ def location_from_hash(location)
780
+ return location if location.is_a?(Location)
781
+ return Location.new(location)
782
+ end
783
+
784
+ def get_sanitized_postal_code(location)
785
+ return nil if location.nil? || location.postal_code.nil?
786
+ location.postal_code.gsub(/\s+/, '').upcase
787
+ end
788
+
789
+ def sanitize_weight_kg(kg)
790
+ kg == 0 ? 0.001 : kg
791
+ end
792
+
793
+ def sanitize_price_from_cents(value)
794
+ value == 0 ? 0.01 : value.round / 100.0
795
+ end
796
+
797
+ def origin_hash_for(root)
798
+ occurrences = root.xpath('significant-events/occurrence')
799
+ earliest = occurrences.sort_by { |occurrence| time_of_occurrence(occurrence) }.first
800
+
801
+ {
802
+ city: earliest.at('event-site').text,
803
+ province: earliest.at('event-province').text,
804
+ address_1: earliest.at('event-retail-location-id').text,
805
+ country: 'Canada'
806
+ }
807
+ end
808
+
809
+ def time_of_occurrence(occurrence)
810
+ time = occurrence.at('event-time').text
811
+ date = occurrence.at('event-date').text
812
+ time_zone = occurrence.at('event-time-zone').text
813
+ DateTime.parse "#{date} #{time} #{time_zone}"
814
+ end
815
+ end
816
+
817
+ module CPPWSErrorResponse
818
+ attr_accessor :error_code
819
+ def handle_error(message, options)
820
+ @error_code = options[:code]
821
+ end
822
+ end
823
+
824
+ class CPPWSRateResponse < RateResponse
825
+ include CPPWSErrorResponse
826
+
827
+ def initialize(success, message, params = {}, options = {})
828
+ handle_error(message, options)
829
+ super
830
+ end
831
+ end
832
+
833
+ class CPPWSTrackingResponse < TrackingResponse
834
+ DELIVERED_EVENT_CODES = %w(1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438 1441 1442 1496 1497 1498 1499 5300)
835
+ include CPPWSErrorResponse
836
+
837
+ attr_reader :service_name, :expected_date, :changed_date, :change_reason, :customer_number
838
+
839
+ def initialize(success, message, params = {}, options = {})
840
+ handle_error(message, options)
841
+ super
842
+ @service_name = options[:service_name]
843
+ @expected_date = options[:expected_date]
844
+ @changed_date = options[:changed_date]
845
+ @change_reason = options[:change_reason]
846
+ @customer_number = options[:customer_number]
847
+ end
848
+
849
+ def delivered?
850
+ !delivered_event.nil?
851
+ end
852
+
853
+ def actual_delivery_time
854
+ delivered_event.time if delivered?
855
+ end
856
+
857
+ private
858
+
859
+ def delivered_event
860
+ @delivered_event ||= @shipment_events.detect { |event| DELIVERED_EVENT_CODES.include? event.name }
861
+ end
862
+ end
863
+
864
+ class CPPWSShippingResponse < ShippingResponse
865
+ include CPPWSErrorResponse
866
+ attr_reader :label_url, :details_url, :receipt_url
867
+ def initialize(success, message, params = {}, options = {})
868
+ handle_error(message, options)
869
+ super
870
+ @label_url = options[:label_url]
871
+ @details_url = options[:details_url]
872
+ @receipt_url = options[:receipt_url]
873
+ end
874
+ end
875
+
876
+ class CPPWSRegisterResponse < Response
877
+ include CPPWSErrorResponse
878
+ attr_reader :token_id
879
+ def initialize(success, message, params = {}, options = {})
880
+ handle_error(message, options)
881
+ super
882
+ @token_id = options[:token_id]
883
+ end
884
+
885
+ def redirect_url(customer_id, return_url)
886
+ "http://www.canadapost.ca/cpotools/apps/drc/merchant?return-url=#{CGI.escape(return_url)}&token-id=#{token_id}&platform-id=#{customer_id}"
887
+ end
888
+ end
889
+
890
+ class CPPWSMerchantDetailsResponse < Response
891
+ include CPPWSErrorResponse
892
+ attr_reader :customer_number, :contract_number, :username, :password, :has_default_credit_card
893
+ def initialize(success, message, params = {}, options = {})
894
+ handle_error(message, options)
895
+ super
896
+ @customer_number = options[:customer_number]
897
+ @contract_number = options[:contract_number]
898
+ @username = options[:username]
899
+ @password = options[:password]
900
+ @has_default_credit_card = options[:has_default_credit_card]
901
+ end
902
+ end
903
+
904
+ class InvalidPinFormatError < StandardError; end
905
+ class MissingCustomerNumberError < StandardError; end
906
+ class MissingShippingNumberError < StandardError; end
907
+ class MissingTokenIdError < StandardError; end
908
+ end