spreedly_core 0.0.2

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