feat: Trinity Console Financials - Revenue analytics from Gemini

GEMINI DELIVERED FINANCIALS & REVENUE ANALYTICS! 💰

Complete business intelligence dashboard with Fire vs Frost path comparison,
MRR tracking, ARPU calculations, and tier breakdown visualization.

GEMINI'S BUSINESS LOGIC (CRITICAL):
1. Use mrr_value from database (handles discounts, grandfathered rates, future price changes)
2. Sovereign = $0 MRR (lifetime isn't recurring revenue!)
3. Separate "Recognized MRR" (cash in hand) vs "At-Risk MRR" (grace period)
4. Pure CSS visualizations (perfect for RV low-bandwidth)

FINANCIAL METRICS:
- Recognized MRR: Sum of active subscription mrr_value
- At-Risk MRR: Sum of grace_period subscription mrr_value
- Active Recurring Subs: Count of active non-Sovereign subscriptions
- Lifetime Revenue: Count of Sovereign × $499
- ARPU: Average Revenue Per User (MRR / Active Subs)
- ARR: Annual Run Rate (MRR × 12)

FIRE VS FROST PATH DOMINANCE:
- Beautiful animated progress bar comparison
- Shows percentage split between Fire and Frost paths
- Separate cards with subscriber counts and MRR per path
- Fire gradient: Orange to Red
- Frost gradient: Cyan to Blue

TIER BREAKDOWN TABLE:
- All 10 tiers listed with Fire/Frost/Universal emojis
- Active subscriber count per tier (green)
- At-Risk count per tier (yellow, grace period)
- Recognized MRR contribution per tier
- Percentage of total MRR with mini progress bar
- Sortable by tier level

SQL OPTIMIZATION:
Uses single efficient query with FILTER clauses instead of multiple SELECTs:
- Query 1: Global health metrics (all key numbers in one go)
- Query 2: Tier breakdown grouped by tier_level and status
- No N+1 queries, no performance issues

FILES ADDED:
- src/routes/admin/financials.js - Revenue analytics router
- src/views/admin/financials/index.ejs - Financial dashboard
- src/routes/admin/index.js - Mounted financials router

VISUAL DESIGN:
- 4 stat cards: Recognized MRR (green), At-Risk MRR (yellow), Active Subs (blue), Lifetime Revenue (purple)
- Fire vs Frost progress bar with animated gradient fills
- Tier breakdown table with inline progress bars
- Export CSV button (placeholder for Phase 3)

BUSINESS INTELLIGENCE:
- Shows which path (Fire/Frost) is dominating
- Identifies most popular tier
- Highlights at-risk revenue in grace period
- Calculates annual run rate for planning
- ARPU helps understand subscriber value

GEMINI'S WISDOM:
"MRR is Monthly Recurring Revenue—the guaranteed cash flow that keeps
the RV moving. Lifetime deals are one-time capital injections."

NEXT FROM GEMINI:
Grace Period Dashboard (CRITICAL for Task #87!)

Signed-off-by: The Golden Chronicler <claude@firefrostgaming.com>
Co-authored-by: Gemini AI <gemini@anthropic-partnership.ai>
This commit is contained in:
Claude (The Golden Chronicler #50)
2026-04-01 04:44:21 +00:00
parent a459432b62
commit cb92e1a1d7
3 changed files with 218 additions and 2 deletions

View File

@@ -0,0 +1,86 @@
const express = require('express');
const router = express.Router();
const db = require('../../database');
const { TIER_INFO } = require('./constants');
router.get('/', async (req, res) => {
try {
// Query 1: Global Health Metrics
const { rows: healthData } = await db.query(`
SELECT
COUNT(*) FILTER (WHERE status = 'active' AND tier_level != 499) as active_subs,
SUM(mrr_value) FILTER (WHERE status = 'active') as recognized_mrr,
COUNT(*) FILTER (WHERE status = 'grace_period') as at_risk_subs,
SUM(mrr_value) FILTER (WHERE status = 'grace_period') as at_risk_mrr,
COUNT(*) FILTER (WHERE tier_level = 499 AND status IN ('active', 'lifetime')) as lifetime_subs
FROM subscriptions;
`);
const metrics = {
activeSubs: parseInt(healthData[0].active_subs || 0),
recognizedMrr: parseFloat(healthData[0].recognized_mrr || 0),
atRiskSubs: parseInt(healthData[0].at_risk_subs || 0),
atRiskMrr: parseFloat(healthData[0].at_risk_mrr || 0),
lifetimeSubs: parseInt(healthData[0].lifetime_subs || 0)
};
metrics.lifetimeRevenue = metrics.lifetimeSubs * 499.00;
metrics.arpu = metrics.activeSubs > 0 ? (metrics.recognizedMrr / metrics.activeSubs).toFixed(2) : 0;
metrics.arr = (metrics.recognizedMrr * 12).toFixed(2);
// Query 2: Tier Breakdown
const { rows: tierData } = await db.query(`
SELECT tier_level, status, COUNT(*) as count, SUM(mrr_value) as total_mrr
FROM subscriptions
WHERE status IN ('active', 'grace_period', 'lifetime')
GROUP BY tier_level, status;
`);
// Process paths (Fire vs Frost)
const paths = {
fire: { mrr: 0, subs: 0 },
frost: { mrr: 0, subs: 0 },
universal: { mrr: 0, subs: 0 }
};
const tierBreakdown = {};
tierData.forEach(row => {
const tLevel = row.tier_level;
const info = TIER_INFO[tLevel] || { name: 'Unknown', path: 'universal' };
const mrr = parseFloat(row.total_mrr || 0);
const count = parseInt(row.count || 0);
// Tally active MRR for the Path comparison (excluding grace period from guaranteed MRR)
if (row.status === 'active') {
if (paths[info.path]) {
paths[info.path].mrr += mrr;
paths[info.path].subs += count;
}
}
// Build detailed table data
if (!tierBreakdown[tLevel]) {
tierBreakdown[tLevel] = { ...info, activeCount: 0, graceCount: 0, totalMrr: 0 };
}
if (row.status === 'active' || row.status === 'lifetime') tierBreakdown[tLevel].activeCount += count;
if (row.status === 'grace_period') tierBreakdown[tLevel].graceCount += count;
if (row.status === 'active') tierBreakdown[tLevel].totalMrr += mrr;
});
res.render('admin/financials/index', {
title: 'Financials & Analytics',
metrics,
paths,
tierBreakdown,
TIER_INFO
});
} catch (error) {
console.error("Financials Error:", error);
res.status(500).send("Error loading financial data.");
}
});
module.exports = router;

View File

@@ -2,10 +2,10 @@ const express = require('express');
const router = express.Router();
const { requireTrinityAccess } = require('./middleware');
// Sub-routers (We will populate these as we go)
// Sub-routers
const playersRouter = require('./players');
const serversRouter = require('./servers');
// const financialsRouter = require('./financials');
const financialsRouter = require('./financials');
router.use(requireTrinityAccess);
@@ -19,5 +19,6 @@ router.get('/dashboard', (req, res) => {
router.use('/players', playersRouter);
router.use('/servers', serversRouter);
router.use('/financials', financialsRouter);
module.exports = router;