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.
@@ -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