profitable 0.1.0 → 0.2.0

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: f82abb3e6ad9cf2adc1c3bc0784458f78151171767d5435844506937432b06f1
4
- data.tar.gz: c688c5cff7678cdbe18fce6d450040b49a4055329953ca2845dce01923ed396a
3
+ metadata.gz: cd833a8b05f27ea248099d3965e9d491a13f90c51c0ad29aa524384d6b92ddce
4
+ data.tar.gz: 3b57ba527cc3ac287688222be53415d16f9bdf2635b07496883e50da3671e77f
5
5
  SHA512:
6
- metadata.gz: 209dd44cb7bbeeca27cd6cd738470bc2179e1cb748669e5c740b6e9d583fcb2a22f6e7af3af87c37b2b9c1967b05d30fe0076d3bf44bf2c791aa9299af94c1d4
7
- data.tar.gz: e50c1a56b94f3da49bafa494dfc77a593ef82df75b7d26211e1c5860846a020897d187e006eba1d451a8694e1bd14eb0e951df66eee5c48d83828c9a60fe03f2
6
+ metadata.gz: daf28e6f4a6da261bb56fce23abb8a0a4f417d2c637eff0c1d696abcd19ebd7f123a283eddffbf66509ef9edf08f4e2ca4e0d9818c64ee9b2f7a188c7dafd2ca
7
+ data.tar.gz: 982180e6b08385d430a3a7576d9e474ff96d108d086aec428272ca2d2399489eb663837efe85f0b03fe830350cab72ab25e264d498951fed1f4cef72c6231ccf
data/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
- ## [Unreleased]
1
+ # `profitable`
2
+
3
+ ## [0.2.0] - 2024-08-31
4
+
5
+ - Initial production ready release
2
6
 
3
7
  ## [0.1.0] - 2024-08-29
4
8
 
5
- - Initial release
9
+ - Initial test release (not production ready)
data/README.md CHANGED
@@ -52,27 +52,34 @@ All methods return numbers that can be converted to a nicely-formatted, human-re
52
52
  - `Profitable.mrr`: Monthly Recurring Revenue (MRR)
53
53
  - `Profitable.arr`: Annual Recurring Revenue (ARR)
54
54
  - `Profitable.all_time_revenue`: Total revenue since launch
55
+ - `Profitable.revenue_in_period(in_the_last: 30.days)`: Total revenue (recurring and non-recurring) in the specified period
56
+ - `Profitable.recurring_revenue_in_period(in_the_last: 30.days)`: Only recurring revenue in the specified period
57
+ - `Profitable.recurring_revenue_percentage(in_the_last: 30.days)`: Percentage of revenue that is recurring in the specified period
55
58
  - `Profitable.new_mrr(in_the_last: 30.days)`: New MRR added in the specified period
56
59
  - `Profitable.churned_mrr(in_the_last: 30.days)`: MRR lost due to churn in the specified period
57
60
  - `Profitable.average_revenue_per_customer`: Average revenue per customer (ARPC)
58
61
  - `Profitable.lifetime_value`: Estimated customer lifetime value (LTV)
62
+ - `Profitable.estimated_valuation(multiplier = "3x")`: Estimated company valuation based on ARR
59
63
 
60
64
  ### Customer metrics
61
65
 
62
- - `Profitable.total_customers`: Total number of customers
63
- - `Profitable.total_subscribers`: Total number of active subscribers
64
- - `Profitable.new_customers(in_the_last: 30.days)`: Number of new customers (both subscribers and non-subscribers) added in the specified period
66
+ - `Profitable.total_customers`: Total number of customers who have ever made a purchase or had a subscription (current and past)
67
+ - `Profitable.total_subscribers`: Total number of customers who have ever had a subscription (active or not)
68
+ - `Profitable.active_subscribers`: Number of customers with currently active subscriptions
69
+ - `Profitable.new_customers(in_the_last: 30.days)`: Number of new customers added in the specified period
65
70
  - `Profitable.new_subscribers(in_the_last: 30.days)`: Number of new subscribers added in the specified period
66
71
  - `Profitable.churned_customers(in_the_last: 30.days)`: Number of customers who churned in the specified period
