Class: ActiveMerchant::Fulfillment::AmazonMarketplaceWebService
- 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
-
.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?(signed_string, expected_signature) ⇒ 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_status_request ⇒ Object
- #build_tracking_request(order_id, options) ⇒ Object
- #commit(verb, service, op, 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_error(http_response) ⇒ Object
- #parse_fulfillment_response(op, document) ⇒ Object
- #parse_inventory_response(document) ⇒ Object
-
#parse_response(service, op, xml) ⇒ Object
PARSING.
- #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
#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( = {}) requires!(, :login, :password) @seller_id = [:seller_id] 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
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
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() opts = Hash[.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, ) params = { :Action => OPERATIONS[:outbound][:create], :SellerFulfillmentOrderId => order_id.to_s, :DisplayableOrderId => order_id.to_s, :DisplayableOrderDateTime => [:order_date].utc.iso8601, :ShippingSpeedCategory => [:shipping_method] } params[:DisplayableOrderComment] = [:comment] if [:comment] request = build_basic_api_query(params.merge()) 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( = {}) start_time = .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()) 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( = {}) response_group = .delete(:response_group) || "Basic" params = { :Action => OPERATIONS[:inventory][:list], :ResponseGroup => response_group } if skus = .delete(:skus) skus.each_with_index do |sku, index| params[LOOKUPS[:list_inventory][:sku] % (index + 1)] = sku end else start_time = .delete(:start_time) || 1.day.ago params[:QueryStartDateTime] = start_time.utc.iso8601 end build_basic_api_query(params.merge()) 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_request ⇒ Object
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, ) params = {:Action => OPERATIONS[:outbound][:tracking], :SellerFulfillmentOrderId => order_id} build_basic_api_query(params.merge()) 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), (response), response) rescue ActiveMerchant::ResponseError => e handle_error(e) end |
#endpoint ⇒ Object
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_orders ⇒ Object
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( = {}) [:skus] = [.delete(:sku)] if .include?(:sku) response = commit :post, :inventory, :list, build_inventory_list_request() 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, = {}) order_ids.reduce(nil) do |previous, order_id| response = commit :post, :outbound, :tracking, build_tracking_request(order_id, ) 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, = {}) requires!(, :order_date, :shipping_method) commit :post, :outbound, :create, build_fulfillment_request(order_id, shipping_address, line_items, ) 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, (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 (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. document = REXML::Document.new(http_response.body) node = REXML::XPath.first(document, '//Error') error_code = REXML::XPath.first(node, '//Code') = REXML::XPath.first(node, '//Message') response[:status] = FAILURE response[:faultcode] = error_code ? error_code.text : "" response[:faultstring] = ? .text : "" response[:response_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() opts = { "returnPathAndParameters" => ["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, ) 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() # remove trailing newline created by encode64 escape(Base64.encode64(OpenSSL::HMAC.digest(SIGNATURE_METHOD, @options[:password], string_to_sign)).chomp) end |
#status ⇒ Object
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
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
170 171 172 |
# File 'lib/active_fulfillment/fulfillment/services/amazon_mws.rb', line 170 def test_mode? false end |
#valid_credentials? ⇒ Boolean
166 167 168 |
# File 'lib/active_fulfillment/fulfillment/services/amazon_mws.rb', line 166 def valid_credentials? fetch_stock_levels.success? end |