billingly 0.0.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.
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: []