profitable 0.2.1 → 0.2.3

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: bf77ba92fb941bb21624c1f677bbfff37d874fefa06050e1fe6b395ae55ea3e2
4
- data.tar.gz: 8d4cf7614e186d7082692faa36794916111205293a45aac055565c2e7923a24b
3
+ metadata.gz: 506d907bbcbaa811d4986d49fffdc7f1874c77528fb99996f49621834a15e263
4
+ data.tar.gz: 6e37aaa616a3238b85cacfd4d3b9373781b4632fb4e4e36ad7946a8d71fa066e
5
5
  SHA512:
6
- metadata.gz: 73c2251e3d2566b1d22f7319d179f9d8f1077dd70354add0fb8718232f094f613fd4aef048a0accbba8214d5ac4911497d4c6704199cf17470be6ed5ad485e06
7
- data.tar.gz: 30abc5f18398a6e1553effc66b59407f43610a90a01448dae90dab8eeb74198921593c3e6cb8bea9b63cce14f9435eccb69a53c1d71912b9214392b3e7375e20
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(at: "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
105
 
106
- # You can also pass the multiplier as a number, and/or ignore the "at:" keyword altogether
107
- Profitable.estimated_valuation(4.5).to_readable # => "$450,000"
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"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Profitable
4
- VERSION = "0.2.1"
4
+ VERSION = "0.2.3"
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,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 = 3, at: nil)
50
- actual_multiplier = at || multiplier
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
- growth_rate = calculate_mrr_growth_rate
108
- return "Unable to calculate. Need more data or positive growth." if growth_rate <= 0
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
- months_to_milestone = (Math.log(next_milestone.to_f / current_mrr) / Math.log(1 + growth_rate)).ceil
111
- days_to_milestone = months_to_milestone * 30
110
+ # Convert monthly growth rate to daily growth rate
111
+ daily_growth_rate = (1 + monthly_growth_rate) ** (1.0 / 30) - 1
112
112
 
113
- return "#{days_to_milestone} days left to $#{number_with_delimiter(next_milestone)} MRR (#{(Time.current + days_to_milestone.days).strftime('%b %d, %Y')})"
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
- churned_subscriptions(period).sum do |subscription|
174
- MrrCalculator.process_subscription(subscription)
175
- end
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
- new_subscriptions = Pay::Subscription
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
- new_subscriptions.sum do |subscription|
188
- MrrCalculator.process_subscription(subscription)
189
- end
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
- actual_customers.count
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
- return 0 if total_customers.zero?
247
- (all_time_revenue.to_f / total_customers).round
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.1
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-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