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
data/CHANGELOG ADDED
@@ -0,0 +1,26 @@
1
+ * Remove DHL from Webgistix shipping methods [Dennis Thiesen]
2
+ * Update Amazon FBA to use AWS credentials [John Tajima]
3
+ * Use new connection code from ActiveMerchant [cody]
4
+ * Add #valid_credentials? support to all fulfillment services [cody]
5
+ * Return 'Access Denied' message when Webgistix credenentials are invalid [cody]
6
+ * Update Shipwire endpoint hostname [cody]
7
+ * Add missing ISO countries [Edward Ocampo-Gooding]
8
+ * Add support for Guernsey to country.rb [cody]
9
+ * Use a Rails 2.3 compatible OrderedHash [cody]
10
+ * Use :words_connector instead of connector in RequiresParameters [cody]
11
+ * Provide Webgistix with a valid test sku to keep remote tests passing
12
+ * Update PostsData to support get requests
13
+ * Update Shipwire to latest version of dtd.
14
+ * Use real addresses for Shipwire remote fulfillment tests
15
+ * Pass Shipwire the ISO country code instead of the previous name and country combo. Always add the country element to the document
16
+ * Update Shipwire warehouses and don't send unneeded Content-Type header
17
+ * Add configurable timeouts from Active Merchant
18
+ * Shipwire: Send the company in address1 if present. Otherwise send address1 in address1.
19
+ * Always send address to Shipwire
20
+ * Map company to address1 with Shipwire
21
+ * Sync posts_data.rb with ActiveMerchant
22
+ * Add support for fetching tracking numbers to Shipwire
23
+ * Move email to the options hash. Refactor Shipwire commit method.
24
+ * Package for initial upload to Google Code
25
+ * Fix remote Webgistix test
26
+ * Add support for Fulfillment by Amazon Basic Fulfillment
data/Rakefile ADDED
@@ -0,0 +1,60 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'rake/testtask'
4
+ require 'rake/rdoctask'
5
+ require 'rake/gempackagetask'
6
+ require 'rake/contrib/rubyforgepublisher'
7
+
8
+ desc "Default Task"
9
+ task :default => 'test:units'
10
+
11
+ # Run the unit tests
12
+
13
+ namespace :test do
14
+ Rake::TestTask.new(:units) do |t|
15
+ t.pattern = 'test/unit/**/*_test.rb'
16
+ t.ruby_opts << '-rubygems'
17
+ t.libs << 'test'
18
+ t.verbose = true
19
+ end
20
+
21
+ Rake::TestTask.new(:remote) do |t|
22
+ t.pattern = 'test/remote/*_test.rb'
23
+ t.ruby_opts << '-rubygems'
24
+ t.libs << 'test'
25
+ t.verbose = true
26
+ end
27
+ end
28
+
29
+ # Genereate the RDoc documentation
30
+ Rake::RDocTask.new do |rdoc|
31
+ rdoc.rdoc_dir = 'doc'
32
+ rdoc.title = "ActiveFulfillment library"
33
+ rdoc.options << '--line-numbers' << '--inline-source'
34
+ rdoc.rdoc_files.include('README', 'CHANGELOG')
35
+ rdoc.rdoc_files.include('lib/**/*.rb')
36
+ end
37
+
38
+ task :install => [:package] do
39
+ `gem install pkg/#{PKG_FILE_NAME}.gem`
40
+ end
41
+
42
+ begin
43
+ require 'jeweler'
44
+ Jeweler::Tasks.new do |gemspec|
45
+ gemspec.name = 'active_fulfillment'
46
+ gemspec.summary = "Framework and tools for dealing with shipping, tracking and order fulfillment services."
47
+ gemspec.email = "cody@shopify.com"
48
+ gemspec.homepage = "http://github.com/shopify/active_fulfillment"
49
+ gemspec.authors = ["Cody Fauser", "James MacAulay"]
50
+ end
51
+ rescue LoadError
52
+ puts "Jeweler not available. Install it with: gem install jeweler"
53
+ end
54
+
55
+ task :update_common do
56
+ STDERR.puts "Updating common include from ../active_merchant. Please make sure this is up-to-date"
57
+ system("diff -u lib/active_merchant/common.rb ../active_merchant/lib/active_merchant/common.rb | patch -p0")
58
+ system("diff -ur lib/active_merchant/common ../active_merchant/lib/active_merchant/common | patch -p0")
59
+ STDERR.puts "done.."
60
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.10.0
@@ -0,0 +1,74 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{active_fulfillment}
8
+ s.version = "0.10.0"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Cody Fauser", "James MacAulay"]
12
+ s.date = %q{2010-07-06}
13
+ s.email = %q{cody@shopify.com}
14
+ s.files = [
15
+ "CHANGELOG",
16
+ "Rakefile",
17
+ "VERSION",
18
+ "active_fulfillment.gemspec",
19
+ "init.rb",
20
+ "lib/active_fulfillment.rb",
21
+ "lib/active_fulfillment/fulfillment/base.rb",
22
+ "lib/active_fulfillment/fulfillment/response.rb",
23
+ "lib/active_fulfillment/fulfillment/service.rb",
24
+ "lib/active_fulfillment/fulfillment/services.rb",
25
+ "lib/active_fulfillment/fulfillment/services/amazon.rb",
26
+ "lib/active_fulfillment/fulfillment/services/shipwire.rb",
27
+ "lib/active_fulfillment/fulfillment/services/webgistix.rb",
28
+ "lib/active_merchant/common.rb",
29
+ "lib/active_merchant/common/connection.rb",
30
+ "lib/active_merchant/common/country.rb",
31
+ "lib/active_merchant/common/error.rb",
32
+ "lib/active_merchant/common/post_data.rb",
33
+ "lib/active_merchant/common/posts_data.rb",
34
+ "lib/active_merchant/common/requires_parameters.rb",
35
+ "lib/active_merchant/common/utils.rb",
36
+ "lib/active_merchant/common/validateable.rb",
37
+ "lib/certs/cacert.pem",
38
+ "test/fixtures.yml",
39
+ "test/remote/amazon_test.rb",
40
+ "test/remote/shipwire_test.rb",
41
+ "test/remote/webgistix_test.rb",
42
+ "test/test_helper.rb",
43
+ "test/unit/base_test.rb",
44
+ "test/unit/services/amazon_test.rb",
45
+ "test/unit/services/shipwire_test.rb",
46
+ "test/unit/services/webgistix_test.rb"
47
+ ]
48
+ s.homepage = %q{http://github.com/shopify/active_fulfillment}
49
+ s.rdoc_options = ["--charset=UTF-8"]
50
+ s.require_paths = ["lib"]
51
+ s.rubygems_version = %q{1.3.7}
52
+ s.summary = %q{Framework and tools for dealing with shipping, tracking and order fulfillment services.}
53
+ s.test_files = [
54
+ "test/remote/amazon_test.rb",
55
+ "test/remote/shipwire_test.rb",
56
+ "test/remote/webgistix_test.rb",
57
+ "test/test_helper.rb",
58
+ "test/unit/base_test.rb",
59
+ "test/unit/services/amazon_test.rb",
60
+ "test/unit/services/shipwire_test.rb",
61
+ "test/unit/services/webgistix_test.rb"
62
+ ]
63
+
64
+ if s.respond_to? :specification_version then
65
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
66
+ s.specification_version = 3
67
+
68
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
69
+ else
70
+ end
71
+ else
72
+ end
73
+ end
74
+
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require 'active_fulfillment'
@@ -0,0 +1,12 @@
1
+ module ActiveMerchant
2
+ module Fulfillment
3
+ module Base
4
+ mattr_accessor :mode
5
+ self.mode = :production
6
+
7
+ def self.service(name)
8
+ ActiveMerchant::Fulfillment.const_get("#{name.to_s.downcase}_service".camelize)
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,32 @@
1
+ module ActiveMerchant
2
+ module Fulfillment
3
+
4
+ class Error < StandardError
5
+ end
6
+
7
+ class Response
8
+ attr_reader :params
9
+ attr_reader :message
10
+ attr_reader :test
11
+
12
+ def success?
13
+ @success
14
+ end
15
+
16
+ def test?
17
+ @test
18
+ end
19
+
20
+ def initialize(success, message, params = {}, options = {})
21
+ @success, @message, @params = success, message, params.stringify_keys
22
+ @test = options[:test] || false
23
+ end
24
+
25
+ private
26
+ def method_missing(method, *args)
27
+ @params[method.to_s] || super
28
+ end
29
+ end
30
+
31
+ end
32
+ end
@@ -0,0 +1,31 @@
1
+ module ActiveMerchant
2
+ module Fulfillment
3
+ class Service
4
+
5
+ include RequiresParameters
6
+ include PostsData
7
+
8
+ def initialize(options = {})
9
+ check_test_mode(options)
10
+
11
+ @options = {}
12
+ @options.update(options)
13
+ end
14
+
15
+ def test_mode?
16
+ false
17
+ end
18
+
19
+ def test?
20
+ @options[:test] || Base.mode == :test
21
+ end
22
+
23
+ private
24
+ def check_test_mode(options)
25
+ if options[:test] and not test_mode?
26
+ raise ArgumentError, 'Test mode is not supported by this gateway'
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,230 @@
1
+ require 'base64'
2
+ require 'openssl'
3
+
4
+ module ActiveMerchant
5
+ module Fulfillment
6
+ class AmazonService < Service
7
+ OUTBOUND_URL = "https://fba-outbound.amazonaws.com"
8
+ OUTBOUND_XMLNS = 'http://fba-outbound.amazonaws.com/doc/2007-08-02/'
9
+ VERSION = "2007-08-02"
10
+
11
+ SUCCESS, FAILURE, ERROR = 'Accepted', 'Failure', 'Error'
12
+ MESSAGES = {
13
+ :status => {
14
+ 'Accepted' => 'Success',
15
+ 'Failure' => 'Failed',
16
+ 'Error' => 'An error occurred'
17
+ },
18
+ :create => {
19
+ 'Accepted' => 'Successfully submitted the order',
20
+ 'Failure' => 'Failed to submit the order',
21
+ 'Error' => 'An error occurred while submitting the order'
22
+ },
23
+ :list => {
24
+ 'Accepted' => 'Successfully submitted request',
25
+ 'Failure' => 'Failed to submit request',
26
+ 'Error' => 'An error occurred while submitting request'
27
+
28
+ }
29
+ }
30
+
31
+ ENV_NAMESPACES = { 'xmlns:xsd' => 'http://www.w3.org/2001/XMLSchema',
32
+ 'xmlns:env' => 'http://schemas.xmlsoap.org/soap/envelope/',
33
+ 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance'
34
+ }
35
+
36
+ AWS_SECURITY_ATTRIBUTES = {
37
+ "env:actor" => "http://schemas.xmlsoap.org/soap/actor/next",
38
+ "env:mustUnderstand" => "0",
39
+ "xmlns:aws" => "http://security.amazonaws.com/doc/2007-01-01/"
40
+ }
41
+
42
+ @@digest = OpenSSL::Digest::Digest.new("sha1")
43
+
44
+ OPERATIONS = {
45
+ :status => 'GetServiceStatus',
46
+ :create => 'CreateFulfillmentOrder',
47
+ :list => 'ListAllFulfillmentOrders'
48
+ }
49
+
50
+ # The first is the label, and the last is the code
51
+ # Standard: 3-5 business days
52
+ # Expedited: 2 business days
53
+ # Priority: 1 business day
54
+ def self.shipping_methods
55
+ [
56
+ [ 'Standard Shipping', 'Standard' ],
57
+ [ 'Expedited Shipping', 'Expedited' ],
58
+ [ 'Priority Shipping', 'Priority' ]
59
+ ].inject(ActiveSupport::OrderedHash.new){|h, (k,v)| h[k] = v; h}
60
+ end
61
+
62
+ def self.sign(aws_secret_access_key, auth_string)
63
+ Base64.encode64(OpenSSL::HMAC.digest(@@digest, aws_secret_access_key, auth_string)).strip
64
+ end
65
+
66
+ def initialize(options = {})
67
+ requires!(options, :login, :password)
68
+ super
69
+ end
70
+
71
+ def status
72
+ commit :status, build_status_request
73
+ end
74
+
75
+ def fulfill(order_id, shipping_address, line_items, options = {})
76
+ requires!(options, :order_date, :comment, :shipping_method)
77
+ commit :create, build_fulfillment_request(order_id, shipping_address, line_items, options)
78
+ end
79
+
80
+ def fetch_current_orders
81
+ commit :list, build_get_current_fulfillment_orders_request
82
+ end
83
+
84
+ def valid_credentials?
85
+ status.success?
86
+ end
87
+
88
+ def test_mode?
89
+ false
90
+ end
91
+
92
+ private
93
+ def soap_request(request)
94
+ xml = Builder::XmlMarkup.new :indent => 2
95
+ xml.instruct!
96
+ xml.tag! "env:Envelope", ENV_NAMESPACES do
97
+ xml.tag! "env:Header" do
98
+ add_credentials(xml, request)
99
+ end
100
+ xml.tag! "env:Body" do
101
+ yield xml
102
+ end
103
+ end
104
+ xml.target!
105
+ end
106
+
107
+ def build_status_request
108
+ request = OPERATIONS[:status]
109
+ soap_request(request) do |xml|
110
+ xml.tag! request, { 'xmlns' => OUTBOUND_XMLNS }
111
+ end
112
+ end
113
+
114
+ def build_get_current_fulfillment_orders_request
115
+ request = OPERATIONS[:list]
116
+ soap_request(request) do |xml|
117
+ xml.tag! request, { 'xmlns' => OUTBOUND_XMLNS } do
118
+ xml.tag! "NumberOfResultsRequested", 5
119
+ xml.tag! "QueryStartDateTime", Time.now.utc.yesterday.strftime("%Y-%m-%dT%H:%M:%SZ")
120
+ end
121
+ end
122
+ end
123
+
124
+ def build_fulfillment_request(order_id, shipping_address, line_items, options)
125
+ request = OPERATIONS[:create]
126
+ soap_request(request) do |xml|
127
+ xml.tag! request, { 'xmlns' => OUTBOUND_XMLNS } do
128
+ xml.tag! "MerchantFulfillmentOrderId", order_id
129
+ xml.tag! "DisplayableOrderId", order_id
130
+ xml.tag! "DisplayableOrderDateTime", options[:order_date].strftime("%Y-%m-%dT%H:%M:%SZ")
131
+ xml.tag! "DisplayableOrderComment", options[:comment]
132
+ xml.tag! "ShippingSpeedCategory", options[:shipping_method]
133
+
134
+ add_address(xml, shipping_address)
135
+ add_items(xml, line_items)
136
+ end
137
+ end
138
+ end
139
+
140
+ def add_credentials(xml, request)
141
+ login = @options[:login]
142
+ timestamp = "#{Time.now.utc.strftime("%Y-%m-%dT%H:%M:%S")}Z"
143
+ signature = self.class.sign(@options[:password], "#{request}#{timestamp}")
144
+
145
+ xml.tag! 'aws:AWSAccessKeyId', login, AWS_SECURITY_ATTRIBUTES
146
+ xml.tag! 'aws:Signature', signature, AWS_SECURITY_ATTRIBUTES
147
+ xml.tag! 'aws:Timestamp', timestamp, AWS_SECURITY_ATTRIBUTES
148
+ end
149
+
150
+ def add_items(xml, line_items)
151
+ Array(line_items).each_with_index do |item, index|
152
+ xml.tag! 'Item' do
153
+ xml.tag! 'MerchantSKU', item[:sku]
154
+ xml.tag! "MerchantFulfillmentOrderItemId", index
155
+ xml.tag! "Quantity", item[:quantity]
156
+ xml.tag! "GiftMessage", item[:gift_message] unless item[:gift_message].blank?
157
+ xml.tag! "DisplayableComment", item[:comment] unless item[:comment].blank?
158
+ end
159
+ end
160
+ end
161
+
162
+ def add_address(xml, address)
163
+ xml.tag! 'DestinationAddress' do
164
+ xml.tag! 'Name', address[:name]
165
+ xml.tag! 'Line1', address[:address1]
166
+ xml.tag! 'Line2', address[:address2] unless address[:address2].blank?
167
+ xml.tag! 'Line3', address[:address3] unless address[:address3].blank?
168
+ xml.tag! 'City', address[:city]
169
+ xml.tag! 'StateOrProvinceCode', address[:state]
170
+ xml.tag! 'CountryCode', address[:country]
171
+ xml.tag! 'PostalCode', address[:zip]
172
+ xml.tag! 'PhoneNumber', address[:phone] unless address[:phone].blank?
173
+ end
174
+ end
175
+
176
+ def commit(op, body)
177
+ data = ssl_post(OUTBOUND_URL, body, 'Content-Type' => 'application/soap+xml; charset=utf-8')
178
+ response = parse(op, data)
179
+ Response.new(success?(response), message_from(response), response)
180
+ rescue ActiveMerchant::ResponseError => e
181
+ response = parse_error(e.response)
182
+ Response.new(false, message_from(response), response)
183
+ end
184
+
185
+ def success?(response)
186
+ response[:response_status] == SUCCESS
187
+ end
188
+
189
+ def message_from(response)
190
+ response[:response_comment]
191
+ end
192
+
193
+ def parse(op, xml)
194
+ response = {}
195
+ action = OPERATIONS[op]
196
+ document = REXML::Document.new(xml)
197
+ node = REXML::XPath.first(document, "//ns1:#{action}Response")
198
+
199
+ response[:response_status] = SUCCESS
200
+ response[:response_comment] = MESSAGES[op][SUCCESS]
201
+ response
202
+ end
203
+
204
+ def parse_error(http_response)
205
+ response = {}
206
+ response[:http_code] = http_response.code
207
+ response[:http_message] = http_response.message
208
+
209
+ document = REXML::Document.new(http_response.body)
210
+
211
+ node = REXML::XPath.first(document, "//env:Fault")
212
+
213
+ failed_node = node.find_first_recursive {|sib| sib.name == "Fault" }
214
+ faultcode_node = node.find_first_recursive {|sib| sib.name == "faultcode" }
215
+ faultstring_node = node.find_first_recursive {|sib| sib.name == "faultstring" }
216
+
217
+ response[:response_status] = FAILURE
218
+ response[:faultcode] = faultcode_node ? faultcode_node.text : ""
219
+ response[:faultstring] = faultstring_node ? faultstring_node.text : ""
220
+ response[:response_comment] = "#{response[:faultcode]} #{response[:faultstring]}"
221
+ response
222
+ rescue REXML::ParseException => e
223
+ response[:http_body] = http_response.body
224
+ response[:response_status] = FAILURE
225
+ response[:response_comment] = "#{response[:http_code]}: #{response[:http_message]}"
226
+ response
227
+ end
228
+ end
229
+ end
230
+ end