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.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +68 -0
  3. data/lib/active_fulfillment.rb +5 -5
  4. data/lib/active_fulfillment/base.rb +10 -0
  5. data/lib/active_fulfillment/response.rb +26 -0
  6. data/lib/active_fulfillment/service.rb +56 -0
  7. data/lib/active_fulfillment/services.rb +5 -0
  8. data/lib/active_fulfillment/services/amazon_mws.rb +473 -0
  9. data/lib/active_fulfillment/services/james_and_james.rb +122 -0
  10. data/lib/active_fulfillment/services/shipwire.rb +266 -0
  11. data/lib/active_fulfillment/services/shopify_api.rb +125 -0
  12. data/lib/active_fulfillment/services/webgistix.rb +334 -0
  13. data/lib/active_fulfillment/version.rb +4 -0
  14. data/test/remote/amazon_mws_test.rb +20 -17
  15. data/test/remote/james_and_james_test.rb +77 -0
  16. data/test/remote/shipwire_test.rb +25 -25
  17. data/test/remote/webgistix_test.rb +21 -21
  18. data/test/test_helper.rb +27 -52
  19. data/test/unit/base_test.rb +4 -4
  20. data/test/unit/services/amazon_mws_test.rb +56 -26
  21. data/test/unit/services/james_and_james_test.rb +90 -0
  22. data/test/unit/services/shipwire_test.rb +18 -18
  23. data/test/unit/services/shopify_api_test.rb +7 -20
  24. data/test/unit/services/webgistix_test.rb +35 -35
  25. metadata +32 -114
  26. data/CHANGELOG +0 -62
  27. data/lib/active_fulfillment/fulfillment/base.rb +0 -12
  28. data/lib/active_fulfillment/fulfillment/response.rb +0 -28
  29. data/lib/active_fulfillment/fulfillment/service.rb +0 -58
  30. data/lib/active_fulfillment/fulfillment/services.rb +0 -5
  31. data/lib/active_fulfillment/fulfillment/services/amazon.rb +0 -389
  32. data/lib/active_fulfillment/fulfillment/services/amazon_mws.rb +0 -454
  33. data/lib/active_fulfillment/fulfillment/services/shipwire.rb +0 -268
  34. data/lib/active_fulfillment/fulfillment/services/shopify_api.rb +0 -125
  35. data/lib/active_fulfillment/fulfillment/services/webgistix.rb +0 -338
  36. data/lib/active_fulfillment/fulfillment/version.rb +0 -6
  37. data/test/fixtures.yml +0 -16
  38. data/test/fixtures/xml/amazon/inventory_get_response.xml +0 -17
  39. data/test/fixtures/xml/amazon/inventory_list_response.xml +0 -29
  40. data/test/fixtures/xml/amazon/inventory_list_response_with_next_1.xml +0 -30
  41. data/test/fixtures/xml/amazon/inventory_list_response_with_next_2.xml +0 -29
  42. data/test/fixtures/xml/amazon/tracking_response_1.xml +0 -56
  43. data/test/fixtures/xml/amazon/tracking_response_2.xml +0 -38
  44. data/test/fixtures/xml/amazon/tracking_response_error.xml +0 -13
  45. data/test/fixtures/xml/amazon/tracking_response_not_found.xml +0 -13
  46. data/test/fixtures/xml/amazon_mws/fulfillment_get_fulfillment_order.xml +0 -114
  47. data/test/fixtures/xml/amazon_mws/fulfillment_get_fulfillment_order_2.xml +0 -90
  48. data/test/fixtures/xml/amazon_mws/fulfillment_get_fullfillment_order_with_multiple_tracking_numbers.xml +0 -121
  49. data/test/fixtures/xml/amazon_mws/fulfillment_list_all_fulfillment_orders.xml +0 -70
  50. data/test/fixtures/xml/amazon_mws/inventory_list_inventory_item_supply.xml +0 -32
  51. data/test/fixtures/xml/amazon_mws/inventory_list_inventory_supply.xml +0 -75
  52. data/test/fixtures/xml/amazon_mws/inventory_list_inventory_supply_by_next_token.xml +0 -38
  53. data/test/fixtures/xml/amazon_mws/tracking_response_error.xml +0 -9
  54. data/test/fixtures/xml/amazon_mws/tracking_response_not_found.xml +0 -9
  55. data/test/fixtures/xml/shipwire/fulfillment_failure_response.xml +0 -7
  56. data/test/fixtures/xml/shipwire/invalid_login_response.xml +0 -7
  57. data/test/fixtures/xml/shipwire/inventory_get_response.xml +0 -44
  58. data/test/fixtures/xml/shipwire/successful_empty_tracking_response.xml +0 -8
  59. data/test/fixtures/xml/shipwire/successful_live_tracking_response.xml +0 -53
  60. data/test/fixtures/xml/shipwire/successful_tracking_response.xml +0 -16
  61. data/test/fixtures/xml/shipwire/successful_tracking_response_with_tracking_urls.xml +0 -31
  62. data/test/fixtures/xml/webgistix/multiple_tracking_response.xml +0 -21
  63. data/test/fixtures/xml/webgistix/tracking_response.xml +0 -14
  64. data/test/remote/amazon_test.rb +0 -124
  65. data/test/unit/services/amazon_test.rb +0 -271
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 908c6b7616644ccd234c5bcb5291ce17c71bacbc
4
- data.tar.gz: a8e969ac0783a479a4001a90aee2655cea935bf9
3
+ metadata.gz: 7731ece9da5575406a9426a3224c057a7ad8d5bc
4
+ data.tar.gz: d9ae056d89696bf9329b99e20ba75aadcba1ff88
5
5
  SHA512:
6
- metadata.gz: a1fc6a0fa38da6054d7553ac8c6741ad8dba55f077b70c7f6662a5396b898955afa370b55341db68f692e455f930b0d3f5229d3ba17801b5eecf33ffcd2eb6dc
7
- data.tar.gz: fa0eb29009332c2090a66fdad0c89dbc23df137ef03f88b033f8cd932b22ef3d7ea64d7ab3fd6d3b5108174e2efc7a2f1433b11562b313dd60b94e86d488278a
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
@@ -40,8 +40,8 @@ require 'net/https'
40
40
  require 'rexml/document'
41
41
  require 'active_utils'
42
42
 
43
- require 'active_fulfillment/fulfillment/version'
44
- require 'active_fulfillment/fulfillment/base'
45
- require 'active_fulfillment/fulfillment/response'
46
- require 'active_fulfillment/fulfillment/service'
47
- require 'active_fulfillment/fulfillment/services'
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,10 @@
1
+ module ActiveFulfillment
2
+ module Base
3
+ mattr_accessor :mode
4
+ self.mode = :production
5
+
6
+ def self.service(name)
7
+ ActiveFulfillment.const_get("#{name.to_s.downcase}_service".camelize)
8
+ end
9
+ end
10
+ end
@@ -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,5 @@
1
+ require 'active_fulfillment/services/shopify_api'
2
+ require 'active_fulfillment/services/shipwire'
3
+ require 'active_fulfillment/services/webgistix'
4
+ require 'active_fulfillment/services/amazon_mws'
5
+ require 'active_fulfillment/services/james_and_james'
@@ -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