billingly 0.0.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (32) hide show
  1. data/README.md +36 -0
  2. data/TUTORIAL.rdoc +158 -0
  3. data/app/controllers/billingly/subscriptions_controller.rb +33 -13
  4. data/app/models/billingly/base_customer.rb +306 -0
  5. data/app/models/billingly/base_plan.rb +16 -0
  6. data/app/models/billingly/base_subscription.rb +118 -0
  7. data/app/models/billingly/customer.rb +2 -115
  8. data/app/models/billingly/invoice.rb +20 -21
  9. data/app/models/billingly/journal_entry.rb +15 -0
  10. data/app/models/billingly/ledger_entry.rb +2 -3
  11. data/app/models/billingly/payment.rb +1 -1
  12. data/app/models/billingly/plan.rb +2 -4
  13. data/app/models/billingly/subscription.rb +2 -81
  14. data/app/views/billingly/subscriptions/_current_subscription.html.haml +22 -0
  15. data/app/views/billingly/subscriptions/_deactivation_notice.html.haml +12 -0
  16. data/app/views/billingly/subscriptions/_invoice_details.html.haml +9 -0
  17. data/app/views/billingly/subscriptions/_invoices.html.haml +30 -0
  18. data/app/views/billingly/subscriptions/_plans.html.haml +30 -0
  19. data/app/views/billingly/subscriptions/index.html.haml +10 -0
  20. data/app/views/billingly/subscriptions/invoice.html.haml +8 -0
  21. data/app/views/billingly/subscriptions/new.html.erb +1 -1
  22. data/app/views/billingly_mailer/paid_notification.html.erb +1 -1
  23. data/lib/billingly/engine.rb +1 -3
  24. data/lib/billingly/rails/routes.rb +3 -1
  25. data/lib/billingly/version.rb +1 -1
  26. data/lib/generators/billingly_mailer_views_generator.rb +9 -0
  27. data/lib/generators/billingly_views_generator.rb +9 -0
  28. data/lib/generators/templates/create_billingly_tables.rb +11 -16
  29. data/lib/tasks/billingly_tasks.rake +2 -0
  30. metadata +46 -22
  31. data/README.rdoc +0 -3
  32. data/app/views/billingly/subscriptions/index.html.erb +0 -2
