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.
This commit is contained in:
Cursor Agent 2026-10-05 14:44:48 +00:00
parent 10137bd427
commit 6a575536c7
No known key found for this signature in database
3 changed files with 60 additions and 3 deletions

View file

@ -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(

View file

@ -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;
}

View file

@ -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 <QueryClientProvider client={createTestQueryClient()}>{children}</QueryClientProvider>;
}
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();
});
});