/** * Integration tests for PO creation server action. * Covers: S-01 (create with line items), S-02 (save as draft), S-03 (submit for approval). */ 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 { makeSession, getSeedUser, getSeedVessel, getSeedAccount, getSeedVendor, makePoForm, deletePosByTitle, } from "./helpers"; const PREFIX = "INTTEST_CREATE_"; let techId: string; let vesselId: string; let accountId: string; let vendorId: string; beforeAll(async () => { const [tech, vessel, account, vendor] = await Promise.all([ getSeedUser("tech@pelagia.local"), getSeedVessel("MV Ocean Pride"), getSeedAccount("700201"), getSeedVendor("Apar Industries Ltd"), ]); techId = tech.id; vesselId = vessel.id; accountId = account.id; vendorId = vendor.id; }); afterEach(async () => { await deletePosByTitle(PREFIX); }); // ── S-02: Save as draft ────────────────────────────────────────────────────── describe("S-02 — save as draft", () => { it("creates a PO in DRAFT status", async () => { vi.mocked(auth).mockResolvedValue(makeSession(techId, "TECHNICAL")); const form = makePoForm({ title: `${PREFIX}Draft`, vesselId, accountId, intent: "draft", }); const result = await createPo(form); expect(result).not.toHaveProperty("error"); const id = (result as { id: string }).id; const po = await db.purchaseOrder.findUnique({ where: { id } }); expect(po?.status).toBe("DRAFT"); expect(po?.submittedAt).toBeNull(); }); it("returns error for unauthenticated request", async () => { vi.mocked(auth).mockResolvedValue(null); const form = makePoForm({ title: `${PREFIX}Unauth`, vesselId, accountId }); const result = await createPo(form); expect(result).toEqual({ error: "Unauthorized" }); }); it("returns error when ACCOUNTS role tries to create a PO", async () => { const acct = await getSeedUser("accounts@pelagia.local"); vi.mocked(auth).mockResolvedValue(makeSession(acct.id, "ACCOUNTS")); const form = makePoForm({ title: `${PREFIX}ForbiddenAccts`, vesselId, accountId }); const result = await createPo(form); expect(result).toHaveProperty("error"); }); it("returns error when a required field (vesselId) is missing", async () => { vi.mocked(auth).mockResolvedValue(makeSession(techId, "TECHNICAL")); const form = new FormData(); form.set("title", `${PREFIX}NoVessel`); form.set("accountId", accountId); form.set("intent", "draft"); form.set("lineItems[0].description", "Item"); form.set("lineItems[0].quantity", "1"); form.set("lineItems[0].unit", "pc"); form.set("lineItems[0].unitPrice", "50"); form.set("lineItems[0].gstRate", "0.18"); const result = await createPo(form); expect(result).toHaveProperty("error"); }); }); // ── S-01: Create with line items ───────────────────────────────────────────── describe("S-01 — create PO with line items", () => { it("stores line items with correct quantity, unit price, and GST rate", async () => { vi.mocked(auth).mockResolvedValue(makeSession(techId, "TECHNICAL")); const form = makePoForm({ title: `${PREFIX}LineItems`, vesselId, accountId, vendorId, intent: "draft", lineItems: [ { description: "Gear Oil 80W90", quantity: 50, unit: "L", unitPrice: 182, gstRate: 0.18 }, { description: "Engine Filter", quantity: 4, unit: "pc", unitPrice: 250, gstRate: 0.12 }, ], }); const result = await createPo(form); const id = (result as { id: string }).id; const po = await db.purchaseOrder.findUnique({ where: { id }, include: { lineItems: { orderBy: { sortOrder: "asc" } } }, }); expect(po?.lineItems).toHaveLength(2); expect(Number(po!.lineItems[0].quantity)).toBe(50); expect(Number(po!.lineItems[0].unitPrice)).toBe(182); expect(Number(po!.lineItems[0].gstRate)).toBeCloseTo(0.18); expect(Number(po!.lineItems[1].unitPrice)).toBe(250); expect(Number(po!.lineItems[1].gstRate)).toBeCloseTo(0.12); }); it("sets totalAmount to grand total including GST", async () => { vi.mocked(auth).mockResolvedValue(makeSession(techId, "TECHNICAL")); // 10 × 100 × 1.18 = 1180 const form = makePoForm({ title: `${PREFIX}GrandTotal`, vesselId, accountId, intent: "draft", lineItems: [{ description: "Item", quantity: 10, unit: "pc", unitPrice: 100, gstRate: 0.18 }], }); const result = await createPo(form); const id = (result as { id: string }).id; const po = await db.purchaseOrder.findUnique({ where: { id } }); expect(Number(po!.totalAmount)).toBeCloseTo(1180, 1); }); it("stores optional fields (PI quotation no, place of delivery, TC fields)", async () => { vi.mocked(auth).mockResolvedValue(makeSession(techId, "TECHNICAL")); const form = makePoForm({ title: `${PREFIX}Optional`, vesselId, accountId, intent: "draft" }); form.set("piQuotationNo", "Verbal"); form.set("placeOfDelivery", "CBD Belapur, Navi Mumbai"); form.set("tcDelivery", "Within 7 days"); form.set("tcPaymentTerms", "Net 45"); const result = await createPo(form); const id = (result as { id: string }).id; const po = await db.purchaseOrder.findUnique({ where: { id } }); expect((po as any).piQuotationNo).toBe("Verbal"); expect((po as any).placeOfDelivery).toBe("CBD Belapur, Navi Mumbai"); expect((po as any).tcDelivery).toBe("Within 7 days"); expect((po as any).tcPaymentTerms).toBe("Net 45"); }); it("allows MANNING role to create a PO", async () => { const manning = await getSeedUser("manning@pelagia.local"); vi.mocked(auth).mockResolvedValue(makeSession(manning.id, "MANNING")); const form = makePoForm({ title: `${PREFIX}Manning`, vesselId, accountId }); const result = await createPo(form); expect(result).not.toHaveProperty("error"); }); }); // ── S-03: Submit for approval ───────────────────────────────────────────────── describe("S-03 — submit for approval", () => { it("creates PO with status MGR_REVIEW and sets submittedAt", async () => { vi.mocked(auth).mockResolvedValue(makeSession(techId, "TECHNICAL")); const form = makePoForm({ title: `${PREFIX}Submit`, vesselId, accountId, intent: "submit" }); const result = await createPo(form); expect(result).not.toHaveProperty("error"); const id = (result as { id: string }).id; const po = await db.purchaseOrder.findUnique({ where: { id } }); expect(po?.status).toBe("MGR_REVIEW"); expect(po?.submittedAt).not.toBeNull(); }); it("sends notification to managers on submit", async () => { const { notify } = await import("@/lib/notifier"); vi.mocked(notify).mockClear(); vi.mocked(auth).mockResolvedValue(makeSession(techId, "TECHNICAL")); const form = makePoForm({ title: `${PREFIX}Notify`, vesselId, accountId, intent: "submit" }); await createPo(form); expect(vi.mocked(notify)).toHaveBeenCalledWith( expect.objectContaining({ event: "PO_SUBMITTED" }) ); }); });