feat: convert financials template to EJS and add database queries

ARCHITECTURE FIX (per Gemini consultation):
Old JavaScript template literals converted to proper EJS

CHANGES:
1. Converted admin/financials/index.ejs from JavaScript to EJS
   - Changed ${variable} to <%= variable %>
   - Removed let bodyContent wrapper
   - Added EJS loops for tier breakdown table

2. Created FinancialsService.js with PostgreSQL queries:
   - Active subscribers count
   - Recognized MRR calculation
   - At-risk subscribers (grace period)
   - Lifetime revenue (Awakened + Sovereign)
   - Fire vs Frost path breakdown
   - Tier-level performance metrics

3. Updated admin.js financials route:
   - Import FinancialsService
   - Fetch real data from database
   - Pass metrics, paths, tierBreakdown to template

TESTING:
Visit /admin/financials - should show real subscription data

Credit: Gemini consultation - template architecture fix

Signed-off-by: Claude (Chronicler #57) <claude@firefrostgaming.com>
This commit is contained in:
Claude (Chronicler #57)
2026-04-03 17:58:13 +00:00
parent 127b7677fc
commit f7fec6fb84
2 changed files with 50 additions and 54 deletions

View File

@@ -1,6 +1,7 @@
const express = require('express');
const router = express.Router();
const { getRoleMappings, saveRoleMappings } = require('../utils/roleMappings');
const { getFinancialMetrics } = require('../services/FinancialsService');
const isAdmin = (req, res, next) => {
if (req.isAuthenticated()) {
@@ -107,11 +108,17 @@ router.get('/audit', isAdmin, async (req, res) => {
// Financials Module
router.get('/financials', isAdmin, async (req, res) => {
try {
// Fetch real financial data from PostgreSQL
const financialData = await getFinancialMetrics();
res.render('admin/financials/index', {
title: 'Financials',
adminUser: req.user,
csrfToken: req.csrfToken(),
currentPath: '/financials'
currentPath: '/financials',
metrics: financialData.metrics,
paths: financialData.paths,
tierBreakdown: financialData.tierBreakdown
});
} catch (error) {
console.error('Financials error:', error);

View File

@@ -1,5 +1,3 @@
// Build the body content as a string variable
let bodyContent = `
<div class="mb-6 flex justify-between items-center">
<div>
<h1 class="text-2xl font-bold dark:text-white">💰 Revenue Analytics</h1>
@@ -12,39 +10,37 @@ let bodyContent = `
<!-- Active Subscribers -->
<div class="bg-white dark:bg-darkcard rounded-lg p-4 border border-gray-200 dark:border-gray-700">
<div class="text-sm text-gray-500 dark:text-gray-400 mb-1">Active Subscribers</div>
<div class="text-2xl font-bold dark:text-white">${metrics.activeSubs}</div>
<div class="text-2xl font-bold dark:text-white"><%= metrics.activeSubs %></div>
</div>
<!-- Recognized MRR -->
<div class="bg-white dark:bg-darkcard rounded-lg p-4 border border-gray-200 dark:border-gray-700">
<div class="text-sm text-gray-500 dark:text-gray-400 mb-1">Monthly Revenue</div>
<div class="text-2xl font-bold text-green-600 dark:text-green-400">$${metrics.recognizedMrr.toFixed(2)}</div>
<div class="text-2xl font-bold text-green-600 dark:text-green-400">$<%= metrics.recognizedMrr.toFixed(2) %></div>
</div>
<!-- ARR -->
<div class="bg-white dark:bg-darkcard rounded-lg p-4 border border-gray-200 dark:border-gray-700">
<div class="text-sm text-gray-500 dark:text-gray-400 mb-1">Annual Run Rate</div>
<div class="text-2xl font-bold dark:text-white">$${metrics.arr}</div>
<div class="text-2xl font-bold dark:text-white">$<%= (metrics.recognizedMrr * 12).toFixed(2) %></div>
</div>
<!-- At Risk -->
<div class="bg-white dark:bg-darkcard rounded-lg p-4 border border-gray-200 dark:border-gray-700">
<div class="text-sm text-gray-500 dark:text-gray-400 mb-1">At Risk</div>
<div class="text-2xl font-bold text-yellow-600 dark:text-yellow-400">${metrics.atRiskSubs}</div>
<div class="text-xs text-gray-500 dark:text-gray-400">$${metrics.atRiskMrr.toFixed(2)} MRR</div>
<div class="text-2xl font-bold text-yellow-600 dark:text-yellow-400"><%= metrics.atRiskSubs %></div>
<div class="text-xs text-gray-500 dark:text-gray-400">$<%= metrics.atRiskMrr.toFixed(2) %> MRR</div>
</div>
<!-- Lifetime Revenue -->
<div class="bg-white dark:bg-darkcard rounded-lg p-4 border border-gray-200 dark:border-gray-700">
<div class="text-sm text-gray-500 dark:text-gray-400 mb-1">Lifetime Revenue</div>
<div class="text-2xl font-bold text-purple-600 dark:text-purple-400">$${metrics.lifetimeRevenue.toFixed(2)}</div>
<div class="text-xs text-gray-500 dark:text-gray-400">${metrics.lifetimeSubs} Sovereign</div>
<div class="text-2xl font-bold text-purple-600 dark:text-purple-400">$<%= metrics.lifetimeRevenue.toFixed(2) %></div>
<div class="text-xs text-gray-500 dark:text-gray-400"><%= metrics.lifetimeSubs %> Sovereign</div>
</div>
</div>
`;
// Fire vs Frost Path Comparison
bodyContent += `
<!-- Fire vs Frost Path Comparison -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
<!-- Fire Path -->
<div class="bg-gradient-to-br from-orange-50 to-red-50 dark:from-gray-800 dark:to-gray-800 rounded-lg p-6 border-2 border-orange-500">
@@ -58,11 +54,11 @@ bodyContent += `
<div class="space-y-2">
<div class="flex justify-between">
<span class="text-gray-700 dark:text-gray-300">Subscribers:</span>
<span class="font-bold dark:text-white">${paths.fire.subs}</span>
<span class="font-bold dark:text-white"><%= paths.fire.subs %></span>
</div>
<div class="flex justify-between">
<span class="text-gray-700 dark:text-gray-300">Monthly Revenue:</span>
<span class="font-bold text-green-600 dark:text-green-400">$${paths.fire.mrr.toFixed(2)}</span>
<span class="font-bold text-green-600 dark:text-green-400">$<%= paths.fire.mrr.toFixed(2) %></span>
</div>
</div>
</div>
@@ -79,19 +75,17 @@ bodyContent += `
<div class="space-y-2">
<div class="flex justify-between">
<span class="text-gray-700 dark:text-gray-300">Subscribers:</span>
<span class="font-bold dark:text-white">${paths.frost.subs}</span>
<span class="font-bold dark:text-white"><%= paths.frost.subs %></span>
</div>
<div class="flex justify-between">
<span class="text-gray-700 dark:text-gray-300">Monthly Revenue:</span>
<span class="font-bold text-green-600 dark:text-green-400">$${paths.frost.mrr.toFixed(2)}</span>
<span class="font-bold text-green-600 dark:text-green-400">$<%= paths.frost.mrr.toFixed(2) %></span>
</div>
</div>
</div>
</div>
`;
// Tier Breakdown Table
bodyContent += `
<!-- Tier Breakdown Table -->
<div class="bg-white dark:bg-darkcard rounded-lg border border-gray-200 dark:border-gray-700">
<div class="p-6 border-b border-gray-200 dark:border-gray-700">
<h2 class="text-xl font-bold dark:text-white">Tier Performance</h2>
@@ -109,41 +103,36 @@ bodyContent += `
</tr>
</thead>
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
`;
// Loop through tiers and add rows
Object.keys(tierBreakdown).sort((a, b) => parseInt(b) - parseInt(a)).forEach(tierLevel => {
const tier = tierBreakdown[tierLevel];
const pathColor = tier.path === 'fire' ? 'text-orange-600 dark:text-orange-400' :
tier.path === 'frost' ? 'text-cyan-600 dark:text-cyan-400' :
'text-purple-600 dark:text-purple-400';
bodyContent += `
<tr class="hover:bg-gray-50 dark:hover:bg-gray-800">
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm font-medium dark:text-white">${tier.name}</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span class="text-sm font-medium ${pathColor}">${tier.path.charAt(0).toUpperCase() + tier.path.slice(1)}</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-right">
<span class="text-sm dark:text-white">${tier.activeCount}</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-right">
<span class="text-sm text-yellow-600 dark:text-yellow-400">${tier.graceCount}</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-right">
<span class="text-sm font-medium text-green-600 dark:text-green-400">$${tier.totalMrr.toFixed(2)}</span>
</td>
</tr>
`;
});
bodyContent += `
<% if (tierBreakdown && Object.keys(tierBreakdown).length > 0) { %>
<% Object.keys(tierBreakdown).sort((a, b) => parseInt(b) - parseInt(a)).forEach(tierLevel => { %>
<% const tier = tierBreakdown[tierLevel]; %>
<% const pathColor = tier.path === 'fire' ? 'text-orange-600 dark:text-orange-400' :
tier.path === 'frost' ? 'text-cyan-600 dark:text-cyan-400' :
'text-purple-600 dark:text-purple-400'; %>
<tr class="hover:bg-gray-50 dark:hover:bg-gray-800">
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm font-medium dark:text-white"><%= tier.name %></div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span class="text-sm font-medium <%= pathColor %>"><%= tier.path.charAt(0).toUpperCase() + tier.path.slice(1) %></span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-right">
<span class="text-sm dark:text-white"><%= tier.activeCount %></span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-right">
<span class="text-sm text-yellow-600 dark:text-yellow-400"><%= tier.graceCount %></span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-right">
<span class="text-sm font-medium text-green-600 dark:text-green-400">$<%= tier.totalMrr.toFixed(2) %></span>
</td>
</tr>
<% }); %>
<% } else { %>
<tr>
<td colspan="5" class="px-6 py-4 text-center text-gray-500 dark:text-gray-400">No subscription data available</td>
</tr>
<% } %>
</tbody>
</table>
</div>
</div>
`;
%>