feat(02-01): COA auto-provisioning on tenant signup + API routes + tests
- Create seed-coa.ts: seedChartOfAccounts(tx, tenantId) seeds 28 accounts in transaction - Update tenant.ts: createTenant() calls seedChartOfAccounts inside $transaction block - Add GET /api/accounting/accounts — list COA for tenant (requires read:Account) - Add GET /api/accounting/periods — list accounting periods (requires read:Account) - Add POST /api/accounting/periods/[id]/close — close period (requires manage:Account) - Add 28 integration tests: COA definition, seeding, period management, createTenant integration - All 121 tests pass (93 existing + 28 new)
This commit is contained in:
45
src/app/api/accounting/accounts/route.ts
Normal file
45
src/app/api/accounting/accounts/route.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { withPermission } from "@/lib/middleware/authorize";
|
||||
import { withTenantContext } from "@/lib/prisma-tenant";
|
||||
|
||||
/**
|
||||
* GET /api/accounting/accounts
|
||||
*
|
||||
* Returns the full Chart of Accounts for the authenticated tenant.
|
||||
* Accounts are ordered by code ascending (1000, 1010, 1020, ... 5090).
|
||||
*
|
||||
* Requires: ADMIN role (read on Account subject).
|
||||
*
|
||||
* Response:
|
||||
* 200 OK — Array of { id, code, name, accountType, normalBalance, parentId, isSystemAccount }
|
||||
* 401 Unauthorized — no session
|
||||
* 403 Forbidden — insufficient role
|
||||
*/
|
||||
export const GET = withPermission("read", "Account")(
|
||||
async (_req: NextRequest, { user }) => {
|
||||
if (!user.tenantId) {
|
||||
return NextResponse.json(
|
||||
{ error: "No tenant context — super-admins must use the admin API" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const tenantPrisma = withTenantContext(user.tenantId);
|
||||
|
||||
const accounts = await tenantPrisma.account.findMany({
|
||||
orderBy: { code: "asc" },
|
||||
select: {
|
||||
id: true,
|
||||
code: true,
|
||||
name: true,
|
||||
accountType: true,
|
||||
normalBalance: true,
|
||||
parentId: true,
|
||||
isSystemAccount: true,
|
||||
createdAt: true,
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(accounts);
|
||||
}
|
||||
);
|
||||
74
src/app/api/accounting/periods/[id]/close/route.ts
Normal file
74
src/app/api/accounting/periods/[id]/close/route.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { withPermission } from "@/lib/middleware/authorize";
|
||||
import { closePeriod } from "@/lib/accounting/accounting-period";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
/**
|
||||
* POST /api/accounting/periods/[id]/close
|
||||
*
|
||||
* Closes an accounting period, preventing future journal entries from
|
||||
* being posted to that period. The period must currently be OPEN.
|
||||
*
|
||||
* Requires: ADMIN role (manage on Account subject).
|
||||
*
|
||||
* Path param:
|
||||
* id — The UUID of the AccountingPeriod to close
|
||||
*
|
||||
* Response:
|
||||
* 200 OK — { id, year, month, status: "CLOSED", closedAt, closedById }
|
||||
* 400 Bad Request — period is already closed
|
||||
* 401 Unauthorized — no session
|
||||
* 403 Forbidden — insufficient role
|
||||
* 404 Not Found — period not found or belongs to different tenant
|
||||
* 500 Internal — unexpected error
|
||||
*/
|
||||
export function POST(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||
return withPermission("manage", "Account")(
|
||||
async (_req: NextRequest, { user }) => {
|
||||
const { id: periodId } = await params;
|
||||
|
||||
if (!user.tenantId) {
|
||||
return NextResponse.json(
|
||||
{ error: "No tenant context — super-admins must use the admin API" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Verify the period exists and belongs to this tenant before closing
|
||||
const period = await prisma.accountingPeriod.findFirst({
|
||||
where: { id: periodId, tenantId: user.tenantId },
|
||||
});
|
||||
|
||||
if (!period) {
|
||||
return NextResponse.json(
|
||||
{ error: "Accounting period not found" },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const closed = await closePeriod(prisma, periodId, user.id);
|
||||
|
||||
return NextResponse.json({
|
||||
id: closed.id,
|
||||
year: closed.year,
|
||||
month: closed.month,
|
||||
status: closed.status,
|
||||
closedAt: closed.closedAt,
|
||||
closedById: closed.closedById,
|
||||
tenantId: closed.tenantId,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message.includes("already closed")) {
|
||||
return NextResponse.json({ error: error.message }, { status: 400 });
|
||||
}
|
||||
|
||||
console.error("[POST /api/accounting/periods/[id]/close] Unexpected error:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "An unexpected error occurred. Please try again." },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
)(req);
|
||||
}
|
||||
44
src/app/api/accounting/periods/route.ts
Normal file
44
src/app/api/accounting/periods/route.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { withPermission } from "@/lib/middleware/authorize";
|
||||
import { withTenantContext } from "@/lib/prisma-tenant";
|
||||
|
||||
/**
|
||||
* GET /api/accounting/periods
|
||||
*
|
||||
* Lists all accounting periods for the authenticated tenant.
|
||||
* Ordered by year descending, then month descending (most recent first).
|
||||
*
|
||||
* Requires: ADMIN role (read on Account subject).
|
||||
*
|
||||
* Response:
|
||||
* 200 OK — Array of { id, year, month, status, closedAt, closedById, createdAt }
|
||||
* 401 Unauthorized — no session
|
||||
* 403 Forbidden — insufficient role
|
||||
*/
|
||||
export const GET = withPermission("read", "Account")(
|
||||
async (_req: NextRequest, { user }) => {
|
||||
if (!user.tenantId) {
|
||||
return NextResponse.json(
|
||||
{ error: "No tenant context — super-admins must use the admin API" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const tenantPrisma = withTenantContext(user.tenantId);
|
||||
|
||||
const periods = await tenantPrisma.accountingPeriod.findMany({
|
||||
orderBy: [{ year: "desc" }, { month: "desc" }],
|
||||
select: {
|
||||
id: true,
|
||||
year: true,
|
||||
month: true,
|
||||
status: true,
|
||||
closedAt: true,
|
||||
closedById: true,
|
||||
createdAt: true,
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(periods);
|
||||
}
|
||||
);
|
||||
Reference in New Issue
Block a user