Class: ActiveFulfillment::AmazonMarketplaceWebService
- Inherits:
-
Service
- Object
- Service
- ActiveFulfillment::AmazonMarketplaceWebService
show all
- Defined in:
- lib/active_fulfillment/services/amazon_mws.rb
Constant Summary
collapse
- APPLICATION_IDENTIFIER =
'active_merchant_mws/0.01 (Language=ruby)'.freeze
- REGISTRATION_URI =
URI.parse('https://sellercentral.amazon.com/gp/mws/registration/register.html').freeze
- SIGNATURE_VERSION =
2
- SIGNATURE_METHOD =
'SHA256'.freeze
- VERSION =
'2010-10-01'.freeze
- XML_FAILURE_RESPONSE =
{ :success => FAILURE }.freeze
- ENDPOINTS =
{
:ca => 'mws.amazonservices.ca',
:cn => 'mws.amazonservices.com.cn',
:de => 'mws-eu.amazonservices.ca',
:es => 'mws-eu.amazonservices.ca',
:fr => 'mws-eu.amazonservices.ca',
:it => 'mws-eu.amazonservices.ca',
:jp => 'mws.amazonservices.jp',
:uk => 'mws-eu.amazonservices.ca',
:us => 'mws.amazonservices.com'
}.freeze
- LOOKUPS =
{
:destination_address => {
:name => "DestinationAddress.Name",
:address1 => "DestinationAddress.Line1",
:address2 => "DestinationAddress.Line2",
:city => "DestinationAddress.City",
:state => "DestinationAddress.StateOrProvinceCode",
:country => "DestinationAddress.CountryCode",
:zip => "DestinationAddress.PostalCode",
:phone => "DestinationAddress.PhoneNumber"
},
:line_items => {
:comment => "Items.member.%d.DisplayableComment",
:gift_message => "Items.member.%d.GiftMessage",
:currency_code => "Items.member.%d.PerUnitDeclaredValue.CurrencyCode",
:value => "Items.member.%d.PerUnitDeclaredValue.Value",
:quantity => "Items.member.%d.Quantity",
:order_id => "Items.member.%d.SellerFulfillmentOrderItemId",
:sku => "Items.member.%d.SellerSKU",
:network_sku => "Items.member.%d.FulfillmentNetworkSKU",
:item_disposition => "Items.member.%d.OrderItemDisposition",
},
:list_inventory => {
:sku => "SellerSkus.member.%d"
}
}.freeze
- SHIPPING_METHODS =
{
'Standard Shipping' => 'Standard',
'Expedited Shipping' => 'Expedited',
'Priority Shipping' => 'Priority'
}.freeze
Class Method Summary
collapse
-
.shipping_methods ⇒ Object
The first is the label, and the last is the code Standard: 3-5 business days Expedited: 2 business days Priority: 1 business day.
Instance Method Summary
collapse
-
#amazon_request?(http_verb, base_url, return_path_and_parameters, post_params) ⇒ Boolean
-
#build_address(address) ⇒ Object
-
#build_basic_api_query(options) ⇒ Object
-
#build_fulfillment_request(order_id, shipping_address, line_items, options) ⇒ Object
-
#build_full_query(verb, uri, params) ⇒ Object
-
#build_get_current_fulfillment_orders_request(options = {}) ⇒ Object
-
#build_headers(querystr) ⇒ Object
-
#build_inventory_list_request(options = {}) ⇒ Object
-
#build_items(line_items) ⇒ Object
-
#build_next_inventory_list_request(token) ⇒ Object
-
#build_query(query_params) ⇒ Object
-
#build_tracking_request(order_id, options) ⇒ Object
-
#commit(verb, action, params) ⇒ Object
-
#endpoint ⇒ Object
-
#escape(str) ⇒ Object
-
#fetch_current_orders ⇒ Object
-
#fetch_stock_levels(options = {}) ⇒ Object
-
#fetch_tracking_data(order_ids, options = {}) ⇒ Object
-
#fulfill(order_id, shipping_address, line_items, options = {}) ⇒ Object
-
#handle_error(e) ⇒ Object
-
#initialize(options = {}) ⇒ AmazonMarketplaceWebService
constructor
A new instance of AmazonMarketplaceWebService.
-
#md5_content(content) ⇒ Object
-
#message_from(response) ⇒ Object
-
#parse_document(xml) ⇒ Object
-
#parse_error(http_response) ⇒ Object
-
#parse_fulfillment_response(message) ⇒ Object
-
#parse_inventory_response(document) ⇒ Object
-
#parse_tracking_response(document) ⇒ Object
-
#registration_url(options) ⇒ Object
-
#seller_id=(seller_id) ⇒ Object
-
#sign(http_verb, uri, options) ⇒ Object
-
#status ⇒ Object
-
#success?(response) ⇒ Boolean
-
#test_mode? ⇒ Boolean
-
#valid_credentials? ⇒ Boolean
Methods inherited from Service
#fetch_tracking_numbers, #test?
Constructor Details
Returns a new instance of AmazonMarketplaceWebService.
73
74
75
76
77
78
79
|
# File 'lib/active_fulfillment/services/amazon_mws.rb', line 73
def initialize(options = {})
requires!(options, :login, :password)
@seller_id = options[:seller_id]
@mws_auth_token = options[:mws_auth_token]
@maximum_response_log_size = options[:maximum_response_log_size] || 0
super
end
|
Class Method Details
.shipping_methods ⇒ Object
The first is the label, and the last is the code Standard: 3-5 business days Expedited: 2 business days Priority: 1 business day
69
70
71
|
# File 'lib/active_fulfillment/services/amazon_mws.rb', line 69
def self.shipping_methods
SHIPPING_METHODS
end
|
Instance Method Details
#amazon_request?(http_verb, base_url, return_path_and_parameters, post_params) ⇒ Boolean
289
290
291
292
293
294
|
# File 'lib/active_fulfillment/services/amazon_mws.rb', line 289
def amazon_request?(http_verb, base_url, return_path_and_parameters, post_params)
signed_params = build_query(post_params.except(:Signature, :SignedString))
string_to_sign = "#{http_verb}\n#{base_url}\n#{return_path_and_parameters}\n#{signed_params}"
calculated_signature = Base64.encode64(OpenSSL::HMAC.digest(SIGNATURE_METHOD, @options[:password], string_to_sign)).chomp
secure_compare(calculated_signature, post_params[:Signature])
end
|
#build_address(address) ⇒ Object
396
397
398
399
400
401
402
403
404
|
# File 'lib/active_fulfillment/services/amazon_mws.rb', line 396
def build_address(address)
requires!(address, :name, :address1, :city, :country, :zip)
address[:state] ||= "N/A"
address[:zip].upcase! if address[:zip]
address[:name] = "#{address[:company]} - #{address[:name]}" if address[:company].present?
address[:name] = address[:name][0...50] if address[:name].present?
ary = address.map{ |key, value| [LOOKUPS[:destination_address][key], value] if LOOKUPS[:destination_address].include?(key) && value.present? }
Hash[ary.compact]
end
|
#build_basic_api_query(options) ⇒ Object
324
325
326
327
328
329
330
331
332
333
334
|
# File 'lib/active_fulfillment/services/amazon_mws.rb', line 324
def build_basic_api_query(options)
opts = Hash[options.map{ |k,v| [k.to_s, v.to_s] }]
opts["AWSAccessKeyId"] = @options[:login] unless opts["AWSAccessKey"]
opts["Timestamp"] = Time.now.utc.iso8601 unless opts["Timestamp"]
opts["Version"] = VERSION unless opts["Version"]
opts["SignatureMethod"] = "Hmac#{SIGNATURE_METHOD}" unless opts["SignatureMethod"]
opts["SignatureVersion"] = SIGNATURE_VERSION unless opts["SignatureVersion"]
opts["SellerId"] = @seller_id unless opts["SellerId"] || !@seller_id
opts["MWSAuthToken"] = @mws_auth_token unless opts["MWSAuthToken"] || !@mws_auth_token
opts
end
|
#build_fulfillment_request(order_id, shipping_address, line_items, options) ⇒ Object
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
|
# File 'lib/active_fulfillment/services/amazon_mws.rb', line 336
def build_fulfillment_request(order_id, shipping_address, line_items, options)
params = {
:Action => 'CreateFulfillmentOrder',
:SellerFulfillmentOrderId => order_id.to_s,
:DisplayableOrderId => order_id.to_s,
:DisplayableOrderDateTime => options[:order_date].utc.iso8601,
:ShippingSpeedCategory => options[:shipping_method]
}
params[:DisplayableOrderComment] = options[:comment] if options[:comment]
request = build_basic_api_query(params.merge(options))
request = request.merge build_address(shipping_address)
request = request.merge build_items(line_items)
request
end
|
#build_full_query(verb, uri, params) ⇒ Object
164
165
166
167
|
# File 'lib/active_fulfillment/services/amazon_mws.rb', line 164
def build_full_query(verb, uri, params)
signature = sign(verb, uri, params)
build_query(params) + "&Signature=#{signature}"
end
|
#build_get_current_fulfillment_orders_request(options = {}) ⇒ Object
353
354
355
356
357
358
359
360
361
|
# File 'lib/active_fulfillment/services/amazon_mws.rb', line 353
def build_get_current_fulfillment_orders_request(options = {})
start_time = options.delete(:start_time) || 1.day.ago.utc
params = {
:Action => 'ListAllFulfillmentOrders',
:QueryStartDateTime => start_time.strftime("%Y-%m-%dT%H:%M:%SZ")
}
build_basic_api_query(params.merge(options))
end
|
316
317
318
319
320
321
322
|
# File 'lib/active_fulfillment/services/amazon_mws.rb', line 316
def (querystr)
{
'User-Agent' => APPLICATION_IDENTIFIER,
'Content-MD5' => md5_content(querystr),
'Content-Type' => 'application/x-www-form-urlencoded'
}
end
|
#build_inventory_list_request(options = {}) ⇒ Object
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
|
# File 'lib/active_fulfillment/services/amazon_mws.rb', line 363
def build_inventory_list_request(options = {})
response_group = options.delete(:response_group) || "Basic"
params = {
:Action => 'ListInventorySupply',
:ResponseGroup => response_group
}
if skus = options.delete(:skus)
skus.each_with_index do |sku, index|
params[LOOKUPS[:list_inventory][:sku] % (index + 1)] = sku
end
else
start_time = options.delete(:start_time) || 1.day.ago
params[:QueryStartDateTime] = start_time.utc.iso8601
end
build_basic_api_query(params.merge(options))
end
|
#build_items(line_items) ⇒ Object
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
|
# File 'lib/active_fulfillment/services/amazon_mws.rb', line 406
def build_items(line_items)
lookup = LOOKUPS[:line_items]
counter = 0
line_items.reduce({}) do |items, line_item|
counter += 1
lookup.each do |key, value|
entry = value % counter
case key
when :sku
items[entry] = line_item[:sku] || "SKU-#{counter}"
when :order_id
items[entry] = line_item[:sku] || "FULFILLMENT-ITEM-ID-#{counter}"
when :quantity
items[entry] = line_item[:quantity] || 1
else
items[entry] = line_item[key] if line_item.include? key
end
end
items
end
end
|
#build_next_inventory_list_request(token) ⇒ Object
381
382
383
384
385
386
387
388
|
# File 'lib/active_fulfillment/services/amazon_mws.rb', line 381
def build_next_inventory_list_request(token)
params = {
:NextToken => token,
:Action => 'ListInventorySupplyByNextToken'
}
build_basic_api_query(params)
end
|
#build_query(query_params) ⇒ Object
312
313
314
|
# File 'lib/active_fulfillment/services/amazon_mws.rb', line 312
def build_query(query_params)
query_params.sort.map{ |key, value| [escape(key.to_s), escape(value.to_s)].join('=') }.join('&')
end
|
#build_tracking_request(order_id, options) ⇒ Object
390
391
392
393
394
|
# File 'lib/active_fulfillment/services/amazon_mws.rb', line 390
def build_tracking_request(order_id, options)
params = {:Action => 'GetFulfillmentOrder', :SellerFulfillmentOrderId => order_id}
build_basic_api_query(params.merge(options))
end
|
#commit(verb, action, params) ⇒ Object
169
170
171
172
173
174
175
176
177
178
179
180
181
|
# File 'lib/active_fulfillment/services/amazon_mws.rb', line 169
def commit(verb, action, params)
uri = URI.parse("https://#{endpoint}/#{action}/#{VERSION}")
query = build_full_query(verb, uri, params)
= (query)
log_query = query.dup
[@options[:login], @options[:app_id], @mws_auth_token].each { |key| log_query.gsub!(key.to_s, '[filtered]') if key.present? }
logger.info "[#{self.class}][#{action}] query=#{log_query}"
data = ssl_post(uri.to_s, query, )
log_data = truncate_long_response(data)
logger.info "[#{self.class}][#{action}] response=#{log_data}"
data
end
|
#endpoint ⇒ Object
85
86
87
|
# File 'lib/active_fulfillment/services/amazon_mws.rb', line 85
def endpoint
ENDPOINTS[@options[:endpoint] || :us]
end
|
#escape(str) ⇒ Object
428
429
430
|
# File 'lib/active_fulfillment/services/amazon_mws.rb', line 428
def escape(str)
CGI.escape(str.to_s).gsub('+', '%20')
end
|
#fetch_current_orders ⇒ Object
104
105
106
107
108
109
|
# File 'lib/active_fulfillment/services/amazon_mws.rb', line 104
def fetch_current_orders
with_error_handling do
data = commit :post, 'FulfillmentOutboundShipment', build_get_current_fulfillment_orders_request
parse_tracking_response(parse_document(data))
end
end
|
#fetch_stock_levels(options = {}) ⇒ Object
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
|
# File 'lib/active_fulfillment/services/amazon_mws.rb', line 111
def fetch_stock_levels(options = {})
options[:skus] = [options.delete(:sku)] if options.include?(:sku)
max_retries = options[:max_retries] || 0
response = with_error_handling(max_retries) do
data = commit :post, 'FulfillmentInventory', build_inventory_list_request(options)
parse_inventory_response(parse_document(data))
end
while token = response.params['next_token'] do
next_page = with_error_handling(max_retries) do
data = commit :post, 'FulfillmentInventory', build_next_inventory_list_request(token)
parse_inventory_response(parse_document(data))
end
return next_page if next_page.params['response_status'] != SUCCESS
next_page.stock_levels.merge!(response.stock_levels)
response = next_page
end
response
end
|
#fetch_tracking_data(order_ids, options = {}) ⇒ Object
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
|
# File 'lib/active_fulfillment/services/amazon_mws.rb', line 134
def fetch_tracking_data(order_ids, options = {})
index = 0
order_ids.reduce(nil) do |previous, order_id|
index += 1
response = with_error_handling do
data = commit :post, 'FulfillmentOutboundShipment', build_tracking_request(order_id, options)
parse_tracking_response(parse_document(data))
end
return response if !response.success?
if previous
sleep_for_throttle_options(options[:throttle], index)
response.tracking_numbers.merge!(previous.tracking_numbers)
response.tracking_companies.merge!(previous.tracking_companies)
response.tracking_urls.merge!(previous.tracking_urls)
end
response
end
end
|
#fulfill(order_id, shipping_address, line_items, options = {}) ⇒ Object
89
90
91
92
93
94
95
|
# File 'lib/active_fulfillment/services/amazon_mws.rb', line 89
def fulfill(order_id, shipping_address, line_items, options = {})
requires!(options, :order_date, :shipping_method)
with_error_handling do
data = commit :post, 'FulfillmentOutboundShipment', build_fulfillment_request(order_id, shipping_address, line_items, options)
parse_fulfillment_response('Successfully submitted the order')
end
end
|
#handle_error(e) ⇒ Object
183
184
185
186
187
188
189
190
191
|
# File 'lib/active_fulfillment/services/amazon_mws.rb', line 183
def handle_error(e)
logger.info "[#{self.class}][ResponseError] response=#{e.response.try(:body)}, message=#{e.message}"
response = parse_error(e.response)
if response.fetch(:faultstring, "").match(/^Requested order \'.+\' not found$/)
Response.new(true, nil, {:status => SUCCESS, :tracking_numbers => {}, :tracking_companies => {}, :tracking_urls => {}})
else
Response.new(false, message_from(response), response)
end
end
|
#md5_content(content) ⇒ Object
308
309
310
|
# File 'lib/active_fulfillment/services/amazon_mws.rb', line 308
def md5_content(content)
Base64.encode64(OpenSSL::Digest.new('md5', content).digest).chomp
end
|
#message_from(response) ⇒ Object
197
198
199
|
# File 'lib/active_fulfillment/services/amazon_mws.rb', line 197
def message_from(response)
response[:response_message]
end
|
#parse_document(xml) ⇒ Object
203
204
205
206
207
208
209
|
# File 'lib/active_fulfillment/services/amazon_mws.rb', line 203
def parse_document(xml)
begin
document = Nokogiri::XML(xml)
rescue Nokogiri::XML::SyntaxError
return XML_FAILURE_RESPONSE
end
end
|
#parse_error(http_response) ⇒ Object
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
|
# File 'lib/active_fulfillment/services/amazon_mws.rb', line 254
def parse_error(http_response)
response = {
http_code: http_response.code,
http_message: http_response.message
}
document = Nokogiri::XML(http_response.body)
node = document.at_css('Error'.freeze)
error_code = node.at_css('Code'.freeze)
error_message = node.at_css('Message'.freeze)
response[:status] = FAILURE
response[:faultcode] = error_code ? error_code.text : ""
response[:faultstring] = error_message ? error_message.text : ""
response[:response_message] = error_message ? error_message.text : ""
response[:response_comment] = "#{response[:faultcode]}: #{response[:faultstring]}"
response
rescue Nokogiri::XML::SyntaxError => e
rescue NoMethodError => e
response[:http_body] = http_response.body
response[:response_status] = FAILURE
response[:response_comment] = "#{response[:http_code]}: #{response[:http_message]}"
response
end
|
#parse_fulfillment_response(message) ⇒ Object
234
235
236
|
# File 'lib/active_fulfillment/services/amazon_mws.rb', line 234
def parse_fulfillment_response(message)
Response.new(true, message, { :response_status => SUCCESS, :response_comment => message })
end
|
#parse_inventory_response(document) ⇒ Object
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
|
# File 'lib/active_fulfillment/services/amazon_mws.rb', line 238
def parse_inventory_response(document)
response = { stock_levels: {} }
document.css('InventorySupplyList > member'.freeze).each do |node|
params = node.elements.to_a.each_with_object({}) { |elem, hash| hash[elem.name] = elem.text }
response[:stock_levels][params['SellerSKU']] = params['InStockSupplyQuantity'].to_i
end
next_token = document.at_css('NextToken'.freeze)
response[:next_token] = next_token ? next_token.text : nil
response[:response_status] = SUCCESS
Response.new(success?(response), message_from(response), response)
end
|
#parse_tracking_response(document) ⇒ Object
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
|
# File 'lib/active_fulfillment/services/amazon_mws.rb', line 211
def parse_tracking_response(document)
response = {
tracking_numbers: {},
tracking_companies: {},
tracking_urls: {}
}
tracking_numbers = document.css('FulfillmentShipmentPackage > member > TrackingNumber'.freeze)
if tracking_numbers.present?
order_id = document.at_css('FulfillmentOrder > SellerFulfillmentOrderId'.freeze).text.strip
response[:tracking_numbers][order_id] = tracking_numbers.map{ |t| t.text.strip }
end
tracking_companies = document.css('FulfillmentShipmentPackage > member > CarrierCode'.freeze)
if tracking_companies.present?
order_id = document.at_css('FulfillmentOrder > SellerFulfillmentOrderId'.freeze).text.strip
response[:tracking_companies][order_id] = tracking_companies.map{ |t| t.text.strip }
end
response[:response_status] = SUCCESS
Response.new(success?(response), message_from(response), response)
end
|
#registration_url(options) ⇒ Object
296
297
298
299
300
301
302
303
304
305
306
|
# File 'lib/active_fulfillment/services/amazon_mws.rb', line 296
def registration_url(options)
opts = {
"returnPathAndParameters" => options["returnPathAndParameters"],
"id" => @options[:app_id],
"AWSAccessKeyId" => @options[:login],
"SignatureMethod" => "Hmac#{SIGNATURE_METHOD}",
"SignatureVersion" => SIGNATURE_VERSION
}
signature = sign(:get, REGISTRATION_URI, opts)
"#{REGISTRATION_URI.to_s}?#{build_query(opts)}&Signature=#{signature}"
end
|
#seller_id=(seller_id) ⇒ Object
81
82
83
|
# File 'lib/active_fulfillment/services/amazon_mws.rb', line 81
def seller_id=(seller_id)
@seller_id = seller_id
end
|
#sign(http_verb, uri, options) ⇒ Object
279
280
281
282
283
284
285
286
287
|
# File 'lib/active_fulfillment/services/amazon_mws.rb', line 279
def sign(http_verb, uri, options)
string_to_sign = "#{http_verb.to_s.upcase}\n"
string_to_sign += "#{uri.host}\n"
string_to_sign += uri.path.length <= 0 ? "/\n" : "#{uri.path}\n"
string_to_sign += build_query(options)
escape(Base64.encode64(OpenSSL::HMAC.digest(SIGNATURE_METHOD, @options[:password], string_to_sign)).chomp)
end
|
#status ⇒ Object
97
98
99
100
101
102
|
# File 'lib/active_fulfillment/services/amazon_mws.rb', line 97
def status
with_error_handling do
data = commit :post, 'FulfillmentOutboundShipment', build_basic_api_query({ :Action => 'GetServiceStatus' })
parse_tracking_response(parse_document(data))
end
end
|
#success?(response) ⇒ Boolean
193
194
195
|
# File 'lib/active_fulfillment/services/amazon_mws.rb', line 193
def success?(response)
response[:response_status] == SUCCESS
end
|
#test_mode? ⇒ Boolean
160
161
162
|
# File 'lib/active_fulfillment/services/amazon_mws.rb', line 160
def test_mode?
false
end
|
#valid_credentials? ⇒ Boolean
156
157
158
|
# File 'lib/active_fulfillment/services/amazon_mws.rb', line 156
def valid_credentials?
fetch_stock_levels.success?
end
|