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,155 @@
1
+ module ReactiveShipping
2
+ class Kunaki < Carrier
3
+ self.retry_safe = true
4
+
5
+ cattr_reader :name
6
+ @@name = "Kunaki"
7
+
8
+ URL = 'http://Kunaki.com/XMLService.ASP'
9
+
10
+ CARRIERS = ["UPS", "USPS", "FedEx", "Royal Mail", "Parcelforce", "Pharos", "Eurotrux", "Canada Post", "DHL"]
11
+
12
+ COUNTRIES = {
13
+ 'AR' => 'Argentina',
14
+ 'AU' => 'Australia',
15
+ 'AT' => 'Austria',
16
+ 'BE' => 'Belgium',
17
+ 'BR' => 'Brazil',
18
+ 'BG' => 'Bulgaria',
19
+ 'CA' => 'Canada',
20
+ 'CN' => 'China',
21
+ 'CY' => 'Cyprus',
22
+ 'CZ' => 'Czech Republic',
23
+ 'DK' => 'Denmark',
24
+ 'EE' => 'Estonia',
25
+ 'FI' => 'Finland',
26
+ 'FR' => 'France',
27
+ 'DE' => 'Germany',
28
+ 'GI' => 'Gibraltar',
29
+ 'GR' => 'Greece',
30
+ 'GL' => 'Greenland',
31
+ 'HK' => 'Hong Kong',
32
+ 'HU' => 'Hungary',
33
+ 'IS' => 'Iceland',
34
+ 'IE' => 'Ireland',
35
+ 'IL' => 'Israel',
36
+ 'IT' => 'Italy',
37
+ 'JP' => 'Japan',
38
+ 'LV' => 'Latvia',
39
+ 'LI' => 'Liechtenstein',
40
+ 'LT' => 'Lithuania',
41
+ 'LU' => 'Luxembourg',
42
+ 'MX' => 'Mexico',
43
+ 'NL' => 'Netherlands',
44
+ 'NZ' => 'New Zealand',
45
+ 'NO' => 'Norway',
46
+ 'PL' => 'Poland',
47
+ 'PT' => 'Portugal',
48
+ 'RO' => 'Romania',
49
+ 'RU' => 'Russia',
50
+ 'SG' => 'Singapore',
51
+ 'SK' => 'Slovakia',
52
+ 'SI' => 'Slovenia',
53
+ 'ES' => 'Spain',
54
+ 'SE' => 'Sweden',
55
+ 'CH' => 'Switzerland',
56
+ 'TW' => 'Taiwan',
57
+ 'TR' => 'Turkey',
58
+ 'UA' => 'Ukraine',
59
+ 'GB' => 'United Kingdom',
60
+ 'US' => 'United States',
61
+ 'VA' => 'Vatican City',
62
+ 'RS' => 'Yugoslavia',
63
+ 'ME' => 'Yugoslavia'
64
+ }
65
+
66
+ def find_rates(origin, destination, packages, options = {})
67
+ requires!(options, :items)
68
+ commit(origin, destination, options)
69
+ end
70
+
71
+ def valid_credentials?
72
+ true
73
+ end
74
+
75
+ private
76
+
77
+ def build_request(destination, options)
78
+ country = COUNTRIES[destination.country_code]
79
+ state_province = %w(US CA).include?(destination.country_code.to_s) ? destination.state : ''
80
+
81
+ builder = Nokogiri::XML::Builder.new do |xml|
82
+ xml.ShippingOptions do
83
+ xml.AddressInfo do
84
+ xml.Country(country)
85
+ xml.State_Province(state_province)
86
+ xml.PostalCode(destination.zip)
87
+ end
88
+
89
+ options[:items].each do |item|
90
+ xml.Product do
91
+ xml.ProductId(item[:sku])
92
+ xml.Quantity(item[:quantity])
93
+ end
94
+ end
95
+ end
96
+ end
97
+ builder.to_xml
98
+ end
99
+
100
+ def commit(origin, destination, options)
101
+ request = build_request(destination, options)
102
+ response = parse( ssl_post(URL, request, "Content-Type" => "text/xml") )
103
+
104
+ RateResponse.new(success?(response), message_from(response), response,
105
+ :rates => build_rate_estimates(response, origin, destination)
106
+ )
107
+ end
108
+
109
+ def build_rate_estimates(response, origin, destination)
110
+ response["Options"].collect do |quote|
111
+ RateEstimate.new(origin, destination, carrier_for(quote["Description"]), quote["Description"],
112
+ :total_price => quote["Price"],
113
+ :currency => "USD"
114
+ )
115
+ end
116
+ end
117
+
118
+ def carrier_for(service)
119
+ CARRIERS.dup.find { |carrier| service.to_s =~ /^#{carrier}/i } || service.to_s.split(" ").first
120
+ end
121
+
122
+ def parse(xml)
123
+ response = {}
124
+ response["Options"] = []
125
+
126
+ document = Nokogiri.XML(sanitize(xml))
127
+
128
+ response["ErrorCode"] = document.at('/Response/ErrorCode').text
129
+ response["ErrorText"] = document.at('/Response/ErrorText').text
130
+
131
+ document.xpath("Response/Option").each do |node|
132
+ rate = {}
133
+ rate["Description"] = node.at("Description").text
134
+ rate["Price"] = node.at("Price").text
135
+ response["Options"] << rate
136
+ end
137
+ response
138
+ end
139
+
140
+ def sanitize(response)
141
+ result = response.to_s
142
+ result.gsub!("\r\n", "")
143
+ result.gsub!(/<(\/)?(BODY|HTML)>/, '')
144
+ result
145
+ end
146
+
147
+ def success?(response)
148
+ response["ErrorCode"] == "0"
149
+ end
150
+
151
+ def message_from(response)
152
+ response["ErrorText"]
153
+ end
154
+ end
155
+ end
@@ -0,0 +1,260 @@
1
+ module ReactiveShipping
2
+ class NewZealandPost < Carrier
3
+ cattr_reader :name
4
+ @@name = "New Zealand Post"
5
+
6
+ URL = "http://api.nzpost.co.nz/ratefinder"
7
+
8
+ def requirements
9
+ [:key]
10
+ end
11
+
12
+ def find_rates(origin, destination, packages, options = {})
13
+ options = @options.merge(options)
14
+ request = RateRequest.from(origin, destination, packages, options)
15
+ request.raw_responses = commit(request.urls) if request.new_zealand_origin?
16
+ request.rate_response
17
+ end
18
+
19
+ protected
20
+
21
+ def commit(urls)
22
+ save_request(urls).map { |url| ssl_get(url) }
23
+ end
24
+
25
+ def self.default_location
26
+ Location.new(
27
+ :country => "NZ",
28
+ :city => "Wellington",
29
+ :address1 => "22 Waterloo Quay",
30
+ :address2 => "Pipitea",
31
+ :postal_code => "6011"
32
+ )
33
+ end
34
+
35
+ class NewZealandPostRateResponse < RateResponse
36
+ attr_reader :raw_responses
37
+
38
+ def initialize(success, message, params = {}, options = {})
39
+ @raw_responses = options[:raw_responses]
40
+ super
41
+ end
42
+ end
43
+
44
+ class RateRequest
45
+ attr_reader :urls
46
+ attr_writer :raw_responses
47
+
48
+ def self.from(*args)
49
+ return International.new(*args) unless domestic?(args[0..1])
50
+ Domestic.new(*args)
51
+ end
52
+
53
+ def initialize(origin, destination, packages, options)
54
+ @origin = Location.from(origin)
55
+ @destination = Location.from(destination)
56
+ @packages = Array(packages).map { |package| NewZealandPostPackage.new(package, api) }
57
+ @params = { :format => "json", :api_key => options[:key] }
58
+ @test = options[:test]
59
+ @rates = @responses = @raw_responses = []
60
+ @urls = @packages.map { |package| url(package) }
61
+ end
62
+
63
+ def rate_response
64
+ @rates = rates
65
+ NewZealandPostRateResponse.new(true, "success", response_params, response_options)
66
+ rescue => error
67
+ NewZealandPostRateResponse.new(false, error.message, response_params, response_options)
68
+ end
69
+
70
+ def new_zealand_origin?
71
+ self.class.new_zealand?(@origin)
72
+ end
73
+
74
+ protected
75
+
76
+ def self.new_zealand?(location)
77
+ ['NZ', nil].include?(Location.from(location).country_code)
78
+ end
79
+
80
+ def self.domestic?(locations)
81
+ locations.select { |location| new_zealand?(location) }.size == 2
82
+ end
83
+
84
+ def response_options
85
+ {
86
+ :rates => @rates,
87
+ :raw_responses => @raw_responses,
88
+ :request => @urls,
89
+ :test => @test
90
+ }
91
+ end
92
+
93
+ def response_params
94
+ { :responses => @responses }
95
+ end
96
+
97
+ def rate_options(products)
98
+ {
99
+ :total_price => products.sum { |product| price(product) },
100
+ :currency => "NZD",
101
+ :service_code => products.first["code"]
102
+ }
103
+ end
104
+
105
+ def rates
106
+ rates_hash.map do |service, products|
107
+ RateEstimate.new(@origin, @destination, NewZealandPost.name, service, rate_options(products))
108
+ end
109
+ end
110
+
111
+ def rates_hash
112
+ products_hash.select { |_service, products| products.size == @packages.size }
113
+ end
114
+
115
+ def products_hash
116
+ product_arrays.flatten.group_by { |product| service_name(product) }
117
+ end
118
+
119
+ def product_arrays
120
+ responses.map do |response|
121
+ raise(response["message"]) unless response["status"] == "success"
122
+ response["products"]
123
+ end
124
+ end
125
+
126
+ def responses
127
+ @responses = @raw_responses.map { |response| parse_response(response) }
128
+ end
129
+
130
+ def parse_response(response)
131
+ JSON.parse(response)
132
+ end
133
+
134
+ def url(package)
135
+ "#{URL}/#{api}?#{params(package).to_query}"
136
+ end
137
+
138
+ def params(package)
139
+ @params.merge(api_params).merge(package.params)
140
+ end
141
+ end
142
+
143
+ class Domestic < RateRequest
144
+ def service_name(product)
145
+ [product["service_group_description"], product["description"]].join(" ")
146
+ end
147
+
148
+ def api
149
+ :domestic
150
+ end
151
+
152
+ def api_params
153
+ {
154
+ :postcode_src => @origin.postal_code,
155
+ :postcode_dest => @destination.postal_code,
156
+ :carrier => "all"
157
+ }
158
+ end
159
+
160
+ def price(product)
161
+ product["cost"].to_f
162
+ end
163
+ end
164
+
165
+ class International < RateRequest
166
+ def rates
167
+ raise "New Zealand Post packages must originate in New Zealand" unless new_zealand_origin?
168
+ super
169
+ end
170
+
171
+ def service_name(product)
172
+ [product["group"], product["name"]].join(" ")
173
+ end
174
+
175
+ def api
176
+ :international
177
+ end
178
+
179
+ def api_params
180
+ { :country_code => @destination.country_code }
181
+ end
182
+
183
+ def price(product)
184
+ product["price"].to_f
185
+ end
186
+ end
187
+
188
+ class NewZealandPostPackage
189
+ def initialize(package, api)
190
+ @package = package
191
+ @api = api
192
+ @params = { :weight => weight, :length => length }
193
+ end
194
+
195
+ def params
196
+ @params.merge(api_params).merge(shape_params)
197
+ end
198
+
199
+ protected
200
+
201
+ def weight
202
+ @package.kg
203
+ end
204
+
205
+ def length
206
+ mm(:length)
207
+ end
208
+
209
+ def height
210
+ mm(:height)
211
+ end
212
+
213
+ def width
214
+ mm(:width)
215
+ end
216
+
217
+ def shape
218
+ return :cylinder if @package.cylinder?
219
+ :cuboid
220
+ end
221
+
222
+ def api_params
223
+ send("#{@api}_params")
224
+ end
225
+
226
+ def international_params
227
+ { :value => value }
228
+ end
229
+
230
+ def domestic_params
231
+ {}
232
+ end
233
+
234
+ def shape_params
235
+ send("#{shape}_params")
236
+ end
237
+
238
+ def cuboid_params
239
+ { :height => height, :thickness => width }
240
+ end
241
+
242
+ def cylinder_params
243
+ { :diameter => width }
244
+ end
245
+
246
+ def mm(measurement)
247
+ @package.cm(measurement) * 10
248
+ end
249
+
250
+ def value
251
+ return 0 unless @package.value && currency == "NZD"
252
+ @package.value / 100
253
+ end
254
+
255
+ def currency
256
+ @package.currency || "NZD"
257
+ end
258
+ end
259
+ end
260
+ end
@@ -0,0 +1,178 @@
1
+ module ReactiveShipping
2
+ class Shipwire < Carrier
3
+ self.retry_safe = true
4
+
5
+ cattr_reader :name
6
+ @@name = "Shipwire"
7
+
8
+ URL = 'https://api.shipwire.com/exec/RateServices.php'
9
+ SCHEMA_URL = 'http://www.shipwire.com/exec/download/RateRequest.dtd'
10
+ WAREHOUSES = { 'CHI' => 'Chicago',
11
+ 'LAX' => 'Los Angeles',
12
+ 'REN' => 'Reno',
13
+ 'VAN' => 'Vancouver',
14
+ 'TOR' => 'Toronto',
15
+ 'UK' => 'United Kingdom'
16
+ }
17
+
18
+ CARRIERS = ["UPS", "USPS", "FedEx", "Royal Mail", "Parcelforce", "Pharos", "Eurotrux", "Canada Post", "DHL"]
19
+
20
+ SUCCESS = "OK"
21
+ SUCCESS_MESSAGE = "Successfully received the shipping rates"
22
+ NO_RATES_MESSAGE = "No shipping rates could be found for the destination address"
23
+ REQUIRED_OPTIONS = [:login, :password].freeze
24
+
25
+ def find_rates(origin, destination, packages, options = {})
26
+ requires!(options, :items)
27
+ commit(origin, destination, options)
28
+ end
29
+
30
+ def valid_credentials?
31
+ location = self.class.default_location
32
+ find_rates(location, location, Package.new(100, [5, 15, 30]),
33
+ :items => [{ :sku => '', :quantity => 1 }]
34
+ )
35
+ rescue ReactiveShipping::ResponseError
36
+ true
37
+ rescue ActiveUtils::ResponseError => e
38
+ e.response.code != '401'
39
+ end
40
+
41
+ private
42
+
43
+ def requirements
44
+ REQUIRED_OPTIONS
45
+ end
46
+
47
+ def build_request(destination, options)
48
+ Nokogiri::XML::Builder.new do |xml|
49
+ xml.doc.create_internal_subset('RateRequest', nil, SCHEMA_URL)
50
+ xml.RateRequest do
51
+ add_credentials(xml)
52
+ add_order(xml, destination, options)
53
+ end
54
+ end.to_xml
55
+ end
56
+
57
+ def add_credentials(xml)
58
+ xml.EmailAddress @options[:login]
59
+ xml.Password @options[:password]
60
+ end
61
+
62
+ def add_order(xml, destination, options)
63
+ xml.Order :id => options[:order_id] do
64
+ xml.Warehouse options[:warehouse] || '00'
65
+
66
+ add_address(xml, destination)
67
+ Array(options[:items]).each_with_index do |line_item, index|
68
+ add_item(xml, line_item, index)
69
+ end
70
+ end
71
+ end
72
+
73
+ def add_address(xml, destination)
74
+ xml.AddressInfo :type => 'Ship' do
75
+ if destination.name.present?
76
+ xml.Name do
77
+ xml.Full destination.name
78
+ end
79
+ end
80
+ xml.Address1 destination.address1
81
+ xml.Address2 destination.address2 unless destination.address2.blank?
82
+ xml.Address3 destination.address3 unless destination.address3.blank?
83
+ xml.Company destination.company unless destination.company.blank?
84
+ xml.City destination.city
85
+ xml.State destination.state unless destination.state.blank?
86
+ xml.Country destination.country_code
87
+ xml.Zip destination.zip unless destination.zip.blank?
88
+ end
89
+ end
90
+
91
+ # Code is limited to 12 characters
92
+ def add_item(xml, item, index)
93
+ xml.Item :num => index do
94
+ xml.Code item[:sku]
95
+ xml.Quantity item[:quantity]
96
+ end
97
+ end
98
+
99
+ def commit(origin, destination, options)
100
+ request = build_request(destination, options)
101
+ save_request(request)
102
+
103
+ response = parse( ssl_post(URL, "RateRequestXML=#{CGI.escape(request)}") )
104
+
105
+ RateResponse.new(response["success"], response["message"], response,
106
+ :xml => response,
107
+ :rates => build_rate_estimates(response, origin, destination),
108
+ :request => last_request
109
+ )
110
+ end
111
+
112
+ def build_rate_estimates(response, origin, destination)
113
+ response["rates"].collect do |quote|
114
+ RateEstimate.new(origin, destination, carrier_for(quote["service"]), quote["service"],
115
+ :service_code => quote["method"],
116
+ :total_price => quote["cost"],
117
+ :currency => quote["currency"],
118
+ :delivery_range => [timestamp_from_business_day(quote["delivery_min"]),
119
+ timestamp_from_business_day(quote["delivery_max"])]
120
+ )
121
+ end
122
+ end
123
+
124
+ def carrier_for(service)
125
+ CARRIERS.dup.find { |carrier| service.to_s =~ /^#{carrier}/i } || service.to_s.split(" ").first
126
+ end
127
+
128
+ def parse(xml)
129
+ response = {}
130
+ response["rates"] = []
131
+
132
+ document = Nokogiri.XML(xml)
133
+
134
+ response["status"] = parse_child_text(document.root, "Status")
135
+
136
+ document.root.xpath("Order/Quotes/Quote").each do |e|
137
+ rate = {}
138
+ rate["method"] = e["method"]
139
+ rate["warehouse"] = parse_child_text(e, "Warehouse")
140
+ rate["service"] = parse_child_text(e, "Service")
141
+ rate["cost"] = parse_child_text(e, "Cost")
142
+ rate["currency"] = parse_child_attribute(e, "Cost", "currency")
143
+ if delivery_estimate = e.at("DeliveryEstimate")
144
+ rate["delivery_min"] = parse_child_text(delivery_estimate, "Minimum").to_i
145
+ rate["delivery_max"] = parse_child_text(delivery_estimate, "Maximum").to_i
146
+ end
147
+ response["rates"] << rate
148
+ end
149
+
150
+ if response["status"] == SUCCESS && response["rates"].any?
151
+ response["success"] = true
152
+ response["message"] = SUCCESS_MESSAGE
153
+ elsif response["status"] == SUCCESS && response["rates"].empty?
154
+ response["success"] = false
155
+ response["message"] = NO_RATES_MESSAGE
156
+ else
157
+ response["success"] = false
158
+ response["message"] = parse_child_text(document.root, "ErrorMessage")
159
+ end
160
+
161
+ response
162
+ rescue NoMethodError => e
163
+ raise ReactiveShipping::ResponseContentError.new(e, xml)
164
+ end
165
+
166
+ def parse_child_text(parent, name)
167
+ if element = parent.at(name)
168
+ element.text
169
+ end
170
+ end
171
+
172
+ def parse_child_attribute(parent, name, attribute)
173
+ if element = parent.at(name)
174
+ element[attribute]
175
+ end
176
+ end
177
+ end
178
+ end