profitable 0.2.0 → 0.2.2
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 +11 -0
- data/README.md +6 -21
- data/lib/profitable/version.rb +1 -1
- data/lib/profitable.rb +65 -18
- 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: e3487fdc62384e3d5900e96c7642b10d44a2d701406677702b8608585f1fd3a5
|
4
|
+
data.tar.gz: e455302bf61fc7aa7aa0f0f369406bb3fd24ebc3e9b13e56516eb89fb5508990
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b0b107c91d16eabd6867b6bb4eafc0392272a41e6380f4c1f02be690f97f994fa4cb6a17d900ab930316ec0e682b391b6d36807d041387680e81e92a8150d774
|
7
|
+
data.tar.gz: 3116c70c4ba0257c59939da5ac83a10a841df147eaba9d8a1980bb7a2e7b1103ab4cbc15317461809ad3927cf5ffd612151a7538e3cfa34bdb47364c0f96bf60
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,16 @@
|
|
1
1
|
# `profitable`
|
2
2
|
|
3
|
+
## [0.2.2] - 2024-09-01
|
4
|
+
|
5
|
+
- Improve MRR calculations with prorated churned and new MRR (hopefully fixes bad churned MRR calculations)
|
6
|
+
- Only consider paid charges for all revenue calculations (hopefully fixes bad ARPC calculations)
|
7
|
+
- Add `multiple:` parameter as another option for `estimated_valuation` (same as `at:`, just syntactic sugar)
|
8
|
+
|
9
|
+
## [0.2.1] - 2024-08-31
|
10
|
+
|
11
|
+
- Add syntactic sugar for `estimated_valuation(at: "3x")`
|
12
|
+
- Now `estimated_valuation` also supports `Numeric`-only inputs like `estimated_valuation(3)`, so that @pretzelhands can avoid writing 3 extra characters and we embrace actual syntactic sugar instead of "syntactic saccharine" (sic.)
|
13
|
+
|
3
14
|
## [0.2.0] - 2024-08-31
|
4
15
|
|
5
16
|
- Initial production ready release
|
data/README.md
CHANGED
@@ -59,7 +59,7 @@ All methods return numbers that can be converted to a nicely-formatted, human-re
|
|
59
59
|
- `Profitable.churned_mrr(in_the_last: 30.days)`: MRR lost due to churn in the specified period
|
60
60
|
- `Profitable.average_revenue_per_customer`: Average revenue per customer (ARPC)
|
61
61
|
- `Profitable.lifetime_value`: Estimated customer lifetime value (LTV)
|
62
|
-
- `Profitable.estimated_valuation(
|
62
|
+
- `Profitable.estimated_valuation(at: "3x")`: Estimated company valuation based on ARR
|
63
63
|
|
64
64
|
### Customer metrics
|
65
65
|
|
@@ -100,8 +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
|
+
|
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"
|
105
108
|
|
106
109
|
# Get the time to next MRR milestone
|
107
110
|
Profitable.time_to_next_mrr_milestone.to_readable # => "26 days left to $10,000 MRR"
|
@@ -128,24 +131,6 @@ Profitable.mrr # => 123456
|
|
128
131
|
- `mrr_growth_rate`: This calculation compares the MRR at the start and end of the specified period. It assumes a linear growth rate over the period, which may not reflect short-term fluctuations. For more accurate results, consider using shorter periods or implementing a more sophisticated growth calculation method if needed.
|
129
132
|
- `time_to_next_mrr_milestone`: This estimation is based on the current MRR and the recent growth rate. It assumes a constant growth rate, which may not reflect real-world conditions. The calculation may be inaccurate for very new businesses or those with irregular growth patterns.
|
130
133
|
|
131
|
-
## Mount the `/profitable` dashboard
|
132
|
-
|
133
|
-
We also provide a simple dashboard with good defaults to see your main business metrics.
|
134
|
-
|
135
|
-
In your `config/routes.rb` file, mount the `profitable` engine:
|
136
|
-
```ruby
|
137
|
-
mount Profitable::Engine => '/profitable'
|
138
|
-
```
|
139
|
-
|
140
|
-
It's a good idea to make sure you're adding some sort of authentication to the `/profitable` route to avoid exposing sensitive information:
|
141
|
-
```ruby
|
142
|
-
authenticate :user, ->(user) { user.admin? } do
|
143
|
-
mount Profitable::Engine => '/profitable'
|
144
|
-
end
|
145
|
-
```
|
146
|
-
|
147
|
-
You can now navigate to `/profitable` to see your app's business metrics like MRR, ARR, churn, etc.
|
148
|
-
|
149
134
|
## Development
|
150
135
|
|
151
136
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
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,9 @@ 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
|
-
|
49
|
+
def estimated_valuation(multiplier = nil, at: nil, multiple: nil)
|
50
|
+
actual_multiplier = multiplier || at || multiple || 3
|
51
|
+
NumericResult.new(calculate_estimated_valuation(actual_multiplier))
|
51
52
|
end
|
52
53
|
|
53
54
|
def total_customers
|
@@ -127,11 +128,26 @@ module Profitable
|
|
127
128
|
(mrr.to_f * 12).round
|
128
129
|
end
|
129
130
|
|
130
|
-
def calculate_estimated_valuation(multiplier =
|
131
|
-
multiplier = multiplier
|
131
|
+
def calculate_estimated_valuation(multiplier = 3)
|
132
|
+
multiplier = parse_multiplier(multiplier)
|
132
133
|
(calculate_arr * multiplier).round
|
133
134
|
end
|
134
135
|
|
136
|
+
def parse_multiplier(input)
|
137
|
+
case input
|
138
|
+
when Numeric
|
139
|
+
input.to_f
|
140
|
+
when String
|
141
|
+
if input.end_with?('x')
|
142
|
+
input.chomp('x').to_f
|
143
|
+
else
|
144
|
+
input.to_f
|
145
|
+
end
|
146
|
+
else
|
147
|
+
3.0 # Default multiplier if input is invalid
|
148
|
+
end.clamp(0.1, 100) # Ensure multiplier is within a reasonable range
|
149
|
+
end
|
150
|
+
|
135
151
|
def calculate_churn(period = DEFAULT_PERIOD)
|
136
152
|
start_date = period.ago
|
137
153
|
total_subscribers_start = Pay::Subscription.active.where('created_at < ?', start_date).distinct.count('customer_id')
|
@@ -154,23 +170,50 @@ module Profitable
|
|
154
170
|
end
|
155
171
|
|
156
172
|
def calculate_churned_mrr(period = DEFAULT_PERIOD)
|
157
|
-
|
158
|
-
|
159
|
-
|
173
|
+
start_date = period.ago
|
174
|
+
end_date = Time.current
|
175
|
+
|
176
|
+
Pay::Subscription
|
177
|
+
.includes(:customer)
|
178
|
+
.select('pay_subscriptions.*, pay_customers.processor as customer_processor')
|
179
|
+
.joins(:customer)
|
180
|
+
.where(status: ['canceled', 'ended'])
|
181
|
+
.where('pay_subscriptions.updated_at BETWEEN ? AND ?', start_date, end_date)
|
182
|
+
.sum do |subscription|
|
183
|
+
if subscription.ends_at && subscription.ends_at > end_date
|
184
|
+
# Subscription ends in the future, don't count it as churned yet
|
185
|
+
0
|
186
|
+
else
|
187
|
+
# Calculate prorated MRR if the subscription ended within the period
|
188
|
+
end_date = [subscription.ends_at, end_date].compact.min
|
189
|
+
days_in_period = (end_date - start_date).to_i
|
190
|
+
total_days = (subscription.current_period_end - subscription.current_period_start).to_i
|
191
|
+
prorated_days = [days_in_period, total_days].min
|
192
|
+
|
193
|
+
mrr = MrrCalculator.process_subscription(subscription)
|
194
|
+
(mrr.to_f * prorated_days / total_days).round
|
195
|
+
end
|
196
|
+
end
|
160
197
|
end
|
161
198
|
|
162
199
|
def calculate_new_mrr(period = DEFAULT_PERIOD)
|
163
|
-
|
200
|
+
start_date = period.ago
|
201
|
+
end_date = Time.current
|
202
|
+
|
203
|
+
Pay::Subscription
|
164
204
|
.active
|
165
|
-
.where(pay_subscriptions: { created_at: period.ago..Time.current })
|
166
|
-
.where.not(status: ['trialing', 'paused'])
|
167
205
|
.includes(:customer)
|
168
206
|
.select('pay_subscriptions.*, pay_customers.processor as customer_processor')
|
169
207
|
.joins(:customer)
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
208
|
+
.where(created_at: start_date..end_date)
|
209
|
+
.where.not(status: ['trialing', 'paused'])
|
210
|
+
.sum do |subscription|
|
211
|
+
mrr = MrrCalculator.process_subscription(subscription)
|
212
|
+
days_in_period = (end_date - subscription.created_at).to_i
|
213
|
+
total_days = (subscription.current_period_end - subscription.current_period_start).to_i
|
214
|
+
prorated_days = [days_in_period, total_days].min
|
215
|
+
(mrr.to_f * prorated_days / total_days).round
|
216
|
+
end
|
174
217
|
end
|
175
218
|
|
176
219
|
def calculate_revenue_in_period(period)
|
@@ -194,7 +237,10 @@ module Profitable
|
|
194
237
|
end
|
195
238
|
|
196
239
|
def calculate_total_customers
|
197
|
-
|
240
|
+
Pay::Customer.joins(:charges)
|
241
|
+
.merge(paid_charges)
|
242
|
+
.distinct
|
243
|
+
.count
|
198
244
|
end
|
199
245
|
|
200
246
|
def calculate_total_subscribers
|
@@ -227,8 +273,9 @@ module Profitable
|
|
227
273
|
end
|
228
274
|
|
229
275
|
def calculate_average_revenue_per_customer
|
230
|
-
|
231
|
-
|
276
|
+
paying_customers = calculate_total_customers
|
277
|
+
return 0 if paying_customers.zero?
|
278
|
+
(all_time_revenue.to_f / paying_customers).round
|
232
279
|
end
|
233
280
|
|
234
281
|
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.2
|
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
|