diff --git a/src/app/(protected)/workorders/_components/wizard/use-wizard-create-with-technicians.ts b/src/app/(protected)/workorders/_components/wizard/use-wizard-create-with-technicians.ts index fe0f1950..a18f1550 100644 --- a/src/app/(protected)/workorders/_components/wizard/use-wizard-create-with-technicians.ts +++ b/src/app/(protected)/workorders/_components/wizard/use-wizard-create-with-technicians.ts @@ -1,5 +1,6 @@ import { toast } from "react-toastify"; import { useAddTechniciansToCompany } from "@/domain/vendors/use-cases/use-add-technicians-to-company"; +import { isValidNorthAmericanPhone } from "@/domain/vendors/schemas/vendor-roster-schema"; import type { VendorDropdownItem } from "@/domain/work-orders/types/work-order"; import type { WizardNewTechnician, @@ -56,6 +57,13 @@ export function useWizardCreateWithTechnicians({ createMutation.mutate(draft); return; } + // Mirror the vendor roster schema so an unparseable phone is caught here with a + // clear message instead of failing the roster PATCH and blocking Create with the + // raw backend sentence. A blank phone stays valid, matching the modal path. + if (pending.some((technician) => !isValidNorthAmericanPhone(technician.phone))) { + toast.error("Enter a 10-digit phone number for each new technician."); + return; + } const anchor = vendors.find((vendor) => vendor.companyName === company); if (!anchor) { toast.error("Select an existing vendor company before adding technicians."); diff --git a/src/test/app/(protected)/workorders/wizard-vendor-inline-technician.test.tsx b/src/test/app/(protected)/workorders/wizard-vendor-inline-technician.test.tsx index 96d7b612..1774db4f 100644 --- a/src/test/app/(protected)/workorders/wizard-vendor-inline-technician.test.tsx +++ b/src/test/app/(protected)/workorders/wizard-vendor-inline-technician.test.tsx @@ -196,6 +196,48 @@ describe("useWizardCreateWithTechnicians (SH-364)", () => { expect(createMutation.mutate).toHaveBeenCalledWith(expect.objectContaining(expectedPatch)); }); + it("blocks Create and skips the roster save when a staged phone is not a valid number", async () => { + const createMutation = { mutate: vi.fn(), isPending: false }; + const { result } = renderHook(() => + useWizardCreateWithTechnicians({ vendors, patchDraft: vi.fn(), createMutation }), + ); + + await act(async () => { + result.current.mutate( + draftWith({ + vendorCompany: "Vinewood LLC", + newTechnicians: [{ name: "Jordan Lee", phone: "(314) 555-01" }], + }), + ); + }); + + expect(mutateAsync).not.toHaveBeenCalled(); + expect(createMutation.mutate).not.toHaveBeenCalled(); + }); + + it("allows a staged technician with a blank phone, matching the modal path", async () => { + mutateAsync.mockResolvedValue([{ ...vendors[0], id: 99, contactName: "Jordan Lee" }]); + const createMutation = { mutate: vi.fn(), isPending: false }; + const { result } = renderHook(() => + useWizardCreateWithTechnicians({ vendors, patchDraft: vi.fn(), createMutation }), + ); + + await act(async () => { + result.current.mutate( + draftWith({ + vendorCompany: "Vinewood LLC", + newTechnicians: [{ name: "Jordan Lee", phone: "" }], + }), + ); + }); + + expect(mutateAsync).toHaveBeenCalledWith({ + anchorVendorId: 41, + technicians: [{ contactName: "Jordan Lee", phone: "", tradeSpecialties: "HVAC" }], + }); + expect(createMutation.mutate).toHaveBeenCalled(); + }); + it("does not create the work order when the technician save fails", async () => { mutateAsync.mockRejectedValue(new Error("boom")); const createMutation = { mutate: vi.fn(), isPending: false };