@@ -0,0 +1,16 @@
1
+ module Billingly
2
+ require 'has_duration'
3
+ class BasePlan < ActiveRecord::Base
4
+ self.abstract_class = true
5
+ self.table_name = 'billingly_plans'
6
+
7
+ attr_accessible :name, :plan_code, :description, :periodicity,
8
+ :amount, :payable_upfront, :grace_period
9
+
10
+ has_duration :grace_period
11
+ validates :grace_period, presence: true
12
+
13
+ has_duration :periodicity
14
+ validates :periodicity, presence: true
15
+ end
16
+ end
@@ -0,0 +1,118 @@
1
+ module Billingly
2
+ require 'has_duration'
3
+
4
+ # A customer will always have at least one subscription to your application.
5
+ # Everytime there is a change in a {Customer customer's} subscription, the current one
6
+ # is terminated immediately and a new one is created.
7
+ #
8
+ # For example, changing a {Plan} consists on terminating the current
9
+ # subscription and creating a new one for the new plan.
10
+ # Also, a new subscription is created when {Customer customers} reactivate their accounts
11
+ # after being deactivated.
12
+ #
13
+ # The most recent subscription is the one currently being charged for, unless the customer
14
+ # is deactivated at the moment, in which case the last subscription should not be considered
15
+ # to be active.
16
+ class BaseSubscription < ActiveRecord::Base
17
+ self.abstract_class = true
18
+ self.table_name = 'billingly_subscriptions'
19
+
20
+ has_many :ledger_entries, foreign_key: :subscription_id
21
+
22
+ # The date in which this subscription started.
23
+ # This subscription's first invoice will have it's period_start date
24
+ # matching the date in which the subscription started.
25
+ # @property subscribed_on
26
+ # @return [DateTime]
27
+ validates :subscribed_on, presence: true
28
+
29
+ # The grace period we use when calculating an invoices due date.
30
+ # If a subscription is payable_upfront, then the customer effectively owes us
31
+ # since the day in which a given period starts.
32
+ # If a subscription is payable on 'due-month', then the customer effectively
33
+ # owes us money since the date in which a given period ended.
34
+ # When we invoice for a given period we will set the due_date a few days
35
+ # ahead of the date in which the debt was made effective, we call this
36
+ # a grace_period.
37
+ # @property grace_period
38
+ # @return [ActiveSupport::Duration] It's what you get by doing '1.month', '10.days', etc.
39
+ has_duration :grace_period
40
+ validates :grace_period, presence: true
41
+
42
+ # A subscription can be a free trial, ending on the date stored in `is_trial_expiring_on'.
43
+ # Free trial subscriptions don't have {Invoice invoices}. The {Customer customer} is required
44
+ # to subscribe to a non-trial plan before the trial subscription expires.
45
+ #
46
+ # The {#trial?} convenience method returns wheter this subscription is a trial or not.
47
+ #
48
+ # @property is_trial_expiring_on
49
+ # @return [DateTime]
50
+
51
+ # When a subscription was started from a {Plan} a reference to the plan is saved.
52
+ # Although, all the plan's fields are denormalized in this subscription.
53
+ #
54
+ # If the subscription was not started from a plan, then this will be nil.
55
+ #
56
+ # @property plan
57
+ # @return [Billingly::Plan, nil]
58
+ belongs_to :plan
59
+
60
+ # (see #is_trial_expiring_on)
61
+ # @property trial?
62
+ # @return [Boolean]
63
+ def trial?
64
+ not is_trial_expiring_on.nil?
65
+ end
66
+
67
+ # Invoices will be generated before their due_date, as soon as possible,
68
+ # but not sooner than GENERATE_AHEAD days.
69
+ GENERATE_AHEAD = 3.days
70
+
71
+ belongs_to :customer
72
+ has_many :invoices, foreign_key: :subscription_id
73
+
74
+ has_duration :periodicity
75
+ validates :periodicity, presence: true
76
+
77
+ # The invoice generation process should run frequently, at least on a daily basis.
78
+ # It will create invoices some time before they are due, to give customers a chance
79
+ # to pay and settle them.
80
+ # This method is idempotent, if an upcoming invoice for a subscription already exists,
81
+ # it does not create yet another one.
82
+ def generate_next_invoice
83
+ return if terminated?
84
+ return if trial?
85
+ from = invoices.empty? ? subscribed_on : invoices.last.period_end
86
+ to = from + periodicity
87
+ due_on = (payable_upfront ? from : to) + grace_period
88
+ return if GENERATE_AHEAD.from_now < from
89
+
90
+ invoice = invoices.create!(customer: customer, amount: amount,
91
+ due_on: due_on, period_start: from, period_end: to)
92
+ invoice.charge
93
+ return invoice
94
+ end
95
+
96
+ # Terminates this subscription, it could be either because we deactivate a debtor
97
+ # or because the customer decided to end his subscription on his own terms.
98
+ def terminate
99
+ return if terminated?
100
+ update_attribute(:unsubscribed_on, Time.now)
101
+ invoices.last.truncate unless trial?
102
+ return self
103
+ end
104
+
105
+ def terminated?
106
+ not unsubscribed_on.nil?
107
+ end
108
+
109
+ # This class method is called from a cron job, it creates invoices for all the subscriptions
110
+ # that still need their invoice created.
111
+ # TODO: This goes through all the active subscriptions, make it smarter so that the batch job runs quicker.
112
+ def self.generate_next_invoices
113
+ where(is_trial_expiring_on: nil, unsubscribed_on: nil).each do |subscription|
114
+ subscription.generate_next_invoice
115
+ end
116
+ end
117
+ end
118
+ end
@@ -1,116 +1,3 @@
1
- # A Customer is the main entity of Billingly:
2
- # * Customers are subscribed to plans
3
- # * Customers are charged for one-time expenses.
4
- # * Payments are received on a Customer's behalf and credited to their account.
5
- # * Invoices are generated periodically calculating charges a Customer incurred in.
6
- # * Receipts are sent to Customers when their invoices are paid.
7
-
8
- require 'validates_email_format_of'
9
-
10
- module Billingly
11
- class Customer < ActiveRecord::Base
12
- has_many :subscriptions
13
- has_many :one_time_charges
14
- has_many :invoices
15
- has_many :ledger_entries
16
-
17
- attr_accessible :email
18
- validates_email_format_of :email
19
-
20
- # Customers subscribe to the service and perform periodic payments to continue using it.
21
- # We offer common plans stating how much and how often they should pay, also, if the
22
- # payment is to be done at the beginning or end of the period (upfront or due-month)
23
- # Every customer can potentially get a special deal, but we offer common
24
- # deals as 'plans' from which a proper subscription is created.
25
- def subscribe_to_plan(plan)
26
- subscriptions.last.terminate if subscriptions.last
27
-
28
- subscriptions.build.tap do |new|
29
- [:payable_upfront, :description, :periodicity, :amount].each do |k|
30
- new[k] = plan[k]
31
- end
32
- new.subscribed_on = Time.now
33
- new.save!
34
- new.generate_next_invoice
35
- end
36
- end
37
-
38
- # Returns the actual subscription of the customer. while working with the
39
- # customer API a customer should only have 1 active subscription at a time.
40
- def active_subscription
41
- subscriptions.last
42
- end
43
-
44
- # Every transaction is registered in the journal from where a general ledger can
45
- # be retrieved.
46
- # Due to silly rounding errors on sqlite we need to convert decimals to float and then to
47
- # decimals again. :S
48
- def ledger
49
- Hash.new(0.0).tap do |all|
50
- ledger_entries.group_by(&:account).collect do |account, entries|
51
- values = entries.collect(&:amount).collect(&:to_f)
52
- all[account.to_sym] = values.inject(0.0) do |sum,item|
53
- (BigDecimal.new(sum.to_s) + BigDecimal.new(item.to_s)).to_f
54
- end
55
- end
56
- end
57
- end
58
-
59
- # Shortcut for adding ledger_entries for a particular customer.
60
- def add_to_ledger(amount, *accounts, extra)
61
- accounts = [] if accounts.nil?
62
- unless extra.is_a?(Hash)
63
- accounts << extra
64
- extra = {}
65
- end
66
-
67
- accounts.each do |account|
68
- ledger_entries.create!(extra.merge(amount: amount, account: account.to_s))
69
- end
70
- end
71
-
72
- # This class method is run periodically deactivate all customers who have overdue invoices.
73
- def self.deactivate_all_debtors
74
- debtors.where(deactivated_since: nil).all.each{|debtor| debtor.deactivate }
75
- end
76
-
77
- def self.debtors
78
- joins(:invoices).readonly(false)
79
- .where("#{Billingly::Invoice.table_name}.due_on < ?", Time.now)
80
- .where(billingly_invoices: {deleted_on: nil, receipt_id: nil})
81
- end
82
-
83
- # Credits a payment for a customer, settling invoices if possible.
84
- def credit_payment(amount)
85
- Billingly::Payment.credit_for(self, amount)
86
- Billingly::Invoice.charge_all(self.invoices)
87
- reactivate
88
- end
89
-
90
- def deactivate
91
- return if deactivated?
92
- active_subscription.terminate
93
- update_attribute(:deactivated_since, Time.now)
94
- return self
95
- end
96
-
97
- # Reactivates a customer that was deactivated when missed a previous payment.
98
- # The new subscription is parametrized the same as the old one. The old subscription
99
- # is terminated.
100
- def reactivate(new_plan = active_subscription)
101
- return unless deactivated?
102
- return if debtor?
103
- update_attribute(:deactivated_since, nil)
104
- subscribe_to_plan(new_plan)
105
- return self
106
- end
107
-
108
- def deactivated?
109
- not deactivated_since.nil?
110
- end
111
-
112
- def debtor?
113
- not self.class.debtors.find_by_id(self.id).nil?
114
- end
115
- end
1
+ # (see Billingly::BaseCustomer)
2
+ class Billingly::Customer < Billingly::BaseCustomer
116
3
  end
@@ -2,12 +2,11 @@ module Billingly
2
2
  class Invoice < ActiveRecord::Base
3
3
  belongs_to :subscription
4
4
  belongs_to :customer
5
- belongs_to :receipt
6
- has_many :ledger_entries
5
+ has_many :journal_entries
7
6
  attr_accessible :customer, :amount, :due_on, :period_start, :period_end
8
7
 
9
8
  def paid?
10
- not receipt.nil?
9
+ not paid_on.nil?
11
10
  end
12
11
 
13
12
  def deleted?
@@ -15,19 +14,19 @@ module Billingly
15
14
  end
16
15
 
17
16
  # Settle an invoice by moving money from the cash balance into an expense,
18
- # generating a receipt and marking the invoice as paid.
17
+ # after charged the invoice becomes paid.
19
18
  def charge
20
19
  return if paid?
21
20
  return unless deleted_on.nil?
21
+ return if period_start > Time.now
22
22
  return if customer.ledger[:cash] < amount
23
-
24
- receipt = create_receipt!(customer: customer, paid_on: Time.now)
25
- extra = {receipt: receipt, subscription: subscription}
26
- customer.add_to_ledger(-(amount), :cash, extra)
27
- customer.add_to_ledger(amount, :spent, extra)
28
-
29
- save! # save receipt
30
- return receipt
23
+
24
+ update_attribute(:paid_on, Time.now)
25
+ extra = {invoice: self, subscription: subscription}
26
+ customer.add_to_journal(-(amount), :cash, extra)
27
+ customer.add_to_journal(amount, :spent, extra)
28
+
29
+ return self
31
30
  end
32
31
 
33
32
  # When a subscription terminates, it's last invoice gets truncated so that it does
@@ -48,8 +47,8 @@ module Billingly
48
47
  if paid?
49
48
  reimburse = (old_amount - self.amount).round(2)
50
49
  extra = {invoice: self, subscription: subscription}
51
- customer.add_to_ledger(reimburse, :cash, extra)
52
- customer.add_to_ledger(-(reimburse), :spent, extra)
50
+ customer.add_to_journal(reimburse, :cash, extra)
51
+ customer.add_to_journal(-(reimburse), :spent, extra)
53
52
  end
54
53
  save!
55
54
  self.charge
@@ -58,15 +57,14 @@ module Billingly
58
57
 
59
58
  # Charges all invoices that can be charged from the existing customer cash balances
60
59
  def self.charge_all(collection = self)
61
- collection.where(deleted_on: nil, receipt_id: nil).order('period_start').each do |invoice|
60
+ collection.where(deleted_on: nil, paid_on: nil).order('period_start').each do |invoice|
62
61
  invoice.charge
63
62
  end
64
63
  end
65
64
 
66
- # Send the email notifying that this invoice is due soon and should be paid.
65
+ # This method is called by Billingly's recurring task to notify all pending invoices.
67
66
  def self.notify_all_pending
68
- where('due_on < ?', Billingly::Subscription::GRACE_PERIOD.from_now)
69
- .where(deleted_on: nil, receipt_id: nil, notified_pending_on: nil)
67
+ where(deleted_on: nil, paid_on: nil, notified_pending_on: nil)
70
68
  .each do |invoice|
71
69
  invoice.notify_pending
72
70
  end
@@ -76,7 +74,7 @@ module Billingly
76
74
  return unless notified_pending_on.nil?
77
75
  return if paid?
78
76
  return if deleted?
79
- return if due_on > Billingly::Subscription::GRACE_PERIOD.from_now
77
+ return if due_on > subscription.grace_period.from_now
80
78
  BillinglyMailer.pending_notification(self).deliver!
81
79
  update_attribute(:notified_pending_on, Time.now)
82
80
  end
@@ -85,7 +83,7 @@ module Billingly
85
83
  # being cancelled
86
84
  def self.notify_all_overdue
87
85
  where('due_on <= ?', Time.now)
88
- .where(deleted_on: nil, receipt_id: nil, notified_overdue_on: nil)
86
+ .where(deleted_on: nil, paid_on: nil, notified_overdue_on: nil)
89
87
  .each do |invoice|
90
88
  invoice.notify_overdue
91
89
  end
@@ -104,11 +102,12 @@ module Billingly
104
102
  # Send the email notifying that this invoice being overdue and the subscription
105
103
  # being cancelled
106
104
  def self.notify_all_paid
107
- where('receipt_id is not null')
105
+ where('paid_on is not null')
108
106
  .where(deleted_on: nil, notified_paid_on: nil).each do |invoice|
109
107
  invoice.notify_paid
110
108
  end
111
109
  end
110
+
112
111
  def notify_paid
113
112
  return unless paid?
114
113
  return unless notified_paid_on.nil?
@@ -0,0 +1,15 @@
1
+ module Billingly
2
+ class JournalEntry < ActiveRecord::Base
3
+ belongs_to :customer
4
+ belongs_to :invoice
5
+ belongs_to :payment
6
+ belongs_to :subscription
7
+
8
+ validates :amount, presence: true
9
+ validates :customer, presence: true
10
+ validates :account, presence: true, inclusion: %w(paid cash spent)
11
+
12
+ attr_accessible :customer, :account, :invoice, :payment, :subscription, :amount
13
+
14
+ end
15
+ end
@@ -1,16 +1,15 @@
1
1
  module Billingly
2
- class LedgerEntry < ActiveRecord::Base
2
+ class JournalEntry < ActiveRecord::Base
3
3
  belongs_to :customer
4
4
  belongs_to :invoice
5
5
  belongs_to :payment
6
- belongs_to :receipt
7
6
  belongs_to :subscription
8
7
 
9
8
  validates :amount, presence: true
10
9
  validates :customer, presence: true
11
10
  validates :account, presence: true, inclusion: %w(paid cash spent)
12
11
 
13
- attr_accessible :customer, :account, :invoice, :payment, :receipt, :subscription, :amount
12
+ attr_accessible :customer, :account, :invoice, :payment, :subscription, :amount
14
13
 
15
14
  end
16
15
  end
@@ -12,7 +12,7 @@ module Billingly
12
12
  # is enough to cover them.
13
13
  def self.credit_for(customer, amount)
14
14
  create!(amount: amount, customer: customer).tap do |payment|
15
- customer.add_to_ledger(amount, :cash, :paid, payment: payment)
15
+ customer.add_to_journal(amount, :cash, :paid, payment: payment)
16
16
  end
17
17
  end
18
18
  end
@@ -1,5 +1,3 @@
1
- module Billingly
2
- class Plan < ActiveRecord::Base
3
- attr_accessible :name, :description, :periodicity, :amount, :payable_upfront
4
- end
1
+ # @see BasePlan
2
+ class Billingly::Plan < Billingly::BasePlan
5
3
  end
@@ -1,82 +1,3 @@
1
- module Billingly
2
- class Subscription < ActiveRecord::Base
3
- has_many :ledger_entries
4
- # If a subscription is payable_upfront, then the customer effectively owes us
5
- # since the day in which a given period starts.
6
- # If a subscription is payable on 'due-month', then the customer effectively
7
- # owes us money since the date in which a given period ended.
8
- # When we invoice for a given period we will set the due_date a few days
9
- # ahead of the date in which the debt was made effective, we call this
10
- # a GRACE_PERIOD.
11
- GRACE_PERIOD = 10.days
12
-
13
- # Invoices will be generated before their due_date, as soon as possible,
14
- # but not sooner than GENERATE_AHEAD days.
15
- GENERATE_AHEAD = 3.days
16
-
17
- # Subscriptions are to be charged periodically. Their periodicity is
18
- # stored semantically on the database, but we want to convert it
19
- # to actual ruby time ranges to do date arithmetic.
20
- PERIODICITIES = {'monthly' => 1.month, 'yearly' => 1.year}
21
-
22
- belongs_to :customer
23
- has_many :invoices
24
-
25
- validates :periodicity, inclusion: PERIODICITIES.keys
26
-
27
- # The periodicity can be set using a symbol, for convenience.
28
- # It's still a string under the hood.
29
- def periodicity=(value)
30
- self[:periodicity] = value.to_s if value
31
- end
32
-
33
- def period_size
34
- case periodicity
35
- when 'monthly' then 1.month
36
- when 'yearly' then 1.year
37
- else
38
- raise ArgumentError.new 'Cannot get period size without periodicity'
39
- end
40
- end
41
-
42
- # The invoice generation process should run frequently, at least on a daily basis.
43
- # It will create invoices some time before they are due, to give customers a chance
44
- # to pay and settle them.
45
- # This method is idempotent, if an upcoming invoice for a subscription already exists,
46
- # it does not create yet another one.
47
- def generate_next_invoice
48
- return if terminated?
49
- from = invoices.empty? ? subscribed_on : invoices.last.period_end
50
- to = from + period_size
51
- due_on = (payable_upfront ? from : to) + GRACE_PERIOD
52
- return if GENERATE_AHEAD.from_now < from
53
-
54
- invoice = invoices.create!(customer: customer, amount: amount,
55
- due_on: due_on, period_start: from, period_end: to)
56
- invoice.charge
57
- return invoice
58
- end
59
-
60
- # Terminates this subscription, it could be either because we deactivate a debtor
61
- # or because the customer decided to end his subscription on his own terms.
62
- def terminate
63
- return if terminated?
64
- update_attribute(:unsubscribed_on, Time.now)
65
- invoices.last.truncate
66
- return self
67
- end
68
-
69
- def terminated?
70
- not unsubscribed_on.nil?
71
- end
72
-
73
- # This class method is called from a cron job, it creates invoices for all the subscriptions
74
- # that still need their invoice created.
75
- # TODO: This goes through all the active subscriptions, make it smarter so that the batch job runs quicker.
76
- def self.generate_next_invoices
77
- where(unsubscribed_on: nil).each do |subscription|
78
- subscription.generate_next_invoice
79
- end
80
- end
81
- end
1
+ # @see BaseSubscription
2
+ class Billingly::Subscription < Billingly::BaseSubscription
82
3
  end