spreedly-core-ruby 0.1.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.
- data/LICENSE +15 -0
- data/README.md +213 -0
- data/Rakefile +34 -0
- data/lib/spreedly_core.rb +72 -0
- data/lib/spreedly_core/base.rb +95 -0
- data/lib/spreedly_core/gateway.rb +23 -0
- data/lib/spreedly_core/payment_method.rb +118 -0
- data/lib/spreedly_core/test_extensions.rb +62 -0
- data/lib/spreedly_core/test_gateway.rb +32 -0
- data/lib/spreedly_core/transactions.rb +142 -0
- data/lib/spreedly_core/version.rb +4 -0
- data/test/config/spreedly_core.yml.example +3 -0
- data/test/configuration_test.rb +27 -0
- data/test/factories.rb +103 -0
- data/test/spreedly_core_test.rb +204 -0
- data/test/test_factory.rb +99 -0
- data/test/test_helper.rb +33 -0
- data/test/transaction_test.rb +62 -0
- metadata +113 -0
@@ -0,0 +1,118 @@
|
|
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",
|
23
|
+
:has_key => "payment_method") do |response|
|
24
|
+
new(response.parsed_response["payment_method"])
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# Create a new PaymentMethod based on the attrs hash and then validate
|
29
|
+
def initialize(attrs={})
|
30
|
+
super(attrs)
|
31
|
+
validate
|
32
|
+
end
|
33
|
+
|
34
|
+
# Retain the payment method
|
35
|
+
def retain
|
36
|
+
verify_put("/payment_methods/#{token}/retain.xml", :body => {}, :has_key => "transaction") do |response|
|
37
|
+
RetainTransaction.new(response.parsed_response["transaction"])
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# Redact the payment method
|
42
|
+
def redact
|
43
|
+
verify_put("/payment_methods/#{token}/redact.xml", :body => {}, :has_key => "transaction") do |response|
|
44
|
+
RedactTransaction.new(response.parsed_response["transaction"])
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Make a purchase against the payment method
|
49
|
+
def purchase(amount, currency=nil, _gateway_token=nil, ip_address=nil)
|
50
|
+
purchase_or_authorize(:purchase, amount, currency, _gateway_token, ip_address)
|
51
|
+
end
|
52
|
+
|
53
|
+
# Make an authorize against payment method. You can then later capture against the authorize
|
54
|
+
def authorize(amount, currency=nil, _gateway_token=nil, ip_address=nil)
|
55
|
+
purchase_or_authorize(:authorize, amount, currency, _gateway_token, ip_address)
|
56
|
+
end
|
57
|
+
|
58
|
+
# Returns the URL that CC data should be submitted to.
|
59
|
+
def self.submit_url
|
60
|
+
Base.base_uri + '/payment_methods'
|
61
|
+
end
|
62
|
+
|
63
|
+
def valid?
|
64
|
+
@errors.empty?
|
65
|
+
end
|
66
|
+
|
67
|
+
|
68
|
+
|
69
|
+
|
70
|
+
protected
|
71
|
+
|
72
|
+
|
73
|
+
|
74
|
+
|
75
|
+
# Validate additional cc fields like first_name, last_name, etc when
|
76
|
+
# configured to do so
|
77
|
+
def validate
|
78
|
+
return if @has_been_validated
|
79
|
+
@has_been_validated = true
|
80
|
+
self.class.additional_required_cc_fields.each do |field|
|
81
|
+
if instance_variable_get("@#{field}").blank?
|
82
|
+
str_field= field.to_s
|
83
|
+
friendly_name = if str_field.respond_to?(:humanize)
|
84
|
+
str_field.humanize
|
85
|
+
else
|
86
|
+
str_field.split("_").join(" ")
|
87
|
+
end
|
88
|
+
|
89
|
+
@errors << "#{friendly_name.capitalize} can't be blank"
|
90
|
+
end
|
91
|
+
end
|
92
|
+
@errors = @errors.sort
|
93
|
+
end
|
94
|
+
|
95
|
+
def purchase_or_authorize(tran_type, amount, currency, _gateway_token, ip_address)
|
96
|
+
transaction_type = tran_type.to_s
|
97
|
+
raise "Unknown transaction type" unless %w{purchase authorize}.include?(transaction_type)
|
98
|
+
|
99
|
+
currency ||= "USD"
|
100
|
+
_gateway_token ||= self.class.gateway_token
|
101
|
+
path = "/gateways/#{_gateway_token}/#{transaction_type}.xml"
|
102
|
+
data = {
|
103
|
+
:transaction => {
|
104
|
+
:transaction_type => transaction_type,
|
105
|
+
:payment_method_token => token,
|
106
|
+
:amount => amount,
|
107
|
+
:currency_code => currency,
|
108
|
+
:ip => ip_address
|
109
|
+
}
|
110
|
+
}
|
111
|
+
self.class.verify_post(path, :body => data,
|
112
|
+
:has_key => "transaction") do |response|
|
113
|
+
klass = SpreedlyCore.const_get("#{transaction_type.capitalize}Transaction")
|
114
|
+
klass.new(response.parsed_response["transaction"])
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'cgi'
|
2
|
+
require 'uri'
|
3
|
+
|
4
|
+
module SpreedlyCore
|
5
|
+
module TestHelper
|
6
|
+
extend self
|
7
|
+
|
8
|
+
def cc_data(cc_type, options={})
|
9
|
+
|
10
|
+
card_numbers = {:master => [5555555555554444, 5105105105105100],
|
11
|
+
:visa => [4111111111111111, 4012888888881881],
|
12
|
+
:american_express => [378282246310005, 371449635398431],
|
13
|
+
:discover => [6011111111111117, 6011000990139424]
|
14
|
+
}
|
15
|
+
|
16
|
+
card_number = options[:card_number] == :failed ? :last : :first
|
17
|
+
number = card_numbers[cc_type].send(card_number)
|
18
|
+
|
19
|
+
{ :credit_card => {
|
20
|
+
:first_name => "John",
|
21
|
+
:last_name => "Foo",
|
22
|
+
:card_type => cc_type,
|
23
|
+
:number => number,
|
24
|
+
:verification_value => 123,
|
25
|
+
:month => 4,
|
26
|
+
:year => Time.now.year + 1 }.merge(options[:credit_card] || {})
|
27
|
+
}
|
28
|
+
end
|
29
|
+
|
30
|
+
# Return the base uri as a mocking framework would expect
|
31
|
+
def mocked_base_uri_string
|
32
|
+
uri = URI.parse(Base.base_uri)
|
33
|
+
auth_params = Base.default_options[:basic_auth]
|
34
|
+
uri.user = auth_params[:username]
|
35
|
+
uri.password = auth_params[:password]
|
36
|
+
uri.to_s
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
class PaymentMethod
|
41
|
+
|
42
|
+
# Call spreedly core to create a test token.
|
43
|
+
# pass_through_data will be added as the "data" field.
|
44
|
+
#
|
45
|
+
def self.create_test_token(cc_data={}, pass_through_data=nil)
|
46
|
+
data = cc_data.merge(:redirect_url => "http://example.com",
|
47
|
+
:api_login => SpreedlyCore::Base.login,
|
48
|
+
:data => pass_through_data)
|
49
|
+
|
50
|
+
response = self.post("/payment_methods", :body => data, :no_follow => true)
|
51
|
+
rescue HTTParty::RedirectionTooDeep => e
|
52
|
+
if e.response.body =~ /href="(.*?)"/
|
53
|
+
# rescuing the RedirectionTooDeep exception is apparently the way to
|
54
|
+
# handle redirect following
|
55
|
+
token = CGI::parse(URI.parse($1).query)["token"].first
|
56
|
+
end
|
57
|
+
raise "Could not find token in body: #{e.response.body}" if token.nil?
|
58
|
+
return token
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module SpreedlyCore
|
2
|
+
class TestGateway < Gateway
|
3
|
+
# gets a test gateway, creates if necessary
|
4
|
+
def self.get_or_create
|
5
|
+
# get the list of gateways and return the first test gateway
|
6
|
+
# if none exist, create one
|
7
|
+
verify_get("/gateways.xml") do |response|
|
8
|
+
# will return Hash if only 1 gateways->gateway, Array otherwise
|
9
|
+
gateways = response.parsed_response["gateways"]["gateway"]
|
10
|
+
gateways = [gateways] unless gateways.is_a?(Array)
|
11
|
+
|
12
|
+
gateways.each do |gateway_hash|
|
13
|
+
g = new gateway_hash
|
14
|
+
return g if g.gateway_type == "test" && g.redacted == false
|
15
|
+
end unless gateways.nil?
|
16
|
+
end
|
17
|
+
|
18
|
+
# no test gateway yet, let's create one
|
19
|
+
opts = {
|
20
|
+
:headers => {"Content-Type" => "application/xml"},
|
21
|
+
:body => '<gateway><gateway_type>test</gateway_type></gateway>'
|
22
|
+
}
|
23
|
+
|
24
|
+
verify_post("/gateways.xml", opts) do |response|
|
25
|
+
return new response.parsed_response["gateway"]
|
26
|
+
end
|
27
|
+
|
28
|
+
# HTTP 724
|
29
|
+
return nil
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,142 @@
|
|
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", :has_key => "transaction") 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
|
+
|
37
|
+
class RedactTransaction < Transaction
|
38
|
+
handles "RedactPaymentMethod"
|
39
|
+
attr_reader :payment_method
|
40
|
+
|
41
|
+
def initialize(attrs={})
|
42
|
+
@payment_method = PaymentMethod.new(attrs.delete("payment_method") || {})
|
43
|
+
super(attrs)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
module NullifiableTransaction
|
48
|
+
# Void is used to cancel out authorizations and, with some gateways, to
|
49
|
+
# cancel actual payment transactions within the first 24 hours
|
50
|
+
def void(ip_address=nil)
|
51
|
+
body = {:transaction => {:ip => ip_address}}
|
52
|
+
self.class.verify_post("/transactions/#{token}/void.xml",
|
53
|
+
:body => body, :has_key => "transaction") do |response|
|
54
|
+
VoidedTransaction.new(response.parsed_response["transaction"])
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# Credit amount. If amount is nil, then credit the entire previous purchase
|
59
|
+
# or captured amount
|
60
|
+
def credit(amount=nil, ip_address=nil)
|
61
|
+
body = if amount.nil?
|
62
|
+
{:ip => ip_address}
|
63
|
+
else
|
64
|
+
{:transaction => {:amount => amount, :ip => ip_address}}
|
65
|
+
end
|
66
|
+
self.class.verify_post("/transactions/#{token}/credit.xml",
|
67
|
+
:body => body, :has_key => "transaction") do |response|
|
68
|
+
CreditTransaction.new(response.parsed_response["transaction"])
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
module HasIpAddress
|
74
|
+
attr_reader :ip
|
75
|
+
end
|
76
|
+
|
77
|
+
class AuthorizeTransaction < Transaction
|
78
|
+
include HasIpAddress
|
79
|
+
|
80
|
+
handles "Authorization"
|
81
|
+
attr_reader :payment_method
|
82
|
+
|
83
|
+
def initialize(attrs={})
|
84
|
+
@payment_method = PaymentMethod.new(attrs.delete("payment_method") || {})
|
85
|
+
@response = Response.new(attrs.delete("response") || {})
|
86
|
+
super(attrs)
|
87
|
+
end
|
88
|
+
|
89
|
+
# Capture the previously authorized payment. If the amount is nil, the
|
90
|
+
# captured amount will the amount from the original authorization. Some
|
91
|
+
# gateways support partial captures which can be done by specifiying an
|
92
|
+
# amount
|
93
|
+
def capture(amount=nil, ip_address=nil)
|
94
|
+
body = if amount.nil?
|
95
|
+
{}
|
96
|
+
else
|
97
|
+
{:transaction => {:amount => amount, :ip => ip_address}}
|
98
|
+
end
|
99
|
+
self.class.verify_post("/transactions/#{token}/capture.xml",
|
100
|
+
:body => body, :has_key => "transaction") do |response|
|
101
|
+
CaptureTransaction.new(response.parsed_response["transaction"])
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
class PurchaseTransaction < Transaction
|
107
|
+
include NullifiableTransaction
|
108
|
+
include HasIpAddress
|
109
|
+
|
110
|
+
handles "Purchase"
|
111
|
+
attr_reader :payment_method
|
112
|
+
|
113
|
+
def initialize(attrs={})
|
114
|
+
@payment_method = PaymentMethod.new(attrs.delete("payment_method") || {})
|
115
|
+
@response = Response.new(attrs.delete("response") || {})
|
116
|
+
super(attrs)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
class CaptureTransaction < Transaction
|
121
|
+
include NullifiableTransaction
|
122
|
+
include HasIpAddress
|
123
|
+
|
124
|
+
handles "Capture"
|
125
|
+
attr_reader :reference_token
|
126
|
+
end
|
127
|
+
|
128
|
+
class VoidedTransaction < Transaction
|
129
|
+
include HasIpAddress
|
130
|
+
|
131
|
+
handles "Void"
|
132
|
+
attr_reader :reference_token
|
133
|
+
end
|
134
|
+
|
135
|
+
class CreditTransaction < Transaction
|
136
|
+
include HasIpAddress
|
137
|
+
|
138
|
+
handles "Credit"
|
139
|
+
attr_reader :reference_token
|
140
|
+
end
|
141
|
+
|
142
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
module SpreedlyCore
|
4
|
+
class ConfigureTest < Test::Unit::TestCase
|
5
|
+
|
6
|
+
def test_configure
|
7
|
+
SpreedlyCore.configure :login => "test",
|
8
|
+
:secret => "secret",
|
9
|
+
:gateway_token => "token"
|
10
|
+
|
11
|
+
SpreedlyCore.configure 'login' => 'test',
|
12
|
+
'secret' => 'secret',
|
13
|
+
'gateway_token' => 'token'
|
14
|
+
|
15
|
+
SpreedlyCore.configure 'test', 'secret', 'token'
|
16
|
+
|
17
|
+
assert_raises ArgumentError do
|
18
|
+
SpreedlyCore.configure
|
19
|
+
end
|
20
|
+
|
21
|
+
assert_raises ArgumentError do
|
22
|
+
SpreedlyCore.configure {}
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
data/test/factories.rb
ADDED
@@ -0,0 +1,103 @@
|
|
1
|
+
module SpreedlyCore
|
2
|
+
module Factory
|
3
|
+
def given_a_payment_method(cc_card=:master, card_options={})
|
4
|
+
token = SpreedlyCore::PaymentMethod.
|
5
|
+
create_test_token(cc_data(cc_card, card_options), "customer-42")
|
6
|
+
assert payment_method = SpreedlyCore::PaymentMethod.find(token)
|
7
|
+
assert_equal "customer-42", payment_method.data
|
8
|
+
assert_equal token, payment_method.token
|
9
|
+
payment_method
|
10
|
+
end
|
11
|
+
|
12
|
+
def given_a_purchase(purchase_amount=100, ip_address='127.0.0.1')
|
13
|
+
payment_method = given_a_payment_method
|
14
|
+
assert transaction = payment_method.purchase(purchase_amount, nil, nil, ip_address=nil)
|
15
|
+
assert_equal purchase_amount, transaction.amount
|
16
|
+
assert_equal "USD", transaction.currency_code
|
17
|
+
assert_equal "Purchase", transaction.transaction_type
|
18
|
+
assert_equal ip_address, transaction.ip
|
19
|
+
assert transaction.succeeded?
|
20
|
+
transaction
|
21
|
+
end
|
22
|
+
|
23
|
+
def given_a_retained_transaction
|
24
|
+
payment_method = given_a_payment_method
|
25
|
+
assert transaction = payment_method.retain
|
26
|
+
assert transaction.succeeded?
|
27
|
+
assert_equal "RetainPaymentMethod", transaction.transaction_type
|
28
|
+
transaction
|
29
|
+
end
|
30
|
+
|
31
|
+
def given_a_redacted_transaction
|
32
|
+
retained_transaction = given_a_retained_transaction
|
33
|
+
assert payment_method = retained_transaction.payment_method
|
34
|
+
transaction = payment_method.redact
|
35
|
+
assert transaction.succeeded?
|
36
|
+
assert_equal "RedactPaymentMethod", transaction.transaction_type
|
37
|
+
assert !transaction.token.blank?
|
38
|
+
transaction
|
39
|
+
end
|
40
|
+
|
41
|
+
def given_an_authorized_transaction(amount=100, ip_address='127.0.0.1')
|
42
|
+
payment_method = given_a_payment_method
|
43
|
+
assert transaction = payment_method.authorize(100, nil, nil, ip_address)
|
44
|
+
assert_equal 100, transaction.amount
|
45
|
+
assert_equal "USD", transaction.currency_code
|
46
|
+
assert_equal ip_address, transaction.ip
|
47
|
+
assert_equal SpreedlyCore::AuthorizeTransaction, transaction.class
|
48
|
+
transaction
|
49
|
+
end
|
50
|
+
|
51
|
+
def given_a_capture(amount=100, ip_address='127.0.0.1')
|
52
|
+
transaction = given_an_authorized_transaction(amount, ip_address)
|
53
|
+
capture = transaction.capture(amount, ip_address)
|
54
|
+
assert capture.succeeded?
|
55
|
+
assert_equal amount, capture.amount
|
56
|
+
assert_equal "Capture", capture.transaction_type
|
57
|
+
assert_equal ip_address, capture.ip
|
58
|
+
assert_equal SpreedlyCore::CaptureTransaction, capture.class
|
59
|
+
capture
|
60
|
+
end
|
61
|
+
|
62
|
+
def given_a_purchase_void(ip_address='127.0.0.1')
|
63
|
+
purchase = given_a_purchase
|
64
|
+
assert void = purchase.void(ip_address)
|
65
|
+
assert_equal purchase.token, void.reference_token
|
66
|
+
assert_equal ip_address, void.ip
|
67
|
+
assert void.succeeded?
|
68
|
+
void
|
69
|
+
end
|
70
|
+
|
71
|
+
def given_a_capture_void(ip_address='127.0.0.1')
|
72
|
+
capture = given_a_capture
|
73
|
+
assert void = capture.void(ip_address)
|
74
|
+
assert_equal capture.token, void.reference_token
|
75
|
+
assert_equal ip_address, void.ip
|
76
|
+
assert void.succeeded?
|
77
|
+
void
|
78
|
+
end
|
79
|
+
|
80
|
+
def given_a_purchase_credit(purchase_amount=100, credit_amount=100, ip_address='127.0.0.1')
|
81
|
+
purchase = given_a_purchase(purchase_amount, ip_address)
|
82
|
+
given_a_credit(purchase, credit_amount, ip_address)
|
83
|
+
end
|
84
|
+
|
85
|
+
def given_a_capture_credit(capture_amount=100, credit_amount=100, ip_address='127.0.0.1')
|
86
|
+
capture = given_a_capture(capture_amount, ip_address)
|
87
|
+
given_a_credit(capture, credit_amount, ip_address)
|
88
|
+
end
|
89
|
+
|
90
|
+
def given_a_credit(trans, credit_amount=100, ip_address='127.0.0.1')
|
91
|
+
assert credit = trans.credit(credit_amount, ip_address)
|
92
|
+
assert_equal trans.token, credit.reference_token
|
93
|
+
assert_equal credit_amount, credit.amount
|
94
|
+
assert_equal ip_address, credit.ip
|
95
|
+
assert credit.succeeded?
|
96
|
+
assert SpreedlyCore::CreditTransaction, credit.class
|
97
|
+
credit
|
98
|
+
end
|
99
|
+
|
100
|
+
|
101
|
+
|
102
|
+
end
|
103
|
+
end
|