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
@@ -1,454 +0,0 @@
1
- require 'base64'
2
- require 'time'
3
- require 'cgi'
4
-
5
- module ActiveMerchant
6
- module Fulfillment
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(ActiveSupport::OrderedHash.new){|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
- super
112
- end
113
-
114
- def seller_id=(seller_id)
115
- @seller_id = seller_id
116
- end
117
-
118
- def endpoint
119
- ENDPOINTS[@options[:endpoint] || :us]
120
- end
121
-
122
- def fulfill(order_id, shipping_address, line_items, options = {})
123
- requires!(options, :order_date, :shipping_method)
124
- commit :post, :outbound, :create, build_fulfillment_request(order_id, shipping_address, line_items, options)
125
- end
126
-
127
- def status
128
- commit :post, :outbound, :status, build_status_request
129
- end
130
-
131
- def fetch_current_orders
132
- commit :post, :outbound, :status, build_get_current_fulfillment_orders_request
133
- end
134
-
135
- def fetch_stock_levels(options = {})
136
- options[:skus] = [options.delete(:sku)] if options.include?(:sku)
137
- response = commit :post, :inventory, :list, build_inventory_list_request(options)
138
-
139
- while token = response.params['next_token'] do
140
- next_page = commit :post, :inventory, :list_next, build_next_inventory_list_request(token)
141
-
142
- # if we fail during the stock-level-via-token gathering, fail the whole request
143
- return next_page if next_page.params['response_status'] != SUCCESS
144
- next_page.stock_levels.merge!(response.stock_levels)
145
- response = next_page
146
- end
147
-
148
- response
149
- end
150
-
151
- def fetch_tracking_data(order_ids, options = {})
152
- order_ids.reduce(nil) do |previous, order_id|
153
- response = commit :post, :outbound, :tracking, build_tracking_request(order_id, options)
154
- return response if !response.success?
155
-
156
- if previous
157
- response.tracking_numbers.merge!(previous.tracking_numbers)
158
- response.tracking_companies.merge!(previous.tracking_companies)
159
- response.tracking_urls.merge!(previous.tracking_urls)
160
- end
161
-
162
- response
163
- end
164
- end
165
-
166
- def valid_credentials?
167
- fetch_stock_levels.success?
168
- end
169
-
170
- def test_mode?
171
- false
172
- end
173
-
174
- def build_full_query(verb, uri, params)
175
- signature = sign(verb, uri, params)
176
- build_query(params) + "&Signature=#{signature}"
177
- end
178
-
179
- def commit(verb, service, op, params)
180
- uri = URI.parse("https://#{endpoint}/#{ACTIONS[service]}/#{VERSION}")
181
- query = build_full_query(verb, uri, params)
182
- headers = build_headers(query)
183
-
184
- data = ssl_post(uri.to_s, query, headers)
185
- response = parse_response(service, op, data)
186
- Response.new(success?(response), message_from(response), response)
187
- rescue ActiveMerchant::ResponseError => e
188
- handle_error(e)
189
- end
190
-
191
- def handle_error(e)
192
- response = parse_error(e.response)
193
- if response.fetch(:faultstring, "").match(/^Requested order \'.+\' not found$/)
194
- Response.new(true, nil, {:status => SUCCESS, :tracking_numbers => {}, :tracking_companies => {}, :tracking_urls => {}})
195
- else
196
- Response.new(false, message_from(response), response)
197
- end
198
- end
199
-
200
- def success?(response)
201
- response[:response_status] == SUCCESS
202
- end
203
-
204
- def message_from(response)
205
- response[:response_message]
206
- end
207
-
208
- ## PARSING
209
-
210
- def parse_response(service, op, xml)
211
- begin
212
- document = REXML::Document.new(xml)
213
- rescue REXML::ParseException
214
- return { :success => FAILURE }
215
- end
216
-
217
- case service
218
- when :outbound
219
- case op
220
- when :tracking
221
- parse_tracking_response(document)
222
- else
223
- parse_fulfillment_response(op, document)
224
- end
225
- when :inventory
226
- parse_inventory_response(document)
227
- else
228
- raise ArgumentError, "Unknown service #{service}"
229
- end
230
- end
231
-
232
- def parse_tracking_response(document)
233
- response = {}
234
- response[:tracking_numbers] = {}
235
- response[:tracking_companies] = {}
236
- response[:tracking_urls] = {}
237
-
238
- tracking_numbers = REXML::XPath.match(document, "//FulfillmentShipmentPackage/member/TrackingNumber")
239
- if tracking_numbers.present?
240
- order_id = REXML::XPath.first(document, "//FulfillmentOrder/SellerFulfillmentOrderId").text.strip
241
- response[:tracking_numbers][order_id] = tracking_numbers.map{ |t| t.text.strip }
242
- end
243
-
244
- tracking_companies = REXML::XPath.match(document, "//FulfillmentShipmentPackage/member/CarrierCode")
245
- if tracking_companies.present?
246
- order_id = REXML::XPath.first(document, "//FulfillmentOrder/SellerFulfillmentOrderId").text.strip
247
- response[:tracking_companies][order_id] = tracking_companies.map{ |t| t.text.strip }
248
- end
249
-
250
- response[:response_status] = SUCCESS
251
- response
252
- end
253
-
254
- def parse_fulfillment_response(op, document)
255
- { :response_status => SUCCESS, :response_comment => MESSAGES[op][SUCCESS] }
256
- end
257
-
258
- def parse_inventory_response(document)
259
- response = {}
260
- response[:stock_levels] = {}
261
-
262
- document.each_element('//InventorySupplyList/member') do |node|
263
- params = node.elements.to_a.each_with_object({}) { |elem, hash| hash[elem.name] = elem.text }
264
-
265
- response[:stock_levels][params['SellerSKU']] = params['InStockSupplyQuantity'].to_i
266
- end
267
-
268
- next_token = REXML::XPath.first(document, '//NextToken')
269
- response[:next_token] = next_token ? next_token.text : nil
270
-
271
- response[:response_status] = SUCCESS
272
- response
273
- end
274
-
275
- def parse_error(http_response)
276
- response = {}
277
- response[:http_code] = http_response.code
278
- response[:http_message] = http_response.message
279
-
280
- document = REXML::Document.new(http_response.body)
281
-
282
- node = REXML::XPath.first(document, '//Error')
283
- error_code = REXML::XPath.first(node, '//Code')
284
- error_message = REXML::XPath.first(node, '//Message')
285
-
286
- response[:status] = FAILURE
287
- response[:faultcode] = error_code ? error_code.text : ""
288
- response[:faultstring] = error_message ? error_message.text : ""
289
- response[:response_message] = error_message ? error_message.text : ""
290
- response[:response_comment] = "#{response[:faultcode]}: #{response[:faultstring]}"
291
- response
292
- rescue REXML::ParseException => e
293
- rescue NoMethodError => e
294
- response[:http_body] = http_response.body
295
- response[:response_status] = FAILURE
296
- response[:response_comment] = "#{response[:http_code]}: #{response[:http_message]}"
297
- response
298
- end
299
-
300
- def sign(http_verb, uri, options)
301
- string_to_sign = "#{http_verb.to_s.upcase}\n"
302
- string_to_sign += "#{uri.host}\n"
303
- string_to_sign += uri.path.length <= 0 ? "/\n" : "#{uri.path}\n"
304
- string_to_sign += build_query(options)
305
-
306
- # remove trailing newline created by encode64
307
- escape(Base64.encode64(OpenSSL::HMAC.digest(SIGNATURE_METHOD, @options[:password], string_to_sign)).chomp)
308
- end
309
-
310
- def amazon_request?(signed_string, expected_signature)
311
- calculated_signature = escape(Base64.encode64(OpenSSL::HMAC.digest(SIGNATURE_METHOD, @options[:password], signed_string)).chomp)
312
- calculated_signature == expected_signature
313
- end
314
-
315
- def registration_url(options)
316
- opts = {
317
- "returnPathAndParameters" => options["returnPathAndParameters"],
318
- "id" => @options[:app_id],
319
- "AWSAccessKeyId" => @options[:login],
320
- "SignatureMethod" => "Hmac#{SIGNATURE_METHOD}",
321
- "SignatureVersion" => SIGNATURE_VERSION
322
- }
323
- signature = sign(:get, REGISTRATION_URI, opts)
324
- "#{REGISTRATION_URI.to_s}?#{build_query(opts)}&Signature=#{signature}"
325
- end
326
-
327
- def md5_content(content)
328
- Base64.encode64(OpenSSL::Digest.new('md5', content).digest).chomp
329
- end
330
-
331
- def build_query(query_params)
332
- query_params.sort.map{ |key, value| [escape(key.to_s), escape(value.to_s)].join('=') }.join('&')
333
- end
334
-
335
- def build_headers(querystr)
336
- {
337
- 'User-Agent' => APPLICATION_IDENTIFIER,
338
- 'Content-MD5' => md5_content(querystr),
339
- 'Content-Type' => 'application/x-www-form-urlencoded'
340
- }
341
- end
342
-
343
- def build_basic_api_query(options)
344
- opts = Hash[options.map{ |k,v| [k.to_s, v.to_s] }]
345
- opts["AWSAccessKeyId"] = @options[:login] unless opts["AWSAccessKey"]
346
- opts["Timestamp"] = Time.now.utc.iso8601 unless opts["Timestamp"]
347
- opts["Version"] = VERSION unless opts["Version"]
348
- opts["SignatureMethod"] = "Hmac#{SIGNATURE_METHOD}" unless opts["SignatureMethod"]
349
- opts["SignatureVersion"] = SIGNATURE_VERSION unless opts["SignatureVersion"]
350
- opts["SellerId"] = @seller_id unless opts["SellerId"] || !@seller_id
351
- opts
352
- end
353
-
354
- def build_fulfillment_request(order_id, shipping_address, line_items, options)
355
- params = {
356
- :Action => OPERATIONS[:outbound][:create],
357
- :SellerFulfillmentOrderId => order_id.to_s,
358
- :DisplayableOrderId => order_id.to_s,
359
- :DisplayableOrderDateTime => options[:order_date].utc.iso8601,
360
- :ShippingSpeedCategory => options[:shipping_method]
361
- }
362
- params[:DisplayableOrderComment] = options[:comment] if options[:comment]
363
-
364
- request = build_basic_api_query(params.merge(options))
365
- request = request.merge build_address(shipping_address)
366
- request = request.merge build_items(line_items)
367
-
368
- request
369
- end
370
-
371
- def build_get_current_fulfillment_orders_request(options = {})
372
- start_time = options.delete(:start_time) || 1.day.ago.utc
373
- params = {
374
- :Action => OPERATIONS[:outbound][:list],
375
- :QueryStartDateTime => start_time.strftime("%Y-%m-%dT%H:%M:%SZ")
376
- }
377
-
378
- build_basic_api_query(params.merge(options))
379
- end
380
-
381
- def build_inventory_list_request(options = {})
382
- response_group = options.delete(:response_group) || "Basic"
383
- params = {
384
- :Action => OPERATIONS[:inventory][:list],
385
- :ResponseGroup => response_group
386
- }
387
- if skus = options.delete(:skus)
388
- skus.each_with_index do |sku, index|
389
- params[LOOKUPS[:list_inventory][:sku] % (index + 1)] = sku
390
- end
391
- else
392
- start_time = options.delete(:start_time) || 1.day.ago
393
- params[:QueryStartDateTime] = start_time.utc.iso8601
394
- end
395
-
396
- build_basic_api_query(params.merge(options))
397
- end
398
-
399
- def build_next_inventory_list_request(token)
400
- params = {
401
- :NextToken => token,
402
- :Action => OPERATIONS[:inventory][:list_next]
403
- }
404
-
405
- build_basic_api_query(params)
406
- end
407
-
408
- def build_tracking_request(order_id, options)
409
- params = {:Action => OPERATIONS[:outbound][:tracking], :SellerFulfillmentOrderId => order_id}
410
-
411
- build_basic_api_query(params.merge(options))
412
- end
413
-
414
- def build_address(address)
415
- requires!(address, :name, :address1, :city, :country, :zip)
416
- address[:state] ||= "N/A"
417
- address[:zip].upcase!
418
- address[:name] = "#{address[:company]} - #{address[:name]}" if address[:company].present?
419
- ary = address.map{ |key, value| [LOOKUPS[:destination_address][key], value] if LOOKUPS[:destination_address].include?(key) && value.present? }
420
- Hash[ary.compact]
421
- end
422
-
423
- def build_items(line_items)
424
- lookup = LOOKUPS[:line_items]
425
- counter = 0
426
- line_items.reduce({}) do |items, line_item|
427
- counter += 1
428
- lookup.each do |key, value|
429
- entry = value % counter
430
- case key
431
- when :sku
432
- items[entry] = line_item[:sku] || "SKU-#{counter}"
433
- when :order_id
434
- items[entry] = line_item[:sku] || "FULFILLMENT-ITEM-ID-#{counter}"
435
- when :quantity
436
- items[entry] = line_item[:quantity] || 1
437
- else
438
- items[entry] = line_item[key] if line_item.include? key
439
- end
440
- end
441
- items
442
- end
443
- end
444
-
445
- def build_status_request
446
- build_basic_api_query({ :Action => OPERATIONS[:outbound][:status] })
447
- end
448
-
449
- def escape(str)
450
- CGI.escape(str.to_s).gsub('+', '%20')
451
- end
452
- end
453
- end
454
- end