billingly 0.0.0 → 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +36 -0
- data/TUTORIAL.rdoc +158 -0
- data/app/controllers/billingly/subscriptions_controller.rb +33 -13
- data/app/models/billingly/base_customer.rb +306 -0
- data/app/models/billingly/base_plan.rb +16 -0
- data/app/models/billingly/base_subscription.rb +118 -0
- data/app/models/billingly/customer.rb +2 -115
- data/app/models/billingly/invoice.rb +20 -21
- data/app/models/billingly/journal_entry.rb +15 -0
- data/app/models/billingly/ledger_entry.rb +2 -3
- data/app/models/billingly/payment.rb +1 -1
- data/app/models/billingly/plan.rb +2 -4
- data/app/models/billingly/subscription.rb +2 -81
- data/app/views/billingly/subscriptions/_current_subscription.html.haml +22 -0
- data/app/views/billingly/subscriptions/_deactivation_notice.html.haml +12 -0
- data/app/views/billingly/subscriptions/_invoice_details.html.haml +9 -0
- data/app/views/billingly/subscriptions/_invoices.html.haml +30 -0
- data/app/views/billingly/subscriptions/_plans.html.haml +30 -0
- data/app/views/billingly/subscriptions/index.html.haml +10 -0
- data/app/views/billingly/subscriptions/invoice.html.haml +8 -0
- data/app/views/billingly/subscriptions/new.html.erb +1 -1
- data/app/views/billingly_mailer/paid_notification.html.erb +1 -1
- data/lib/billingly/engine.rb +1 -3
- data/lib/billingly/rails/routes.rb +3 -1
- data/lib/billingly/version.rb +1 -1
- data/lib/generators/billingly_mailer_views_generator.rb +9 -0
- data/lib/generators/billingly_views_generator.rb +9 -0
- data/lib/generators/templates/create_billingly_tables.rb +11 -16
- data/lib/tasks/billingly_tasks.rake +2 -0
- metadata +46 -22
- data/README.rdoc +0 -3
- data/app/views/billingly/subscriptions/index.html.erb +0 -2
data/README.md
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
# Billingly
|
2
|
+
|
3
|
+
Billingly is a rails 3 engine that manages paid subscriptions and free trials to your web application.
|
4
|
+
|
5
|
+
Billingly can:
|
6
|
+
|
7
|
+
* Subscribe customers to your service:
|
8
|
+
* Subscriptions can have an arbitrary length: A year, a month, 90 days ...
|
9
|
+
* You can request payments upfront or after the subscription period is over.
|
10
|
+
|
11
|
+
* Offer standarized subscription plans for self-service sign ups.
|
12
|
+
|
13
|
+
* Offer special deals on a per-customer basis.
|
14
|
+
|
15
|
+
* Invoice your customers automatically and send receipts once they pay.
|
16
|
+
|
17
|
+
* Notify customers about due dates.
|
18
|
+
|
19
|
+
* Restrict access to debtors, and let them back in once they pay their debt.
|
20
|
+
|
21
|
+
* Let customers upgrade or downgrade to another plan. Prorating and reimbursing in case there were any upfront payments.
|
22
|
+
|
23
|
+
* Let you give arbitrary bonuses, vouchers and gifts credited to your customer's account.
|
24
|
+
|
25
|
+
* Offer a trial period before you require people to become paying customers.
|
26
|
+
|
27
|
+
Billingly does not receive payments directly (from Paypal, or Worldpay for example). However, you can use [ActiveMerchant](http://activemerchant.org) for handling payment notifications from third party services, and easily hookup Billingly to credit the payment into your customer's account. Billingly will take care of all the rest.
|
28
|
+
|
29
|
+
# Getting Started
|
30
|
+
* Read the [Getting Started Guide](http://rubydoc.info/github/nubis/billingly/master/file/TUTORIAL.rdoc).
|
31
|
+
* Check out the [Demo App](http://billing.ly).
|
32
|
+
* Explore the [Docs](http://rubydoc.info/github/nubis/billingly/master/frames/file/README.md).
|
33
|
+
|
34
|
+
# License
|
35
|
+
MIT License. Copyright 2012 Nubis (nubis@woobiz.com.ar)
|
36
|
+
|
data/TUTORIAL.rdoc
ADDED
@@ -0,0 +1,158 @@
|
|
1
|
+
# @markup markdown
|
2
|
+
|
3
|
+
# Getting Started
|
4
|
+
|
5
|
+
## Preface
|
6
|
+
|
7
|
+
At its core, billingly has a {Billingly::Customer} class. A {Billingly::Customer} has a {Billingly::Subscription} to your service, for which she will receive {Billingly::Invoice Invoices} regularly via email.
|
8
|
+
Billingly keeps a balance for each one of your {Customer customers}, whenever you receive a payment you should {Billingly::BaseCustomer#credit_payment credit the payment} into their account.
|
9
|
+
|
10
|
+
When a payment is credited, billingly will try to settle outstanding invoices, always starting from the oldest one. If the customer's balance is not enough to cover the last pending invoice then nothing will happen. Once an invoice is settled the customer will be sent a receipt via email.
|
11
|
+
|
12
|
+
Invoices have a due date, customers will notified about pending invoices before they are overdue. When a customer misses a payment, billingly will immediately deactivate his account and notify via email about the deactivation.
|
13
|
+
|
14
|
+
Deactivated customers will be redirected forcefully to their subscription page where they can see all their invoices. Once they pay their overdue invoices their account is re-activated.
|
15
|
+
|
16
|
+
You may change a customers subscription at any point. Under the hood, changing a subscription consist on terminating the current subscription and creating a new one. Any invoices paid for the terminated subscription will be automatically prorated and the remaining balance will be credited back into the customer's account.
|
17
|
+
|
18
|
+
Each customer can have a completely custom Subscription, but you will usually want people to sign up to a predefined {Billingly::Plan}. Billingly comes with a {Billingly::Plan} model and a {Billingly::SubscriptionsController} which can be extended and enable you to support self-service subscriptions out of the box.
|
19
|
+
|
20
|
+
Billingly also lets you offer free trial subscriptions. You can configure a trial termination date when subscribing a customer to any type of plan, billingly will deactivate the customers account when the date of expiration comes, and will show them a subscription page from where they can signup to any other full plan.
|
21
|
+
|
22
|
+
# Installing
|
23
|
+
|
24
|
+
## Get the gem
|
25
|
+
|
26
|
+
gem install billingly
|
27
|
+
|
28
|
+
gem 'billingly'
|
29
|
+
|
30
|
+
## Create the tables
|
31
|
+
|
32
|
+
Use the provided generator to create the migration that will generate all the required tables.
|
33
|
+
|
34
|
+
You can add your custom attributes to the customer and plans tables, but changing the table names is not advised. Tables are namespaced with the 'billingly_' prefix.
|
35
|
+
|
36
|
+
rails g billingly_migration
|
37
|
+
|
38
|
+
For example, if your application's plans differ in the amount of users they allow, you can
|
39
|
+
add a `user_quota` field to your `billingly_plans` table.
|
40
|
+
|
41
|
+
You may also add a foreign key to your `billingly_customers` table so that each customer points
|
42
|
+
to a user.
|
43
|
+
|
44
|
+
## Customize the Models
|
45
|
+
|
46
|
+
Billingly models are abstract classes with default implementations, you don't need to override them but you would probably want to override the {Billingly::Customer} model to provide your implementations for the {Billingly::BaseCustomer#on\_subscription\_success} and {Billingly::BaseCustomer#can\_subscribe\_to?}
|
47
|
+
|
48
|
+
Continuing with the previous example, this snippet adds a user association to the customer and denormalizes the `user_quota` from the chosen plan into the User for easier lookup. It also prevents customers from subscribing to a plan offering a smaller quota than their current one.
|
49
|
+
|
50
|
+
# app/models/billingly/customer.rb
|
51
|
+
class Billingly::Customer < Billingly::BaseCustomer
|
52
|
+
belongs_to :user
|
53
|
+
|
54
|
+
def on_subscription_success
|
55
|
+
# For simplicity, we assume there is always a user.
|
56
|
+
user.update_attribute(:user_quota, active_subscription.plan.user_quota)
|
57
|
+
end
|
58
|
+
|
59
|
+
# Return false signifies that the user cannot subscribe to the provided plan
|
60
|
+
# if the current plan has a larger storage quota.
|
61
|
+
def can_subscribe_to?(plan)
|
62
|
+
current_plan = active_subscription.plan
|
63
|
+
if current_plan && current_plan.user_quota > plan.user_quota
|
64
|
+
return false
|
65
|
+
end
|
66
|
+
super
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
## Provide a Customer to your controllers
|
71
|
+
|
72
|
+
Billingly comes with 2 before filters for requiring the current user to be a customer, and
|
73
|
+
requiring the current customer to be active (that is, to not be deactivated because of an expired trial, overdue invoices, etc). These methods are called `requires_customer` and `requires_active_customer` respectively.
|
74
|
+
|
75
|
+
You provide a customer by overriding `current_customer` on your ApplicationController. Ideally, you would always have a `current_customer` as long as there is a user logged in, even if the currently logged in user has not subscribed to any plan yet. `current_customer` can return `nil` if no concept of a customer is available.
|
76
|
+
|
77
|
+
The `requires_customer` before\_filter will call `on_empty_customer` when `current_customer` is `nil`. `on_empty_customer` simply redirects to your root_path but you can override it too.
|
78
|
+
|
79
|
+
`requires_active_customer` redirects to the {Billingly::SubscriptionsController#index}, which presents the user with the steps required to reactivate his account.
|
80
|
+
|
81
|
+
Continuing with the {https://github.com/plataformatec/devise Devise} compatible examples, here's a snippet that overrides `ApplicationController` to use the customer associated to a user.
|
82
|
+
|
83
|
+
class ApplicationController < ActionController::Base
|
84
|
+
def current_customer
|
85
|
+
current_user.customer
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
`current_customer` is also available on your views as a helper method.
|
90
|
+
|
91
|
+
## Customize the Controller
|
92
|
+
|
93
|
+
The {Billingly::SubscriptionsController} can be overriden too. You may want to override {Billingly::SubscriptionsController#on\_subscription\_success #on\_subscription\_success} and {Billingly::SubscriptionsController#on\_reactivation\_success #on\_reactivation\_success}.
|
94
|
+
|
95
|
+
These are called when your customer subscribes or reactivates his account, respectively.
|
96
|
+
|
97
|
+
They both redirect to the `index` action by default.
|
98
|
+
|
99
|
+
Here's a snippet overriding them to go to the root path with a flash notice.
|
100
|
+
|
101
|
+
# app/controller/custom_subscriptions_controller.rb
|
102
|
+
class CustomSubscriptionsController < Billingly::SubscriptionsController
|
103
|
+
def on_subscription_success
|
104
|
+
redirect_to root_path, notice: "yay, you subscribed!"
|
105
|
+
end
|
106
|
+
|
107
|
+
def on_reactivation_success
|
108
|
+
redirect_to root_path, notice: "yay, your account was reactivated!"
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
## Mount Routes
|
113
|
+
|
114
|
+
We provide a shortcut so you can add the {Billingly::SubscriptionsController SubscriptionsController} to your `routes.rb`.
|
115
|
+
|
116
|
+
Here's a snippet showing two different ways of mounting the routes:
|
117
|
+
|
118
|
+
# your routes.rb
|
119
|
+
YourApp::Application.routes.draw do
|
120
|
+
|
121
|
+
# Will mount the default Billingly::SubscriptionsController in the /subscriptions path.
|
122
|
+
add_billingly_routes
|
123
|
+
|
124
|
+
# Will mount CustomModule::CustomSubscriptionsController in the
|
125
|
+
# /namespaced/subscriptions path
|
126
|
+
add_billingly_routes 'namespaced', 'custom_module/custom_subscriptions_controller'
|
127
|
+
end
|
128
|
+
|
129
|
+
## Schedule all the recurring jobs
|
130
|
+
All the invoicing, deactivating and emailing is done through a rake task.
|
131
|
+
|
132
|
+
The tasks are designed to fail gracefully and you can run them as often as you want without getting into undesired or invalid states.
|
133
|
+
|
134
|
+
Configure a cron job to run this around 4 times a day (should run without failures at least once a day)
|
135
|
+
|
136
|
+
$ rake billingly:all
|
137
|
+
|
138
|
+
## Customize the SubscriptionsController templates
|
139
|
+
|
140
|
+
Use the provided template generator to copy all of billingly's templates and partials into your application's directory structure.
|
141
|
+
|
142
|
+
All templates are provided in Haml format, using Twitter Bootstrap compatible markup.
|
143
|
+
|
144
|
+
These templates are used directly on the {http://billing.ly Demo App}
|
145
|
+
|
146
|
+
$ rails g billingly_views
|
147
|
+
|
148
|
+
## Customize the mailer templates
|
149
|
+
|
150
|
+
Billingly will send emails for the following scenarios:
|
151
|
+
|
152
|
+
* An invoice is available to be paid.
|
153
|
+
* An invoice is about to go overdue.
|
154
|
+
* A payment was processed successfully.
|
155
|
+
|
156
|
+
You can copy all the built in templates into your app's directory structure and customize them:
|
157
|
+
|
158
|
+
$ rails g billingly_mailer_views
|
@@ -1,25 +1,23 @@
|
|
1
1
|
# This controller takes care of managing subscriptions.
|
2
2
|
class Billingly::SubscriptionsController < ::ApplicationController
|
3
|
-
before_filter :requires_customer
|
4
|
-
before_filter :requires_active_customer, except: [:index, :reactivate]
|
3
|
+
before_filter :requires_customer
|
4
|
+
before_filter :requires_active_customer, except: [:index, :reactivate, :invoice]
|
5
5
|
|
6
6
|
# Index shows the current subscription to customers while they are active.
|
7
7
|
# It's also the page that prompts them to reactivate their account when deactivated.
|
8
8
|
# It's likely the only reachable page for deactivated customers.
|
9
9
|
def index
|
10
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
11
|
@plans = Billingly::Plan.all
|
12
|
+
@invoices = current_customer.invoices.order('created_at DESC')
|
18
13
|
end
|
19
|
-
|
14
|
+
|
20
15
|
# Subscribe the customer to a plan, or change his current plan.
|
21
16
|
def create
|
22
17
|
plan = Billingly::Plan.find(params[:plan_id])
|
18
|
+
unless current_customer.can_subscribe_to?(plan)
|
19
|
+
return redirect_to subscriptions_path, notice: 'Cannot subscribe to that plan'
|
20
|
+
end
|
23
21
|
current_customer.subscribe_to_plan(plan)
|
24
22
|
on_subscription_success
|
25
23
|
end
|
@@ -29,20 +27,42 @@ class Billingly::SubscriptionsController < ::ApplicationController
|
|
29
27
|
# Their account will be reactivated to their old subscription plan immediately.
|
30
28
|
# They can change plans afterwards.
|
31
29
|
def reactivate
|
32
|
-
|
30
|
+
plan = Billingly::Plan.find_by_id(params[:plan_id])
|
31
|
+
if current_customer.reactivate(plan).nil?
|
32
|
+
return render nothing: true, status: 403
|
33
|
+
end
|
33
34
|
on_reactivation_success
|
34
35
|
end
|
36
|
+
|
37
|
+
# Unsubscribes the customer from his last subscription and deactivates his account.
|
38
|
+
# Performing this action would set the deactivation reason to be 'left_voluntarily'
|
39
|
+
def deactivate
|
40
|
+
current_customer.deactivate_left_voluntarily
|
41
|
+
redirect_to(action: :index)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Shows an invoice.
|
45
|
+
# @todo
|
46
|
+
# This should actually be the #show action of an InvoicesController but we're lazy ATM.
|
47
|
+
def invoice
|
48
|
+
@invoice = current_customer.invoices.find(params[:invoice_id])
|
49
|
+
end
|
35
50
|
|
36
51
|
# When a subscription is sucessful this callback is triggered.
|
37
52
|
# Host applications should override it by subclassing this subscriptionscontroller,
|
38
53
|
# and include their own behaviour, and for example grant the privileges associated
|
39
54
|
# to the subscription plan.
|
55
|
+
#
|
56
|
+
# Redirects to the last invoice by default.
|
40
57
|
def on_subscription_success
|
41
|
-
|
58
|
+
current_customer.reload
|
59
|
+
redirect_to(invoice_subscriptions_path(current_customer.active_subscription.invoices.last.id))
|
42
60
|
end
|
43
61
|
|
62
|
+
# Should be overriden to provide a response when the user account is reactivated.
|
63
|
+
#
|
64
|
+
# Defaults to a redirect to :index
|
44
65
|
def on_reactivation_success
|
45
|
-
|
66
|
+
redirect_to(action: :index)
|
46
67
|
end
|
47
|
-
|
48
68
|
end
|
@@ -0,0 +1,306 @@
|
|
1
|
+
require 'validates_email_format_of'
|
2
|
+
|
3
|
+
module Billingly
|
4
|
+
|
5
|
+
# A {Customer} is Billingly's main actor.
|
6
|
+
# * Customers have a {Subscription} to your service which entitles them to use it.
|
7
|
+
# * Customers are {Invoice invoiced} regularly to pay for their {Subscription}
|
8
|
+
# * {Payment Payments} are received on a {Customer}s behalf and credited to their account.
|
9
|
+
# * Invoices are generated periodically calculating charges a Customer incurred in.
|
10
|
+
# * Receipts are sent to Customers when their invoices are paid.
|
11
|
+
class BaseCustomer < ActiveRecord::Base
|
12
|
+
self.abstract_class = true
|
13
|
+
self.table_name = 'billingly_customers'
|
14
|
+
|
15
|
+
# The reason why this customer is deactivated.
|
16
|
+
# A customer can be deactivated for one of 3 reasons:
|
17
|
+
# * trial_expired: Their trial period expired.
|
18
|
+
# * debtor: They have unpaid invoices.
|
19
|
+
# * left_voluntarily: They decided to leave the site.
|
20
|
+
# This is important when reactivating their account. If they left in their own terms,
|
21
|
+
# we won't try to reactivate their account when we receive a payment from them.
|
22
|
+
# The message shown to them when they reactivate will also be different depending on
|
23
|
+
# how they left.
|
24
|
+
DEACTIVATION_REASONS = [:trial_expired, :debtor, :left_voluntarily]
|
25
|
+
|
26
|
+
# The Date and Time in which the Customer's account was deactivated (see {#deactivated?}).
|
27
|
+
# This field denormalizes the date in which this customer's last subscription was ended.
|
28
|
+
# @!attribute [r] deactivated_since
|
29
|
+
# @return [DateTime]
|
30
|
+
validates :deactivated_since, presence: true, if: :deactivation_reason
|
31
|
+
|
32
|
+
# (see Customer::DEACTIVATION_REASONS)
|
33
|
+
# @return [String]
|
34
|
+
# @!attribute deactivation_reason
|
35
|
+
validates :deactivation_reason, inclusion: DEACTIVATION_REASONS, if: :deactivated?
|
36
|
+
|
37
|
+
# A customer can be {#deactivate deactivated} when they cancel their subscription
|
38
|
+
# or when they miss a payment. Under the hood this function checks the
|
39
|
+
# {#deactivated_since} attribute.
|
40
|
+
# @!attribute [r] deactivated?
|
41
|
+
def deactivated?
|
42
|
+
not deactivated_since.nil?
|
43
|
+
end
|
44
|
+
|
45
|
+
# Used as contact address, validates format but does not check uniqueness.
|
46
|
+
# @!attribute email
|
47
|
+
# @return [String]
|
48
|
+
attr_accessible :email
|
49
|
+
validates_email_format_of :email
|
50
|
+
|
51
|
+
# All subscriptions this customer was ever subscribed to.
|
52
|
+
# @!attribute subscriptions
|
53
|
+
# @return [Array<Subscription>]
|
54
|
+
has_many :subscriptions, foreign_key: 'customer_id'
|
55
|
+
|
56
|
+
# All paymetns that were ever credited for this customer
|
57
|
+
# @!attribute payments
|
58
|
+
# @return [Array<Payment>]
|
59
|
+
has_many :payments, foreign_key: 'customer_id'
|
60
|
+
|
61
|
+
# The {Subscription} for which the customer is currently being charged.
|
62
|
+
# @!attribute [r] active_subscription
|
63
|
+
# @return [Subscription, nil]
|
64
|
+
def active_subscription
|
65
|
+
last = subscriptions.last
|
66
|
+
last unless last.nil? || last.terminated?
|
67
|
+
end
|
68
|
+
|
69
|
+
# (see Customer.debtors)
|
70
|
+
# @!attribute [r] debtor?
|
71
|
+
# @return [Boolean] whether this customer is a debtor or not.
|
72
|
+
def debtor?
|
73
|
+
not self.class.debtors.find_by_id(self.id).nil?
|
74
|
+
end
|
75
|
+
|
76
|
+
# All {Invoice invoices} ever created for this customer, for any {Subscription}
|
77
|
+
# @!attribute invoices
|
78
|
+
# @return [Array<Invoice>]
|
79
|
+
has_many :invoices, foreign_key: 'customer_id'
|
80
|
+
|
81
|
+
# Every {JournalEntry} ever created for this customer, a {#ledger} is created from these.
|
82
|
+
# See {JournalEntry} for a description on what they are.
|
83
|
+
# @!attribute journal_entries
|
84
|
+
# @return [Array<JournalEntry>]
|
85
|
+
has_many :journal_entries, foreign_key: 'customer_id'
|
86
|
+
|
87
|
+
# (see Customer::DEACTIVATION_REASONS)
|
88
|
+
# @return [Symbol]
|
89
|
+
# @!attribute deactivation_reason
|
90
|
+
validates :deactivation_reason, inclusion: DEACTIVATION_REASONS, if: :deactivated?
|
91
|
+
|
92
|
+
# Whether the user is on an unfinished trial period.
|
93
|
+
# @!attribute [r] doing_trial?
|
94
|
+
# @return [Boolean]
|
95
|
+
def doing_trial?
|
96
|
+
active_subscription && active_subscription.trial?
|
97
|
+
end
|
98
|
+
|
99
|
+
# When the user is doing a trial, this would be how many days are left until it's over.
|
100
|
+
# @!attribute [r] trial_days_left
|
101
|
+
# @return [Integer]
|
102
|
+
def trial_days_left
|
103
|
+
return unless doing_trial?
|
104
|
+
(active_subscription.is_trial_expiring_on.to_date - Date.today).to_i
|
105
|
+
end
|
106
|
+
|
107
|
+
# Customers subscribe to the service under certain conditions referred to as a {Plan},
|
108
|
+
# and perform periodic payments to continue using it.
|
109
|
+
# We offer common plans stating how much and how often they should pay, also, if the
|
110
|
+
# payment is to be done at the beginning or end of the period (upfront or due-month)
|
111
|
+
# Every customer can potentially get a special deal, but we offer common
|
112
|
+
# deals as {Plan Plans} from which a proper {Subscription} is created.
|
113
|
+
# A {Subscription} is also an acceptable argument, in that case the new one
|
114
|
+
# will maintain all the characteristics of that one, except the starting date.
|
115
|
+
# @param [Plan, Subscription]
|
116
|
+
# @return [Subscription] The newly created {Subscription}
|
117
|
+
def subscribe_to_plan(plan, is_trial_expiring_on = nil)
|
118
|
+
subscriptions.last.terminate if subscriptions.last
|
119
|
+
|
120
|
+
subscriptions.build.tap do |new|
|
121
|
+
[:payable_upfront, :description, :periodicity,
|
122
|
+
:amount, :grace_period].each do |k|
|
123
|
+
new[k] = plan[k]
|
124
|
+
end
|
125
|
+
new.plan = plan if plan.is_a?(Billingly::Plan)
|
126
|
+
new.is_trial_expiring_on = is_trial_expiring_on
|
127
|
+
new.subscribed_on = Time.now
|
128
|
+
new.save!
|
129
|
+
new.generate_next_invoice
|
130
|
+
on_subscription_success
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
# Callback called whenever this customer is successfully subscribed to a plan.
|
135
|
+
# This callback does not differentiate if the customer is subscribing for the first time,
|
136
|
+
# reactivating his account or just changing from one plan to another.
|
137
|
+
# self.active_subscription will be the current subscription when this method is called.
|
138
|
+
def on_subscription_success
|
139
|
+
end
|
140
|
+
|
141
|
+
# Creates a general ledger from {JournalEntry journal entries}.
|
142
|
+
# Every {Invoice} and {Payment} involves movements to the customer's account.
|
143
|
+
# which are registered as a {JournalEntry}.
|
144
|
+
# The ledger can tell us whats the cash balance
|
145
|
+
# in our customer's favor and how much money have they paid overall.
|
146
|
+
# @see JournalEntry
|
147
|
+
# @return [{Symbol => BigDecimal}]
|
148
|
+
# @todo Due to silly rounding errors on sqlite this implementation needs
|
149
|
+
# to convert decimals to float and then to decimals again. :S
|
150
|
+
def ledger
|
151
|
+
Hash.new(0.0).tap do |all|
|
152
|
+
journal_entries.group_by(&:account).collect do |account, entries|
|
153
|
+
values = entries.collect(&:amount).collect(&:to_f)
|
154
|
+
all[account.to_sym] = values.inject(0.0) do |sum,item|
|
155
|
+
(BigDecimal.new(sum.to_s) + BigDecimal.new(item.to_s)).to_f
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
# Shortcut for adding {#journal_entries} for this customer.
|
162
|
+
# @note Most likely, you will never add entries to the customer journal yourself.
|
163
|
+
# These are created when {Invoice invoicing} or crediting {Payment payments}
|
164
|
+
def add_to_journal(amount, *accounts, extra)
|
165
|
+
accounts = [] if accounts.nil?
|
166
|
+
unless extra.is_a?(Hash)
|
167
|
+
accounts << extra
|
168
|
+
extra = {}
|
169
|
+
end
|
170
|
+
|
171
|
+
accounts.each do |account|
|
172
|
+
journal_entries.create!(extra.merge(amount: amount, account: account.to_s))
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
# This method will deactivate all customers who have overdue {Invoice Invoices}.
|
177
|
+
# It's run periodically through Billingly's Rake Task.
|
178
|
+
def self.deactivate_all_debtors
|
179
|
+
debtors.where(deactivated_since: nil).all.each{|debtor| debtor.deactivate_debtor }
|
180
|
+
end
|
181
|
+
|
182
|
+
# A customer who has overdue invoices at the time of asking this question is
|
183
|
+
# considered a debtor.
|
184
|
+
#
|
185
|
+
# @note
|
186
|
+
# A customer may be a debtor and still have an active account until Billingly's
|
187
|
+
# rake task goes through the process of {#deactivate_all_debtors deactivating all debtors}.
|
188
|
+
#
|
189
|
+
# Furthermore, customers may unsubscribe before their {Invoice invoices} become overdue,
|
190
|
+
# hence they may be in a deactivated state and not be debtors yet.
|
191
|
+
def self.debtors
|
192
|
+
joins(:invoices).readonly(false)
|
193
|
+
.where("#{Billingly::Invoice.table_name}.due_on < ?", Time.now)
|
194
|
+
.where(billingly_invoices: {deleted_on: nil, paid_on: nil})
|
195
|
+
end
|
196
|
+
|
197
|
+
# Credits an amount of money to customer's account and then triggers the corresponding
|
198
|
+
# actions if a {Payment payment} was expected from this customer.
|
199
|
+
#
|
200
|
+
# Apart from creating a {Payment} object this method will try to charge pending invoices
|
201
|
+
# and reactivate a customer who was deactivated for being a debtor.
|
202
|
+
#
|
203
|
+
# @note
|
204
|
+
# This is the single point of entry for {Payment Payments}.
|
205
|
+
#
|
206
|
+
# If you're processing payments using {http://activemerchant.org} you should hook
|
207
|
+
# your 'Incoming Payment Notifications' to call this method to credit the received
|
208
|
+
# amount to the customer's account.
|
209
|
+
#
|
210
|
+
# @param amount [BigDecimal, float] the amount to be credited.
|
211
|
+
def credit_payment(amount)
|
212
|
+
Billingly::Payment.credit_for(self, amount)
|
213
|
+
Billingly::Invoice.charge_all(self.invoices)
|
214
|
+
reactivate if deactivated? && deactivation_reason == :debtor
|
215
|
+
end
|
216
|
+
|
217
|
+
# Terminate a customer's subscription to the service.
|
218
|
+
# Customers are deactivated due to lack of payment, because they decide to end their
|
219
|
+
# subscription to your service or because their trial period expired.
|
220
|
+
#
|
221
|
+
# Use the shortcuts:
|
222
|
+
# {#deactivate_left_voluntarily}, {#deactivate_trial_expired} or {#deactivate_debtor}
|
223
|
+
#
|
224
|
+
# Deactivated customers can always be {#reactivate reactivated} later.
|
225
|
+
# @param reason [Symbol] the deactivation reason, see {DEACTIVATION_REASONS}
|
226
|
+
# @return [self, nil] nil if the account was already deactivated, self otherwise.
|
227
|
+
def deactivate(reason)
|
228
|
+
return if deactivated?
|
229
|
+
active_subscription.terminate
|
230
|
+
self.deactivated_since = Time.now
|
231
|
+
self.deactivation_reason = reason
|
232
|
+
self.save!
|
233
|
+
return self
|
234
|
+
end
|
235
|
+
|
236
|
+
DEACTIVATION_REASONS.each do |reason|
|
237
|
+
define_method("deactivate_#{reason}") do
|
238
|
+
deactivate(reason.to_sym)
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
# @see #deactivate
|
243
|
+
def deactivate_left_voluntarily
|
244
|
+
deactivate(:left_voluntarily)
|
245
|
+
end
|
246
|
+
|
247
|
+
# @see #deactivate
|
248
|
+
def deactivate_trial_expired
|
249
|
+
deactivate(:trial_expired)
|
250
|
+
end
|
251
|
+
|
252
|
+
# @see #deactivate
|
253
|
+
def deactivate_debtor
|
254
|
+
deactivate(:debtor)
|
255
|
+
end
|
256
|
+
|
257
|
+
# Customers whose account has been {#deactivate deactivated} can always re-join the service
|
258
|
+
# as long as they {#debtor? don't owe any money}
|
259
|
+
# @return [self, nil] nil if the customer could not be reactivated, self otherwise.
|
260
|
+
def reactivate(new_plan = nil)
|
261
|
+
new_plan = new_plan || subscriptions.last
|
262
|
+
return if new_plan.nil?
|
263
|
+
return unless deactivated?
|
264
|
+
return if debtor?
|
265
|
+
update_attribute(:deactivated_since, nil)
|
266
|
+
subscribe_to_plan(new_plan)
|
267
|
+
return self
|
268
|
+
end
|
269
|
+
|
270
|
+
# Customers may be subscribed for a trial period, and they are supposed to re-subscribe
|
271
|
+
# before their trial expires.
|
272
|
+
#
|
273
|
+
# When their trial expires and they have not yet subscribed to another plan, we
|
274
|
+
# deactivate their account immediately.
|
275
|
+
#
|
276
|
+
# This method will deactivate all customers whose trial has expired.
|
277
|
+
# It's run periodically through Billingly's Rake Task.
|
278
|
+
def self.deactivate_all_expired_trials
|
279
|
+
customers = joins(:subscriptions).readonly(false)
|
280
|
+
.where("#{Billingly::Subscription.table_name}.is_trial_expiring_on < ?", Time.now)
|
281
|
+
.where(billingly_subscriptions: {unsubscribed_on: nil})
|
282
|
+
|
283
|
+
customers.each do |customer|
|
284
|
+
customer.deactivate_trial_expired
|
285
|
+
end
|
286
|
+
end
|
287
|
+
|
288
|
+
# Can this customer subscribe to a plan?.
|
289
|
+
# You may want to prevent customers from upgrading or downgrading to other plans
|
290
|
+
# depending on their usage of your service.
|
291
|
+
#
|
292
|
+
# This method is only used in views and controllers to prevent customers from requesting
|
293
|
+
# to be upgraded or downgraded to a plan without your consent.
|
294
|
+
# The model layer can still subscribe the customer if you so desire.
|
295
|
+
#
|
296
|
+
# The default implementation lets Customers upgrade to any if they are currently doing
|
297
|
+
# a trial period, and it does not let them re-subscribe to the same plan afterwards.
|
298
|
+
# It also always disallows debtors to subscribe to another plan.
|
299
|
+
# @param plan [Billingly::Plan]
|
300
|
+
def can_subscribe_to?(plan)
|
301
|
+
return false if !doing_trial? && active_subscription && active_subscription.plan == plan
|
302
|
+
return false if debtor?
|
303
|
+
return true
|
304
|
+
end
|
305
|
+
end
|
306
|
+
end
|