mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-02 17:53:12 +00:00
376 lines
12 KiB
TypeScript
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("");
|
|
},
|
|
);
|
|
});
|