feat(04-05): financial report API routes — trial balance, income statement, balance sheet, drill-down
- 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 <noreply@anthropic.com>
This commit is contained in:
52
src/app/api/reports/accounts/[id]/entries/route.ts
Normal file
52
src/app/api/reports/accounts/[id]/entries/route.ts
Normal file
@@ -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);
|
||||
}
|
||||
57
src/app/api/reports/balance-sheet/route.ts
Normal file
57
src/app/api/reports/balance-sheet/route.ts
Normal file
@@ -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 });
|
||||
}
|
||||
}
|
||||
);
|
||||
61
src/app/api/reports/income-statement/route.ts
Normal file
61
src/app/api/reports/income-statement/route.ts
Normal file
@@ -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 });
|
||||
}
|
||||
}
|
||||
);
|
||||
41
src/app/api/reports/trial-balance/route.ts
Normal file
41
src/app/api/reports/trial-balance/route.ts
Normal file
@@ -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 });
|
||||
}
|
||||
}
|
||||
);
|
||||
Reference in New Issue
Block a user