Files
pn-new-crm/src/app/api/v1/reports/financial/route.ts
2026-06-02 10:13:42 +02:00

105 lines
3.0 KiB
TypeScript

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,
financialHasData,
} 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,
hasData,
] = 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),
financialHasData(ctx.portId),
]);
return NextResponse.json({
data: {
kpis,
revenueByMonth,
collectionFunnel,
aging,
cashFlow,
expenseBreakdown,
outstandingDeposits,
recentPayments,
refundLog,
expenseLedger,
hasData,
range: { from: range.from.toISOString(), to: range.to.toISOString() },
},
});
} catch (error) {
return errorResponse(error);
}
}),
);