active_fulfillment 2.1.9 → 3.0.0.pre2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +68 -0
- data/lib/active_fulfillment.rb +5 -5
- data/lib/active_fulfillment/base.rb +10 -0
- data/lib/active_fulfillment/response.rb +26 -0
- data/lib/active_fulfillment/service.rb +56 -0
- data/lib/active_fulfillment/services.rb +5 -0
- data/lib/active_fulfillment/services/amazon_mws.rb +473 -0
- data/lib/active_fulfillment/services/james_and_james.rb +122 -0
- data/lib/active_fulfillment/services/shipwire.rb +266 -0
- data/lib/active_fulfillment/services/shopify_api.rb +125 -0
- data/lib/active_fulfillment/services/webgistix.rb +334 -0
- data/lib/active_fulfillment/version.rb +4 -0
- data/test/remote/amazon_mws_test.rb +20 -17
- data/test/remote/james_and_james_test.rb +77 -0
- data/test/remote/shipwire_test.rb +25 -25
- data/test/remote/webgistix_test.rb +21 -21
- data/test/test_helper.rb +27 -52
- data/test/unit/base_test.rb +4 -4
- data/test/unit/services/amazon_mws_test.rb +56 -26
- data/test/unit/services/james_and_james_test.rb +90 -0
- data/test/unit/services/shipwire_test.rb +18 -18
- data/test/unit/services/shopify_api_test.rb +7 -20
- data/test/unit/services/webgistix_test.rb +35 -35
- metadata +32 -114
- data/CHANGELOG +0 -62
- data/lib/active_fulfillment/fulfillment/base.rb +0 -12
- data/lib/active_fulfillment/fulfillment/response.rb +0 -28
- data/lib/active_fulfillment/fulfillment/service.rb +0 -58
- data/lib/active_fulfillment/fulfillment/services.rb +0 -5
- data/lib/active_fulfillment/fulfillment/services/amazon.rb +0 -389
- data/lib/active_fulfillment/fulfillment/services/amazon_mws.rb +0 -454
- data/lib/active_fulfillment/fulfillment/services/shipwire.rb +0 -268
- data/lib/active_fulfillment/fulfillment/services/shopify_api.rb +0 -125
- data/lib/active_fulfillment/fulfillment/services/webgistix.rb +0 -338
- data/lib/active_fulfillment/fulfillment/version.rb +0 -6
- data/test/fixtures.yml +0 -16
- data/test/fixtures/xml/amazon/inventory_get_response.xml +0 -17
- data/test/fixtures/xml/amazon/inventory_list_response.xml +0 -29
- data/test/fixtures/xml/amazon/inventory_list_response_with_next_1.xml +0 -30
- data/test/fixtures/xml/amazon/inventory_list_response_with_next_2.xml +0 -29
- data/test/fixtures/xml/amazon/tracking_response_1.xml +0 -56
- data/test/fixtures/xml/amazon/tracking_response_2.xml +0 -38
- data/test/fixtures/xml/amazon/tracking_response_error.xml +0 -13
- data/test/fixtures/xml/amazon/tracking_response_not_found.xml +0 -13
- data/test/fixtures/xml/amazon_mws/fulfillment_get_fulfillment_order.xml +0 -114
- data/test/fixtures/xml/amazon_mws/fulfillment_get_fulfillment_order_2.xml +0 -90
- data/test/fixtures/xml/amazon_mws/fulfillment_get_fullfillment_order_with_multiple_tracking_numbers.xml +0 -121
- data/test/fixtures/xml/amazon_mws/fulfillment_list_all_fulfillment_orders.xml +0 -70
- data/test/fixtures/xml/amazon_mws/inventory_list_inventory_item_supply.xml +0 -32
- data/test/fixtures/xml/amazon_mws/inventory_list_inventory_supply.xml +0 -75
- data/test/fixtures/xml/amazon_mws/inventory_list_inventory_supply_by_next_token.xml +0 -38
- data/test/fixtures/xml/amazon_mws/tracking_response_error.xml +0 -9
- data/test/fixtures/xml/amazon_mws/tracking_response_not_found.xml +0 -9
- data/test/fixtures/xml/shipwire/fulfillment_failure_response.xml +0 -7
- data/test/fixtures/xml/shipwire/invalid_login_response.xml +0 -7
- data/test/fixtures/xml/shipwire/inventory_get_response.xml +0 -44
- data/test/fixtures/xml/shipwire/successful_empty_tracking_response.xml +0 -8
- data/test/fixtures/xml/shipwire/successful_live_tracking_response.xml +0 -53
- data/test/fixtures/xml/shipwire/successful_tracking_response.xml +0 -16
- data/test/fixtures/xml/shipwire/successful_tracking_response_with_tracking_urls.xml +0 -31
- data/test/fixtures/xml/webgistix/multiple_tracking_response.xml +0 -21
- data/test/fixtures/xml/webgistix/tracking_response.xml +0 -14
- data/test/remote/amazon_test.rb +0 -124
- data/test/unit/services/amazon_test.rb +0 -271
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7731ece9da5575406a9426a3224c057a7ad8d5bc
|
4
|
+
data.tar.gz: d9ae056d89696bf9329b99e20ba75aadcba1ff88
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d430e8b4b8572c5f9c223b322ea7dff5234572a4769ea192fc1d6615c265a54913e5c2a451ab4e190bc377fcd0b9ba767d6accaf772d010328d269359e2f5dd0
|
7
|
+
data.tar.gz: 3af6f439439687ce84f596108fe1cbe79d71f4d23a32ff26f523a854d7af0f0e943871610f8e4a41412a8d3a52ac54944d033e9a05228bbe9020797df346d3a5
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
# ActiveFulfillment changelog
|
2
|
+
|
3
|
+
## Unreleased
|
4
|
+
|
5
|
+
- Update dependencies
|
6
|
+
- Remove old Amazon fulfillment service (use amazon_aws instead)
|
7
|
+
- Add contributing guidelines
|
8
|
+
|
9
|
+
### Version 2.1.7
|
10
|
+
|
11
|
+
- Add shopify_api service
|
12
|
+
- Drop Ruby 1.9.3 support
|
13
|
+
|
14
|
+
### Version 2.1.0
|
15
|
+
|
16
|
+
- Added fetch_tracking_data methods which returns tracking_companies and tracking_urls if available in addition to tracking_numbers for each service
|
17
|
+
|
18
|
+
### Version 2.0.0 (Jan 5, 2013)
|
19
|
+
|
20
|
+
- API Change on tracking numbers, returns array instead of single string [csaunders]
|
21
|
+
|
22
|
+
### Version 1.0.3 (Jan 21, 2010)
|
23
|
+
|
24
|
+
- Include "pending" counts in stock levels for Shipwire [wisq]
|
25
|
+
- Add option to include pending stock in Shipwire inventory calculations [jessehk]
|
26
|
+
|
27
|
+
### Version 1.0.2 (Jan 12, 2010)
|
28
|
+
|
29
|
+
- Include "pending" counts in stock levels for Shipwire [wisq]
|
30
|
+
|
31
|
+
### Version 1.0.1 (Dec 13, 2010)
|
32
|
+
|
33
|
+
- Updated common files with changes from activemerchant [Dennis Theisen]
|
34
|
+
- Updated Webgistix USPS shipping methods (4 added, 1 removed) [Dennis Theisen]
|
35
|
+
- Changed Webgistix to treat a duplicate response as success instead of failure and to retry failed connection errors. [Dennis Theisen]
|
36
|
+
|
37
|
+
### Version 1.0.0 (July 12, 2010)
|
38
|
+
|
39
|
+
- Add inventory support to Amazon and Webgistix [wisq]
|
40
|
+
|
41
|
+
### Version 0.10.0 (July 6, 2010)
|
42
|
+
|
43
|
+
- Remove DHL from Webgistix shipping methods [Dennis Thiesen]
|
44
|
+
- Update Amazon FBA to use AWS credentials [John Tajima]
|
45
|
+
- Use new connection code from ActiveMerchant [cody]
|
46
|
+
- Add #valid_credentials? support to all fulfillment services [cody]
|
47
|
+
- Return 'Access Denied' message when Webgistix credenentials are invalid [cody]
|
48
|
+
- Update Shipwire endpoint hostname [cody]
|
49
|
+
- Add missing ISO countries [Edward Ocampo-Gooding]
|
50
|
+
- Add support for Guernsey to country.rb [cody]
|
51
|
+
- Use a Rails 2.3 compatible OrderedHash [cody]
|
52
|
+
- Use :words_connector instead of connector in RequiresParameters [cody]
|
53
|
+
- Provide Webgistix with a valid test sku to keep remote tests passing
|
54
|
+
- Update PostsData to support get requests
|
55
|
+
- Update Shipwire to latest version of dtd.
|
56
|
+
- Use real addresses for Shipwire remote fulfillment tests
|
57
|
+
- Pass Shipwire the ISO country code instead of the previous name and country combo. Always add the country element to the document
|
58
|
+
- Update Shipwire warehouses and don't send unneeded Content-Type header
|
59
|
+
- Add configurable timeouts from Active Merchant
|
60
|
+
- Shipwire: Send the company in address1 if present. Otherwise send address1 in address1.
|
61
|
+
- Always send address to Shipwire
|
62
|
+
- Map company to address1 with Shipwire
|
63
|
+
- Sync posts_data.rb with ActiveMerchant
|
64
|
+
- Add support for fetching tracking numbers to Shipwire
|
65
|
+
- Move email to the options hash. Refactor Shipwire commit method.
|
66
|
+
- Package for initial upload to Google Code
|
67
|
+
- Fix remote Webgistix test
|
68
|
+
- Add support for Fulfillment by Amazon Basic Fulfillment
|
data/lib/active_fulfillment.rb
CHANGED
@@ -40,8 +40,8 @@ require 'net/https'
|
|
40
40
|
require 'rexml/document'
|
41
41
|
require 'active_utils'
|
42
42
|
|
43
|
-
require 'active_fulfillment/
|
44
|
-
require 'active_fulfillment/
|
45
|
-
require 'active_fulfillment/
|
46
|
-
require 'active_fulfillment/
|
47
|
-
require 'active_fulfillment/
|
43
|
+
require 'active_fulfillment/version'
|
44
|
+
require 'active_fulfillment/base'
|
45
|
+
require 'active_fulfillment/response'
|
46
|
+
require 'active_fulfillment/service'
|
47
|
+
require 'active_fulfillment/services'
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module ActiveFulfillment
|
2
|
+
class Response
|
3
|
+
attr_reader :params
|
4
|
+
attr_reader :message
|
5
|
+
attr_reader :test
|
6
|
+
|
7
|
+
def success?
|
8
|
+
@success
|
9
|
+
end
|
10
|
+
|
11
|
+
def test?
|
12
|
+
@test
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize(success, message, params = {}, options = {})
|
16
|
+
@success, @message, @params = success, message, params.stringify_keys
|
17
|
+
@test = options[:test] || false
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
def method_missing(method, *args)
|
22
|
+
@params[method.to_s] || super
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module ActiveFulfillment
|
2
|
+
class Service
|
3
|
+
|
4
|
+
include ActiveUtils::RequiresParameters
|
5
|
+
include ActiveUtils::PostsData
|
6
|
+
|
7
|
+
class_attribute :logger
|
8
|
+
|
9
|
+
def initialize(options = {})
|
10
|
+
check_test_mode(options)
|
11
|
+
|
12
|
+
@options = {}
|
13
|
+
@options.update(options)
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_mode?
|
17
|
+
false
|
18
|
+
end
|
19
|
+
|
20
|
+
def test?
|
21
|
+
@options[:test] || Base.mode == :test
|
22
|
+
end
|
23
|
+
|
24
|
+
def valid_credentials?
|
25
|
+
true
|
26
|
+
end
|
27
|
+
|
28
|
+
# API Requirements for Implementors
|
29
|
+
def fulfill(order_id, shipping_address, line_items, options = {})
|
30
|
+
raise NotImplementedError.new("Subclasses must implement")
|
31
|
+
end
|
32
|
+
|
33
|
+
def fetch_stock_levels(options = {})
|
34
|
+
raise NotImplementedError.new("Subclasses must implement")
|
35
|
+
end
|
36
|
+
|
37
|
+
def fetch_tracking_numbers(order_ids, options = {})
|
38
|
+
response = fetch_tracking_data(order_ids, options)
|
39
|
+
response.params.delete('tracking_companies')
|
40
|
+
response.params.delete('tracking_urls')
|
41
|
+
response
|
42
|
+
end
|
43
|
+
|
44
|
+
def fetch_tracking_data(order_ids, options = {})
|
45
|
+
raise NotImplementedError.new("Subclasses must implement")
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def check_test_mode(options)
|
51
|
+
if options[:test] and not test_mode?
|
52
|
+
raise ArgumentError, 'Test mode is not supported by this gateway'
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,473 @@
|
|
1
|
+
require 'base64'
|
2
|
+
require 'time'
|
3
|
+
require 'cgi'
|
4
|
+
require 'active_support/core_ext/hash/except'
|
5
|
+
|
6
|
+
module ActiveFulfillment
|
7
|
+
class AmazonMarketplaceWebService < Service
|
8
|
+
|
9
|
+
APPLICATION_IDENTIFIER = "active_merchant_mws/0.01 (Language=ruby)"
|
10
|
+
|
11
|
+
REGISTRATION_URI = URI.parse("https://sellercentral.amazon.com/gp/mws/registration/register.html")
|
12
|
+
|
13
|
+
SIGNATURE_VERSION = 2
|
14
|
+
SIGNATURE_METHOD = "SHA256"
|
15
|
+
VERSION = "2010-10-01"
|
16
|
+
|
17
|
+
SUCCESS, FAILURE, ERROR = 'Accepted', 'Failure', 'Error'
|
18
|
+
|
19
|
+
MESSAGES = {
|
20
|
+
:status => {
|
21
|
+
'Accepted' => 'Success',
|
22
|
+
'Failure' => 'Failed',
|
23
|
+
'Error' => 'An error occurred'
|
24
|
+
},
|
25
|
+
:create => {
|
26
|
+
'Accepted' => 'Successfully submitted the order',
|
27
|
+
'Failure' => 'Failed to submit the order',
|
28
|
+
'Error' => 'An error occurred while submitting the order'
|
29
|
+
},
|
30
|
+
:list => {
|
31
|
+
'Accepted' => 'Successfully submitted request',
|
32
|
+
'Failure' => 'Failed to submit request',
|
33
|
+
'Error' => 'An error occurred while submitting request'
|
34
|
+
|
35
|
+
}
|
36
|
+
}
|
37
|
+
|
38
|
+
ENDPOINTS = {
|
39
|
+
:ca => 'mws.amazonservices.ca',
|
40
|
+
:cn => 'mws.amazonservices.com.cn',
|
41
|
+
:de => 'mws-eu.amazonservices.ca',
|
42
|
+
:es => 'mws-eu.amazonservices.ca',
|
43
|
+
:fr => 'mws-eu.amazonservices.ca',
|
44
|
+
:it => 'mws-eu.amazonservices.ca',
|
45
|
+
:jp => 'mws.amazonservices.jp',
|
46
|
+
:uk => 'mws-eu.amazonservices.ca',
|
47
|
+
:us => 'mws.amazonservices.com'
|
48
|
+
}
|
49
|
+
|
50
|
+
LOOKUPS = {
|
51
|
+
:destination_address => {
|
52
|
+
:name => "DestinationAddress.Name",
|
53
|
+
:address1 => "DestinationAddress.Line1",
|
54
|
+
:address2 => "DestinationAddress.Line2",
|
55
|
+
:city => "DestinationAddress.City",
|
56
|
+
:state => "DestinationAddress.StateOrProvinceCode",
|
57
|
+
:country => "DestinationAddress.CountryCode",
|
58
|
+
:zip => "DestinationAddress.PostalCode",
|
59
|
+
:phone => "DestinationAddress.PhoneNumber"
|
60
|
+
},
|
61
|
+
:line_items => {
|
62
|
+
:comment => "Items.member.%d.DisplayableComment",
|
63
|
+
:gift_message => "Items.member.%d.GiftMessage",
|
64
|
+
:currency_code => "Items.member.%d.PerUnitDeclaredValue.CurrencyCode",
|
65
|
+
:value => "Items.member.%d.PerUnitDeclaredValue.Value",
|
66
|
+
:quantity => "Items.member.%d.Quantity",
|
67
|
+
:order_id => "Items.member.%d.SellerFulfillmentOrderItemId",
|
68
|
+
:sku => "Items.member.%d.SellerSKU",
|
69
|
+
:network_sku => "Items.member.%d.FulfillmentNetworkSKU",
|
70
|
+
:item_disposition => "Items.member.%d.OrderItemDisposition",
|
71
|
+
},
|
72
|
+
:list_inventory => {
|
73
|
+
:sku => "SellerSkus.member.%d"
|
74
|
+
}
|
75
|
+
}
|
76
|
+
|
77
|
+
ACTIONS = {
|
78
|
+
:outbound => "FulfillmentOutboundShipment",
|
79
|
+
:inventory => "FulfillmentInventory"
|
80
|
+
}
|
81
|
+
|
82
|
+
OPERATIONS = {
|
83
|
+
:outbound => {
|
84
|
+
:status => 'GetServiceStatus',
|
85
|
+
:create => 'CreateFulfillmentOrder',
|
86
|
+
:list => 'ListAllFulfillmentOrders',
|
87
|
+
:tracking => 'GetFulfillmentOrder'
|
88
|
+
},
|
89
|
+
:inventory => {
|
90
|
+
:get => 'ListInventorySupply',
|
91
|
+
:list => 'ListInventorySupply',
|
92
|
+
:list_next => 'ListInventorySupplyByNextToken'
|
93
|
+
}
|
94
|
+
}
|
95
|
+
|
96
|
+
# The first is the label, and the last is the code
|
97
|
+
# Standard: 3-5 business days
|
98
|
+
# Expedited: 2 business days
|
99
|
+
# Priority: 1 business day
|
100
|
+
def self.shipping_methods
|
101
|
+
[
|
102
|
+
[ 'Standard Shipping', 'Standard' ],
|
103
|
+
[ 'Expedited Shipping', 'Expedited' ],
|
104
|
+
[ 'Priority Shipping', 'Priority' ]
|
105
|
+
].inject({}){|h, (k,v)| h[k] = v; h}
|
106
|
+
end
|
107
|
+
|
108
|
+
def initialize(options = {})
|
109
|
+
requires!(options, :login, :password)
|
110
|
+
@seller_id = options[:seller_id]
|
111
|
+
@mws_auth_token = options[:mws_auth_token]
|
112
|
+
super
|
113
|
+
end
|
114
|
+
|
115
|
+
def seller_id=(seller_id)
|
116
|
+
@seller_id = seller_id
|
117
|
+
end
|
118
|
+
|
119
|
+
def endpoint
|
120
|
+
ENDPOINTS[@options[:endpoint] || :us]
|
121
|
+
end
|
122
|
+
|
123
|
+
def fulfill(order_id, shipping_address, line_items, options = {})
|
124
|
+
requires!(options, :order_date, :shipping_method)
|
125
|
+
commit :post, :outbound, :create, build_fulfillment_request(order_id, shipping_address, line_items, options)
|
126
|
+
end
|
127
|
+
|
128
|
+
def status
|
129
|
+
commit :post, :outbound, :status, build_status_request
|
130
|
+
end
|
131
|
+
|
132
|
+
def fetch_current_orders
|
133
|
+
commit :post, :outbound, :status, build_get_current_fulfillment_orders_request
|
134
|
+
end
|
135
|
+
|
136
|
+
def fetch_stock_levels(options = {})
|
137
|
+
options[:skus] = [options.delete(:sku)] if options.include?(:sku)
|
138
|
+
response = commit :post, :inventory, :list, build_inventory_list_request(options)
|
139
|
+
|
140
|
+
while token = response.params['next_token'] do
|
141
|
+
next_page = commit :post, :inventory, :list_next, build_next_inventory_list_request(token)
|
142
|
+
|
143
|
+
# if we fail during the stock-level-via-token gathering, fail the whole request
|
144
|
+
return next_page if next_page.params['response_status'] != SUCCESS
|
145
|
+
next_page.stock_levels.merge!(response.stock_levels)
|
146
|
+
response = next_page
|
147
|
+
end
|
148
|
+
|
149
|
+
response
|
150
|
+
end
|
151
|
+
|
152
|
+
def fetch_tracking_data(order_ids, options = {})
|
153
|
+
order_ids.reduce(nil) do |previous, order_id|
|
154
|
+
response = commit :post, :outbound, :tracking, build_tracking_request(order_id, options)
|
155
|
+
return response if !response.success?
|
156
|
+
|
157
|
+
if previous
|
158
|
+
response.tracking_numbers.merge!(previous.tracking_numbers)
|
159
|
+
response.tracking_companies.merge!(previous.tracking_companies)
|
160
|
+
response.tracking_urls.merge!(previous.tracking_urls)
|
161
|
+
end
|
162
|
+
|
163
|
+
response
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
def valid_credentials?
|
168
|
+
fetch_stock_levels.success?
|
169
|
+
end
|
170
|
+
|
171
|
+
def test_mode?
|
172
|
+
false
|
173
|
+
end
|
174
|
+
|
175
|
+
def build_full_query(verb, uri, params)
|
176
|
+
signature = sign(verb, uri, params)
|
177
|
+
build_query(params) + "&Signature=#{signature}"
|
178
|
+
end
|
179
|
+
|
180
|
+
def commit(verb, service, op, params)
|
181
|
+
uri = URI.parse("https://#{endpoint}/#{ACTIONS[service]}/#{VERSION}")
|
182
|
+
query = build_full_query(verb, uri, params)
|
183
|
+
headers = build_headers(query)
|
184
|
+
|
185
|
+
data = ssl_post(uri.to_s, query, headers)
|
186
|
+
if service == :inventory
|
187
|
+
logger.info "[#{self.class}][inventory] query=#{build_full_query(verb, uri, params.except('AWSAccessKeyId', 'MWSAuthToken'))} response=#{data}"
|
188
|
+
end
|
189
|
+
response = parse_response(service, op, data)
|
190
|
+
Response.new(success?(response), message_from(response), response)
|
191
|
+
rescue ActiveUtils::ResponseError => e
|
192
|
+
handle_error(e)
|
193
|
+
end
|
194
|
+
|
195
|
+
def handle_error(e)
|
196
|
+
response = parse_error(e.response)
|
197
|
+
if response.fetch(:faultstring, "").match(/^Requested order \'.+\' not found$/)
|
198
|
+
Response.new(true, nil, {:status => SUCCESS, :tracking_numbers => {}, :tracking_companies => {}, :tracking_urls => {}})
|
199
|
+
else
|
200
|
+
Response.new(false, message_from(response), response)
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
def success?(response)
|
205
|
+
response[:response_status] == SUCCESS
|
206
|
+
end
|
207
|
+
|
208
|
+
def message_from(response)
|
209
|
+
response[:response_message]
|
210
|
+
end
|
211
|
+
|
212
|
+
## PARSING
|
213
|
+
|
214
|
+
def parse_response(service, op, xml)
|
215
|
+
begin
|
216
|
+
document = REXML::Document.new(xml)
|
217
|
+
rescue REXML::ParseException
|
218
|
+
return { :success => FAILURE }
|
219
|
+
end
|
220
|
+
|
221
|
+
case service
|
222
|
+
when :outbound
|
223
|
+
case op
|
224
|
+
when :tracking
|
225
|
+
parse_tracking_response(document)
|
226
|
+
else
|
227
|
+
parse_fulfillment_response(op, document)
|
228
|
+
end
|
229
|
+
when :inventory
|
230
|
+
parse_inventory_response(document)
|
231
|
+
else
|
232
|
+
raise ArgumentError, "Unknown service #{service}"
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
def parse_tracking_response(document)
|
237
|
+
response = {}
|
238
|
+
response[:tracking_numbers] = {}
|
239
|
+
response[:tracking_companies] = {}
|
240
|
+
response[:tracking_urls] = {}
|
241
|
+
|
242
|
+
tracking_numbers = REXML::XPath.match(document, "//FulfillmentShipmentPackage/member/TrackingNumber")
|
243
|
+
if tracking_numbers.present?
|
244
|
+
order_id = REXML::XPath.first(document, "//FulfillmentOrder/SellerFulfillmentOrderId").text.strip
|
245
|
+
response[:tracking_numbers][order_id] = tracking_numbers.map{ |t| t.text.strip }
|
246
|
+
end
|
247
|
+
|
248
|
+
tracking_companies = REXML::XPath.match(document, "//FulfillmentShipmentPackage/member/CarrierCode")
|
249
|
+
if tracking_companies.present?
|
250
|
+
order_id = REXML::XPath.first(document, "//FulfillmentOrder/SellerFulfillmentOrderId").text.strip
|
251
|
+
response[:tracking_companies][order_id] = tracking_companies.map{ |t| t.text.strip }
|
252
|
+
end
|
253
|
+
|
254
|
+
response[:response_status] = SUCCESS
|
255
|
+
response
|
256
|
+
end
|
257
|
+
|
258
|
+
def parse_fulfillment_response(op, document)
|
259
|
+
{ :response_status => SUCCESS, :response_comment => MESSAGES[op][SUCCESS] }
|
260
|
+
end
|
261
|
+
|
262
|
+
def parse_inventory_response(document)
|
263
|
+
response = {}
|
264
|
+
response[:stock_levels] = {}
|
265
|
+
|
266
|
+
document.each_element('//InventorySupplyList/member') do |node|
|
267
|
+
params = node.elements.to_a.each_with_object({}) { |elem, hash| hash[elem.name] = elem.text }
|
268
|
+
|
269
|
+
response[:stock_levels][params['SellerSKU']] = params['InStockSupplyQuantity'].to_i
|
270
|
+
end
|
271
|
+
|
272
|
+
next_token = REXML::XPath.first(document, '//NextToken')
|
273
|
+
response[:next_token] = next_token ? next_token.text : nil
|
274
|
+
|
275
|
+
response[:response_status] = SUCCESS
|
276
|
+
response
|
277
|
+
end
|
278
|
+
|
279
|
+
def parse_error(http_response)
|
280
|
+
response = {}
|
281
|
+
response[:http_code] = http_response.code
|
282
|
+
response[:http_message] = http_response.message
|
283
|
+
|
284
|
+
document = REXML::Document.new(http_response.body)
|
285
|
+
|
286
|
+
node = REXML::XPath.first(document, '//Error')
|
287
|
+
error_code = REXML::XPath.first(node, '//Code')
|
288
|
+
error_message = REXML::XPath.first(node, '//Message')
|
289
|
+
|
290
|
+
response[:status] = FAILURE
|
291
|
+
response[:faultcode] = error_code ? error_code.text : ""
|
292
|
+
response[:faultstring] = error_message ? error_message.text : ""
|
293
|
+
response[:response_message] = error_message ? error_message.text : ""
|
294
|
+
response[:response_comment] = "#{response[:faultcode]}: #{response[:faultstring]}"
|
295
|
+
response
|
296
|
+
rescue REXML::ParseException => e
|
297
|
+
rescue NoMethodError => e
|
298
|
+
response[:http_body] = http_response.body
|
299
|
+
response[:response_status] = FAILURE
|
300
|
+
response[:response_comment] = "#{response[:http_code]}: #{response[:http_message]}"
|
301
|
+
response
|
302
|
+
end
|
303
|
+
|
304
|
+
def sign(http_verb, uri, options)
|
305
|
+
string_to_sign = "#{http_verb.to_s.upcase}\n"
|
306
|
+
string_to_sign += "#{uri.host}\n"
|
307
|
+
string_to_sign += uri.path.length <= 0 ? "/\n" : "#{uri.path}\n"
|
308
|
+
string_to_sign += build_query(options)
|
309
|
+
|
310
|
+
# remove trailing newline created by encode64
|
311
|
+
escape(Base64.encode64(OpenSSL::HMAC.digest(SIGNATURE_METHOD, @options[:password], string_to_sign)).chomp)
|
312
|
+
end
|
313
|
+
|
314
|
+
def amazon_request?(http_verb, base_url, return_path_and_parameters, post_params)
|
315
|
+
signed_params = build_query(post_params.except(:Signature, :SignedString))
|
316
|
+
string_to_sign = "#{http_verb}\n#{base_url}\n#{return_path_and_parameters}\n#{signed_params}"
|
317
|
+
calculated_signature = Base64.encode64(OpenSSL::HMAC.digest(SIGNATURE_METHOD, @options[:password], string_to_sign)).chomp
|
318
|
+
secure_compare(calculated_signature, post_params[:Signature])
|
319
|
+
end
|
320
|
+
|
321
|
+
def registration_url(options)
|
322
|
+
opts = {
|
323
|
+
"returnPathAndParameters" => options["returnPathAndParameters"],
|
324
|
+
"id" => @options[:app_id],
|
325
|
+
"AWSAccessKeyId" => @options[:login],
|
326
|
+
"SignatureMethod" => "Hmac#{SIGNATURE_METHOD}",
|
327
|
+
"SignatureVersion" => SIGNATURE_VERSION
|
328
|
+
}
|
329
|
+
signature = sign(:get, REGISTRATION_URI, opts)
|
330
|
+
"#{REGISTRATION_URI.to_s}?#{build_query(opts)}&Signature=#{signature}"
|
331
|
+
end
|
332
|
+
|
333
|
+
def md5_content(content)
|
334
|
+
Base64.encode64(OpenSSL::Digest.new('md5', content).digest).chomp
|
335
|
+
end
|
336
|
+
|
337
|
+
def build_query(query_params)
|
338
|
+
query_params.sort.map{ |key, value| [escape(key.to_s), escape(value.to_s)].join('=') }.join('&')
|
339
|
+
end
|
340
|
+
|
341
|
+
def build_headers(querystr)
|
342
|
+
{
|
343
|
+
'User-Agent' => APPLICATION_IDENTIFIER,
|
344
|
+
'Content-MD5' => md5_content(querystr),
|
345
|
+
'Content-Type' => 'application/x-www-form-urlencoded'
|
346
|
+
}
|
347
|
+
end
|
348
|
+
|
349
|
+
def build_basic_api_query(options)
|
350
|
+
opts = Hash[options.map{ |k,v| [k.to_s, v.to_s] }]
|
351
|
+
opts["AWSAccessKeyId"] = @options[:login] unless opts["AWSAccessKey"]
|
352
|
+
opts["Timestamp"] = Time.now.utc.iso8601 unless opts["Timestamp"]
|
353
|
+
opts["Version"] = VERSION unless opts["Version"]
|
354
|
+
opts["SignatureMethod"] = "Hmac#{SIGNATURE_METHOD}" unless opts["SignatureMethod"]
|
355
|
+
opts["SignatureVersion"] = SIGNATURE_VERSION unless opts["SignatureVersion"]
|
356
|
+
opts["SellerId"] = @seller_id unless opts["SellerId"] || !@seller_id
|
357
|
+
opts["MWSAuthToken"] = @mws_auth_token unless opts["MWSAuthToken"] || !@mws_auth_token
|
358
|
+
opts
|
359
|
+
end
|
360
|
+
|
361
|
+
def build_fulfillment_request(order_id, shipping_address, line_items, options)
|
362
|
+
params = {
|
363
|
+
:Action => OPERATIONS[:outbound][:create],
|
364
|
+
:SellerFulfillmentOrderId => order_id.to_s,
|
365
|
+
:DisplayableOrderId => order_id.to_s,
|
366
|
+
:DisplayableOrderDateTime => options[:order_date].utc.iso8601,
|
367
|
+
:ShippingSpeedCategory => options[:shipping_method]
|
368
|
+
}
|
369
|
+
params[:DisplayableOrderComment] = options[:comment] if options[:comment]
|
370
|
+
|
371
|
+
request = build_basic_api_query(params.merge(options))
|
372
|
+
request = request.merge build_address(shipping_address)
|
373
|
+
request = request.merge build_items(line_items)
|
374
|
+
|
375
|
+
request
|
376
|
+
end
|
377
|
+
|
378
|
+
def build_get_current_fulfillment_orders_request(options = {})
|
379
|
+
start_time = options.delete(:start_time) || 1.day.ago.utc
|
380
|
+
params = {
|
381
|
+
:Action => OPERATIONS[:outbound][:list],
|
382
|
+
:QueryStartDateTime => start_time.strftime("%Y-%m-%dT%H:%M:%SZ")
|
383
|
+
}
|
384
|
+
|
385
|
+
build_basic_api_query(params.merge(options))
|
386
|
+
end
|
387
|
+
|
388
|
+
def build_inventory_list_request(options = {})
|
389
|
+
response_group = options.delete(:response_group) || "Basic"
|
390
|
+
params = {
|
391
|
+
:Action => OPERATIONS[:inventory][:list],
|
392
|
+
:ResponseGroup => response_group
|
393
|
+
}
|
394
|
+
if skus = options.delete(:skus)
|
395
|
+
skus.each_with_index do |sku, index|
|
396
|
+
params[LOOKUPS[:list_inventory][:sku] % (index + 1)] = sku
|
397
|
+
end
|
398
|
+
else
|
399
|
+
start_time = options.delete(:start_time) || 1.day.ago
|
400
|
+
params[:QueryStartDateTime] = start_time.utc.iso8601
|
401
|
+
end
|
402
|
+
|
403
|
+
build_basic_api_query(params.merge(options))
|
404
|
+
end
|
405
|
+
|
406
|
+
def build_next_inventory_list_request(token)
|
407
|
+
params = {
|
408
|
+
:NextToken => token,
|
409
|
+
:Action => OPERATIONS[:inventory][:list_next]
|
410
|
+
}
|
411
|
+
|
412
|
+
build_basic_api_query(params)
|
413
|
+
end
|
414
|
+
|
415
|
+
def build_tracking_request(order_id, options)
|
416
|
+
params = {:Action => OPERATIONS[:outbound][:tracking], :SellerFulfillmentOrderId => order_id}
|
417
|
+
|
418
|
+
build_basic_api_query(params.merge(options))
|
419
|
+
end
|
420
|
+
|
421
|
+
def build_address(address)
|
422
|
+
requires!(address, :name, :address1, :city, :country, :zip)
|
423
|
+
address[:state] ||= "N/A"
|
424
|
+
address[:zip].upcase!
|
425
|
+
address[:name] = "#{address[:company]} - #{address[:name]}" if address[:company].present?
|
426
|
+
address[:name] = address[:name][0...50] if address[:name].present?
|
427
|
+
ary = address.map{ |key, value| [LOOKUPS[:destination_address][key], value] if LOOKUPS[:destination_address].include?(key) && value.present? }
|
428
|
+
Hash[ary.compact]
|
429
|
+
end
|
430
|
+
|
431
|
+
def build_items(line_items)
|
432
|
+
lookup = LOOKUPS[:line_items]
|
433
|
+
counter = 0
|
434
|
+
line_items.reduce({}) do |items, line_item|
|
435
|
+
counter += 1
|
436
|
+
lookup.each do |key, value|
|
437
|
+
entry = value % counter
|
438
|
+
case key
|
439
|
+
when :sku
|
440
|
+
items[entry] = line_item[:sku] || "SKU-#{counter}"
|
441
|
+
when :order_id
|
442
|
+
items[entry] = line_item[:sku] || "FULFILLMENT-ITEM-ID-#{counter}"
|
443
|
+
when :quantity
|
444
|
+
items[entry] = line_item[:quantity] || 1
|
445
|
+
else
|
446
|
+
items[entry] = line_item[key] if line_item.include? key
|
447
|
+
end
|
448
|
+
end
|
449
|
+
items
|
450
|
+
end
|
451
|
+
end
|
452
|
+
|
453
|
+
def build_status_request
|
454
|
+
build_basic_api_query({ :Action => OPERATIONS[:outbound][:status] })
|
455
|
+
end
|
456
|
+
|
457
|
+
def escape(str)
|
458
|
+
CGI.escape(str.to_s).gsub('+', '%20')
|
459
|
+
end
|
460
|
+
|
461
|
+
private
|
462
|
+
|
463
|
+
def secure_compare(a, b)
|
464
|
+
return false unless a.bytesize == b.bytesize
|
465
|
+
|
466
|
+
l = a.unpack "C#{a.bytesize}"
|
467
|
+
|
468
|
+
res = 0
|
469
|
+
b.each_byte { |byte| res |= byte ^ l.shift }
|
470
|
+
res == 0
|
471
|
+
end
|
472
|
+
end
|
473
|
+
end
|