67
72
 
68
73
  ### Other metrics
69
74
 
70
- - `Profitable.churn(in_the_last: 30.days)`: Churn rate as a percentage
71
- - `Profitable.estimated_valuation(multiplier = "3x")`: Estimated valuation based on ARR
75
+ - `Profitable.churn(in_the_last: 30.days)`: Churn rate for the specified period
76
+ - `Profitable.mrr_growth_rate(in_the_last: 30.days)`: MRR growth rate for the specified period
77
+ - `Profitable.time_to_next_mrr_milestone`: Estimated time to reach the next MRR milestone
72
78
 
73
79
  ### Growth metrics
74
80
 
75
- - `Profitable.mrr_growth_rate(period: 30.days)`: Calculates the MRR growth rate over the specified period
81
+ - `Profitable.mrr_growth(in_the_last: 30.days)`: Calculates the absolute MRR growth over the specified period
82
+ - `Profitable.mrr_growth_rate(in_the_last: 30.days)`: Calculates the MRR growth rate (as a percentage) over the specified period
76
83
 
77
84
  ### Milestone metrics
78
85
 
@@ -146,15 +153,16 @@ After checking out the repo, run `bin/setup` to install dependencies. Then, run
146
153
  To install this gem onto your local machine, run `bundle exec rake install`.
147
154
 
148
155
  ## TODO
156
+ - [ ] Calculate split by plan / add support for multiple plans (churn by plan, MRR by plan, etc) – not just aggregated
157
+ - [ ] Calculate MRR expansion (plan upgrades), contraction (plan downgrades), etc. like Stripe does
149
158
  - [ ] Add active customers (not just total customers)
