billingly 0.1.1 → 0.1.4
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.
- 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
|