active_fulfillment 3.0.0.pre8 → 3.0.1
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.
- 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
|