150
- - [ ] Add revenue last month to dashboard
151
- - [ ] Add % of change over last period
152
- - [ ] Support other currencies other than USD
153
- - [ ] Support for multiple plans (churn by plan, MRR by plan, etc)
154
- - [ ] Make sure other payment processors other than Stripe work as intended
155
- - [ ] Account for subscription upgrades/downgrades within a period
159
+ - [ ] Add % of change over last period (this period vs last period)
160
+ - [ ] Calculate total period revenue vs period recurring revenue (started, but not sure if accurate)
161
+ - [ ] Add revenue last month to dashboard (not just past 30d, like previous month)
162
+ - [ ] Support other currencies other than USD (convert currencies)
163
+ - [ ] Make sure other payment processors other than Stripe work as intended (Paddle, Braintree, etc. – I've never used them)
156
164
  - [ ] Add a way to input monthly costs (maybe via config file?) so that we can calculate a profit margin %
157
- - [ ] Allow dashboard configuration via config file
165
+ - [ ] Allow dashboard configuration via config file (which metrics to show, etc.)
158
166
  - [ ] Return a JSON in the dashboard endpoint with main metrics (for monitoring / downstream consumption)
159
167
 
160
168
  ## Contributing
@@ -88,6 +88,7 @@
88
88
  <h2><%= Profitable.churn(in_the_last: period).to_readable %></h2>
89
89
  <p>churn (<%= period_short %>)</p>
90
90
  </div>
91
+
91
92
  <div class="card">
92
93
  <h2><%= Profitable.new_mrr(in_the_last: period).to_readable %></h2>
93
94
  <p>new MRR (<%= period_short %>)</p>
@@ -96,6 +97,16 @@
96
97
  <h2><%= Profitable.churned_mrr(in_the_last: period).to_readable %></h2>
97
98
  <p>churned MRR (<%= period_short %>)</p>
98
99
  </div>
100
+ <div class="card">
101
+ <h2><%= Profitable.mrr_growth(in_the_last: period).to_readable %></h2>
102
+ <p>MRR growth (<%= period_short %>)</p>
103
+ </div>
104
+
105
+ <div class="card">
106
+ <h2><%= Profitable.revenue_in_period(in_the_last: period).to_readable %></h2>
107
+ <p>total revenue (<%= period_short %>)</p>
108
+ </div>
109
+
99
110
  </div>
100
111
  <% end %>
101
112
 
@@ -17,7 +17,7 @@ module Profitable
17
17
 
18
18
  subscriptions.find_each do |subscription|
19
19
  mrr = process_subscription(subscription)
20
- total_mrr += mrr
20
+ total_mrr += mrr if mrr.is_a?(Numeric) && mrr > 0
21
21
  end
22
22
 
23
23
  total_mrr
@@ -30,7 +30,10 @@ module Profitable
30
30
  return 0 if subscription.nil? || subscription.data.nil?
31
31
 
32
32
  processor_class = processor_for(subscription.customer_processor)
33
- processor_class.new(subscription).calculate_mrr
33
+ mrr = processor_class.new(subscription).calculate_mrr
34
+
35
+ # Ensure MRR is a non-negative number
36
+ mrr.is_a?(Numeric) ? [mrr, 0].max : 0
34
37
  rescue => e
35
38
  Rails.logger.error("Error calculating MRR for subscription #{subscription.id}: #{e.message}")
36
39
  0
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Profitable
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
data/lib/profitable.rb CHANGED
@@ -34,29 +34,40 @@ module Profitable
34
34
  NumericResult.new(calculate_all_time_revenue)
35
35
  end
36
36
 
37
+ def revenue_in_period(in_the_last: DEFAULT_PERIOD)
38
+ NumericResult.new(calculate_revenue_in_period(in_the_last))
39
+ end
40
+
41
+ def recurring_revenue_in_period(in_the_last: DEFAULT_PERIOD)
42
+ NumericResult.new(calculate_recurring_revenue_in_period(in_the_last))
43
+ end
44
+
45
+ def recurring_revenue_percentage(in_the_last: DEFAULT_PERIOD)
46
+ NumericResult.new(calculate_recurring_revenue_percentage(in_the_last), :percentage)
47
+ end
48
+
37
49
  def estimated_valuation(multiplier = "3x")
38
50
  NumericResult.new(calculate_estimated_valuation(multiplier))
39
51
  end
40
52
 
41
53
  def total_customers
42
- NumericResult.new(Pay::Customer.count, :integer)
54
+ NumericResult.new(calculate_total_customers, :integer)
43
55
  end
44
56
 
45
57
  def total_subscribers
46
- NumericResult.new(Pay::Subscription.active.distinct.count('customer_id'), :integer)
58
+ NumericResult.new(calculate_total_subscribers, :integer)
59
+ end
60
+
61
+ def active_subscribers
62
+ NumericResult.new(calculate_active_subscribers, :integer)
47
63
  end
48
64
 
49
65
  def new_customers(in_the_last: DEFAULT_PERIOD)
50
- NumericResult.new(Pay::Customer.where(created_at: in_the_last.ago..Time.current).count, :integer)
66
+ NumericResult.new(calculate_new_customers(in_the_last), :integer)
51
67
  end
52
68
 
53
69
  def new_subscribers(in_the_last: DEFAULT_PERIOD)
54
- NumericResult.new(
55
- Pay::Subscription.active
56
- .where(pay_subscriptions: { created_at: in_the_last.ago..Time.current })
57
- .distinct.count('pay_customers.id'),
58
- :integer
59
- )
70
+ NumericResult.new(calculate_new_subscribers(in_the_last), :integer)
60
71
  end
61
72
 
62
73
  def churned_customers(in_the_last: DEFAULT_PERIOD)
@@ -79,6 +90,10 @@ module Profitable
79
90
  NumericResult.new(calculate_lifetime_value)
80
91
  end
81
92
 
93
+ def mrr_growth(in_the_last: DEFAULT_PERIOD)
94
+ NumericResult.new(calculate_mrr_growth(in_the_last))
95
+ end
96
+
82
97
  def mrr_growth_rate(in_the_last: DEFAULT_PERIOD)
83
98
  NumericResult.new(calculate_mrr_growth_rate(in_the_last), :percentage)
84
99
  end
@@ -99,8 +114,13 @@ module Profitable
99
114
 
100
115
  private
101
116
 
117
+ def paid_charges
118
+ Pay::Charge.where("(pay_charges.data ->> 'paid' IS NULL OR pay_charges.data ->> 'paid' != ?) AND pay_charges.amount > 0", 'false')
119
+ .where("pay_charges.data ->> 'status' = ? OR pay_charges.data ->> 'status' IS NULL", 'succeeded')
120
+ end
121
+
102
122
  def calculate_all_time_revenue
103
- Pay::Charge.sum(:amount)
123
+ paid_charges.sum(:amount)
104
124
  end
105
125
 
106
126
  def calculate_arr
@@ -122,6 +142,9 @@ module Profitable
122
142
 
123
143
  def churned_subscriptions(period = DEFAULT_PERIOD)
124
144
  Pay::Subscription
145
+ .includes(:customer)
146
+ .select('pay_subscriptions.*, pay_customers.processor as customer_processor')
147
+ .joins(:customer)
125
148
  .where(status: ['canceled', 'ended'])
126
149
  .where(ends_at: period.ago..Time.current)
127
150
  end
@@ -150,6 +173,59 @@ module Profitable
150
173
  end
151
174
  end
152
175
 
176
+ def calculate_revenue_in_period(period)
177
+ paid_charges.where(created_at: period.ago..Time.current).sum(:amount)
178
+ end
179
+
180
+ def calculate_recurring_revenue_in_period(period)
181
+ paid_charges
182
+ .joins('INNER JOIN pay_subscriptions ON pay_charges.subscription_id = pay_subscriptions.id')
183
+ .where(created_at: period.ago..Time.current)
184
+ .sum(:amount)
185
+ end
186
+
187
+ def calculate_recurring_revenue_percentage(period)
188
+ total_revenue = calculate_revenue_in_period(period)
189
+ recurring_revenue = calculate_recurring_revenue_in_period(period)
190
+
191
+ return 0 if total_revenue.zero?
192
+
193
+ ((recurring_revenue.to_f / total_revenue) * 100).round(2)
194
+ end
195
+
196
+ def calculate_total_customers
197
+ actual_customers.count
198
+ end
199
+
200
+ def calculate_total_subscribers
201
+ Pay::Customer.joins(:subscriptions).distinct.count
202
+ end
203
+
204
+ def calculate_active_subscribers
205
+ Pay::Customer.joins(:subscriptions)
206
+ .where(pay_subscriptions: { status: 'active' })
207
+ .distinct
208
+ .count
209
+ end
210
+
211
+ def actual_customers
212
+ Pay::Customer.joins("LEFT JOIN pay_subscriptions ON pay_customers.id = pay_subscriptions.customer_id")
213
+ .joins("LEFT JOIN pay_charges ON pay_customers.id = pay_charges.customer_id")
214
+ .where("pay_subscriptions.id IS NOT NULL OR pay_charges.amount > 0")
215
+ .distinct
216
+ end
217
+
218
+ def calculate_new_customers(period)
219
+ actual_customers.where(created_at: period.ago..Time.current).count
220
+ end
221
+
222
+ def calculate_new_subscribers(period)
223
+ Pay::Customer.joins(:subscriptions)
224
+ .where(created_at: period.ago..Time.current)
225
+ .distinct
226
+ .count
227
+ end
228
+
153
229
  def calculate_average_revenue_per_customer
154
230
  return 0 if total_customers.zero?
155
231
  (all_time_revenue.to_f / total_customers).round
@@ -162,6 +238,12 @@ module Profitable
162
238
  (average_revenue_per_customer.to_f / churn_rate).round
163
239
  end
164
240
 
241
+ def calculate_mrr_growth(period = DEFAULT_PERIOD)
242
+ new_mrr = calculate_new_mrr(period)
243
+ churned_mrr = calculate_churned_mrr(period)
244
+ new_mrr - churned_mrr
245
+ end
246
+
165
247
  def calculate_mrr_growth_rate(period = DEFAULT_PERIOD)
166
248
  end_date = Time.current
167
249
  start_date = end_date - period
@@ -185,5 +267,6 @@ module Profitable
185
267
  MrrCalculator.process_subscription(subscription)
186
268
  end
187
269
  end
270
+
188
271
  end
189
272
  end
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.1.0
4
+ version: 0.2.0
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-30 00:00:00.000000000 Z
11
+ date: 2024-08-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: pay