/** * Integration tests for accounts payment server actions. * Covers: A-01 (payment queue — PO reaches MGR_APPROVED), A-02 (mark paid with reference number). */ import { vi, describe, it, expect, beforeAll, afterEach } from "vitest"; vi.mock("@/auth", () => ({ auth: vi.fn() })); vi.mock("next/cache", () => ({ revalidatePath: vi.fn() })); vi.mock("@/lib/notifier", () => ({ notify: vi.fn() })); import { auth } from "@/auth"; import { db } from "@/lib/db"; import { createPo } from "@/app/(portal)/po/new/actions"; import { approvePo } from "@/app/(portal)/approvals/[id]/actions"; import { processPayment, markPaid } from "@/app/(portal)/payments/actions"; import { makeSession, getSeedUser, getSeedVessel, getSeedAccount, makePoForm, deletePosByTitle, } from "./helpers"; const PREFIX = "INTTEST_PAYMENT_"; let techId: string; let managerId: string; let accountsId: string; let vesselId: string; let accountId: string; beforeAll(async () => { const [tech, mgr, acct, vessel, account] = await Promise.all([ getSeedUser("tech@pelagia.local"), getSeedUser("manager@pelagia.local"), getSeedUser("accounts@pelagia.local"), getSeedVessel("MV Sea Breeze"), getSeedAccount("700202"), ]); techId = tech.id; managerId = mgr.id; accountsId = acct.id; vesselId = vessel.id; accountId = account.id; }); afterEach(async () => { await deletePosByTitle(PREFIX); }); // Helper: create PO → submit → approve (reaches MGR_APPROVED) async function createApprovedPo(title: string): Promise { vi.mocked(auth).mockResolvedValue(makeSession(techId, "TECHNICAL")); const form = makePoForm({ title, vesselId, accountId, intent: "submit" }); const { id: poId } = (await createPo(form)) as { id: string }; vi.mocked(auth).mockResolvedValue(makeSession(managerId, "MANAGER")); await approvePo({ poId }); return poId; } // ── A-01: PO reaches payment queue (MGR_APPROVED) ─────────────────────────── describe("A-01 — approved PO appears in payment queue", () => { it("PO has status MGR_APPROVED after manager approves", async () => { const poId = await createApprovedPo(`${PREFIX}Queue`); const po = await db.purchaseOrder.findUnique({ where: { id: poId } }); expect(po?.status).toBe("MGR_APPROVED"); }); it("processPayment transitions MGR_APPROVED to SENT_FOR_PAYMENT", async () => { const poId = await createApprovedPo(`${PREFIX}ProcessPayment`); vi.mocked(auth).mockResolvedValue(makeSession(accountsId, "ACCOUNTS")); const result = await processPayment({ poId }); expect(result).toEqual({ ok: true }); const po = await db.purchaseOrder.findUnique({ where: { id: poId } }); expect(po?.status).toBe("SENT_FOR_PAYMENT"); }); it("TECHNICAL role cannot process payment", async () => { const poId = await createApprovedPo(`${PREFIX}PaymentForbidden`); vi.mocked(auth).mockResolvedValue(makeSession(techId, "TECHNICAL")); const result = await processPayment({ poId }); expect(result).toHaveProperty("error"); }); }); // ── A-02: Mark as paid with reference number ───────────────────────────────── describe("A-02 — mark PO as paid with reference number", () => { it("transitions SENT_FOR_PAYMENT to PAID_DELIVERED and stores paymentRef", async () => { const poId = await createApprovedPo(`${PREFIX}MarkPaid`); vi.mocked(auth).mockResolvedValue(makeSession(accountsId, "ACCOUNTS")); await processPayment({ poId }); const result = await markPaid({ poId, paymentRef: "NEFT/2026/001234" }); expect(result).toEqual({ ok: true }); const po = await db.purchaseOrder.findUnique({ where: { id: poId } }); expect(po?.status).toBe("PAID_DELIVERED"); expect(po?.paymentRef).toBe("NEFT/2026/001234"); expect(po?.paidAt).not.toBeNull(); }); it("creates a PAYMENT_SENT action in the audit trail", async () => { const poId = await createApprovedPo(`${PREFIX}PaidAudit`); vi.mocked(auth).mockResolvedValue(makeSession(accountsId, "ACCOUNTS")); await processPayment({ poId }); await markPaid({ poId, paymentRef: "TXN-9999" }); const action = await db.pOAction.findFirst({ where: { poId, actionType: "PAYMENT_SENT" } }); expect(action).not.toBeNull(); expect((action?.metadata as { paymentRef?: string })?.paymentRef).toBe("TXN-9999"); }); it("returns error when paymentRef is missing", async () => { const poId = await createApprovedPo(`${PREFIX}PaidNoRef`); vi.mocked(auth).mockResolvedValue(makeSession(accountsId, "ACCOUNTS")); await processPayment({ poId }); const result = await markPaid({ poId, paymentRef: "" }); expect(result).toHaveProperty("error"); }); it("notifies submitter and managers when payment is marked", async () => { const { notify } = await import("@/lib/notifier"); const poId = await createApprovedPo(`${PREFIX}PaidNotify`); vi.mocked(auth).mockResolvedValue(makeSession(accountsId, "ACCOUNTS")); vi.mocked(notify).mockClear(); await processPayment({ poId }); await markPaid({ poId, paymentRef: "REF-42" }); const calls = vi.mocked(notify).mock.calls.map((c) => c[0].event); expect(calls).toContain("PAYMENT_SENT"); }); it("MANAGER role cannot mark as paid (wrong permission)", async () => { const poId = await createApprovedPo(`${PREFIX}PaidMgrForbidden`); vi.mocked(auth).mockResolvedValue(makeSession(accountsId, "ACCOUNTS")); await processPayment({ poId }); vi.mocked(auth).mockResolvedValue(makeSession(managerId, "MANAGER")); const result = await markPaid({ poId, paymentRef: "MGR-REF" }); expect(result).toHaveProperty("error"); }); });