/** * Integration tests for manager approval server actions. * Covers: M-02 (approve / approve+note), M-03 (reject), M-04 (request edits, vendor ID), S-06 (provide vendor ID), S-07 (resubmit after edits). */ import { vi, describe, it, expect, beforeAll, beforeEach, 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 { updatePo } from "@/app/(portal)/po/[id]/edit/actions"; import { approvePo, rejectPo, requestEdits, requestVendorId, } from "@/app/(portal)/approvals/[id]/actions"; import { provideVendorId } from "@/app/(portal)/po/[id]/actions"; import { makeSession, getSeedUser, getSeedVessel, getSeedAccount, getSeedVendor, makePoForm, deletePosByTitle, } from "./helpers"; const PREFIX = "INTTEST_APPROVAL_"; let techId: string; let managerId: string; let vesselId: string; let accountId: string; let vendorId: string; beforeAll(async () => { const [tech, mgr, vessel, account, vendor] = await Promise.all([ getSeedUser("tech@pelagia.local"), getSeedUser("manager@pelagia.local"), getSeedVessel("MV Ocean Pride"), getSeedAccount("700201"), getSeedVendor("Apar Industries Ltd"), ]); techId = tech.id; managerId = mgr.id; vesselId = vessel.id; accountId = account.id; vendorId = vendor.id; }); afterEach(async () => { await deletePosByTitle(PREFIX); }); // Helper: create a PO in MGR_REVIEW state async function createSubmittedPo(title: string): Promise { vi.mocked(auth).mockResolvedValue(makeSession(techId, "TECHNICAL")); const form = makePoForm({ title, vesselId, accountId, intent: "submit" }); const result = await createPo(form); return (result as { id: string }).id; } // ── M-02: Approve ───────────────────────────────────────────────────────────── describe("M-02 — approve PO", () => { it("transitions PO from MGR_REVIEW to MGR_APPROVED", async () => { const poId = await createSubmittedPo(`${PREFIX}Approve`); vi.mocked(auth).mockResolvedValue(makeSession(managerId, "MANAGER")); const result = await approvePo({ poId }); expect(result).toEqual({ ok: true }); const po = await db.purchaseOrder.findUnique({ where: { id: poId } }); expect(po?.status).toBe("MGR_APPROVED"); expect(po?.approvedAt).not.toBeNull(); }); it("stores managerNote when approving with note", async () => { const poId = await createSubmittedPo(`${PREFIX}ApproveNote`); vi.mocked(auth).mockResolvedValue(makeSession(managerId, "MANAGER")); await approvePo({ poId, note: "Approved — expedite delivery", withNote: true }); const po = await db.purchaseOrder.findUnique({ where: { id: poId } }); expect(po?.managerNote).toBe("Approved — expedite delivery"); const action = await db.pOAction.findFirst({ where: { poId, actionType: "APPROVED_WITH_NOTE" }, }); expect(action).not.toBeNull(); }); it("notifies submitter and accounts on approval", async () => { const { notify } = await import("@/lib/notifier"); vi.mocked(notify).mockClear(); const poId = await createSubmittedPo(`${PREFIX}ApproveNotify`); vi.mocked(auth).mockResolvedValue(makeSession(managerId, "MANAGER")); await approvePo({ poId }); expect(vi.mocked(notify)).toHaveBeenCalledWith( expect.objectContaining({ event: "PO_APPROVED" }) ); }); it("returns error when TECHNICAL role tries to approve", async () => { const poId = await createSubmittedPo(`${PREFIX}ApproveForbidden`); vi.mocked(auth).mockResolvedValue(makeSession(techId, "TECHNICAL")); const result = await approvePo({ poId }); expect(result).toHaveProperty("error"); }); it("returns error when PO is not in MGR_REVIEW state", async () => { // Create a DRAFT PO, don't submit vi.mocked(auth).mockResolvedValue(makeSession(techId, "TECHNICAL")); const form = makePoForm({ title: `${PREFIX}ApproveDraft`, vesselId, accountId, intent: "draft" }); const { id: poId } = (await createPo(form)) as { id: string }; vi.mocked(auth).mockResolvedValue(makeSession(managerId, "MANAGER")); const result = await approvePo({ poId }); expect(result).toHaveProperty("error"); }); }); // ── M-03: Reject ────────────────────────────────────────────────────────────── describe("M-03 — reject PO", () => { it("transitions PO from MGR_REVIEW to REJECTED with note", async () => { const poId = await createSubmittedPo(`${PREFIX}Reject`); vi.mocked(auth).mockResolvedValue(makeSession(managerId, "MANAGER")); const result = await rejectPo({ poId, note: "Budget exceeded for this quarter" }); expect(result).toEqual({ ok: true }); const po = await db.purchaseOrder.findUnique({ where: { id: poId } }); expect(po?.status).toBe("REJECTED"); expect(po?.managerNote).toBe("Budget exceeded for this quarter"); }); it("creates a REJECTED action entry in the audit trail", async () => { const poId = await createSubmittedPo(`${PREFIX}RejectAudit`); vi.mocked(auth).mockResolvedValue(makeSession(managerId, "MANAGER")); await rejectPo({ poId, note: "Not needed" }); const action = await db.pOAction.findFirst({ where: { poId, actionType: "REJECTED" } }); expect(action?.note).toBe("Not needed"); }); it("notifies submitter on rejection", async () => { const { notify } = await import("@/lib/notifier"); vi.mocked(notify).mockClear(); const poId = await createSubmittedPo(`${PREFIX}RejectNotify`); vi.mocked(auth).mockResolvedValue(makeSession(managerId, "MANAGER")); await rejectPo({ poId, note: "See notes" }); expect(vi.mocked(notify)).toHaveBeenCalledWith( expect.objectContaining({ event: "PO_REJECTED" }) ); }); }); // ── M-04: Request edits ────────────────────────────────────────────────────── describe("M-04 — request edits", () => { it("transitions PO to EDITS_REQUESTED with manager note", async () => { const poId = await createSubmittedPo(`${PREFIX}Edits`); vi.mocked(auth).mockResolvedValue(makeSession(managerId, "MANAGER")); const result = await requestEdits({ poId, note: "Please add vendor ID" }); expect(result).toEqual({ ok: true }); const po = await db.purchaseOrder.findUnique({ where: { id: poId } }); expect(po?.status).toBe("EDITS_REQUESTED"); expect(po?.managerNote).toBe("Please add vendor ID"); }); }); // ── M-04: Request vendor ID ────────────────────────────────────────────────── describe("M-04 — request vendor ID", () => { it("transitions PO to VENDOR_ID_PENDING", async () => { const poId = await createSubmittedPo(`${PREFIX}VendorIdReq`); vi.mocked(auth).mockResolvedValue(makeSession(managerId, "MANAGER")); const result = await requestVendorId({ poId }); expect(result).toEqual({ ok: true }); const po = await db.purchaseOrder.findUnique({ where: { id: poId } }); expect(po?.status).toBe("VENDOR_ID_PENDING"); }); }); // ── S-06: Provide vendor ID ────────────────────────────────────────────────── describe("S-06 — provide vendor ID", () => { it("transitions VENDOR_ID_PENDING back to MGR_REVIEW", async () => { const poId = await createSubmittedPo(`${PREFIX}ProvideVendor`); vi.mocked(auth).mockResolvedValue(makeSession(managerId, "MANAGER")); await requestVendorId({ poId }); vi.mocked(auth).mockResolvedValue(makeSession(techId, "TECHNICAL")); const result = await provideVendorId({ poId, vendorId }); expect(result).toEqual({ ok: true }); const po = await db.purchaseOrder.findUnique({ where: { id: poId } }); expect(po?.status).toBe("MGR_REVIEW"); expect(po?.vendorId).toBe(vendorId); }); }); // ── S-07: Edit and resubmit ────────────────────────────────────────────────── describe("S-07 — edit and resubmit after edits requested", () => { it("resubmitting from EDITS_REQUESTED transitions to MGR_REVIEW", async () => { const poId = await createSubmittedPo(`${PREFIX}Resubmit`); vi.mocked(auth).mockResolvedValue(makeSession(managerId, "MANAGER")); await requestEdits({ poId, note: "Update line items" }); vi.mocked(auth).mockResolvedValue(makeSession(techId, "TECHNICAL")); const form = makePoForm({ title: `${PREFIX}Resubmit`, vesselId, accountId, intent: "resubmit" }); const result = await updatePo(poId, form); expect(result).toEqual({ id: poId }); const po = await db.purchaseOrder.findUnique({ where: { id: poId } }); expect(po?.status).toBe("MGR_REVIEW"); }); it("saving edits without resubmitting stays as DRAFT (save intent)", async () => { // Create a DRAFT PO vi.mocked(auth).mockResolvedValue(makeSession(techId, "TECHNICAL")); const form = makePoForm({ title: `${PREFIX}SaveDraft`, vesselId, accountId, intent: "draft" }); const { id: poId } = (await createPo(form)) as { id: string }; const editForm = makePoForm({ title: `${PREFIX}SaveDraft`, vesselId, accountId, intent: "save" }); const result = await updatePo(poId, editForm); expect(result).toEqual({ id: poId }); const po = await db.purchaseOrder.findUnique({ where: { id: poId } }); expect(po?.status).toBe("DRAFT"); }); });