active_shipping 0.12.6 → 1.0.0.pre1

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