redline 0.1.4 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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