import type { RequisitionStatus, Role } from "@prisma/client"; // Requisition lifecycle state machine — mirrors the PO state machine // (lib/po-state-machine.ts) and the reconciled spec (Crewing-Implementation-Spec // §5.2): OPEN → SHORTLISTING → PROPOSING → INTERVIEWING → SELECTED → FILLED, // with CANCELLED reachable from OPEN/SHORTLISTING (Manager). // // The intermediate stage advances are driven by the recruitment pipeline that // lands in Phase 3; they are modelled here now so the transitions, allowed // roles and audit are settled and testable. Phase 2 wires raise (create OPEN) // and cancel via server actions; selection is Manager-only (spec §6). export type RequisitionAction = | "start_shortlisting" | "mark_proposing" | "start_interviewing" | "mark_selected" | "mark_filled"; interface Transition { to: RequisitionStatus; allowedRoles: Role[]; requiresNote: boolean; } type TransitionMap = Partial>; // MPO (MANNING) and Manager source recruitment; final selection is Manager-only. const SOURCING_ROLES: Role[] = ["MANNING", "MANAGER", "SUPERUSER"]; const MANAGER_ROLES: Role[] = ["MANAGER", "SUPERUSER"]; const TRANSITIONS: Partial> = { OPEN: { start_shortlisting: { to: "SHORTLISTING", allowedRoles: SOURCING_ROLES, requiresNote: false }, }, SHORTLISTING: { mark_proposing: { to: "PROPOSING", allowedRoles: SOURCING_ROLES, requiresNote: false }, }, PROPOSING: { start_interviewing: { to: "INTERVIEWING", allowedRoles: SOURCING_ROLES, requiresNote: false }, }, INTERVIEWING: { // Final selection of a candidate is a Manager approval (spec §6). mark_selected: { to: "SELECTED", allowedRoles: MANAGER_ROLES, requiresNote: false }, }, SELECTED: { // The onboarding side-effect (Phase 3) fills the vacancy. mark_filled: { to: "FILLED", allowedRoles: SOURCING_ROLES, requiresNote: false }, }, }; export function getTransition(from: RequisitionStatus, action: RequisitionAction): Transition | null { return TRANSITIONS[from]?.[action] ?? null; } export function canPerformAction( from: RequisitionStatus, action: RequisitionAction, role: Role ): boolean { return getTransition(from, action)?.allowedRoles.includes(role) ?? false; } export function getAvailableActions(status: RequisitionStatus, role: Role): RequisitionAction[] { const map = TRANSITIONS[status]; if (!map) return []; return (Object.keys(map) as RequisitionAction[]).filter((action) => canPerformAction(status, action, role) ); } export function requiresNote(from: RequisitionStatus, action: RequisitionAction): boolean { return getTransition(from, action)?.requiresNote ?? false; } // ── Cancellation (orthogonal) ──────────────────────────────────────────────── // A requisition may be withdrawn while it is still early in the pipeline — OPEN // or SHORTLISTING (spec §5.2) — and a reason is required. WHO may cancel is the // `cancel_requisition` grant (spec §6: MPO + Manager + SuperUser); the actions // enforce that permission, and CANCEL_ROLES mirrors it so the state machine and // the matrix agree. Modelled separately from TRANSITIONS, like PO CANCEL_ROLES. export const CANCEL_ROLES: Role[] = ["MANNING", "MANAGER", "SUPERUSER"]; export const CANCELLABLE_FROM: RequisitionStatus[] = ["OPEN", "SHORTLISTING"]; export function canCancel(from: RequisitionStatus, role: Role): boolean { return CANCELLABLE_FROM.includes(from) && CANCEL_ROLES.includes(role); }