pelagia-portal/App/tests/integration/payment-actions.test.ts
2026-05-18 23:18:58 +05:30

147 lines
5.6 KiB
TypeScript

/**
* 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<string> {
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");
});
});