147 lines
5.6 KiB
TypeScript
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");
|
|
});
|
|
});
|