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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 81e4d46fc9e29790dedd89eeab465416f9bc2b8e
4
- data.tar.gz: f8b1cb46b86a27625bb50fc3a68e4c301eed1315
3
+ metadata.gz: edee633e1484a24673765631ab0a8a85cb114f4d
4
+ data.tar.gz: 1b3c6f2099542ca7f988a0a7ac9ee9f3309cc809
5
5
  SHA512:
6
- metadata.gz: faff4e5e9c15dda5586ead4afcb10a973d78ddbf56f4bac42ae7403e7a90ff1f2558287ee23e0878c08dc0bea01259ad03d28a47ce12fcdc63a3c8a7b28285e5
7
- data.tar.gz: 06bbeab03d5155dd553af4ffe72fae585aeb06764e904218f5310e75c6f04933e90b0adeb71a95da7289b7efd9953ef40ed0f10ee01f5bbb72c12488268e1393
6
+ metadata.gz: 21c48116701f5281c5a4deacc5a445126e12b6e407551fc097709d579231a31d74944e88eb404b476bc2667d073686717dbce7c79388608600f73445b10a2538
7
+ data.tar.gz: 90872e2cce274d7bc42593f93da678bf8a234fed5c4b12d3bf4e83d8ae665ab046cf22c7ded2e26fb8f1425aa0a108d256c61ca7060f3434268e1c713bc81b31
data/CHANGELOG.md CHANGED
@@ -1,7 +1,13 @@
1
1
  # ActiveFulfillment changelog
2
2
 
3
- ## Unreleased
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
@@ -36,7 +36,7 @@ end
36
36
  require 'builder'
37
37
  require 'cgi'
38
38
  require 'net/https'
39
- require 'rexml/document'
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 = "active_merchant_mws/0.01 (Language=ruby)"
9
+ APPLICATION_IDENTIFIER = 'active_merchant_mws/0.01 (Language=ruby)'.freeze
10
10
 
11
- REGISTRATION_URI = URI.parse("https://sellercentral.amazon.com/gp/mws/registration/register.html")
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 = "SHA256"
15
- VERSION = "2010-10-01"
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(parse_document(data), 'Successfully submitted the order')
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 = REXML::Document.new(xml)
199
- rescue REXML::ParseException
200
- return { :success => FAILURE }
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
- response[:tracking_numbers] = {}
207
- response[:tracking_companies] = {}
208
- response[:tracking_urls] = {}
208
+ response = {
209
+ tracking_numbers: {},
210
+ tracking_companies: {},
211
+ tracking_urls: {}
212
+ }
209
213
 
210
- tracking_numbers = REXML::XPath.match(document, "//FulfillmentShipmentPackage/member/TrackingNumber")
214
+ tracking_numbers = document.css('FulfillmentShipmentPackage > member > TrackingNumber'.freeze)
211
215
  if tracking_numbers.present?
212
- order_id = REXML::XPath.first(document, "//FulfillmentOrder/SellerFulfillmentOrderId").text.strip
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 = REXML::XPath.match(document, "//FulfillmentShipmentPackage/member/CarrierCode")
220
+ tracking_companies = document.css('FulfillmentShipmentPackage > member > CarrierCode'.freeze)
217
221
  if tracking_companies.present?
218
- order_id = REXML::XPath.first(document, "//FulfillmentOrder/SellerFulfillmentOrderId").text.strip
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(document, message)
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.each_element('//InventorySupplyList/member') do |node|
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 = REXML::XPath.first(document, '//NextToken')
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
- response[:http_code] = http_response.code
250
- response[:http_message] = http_response.message
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
- node = REXML::XPath.first(document, '//Error')
255
- error_code = REXML::XPath.first(node, '//Code')
256
- error_message = REXML::XPath.first(node, '//Message')
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 REXML::ParseException => e
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,7 +6,7 @@ module ActiveFulfillment
6
6
  SERVICE_URLS = {
7
7
  fulfillment: 'https://%{subdomain}.sixworks.co.uk/api/1/',
8
8
  inventory: 'https://%{subdomain}.sixworks.co.uk/api/1/stock'
9
- }
9
+ }.freeze
10
10
 
11
11
  def initialize(options = {})
12
12
  requires!(options, :subdomain, :key)
