import { describe, expect, it } from "vitest"; import type { WoClosabilityInput } from "@/domain/work-orders/utils/wo-closability"; import { PENDING_UPLIFT_GAP, UPLIFT_PENDING_COMPLETED_TITLE, canGenerateCompDoc, formatClosabilityMissingTitle, getClosabilityGaps, hasPendingUplift, isRealWoNumber, } from "@/domain/work-orders/utils/wo-closability"; function makeInput(overrides: Partial = {}): WoClosabilityInput { return { dispatcherId: "u1", dispatcherName: "Alex", company: "Acme HVAC", tech: "Sam", completedDate: "2026-07-01", woNumber: "1001", pm: "HVAC", docStatus: "Yes", mediaCount: 1, type: "PM", hasPendingUplift: false, ...overrides, }; } describe("isRealWoNumber", () => { it("rejects empty and SH-prefixed provisional numbers", () => { expect(isRealWoNumber("")).toBe(false); expect(isRealWoNumber("SH-9")).toBe(false); expect(isRealWoNumber("sh123")).toBe(false); expect(isRealWoNumber("98765")).toBe(true); }); }); describe("hasPendingUplift", () => { it("is true when hasPendingUplift flag is set", () => { expect(hasPendingUplift(makeInput({ hasPendingUplift: true }))).toBe(true); }); it("is true when upliftSummary.pendingCount is positive", () => { expect( hasPendingUplift(makeInput({ upliftSummary: { hasUplift: true, pendingCount: 2 } })), ).toBe(true); }); it("is false when uplift is explicitly not pending", () => { expect(hasPendingUplift(makeInput())).toBe(false); expect( hasPendingUplift(makeInput({ upliftSummary: { hasUplift: false, pendingCount: 0 } })), ).toBe(false); }); }); describe("getClosabilityGaps", () => { it("returns empty when all gates are satisfied", () => { expect(getClosabilityGaps(makeInput())).toEqual([]); }); it("blocks Completed when technician is missing", () => { expect(getClosabilityGaps(makeInput({ tech: "" }))).toEqual(["Technician"]); }); it("does not block Completed when vendor/company is empty if technician is assigned", () => { expect(getClosabilityGaps(makeInput({ company: "" }))).toEqual([]); }); it("lists field gaps before uplift and document gaps", () => { expect( getClosabilityGaps( makeInput({ dispatcherId: "", dispatcherName: "", company: " ", tech: " ", completedDate: "", woNumber: "SH-1", pm: "", hasPendingUplift: true, docStatus: "No", type: "PM", }), ), ).toEqual([ "Dispatcher", "Technician", "Completed Date", "WO number", "Service", PENDING_UPLIFT_GAP, "Completion document", ]); }); it("blocks Completed when uplift is pending via summary", () => { expect( getClosabilityGaps(makeInput({ upliftSummary: { hasUplift: true, pendingCount: 1 } })), ).toEqual([PENDING_UPLIFT_GAP]); }); it("blocks Completed when hasPendingUplift is true", () => { expect(getClosabilityGaps(makeInput({ hasPendingUplift: true }))).toEqual([PENDING_UPLIFT_GAP]); }); it("allows Completed when uplift status fields are absent (no pending evidence)", () => { expect( getClosabilityGaps(makeInput({ hasPendingUplift: undefined, upliftSummary: undefined })), ).toEqual([]); }); it("requires a photo for Emergency / Reactive", () => { expect( getClosabilityGaps(makeInput({ type: "Emergency", mediaCount: 0, docStatus: "No" })), ).toEqual(["At least 1 photo"]); }); }); describe("formatClosabilityMissingTitle", () => { it("uses design Completed title when only uplift is missing", () => { expect(formatClosabilityMissingTitle([PENDING_UPLIFT_GAP])).toBe( UPLIFT_PENDING_COMPLETED_TITLE, ); }); it("joins multiple gaps with Missing prefix", () => { expect(formatClosabilityMissingTitle(["Dispatcher", PENDING_UPLIFT_GAP])).toBe( `Missing: Dispatcher, ${PENDING_UPLIFT_GAP}`, ); }); }); describe("canGenerateCompDoc", () => { it("allows generation when no closability gaps", () => { expect(canGenerateCompDoc(makeInput())).toEqual({ allowed: true }); }); it("allows generation when only the completion document is missing", () => { expect(canGenerateCompDoc(makeInput({ docStatus: "No" }))).toEqual({ allowed: true }); }); it("blocks generation with design reason when only pending uplift exists", () => { expect(canGenerateCompDoc(makeInput({ hasPendingUplift: true }))).toEqual({ allowed: false, reason: UPLIFT_PENDING_COMPLETED_TITLE, }); }); });