active_fulfillment 0.10.0

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