active_shipping 0.12.6 → 1.0.0.pre1

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