billingly 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (31) hide show
  1. data/MIT-LICENSE +20 -0
  2. data/README.rdoc +3 -0
  3. data/Rakefile +24 -0
  4. data/app/controllers/billingly/subscriptions_controller.rb +48 -0
  5. data/app/mailers/billingly_mailer.rb +20 -0
  6. data/app/models/billingly/customer.rb +116 -0
  7. data/app/models/billingly/invoice.rb +120 -0
  8. data/app/models/billingly/invoice_item.rb +4 -0
  9. data/app/models/billingly/ledger_entry.rb +16 -0
  10. data/app/models/billingly/one_time_charge.rb +5 -0
  11. data/app/models/billingly/payment.rb +19 -0
  12. data/app/models/billingly/plan.rb +5 -0
  13. data/app/models/billingly/receipt.rb +9 -0
  14. data/app/models/billingly/subscription.rb +82 -0
  15. data/app/views/billingly/subscriptions/create.html.erb +1 -0
  16. data/app/views/billingly/subscriptions/index.html.erb +2 -0
  17. data/app/views/billingly/subscriptions/new.html.erb +1 -0
  18. data/app/views/billingly_mailer/overdue_notification.html.erb +21 -0
  19. data/app/views/billingly_mailer/paid_notification.html.erb +10 -0
  20. data/app/views/billingly_mailer/pending_notification.html.erb +17 -0
  21. data/app/views/billingly_mailer/pending_notification.plain.erb +17 -0
  22. data/app/views/billingly_mailer/pending_notification.text.erb +17 -0
  23. data/config/locales/en.yml +23 -0
  24. data/lib/billingly.rb +4 -0
  25. data/lib/billingly/engine.rb +45 -0
  26. data/lib/billingly/rails/routes.rb +12 -0
  27. data/lib/billingly/version.rb +3 -0
  28. data/lib/generators/billingly_migration_generator.rb +20 -0
  29. data/lib/generators/templates/create_billingly_tables.rb +71 -0
  30. data/lib/tasks/billingly_tasks.rake +21 -0
  31. metadata +143 -0
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2012 YOURNAME
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,3 @@
1
+ = RailsSubscriptionBilling
2
+ test
3
+ This project rocks and uses MIT-LICENSE.
data/Rakefile ADDED
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env rake
2
+ begin
3
+ require 'bundler/setup'
4
+ rescue LoadError
5
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
6
+ end
7
+ begin
8
+ require 'rdoc/task'
9
+ rescue LoadError
10
+ require 'rdoc/rdoc'
11
+ require 'rake/rdoctask'
12
+ RDoc::Task = Rake::RDocTask
13
+ end
14
+
15
+ RDoc::Task.new(:rdoc) do |rdoc|
16
+ rdoc.rdoc_dir = 'rdoc'
17
+ rdoc.title = 'RailsSubscriptionBilling'
18
+ rdoc.options << '--line-numbers'
19
+ rdoc.rdoc_files.include('README.rdoc')
20
+ rdoc.rdoc_files.include('lib/**/*.rb')
21
+ end
22
+
23
+ Bundler::GemHelper.install_tasks
24
+
@@ -0,0 +1,48 @@
1
+ # This controller takes care of managing subscriptions.
2
+ class Billingly::SubscriptionsController < ::ApplicationController
3
+ before_filter :requires_customer, only: [:index, :reactivate]
4
+ before_filter :requires_active_customer, except: [:index, :reactivate]
5
+
6
+ # Index shows the current subscription to customers while they are active.
7
+ # It's also the page that prompts them to reactivate their account when deactivated.
8
+ # It's likely the only reachable page for deactivated customers.
9
+ def index
10
+ @subscription = current_customer.active_subscription
11
+ redirect_to(action: :new) unless @subscription
12
+ end
13
+
14
+ # Should let customers choose a plan to subscribe to, wheter they are subscribing
15
+ # for the first time or upgrading their plan.
16
+ def new
17
+ @plans = Billingly::Plan.all
18
+ end
19
+
20
+ # Subscribe the customer to a plan, or change his current plan.
21
+ def create
22
+ plan = Billingly::Plan.find(params[:plan_id])
23
+ current_customer.subscribe_to_plan(plan)
24
+ on_subscription_success
25
+ end
26
+
27
+ # Action to reactivate an account for a customer that left voluntarily and does
28
+ # not owe any money to us.
29
+ # Their account will be reactivated to their old subscription plan immediately.
30
+ # They can change plans afterwards.
31
+ def reactivate
32
+ return render nothing: true, status: 403 unless current_customer.reactivate
33
+ on_reactivation_success
34
+ end
35
+
36
+ # When a subscription is sucessful this callback is triggered.
37
+ # Host applications should override it by subclassing this subscriptionscontroller,
38
+ # and include their own behaviour, and for example grant the privileges associated
39
+ # to the subscription plan.
40
+ def on_subscription_success
41
+ redirect_to(action: :index)
42
+ end
43
+
44
+ def on_reactivation_success
45
+ on_subscription_success
46
+ end
47
+
48
+ end
@@ -0,0 +1,20 @@
1
+ class BillinglyMailer < ActionMailer::Base
2
+ default from: 'example@example.com'
3
+
4
+ def pending_notification(invoice)
5
+ @invoice = invoice
6
+ @cash = invoice.customer.ledger[:cash]
7
+ mail(to: invoice.customer.email, subject: I18n.t('billingly.your_invoice_is_available'))
8
+ end
9
+
10
+ def overdue_notification(invoice)
11
+ @invoice = invoice
12
+ @cash = invoice.customer.ledger[:cash]
13
+ mail(to: invoice.customer.email, subject: I18n.t('billingly.your_account_was_suspended'))
14
+ end
15
+
16
+ def paid_notification(invoice)
17
+ @invoice = invoice
18
+ mail(to: invoice.customer.email, subject: I18n.t('billingly.payment_receipt'))
19
+ end
20
+ end
@@ -0,0 +1,116 @@
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
116
+ end
@@ -0,0 +1,120 @@
1
+ module Billingly
2
+ class Invoice < ActiveRecord::Base
3
+ belongs_to :subscription
4
+ belongs_to :customer
5
+ belongs_to :receipt
6
+ has_many :ledger_entries
7
+ attr_accessible :customer, :amount, :due_on, :period_start, :period_end
8
+
9
+ def paid?
10
+ not receipt.nil?
11
+ end
12
+
13
+ def deleted?
14
+ not deleted_on.nil?
15
+ end
16
+
17
+ # Settle an invoice by moving money from the cash balance into an expense,
18
+ # generating a receipt and marking the invoice as paid.
19
+ def charge
20
+ return if paid?
21
+ return unless deleted_on.nil?
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
31
+ end
32
+
33
+ # When a subscription terminates, it's last invoice gets truncated so that it does
34
+ # not extend beyond the duration of the subscription.
35
+ def truncate
36
+ return if Time.now > self.period_end
37
+ old_amount = self.amount
38
+ new_amount = if self.period_start < Time.now
39
+ whole_period = self.period_end - self.period_start
40
+ used_period = Time.now - self.period_start
41
+ self.period_end = Time.now
42
+ used_period.round * old_amount / whole_period.round
43
+ else
44
+ self.deleted_on = Time.now
45
+ 0
46
+ end
47
+ self.amount = BigDecimal.new(new_amount.round(2).to_s)
48
+ if paid?
49
+ reimburse = (old_amount - self.amount).round(2)
50
+ extra = {invoice: self, subscription: subscription}
51
+ customer.add_to_ledger(reimburse, :cash, extra)
52
+ customer.add_to_ledger(-(reimburse), :spent, extra)
53
+ end
54
+ save!
55
+ self.charge
56
+ return self
57
+ end
58
+
59
+ # Charges all invoices that can be charged from the existing customer cash balances
60
+ def self.charge_all(collection = self)
61
+ collection.where(deleted_on: nil, receipt_id: nil).order('period_start').each do |invoice|
62
+ invoice.charge
63
+ end
64
+ end
65
+
66
+ # Send the email notifying that this invoice is due soon and should be paid.
67
+ 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)
70
+ .each do |invoice|
71
+ invoice.notify_pending
72
+ end
73
+ end
74
+
75
+ def notify_pending
76
+ return unless notified_pending_on.nil?
77
+ return if paid?
78
+ return if deleted?
79
+ return if due_on > Billingly::Subscription::GRACE_PERIOD.from_now
80
+ BillinglyMailer.pending_notification(self).deliver!
81
+ update_attribute(:notified_pending_on, Time.now)
82
+ end
83
+
84
+ # Send the email notifying that this invoice being overdue and the subscription
85
+ # being cancelled
86
+ def self.notify_all_overdue
87
+ where('due_on <= ?', Time.now)
88
+ .where(deleted_on: nil, receipt_id: nil, notified_overdue_on: nil)
89
+ .each do |invoice|
90
+ invoice.notify_overdue
91
+ end
92
+ end
93
+
94
+ def notify_overdue
95
+ return unless notified_overdue_on.nil?
96
+ return if paid?
97
+ return if deleted?
98
+ return if due_on > Time.now
99
+ BillinglyMailer.overdue_notification(self).deliver!
100
+ update_attribute(:notified_overdue_on, Time.now)
101
+ end
102
+
103
+ # Notifies that the invoice has been charged successfully.
104
+ # Send the email notifying that this invoice being overdue and the subscription
105
+ # being cancelled
106
+ def self.notify_all_paid
107
+ where('receipt_id is not null')
108
+ .where(deleted_on: nil, notified_paid_on: nil).each do |invoice|
109
+ invoice.notify_paid
110
+ end
111
+ end
112
+ def notify_paid
113
+ return unless paid?
114
+ return unless notified_paid_on.nil?
115
+ return if deleted?
116
+ BillinglyMailer.paid_notification(self).deliver!
117
+ update_attribute(:notified_paid_on, Time.now)
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,4 @@
1
+ module Billingly
2
+ class InvoiceItem < ActiveRecord::Base
3
+ end
4
+ end
@@ -0,0 +1,16 @@
1
+ module Billingly
2
+ class LedgerEntry < ActiveRecord::Base
3
+ belongs_to :customer
4
+ belongs_to :invoice
5
+ belongs_to :payment
6
+ belongs_to :receipt
7
+ belongs_to :subscription
8
+
9
+ validates :amount, presence: true
10
+ validates :customer, presence: true
11
+ validates :account, presence: true, inclusion: %w(paid cash spent)
12
+
13
+ attr_accessible :customer, :account, :invoice, :payment, :receipt, :subscription, :amount
14
+
15
+ end
16
+ end
@@ -0,0 +1,5 @@
1
+ module Billingly
2
+ class OneTimeCharge < ActiveRecord::Base
3
+ attr_accessible :charge_on, :amount, :description
4
+ end
5
+ end
@@ -0,0 +1,19 @@
1
+ # Processes payments
2
+ module Billingly
3
+ class Payment < ActiveRecord::Base
4
+ belongs_to :customer
5
+ has_many :ledger_entries
6
+ attr_accessible :amount, :customer
7
+
8
+ # Process a new payment done by a customer.
9
+ # Payments can be credited at any point and they are bound to increase
10
+ # a customer's balance. Payments are not mapped 1 to 1 with invoices,
11
+ # instead, invoices are deemed as paid whenever the customer's balance
12
+ # is enough to cover them.
13
+ def self.credit_for(customer, amount)
14
+ create!(amount: amount, customer: customer).tap do |payment|
15
+ customer.add_to_ledger(amount, :cash, :paid, payment: payment)
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,5 @@
1
+ module Billingly
2
+ class Plan < ActiveRecord::Base
3
+ attr_accessible :name, :description, :periodicity, :amount, :payable_upfront
4
+ end
5
+ end
@@ -0,0 +1,9 @@
1
+ module Billingly
2
+ class Receipt < ActiveRecord::Base
3
+ belongs_to :customer
4
+ has_one :invoice
5
+ has_many :ledger_entries
6
+
7
+ attr_accessible :customer, :paid_on
8
+ end
9
+ end
@@ -0,0 +1,82 @@
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
82
+ end
@@ -0,0 +1 @@
1
+ placeholder!
@@ -0,0 +1,2 @@
1
+ <%= @subscription.inspect %>
2
+
@@ -0,0 +1 @@
1
+ placeholder!
@@ -0,0 +1,21 @@
1
+ <%=t 'billingly.make_sure_to_pay' %>
2
+
3
+ <%=t 'billingly.you_can_reactivate_your_account_by_visiting' %> http://payments_page
4
+
5
+ ----- <%=t 'billingly.your_overdue_invoice_is_detailed_below' %> -----
6
+
7
+ <%=t 'billingly.invoice_number' %>: <%= '%.10i' % @invoice.id %>
8
+
9
+ <%=t 'billingly.due_date' %>: <%= @invoice.due_on.to_date %>
10
+
11
+ <%= "#{t('billingly.covering_from')} #{@invoice.period_start.to_date} #{t('billingly.until')} #{@invoice.period_end.to_date}" %>
12
+
13
+ <%=t 'billingly.detail' %>
14
+ <%= '-' * 70 %>
15
+ <%= '%-55s%15s' % [@invoice.subscription.description, "$#{@invoice.amount} USD"] %>
16
+ <% if @cash > 0 %>
17
+ <%= '%-55s%15s' % [t('billingly.balance_in_your_favour'), "-$#{@cash} USD"] %>
18
+ <% end %>
19
+
20
+ <%= '%-55s%15s' % [t('billingly.you_should_pay'), "$#{@invoice.amount - @cash} USD"] %>
21
+
@@ -0,0 +1,10 @@
1
+ <%=t 'billingly.receipt_greeting' %>:
2
+
3
+ <%=t 'billingly.receipt_text' %>
4
+
5
+ <%= '-' * 70 %>
6
+ <%=t 'billingly.invoice_number' %>: <%= '%.10i' % @invoice.id %>
7
+ <%=t 'billingly.paid_on' %>: <%= @invoice.receipt.paid_on.to_date %>
8
+ <%=t 'billingly.plan' %>: <%= @invoice.subscription.description %>
9
+ <%=t 'billingly.amount' %>: $<%= @invoice.amount %> USD
10
+
@@ -0,0 +1,17 @@
1
+ <%=t 'billingly.invoice_number' %>: <%= '%.10i' % @invoice.id %>
2
+
3
+ <%=t 'billingly.due_date' %>: <%= @invoice.due_on.to_date %>
4
+
5
+ <%= "#{t('billingly.covering_from')} #{@invoice.period_start.to_date} #{t('billingly.until')} #{@invoice.period_end.to_date}" %>
6
+
7
+ <%=t 'billingly.detail' %>
8
+ <%= '-' * 70 %>
9
+ <%= '%-55s%15s' % [@invoice.subscription.description, "$#{@invoice.amount} USD"] %>
10
+ <% if @cash > 0 %>
11
+ <%= '%-55s%15s' % [t('billingly.balance_in_your_favour'), "-$#{@cash} USD"] %>
12
+ <% end %>
13
+
14
+ <%= '%-55s%15s' % [t('billingly.you_should_pay'), "$#{@invoice.amount - @cash} USD"] %>
15
+
16
+ <%=t 'billingly.you_can_pay_this_invoice_by_visiting' %> http://payments_page
17
+
@@ -0,0 +1,17 @@
1
+ <%=t 'billingly.invoice_number' %>: <%= '%.10i' % @invoice.id %>
2
+
3
+ <%=t 'billingly.due_date' %>: <%= @invoice.due_on.to_date %>
4
+
5
+ <%= "#{t('billingly.covering_from')} #{@invoice.period_start.to_date} #{t('billingly.until')} #{@invoice.period_end.to_date}" %>
6
+
7
+ <%=t 'billingly.detail' %>
8
+ <%= '-' * 70 %>
9
+ <%= '%-55s%15s' % [@invoice.subscription.description, "$#{@invoice.amount} USD"] %>
10
+ <% if @cash > 0 %>
11
+ <%= '%-55s%15s' % [t('billingly.balance_in_your_favour'), "-$#{@cash} USD"] %>
12
+ <% end %>
13
+
14
+ <%= '%-55s%15s' % [t('billingly.you_should_pay'), "$#{@invoice.amount - @cash} USD"] %>
15
+
16
+ <%=t 'billingly.you_can_pay_this_invoice_by_visiting' %> http://payments_page
17
+
@@ -0,0 +1,17 @@
1
+ <%=t 'billingly.invoice_number' %>: <%= '%.10i' % @invoice.id %>
2
+
3
+ <%=t 'billingly.due_date' %>: <%= @invoice.due_on.to_date %>
4
+
5
+ <%= "#{t('billingly.covering_from')} #{@invoice.period_start.to_date} #{t('billingly.until')} #{@invoice.period_end.to_date}" %>
6
+
7
+ <%=t 'billingly.detail' %>
8
+ <%= '-' * 70 %>
9
+ <%= '%-55s%15s' % [@invoice.subscription.description, "$#{@invoice.amount} USD"] %>
10
+ <% if @cash > 0 %>
11
+ <%= '%-55s%15s' % [t('billingly.balance_in_your_favour'), "-$#{@cash} USD"] %>
12
+ <% end %>
13
+
14
+ <%= '%-55s%15s' % [t('billingly.you_should_pay'), "$#{@invoice.amount - @cash} USD"] %>
15
+
16
+ <%=t 'billingly.you_can_pay_this_invoice_by_visiting' %> http://payments_page
17
+
@@ -0,0 +1,23 @@
1
+ en:
2
+ billingly:
3
+ your_account_was_suspended: Your account was suspended
4
+ your_invoice_is_available: Your invoice is available
5
+ payment_receipt: Payment Receipt
6
+ invoice_number: Invoice Number
7
+ covering_from: For period starting on
8
+ until: until
9
+ due_date: Due Date
10
+ detail: Detail
11
+ balance_in_your_favour: Balance in your favour
12
+ you_should_pay: You should pay
13
+ you_can_pay_this_invoice_by_visiting: You can pay this invoice by visiting
14
+ you_can_reactivate_your_account_by_visiting: You can reactivate your account by visiting
15
+ make_sure_to_pay:
16
+ Your account was suspended. Make sure to pay your overdue invoices to reactivate it.
17
+ your_overdue_invoice_is_detailed_below: Your overdue invoice is detailed below
18
+ receipt_greeting: Dear customer
19
+ receipt_text:
20
+ "This is a receipt for your subscription.\nThis is only a receipt, no payment is due.\nThank you for your business!"
21
+ paid_on: Paid on
22
+ plan: Plan
23
+ amount: Amount
data/lib/billingly.rb ADDED
@@ -0,0 +1,4 @@
1
+ module Billingly
2
+ end
3
+ require 'billingly/engine'
4
+ require 'billingly/rails/routes'
@@ -0,0 +1,45 @@
1
+ module Billingly
2
+ def self.table_name_prefix
3
+ 'billingly_'
4
+ end
5
+
6
+ class Engine < Rails::Engine
7
+
8
+ # Extends the ApplicationController with all the
9
+ # billingly before_filters and helper methods
10
+ initializer 'billingly.app_controller' do |app|
11
+ ActiveSupport.on_load(:action_controller) do
12
+ class_eval do
13
+ def current_customer
14
+ nil
15
+ end
16
+
17
+ def requires_customer
18
+ on_empty_customer if current_customer.nil?
19
+ end
20
+
21
+ # This method is call on a before filter when a customer was required
22
+ # but none was found. It's reccommended that this method redirects to
23
+ # a login url or out of the site.
24
+ def on_empty_customer
25
+ redirect_to(root_path)
26
+ end
27
+
28
+ # This before filter should apply to all actions that require an active
29
+ # customer. Usually this would mean all your non-public pages.
30
+ # The billingly controllers already apply this before filter, you should
31
+ # use it in your own controllers too.
32
+ def requires_active_customer
33
+ if requires_customer.nil? && current_customer.deactivated?
34
+ redirect_to(controller: 'billingly/subscriptions', action: 'index')
35
+ end
36
+ end
37
+ end
38
+
39
+ helper_method :current_customer
40
+ end
41
+ end
42
+ end
43
+ end
44
+
45
+
@@ -0,0 +1,12 @@
1
+ class ActionDispatch::Routing::Mapper
2
+ def add_billingly_routes(skope=nil, controller='billingly/subscriptions')
3
+ route = lambda do
4
+ resources :subscriptions, controller: controller do
5
+ collection do
6
+ post :reactivate
7
+ end
8
+ end
9
+ end
10
+ if skope then scope(skope, as: skope, &route) else route.call end
11
+ end
12
+ end
@@ -0,0 +1,3 @@
1
+ module Billingly
2
+ VERSION = "0.0.0"
3
+ end
@@ -0,0 +1,20 @@
1
+ require 'rails/generators/migration'
2
+ require 'rails/generators'
3
+
4
+ class BillinglyMigrationGenerator < Rails::Generators::Base
5
+ include Rails::Generators::Migration
6
+
7
+ def create_billingly_migration
8
+ migration_template 'create_billingly_tables.rb', "db/migrate/create_billingly_tables.rb"
9
+ end
10
+
11
+ private
12
+
13
+ def source_paths
14
+ [File.expand_path("../templates", __FILE__)]
15
+ end
16
+
17
+ def self.next_migration_number(path)
18
+ Time.now.utc.strftime("%Y%m%d%H%M%S")
19
+ end
20
+ end
@@ -0,0 +1,71 @@
1
+ class CreateBillinglyTables < ActiveRecord::Migration
2
+ def self.up
3
+ create_table :billingly_customers do |t|
4
+ t.datetime 'deactivated_since'
5
+ t.string 'email', null: false
6
+ end
7
+
8
+ create_table :billingly_invoices do |t|
9
+ t.references :customer, null: false
10
+ t.references :receipt
11
+ t.references :subscription
12
+ t.decimal 'amount', precision: 11, scale: 2, default: 0.0, null: false
13
+ t.datetime 'due_on', null: false
14
+ t.datetime 'period_start', null: false
15
+ t.datetime 'period_end', null: false
16
+ t.datetime 'deleted_on'
17
+ t.datetime 'notified_pending_on'
18
+ t.datetime 'notified_overdue_on'
19
+ t.datetime 'notified_paid_on'
20
+ t.text 'comment'
21
+ t.timestamps
22
+ end
23
+
24
+ create_table :billingly_payments do |t|
25
+ t.references :customer, null: false
26
+ t.decimal 'amount', precision: 11, scale: 2, default: 0.0, null: false
27
+ end
28
+
29
+ create_table :billingly_receipts do |t|
30
+ t.references :customer, null: false
31
+ t.datetime 'paid_on'
32
+ t.timestamps
33
+ end
34
+
35
+ create_table :billingly_ledger_entries do |t|
36
+ t.references :customer, null: false
37
+ t.string :account, null: false
38
+ t.decimal 'amount', precision: 11, scale: 2, default: 0.0, null: false
39
+ t.references :subscription
40
+ t.references :invoice
41
+ t.references :payment
42
+ t.references :receipt
43
+ t.timestamps
44
+ end
45
+
46
+ create_table :billingly_subscriptions do |t|
47
+ t.references :customer, null: false
48
+ t.string 'description', null: false
49
+ t.datetime 'subscribed_on', null: false
50
+ t.string 'periodicity', null: false
51
+ t.decimal 'amount', precision: 11, scale: 2, default: 0.0, null: false
52
+ t.datetime 'unsubscribed_on'
53
+ t.boolean 'payable_upfront', null: false, default: false
54
+ t.timestamps
55
+ end
56
+
57
+ create_table :billingly_plans do |t|
58
+ t.string 'name' # Pro 50
59
+ t.string 'description' # 50GB for 9,99 a month.
60
+ t.string 'periodicity'
61
+ t.decimal 'amount', precision: 11, scale: 2, default: 0.0, null: false # 9.99
62
+ t.boolean 'payable_upfront' # true
63
+ t.timestamps
64
+ end
65
+
66
+ end
67
+
68
+ def self.down
69
+ end
70
+ end
71
+
@@ -0,0 +1,21 @@
1
+ desc """
2
+ Run all periodic billing tasks like generating new invoices,
3
+ deactivating debtors, and emailing.
4
+ You can run it as often as you want.
5
+ """
6
+ namespace :billingly do
7
+ task all: :environment do
8
+ puts 'Generating invoices'
9
+ Billingly::Subscription.generate_next_invoices
10
+ puts 'Charging invoices if possible'
11
+ Billingly::Invoice.charge_all
12
+ puts 'Deactivating debtors'
13
+ Billingly::Customer.deactivate_all_debtors
14
+ puts 'Sending payment receipts'
15
+ Billingly::Invoice.notify_all_paid
16
+ puts 'Notifying pending invoices'
17
+ Billingly::Invoice.notify_all_pending
18
+ puts 'Notifying deactivated debtors'
19
+ Billingly::Invoice.notify_all_overdue
20
+ end
21
+ end
metadata ADDED
@@ -0,0 +1,143 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: billingly
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Nubis
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-09-11 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rails
16
+ requirement: &70238504847900 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: '3.2'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *70238504847900
25
+ - !ruby/object:Gem::Dependency
26
+ name: validates_email_format_of
27
+ requirement: &70238504847400 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: *70238504847400
36
+ - !ruby/object:Gem::Dependency
37
+ name: timecop
38
+ requirement: &70238504846940 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ type: :development
45
+ prerelease: false
46
+ version_requirements: *70238504846940
47
+ - !ruby/object:Gem::Dependency
48
+ name: sqlite3
49
+ requirement: &70238504846520 !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ! '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ type: :development
56
+ prerelease: false
57
+ version_requirements: *70238504846520
58
+ - !ruby/object:Gem::Dependency
59
+ name: rspec-rails
60
+ requirement: &70238504846080 !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ! '>='
64
+ - !ruby/object:Gem::Version
65
+ version: '0'
66
+ type: :development
67
+ prerelease: false
68
+ version_requirements: *70238504846080
69
+ - !ruby/object:Gem::Dependency
70
+ name: factory_girl_rails
71
+ requirement: &70238504845640 !ruby/object:Gem::Requirement
72
+ none: false
73
+ requirements:
74
+ - - ! '>='
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: *70238504845640
80
+ description: Engine for subscriptions billing - still alpha - contact me if you want
81
+ to use it
82
+ email:
83
+ - nubis@woobiz.com.ar
84
+ executables: []
85
+ extensions: []
86
+ extra_rdoc_files: []
87
+ files:
88
+ - app/controllers/billingly/subscriptions_controller.rb
89
+ - app/mailers/billingly_mailer.rb
90
+ - app/models/billingly/customer.rb
91
+ - app/models/billingly/invoice.rb
92
+ - app/models/billingly/invoice_item.rb
93
+ - app/models/billingly/ledger_entry.rb
94
+ - app/models/billingly/one_time_charge.rb
95
+ - app/models/billingly/payment.rb
96
+ - app/models/billingly/plan.rb
97
+ - app/models/billingly/receipt.rb
98
+ - app/models/billingly/subscription.rb
99
+ - app/views/billingly/subscriptions/create.html.erb
100
+ - app/views/billingly/subscriptions/index.html.erb
101
+ - app/views/billingly/subscriptions/new.html.erb
102
+ - app/views/billingly_mailer/overdue_notification.html.erb
103
+ - app/views/billingly_mailer/paid_notification.html.erb
104
+ - app/views/billingly_mailer/pending_notification.html.erb
105
+ - app/views/billingly_mailer/pending_notification.plain.erb
106
+ - app/views/billingly_mailer/pending_notification.text.erb
107
+ - config/locales/en.yml
108
+ - lib/billingly/engine.rb
109
+ - lib/billingly/rails/routes.rb
110
+ - lib/billingly/version.rb
111
+ - lib/billingly.rb
112
+ - lib/generators/billingly_migration_generator.rb
113
+ - lib/generators/templates/create_billingly_tables.rb
114
+ - lib/tasks/billingly_tasks.rake
115
+ - MIT-LICENSE
116
+ - Rakefile
117
+ - README.rdoc
118
+ homepage: ''
119
+ licenses: []
120
+ post_install_message:
121
+ rdoc_options: []
122
+ require_paths:
123
+ - lib
124
+ required_ruby_version: !ruby/object:Gem::Requirement
125
+ none: false
126
+ requirements:
127
+ - - ! '>='
128
+ - !ruby/object:Gem::Version
129
+ version: '0'
130
+ required_rubygems_version: !ruby/object:Gem::Requirement
131
+ none: false
132
+ requirements:
133
+ - - ! '>='
134
+ - !ruby/object:Gem::Version
135
+ version: '0'
136
+ requirements: []
137
+ rubyforge_project:
138
+ rubygems_version: 1.8.15
139
+ signing_key:
140
+ specification_version: 3
141
+ summary: Engine for subscriptions billing - still alpha - contact me if you want to
142
+ use it
143
+ test_files: []