active_fulfillment 2.1.9 → 3.0.0.pre2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
-
|