profitable 0.2.1 → 0.2.3
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +10 -0
- data/README.md +4 -4
- data/lib/profitable/version.rb +1 -1
- data/lib/profitable.rb +57 -21
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 506d907bbcbaa811d4986d49fffdc7f1874c77528fb99996f49621834a15e263
|
|
4
|
+
data.tar.gz: 6e37aaa616a3238b85cacfd4d3b9373781b4632fb4e4e36ad7946a8d71fa066e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: aafa273d6430cd1e4c30dada038bdc83a9ecacec8c2d9be0f9f6bad642dc818399dbb494876244eb50c96c1aa3af7932ef43f56e839ab11a8b8f036a375f822f
|
|
7
|
+
data.tar.gz: 7dca54330874ae397c77b2400364429509dffc5de96a2096fe963984455ff4c46e67849c5305218086684beb7b1d5300815f49af252ba9e875c028c42b717c1a
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# `profitable`
|
|
2
2
|
|
|
3
|
+
## [0.2.3] - 2024-09-01
|
|
4
|
+
|
|
5
|
+
- Fix the `time_to_next_mrr_milestone` estimation and make it accurate to the day
|
|
6
|
+
|
|
7
|
+
## [0.2.2] - 2024-09-01
|
|
8
|
+
|
|
9
|
+
- Improve MRR calculations with prorated churned and new MRR (hopefully fixes bad churned MRR calculations)
|
|
10
|
+
- Only consider paid charges for all revenue calculations (hopefully fixes bad ARPC calculations)
|
|
11
|
+
- Add `multiple:` parameter as another option for `estimated_valuation` (same as `at:`, just syntactic sugar)
|
|
12
|
+
|
|
3
13
|
## [0.2.1] - 2024-08-31
|
|
4
14
|
|
|
5
15
|
- Add syntactic sugar for `estimated_valuation(at: "3x")`
|
data/README.md
CHANGED
|
@@ -100,11 +100,11 @@ Profitable.churn(in_the_last: 3.months).to_readable # => "12%"
|
|
|
100
100
|
# You can specify the precision of the output number (no decimals by default)
|
|
101
101
|
Profitable.new_mrr(in_the_last: 24.hours).to_readable(2) # => "$123.45"
|
|
102
102
|
|
|
103
|
-
# Get the estimated valuation at 5x ARR
|
|
104
|
-
Profitable.estimated_valuation(
|
|
103
|
+
# Get the estimated valuation at 5x ARR (defaults to 3x if no multiple is specified)
|
|
104
|
+
Profitable.estimated_valuation(multiple: 5).to_readable # => "$500,000"
|
|
105
105
|
|
|
106
|
-
# You can also pass the multiplier as a
|
|
107
|
-
Profitable.estimated_valuation(4.
|
|
106
|
+
# You can also pass the multiplier as a string. You can also use the `at:` keyword argument (same thing as `multiplier:`) – and/or ignore the `at:` or `multiplier:` named arguments altogether
|
|
107
|
+
Profitable.estimated_valuation(at: "4.5x").to_readable # => "$450,000"
|
|
108
108
|
|
|
109
109
|
# Get the time to next MRR milestone
|
|
110
110
|
Profitable.time_to_next_mrr_milestone.to_readable # => "26 days left to $10,000 MRR"
|
data/lib/profitable/version.rb
CHANGED
data/lib/profitable.rb
CHANGED
|
@@ -16,7 +16,7 @@ module Profitable
|
|
|
16
16
|
include ActionView::Helpers::NumberHelper
|
|
17
17
|
|
|
18
18
|
DEFAULT_PERIOD = 30.days
|
|
19
|
-
MRR_MILESTONES = [100, 1_000, 10_000, 100_000, 1_000_000, 10_000_000, 100_000_000]
|
|
19
|
+
MRR_MILESTONES = [5, 10, 20, 30, 50, 75, 100, 200, 300, 400, 500, 1_000, 2_000, 3_000, 5_000, 10_000, 20_000, 30_000, 50_000, 83_333, 100_000, 250_000, 500_000, 1_000_000, 5_000_000, 10_000_000, 25_000_000, 50_000_000, 75_000_000, 100_000_000]
|
|
20
20
|
|
|
21
21
|
def mrr
|
|
22
22
|
NumericResult.new(MrrCalculator.calculate)
|
|
@@ -46,8 +46,8 @@ module Profitable
|
|
|
46
46
|
NumericResult.new(calculate_recurring_revenue_percentage(in_the_last), :percentage)
|
|
47
47
|
end
|
|
48
48
|
|
|
49
|
-
def estimated_valuation(multiplier =
|
|
50
|
-
actual_multiplier = at ||
|
|
49
|
+
def estimated_valuation(multiplier = nil, at: nil, multiple: nil)
|
|
50
|
+
actual_multiplier = multiplier || at || multiple || 3
|
|
51
51
|
NumericResult.new(calculate_estimated_valuation(actual_multiplier))
|
|
52
52
|
end
|
|
53
53
|
|
|
@@ -104,13 +104,18 @@ module Profitable
|
|
|
104
104
|
next_milestone = MRR_MILESTONES.find { |milestone| milestone > current_mrr }
|
|
105
105
|
return "Congratulations! You've reached the highest milestone." unless next_milestone
|
|
106
106
|
|
|
107
|
-
|
|
108
|
-
return "Unable to calculate. Need more data or positive growth." if
|
|
107
|
+
monthly_growth_rate = calculate_mrr_growth_rate / 100
|
|
108
|
+
return "Unable to calculate. Need more data or positive growth." if monthly_growth_rate <= 0
|
|
109
109
|
|
|
110
|
-
|
|
111
|
-
|
|
110
|
+
# Convert monthly growth rate to daily growth rate
|
|
111
|
+
daily_growth_rate = (1 + monthly_growth_rate) ** (1.0 / 30) - 1
|
|
112
112
|
|
|
113
|
-
|
|
113
|
+
# Calculate the number of days to reach the next milestone
|
|
114
|
+
days_to_milestone = (Math.log(next_milestone.to_f / current_mrr) / Math.log(1 + daily_growth_rate)).ceil
|
|
115
|
+
|
|
116
|
+
target_date = Time.current + days_to_milestone.days
|
|
117
|
+
|
|
118
|
+
"#{days_to_milestone} days left to $#{number_with_delimiter(next_milestone)} MRR (#{target_date.strftime('%b %d, %Y')})"
|
|
114
119
|
end
|
|
115
120
|
|
|
116
121
|
private
|
|
@@ -170,23 +175,50 @@ module Profitable
|
|
|
170
175
|
end
|
|
171
176
|
|
|
172
177
|
def calculate_churned_mrr(period = DEFAULT_PERIOD)
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
178
|
+
start_date = period.ago
|
|
179
|
+
end_date = Time.current
|
|
180
|
+
|
|
181
|
+
Pay::Subscription
|
|
182
|
+
.includes(:customer)
|
|
183
|
+
.select('pay_subscriptions.*, pay_customers.processor as customer_processor')
|
|
184
|
+
.joins(:customer)
|
|
185
|
+
.where(status: ['canceled', 'ended'])
|
|
186
|
+
.where('pay_subscriptions.updated_at BETWEEN ? AND ?', start_date, end_date)
|
|
187
|
+
.sum do |subscription|
|
|
188
|
+
if subscription.ends_at && subscription.ends_at > end_date
|
|
189
|
+
# Subscription ends in the future, don't count it as churned yet
|
|
190
|
+
0
|
|
191
|
+
else
|
|
192
|
+
# Calculate prorated MRR if the subscription ended within the period
|
|
193
|
+
end_date = [subscription.ends_at, end_date].compact.min
|
|
194
|
+
days_in_period = (end_date - start_date).to_i
|
|
195
|
+
total_days = (subscription.current_period_end - subscription.current_period_start).to_i
|
|
196
|
+
prorated_days = [days_in_period, total_days].min
|
|
197
|
+
|
|
198
|
+
mrr = MrrCalculator.process_subscription(subscription)
|
|
199
|
+
(mrr.to_f * prorated_days / total_days).round
|
|
200
|
+
end
|
|
201
|
+
end
|
|
176
202
|
end
|
|
177
203
|
|
|
178
204
|
def calculate_new_mrr(period = DEFAULT_PERIOD)
|
|
179
|
-
|
|
205
|
+
start_date = period.ago
|
|
206
|
+
end_date = Time.current
|
|
207
|
+
|
|
208
|
+
Pay::Subscription
|
|
180
209
|
.active
|
|
181
|
-
.where(pay_subscriptions: { created_at: period.ago..Time.current })
|
|
182
|
-
.where.not(status: ['trialing', 'paused'])
|
|
183
210
|
.includes(:customer)
|
|
184
211
|
.select('pay_subscriptions.*, pay_customers.processor as customer_processor')
|
|
185
212
|
.joins(:customer)
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
213
|
+
.where(created_at: start_date..end_date)
|
|
214
|
+
.where.not(status: ['trialing', 'paused'])
|
|
215
|
+
.sum do |subscription|
|
|
216
|
+
mrr = MrrCalculator.process_subscription(subscription)
|
|
217
|
+
days_in_period = (end_date - subscription.created_at).to_i
|
|
218
|
+
total_days = (subscription.current_period_end - subscription.current_period_start).to_i
|
|
219
|
+
prorated_days = [days_in_period, total_days].min
|
|
220
|
+
(mrr.to_f * prorated_days / total_days).round
|
|
221
|
+
end
|
|
190
222
|
end
|
|
191
223
|
|
|
192
224
|
def calculate_revenue_in_period(period)
|
|
@@ -210,7 +242,10 @@ module Profitable
|
|
|
210
242
|
end
|
|
211
243
|
|
|
212
244
|
def calculate_total_customers
|
|
213
|
-
|
|
245
|
+
Pay::Customer.joins(:charges)
|
|
246
|
+
.merge(paid_charges)
|
|
247
|
+
.distinct
|
|
248
|
+
.count
|
|
214
249
|
end
|
|
215
250
|
|
|
216
251
|
def calculate_total_subscribers
|
|
@@ -243,8 +278,9 @@ module Profitable
|
|
|
243
278
|
end
|
|
244
279
|
|
|
245
280
|
def calculate_average_revenue_per_customer
|
|
246
|
-
|
|
247
|
-
|
|
281
|
+
paying_customers = calculate_total_customers
|
|
282
|
+
return 0 if paying_customers.zero?
|
|
283
|
+
(all_time_revenue.to_f / paying_customers).round
|
|
248
284
|
end
|
|
249
285
|
|
|
250
286
|
def calculate_lifetime_value
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: profitable
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.2.
|
|
4
|
+
version: 0.2.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- rameerez
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2024-
|
|
11
|
+
date: 2024-09-01 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: pay
|