diff --git a/App/app/(portal)/crewing/applications/actions.ts b/App/app/(portal)/crewing/applications/actions.ts index 4ea7f32..8abf91c 100644 --- a/App/app/(portal)/crewing/applications/actions.ts +++ b/App/app/(portal)/crewing/applications/actions.ts @@ -562,8 +562,33 @@ export async function onboardCandidate(formData: FormData): Promise 0) { + const key = buildStorageKey("contract", app.crewMember.id, file.name); + await uploadBuffer(key, Buffer.from(await file.arrayBuffer()), file.type || "application/octet-stream"); + contract = { fileKey: key, salaryRestricted: formData.get("salaryRestricted") !== "false" }; + } + const result = await db.$transaction(async (tx) => { const employeeId = await generateEmployeeId(tx); const assignment = await tx.crewAssignment.create({ @@ -582,9 +607,23 @@ export async function onboardCandidate(formData: FormData): Promise 0) { - const key = buildStorageKey("contract", result.assignmentId, file.name); - await uploadBuffer(key, Buffer.from(await file.arrayBuffer()), file.type || "application/octet-stream"); - await db.contractLetter.create({ - data: { assignmentId: result.assignmentId, fileKey: key, salaryRestricted: formData.get("salaryRestricted") !== "false" }, - }); - } - revalidateApp(id, app.requisition.id); return { ok: true, id: result.employeeId }; } diff --git a/App/tests/integration/onboarding.test.ts b/App/tests/integration/onboarding.test.ts index 4e833ff..6e20488 100644 --- a/App/tests/integration/onboarding.test.ts +++ b/App/tests/integration/onboarding.test.ts @@ -88,6 +88,29 @@ describe("onboardCandidate", () => { const action = await db.crewAction.findFirstOrThrow({ where: { actionType: "CREW_ONBOARDED" } }); expect(action.actorId).toBe(managerId); + // D3 AC2: the audit row records the created IDs in metadata. + const meta = action.metadata as { assignmentId?: string; employeeId?: string; salaryStructureId?: string } | null; + expect(meta?.assignmentId).toBe(assignment.id); + expect(meta?.employeeId).toBe(cm.employeeId); + expect(meta?.salaryStructureId).toBe(sal.id); + }); + + it("blocks onboarding when no salary structure is Manager-approved (D1)", async () => { + seq += 1; + const req = await db.requisition.create({ data: { code: `REQ-O${seq}`, rankId, vesselId, reason: "NEW_VACANCY", status: "SELECTED" } }); + const cand = await db.crewMember.create({ data: { name: "Unapproved Sal", type: "NEW", status: "CANDIDATE", source: "CAREERS", appliedRankId: rankId } }); + const appRow = await db.application.create({ data: { requisitionId: req.id, crewMemberId: cand.id, stage: "SELECTED", type: "NEW" } }); + // Salary agreed but NOT Manager-approved (approvedById null). + await db.salaryStructure.create({ data: { applicationId: appRow.id, rateBasis: "MONTHLY", basic: 40000 } }); + + as(managerId, "MANAGER"); + const res = await onboardCandidate(fd({ applicationId: appRow.id, joiningDate: "2026-07-01" })); + expect("error" in res).toBe(true); + expect(await db.crewAssignment.count()).toBe(0); + // The candidate is untouched — still a CANDIDATE, no employee number. + const after = await db.crewMember.findUniqueOrThrow({ where: { id: cand.id } }); + expect(after.status).toBe("CANDIDATE"); + expect(after.employeeId).toBeNull(); }); it("requires a joining date", async () => {