active_fulfillment 0.10.0

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.
Files changed (33) hide show
  1. data/CHANGELOG +26 -0
  2. data/Rakefile +60 -0
  3. data/VERSION +1 -0
  4. data/active_fulfillment.gemspec +74 -0
  5. data/init.rb +1 -0
  6. data/lib/active_fulfillment/fulfillment/base.rb +12 -0
  7. data/lib/active_fulfillment/fulfillment/response.rb +32 -0
  8. data/lib/active_fulfillment/fulfillment/service.rb +31 -0
  9. data/lib/active_fulfillment/fulfillment/services/amazon.rb +230 -0
  10. data/lib/active_fulfillment/fulfillment/services/shipwire.rb +236 -0
  11. data/lib/active_fulfillment/fulfillment/services/webgistix.rb +207 -0
  12. data/lib/active_fulfillment/fulfillment/services.rb +3 -0
  13. data/lib/active_fulfillment.rb +50 -0
  14. data/lib/active_merchant/common/connection.rb +172 -0
  15. data/lib/active_merchant/common/country.rb +319 -0
  16. data/lib/active_merchant/common/error.rb +26 -0
  17. data/lib/active_merchant/common/post_data.rb +24 -0
  18. data/lib/active_merchant/common/posts_data.rb +47 -0
  19. data/lib/active_merchant/common/requires_parameters.rb +16 -0
  20. data/lib/active_merchant/common/utils.rb +18 -0
  21. data/lib/active_merchant/common/validateable.rb +76 -0
  22. data/lib/active_merchant/common.rb +14 -0
  23. data/lib/certs/cacert.pem +7815 -0
  24. data/test/fixtures.yml +11 -0
  25. data/test/remote/amazon_test.rb +93 -0
  26. data/test/remote/shipwire_test.rb +145 -0
  27. data/test/remote/webgistix_test.rb +80 -0
  28. data/test/test_helper.rb +60 -0
  29. data/test/unit/base_test.rb +17 -0
  30. data/test/unit/services/amazon_test.rb +187 -0
  31. data/test/unit/services/shipwire_test.rb +164 -0
  32. data/test/unit/services/webgistix_test.rb +145 -0
  33. metadata +106 -0
