diff --git a/App/tests/fixtures/Sample_PO.xlsx b/App/tests/fixtures/Sample_PO.xlsx new file mode 100644 index 0000000..3cba5eb Binary files /dev/null and b/App/tests/fixtures/Sample_PO.xlsx differ diff --git a/App/tests/integration/approval-actions.test.ts b/App/tests/integration/approval-actions.test.ts index 76890f3..31f17ca 100644 --- a/App/tests/integration/approval-actions.test.ts +++ b/App/tests/integration/approval-actions.test.ts @@ -32,7 +32,7 @@ beforeAll(async () => { const [tech, mgr, vessel, account, vendor] = await Promise.all([ getSeedUser("tech@pelagia.local"), getSeedUser("manager@pelagia.local"), - getSeedVessel("MV Ocean Pride"), + getSeedVessel("MV Poseidon"), getSeedAccount("700201"), getSeedVendor("Apar Industries Ltd"), ]); @@ -52,7 +52,11 @@ async function createSubmittedPo(title: string): Promise { vi.mocked(auth as unknown as () => Promise).mockResolvedValue(makeSession(techId, "TECHNICAL")); const form = makePoForm({ title, vesselId, accountId, intent: "submit" }); const result = await createPo(form); - return (result as { id: string }).id; + const id = (result as { id: string }).id; + // Vendor gating: a vendor must be assigned before a PO can be approved. + // Attach the seeded verified vendor directly (test setup) so approval-path tests run. + await db.purchaseOrder.update({ where: { id }, data: { vendorId } }); + return id; } // ── M-02: Approve ───────────────────────────────────────────────────────────── diff --git a/App/tests/integration/confirm-receipt.test.ts b/App/tests/integration/confirm-receipt.test.ts index 49a3891..22a0bc2 100644 --- a/App/tests/integration/confirm-receipt.test.ts +++ b/App/tests/integration/confirm-receipt.test.ts @@ -20,6 +20,7 @@ import { getSeedUser, getSeedVessel, getSeedAccount, + getSeedVendor, makePoForm, deletePosByTitle, } from "./helpers"; @@ -32,20 +33,23 @@ let managerId: string; let accountsId: string; let vesselId: string; let accountId: string; +let vendorId: string; beforeAll(async () => { - const [tech, mgr, acct, vessel, account] = await Promise.all([ + const [tech, mgr, acct, vessel, account, vendor] = await Promise.all([ getSeedUser("tech@pelagia.local"), getSeedUser("manager@pelagia.local"), getSeedUser("accounts@pelagia.local"), - getSeedVessel("MV Sea Breeze"), + getSeedVessel("MV Nereid"), getSeedAccount("700202"), + getSeedVendor("Apar Industries Ltd"), ]); techId = tech.id; managerId = mgr.id; accountsId = acct.id; vesselId = vessel.id; accountId = account.id; + vendorId = vendor.id; }); afterEach(async () => { @@ -57,6 +61,8 @@ async function createPaidPo(title: string): Promise { vi.mocked(auth as unknown as () => Promise).mockResolvedValue(makeSession(techId, "TECHNICAL")); const form = makePoForm({ title, vesselId, accountId, intent: "submit" }); const { id: poId } = (await createPo(form)) as { id: string }; + // Vendor gating: approval requires an assigned vendor. + await db.purchaseOrder.update({ where: { id: poId }, data: { vendorId } }); vi.mocked(auth as unknown as () => Promise).mockResolvedValue(makeSession(managerId, "MANAGER")); await approvePo({ poId }); diff --git a/App/tests/integration/create-po.test.ts b/App/tests/integration/create-po.test.ts index 340bf87..2cb5724 100644 --- a/App/tests/integration/create-po.test.ts +++ b/App/tests/integration/create-po.test.ts @@ -25,7 +25,7 @@ let vendorId: string; beforeAll(async () => { const [tech, vessel, account, vendor] = await Promise.all([ getSeedUser("tech@pelagia.local"), - getSeedVessel("MV Ocean Pride"), + getSeedVessel("MV Aegean Wind"), getSeedAccount("700201"), getSeedVendor("Apar Industries Ltd"), ]); @@ -79,7 +79,7 @@ describe("S-02 — save as draft", () => { form.set("title", `${PREFIX}NoVessel`); form.set("accountId", accountId); form.set("intent", "draft"); - form.set("lineItems[0].description", "Item"); + form.set("lineItems[0].name", "Item"); form.set("lineItems[0].quantity", "1"); form.set("lineItems[0].unit", "pc"); form.set("lineItems[0].unitPrice", "50"); diff --git a/App/tests/integration/discard-po.test.ts b/App/tests/integration/discard-po.test.ts index 694c6f8..b1a2269 100644 --- a/App/tests/integration/discard-po.test.ts +++ b/App/tests/integration/discard-po.test.ts @@ -30,7 +30,7 @@ beforeAll(async () => { getSeedUser("manager@pelagia.local"), getSeedUser("accounts@pelagia.local"), getSeedVessel("MV Pelagia Star"), - getSeedAccount("TECH-OPS"), + getSeedAccount("700201"), ]); techId = tech.id; managerId = mgr.id; diff --git a/App/tests/integration/helpers.ts b/App/tests/integration/helpers.ts index c5483a3..47c73c2 100644 --- a/App/tests/integration/helpers.ts +++ b/App/tests/integration/helpers.ts @@ -46,7 +46,7 @@ export function appendLineItem( idx: number, item: { description: string; quantity: number; unit: string; unitPrice: number; gstRate?: number } ) { - form.set(`lineItems[${idx}].description`, item.description); + form.set(`lineItems[${idx}].name`, item.description); form.set(`lineItems[${idx}].quantity`, String(item.quantity)); form.set(`lineItems[${idx}].unit`, item.unit); form.set(`lineItems[${idx}].unitPrice`, String(item.unitPrice)); @@ -76,12 +76,22 @@ export function makePoForm(overrides: { // ── Cleanup helpers ────────────────────────────────────────────────────────── +// POAction has no onDelete: Cascade, so its rows must be removed before the PO. +// (POLineItem / PODocument / Receipt cascade automatically.) +async function deletePosByIds(ids: string[]) { + if (ids.length === 0) return; + await db.pOAction.deleteMany({ where: { poId: { in: ids } } }); + await db.purchaseOrder.deleteMany({ where: { id: { in: ids } } }); +} + export async function deletePo(poId: string) { - await db.purchaseOrder.delete({ where: { id: poId } }).catch(() => {}); + await deletePosByIds([poId]).catch(() => {}); } export async function deletePosByTitle(titlePrefix: string) { - await db.purchaseOrder.deleteMany({ + const pos = await db.purchaseOrder.findMany({ where: { title: { startsWith: titlePrefix } }, + select: { id: true }, }); + await deletePosByIds(pos.map((p) => p.id)); } diff --git a/App/tests/integration/import-api.test.ts b/App/tests/integration/import-api.test.ts index fe78380..64c4fd5 100644 --- a/App/tests/integration/import-api.test.ts +++ b/App/tests/integration/import-api.test.ts @@ -15,7 +15,7 @@ import { POST } from "@/app/api/po/import/route"; import { makeSession, getSeedUser } from "./helpers"; import type { ParsedImport } from "@/lib/po-import-parser"; -const SAMPLE_XLSX = resolve(__dirname, "../../../../Prototype/Sample_PO.xlsx"); +const SAMPLE_XLSX = resolve(__dirname, "../fixtures/Sample_PO.xlsx"); let techId: string; let managerId: string; diff --git a/App/tests/integration/manager-po-creation.test.ts b/App/tests/integration/manager-po-creation.test.ts index 375bd8c..a87b319 100644 --- a/App/tests/integration/manager-po-creation.test.ts +++ b/App/tests/integration/manager-po-creation.test.ts @@ -30,7 +30,7 @@ beforeAll(async () => { getSeedUser("manager@pelagia.local"), getSeedUser("accounts@pelagia.local"), getSeedVessel("MV Pelagia Star"), - getSeedAccount("TECH-OPS"), + getSeedAccount("700201"), getSeedVendor("Apar Industries Ltd"), ]); managerId = mgr.id; diff --git a/App/tests/integration/payment-actions.test.ts b/App/tests/integration/payment-actions.test.ts index 4538053..dedd664 100644 --- a/App/tests/integration/payment-actions.test.ts +++ b/App/tests/integration/payment-actions.test.ts @@ -14,7 +14,7 @@ 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, + makeSession, getSeedUser, getSeedVessel, getSeedAccount, getSeedVendor, makePoForm, deletePosByTitle, } from "./helpers"; @@ -25,20 +25,23 @@ let managerId: string; let accountsId: string; let vesselId: string; let accountId: string; +let vendorId: string; beforeAll(async () => { - const [tech, mgr, acct, vessel, account] = await Promise.all([ + const [tech, mgr, acct, vessel, account, vendor] = await Promise.all([ getSeedUser("tech@pelagia.local"), getSeedUser("manager@pelagia.local"), getSeedUser("accounts@pelagia.local"), - getSeedVessel("MV Sea Breeze"), + getSeedVessel("MV Thetis"), getSeedAccount("700202"), + getSeedVendor("Apar Industries Ltd"), ]); techId = tech.id; managerId = mgr.id; accountsId = acct.id; vesselId = vessel.id; accountId = account.id; + vendorId = vendor.id; }); afterEach(async () => { @@ -50,6 +53,8 @@ async function createApprovedPo(title: string): Promise { vi.mocked(auth as unknown as () => Promise).mockResolvedValue(makeSession(techId, "TECHNICAL")); const form = makePoForm({ title, vesselId, accountId, intent: "submit" }); const { id: poId } = (await createPo(form)) as { id: string }; + // Vendor gating: approval requires an assigned vendor. + await db.purchaseOrder.update({ where: { id: poId }, data: { vendorId } }); vi.mocked(auth as unknown as () => Promise).mockResolvedValue(makeSession(managerId, "MANAGER")); await approvePo({ poId }); diff --git a/App/tests/integration/products-search.test.ts b/App/tests/integration/products-search.test.ts index afc0129..2b27a0a 100644 --- a/App/tests/integration/products-search.test.ts +++ b/App/tests/integration/products-search.test.ts @@ -91,7 +91,8 @@ describe("GET /api/products/search — search behaviour", () => { it("finds products by product code", async () => { const res = await GET(makeRequest("LUBE")); const data: { code: string }[] = await res.json(); - expect(data.every((p) => p.code.toUpperCase().includes("LUBE"))).toBe(true); + // search spans code/name/description, so assert the code matches are present (not that every hit is a code match) + expect(data.some((p) => p.code.toUpperCase().includes("LUBE"))).toBe(true); }); it("finds products by description text", async () => { diff --git a/App/tests/integration/vendor-approval.test.ts b/App/tests/integration/vendor-approval.test.ts index 7917ebc..daff16a 100644 --- a/App/tests/integration/vendor-approval.test.ts +++ b/App/tests/integration/vendor-approval.test.ts @@ -39,7 +39,7 @@ beforeAll(async () => { getSeedUser("manager@pelagia.local"), getSeedUser("accounts@pelagia.local"), getSeedVessel("MV Pelagia Star"), - getSeedAccount("TECH-OPS"), + getSeedAccount("700201"), getSeedVendor("Apar Industries Ltd"), ]); techId = tech.id;