From b5514211158252424ec97b0120744c3bbd8c498e Mon Sep 17 00:00:00 2001 From: Codex Review Integration Date: Thu, 17 Sep 2026 06:15:23 -0300 Subject: [PATCH] fix(work-orders): show duplicate-WO copy for edit conflicts (SH-375) A duplicate work order number edit returns 422 DuplicateWoNumber from the board PATCH endpoint, which mapped to the generic save-failed copy. Add the code to the known validation copy map so edits reuse the same duplicate-WO product text as create, and share the message constant across both paths. --- .../work-orders/errors/board-create-errors.ts | 8 ++--- .../work-orders/errors/board-patch-errors.ts | 6 ++++ .../work-orders/api/work-orders-api.test.ts | 22 ++++++++++++++ .../errors/board-patch-errors.test.ts | 29 +++++++++++++++++++ 4 files changed, 61 insertions(+), 4 deletions(-) create mode 100644 src/test/domain/work-orders/errors/board-patch-errors.test.ts diff --git a/src/domain/work-orders/errors/board-create-errors.ts b/src/domain/work-orders/errors/board-create-errors.ts index 446eaf5f..60437ef2 100644 --- a/src/domain/work-orders/errors/board-create-errors.ts +++ b/src/domain/work-orders/errors/board-create-errors.ts @@ -1,12 +1,12 @@ import { isHTTPError, isNetworkError, isTimeoutError } from "ky"; import { mapHttpStatusToMessage } from "@/api/api-error"; -import { DISPATCH_REQUIRED_MESSAGE } from "@/domain/work-orders/errors/board-patch-errors"; +import { + DISPATCH_REQUIRED_MESSAGE, + DUPLICATE_WO_NUMBER_MESSAGE, +} from "@/domain/work-orders/errors/board-patch-errors"; export const BOARD_CREATE_FAILED_MESSAGE = "Unable to create the work order. Please try again."; -const DUPLICATE_WO_NUMBER_MESSAGE = - "This work order number is already assigned to another work order."; - /** Statuses whose mapped copy is fixed product text, never the response body. */ const FIXED_COPY_STATUSES = new Set([401, 403, 404, 500]); diff --git a/src/domain/work-orders/errors/board-patch-errors.ts b/src/domain/work-orders/errors/board-patch-errors.ts index 02af77cb..98481f87 100644 --- a/src/domain/work-orders/errors/board-patch-errors.ts +++ b/src/domain/work-orders/errors/board-patch-errors.ts @@ -31,9 +31,15 @@ export const BOARD_PATCH_FAILED_MESSAGE = "Unable to save the work order. Please export const NO_PERSISTABLE_FIELDS_MESSAGE = "These changes cannot be saved on the board. Refresh and edit a supported field."; +export const DUPLICATE_WO_NUMBER_MESSAGE = + "This work order number is already assigned to another work order."; + const KNOWN_VALIDATION_COPY: ReadonlyMap = new Map([ ["DispatchRequired", DISPATCH_REQUIRED_MESSAGE], ["NoPersistableFields", NO_PERSISTABLE_FIELDS_MESSAGE], + // why: editing a WO number to one that already exists returns 422 DuplicateWoNumber; + // show the same product copy as create, not the generic save-failed fallback. + ["DuplicateWoNumber", DUPLICATE_WO_NUMBER_MESSAGE], ]); /** diff --git a/src/test/domain/work-orders/api/work-orders-api.test.ts b/src/test/domain/work-orders/api/work-orders-api.test.ts index 75f1e499..0d0002f3 100644 --- a/src/test/domain/work-orders/api/work-orders-api.test.ts +++ b/src/test/domain/work-orders/api/work-orders-api.test.ts @@ -615,6 +615,28 @@ describe("workOrdersApi.patchBoardField", () => { }), ).rejects.toBeInstanceOf(BoardPatchConflictError); }); + + it("throws BoardPatchValidationError with DuplicateWoNumber code on 422", async () => { + apiPatchFn.mockResolvedValue({ + ok: false, + status: 422, + json: async () => ({ + code: "DuplicateWoNumber", + message: "WO number already exists.", + }), + }); + + await expect( + workOrdersApi.patchBoardField(10, { + field: "woNumber", + value: "12345678901", + workOrderVersion: "AAAAAAA=", + }), + ).rejects.toMatchObject({ + name: "BoardPatchValidationError", + code: "DuplicateWoNumber", + }); + }); }); describe("workOrdersApi.createComment", () => { diff --git a/src/test/domain/work-orders/errors/board-patch-errors.test.ts b/src/test/domain/work-orders/errors/board-patch-errors.test.ts new file mode 100644 index 00000000..d38217cd --- /dev/null +++ b/src/test/domain/work-orders/errors/board-patch-errors.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, it } from "vitest"; +import { + BOARD_PATCH_CONFLICT_MESSAGE, + BOARD_PATCH_FAILED_MESSAGE, + BoardPatchConflictError, + BoardPatchValidationError, + DUPLICATE_WO_NUMBER_MESSAGE, + boardPatchErrorMessage, +} from "@/domain/work-orders/errors/board-patch-errors"; + +describe("boardPatchErrorMessage", () => { + it("maps a DuplicateWoNumber validation error to the duplicate-WO product copy", () => { + const error = new BoardPatchValidationError("DuplicateWoNumber", "WO number already exists."); + + expect(boardPatchErrorMessage(error)).toBe(DUPLICATE_WO_NUMBER_MESSAGE); + }); + + it("never leaks backend validation text for unknown codes", () => { + const error = new BoardPatchValidationError("SomethingElse", "vendorId is required."); + + expect(boardPatchErrorMessage(error)).toBe(BOARD_PATCH_FAILED_MESSAGE); + }); + + it("maps a conflict error to the concurrent-edit copy", () => { + const error = new BoardPatchConflictError("stale", null); + + expect(boardPatchErrorMessage(error)).toBe(BOARD_PATCH_CONFLICT_MESSAGE); + }); +});