Phase 1 of the Crewing module per wiki Crewing-Implementation-Spec §12, all dark behind NEXT_PUBLIC_CREWING_ENABLED (off by default — production unchanged). - schema: add SITE_STAFF to Role; add Rank (self-referential org hierarchy, like Account) + RankDocRequirement, RankCategory & SeafarerDocType enums. - permissions: full §6 crewing grant matrix (PO_ROLE_PERMISSIONS + CREWING_ROLE_PERMISSIONS merged); SITE_STAFF row; MPO has no attendance/leave, approvals are Manager-only, manage_ranks is Manager+Admin. - feature flag: CREWING_ENABLED (opt-in "true"). - nav: flag-gated Crewing section scaffold + "Ranks & documents" under Admin. - reference data: rank-data.ts + rank-doc-data.ts seeded via shared seed-ranks.ts in both dev and prod seeds (19 ranks, 118 doc requirements). - screen: /admin/ranks — rank hierarchy card + per-rank required-documents card. - role-label/prefix maps updated for the new role. Tests: unit (permission matrix + flag), integration (ranks admin CRUD, parent linking, cycle/children guards, doc-requirement upsert/remove, permission gating). Docs: CLAUDE.md "Crewing (feature-flagged)" section + env var. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
104 lines
4 KiB
TypeScript
104 lines
4 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_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);
|
|
});
|
|
});
|