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:
86
services/arbiter-3.0/src/routes/admin/financials.js
Normal file
86
services/arbiter-3.0/src/routes/admin/financials.js
Normal 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;
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user