Class: ActiveMerchant::Fulfillment::AmazonMarketplaceWebService

Inherits:
Service
  • Object
show all
Defined in:
lib/active_fulfillment/fulfillment/services/amazon_mws.rb

Constant Summary collapse

APPLICATION_IDENTIFIER =
"active_merchant_mws/0.01 (Language=ruby)"
REGISTRATION_URI =
URI.parse("https://sellercentral.amazon.com/gp/mws/registration/register.html")
SIGNATURE_VERSION =
2
SIGNATURE_METHOD =
"SHA256"
VERSION =
"2010-10-01"
MESSAGES =
{
  :status => {
    'Accepted' => 'Success',
    'Failure'  => 'Failed',
    'Error'    => 'An error occurred'
  },
  :create => {
    'Accepted' => 'Successfully submitted the order',
    'Failure'  => 'Failed to submit the order',
    'Error'    => 'An error occurred while submitting the order'
  },
  :list   => {
    'Accepted' => 'Successfully submitted request',
    'Failure'  => 'Failed to submit request',
    'Error'    => 'An error occurred while submitting request'

  }
}
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'
}
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"
  }
}
ACTIONS =
{
  :outbound => "FulfillmentOutboundShipment",
  :inventory => "FulfillmentInventory"
}
OPERATIONS =
{
  :outbound => {
    :status => 'GetServiceStatus',
    :create => 'CreateFulfillmentOrder',
    :list   => 'ListAllFulfillmentOrders',
    :tracking => 'GetFulfillmentOrder'
  },
  :inventory => {
    :get  => 'ListInventorySupply',
    :list => 'ListInventorySupply',
    :list_next => 'ListInventorySupplyByNextToken'
  }
}

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.



108
109
110
111
112
# File 'lib/active_fulfillment/fulfillment/services/amazon_mws.rb', line 108

def initialize(options = {})
  requires!(options, :login, :password)
  @seller_id = options[:seller_id]
  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



100
101
102
103
104
105
106
# File 'lib/active_fulfillment/fulfillment/services/amazon_mws.rb', line 100

def self.shipping_methods
  [
    [ 'Standard Shipping', 'Standard' ],
    [ 'Expedited Shipping', 'Expedited' ],
    [ 'Priority Shipping', 'Priority' ]
  ].inject(ActiveSupport::OrderedHash.new){|h, (k,v)| h[k] = v; h}
end

Instance Method Details

#amazon_request?(signed_string, expected_signature) ⇒ Boolean

Returns:

  • (Boolean)


310
311
312
313
# File 'lib/active_fulfillment/fulfillment/services/amazon_mws.rb', line 310

def amazon_request?(signed_string, expected_signature)
  calculated_signature = escape(Base64.encode64(OpenSSL::HMAC.digest(SIGNATURE_METHOD, @options[:password], signed_string)).chomp)
  calculated_signature == expected_signature
end

#build_address(address) ⇒ Object



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

def build_address(address)
  requires!(address, :name, :address1, :city, :country, :zip)
  address[:state] ||= "N/A"
  address[:zip].upcase!
  address[:name] = "#{address[:company]} - #{address[:name]}" if address[:company].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



343
344
345
346
347
348
349
350
351
352
# File 'lib/active_fulfillment/fulfillment/services/amazon_mws.rb', line 343

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
end

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



354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
# File 'lib/active_fulfillment/fulfillment/services/amazon_mws.rb', line 354