@@ -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
- [ ['1 Day Service', '1D'],
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
- response = {}
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
- document = REXML::Document.new(xml)
194
- document.root.elements.each do |node|
195
- response[node.name.underscore.to_sym] = text_content(node)
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
- response[:success] = response[:status] == '0'
199
- response[:message] = response[:success] ? "Successfully submitted the order" : message_from(response[:error_message])
200
- response
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[:stock_levels] = {}
206
-
207
- document = REXML::Document.new(xml)
208
- document.root.elements.each do |node|
209
- if node.name == 'Product'
210
- to_check = ['quantity']
211
- to_check << 'pending' if include_pending_stock?
212
-
213
- amount = to_check.sum { |a| node.attributes[a].to_i }
214
- response[:stock_levels][node.attributes['code']] = amount
215
- else
216
- response[node.name.underscore.to_sym] = text_content(node)
217
- end
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
- response[:success] = test? ? response[:status] == 'Test' : response[:status] == '0'
221
- response[:message] = response[:success] ? "Successfully received the stock levels" : message_from(response[:error_message])
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
- response[:tracking_numbers] = {}
229
- response[:tracking_companies] = {}
230
- response[:tracking_urls] = {}
231
-
232
- document = REXML::Document.new(xml)
233
- document.root.elements.each do |node|
234
- if node.name == 'Order'
235
- if node.attributes["shipped"] == "YES" && node.elements['TrackingNumber']
236
- tracking_number = node.elements['TrackingNumber'].text.strip
237
- response[:tracking_numbers][node.attributes['id']] = [tracking_number]
238
-
239
- tracking_company = node.elements['TrackingNumber'].attributes['carrier']
240
- response[:tracking_companies][node.attributes['id']] = [tracking_company.strip] if tracking_company
241
-
242
- tracking_url = node.elements['TrackingNumber'].attributes['href']
243
- response[:tracking_urls][node.attributes['id']] = [tracking_url.strip] if tracking_url
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] ? "Successfully received the tracking numbers" : message_from(response[:error_message])
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("2015-03-01")
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("Shopify API Service must listen to fulfillment/create Webhooks")
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, "API stock levels", {:stock_levels => stock_levels})
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, "Unable to fetch remote stock levels")
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, "API tracking_numbers", {:tracking_numbers => tracking_numbers,
59
- :tracking_companies => {},
60
- :tracking_urls => {}})
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 "[" + @name.upcase + " APP] Post #{uri}"
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 = "[" + @name.upcase + "APP] Response from #{uri} --> "
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 parse_response(response, root, type, key, value)
96
- case @format
97
- when 'json'
98
- response_data = ActiveSupport::JSON.decode(response)
99
- return {} unless response_data.is_a?(Hash)
100
- response_data[root.underscore] || response_data
101
- when 'xml'
102
- response_data = {}
103
- document = REXML::Document.new(response)
104
- document.elements[root].each do |node|
105
- if node.name == type
106
- response_data[node.elements[key].text] = node.elements[value].text
107
- end
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
- response_data
113
+ response
110
114
  end
115
+ end
111
116
 
112
- rescue ActiveSupport::JSON.parse_error, REXML::ParseException
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
- TEST_URLS = SERVICE_URLS.merge({
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 = REXML::Document.new("<response>#{xml}</response>")
249
- rescue REXML::ParseException
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 = REXML::XPath.first(document, '//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.each_element('//Item') do |node|
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[:tracking_numbers] = {}
299
- response[:tracking_companies] = {}
300
- response[:tracking_urls] = {}
302
+ response = response.merge(tracking_numbers: {}, tracking_companies: {}, tracking_urls: {})
301
303
 
302
- document.root.each_element('//Shipment') do |node|
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
- REXML::XPath.match(document, "//Errors/Error").to_a.each_with_index do |e, i|
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
 
@@ -1,4 +1,4 @@
1
1
  # encoding: utf-8
2
2
  module ActiveFulfillment
3
- VERSION = "3.0.0.pre8"
3
+ VERSION = "3.0.1"
4
4
  end
data/test/test_helper.rb CHANGED
@@ -5,6 +5,7 @@ require 'active_fulfillment'
5
5
  require 'minitest/autorun'
6
6
  require 'mocha/setup'
7
7
  require 'timecop'
8
+ require 'rexml/document'
8
9
 
9
10
  require 'logger'
10
11
  ActiveFulfillment::Service.logger = Logger.new(nil)
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.0.pre8
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: 2015-12-10 00:00:00.000000000 Z
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/james_and_james_test.rb
175
- - test/unit/services/amazon_mws_test.rb
190
+ - test/unit/services/webgistix_test.rb
176
191
  - test/unit/base_test.rb