const Fastify = require("fastify"); const apOperationsRoutes = require("../../../src/routes/apOperations"); const { hasPermission, resolvePermissionsForRoles } = require("../../../src/constants/permissions"); const paths = [ "/api/ap-operations", "/api/ap-operations/reimbursement-ledger", "/api/ap-operations/reimbursement-ledger?page=2&pageSize=10&status=approved&invoiceStatus=none&dateFrom=2026-01-01&dateTo=2026-09-07&q=synthetic", ]; async function buildApp(permissionCodes, enabled = true) { const app = Fastify(); const queries = []; const prisma = { featureFlag: { findUnique: vi.fn().mockResolvedValue({ flag_key: "payment_batch_v1", override_mode: enabled ? "force_on" : "force_off", rollout_type: "global", department_ids: [], }), }, }; for (const model of ["paymentBatch", "paymentBatchItem", "entityRiskFlag", "travelReconciliationItem", "reimbursementForm"]) { prisma[model] = { findMany: vi.fn().mockResolvedValue([]), count: vi.fn().mockResolvedValue(0), }; queries.push(...Object.values(prisma[model])); } app.decorate("config", { paymentBatchEnabled: true }); app.decorate("prisma", prisma); app.decorate("authenticate", async (request) => { request.employee = { id: "synthetic-actor", permissionCodes }; }); app.decorate("hasPermission", (employee, permission) => hasPermission(employee.permissionCodes, permission)); app.setErrorHandler((error, request, reply) => { reply.code(error.statusCode ?? 500).send({ error: { code: error.code, ...(error.details && Object.keys(error.details).length ? { details: error.details } : {}) } }); }); await app.register(apOperationsRoutes, { prefix: "/api" }); return { app, queries }; } describe("AP full-ledger permission boundary", () => { it.each([ ["employee", false], ["manager", false], ["system_admin", false], ["ceo", false], ["finance", true], ["finance_mgr", true], ["admin", true], ])("checks both reads for %s", async (role, allowed) => { const { app, queries } = await buildApp(resolvePermissionsForRoles([role])); try { for (const url of paths) { const response = await app.inject({ method: "GET", url }); expect(response.statusCode).toBe(allowed ? 200 : 403); if (!allowed) { expect(response.json()).toEqual({ error: { code: "PERM_001" } }); for (const query of queries) expect(query).not.toHaveBeenCalled(); } } if (allowed) expect(queries.some((query) => query.mock.calls.length > 0)).toBe(true); } finally { await app.close(); } }); it.each(["payment.batch.manage", "payment.batch.review", "risk.review.manage", "expense.finance.pay"])( "does not infer ledger reading from %s", async (permission) => { const { app, queries } = await buildApp([permission]); try { for (const url of paths) expect((await app.inject({ method: "GET", url })).statusCode).toBe(403); for (const query of queries) expect(query).not.toHaveBeenCalled(); } finally { await app.close(); } }, ); it.each([["finance.ledger.view"], resolvePermissionsForRoles(["manager", "finance"]), resolvePermissionsForRoles(["ceo", "finance"])].map((permissions) => [permissions]))( "honors explicit or combined-role ledger authority %j", async (permissions) => { const { app } = await buildApp(permissions); try { for (const url of paths) expect((await app.inject({ method: "GET", url })).statusCode).toBe(200); } finally { await app.close(); } }, ); it("preserves feature-off refusal without querying financial data", async () => { const { app, queries } = await buildApp(["finance.ledger.view"], false); try { for (const url of paths) { const response = await app.inject({ method: "GET", url }); expect(response.statusCode).toBe(409); expect(response.json().error.details).toEqual({ featureFlag: "payment_batch_v1_disabled", reason: "flag_force_off", reasonCode: "ap_operations.disabled" }); } for (const query of queries) expect(query).not.toHaveBeenCalled(); } finally { await app.close(); } }); }); describe("AP direct ledger calendar boundary", () => { it.each(["dateFrom", "dateTo"].flatMap((field) => ["2026-02-29", "2026-04-31", "2026-13-01"].map((date) => [field, date])))( "rejects %s=%s before ledger queries", async (field, date) => { const { app, queries } = await buildApp(["finance.ledger.view"]); try { const response = await app.inject({ method: "GET", url: `/api/ap-operations/reimbursement-ledger?${field}=${date}` }); expect(response.statusCode).toBe(400); expect(response.json().error.code).toBe("VALID_001"); expect(response.json().error.details).toEqual({ formErrors: [], fieldErrors: { [field]: ["invalid_calendar_date"] }, reasonCode: "ap_operations.ledger_query_invalid" }); for (const query of queries) expect(query).not.toHaveBeenCalled(); } finally { await app.close(); } }, ); it.each([["2024-02-29", "2026-04-30"], ["2026-04-30", "2024-02-29"]])( "preserves UTC bounds and existing reverse-range semantics %s / %s", async (dateFrom, dateTo) => { const { app } = await buildApp(["finance.ledger.view"]); try { expect((await app.inject({ method: "GET", url: `/api/ap-operations/reimbursement-ledger?dateFrom=${dateFrom}&dateTo=${dateTo}` })).statusCode).toBe(200); expect(app.prisma.reimbursementForm.findMany).toHaveBeenCalledWith(expect.objectContaining({ where: { AND: expect.arrayContaining([{ reimbursement_date: { gte: new Date(`${dateFrom}T00:00:00.000Z`), lte: new Date(`${dateTo}T23:59:59.999Z`) } }]) } })); } finally { await app.close(); } }, ); });