spreedly_core 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +15 -0
- data/README.md +186 -0
- data/Rakefile +33 -0
- data/lib/spreedly_core/base.rb +93 -0
- data/lib/spreedly_core/gateway.rb +19 -0
- data/lib/spreedly_core/payment_method.rb +106 -0
- data/lib/spreedly_core/test_extensions.rb +47 -0
- data/lib/spreedly_core/transactions.rb +127 -0
- data/lib/spreedly_core/version.rb +3 -0
- data/lib/spreedly_core.rb +37 -0
- data/test/config/spreedly_core.yml +3 -0
- data/test/config/spreedly_core.yml.example +3 -0
- data/test/spreedly_core_test.rb +237 -0
- data/test/test_helper.rb +17 -0
- metadata +107 -0
data/LICENSE
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
Copyright (c) 2011 403 Labs, LLC <http://www.403labs.com>
|
2
|
+
|
3
|
+
SpreedlyCore library is free software: you can redistribute it and/or
|
4
|
+
modify it under the terms of the GNU Lesser General Public License as
|
5
|
+
published by the Free Software Foundation, either version 3 of the
|
6
|
+
License, or (at your option) any later version.
|
7
|
+
|
8
|
+
SpreedlyCore library is distributed in the hope that it will be useful,
|
9
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
10
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser
|
11
|
+
General Public License for more details.
|
12
|
+
|
13
|
+
You should have received a copy of the GNU Lesser General Public License
|
14
|
+
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
15
|
+
|
data/README.md
ADDED
@@ -0,0 +1,186 @@
|
|
1
|
+
SpreedlyCore
|
2
|
+
======
|
3
|
+
|
4
|
+
SpreedlyCore is a Ruby library for accessing the [Spreedly Core API](https://spreedlycore.com/).
|
5
|
+
|
6
|
+
The beauty behind Spreedly Core is that you lower your
|
7
|
+
[PCI Compliance](https://www.pcisecuritystandards.org/) risk
|
8
|
+
by storing credit card information on their service while still having access
|
9
|
+
to make payments and credits. This is possible by having your customers POST their
|
10
|
+
credit card info to the spreedly core service while embedding a transparent
|
11
|
+
redirect URL back to your application. See "Submit payment form" on
|
12
|
+
[the quick start guide](https://spreedlycore.com/manual/quickstart)
|
13
|
+
how the magic happens.
|
14
|
+
|
15
|
+
|
16
|
+
Quickstart
|
17
|
+
----------
|
18
|
+
|
19
|
+
RubyGems:
|
20
|
+
|
21
|
+
gem install spreedly_core
|
22
|
+
irb
|
23
|
+
require 'rubygems'
|
24
|
+
require 'spreedly_core'
|
25
|
+
SpreedlyCore.configure("Your API Login", "Your API Secret", "Test Gateway Token")
|
26
|
+
See the [quickstart guide](https://spreedlycore.com/manual/quickstart) for
|
27
|
+
information regarding tokens.
|
28
|
+
|
29
|
+
We'll now lookup the payment method stored on SpreedlyCore using token param
|
30
|
+
from the transparent redirect url
|
31
|
+
|
32
|
+
payment_token = SpreedlyCore::PaymentMethod.find(payment_token)
|
33
|
+
transaction = payment_token.purchase(100)
|
34
|
+
|
35
|
+
Test Integration
|
36
|
+
----------
|
37
|
+
|
38
|
+
Since your web form handles the creation of payment methods on their service,
|
39
|
+
integration testing can be a bit of a headache. No worries though:
|
40
|
+
|
41
|
+
require 'spreedly_core'
|
42
|
+
require 'spreedly_core/test_extensions'
|
43
|
+
SpreedlyCore.configure("Your API Login", "Your API Secret", "Test Gateway Token")
|
44
|
+
master_card_data = SpreedlyCore::TestHelper.cc_data(:master) # Lookup test credit card data
|
45
|
+
token = SpreedlyCore::PaymentMethod.create_test_token(master_card_data)
|
46
|
+
|
47
|
+
You now have access to a payment method token, which can be used just like your
|
48
|
+
application would use it. Note, you should use a test gateway since you are
|
49
|
+
actually hitting the Spreedly Core service. Let's use the test credit card
|
50
|
+
payment method to make a purchase:
|
51
|
+
|
52
|
+
payment_method = SpreedlyCore::PaymentMethod.find(token)
|
53
|
+
purchase_transaction = payment_method.purchase(100)
|
54
|
+
purchase_transaction.succeeded? # true
|
55
|
+
|
56
|
+
Let's now use a credit card that is configured to fail upon purchase:
|
57
|
+
|
58
|
+
master_card_data = SpreedlyCore::TestHelper.cc_data(:master, :card_type => :failed)
|
59
|
+
token = SpreedlyCore::PaymentMethod.create_test_token(master_card_data)
|
60
|
+
payment_method = SpreedlyCore::PaymentMethod.find(token)
|
61
|
+
purchase_transaction = payment_method.purchase(100)
|
62
|
+
purchase_transaction.succeeded? # false
|
63
|
+
|
64
|
+
Other test cards available include :visa, :american_express, and :discover
|
65
|
+
|
66
|
+
Usage
|
67
|
+
----------
|
68
|
+
|
69
|
+
Using spreedly_core in irb:
|
70
|
+
|
71
|
+
require 'spreedly_core'
|
72
|
+
require 'spreedly_core/test_extensions' # allow creating payment methods from the command line
|
73
|
+
SpreedlyCore.configure("Your API Login", "Your API Secret", "Test Gateway Token")
|
74
|
+
master_card_data = SpreedlyCore::TestHelper.cc_data(:master)
|
75
|
+
token = SpreedlyCore::PaymentMethod.create_test_token(master_card_data)
|
76
|
+
|
77
|
+
|
78
|
+
Look up a payment method:
|
79
|
+
|
80
|
+
payment_method = SpreedlyCore::PaymentMethod.find(token)
|
81
|
+
|
82
|
+
Retain a payment method for later use:
|
83
|
+
|
84
|
+
retain_transaction = payment_method.retain
|
85
|
+
retain_transaction.succeeded? # true
|
86
|
+
|
87
|
+
Redact a previously retained payment method:
|
88
|
+
|
89
|
+
redact_transaction = payment_method.redact
|
90
|
+
redact_transaction.succeeded?
|
91
|
+
|
92
|
+
Make a purchase against a payment method:
|
93
|
+
|
94
|
+
purchase_transaction = payment_method.purchase(100)
|
95
|
+
purchase_transaction.succeeded? # true
|
96
|
+
|
97
|
+
Make an authorize request against a payment method, then capture the payment
|
98
|
+
|
99
|
+
authorize = payment_method.authorize(100)
|
100
|
+
authorize.succeeded? # true
|
101
|
+
capture = authorize.capture(50) # Capture only half of the authorized amount
|
102
|
+
capture.succeeded? # true
|
103
|
+
|
104
|
+
authorize = payment_method.authorize(100)
|
105
|
+
authorize.succeeded? # true
|
106
|
+
authorized.capture # Capture the full amount
|
107
|
+
capture.succeeded? # true
|
108
|
+
|
109
|
+
Void a previous purchase:
|
110
|
+
|
111
|
+
purchase_transaction.void # void the purchase
|
112
|
+
|
113
|
+
Credit a previous purchase:
|
114
|
+
|
115
|
+
purchase_transaction = payment_method.purchase(100) # make a purchase
|
116
|
+
purchase_transaction.credit
|
117
|
+
purchase_transaction.succeeded? # true
|
118
|
+
|
119
|
+
Credit part of a previous purchase:
|
120
|
+
|
121
|
+
purchase_transaction = payment_method.purchase(100) # make a purchase
|
122
|
+
purchase_transaction.credit(50) # provide a partial credit
|
123
|
+
purchase_transaction.succeeded? # true
|
124
|
+
|
125
|
+
|
126
|
+
Additional Field Validation
|
127
|
+
----------
|
128
|
+
|
129
|
+
|
130
|
+
The Spreedyly Core API provides validation of the credit card number, cve, and
|
131
|
+
first and last name. In most cases this is enough, however sometimes you want to
|
132
|
+
enforce the billing information as well. This can be accomplished via:
|
133
|
+
|
134
|
+
require 'spreedly_core'
|
135
|
+
require 'spreedly_core/test_extensions'
|
136
|
+
SpreedlyCore.configure("Your API Login", "Your API Secret", "Test Gateway Token")
|
137
|
+
SpreedlyCore::PaymentMethod.additional_required_cc_fields :address1, :city, :state, :zip
|
138
|
+
master_card_data = SpreedlyCore::TestHelper.cc_data(:master)
|
139
|
+
token = SpreedlyCore::PaymentMethod.create_test_token(master_card_data)
|
140
|
+
payment_method = SpreedlyCore::PaymentMethod.find(token)
|
141
|
+
payment_method.valid? # returns false
|
142
|
+
payment_method.errors # ["Address1 can't be blank", "City can't be blank", "State can't be blank", "Zip can't be blank"]
|
143
|
+
master_card_data = SpreedlyCore::TestHelper.cc_data(:master, :credit_card => {:address1 => "742 Evergreen Terrace", :city => "Springfield", :state => "IL", 62701})
|
144
|
+
payment_method = SpreedlyCore::PaymentMethod.find(token)
|
145
|
+
payment_method.valid? # returns true
|
146
|
+
payment_method.errors # []
|
147
|
+
|
148
|
+
|
149
|
+
Configuring SpreedlyCore with Rails
|
150
|
+
----------
|
151
|
+
|
152
|
+
Inside your Rails project create config/spreedly_core.yml formatted like config/database.yml. For example:
|
153
|
+
|
154
|
+
development:
|
155
|
+
login: <Login Key>
|
156
|
+
secret: <Secret Key>
|
157
|
+
gateway_token: 'JncEWj22g59t3CRB1VnPXmUUgKc' # this is the test gateway, replace with your real gateway in production
|
158
|
+
test:
|
159
|
+
login: <Login Key>
|
160
|
+
secret: <Secret Key>
|
161
|
+
gateway_token: 'JncEWj22g59t3CRB1VnPXmUUgKc' # this is the test gateway, replace with your real gateway in production
|
162
|
+
production:
|
163
|
+
login: <Login Key>
|
164
|
+
secret: <Secret Key>
|
165
|
+
gateway_token: 'JncEWj22g59t3CRB1VnPXmUUgKc' # this is the test gateway, replace with your real gateway in production
|
166
|
+
|
167
|
+
Then create config/initializers/spreedly_core.rb with the following:
|
168
|
+
|
169
|
+
config = YAML.load(File.read(RAILS_ROOT + '/config/spreedly_core.yml'))[RAILS_ENV]
|
170
|
+
SpreedlyCore.configure(config['login'], config['secret'], config['gateway_token'])
|
171
|
+
|
172
|
+
Optionally require additional credit card fields:
|
173
|
+
|
174
|
+
SpreedlyCore::PaymentMethod.additional_required_cc_fields :address1, :city, :state, :zip
|
175
|
+
|
176
|
+
Contributing
|
177
|
+
------------
|
178
|
+
|
179
|
+
Once you've made your commits:
|
180
|
+
|
181
|
+
1. [Fork](http://help.github.com/forking/) SpreedlyCore
|
182
|
+
2. Create a topic branch - `git checkout -b my_branch`
|
183
|
+
3. Push to your branch - `git push origin my_branch`
|
184
|
+
4. Create a [Pull Request](http://help.github.com/pull-requests/) from your branch
|
185
|
+
5. Profit!
|
186
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'rake/testtask'
|
2
|
+
|
3
|
+
Rake::TestTask.new(:test) do |test|
|
4
|
+
test.libs << 'lib' << 'test'
|
5
|
+
test.pattern = 'test/**/*_test.rb'
|
6
|
+
test.verbose = true
|
7
|
+
end
|
8
|
+
|
9
|
+
begin
|
10
|
+
require 'rcov/rcovtask'
|
11
|
+
Rcov::RcovTask.new do |test|
|
12
|
+
test.libs << 'test'
|
13
|
+
test.pattern = 'test/**/test_*.rb'
|
14
|
+
test.verbose = true
|
15
|
+
end
|
16
|
+
rescue LoadError
|
17
|
+
task :rcov do
|
18
|
+
abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
task :default => :test
|
23
|
+
|
24
|
+
require 'rake/rdoctask'
|
25
|
+
Rake::RDocTask.new do |rdoc|
|
26
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
27
|
+
|
28
|
+
rdoc.rdoc_dir = 'rdoc'
|
29
|
+
rdoc.title = "someproject #{version}"
|
30
|
+
rdoc.rdoc_files.include('README*')
|
31
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
32
|
+
end
|
33
|
+
|
@@ -0,0 +1,93 @@
|
|
1
|
+
module SpreedlyCore
|
2
|
+
# Base class for all SpreedlyCore API requests
|
3
|
+
class Base
|
4
|
+
include HTTParty
|
5
|
+
|
6
|
+
# Net::HTTP::Options is configured to not have a body.
|
7
|
+
# Lets give it the body it's always dreamed of
|
8
|
+
Net::HTTP::Options::RESPONSE_HAS_BODY = true
|
9
|
+
|
10
|
+
format :xml
|
11
|
+
|
12
|
+
# timeout requests after 10 seconds
|
13
|
+
default_timeout 10
|
14
|
+
|
15
|
+
base_uri "https://spreedlycore.com/v1"
|
16
|
+
|
17
|
+
def self.configure(login, secret, gateway_token)
|
18
|
+
@@login = login
|
19
|
+
self.basic_auth(login, secret)
|
20
|
+
@@gateway_token = gateway_token
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.login; @@login; end
|
24
|
+
def self.gateway_token; @@gateway_token; end
|
25
|
+
|
26
|
+
# make a post request to path
|
27
|
+
# If the request succeeds, provide the respones to the &block
|
28
|
+
def self.verify_post(path, options={}, &block)
|
29
|
+
verify_request(:post, path, options, 200, 422, &block)
|
30
|
+
end
|
31
|
+
|
32
|
+
# make a put request to path
|
33
|
+
# If the request succeeds, provide the respones to the &block
|
34
|
+
def self.verify_put(path, options={}, &block)
|
35
|
+
verify_request(:put, path, options, &block)
|
36
|
+
end
|
37
|
+
|
38
|
+
# make a get request to path
|
39
|
+
# If the request succeeds, provide the respones to the &block
|
40
|
+
def self.verify_get(path, options={}, &block)
|
41
|
+
verify_request(:get, path, options, 200, &block)
|
42
|
+
end
|
43
|
+
|
44
|
+
# make an options request to path
|
45
|
+
# If the request succeeds, provide the respones to the &block
|
46
|
+
def self.verify_options(path, options={}, &block)
|
47
|
+
verify_request(:options, path, options, 200, &block)
|
48
|
+
end
|
49
|
+
|
50
|
+
# make a request to path using the HTTP method provided as request_type
|
51
|
+
# *allowed_codes are passed in, verify the response code (200, 404, etc)
|
52
|
+
# is one of the allowed codes.
|
53
|
+
# If *allowed_codes is empty, don't check the response code, but set an instance
|
54
|
+
# variable on the object created in the block containing the response code.
|
55
|
+
def self.verify_request(request_type, path, options, *allowed_codes, &block)
|
56
|
+
begin
|
57
|
+
response = self.send(request_type, path, options)
|
58
|
+
rescue Timeout::Error, Errno::ETIMEDOUT => e
|
59
|
+
raise TimeOutError.new("Request to #{path} timed out. Is Spreedly Core down?")
|
60
|
+
end
|
61
|
+
|
62
|
+
if allowed_codes.any?
|
63
|
+
if allowed_codes.include?(response.code)
|
64
|
+
block.call(response)
|
65
|
+
else
|
66
|
+
raise "Error retrieving #{path}. Got status of #{response.code}. Expected status to be in #{allowed_codes.join(",")}\n#{response.body}"
|
67
|
+
end
|
68
|
+
else
|
69
|
+
block.call(response).tap do |obj|
|
70
|
+
obj.instance_variable_set("@http_code", response.code)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# Given a hash of attrs, assign instance variables using the hash key as the
|
76
|
+
# attribute name and hash value as the attribute value
|
77
|
+
#
|
78
|
+
def initialize(attrs={})
|
79
|
+
attrs.each do |k, v|
|
80
|
+
instance_variable_set("@#{k}", v)
|
81
|
+
end
|
82
|
+
# errors may be nil, empty, a string, or an array of strings.
|
83
|
+
@errors = if @errors.nil? || @errors["error"].blank?
|
84
|
+
[]
|
85
|
+
elsif @errors["error"].is_a?(String)
|
86
|
+
[@errors["error"]]
|
87
|
+
else
|
88
|
+
@errors["error"]
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module SpreedlyCore
|
2
|
+
class Gateway < Base
|
3
|
+
attr_reader(:name, :gateway_type, :auth_modes, :supports_capture,
|
4
|
+
:supports_authorize, :supports_purchase, :supports_void,
|
5
|
+
:supports_credit)
|
6
|
+
|
7
|
+
# returns an array of Gateway which are supported
|
8
|
+
def self.supported_gateways
|
9
|
+
verify_options("/gateways.xml") do |response|
|
10
|
+
response.parsed_response["gateways"]["gateway"].map{|h| new(h) }
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize(attrs={})
|
15
|
+
attrs.merge!(attrs.delete("characteristics") || {})
|
16
|
+
super(attrs)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
module SpreedlyCore
|
2
|
+
class PaymentMethod < Base
|
3
|
+
attr_reader( :address1, :address2, :card_type, :city, :country, :created_at,
|
4
|
+
:data, :email, :errors, :first_name, :last_four_digits,
|
5
|
+
:last_name, :month, :number, :payment_method_type, :phone_number,
|
6
|
+
:state, :token, :updated_at, :verification_value, :year, :zip)
|
7
|
+
|
8
|
+
# configure additional required fiels. Like :address1, :city, :state
|
9
|
+
def self.additional_required_cc_fields *fields
|
10
|
+
@@additional_required_fields ||= Set.new
|
11
|
+
@@additional_required_fields += fields
|
12
|
+
end
|
13
|
+
|
14
|
+
# clear the configured additional required fields
|
15
|
+
def self.reset_additional_required_cc_fields
|
16
|
+
@@additional_required_fields = Set.new
|
17
|
+
end
|
18
|
+
|
19
|
+
# Lookup the PaymentMethod by token
|
20
|
+
def self.find(token)
|
21
|
+
return nil if token.nil?
|
22
|
+
verify_get("/payment_methods/#{token}.xml") do |response|
|
23
|
+
new(response.parsed_response["payment_method"])
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# Create a new PaymentMethod based on the attrs hash and then validate
|
28
|
+
def initialize(attrs={})
|
29
|
+
super(attrs)
|
30
|
+
validate
|
31
|
+
end
|
32
|
+
|
33
|
+
# Retain the payment method
|
34
|
+
def retain
|
35
|
+
self.class.verify_put("/payment_methods/#{token}/retain.xml", :body => {}) do |response|
|
36
|
+
RetainTransaction.new(response.parsed_response["transaction"])
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Redact the payment method
|
41
|
+
def redact
|
42
|
+
self.class.verify_put("/payment_methods/#{token}/redact.xml", :body => {}) do |response|
|
43
|
+
RedactTransaction.new(response.parsed_response["transaction"])
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Make a purchase against the payment method
|
48
|
+
def purchase(amount, currency="USD", _gateway_token=nil)
|
49
|
+
purchase_or_authorize(:purchase, amount, currency, _gateway_token)
|
50
|
+
end
|
51
|
+
|
52
|
+
# Make an authorize against payment method. You can then later capture against the authorize
|
53
|
+
def authorize(amount, currency="USD", _gateway_token=nil)
|
54
|
+
purchase_or_authorize(:authorize, amount, currency, _gateway_token)
|
55
|
+
end
|
56
|
+
|
57
|
+
# Returns the URL that CC data should be submitted to.
|
58
|
+
def self.submit_url
|
59
|
+
Base.base_uri + '/payment_methods'
|
60
|
+
end
|
61
|
+
|
62
|
+
def valid?
|
63
|
+
@errors.empty?
|
64
|
+
end
|
65
|
+
|
66
|
+
protected
|
67
|
+
|
68
|
+
# Validate additional cc fields like first_name, last_name, etc when
|
69
|
+
# configured to do so
|
70
|
+
def validate
|
71
|
+
return if @has_been_validated
|
72
|
+
@has_been_validated = true
|
73
|
+
self.class.additional_required_cc_fields.each do |field|
|
74
|
+
if instance_variable_get("@#{field}").blank?
|
75
|
+
str_field= field.to_s
|
76
|
+
friendly_name = if str_field.respond_to?(:humanize)
|
77
|
+
str_field.humanize
|
78
|
+
else
|
79
|
+
str_field.split("_").join(" ")
|
80
|
+
end
|
81
|
+
|
82
|
+
@errors << "#{friendly_name.capitalize} can't be blank"
|
83
|
+
end
|
84
|
+
end
|
85
|
+
@errors = @errors.sort
|
86
|
+
end
|
87
|
+
|
88
|
+
def purchase_or_authorize(tran_type, amount, currency, _gateway_token)
|
89
|
+
transaction_type = tran_type.to_s
|
90
|
+
raise "Unknown transaction type" unless %w{purchase authorize}.include?(transaction_type)
|
91
|
+
|
92
|
+
_gateway_token ||= self.class.gateway_token
|
93
|
+
path = "/gateways/#{_gateway_token}/#{transaction_type}.xml"
|
94
|
+
data = {:transaction => {
|
95
|
+
:amount => amount,
|
96
|
+
:transaction_type => transaction_type,
|
97
|
+
:payment_method_token => token,
|
98
|
+
:currency_code => currency }}
|
99
|
+
|
100
|
+
self.class.verify_post(path, :body => data) do |response|
|
101
|
+
klass = SpreedlyCore.const_get("#{transaction_type.capitalize}Transaction")
|
102
|
+
klass.new(response.parsed_response["transaction"])
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'cgi'
|
2
|
+
|
3
|
+
module SpreedlyCore::TestHelper
|
4
|
+
extend self
|
5
|
+
|
6
|
+
def cc_data(cc_type, options={})
|
7
|
+
|
8
|
+
card_numbers = {:master => [5555555555554444, 5105105105105100],
|
9
|
+
:visa => [4111111111111111, 4012888888881881],
|
10
|
+
:american_express => [378282246310005, 371449635398431],
|
11
|
+
:discover => [6011111111111117, 6011000990139424]
|
12
|
+
}
|
13
|
+
|
14
|
+
card_number = options[:card_number] == :failed ? :last : :first
|
15
|
+
number = card_numbers[cc_type].send(card_number)
|
16
|
+
|
17
|
+
{ :credit_card => {
|
18
|
+
:first_name => "John",
|
19
|
+
:last_name => "Foo",
|
20
|
+
:card_type => cc_type,
|
21
|
+
:number => number,
|
22
|
+
:verification_value => 123,
|
23
|
+
:month => 4,
|
24
|
+
:year => Time.now.year + 1 }.merge(options[:credit_card] || {})
|
25
|
+
}
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
module SpreedlyCore
|
30
|
+
class PaymentMethod
|
31
|
+
def self.create_test_token(cc_data={})
|
32
|
+
data = cc_data.merge(:redirect_url => "http://example.com",
|
33
|
+
:api_login => SpreedlyCore::Base.login)
|
34
|
+
|
35
|
+
response = self.post("/payment_methods", :body => data, :no_follow => true)
|
36
|
+
rescue HTTParty::RedirectionTooDeep => e
|
37
|
+
if e.response.body =~ /href="(.*?)"/
|
38
|
+
# rescuing the redirection too deep is apparently the way to
|
39
|
+
# handle redirect following
|
40
|
+
token = CGI::parse(URI.parse($1).query)["token"].first
|
41
|
+
end
|
42
|
+
raise "Could not find token in body: #{response}" if token.nil?
|
43
|
+
return token
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
@@ -0,0 +1,127 @@
|
|
1
|
+
module SpreedlyCore
|
2
|
+
# Abstract class for all the different spreedly core transactions
|
3
|
+
class Transaction < Base
|
4
|
+
attr_reader(:amount, :on_test_gateway, :created_at, :updated_at, :currency_code,
|
5
|
+
:succeeded, :token, :message, :transaction_type, :gateway_token,
|
6
|
+
:response)
|
7
|
+
alias :succeeded? :succeeded
|
8
|
+
|
9
|
+
# Breaks enacapsulation a bit, but allow subclasses to register the 'transaction_type'
|
10
|
+
# they handle.
|
11
|
+
def self.handles(transaction_type)
|
12
|
+
@@transaction_type_to_class ||= {}
|
13
|
+
@@transaction_type_to_class[transaction_type] = self
|
14
|
+
end
|
15
|
+
|
16
|
+
# Lookup the transaction by its token. Returns the correct subclass
|
17
|
+
def self.find(token)
|
18
|
+
return nil if token.nil?
|
19
|
+
verify_get("/transactions/#{token}.xml") do |response|
|
20
|
+
attrs = response.parsed_response["transaction"]
|
21
|
+
klass = @@transaction_type_to_class[attrs["transaction_type"]] || self
|
22
|
+
klass.new(attrs)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
class RetainTransaction < Transaction
|
28
|
+
handles "RetainPaymentMethod"
|
29
|
+
attr_reader :payment_method
|
30
|
+
|
31
|
+
def initialize(attrs={})
|
32
|
+
@payment_method = PaymentMethod.new(attrs.delete("payment_method") || {})
|
33
|
+
super(attrs)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
class RedactTransaction < Transaction
|
37
|
+
handles "RedactPaymentMethod"
|
38
|
+
attr_reader :payment_method
|
39
|
+
|
40
|
+
def initialize(attrs={})
|
41
|
+
@payment_method = PaymentMethod.new(attrs.delete("payment_method") || {})
|
42
|
+
super(attrs)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
module NullifiableTransaction
|
47
|
+
# Void is used to cancel out authorizations and, with some gateways, to
|
48
|
+
# cancel actual payment transactions within the first 24 hours
|
49
|
+
def void
|
50
|
+
self.class.verify_post("/transactions/#{token}/void.xml") do |response|
|
51
|
+
VoidedTransaction.new(response.parsed_response["transaction"])
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# Credit amount. If amount is nil, then credit the entire previous purchase
|
56
|
+
# or captured amount
|
57
|
+
def credit(amount=nil)
|
58
|
+
body = if amount.nil?
|
59
|
+
{}
|
60
|
+
else
|
61
|
+
{:transaction => {:amount => amount}}
|
62
|
+
end
|
63
|
+
self.class.verify_post("/transactions/#{token}/credit.xml",
|
64
|
+
:body => body) do |response|
|
65
|
+
CreditTransaction.new(response.parsed_response["transaction"])
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
class AuthorizeTransaction < Transaction
|
71
|
+
handles "authorization"
|
72
|
+
attr_reader :payment_method
|
73
|
+
|
74
|
+
def initialize(attrs={})
|
75
|
+
@payment_method = PaymentMethod.new(attrs.delete("payment_method") || {})
|
76
|
+
@response = Response.new(attrs.delete("response") || {})
|
77
|
+
super(attrs)
|
78
|
+
end
|
79
|
+
|
80
|
+
# Capture the previously authorized payment. If the amount is nil, the
|
81
|
+
# captured amount will the amount from the original authorization. Some
|
82
|
+
# gateways support partial captures which can be done by specifiying an
|
83
|
+
# amount
|
84
|
+
def capture(amount=nil)
|
85
|
+
body = if amount.nil?
|
86
|
+
{}
|
87
|
+
else
|
88
|
+
{:transaction => {:amount => amount}}
|
89
|
+
end
|
90
|
+
self.class.verify_post("/transactions/#{token}/capture.xml",
|
91
|
+
:body => body) do |response|
|
92
|
+
CaptureTransaction.new(response.parsed_response["transaction"])
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
class PurchaseTransaction < Transaction
|
98
|
+
include NullifiableTransaction
|
99
|
+
|
100
|
+
handles "purchase"
|
101
|
+
attr_reader :payment_method
|
102
|
+
|
103
|
+
def initialize(attrs={})
|
104
|
+
@payment_method = PaymentMethod.new(attrs.delete("payment_method") || {})
|
105
|
+
@response = Response.new(attrs.delete("response") || {})
|
106
|
+
super(attrs)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
class CaptureTransaction < Transaction
|
111
|
+
include NullifiableTransaction
|
112
|
+
|
113
|
+
handles "capture"
|
114
|
+
attr_reader :reference_token
|
115
|
+
end
|
116
|
+
|
117
|
+
class VoidedTransaction < Transaction
|
118
|
+
handles "void"
|
119
|
+
attr_reader :reference_token
|
120
|
+
end
|
121
|
+
|
122
|
+
class CreditTransaction < Transaction
|
123
|
+
handles "credit"
|
124
|
+
attr_reader :reference_token
|
125
|
+
end
|
126
|
+
|
127
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
require 'httparty'
|
4
|
+
|
5
|
+
require 'spreedly_core/base'
|
6
|
+
require 'spreedly_core/payment_method'
|
7
|
+
require 'spreedly_core/gateway'
|
8
|
+
require 'spreedly_core/transactions'
|
9
|
+
|
10
|
+
module SpreedlyCore
|
11
|
+
# Hash of user friendly credit card name to SpreedlyCore API name
|
12
|
+
CARD_TYPES = {
|
13
|
+
"Visa" => "visa",
|
14
|
+
"MasterCard" => "master",
|
15
|
+
"American Express" => "american_express",
|
16
|
+
"Discover" => "discover"
|
17
|
+
}
|
18
|
+
|
19
|
+
# Custom exception which occurs when a request to SpreedlyCore times out
|
20
|
+
# See SpreedlyCore::Base.default_timeout
|
21
|
+
class TimeOutError < RuntimeError; end
|
22
|
+
|
23
|
+
# Configure SpreedlyCore with a particular account and default gateway
|
24
|
+
def self.configure(login, secret, gateway_token)
|
25
|
+
Base.configure(login, secret, gateway_token)
|
26
|
+
end
|
27
|
+
|
28
|
+
# returns the configured SpreedlyCore login
|
29
|
+
def self.login; Base.login; end
|
30
|
+
|
31
|
+
# A container for a response from a payment gateway
|
32
|
+
class Response < Base
|
33
|
+
attr_reader(:success, :message, :avs_code, :avs_message, :cvv_code,
|
34
|
+
:cvv_message, :error_code, :error_detail, :created_at,
|
35
|
+
:updated_at)
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,237 @@
|
|
1
|
+
require "#{File.dirname(__FILE__)}/test_helper"
|
2
|
+
|
3
|
+
|
4
|
+
# In order to run tests
|
5
|
+
# 1. cp test/config/spreedly_core.yml.example to test/config/spreedly_core.yml
|
6
|
+
# 2. Add your spreedly core credentials to test/config/spreedly_core.yml
|
7
|
+
class SpreedlyCoreTest < Test::Unit::TestCase
|
8
|
+
include SpreedlyCore::TestHelper
|
9
|
+
|
10
|
+
def setup
|
11
|
+
SpreedlyCore::PaymentMethod.reset_additional_required_cc_fields
|
12
|
+
end
|
13
|
+
|
14
|
+
def given_a_payment_method(cc_card=:master, card_options={})
|
15
|
+
token = SpreedlyCore::PaymentMethod.create_test_token(cc_data(cc_card, card_options))
|
16
|
+
assert payment_method = SpreedlyCore::PaymentMethod.find(token)
|
17
|
+
assert token, payment_method.token
|
18
|
+
payment_method
|
19
|
+
end
|
20
|
+
|
21
|
+
def given_a_purchase(purchase_amount=100)
|
22
|
+
payment_method = given_a_payment_method
|
23
|
+
assert transaction = payment_method.purchase(purchase_amount)
|
24
|
+
assert_equal purchase_amount, transaction.amount
|
25
|
+
assert_equal "USD", transaction.currency_code
|
26
|
+
assert_equal "purchase", transaction.transaction_type
|
27
|
+
assert transaction.succeeded?
|
28
|
+
transaction
|
29
|
+
end
|
30
|
+
|
31
|
+
def given_a_retained_transaction
|
32
|
+
payment_method = given_a_payment_method
|
33
|
+
assert transaction = payment_method.retain
|
34
|
+
assert transaction.succeeded?
|
35
|
+
assert_equal "RetainPaymentMethod", transaction.transaction_type
|
36
|
+
transaction
|
37
|
+
end
|
38
|
+
|
39
|
+
def given_a_redacted_transaction
|
40
|
+
retained_transaction = given_a_retained_transaction
|
41
|
+
assert payment_method = retained_transaction.payment_method
|
42
|
+
transaction = payment_method.redact
|
43
|
+
assert transaction.succeeded?
|
44
|
+
assert_equal "RedactPaymentMethod", transaction.transaction_type
|
45
|
+
assert !transaction.token.blank?
|
46
|
+
transaction
|
47
|
+
end
|
48
|
+
|
49
|
+
def given_an_authorized_transaction(amount=100)
|
50
|
+
payment_method = given_a_payment_method
|
51
|
+
assert transaction = payment_method.authorize(100)
|
52
|
+
assert_equal 100, transaction.amount
|
53
|
+
assert_equal "USD", transaction.currency_code
|
54
|
+
assert_equal SpreedlyCore::AuthorizeTransaction, transaction.class
|
55
|
+
transaction
|
56
|
+
end
|
57
|
+
|
58
|
+
def given_a_capture(amount=100)
|
59
|
+
transaction = given_an_authorized_transaction
|
60
|
+
capture = transaction.capture(amount)
|
61
|
+
assert capture.succeeded?
|
62
|
+
assert_equal amount, capture.amount
|
63
|
+
assert_equal "capture", capture.transaction_type
|
64
|
+
assert_equal SpreedlyCore::CaptureTransaction, capture.class
|
65
|
+
capture
|
66
|
+
end
|
67
|
+
|
68
|
+
def given_a_purchase_void
|
69
|
+
purchase = given_a_purchase
|
70
|
+
assert void = purchase.void
|
71
|
+
assert_equal purchase.token, void.reference_token
|
72
|
+
assert void.succeeded?
|
73
|
+
void
|
74
|
+
end
|
75
|
+
|
76
|
+
def given_a_capture_void
|
77
|
+
capture = given_a_capture
|
78
|
+
assert void = capture.void
|
79
|
+
assert_equal capture.token, void.reference_token
|
80
|
+
assert void.succeeded?
|
81
|
+
void
|
82
|
+
end
|
83
|
+
|
84
|
+
def given_a_purchase_credit(purchase_amount=100, credit_amount=100)
|
85
|
+
purchase = given_a_purchase(purchase_amount)
|
86
|
+
given_a_credit(purchase, credit_amount)
|
87
|
+
end
|
88
|
+
|
89
|
+
def given_a_capture_credit(capture_amount=100, credit_amount=100)
|
90
|
+
capture = given_a_capture(capture_amount)
|
91
|
+
given_a_credit(capture, credit_amount)
|
92
|
+
end
|
93
|
+
|
94
|
+
def given_a_credit(trans, credit_amount=100)
|
95
|
+
assert credit = trans.credit(credit_amount)
|
96
|
+
assert_equal trans.token, credit.reference_token
|
97
|
+
assert_equal credit_amount, credit.amount
|
98
|
+
assert credit.succeeded?
|
99
|
+
assert SpreedlyCore::CreditTransaction, credit.class
|
100
|
+
credit
|
101
|
+
end
|
102
|
+
|
103
|
+
|
104
|
+
|
105
|
+
def test_can_get_payment_token
|
106
|
+
payment_method = given_a_payment_method
|
107
|
+
assert_equal "John", payment_method.first_name
|
108
|
+
assert_equal "Foo", payment_method.last_name
|
109
|
+
assert_equal "XXX", payment_method.verification_value
|
110
|
+
assert payment_method.errors.empty?
|
111
|
+
assert_equal 4, payment_method.month
|
112
|
+
assert_equal 2015, payment_method.year
|
113
|
+
end
|
114
|
+
|
115
|
+
def test_can_retain_payment_method
|
116
|
+
given_a_retained_transaction
|
117
|
+
end
|
118
|
+
|
119
|
+
def test_can_redact_payment_method
|
120
|
+
given_a_redacted_transaction
|
121
|
+
end
|
122
|
+
|
123
|
+
def test_can_make_purchase
|
124
|
+
given_a_purchase
|
125
|
+
end
|
126
|
+
|
127
|
+
def test_can_authorize
|
128
|
+
given_an_authorized_transaction
|
129
|
+
end
|
130
|
+
|
131
|
+
def test_payment_failed
|
132
|
+
payment_method = given_a_payment_method(:master, :card_number => :failed)
|
133
|
+
|
134
|
+
assert transaction = payment_method.purchase(100)
|
135
|
+
assert !transaction.succeeded?
|
136
|
+
assert_equal("Unable to obtain a successful response from the gateway.",
|
137
|
+
transaction.message)
|
138
|
+
|
139
|
+
assert_equal("Unable to process the transaction.", transaction.response.message)
|
140
|
+
end
|
141
|
+
|
142
|
+
def test_can_capture_after_authorize
|
143
|
+
given_a_capture
|
144
|
+
end
|
145
|
+
|
146
|
+
def test_can_capture_partial_after_authorize
|
147
|
+
given_a_capture 50
|
148
|
+
end
|
149
|
+
|
150
|
+
def test_can_void_after_purchase
|
151
|
+
given_a_purchase_void
|
152
|
+
end
|
153
|
+
|
154
|
+
def test_can_void_after_capture
|
155
|
+
given_a_capture_void
|
156
|
+
end
|
157
|
+
|
158
|
+
def test_can_credit_after_purchase
|
159
|
+
given_a_purchase_credit
|
160
|
+
end
|
161
|
+
|
162
|
+
def test_can_credit_partial_after_purchase
|
163
|
+
given_a_purchase_credit(100, 50)
|
164
|
+
end
|
165
|
+
|
166
|
+
def test_can_credit_after_capture
|
167
|
+
given_a_capture_credit
|
168
|
+
end
|
169
|
+
|
170
|
+
def test_can_credit_partial_after_capture
|
171
|
+
given_a_capture_credit(50, 25)
|
172
|
+
end
|
173
|
+
|
174
|
+
def test_find_returns_retain_transaction_type
|
175
|
+
retain = given_a_retained_transaction
|
176
|
+
assert_find_transaction(retain, SpreedlyCore::RetainTransaction)
|
177
|
+
end
|
178
|
+
|
179
|
+
def test_find_returns_redact_transaction_type
|
180
|
+
redact = given_a_redacted_transaction
|
181
|
+
assert_find_transaction(redact, SpreedlyCore::RedactTransaction)
|
182
|
+
end
|
183
|
+
|
184
|
+
def test_find_returns_authorize_transaction_type
|
185
|
+
authorize = given_an_authorized_transaction
|
186
|
+
assert_find_transaction(authorize, SpreedlyCore::AuthorizeTransaction)
|
187
|
+
end
|
188
|
+
|
189
|
+
def test_find_returns_purchase_transaction_type
|
190
|
+
purchase = given_a_purchase
|
191
|
+
assert_find_transaction(purchase, SpreedlyCore::PurchaseTransaction)
|
192
|
+
end
|
193
|
+
|
194
|
+
def test_find_returns_capture_transaction_type
|
195
|
+
capture = given_a_capture
|
196
|
+
assert_find_transaction(capture, SpreedlyCore::CaptureTransaction)
|
197
|
+
end
|
198
|
+
|
199
|
+
def test_find_returns_voided_transaction_type
|
200
|
+
void = given_a_capture_void
|
201
|
+
assert_find_transaction(void, SpreedlyCore::VoidedTransaction)
|
202
|
+
end
|
203
|
+
|
204
|
+
def test_find_returns_credit_transaction_type
|
205
|
+
credit = given_a_capture_credit
|
206
|
+
assert_find_transaction(credit, SpreedlyCore::CreditTransaction)
|
207
|
+
end
|
208
|
+
|
209
|
+
|
210
|
+
def test_can_enforce_additional_payment_method_validations
|
211
|
+
SpreedlyCore::PaymentMethod.additional_required_cc_fields :state
|
212
|
+
|
213
|
+
token = SpreedlyCore::PaymentMethod.create_test_token(cc_data(:master))
|
214
|
+
assert payment_method = SpreedlyCore::PaymentMethod.find(token)
|
215
|
+
assert !payment_method.valid?
|
216
|
+
assert_equal 1, payment_method.errors.size
|
217
|
+
|
218
|
+
assert_equal "State can't be blank", payment_method.errors.first
|
219
|
+
|
220
|
+
token = SpreedlyCore::PaymentMethod.
|
221
|
+
create_test_token(cc_data(:master, :credit_card => {:state => "IL"}))
|
222
|
+
|
223
|
+
assert payment_method = SpreedlyCore::PaymentMethod.find(token)
|
224
|
+
|
225
|
+
assert payment_method.valid?
|
226
|
+
end
|
227
|
+
|
228
|
+
def test_can_list_supported_gateways
|
229
|
+
assert SpreedlyCore::Gateway.supported_gateways.any?
|
230
|
+
end
|
231
|
+
|
232
|
+
protected
|
233
|
+
def assert_find_transaction(trans, expected_class)
|
234
|
+
assert actual = SpreedlyCore::Transaction.find(trans.token)
|
235
|
+
assert_equal expected_class, actual.class
|
236
|
+
end
|
237
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'bundler'
|
5
|
+
|
6
|
+
Bundler.setup(:default, :development)
|
7
|
+
|
8
|
+
Bundler.require(:default, :development)
|
9
|
+
|
10
|
+
$LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'
|
11
|
+
require 'spreedly_core'
|
12
|
+
require 'spreedly_core/test_extensions'
|
13
|
+
|
14
|
+
|
15
|
+
config = YAML.load(File.read(File.dirname(__FILE__) + '/config/spreedly_core.yml'))
|
16
|
+
SpreedlyCore.configure(config['login'], config['secret'], config['gateway_token'])
|
17
|
+
|
metadata
ADDED
@@ -0,0 +1,107 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: spreedly_core
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 27
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 0
|
9
|
+
- 2
|
10
|
+
version: 0.0.2
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- 403 Labs
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2011-05-18 00:00:00 Z
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: httparty
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
none: false
|
25
|
+
requirements:
|
26
|
+
- - "="
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
hash: 13
|
29
|
+
segments:
|
30
|
+
- 0
|
31
|
+
- 7
|
32
|
+
- 7
|
33
|
+
version: 0.7.7
|
34
|
+
type: :runtime
|
35
|
+
version_requirements: *id001
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: ruby-debug
|
38
|
+
prerelease: false
|
39
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
40
|
+
none: false
|
41
|
+
requirements:
|
42
|
+
- - ">="
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
hash: 3
|
45
|
+
segments:
|
46
|
+
- 0
|
47
|
+
version: "0"
|
48
|
+
type: :development
|
49
|
+
version_requirements: *id002
|
50
|
+
description:
|
51
|
+
email: github@403labs.com
|
52
|
+
executables: []
|
53
|
+
|
54
|
+
extensions: []
|
55
|
+
|
56
|
+
extra_rdoc_files: []
|
57
|
+
|
58
|
+
files:
|
59
|
+
- README.md
|
60
|
+
- Rakefile
|
61
|
+
- LICENSE
|
62
|
+
- lib/spreedly_core/base.rb
|
63
|
+
- lib/spreedly_core/gateway.rb
|
64
|
+
- lib/spreedly_core/payment_method.rb
|
65
|
+
- lib/spreedly_core/test_extensions.rb
|
66
|
+
- lib/spreedly_core/transactions.rb
|
67
|
+
- lib/spreedly_core/version.rb
|
68
|
+
- lib/spreedly_core.rb
|
69
|
+
- test/config/spreedly_core.yml
|
70
|
+
- test/config/spreedly_core.yml.example
|
71
|
+
- test/spreedly_core_test.rb
|
72
|
+
- test/test_helper.rb
|
73
|
+
homepage: http://github.com/403labs/spreedly_core
|
74
|
+
licenses: []
|
75
|
+
|
76
|
+
post_install_message:
|
77
|
+
rdoc_options: []
|
78
|
+
|
79
|
+
require_paths:
|
80
|
+
- lib
|
81
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
82
|
+
none: false
|
83
|
+
requirements:
|
84
|
+
- - ">="
|
85
|
+
- !ruby/object:Gem::Version
|
86
|
+
hash: 3
|
87
|
+
segments:
|
88
|
+
- 0
|
89
|
+
version: "0"
|
90
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
91
|
+
none: false
|
92
|
+
requirements:
|
93
|
+
- - ">="
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
hash: 3
|
96
|
+
segments:
|
97
|
+
- 0
|
98
|
+
version: "0"
|
99
|
+
requirements: []
|
100
|
+
|
101
|
+
rubyforge_project:
|
102
|
+
rubygems_version: 1.7.2
|
103
|
+
signing_key:
|
104
|
+
specification_version: 3
|
105
|
+
summary: Ruby API for Spreedly Core
|
106
|
+
test_files: []
|
107
|
+
|