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