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
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,213 @@
|
|
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 look up 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
|
+
# or if loading from YAML for example, configure takes a hash as well
|
75
|
+
SpreedlyCore.configure(:login => "Your API Login", :secret => "Your API Secret",
|
76
|
+
:token => "Test Gateway Token")
|
77
|
+
master_card_data = SpreedlyCore::TestHelper.cc_data(:master)
|
78
|
+
token = SpreedlyCore::PaymentMethod.create_test_token(master_card_data)
|
79
|
+
|
80
|
+
|
81
|
+
Look up a payment method:
|
82
|
+
|
83
|
+
payment_method = SpreedlyCore::PaymentMethod.find(token)
|
84
|
+
|
85
|
+
Retain a payment method for later use:
|
86
|
+
|
87
|
+
retain_transaction = payment_method.retain
|
88
|
+
retain_transaction.succeeded? # true
|
89
|
+
|
90
|
+
Redact a previously retained payment method:
|
91
|
+
|
92
|
+
redact_transaction = payment_method.redact
|
93
|
+
redact_transaction.succeeded?
|
94
|
+
|
95
|
+
Make a purchase against a payment method:
|
96
|
+
|
97
|
+
purchase_transaction = payment_method.purchase(100)
|
98
|
+
purchase_transaction.succeeded? # true
|
99
|
+
|
100
|
+
Make an authorize request against a payment method, then capture the payment
|
101
|
+
|
102
|
+
authorize = payment_method.authorize(100)
|
103
|
+
authorize.succeeded? # true
|
104
|
+
capture = authorize.capture(50) # Capture only half of the authorized amount
|
105
|
+
capture.succeeded? # true
|
106
|
+
|
107
|
+
authorize = payment_method.authorize(100)
|
108
|
+
authorize.succeeded? # true
|
109
|
+
authorized.capture # Capture the full amount
|
110
|
+
capture.succeeded? # true
|
111
|
+
|
112
|
+
Void a previous purchase:
|
113
|
+
|
114
|
+
purchase_transaction.void # void the purchase
|
115
|
+
|
116
|
+
Credit a previous purchase:
|
117
|
+
|
118
|
+
purchase_transaction = payment_method.purchase(100) # make a purchase
|
119
|
+
purchase_transaction.credit
|
120
|
+
purchase_transaction.succeeded? # true
|
121
|
+
|
122
|
+
Credit part of a previous purchase:
|
123
|
+
|
124
|
+
purchase_transaction = payment_method.purchase(100) # make a purchase
|
125
|
+
purchase_transaction.credit(50) # provide a partial credit
|
126
|
+
purchase_transaction.succeeded? # true
|
127
|
+
|
128
|
+
Handling Exceptions:
|
129
|
+
|
130
|
+
There are 2 types of exceptions which can be raised by the library:
|
131
|
+
|
132
|
+
1. SpreedlyCore::TimeOutError is raised if communication with SpreedlyCore
|
133
|
+
takes longer than 10 seconds
|
134
|
+
2. SpreedlyCore::InvalidResponse is raised when the response code is
|
135
|
+
unexpected (I.E. we expect a HTTP response code of 200 bunt instead got a
|
136
|
+
500) or if the response does not contain an expected attribute. For
|
137
|
+
example, the response from retaining a payment method should contain an XML
|
138
|
+
attribute of "transaction". If this is not found (for example a HTTP
|
139
|
+
response 404 or 500 is returned), then an InvalidResponse is raised.
|
140
|
+
|
141
|
+
|
142
|
+
Both TimeOutError and InvalidResponse subclass SpreedlyCore::Error.
|
143
|
+
|
144
|
+
Look up a payment method that does not exist:
|
145
|
+
|
146
|
+
begin
|
147
|
+
payment_method = SpreedlyCore::PaymentMethod.find("NOT-FOUND")
|
148
|
+
rescue SpreedlyCore::InvalidResponse => e
|
149
|
+
puts e.inspect
|
150
|
+
end
|
151
|
+
|
152
|
+
|
153
|
+
Additional Field Validation
|
154
|
+
----------
|
155
|
+
|
156
|
+
|
157
|
+
The Spreedyly Core API provides validation of the credit card number, CVE, and
|
158
|
+
first and last name. In most cases this is enough, however sometimes you want to
|
159
|
+
enforce the billing information as well. This can be accomplished via:
|
160
|
+
|
161
|
+
require 'spreedly_core'
|
162
|
+
require 'spreedly_core/test_extensions'
|
163
|
+
SpreedlyCore.configure("Your API Login", "Your API Secret", "Test Gateway Token")
|
164
|
+
SpreedlyCore::PaymentMethod.additional_required_cc_fields :address1, :city, :state, :zip
|
165
|
+
master_card_data = SpreedlyCore::TestHelper.cc_data(:master)
|
166
|
+
token = SpreedlyCore::PaymentMethod.create_test_token(master_card_data)
|
167
|
+
payment_method = SpreedlyCore::PaymentMethod.find(token)
|
168
|
+
payment_method.valid? # returns false
|
169
|
+
payment_method.errors # ["Address1 can't be blank", "City can't be blank", "State can't be blank", "Zip can't be blank"]
|
170
|
+
master_card_data = SpreedlyCore::TestHelper.cc_data(:master, :credit_card => {:address1 => "742 Evergreen Terrace", :city => "Springfield", :state => "IL", 62701})
|
171
|
+
payment_method = SpreedlyCore::PaymentMethod.find(token)
|
172
|
+
payment_method.valid? # returns true
|
173
|
+
payment_method.errors # []
|
174
|
+
|
175
|
+
|
176
|
+
Configuring SpreedlyCore with Rails
|
177
|
+
----------
|
178
|
+
|
179
|
+
Inside your Rails project create config/spreedly_core.yml formatted like config/database.yml. For example:
|
180
|
+
|
181
|
+
development:
|
182
|
+
login: <Login Key>
|
183
|
+
secret: <Secret Key>
|
184
|
+
gateway_token: 'JncEWj22g59t3CRB1VnPXmUUgKc' # this is the test gateway, replace with your real gateway in production
|
185
|
+
test:
|
186
|
+
login: <Login Key>
|
187
|
+
secret: <Secret Key>
|
188
|
+
gateway_token: 'JncEWj22g59t3CRB1VnPXmUUgKc' # this is the test gateway, replace with your real gateway in production
|
189
|
+
production:
|
190
|
+
login: <Login Key>
|
191
|
+
secret: <Secret Key>
|
192
|
+
gateway_token: 'JncEWj22g59t3CRB1VnPXmUUgKc' # this is the test gateway, replace with your real gateway in production
|
193
|
+
|
194
|
+
Then create config/initializers/spreedly_core.rb with the following:
|
195
|
+
|
196
|
+
config = YAML.load(File.read(RAILS_ROOT + '/config/spreedly_core.yml'))[RAILS_ENV]
|
197
|
+
SpreedlyCore.configure(config)
|
198
|
+
|
199
|
+
Optionally require additional credit card fields:
|
200
|
+
|
201
|
+
SpreedlyCore::PaymentMethod.additional_required_cc_fields :address1, :city, :state, :zip
|
202
|
+
|
203
|
+
Contributing
|
204
|
+
------------
|
205
|
+
|
206
|
+
Once you've made your commits:
|
207
|
+
|
208
|
+
1. [Fork](http://help.github.com/forking/) SpreedlyCore
|
209
|
+
2. Create a topic branch - `git checkout -b my_branch`
|
210
|
+
3. Push to your branch - `git push origin my_branch`
|
211
|
+
4. Create a [Pull Request](http://help.github.com/pull-requests/) from your branch
|
212
|
+
5. Profit!
|
213
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
require 'rake/testtask'
|
3
|
+
|
4
|
+
Rake::TestTask.new(:test) do |test|
|
5
|
+
test.libs << 'lib' << 'test'
|
6
|
+
test.pattern = 'test/**/*_test.rb'
|
7
|
+
test.verbose = true
|
8
|
+
end
|
9
|
+
|
10
|
+
begin
|
11
|
+
require 'rcov/rcovtask'
|
12
|
+
Rcov::RcovTask.new do |test|
|
13
|
+
test.libs << 'test'
|
14
|
+
test.pattern = 'test/**/test_*.rb'
|
15
|
+
test.verbose = true
|
16
|
+
end
|
17
|
+
rescue LoadError
|
18
|
+
task :rcov do
|
19
|
+
abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
task :default => :test
|
24
|
+
|
25
|
+
require 'rake/rdoctask'
|
26
|
+
Rake::RDocTask.new do |rdoc|
|
27
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
28
|
+
|
29
|
+
rdoc.rdoc_dir = 'rdoc'
|
30
|
+
rdoc.title = "someproject #{version}"
|
31
|
+
rdoc.rdoc_files.include('README*')
|
32
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
33
|
+
end
|
34
|
+
|
@@ -0,0 +1,72 @@
|
|
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/test_gateway'
|
9
|
+
require 'spreedly_core/transactions'
|
10
|
+
|
11
|
+
module SpreedlyCore
|
12
|
+
# Hash of user friendly credit card name to SpreedlyCore API name
|
13
|
+
CARD_TYPES = {
|
14
|
+
"Visa" => "visa",
|
15
|
+
"MasterCard" => "master",
|
16
|
+
"American Express" => "american_express",
|
17
|
+
"Discover" => "discover"
|
18
|
+
}
|
19
|
+
|
20
|
+
class Error < RuntimeError; end
|
21
|
+
# Custom exception which occurs when a request to SpreedlyCore times out
|
22
|
+
# See SpreedlyCore::Base.default_timeout
|
23
|
+
class TimeOutError < Error; end
|
24
|
+
class InvalidResponse < Error
|
25
|
+
def initialize(response, message)
|
26
|
+
super("#{message}\nResponse:\n#{response.inspect}")
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# Configure SpreedlyCore with a particular account.
|
31
|
+
# Strongly prefers environment variables for credentials
|
32
|
+
# and will issue a stern warning should they not be present.
|
33
|
+
# Reluctantly accepts :login and :secret as options
|
34
|
+
def self.configure(options = {})
|
35
|
+
login = ENV['SPREEDLYCORE_API_LOGIN']
|
36
|
+
secret = ENV['SPREEDLYCORE_API_SECRET']
|
37
|
+
|
38
|
+
if options[:api_login]
|
39
|
+
Kernel.warn("ENV and arg both present for api_login. Defaulting to arg value") if login
|
40
|
+
login = options[:api_login]
|
41
|
+
end
|
42
|
+
|
43
|
+
if options[:api_secret]
|
44
|
+
Kernel.warn("ENV and arg both present for api_secret. Defaulting to arg value") if login
|
45
|
+
secret = options[:api_secret]
|
46
|
+
end
|
47
|
+
|
48
|
+
if options[:api_login] || options[:api_secret]
|
49
|
+
Kernel.warn("It is STRONGLY preferred that you house your Spreedly Core credentials only in environment variables.")
|
50
|
+
Kernel.warn("This gem prefers only environment variables named SPREEDLYCORE_API_LOGIN and SPREEDLYCORE_API_SECRET.")
|
51
|
+
end
|
52
|
+
|
53
|
+
if login.nil? || secret.nil?
|
54
|
+
raise ArgumentError.new("You must provide a login and a secret. Gem will look for ENV['SPREEDLYCORE_API_LOGIN'] and ENV['SPREEDLYCORE_API_SECRET'], but you may also pass in a hash with :api_login and :api_secret keys.")
|
55
|
+
end
|
56
|
+
Base.configure(login, secret, options)
|
57
|
+
end
|
58
|
+
|
59
|
+
def self.gateway_token=(gateway_token)
|
60
|
+
Base.gateway_token = gateway_token
|
61
|
+
end
|
62
|
+
|
63
|
+
# returns the configured SpreedlyCore login
|
64
|
+
def self.login; Base.login; end
|
65
|
+
|
66
|
+
# A container for a response from a payment gateway
|
67
|
+
class Response < Base
|
68
|
+
attr_reader(:success, :message, :avs_code, :avs_message, :cvv_code,
|
69
|
+
:cvv_message, :error_code, :error_detail, :created_at,
|
70
|
+
:updated_at)
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,95 @@
|
|
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/#{API_VERSION}"
|
16
|
+
base_uri "http://core.spreedly.dev:11001/#{API_VERSION}"
|
17
|
+
|
18
|
+
def self.configure(login, secret, options = {})
|
19
|
+
@@login = login
|
20
|
+
self.basic_auth(@@login, secret)
|
21
|
+
@@gateway_token = options.delete(:gateway_token)
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.login; @@login; end
|
25
|
+
def self.gateway_token; @@gateway_token; end
|
26
|
+
def self.gateway_token=(gateway_token); @@gateway_token = gateway_token; end
|
27
|
+
|
28
|
+
# make a post request to path
|
29
|
+
# If the request succeeds, provide the respones to the &block
|
30
|
+
def self.verify_post(path, options={}, &block)
|
31
|
+
verify_request(:post, path, options, 200, 201, 422, &block)
|
32
|
+
end
|
33
|
+
|
34
|
+
# make a put request to path
|
35
|
+
# If the request succeeds, provide the respones to the &block
|
36
|
+
def self.verify_put(path, options={}, &block)
|
37
|
+
verify_request(:put, path, options, &block)
|
38
|
+
end
|
39
|
+
|
40
|
+
# make a get request to path
|
41
|
+
# If the request succeeds, provide the respones to the &block
|
42
|
+
def self.verify_get(path, options={}, &block)
|
43
|
+
verify_request(:get, path, options, 200, &block)
|
44
|
+
end
|
45
|
+
|
46
|
+
# make an options request to path
|
47
|
+
# If the request succeeds, provide the respones to the &block
|
48
|
+
def self.verify_options(path, options={}, &block)
|
49
|
+
verify_request(:options, path, options, 200, &block)
|
50
|
+
end
|
51
|
+
|
52
|
+
# make a request to path using the HTTP method provided as request_type
|
53
|
+
# *allowed_codes are passed in, verify the response code (200, 404, etc)
|
54
|
+
# is one of the allowed codes.
|
55
|
+
# If *allowed_codes is empty, don't check the response code, but set an instance
|
56
|
+
# variable on the object created in the block containing the response code.
|
57
|
+
def self.verify_request(request_type, path, options, *allowed_codes, &block)
|
58
|
+
begin
|
59
|
+
response = self.send(request_type, path, options)
|
60
|
+
rescue Timeout::Error, Errno::ETIMEDOUT => e
|
61
|
+
raise TimeOutError.new("Request to #{path} timed out. Is Spreedly Core down?")
|
62
|
+
end
|
63
|
+
|
64
|
+
if allowed_codes.any? && !allowed_codes.include?(response.code)
|
65
|
+
raise InvalidResponse.new(response, "Error retrieving #{path}. Got status of #{response.code}. Expected status to be in #{allowed_codes.join(",")}")
|
66
|
+
end
|
67
|
+
|
68
|
+
if options.has_key?(:has_key) &&
|
69
|
+
(response.parsed_response.nil? || !response.parsed_response.has_key?(options[:has_key]))
|
70
|
+
raise InvalidResponse.new(response, "Expected parsed response to contain key '#{options[:has_key]}'")
|
71
|
+
end
|
72
|
+
|
73
|
+
block.call(response).tap do |obj|
|
74
|
+
obj.instance_variable_set("@http_code", response.code)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# Given a hash of attrs, assign instance variables using the hash key as the
|
79
|
+
# attribute name and hash value as the attribute value
|
80
|
+
#
|
81
|
+
def initialize(attrs={})
|
82
|
+
attrs.each do |k, v|
|
83
|
+
instance_variable_set("@#{k}", v)
|
84
|
+
end
|
85
|
+
# errors may be nil, empty, a string, or an array of strings.
|
86
|
+
@errors = if @errors.nil? || @errors["error"].blank?
|
87
|
+
[]
|
88
|
+
elsif @errors["error"].is_a?(String)
|
89
|
+
[@errors["error"]]
|
90
|
+
else
|
91
|
+
@errors["error"]
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module SpreedlyCore
|
2
|
+
class Gateway < Base
|
3
|
+
attr_reader(:name, :token, :gateway_type, :auth_modes, :supports_capture,
|
4
|
+
:supports_authorize, :supports_purchase, :supports_void,
|
5
|
+
:supports_credit, :redacted)
|
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
|
+
|
19
|
+
def use!
|
20
|
+
self.class.gateway_token = self.token
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|