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

#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)


297
298
299
300
# File 'lib/active_fulfillment/fulfillment/services/amazon_mws.rb', line 297

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



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

def build_address(address)
  requires!(address, :name, :address1, :city, :state, :country, :zip)
  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



330
331
332
333
334
335
336
337
338
339
# File 'lib/active_fulfillment/fulfillment/services/amazon_mws.rb', line 330

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



341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
# File 'lib/active_fulfillment/fulfillment/services/amazon_mws.rb', line 341

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



177
178
179
180
# File 'lib/active_fulfillment/fulfillment/services/amazon_mws.rb', line 177

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



358
359
360
361
362
363
364
365
366
# File 'lib/active_fulfillment/fulfillment/services/amazon_mws.rb', line 358

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



322
323
324
325
326
327
328
# File 'lib/active_fulfillment/fulfillment/services/amazon_mws.rb', line 322

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



368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
# File 'lib/active_fulfillment/fulfillment/services/amazon_mws.rb', line 368

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



407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
# File 'lib/active_fulfillment/fulfillment/services/amazon_mws.rb', line 407

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



386
387
388
389
390
391
392
393
# File 'lib/active_fulfillment/fulfillment/services/amazon_mws.rb', line 386

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



318
319
320
# File 'lib/active_fulfillment/fulfillment/services/amazon_mws.rb', line 318

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



429
430
431
# File 'lib/active_fulfillment/fulfillment/services/amazon_mws.rb', line 429

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

#build_tracking_request(order_id, options) ⇒ Object



395
396
397
398
399
# File 'lib/active_fulfillment/fulfillment/services/amazon_mws.rb', line 395

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



182
183
184
185
186
187
188
189
190
191
192
193
# File 'lib/active_fulfillment/fulfillment/services/amazon_mws.rb', line 182

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
  response = parse_error(e.response)
  Response.new(false, message_from(response), response)
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



433
434
435
# File 'lib/active_fulfillment/fulfillment/services/amazon_mws.rb', line 433

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
# 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)

    next_page.stock_levels.merge!(response.stock_levels)
    response = next_page
  end

  response
end

#fetch_tracking_numbers(order_ids, options = {}) ⇒ Object



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

def fetch_tracking_numbers(order_ids, options = {})
  order_ids.reduce(nil) do |previous, order_id|
    response = commit :post, :outbound, :tracking, build_tracking_request(order_id, options)

    if !response.success?
      if response.faultstring.match(/^Requested order \'.+\' not found$/)
        response = Response.new(true, nil, {
                                  :status => SUCCESS,
                                  :tracking_numbers => {}
                                })
      else
        return response
      end
    end

    response.tracking_numbers.merge!(previous.tracking_numbers) if previous
    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

#md5_content(content) ⇒ Object



314
315
316
# File 'lib/active_fulfillment/fulfillment/services/amazon_mws.rb', line 314

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

#message_from(response) ⇒ Object



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

def message_from(response)
  response[:response_message]
end

#parse_error(http_response) ⇒ Object



262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
# File 'lib/active_fulfillment/fulfillment/services/amazon_mws.rb', line 262

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



241
242
243
# File 'lib/active_fulfillment/fulfillment/services/amazon_mws.rb', line 241

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

#parse_inventory_response(document) ⇒ Object



245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
# File 'lib/active_fulfillment/fulfillment/services/amazon_mws.rb', line 245

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



205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
# File 'lib/active_fulfillment/fulfillment/services/amazon_mws.rb', line 205

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



227
228
229
230
231
232
233
234
235
236
237
238
239
# File 'lib/active_fulfillment/fulfillment/services/amazon_mws.rb', line 227

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

  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

  response[:response_status] = SUCCESS
  response
end

#registration_url(options) ⇒ Object



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

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



287
288
289
290
291
292
293
294
295
# File 'lib/active_fulfillment/fulfillment/services/amazon_mws.rb', line 287

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)


195
196
197
# File 'lib/active_fulfillment/fulfillment/services/amazon_mws.rb', line 195

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

#test_mode?Boolean

Returns:

  • (Boolean)


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

def test_mode?
  false
end

#valid_credentials?Boolean

Returns:

  • (Boolean)


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

def valid_credentials?
  fetch_stock_levels.success?
end