Class: ActiveFulfillment::AmazonMarketplaceWebService

Inherits:
Service
  • Object
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 =
{
  :au => 'mws.amazonservices.com.au',
  :br => 'mws.amazonservices.com',
  :ca => 'mws.amazonservices.ca',
  :cn => 'mws.amazonservices.com.cn',
  :de => 'mws-eu.amazonservices.com',
  :es => 'mws-eu.amazonservices.com',
  :fr => 'mws-eu.amazonservices.com',
  :gb => 'mws-eu.amazonservices.com',
  :in => 'mws.amazonservices.in',
  :it => 'mws-eu.amazonservices.com',
  :jp => 'mws.amazonservices.jp',
  :mx => 'mws.amazonservices.com.mx',
  :uk => 'mws-eu.amazonservices.com',
  :us => 'mws.amazonservices.com',
}.freeze
MARKETPLACE_IDS =
{
  :au => 'A39IBJ37TRP1C6',
  :br => 'A2Q3Y263D00KWC',
  :ca => 'A2EUQ1WTGCTBG2',
  :cn => 'AAHKV2X7AFYLW',
  :de => 'A1PA6795UKMFR9',
  :es => 'A1RKKUPIHCS9HS',
  :fr => 'A13V1IB3VIYZZH',
  :gb => 'A1F83G8C2ARO7P',
  :in => 'A21TJRUUN4KGV',
  :it => 'APJ6JRA9NG5V4',
  :jp => 'A1VC38T7YXB528',
  :mx => 'A1AM78C64UM0Y8',
  :uk => 'A1F83G8C2ARO7P',
  :us => 'ATVPDKIKX0DER',
}.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

Instance Method Summary collapse

Methods inherited from Service

#fetch_tracking_numbers, #test?

Constructor Details

#initialize(options = {}) ⇒ AmazonMarketplaceWebService

Returns a new instance of AmazonMarketplaceWebService.



95
96
97
98
99
100
101
# File 'lib/active_fulfillment/services/amazon_mws.rb', line 95

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_methodsObject

The first is the label, and the last is the code Standard: 3-5 business days Expedited: 2 business days Priority: 1 business day



91
92
93
# File 'lib/active_fulfillment/services/amazon_mws.rb', line 91

def self.shipping_methods
  SHIPPING_METHODS
end

Instance Method Details

#amazon_request?(http_verb, base_url, return_path_and_parameters, post_params) ⇒ Boolean

Returns:

  • (Boolean)


315
316
317
318
319
320
# File 'lib/active_fulfillment/services/amazon_mws.rb', line 315

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



423
424
425
426
427
428
429
430
431
# File 'lib/active_fulfillment/services/amazon_mws.rb', line 423

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



350
351
352
353
354
355
356
357
358
359
360
361
# File 'lib/active_fulfillment/services/amazon_mws.rb', line 350

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["MarketplaceId"] = marketplace_id
  opts
end

#build_fulfillment_request(order_id, shipping_address, line_items, options) ⇒ Object



363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
# File 'lib/active_fulfillment/services/amazon_mws.rb', line 363

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



190
191
192
193
# File 'lib/active_fulfillment/services/amazon_mws.rb', line 190

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



380
381
382
383
384
385
386
387
388
# File 'lib/active_fulfillment/services/amazon_mws.rb', line 380

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

#build_headers(querystr) ⇒ Object



342
343
344
345
346
347
348
# File 'lib/active_fulfillment/services/amazon_mws.rb', line 342

def build_headers(querystr)
  {
    'User-Agent' => APPLICATION_IDENTIFIER,
    'Content-MD5' => md5_content(querystr),
    'Content-Type' => 'application/x-www-form-urlencoded'
  }
end

#build_inventory_list_request(options = {}) ⇒ Object



390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
# File 'lib/active_fulfillment/services/amazon_mws.rb', line 390

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



433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
# File 'lib/active_fulfillment/services/amazon_mws.rb', line 433

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



408
409
410
411
412
413
414
415
# File 'lib/active_fulfillment/services/amazon_mws.rb', line 408

def build_next_inventory_list_request(token)
  params = {
    :NextToken => token,
    :Action => 'ListInventorySupplyByNextToken'
  }

  build_basic_api_query(params)
end

#build_query(query_params) ⇒ Object



338
339
340
# File 'lib/active_fulfillment/services/amazon_mws.rb', line 338

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



417
418
419
420
421
# File 'lib/active_fulfillment/services/amazon_mws.rb', line 417

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



