active_fulfillment 2.1.9 → 3.0.0.pre2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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