profitable 0.2.3 → 0.4.0
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 +4 -4
- data/.simplecov +47 -0
- data/AGENTS.md +5 -0
- data/Appraisals +50 -0
- data/CHANGELOG.md +11 -0
- data/CLAUDE.md +5 -0
- data/README.md +57 -5
- data/Rakefile +10 -1
- data/app/controllers/profitable/dashboard_controller.rb +16 -4
- data/app/views/profitable/dashboard/index.html.erb +151 -18
- data/gemfiles/pay_10.0.gemfile +23 -0
- data/gemfiles/pay_11.0.gemfile +23 -0
- data/gemfiles/pay_7.3.gemfile +23 -0
- data/gemfiles/pay_8.3.gemfile +23 -0
- data/gemfiles/pay_9.0.gemfile +23 -0
- data/gemfiles/rails_7.2.gemfile +23 -0
- data/gemfiles/rails_8.1.gemfile +23 -0
- data/lib/profitable/json_helpers.rb +68 -0
- data/lib/profitable/mrr_calculator.rb +13 -3
- data/lib/profitable/processors/base.rb +28 -5
- data/lib/profitable/processors/braintree_processor.rb +8 -3
- data/lib/profitable/processors/paddle_billing_processor.rb +22 -7
- data/lib/profitable/processors/paddle_classic_processor.rb +7 -2
- data/lib/profitable/processors/stripe_processor.rb +24 -8
- data/lib/profitable/version.rb +1 -1
- data/lib/profitable.rb +282 -75
- metadata +16 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f24a9849fbf5f3d6b87beaa3f0a4100d18a45ccb2b585e8e48b417d303759593
|
|
4
|
+
data.tar.gz: 7c8523a9547f30468ee7517a92af604aa66c7bcc269e6292200d8722f83c30ff
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4b79c94e7288c859f7419a8afe27c4352db1be8cff2c269b9d9d57348761a59e5f3a48cfe4b601496a1d4642b88fd5e2e5f349a1733f133488bb47a153b6a453
|
|
7
|
+
data.tar.gz: 216578eb9882a946fed3818c7d9301449727cd2b1bc64b282e90e1d8cf056b2f05ff801e49748ec46cf89a008771d796ec65daf9ce99d7ed058da5fa0d284e01
|
data/.simplecov
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# SimpleCov configuration file (auto-loaded before test suite)
|
|
4
|
+
# This keeps test_helper.rb clean and follows best practices
|
|
5
|
+
|
|
6
|
+
SimpleCov.start do
|
|
7
|
+
# Use SimpleFormatter for terminal-only output (no HTML generation)
|
|
8
|
+
formatter SimpleCov::Formatter::SimpleFormatter
|
|
9
|
+
|
|
10
|
+
# Track coverage for the lib directory (gem source code)
|
|
11
|
+
add_filter "/test/"
|
|
12
|
+
|
|
13
|
+
# Exclude Rails engine components that require integration testing
|
|
14
|
+
# These are tested via Appraisal with a full Rails app
|
|
15
|
+
add_filter "/lib/profitable/engine.rb"
|
|
16
|
+
add_filter "/app/"
|
|
17
|
+
|
|
18
|
+
# Exclude the main profitable.rb entry point - it loads the engine and
|
|
19
|
+
# defines the Profitable module. Our unit tests use a test-specific
|
|
20
|
+
# Profitable module (defined in test_helper.rb) to avoid Rails dependencies.
|
|
21
|
+
# The core logic is tested via the individual lib/profitable/*.rb files.
|
|
22
|
+
add_filter "/lib/profitable.rb"
|
|
23
|
+
|
|
24
|
+
# Track the lib directory (core gem logic)
|
|
25
|
+
track_files "lib/**/*.rb"
|
|
26
|
+
|
|
27
|
+
# Enable branch coverage for more detailed metrics
|
|
28
|
+
enable_coverage :branch
|
|
29
|
+
|
|
30
|
+
# Set minimum coverage threshold for the core calculation logic
|
|
31
|
+
# The Rails engine/controllers are tested separately via Appraisal
|
|
32
|
+
minimum_coverage line: 80, branch: 70
|
|
33
|
+
|
|
34
|
+
# Disambiguate parallel test runs
|
|
35
|
+
command_name "Job #{ENV['TEST_ENV_NUMBER']}" if ENV['TEST_ENV_NUMBER']
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Print coverage summary to terminal after tests complete
|
|
39
|
+
SimpleCov.at_exit do
|
|
40
|
+
SimpleCov.result.format!
|
|
41
|
+
puts "\n" + "=" * 60
|
|
42
|
+
puts "COVERAGE SUMMARY"
|
|
43
|
+
puts "=" * 60
|
|
44
|
+
puts "Line Coverage: #{SimpleCov.result.covered_percent.round(2)}%"
|
|
45
|
+
puts "Branch Coverage: #{SimpleCov.result.coverage_statistics[:branch]&.percent&.round(2) || 'N/A'}%"
|
|
46
|
+
puts "=" * 60
|
|
47
|
+
end
|
data/AGENTS.md
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to AI Agents (like OpenAI's Codex, Cursor Agent, Claude Code, etc) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
Please go ahead and read the full context for this project at `.cursor/rules/0-overview.mdc` and `.cursor/rules/1-quality.mdc` now. Also read the README for a good overview of the project.
|
data/Appraisals
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Test minimum supported Rails version (with latest Pay)
|
|
4
|
+
appraise "rails-7.2" do
|
|
5
|
+
gem "rails", "~> 7.2.0"
|
|
6
|
+
gem "pay", "~> 11.0"
|
|
7
|
+
gem "stripe", "~> 18.0"
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# Test latest Rails version (with latest Pay)
|
|
11
|
+
appraise "rails-8.1" do
|
|
12
|
+
gem "rails", "~> 8.1.0"
|
|
13
|
+
gem "pay", "~> 11.0"
|
|
14
|
+
gem "stripe", "~> 18.0"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Test against Pay 7.x (original minimum supported version)
|
|
18
|
+
appraise "pay-7.3" do
|
|
19
|
+
gem "pay", "~> 7.3.0"
|
|
20
|
+
gem "stripe", "~> 12.0"
|
|
21
|
+
gem "rails", "~> 8.1.0"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Test against Pay 8.x
|
|
25
|
+
appraise "pay-8.3" do
|
|
26
|
+
gem "pay", "~> 8.3.0"
|
|
27
|
+
gem "stripe", "~> 13.0"
|
|
28
|
+
gem "rails", "~> 8.1.0"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Test against Pay 9.x
|
|
32
|
+
appraise "pay-9.0" do
|
|
33
|
+
gem "pay", "~> 9.0.0"
|
|
34
|
+
gem "stripe", "~> 13.0"
|
|
35
|
+
gem "rails", "~> 8.1.0"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Test against Pay 10.x (newly supported version with object column)
|
|
39
|
+
appraise "pay-10.0" do
|
|
40
|
+
gem "pay", "~> 10.0.0"
|
|
41
|
+
gem "stripe", "~> 15.0"
|
|
42
|
+
gem "rails", "~> 8.1.0"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Test against Pay 11.x (latest version as of 2025)
|
|
46
|
+
appraise "pay-11.0" do
|
|
47
|
+
gem "pay", "~> 11.0"
|
|
48
|
+
gem "stripe", "~> 18.0"
|
|
49
|
+
gem "rails", "~> 8.1.0"
|
|
50
|
+
end
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# `profitable`
|
|
2
2
|
|
|
3
|
+
## [0.4.0] - 2026-02-10
|
|
4
|
+
- Add monthly summary (12mo) and daily summary (30d) tables to dashboard
|
|
5
|
+
- Add `period_data` method for efficient batch computation of period metrics
|
|
6
|
+
- Fix `new_mrr` counting incomplete/unpaid subscriptions (now only counts active)
|
|
7
|
+
- Fix `new_subscribers` not filtering out trialing/paused subscriptions
|
|
8
|
+
- DRY up period methods (churn, churned_customers, new_mrr, etc.) via `_in_period` delegation
|
|
9
|
+
- Optimize dashboard from ~176 to 38 queries (batch summary queries, precompute in controller)
|
|
10
|
+
|
|
11
|
+
## [0.3.0] - 2026-01-01
|
|
12
|
+
- Add Pay v10+ support, comprehensive Minitest test suite, and 16 critical bugfixes re: wrong calculations
|
|
13
|
+
|
|
3
14
|
## [0.2.3] - 2024-09-01
|
|
4
15
|
|
|
5
16
|
- Fix the `time_to_next_mrr_milestone` estimation and make it accurate to the day
|
data/CLAUDE.md
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
Please go ahead and read the full context for this project at `.cursor/rules/0-overview.mdc` and `.cursor/rules/1-quality.mdc` now. Also read the README for a good overview of the project.
|
data/README.md
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
# 💸 `profitable` - SaaS metrics for your Rails app
|
|
1
|
+
# 💸 `profitable` - MRR dashboard & SaaS metrics for your Rails app
|
|
2
2
|
|
|
3
|
-
[](https://badge.fury.io/rb/profitable)
|
|
3
|
+
[](https://badge.fury.io/rb/profitable) [](https://github.com/rameerez/profitable/actions)
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
> [!TIP]
|
|
6
|
+
> **🚀 Ship your next Rails app 10x faster!** I've built **[RailsFast](https://railsfast.com/?ref=profitable)**, a production-ready Rails boilerplate template that comes with everything you need to launch a software business in days, not weeks. Go [check it out](https://railsfast.com/?ref=profitable)!
|
|
7
|
+
|
|
8
|
+
`profitable` allows you to calculate the MRR, ARR, churn, LTV, ARPU, total revenue & estimated valuation of your `pay`-powered Rails SaaS app, and display them in a simple dashboard. It also provides handy methods you can use independently if you don't want the full dashboard.
|
|
6
9
|
|
|
7
10
|

|
|
8
11
|
|
|
@@ -12,7 +15,7 @@ Calculate the MRR, ARR, churn, LTV, ARPU, total revenue & estimated valuation of
|
|
|
12
15
|
|
|
13
16
|
Usually, you would look into your Stripe Dashboard or query the Stripe API to know your MRR / ARR / churn – but when you're using `pay`, you already have that data available and auto synced to your own database. So we can leverage it to make handy, composable ActiveRecord queries that you can reuse in any part of your Rails app (dashboards, internal pages, reports, status messages, etc.)
|
|
14
17
|
|
|
15
|
-
Think doing something like: `"
|
|
18
|
+
Think doing something like: `"Current MRR: $#{Profitable.mrr}"` or `"Your app is worth $#{Profitable.valuation_estimate("3x")} at a 3x valuation"`
|
|
16
19
|
|
|
17
20
|
## Installation
|
|
18
21
|
|
|
@@ -133,7 +136,56 @@ Profitable.mrr # => 123456
|
|
|
133
136
|
|
|
134
137
|
## Development
|
|
135
138
|
|
|
136
|
-
After checking out the repo,
|
|
139
|
+
After checking out the repo, install dependencies:
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
bundle install
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Running Tests
|
|
146
|
+
|
|
147
|
+
The gem includes a Minitest test suite. Run it with:
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
# Run all tests
|
|
151
|
+
bundle exec rake test
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Testing Against Multiple Pay Gem Versions
|
|
155
|
+
|
|
156
|
+
This gem uses [Appraisal](https://github.com/thoughtbot/appraisal) to test against multiple versions of the Pay gem, ensuring compatibility across Pay 7.x through 11.x.
|
|
157
|
+
|
|
158
|
+
**Generate appraisal gemfiles:**
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
bundle exec appraisal install
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
**Run tests against a specific Pay version:**
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
# Test against Pay 10.x
|
|
168
|
+
bundle exec appraisal pay-10.0 rake test
|
|
169
|
+
|
|
170
|
+
# Test against Pay 11.x
|
|
171
|
+
bundle exec appraisal pay-11.0 rake test
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
**Run tests against all Pay versions:**
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
bundle exec appraisal rake test
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Database Compatibility
|
|
181
|
+
|
|
182
|
+
Tests run on SQLite by default, but the gem supports:
|
|
183
|
+
- PostgreSQL (9.3+)
|
|
184
|
+
- MySQL (5.7.9+)
|
|
185
|
+
- MariaDB (10.2.7+)
|
|
186
|
+
- SQLite (3.9.0+)
|
|
187
|
+
|
|
188
|
+
The gem automatically detects your database adapter and uses the appropriate JSON query syntax.
|
|
137
189
|
|
|
138
190
|
To install this gem onto your local machine, run `bundle exec rake install`.
|
|
139
191
|
|
data/Rakefile
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "bundler/gem_tasks"
|
|
4
|
-
|
|
4
|
+
require "rake/testtask"
|
|
5
|
+
|
|
6
|
+
Rake::TestTask.new(:test) do |t|
|
|
7
|
+
t.libs << "test"
|
|
8
|
+
t.libs << "lib"
|
|
9
|
+
t.test_files = FileList["test/**/*_test.rb"]
|
|
10
|
+
t.warning = false
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
task default: :test
|
|
@@ -1,12 +1,24 @@
|
|
|
1
1
|
module Profitable
|
|
2
2
|
class DashboardController < BaseController
|
|
3
3
|
def index
|
|
4
|
-
|
|
4
|
+
@mrr = Profitable.mrr
|
|
5
|
+
@mrr_growth_rate = Profitable.mrr_growth_rate
|
|
6
|
+
@total_customers = Profitable.total_customers
|
|
7
|
+
@all_time_revenue = Profitable.all_time_revenue
|
|
8
|
+
@estimated_valuation = Profitable.estimated_valuation
|
|
9
|
+
@average_revenue_per_customer = Profitable.average_revenue_per_customer
|
|
10
|
+
@lifetime_value = Profitable.lifetime_value
|
|
5
11
|
|
|
6
|
-
|
|
12
|
+
@show_milestone = @mrr_growth_rate > 0
|
|
13
|
+
@milestone_message = Profitable.time_to_next_mrr_milestone if @show_milestone
|
|
7
14
|
|
|
8
|
-
|
|
9
|
-
|
|
15
|
+
@monthly_summary = Profitable.monthly_summary(months: 12)
|
|
16
|
+
@daily_summary = Profitable.daily_summary(days: 30)
|
|
10
17
|
|
|
18
|
+
@periods = [24.hours, 7.days, 30.days]
|
|
19
|
+
@period_data = @periods.each_with_object({}) do |period, hash|
|
|
20
|
+
hash[period] = Profitable.period_data(in_the_last: period)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
11
23
|
end
|
|
12
24
|
end
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
<style>
|
|
2
|
+
main h2.title {
|
|
3
|
+
font-size: 1.25rem;
|
|
4
|
+
margin: 64px 0 12px 0;
|
|
5
|
+
}
|
|
6
|
+
|
|
2
7
|
.card-grid {
|
|
3
8
|
display: flex;
|
|
4
9
|
flex-wrap: wrap;
|
|
@@ -32,8 +37,8 @@
|
|
|
32
37
|
|
|
33
38
|
<header>
|
|
34
39
|
<h1>💸 <%= Rails.application.class.module_parent_name %></h1>
|
|
35
|
-
<% if
|
|
36
|
-
<p><%=
|
|
40
|
+
<% if @show_milestone %>
|
|
41
|
+
<p><%= @milestone_message %></p>
|
|
37
42
|
<% end %>
|
|
38
43
|
</header>
|
|
39
44
|
|
|
@@ -41,69 +46,197 @@
|
|
|
41
46
|
|
|
42
47
|
<div class="card-grid">
|
|
43
48
|
<div class="card">
|
|
44
|
-
<h2><%=
|
|
49
|
+
<h2><%= @total_customers.to_readable %></h2>
|
|
45
50
|
<p>total customers</p>
|
|
46
51
|
</div>
|
|
47
52
|
<div class="card">
|
|
48
|
-
<h2><%=
|
|
53
|
+
<h2><%= @mrr.to_readable %></h2>
|
|
49
54
|
<p>MRR</p>
|
|
50
55
|
</div>
|
|
51
56
|
<div class="card">
|
|
52
|
-
<h2><%=
|
|
57
|
+
<h2><%= @estimated_valuation.to_readable %></h2>
|
|
53
58
|
<p>Valuation at 3x ARR</p>
|
|
54
59
|
</div>
|
|
55
60
|
<div class="card">
|
|
56
|
-
<h2><%=
|
|
61
|
+
<h2><%= @mrr_growth_rate.to_readable %></h2>
|
|
57
62
|
<p>MRR growth rate</p>
|
|
58
63
|
</div>
|
|
59
64
|
<div class="card">
|
|
60
|
-
<h2><%=
|
|
65
|
+
<h2><%= @average_revenue_per_customer.to_readable %></h2>
|
|
61
66
|
<p>ARPC</p>
|
|
62
67
|
</div>
|
|
63
68
|
<div class="card">
|
|
64
|
-
<h2><%=
|
|
69
|
+
<h2><%= @lifetime_value.to_readable %></h2>
|
|
65
70
|
<p>LTV</p>
|
|
66
71
|
</div>
|
|
67
72
|
<div class="card">
|
|
68
|
-
<h2><%=
|
|
73
|
+
<h2><%= @all_time_revenue.to_readable %></h2>
|
|
69
74
|
<p>All-time revenue</p>
|
|
70
75
|
</div>
|
|
71
76
|
</div>
|
|
72
77
|
|
|
73
|
-
|
|
78
|
+
<h2 class="title">Monthly summary</h2>
|
|
79
|
+
<small>(last 12 months)</small>
|
|
80
|
+
|
|
81
|
+
<style>
|
|
82
|
+
.summary-table {
|
|
83
|
+
width: 100%;
|
|
84
|
+
border-collapse: collapse;
|
|
85
|
+
margin-top: 16px;
|
|
86
|
+
background-color: var(--bg);
|
|
87
|
+
border: 1px solid var(--border);
|
|
88
|
+
border-radius: var(--standard-border-radius);
|
|
89
|
+
overflow: hidden;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.summary-table th,
|
|
93
|
+
.summary-table td {
|
|
94
|
+
padding: 12px 16px;
|
|
95
|
+
text-align: left;
|
|
96
|
+
border-bottom: 1px solid var(--border);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.summary-table th {
|
|
100
|
+
font-weight: 600;
|
|
101
|
+
background-color: var(--bg);
|
|
102
|
+
position: sticky;
|
|
103
|
+
top: 0;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.summary-table tr:last-child td {
|
|
107
|
+
border-bottom: none;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.summary-table .positive {
|
|
111
|
+
color: #10b981;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.summary-table .negative {
|
|
115
|
+
color: #ef4444;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.summary-table .muted {
|
|
119
|
+
color: #6b7280;
|
|
120
|
+
font-size: 0.875em;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.summary-table tbody tr:hover {
|
|
124
|
+
background-color: rgba(0, 0, 0, 0.02);
|
|
125
|
+
}
|
|
126
|
+
</style>
|
|
127
|
+
|
|
128
|
+
<table class="summary-table">
|
|
129
|
+
<thead>
|
|
130
|
+
<tr>
|
|
131
|
+
<th>Month</th>
|
|
132
|
+
<th>New</th>
|
|
133
|
+
<th>Churned</th>
|
|
134
|
+
<th>Net</th>
|
|
135
|
+
<th>Churn %</th>
|
|
136
|
+
</tr>
|
|
137
|
+
</thead>
|
|
138
|
+
<tbody>
|
|
139
|
+
<% @monthly_summary.reverse.each do |month_data| %>
|
|
140
|
+
<tr>
|
|
141
|
+
<td><strong><%= month_data[:month_date].strftime('%b %Y') %></strong></td>
|
|
142
|
+
<td>
|
|
143
|
+
<span class="positive">+<%= month_data[:new_subscribers] %></span>
|
|
144
|
+
<span class="muted">(~<%= number_to_currency(month_data[:new_mrr] / 100.0, precision: 0) %>)</span>
|
|
145
|
+
</td>
|
|
146
|
+
<td>
|
|
147
|
+
<% if month_data[:churned_subscribers] > 0 %>
|
|
148
|
+
<span class="negative">-<%= month_data[:churned_subscribers] %></span>
|
|
149
|
+
<span class="muted">(~<%= number_to_currency(month_data[:churned_mrr] / 100.0, precision: 0) %>)</span>
|
|
150
|
+
<% else %>
|
|
151
|
+
<span>0</span>
|
|
152
|
+
<% end %>
|
|
153
|
+
</td>
|
|
154
|
+
<td>
|
|
155
|
+
<% if month_data[:net_subscribers] > 0 %>
|
|
156
|
+
<span class="positive">+<%= month_data[:net_subscribers] %></span>
|
|
157
|
+
<span class="muted">(~+<%= number_to_currency(month_data[:net_mrr] / 100.0, precision: 0) %>)</span>
|
|
158
|
+
<% elsif month_data[:net_subscribers] < 0 %>
|
|
159
|
+
<span class="negative"><%= month_data[:net_subscribers] %></span>
|
|
160
|
+
<span class="muted">(~<%= number_to_currency(month_data[:net_mrr] / 100.0, precision: 0) %>)</span>
|
|
161
|
+
<% else %>
|
|
162
|
+
<span>0</span>
|
|
163
|
+
<% end %>
|
|
164
|
+
</td>
|
|
165
|
+
<td><%= month_data[:churn_rate] %>%</td>
|
|
166
|
+
</tr>
|
|
167
|
+
<% end %>
|
|
168
|
+
</tbody>
|
|
169
|
+
</table>
|
|
170
|
+
|
|
171
|
+
<h2 class="title">Daily summary</h2>
|
|
172
|
+
<small>(last 30 days)</small>
|
|
173
|
+
|
|
174
|
+
<table class="summary-table">
|
|
175
|
+
<thead>
|
|
176
|
+
<tr>
|
|
177
|
+
<th>Date</th>
|
|
178
|
+
<th>New Subscribers</th>
|
|
179
|
+
<th>Churned</th>
|
|
180
|
+
</tr>
|
|
181
|
+
</thead>
|
|
182
|
+
<tbody>
|
|
183
|
+
<% @daily_summary.reverse.each do |day_data| %>
|
|
184
|
+
<tr>
|
|
185
|
+
<td><strong><%= day_data[:date].strftime('%b %-d, %Y') %></strong></td>
|
|
186
|
+
<td>
|
|
187
|
+
<% if day_data[:new_subscribers] > 0 %>
|
|
188
|
+
<span class="positive">+<%= day_data[:new_subscribers] %></span>
|
|
189
|
+
<% else %>
|
|
190
|
+
<span>0</span>
|
|
191
|
+
<% end %>
|
|
192
|
+
</td>
|
|
193
|
+
<td>
|
|
194
|
+
<% if day_data[:churned_subscribers] > 0 %>
|
|
195
|
+
<span class="negative">-<%= day_data[:churned_subscribers] %></span>
|
|
196
|
+
<% else %>
|
|
197
|
+
<span>0</span>
|
|
198
|
+
<% end %>
|
|
199
|
+
</td>
|
|
200
|
+
</tr>
|
|
201
|
+
<% end %>
|
|
202
|
+
</tbody>
|
|
203
|
+
</table>
|
|
204
|
+
|
|
205
|
+
<% @periods.each do |period| %>
|
|
74
206
|
<% period_short = period.inspect.gsub("days", "d").gsub("hours", "h").gsub(" ", "") %>
|
|
207
|
+
<% data = @period_data[period] %>
|
|
75
208
|
|
|
76
|
-
<h2>Last <%= period.inspect %></h2>
|
|
209
|
+
<h2 class="title">Last <%= period.inspect %></h2>
|
|
77
210
|
|
|
78
211
|
<div class="card-grid">
|
|
79
212
|
<div class="card">
|
|
80
|
-
<h2><%=
|
|
213
|
+
<h2><%= data[:new_customers].to_readable %></h2>
|
|
81
214
|
<p>new customers (<%= period_short %>)</p>
|
|
82
215
|
</div>
|
|
83
216
|
<div class="card">
|
|
84
|
-
<h2><%=
|
|
217
|
+
<h2><%= data[:churned_customers].to_readable %></h2>
|
|
85
218
|
<p>churned customers (<%= period_short %>)</p>
|
|
86
219
|
</div>
|
|
87
220
|
<div class="card">
|
|
88
|
-
<h2><%=
|
|
221
|
+
<h2><%= data[:churn].to_readable %></h2>
|
|
89
222
|
<p>churn (<%= period_short %>)</p>
|
|
90
223
|
</div>
|
|
91
224
|
|
|
92
225
|
<div class="card">
|
|
93
|
-
<h2><%=
|
|
226
|
+
<h2><%= data[:new_mrr].to_readable %></h2>
|
|
94
227
|
<p>new MRR (<%= period_short %>)</p>
|
|
95
228
|
</div>
|
|
96
229
|
<div class="card">
|
|
97
|
-
<h2><%=
|
|
230
|
+
<h2><%= data[:churned_mrr].to_readable %></h2>
|
|
98
231
|
<p>churned MRR (<%= period_short %>)</p>
|
|
99
232
|
</div>
|
|
100
233
|
<div class="card">
|
|
101
|
-
<h2><%=
|
|
234
|
+
<h2><%= data[:mrr_growth].to_readable %></h2>
|
|
102
235
|
<p>MRR growth (<%= period_short %>)</p>
|
|
103
236
|
</div>
|
|
104
237
|
|
|
105
238
|
<div class="card">
|
|
106
|
-
<h2><%=
|
|
239
|
+
<h2><%= data[:revenue].to_readable %></h2>
|
|
107
240
|
<p>total revenue (<%= period_short %>)</p>
|
|
108
241
|
</div>
|
|
109
242
|
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# This file was generated by Appraisal
|
|
2
|
+
|
|
3
|
+
source "https://rubygems.org"
|
|
4
|
+
|
|
5
|
+
gem "rake", "~> 13.0"
|
|
6
|
+
gem "pay", "~> 10.0.0"
|
|
7
|
+
gem "stripe", "~> 15.0"
|
|
8
|
+
gem "rails", "~> 8.1.0"
|
|
9
|
+
|
|
10
|
+
group :development, :test do
|
|
11
|
+
gem "appraisal"
|
|
12
|
+
gem "minitest", "~> 6.0"
|
|
13
|
+
gem "minitest-mock"
|
|
14
|
+
gem "minitest-reporters"
|
|
15
|
+
gem "rack-test"
|
|
16
|
+
gem "simplecov", require: false
|
|
17
|
+
gem "sqlite3", ">= 2.1"
|
|
18
|
+
gem "mocha", "~> 2.1"
|
|
19
|
+
gem "activerecord", ">= 7.0"
|
|
20
|
+
gem "actionview", ">= 7.0"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
gemspec path: "../"
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# This file was generated by Appraisal
|
|
2
|
+
|
|
3
|
+
source "https://rubygems.org"
|
|
4
|
+
|
|
5
|
+
gem "rake", "~> 13.0"
|
|
6
|
+
gem "pay", "~> 11.0"
|
|
7
|
+
gem "stripe", "~> 18.0"
|
|
8
|
+
gem "rails", "~> 8.1.0"
|
|
9
|
+
|
|
10
|
+
group :development, :test do
|
|
11
|
+
gem "appraisal"
|
|
12
|
+
gem "minitest", "~> 6.0"
|
|
13
|
+
gem "minitest-mock"
|
|
14
|
+
gem "minitest-reporters"
|
|
15
|
+
gem "rack-test"
|
|
16
|
+
gem "simplecov", require: false
|
|
17
|
+
gem "sqlite3", ">= 2.1"
|
|
18
|
+
gem "mocha", "~> 2.1"
|
|
19
|
+
gem "activerecord", ">= 7.0"
|
|
20
|
+
gem "actionview", ">= 7.0"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
gemspec path: "../"
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# This file was generated by Appraisal
|
|
2
|
+
|
|
3
|
+
source "https://rubygems.org"
|
|
4
|
+
|
|
5
|
+
gem "rake", "~> 13.0"
|
|
6
|
+
gem "pay", "~> 7.3.0"
|
|
7
|
+
gem "stripe", "~> 12.0"
|
|
8
|
+
gem "rails", "~> 8.1.0"
|
|
9
|
+
|
|
10
|
+
group :development, :test do
|
|
11
|
+
gem "appraisal"
|
|
12
|
+
gem "minitest", "~> 6.0"
|
|
13
|
+
gem "minitest-mock"
|
|
14
|
+
gem "minitest-reporters"
|
|
15
|
+
gem "rack-test"
|
|
16
|
+
gem "simplecov", require: false
|
|
17
|
+
gem "sqlite3", ">= 2.1"
|
|
18
|
+
gem "mocha", "~> 2.1"
|
|
19
|
+
gem "activerecord", ">= 7.0"
|
|
20
|
+
gem "actionview", ">= 7.0"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
gemspec path: "../"
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# This file was generated by Appraisal
|
|
2
|
+
|
|
3
|
+
source "https://rubygems.org"
|
|
4
|
+
|
|
5
|
+
gem "rake", "~> 13.0"
|
|
6
|
+
gem "pay", "~> 8.3.0"
|
|
7
|
+
gem "stripe", "~> 13.0"
|
|
8
|
+
gem "rails", "~> 8.1.0"
|
|
9
|
+
|
|
10
|
+
group :development, :test do
|
|
11
|
+
gem "appraisal"
|
|
12
|
+
gem "minitest", "~> 6.0"
|
|
13
|
+
gem "minitest-mock"
|
|
14
|
+
gem "minitest-reporters"
|
|
15
|
+
gem "rack-test"
|
|
16
|
+
gem "simplecov", require: false
|
|
17
|
+
gem "sqlite3", ">= 2.1"
|
|
18
|
+
gem "mocha", "~> 2.1"
|
|
19
|
+
gem "activerecord", ">= 7.0"
|
|
20
|
+
gem "actionview", ">= 7.0"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
gemspec path: "../"
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# This file was generated by Appraisal
|
|
2
|
+
|
|
3
|
+
source "https://rubygems.org"
|
|
4
|
+
|
|
5
|
+
gem "rake", "~> 13.0"
|
|
6
|
+
gem "pay", "~> 9.0.0"
|
|
7
|
+
gem "stripe", "~> 13.0"
|
|
8
|
+
gem "rails", "~> 8.1.0"
|
|
9
|
+
|
|
10
|
+
group :development, :test do
|
|
11
|
+
gem "appraisal"
|
|
12
|
+
gem "minitest", "~> 6.0"
|
|
13
|
+
gem "minitest-mock"
|
|
14
|
+
gem "minitest-reporters"
|
|
15
|
+
gem "rack-test"
|
|
16
|
+
gem "simplecov", require: false
|
|
17
|
+
gem "sqlite3", ">= 2.1"
|
|
18
|
+
gem "mocha", "~> 2.1"
|
|
19
|
+
gem "activerecord", ">= 7.0"
|
|
20
|
+
gem "actionview", ">= 7.0"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
gemspec path: "../"
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# This file was generated by Appraisal
|
|
2
|
+
|
|
3
|
+
source "https://rubygems.org"
|
|
4
|
+
|
|
5
|
+
gem "rake", "~> 13.0"
|
|
6
|
+
gem "rails", "~> 7.2.0"
|
|
7
|
+
gem "pay", "~> 11.0"
|
|
8
|
+
gem "stripe", "~> 18.0"
|
|
9
|
+
|
|
10
|
+
group :development, :test do
|
|
11
|
+
gem "appraisal"
|
|
12
|
+
gem "minitest", "~> 6.0"
|
|
13
|
+
gem "minitest-mock"
|
|
14
|
+
gem "minitest-reporters"
|
|
15
|
+
gem "rack-test"
|
|
16
|
+
gem "simplecov", require: false
|
|
17
|
+
gem "sqlite3", ">= 2.1"
|
|
18
|
+
gem "mocha", "~> 2.1"
|
|
19
|
+
gem "activerecord", ">= 7.0"
|
|
20
|
+
gem "actionview", ">= 7.0"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
gemspec path: "../"
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# This file was generated by Appraisal
|
|
2
|
+
|
|
3
|
+
source "https://rubygems.org"
|
|
4
|
+
|
|
5
|
+
gem "rake", "~> 13.0"
|
|
6
|
+
gem "rails", "~> 8.1.0"
|
|
7
|
+
gem "pay", "~> 11.0"
|
|
8
|
+
gem "stripe", "~> 18.0"
|
|
9
|
+
|
|
10
|
+
group :development, :test do
|
|
11
|
+
gem "appraisal"
|
|
12
|
+
gem "minitest", "~> 6.0"
|
|
13
|
+
gem "minitest-mock"
|
|
14
|
+
gem "minitest-reporters"
|
|
15
|
+
gem "rack-test"
|
|
16
|
+
gem "simplecov", require: false
|
|
17
|
+
gem "sqlite3", ">= 2.1"
|
|
18
|
+
gem "mocha", "~> 2.1"
|
|
19
|
+
gem "activerecord", ">= 7.0"
|
|
20
|
+
gem "actionview", ">= 7.0"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
gemspec path: "../"
|