exchequer 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ source :rubygems
2
+
3
+ gemspec
4
+
5
+ gem 'rake'
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.
@@ -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.
@@ -0,0 +1,9 @@
1
+ require 'bundler'
2
+
3
+ Bundler::GemHelper.install_tasks
4
+
5
+ require 'rspec/core/rake_task'
6
+
7
+ RSpec::Core::RakeTask.new
8
+
9
+ task :default => :spec
@@ -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
@@ -0,0 +1,7 @@
1
+ module Exchequer
2
+ #
3
+ end
4
+
5
+ require 'exchequer/biller'
6
+ require 'exchequer/card'
7
+ require 'exchequer/error_mapper'
@@ -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: