billingly 0.0.0 → 0.1.1

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.
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