Final slice of Phase 5. The appraisal lifecycle raise → verify → approve across three role-gated surfaces, per Crewing-Implementation-Spec §5.4/§8.14. Stacks on 5a verification. Behind NEXT_PUBLIC_CREWING_ENABLED. Completes Phase 5. What's in - Schema: Appraisal (on CrewAssignment) + AppraisalStatus (DRAFT/SUBMITTED/MPO_VERIFIED/MANAGER_APPROVED/REJECTED); CrewActionType += APPRAISAL_SUBMITTED/VERIFIED/APPROVED/REJECTED. Migration crewing_appraisal. - State machine lib/appraisal-state-machine.ts: verify (SUBMITTED→MPO_VERIFIED, MPO/Manager), approve (MPO_VERIFIED→MANAGER_APPROVED, Manager); orthogonal reject. - Actions (crewing/appraisals/actions.ts): raiseAppraisal (raise_appraisal — PM/ site staff), verifyAppraisal (verify_appraisal — MPO), approveAppraisal (approve_appraisal — Manager); reject paths require remarks; notifications APPRAISAL_FOR_VERIFICATION / APPRAISAL_FOR_APPROVAL. - Three surfaces (§8.14): PM raises + tracks status on the crew-profile Appraisals tab; MPO verifies in the Verification queue (Appraisals section); Manager approves in the central /approvals queue (Appraisal kind). Tests & docs - Unit: appraisal-state-machine.test.ts (4). Integration: appraisal.test.ts (4) — raise→verify→approve happy path, MPO reject, permission gating (MPO can't raise, site staff can't verify, MPO can't approve). type-check clean; full unit (245) + integration (205) green (verified with RESEND_API_KEY unset). - CLAUDE.md updated — completes Phase 5 (I + H). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
30 lines
1.3 KiB
TypeScript
30 lines
1.3 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
import { getTransition, canPerformAction, canReject } from "@/lib/appraisal-state-machine";
|
|
|
|
// Appraisal lifecycle (Crewing-Implementation-Spec §5.4).
|
|
describe("Appraisal state machine", () => {
|
|
it("MPO verifies a SUBMITTED appraisal", () => {
|
|
expect(getTransition("SUBMITTED", "verify")?.to).toBe("MPO_VERIFIED");
|
|
expect(canPerformAction("SUBMITTED", "verify", "MANNING")).toBe(true);
|
|
expect(canPerformAction("SUBMITTED", "verify", "MANAGER")).toBe(true);
|
|
expect(canPerformAction("SUBMITTED", "verify", "SITE_STAFF")).toBe(false);
|
|
});
|
|
|
|
it("Manager approves an MPO_VERIFIED appraisal (not the MPO)", () => {
|
|
expect(getTransition("MPO_VERIFIED", "approve")?.to).toBe("MANAGER_APPROVED");
|
|
expect(canPerformAction("MPO_VERIFIED", "approve", "MANAGER")).toBe(true);
|
|
expect(canPerformAction("MPO_VERIFIED", "approve", "MANNING")).toBe(false);
|
|
});
|
|
|
|
it("rejects out-of-order actions", () => {
|
|
expect(getTransition("SUBMITTED", "approve")).toBeNull();
|
|
expect(getTransition("MANAGER_APPROVED", "verify")).toBeNull();
|
|
});
|
|
|
|
it("is rejectable only while in review", () => {
|
|
expect(canReject("SUBMITTED")).toBe(true);
|
|
expect(canReject("MPO_VERIFIED")).toBe(true);
|
|
expect(canReject("MANAGER_APPROVED")).toBe(false);
|
|
expect(canReject("REJECTED")).toBe(false);
|
|
});
|
|
});
|