redline 0.1.4 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -19,4 +19,5 @@ rdoc
19
19
  pkg
20
20
 
21
21
  ## PROJECT::SPECIFIC
22
- spec/db/test.sqlite3
22
+ spec/db/test.sqlite3
23
+ tmp/
data/README.rdoc CHANGED
@@ -2,7 +2,18 @@
2
2
 
3
3
  Braintree via Redline: riding the rails
4
4
 
5
- Redline helps you with Braintree on Rails, it automatically makes creation/deletion/update scripts for your model, translating and syncing them with a Braintree customer profile. In the future I plan to increase the capabilities with helpers for Braintree models and light-weight subscription support, perhaps using Braintree's reoccurring billing.
5
+ Redline helps you with Braintree on Rails, it automatically makes customer creation/deletion/update scripts for your model, translating and syncing them with a Braintree customer profile.
6
+ Redline also includes a lightweight manual subscription billing system, if you desire that functionality.
7
+
8
+ === Please note: this repository may be a prerelease version, please read the README of the version you have installed.
9
+
10
+ == Heads up
11
+
12
+ === This software is delivered "as is" without warranty. As with any piece of code, please study it if you include it in your project, especially for such a critical component of your application.
13
+
14
+ While I believe the tests cover the code well, there may be certain cases that I have not expirienced or predicted. This is a manual billing script, so dreadful things may occur; please help out and add to the tests if you see, or god forbid expirience, any problems.
15
+
16
+ It is my intention to convert to gateway handled subscriptions, once Braintree adds this functionality to their gem.
6
17
 
7
18
  == Getting started
8
19
 
@@ -15,34 +26,46 @@ Run your install task:
15
26
 
16
27
  $ rake gems:install
17
28
 
18
- Configure Braintree normally in an initializer:
19
-
20
- Braintree::Configuration.environment = CONFIG[:braintree][:environment].to_sym
21
- Braintree::Configuration.merchant_id = CONFIG[:braintree][:merchant_id]
22
- Braintree::Configuration.public_key = CONFIG[:braintree][:public_key]
23
- Braintree::Configuration.private_key = CONFIG[:braintree][:private_key]
29
+ Configure Braintree normally in an initializer and add the following magic columns to a model of your choice (e.g, User):
24
30
 
25
- Add acts_as_braintree_customer and a column customer_id:integer to a model of your choice:
31
+ add_column :users, :customer_id, :integer # Required
32
+
33
+ add_column :users, :subscription_key, :string # Required if User has_a_subscription
34
+ add_column :users, :paid_until, :date # Required if User has_a_subscription
35
+ add_column :users, :trial_until, :date # Required if User has_a_subscription and trial length is greater than 0.days
26
36
 
27
- add_column :users, :customer_id, :integer
37
+ And the following definition calls:
28
38
 
29
- Class User < ActiveRecord::Base
30
- acts_as_braintree_customer
39
+ class User < ActiveRecord::Base
40
+
41
+ has_a_braintree_customer
42
+
43
+ has_a_subscription do
44
+ plans :mild => {:price => 0.00}, :medium => {:price => 5.00}, :spicy => {:price => 10.00}
45
+ end
46
+
31
47
  end
32
48
 
33
- == Example and configuration
49
+ And then run this daily:
34
50
 
35
- acts_as_braintree_customer takes an optional series of attributes that represent an attribute[] mapper
51
+ User.run_billing!
36
52
 
37
- class User < ActiveRecord::Base
38
- acts_as_braintree_customer :business_name => :company, :firstname => :first_name, :lastname => :last_name
39
- attr_accessible :firstname, :lastname, :website, :email, :business_name, :password, :password_confirmation
40
- end
53
+ == More advanced configuration
41
54
 
42
- james = User.find(1) #=> <User id: 1, firstname: "James", lastname: "Daniels", business_name: "MarginLeft, LLC", email: 'james@jamesdaniels.net', website: 'http://www.marginleft.com', crypted_password: "45fres8...", persistence_token: "4jsdf88...", ...>
43
- james.customer #=> <Braintree::Customer id: "9999999", first_name: "James", last_name: "Daniels", company: "MarginLeft, LLC", email: "james@jamesdaniels.net", website: "http://www.marginleft.com", ..., addresses: [], credit_cards: []>
55
+ If you don't like the default settings or your user needs a field mapping, you can override many assumptions RedLine makes:
44
56
 
45
- More to come.
57
+ class User < ActiveRecord::Base
58
+ has_a_braintree_customer do
59
+ attribute_map :firstname => :first_name, :lastname => :last_name
60
+ end
61
+
62
+ has_a_subscription do
63
+ plans :mild => {:price => 0.00}, :medium => {:price => 5.00}, :spicy => {:price => 10.00}
64
+ default_plan :medium
65
+ billing_frequency 30.days, :grace_period => 7.days
66
+ free_trial 30.days, :reminder => 7.days
67
+ end
68
+ end
46
69
 
47
70
  == Note on Patches/Pull Requests
48
71
 
data/Rakefile CHANGED
@@ -5,8 +5,8 @@ begin
5
5
  require 'jeweler'
6
6
  Jeweler::Tasks.new do |gem|
7
7
  gem.name = "redline"
8
- gem.summary = %Q{Braintree via redline: riding the rails}
9
- gem.description = %Q{Manual subscriptions via the Braintree gem}
8
+ gem.summary = %Q{Syncs your AR models with Braintree (Payment Gateway) and offers a lightweight reoccurring billing script}
9
+ gem.description = %Q{Syncs your AR models with Braintree (Payment Gateway) and offers a lightweight reoccurring billing script}
10
10
  gem.email = "james@marginleft.com"
