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
@@ -0,0 +1,122 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module ActiveFulfillment
|
4
|
+
class JamesAndJamesService < Service
|
5
|
+
|
6
|
+
SERVICE_URLS = {
|
7
|
+
fulfillment: 'https://%{subdomain}.sixworks.co.uk/api/1/',
|
8
|
+
inventory: 'https://%{subdomain}.sixworks.co.uk/api/1/stock'
|
9
|
+
}
|
10
|
+
|
11
|
+
def initialize(options = {})
|
12
|
+
requires!(options, :subdomain, :key)
|
13
|
+
super
|
14
|
+
end
|
15
|
+
|
16
|
+
def fulfill(order_id, shipping_address, line_items, options = {})
|
17
|
+
requires!(options, :billing_address)
|
18
|
+
commit :fulfillment, build_fulfillment_request(order_id, shipping_address, line_items, options)
|
19
|
+
end
|
20
|
+
|
21
|
+
def fetch_stock_levels(options = {})
|
22
|
+
get :inventory, build_inventory_request(options)
|
23
|
+
end
|
24
|
+
|
25
|
+
def test_mode?
|
26
|
+
true
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def build_fulfillment_request(order_id, shipping_address, line_items, options)
|
32
|
+
data = {
|
33
|
+
order: {
|
34
|
+
client_ref: order_id,
|
35
|
+
ShippingContact: format_address(shipping_address),
|
36
|
+
BillingContact: format_address(options[:billing_address]),
|
37
|
+
items: format_line_items(line_items)
|
38
|
+
}
|
39
|
+
}
|
40
|
+
data[:allow_preorder] = options[:allow_preorder] unless options[:allow_preorder].blank?
|
41
|
+
data[:update_stock] = options[:update_stock] unless options[:update_stock].blank?
|
42
|
+
data[:order][:po_number] = options[:po_number] unless options[:po_number].blank?
|
43
|
+
data[:order][:date_placed] = options[:date_placed] unless options[:po_number].blank?
|
44
|
+
data[:order][:postage_speed] = options[:postage_speed] unless options[:postage_speed].blank?
|
45
|
+
data[:order][:postage_cost] = options[:postage_cost] unless options[:postage_cost].blank?
|
46
|
+
data[:order][:total_value] = options[:total_value] unless options[:total_value].blank?
|
47
|
+
data[:order][:days_before_bbe] = options[:days_before_bbe] unless options[:days_before_bbe].blank?
|
48
|
+
data[:order][:callback_url] = options[:callback_url] unless options[:callback_url].blank?
|
49
|
+
return data
|
50
|
+
end
|
51
|
+
|
52
|
+
def build_inventory_request(options)
|
53
|
+
{}
|
54
|
+
end
|
55
|
+
|
56
|
+
def commit(action, request)
|
57
|
+
request = request.merge({api_key: @options[:key], test: test? })
|
58
|
+
data = ssl_post(SERVICE_URLS[action] % {subdomain: @options[:subdomain]}, JSON.generate(request))
|
59
|
+
response = parse_response(data)
|
60
|
+
Response.new(response["success"], "message", response, test: response["test"])
|
61
|
+
rescue ActiveUtils::ResponseError => e
|
62
|
+
handle_error(e)
|
63
|
+
rescue JSON::ParserError => e
|
64
|
+
Response.new(false, e.message)
|
65
|
+
end
|
66
|
+
|
67
|
+
def get(action, request)
|
68
|
+
request = request.merge({api_key: @options[:key], test: test? })
|
69
|
+
data = ssl_get(SERVICE_URLS[action] % {subdomain: @options[:subdomain]} + "?" + request.to_query)
|
70
|
+
response = parse_response(data)
|
71
|
+
Response.new(response["success"], "message", response, test: response["test"])
|
72
|
+
rescue ActiveUtils::ResponseError => e
|
73
|
+
handle_error(e)
|
74
|
+
rescue JSON::ParserError => e
|
75
|
+
Response.new(false, e.message)
|
76
|
+
end
|
77
|
+
|
78
|
+
def parse_response(json)
|
79
|
+
JSON.parse(json)
|
80
|
+
end
|
81
|
+
|
82
|
+
def handle_error(e)
|
83
|
+
response = parse_error(e.response)
|
84
|
+
Response.new(false, response[:http_message], response)
|
85
|
+
end
|
86
|
+
|
87
|
+
def parse_error(http_response)
|
88
|
+
response = {}
|
89
|
+
response[:http_code] = http_response.code
|
90
|
+
response[:http_message] = http_response.message
|
91
|
+
response
|
92
|
+
end
|
93
|
+
|
94
|
+
def format_address(address)
|
95
|
+
data = {
|
96
|
+
name: address[:name],
|
97
|
+
address: address[:address1],
|
98
|
+
city: address[:city],
|
99
|
+
country: address[:country],
|
100
|
+
postcode: address[:zip].blank? ? "-" : address[:zip]
|
101
|
+
}
|
102
|
+
data[:company] = address[:company] unless address[:company].blank?
|
103
|
+
data[:email] = address[:email] unless address[:email].blank?
|
104
|
+
data[:address_contd] = address[:address2] unless address[:address2].blank?
|
105
|
+
data[:county] = address[:state] unless address[:state].blank?
|
106
|
+
return data
|
107
|
+
end
|
108
|
+
|
109
|
+
def format_line_items(items)
|
110
|
+
data = []
|
111
|
+
items.each do |item|
|
112
|
+
data << {
|
113
|
+
client_ref: item[:sku],
|
114
|
+
quantity: item[:quantity],
|
115
|
+
price: item[:price]
|
116
|
+
}
|
117
|
+
end
|
118
|
+
return data
|
119
|
+
end
|
120
|
+
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,266 @@
|
|
1
|
+
require 'cgi'
|
2
|
+
|
3
|
+
module ActiveFulfillment
|
4
|
+
class ShipwireService < Service
|
5
|
+
|
6
|
+
SERVICE_URLS = { :fulfillment => 'https://api.shipwire.com/exec/FulfillmentServices.php',
|
7
|
+
:inventory => 'https://api.shipwire.com/exec/InventoryServices.php',
|
8
|
+
:tracking => 'https://api.shipwire.com/exec/TrackingServices.php'
|
9
|
+
}
|
10
|
+
|
11
|
+
SCHEMA_URLS = { :fulfillment => 'http://www.shipwire.com/exec/download/OrderList.dtd',
|
12
|
+
:inventory => 'http://www.shipwire.com/exec/download/InventoryUpdate.dtd',
|
13
|
+
:tracking => 'http://www.shipwire.com/exec/download/TrackingUpdate.dtd'
|
14
|
+
}
|
15
|
+
|
16
|
+
POST_VARS = { :fulfillment => 'OrderListXML',
|
17
|
+
:inventory => 'InventoryUpdateXML',
|
18
|
+
:tracking => 'TrackingUpdateXML'
|
19
|
+
}
|
20
|
+
|
21
|
+
WAREHOUSES = { 'CHI' => 'Chicago',
|
22
|
+
'LAX' => 'Los Angeles',
|
23
|
+
'REN' => 'Reno',
|
24
|
+
'VAN' => 'Vancouver',
|
25
|
+
'TOR' => 'Toronto',
|
26
|
+
'UK' => 'United Kingdom'
|
27
|
+
}
|
28
|
+
|
29
|
+
INVALID_LOGIN = /(Error with Valid Username\/EmailAddress and Password Required)|(Could not verify Username\/EmailAddress and Password combination)/
|
30
|
+
|
31
|
+
class_attribute :affiliate_id
|
32
|
+
|
33
|
+
# The first is the label, and the last is the code
|
34
|
+
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}
|
41
|
+
end
|
42
|
+
|
43
|
+
# Pass in the login and password for the shipwire account.
|
44
|
+
# Optionally pass in the :test => true to force test mode
|
45
|
+
def initialize(options = {})
|
46
|
+
requires!(options, :login, :password)
|
47
|
+
|
48
|
+
super
|
49
|
+
end
|
50
|
+
|
51
|
+
def fulfill(order_id, shipping_address, line_items, options = {})
|
52
|
+
commit :fulfillment, build_fulfillment_request(order_id, shipping_address, line_items, options)
|
53
|
+
end
|
54
|
+
|
55
|
+
def fetch_stock_levels(options = {})
|
56
|
+
commit :inventory, build_inventory_request(options)
|
57
|
+
end
|
58
|
+
|
59
|
+
def fetch_tracking_data(order_ids, options = {})
|
60
|
+
commit :tracking, build_tracking_request(order_ids)
|
61
|
+
end
|
62
|
+
|
63
|
+
def valid_credentials?
|
64
|
+
response = fetch_tracking_numbers([])
|
65
|
+
response.message !~ INVALID_LOGIN
|
66
|
+
end
|
67
|
+
|
68
|
+
def test_mode?
|
69
|
+
true
|
70
|
+
end
|
71
|
+
|
72
|
+
def include_pending_stock?
|
73
|
+
@options[:include_pending_stock]
|
74
|
+
end
|
75
|
+
|
76
|
+
def include_empty_stock?
|
77
|
+
@options[:include_empty_stock]
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
def build_fulfillment_request(order_id, shipping_address, line_items, options)
|
82
|
+
xml = Builder::XmlMarkup.new :indent => 2
|
83
|
+
xml.instruct!
|
84
|
+
xml.declare! :DOCTYPE, :OrderList, :SYSTEM, SCHEMA_URLS[:fulfillment]
|
85
|
+
xml.tag! 'OrderList' do
|
86
|
+
add_credentials(xml)
|
87
|
+
xml.tag! 'Referer', 'Active Fulfillment'
|
88
|
+
add_order(xml, order_id, shipping_address, line_items, options)
|
89
|
+
end
|
90
|
+
xml.target!
|
91
|
+
end
|
92
|
+
|
93
|
+
def build_inventory_request(options)
|
94
|
+
xml = Builder::XmlMarkup.new :indent => 2
|
95
|
+
xml.instruct!
|
96
|
+
xml.declare! :DOCTYPE, :InventoryStatus, :SYSTEM, SCHEMA_URLS[:inventory]
|
97
|
+
xml.tag! 'InventoryUpdate' do
|
98
|
+
add_credentials(xml)
|
99
|
+
xml.tag! 'Warehouse', WAREHOUSES[options[:warehouse]]
|
100
|
+
xml.tag! 'ProductCode', options[:sku]
|
101
|
+
xml.tag! 'IncludeEmpty' if include_empty_stock?
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def build_tracking_request(order_ids)
|
106
|
+
xml = Builder::XmlMarkup.new
|
107
|
+
xml.instruct!
|
108
|
+
xml.declare! :DOCTYPE, :InventoryStatus, :SYSTEM, SCHEMA_URLS[:inventory]
|
109
|
+
xml.tag! 'TrackingUpdate' do
|
110
|
+
add_credentials(xml)
|
111
|
+
xml.tag! 'Server', test? ? 'Test' : 'Production'
|
112
|
+
order_ids.each do |o_id|
|
113
|
+
xml.tag! 'OrderNo', o_id
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def add_credentials(xml)
|
119
|
+
xml.tag! 'EmailAddress', @options[:login]
|
120
|
+
xml.tag! 'Password', @options[:password]
|
121
|
+
xml.tag! 'Server', test? ? 'Test' : 'Production'
|
122
|
+
xml.tag! 'AffiliateId', affiliate_id if affiliate_id.present?
|
123
|
+
end
|
124
|
+
|
125
|
+
def add_order(xml, order_id, shipping_address, line_items, options)
|
126
|
+
xml.tag! 'Order', :id => order_id do
|
127
|
+
xml.tag! 'Warehouse', options[:warehouse] || '00'
|
128
|
+
|
129
|
+
add_address(xml, shipping_address, options)
|
130
|
+
xml.tag! 'Shipping', options[:shipping_method] unless options[:shipping_method].blank?
|
131
|
+
|
132
|
+
Array(line_items).each_with_index do |line_item, index|
|
133
|
+
add_item(xml, line_item, index)
|
134
|
+
end
|
135
|
+
xml.tag! 'Note' do
|
136
|
+
xml.cdata! options[:note] unless options[:note].blank?
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def add_address(xml, address, options)
|
142
|
+
xml.tag! 'AddressInfo', :type => 'Ship' do
|
143
|
+
xml.tag! 'Name' do
|
144
|
+
xml.tag! 'Full', address[:name]
|
145
|
+
end
|
146
|
+
|
147
|
+
xml.tag! 'Address1', address[:address1]
|
148
|
+
xml.tag! 'Address2', address[:address2]
|
149
|
+
|
150
|
+
xml.tag! 'Company', address[:company]
|
151
|
+
|
152
|
+
xml.tag! 'City', address[:city]
|
153
|
+
xml.tag! 'State', address[:state] unless address[:state].blank?
|
154
|
+
xml.tag! 'Country', address[:country]
|
155
|
+
|
156
|
+
xml.tag! 'Zip', address[:zip]
|
157
|
+
xml.tag! 'Phone', address[:phone] unless address[:phone].blank?
|
158
|
+
xml.tag! 'Email', options[:email] unless options[:email].blank?
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
# Code is limited to 12 characters
|
163
|
+
def add_item(xml, item, index)
|
164
|
+
xml.tag! 'Item', :num => index do
|
165
|
+
xml.tag! 'Code', item[:sku]
|
166
|
+
xml.tag! 'Quantity', item[:quantity]
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
def commit(action, request)
|
171
|
+
data = ssl_post(SERVICE_URLS[action], "#{POST_VARS[action]}=#{CGI.escape(request)}")
|
172
|
+
|
173
|
+
response = parse_response(action, data)
|
174
|
+
Response.new(response[:success], response[:message], response, :test => test?)
|
175
|
+
end
|
176
|
+
|
177
|
+
def parse_response(action, data)
|
178
|
+
case action
|
179
|
+
when :fulfillment
|
180
|
+
parse_fulfillment_response(data)
|
181
|
+
when :inventory
|
182
|
+
parse_inventory_response(data)
|
183
|
+
when :tracking
|
184
|
+
parse_tracking_response(data)
|
185
|
+
else
|
186
|
+
raise ArgumentError, "Unknown action #{action}"
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
def parse_fulfillment_response(xml)
|
191
|
+
response = {}
|
192
|
+
|
193
|
+
document = REXML::Document.new(xml)
|
194
|
+
document.root.elements.each do |node|
|
195
|
+
response[node.name.underscore.to_sym] = text_content(node)
|
196
|
+
end
|
197
|
+
|
198
|
+
response[:success] = response[:status] == '0'
|
199
|
+
response[:message] = response[:success] ? "Successfully submitted the order" : message_from(response[:error_message])
|
200
|
+
response
|
201
|
+
end
|
202
|
+
|
203
|
+
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
|
218
|
+
end
|
219
|
+
|
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
|
224
|
+
end
|
225
|
+
|
226
|
+
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
|
244
|
+
end
|
245
|
+
else
|
246
|
+
response[node.name.underscore.to_sym] = text_content(node)
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
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])
|
252
|
+
response
|
253
|
+
end
|
254
|
+
|
255
|
+
def message_from(string)
|
256
|
+
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
|
264
|
+
end
|
265
|
+
end
|
266
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
require 'active_support/core_ext/object/to_query'
|
2
|
+
|
3
|
+
module ActiveFulfillment
|
4
|
+
class ShopifyAPIService < Service
|
5
|
+
|
6
|
+
OrderIdCutoffDate = Date.iso8601("2015-03-01")
|
7
|
+
|
8
|
+
RESCUABLE_CONNECTION_ERRORS = [
|
9
|
+
Net::ReadTimeout,
|
10
|
+
Net::OpenTimeout,
|
11
|
+
TimeoutError,
|
12
|
+
Errno::ETIMEDOUT,
|
13
|
+
Timeout::Error,
|
14
|
+
IOError,
|
15
|
+
EOFError,
|
16
|
+
SocketError,
|
17
|
+
Errno::ECONNRESET,
|
18
|
+
Errno::ECONNABORTED,
|
19
|
+
Errno::EPIPE,
|
20
|
+
Errno::ECONNREFUSED,
|
21
|
+
Errno::EAGAIN,
|
22
|
+
Errno::EHOSTUNREACH,
|
23
|
+
Errno::ENETUNREACH,
|
24
|
+
Resolv::ResolvError,
|
25
|
+
Net::HTTPBadResponse,
|
26
|
+
Net::HTTPHeaderSyntaxError,
|
27
|
+
Net::ProtocolError,
|
28
|
+
ActiveUtils::ConnectionError,
|
29
|
+
ActiveUtils::ResponseError,
|
30
|
+
ActiveUtils::InvalidResponseError
|
31
|
+
]
|
32
|
+
|
33
|
+
def initialize(options = {})
|
34
|
+
@name = options[:name]
|
35
|
+
@callback_url = options[:callback_url]
|
36
|
+
@format = options[:format]
|
37
|
+
end
|
38
|
+
|
39
|
+
def fulfill(order_id, shipping_address, line_items, options = {})
|
40
|
+
raise NotImplementedError.new("Shopify API Service must listen to fulfillment/create Webhooks")
|
41
|
+
end
|
42
|
+
|
43
|
+
def fetch_stock_levels(options = {})
|
44
|
+
response = send_app_request('fetch_stock', options.delete(:headers), options)
|
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})
|
48
|
+
else
|
49
|
+
Response.new(false, "Unable to fetch remote stock levels")
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def fetch_tracking_data(order_numbers, options = {})
|
54
|
+
options.merge!({:order_ids => order_numbers, :order_names => order_numbers})
|
55
|
+
response = send_app_request('fetch_tracking_numbers', options.delete(:headers), options)
|
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 => {}})
|
61
|
+
else
|
62
|
+
Response.new(false, "Unable to fetch remote tracking numbers #{order_numbers.inspect}")
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
def request_uri(action, data)
|
69
|
+
URI.parse "#{@callback_url}/#{action}.#{@format}?#{data.to_query}"
|
70
|
+
end
|
71
|
+
|
72
|
+
def send_app_request(action, headers, data)
|
73
|
+
uri = request_uri(action, data)
|
74
|
+
|
75
|
+
logger.info "[" + @name.upcase + " APP] Post #{uri}"
|
76
|
+
|
77
|
+
response = nil
|
78
|
+
realtime = Benchmark.realtime do
|
79
|
+
begin
|
80
|
+
Timeout.timeout(20.seconds) do
|
81
|
+
response = ssl_get(uri, headers)
|
82
|
+
end
|
83
|
+
rescue *(RESCUABLE_CONNECTION_ERRORS) => e
|
84
|
+
logger.warn "[#{self}] Error while contacting fulfillment service error =\"#{e.message}\""
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
line = "[" + @name.upcase + "APP] Response from #{uri} --> "
|
89
|
+
line << "#{response} #{"%.4fs" % realtime}"
|
90
|
+
logger.info line
|
91
|
+
|
92
|
+
response
|
93
|
+
end
|
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
|
108
|
+
end
|
109
|
+
response_data
|
110
|
+
end
|
111
|
+
|
112
|
+
rescue ActiveSupport::JSON.parse_error, REXML::ParseException
|
113
|
+
{}
|
114
|
+
end
|
115
|
+
|
116
|
+
def encode_payload(payload, root)
|
117
|
+
case @format
|
118
|
+
when 'json'
|
119
|
+
{root => payload}.to_json
|
120
|
+
when 'xml'
|
121
|
+
payload.to_xml(:root => root)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|