import { describe, expect, it } from "vitest"; import { readDetailWoType } from "@/domain/work-orders/utils/read-detail-closability-fields"; import type { WorkOrderDetail } from "@/domain/work-orders/types/work-order-detail"; import { EMPTY_WIZARD_DRAFT } from "@/domain/work-orders/types/work-order-wizard"; import type { WoClosabilityInput } from "@/domain/work-orders/utils/wo-closability"; import { PENDING_UPLIFT_GAP, UPLIFT_PENDING_COMPLETED_TITLE, canGenerateCompDoc, detailToClosabilityInput, formatClosabilityMissingTitle, getClosabilityGaps, hasPendingUplift, isCompletionStatusLabel, isRealWoNumber, wizardDraftToClosabilityInput, } from "@/domain/work-orders/utils/wo-closability"; function makeInput(overrides: Partial = {}): WoClosabilityInput { return { dispatcherId: "u1", dispatcherName: "Alex", company: "Acme HVAC", vendorId: "", 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("isCompletionStatusLabel", () => { it("treats Done and Completed as completion labels", () => { expect(isCompletionStatusLabel("Done")).toBe(true); expect(isCompletionStatusLabel("Completed")).toBe(true); expect(isCompletionStatusLabel("Scheduled")).toBe(false); }); }); 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("allows company without technician (SH-183)", () => { const gaps = getClosabilityGaps(makeInput({ tech: "" })); expect(gaps).toEqual([]); expect(gaps).not.toContain("Technician"); }); it("blocks Completed when company is missing even if technician is assigned", () => { expect(getClosabilityGaps(makeInput({ company: "", tech: "Sam" }))).toEqual(["Company"]); expect(getClosabilityGaps(makeInput({ company: " ", tech: "Sam" }))).toEqual(["Company"]); }); it("allows Completed when vendorId is set and company/technician are empty (SH-183)", () => { expect(getClosabilityGaps(makeInput({ company: "", vendorId: "7", tech: "" }))).toEqual([]); }); it("blocks Completed when both company and vendorId are empty even with a technician", () => { expect(getClosabilityGaps(makeInput({ company: "", vendorId: " ", tech: "Sam" }))).toEqual([ "Company", ]); }); it.each(["PM", "Reactive", "Emergency", "Overdue", ""] as const)( "blocks Completed without a company for type %s (SH-183)", (type) => { const gaps = getClosabilityGaps( makeInput({ company: "", vendorId: "", type, docStatus: "Yes", mediaCount: 1, }), ); expect(gaps).toContain("Company"); }, ); 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", "Company", "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("uses design Completed title when pending uplift is among mixed gaps", () => { expect(formatClosabilityMissingTitle(["Dispatcher", PENDING_UPLIFT_GAP])).toBe( UPLIFT_PENDING_COMPLETED_TITLE, ); }); it("joins multiple non-uplift gaps with Missing prefix", () => { expect(formatClosabilityMissingTitle(["Dispatcher", "Company"])).toBe( "Missing: Dispatcher, Company", ); }); }); 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, }); }); }); describe("wizardDraftToClosabilityInput", () => { it("maps vendorName to company and leaves tech empty", () => { const input = wizardDraftToClosabilityInput({ ...EMPTY_WIZARD_DRAFT, assigneeId: "u1", assigneeName: "Alice", vendorName: "ACME", woNumber: "20260623001", pm: "HVAC", type: "PM", }); expect(input).toEqual({ dispatcherId: "u1", dispatcherName: "Alice", company: "ACME", vendorId: "", tech: "", completedDate: "", woNumber: "20260623001", pm: "HVAC", docStatus: "", mediaCount: 0, type: "PM", }); expect(getClosabilityGaps(input)).toContain("Completed Date"); expect(getClosabilityGaps(input)).toContain("Completion document"); expect(getClosabilityGaps(input)).not.toContain("Company"); expect(getClosabilityGaps(input)).not.toContain("Technician"); }); }); describe("detailToClosabilityInput", () => { it("maps authoritative type/doc/media and problem so Done is not permanently blocked", () => { const detail = { assignedTo: "Alice", completedDate: "2026-07-15", workOrderNumber: "20260623001", internalWONumber: "SH-1", problem: "HVAC PM", source: "procurement-integration", type: "", docStatus: "", mediaCount: 0, dispatches: [{ vendorName: "ACME Tech" }], } as WorkOrderDetail; const input = detailToClosabilityInput(detail); expect(input.dispatcherName).toBe("Alice"); expect(input.company).toBe("ACME Tech"); expect(input.vendorId).toBe(""); expect(input.woNumber).toBe("20260623001"); expect(input.pm).toBe("HVAC PM"); expect(getClosabilityGaps(input)).toEqual([]); }); it("does not treat integration source as WorkOrderType", () => { const detail = { assignedTo: "Alice", completedDate: "2026-07-15", workOrderNumber: "20260623001", internalWONumber: "SH-1", problem: "HVAC", source: "PM", type: "", docStatus: "No", mediaCount: 0, dispatches: [{ vendorName: "ACME Tech" }], } as WorkOrderDetail; expect(detailToClosabilityInput(detail).type).toBe(""); expect(getClosabilityGaps(detailToClosabilityInput(detail))).toEqual([]); }); it("blocks PM detail transition without completion document", () => { const detail = { assignedTo: "Alice", completedDate: "2026-07-15", workOrderNumber: "20260623001", internalWONumber: "SH-1", problem: "HVAC PM", source: "procurement", type: "PM", docStatus: "No", mediaCount: 0, dispatches: [{ vendorName: "ACME Tech" }], } as WorkOrderDetail; expect(getClosabilityGaps(detailToClosabilityInput(detail))).toEqual(["Completion document"]); }); it("blocks Emergency detail transition without photos", () => { const detail = { assignedTo: "Alice", completedDate: "2026-07-15", workOrderNumber: "20260623001", internalWONumber: "SH-1", problem: "Leak", source: "portal", type: "Emergency", docStatus: "No", mediaCount: 0, dispatches: [{ vendorName: "ACME Tech" }], } as WorkOrderDetail; expect(getClosabilityGaps(detailToClosabilityInput(detail))).toEqual(["At least 1 photo"]); }); it("treats vendorId without vendorName as assigned company (SH-183)", () => { const detail = { assignedTo: "Alice", completedDate: "2026-07-15", workOrderNumber: "20260623001", internalWONumber: "SH-1", problem: "HVAC PM", source: "procurement-integration", type: "", docStatus: "", mediaCount: 0, dispatches: [{ vendorName: "", vendorId: 45 }], } as WorkOrderDetail; const input = detailToClosabilityInput(detail); expect(input.company).toBe(""); expect(input.vendorId).toBe("45"); expect(getClosabilityGaps(input)).toEqual([]); }); it("ignores cancelled dispatches when resolving assigned company (SH-183)", () => { const cancelledOnly = { assignedTo: "Alice", completedDate: "2026-07-15", workOrderNumber: "20260623001", internalWONumber: "SH-1", problem: "HVAC PM", source: "procurement-integration", type: "", docStatus: "", mediaCount: 0, dispatches: [{ vendorName: "", vendorId: 45, status: "Cancelled" }], } as WorkOrderDetail; expect(detailToClosabilityInput(cancelledOnly).vendorId).toBe(""); expect(getClosabilityGaps(detailToClosabilityInput(cancelledOnly))).toContain("Company"); const supersededThenCurrent = { ...cancelledOnly, dispatches: [ { vendorName: "Old Co", vendorId: 1, status: "Cancelled" }, { vendorName: "ACME Tech", vendorId: 45, status: "Sent" }, ], } as WorkOrderDetail; const input = detailToClosabilityInput(supersededThenCurrent); expect(input.company).toBe("ACME Tech"); expect(input.vendorId).toBe("45"); expect(getClosabilityGaps(input)).toEqual([]); }); }); describe("readDetailWoType", () => { it.each(["PM", "Reactive", "Emergency", "Overdue"])( "passes through the business type %s", (type) => { expect(readDetailWoType({ workOrderType: type })).toBe(type); }, ); it.each(["Add-On", "AddOn"])( "normalizes the legacy Add-On payload %s to empty (SH-126/SH-184)", (legacy) => { expect(readDetailWoType({ workOrderType: legacy })).toBe(""); }, ); });