@@ -0,0 +1,236 @@
1
+ require 'cgi'
2
+
3
+ module ActiveMerchant
4
+ module Fulfillment
5
+ class ShipwireService < Service
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 EmailAddress, valid email is required/
30
+
31
+ # The first is the label, and the last is the code
32
+ def self.shipping_methods
33
+ [ ['1 Day Service', '1D'],
34
+ ['2 Day Service', '2D'],
35
+ ['Ground Service', 'GD'],
36
+ ['Freight Service', 'FT'],
37
+ ['International', 'INTL']
38
+ ].inject(ActiveSupport::OrderedHash.new){|h, (k,v)| h[k] = v; h}
39
+ end
40
+
41
+ # Pass in the login and password for the shipwire account.
42
+ # Optionally pass in the :test => true to force test mode
43
+ def initialize(options = {})
44
+ requires!(options, :login, :password)
45
+
46
+ super
47
+ end
48
+
49
+ def fulfill(order_id, shipping_address, line_items, options = {})
50
+ commit :fulfillment, build_fulfillment_request(order_id, shipping_address, line_items, options)
51
+ end
52
+
53
+ def fetch_stock_levels(options = {})
54
+ commit :inventory, build_inventory_request(options)
55
+ end
56
+
57
+ def fetch_tracking_numbers(options = {})
58
+ commit :tracking, build_tracking_request(options)
59
+ end
60
+
61
+ def valid_credentials?
62
+ response = fetch_tracking_numbers
63
+ response.message !~ INVALID_LOGIN
64
+ end
65
+
66
+ def test_mode?
67
+ true
68
+ end
69
+
70
+ private
71
+ def build_fulfillment_request(order_id, shipping_address, line_items, options)
72
+ xml = Builder::XmlMarkup.new :indent => 2
73
+ xml.instruct!
74
+ xml.declare! :DOCTYPE, :OrderList, :SYSTEM, SCHEMA_URLS[:fulfillment]
75
+ xml.tag! 'OrderList' do
76
+ add_credentials(xml)
77
+ xml.tag! 'Referer', 'Active Fulfillment'
78
+ add_order(xml, order_id, shipping_address, line_items, options)
79
+ end
80
+ xml.target!
81
+ end
82
+
83
+ def build_inventory_request(options)
84
+ xml = Builder::XmlMarkup.new :indent => 2
85
+ xml.instruct!
86
+ xml.declare! :DOCTYPE, :InventoryStatus, :SYSTEM, SCHEMA_URLS[:inventory]
87
+ xml.tag! 'InventoryUpdate' do
88
+ add_credentials(xml)
89
+ xml.tag! 'Warehouse', WAREHOUSES[options[:warehouse]]
90
+ xml.tag! 'ProductCode', options[:sku]
91
+ end
92
+ end
93
+
94
+ def build_tracking_request(options)
95
+ xml = Builder::XmlMarkup.new
96
+ xml.instruct!
97
+ xml.declare! :DOCTYPE, :InventoryStatus, :SYSTEM, SCHEMA_URLS[:inventory]
98
+ xml.tag! 'TrackingUpdate' do
99
+ add_credentials(xml)
100
+ xml.tag! 'Server', test? ? 'Test' : 'Production'
101
+ xml.tag! 'Bookmark', '3'
102
+ end
103
+ end
104
+
105
+ def add_credentials(xml)
106
+ xml.tag! 'EmailAddress', @options[:login]
107
+ xml.tag! 'Password', @options[:password]
108
+ xml.tag! 'Server', test? ? 'Test' : 'Production'
109
+ end
110
+
111
+ def add_order(xml, order_id, shipping_address, line_items, options)
112
+ xml.tag! 'Order', :id => order_id do
113
+ xml.tag! 'Warehouse', options[:warehouse] || '00'
114
+
115
+ add_address(xml, shipping_address, options)
116
+ xml.tag! 'Shipping', options[:shipping_method] unless options[:shipping_method].blank?
117
+
118
+ Array(line_items).each_with_index do |line_item, index|
119
+ add_item(xml, line_item, index)
120
+ end
121
+ end
122
+ end
123
+
124
+ def add_address(xml, address, options)
125
+ xml.tag! 'AddressInfo', :type => 'Ship' do
126
+ xml.tag! 'Name' do
127
+ xml.tag! 'Full', address[:name]
128
+ end
129
+
130
+ if address[:company].blank?
131
+ xml.tag! 'Address1', address[:address1]
132
+ xml.tag! 'Address2', address[:address2]
133
+ else
134
+ xml.tag! 'Address1', address[:company]
135
+ xml.tag! 'Address2', address[:address1]
136
+ xml.tag! 'Address3', address[:address2]
137
+ end
138
+
139
+ xml.tag! 'City', address[:city]
140
+ xml.tag! 'State', address[:state] unless address[:state].blank?
141
+ xml.tag! 'Country', address[:country]
142
+
143
+ xml.tag! 'Zip', address[:zip]
144
+ xml.tag! 'Phone', address[:phone] unless address[:phone].blank?
145
+ xml.tag! 'Email', options[:email] unless options[:email].blank?
146
+ end
147
+ end
148
+
149
+ # Code is limited to 12 characters
150
+ def add_item(xml, item, index)
151
+ xml.tag! 'Item', :num => index do
152
+ xml.tag! 'Code', item[:sku]
153
+ xml.tag! 'Quantity', item[:quantity]
154
+ end
155
+ end
156
+
157
+ def commit(action, request)
158
+ data = ssl_post(SERVICE_URLS[action], "#{POST_VARS[action]}=#{CGI.escape(request)}")
159
+
160
+ response = parse_response(action, data)
161
+ Response.new(response[:success], response[:message], response, :test => test?)
162
+ end
163
+
164
+ def parse_response(action, data)
165
+ case action
166
+ when :fulfillment
167
+ parse_fulfillment_response(data)
168
+ when :inventory
169
+ parse_inventory_response(data)
170
+ when :tracking
171
+ parse_tracking_response(data)
172
+ else
173
+ raise ArgumentError, "Unknown action #{action}"
174
+ end
175
+ end
176
+
177
+ def parse_fulfillment_response(xml)
178
+ response = {}
179
+
180
+ document = REXML::Document.new(xml)
181
+ document.root.elements.each do |node|
182
+ response[node.name.underscore.to_sym] = node.text
183
+ end
184
+
185
+ response[:success] = response[:status] == '0'
186
+ response[:message] = response[:success] ? "Successfully submitted the order" : message_from(response[:error_message])
187
+ response
188
+ end
189
+
190
+ def parse_inventory_response(xml)
191
+ response = {}
192
+ response[:stock_levels] = {}
193
+
194
+ document = REXML::Document.new(xml)
195
+ document.root.elements.each do |node|
196
+ if node.name == 'Product'
197
+ response[:stock_levels][node.attributes['code']] = node.attributes['quantity'].to_i
198
+ else
199
+ response[node.name.underscore.to_sym] = node.text
200
+ end
201
+ end
202
+
203
+ response[:success] = test? ? response[:status] == 'Test' : response[:status] == '0'
204
+ response[:message] = response[:success] ? "Successfully received the stock levels" : message_from(response[:error_message])
205
+
206
+ response
207
+ end
208
+
209
+ def parse_tracking_response(xml)
210
+ response = {}
211
+ response[:tracking_numbers] = {}
212
+
213
+ document = REXML::Document.new(xml)
214
+
215
+ document.root.elements.each do |node|
216
+ if node.name == 'Order'
217
+ response[:tracking_numbers][node.attributes['id']] = node.attributes['trackingNumber'] if node.attributes["shipped"] == "YES"
218
+ else
219
+ response[node.name.underscore.to_sym] = node.text
220
+ end
221
+ end
222
+
223
+ response[:success] = test? ? (response[:status] == '0' || response[:status] == 'Test') : response[:status] == '0'
224
+ response[:message] = response[:success] ? "Successfully received the tracking numbers" : message_from(response[:error_message])
225
+ response
226
+ end
227
+
228
+ def message_from(string)
229
+ return if string.blank?
230
+ string.gsub("\n", '').squeeze(" ")
231
+ end
232
+ end
233
+ end
234
+ end
235
+
236
+
@@ -0,0 +1,207 @@
1
+ module ActiveMerchant
2
+ module Fulfillment
3
+ class WebgistixService < Service
4
+ TEST_URL = 'https://www.webgistix.com/XML/shippingTest.asp'
5
+ LIVE_URL = 'https://www.webgistix.com/XML/API.asp'
6
+
7
+ SUCCESS, FAILURE = 'True', 'False'
8
+ SUCCESS_MESSAGE = 'Successfully submitted the order'
9
+ FAILURE_MESSAGE = 'Failed to submit the order'
10
+ INVALID_LOGIN = 'Access Denied'
11
+
12
+ # The first is the label, and the last is the code
13
+ def self.shipping_methods
14
+ [
15
+ ["UPS Ground Shipping", "Ground"],
16
+ ["UPS Standard Shipping (Canada Only)", "Standard"],
17
+ ["UPS 3-Business Day", "3-Day Select"],
18
+ ["UPS 2-Business Day", "2nd Day Air"],
19
+ ["UPS 2-Business Day AM", "2nd Day Air AM"],
20
+ ["UPS Next Day", "Next Day Air"],
21
+ ["UPS Next Day Saver", "Next Day Air Saver"],
22
+ ["UPS Next Day Early AM", "Next Day Air Early AM"],
23
+ ["UPS Worldwide Express (Next Day)", "Worldwide Express"],
24
+ ["UPS Worldwide Expedited (2nd Day)", "Worldwide Expedited"],
25
+ ["UPS Worldwide Express Saver", "Worldwide Express Saver"],
26
+ ["FedEx Priority Overnight", "FedEx Priority Overnight"],
27
+ ["FedEx Standard Overnight", "FedEx Standard Overnight"],
28
+ ["FedEx First Overnight", "FedEx First Overnight"],
29
+ ["FedEx 2nd Day", "FedEx 2nd Day"],
30
+ ["FedEx Express Saver", "FedEx Express Saver"],
31
+ ["FedEx International Priority", "FedEx International Priority"],
32
+ ["FedEx International Economy", "FedEx International Economy"],
33
+ ["FedEx International First", "FedEx International First"],
34
+ ["FedEx Ground", "FedEx Ground"],
35
+ ["USPS Priority Mail & Global Priority Mail", "Priority"],
36
+ ["USPS Priority Mail & Global Priority Mail (flat rate)", "Flat Rate Priority"],
37
+ ["USPS First Class Mail", "First Class"],
38
+ ["USPS Express Mail & Global Express Mail", "Express"],
39
+ ["USPS Flat Rate Global Express Mail", "Global Express Mail Flat Rate"],
40
+ ["USPS Parcel Post", "Parcel"],
41
+ ["USPS First Class International", "First Class International"],
42
+ ["USPS Media Mail", "Media Mail"],
43
+ ["USPS Economy Parcel Post", "Economy Parcel"],
44
+ ["USPS Economy Air Letter Post", "Economy Letter"],
45
+ ].inject(ActiveSupport::OrderedHash.new){|h, (k,v)| h[k] = v; h}
46
+ end
47
+
48
+ # Pass in the login and password for the shipwire account.
49
+ # Optionally pass in the :test => true to force test mode
50
+ def initialize(options = {})
51
+ requires!(options, :login, :password)
52
+ super
53
+ @url = test? ? TEST_URL : LIVE_URL
54
+ end
55
+
56
+ def fulfill(order_id, shipping_address, line_items, options = {})
57
+ requires!(options, :shipping_method)
58
+ commit build_fulfillment_request(order_id, shipping_address, line_items, options)
59
+ end
60
+
61
+ def valid_credentials?
62
+ response = fulfill('', {}, [], :shipping_method => '')
63
+ response.message != INVALID_LOGIN
64
+ end
65
+
66
+ def test_mode?
67
+ true
68
+ end
69
+
70
+ private
71
+ #<?xml version="1.0"?>
72
+ # <OrderXML>
73
+ # <Password>Webgistix</Password>
74
+ # <CustomerID>3</CustomerID>
75
+ # <Order>
76
+ # <ReferenceNumber></ReferenceNumber>
77
+ # <Company>Test Company</Company>
78
+ # <Name>Joe Smith</Name>
79
+ # <Address1>123 Main St.</Address1>
80
+ # <Address2></Address2>
81
+ # <Address3></Address3>
82
+ # <City>Olean</City>
83
+ # <State>NY</State>
84
+ # <ZipCode>14760</ZipCode>
85
+ # <Country>United States</Country>
86
+ # <Email>info@webgistix.com</Email>
87
+ # <Phone>1-123-456-7890</Phone>
88
+ # <ShippingInstructions>Ground</ShippingInstructions>
89
+ # <OrderComments>Test Order</OrderComments>
90
+ # <Approve>0</Approve>
91
+ # <Item>
92
+ # <ItemID>testitem</ItemID>
93
+ # <ItemQty>2</ItemQty>
94
+ # </Item>
95
+ # </Order>
96
+ # </OrderXML>
97
+ def build_fulfillment_request(order_id, shipping_address, line_items, options)
98
+ xml = Builder::XmlMarkup.new :indent => 2
99
+ xml.instruct!
100
+ xml.tag! 'OrderXML' do
101
+ add_credentials(xml)
102
+ add_order(xml, order_id, shipping_address, line_items, options)
103
+ end
104
+ xml.target!
105
+ end
106
+
107
+ def add_credentials(xml)
108
+ xml.tag! 'CustomerID', @options[:login]
109
+ xml.tag! 'Password', @options[:password]
110
+ end
111
+
112
+ def add_order(xml, order_id, shipping_address, line_items, options)
113
+ xml.tag! 'Order' do
114
+ xml.tag! 'ReferenceNumber', order_id
115
+ xml.tag! 'ShippingInstructions', options[:shipping_method]
116
+ xml.tag! 'Approve', 1
117
+ xml.tag! 'OrderComments', options[:comment] unless options[:comment].blank?
118
+
119
+ add_address(xml, shipping_address, options)
120
+
121
+ Array(line_items).each_with_index do |line_item, index|
122
+ add_item(xml, line_item, index)
123
+ end
124
+ end
125
+ end
126
+
127
+ def add_address(xml, address, options)
128
+ xml.tag! 'Name', address[:name]
129
+ xml.tag! 'Address1', address[:address1]
130
+ xml.tag! 'Address2', address[:address2] unless address[:address2].blank?
131
+ xml.tag! 'Address3', address[:address3] unless address[:address3].blank?
132
+ xml.tag! 'City', address[:city]
133
+ xml.tag! 'State', address[:state]
134
+ xml.tag! 'ZipCode', address[:zip]
135
+ xml.tag! 'Company', address[:company]
136
+
137
+ unless address[:country].blank?
138
+ country = Country.find(address[:country])
139
+ xml.tag! 'Country', country.name
140
+ end
141
+
142
+ xml.tag! 'Phone', address[:phone]
143
+ xml.tag! 'Email', options[:email] unless options[:email].blank?
144
+ end
145
+
146
+ def add_item(xml, item, index)
147
+ xml.tag! 'Item' do
148
+ xml.tag! 'ItemID', item[:sku] unless item[:sku].blank?
149
+ xml.tag! 'ItemQty', item[:quantity] unless item[:quantity].blank?
150
+ end
151
+ end
152
+
153
+ def commit(request)
154
+ @response = parse(ssl_post(@url, request,
155
+ 'EndPointURL' => @url,
156
+ 'Content-Type' => 'text/xml; charset="utf-8"')
157
+ )
158
+
159
+ Response.new(success?(@response), message_from(@response), @response,
160
+ :test => test?
161
+ )
162
+ end
163
+
164
+ def success?(response)
165
+ response[:success] == SUCCESS
166
+ end
167
+
168
+ def message_from(response)
169
+ return SUCCESS_MESSAGE if success?(response)
170
+
171
+ if response[:error_0] == INVALID_LOGIN
172
+ INVALID_LOGIN
173
+ else
174
+ FAILURE_MESSAGE
175
+ end
176
+ end
177
+
178
+ def parse(xml)
179
+ response = {}
180
+
181
+ begin
182
+ document = REXML::Document.new("<response>#{xml}</response>")
183
+ rescue REXML::ParseException
184
+ response[:success] = FAILURE
185
+ return response
186
+ end
187
+ # Fetch the errors
188
+ document.root.elements.to_a("Error").each_with_index do |e, i|
189
+ response["error_#{i}".to_sym] = e.text
190
+ end
191
+
192
+ # Check if completed
193
+ if completed = REXML::XPath.first(document, '//Completed')
194
+ completed.elements.each do |e|
195
+ response[e.name.underscore.to_sym] = e.text
196
+ end
197
+ else
198
+ response[:success] = FAILURE
199
+ end
200
+
201
+ response
202
+ end
203
+ end
204
+ end
205
+ end
206
+
207
+
@@ -0,0 +1,3 @@
1
+ require 'active_fulfillment/fulfillment/services/shipwire'
2
+ require 'active_fulfillment/fulfillment/services/webgistix'
3
+ require 'active_fulfillment/fulfillment/services/amazon'
@@ -0,0 +1,50 @@
1
+ #--
2
+ # Copyright (c) 2009 Jaded Pixel
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # "Software"), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+ #++
23
+
24
+ $:.unshift File.dirname(__FILE__)
25
+
26
+ begin
27
+ require 'active_support'
28
+ rescue LoadError
29
+ require 'rubygems'
30
+ require 'active_support'
31
+ end
32
+
33
+ begin
34
+ require 'builder'
35
+ rescue LoadError
36
+ require 'rubygems'
37
+ require_gem 'builder'
38
+ end
39
+
40
+
41
+ require 'cgi'
42
+ require 'net/https'
43
+ require 'rexml/document'
44
+ require 'active_merchant/common'
45
+
46
+ require 'active_fulfillment/fulfillment/base'
47
+ require 'active_fulfillment/fulfillment/response'
48
+ require 'active_fulfillment/fulfillment/service'
49
+ require 'active_fulfillment/fulfillment/services'
50
+