billingly 0.1.1 → 0.1.4
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +2 -0
- data/app/mailers/{billingly_mailer.rb → billingly/base_mailer.rb} +16 -1
- data/app/mailers/billingly/mailer.rb +3 -0
- data/app/models/billingly/base_customer.rb +20 -33
- data/app/models/billingly/base_subscription.rb +58 -13
- data/app/models/billingly/invoice.rb +3 -38
- data/app/models/billingly/tasks.rb +200 -0
- data/app/views/{billingly_mailer → billingly/mailer}/overdue_notification.html.erb +0 -0
- data/app/views/{billingly_mailer → billingly/mailer}/paid_notification.html.erb +0 -0
- data/app/views/{billingly_mailer → billingly/mailer}/pending_notification.html.erb +0 -0
- data/app/views/{billingly_mailer → billingly/mailer}/pending_notification.plain.erb +0 -0
- data/app/views/{billingly_mailer → billingly/mailer}/pending_notification.text.erb +0 -0
- data/app/views/billingly/mailer/task_results.text.erb +9 -0
- data/app/views/billingly/mailer/trial_expired_notification.text.erb +4 -0
- data/config/locales/en.yml +5 -0
- data/lib/billingly/version.rb +1 -1
- data/lib/generators/templates/create_billingly_tables.rb +3 -0
- data/lib/tasks/billingly_tasks.rake +1 -14
- metadata +41 -23
data/README.md
CHANGED
@@ -1,5 +1,8 @@
|
|
1
|
-
class
|
1
|
+
class Billingly::BaseMailer < ActionMailer::Base
|
2
2
|
default from: 'example@example.com'
|
3
|
+
|
4
|
+
cattr_accessor :admin_emails
|
5
|
+
self.admin_emails = 'admin@example.com'
|
3
6
|
|
4
7
|
def pending_notification(invoice)
|
5
8
|
@invoice = invoice
|
@@ -17,4 +20,16 @@ class BillinglyMailer < ActionMailer::Base
|
|
17
20
|
@invoice = invoice
|
18
21
|
mail(to: invoice.customer.email, subject: I18n.t('billingly.payment_receipt'))
|
19
22
|
end
|
23
|
+
|
24
|
+
def task_results(runner)
|
25
|
+
@runner = runner
|
26
|
+
mail to: self.class.admin_emails, subject: "Your Billingly Status Report"
|
27
|
+
end
|
28
|
+
|
29
|
+
# Sends the email about an expired trial.
|
30
|
+
# param trial [Subscription] a trial which should be expired.
|
31
|
+
def trial_expired_notification(subscription)
|
32
|
+
@subscription = subscription
|
33
|
+
mail to: subscription.customer.email, subject: I18n.t('billingly.your_trial_has_expired')
|
34
|
+
end
|
20
35
|
end
|
@@ -21,7 +21,7 @@ module Billingly
|
|
21
21
|
# we won't try to reactivate their account when we receive a payment from them.
|
22
22
|
# The message shown to them when they reactivate will also be different depending on
|
23
23
|
# how they left.
|
24
|
-
DEACTIVATION_REASONS =
|
24
|
+
DEACTIVATION_REASONS = %w(trial_expired debtor left_voluntarily)
|
25
25
|
|
26
26
|
# The Date and Time in which the Customer's account was deactivated (see {#deactivated?}).
|
27
27
|
# This field denormalizes the date in which this customer's last subscription was ended.
|
@@ -101,7 +101,7 @@ module Billingly
|
|
101
101
|
# @return [Integer]
|
102
102
|
def trial_days_left
|
103
103
|
return unless doing_trial?
|
104
|
-
(active_subscription.is_trial_expiring_on.to_date -
|
104
|
+
(active_subscription.is_trial_expiring_on.to_date - Time.now.utc.to_date).to_i
|
105
105
|
end
|
106
106
|
|
107
107
|
# Customers subscribe to the service under certain conditions referred to as a {Plan},
|
@@ -115,7 +115,7 @@ module Billingly
|
|
115
115
|
# @param [Plan, Subscription]
|
116
116
|
# @return [Subscription] The newly created {Subscription}
|
117
117
|
def subscribe_to_plan(plan, is_trial_expiring_on = nil)
|
118
|
-
subscriptions.last.
|
118
|
+
subscriptions.last.terminate_changed_subscription if subscriptions.last
|
119
119
|
|
120
120
|
subscriptions.build.tap do |new|
|
121
121
|
[:payable_upfront, :description, :periodicity,
|
@@ -173,12 +173,6 @@ module Billingly
|
|
173
173
|
end
|
174
174
|
end
|
175
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
176
|
# A customer who has overdue invoices at the time of asking this question is
|
183
177
|
# considered a debtor.
|
184
178
|
#
|
@@ -210,8 +204,8 @@ module Billingly
|
|
210
204
|
# @param amount [BigDecimal, float] the amount to be credited.
|
211
205
|
def credit_payment(amount)
|
212
206
|
Billingly::Payment.credit_for(self, amount)
|
213
|
-
|
214
|
-
reactivate if deactivated? && deactivation_reason ==
|
207
|
+
charge_pending_invoices
|
208
|
+
reactivate if deactivated? && deactivation_reason == 'debtor'
|
215
209
|
end
|
216
210
|
|
217
211
|
# Terminate a customer's subscription to the service.
|
@@ -226,10 +220,10 @@ module Billingly
|
|
226
220
|
# @return [self, nil] nil if the account was already deactivated, self otherwise.
|
227
221
|
def deactivate(reason)
|
228
222
|
return if deactivated?
|
229
|
-
active_subscription.terminate
|
223
|
+
active_subscription.terminate(reason)
|
230
224
|
self.deactivated_since = Time.now
|
231
225
|
self.deactivation_reason = reason
|
232
|
-
|
226
|
+
save!
|
233
227
|
return self
|
234
228
|
end
|
235
229
|
|
@@ -241,17 +235,17 @@ module Billingly
|
|
241
235
|
|
242
236
|
# @see #deactivate
|
243
237
|
def deactivate_left_voluntarily
|
244
|
-
deactivate(
|
238
|
+
deactivate('left_voluntarily')
|
245
239
|
end
|
246
240
|
|
247
241
|
# @see #deactivate
|
248
242
|
def deactivate_trial_expired
|
249
|
-
deactivate(
|
243
|
+
deactivate('trial_expired')
|
250
244
|
end
|
251
245
|
|
252
246
|
# @see #deactivate
|
253
247
|
def deactivate_debtor
|
254
|
-
deactivate(
|
248
|
+
deactivate('debtor')
|
255
249
|
end
|
256
250
|
|
257
251
|
# Customers whose account has been {#deactivate deactivated} can always re-join the service
|
@@ -266,25 +260,18 @@ module Billingly
|
|
266
260
|
subscribe_to_plan(new_plan)
|
267
261
|
return self
|
268
262
|
end
|
269
|
-
|
270
|
-
#
|
271
|
-
#
|
272
|
-
#
|
273
|
-
# When their trial expires and they have not yet subscribed to another plan, we
|
274
|
-
# deactivate their account immediately.
|
263
|
+
|
264
|
+
# Charges all invoices for which the customer has enough balance.
|
265
|
+
# Oldest invoices are charged first, newer invoices should not be charged until
|
266
|
+
# the oldest ones are paid.
|
275
267
|
#
|
276
|
-
#
|
277
|
-
#
|
278
|
-
def
|
279
|
-
|
280
|
-
.
|
281
|
-
.where(billingly_subscriptions: {unsubscribed_on: nil})
|
282
|
-
|
283
|
-
customers.each do |customer|
|
284
|
-
customer.deactivate_trial_expired
|
285
|
-
end
|
268
|
+
# See {Billingly::Invoice#charge Invoice#charge} for more information
|
269
|
+
# on how invoices are charged from the customer's balance.
|
270
|
+
def charge_pending_invoices
|
271
|
+
invoices.where(deleted_on: nil, paid_on: nil).order('period_start')
|
272
|
+
.each{|invoice| break unless invoice.charge}
|
286
273
|
end
|
287
|
-
|
274
|
+
|
288
275
|
# Can this customer subscribe to a plan?.
|
289
276
|
# You may want to prevent customers from upgrading or downgrading to other plans
|
290
277
|
# depending on their usage of your service.
|
@@ -25,7 +25,38 @@ module Billingly
|
|
25
25
|
# @property subscribed_on
|
26
26
|
# @return [DateTime]
|
27
27
|
validates :subscribed_on, presence: true
|
28
|
+
|
29
|
+
# Subscriptions are terminated for a reason which could be:
|
30
|
+
# * trial_expired: Subscription was a trial and it just expired.
|
31
|
+
# * debtor: The customer owed an invoice for this subscription and did not pay.
|
32
|
+
# * changed_subscription: This subscription was immediately replaced by another one.
|
33
|
+
# * left_voluntarily: This subscription was terminated because the customer left.
|
34
|
+
#
|
35
|
+
# TERMINATION_REASONS are important for auditing and for the mailing tasks to notify
|
36
|
+
# about subscriptions terminated automatically by the system.
|
37
|
+
TERMINATION_REASONS = %w(trial_expired debtor changed_subscription left_voluntarily)
|
38
|
+
|
39
|
+
# The date in which this subscription ended.
|
40
|
+
#
|
41
|
+
# Every ended subscription ended for a reason, look at {TERMINATION_REASONS}.
|
42
|
+
# @property unsubscribed_on
|
43
|
+
# @return [DateTime]
|
44
|
+
validates :unsubscribed_on, presence: true, if: :unsubscribed_because
|
28
45
|
|
46
|
+
# The reason why this subscription ended.
|
47
|
+
#
|
48
|
+
# Every ended subscription ended for a reason, look at {TERMINATION_REASONS}.
|
49
|
+
# @property unsubscribed_because
|
50
|
+
# @return [DateTime]
|
51
|
+
validates :unsubscribed_because, inclusion: TERMINATION_REASONS, if: :terminated?
|
52
|
+
|
53
|
+
# Was this subscription terminated?
|
54
|
+
# @property [r] terminated?
|
55
|
+
# @return [Boolean] Whether the subscription was terminated or not.
|
56
|
+
def terminated?
|
57
|
+
not unsubscribed_on.nil?
|
58
|
+
end
|
59
|
+
|
29
60
|
# The grace period we use when calculating an invoices due date.
|
30
61
|
# If a subscription is payable_upfront, then the customer effectively owes us
|
31
62
|
# since the day in which a given period starts.
|
@@ -58,7 +89,7 @@ module Billingly
|
|
58
89
|
belongs_to :plan
|
59
90
|
|
60
91
|
# (see #is_trial_expiring_on)
|
61
|
-
# @property trial?
|
92
|
+
# @property [r] trial?
|
62
93
|
# @return [Boolean]
|
63
94
|
def trial?
|
64
95
|
not is_trial_expiring_on.nil?
|
@@ -95,24 +126,38 @@ module Billingly
|
|
95
126
|
|
96
127
|
# Terminates this subscription, it could be either because we deactivate a debtor
|
97
128
|
# or because the customer decided to end his subscription on his own terms.
|
98
|
-
|
129
|
+
#
|
130
|
+
# Use the shortcuts:
|
131
|
+
# {#terminate_left_voluntarily}, {#terminate_trial_expired},
|
132
|
+
# {#terminate_debtor}, {#terminate_changed_subscription}
|
133
|
+
#
|
134
|
+
# Once terminated, a subscription cannot be re-open, just create a new one.
|
135
|
+
# @param reason [Symbol] the reason to terminate this subscription, see {TERMINATION_REASONS}
|
136
|
+
# @return [self, nil] nil if the account was already terminated, self otherwise.
|
137
|
+
def terminate(reason)
|
99
138
|
return if terminated?
|
100
|
-
|
139
|
+
self.unsubscribed_on = Time.now
|
140
|
+
self.unsubscribed_because = reason
|
101
141
|
invoices.last.truncate unless trial?
|
142
|
+
save!
|
102
143
|
return self
|
103
144
|
end
|
104
|
-
|
105
|
-
|
106
|
-
|
145
|
+
|
146
|
+
TERMINATION_REASONS.each do |reason|
|
147
|
+
define_method("terminate_#{reason}") do
|
148
|
+
terminate(reason)
|
149
|
+
end
|
107
150
|
end
|
108
151
|
|
109
|
-
#
|
110
|
-
#
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
152
|
+
# When a trial subscription ends the customer is notified about it via email.
|
153
|
+
# @return [self, nil] not nil means the notification was sent successfully.
|
154
|
+
def notify_trial_expired
|
155
|
+
return unless trial?
|
156
|
+
return unless terminated? && unsubscribed_because == 'trial_expired'
|
157
|
+
return unless notified_trial_expired_on.nil?
|
158
|
+
Billingly::Mailer.trial_expired_notification(self).deliver!
|
159
|
+
update_attribute(:notified_trial_expired_on, Time.now)
|
160
|
+
return self
|
116
161
|
end
|
117
162
|
end
|
118
163
|
end
|
@@ -55,64 +55,29 @@ module Billingly
|
|
55
55
|
return self
|
56
56
|
end
|
57
57
|
|
58
|
-
# Charges all invoices that can be charged from the existing customer cash balances
|
59
|
-
def self.charge_all(collection = self)
|
60
|
-
collection.where(deleted_on: nil, paid_on: nil).order('period_start').each do |invoice|
|
61
|
-
invoice.charge
|
62
|
-
end
|
63
|
-
end
|
64
|
-
|
65
|
-
# This method is called by Billingly's recurring task to notify all pending invoices.
|
66
|
-
def self.notify_all_pending
|
67
|
-
where(deleted_on: nil, paid_on: nil, notified_pending_on: nil)
|
68
|
-
.each do |invoice|
|
69
|
-
invoice.notify_pending
|
70
|
-
end
|
71
|
-
end
|
72
|
-
|
73
58
|
def notify_pending
|
74
59
|
return unless notified_pending_on.nil?
|
75
60
|
return if paid?
|
76
61
|
return if deleted?
|
77
62
|
return if due_on > subscription.grace_period.from_now
|
78
|
-
|
63
|
+
Billingly::Mailer.pending_notification(self).deliver!
|
79
64
|
update_attribute(:notified_pending_on, Time.now)
|
80
65
|
end
|
81
66
|
|
82
|
-
# Send the email notifying that this invoice being overdue and the subscription
|
83
|
-
# being cancelled
|
84
|
-
def self.notify_all_overdue
|
85
|
-
where('due_on <= ?', Time.now)
|
86
|
-
.where(deleted_on: nil, paid_on: nil, notified_overdue_on: nil)
|
87
|
-
.each do |invoice|
|
88
|
-
invoice.notify_overdue
|
89
|
-
end
|
90
|
-
end
|
91
|
-
|
92
67
|
def notify_overdue
|
93
68
|
return unless notified_overdue_on.nil?
|
94
69
|
return if paid?
|
95
70
|
return if deleted?
|
96
71
|
return if due_on > Time.now
|
97
|
-
|
72
|
+
Billingly::Mailer.overdue_notification(self).deliver!
|
98
73
|
update_attribute(:notified_overdue_on, Time.now)
|
99
74
|
end
|
100
75
|
|
101
|
-
# Notifies that the invoice has been charged successfully.
|
102
|
-
# Send the email notifying that this invoice being overdue and the subscription
|
103
|
-
# being cancelled
|
104
|
-
def self.notify_all_paid
|
105
|
-
where('paid_on is not null')
|
106
|
-
.where(deleted_on: nil, notified_paid_on: nil).each do |invoice|
|
107
|
-
invoice.notify_paid
|
108
|
-
end
|
109
|
-
end
|
110
|
-
|
111
76
|
def notify_paid
|
112
77
|
return unless paid?
|
113
78
|
return unless notified_paid_on.nil?
|
114
79
|
return if deleted?
|
115
|
-
|
80
|
+
Billingly::Mailer.paid_notification(self).deliver!
|
116
81
|
update_attribute(:notified_paid_on, Time.now)
|
117
82
|
end
|
118
83
|
end
|
@@ -0,0 +1,200 @@
|
|
1
|
+
# The Tasks model has all the tasks that should be run periodically through rake.
|
2
|
+
# A special log is created for the tasks being run and the results are reported
|
3
|
+
# back to the website administrator.
|
4
|
+
class Billingly::Tasks
|
5
|
+
# The date in which the tasks started running.
|
6
|
+
# @!attribute started
|
7
|
+
# @return [DateTime]
|
8
|
+
attr_accessor :started
|
9
|
+
|
10
|
+
# The date in which the tasks ended
|
11
|
+
# @!attribute ended
|
12
|
+
# @return [DateTime]
|
13
|
+
attr_accessor :ended
|
14
|
+
|
15
|
+
# A summary of all the tasks that were run and their overall results.
|
16
|
+
# @!attribute summary
|
17
|
+
# @return [String]
|
18
|
+
attr_accessor :summary
|
19
|
+
|
20
|
+
# A detailed description of errors that ocurred while running all the tasks.
|
21
|
+
#
|
22
|
+
# @!attribute extended
|
23
|
+
# @return [File]
|
24
|
+
attr_accessor :extended
|
25
|
+
|
26
|
+
# Runs all of Billingly's periodic tasks and creates a report with the results at the end.
|
27
|
+
def run_all
|
28
|
+
self.started = Time.now
|
29
|
+
|
30
|
+
generate_next_invoices
|
31
|
+
charge_invoices
|
32
|
+
deactivate_all_debtors
|
33
|
+
deactivate_all_expired_trials
|
34
|
+
notify_all_paid
|
35
|
+
notify_all_pending
|
36
|
+
notify_all_overdue
|
37
|
+
|
38
|
+
self.ended = Time.now
|
39
|
+
self.extended.close unless self.extended.nil?
|
40
|
+
Billingly::Mailer.task_results(self).deliver
|
41
|
+
end
|
42
|
+
|
43
|
+
# Writes a line to the {#extended} section of this tasks results report.
|
44
|
+
# @param text [String]
|
45
|
+
def log_extended(text)
|
46
|
+
if self.extended.nil?
|
47
|
+
time = Time.now.utc.strftime("%Y%m%d%H%M%S")
|
48
|
+
self.extended = File.open("#{Rails.root}/log/billingly_#{time}.log", 'w')
|
49
|
+
end
|
50
|
+
self.extended.write("#{text}\n\n")
|
51
|
+
end
|
52
|
+
|
53
|
+
# Writes a line to the {#summary} section of this task results report.
|
54
|
+
# @param text [String]
|
55
|
+
def log_summary(text)
|
56
|
+
self.summary ||= ''
|
57
|
+
self.summary += "#{text}\n"
|
58
|
+
end
|
59
|
+
|
60
|
+
# The batch runner is a helper function for running a method on each item of a
|
61
|
+
# collection logging the results, without aborting excecution if calling the rest of the
|
62
|
+
# items if any of them fails.
|
63
|
+
#
|
64
|
+
# The method called on each item will not receive parameters and should return
|
65
|
+
# a Truthy value if successfull, or raise an exception otherwise.
|
66
|
+
# Returning nil means that there was nothing to be done on that item.
|
67
|
+
#
|
68
|
+
# The collection to be used should be returned by a block provided to this method.
|
69
|
+
# Any problem fetching the collection will also be universally captured
|
70
|
+
#
|
71
|
+
# See {#generate_next_invoices} for an example use.
|
72
|
+
#
|
73
|
+
# @param task_name [String] the name to use for this task in the generated log.
|
74
|
+
# @param method [Symbol] the method to call on each one of the given items.
|
75
|
+
# @param collection_getter [Proc] a block which should return the collection to use.
|
76
|
+
def batch_runner(task_name, method, &collection_getter)
|
77
|
+
collection = begin
|
78
|
+
collection_getter.call
|
79
|
+
rescue Exception => e
|
80
|
+
failure += 1
|
81
|
+
log_extended("#{task_name}:\nCollection getter failed\n#{e.message}\n\n#{e.backtrace}")
|
82
|
+
return
|
83
|
+
end
|
84
|
+
|
85
|
+
success = 0
|
86
|
+
failure = 0
|
87
|
+
|
88
|
+
collection.each do |item|
|
89
|
+
begin
|
90
|
+
success += 1 if item.send(method)
|
91
|
+
rescue Exception => e
|
92
|
+
failure += 1
|
93
|
+
log_extended("#{task_name}:\n#{e.message}\n#{item.inspect}\n\n#{e.backtrace}")
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
if failure == 0
|
98
|
+
log_summary("Success: #{task_name}, #{success} OK.")
|
99
|
+
else
|
100
|
+
log_summary("Failure: #{task_name}, #{success} OK, #{failure} failed.")
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
# Invoices for running subscriptions which are not trials are generated by this task.
|
105
|
+
# See {Billingly::Subscription#generate_next_invoice} for more information about
|
106
|
+
# how the next invoice for a subscription is created.
|
107
|
+
def generate_next_invoices
|
108
|
+
batch_runner('Generating Invoices', :generate_next_invoice) do
|
109
|
+
Billingly::Subscription
|
110
|
+
.where(is_trial_expiring_on: nil, unsubscribed_on: nil)
|
111
|
+
.readonly(false)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
# Charges all invoices for which the customer has enough balance.
|
116
|
+
# Oldest invoices are charged first, newer invoices should not be charged until
|
117
|
+
# the oldest ones are paid.
|
118
|
+
# See {Billingly::Invoice#charge Invoice#Charge} for more information on
|
119
|
+
# how invoices are charged from the customer's balance.
|
120
|
+
# @param collection [Array<Invoice>] The list of invoices to attempt charging.
|
121
|
+
# Defaults to all invoices in the system.
|
122
|
+
def charge_invoices
|
123
|
+
batch_runner('Charging pending invoices', :charge_pending_invoices) do
|
124
|
+
Billingly::Customer
|
125
|
+
.joins(:invoices)
|
126
|
+
.where(billingly_invoices: {deleted_on: nil, paid_on: nil})
|
127
|
+
.readonly(false)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
# Notifies invoices that have been charged successfully, sending a receipt.
|
132
|
+
# See {Billingly::Invoice#notify_paid Invoice#notify_paid} for more information on
|
133
|
+
# how receipts are sent for paid invoices.
|
134
|
+
def notify_all_paid
|
135
|
+
batch_runner('Notifying Paid Invoices', :notify_paid) do
|
136
|
+
Billingly::Invoice
|
137
|
+
.where('paid_on is not null')
|
138
|
+
.where(deleted_on: nil, notified_paid_on: nil)
|
139
|
+
.readonly(false)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
# Customers are notified about their pending invoices by this task.
|
144
|
+
# See {Billingly::Invoice#notify_pending Invoice#notify_pending} for more info
|
145
|
+
# on how pending invoices are notified.
|
146
|
+
def notify_all_pending
|
147
|
+
batch_runner('Notifying Pending Invoices', :notify_pending) do
|
148
|
+
Billingly::Invoice
|
149
|
+
.where(deleted_on: nil, paid_on: nil, notified_pending_on: nil)
|
150
|
+
.readonly(false)
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
# This task notifies customers when one of their invoices is overdue.
|
155
|
+
# Overdue invoices go together with account deactivations so the email sent
|
156
|
+
# by this task also includes the deactivation notice.
|
157
|
+
#
|
158
|
+
# This task does not perform the actual deactivation, {#deactivate_all_debtors} does.
|
159
|
+
#
|
160
|
+
# See {Billingly::Invoice#notify_overdue Invoice#notify_overdue} for more info
|
161
|
+
# on how overdue invoices are notified.
|
162
|
+
def notify_all_overdue
|
163
|
+
batch_runner('Notifying Pending Invoices', :notify_overdue) do
|
164
|
+
Billingly::Invoice
|
165
|
+
.where('due_on <= ?', Time.now)
|
166
|
+
.where(deleted_on: nil, paid_on: nil, notified_overdue_on: nil)
|
167
|
+
.readonly(false)
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
# This method will deactivate all customers who have overdue {Billingly::Invoice Invoices}.
|
172
|
+
#
|
173
|
+
# This only deactivates the debtor, it does not notify them via email.
|
174
|
+
# Look at {#notify_all_overdue} to see the email notification customers receive.
|
175
|
+
#
|
176
|
+
# See {Billingly::Customer#deactivate_debtor Customer#deactivate_debtor} for more info
|
177
|
+
# on how debtors are deactivated.
|
178
|
+
def deactivate_all_debtors
|
179
|
+
batch_runner('Deactivating Debtors', :deactivate_debtor) do
|
180
|
+
Billingly::Customer.debtors.where(deactivated_since: nil).readonly(false)
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
# Customers may be subscribed for a trial period, and they are supposed to re-subscribe
|
185
|
+
# before their trial expires.
|
186
|
+
#
|
187
|
+
# When their trial expires and they have not yet subscribed to another plan, we
|
188
|
+
# deactivate their account immediately. This method does not email them about the
|
189
|
+
# expired trial.
|
190
|
+
#
|
191
|
+
# See {Billingly::Customer#deactivate_trial_expired Customer#deactivate_trial_expired}
|
192
|
+
# for more info on how trials are deactivated.
|
193
|
+
def deactivate_all_expired_trials
|
194
|
+
batch_runner('Deactivating Expired Trials', :deactivate_trial_expired) do
|
195
|
+
Billingly::Customer.joins(:subscriptions).readonly(false)
|
196
|
+
.where("#{Billingly::Subscription.table_name}.is_trial_expiring_on < ?", Time.now)
|
197
|
+
.where(billingly_subscriptions: {unsubscribed_on: nil})
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
data/config/locales/en.yml
CHANGED
@@ -21,3 +21,8 @@ en:
|
|
21
21
|
paid_on: Paid on
|
22
22
|
plan: Plan
|
23
23
|
amount: Amount
|
24
|
+
your_trial_has_expired: Your trial period has expired
|
25
|
+
your_trial_has_expired_please_choose_a_plan:
|
26
|
+
"Your trial period has ended on %{end_date} please choose a plan and sign up again."
|
27
|
+
please_visit: Please visit
|
28
|
+
|
data/lib/billingly/version.rb
CHANGED
@@ -46,7 +46,10 @@ class CreateBillinglyTables < ActiveRecord::Migration
|
|
46
46
|
t.boolean 'payable_upfront', null: false, default: false
|
47
47
|
t.decimal 'amount', precision: 11, scale: 2, default: 0.0, null: false
|
48
48
|
t.datetime 'unsubscribed_on'
|
49
|
+
t.string 'unsubscribed_because'
|
49
50
|
t.datetime 'is_trial_expiring_on'
|
51
|
+
t.datetime 'notified_trial_will_expire_on'
|
52
|
+
t.datetime 'notified_trial_expired_on'
|
50
53
|
t.references :plan
|
51
54
|
t.timestamps
|
52
55
|
end
|
@@ -5,19 +5,6 @@ You can run it as often as you want.
|
|
5
5
|
"""
|
6
6
|
namespace :billingly do
|
7
7
|
task all: :environment do
|
8
|
-
|
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 'Deactivating all expired trials'
|
15
|
-
Billingly::Customer.deactivate_all_expired_trials
|
16
|
-
puts 'Sending payment receipts'
|
17
|
-
Billingly::Invoice.notify_all_paid
|
18
|
-
puts 'Notifying pending invoices'
|
19
|
-
Billingly::Invoice.notify_all_pending
|
20
|
-
puts 'Notifying deactivated debtors'
|
21
|
-
Billingly::Invoice.notify_all_overdue
|
8
|
+
Billingly::Tasks.new.run_all
|
22
9
|
end
|
23
10
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: billingly
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.4
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,11 +9,11 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-10-
|
12
|
+
date: 2012-10-16 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rails
|
16
|
-
requirement: &
|
16
|
+
requirement: &70310037569740 !ruby/object:Gem::Requirement
|
17
17
|
none: false
|
18
18
|
requirements:
|
19
19
|
- - ~>
|
@@ -21,10 +21,10 @@ dependencies:
|
|
21
21
|
version: 3.2.0
|
22
22
|
type: :runtime
|
23
23
|
prerelease: false
|
24
|
-
version_requirements: *
|
24
|
+
version_requirements: *70310037569740
|
25
25
|
- !ruby/object:Gem::Dependency
|
26
26
|
name: validates_email_format_of
|
27
|
-
requirement: &
|
27
|
+
requirement: &70310037569280 !ruby/object:Gem::Requirement
|
28
28
|
none: false
|
29
29
|
requirements:
|
30
30
|
- - ! '>='
|
@@ -32,10 +32,10 @@ dependencies:
|
|
32
32
|
version: '0'
|
33
33
|
type: :runtime
|
34
34
|
prerelease: false
|
35
|
-
version_requirements: *
|
35
|
+
version_requirements: *70310037569280
|
36
36
|
- !ruby/object:Gem::Dependency
|
37
37
|
name: has_duration
|
38
|
-
requirement: &
|
38
|
+
requirement: &70310037568740 !ruby/object:Gem::Requirement
|
39
39
|
none: false
|
40
40
|
requirements:
|
41
41
|
- - ! '>='
|
@@ -43,10 +43,10 @@ dependencies:
|
|
43
43
|
version: '0'
|
44
44
|
type: :runtime
|
45
45
|
prerelease: false
|
46
|
-
version_requirements: *
|
46
|
+
version_requirements: *70310037568740
|
47
47
|
- !ruby/object:Gem::Dependency
|
48
48
|
name: timecop
|
49
|
-
requirement: &
|
49
|
+
requirement: &70310037568220 !ruby/object:Gem::Requirement
|
50
50
|
none: false
|
51
51
|
requirements:
|
52
52
|
- - ! '>='
|
@@ -54,10 +54,10 @@ dependencies:
|
|
54
54
|
version: '0'
|
55
55
|
type: :development
|
56
56
|
prerelease: false
|
57
|
-
version_requirements: *
|
57
|
+
version_requirements: *70310037568220
|
58
58
|
- !ruby/object:Gem::Dependency
|
59
59
|
name: sqlite3
|
60
|
-
requirement: &
|
60
|
+
requirement: &70310037608420 !ruby/object:Gem::Requirement
|
61
61
|
none: false
|
62
62
|
requirements:
|
63
63
|
- - ! '>='
|
@@ -65,10 +65,10 @@ dependencies:
|
|
65
65
|
version: '0'
|
66
66
|
type: :development
|
67
67
|
prerelease: false
|
68
|
-
version_requirements: *
|
68
|
+
version_requirements: *70310037608420
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
70
|
name: rspec-rails
|
71
|
-
requirement: &
|
71
|
+
requirement: &70310037607540 !ruby/object:Gem::Requirement
|
72
72
|
none: false
|
73
73
|
requirements:
|
74
74
|
- - ! '>='
|
@@ -76,10 +76,10 @@ dependencies:
|
|
76
76
|
version: '0'
|
77
77
|
type: :development
|
78
78
|
prerelease: false
|
79
|
-
version_requirements: *
|
79
|
+
version_requirements: *70310037607540
|
80
80
|
- !ruby/object:Gem::Dependency
|
81
81
|
name: factory_girl_rails
|
82
|
-
requirement: &
|
82
|
+
requirement: &70310037607120 !ruby/object:Gem::Requirement
|
83
83
|
none: false
|
84
84
|
requirements:
|
85
85
|
- - ! '>='
|
@@ -87,7 +87,7 @@ dependencies:
|
|
87
87
|
version: '0'
|
88
88
|
type: :development
|
89
89
|
prerelease: false
|
90
|
-
version_requirements: *
|
90
|
+
version_requirements: *70310037607120
|
91
91
|
description: Rails Engine for SaaS subscription management. Manage subscriptions,
|
92
92
|
plan changes, free trials and more!!!
|
93
93
|
email:
|
@@ -97,7 +97,8 @@ extensions: []
|
|
97
97
|
extra_rdoc_files: []
|
98
98
|
files:
|
99
99
|
- app/controllers/billingly/subscriptions_controller.rb
|
100
|
-
- app/mailers/
|
100
|
+
- app/mailers/billingly/base_mailer.rb
|
101
|
+
- app/mailers/billingly/mailer.rb
|
101
102
|
- app/models/billingly/base_customer.rb
|
102
103
|
- app/models/billingly/base_plan.rb
|
103
104
|
- app/models/billingly/base_subscription.rb
|
@@ -111,6 +112,14 @@ files:
|
|
111
112
|
- app/models/billingly/plan.rb
|
112
113
|
- app/models/billingly/receipt.rb
|
113
114
|
- app/models/billingly/subscription.rb
|
115
|
+
- app/models/billingly/tasks.rb
|
116
|
+
- app/views/billingly/mailer/overdue_notification.html.erb
|
117
|
+
- app/views/billingly/mailer/paid_notification.html.erb
|
118
|
+
- app/views/billingly/mailer/pending_notification.html.erb
|
119
|
+
- app/views/billingly/mailer/pending_notification.plain.erb
|
120
|
+
- app/views/billingly/mailer/pending_notification.text.erb
|
121
|
+
- app/views/billingly/mailer/task_results.text.erb
|
122
|
+
- app/views/billingly/mailer/trial_expired_notification.text.erb
|
114
123
|
- app/views/billingly/subscriptions/_current_subscription.html.haml
|
115
124
|
- app/views/billingly/subscriptions/_deactivation_notice.html.haml
|
116
125
|
- app/views/billingly/subscriptions/_invoice_details.html.haml
|
@@ -120,11 +129,6 @@ files:
|
|
120
129
|
- app/views/billingly/subscriptions/index.html.haml
|
121
130
|
- app/views/billingly/subscriptions/invoice.html.haml
|
122
131
|
- app/views/billingly/subscriptions/new.html.erb
|
123
|
-
- app/views/billingly_mailer/overdue_notification.html.erb
|
124
|
-
- app/views/billingly_mailer/paid_notification.html.erb
|
125
|
-
- app/views/billingly_mailer/pending_notification.html.erb
|
126
|
-
- app/views/billingly_mailer/pending_notification.plain.erb
|
127
|
-
- app/views/billingly_mailer/pending_notification.text.erb
|
128
132
|
- config/locales/en.yml
|
129
133
|
- lib/billingly/engine.rb
|
130
134
|
- lib/billingly/rails/routes.rb
|
@@ -141,7 +145,21 @@ files:
|
|
141
145
|
- TUTORIAL.rdoc
|
142
146
|
homepage: http://billing.ly
|
143
147
|
licenses: []
|
144
|
-
post_install_message:
|
148
|
+
post_install_message: ! " Add the following migration:\n \n class CreateBillinglyTables
|
149
|
+
< ActiveRecord::Migration\n def up\n add_column :billingly_subscriptions,
|
150
|
+
:notified_trial_will_expire_on, :datetime\n add_column :billingly_subscriptions,
|
151
|
+
:notified_trial_expired_on, :datetime\n add_column :billingly_subscriptions,
|
152
|
+
:unsubscribed_because, :string\n \n Billingly::Subscription.where('unsubscribed_on
|
153
|
+
IS NOT NULL').find_each do |s|\n # Notice: You should pre-populate unsubscribed
|
154
|
+
because\n # with an appropriate value for each terminated subscription.\n
|
155
|
+
\ # \n reason = if s == s.customer.subscriptions.last && s.customer.deactivation_reason\n
|
156
|
+
\ s.deactivation_reason\n elsif s.trial? && s.is_trial_expiring_on
|
157
|
+
<= s.unsubscribed_on\n 'trial_expired'\n else\n 'changed_subscription'\n
|
158
|
+
\ end\n s.update_attribute(unsubscribed_because: reason)\n end\n
|
159
|
+
\ end\n \n def down\n remove_column :billingly_subscriptions,
|
160
|
+
:notified_trial_will_expire_on\n remove_column :billingly_subscriptions,
|
161
|
+
:notified_trial_expired_on\n remove_column :billingly_subscriptions, :unsubscribed_because\n
|
162
|
+
\ end\n end\n"
|
145
163
|
rdoc_options: []
|
146
164
|
require_paths:
|
147
165
|
- lib
|