shoc-frontend-new/src/test/domain/work-orders/utils/wo-closability.test.ts

376 lines
12 KiB
TypeScript

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> = {}): 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("");
},
);
});