fix(work-orders): validate inline technician phone before roster save

The inline technician rows on the wizard's Vendor & time step had no
client-side phone check, so a number that does not normalize to ten digits
failed the roster PATCH and blocked the whole Create with the raw backend
message in a toast. Validate each staged phone with the same
isValidNorthAmericanPhone predicate the vendor roster schema uses, so the
inline path matches the modal path. A blank phone stays valid.
This commit is contained in:
Codex Review Integration 2026-09-16 19:45:07 -03:00
parent 712d8e0dc6
commit 0b229fd054
2 changed files with 50 additions and 0 deletions

View file

@ -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.");

View file

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