feat(reports): Financial report (Initiative 1 Phase 4)
Builds the Financial report on the canonical payments + expenses tables
(the CRM records money received; it does not invoice — invoices module
is off, dev DB has zero invoice rows). The invoice-centric spec is
reframed onto the payments model: "outstanding AR" → expected-deposit
shortfall on active deals; "AR aging" → outstanding deposits bucketed by
deal age.
Service (financial.service.ts):
- 7 KPIs: revenue collected (net of refunds), deposits, balance,
pipeline expected, outstanding deposits, expenses, net contribution
- 6 chart datasets: revenue by month (deposit/balance), collection
funnel (EOI→deposit→contract→won), expected-deposit aging, cash flow
(inflow vs outflow), expense breakdown by category
- 4 tables: outstanding deposits, recent payments, refund log, expense
ledger
- every money figure normalised to port currency via a shared
resolvePortCurrency/normalizeAmount helper (new reports/currency.ts)
UI (financial-report-client.tsx): KPI strip + recharts (stacked bar /
horizontal bar / line / donut) + month/quarter/year toggle + branded
empty states; DateRangePicker + Templates + Export wired. Un-hidden the
Financial card on the reports landing.
Plumbing: added '1y' (trailing 12mo) preset to the shared range system
(financial trends want a year); added 'financial'/'marketing' to the
report-template kind enum for template parity.
TDD: 6 financial-math unit tests (aging buckets, month keys/range, net
contribution). tsc clean; full unit suite green except pre-existing
Redis/storage-dependent integration tests. Browser-verified against live
data: API 200, KPIs correct ($5,849 expenses / -$5,849 net, $0 revenue
correct given 0 payment rows), expense ledger + breakdown populate,
payment-derived sections show graceful empty states.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 00:43:36 +02:00
|
|
|
import { NextRequest, NextResponse } from 'next/server';
|
|
|
|
|
import { z } from 'zod';
|
|
|
|
|
|
|
|
|
|
import { withAuth, withPermission } from '@/lib/api/helpers';
|
|
|
|
|
import { errorResponse } from '@/lib/errors';
|
|
|
|
|
import {
|
|
|
|
|
getFinancialKpis,
|
|
|
|
|
getRevenueByMonth,
|
|
|
|
|
getCollectionFunnel,
|
|
|
|
|
getExpectedDepositAging,
|
|
|
|
|
getCashFlow,
|
|
|
|
|
getExpenseBreakdown,
|
|
|
|
|
getOutstandingDeposits,
|
|
|
|
|
getRecentPayments,
|
|
|
|
|
getRefundLog,
|
|
|
|
|
getExpenseLedger,
|
2026-06-02 10:13:42 +02:00
|
|
|
financialHasData,
|
feat(reports): Financial report (Initiative 1 Phase 4)
Builds the Financial report on the canonical payments + expenses tables
(the CRM records money received; it does not invoice — invoices module
is off, dev DB has zero invoice rows). The invoice-centric spec is
reframed onto the payments model: "outstanding AR" → expected-deposit
shortfall on active deals; "AR aging" → outstanding deposits bucketed by
deal age.
Service (financial.service.ts):
- 7 KPIs: revenue collected (net of refunds), deposits, balance,
pipeline expected, outstanding deposits, expenses, net contribution
- 6 chart datasets: revenue by month (deposit/balance), collection
funnel (EOI→deposit→contract→won), expected-deposit aging, cash flow
(inflow vs outflow), expense breakdown by category
- 4 tables: outstanding deposits, recent payments, refund log, expense
ledger
- every money figure normalised to port currency via a shared
resolvePortCurrency/normalizeAmount helper (new reports/currency.ts)
UI (financial-report-client.tsx): KPI strip + recharts (stacked bar /
horizontal bar / line / donut) + month/quarter/year toggle + branded
empty states; DateRangePicker + Templates + Export wired. Un-hidden the
Financial card on the reports landing.
Plumbing: added '1y' (trailing 12mo) preset to the shared range system
(financial trends want a year); added 'financial'/'marketing' to the
report-template kind enum for template parity.
TDD: 6 financial-math unit tests (aging buckets, month keys/range, net
contribution). tsc clean; full unit suite green except pre-existing
Redis/storage-dependent integration tests. Browser-verified against live
data: API 200, KPIs correct ($5,849 expenses / -$5,849 net, $0 revenue
correct given 0 payment rows), expense ledger + breakdown populate,
payment-derived sections show graceful empty states.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 00:43:36 +02:00
|
|
|
} from '@/lib/services/reports/financial.service';
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* GET /api/v1/reports/financial?from=&to=
|
|
|
|
|
*
|
|
|
|
|
* Financial report payload, sourced from the canonical `payments` +
|
|
|
|
|
* `expenses` tables (the CRM records money received; it does not invoice
|
|
|
|
|
* — see docs/reports-content-spec.md § Report 02). "Outstanding AR" is
|
|
|
|
|
* reframed as expected-deposit shortfall on active deals.
|
|
|
|
|
*
|
|
|
|
|
* Permission: `reports.view_dashboard` (same gate as the other report
|
|
|
|
|
* dashboards).
|
|
|
|
|
*/
|
|
|
|
|
const querySchema = z.object({
|
|
|
|
|
from: z.string().datetime().optional(),
|
|
|
|
|
to: z.string().datetime().optional(),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
function resolveRange(from?: string, to?: string): { from: Date; to: Date } {
|
|
|
|
|
const now = new Date();
|
|
|
|
|
// Default: trailing 12 months — financial trends read better over a
|
|
|
|
|
// year than 30 days, and the month/cash-flow charts want the span.
|
|
|
|
|
const defaultFrom = new Date(now);
|
|
|
|
|
defaultFrom.setMonth(defaultFrom.getMonth() - 12);
|
|
|
|
|
return {
|
|
|
|
|
from: from ? new Date(from) : defaultFrom,
|
|
|
|
|
to: to ? new Date(to) : now,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const GET = withAuth(
|
|
|
|
|
withPermission('reports', 'view_dashboard', async (req: NextRequest, ctx) => {
|
|
|
|
|
try {
|
|
|
|
|
const params = req.nextUrl.searchParams;
|
|
|
|
|
const { from, to } = querySchema.parse({
|
|
|
|
|
from: params.get('from') ?? undefined,
|
|
|
|
|
to: params.get('to') ?? undefined,
|
|
|
|
|
});
|
|
|
|
|
const range = resolveRange(from, to);
|
|
|
|
|
|
|
|
|
|
const [
|
|
|
|
|
kpis,
|
|
|
|
|
revenueByMonth,
|
|
|
|
|
collectionFunnel,
|
|
|
|
|
aging,
|
|
|
|
|
cashFlow,
|
|
|
|
|
expenseBreakdown,
|
|
|
|
|
outstandingDeposits,
|
|
|
|
|
recentPayments,
|
|
|
|
|
refundLog,
|
|
|
|
|
expenseLedger,
|
2026-06-02 10:13:42 +02:00
|
|
|
hasData,
|
feat(reports): Financial report (Initiative 1 Phase 4)
Builds the Financial report on the canonical payments + expenses tables
(the CRM records money received; it does not invoice — invoices module
is off, dev DB has zero invoice rows). The invoice-centric spec is
reframed onto the payments model: "outstanding AR" → expected-deposit
shortfall on active deals; "AR aging" → outstanding deposits bucketed by
deal age.
Service (financial.service.ts):
- 7 KPIs: revenue collected (net of refunds), deposits, balance,
pipeline expected, outstanding deposits, expenses, net contribution
- 6 chart datasets: revenue by month (deposit/balance), collection
funnel (EOI→deposit→contract→won), expected-deposit aging, cash flow
(inflow vs outflow), expense breakdown by category
- 4 tables: outstanding deposits, recent payments, refund log, expense
ledger
- every money figure normalised to port currency via a shared
resolvePortCurrency/normalizeAmount helper (new reports/currency.ts)
UI (financial-report-client.tsx): KPI strip + recharts (stacked bar /
horizontal bar / line / donut) + month/quarter/year toggle + branded
empty states; DateRangePicker + Templates + Export wired. Un-hidden the
Financial card on the reports landing.
Plumbing: added '1y' (trailing 12mo) preset to the shared range system
(financial trends want a year); added 'financial'/'marketing' to the
report-template kind enum for template parity.
TDD: 6 financial-math unit tests (aging buckets, month keys/range, net
contribution). tsc clean; full unit suite green except pre-existing
Redis/storage-dependent integration tests. Browser-verified against live
data: API 200, KPIs correct ($5,849 expenses / -$5,849 net, $0 revenue
correct given 0 payment rows), expense ledger + breakdown populate,
payment-derived sections show graceful empty states.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 00:43:36 +02:00
|
|
|
] = await Promise.all([
|
|
|
|
|
getFinancialKpis(ctx.portId, range),
|
|
|
|
|
getRevenueByMonth(ctx.portId, range),
|
|
|
|
|
getCollectionFunnel(ctx.portId, range),
|
|
|
|
|
getExpectedDepositAging(ctx.portId),
|
|
|
|
|
getCashFlow(ctx.portId, range),
|
|
|
|
|
getExpenseBreakdown(ctx.portId, range),
|
|
|
|
|
getOutstandingDeposits(ctx.portId),
|
|
|
|
|
getRecentPayments(ctx.portId, range),
|
|
|
|
|
getRefundLog(ctx.portId, range),
|
|
|
|
|
getExpenseLedger(ctx.portId, range),
|
2026-06-02 10:13:42 +02:00
|
|
|
financialHasData(ctx.portId),
|
feat(reports): Financial report (Initiative 1 Phase 4)
Builds the Financial report on the canonical payments + expenses tables
(the CRM records money received; it does not invoice — invoices module
is off, dev DB has zero invoice rows). The invoice-centric spec is
reframed onto the payments model: "outstanding AR" → expected-deposit
shortfall on active deals; "AR aging" → outstanding deposits bucketed by
deal age.
Service (financial.service.ts):
- 7 KPIs: revenue collected (net of refunds), deposits, balance,
pipeline expected, outstanding deposits, expenses, net contribution
- 6 chart datasets: revenue by month (deposit/balance), collection
funnel (EOI→deposit→contract→won), expected-deposit aging, cash flow
(inflow vs outflow), expense breakdown by category
- 4 tables: outstanding deposits, recent payments, refund log, expense
ledger
- every money figure normalised to port currency via a shared
resolvePortCurrency/normalizeAmount helper (new reports/currency.ts)
UI (financial-report-client.tsx): KPI strip + recharts (stacked bar /
horizontal bar / line / donut) + month/quarter/year toggle + branded
empty states; DateRangePicker + Templates + Export wired. Un-hidden the
Financial card on the reports landing.
Plumbing: added '1y' (trailing 12mo) preset to the shared range system
(financial trends want a year); added 'financial'/'marketing' to the
report-template kind enum for template parity.
TDD: 6 financial-math unit tests (aging buckets, month keys/range, net
contribution). tsc clean; full unit suite green except pre-existing
Redis/storage-dependent integration tests. Browser-verified against live
data: API 200, KPIs correct ($5,849 expenses / -$5,849 net, $0 revenue
correct given 0 payment rows), expense ledger + breakdown populate,
payment-derived sections show graceful empty states.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 00:43:36 +02:00
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
return NextResponse.json({
|
|
|
|
|
data: {
|
|
|
|
|
kpis,
|
|
|
|
|
revenueByMonth,
|
|
|
|
|
collectionFunnel,
|
|
|
|
|
aging,
|
|
|
|
|
cashFlow,
|
|
|
|
|
expenseBreakdown,
|
|
|
|
|
outstandingDeposits,
|
|
|
|
|
recentPayments,
|
|
|
|
|
refundLog,
|
|
|
|
|
expenseLedger,
|
2026-06-02 10:13:42 +02:00
|
|
|
hasData,
|
feat(reports): Financial report (Initiative 1 Phase 4)
Builds the Financial report on the canonical payments + expenses tables
(the CRM records money received; it does not invoice — invoices module
is off, dev DB has zero invoice rows). The invoice-centric spec is
reframed onto the payments model: "outstanding AR" → expected-deposit
shortfall on active deals; "AR aging" → outstanding deposits bucketed by
deal age.
Service (financial.service.ts):
- 7 KPIs: revenue collected (net of refunds), deposits, balance,
pipeline expected, outstanding deposits, expenses, net contribution
- 6 chart datasets: revenue by month (deposit/balance), collection
funnel (EOI→deposit→contract→won), expected-deposit aging, cash flow
(inflow vs outflow), expense breakdown by category
- 4 tables: outstanding deposits, recent payments, refund log, expense
ledger
- every money figure normalised to port currency via a shared
resolvePortCurrency/normalizeAmount helper (new reports/currency.ts)
UI (financial-report-client.tsx): KPI strip + recharts (stacked bar /
horizontal bar / line / donut) + month/quarter/year toggle + branded
empty states; DateRangePicker + Templates + Export wired. Un-hidden the
Financial card on the reports landing.
Plumbing: added '1y' (trailing 12mo) preset to the shared range system
(financial trends want a year); added 'financial'/'marketing' to the
report-template kind enum for template parity.
TDD: 6 financial-math unit tests (aging buckets, month keys/range, net
contribution). tsc clean; full unit suite green except pre-existing
Redis/storage-dependent integration tests. Browser-verified against live
data: API 200, KPIs correct ($5,849 expenses / -$5,849 net, $0 revenue
correct given 0 payment rows), expense ledger + breakdown populate,
payment-derived sections show graceful empty states.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 00:43:36 +02:00
|
|
|
range: { from: range.from.toISOString(), to: range.to.toISOString() },
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
} catch (error) {
|
|
|
|
|
return errorResponse(error);
|
|
|
|
|
}
|
|
|
|
|
}),
|
|
|
|
|
);
|