active_fulfillment 3.0.0.pre8 → 3.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +7 -1
- data/lib/active_fulfillment.rb +2 -1
- data/lib/active_fulfillment/parsing.rb +15 -0
- data/lib/active_fulfillment/services/amazon_mws.rb +41 -38
- data/lib/active_fulfillment/services/james_and_james.rb +1 -1
- data/lib/active_fulfillment/services/shipwire.rb +78 -63
- data/lib/active_fulfillment/services/shopify_api.rb +38 -31
- data/lib/active_fulfillment/services/webgistix.rb +68 -66
- data/lib/active_fulfillment/version.rb +1 -1
- data/test/test_helper.rb +1 -0
- metadata +23 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: edee633e1484a24673765631ab0a8a85cb114f4d
|
4
|
+
data.tar.gz: 1b3c6f2099542ca7f988a0a7ac9ee9f3309cc809
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 21c48116701f5281c5a4deacc5a445126e12b6e407551fc097709d579231a31d74944e88eb404b476bc2667d073686717dbce7c79388608600f73445b10a2538
|
7
|
+
data.tar.gz: 90872e2cce274d7bc42593f93da678bf8a234fed5c4b12d3bf4e83d8ae665ab046cf22c7ded2e26fb8f1425aa0a108d256c61ca7060f3434268e1c713bc81b31
|
data/CHANGELOG.md
CHANGED
@@ -1,7 +1,13 @@
|
|
1
1
|
# ActiveFulfillment changelog
|
2
2
|
|
3
|
-
|
3
|
+
### Version 3.0.1 (January 2015)
|
4
4
|
|
5
|
+
- Use Nokogiri for all xml handling.
|
6
|
+
- Ruby 2.3.0 support.
|
7
|
+
- Refactor Amazon MWS calls/parsing.
|
8
|
+
- Freeze constants and hashes.
|
9
|
+
|
10
|
+
### Version 2.1.8
|
5
11
|
- Update dependencies
|
6
12
|
- Remove old Amazon fulfillment service (use amazon_aws instead)
|
7
13
|
- Add contributing guidelines
|
data/lib/active_fulfillment.rb
CHANGED
@@ -36,7 +36,7 @@ end
|
|
36
36
|
require 'builder'
|
37
37
|
require 'cgi'
|
38
38
|
require 'net/https'
|
39
|
-
require '
|
39
|
+
require 'nokogiri'
|
40
40
|
require 'active_utils'
|
41
41
|
|
42
42
|
require 'active_fulfillment/version'
|
@@ -44,3 +44,4 @@ require 'active_fulfillment/base'
|
|
44
44
|
require 'active_fulfillment/response'
|
45
45
|
require 'active_fulfillment/service'
|
46
46
|
require 'active_fulfillment/services'
|
47
|
+
require 'active_fulfillment/parsing'
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module ActiveFulfillment
|
2
|
+
module Parsing
|
3
|
+
module_function
|
4
|
+
|
5
|
+
def with_xml_document(xml, response = {})
|
6
|
+
begin
|
7
|
+
document = Nokogiri::XML(xml)
|
8
|
+
rescue Nokogiri::XML::SyntaxError
|
9
|
+
return response
|
10
|
+
end
|
11
|
+
|
12
|
+
yield document, response
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -6,15 +6,16 @@ require 'active_support/core_ext/hash/except'
|
|
6
6
|
module ActiveFulfillment
|
7
7
|
class AmazonMarketplaceWebService < Service
|
8
8
|
|
9
|
-
APPLICATION_IDENTIFIER =
|
9
|
+
APPLICATION_IDENTIFIER = 'active_merchant_mws/0.01 (Language=ruby)'.freeze
|
10
10
|
|
11
|
-
REGISTRATION_URI = URI.parse(
|
11
|
+
REGISTRATION_URI = URI.parse('https://sellercentral.amazon.com/gp/mws/registration/register.html').freeze
|
12
12
|
|
13
13
|
SIGNATURE_VERSION = 2
|
14
|
-
SIGNATURE_METHOD =
|
15
|
-
VERSION =
|
14
|
+
SIGNATURE_METHOD = 'SHA256'.freeze
|
15
|
+
VERSION = '2010-10-01'.freeze
|
16
16
|
|
17
|
-
SUCCESS, FAILURE, ERROR = 'Accepted', 'Failure', 'Error'
|
17
|
+
SUCCESS, FAILURE, ERROR = 'Accepted'.freeze, 'Failure'.freeze, 'Error'.freeze
|
18
|
+
XML_FAILURE_RESPONSE = { :success => FAILURE }.freeze
|
18
19
|
|
19
20
|
ENDPOINTS = {
|
20
21
|
:ca => 'mws.amazonservices.ca',
|
@@ -26,7 +27,7 @@ module ActiveFulfillment
|
|
26
27
|
:jp => 'mws.amazonservices.jp',
|
27
28
|
:uk => 'mws-eu.amazonservices.ca',
|
28
29
|
:us => 'mws.amazonservices.com'
|
29
|
-
}
|
30
|
+
}.freeze
|
30
31
|
|
31
32
|
LOOKUPS = {
|
32
33
|
:destination_address => {
|
@@ -53,18 +54,20 @@ module ActiveFulfillment
|
|
53
54
|
:list_inventory => {
|
54
55
|
:sku => "SellerSkus.member.%d"
|
55
56
|
}
|
56
|
-
}
|
57
|
+
}.freeze
|
58
|
+
|
59
|
+
SHIPPING_METHODS = {
|
60
|
+
'Standard Shipping' => 'Standard',
|
61
|
+
'Expedited Shipping' => 'Expedited',
|
62
|
+
'Priority Shipping' => 'Priority'
|
63
|
+
}.freeze
|
57
64
|
|
58
65
|
# The first is the label, and the last is the code
|
59
66
|
# Standard: 3-5 business days
|
60
67
|
# Expedited: 2 business days
|
61
68
|
# Priority: 1 business day
|
62
69
|
def self.shipping_methods
|
63
|
-
|
64
|
-
[ 'Standard Shipping', 'Standard' ],
|
65
|
-
[ 'Expedited Shipping', 'Expedited' ],
|
66
|
-
[ 'Priority Shipping', 'Priority' ]
|
67
|
-
].inject({}){|h, (k,v)| h[k] = v; h}
|
70
|
+
SHIPPING_METHODS
|
68
71
|
end
|
69
72
|
|
70
73
|
def initialize(options = {})
|
@@ -86,7 +89,7 @@ module ActiveFulfillment
|
|
86
89
|
requires!(options, :order_date, :shipping_method)
|
87
90
|
with_error_handling do
|
88
91
|
data = commit :post, 'FulfillmentOutboundShipment', build_fulfillment_request(order_id, shipping_address, line_items, options)
|
89
|
-
parse_fulfillment_response(
|
92
|
+
parse_fulfillment_response('Successfully submitted the order')
|
90
93
|
end
|
91
94
|
end
|
92
95
|
|
@@ -195,27 +198,28 @@ module ActiveFulfillment
|
|
195
198
|
|
196
199
|
def parse_document(xml)
|
197
200
|
begin
|
198
|
-
document =
|
199
|
-
rescue
|
200
|
-
return
|
201
|
+
document = Nokogiri::XML(xml)
|
202
|
+
rescue Nokogiri::XML::SyntaxError
|
203
|
+
return XML_FAILURE_RESPONSE
|
201
204
|
end
|
202
205
|
end
|
203
206
|
|
204
207
|
def parse_tracking_response(document)
|
205
|
-
response = {
|
206
|
-
|
207
|
-
|
208
|
-
|
208
|
+
response = {
|
209
|
+
tracking_numbers: {},
|
210
|
+
tracking_companies: {},
|
211
|
+
tracking_urls: {}
|
212
|
+
}
|
209
213
|
|
210
|
-
tracking_numbers =
|
214
|
+
tracking_numbers = document.css('FulfillmentShipmentPackage > member > TrackingNumber'.freeze)
|
211
215
|
if tracking_numbers.present?
|
212
|
-
order_id =
|
216
|
+
order_id = document.at_css('FulfillmentOrder > SellerFulfillmentOrderId'.freeze).text.strip
|
213
217
|
response[:tracking_numbers][order_id] = tracking_numbers.map{ |t| t.text.strip }
|
214
218
|
end
|
215
219
|
|
216
|
-
tracking_companies =
|
220
|
+
tracking_companies = document.css('FulfillmentShipmentPackage > member > CarrierCode'.freeze)
|
217
221
|
if tracking_companies.present?
|
218
|
-
order_id =
|
222
|
+
order_id = document.at_css('FulfillmentOrder > SellerFulfillmentOrderId'.freeze).text.strip
|
219
223
|
response[:tracking_companies][order_id] = tracking_companies.map{ |t| t.text.strip }
|
220
224
|
end
|
221
225
|
|
@@ -223,21 +227,20 @@ module ActiveFulfillment
|
|
223
227
|
Response.new(success?(response), message_from(response), response)
|
224
228
|
end
|
225
229
|
|
226
|
-
def parse_fulfillment_response(
|
230
|
+
def parse_fulfillment_response(message)
|
227
231
|
Response.new(true, message, { :response_status => SUCCESS, :response_comment => message })
|
228
232
|
end
|
229
233
|
|
230
234
|
def parse_inventory_response(document)
|
231
|
-
response = {}
|
232
|
-
response[:stock_levels] = {}
|
235
|
+
response = { stock_levels: {} }
|
233
236
|
|
234
|
-
document.
|
237
|
+
document.css('InventorySupplyList > member'.freeze).each do |node|
|
235
238
|
params = node.elements.to_a.each_with_object({}) { |elem, hash| hash[elem.name] = elem.text }
|
236
239
|
|
237
240
|
response[:stock_levels][params['SellerSKU']] = params['InStockSupplyQuantity'].to_i
|
238
241
|
end
|
239
242
|
|
240
|
-
next_token =
|
243
|
+
next_token = document.at_css('NextToken'.freeze)
|
241
244
|
response[:next_token] = next_token ? next_token.text : nil
|
242
245
|
|
243
246
|
response[:response_status] = SUCCESS
|
@@ -245,15 +248,15 @@ module ActiveFulfillment
|
|
245
248
|
end
|
246
249
|
|
247
250
|
def parse_error(http_response)
|
248
|
-
response = {
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
document = REXML::Document.new(http_response.body)
|
251
|
+
response = {
|
252
|
+
http_code: http_response.code,
|
253
|
+
http_message: http_response.message
|
254
|
+
}
|
253
255
|
|
254
|
-
|
255
|
-
|
256
|
-
|
256
|
+
document = Nokogiri::XML(http_response.body)
|
257
|
+
node = document.at_css('Error'.freeze)
|
258
|
+
error_code = node.at_css('Code'.freeze)
|
259
|
+
error_message = node.at_css('Message'.freeze)
|
257
260
|
|
258
261
|
response[:status] = FAILURE
|
259
262
|
response[:faultcode] = error_code ? error_code.text : ""
|
@@ -261,7 +264,7 @@ module ActiveFulfillment
|
|
261
264
|
response[:response_message] = error_message ? error_message.text : ""
|
262
265
|
response[:response_comment] = "#{response[:faultcode]}: #{response[:faultstring]}"
|
263
266
|
response
|
264
|
-
rescue
|
267
|
+
rescue Nokogiri::XML::SyntaxError => e
|
265
268
|
rescue NoMethodError => e
|
266
269
|
response[:http_body] = http_response.body
|
267
270
|
response[:response_status] = FAILURE
|
@@ -6,17 +6,17 @@ module ActiveFulfillment
|
|
6
6
|
SERVICE_URLS = { :fulfillment => 'https://api.shipwire.com/exec/FulfillmentServices.php',
|
7
7
|
:inventory => 'https://api.shipwire.com/exec/InventoryServices.php',
|
8
8
|
:tracking => 'https://api.shipwire.com/exec/TrackingServices.php'
|
9
|
-
}
|
9
|
+
}.freeze
|
10
10
|
|
11
11
|
SCHEMA_URLS = { :fulfillment => 'http://www.shipwire.com/exec/download/OrderList.dtd',
|
12
12
|
:inventory => 'http://www.shipwire.com/exec/download/InventoryUpdate.dtd',
|
13
13
|
:tracking => 'http://www.shipwire.com/exec/download/TrackingUpdate.dtd'
|
14
|
-
}
|
14
|
+
}.freeze
|
15
15
|
|
16
16
|
POST_VARS = { :fulfillment => 'OrderListXML',
|
17
17
|
:inventory => 'InventoryUpdateXML',
|
18
18
|
:tracking => 'TrackingUpdateXML'
|
19
|
-
}
|
19
|
+
}.freeze
|
20
20
|
|
21
21
|
WAREHOUSES = { 'CHI' => 'Chicago',
|
22
22
|
'LAX' => 'Los Angeles',
|
@@ -24,7 +24,15 @@ module ActiveFulfillment
|
|
24
24
|
'VAN' => 'Vancouver',
|
25
25
|
'TOR' => 'Toronto',
|
26
26
|
'UK' => 'United Kingdom'
|
27
|
-
}
|
27
|
+
}.freeze
|
28
|
+
|
29
|
+
SHIPPING_METHODS = {
|
30
|
+
'1 Day Service' => '1D',
|
31
|
+
'2 Day Service' => '2D',
|
32
|
+
'Ground Service' => 'GD',
|
33
|
+
'Freight Service' => 'FT',
|
34
|
+
'International' => 'INTL'
|
35
|
+
}.freeze
|
28
36
|
|
29
37
|
INVALID_LOGIN = /(Error with Valid Username\/EmailAddress and Password Required)|(Could not verify Username\/EmailAddress and Password combination)/
|
30
38
|
|
@@ -32,12 +40,7 @@ module ActiveFulfillment
|
|
32
40
|
|
33
41
|
# The first is the label, and the last is the code
|
34
42
|
def self.shipping_methods
|
35
|
-
|
36
|
-
['2 Day Service', '2D'],
|
37
|
-
['Ground Service', 'GD'],
|
38
|
-
['Freight Service', 'FT'],
|
39
|
-
['International', 'INTL']
|
40
|
-
].inject({}){|h, (k,v)| h[k] = v; h}
|
43
|
+
SHIPPING_METHODS
|
41
44
|
end
|
42
45
|
|
43
46
|
# Pass in the login and password for the shipwire account.
|
@@ -188,79 +191,91 @@ module ActiveFulfillment
|
|
188
191
|
end
|
189
192
|
|
190
193
|
def parse_fulfillment_response(xml)
|
191
|
-
|
194
|
+
Parsing.with_xml_document(xml) do |document, response|
|
195
|
+
document.root.try do |root_document|
|
196
|
+
root_document.elements.each do |node|
|
197
|
+
response[node.name.underscore.to_sym] = node.text.strip
|
198
|
+
end
|
199
|
+
end
|
192
200
|
|
193
|
-
|
194
|
-
|
195
|
-
response
|
201
|
+
response[:success] = response[:status] == '0'.freeze
|
202
|
+
response[:message] = response[:success] ? 'Successfully submitted the order'.freeze : message_from(response[:error_message])
|
203
|
+
response
|
196
204
|
end
|
205
|
+
end
|
197
206
|
|
198
|
-
|
199
|
-
|
200
|
-
|
207
|
+
def compute_stock_levels(document)
|
208
|
+
items = {}
|
209
|
+
products = document.xpath('//Product'.freeze)
|
210
|
+
products.each do |product|
|
211
|
+
qty = product.at_xpath('@quantity'.freeze).child.content.to_i
|
212
|
+
code = product.at_xpath('@code'.freeze).child.content
|
213
|
+
pending_qty = include_pending_stock? ? product.at_xpath('@pending'.freeze).child.content.to_i : 0
|
214
|
+
items[code] = qty + pending_qty
|
215
|
+
end
|
216
|
+
items
|
201
217
|
end
|
202
218
|
|
203
219
|
def parse_inventory_response(xml)
|
204
|
-
response = {}
|
205
|
-
response
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
220
|
+
response = { stock_levels: {} }
|
221
|
+
Parsing.with_xml_document(xml, response) do |document|
|
222
|
+
status = document.at_xpath('//Status'.freeze).child.content
|
223
|
+
total_products = document.at_xpath('//TotalProducts'.freeze).child.content
|
224
|
+
success = test? ? status == 'Test'.freeze : status == '0'.freeze
|
225
|
+
message = success ? 'Successfully received the stock levels'.freeze : document.at_xpath('//ErrorMessage'.freeze).child.content
|
226
|
+
|
227
|
+
{
|
228
|
+
status: status,
|
229
|
+
total_products: total_products,
|
230
|
+
stock_levels: compute_stock_levels(document),
|
231
|
+
message: message,
|
232
|
+
success: success
|
233
|
+
}
|
218
234
|
end
|
235
|
+
end
|
219
236
|
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
response
|
237
|
+
def shipped_order?(node)
|
238
|
+
node.name == 'Order'.freeze && node.attributes['shipped'.freeze].text == 'YES'.freeze
|
224
239
|
end
|
225
240
|
|
226
241
|
def parse_tracking_response(xml)
|
227
|
-
response = {
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
242
|
+
response = {
|
243
|
+
tracking_numbers: {},
|
244
|
+
tracking_companies: {},
|
245
|
+
tracking_urls: {}
|
246
|
+
}
|
247
|
+
|
248
|
+
Parsing.with_xml_document(xml, response) do |document, response|
|
249
|
+
document.root.try do |root_document|
|
250
|
+
root_document.elements.each do |node|
|
251
|
+
if shipped_order?(node)
|
252
|
+
node_tracking = node.at_css('TrackingNumber'.freeze)
|
253
|
+
unless node_tracking.nil?
|
254
|
+
node_id = node.attributes['id'.freeze].text.strip
|
255
|
+
tracking_number = node_tracking.text.strip
|
256
|
+
response[:tracking_numbers][node_id] = [tracking_number]
|
257
|
+
|
258
|
+
tracking_company = node_tracking.attributes['carrier'.freeze].try { |item| item.text.strip }
|
259
|
+
response[:tracking_companies][node_id] = [tracking_company] if tracking_company
|
260
|
+
|
261
|
+
tracking_url = node_tracking.attributes['href'.freeze].try { |item| item.text.strip }
|
262
|
+
response[:tracking_urls][node_id] = [tracking_url] if tracking_url
|
263
|
+
end
|
264
|
+
else
|
265
|
+
response[node.name.underscore.to_sym] = node.text.strip
|
266
|
+
end
|
244
267
|
end
|
245
|
-
else
|
246
|
-
response[node.name.underscore.to_sym] = text_content(node)
|
247
268
|
end
|
248
269
|
end
|
249
270
|
|
250
|
-
response[:success] = test? ? (response[:status] == '0' || response[:status] == 'Test') : response[:status] == '0'
|
251
|
-
response[:message] = response[:success] ?
|
271
|
+
response[:success] = test? ? (response[:status] == '0'.freeze || response[:status] == 'Test'.freeze) : response[:status] == '0'.freeze
|
272
|
+
response[:message] = response[:success] ? 'Successfully received the tracking numbers'.freeze : message_from(response[:error_message])
|
252
273
|
response
|
253
274
|
end
|
254
275
|
|
255
276
|
def message_from(string)
|
256
277
|
return if string.blank?
|
257
|
-
string.gsub("\n", '').squeeze(
|
258
|
-
end
|
259
|
-
|
260
|
-
def text_content(xml_node)
|
261
|
-
text = xml_node.text
|
262
|
-
text = xml_node.cdatas.join if text.blank?
|
263
|
-
text
|
278
|
+
string.gsub("\n", ''.freeze).squeeze(' '.freeze)
|
264
279
|
end
|
265
280
|
end
|
266
281
|
end
|
@@ -3,7 +3,7 @@ require 'active_support/core_ext/object/to_query'
|
|
3
3
|
module ActiveFulfillment
|
4
4
|
class ShopifyAPIService < Service
|
5
5
|
|
6
|
-
OrderIdCutoffDate = Date.iso8601(
|
6
|
+
OrderIdCutoffDate = Date.iso8601('2015-03-01').freeze
|
7
7
|
|
8
8
|
RESCUABLE_CONNECTION_ERRORS = [
|
9
9
|
Net::ReadTimeout,
|
@@ -28,7 +28,7 @@ module ActiveFulfillment
|
|
28
28
|
ActiveUtils::ConnectionError,
|
29
29
|
ActiveUtils::ResponseError,
|
30
30
|
ActiveUtils::InvalidResponseError
|
31
|
-
]
|
31
|
+
].freeze
|
32
32
|
|
33
33
|
def initialize(options = {})
|
34
34
|
@name = options[:name]
|
@@ -37,27 +37,27 @@ module ActiveFulfillment
|
|
37
37
|
end
|
38
38
|
|
39
39
|
def fulfill(order_id, shipping_address, line_items, options = {})
|
40
|
-
raise NotImplementedError.new(
|
40
|
+
raise NotImplementedError.new('Shopify API Service must listen to fulfillment/create Webhooks'.freeze)
|
41
41
|
end
|
42
42
|
|
43
43
|
def fetch_stock_levels(options = {})
|
44
|
-
response = send_app_request('fetch_stock', options.delete(:headers), options)
|
44
|
+
response = send_app_request('fetch_stock'.freeze, options.delete(:headers), options)
|
45
45
|
if response
|
46
|
-
stock_levels = parse_response(response, 'StockLevels', 'Product', 'Sku', 'Quantity') { |p| p.to_i }
|
47
|
-
Response.new(true,
|
46
|
+
stock_levels = parse_response(response, 'StockLevels'.freeze, 'Product'.freeze, 'Sku'.freeze, 'Quantity'.freeze) { |p| p.to_i }
|
47
|
+
Response.new(true, 'API stock levels'.freeze, {:stock_levels => stock_levels})
|
48
48
|
else
|
49
|
-
Response.new(false,
|
49
|
+
Response.new(false, 'Unable to fetch remote stock levels'.freeze)
|
50
50
|
end
|
51
51
|
end
|
52
52
|
|
53
53
|
def fetch_tracking_data(order_numbers, options = {})
|
54
54
|
options.merge!({:order_ids => order_numbers, :order_names => order_numbers})
|
55
|
-
response = send_app_request('fetch_tracking_numbers', options.delete(:headers), options)
|
55
|
+
response = send_app_request('fetch_tracking_numbers'.freeze, options.delete(:headers), options)
|
56
56
|
if response
|
57
|
-
tracking_numbers = parse_response(response, 'TrackingNumbers', 'Order', 'ID', 'Tracking') { |o| o }
|
58
|
-
Response.new(true,
|
59
|
-
|
60
|
-
|
57
|
+
tracking_numbers = parse_response(response, 'TrackingNumbers'.freeze, 'Order'.freeze, 'ID'.freeze, 'Tracking'.freeze) { |o| o }
|
58
|
+
Response.new(true, 'API tracking_numbers'.freeze, {:tracking_numbers => tracking_numbers,
|
59
|
+
:tracking_companies => {},
|
60
|
+
:tracking_urls => {}})
|
61
61
|
else
|
62
62
|
Response.new(false, "Unable to fetch remote tracking numbers #{order_numbers.inspect}")
|
63
63
|
end
|
@@ -72,7 +72,7 @@ module ActiveFulfillment
|
|
72
72
|
def send_app_request(action, headers, data)
|
73
73
|
uri = request_uri(action, data)
|
74
74
|
|
75
|
-
logger.info "[
|
75
|
+
logger.info "[#{@name.upcase} APP] Post #{uri}"
|
76
76
|
|
77
77
|
response = nil
|
78
78
|
realtime = Benchmark.realtime do
|
@@ -85,39 +85,46 @@ module ActiveFulfillment
|
|
85
85
|
end
|
86
86
|
end
|
87
87
|
|
88
|
-
line = "[
|
88
|
+
line = "[#{@name.upcase} APP] Response from #{uri} --> "
|
89
89
|
line << "#{response} #{"%.4fs" % realtime}"
|
90
90
|
logger.info line
|
91
91
|
|
92
92
|
response
|
93
93
|
end
|
94
94
|
|
95
|
-
def
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
95
|
+
def parse_json(json_data, root)
|
96
|
+
response_data = ActiveSupport::JSON.decode(json_data)
|
97
|
+
return {} unless response_data.is_a?(Hash)
|
98
|
+
response_data[root.underscore] || response_data
|
99
|
+
rescue ActiveSupport::JSON.parse_error
|
100
|
+
{}
|
101
|
+
end
|
102
|
+
|
103
|
+
|
104
|
+
def parse_xml(xml_data, type, key, value)
|
105
|
+
Parsing.with_xml_document(xml_data) do |document, response|
|
106
|
+
# Extract All elements of type and map them!
|
107
|
+
root_element = document.xpath("//#{type}")
|
108
|
+
root_element.each do |type_item|
|
109
|
+
key_item = type_item.at_css(key).child.content
|
110
|
+
value_item = type_item.at_css(value).child.content
|
111
|
+
response[key_item] = value_item
|
108
112
|
end
|
109
|
-
|
113
|
+
response
|
110
114
|
end
|
115
|
+
end
|
111
116
|
|
112
|
-
|
117
|
+
def parse_response(response, root, type, key, value)
|
118
|
+
return parse_json(response, root) if @format == 'json'.freeze
|
119
|
+
return parse_xml(response, type, key, value) if @format == 'xml'.freeze
|
113
120
|
{}
|
114
121
|
end
|
115
122
|
|
116
123
|
def encode_payload(payload, root)
|
117
124
|
case @format
|
118
|
-
when 'json'
|
125
|
+
when 'json'.freeze
|
119
126
|
{root => payload}.to_json
|
120
|
-
when 'xml'
|
127
|
+
when 'xml'.freeze
|
121
128
|
payload.to_xml(:root => root)
|
122
129
|
end
|
123
130
|
end
|
@@ -1,24 +1,63 @@
|
|
1
1
|
module ActiveFulfillment
|
2
2
|
class WebgistixService < Service
|
3
|
+
|
3
4
|
SERVICE_URLS = {
|
4
5
|
:fulfillment => 'https://www.webgistix.com/XML/CreateOrder.asp',
|
5
6
|
:inventory => 'https://www.webgistix.com/XML/GetInventory.asp',
|
6
7
|
:tracking => 'https://www.webgistix.com/XML/GetTracking.asp'
|
7
|
-
}
|
8
|
-
|
8
|
+
}.freeze
|
9
|
+
|
10
|
+
TEST_URLS = SERVICE_URLS.dup.merge({
|
9
11
|
:fulfillment => 'https://www.webgistix.com/XML/CreateOrderTest.asp'
|
10
|
-
})
|
11
|
-
|
12
|
-
SUCCESS, DUPLICATE, FAILURE = 'True', 'Duplicate', 'False'
|
13
|
-
|
14
|
-
SUCCESS_MESSAGE = 'Successfully submitted the order'
|
15
|
-
FAILURE_MESSAGE = 'Failed to submit the order'
|
16
|
-
DUPLICATE_MESSAGE = 'This order has already been successfully submitted'
|
17
|
-
|
18
|
-
INVALID_LOGIN = 'Invalid Credentials'
|
19
|
-
NOT_SHIPPED = 'Not Shipped'
|
20
|
-
|
21
|
-
TRACKING_COMPANIES = %w(UPS FedEx USPS)
|
12
|
+
}).freeze
|
13
|
+
|
14
|
+
SUCCESS, DUPLICATE, FAILURE = 'True'.freeze, 'Duplicate'.freeze, 'False'.freeze
|
15
|
+
|
16
|
+
SUCCESS_MESSAGE = 'Successfully submitted the order'.freeze
|
17
|
+
FAILURE_MESSAGE = 'Failed to submit the order'.freeze
|
18
|
+
DUPLICATE_MESSAGE = 'This order has already been successfully submitted'.freeze
|
19
|
+
|
20
|
+
INVALID_LOGIN = 'Invalid Credentials'.freeze
|
21
|
+
NOT_SHIPPED = 'Not Shipped'.freeze
|
22
|
+
|
23
|
+
TRACKING_COMPANIES = %w(UPS FedEx USPS).freeze
|
24
|
+
|
25
|
+
SHIPPING_PROVIDERS = {
|
26
|
+
'UPS Ground Shipping' => 'Ground',
|
27
|
+
'UPS Ground' => 'Ground',
|
28
|
+
'UPS Standard Shipping (Canada Only)' => 'Standard',
|
29
|
+
'UPS Standard Shipping (CA & MX Only)' => 'Standard',
|
30
|
+
'UPS 3-Business Day' => '3-Day Select',
|
31
|
+
'UPS 2-Business Day' => '2nd Day Air',
|
32
|
+
'UPS 2-Business Day AM' => '2nd Day Air AM',
|
33
|
+
'UPS Next Day' => 'Next Day Air',
|
34
|
+
'UPS Next Day Saver' => 'Next Day Air Saver',
|
35
|
+
'UPS Next Day Early AM' => 'Next Day Air Early AM',
|
36
|
+
'UPS Worldwide Express (Next Day)' => 'Worldwide Express',
|
37
|
+
'UPS Worldwide Expedited (2nd Day)' => 'Worldwide Expedited',
|
38
|
+
'UPS Worldwide Express Saver' => 'Worldwide Express Saver',
|
39
|
+
'FedEx Priority Overnight' => 'FedEx Priority Overnight',
|
40
|
+
'FedEx Standard Overnight' => 'FedEx Standard Overnight',
|
41
|
+
'FedEx First Overnight' => 'FedEx First Overnight',
|
42
|
+
'FedEx 2nd Day' => 'FedEx 2nd Day',
|
43
|
+
'FedEx Express Saver' => 'FedEx Express Saver',
|
44
|
+
'FedEx International Priority' => 'FedEx International Priority',
|
45
|
+
'FedEx International Economy' => 'FedEx International Economy',
|
46
|
+
'FedEx International First' => 'FedEx International First',
|
47
|
+
'FedEx Ground' => 'FedEx Ground',
|
48
|
+
'USPS Priority Mail' => 'Priority Mail',
|
49
|
+
'USPS Priority Mail International' => 'Priority Mail International',
|
50
|
+
'USPS Priority Mail Small Flat Rate Box' => 'Priority Mail Small Flat Rate Box',
|
51
|
+
'USPS Priority Mail Medium Flat Rate Box' => 'Priority Mail Medium Flat Rate Box',
|
52
|
+
'USPS Priority Mail Large Flat Rate Box' => "Priority Mail Large Flat Rate Box",
|
53
|
+
'USPS Priority Mail Flat Rate Envelope' => 'Priority Mail Flat Rate Envelope',
|
54
|
+
'USPS First Class Mail' => 'First Class',
|
55
|
+
'USPS First Class International' => 'First Class International',
|
56
|
+
'USPS Express Mail' => 'Express',
|
57
|
+
'USPS Express Mail International' => 'Express Mail International',
|
58
|
+
'USPS Parcel Post' => 'Parcel',
|
59
|
+
'USPS Media Mail' => 'Media Mail'
|
60
|
+
}.freeze
|
22
61
|
|
23
62
|
# If a request is detected as a duplicate only the original data will be
|
24
63
|
# used by Webgistix, and the subsequent responses will have a
|
@@ -27,42 +66,7 @@ module ActiveFulfillment
|
|
27
66
|
|
28
67
|
# The first is the label, and the last is the code
|
29
68
|
def self.shipping_methods
|
30
|
-
|
31
|
-
["UPS Ground Shipping", "Ground"],
|
32
|
-
["UPS Ground", "Ground"],
|
33
|
-
["UPS Standard Shipping (Canada Only)", "Standard"],
|
34
|
-
["UPS Standard Shipping (CA & MX Only)", "Standard"],
|
35
|
-
["UPS 3-Business Day", "3-Day Select"],
|
36
|
-
["UPS 2-Business Day", "2nd Day Air"],
|
37
|
-
["UPS 2-Business Day AM", "2nd Day Air AM"],
|
38
|
-
["UPS Next Day", "Next Day Air"],
|
39
|
-
["UPS Next Day Saver", "Next Day Air Saver"],
|
40
|
-
["UPS Next Day Early AM", "Next Day Air Early AM"],
|
41
|
-
["UPS Worldwide Express (Next Day)", "Worldwide Express"],
|
42
|
-
["UPS Worldwide Expedited (2nd Day)", "Worldwide Expedited"],
|
43
|
-
["UPS Worldwide Express Saver", "Worldwide Express Saver"],
|
44
|
-
["FedEx Priority Overnight", "FedEx Priority Overnight"],
|
45
|
-
["FedEx Standard Overnight", "FedEx Standard Overnight"],
|
46
|
-
["FedEx First Overnight", "FedEx First Overnight"],
|
47
|
-
["FedEx 2nd Day", "FedEx 2nd Day"],
|
48
|
-
["FedEx Express Saver", "FedEx Express Saver"],
|
49
|
-
["FedEx International Priority", "FedEx International Priority"],
|
50
|
-
["FedEx International Economy", "FedEx International Economy"],
|
51
|
-
["FedEx International First", "FedEx International First"],
|
52
|
-
["FedEx Ground", "FedEx Ground"],
|
53
|
-
["USPS Priority Mail", "Priority Mail"],
|
54
|
-
["USPS Priority Mail International", "Priority Mail International"],
|
55
|
-
["USPS Priority Mail Small Flat Rate Box", "Priority Mail Small Flat Rate Box"],
|
56
|
-
["USPS Priority Mail Medium Flat Rate Box", "Priority Mail Medium Flat Rate Box"],
|
57
|
-
["USPS Priority Mail Large Flat Rate Box", "Priority Mail Large Flat Rate Box"],
|
58
|
-
["USPS Priority Mail Flat Rate Envelope", "Priority Mail Flat Rate Envelope"],
|
59
|
-
["USPS First Class Mail", "First Class"],
|
60
|
-
["USPS First Class International", "First Class International"],
|
61
|
-
["USPS Express Mail", "Express"],
|
62
|
-
["USPS Express Mail International", "Express Mail International"],
|
63
|
-
["USPS Parcel Post", "Parcel"],
|
64
|
-
["USPS Media Mail", "Media Mail"]
|
65
|
-
].inject({}){|h, (k,v)| h[k] = v; h}
|
69
|
+
SHIPPING_PROVIDERS
|
66
70
|
end
|
67
71
|
|
68
72
|
# Pass in the login and password for the shipwire account.
|
@@ -245,8 +249,8 @@ module ActiveFulfillment
|
|
245
249
|
|
246
250
|
def parse_response(action, xml)
|
247
251
|
begin
|
248
|
-
document =
|
249
|
-
rescue
|
252
|
+
document = Nokogiri::XML("<response>#{xml}</response>")
|
253
|
+
rescue Nokogiri::XML::SyntaxError
|
250
254
|
return {:success => FAILURE}
|
251
255
|
end
|
252
256
|
|
@@ -266,7 +270,7 @@ module ActiveFulfillment
|
|
266
270
|
response = parse_errors(document)
|
267
271
|
|
268
272
|
# Check if completed
|
269
|
-
if completed =
|
273
|
+
if completed = document.at_xpath('//Completed'.freeze)
|
270
274
|
completed.elements.each do |e|
|
271
275
|
response[e.name.underscore.to_sym] = e.text
|
272
276
|
end
|
@@ -283,11 +287,11 @@ module ActiveFulfillment
|
|
283
287
|
response = parse_errors(document)
|
284
288
|
response[:stock_levels] = {}
|
285
289
|
|
286
|
-
document.root.
|
290
|
+
document.root.xpath('//Item'.freeze).each do |node|
|
287
291
|
# {ItemID => 'SOME-ID', ItemQty => '101'}
|
288
292
|
params = node.elements.to_a.each_with_object({}) {|elem, hash| hash[elem.name] = elem.text}
|
289
293
|
|
290
|
-
response[:stock_levels][params['ItemID']] = params['ItemQty'].to_i
|
294
|
+
response[:stock_levels][params['ItemID'.freeze]] = params['ItemQty'.freeze].to_i
|
291
295
|
end
|
292
296
|
|
293
297
|
response
|
@@ -295,25 +299,23 @@ module ActiveFulfillment
|
|
295
299
|
|
296
300
|
def parse_tracking_response(document)
|
297
301
|
response = parse_errors(document)
|
298
|
-
response
|
299
|
-
response[:tracking_companies] = {}
|
300
|
-
response[:tracking_urls] = {}
|
302
|
+
response = response.merge(tracking_numbers: {}, tracking_companies: {}, tracking_urls: {})
|
301
303
|
|
302
|
-
document.root.
|
304
|
+
document.root.xpath('//Shipment'.freeze).each do |node|
|
303
305
|
# {InvoiceNumber => 'SOME-ID', ShipmentTrackingNumber => 'SOME-TRACKING-NUMBER'}
|
304
306
|
params = node.elements.to_a.each_with_object({}) {|elem, hash| hash[elem.name] = elem.text}
|
305
307
|
|
306
|
-
tracking = params['ShipmentTrackingNumber']
|
308
|
+
tracking = params['ShipmentTrackingNumber'.freeze]
|
307
309
|
|
308
310
|
unless tracking == NOT_SHIPPED
|
309
|
-
response[:tracking_numbers][params['InvoiceNumber']] ||= []
|
310
|
-
response[:tracking_numbers][params['InvoiceNumber']] << tracking
|
311
|
+
response[:tracking_numbers][params['InvoiceNumber'.freeze]] ||= []
|
312
|
+
response[:tracking_numbers][params['InvoiceNumber'.freeze]] << tracking
|
311
313
|
end
|
312
314
|
|
313
|
-
company = params['Method'].split[0] if params['Method']
|
315
|
+
company = params['Method'.freeze].split[0] if params['Method'.freeze]
|
314
316
|
if TRACKING_COMPANIES.include? company
|
315
|
-
response[:tracking_companies][params['InvoiceNumber']] ||= []
|
316
|
-
response[:tracking_companies][params['InvoiceNumber']] << company
|
317
|
+
response[:tracking_companies][params['InvoiceNumber'.freeze]] ||= []
|
318
|
+
response[:tracking_companies][params['InvoiceNumber'.freeze]] << company
|
317
319
|
end
|
318
320
|
end
|
319
321
|
|
@@ -323,7 +325,7 @@ module ActiveFulfillment
|
|
323
325
|
def parse_errors(document)
|
324
326
|
response = {}
|
325
327
|
|
326
|
-
|
328
|
+
document.xpath('//Errors/Error'.freeze).each_with_index do |e, i|
|
327
329
|
response["error_#{i}".to_sym] = e.text
|
328
330
|
end
|
329
331
|
|
data/test/test_helper.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: active_fulfillment
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 3.0.
|
4
|
+
version: 3.0.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Cody Fauser
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2016-01-07 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: activesupport
|
@@ -53,6 +53,20 @@ dependencies:
|
|
53
53
|
- - "~>"
|
54
54
|
- !ruby/object:Gem::Version
|
55
55
|
version: '3.0'
|
56
|
+
- !ruby/object:Gem::Dependency
|
57
|
+
name: nokogiri
|
58
|
+
requirement: !ruby/object:Gem::Requirement
|
59
|
+
requirements:
|
60
|
+
- - "~>"
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: '1.6'
|
63
|
+
type: :runtime
|
64
|
+
prerelease: false
|
65
|
+
version_requirements: !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - "~>"
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '1.6'
|
56
70
|
- !ruby/object:Gem::Dependency
|
57
71
|
name: rake
|
58
72
|
requirement: !ruby/object:Gem::Requirement
|
@@ -118,6 +132,7 @@ files:
|
|
118
132
|
- CHANGELOG.md
|
119
133
|
- lib/active_fulfillment.rb
|
120
134
|
- lib/active_fulfillment/base.rb
|
135
|
+
- lib/active_fulfillment/parsing.rb
|
121
136
|
- lib/active_fulfillment/response.rb
|
122
137
|
- lib/active_fulfillment/service.rb
|
123
138
|
- lib/active_fulfillment/services.rb
|
@@ -164,13 +179,13 @@ summary: Framework and tools for dealing with shipping, tracking and order fulfi
|
|
164
179
|
services.
|
165
180
|
test_files:
|
166
181
|
- test/test_helper.rb
|
167
|
-
- test/remote/webgistix_test.rb
|
168
|
-
- test/remote/shipwire_test.rb
|
169
|
-
- test/remote/james_and_james_test.rb
|
170
182
|
- test/remote/amazon_mws_test.rb
|
183
|
+
- test/remote/james_and_james_test.rb
|
184
|
+
- test/remote/shipwire_test.rb
|
185
|
+
- test/remote/webgistix_test.rb
|
186
|
+
- test/unit/services/amazon_mws_test.rb
|
187
|
+
- test/unit/services/james_and_james_test.rb
|
171
188
|
- test/unit/services/shopify_api_test.rb
|
172
|
-
- test/unit/services/webgistix_test.rb
|
173
189
|
- test/unit/services/shipwire_test.rb
|
174
|
-
- test/unit/services/
|
175
|
-
- test/unit/services/amazon_mws_test.rb
|
190
|
+
- test/unit/services/webgistix_test.rb
|
176
191
|
- test/unit/base_test.rb
|