exchequer 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +5 -0
- data/LICENCE +20 -0
- data/README.textile +74 -0
- data/Rakefile +9 -0
- data/exchequer.gemspec +18 -0
- data/lib/exchequer.rb +7 -0
- data/lib/exchequer/biller.rb +46 -0
- data/lib/exchequer/card.rb +32 -0
- data/lib/exchequer/error_mapper.rb +33 -0
- data/spec/exchequer/biller_spec.rb +96 -0
- data/spec/exchequer/error_mapper_spec.rb +73 -0
- metadata +105 -0
data/LICENCE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2012-2013 Pat Allan
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.textile
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
h1. Exchequer
|
2
|
+
|
3
|
+
A collection of helpful classes for a clean, object-oriented approach to dealing with Chargify.
|
4
|
+
|
5
|
+
h2. Installation
|
6
|
+
|
7
|
+
<pre><code>gem install exchequer</code></pre>
|
8
|
+
|
9
|
+
h2. Usage
|
10
|
+
|
11
|
+
This library currently comes with three classes: Card, Biller and ErrorMapper.
|
12
|
+
|
13
|
+
h3. Card
|
14
|
+
|
15
|
+
Card objects represent a Chargify credit card. It's built to be used in Rails forms, and has all the essential card attributes (first and last name, number, cvv, expiration month and year) plus billing address details (address, city, state, zip and country). This means you don't need to add misleading attribute accessors to your own models. At a basic level, it's much like any other ActiveModel object: you initialise it with a set of parameters (perhaps via @params@), and it responds to @#errors@.
|
16
|
+
|
17
|
+
<pre><code>card = Exchequer::Card.new(
|
18
|
+
:first_name => 'John',
|
19
|
+
:last_name => 'Smith',
|
20
|
+
:full_number => '1234567812345678',
|
21
|
+
:cvv => '123',
|
22
|
+
:expiration_month => '10',
|
23
|
+
:expiration_year => '2016',
|
24
|
+
:billing_address => '1 Main Road',
|
25
|
+
:billing_city => 'Melbourne',
|
26
|
+
:billing_state => 'Victoria',
|
27
|
+
:billing_zip => '3000',
|
28
|
+
:billing_country => 'Australia'
|
29
|
+
)</code></pre>
|
30
|
+
|
31
|
+
It also provides a useful helper method @#to_hash@, which is useful if you want to feed it in to a Chargify::Subscription object's @credit_card_attributes@. The hash returned is much like what was passed in when initializing in the above example, except it always uses string keys.
|
32
|
+
|
33
|
+
h3. Biller
|
34
|
+
|
35
|
+
This is the most complicated piece of code - but it's nothing too tricky. It looks after the creation of Chargify customers and subscribers. You initialise it by passing through a Card object and some options - all of which are required: an email address for the customer, a unique reference for the customer, and the handle for the product the subscription is being applied to.
|
36
|
+
|
37
|
+
<pre><code>biller = Exchequer::Biller.new card,
|
38
|
+
:email => 'john@smith.com',
|
39
|
+
:reference => 'customer-123',
|
40
|
+
:product_handle => 'default-subscription'</code></pre>
|
41
|
+
|
42
|
+
When the biller object is created, you can then call @#charge@, which takes care of the communication with Chargify. It'll return true if everything is successful, but otherwise false - and will populate the card with any errors Chargify provides.
|
43
|
+
|
44
|
+
It's worth noting that the customer's first and last name will be taken from the card object. Customising that may be an option in a later release.
|
45
|
+
|
46
|
+
Once that's done, you can then call @#subscription_id@ to get the unique identifier for the subscription. It's probably worth storing that somewhere so you can interact with the subscription again at a later point.
|
47
|
+
|
48
|
+
<pre><code>if biller.charged
|
49
|
+
customer.subscription_id = biller.subscription_id
|
50
|
+
customer.save
|
51
|
+
else
|
52
|
+
# Use card.errors to determine why the subscription or customer could not
|
53
|
+
# be saved.
|
54
|
+
end</code></pre>
|
55
|
+
|
56
|
+
h3. ErrorMapper
|
57
|
+
|
58
|
+
This has been built as an internal class for transferring errors from one ActiveModel object to another, with a few translations to shift errors on @:base@ to the appropriate attributes (eg: messages starting with 'Credit card number' are attached to the :full_number attribute). So, it's built with Card objects in mind, but if you find your own uses for it, wonderful.
|
59
|
+
|
60
|
+
<pre><code>Exchequer::ErrorMapper.map(subscription, :to => card)</code></pre>
|
61
|
+
|
62
|
+
h3. Putting it all together
|
63
|
+
|
64
|
+
The last code example within the Biller details above should give you a good idea on how this library's expected to be used....
|
65
|
+
|
66
|
+
h2. Contributing
|
67
|
+
|
68
|
+
Patches are certainly welcome - though it's a good idea to contact me (either via email or through the GitHub issues on this project) to discuss what your planned changes are if they're beyond bug fixes. I want this project to reflect a clear and focused approach for dealing with Chargify (and possibly other payment gateways), and so I don't want people wasting their time on patches that I would consider inappropriate for that purpose.
|
69
|
+
|
70
|
+
Yes, it's opinionated - but there's nothing stopping you from forking this code, modifying it, and using your version in whatever way you see fit.
|
71
|
+
|
72
|
+
h2. Licence
|
73
|
+
|
74
|
+
Copyright (c) 2012-2013, Exchequer is developed and maintained by Pat Allan, and is released under the open MIT Licence.
|
data/Rakefile
ADDED
data/exchequer.gemspec
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
Gem::Specification.new do |s|
|
3
|
+
s.name = 'exchequer'
|
4
|
+
s.version = '0.1.0'
|
5
|
+
s.authors = ['Pat Allan']
|
6
|
+
s.email = ['pat@freelancing-gods.com']
|
7
|
+
s.homepage = 'https://github.com/pat/exchequer'
|
8
|
+
s.summary = 'Chargify Workflow Toolbox'
|
9
|
+
s.description = 'An Object-Oriented approach to working with Chargify in Ruby'
|
10
|
+
|
11
|
+
s.files = `git ls-files`.split("\n")
|
12
|
+
s.require_paths = ['lib']
|
13
|
+
|
14
|
+
s.add_runtime_dependency 'chargify_api_ares', '> 0'
|
15
|
+
s.add_runtime_dependency 'activemodel', '> 0'
|
16
|
+
|
17
|
+
s.add_development_dependency 'rspec', '>= 2.11.0'
|
18
|
+
end
|
data/lib/exchequer.rb
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
class Exchequer::Biller
|
2
|
+
def initialize(card, options = {})
|
3
|
+
@card = card
|
4
|
+
@options = options
|
5
|
+
end
|
6
|
+
|
7
|
+
def charge
|
8
|
+
map_errors_from(customer) and return false if customer.id.nil?
|
9
|
+
map_errors_from(subscription) and return false if !subscription.save
|
10
|
+
|
11
|
+
true
|
12
|
+
end
|
13
|
+
|
14
|
+
def subscription_id
|
15
|
+
subscription.id
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
attr_reader :card, :options
|
21
|
+
|
22
|
+
def customer
|
23
|
+
@customer ||= begin
|
24
|
+
Chargify::Customer.find_by_reference(options[:reference])
|
25
|
+
rescue ActiveResource::ResourceNotFound
|
26
|
+
Chargify::Customer.create(
|
27
|
+
:first_name => card.first_name,
|
28
|
+
:last_name => card.last_name,
|
29
|
+
:email => options[:email],
|
30
|
+
:reference => options[:reference]
|
31
|
+
)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def map_errors_from(object)
|
36
|
+
Exchequer::ErrorMapper.map object, :to => card
|
37
|
+
end
|
38
|
+
|
39
|
+
def subscription
|
40
|
+
@subscription ||= Chargify::Subscription.new(
|
41
|
+
:customer_id => customer.id,
|
42
|
+
:product_handle => options[:product_handle],
|
43
|
+
:credit_card_attributes => card.to_hash
|
44
|
+
)
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
class Exchequer::Card
|
2
|
+
attr_accessor :first_name, :last_name, :full_number, :cvv,
|
3
|
+
:expiration_month, :expiration_year, :billing_address, :billing_city,
|
4
|
+
:billing_state, :billing_zip, :billing_country
|
5
|
+
|
6
|
+
def initialize(params = {})
|
7
|
+
params ||= {}
|
8
|
+
params.keys.each do |key|
|
9
|
+
send "#{key}=", params[key]
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def errors
|
14
|
+
@errors ||= ActiveModel::Errors.new(self)
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_hash
|
18
|
+
{
|
19
|
+
'first_name' => first_name,
|
20
|
+
'last_name' => last_name,
|
21
|
+
'full_number' => full_number,
|
22
|
+
'cvv' => cvv,
|
23
|
+
'expiration_month' => expiration_month,
|
24
|
+
'expiration_year' => expiration_year,
|
25
|
+
'billing_address' => billing_address,
|
26
|
+
'billing_city' => billing_city,
|
27
|
+
'billing_state' => billing_state,
|
28
|
+
'billing_zip' => billing_zip,
|
29
|
+
'billing_country' => billing_country
|
30
|
+
}
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
class Exchequer::ErrorMapper
|
2
|
+
MAPPINGS = {
|
3
|
+
'Credit card expiration year' => :expiration_year,
|
4
|
+
'Credit card expiration month' => :expiration_month,
|
5
|
+
'Credit card number' => :full_number,
|
6
|
+
'First name' => :first_name,
|
7
|
+
'Last name' => :last_name
|
8
|
+
}
|
9
|
+
|
10
|
+
def self.map(source, arguments)
|
11
|
+
new(source, arguments[:to]).map_errors
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize(source, destination)
|
15
|
+
@source, @destination = source, destination
|
16
|
+
end
|
17
|
+
|
18
|
+
def map_errors
|
19
|
+
source.errors[:base].dup.each do |error|
|
20
|
+
label = MAPPINGS.keys.detect { |key| error[/^#{key}:/] }
|
21
|
+
|
22
|
+
if label
|
23
|
+
destination.errors[MAPPINGS[label]] << error.gsub(/^#{label}:\s+/, '')
|
24
|
+
else
|
25
|
+
destination.errors[:base] << error
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
attr_reader :source, :destination
|
33
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
require 'active_resource/exceptions'
|
2
|
+
require './lib/exchequer'
|
3
|
+
|
4
|
+
describe Exchequer::Biller do
|
5
|
+
let(:biller) { Exchequer::Biller.new card,
|
6
|
+
:reference => 'my-unique-key', :email => 'me@domain.com',
|
7
|
+
:product_handle => 'standard-product' }
|
8
|
+
let(:card) { double(:first_name => 'John', :last_name => 'Smith',
|
9
|
+
:to_hash => double) }
|
10
|
+
let(:customer) { double(:id => 5234) }
|
11
|
+
let(:subscription) { double(:save => true) }
|
12
|
+
|
13
|
+
describe '#charge' do
|
14
|
+
before :each do
|
15
|
+
stub_const 'Chargify::Customer', double(:find_by_reference => customer)
|
16
|
+
stub_const 'Chargify::Subscription', double(:new => subscription)
|
17
|
+
stub_const 'Exchequer::ErrorMapper', double(:map => true)
|
18
|
+
end
|
19
|
+
|
20
|
+
it "uses an existing company for the given reference" do
|
21
|
+
Chargify::Customer.should_receive(:find_by_reference).
|
22
|
+
with('my-unique-key').and_return(customer)
|
23
|
+
|
24
|
+
biller.charge
|
25
|
+
end
|
26
|
+
|
27
|
+
it "creates a new company with the reference if necessary" do
|
28
|
+
Chargify::Customer.stub(:find_by_reference).
|
29
|
+
and_raise(ActiveResource::ResourceNotFound.new(''))
|
30
|
+
|
31
|
+
Chargify::Customer.should_receive(:create).with(
|
32
|
+
:first_name => 'John',
|
33
|
+
:last_name => 'Smith',
|
34
|
+
:email => 'me@domain.com',
|
35
|
+
:reference => 'my-unique-key'
|
36
|
+
).and_return(customer)
|
37
|
+
|
38
|
+
biller.charge
|
39
|
+
end
|
40
|
+
|
41
|
+
it "creates a new subscription" do
|
42
|
+
Chargify::Subscription.should_receive(:new).with(
|
43
|
+
:customer_id => 5234,
|
44
|
+
:product_handle => 'standard-product',
|
45
|
+
:credit_card_attributes => card.to_hash
|
46
|
+
).and_return(subscription)
|
47
|
+
subscription.should_receive(:save)
|
48
|
+
|
49
|
+
biller.charge
|
50
|
+
end
|
51
|
+
|
52
|
+
it "returns true" do
|
53
|
+
biller.charge.should be_true
|
54
|
+
end
|
55
|
+
|
56
|
+
context 'customer cannot be saved' do
|
57
|
+
before :each do
|
58
|
+
customer.stub :id => nil
|
59
|
+
end
|
60
|
+
|
61
|
+
it "maps customer errors to the card" do
|
62
|
+
Exchequer::ErrorMapper.should_receive(:map).
|
63
|
+
with(customer, :to => card).and_return(true)
|
64
|
+
|
65
|
+
biller.charge
|
66
|
+
end
|
67
|
+
|
68
|
+
it "does not save a subscription" do
|
69
|
+
subscription.should_not_receive(:save)
|
70
|
+
|
71
|
+
biller.charge
|
72
|
+
end
|
73
|
+
|
74
|
+
it "returns false" do
|
75
|
+
biller.charge.should be_false
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
context 'subscription cannot be saved' do
|
80
|
+
before :each do
|
81
|
+
subscription.stub :save => false
|
82
|
+
end
|
83
|
+
|
84
|
+
it "maps the subscription errors to the card" do
|
85
|
+
Exchequer::ErrorMapper.should_receive(:map).
|
86
|
+
with(subscription, :to => card).and_return(true)
|
87
|
+
|
88
|
+
biller.charge
|
89
|
+
end
|
90
|
+
|
91
|
+
it "returns false" do
|
92
|
+
biller.charge.should be_false
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require './lib/exchequer'
|
2
|
+
|
3
|
+
describe Exchequer::ErrorMapper do
|
4
|
+
def errors_hash
|
5
|
+
Hash.new { |hash, key| hash[key] = [] }
|
6
|
+
end
|
7
|
+
|
8
|
+
let(:map_errors) { Exchequer::ErrorMapper.map(source, :to => destination) }
|
9
|
+
let(:source) { double(:errors => errors_hash) }
|
10
|
+
let(:destination) { double(:errors => errors_hash) }
|
11
|
+
|
12
|
+
describe '#map_errors' do
|
13
|
+
it "transfers credit card expiration year attributes" do
|
14
|
+
source.errors[:base] << 'Credit card expiration year: Not valid'
|
15
|
+
|
16
|
+
map_errors
|
17
|
+
|
18
|
+
destination.errors.should == {
|
19
|
+
:expiration_year => ['Not valid']
|
20
|
+
}
|
21
|
+
end
|
22
|
+
|
23
|
+
it "transfers credit card expiration month attributes" do
|
24
|
+
source.errors[:base] << 'Credit card expiration month: Not valid'
|
25
|
+
|
26
|
+
map_errors
|
27
|
+
|
28
|
+
destination.errors.should == {
|
29
|
+
:expiration_month => ['Not valid']
|
30
|
+
}
|
31
|
+
end
|
32
|
+
|
33
|
+
it "transfers credit card number attributes" do
|
34
|
+
source.errors[:base] << 'Credit card number: Not valid'
|
35
|
+
|
36
|
+
map_errors
|
37
|
+
|
38
|
+
destination.errors.should == {
|
39
|
+
:full_number => ['Not valid']
|
40
|
+
}
|
41
|
+
end
|
42
|
+
|
43
|
+
it "transfers first name attributes" do
|
44
|
+
source.errors[:base] << 'First name: Not valid'
|
45
|
+
|
46
|
+
map_errors
|
47
|
+
|
48
|
+
destination.errors.should == {
|
49
|
+
:first_name => ['Not valid']
|
50
|
+
}
|
51
|
+
end
|
52
|
+
|
53
|
+
it "transfers last name attributes" do
|
54
|
+
source.errors[:base] << 'Last name: Not valid'
|
55
|
+
|
56
|
+
map_errors
|
57
|
+
|
58
|
+
destination.errors.should == {
|
59
|
+
:last_name => ['Not valid']
|
60
|
+
}
|
61
|
+
end
|
62
|
+
|
63
|
+
it "transfers other errors to base" do
|
64
|
+
source.errors[:base] << 'Gateway error'
|
65
|
+
|
66
|
+
map_errors
|
67
|
+
|
68
|
+
destination.errors.should == {
|
69
|
+
:base => ['Gateway error']
|
70
|
+
}
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
metadata
ADDED
@@ -0,0 +1,105 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: exchequer
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Pat Allan
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-01-07 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: chargify_api_ares
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>'
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>'
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: activemodel
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>'
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :runtime
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>'
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: rspec
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: 2.11.0
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 2.11.0
|
62
|
+
description: An Object-Oriented approach to working with Chargify in Ruby
|
63
|
+
email:
|
64
|
+
- pat@freelancing-gods.com
|
65
|
+
executables: []
|
66
|
+
extensions: []
|
67
|
+
extra_rdoc_files: []
|
68
|
+
files:
|
69
|
+
- Gemfile
|
70
|
+
- LICENCE
|
71
|
+
- README.textile
|
72
|
+
- Rakefile
|
73
|
+
- exchequer.gemspec
|
74
|
+
- lib/exchequer.rb
|
75
|
+
- lib/exchequer/biller.rb
|
76
|
+
- lib/exchequer/card.rb
|
77
|
+
- lib/exchequer/error_mapper.rb
|
78
|
+
- spec/exchequer/biller_spec.rb
|
79
|
+
- spec/exchequer/error_mapper_spec.rb
|
80
|
+
homepage: https://github.com/pat/exchequer
|
81
|
+
licenses: []
|
82
|
+
post_install_message:
|
83
|
+
rdoc_options: []
|
84
|
+
require_paths:
|
85
|
+
- lib
|
86
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
87
|
+
none: false
|
88
|
+
requirements:
|
89
|
+
- - ! '>='
|
90
|
+
- !ruby/object:Gem::Version
|
91
|
+
version: '0'
|
92
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
93
|
+
none: false
|
94
|
+
requirements:
|
95
|
+
- - ! '>='
|
96
|
+
- !ruby/object:Gem::Version
|
97
|
+
version: '0'
|
98
|
+
requirements: []
|
99
|
+
rubyforge_project:
|
100
|
+
rubygems_version: 1.8.23
|
101
|
+
signing_key:
|
102
|
+
specification_version: 3
|
103
|
+
summary: Chargify Workflow Toolbox
|
104
|
+
test_files: []
|
105
|
+
has_rdoc:
|