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

235 lines
9.8 KiB
TypeScript

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