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, 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", details: {} } }); 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()).toEqual({ error: { code: "VALID_001", details: { featureFlag: "payment_batch_v1_disabled", reason: "flag_force_off", reasonCode: "ap_operations.feature_disabled" }, }, }); } for (const query of queries) expect(query).not.toHaveBeenCalled(); } finally { await app.close(); } }); it("rejects invalid ledger query params with a precise reasonCode and no ledger read", async () => { const { app, queries } = await buildApp(["finance.ledger.view"], true); try { const response = await app.inject({ method: "GET", url: "/api/ap-operations/reimbursement-ledger?pageSize=999", }); expect(response.statusCode).toBe(400); const body = response.json(); expect(body.error.code).toBe("VALID_001"); expect(body.error.details.reasonCode).toBe("ap_operations.query_invalid"); expect(body.error.details.fieldErrors).toHaveProperty("pageSize"); for (const query of queries) expect(query).not.toHaveBeenCalled(); } finally { await app.close(); } }); it("still returns 200 for a legitimate ledger query when the feature is on", async () => { const { app } = await buildApp(["finance.ledger.view"], true); try { const response = await app.inject({ method: "GET", url: "/api/ap-operations/reimbursement-ledger" }); expect(response.statusCode).toBe(200); } finally { await app.close(); } }); });