From 6a575536c76081703667136f9a4415c8d2797722 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 5 Oct 2026 14:44:48 +0000 Subject: [PATCH] fix(work-orders): show evidence scan failures on uplift create Rejected and timed-out scans throw UpliftEvidenceScanError so the create toast uses the client scan message instead of the generic fallback. --- .../use-cases/submit-work-order-uplift.ts | 11 ++++- .../use-cases/use-work-order-uplifts.ts | 8 +++- ...eate-work-order-uplift-scan-error.test.tsx | 44 +++++++++++++++++++ 3 files changed, 60 insertions(+), 3 deletions(-) create mode 100644 src/test/domain/work-orders/use-cases/use-create-work-order-uplift-scan-error.test.tsx diff --git a/src/domain/work-orders/use-cases/submit-work-order-uplift.ts b/src/domain/work-orders/use-cases/submit-work-order-uplift.ts index 02f44973..111e258f 100644 --- a/src/domain/work-orders/use-cases/submit-work-order-uplift.ts +++ b/src/domain/work-orders/use-cases/submit-work-order-uplift.ts @@ -37,6 +37,13 @@ function scanFailureMessage(scanStatus: "Rejected" | "Timeout"): string { return "The evidence file is still being scanned. Try again in a moment."; } +export class UpliftEvidenceScanError extends Error { + constructor(readonly outcome: "Rejected" | "Timeout") { + super(scanFailureMessage(outcome)); + this.name = "UpliftEvidenceScanError"; + } +} + async function waitForPassedScan( workOrderId: string | number, file: File, @@ -51,11 +58,11 @@ async function waitForPassedScan( return uploaded.id; } if (document.scanStatus === "Rejected") { - throw new Error(scanFailureMessage("Rejected")); + throw new UpliftEvidenceScanError("Rejected"); } await pause(POLL_INTERVAL_MS); } - throw new Error(scanFailureMessage("Timeout")); + throw new UpliftEvidenceScanError("Timeout"); } export async function submitWorkOrderUplift( diff --git a/src/domain/work-orders/use-cases/use-work-order-uplifts.ts b/src/domain/work-orders/use-cases/use-work-order-uplifts.ts index bb018d1d..d2384caf 100644 --- a/src/domain/work-orders/use-cases/use-work-order-uplifts.ts +++ b/src/domain/work-orders/use-cases/use-work-order-uplifts.ts @@ -9,7 +9,10 @@ import { isHTTPError, isNetworkError, isTimeoutError } from "ky"; import { toast } from "react-toastify"; import { mapHttpStatusToMessage, REQUEST_UPLIFTS_ROLE_DENIAL_MESSAGE } from "@/api/api-error"; import { workOrderUpliftsApi } from "@/domain/work-orders/api/work-order-uplifts-api"; -import { submitWorkOrderUplift } from "@/domain/work-orders/use-cases/submit-work-order-uplift"; +import { + submitWorkOrderUplift, + UpliftEvidenceScanError, +} from "@/domain/work-orders/use-cases/submit-work-order-uplift"; import type { CreateWorkOrderUpliftInput, WorkOrderUplift, @@ -33,6 +36,9 @@ function createUpliftErrorMessage(error: unknown): string { if (isNetworkError(error) || isTimeoutError(error)) { return error.message; } + if (error instanceof UpliftEvidenceScanError) { + return error.message; + } return CREATE_UPLIFT_ERROR_FALLBACK; } diff --git a/src/test/domain/work-orders/use-cases/use-create-work-order-uplift-scan-error.test.tsx b/src/test/domain/work-orders/use-cases/use-create-work-order-uplift-scan-error.test.tsx new file mode 100644 index 00000000..dcb36cad --- /dev/null +++ b/src/test/domain/work-orders/use-cases/use-create-work-order-uplift-scan-error.test.tsx @@ -0,0 +1,44 @@ +import { QueryClientProvider } from "@tanstack/react-query"; +import { act, renderHook, waitFor } from "@testing-library/react"; +import type { ReactNode } from "react"; +import { toast } from "react-toastify"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { workOrderUpliftsApi } from "@/domain/work-orders/api/work-order-uplifts-api"; +import { useCreateWorkOrderUplift } from "@/domain/work-orders/use-cases/use-work-order-uplifts"; +import { createTestQueryClient } from "@/test/test-utils"; + +vi.mock("react-toastify", () => ({ + toast: { error: vi.fn(), success: vi.fn() }, +})); + +function wrapper({ children }: { children: ReactNode }) { + return {children}; +} + +describe("useCreateWorkOrderUplift scan errors", () => { + beforeEach(() => { + vi.restoreAllMocks(); + vi.mocked(toast.error).mockClear(); + }); + + it("shows the security-scan rejection message when evidence is rejected", async () => { + vi.spyOn(workOrderUpliftsApi, "uploadEvidence").mockResolvedValue({ + id: 22, + scanStatus: "Rejected", + originalFileName: "quote.pdf", + }); + const create = vi.spyOn(workOrderUpliftsApi, "create"); + const quote = new File(["%PDF"], "quote.pdf", { type: "application/pdf" }); + const { result } = renderHook(() => useCreateWorkOrderUplift(7), { wrapper }); + + act(() => { + result.current.mutate({ amount: 25, notes: "Leak", files: [quote] }); + }); + + await waitFor(() => expect(toast.error).toHaveBeenCalledTimes(1)); + expect(vi.mocked(toast.error).mock.calls[0]?.[0]).toBe( + "The evidence file was rejected by the security scan.", + ); + expect(create).not.toHaveBeenCalled(); + }); +});