11
11
  gem.homepage = "http://github.com/jamesdaniels/redline"
12
12
  gem.authors = ["James Daniels"]
@@ -21,6 +21,11 @@ rescue LoadError
21
21
  puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
22
22
  end
23
23
 
24
+ begin
25
+ require 'metric_fu'
26
+ rescue
27
+ end
28
+
24
29
  require 'spec/rake/spectask'
25
30
  Spec::Rake::SpecTask.new(:spec) do |spec|
26
31
  spec.libs << 'lib' << 'spec'
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.4
1
+ 0.2.0
data/lib/redline.rb CHANGED
@@ -2,12 +2,21 @@ require 'rubygems'
2
2
  require 'active_record'
3
3
 
4
4
  module RedLine
5
-
6
- autoload :Customer, 'redline/customer'
5
+
6
+ autoload :Customer, 'redline/customer/base'
7
+ autoload :Subscription, 'redline/subscription/base'
8
+ autoload :Billing, 'redline/billing/base'
9
+
10
+ def has_a_braintree_customer
11
+ include RedLine::Customer
12
+ yield if block_given?
13
+ set_default_customer_options
14
+ end
7
15
 
8
- def acts_as_braintree_customer(attribute_rewriting = {})
9
- send :include, RedLine::Customer
10
- send 'braintree_customer=', attribute_rewriting
16
+ def has_a_subscription
17
+ include RedLine::Subscription
18
+ yield if block_given?
19
+ set_default_subscription_options
11
20
  end
12
21
 
13
22
  end
@@ -0,0 +1,11 @@
1
+ module RedLine
2
+ module Billing
3
+ def run_billing!
4
+ # should clean this up, put it in a named scope
5
+ self.all.select{|u| u.next_due == Date.today}.each do |record|
6
+ record.pay rescue nil
7
+ record.save
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,18 @@
1
+ module RedLine
2
+ module Customer
3
+ autoload :Settings, 'redline/customer/settings'
4
+ autoload :InstanceMethods, 'redline/customer/instance'
5
+ def self.included(base)
6
+ base.class_eval do
7
+ extend ActiveSupport::Memoizable
8
+ extend RedLine::Customer::Settings
9
+ include InstanceMethods
10
+ memoize :customer
11
+ before_create :create_customer
12
+ before_update :update_customer
13
+ before_destroy :delete_customer
14
+ cattr_accessor :braintree_customer
15
+ end
16
+ end
17
+ end
18
+ end
@@ -1,16 +1,5 @@
1
1
  module RedLine
2
2
  module Customer
3
- def self.included(base)
4
- base.class_eval do
5
- send :extend, ActiveSupport::Memoizable
6
- send :include, InstanceMethods
7
- memoize :customer
8
- before_create :create_customer
9
- before_update :update_customer
10
- before_destroy :delete_customer
11
- cattr_accessor :braintree_customer
12
- end
13
- end
14
3
  module InstanceMethods
15
4
  def braintree_customer_attributes
16
5
  wanted_attributes = Braintree::Customer._create_signature.reject{|a| a.is_a? Hash}.reject{|a| a == :id}
@@ -0,0 +1,12 @@
1
+ module RedLine
2
+ module Customer
3
+ module Settings
4
+ def set_default_customer_options
5
+ attribute_map {} unless (send :braintree_customer)
6
+ end
7
+ def attribute_map(attributes = {})
8
+ self.braintree_customer = attributes
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,16 @@
1
+ module RedLine
2
+ module Subscription
3
+ autoload :Settings, 'redline/subscription/settings'
4
+ autoload :InstanceMethods, 'redline/subscription/instance'
5
+ def self.included(base)
6
+ base.class_eval do
7
+ extend ActiveSupport::Memoizable
8
+ extend RedLine::Subscription::Settings
9
+ extend RedLine::Billing
10
+ include InstanceMethods
11
+ before_create :set_plan
12
+ cattr_accessor :subscription_plans, :trial_settings, :billing_settings, :default_subscription_plan
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,42 @@
1
+ module RedLine
2
+ module Subscription
3
+ module InstanceMethods
4
+ def plan
5
+ self.class.subscription_plans[(subscription_key || self.class.default_subscription_plan).to_sym]
6
+ end
7
+ def plan=(plan)
8
+ if (plan.is_a?(String) || plan.is_a?(Symbol)) && self.class.subscription_plans.keys.include?(plan.to_sym)
9
+ self.subscription_key = plan
10
+ elsif plan.is_a?(Hash)
11
+ self.subscription_key = self.class.subscription_plans.index(plan)
12
+ end
13
+ self.paid_until = Date.today
14
+ self.trial_until ||= Date.today + self.class.trial_settings[:period] if trial?
15
+ end
16
+ def set_plan
17
+ self.subscription_key ||= self.class.default_subscription_plan
18
+ end
19
+ def pay
20
+ self.customer.sale!(:amount => ("%0.2f" % plan[:price]), :options => {:submit_for_settlement => true})
21
+ self.paid_until = Date.today + self.class.billing_settings[:period] - (past_due || 0)
22
+ end
23
+ def past_grace?
24
+ past_due && past_due >= self.class.billing_settings[:grace_period]
25
+ end
26
+ def paid_plan?
27
+ plan[:price] > 0
28
+ end
29
+ def trial?
30
+ self.class.trial_settings[:period] > 0.days
31
+ end
32
+ def next_due
33
+ paid_plan? && [trial? && trial_until || nil, paid_until, Date.today].compact.max
34
+ end
35
+ def past_due
36
+ due_date = [trial? && trial_until || nil, paid_until].compact.max
37
+ amount = (due_date && (Date.today - due_date) || 0).days
38
+ paid_plan? && amount > 0 && amount
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,24 @@
1
+ module RedLine
2
+ module Subscription
3
+ module Settings
4
+ def set_default_subscription_options
5
+ billing_frequency 30.days unless billing_settings
6
+ free_trial 30.days unless trial_settings
7
+ default_plan ((send :subscription_plans).to_a.sort_by{|a| a[1][:price]}.first.first) unless default_subscription_plan
8
+ end
9
+ def plans(subscriptions = {})
10
+ include RedLine::Subscription
11
+ self.subscription_plans = subscriptions
12
+ end
13
+ def free_trial(period, options = {})
14
+ self.trial_settings = {:period => period, :reminder => 5.days}.merge(options)
15
+ end
16
+ def billing_frequency(period, options = {})
17
+ self.billing_settings = {:period => period, :grace_period => 5.days}.merge(options)
18
+ end
19
+ def default_plan(subscription_plan)
20
+ self.default_subscription_plan = subscription_plan
21
+ end
22
+ end
23
+ end
24
+ end
data/redline.gemspec CHANGED
@@ -5,12 +5,12 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{redline}
8
- s.version = "0.1.4"
8
+ s.version = "0.2.0"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["James Daniels"]
12
- s.date = %q{2010-02-03}
13
- s.description = %q{Manual subscriptions via the Braintree gem}
12
+ s.date = %q{2010-02-10}
13
+ s.description = %q{Syncs your AR models with Braintree (Payment Gateway) and offers a lightweight reoccurring billing script}
14
14
  s.email = %q{james@marginleft.com}
