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