/** * Integration tests for Crewing Phase 5a verification: documents (MPO) and * bank/EPF (Accounts), with role gating per §6/§8.11. */ import { vi, describe, it, expect, beforeAll, afterAll, afterEach } from "vitest"; vi.mock("@/auth", () => ({ auth: vi.fn() })); vi.mock("next/cache", () => ({ revalidatePath: vi.fn() })); vi.mock("@/lib/feature-flags", () => ({ CREWING_ENABLED: true, INVENTORY_ENABLED: true })); import { auth } from "@/auth"; import { db } from "@/lib/db"; import { verifyDocument, verifyBankEpf } from "@/app/(portal)/crewing/verification/actions"; import { makeSession, getSeedUser } from "./helpers"; import type { Role } from "@prisma/client"; let manningId: string; let accountsId: string; let siteStaffId: string; const SS_EMAIL = "sitestaff@itver.local"; const as = (userId: string, role: Role) => vi.mocked(auth as unknown as () => Promise).mockResolvedValue(makeSession(userId, role)); async function crewWithRecords() { const c = await db.crewMember.create({ data: { name: "To Verify", status: "EMPLOYEE", type: "NEW", source: "CAREERS" } }); const doc = await db.seafarerDocument.create({ data: { crewMemberId: c.id, docType: "PASSPORT", number: "P999" } }); await db.bankDetail.create({ data: { crewMemberId: c.id, accountNumber: "123456789", ifsc: "HDFC0001" } }); await db.epfDetail.create({ data: { crewMemberId: c.id, uan: "UAN-1" } }); return { crewId: c.id, docId: doc.id }; } beforeAll(async () => { manningId = (await getSeedUser("manning@pelagia.local")).id; accountsId = (await getSeedUser("accounts@pelagia.local")).id; const ss = await db.user.upsert({ where: { email: SS_EMAIL }, update: { role: "SITE_STAFF", isActive: true }, create: { employeeId: "ITVER-SS", email: SS_EMAIL, name: "SS Ver", role: "SITE_STAFF" } }); siteStaffId = ss.id; }); afterEach(async () => { await db.crewAction.deleteMany({}); await db.seafarerDocument.deleteMany({}); await db.bankDetail.deleteMany({}); await db.epfDetail.deleteMany({}); await db.crewMember.deleteMany({}); vi.clearAllMocks(); }); afterAll(async () => { await db.user.deleteMany({ where: { email: SS_EMAIL } }); }); describe("document verification (MPO)", () => { it("verifies a document with an audit row", async () => { const { crewId, docId } = await crewWithRecords(); as(manningId, "MANNING"); expect("ok" in (await verifyDocument(docId, true))).toBe(true); const d = await db.seafarerDocument.findUniqueOrThrow({ where: { id: docId } }); expect(d.verificationStatus).toBe("VERIFIED"); expect(d.verifiedById).toBe(manningId); expect(await db.crewAction.count({ where: { crewMemberId: crewId, actionType: "RECORD_VERIFIED" } })).toBe(1); }); it("rejection requires a reason and records it", async () => { const { docId } = await crewWithRecords(); as(manningId, "MANNING"); expect("error" in (await verifyDocument(docId, false))).toBe(true); expect("ok" in (await verifyDocument(docId, false, "Illegible scan"))).toBe(true); expect((await db.seafarerDocument.findUniqueOrThrow({ where: { id: docId } })).verificationStatus).toBe("REJECTED"); }); it("won't re-verify an already-decided document", async () => { const { docId } = await crewWithRecords(); as(manningId, "MANNING"); await verifyDocument(docId, true); expect("error" in (await verifyDocument(docId, true))).toBe(true); }); it("is rejected for roles without verify_site_records (accounts, site staff)", async () => { const { docId } = await crewWithRecords(); as(accountsId, "ACCOUNTS"); expect(await verifyDocument(docId, true)).toEqual({ error: "Unauthorized" }); as(siteStaffId, "SITE_STAFF"); expect(await verifyDocument(docId, true)).toEqual({ error: "Unauthorized" }); }); }); describe("bank/EPF verification (Accounts)", () => { it("Accounts verifies bank and EPF", async () => { const { crewId } = await crewWithRecords(); as(accountsId, "ACCOUNTS"); expect("ok" in (await verifyBankEpf(crewId, "bank", true))).toBe(true); expect((await db.bankDetail.findUniqueOrThrow({ where: { crewMemberId: crewId } })).verificationStatus).toBe("VERIFIED"); expect("ok" in (await verifyBankEpf(crewId, "epf", true))).toBe(true); expect((await db.epfDetail.findUniqueOrThrow({ where: { crewMemberId: crewId } })).verificationStatus).toBe("VERIFIED"); }); it("is rejected for the MPO (no verify_bank_epf)", async () => { const { crewId } = await crewWithRecords(); as(manningId, "MANNING"); expect(await verifyBankEpf(crewId, "bank", true)).toEqual({ error: "Unauthorized" }); }); });