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