pelagia-portal/App/tests/unit/permissions-crewing.test.ts
Hardik bb5f4126b0 feat(crewing): admin crew management — direct placement, CRUD, strength config
Office/admin crewing-management surface behind a new manage_crew permission
(Manager + SuperUser + Admin). Stacks on 4b. Behind NEXT_PUBLIC_CREWING_ENABLED.

What's in
- Permission: manage_crew added to the §6 matrix (MGR/SU/ADMIN).
- Direct placement (placeCrew): a Manager assigns a crew member to a vessel/site
  WITHOUT a requisition — creates an ACTIVE CrewAssignment, promotes a candidate to
  EMPLOYEE with a CRW- number (generateEmployeeId), blocked if already actively
  assigned.
- Admin crew CRUD: createCrewMember / updateCrewMember / deleteCrewMember (delete
  blocked when assignments/applications exist).
- Crew strength config: upsert/delete VesselRankRequirement (the minStrength that
  drives R6 leave-clash detection).
- Screens under Administration (flag-gated, MGR/SU/ADMIN): /admin/crew (list + add/
  edit/delete + Place modal) and /admin/crew-strength (requirement table + form).

Tests & docs
- Unit: permissions-crewing.test.ts gains a manage_crew check. Integration:
  crewing-admin.test.ts (9) — CRUD, delete guard, direct placement (+promotion,
  +active-assignment guard), strength upsert/delete, manage_crew gating.
  type-check clean; full unit (241) + integration (192) green.
- CLAUDE.md updated with the crewing-admin surface.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 21:23:31 +05:30

113 lines
4.5 KiB
TypeScript

import { describe, it, expect, afterEach, vi } from "vitest";
import { hasPermission } from "@/lib/permissions";
// Verifies the crewing rows of the §6 grant matrix in the wiki
// Crewing-Implementation-Spec are wired up exactly as written.
describe("Crewing permissions (spec §6)", () => {
it("SITE_STAFF holds its site-level grants", () => {
for (const p of [
"request_relief_cover",
"sign_off_crew",
"view_crew_records",
"upload_crew_records",
"issue_ppe",
"apply_leave",
"record_attendance",
"view_attendance",
"raise_appraisal",
] as const) {
expect(hasPermission("SITE_STAFF", p)).toBe(true);
}
});
it("SITE_STAFF cannot raise requisitions or decide leave or do any purchasing", () => {
expect(hasPermission("SITE_STAFF", "raise_requisition")).toBe(false);
expect(hasPermission("SITE_STAFF", "decide_leave")).toBe(false);
expect(hasPermission("SITE_STAFF", "create_po")).toBe(false);
expect(hasPermission("SITE_STAFF", "manage_ranks")).toBe(false);
});
it("MPO (MANNING) has NO attendance or leave access (R5/R1)", () => {
expect(hasPermission("MANNING", "record_attendance")).toBe(false);
expect(hasPermission("MANNING", "view_attendance")).toBe(false);
expect(hasPermission("MANNING", "apply_leave")).toBe(false);
expect(hasPermission("MANNING", "decide_leave")).toBe(false);
});
it("MPO sources recruitment but never gives final approvals", () => {
expect(hasPermission("MANNING", "raise_requisition")).toBe(true);
expect(hasPermission("MANNING", "manage_candidates")).toBe(true);
expect(hasPermission("MANNING", "record_interview_result")).toBe(true);
expect(hasPermission("MANNING", "verify_site_records")).toBe(true);
// Approvals are Manager-only:
expect(hasPermission("MANNING", "approve_salary_structure")).toBe(false);
expect(hasPermission("MANNING", "select_candidate")).toBe(false);
expect(hasPermission("MANNING", "approve_interview_waiver")).toBe(false);
});
it("Manager owns every crewing approval gate (R1/R2/R8)", () => {
for (const p of [
"decide_leave",
"approve_interview_waiver",
"approve_salary_structure",
"select_candidate",
"approve_appraisal",
"approve_wage_report",
"generate_wage_report",
] as const) {
expect(hasPermission("MANAGER", p)).toBe(true);
}
});
it("Accounts verifies bank/EPF and sees wages only (R11)", () => {
expect(hasPermission("ACCOUNTS", "verify_bank_epf")).toBe(true);
expect(hasPermission("ACCOUNTS", "view_wage_report")).toBe(true);
expect(hasPermission("ACCOUNTS", "view_crew_records")).toBe(true);
expect(hasPermission("ACCOUNTS", "verify_site_records")).toBe(false);
expect(hasPermission("ACCOUNTS", "record_attendance")).toBe(false);
});
it("manage_crew is Manager + SuperUser + Admin (office crew management)", () => {
expect(hasPermission("MANAGER", "manage_crew")).toBe(true);
expect(hasPermission("SUPERUSER", "manage_crew")).toBe(true);
expect(hasPermission("ADMIN", "manage_crew")).toBe(true);
expect(hasPermission("SITE_STAFF", "manage_crew")).toBe(false);
expect(hasPermission("MANNING", "manage_crew")).toBe(false);
expect(hasPermission("ACCOUNTS", "manage_crew")).toBe(false);
});
it("manage_ranks is Manager + Admin only (not SuperUser)", () => {
expect(hasPermission("MANAGER", "manage_ranks")).toBe(true);
expect(hasPermission("ADMIN", "manage_ranks")).toBe(true);
expect(hasPermission("SUPERUSER", "manage_ranks")).toBe(false);
expect(hasPermission("MANNING", "manage_ranks")).toBe(false);
});
it("Auditor keeps read-only crewing visibility", () => {
expect(hasPermission("AUDITOR", "view_requisitions")).toBe(true);
expect(hasPermission("AUDITOR", "view_crew_records")).toBe(true);
expect(hasPermission("AUDITOR", "view_wage_report")).toBe(true);
expect(hasPermission("AUDITOR", "raise_requisition")).toBe(false);
});
});
describe("CREWING_ENABLED flag", () => {
afterEach(() => {
vi.unstubAllEnvs();
vi.resetModules();
});
it("defaults off when the env var is unset", async () => {
vi.resetModules();
vi.stubEnv("NEXT_PUBLIC_CREWING_ENABLED", "");
const flags = await import("@/lib/feature-flags");
expect(flags.CREWING_ENABLED).toBe(false);
});
it("is on only for the exact string \"true\"", async () => {
vi.resetModules();
vi.stubEnv("NEXT_PUBLIC_CREWING_ENABLED", "true");
const flags = await import("@/lib/feature-flags");
expect(flags.CREWING_ENABLED).toBe(true);
});
});