15
15
  s.extra_rdoc_files = [
16
16
  "LICENSE",
@@ -24,25 +24,37 @@ Gem::Specification.new do |s|
24
24
  "Rakefile",
25
25
  "VERSION",
26
26
  "lib/redline.rb",
27
- "lib/redline/customer.rb",
27
+ "lib/redline/billing/base.rb",
28
+ "lib/redline/customer/base.rb",
29
+ "lib/redline/customer/instance.rb",
30
+ "lib/redline/customer/settings.rb",
31
+ "lib/redline/subscription/base.rb",
32
+ "lib/redline/subscription/instance.rb",
33
+ "lib/redline/subscription/settings.rb",
28
34
  "redline.gemspec",
35
+ "spec/billing_spec.rb",
36
+ "spec/customer_spec.rb",
29
37
  "spec/db/database.yml",
30
38
  "spec/db/models.rb",
31
39
  "spec/db/schema.rb",
32
40
  "spec/redline_spec.rb",
33
41
  "spec/spec.opts",
34
- "spec/spec_helper.rb"
42
+ "spec/spec_helper.rb",
43
+ "spec/subscription_spec.rb"
35
44
  ]
36
45
  s.homepage = %q{http://github.com/jamesdaniels/redline}
37
46
  s.rdoc_options = ["--charset=UTF-8"]
38
47
  s.require_paths = ["lib"]
39
48
  s.rubygems_version = %q{1.3.5}
40
- s.summary = %q{Braintree via redline: riding the rails}
49
+ s.summary = %q{Syncs your AR models with Braintree (Payment Gateway) and offers a lightweight reoccurring billing script}
41
50
  s.test_files = [
42
- "spec/db/models.rb",
51
+ "spec/billing_spec.rb",
52
+ "spec/customer_spec.rb",
53
+ "spec/db/models.rb",
43
54
  "spec/db/schema.rb",
44
55
  "spec/redline_spec.rb",
45
- "spec/spec_helper.rb"
56
+ "spec/spec_helper.rb",
57
+ "spec/subscription_spec.rb"
46
58
  ]
47
59
 
48
60
  if s.respond_to? :specification_version then
@@ -0,0 +1,121 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe User do
4
+
5
+ def valid_user
6
+ Braintree::Customer.stub!('_create_signature').and_return([:first_name, :last_name, :email])
7
+ @user ||= User.new(
8
+ :first_name => 'James',
9
+ :last_name => 'Daniels',
10
+ :email => 'james@marginleft.com',
11
+ :unused_attribute => 'unused'
12
+ )
13
+ end
14
+
15
+ def mock_customer
16
+ @customer ||= mock(Braintree::Customer, :id => 1)
17
+ end
18
+
19
+ it 'should run billing' do
20
+ Braintree::Customer.stub!('_create_signature').and_return([:first_name, :last_name, :email])
21
+ Braintree::Customer.should_receive('create!').with(valid_user.braintree_customer_attributes).exactly(3).and_return(mock_customer)
22
+ Braintree::Customer.should_receive('update!').twice.and_return(mock_customer)
23
+ users = User.subscription_plans.keys.map do |plan|
24
+ user = User.new(
25
+ :first_name => 'James',
26
+ :last_name => 'Daniels',
27
+ :email => 'james@marginleft.com',
28
+ :unused_attribute => 'unused'
29
+ )
30
+ user.plan = plan
31
+ if plan == :mild
32
+ user.should_receive(:next_due).and_return(nil)
33
+ user.should_not_receive(:pay)
34
+ else
35
+ user.should_receive(:next_due).and_return(Date.today)
36
+ user.should_receive(:pay).and_return(true)
37
+ end
38
+ user.save
39
+ user
40
+ end
41
+ User.stub!(:all).and_return(users)
42
+ User.run_billing!
43
+ end
44
+
45
+ it 'should run billing with failures' do
46
+ Braintree::Customer.stub!('_create_signature').and_return([:first_name, :last_name, :email])
47
+ Braintree::Customer.should_receive('create!').with(valid_user.braintree_customer_attributes).once.and_return(mock_customer)
48
+ Braintree::Customer.should_receive('update!').once.and_return(mock_customer)
49
+ user = User.new(
50
+ :first_name => 'James',
51
+ :last_name => 'Daniels',
52
+ :email => 'james@marginleft.com',
53
+ :unused_attribute => 'unused'
54
+ )
55
+ user.plan = :medium
56
+ user.paid_until = Date.today - 1
57
+ user.trial_until = Date.today - 1
58
+ user.stub!('customer').and_return(mock_customer)
59
+ user.stub!(:pay).and_raise(ArgumentError)
60
+ user.save
61
+ User.stub!(:all).and_return([user])
62
+ User.run_billing!
63
+ user.next_due.should eql(Date.today)
64
+ end
65
+
66
+ describe 'paid bill' do
67
+ (0).upto(5) do |x|
68
+ it "should push the due date 30 days when #{x} days past due" do
69
+ user = User.new
70
+ mock_customer = mock(Braintree::Customer, :id => 1)
71
+ user.stub!('customer').and_return(mock_customer)
72
+ mock_customer.should_receive('sale!').with({:amount=>"10.00", :options=>{:submit_for_settlement=>true}}).and_return(true)
73
+ user.plan = :spicy
74
+ user.paid_until = Date.today - x.days
75
+ user.trial_until = Date.today - 30.days
76
+ user.pay
77
+ user.next_due.should eql(Date.today + 30.days - x.days)
78
+ end
79
+ end
80
+ end
81
+
82
+ end
83
+
84
+ describe ComplexUser do
85
+
86
+ describe 'paid bill' do
87
+ (0).upto(5) do |x|
88
+ it "should push the due date 30 days when #{x} days past due" do
89
+ user = ComplexUser.new
90
+ mock_customer = mock(Braintree::Customer, :id => 1)
91
+ user.stub!('customer').and_return(mock_customer)
92
+ mock_customer.should_receive('sale!').with({:amount=>"5.00", :options=>{:submit_for_settlement=>true}}).and_return(true)
93
+ user.plan = :medium
94
+ user.paid_until = Date.today - x.days
95
+ user.trial_until = Date.today - 31.days
96
+ user.pay
97
+ user.next_due.should eql(Date.today + 31.days - x.days)
98
+ end
99
+ end
100
+ end
101
+
102
+ end
103
+
104
+ describe UserWithoutTrial do
105
+
106
+ describe 'paid bill' do
107
+ (0).upto(5) do |x|
108
+ it "should push the due date 30 days when #{x} days past due" do
109
+ user = UserWithoutTrial.new
110
+ mock_customer = mock(Braintree::Customer, :id => 1)
111
+ user.stub!('customer').and_return(mock_customer)
112
+ mock_customer.should_receive('sale!').with({:amount=>"10.00", :options=>{:submit_for_settlement=>true}}).and_return(true)
113
+ user.plan = :spicy
114
+ user.paid_until = Date.today - x.days
115
+ user.pay
116
+ user.next_due.should eql(Date.today + 30.days - x.days)
117
+ end
118
+ end
119
+ end
120
+
121
+ end
@@ -0,0 +1,76 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe User do
4
+
5
+ def valid_user
6
+ Braintree::Customer.stub!('_create_signature').and_return([:first_name, :last_name, :email])
7
+ @user ||= User.new(
8
+ :first_name => 'James',
9
+ :last_name => 'Daniels',
10
+ :email => 'james@marginleft.com',
11
+ :unused_attribute => 'unused'
12
+ )
13
+ end
14
+
15
+ def mock_customer
16
+ @customer ||= mock(Braintree::Customer, :id => 1)
17
+ end
18
+
19
+ it "should have a empty braintree_customer" do
20
+ User.braintree_customer.should eql({})
21
+ end
22
+
23
+ it "should inherit the instance methods" do
24
+ expected_methods = %w(braintree_customer_attributes customer create_customer update_customer delete_customer)
25
+ (User.instance_methods & expected_methods).sort.should eql(expected_methods.sort)
26
+ end
27
+ it "should have proper braintree attributes" do
28
+ valid_user.braintree_customer_attributes.should eql({:first_name=>"James", :last_name=>"Daniels", :email=>"james@marginleft.com"})
29
+ end
30
+ it "should fire Braintree::Customer.create!" do
31
+ Braintree::Customer.should_receive('create!').with(valid_user.braintree_customer_attributes).and_return(mock_customer)
32
+ valid_user.save!
33
+ valid_user.reload
34
+ valid_user.customer_id.should eql(mock_customer.id)
35
+ end
36
+ it "should fire Braintree::Customer.update!" do
37
+ Braintree::Customer.stub!('create!').and_return(mock_customer)
38
+ Braintree::Customer.should_receive('update!').with(mock_customer.id, valid_user.braintree_customer_attributes.merge(:email => 'james@jamesdaniels.net')).and_return(true)
39
+ valid_user.save!
40
+ valid_user.update_attributes(:email => 'james@jamesdaniels.net')
41
+ valid_user.reload
42
+ valid_user.customer_id.should eql(mock_customer.id)
43
+ end
44
+ it "should fire Braintree::Customer.delete" do
45
+ Braintree::Customer.stub!('create!').and_return(mock_customer)
46
+ Braintree::Customer.stub!('find').and_return(true)
47
+ Braintree::Customer.should_receive('delete').with(mock_customer.id).and_return(true)
48
+ valid_user.save!
49
+ valid_user.destroy
50
+ end
51
+ it "should fire Braintree::Customer.find" do
52
+ Braintree::Customer.stub!('create!').and_return(mock_customer)
53
+ Braintree::Customer.stub!('update!').and_return(true)
54
+ Braintree::Customer.should_receive('find').with(mock_customer.id).and_return(true)
55
+ valid_user.save!
56
+ valid_user.customer
57
+ end
58
+
59
+ end
60
+
61
+ describe ComplexUser do
62
+
63
+ it "should have a empty braintree_customer" do
64
+ ComplexUser.braintree_customer.should eql({:firstname=>:first_name, :lastname=>:last_name})
65
+ end
66
+ it "should have proper braintree attributes" do
67
+ Braintree::Customer.stub!('_create_signature').and_return([:first_name, :last_name, :email])
68
+ ComplexUser.new(
69
+ :firstname => 'James',
70
+ :lastname => 'Daniels',
71
+ :email => 'james@marginleft.com',
72
+ :unused_attribute => 'unused'
73
+ ).braintree_customer_attributes.should eql({:first_name=>"James", :last_name=>"Daniels", :email=>"james@marginleft.com"})
74
+ end
75
+
76
+ end
data/spec/db/models.rb CHANGED
@@ -1,6 +1,10 @@
1
1
  class User < ActiveRecord::Base
2
2
 
3
- acts_as_braintree_customer
3
+ has_a_braintree_customer
4
+
5
+ has_a_subscription do
6
+ plans :mild => {:price => 0.00}, :medium => {:price => 5.00}, :spicy => {:price => 10.00}
7
+ end
4
8
 
5
9
  validates_presence_of :first_name, :last_name, :unused_attribute, :email
6
10
 
@@ -8,8 +12,36 @@ end
8
12
 
9
13
  class ComplexUser < ActiveRecord::Base
10
14
 
11
- acts_as_braintree_customer :firstname => :first_name, :lastname => :last_name
15
+ has_a_braintree_customer do
16
+ attribute_map :firstname => :first_name, :lastname => :last_name
17
+ end
18
+
19
+ has_a_subscription do
20
+ plans :mild => {:price => 0.00}, :medium => {:price => 5.00}, :spicy => {:price => 10.00}
21
+ default_plan :medium
22
+ billing_frequency 31.days, :grace_period => 6.days
23
+ free_trial 31.days, :reminder => 6.days
24
+ end
12
25
 
13
26
  validates_presence_of :firstname, :lastname, :unused_attribute, :email
14
27
 
28
+ end
29
+
30
+ class UserWithoutTrial < ActiveRecord::Base
31
+
32
+ has_a_braintree_customer do
33
+ attribute_map :firstname => :first_name, :lastname => :last_name
34
+ end
35
+
36
+ has_a_subscription do
37
+ plans :mild => {:price => 0.00}, :medium => {:price => 5.00}, :spicy => {:price => 10.00}
38
+ free_trial 0.days
39
+ end
40
+
41
+ end
42
+
43
+ class UserWithoutSubscription < ActiveRecord::Base
44
+
45
+ has_a_braintree_customer
46
+
15
47
  end
data/spec/db/schema.rb CHANGED
@@ -1,16 +1,37 @@
1
1
  ActiveRecord::Schema.define(:version => 1) do
2
- create_table :users, :force => true do |t|
3
- t.column :first_name, :string
4
- t.column :last_name, :string
5
- t.column :email, :string
6
- t.column :unused_attribute, :string
7
- t.column :customer_id, :integer
8
- end
9
- create_table :complex_users, :force => true do |t|
10
- t.column :firstname, :string
11
- t.column :lastname, :string
12
- t.column :email, :string
13
- t.column :unused_attribute, :string
14
- t.column :customer_id, :integer
15
- end
2
+ create_table :users, :force => true do |t|
3
+ t.column :first_name, :string
4
+ t.column :last_name, :string
5
+ t.column :email, :string
6
+ t.column :unused_attribute, :string
7
+ t.column :customer_id, :integer
8
+ t.column :subscription_key, :string
9
+ t.column :trial_until, :date
10
+ t.column :paid_until, :date
11
+ end
12
+ create_table :complex_users, :force => true do |t|
13
+ t.column :firstname, :string
14
+ t.column :lastname, :string
15
+ t.column :email, :string
16
+ t.column :unused_attribute, :string
17
+ t.column :customer_id, :integer
18
+ t.column :subscription_key, :string
19
+ t.column :trial_until, :date
20
+ t.column :paid_until, :date
21
+ end
22
+ create_table :user_without_trials, :force => true do |t|
23
+ t.column :firstname, :string
24
+ t.column :lastname, :string
25
+ t.column :email, :string
26
+ t.column :unused_attribute, :string
27
+ t.column :customer_id, :integer
28
+ t.column :subscription_key, :string
29
+ t.column :paid_until, :date
30
+ end
31
+ create_table :user_without_subscription, :force => true do |t|
32
+ t.column :firstname, :string
33
+ t.column :lastname, :string
34
+ t.column :email, :string
35
+ t.column :customer_id, :integer
36
+ end
16
37
  end
data/spec/redline_spec.rb CHANGED
@@ -1,70 +1,42 @@
1
1
  require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
2
 
3
- describe User do
4
-
5
- def valid_user
6
- Braintree::Customer.stub!('_create_signature').and_return([:first_name, :last_name, :email])
7
- @user ||= User.new(
8
- :first_name => 'James',
9
- :last_name => 'Daniels',
10
- :email => 'james@marginleft.com',
11
- :unused_attribute => 'unused'
12
- )
13
- end
14
-
15
- def mock_customer
16
- @customer ||= mock(Braintree::Customer, :id => 1)
17
- end
18
-
19
- it "should have a empty braintree_customer" do
20
- User.braintree_customer.should eql({})
21
- end
22
- it "should inherit the instance methods" do
23
- expected_methods = %w(braintree_customer_attributes customer create_customer update_customer delete_customer)
24
- (User.instance_methods & expected_methods).sort.should eql(expected_methods.sort)
25
- end
26
- it "should have proper braintree attributes" do
27
- valid_user.braintree_customer_attributes.should eql({:first_name=>"James", :last_name=>"Daniels", :email=>"james@marginleft.com"})
28
- end
29
- it "should fire Braintree::Customer.create!" do
30
- Braintree::Customer.should_receive('create!').with(valid_user.braintree_customer_attributes).and_return(mock_customer)
31
- valid_user.save!
32
- valid_user.customer_id.should eql(mock_customer.id)
33
- end
34
- it "should fire Braintree::Customer.update!" do
35
- Braintree::Customer.stub!('create!').and_return(mock_customer)
36
- Braintree::Customer.should_receive('update!').with(mock_customer.id, valid_user.braintree_customer_attributes.merge(:email => 'james@jamesdaniels.net')).and_return(true)
37
- valid_user.save!
38
- valid_user.update_attributes(:email => 'james@jamesdaniels.net')
39
- valid_user.customer_id.should eql(mock_customer.id)
40
- end
41
- it "should fire Braintree::Customer.delete" do
42
- Braintree::Customer.stub!('create!').and_return(mock_customer)
43
- Braintree::Customer.stub!('find').and_return(true)
44
- Braintree::Customer.should_receive('delete').with(mock_customer.id).and_return(true)
45
- valid_user.save!
46
- valid_user.destroy
47
- end
48
- it "should fire Braintree::Customer.find" do
49
- Braintree::Customer.stub!('create!').and_return(mock_customer)
50
- Braintree::Customer.stub!('update!').and_return(true)
51
- Braintree::Customer.should_receive('find').with(mock_customer.id).and_return(true)
52
- valid_user.save!
53
- valid_user.customer
3
+ describe ActiveRecord do
4
+ it 'should extend our module' do
5
+ ActiveRecord::Base.is_a?(RedLine).should be_true
54
6
  end
55
7
  end
56
8
 
57
- describe ComplexUser do
58
- it "should have a empty braintree_customer" do
59
- ComplexUser.braintree_customer.should eql({:firstname=>:first_name, :lastname=>:last_name})
60
- end
61
- it "should have proper braintree attributes" do
62
- Braintree::Customer.stub!('_create_signature').and_return([:first_name, :last_name, :email])
63
- ComplexUser.new(
64
- :firstname => 'James',
65
- :lastname => 'Daniels',
66
- :email => 'james@marginleft.com',
67
- :unused_attribute => 'unused'
68
- ).braintree_customer_attributes.should eql({:first_name=>"James", :last_name=>"Daniels", :email=>"james@marginleft.com"})
9
+ [User, ComplexUser, UserWithoutTrial, UserWithoutSubscription].each do |klass|
10
+ describe klass do
11
+ it 'should extend our module' do
12
+ klass.is_a?(RedLine).should be_true
13
+ end
14
+ it 'should respond to a customer' do
15
+ klass.respond_to?(:has_a_braintree_customer).should be_true
16
+ end
17
+ it 'should respond to a subscription' do
18
+ klass.respond_to?(:has_a_subscription).should be_true
19
+ end
20
+ it 'should include RedLine::Customer' do
21
+ klass.include?(RedLine::Customer).should be_true
22
+ end
23
+ it 'should extend RedLine::Customer::Settings' do
24
+ klass.is_a?(RedLine::Customer::Settings).should be_true
25
+ end
26
+ it 'should include RedLine::Customer::InstanceMethods' do
27
+ klass.include?(RedLine::Customer::InstanceMethods).should be_true
28
+ end
29
+ it "should#{(klass == UserWithoutSubscription) && ' not' || ''} include RedLine::Subscription" do
30
+ klass.include?(RedLine::Subscription).should eql(klass != UserWithoutSubscription)
31
+ end
32
+ it "should#{(klass == UserWithoutSubscription) && ' not' || ''} extend RedLine::Subscription::Settings" do
33
+ klass.is_a?(RedLine::Subscription::Settings).should eql(klass != UserWithoutSubscription)
34
+ end
35
+ it "should#{(klass == UserWithoutSubscription) && ' not' || ''} include RedLine::Subscription::InstanceMethods" do
36
+ klass.include?(RedLine::Subscription::InstanceMethods).should eql(klass != UserWithoutSubscription)
37
+ end
38
+ it "should#{(klass == UserWithoutSubscription) && ' not' || ''} include RedLine::Billing" do
39
+ klass.is_a?(RedLine::Billing).should eql(klass != UserWithoutSubscription)
40
+ end
69
41
  end
70
42
  end
@@ -0,0 +1,237 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe User do
4
+
5
+ def valid_user
6
+ Braintree::Customer.stub!('_create_signature').and_return([:first_name, :last_name, :email])
7
+ @user ||= User.new(
8
+ :first_name => 'James',
9
+ :last_name => 'Daniels',
10
+ :email => 'james@marginleft.com',
11
+ :unused_attribute => 'unused'
12
+ )
13
+ end
14
+
15
+ def mock_customer
16
+ @customer ||= mock(Braintree::Customer, :id => 1)
17
+ end
18
+
19
+ it 'should have a default plan' do
20
+ Braintree::Customer.should_receive('create!').with(valid_user.braintree_customer_attributes).and_return(mock_customer)
21
+ user = valid_user
22
+ user.save
23
+ user.reload
24
+ user.subscription_key = :mild
25
+ end
26
+
27
+ it 'should have possible subscription plans' do
28
+ User.subscription_plans.should == {:mild => {:price => 0.00}, :medium => {:price => 5.00}, :spicy => {:price => 10.00}}
29
+ end
30
+ it 'should have a default plan' do
31
+ User.default_subscription_plan.should eql(:mild)
32
+ end
33
+ it 'should have billing settings' do
34
+ User.billing_settings.should == {:period => 30.days, :grace_period => 5.days}
35
+ end
36
+ it 'should have trial settings' do
37
+ User.trial_settings.should == {:period => 30.days, :reminder=>5.days}
38
+ end
39
+
40
+ describe 'should handle trial period' do
41
+ it 'sets trial' do
42
+ user = User.new
43
+ user.plan = :mild
44
+ user.trial_until.should eql(Date.today + 30.days)
45
+ end
46
+ it 'upgrades account within trial' do
47
+ user = User.new
48
+ user.plan = :medium
49
+ user.plan = :spicy
50
+ user.trial_until.should eql(Date.today + 30.days)
51
+ end
52
+ it 'upgrades account outside of trial' do
53
+ user = User.new
54
+ user.plan = :medium
55
+ user.trial_until = Date.today-1.days
56
+ user.plan = :spicy
57
+ user.trial_until.should eql(Date.today - 1.days)
58
+ end
59
+ it 'downgrades account within trial' do
60
+ user = User.new
61
+ user.plan = :spicy
62
+ user.plan = :medium
63
+ user.trial_until.should eql(Date.today + 30.days)
64
+ end
65
+ it 'downgrades account outside of trial' do
66
+ user = User.new
67
+ user.plan = :spicy
68
+ user.trial_until = Date.today-1.days
69
+ user.plan = :medium
70
+ user.trial_until.should eql(Date.today - 1.days)
71
+ end
72
+ end
73
+ end
74
+
75
+ describe ComplexUser do
76
+
77
+ it 'should have possible subscription plans' do
78
+ ComplexUser.subscription_plans.should == {:mild => {:price => 0.00}, :medium => {:price => 5.00}, :spicy => {:price => 10.00}}
79
+ end
80
+ it 'should have a default plan' do
81
+ ComplexUser.default_subscription_plan.should eql(:medium)
82
+ end
83
+ it 'should have billing settings' do
84
+ ComplexUser.billing_settings.should == {:period => 31.days, :grace_period => 6.days}
85
+ end
86
+ it 'should have trial settings' do
87
+ ComplexUser.trial_settings.should == {:period => 31.days, :reminder=>6.days}
88
+ end
89
+
90
+ it 'should hand plan as a hash' do
91
+ user = ComplexUser.new
92
+ user.plan = ComplexUser.subscription_plans.values.first
93
+ user.subscription_key.should eql(ComplexUser.subscription_plans.keys.first)
94
+ end
95
+
96
+ describe 'should handle next due correctly' do
97
+ it 'on free' do
98
+ user = ComplexUser.new
99
+ user.plan = :mild
100
+ user.next_due.should be_false
101
+ end
102
+ it 'on past due' do
103
+ user = ComplexUser.new
104
+ user.plan = :spicy
105
+ user.paid_until = Date.today - 6.days
106
+ user.trial_until = Date.today - 37.days
107
+ user.next_due.should eql(Date.today)
108
+ end
109
+ it 'on paid' do
110
+ user = ComplexUser.new
111
+ user.plan = :spicy
112
+ user.paid_until = Date.today + 6.days
113
+ user.trial_until = Date.today - 31.days + 6.days
114
+ user.next_due.should eql(Date.today+6.days)
115
+ end
116
+ it 'on upgrade' do
117
+ user = ComplexUser.new
118
+ user.plan = :mild
119
+ user.trial_until = Date.today - 30.days
120
+ user.paid_until = Date.today - 1.days
121
+ user.plan = :spicy
122
+ user.next_due.should eql(Date.today)
123
+ end
124
+ it 'on downgrade' do
125
+ user = ComplexUser.new
126
+ user.plan = :spicy
127
+ user.trial_until = Date.today - 30.days
128
+ user.paid_until = Date.today - 1.days
129
+ user.plan = :medium
130
+ user.next_due.should eql(Date.today)
131
+ end
132
+ end
133
+
134
+ describe 'should handle trial period' do
135
+ it 'sets trial' do
136
+ user = ComplexUser.new
137
+ user.plan = :mild
138
+ user.trial_until.should eql(Date.today + 31.days)
139
+ end
140
+ end
141
+
142
+ describe 'should handle grace period' do
143
+ it 'on not past due' do
144
+ user = ComplexUser.new
145
+ user.should_receive(:past_due).and_return(false)
146
+ user.past_grace?.should be_false
147
+ end
148
+ 0.upto(6) do |x|
149
+ it "on past due by #{x} days" do
150
+ user = ComplexUser.new
151
+ user.should_receive(:past_due).twice.and_return(x.days)
152
+ user.past_grace?.should eql(x > 5)
153
+ end
154
+ end
155
+ end
156
+
157
+ describe 'should handle due correctly' do
158
+ describe 'past due' do
159
+ it 'should work via past_due method' do
160
+ user = ComplexUser.new
161
+ user.plan = :spicy
162
+ user.paid_until = Date.today - 6.days
163
+ user.trial_until = Date.today - 37.days
164
+ user.past_due.should eql(6.days)
165
+ end
166
+ it 'should handle trials' do
167
+ user = ComplexUser.new
168
+ user.plan = :spicy
169
+ user.paid_until = nil
170
+ user.trial_until = Date.today - 1.days
171
+ user.past_due.should eql(1.days)
172
+ end
173
+ end
174
+ describe 'not overdue' do
175
+ it 'should work via past_due method' do
176
+ user = ComplexUser.new
177
+ user.plan = :spicy
178
+ user.paid_until = Date.today + 6.days
179
+ user.trial_until = Date.today - 31.days + 6.days
180
+ user.past_due.should be_false
181
+ end
182
+ it 'should handle end of trial' do
183
+ user = ComplexUser.new
184
+ user.plan = :spicy
185
+ user.paid_until = nil
186
+ user.trial_until = nil
187
+ user.past_due.should be_false
188
+ end
189
+ it 'should handle trials' do
190
+ user = ComplexUser.new
191
+ user.plan = :spicy
192
+ user.paid_until = nil
193
+ user.trial_until = Date.today + 1.days
194
+ user.past_due.should be_false
195
+ end
196
+ end
197
+ describe 'free account' do
198
+ it 'should work via past_due trial' do
199
+ user = ComplexUser.new
200
+ user.plan = :mild
201
+ user.paid_until = Date.today - 6.days
202
+ user.trial_until = Date.today - 37.days
203
+ user.past_due.should be_false
204
+ end
205
+ it 'should handle past_due trials' do
206
+ user = ComplexUser.new
207
+ user.plan = :mild
208
+ user.paid_until = nil
209
+ user.trial_until = Date.today - 1.days
210
+ user.past_due.should be_false
211
+ end
212
+ it 'should work via trial' do
213
+ user = ComplexUser.new
214
+ user.plan = :mild
215
+ user.paid_until = Date.today + 6.days
216
+ user.trial_until = Date.today - 31.days + 6.days
217
+ user.past_due.should be_false
218
+ end
219
+ it 'should handle trials' do
220
+ user = ComplexUser.new
221
+ user.plan = :mild
222
+ user.paid_until = nil
223
+ user.trial_until = Date.today + 1.days
224
+ user.past_due.should be_false
225
+ end
226
+ end
227
+ end
228
+
229
+ end
230
+
231
+ describe UserWithoutTrial do
232
+
233
+ it 'has no trial' do
234
+ UserWithoutTrial.trial_settings.should == {:period=>0.days, :reminder=>5.days}
235
+ end
236
+
237
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: redline
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.4
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - James Daniels
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2010-02-03 00:00:00 -05:00
12
+ date: 2010-02-10 00:00:00 -05:00
13
13
  default_executable:
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
@@ -52,7 +52,7 @@ dependencies:
52
52
  - !ruby/object:Gem::Version
53
53
  version: "0"
54
54
  version:
55
- description: Manual subscriptions via the Braintree gem
55
+ description: Syncs your AR models with Braintree (Payment Gateway) and offers a lightweight reoccurring billing script
56
56
  email: james@marginleft.com
57
57
  executables: []
58
58
 
@@ -69,14 +69,23 @@ files:
69
69
  - Rakefile
70
70
  - VERSION
71
71
  - lib/redline.rb
72
- - lib/redline/customer.rb
72
+ - lib/redline/billing/base.rb
73
+ - lib/redline/customer/base.rb
74
+ - lib/redline/customer/instance.rb
75
+ - lib/redline/customer/settings.rb
76
+ - lib/redline/subscription/base.rb
77
+ - lib/redline/subscription/instance.rb
78
+ - lib/redline/subscription/settings.rb
73
79
  - redline.gemspec
80
+ - spec/billing_spec.rb
81
+ - spec/customer_spec.rb
74
82
  - spec/db/database.yml
75
83
  - spec/db/models.rb
76
84
  - spec/db/schema.rb
77
85
  - spec/redline_spec.rb
78
86
  - spec/spec.opts
79
87
  - spec/spec_helper.rb
88
+ - spec/subscription_spec.rb
80
89
  has_rdoc: true
81
90
  homepage: http://github.com/jamesdaniels/redline
82
91
  licenses: []
@@ -104,9 +113,12 @@ rubyforge_project:
104
113
  rubygems_version: 1.3.5
105
114
  signing_key:
106
115
  specification_version: 3
107
- summary: "Braintree via redline: riding the rails"
116
+ summary: Syncs your AR models with Braintree (Payment Gateway) and offers a lightweight reoccurring billing script
108
117
  test_files:
118
+ - spec/billing_spec.rb
119
+ - spec/customer_spec.rb
109
120
  - spec/db/models.rb
110
121
  - spec/db/schema.rb
111
122
  - spec/redline_spec.rb
112
123
  - spec/spec_helper.rb
124
+ - spec/subscription_spec.rb