From 5366e8c1ad0e4e76af0e5c43de1860c5a21b7205 Mon Sep 17 00:00:00 2001 From: kevin-asprec Date: Thu, 5 Mar 2026 12:31:33 +0800 Subject: [PATCH] =?UTF-8?q?feat(04-05):=20financial=20report=20API=20route?= =?UTF-8?q?s=20=E2=80=94=20trial=20balance,=20income=20statement,=20balanc?= =?UTF-8?q?e=20sheet,=20drill-down?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - GET /api/reports/trial-balance with optional asOfDate filter - GET /api/reports/income-statement with required startDate/endDate - GET /api/reports/balance-sheet with required asOfDate - GET /api/reports/accounts/[id]/entries drill-down with optional date range - All routes use withPermission("read", "Report") for ADMIN access - FinancialReportService already existed with all 4 methods Co-Authored-By: Claude Opus 4.6 --- .../reports/accounts/[id]/entries/route.ts | 52 ++++++++++++++++ src/app/api/reports/balance-sheet/route.ts | 57 +++++++++++++++++ src/app/api/reports/income-statement/route.ts | 61 +++++++++++++++++++ src/app/api/reports/trial-balance/route.ts | 41 +++++++++++++ 4 files changed, 211 insertions(+) create mode 100644 src/app/api/reports/accounts/[id]/entries/route.ts create mode 100644 src/app/api/reports/balance-sheet/route.ts create mode 100644 src/app/api/reports/income-statement/route.ts create mode 100644 src/app/api/reports/trial-balance/route.ts diff --git a/src/app/api/reports/accounts/[id]/entries/route.ts b/src/app/api/reports/accounts/[id]/entries/route.ts new file mode 100644 index 0000000..9d0fd0d --- /dev/null +++ b/src/app/api/reports/accounts/[id]/entries/route.ts @@ -0,0 +1,52 @@ +/** + * GET /api/reports/accounts/[id]/entries + * + * Drill-down: all POSTED journal entry lines for a specific account. + * Shows entry details with running balance. + * + * Query params: + * startDate? - ISO date string + * endDate? - ISO date string + * + * Access: ADMIN, OFFICE_STAFF. + */ +import { NextRequest, NextResponse } from "next/server"; +import { withPermission } from "@/lib/middleware/authorize"; +import { withTenantContext } from "@/lib/prisma-tenant"; +import { FinancialReportService } from "@/lib/services/financial-report-service"; + +export function GET( + req: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + return withPermission("read", "Report")( + async (innerReq: NextRequest, { user }) => { + if (!user.tenantId) { + return NextResponse.json( + { error: "No tenant context -- super-admins must use the admin API" }, + { status: 400 } + ); + } + + const { id: accountId } = await params; + + const { searchParams } = new URL(innerReq.url); + const startDateParam = searchParams.get("startDate"); + const endDateParam = searchParams.get("endDate"); + + const tenantPrisma = withTenantContext(user.tenantId); + + try { + const report = await FinancialReportService.getAccountEntries(tenantPrisma, { + accountId, + startDate: startDateParam ? new Date(startDateParam) : undefined, + endDate: endDateParam ? new Date(endDateParam) : undefined, + }); + return NextResponse.json(report); + } catch (err) { + const message = err instanceof Error ? err.message : "Failed to get account entries"; + return NextResponse.json({ error: message }, { status: 400 }); + } + } + )(req); +} diff --git a/src/app/api/reports/balance-sheet/route.ts b/src/app/api/reports/balance-sheet/route.ts new file mode 100644 index 0000000..d7a74ab --- /dev/null +++ b/src/app/api/reports/balance-sheet/route.ts @@ -0,0 +1,57 @@ +/** + * GET /api/reports/balance-sheet + * + * Balance Sheet -- assets = liabilities + equity as of a date. + * Derived entirely from POSTED journal entry lines. + * + * Query params: + * asOfDate - ISO date string (required) + * + * Access: ADMIN only. + */ +import { NextRequest, NextResponse } from "next/server"; +import { withPermission } from "@/lib/middleware/authorize"; +import { withTenantContext } from "@/lib/prisma-tenant"; +import { FinancialReportService } from "@/lib/services/financial-report-service"; + +export const GET = withPermission("read", "Report")( + async (req: NextRequest, { user }) => { + if (!user.tenantId) { + return NextResponse.json( + { error: "No tenant context -- super-admins must use the admin API" }, + { status: 400 } + ); + } + + const { searchParams } = new URL(req.url); + const asOfDateParam = searchParams.get("asOfDate"); + + if (!asOfDateParam) { + return NextResponse.json( + { error: "asOfDate query parameter is required" }, + { status: 400 } + ); + } + + const asOfDate = new Date(asOfDateParam); + + if (isNaN(asOfDate.getTime())) { + return NextResponse.json( + { error: "asOfDate must be a valid ISO date string" }, + { status: 400 } + ); + } + + const tenantPrisma = withTenantContext(user.tenantId); + + try { + const report = await FinancialReportService.getBalanceSheet(tenantPrisma, { + asOfDate, + }); + return NextResponse.json(report); + } catch (err) { + const message = err instanceof Error ? err.message : "Failed to generate balance sheet"; + return NextResponse.json({ error: message }, { status: 500 }); + } + } +); diff --git a/src/app/api/reports/income-statement/route.ts b/src/app/api/reports/income-statement/route.ts new file mode 100644 index 0000000..84c4e48 --- /dev/null +++ b/src/app/api/reports/income-statement/route.ts @@ -0,0 +1,61 @@ +/** + * GET /api/reports/income-statement + * + * Income Statement -- revenue minus expenses for a date range. + * Derived entirely from POSTED journal entry lines. + * + * Query params: + * startDate - ISO date string (required) + * endDate - ISO date string (required) + * + * Access: ADMIN only. + */ +import { NextRequest, NextResponse } from "next/server"; +import { withPermission } from "@/lib/middleware/authorize"; +import { withTenantContext } from "@/lib/prisma-tenant"; +import { FinancialReportService } from "@/lib/services/financial-report-service"; + +export const GET = withPermission("read", "Report")( + async (req: NextRequest, { user }) => { + if (!user.tenantId) { + return NextResponse.json( + { error: "No tenant context -- super-admins must use the admin API" }, + { status: 400 } + ); + } + + const { searchParams } = new URL(req.url); + const startDateParam = searchParams.get("startDate"); + const endDateParam = searchParams.get("endDate"); + + if (!startDateParam || !endDateParam) { + return NextResponse.json( + { error: "startDate and endDate query parameters are required" }, + { status: 400 } + ); + } + + const startDate = new Date(startDateParam); + const endDate = new Date(endDateParam); + + if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) { + return NextResponse.json( + { error: "startDate and endDate must be valid ISO date strings" }, + { status: 400 } + ); + } + + const tenantPrisma = withTenantContext(user.tenantId); + + try { + const report = await FinancialReportService.getIncomeStatement(tenantPrisma, { + startDate, + endDate, + }); + return NextResponse.json(report); + } catch (err) { + const message = err instanceof Error ? err.message : "Failed to generate income statement"; + return NextResponse.json({ error: message }, { status: 500 }); + } + } +); diff --git a/src/app/api/reports/trial-balance/route.ts b/src/app/api/reports/trial-balance/route.ts new file mode 100644 index 0000000..070c9d1 --- /dev/null +++ b/src/app/api/reports/trial-balance/route.ts @@ -0,0 +1,41 @@ +/** + * GET /api/reports/trial-balance + * + * Trial Balance report -- all account balances, verifies debits = credits. + * Derived entirely from POSTED journal entry lines. + * + * Query params: + * asOfDate? - ISO date string (default: now) + * + * Access: ADMIN only. + */ +import { NextRequest, NextResponse } from "next/server"; +import { withPermission } from "@/lib/middleware/authorize"; +import { withTenantContext } from "@/lib/prisma-tenant"; +import { FinancialReportService } from "@/lib/services/financial-report-service"; + +export const GET = withPermission("read", "Report")( + async (req: NextRequest, { user }) => { + if (!user.tenantId) { + return NextResponse.json( + { error: "No tenant context -- super-admins must use the admin API" }, + { status: 400 } + ); + } + + const { searchParams } = new URL(req.url); + const asOfDateParam = searchParams.get("asOfDate"); + + const tenantPrisma = withTenantContext(user.tenantId); + + try { + const report = await FinancialReportService.getTrialBalance(tenantPrisma, { + asOfDate: asOfDateParam ? new Date(asOfDateParam) : undefined, + }); + return NextResponse.json(report); + } catch (err) { + const message = err instanceof Error ? err.message : "Failed to generate trial balance"; + return NextResponse.json({ error: message }, { status: 500 }); + } + } +);