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

324 lines
10 KiB
TypeScript
Raw Normal View History

import { describe, expect, it } from "vitest";
import type { WorkOrderDetail } from "@/domain/work-orders/types/work-order-detail";
import { readDetailWoType } from "@/domain/work-orders/mappers/read-detail-closability-fields";
import type { WoClosabilityInput } from "@/domain/work-orders/utils/wo-closability";
import {
detailToClosabilityInput,
formatClosabilityMissingTitle,
getClosabilityGaps,
isRealWoNumber,
wizardDraftToClosabilityInput,
} from "@/domain/work-orders/utils/wo-closability";
import { EMPTY_WIZARD_DRAFT } from "@/domain/work-orders/types/work-order-wizard";
function makeInput(overrides: Partial<WoClosabilityInput> = {}): WoClosabilityInput {
return {
dispatcherId: "u1",
dispatcherName: "Alice",
company: "ACME",
tech: "Bob",
completedDate: "2026-07-15",
woNumber: "20260623001",
pm: "HVAC",
docStatus: "Yes",
mediaCount: 2,
type: "PM",
...overrides,
};
}
describe("isRealWoNumber", () => {
it("rejects empty and SH-prefixed placeholders", () => {
expect(isRealWoNumber("")).toBe(false);
expect(isRealWoNumber(" ")).toBe(false);
expect(isRealWoNumber("SH-123")).toBe(false);
expect(isRealWoNumber("sh999")).toBe(false);
});
it("accepts non-SH numbers", () => {
expect(isRealWoNumber("20260623001")).toBe(true);
expect(isRealWoNumber(" 42 ")).toBe(true);
});
});
describe("getClosabilityGaps", () => {
it("returns empty when all requirements are met", () => {
expect(getClosabilityGaps(makeInput())).toEqual([]);
});
it("lists universal gaps", () => {
expect(
getClosabilityGaps(
makeInput({
dispatcherId: "",
dispatcherName: "",
company: "",
tech: "",
completedDate: "",
woNumber: "SH-1",
pm: "",
}),
),
).toEqual(["Dispatcher", "Company", "Completed Date", "WO number", "Service"]);
});
it("treats dispatcherName alone as sufficient", () => {
expect(getClosabilityGaps(makeInput({ dispatcherId: "", dispatcherName: "Alice" }))).toEqual(
[],
);
});
it("allows company without technician (SH-183)", () => {
const gaps = getClosabilityGaps(makeInput({ company: "ACME", tech: "" }));
expect(gaps).toEqual([]);
expect(gaps).not.toContain("Technician");
expect(gaps).not.toContain("Company");
});
it("blocks missing company even when technician is set", () => {
expect(getClosabilityGaps(makeInput({ company: "", tech: "Bob" }))).toEqual(["Company"]);
expect(getClosabilityGaps(makeInput({ company: " ", tech: "Bob" }))).toEqual(["Company"]);
});
it("requires completion document for PM and Overdue", () => {
expect(getClosabilityGaps(makeInput({ type: "PM", docStatus: "No" }))).toEqual([
"Completion document",
]);
expect(getClosabilityGaps(makeInput({ type: "Overdue", docStatus: "" }))).toEqual([
"Completion document",
]);
expect(getClosabilityGaps(makeInput({ type: "PM", docStatus: "NN" }))).toEqual([]);
expect(getClosabilityGaps(makeInput({ type: "Overdue", docStatus: "Yes" }))).toEqual([]);
});
it("requires at least one photo for Emergency and Reactive", () => {
expect(
getClosabilityGaps(makeInput({ type: "Emergency", mediaCount: 0, docStatus: "No" })),
).toEqual(["At least 1 photo"]);
expect(getClosabilityGaps(makeInput({ type: "Reactive", mediaCount: 0 }))).toEqual([
"At least 1 photo",
]);
expect(
getClosabilityGaps(makeInput({ type: "Emergency", mediaCount: 1, docStatus: "No" })),
).toEqual([]);
});
it("blocks missing service on every type", () => {
expect(getClosabilityGaps(makeInput({ type: "Reactive", pm: "", mediaCount: 1 }))).toEqual([
"Service",
]);
expect(getClosabilityGaps(makeInput({ type: "PM", pm: " ", docStatus: "Yes" }))).toEqual([
"Service",
]);
});
});
describe("formatClosabilityMissingTitle", () => {
it("joins gaps for tooltip copy", () => {
expect(formatClosabilityMissingTitle(["Dispatcher", "Service"])).toBe(
"Missing: Dispatcher, Service",
);
});
});
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",
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");
});
it("blocks Completed for a sparse create draft", () => {
const gaps = getClosabilityGaps(wizardDraftToClosabilityInput(EMPTY_WIZARD_DRAFT));
expect(gaps.length).toBeGreaterThan(0);
expect(gaps).toEqual(
expect.arrayContaining(["Dispatcher", "Company", "Completed Date", "WO number", "Service"]),
);
expect(gaps).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",
// SH-126/SH-184: Add-On is a backend-authoritative indicator (`isAddOn`), not a
// WOType, so it is no longer a valid value here. Normalization of a legacy
// "Add-On"/"AddOn" payload is covered by readDetailWoType.
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.tech).toBe("");
expect(input.completedDate).toBe("2026-07-15");
expect(input.woNumber).toBe("20260623001");
expect(input.pm).toBe("HVAC PM");
expect(input.type).toBe("");
expect(input.docStatus).toBe("");
expect(input.mediaCount).toBe(0);
expect(getClosabilityGaps(input)).not.toContain("Service");
expect(getClosabilityGaps(input)).not.toContain("Company");
expect(getClosabilityGaps(input)).not.toContain("Technician");
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;
const input = detailToClosabilityInput(detail);
expect(input.type).toBe("");
expect(getClosabilityGaps(input)).toEqual([]);
expect(getClosabilityGaps(input)).not.toContain("Completion document");
});
it("fails closed on missing service when problem is empty", () => {
const detail = {
assignedTo: "Alice",
completedDate: "2026-07-15",
workOrderNumber: "20260623001",
internalWONumber: "SH-1",
problem: "",
source: "Reactive",
type: "Reactive",
docStatus: "",
mediaCount: 1,
dispatches: [{ vendorName: "ACME Tech" }],
} as WorkOrderDetail;
const input = detailToClosabilityInput(detail);
expect(input.pm).toBe("");
expect(input.type).toBe("Reactive");
expect(getClosabilityGaps(input)).toContain("Service");
});
it("allows PM detail transition when completion document is present", () => {
const detail = {
assignedTo: "Alice",
completedDate: "2026-07-15",
workOrderNumber: "20260623001",
internalWONumber: "SH-1",
problem: "HVAC PM",
source: "procurement",
type: "PM",
docStatus: "Yes",
mediaCount: 0,
dispatches: [{ vendorName: "ACME Tech" }],
} as WorkOrderDetail;
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("allows Emergency detail transition when at least one photo exists", () => {
const detail = {
assignedTo: "Alice",
completedDate: "2026-07-15",
workOrderNumber: "20260623001",
internalWONumber: "SH-1",
problem: "Leak",
source: "portal",
type: "Emergency",
docStatus: "No",
mediaCount: 1,
dispatches: [{ vendorName: "ACME Tech" }],
} as WorkOrderDetail;
expect(getClosabilityGaps(detailToClosabilityInput(detail))).toEqual([]);
});
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"]);
});
});
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) => {
// Add-On became a backend-authoritative indicator (`isAddOn`) and is no longer a
// selectable business type, so a stale payload must not resolve to a WOType.
expect(readDetailWoType({ workOrderType: legacy })).toBe("");
},
);
});