/** * Integration tests for the Crewing Phase 4a crew-records actions (documents, * bank/EPF, next of kin, PPE, experience). The records tables are new this phase, * so afterEach wipes them. */ 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 { uploadDocument, deleteDocument, saveBankEpf, addNextOfKin, issuePpe, returnPpe, addExperience, } from "@/app/(portal)/crewing/crew/actions"; import { makeSession, getSeedUser, fd } from "./helpers"; import type { Role } from "@prisma/client"; let managerId: string; let accountsId: string; let siteStaffId: string; let crewId: string; const SS_EMAIL = "sitestaff@itcrew.local"; const as = (userId: string, role: Role) => vi.mocked(auth as unknown as () => Promise).mockResolvedValue(makeSession(userId, role)); beforeAll(async () => { managerId = (await getSeedUser("manager@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: "ITCREW-SS", email: SS_EMAIL, name: "SS Crew", role: "SITE_STAFF" } }); siteStaffId = ss.id; }); afterEach(async () => { await db.crewAction.deleteMany({}); await db.seafarerDocument.deleteMany({}); await db.nextOfKin.deleteMany({}); await db.ppeIssue.deleteMany({}); await db.experienceRecord.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 } }); }); async function makeCrew() { const c = await db.crewMember.create({ data: { name: "Active Crew", status: "EMPLOYEE", type: "NEW", source: "CAREERS", employeeId: `CRW-T${Date.now() % 100000}` } }); crewId = c.id; return c.id; } describe("documents", () => { it("uploads and removes a document (with audit)", async () => { const id = await makeCrew(); as(managerId, "MANAGER"); expect("ok" in (await uploadDocument(fd({ crewMemberId: id, docType: "PASSPORT", number: "P123", expiryDate: "2030-01-01" })))).toBe(true); const doc = await db.seafarerDocument.findFirstOrThrow({ where: { crewMemberId: id } }); expect(doc.docType).toBe("PASSPORT"); expect(await db.crewAction.count({ where: { actionType: "DOCUMENT_UPLOADED" } })).toBe(1); expect("ok" in (await deleteDocument(doc.id))).toBe(true); expect(await db.seafarerDocument.count({ where: { crewMemberId: id } })).toBe(0); }); it("is rejected for a role without upload_crew_records (accounts)", async () => { const id = await makeCrew(); as(accountsId, "ACCOUNTS"); expect(await uploadDocument(fd({ crewMemberId: id, docType: "PASSPORT" }))).toEqual({ error: "Unauthorized" }); }); }); describe("bank & EPF", () => { it("upserts bank and EPF details", async () => { const id = await makeCrew(); as(managerId, "MANAGER"); expect("ok" in (await saveBankEpf(fd({ crewMemberId: id, accountNumber: "999888777", ifsc: "HDFC0009", uan: "UAN-1" })))).toBe(true); expect((await db.bankDetail.findUniqueOrThrow({ where: { crewMemberId: id } })).accountNumber).toBe("999888777"); expect((await db.epfDetail.findUniqueOrThrow({ where: { crewMemberId: id } })).uan).toBe("UAN-1"); // Upsert again updates rather than duplicating. await saveBankEpf(fd({ crewMemberId: id, accountNumber: "111", ifsc: "X", uan: "UAN-2" })); expect((await db.bankDetail.findUniqueOrThrow({ where: { crewMemberId: id } })).accountNumber).toBe("111"); expect(await db.bankDetail.count({ where: { crewMemberId: id } })).toBe(1); }); }); describe("next of kin", () => { it("adds an emergency contact", async () => { const id = await makeCrew(); as(siteStaffId, "SITE_STAFF"); // site staff can upload crew records expect("ok" in (await addNextOfKin(fd({ crewMemberId: id, name: "Spouse", relationship: "Wife", isEmergency: "true" })))).toBe(true); const nok = await db.nextOfKin.findFirstOrThrow({ where: { crewMemberId: id } }); expect(nok.isEmergency).toBe(true); }); }); describe("PPE", () => { it("issues PPE then marks it returned", async () => { const id = await makeCrew(); as(siteStaffId, "SITE_STAFF"); expect("ok" in (await issuePpe(fd({ crewMemberId: id, item: "SAFETY_SHOES", size: "9", quantity: "1" })))).toBe(true); const ppe = await db.ppeIssue.findFirstOrThrow({ where: { crewMemberId: id } }); expect(ppe.returnedDate).toBeNull(); expect("ok" in (await returnPpe(ppe.id))).toBe(true); expect((await db.ppeIssue.findUniqueOrThrow({ where: { id: ppe.id } })).returnedDate).not.toBeNull(); }); it("is rejected for a role without issue_ppe (accounts)", async () => { const id = await makeCrew(); as(accountsId, "ACCOUNTS"); expect(await issuePpe(fd({ crewMemberId: id, item: "HELMET" }))).toEqual({ error: "Unauthorized" }); }); }); describe("experience", () => { it("adds a declared experience record", async () => { const id = await makeCrew(); as(managerId, "MANAGER"); expect("ok" in (await addExperience(fd({ crewMemberId: id, vesselType: "Dredger", durationMonths: "36" })))).toBe(true); const e = await db.experienceRecord.findFirstOrThrow({ where: { crewMemberId: id } }); expect(e.source).toBe("declared"); expect(e.durationMonths).toBe(36); }); });