active_fulfillment 2.1.9 → 3.0.0.pre2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +68 -0
- data/lib/active_fulfillment.rb +5 -5
- data/lib/active_fulfillment/base.rb +10 -0
- data/lib/active_fulfillment/response.rb +26 -0
- data/lib/active_fulfillment/service.rb +56 -0
- data/lib/active_fulfillment/services.rb +5 -0
- data/lib/active_fulfillment/services/amazon_mws.rb +473 -0
- data/lib/active_fulfillment/services/james_and_james.rb +122 -0
- data/lib/active_fulfillment/services/shipwire.rb +266 -0
- data/lib/active_fulfillment/services/shopify_api.rb +125 -0
- data/lib/active_fulfillment/services/webgistix.rb +334 -0
- data/lib/active_fulfillment/version.rb +4 -0
- data/test/remote/amazon_mws_test.rb +20 -17
- data/test/remote/james_and_james_test.rb +77 -0
- data/test/remote/shipwire_test.rb +25 -25
- data/test/remote/webgistix_test.rb +21 -21
- data/test/test_helper.rb +27 -52
- data/test/unit/base_test.rb +4 -4
- data/test/unit/services/amazon_mws_test.rb +56 -26
- data/test/unit/services/james_and_james_test.rb +90 -0
- data/test/unit/services/shipwire_test.rb +18 -18
- data/test/unit/services/shopify_api_test.rb +7 -20
- data/test/unit/services/webgistix_test.rb +35 -35
- metadata +32 -114
- data/CHANGELOG +0 -62
- data/lib/active_fulfillment/fulfillment/base.rb +0 -12
- data/lib/active_fulfillment/fulfillment/response.rb +0 -28
- data/lib/active_fulfillment/fulfillment/service.rb +0 -58
- data/lib/active_fulfillment/fulfillment/services.rb +0 -5
- data/lib/active_fulfillment/fulfillment/services/amazon.rb +0 -389
- data/lib/active_fulfillment/fulfillment/services/amazon_mws.rb +0 -454
- data/lib/active_fulfillment/fulfillment/services/shipwire.rb +0 -268
- data/lib/active_fulfillment/fulfillment/services/shopify_api.rb +0 -125
- data/lib/active_fulfillment/fulfillment/services/webgistix.rb +0 -338
- data/lib/active_fulfillment/fulfillment/version.rb +0 -6
- data/test/fixtures.yml +0 -16
- data/test/fixtures/xml/amazon/inventory_get_response.xml +0 -17
- data/test/fixtures/xml/amazon/inventory_list_response.xml +0 -29
- data/test/fixtures/xml/amazon/inventory_list_response_with_next_1.xml +0 -30
- data/test/fixtures/xml/amazon/inventory_list_response_with_next_2.xml +0 -29
- data/test/fixtures/xml/amazon/tracking_response_1.xml +0 -56
- data/test/fixtures/xml/amazon/tracking_response_2.xml +0 -38
- data/test/fixtures/xml/amazon/tracking_response_error.xml +0 -13
- data/test/fixtures/xml/amazon/tracking_response_not_found.xml +0 -13
- data/test/fixtures/xml/amazon_mws/fulfillment_get_fulfillment_order.xml +0 -114
- data/test/fixtures/xml/amazon_mws/fulfillment_get_fulfillment_order_2.xml +0 -90
- data/test/fixtures/xml/amazon_mws/fulfillment_get_fullfillment_order_with_multiple_tracking_numbers.xml +0 -121
- data/test/fixtures/xml/amazon_mws/fulfillment_list_all_fulfillment_orders.xml +0 -70
- data/test/fixtures/xml/amazon_mws/inventory_list_inventory_item_supply.xml +0 -32
- data/test/fixtures/xml/amazon_mws/inventory_list_inventory_supply.xml +0 -75
- data/test/fixtures/xml/amazon_mws/inventory_list_inventory_supply_by_next_token.xml +0 -38
- data/test/fixtures/xml/amazon_mws/tracking_response_error.xml +0 -9
- data/test/fixtures/xml/amazon_mws/tracking_response_not_found.xml +0 -9
- data/test/fixtures/xml/shipwire/fulfillment_failure_response.xml +0 -7
- data/test/fixtures/xml/shipwire/invalid_login_response.xml +0 -7
- data/test/fixtures/xml/shipwire/inventory_get_response.xml +0 -44
- data/test/fixtures/xml/shipwire/successful_empty_tracking_response.xml +0 -8
- data/test/fixtures/xml/shipwire/successful_live_tracking_response.xml +0 -53
- data/test/fixtures/xml/shipwire/successful_tracking_response.xml +0 -16
- data/test/fixtures/xml/shipwire/successful_tracking_response_with_tracking_urls.xml +0 -31
- data/test/fixtures/xml/webgistix/multiple_tracking_response.xml +0 -21
- data/test/fixtures/xml/webgistix/tracking_response.xml +0 -14
- data/test/remote/amazon_test.rb +0 -124
- data/test/unit/services/amazon_test.rb +0 -271
@@ -1,268 +0,0 @@
|
|
1
|
-
require 'cgi'
|
2
|
-
|
3
|
-
module ActiveMerchant
|
4
|
-
module Fulfillment
|
5
|
-
class ShipwireService < Service
|
6
|
-
|
7
|
-
SERVICE_URLS = { :fulfillment => 'https://api.shipwire.com/exec/FulfillmentServices.php',
|
8
|
-
:inventory => 'https://api.shipwire.com/exec/InventoryServices.php',
|
9
|
-
:tracking => 'https://api.shipwire.com/exec/TrackingServices.php'
|
10
|
-
}
|
11
|
-
|
12
|
-
SCHEMA_URLS = { :fulfillment => 'http://www.shipwire.com/exec/download/OrderList.dtd',
|
13
|
-
:inventory => 'http://www.shipwire.com/exec/download/InventoryUpdate.dtd',
|
14
|
-
:tracking => 'http://www.shipwire.com/exec/download/TrackingUpdate.dtd'
|
15
|
-
}
|
16
|
-
|
17
|
-
POST_VARS = { :fulfillment => 'OrderListXML',
|
18
|
-
:inventory => 'InventoryUpdateXML',
|
19
|
-
:tracking => 'TrackingUpdateXML'
|
20
|
-
}
|
21
|
-
|
22
|
-
WAREHOUSES = { 'CHI' => 'Chicago',
|
23
|
-
'LAX' => 'Los Angeles',
|
24
|
-
'REN' => 'Reno',
|
25
|
-
'VAN' => 'Vancouver',
|
26
|
-
'TOR' => 'Toronto',
|
27
|
-
'UK' => 'United Kingdom'
|
28
|
-
}
|
29
|
-
|
30
|
-
INVALID_LOGIN = /(Error with Valid Username\/EmailAddress and Password Required)|(Could not verify Username\/EmailAddress and Password combination)/
|
31
|
-
|
32
|
-
class_attribute :affiliate_id
|
33
|
-
|
34
|
-
# The first is the label, and the last is the code
|
35
|
-
def self.shipping_methods
|
36
|
-
[ ['1 Day Service', '1D'],
|
37
|
-
['2 Day Service', '2D'],
|
38
|
-
['Ground Service', 'GD'],
|
39
|
-
['Freight Service', 'FT'],
|
40
|
-
['International', 'INTL']
|
41
|
-
].inject(ActiveSupport::OrderedHash.new){|h, (k,v)| h[k] = v; h}
|
42
|
-
end
|
43
|
-
|
44
|
-
# Pass in the login and password for the shipwire account.
|
45
|
-
# Optionally pass in the :test => true to force test mode
|
46
|
-
def initialize(options = {})
|
47
|
-
requires!(options, :login, :password)
|
48
|
-
|
49
|
-
super
|
50
|
-
end
|
51
|
-
|
52
|
-
def fulfill(order_id, shipping_address, line_items, options = {})
|
53
|
-
commit :fulfillment, build_fulfillment_request(order_id, shipping_address, line_items, options)
|
54
|
-
end
|
55
|
-
|
56
|
-
def fetch_stock_levels(options = {})
|
57
|
-
commit :inventory, build_inventory_request(options)
|
58
|
-
end
|
59
|
-
|
60
|
-
def fetch_tracking_data(order_ids, options = {})
|
61
|
-
commit :tracking, build_tracking_request(order_ids)
|
62
|
-
end
|
63
|
-
|
64
|
-
def valid_credentials?
|
65
|
-
response = fetch_tracking_numbers([])
|
66
|
-
response.message !~ INVALID_LOGIN
|
67
|
-
end
|
68
|
-
|
69
|
-
def test_mode?
|
70
|
-
true
|
71
|
-
end
|
72
|
-
|
73
|
-
def include_pending_stock?
|
74
|
-
@options[:include_pending_stock]
|
75
|
-
end
|
76
|
-
|
77
|
-
def include_empty_stock?
|
78
|
-
@options[:include_empty_stock]
|
79
|
-
end
|
80
|
-
|
81
|
-
private
|
82
|
-
def build_fulfillment_request(order_id, shipping_address, line_items, options)
|
83
|
-
xml = Builder::XmlMarkup.new :indent => 2
|
84
|
-
xml.instruct!
|
85
|
-
xml.declare! :DOCTYPE, :OrderList, :SYSTEM, SCHEMA_URLS[:fulfillment]
|
86
|
-
xml.tag! 'OrderList' do
|
87
|
-
add_credentials(xml)
|
88
|
-
xml.tag! 'Referer', 'Active Fulfillment'
|
89
|
-
add_order(xml, order_id, shipping_address, line_items, options)
|
90
|
-
end
|
91
|
-
xml.target!
|
92
|
-
end
|
93
|
-
|
94
|
-
def build_inventory_request(options)
|
95
|
-
xml = Builder::XmlMarkup.new :indent => 2
|
96
|
-
xml.instruct!
|
97
|
-
xml.declare! :DOCTYPE, :InventoryStatus, :SYSTEM, SCHEMA_URLS[:inventory]
|
98
|
-
xml.tag! 'InventoryUpdate' do
|
99
|
-
add_credentials(xml)
|
100
|
-
xml.tag! 'Warehouse', WAREHOUSES[options[:warehouse]]
|
101
|
-
xml.tag! 'ProductCode', options[:sku]
|
102
|
-
xml.tag! 'IncludeEmpty' if include_empty_stock?
|
103
|
-
end
|
104
|
-
end
|
105
|
-
|
106
|
-
def build_tracking_request(order_ids)
|
107
|
-
xml = Builder::XmlMarkup.new
|
108
|
-
xml.instruct!
|
109
|
-
xml.declare! :DOCTYPE, :InventoryStatus, :SYSTEM, SCHEMA_URLS[:inventory]
|
110
|
-
xml.tag! 'TrackingUpdate' do
|
111
|
-
add_credentials(xml)
|
112
|
-
xml.tag! 'Server', test? ? 'Test' : 'Production'
|
113
|
-
order_ids.each do |o_id|
|
114
|
-
xml.tag! 'OrderNo', o_id
|
115
|
-
end
|
116
|
-
end
|
117
|
-
end
|
118
|
-
|
119
|
-
def add_credentials(xml)
|
120
|
-
xml.tag! 'EmailAddress', @options[:login]
|
121
|
-
xml.tag! 'Password', @options[:password]
|
122
|
-
xml.tag! 'Server', test? ? 'Test' : 'Production'
|
123
|
-
xml.tag! 'AffiliateId', affiliate_id if affiliate_id.present?
|
124
|
-
end
|
125
|
-
|
126
|
-
def add_order(xml, order_id, shipping_address, line_items, options)
|
127
|
-
xml.tag! 'Order', :id => order_id do
|
128
|
-
xml.tag! 'Warehouse', options[:warehouse] || '00'
|
129
|
-
|
130
|
-
add_address(xml, shipping_address, options)
|
131
|
-
xml.tag! 'Shipping', options[:shipping_method] unless options[:shipping_method].blank?
|
132
|
-
|
133
|
-
Array(line_items).each_with_index do |line_item, index|
|
134
|
-
add_item(xml, line_item, index)
|
135
|
-
end
|
136
|
-
xml.tag! 'Note' do
|
137
|
-
xml.cdata! options[:note] unless options[:note].blank?
|
138
|
-
end
|
139
|
-
end
|
140
|
-
end
|
141
|
-
|
142
|
-
def add_address(xml, address, options)
|
143
|
-
xml.tag! 'AddressInfo', :type => 'Ship' do
|
144
|
-
xml.tag! 'Name' do
|
145
|
-
xml.tag! 'Full', address[:name]
|
146
|
-
end
|
147
|
-
|
148
|
-
xml.tag! 'Address1', address[:address1]
|
149
|
-
xml.tag! 'Address2', address[:address2]
|
150
|
-
|
151
|
-
xml.tag! 'Company', address[:company]
|
152
|
-
|
153
|
-
xml.tag! 'City', address[:city]
|
154
|
-
xml.tag! 'State', address[:state] unless address[:state].blank?
|
155
|
-
xml.tag! 'Country', address[:country]
|
156
|
-
|
157
|
-
xml.tag! 'Zip', address[:zip]
|
158
|
-
xml.tag! 'Phone', address[:phone] unless address[:phone].blank?
|
159
|
-
xml.tag! 'Email', options[:email] unless options[:email].blank?
|
160
|
-
end
|
161
|
-
end
|
162
|
-
|
163
|
-
# Code is limited to 12 characters
|
164
|
-
def add_item(xml, item, index)
|
165
|
-
xml.tag! 'Item', :num => index do
|
166
|
-
xml.tag! 'Code', item[:sku]
|
167
|
-
xml.tag! 'Quantity', item[:quantity]
|
168
|
-
end
|
169
|
-
end
|
170
|
-
|
171
|
-
def commit(action, request)
|
172
|
-
data = ssl_post(SERVICE_URLS[action], "#{POST_VARS[action]}=#{CGI.escape(request)}")
|
173
|
-
|
174
|
-
response = parse_response(action, data)
|
175
|
-
Response.new(response[:success], response[:message], response, :test => test?)
|
176
|
-
end
|
177
|
-
|
178
|
-
def parse_response(action, data)
|
179
|
-
case action
|
180
|
-
when :fulfillment
|
181
|
-
parse_fulfillment_response(data)
|
182
|
-
when :inventory
|
183
|
-
parse_inventory_response(data)
|
184
|
-
when :tracking
|
185
|
-
parse_tracking_response(data)
|
186
|
-
else
|
187
|
-
raise ArgumentError, "Unknown action #{action}"
|
188
|
-
end
|
189
|
-
end
|
190
|
-
|
191
|
-
def parse_fulfillment_response(xml)
|
192
|
-
response = {}
|
193
|
-
|
194
|
-
document = REXML::Document.new(xml)
|
195
|
-
document.root.elements.each do |node|
|
196
|
-
response[node.name.underscore.to_sym] = text_content(node)
|
197
|
-
end
|
198
|
-
|
199
|
-
response[:success] = response[:status] == '0'
|
200
|
-
response[:message] = response[:success] ? "Successfully submitted the order" : message_from(response[:error_message])
|
201
|
-
response
|
202
|
-
end
|
203
|
-
|
204
|
-
def parse_inventory_response(xml)
|
205
|
-
response = {}
|
206
|
-
response[:stock_levels] = {}
|
207
|
-
|
208
|
-
document = REXML::Document.new(xml)
|
209
|
-
document.root.elements.each do |node|
|
210
|
-
if node.name == 'Product'
|
211
|
-
to_check = ['quantity']
|
212
|
-
to_check << 'pending' if include_pending_stock?
|
213
|
-
|
214
|
-
amount = to_check.sum { |a| node.attributes[a].to_i }
|
215
|
-
response[:stock_levels][node.attributes['code']] = amount
|
216
|
-
else
|
217
|
-
response[node.name.underscore.to_sym] = text_content(node)
|
218
|
-
end
|
219
|
-
end
|
220
|
-
|
221
|
-
response[:success] = test? ? response[:status] == 'Test' : response[:status] == '0'
|
222
|
-
response[:message] = response[:success] ? "Successfully received the stock levels" : message_from(response[:error_message])
|
223
|
-
|
224
|
-
response
|
225
|
-
end
|
226
|
-
|
227
|
-
def parse_tracking_response(xml)
|
228
|
-
response = {}
|
229
|
-
response[:tracking_numbers] = {}
|
230
|
-
response[:tracking_companies] = {}
|
231
|
-
response[:tracking_urls] = {}
|
232
|
-
|
233
|
-
document = REXML::Document.new(xml)
|
234
|
-
document.root.elements.each do |node|
|
235
|
-
if node.name == 'Order'
|
236
|
-
if node.attributes["shipped"] == "YES" && node.elements['TrackingNumber']
|
237
|
-
tracking_number = node.elements['TrackingNumber'].text.strip
|
238
|
-
response[:tracking_numbers][node.attributes['id']] = [tracking_number]
|
239
|
-
|
240
|
-
tracking_company = node.elements['TrackingNumber'].attributes['carrier']
|
241
|
-
response[:tracking_companies][node.attributes['id']] = [tracking_company.strip] if tracking_company
|
242
|
-
|
243
|
-
tracking_url = node.elements['TrackingNumber'].attributes['href']
|
244
|
-
response[:tracking_urls][node.attributes['id']] = [tracking_url.strip] if tracking_url
|
245
|
-
end
|
246
|
-
else
|
247
|
-
response[node.name.underscore.to_sym] = text_content(node)
|
248
|
-
end
|
249
|
-
end
|
250
|
-
|
251
|
-
response[:success] = test? ? (response[:status] == '0' || response[:status] == 'Test') : response[:status] == '0'
|
252
|
-
response[:message] = response[:success] ? "Successfully received the tracking numbers" : message_from(response[:error_message])
|
253
|
-
response
|
254
|
-
end
|
255
|
-
|
256
|
-
def message_from(string)
|
257
|
-
return if string.blank?
|
258
|
-
string.gsub("\n", '').squeeze(" ")
|
259
|
-
end
|
260
|
-
|
261
|
-
def text_content(xml_node)
|
262
|
-
text = xml_node.text
|
263
|
-
text = xml_node.cdatas.join if text.blank?
|
264
|
-
text
|
265
|
-
end
|
266
|
-
end
|
267
|
-
end
|
268
|
-
end
|
@@ -1,125 +0,0 @@
|
|
1
|
-
module ActiveMerchant
|
2
|
-
module Fulfillment
|
3
|
-
class ShopifyAPIService < Service
|
4
|
-
|
5
|
-
OrderIdCutoffDate = Date.iso8601("2015-03-01")
|
6
|
-
|
7
|
-
RESCUABLE_CONNECTION_ERRORS = [
|
8
|
-
Net::ReadTimeout,
|
9
|
-
Net::OpenTimeout,
|
10
|
-
TimeoutError,
|
11
|
-
Errno::ETIMEDOUT,
|
12
|
-
Timeout::Error,
|
13
|
-
IOError,
|
14
|
-
EOFError,
|
15
|
-
SocketError,
|
16
|
-
Errno::ECONNRESET,
|
17
|
-
Errno::ECONNABORTED,
|
18
|
-
Errno::EPIPE,
|
19
|
-
Errno::ECONNREFUSED,
|
20
|
-
Errno::EAGAIN,
|
21
|
-
Errno::EHOSTUNREACH,
|
22
|
-
Errno::ENETUNREACH,
|
23
|
-
Resolv::ResolvError,
|
24
|
-
Net::HTTPBadResponse,
|
25
|
-
Net::HTTPHeaderSyntaxError,
|
26
|
-
Net::ProtocolError,
|
27
|
-
ActiveMerchant::ConnectionError,
|
28
|
-
ActiveMerchant::ResponseError,
|
29
|
-
ActiveMerchant::InvalidResponseError
|
30
|
-
]
|
31
|
-
|
32
|
-
def initialize(options = {})
|
33
|
-
@name = options[:name]
|
34
|
-
@callback_url = options[:callback_url]
|
35
|
-
@format = options[:format]
|
36
|
-
end
|
37
|
-
|
38
|
-
def fulfill(order_id, shipping_address, line_items, options = {})
|
39
|
-
raise NotImplementedError.new("Shopify API Service must listen to fulfillment/create Webhooks")
|
40
|
-
end
|
41
|
-
|
42
|
-
def fetch_stock_levels(options = {})
|
43
|
-
response = send_app_request('fetch_stock', options.delete(:headers), options)
|
44
|
-
if response
|
45
|
-
stock_levels = parse_response(response, 'StockLevels', 'Product', 'Sku', 'Quantity') { |p| p.to_i }
|
46
|
-
Response.new(true, "API stock levels", {:stock_levels => stock_levels})
|
47
|
-
else
|
48
|
-
Response.new(false, "Unable to fetch remote stock levels")
|
49
|
-
end
|
50
|
-
end
|
51
|
-
|
52
|
-
def fetch_tracking_data(order_numbers, options = {})
|
53
|
-
options.merge!({:order_ids => order_numbers, :order_names => order_numbers})
|
54
|
-
response = send_app_request('fetch_tracking_numbers', options.delete(:headers), options)
|
55
|
-
if response
|
56
|
-
tracking_numbers = parse_response(response, 'TrackingNumbers', 'Order', 'ID', 'Tracking') { |o| o }
|
57
|
-
Response.new(true, "API tracking_numbers", {:tracking_numbers => tracking_numbers,
|
58
|
-
:tracking_companies => {},
|
59
|
-
:tracking_urls => {}})
|
60
|
-
else
|
61
|
-
Response.new(false, "Unable to fetch remote tracking numbers #{order_numbers.inspect}")
|
62
|
-
end
|
63
|
-
end
|
64
|
-
|
65
|
-
private
|
66
|
-
|
67
|
-
def request_uri(action, data)
|
68
|
-
URI.parse "#{@callback_url}/#{action}.#{@format}?#{data.to_param}"
|
69
|
-
end
|
70
|
-
|
71
|
-
def send_app_request(action, headers, data)
|
72
|
-
uri = request_uri(action, data)
|
73
|
-
|
74
|
-
logger.info "[" + @name.upcase + " APP] Post #{uri}"
|
75
|
-
|
76
|
-
response = nil
|
77
|
-
realtime = Benchmark.realtime do
|
78
|
-
begin
|
79
|
-
Timeout.timeout(20.seconds) do
|
80
|
-
response = ssl_get(uri, headers)
|
81
|
-
end
|
82
|
-
rescue *(RESCUABLE_CONNECTION_ERRORS) => e
|
83
|
-
logger.warn "[#{self}] Error while contacting fulfillment service error =\"#{e.message}\""
|
84
|
-
end
|
85
|
-
end
|
86
|
-
|
87
|
-
line = "[" + @name.upcase + "APP] Response from #{uri} --> "
|
88
|
-
line << "#{response} #{"%.4fs" % realtime}"
|
89
|
-
logger.info line
|
90
|
-
|
91
|
-
response
|
92
|
-
end
|
93
|
-
|
94
|
-
def parse_response(response, root, type, key, value)
|
95
|
-
case @format
|
96
|
-
when 'json'
|
97
|
-
response_data = ActiveSupport::JSON.decode(response)
|
98
|
-
return {} unless response_data.is_a?(Hash)
|
99
|
-
response_data[root.underscore] || response_data
|
100
|
-
when 'xml'
|
101
|
-
response_data = {}
|
102
|
-
document = REXML::Document.new(response)
|
103
|
-
document.elements[root].each do |node|
|
104
|
-
if node.name == type
|
105
|
-
response_data[node.elements[key].text] = node.elements[value].text
|
106
|
-
end
|
107
|
-
end
|
108
|
-
response_data
|
109
|
-
end
|
110
|
-
|
111
|
-
rescue ActiveSupport::JSON.parse_error, REXML::ParseException
|
112
|
-
{}
|
113
|
-
end
|
114
|
-
|
115
|
-
def encode_payload(payload, root)
|
116
|
-
case @format
|
117
|
-
when 'json'
|
118
|
-
{root => payload}.to_json
|
119
|
-
when 'xml'
|
120
|
-
payload.to_xml(:root => root)
|
121
|
-
end
|
122
|
-
end
|
123
|
-
end
|
124
|
-
end
|
125
|
-
end
|
@@ -1,338 +0,0 @@
|
|
1
|
-
module ActiveMerchant
|
2
|
-
module Fulfillment
|
3
|
-
class WebgistixService < Service
|
4
|
-
SERVICE_URLS = {
|
5
|
-
:fulfillment => 'https://www.webgistix.com/XML/CreateOrder.asp',
|
6
|
-
:inventory => 'https://www.webgistix.com/XML/GetInventory.asp',
|
7
|
-
:tracking => 'https://www.webgistix.com/XML/GetTracking.asp'
|
8
|
-
}
|
9
|
-
TEST_URLS = SERVICE_URLS.merge({
|
10
|
-
:fulfillment => 'https://www.webgistix.com/XML/CreateOrderTest.asp'
|
11
|
-
})
|
12
|
-
|
13
|
-
SUCCESS, DUPLICATE, FAILURE = 'True', 'Duplicate', 'False'
|
14
|
-
|
15
|
-
SUCCESS_MESSAGE = 'Successfully submitted the order'
|
16
|
-
FAILURE_MESSAGE = 'Failed to submit the order'
|
17
|
-
DUPLICATE_MESSAGE = 'This order has already been successfully submitted'
|
18
|
-
|
19
|
-
INVALID_LOGIN = 'Invalid Credentials'
|
20
|
-
NOT_SHIPPED = 'Not Shipped'
|
21
|
-
|
22
|
-
TRACKING_COMPANIES = %w(UPS FedEx USPS)
|
23
|
-
|
24
|
-
# If a request is detected as a duplicate only the original data will be
|
25
|
-
# used by Webgistix, and the subsequent responses will have a
|
26
|
-
# :duplicate parameter set in the params hash.
|
27
|
-
self.retry_safe = true
|
28
|
-
|
29
|
-
# The first is the label, and the last is the code
|
30
|
-
def self.shipping_methods
|
31
|
-
[
|
32
|
-
["UPS Ground Shipping", "Ground"],
|
33
|
-
["UPS Ground", "Ground"],
|
34
|
-
["UPS Standard Shipping (Canada Only)", "Standard"],
|
35
|
-
["UPS Standard Shipping (CA & MX Only)", "Standard"],
|
36
|
-
["UPS 3-Business Day", "3-Day Select"],
|
37
|
-
["UPS 2-Business Day", "2nd Day Air"],
|
38
|
-
["UPS 2-Business Day AM", "2nd Day Air AM"],
|
39
|
-
["UPS Next Day", "Next Day Air"],
|
40
|
-
["UPS Next Day Saver", "Next Day Air Saver"],
|
41
|
-
["UPS Next Day Early AM", "Next Day Air Early AM"],
|
42
|
-
["UPS Worldwide Express (Next Day)", "Worldwide Express"],
|
43
|
-
["UPS Worldwide Expedited (2nd Day)", "Worldwide Expedited"],
|
44
|
-
["UPS Worldwide Express Saver", "Worldwide Express Saver"],
|
45
|
-
["FedEx Priority Overnight", "FedEx Priority Overnight"],
|
46
|
-
["FedEx Standard Overnight", "FedEx Standard Overnight"],
|
47
|
-
["FedEx First Overnight", "FedEx First Overnight"],
|
48
|
-
["FedEx 2nd Day", "FedEx 2nd Day"],
|
49
|
-
["FedEx Express Saver", "FedEx Express Saver"],
|
50
|
-
["FedEx International Priority", "FedEx International Priority"],
|
51
|
-
["FedEx International Economy", "FedEx International Economy"],
|
52
|
-
["FedEx International First", "FedEx International First"],
|
53
|
-
["FedEx Ground", "FedEx Ground"],
|
54
|
-
["USPS Priority Mail", "Priority Mail"],
|
55
|
-
["USPS Priority Mail International", "Priority Mail International"],
|
56
|
-
["USPS Priority Mail Small Flat Rate Box", "Priority Mail Small Flat Rate Box"],
|
57
|
-
["USPS Priority Mail Medium Flat Rate Box", "Priority Mail Medium Flat Rate Box"],
|
58
|
-
["USPS Priority Mail Large Flat Rate Box", "Priority Mail Large Flat Rate Box"],
|
59
|
-
["USPS Priority Mail Flat Rate Envelope", "Priority Mail Flat Rate Envelope"],
|
60
|
-
["USPS First Class Mail", "First Class"],
|
61
|
-
["USPS First Class International", "First Class International"],
|
62
|
-
["USPS Express Mail", "Express"],
|
63
|
-
["USPS Express Mail International", "Express Mail International"],
|
64
|
-
["USPS Parcel Post", "Parcel"],
|
65
|
-
["USPS Media Mail", "Media Mail"]
|
66
|
-
].inject(ActiveSupport::OrderedHash.new){|h, (k,v)| h[k] = v; h}
|
67
|
-
end
|
68
|
-
|
69
|
-
# Pass in the login and password for the shipwire account.
|
70
|
-
# Optionally pass in the :test => true to force test mode
|
71
|
-
def initialize(options = {})
|
72
|
-
requires!(options, :login, :password)
|
73
|
-
super
|
74
|
-
end
|
75
|
-
|
76
|
-
def fulfill(order_id, shipping_address, line_items, options = {})
|
77
|
-
requires!(options, :shipping_method)
|
78
|
-
commit :fulfillment, build_fulfillment_request(order_id, shipping_address, line_items, options)
|
79
|
-
end
|
80
|
-
|
81
|
-
def fetch_stock_levels(options = {})
|
82
|
-
commit :inventory, build_inventory_request(options)
|
83
|
-
end
|
84
|
-
|
85
|
-
def fetch_tracking_data(order_ids, options = {})
|
86
|
-
commit :tracking, build_tracking_request(order_ids, options)
|
87
|
-
end
|
88
|
-
|
89
|
-
def valid_credentials?
|
90
|
-
response = fulfill('', {}, [], :shipping_method => '')
|
91
|
-
response.message != INVALID_LOGIN
|
92
|
-
end
|
93
|
-
|
94
|
-
def test_mode?
|
95
|
-
true
|
96
|
-
end
|
97
|
-
|
98
|
-
private
|
99
|
-
#<?xml version="1.0"?>
|
100
|
-
# <OrderXML>
|
101
|
-
# <Password>Webgistix</Password>
|
102
|
-
# <CustomerID>3</CustomerID>
|
103
|
-
# <Order>
|
104
|
-
# <ReferenceNumber></ReferenceNumber>
|
105
|
-
# <Company>Test Company</Company>
|
106
|
-
# <Name>Joe Smith</Name>
|
107
|
-
# <Address1>123 Main St.</Address1>
|
108
|
-
# <Address2></Address2>
|
109
|
-
# <Address3></Address3>
|
110
|
-
# <City>Olean</City>
|
111
|
-
# <State>NY</State>
|
112
|
-
# <ZipCode>14760</ZipCode>
|
113
|
-
# <Country>United States</Country>
|
114
|
-
# <Email>info@webgistix.com</Email>
|
115
|
-
# <Phone>1-123-456-7890</Phone>
|
116
|
-
# <ShippingInstructions>Ground</ShippingInstructions>
|
117
|
-
# <OrderComments>Test Order</OrderComments>
|
118
|
-
# <Approve>0</Approve>
|
119
|
-
# <Item>
|
120
|
-
# <ItemID>testitem</ItemID>
|
121
|
-
# <ItemQty>2</ItemQty>
|
122
|
-
# </Item>
|
123
|
-
# </Order>
|
124
|
-
# </OrderXML>
|
125
|
-
def build_fulfillment_request(order_id, shipping_address, line_items, options)
|
126
|
-
xml = Builder::XmlMarkup.new :indent => 2
|
127
|
-
xml.instruct!
|
128
|
-
xml.tag! 'OrderXML' do
|
129
|
-
add_credentials(xml)
|
130
|
-
add_order(xml, order_id, shipping_address, line_items, options)
|
131
|
-
end
|
132
|
-
xml.target!
|
133
|
-
end
|
134
|
-
|
135
|
-
#<?xml version="1.0"?>
|
136
|
-
# <InventoryXML>
|
137
|
-
# <Password>Webgistix</Password>
|
138
|
-
# <CustomerID>3</CustomerID>
|
139
|
-
# </InventoryXML>
|
140
|
-
def build_inventory_request(options)
|
141
|
-
xml = Builder::XmlMarkup.new :indent => 2
|
142
|
-
xml.instruct!
|
143
|
-
xml.tag! 'InventoryXML' do
|
144
|
-
add_credentials(xml)
|
145
|
-
end
|
146
|
-
end
|
147
|
-
|
148
|
-
#<?xml version="1.0"?>
|
149
|
-
# <TrackingXML>
|
150
|
-
# <Password>Webgistix</Password>
|
151
|
-
# <CustomerID>3</CustomerID>
|
152
|
-
# <Tracking>
|
153
|
-
# <Order>AB12345</Order>
|
154
|
-
# </Tracking>
|
155
|
-
# <Tracking>
|
156
|
-
# <Order>XY4567</Order>
|
157
|
-
# </Tracking>
|
158
|
-
# </TrackingXML>
|
159
|
-
def build_tracking_request(order_ids, options)
|
160
|
-
xml = Builder::XmlMarkup.new :indent => 2
|
161
|
-
xml.instruct!
|
162
|
-
xml.tag! 'TrackingXML' do
|
163
|
-
add_credentials(xml)
|
164
|
-
|
165
|
-
order_ids.each do |o_id|
|
166
|
-
xml.tag! 'Tracking' do
|
167
|
-
xml.tag! 'Order', o_id
|
168
|
-
end
|
169
|
-
end
|
170
|
-
end
|
171
|
-
end
|
172
|
-
|
173
|
-
def add_credentials(xml)
|
174
|
-
xml.tag! 'CustomerID', @options[:login]
|
175
|
-
xml.tag! 'Password', @options[:password]
|
176
|
-
end
|
177
|
-
|
178
|
-
def add_order(xml, order_id, shipping_address, line_items, options)
|
179
|
-
xml.tag! 'Order' do
|
180
|
-
xml.tag! 'ReferenceNumber', order_id
|
181
|
-
xml.tag! 'ShippingInstructions', options[:shipping_method]
|
182
|
-
xml.tag! 'Approve', 1
|
183
|
-
xml.tag! 'OrderComments', options[:comment] unless options[:comment].blank?
|
184
|
-
|
185
|
-
add_address(xml, shipping_address, options)
|
186
|
-
|
187
|
-
Array(line_items).each_with_index do |line_item, index|
|
188
|
-
add_item(xml, line_item, index)
|
189
|
-
end
|
190
|
-
end
|
191
|
-
end
|
192
|
-
|
193
|
-
def add_address(xml, address, options)
|
194
|
-
xml.tag! 'Name', address[:name]
|
195
|
-
xml.tag! 'Address1', address[:address1]
|
196
|
-
xml.tag! 'Address2', address[:address2] unless address[:address2].blank?
|
197
|
-
xml.tag! 'Address3', address[:address3] unless address[:address3].blank?
|
198
|
-
xml.tag! 'City', address[:city]
|
199
|
-
xml.tag! 'State', address[:state]
|
200
|
-
xml.tag! 'ZipCode', address[:zip]
|
201
|
-
xml.tag! 'Company', address[:company]
|
202
|
-
|
203
|
-
unless address[:country].blank?
|
204
|
-
country = Country.find(address[:country])
|
205
|
-
xml.tag! 'Country', country.name
|
206
|
-
end
|
207
|
-
|
208
|
-
xml.tag! 'Phone', address[:phone]
|
209
|
-
xml.tag! 'Email', options[:email] unless options[:email].blank?
|
210
|
-
end
|
211
|
-
|
212
|
-
def add_item(xml, item, index)
|
213
|
-
xml.tag! 'Item' do
|
214
|
-
xml.tag! 'ItemID', item[:sku] unless item[:sku].blank?
|
215
|
-
xml.tag! 'ItemQty', item[:quantity] unless item[:quantity].blank?
|
216
|
-
end
|
217
|
-
end
|
218
|
-
|
219
|
-
def commit(action, request)
|
220
|
-
url = test? ? TEST_URLS[action] : SERVICE_URLS[action]
|
221
|
-
|
222
|
-
data = ssl_post(url, request,
|
223
|
-
'EndPointURL' => url,
|
224
|
-
'Content-Type' => 'text/xml; charset="utf-8"'
|
225
|
-
)
|
226
|
-
|
227
|
-
response = parse_response(action, data)
|
228
|
-
Response.new(success?(response), message_from(response), response, :test => test?)
|
229
|
-
end
|
230
|
-
|
231
|
-
def success?(response)
|
232
|
-
response[:success] == SUCCESS || response[:success] == DUPLICATE
|
233
|
-
end
|
234
|
-
|
235
|
-
def message_from(response)
|
236
|
-
if response[:duplicate]
|
237
|
-
DUPLICATE_MESSAGE
|
238
|
-
elsif success?(response)
|
239
|
-
SUCCESS_MESSAGE
|
240
|
-
elsif response[:error_0] == INVALID_LOGIN
|
241
|
-
INVALID_LOGIN
|
242
|
-
else
|
243
|
-
FAILURE_MESSAGE
|
244
|
-
end
|
245
|
-
end
|
246
|
-
|
247
|
-
def parse_response(action, xml)
|
248
|
-
begin
|
249
|
-
document = REXML::Document.new("<response>#{xml}</response>")
|
250
|
-
rescue REXML::ParseException
|
251
|
-
return {:success => FAILURE}
|
252
|
-
end
|
253
|
-
|
254
|
-
case action
|
255
|
-
when :fulfillment
|
256
|
-
parse_fulfillment_response(document)
|
257
|
-
when :inventory
|
258
|
-
parse_inventory_response(document)
|
259
|
-
when :tracking
|
260
|
-
parse_tracking_response(document)
|
261
|
-
else
|
262
|
-
raise ArgumentError, "Unknown action #{action}"
|
263
|
-
end
|
264
|
-
end
|
265
|
-
|
266
|
-
def parse_fulfillment_response(document)
|
267
|
-
response = parse_errors(document)
|
268
|
-
|
269
|
-
# Check if completed
|
270
|
-
if completed = REXML::XPath.first(document, '//Completed')
|
271
|
-
completed.elements.each do |e|
|
272
|
-
response[e.name.underscore.to_sym] = e.text
|
273
|
-
end
|
274
|
-
else
|
275
|
-
response[:success] = FAILURE
|
276
|
-
end
|
277
|
-
|
278
|
-
response[:duplicate] = response[:success] == DUPLICATE
|
279
|
-
|
280
|
-
response
|
281
|
-
end
|
282
|
-
|
283
|
-
def parse_inventory_response(document)
|
284
|
-
response = parse_errors(document)
|
285
|
-
response[:stock_levels] = {}
|
286
|
-
|
287
|
-
document.root.each_element('//Item') do |node|
|
288
|
-
# {ItemID => 'SOME-ID', ItemQty => '101'}
|
289
|
-
params = node.elements.to_a.each_with_object({}) {|elem, hash| hash[elem.name] = elem.text}
|
290
|
-
|
291
|
-
response[:stock_levels][params['ItemID']] = params['ItemQty'].to_i
|
292
|
-
end
|
293
|
-
|
294
|
-
response
|
295
|
-
end
|
296
|
-
|
297
|
-
def parse_tracking_response(document)
|
298
|
-
response = parse_errors(document)
|
299
|
-
response[:tracking_numbers] = {}
|
300
|
-
response[:tracking_companies] = {}
|
301
|
-
response[:tracking_urls] = {}
|
302
|
-
|
303
|
-
document.root.each_element('//Shipment') do |node|
|
304
|
-
# {InvoiceNumber => 'SOME-ID', ShipmentTrackingNumber => 'SOME-TRACKING-NUMBER'}
|
305
|
-
params = node.elements.to_a.each_with_object({}) {|elem, hash| hash[elem.name] = elem.text}
|
306
|
-
|
307
|
-
tracking = params['ShipmentTrackingNumber']
|
308
|
-
|
309
|
-
unless tracking == NOT_SHIPPED
|
310
|
-
response[:tracking_numbers][params['InvoiceNumber']] ||= []
|
311
|
-
response[:tracking_numbers][params['InvoiceNumber']] << tracking
|
312
|
-
end
|
313
|
-
|
314
|
-
company = params['Method'].split[0] if params['Method']
|
315
|
-
if TRACKING_COMPANIES.include? company
|
316
|
-
response[:tracking_companies][params['InvoiceNumber']] ||= []
|
317
|
-
response[:tracking_companies][params['InvoiceNumber']] << company
|
318
|
-
end
|
319
|
-
end
|
320
|
-
|
321
|
-
response
|
322
|
-
end
|
323
|
-
|
324
|
-
def parse_errors(document)
|
325
|
-
response = {}
|
326
|
-
|
327
|
-
REXML::XPath.match(document, "//Errors/Error").to_a.each_with_index do |e, i|
|
328
|
-
response["error_#{i}".to_sym] = e.text
|
329
|
-
end
|
330
|
-
|
331
|
-
response[:success] = response.empty? ? SUCCESS : FAILURE
|
332
|
-
response
|
333
|
-
end
|
334
|
-
end
|
335
|
-
end
|
336
|
-
end
|
337
|
-
|
338
|
-
|