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