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 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