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
@@ -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