def build_fulfillment_request(order_id, shipping_address, line_items, options)
  params = {
    :Action => OPERATIONS[:outbound][:create],
    :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



174
175
176
177
# File 'lib/active_fulfillment/fulfillment/services/amazon_mws.rb', line 174

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



371
372
373
374
375
376
377
378
379
# File 'lib/active_fulfillment/fulfillment/services/amazon_mws.rb', line 371

def build_get_current_fulfillment_orders_request(options = {})
  start_time = options.delete(:start_time) || 1.day.ago.utc
  params = {
    :Action => OPERATIONS[:outbound][:list],
    :QueryStartDateTime => start_time.strftime("%Y-%m-%dT%H:%M:%SZ")
  }

  build_basic_api_query(params.merge(options))
end

#build_headers(querystr) ⇒ Object



335
336
337
338
339
340
341
# File 'lib/active_fulfillment/fulfillment/services/amazon_mws.rb', line 335

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



381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
# File 'lib/active_fulfillment/fulfillment/services/amazon_mws.rb', line 381

def build_inventory_list_request(options = {})
  response_group = options.delete(:response_group) || "Basic"
  params = {
    :Action => OPERATIONS[:inventory][:list],
    :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



423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
# File 'lib/active_fulfillment/fulfillment/services/amazon_mws.rb', line 423

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



399
400
401
402
403
404
405
406
# File 'lib/active_fulfillment/fulfillment/services/amazon_mws.rb', line 399

def build_next_inventory_list_request(token)
  params = {
    :NextToken => token,
    :Action => OPERATIONS[:inventory][:list_next]
  }

  build_basic_api_query(params)
end

#build_query(query_params) ⇒ Object



331
332
333
# File 'lib/active_fulfillment/fulfillment/services/amazon_mws.rb', line 331

def build_query(query_params)
  query_params.sort.map{ |key, value| [escape(key.to_s), escape(value.to_s)].join('=') }.join('&')
end

#build_status_requestObject



445
446
447
# File 'lib/active_fulfillment/fulfillment/services/amazon_mws.rb', line 445

def build_status_request
  build_basic_api_query({ :Action => OPERATIONS[:outbound][:status] })
end

#build_tracking_request(order_id, options) ⇒ Object



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

def build_tracking_request(order_id, options)
  params = {:Action => OPERATIONS[:outbound][:tracking], :SellerFulfillmentOrderId => order_id}

  build_basic_api_query(params.merge(options))
end

#commit(verb, service, op, params) ⇒ Object



179
180
181
182
183
184
185
186
187
188
189
# File 'lib/active_fulfillment/fulfillment/services/amazon_mws.rb', line 179

def commit(verb, service, op, params)
  uri = URI.parse("https://#{endpoint}/#{ACTIONS[service]}/#{VERSION}")
  query = build_full_query(verb, uri, params)
  headers = build_headers(query)

  data = ssl_post(uri.to_s, query, headers)
  response = parse_response(service, op, data)
  Response.new(success?(response), message_from(response), response)
rescue ActiveMerchant::ResponseError => e
  handle_error(e)
end

#endpointObject



118
119
120
# File 'lib/active_fulfillment/fulfillment/services/amazon_mws.rb', line 118

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

#escape(str) ⇒ Object



449
450
451
# File 'lib/active_fulfillment/fulfillment/services/amazon_mws.rb', line 449

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

#fetch_current_ordersObject



131
132
133
# File 'lib/active_fulfillment/fulfillment/services/amazon_mws.rb', line 131

def fetch_current_orders
  commit :post, :outbound, :status, build_get_current_fulfillment_orders_request
end

#fetch_stock_levels(options = {}) ⇒ Object



135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
# File 'lib/active_fulfillment/fulfillment/services/amazon_mws.rb', line 135

def fetch_stock_levels(options = {})
  options[:skus] = [options.delete(:sku)] if options.include?(:sku)
  response = commit :post, :inventory, :list, build_inventory_list_request(options)

  while token = response.params['next_token'] do
    next_page = commit :post, :inventory, :list_next, build_next_inventory_list_request(token)

    # 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



151
152
153
154
155
156
157
158
159
160
161
162
163
164
# File 'lib/active_fulfillment/fulfillment/services/amazon_mws.rb', line 151

def fetch_tracking_data(order_ids, options = {})
  order_ids.reduce(nil) do |previous, order_id|
  response = commit :post, :outbound, :tracking, build_tracking_request(order_id, options)
  return response if !response.success?

  if previous
    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



122
123
124
125
# File 'lib/active_fulfillment/fulfillment/services/amazon_mws.rb', line 122

def fulfill(order_id, shipping_address, line_items, options = {})
  requires!(options, :order_date, :shipping_method)
  commit :post, :outbound, :create, build_fulfillment_request(order_id, shipping_address, line_items, options)
end

#handle_error(e) ⇒ Object



191
192
193
194
195
196
197
198
# File 'lib/active_fulfillment/fulfillment/services/amazon_mws.rb', line 191

def handle_error(e)
  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



327
328
329
# File 'lib/active_fulfillment/fulfillment/services/amazon_mws.rb', line 327

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

#message_from(response) ⇒ Object



204
205
206
# File 'lib/active_fulfillment/fulfillment/services/amazon_mws.rb', line 204

def message_from(response)
  response[:response_message]
end

#parse_error(http_response) ⇒ Object



275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
# File 'lib/active_fulfillment/fulfillment/services/amazon_mws.rb', line 275

def parse_error(http_response)
  response = {}
  response[:http_code] = http_response.code
  response[:http_message] = http_response.message

  document = REXML::Document.new(http_response.body)

  node = REXML::XPath.first(document, '//Error')
  error_code = REXML::XPath.first(node, '//Code')
  error_message = REXML::XPath.first(node, '//Message')

  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 REXML::ParseException => 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(op, document) ⇒ Object



254
255
256
# File 'lib/active_fulfillment/fulfillment/services/amazon_mws.rb', line 254

def parse_fulfillment_response(op, document)
  { :response_status => SUCCESS, :response_comment => MESSAGES[op][SUCCESS] }
end

#parse_inventory_response(document) ⇒ Object



258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
# File 'lib/active_fulfillment/fulfillment/services/amazon_mws.rb', line 258

def parse_inventory_response(document)
  response = {}
  response[:stock_levels] = {}

  document.each_element('//InventorySupplyList/member') 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 = REXML::XPath.first(document, '//NextToken')
  response[:next_token] = next_token ? next_token.text : nil

  response[:response_status] = SUCCESS
  response
end

#parse_response(service, op, xml) ⇒ Object

PARSING



210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
# File 'lib/active_fulfillment/fulfillment/services/amazon_mws.rb', line 210

def parse_response(service, op, xml)
  begin
    document = REXML::Document.new(xml)
  rescue REXML::ParseException
    return { :success => FAILURE }
  end

  case service
  when :outbound
    case op
    when :tracking
      parse_tracking_response(document)
    else
      parse_fulfillment_response(op, document)
    end
  when :inventory
    parse_inventory_response(document)
  else
    raise ArgumentError, "Unknown service #{service}"
  end
end

#parse_tracking_response(document) ⇒ Object



232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
# File 'lib/active_fulfillment/fulfillment/services/amazon_mws.rb', line 232

def parse_tracking_response(document)
  response = {}
  response[:tracking_numbers] = {}
  response[:tracking_companies] = {}
  response[:tracking_urls] = {}

  tracking_numbers = REXML::XPath.match(document, "//FulfillmentShipmentPackage/member/TrackingNumber")
  if tracking_numbers.present?
    order_id = REXML::XPath.first(document, "//FulfillmentOrder/SellerFulfillmentOrderId").text.strip
    response[:tracking_numbers][order_id] = tracking_numbers.map{ |t| t.text.strip }
  end

  tracking_companies = REXML::XPath.match(document, "//FulfillmentShipmentPackage/member/CarrierCode")
  if tracking_companies.present?
    order_id = REXML::XPath.first(document, "//FulfillmentOrder/SellerFulfillmentOrderId").text.strip
    response[:tracking_companies][order_id] = tracking_companies.map{ |t| t.text.strip }
  end

  response[:response_status] = SUCCESS
  response
end

#registration_url(options) ⇒ Object



315
316
317
318
319
320
321
322
323
324
325
# File 'lib/active_fulfillment/fulfillment/services/amazon_mws.rb', line 315

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



114
115
116
# File 'lib/active_fulfillment/fulfillment/services/amazon_mws.rb', line 114

def seller_id=(seller_id)
  @seller_id = seller_id
end

#sign(http_verb, uri, options) ⇒ Object



300
301
302
303
304
305
306
307
308
# File 'lib/active_fulfillment/fulfillment/services/amazon_mws.rb', line 300

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



127
128
129
# File 'lib/active_fulfillment/fulfillment/services/amazon_mws.rb', line 127

def status
  commit :post, :outbound, :status, build_status_request
end

#success?(response) ⇒ Boolean

Returns:

  • (Boolean)


200
201
202
# File 'lib/active_fulfillment/fulfillment/services/amazon_mws.rb', line 200

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

#test_mode?Boolean

Returns:

  • (Boolean)


170
171
172
# File 'lib/active_fulfillment/fulfillment/services/amazon_mws.rb', line 170

def test_mode?
  false
end

#valid_credentials?Boolean

Returns:

  • (Boolean)


166
167
168
# File 'lib/active_fulfillment/fulfillment/services/amazon_mws.rb', line 166

def valid_credentials?
  fetch_stock_levels.success?
end