pelagia-portal/App/tests/integration/create-po.test.ts
Hardik b70eec261b test(integration): repair stale integration suite (wip: 97/108)
The integration suite had rotted against the app. Systematic fixes:
- seed refs: MV Ocean Pride/Sea Breeze/TECH-OPS → current seed entities
- helper appendLineItem set lineItems[i].description; createPo now keys on
  lineItems[i].name → zero line items. Fixed to .name.
- vendor gating: lifecycle setups (approval/payment/receipt) now attach the
  seeded verified vendor before approval.
- cleanup: POAction has no onDelete:Cascade, so deletePo(sByTitle) now removes
  POAction rows before the PO.
- import-api: fixture committed to tests/fixtures/Sample_PO.xlsx (was an
  absolute path to a non-existent dir).
- products-search: code search assertion .every → .some (search spans fields).

11 failures remain (behavioral drift — separate commit).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 02:34:07 +05:30

192 lines
7.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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 Aegean Wind"),
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 as unknown as () => Promise<unknown>).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 as unknown as () => Promise<unknown>).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 as unknown as () => Promise<unknown>).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 as unknown as () => Promise<unknown>).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].name", "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 as unknown as () => Promise<unknown>).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 as unknown as () => Promise<unknown>).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 as unknown as () => Promise<unknown>).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 as unknown as () => Promise<unknown>).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 as unknown as () => Promise<unknown>).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 as unknown as () => Promise<unknown>).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" })
);
});
});