diff --git a/src/lib/validators/clients.ts b/src/lib/validators/clients.ts index cbefa7bd..1b4ab11b 100644 --- a/src/lib/validators/clients.ts +++ b/src/lib/validators/clients.ts @@ -1,6 +1,7 @@ import { z } from 'zod'; import { baseListQuerySchema } from '@/lib/api/list-query'; +import { humanTextSchema } from '@/lib/validators/text'; import { optionalCountryIsoSchema, optionalIanaTimezoneSchema, @@ -9,22 +10,35 @@ import { // ─── Contact sub-schema ────────────────────────────────────────────────────── -export const contactSchema = z.object({ - channel: z.enum(['email', 'phone', 'whatsapp', 'other']), - value: z.string().min(1), - /** E.164-normalized number; required when channel is phone/whatsapp. */ - valueE164: optionalPhoneE164Schema.optional(), - /** ISO-3166-1 alpha-2 country the number was parsed against. */ - valueCountry: optionalCountryIsoSchema.optional(), - label: z.string().optional(), - isPrimary: z.boolean().optional().default(false), - notes: z.string().optional(), -}); +export const contactSchema = z + .object({ + channel: z.enum(['email', 'phone', 'whatsapp', 'other']), + value: z.string().min(1), + /** E.164-normalized number; required when channel is phone/whatsapp. */ + valueE164: optionalPhoneE164Schema.optional(), + /** ISO-3166-1 alpha-2 country the number was parsed against. */ + valueCountry: optionalCountryIsoSchema.optional(), + label: z.string().optional(), + isPrimary: z.boolean().optional().default(false), + notes: z.string().optional(), + }) + .superRefine((data, ctx) => { + // Post-audit F6: email channel must carry a syntactically valid + // address. Otherwise sales send-outs bounce silently and the bounce + // monitor can't classify. + if (data.channel === 'email' && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.value)) { + ctx.addIssue({ + code: 'custom', + path: ['value'], + message: 'Must be a valid email address.', + }); + } + }); // ─── Create ────────────────────────────────────────────────────────────────── export const createClientSchema = z.object({ - fullName: z.string().min(1).max(200), + fullName: humanTextSchema({ min: 1, max: 200 }), contacts: z.array(contactSchema).min(1, 'At least one contact is required'), /** ISO-3166-1 alpha-2 nationality code. */ nationalityIso: optionalCountryIsoSchema.optional(), diff --git a/src/lib/validators/text.ts b/src/lib/validators/text.ts new file mode 100644 index 00000000..d025a59c Binary files /dev/null and b/src/lib/validators/text.ts differ