diff --git a/src/domain/work-orders/use-cases/use-work-order-table-mutations.ts b/src/domain/work-orders/use-cases/use-work-order-table-mutations.ts index 98fe8fd7..b6a21a2a 100644 --- a/src/domain/work-orders/use-cases/use-work-order-table-mutations.ts +++ b/src/domain/work-orders/use-cases/use-work-order-table-mutations.ts @@ -181,6 +181,9 @@ async function runPatchAndSyncDetail( } catch (error) { if (error instanceof BoardPatchPartialSaveError) { recordSavedRow(sync, id, error.partialRow); + } else if (error instanceof BoardPatchConflictError && error.currentState) { + // A retry before the refetch lands must patch the server's current version. + recordSavedRow(sync, id, error.currentState); } throw error; } diff --git a/src/test/domain/work-orders/use-cases/use-work-order-table-mutations-off-list.test.tsx b/src/test/domain/work-orders/use-cases/use-work-order-table-mutations-off-list.test.tsx index d2a42d08..715592b9 100644 --- a/src/test/domain/work-orders/use-cases/use-work-order-table-mutations-off-list.test.tsx +++ b/src/test/domain/work-orders/use-cases/use-work-order-table-mutations-off-list.test.tsx @@ -2,6 +2,7 @@ import type { ReactNode } from "react"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { act, renderHook, waitFor } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; +import { BoardPatchConflictError } from "@/domain/work-orders/errors/board-patch-errors"; import type { WorkOrderBoardDetail } from "@/domain/work-orders/types/work-order-board-detail"; import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row"; import { queryKeys } from "@/infra/query-key/query-key"; @@ -195,6 +196,49 @@ describe("useWorkOrderTableMutations for a work order outside the loaded list", expect(patchBoardField.mock.calls[1][1]).toMatchObject({ workOrderVersion: "saved-1" }); }); + it("retries an off-list save on the conflict's current version, not the stale panel row", async () => { + const client = makeClient(); + client.setQueryData(queryKeys.workOrders.boardDetail(9), { + info: DETAIL_ROW, + } as WorkOrderBoardDetail); + patchBoardField.mockRejectedValueOnce( + new BoardPatchConflictError("Work order was modified.", { + ...DETAIL_ROW, + rowVersion: "server-current", + }), + ); + const onRetrySuccess = vi.fn(); + const onFirstError = vi.fn(); + const { result } = renderHook( + () => + useWorkOrderTableMutations({ + onPatch: vi.fn(), + clearPatch: vi.fn(), + getRow: () => undefined, + }), + { wrapper: makeWrapper(client) }, + ); + + act(() => { + result.current.patchField( + 9, + { dueDate: "2026-10-09" }, + { fallbackRow: DETAIL_ROW, onError: onFirstError }, + ); + }); + await waitFor(() => expect(onFirstError).toHaveBeenCalled()); + act(() => { + result.current.patchField( + 9, + { dueDate: "2026-10-09" }, + { fallbackRow: DETAIL_ROW, onSuccess: onRetrySuccess }, + ); + }); + + await waitFor(() => expect(onRetrySuccess).toHaveBeenCalled()); + expect(patchBoardField.mock.calls[1][1]).toMatchObject({ workOrderVersion: "server-current" }); + }); + it("prefers the table row when it still holds the work order", async () => { const tableRow = { ...DETAIL_ROW, rowVersion: "table-version" }; const onSuccess = vi.fn();