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); } }), );