diff --git a/src/app/(protected)/workorders/_components/wizard/use-new-wo-wizard-state.ts b/src/app/(protected)/workorders/_components/wizard/use-new-wo-wizard-state.ts index 871cb979..9b6c7120 100644 --- a/src/app/(protected)/workorders/_components/wizard/use-new-wo-wizard-state.ts +++ b/src/app/(protected)/workorders/_components/wizard/use-new-wo-wizard-state.ts @@ -48,8 +48,14 @@ export function useNewWoWizardState({ const [isCheckingDuplicate, setIsCheckingDuplicate] = useState(false); const openRef = useRef(open); const draftRef = useRef(draft); - openRef.current = open; - draftRef.current = draft; + + useEffect(() => { + openRef.current = open; + }, [open]); + + useEffect(() => { + draftRef.current = draft; + }, [draft]); const { data: apiLocations = [] } = useWorkOrderLocations(); const { data: apiProblems = [] } = useDropdownOptionsByCategory("Problem"); diff --git a/src/domain/work-orders/api/work-orders-api.ts b/src/domain/work-orders/api/work-orders-api.ts index 4f5fa808..c4c754e6 100644 --- a/src/domain/work-orders/api/work-orders-api.ts +++ b/src/domain/work-orders/api/work-orders-api.ts @@ -193,20 +193,26 @@ export const workOrdersApi = { findByWoNumber: async (woNumber: string): Promise => { const normalized = woNumber.trim(); - // Skip empty / very short queries — fuzzy board/search would page endlessly for 1–2 chars. - const minLength = 3; - const maxPages = 5; + // board/search contract: only send search when length >= 2. + const minLength = 2; + // Safety cap — if hit before exhausting results, fail closed (do not treat as unique). + const safetyMaxPages = 50; if (normalized.length < minLength) { return undefined; } - // `board/search` is fuzzy and paginated (0-based); scan a bounded number of pages for an exact WO#. + // `board/search` is fuzzy and paginated (0-based); exhaust pages for an exact WO#. const pageSize = 100; let page = 0; let scanned = 0; let totalCount = Number.POSITIVE_INFINITY; - while (scanned < totalCount && page < maxPages) { + while (scanned < totalCount) { + if (page >= safetyMaxPages) { + throw new Error( + "Unable to verify the work order number: search result set exceeded the safety page limit.", + ); + } const result = await workOrdersApi.searchBoard({ search: normalized, page, 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 4d82981a..cc84a5ed 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 @@ -364,13 +364,13 @@ describe("workOrdersApi.findByWoNumber", () => { expect(result).toMatchObject({ id: 999, woNumber: "10000009999" }); }); - it("skips remote search for short WO numbers", async () => { - const result = await workOrdersApi.findByWoNumber("12"); + it("skips remote search for single-character WO numbers", async () => { + const result = await workOrdersApi.findByWoNumber("1"); expect(apiGet).not.toHaveBeenCalled(); expect(result).toBeUndefined(); }); - it("caps paging when scanning fuzzy board search results", async () => { + it("throws when the safety page limit is hit before exhausting results", async () => { apiGet.mockResolvedValue({ items: Array.from({ length: 100 }, (_, index) => ({ id: index + 1, @@ -382,9 +382,36 @@ describe("workOrdersApi.findByWoNumber", () => { pageSize: 100, }); + await expect(workOrdersApi.findByWoNumber("10000005555")).rejects.toThrow(/safety page limit/i); + expect(apiGet).toHaveBeenCalledTimes(50); + }); + + it("returns undefined after exhausting all fuzzy search pages", async () => { + apiGet + .mockResolvedValueOnce({ + items: Array.from({ length: 100 }, (_, index) => ({ + id: index + 1, + woNumber: `fuzzy-match-${index}`, + lifecycleStatusLabel: "Incomplete", + })), + totalCount: 150, + page: 0, + pageSize: 100, + }) + .mockResolvedValueOnce({ + items: Array.from({ length: 50 }, (_, index) => ({ + id: index + 101, + woNumber: `fuzzy-match-${index + 100}`, + lifecycleStatusLabel: "Incomplete", + })), + totalCount: 150, + page: 1, + pageSize: 100, + }); + const result = await workOrdersApi.findByWoNumber("10000005555"); - expect(apiGet).toHaveBeenCalledTimes(5); + expect(apiGet).toHaveBeenCalledTimes(2); expect(result).toBeUndefined(); }); });