profitable 0.2.0 → 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cd833a8b05f27ea248099d3965e9d491a13f90c51c0ad29aa524384d6b92ddce
4
- data.tar.gz: 3b57ba527cc3ac287688222be53415d16f9bdf2635b07496883e50da3671e77f
3
+ metadata.gz: e3487fdc62384e3d5900e96c7642b10d44a2d701406677702b8608585f1fd3a5
4
+ data.tar.gz: e455302bf61fc7aa7aa0f0f369406bb3fd24ebc3e9b13e56516eb89fb5508990
5
5
  SHA512:
6
- metadata.gz: daf28e6f4a6da261bb56fce23abb8a0a4f417d2c637eff0c1d696abcd19ebd7f123a283eddffbf66509ef9edf08f4e2ca4e0d9818c64ee9b2f7a188c7dafd2ca
7
- data.tar.gz: 982180e6b08385d430a3a7576d9e474ff96d108d086aec428272ca2d2399489eb663837efe85f0b03fe830350cab72ab25e264d498951fed1f4cef72c6231ccf
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(multiplier = "3x")`: Estimated company valuation based on ARR
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("5x").to_readable # => "$500,000"
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.
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Profitable
4
- VERSION = "0.2.0"
4
+ VERSION = "0.2.2"
5
5
  end
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 = "3x")
50
- NumericResult.new(calculate_estimated_valuation(multiplier))
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 = "3x")
131
- multiplier = multiplier.to_s.gsub('x', '').to_f
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
- churned_subscriptions(period).sum do |subscription|
158
- MrrCalculator.process_subscription(subscription)
159
- end
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
- new_subscriptions = Pay::Subscription
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
- new_subscriptions.sum do |subscription|
172
- MrrCalculator.process_subscription(subscription)
173
- end
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
- actual_customers.count
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
- return 0 if total_customers.zero?
231
- (all_time_revenue.to_f / total_customers).round
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.0
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-08-31 00:00:00.000000000 Z
11
+ date: 2024-09-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: pay