195
196
197
198
199
200
201
202
203
204
205
206
207
# File 'lib/active_fulfillment/services/amazon_mws.rb', line 195

def commit(verb, action, params)
  uri = URI.parse("https://#{endpoint}/#{action}/#{VERSION}")
  query = build_full_query(verb, uri, params)
  headers = build_headers(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, headers)
  log_data = truncate_long_response(data)
  logger.info "[#{self.class}][#{action}] response=#{log_data}"
  data
end

#endpointObject



107
108
109
# File 'lib/active_fulfillment/services/amazon_mws.rb', line 107

def endpoint
  ENDPOINTS[@options[:endpoint] || :us]
end

#escape(str) ⇒ Object



455
456
457
# File 'lib/active_fulfillment/services/amazon_mws.rb', line 455

def escape(str)
  CGI.escape(str.to_s).gsub('+', '%20')
end

#fetch_current_ordersObject



130
131
132
133
134
135
# File 'lib/active_fulfillment/services/amazon_mws.rb', line 130

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



137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
# File 'lib/active_fulfillment/services/amazon_mws.rb', line 137

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

    # if we fail during the stock-level-via-token gathering, fail the whole request
    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



160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
# File 'lib/active_fulfillment/services/amazon_mws.rb', line 160

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



115
116
117
118
119
120
121
# File 'lib/active_fulfillment/services/amazon_mws.rb', line 115

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



209
210
211
212
213
214
215
216
217
# File 'lib/active_fulfillment/services/amazon_mws.rb', line 209

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

#marketplace_idObject



111
112
113
# File 'lib/active_fulfillment/services/amazon_mws.rb', line 111

def marketplace_id
  MARKETPLACE_IDS[@options[:endpoint] || :us]
end

#md5_content(content) ⇒ Object



334
335
336
# File 'lib/active_fulfillment/services/amazon_mws.rb', line 334

def md5_content(content)
  Base64.encode64(OpenSSL::Digest.new('md5', content).digest).chomp
end

#message_from(response) ⇒ Object



223
224
225
# File 'lib/active_fulfillment/services/amazon_mws.rb', line 223

def message_from(response)
  response[:response_message]
end

#parse_document(xml) ⇒ Object

PARSING



229
230
231
232
233
234
235
# File 'lib/active_fulfillment/services/amazon_mws.rb', line 229

def parse_document(xml)
  begin
    document = Nokogiri::XML(xml)
  rescue Nokogiri::XML::SyntaxError
    return XML_FAILURE_RESPONSE
  end
end

#parse_error(http_response) ⇒ Object



280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
# File 'lib/active_fulfillment/services/amazon_mws.rb', line 280

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



260
261
262
# File 'lib/active_fulfillment/services/amazon_mws.rb', line 260

def parse_fulfillment_response(message)
  Response.new(true, message, { :response_status => SUCCESS, :response_comment => message })
end

#parse_inventory_response(document) ⇒ Object



264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
# File 'lib/active_fulfillment/services/amazon_mws.rb', line 264

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



237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
# File 'lib/active_fulfillment/services/amazon_mws.rb', line 237

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



322
323
324
325
326
327
328
329
330
331
332
# File 'lib/active_fulfillment/services/amazon_mws.rb', line 322

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



103
104
105
# File 'lib/active_fulfillment/services/amazon_mws.rb', line 103

def seller_id=(seller_id)
  @seller_id = seller_id
end

#sign(http_verb, uri, options) ⇒ Object



305
306
307
308
309
310
311
312
313
# File 'lib/active_fulfillment/services/amazon_mws.rb', line 305

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)

  # remove trailing newline created by encode64
  escape(Base64.encode64(OpenSSL::HMAC.digest(SIGNATURE_METHOD, @options[:password], string_to_sign)).chomp)
end

#statusObject



123
124
125
126
127
128
# File 'lib/active_fulfillment/services/amazon_mws.rb', line 123

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

Returns:

  • (Boolean)


219
220
221
# File 'lib/active_fulfillment/services/amazon_mws.rb', line 219

def success?(response)
  response[:response_status] == SUCCESS
end

#test_mode?Boolean

Returns:

  • (Boolean)


186
187
188
# File 'lib/active_fulfillment/services/amazon_mws.rb', line 186

def test_mode?
  false
end

#valid_credentials?Boolean

Returns:

  • (Boolean)


182
183
184
# File 'lib/active_fulfillment/services/amazon_mws.rb', line 182

def valid_credentials?
  fetch_stock_levels.success?
end