spreedly-core-ruby 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,4 @@
1
+ module SpreedlyCore
2
+ Version = VERSION = "0.1.0"
3
+ ApiVersion = API_VERSION = "v1"
4
+ end
@@ -0,0 +1,3 @@
1
+ login: <Login Key>
2
+ secret: <Secret Key>
3
+ gateway_token: 'JncEWj22g59t3CRB1VnPXmUUgKc' # test gateway
@@ -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
@@ -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