From a43de20ed4ee7dabba34d227926c155c8a17f274 Mon Sep 17 00:00:00 2001 From: Arthur Bassi Date: Wed, 19 Aug 2026 12:02:17 -0300 Subject: [PATCH] fix(work-orders): ignore cancelled dispatches in detail closability (SH-183) --- .../work-orders/utils/wo-closability.ts | 19 ++++++-- .../work-orders/utils/wo-closability.test.ts | 47 +++++++++++++++++++ 2 files changed, 62 insertions(+), 4 deletions(-) diff --git a/src/domain/work-orders/utils/wo-closability.ts b/src/domain/work-orders/utils/wo-closability.ts index 823a241c..ab9f9494 100644 --- a/src/domain/work-orders/utils/wo-closability.ts +++ b/src/domain/work-orders/utils/wo-closability.ts @@ -1,3 +1,4 @@ +import type { WorkOrderDispatchSummary } from "@/domain/work-orders/types/dispatch"; import type { WorkOrderDetail } from "@/domain/work-orders/types/work-order-detail"; import type { CompletionDocStatus, @@ -156,16 +157,26 @@ export function wizardDraftToClosabilityInput(draft: WorkOrderWizardDraft): WoCl }; } +/** Current vendor assignment — cancelled dispatches do not count as company (SH-183). */ +function currentAssignedDispatch( + dispatches: WorkOrderDispatchSummary[], +): WorkOrderDispatchSummary | undefined { + return dispatches.find((dispatch) => { + if (dispatch.status === "Cancelled") { + return false; + } + const vendorId = String(dispatch.vendorId ?? "").trim(); + return Boolean((dispatch.vendorName ?? "").trim() || vendorId); + }); +} + /** * Map standalone detail DTO onto closability. Uses authoritative `type` / `docStatus` / * `mediaCount` (enriched from board detail on the view route). Never treats `source` * as WorkOrderType. `problem` is the same service field the board maps into `pm`. */ export function detailToClosabilityInput(wo: WorkOrderDetail): WoClosabilityInput { - const assigned = wo.dispatches.find((dispatch) => { - const vendorId = String(dispatch.vendorId ?? "").trim(); - return Boolean(dispatch.vendorName.trim() || vendorId); - }); + const assigned = currentAssignedDispatch(wo.dispatches); return { dispatcherId: "", dispatcherName: wo.assignedTo, diff --git a/src/test/domain/work-orders/utils/wo-closability.test.ts b/src/test/domain/work-orders/utils/wo-closability.test.ts index 7e60565f..9aa2c15e 100644 --- a/src/test/domain/work-orders/utils/wo-closability.test.ts +++ b/src/test/domain/work-orders/utils/wo-closability.test.ts @@ -96,6 +96,22 @@ describe("getClosabilityGaps", () => { ]); }); + 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( @@ -310,6 +326,37 @@ describe("detailToClosabilityInput", () => { 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", () => {