import { z } from 'zod'; import { baseListQuerySchema } from '@/lib/api/list-query'; import { optionalCountryIsoSchema, optionalIanaTimezoneSchema, optionalPhoneE164Schema, optionalSubdivisionIsoSchema, } from '@/lib/validators/i18n'; // ─── Residential client ────────────────────────────────────────────────────── export const createResidentialClientSchema = z.object({ fullName: z.string().min(1).max(200), email: z .string() .email() .optional() .or(z.literal('').transform(() => undefined)), phone: z.string().optional(), /** E.164-normalized phone alongside the legacy free-text `phone`. */ phoneE164: optionalPhoneE164Schema.optional(), /** ISO-3166-1 alpha-2 the phone was parsed against. */ phoneCountry: optionalCountryIsoSchema.optional(), /** ISO-3166-1 alpha-2 nationality. */ nationalityIso: optionalCountryIsoSchema.optional(), /** IANA timezone. */ timezone: optionalIanaTimezoneSchema.optional(), placeOfResidence: z.string().optional(), /** ISO-3166-1 alpha-2 country of residence. */ placeOfResidenceCountryIso: optionalCountryIsoSchema.optional(), /** ISO 3166-2 subdivision code for place of residence. */ subdivisionIso: optionalSubdivisionIsoSchema.optional(), preferredContactMethod: z.enum(['email', 'phone']).optional(), status: z.enum(['prospect', 'active', 'inactive']).optional().default('prospect'), source: z.enum(['website', 'manual', 'referral', 'broker', 'other']).optional(), notes: z.string().optional(), }); export const updateResidentialClientSchema = createResidentialClientSchema.partial(); export const listResidentialClientsSchema = baseListQuerySchema.extend({ status: z.enum(['prospect', 'active', 'inactive']).optional(), source: z.enum(['website', 'manual', 'referral', 'broker', 'other']).optional(), }); // ─── Residential interest ──────────────────────────────────────────────────── /** * Default pipeline stages - used as the fallback when a port hasn't * configured its own list via the residential admin page. Mirror the * legacy hard-coded set so existing data continues to validate. * * Per-port stages are stored in `system_settings.residential_pipeline_stages` * (JSON array of stage ids). The validators below accept any string and * defer the membership check to a runtime helper that reads the live * stage list. This lets admins add/rename stages without a deploy. */ export const DEFAULT_RESIDENTIAL_PIPELINE_STAGES = [ 'new', 'contacted', 'viewing_scheduled', 'offer_made', 'offer_accepted', 'closed_won', 'closed_lost', ] as const; /** Backwards-compat alias kept for any existing imports. */ export const PIPELINE_STAGES = DEFAULT_RESIDENTIAL_PIPELINE_STAGES; export const createResidentialInterestSchema = z.object({ residentialClientId: z.string().min(1), pipelineStage: z.string().optional().default('new'), source: z.enum(['website', 'manual', 'referral', 'broker', 'other']).optional(), notes: z.string().optional(), preferences: z.string().optional(), assignedTo: z.string().optional(), }); export const updateResidentialInterestSchema = createResidentialInterestSchema .omit({ residentialClientId: true }) .partial(); export const listResidentialInterestsSchema = baseListQuerySchema.extend({ // pipelineStage accepts a string OR string[] so the FilterBar can filter // multi-select. The legacy single-string form stays accepted so existing // callers (e.g. residential-client-tabs) don't need to migrate. pipelineStage: z.union([z.string(), z.array(z.string())]).optional(), // Source filter - mirrors the main interest list. Comma-separated when // submitted as a query string ("website,referral"). source: z.union([z.string(), z.array(z.string())]).optional(), assignedTo: z.string().optional(), residentialClientId: z.string().optional(), }); // ─── Public website inquiry ────────────────────────────────────────────────── /** * Shape posted by the public website's residential interest form. Coerces * to internal create-shapes inside the public route. * * The legacy `phone` field stays free-text - older website builds may post * raw international strings ('+44 7700 900123'). The route handler parses * it server-side into `phoneE164` + `phoneCountry`. Newer website builds * can post normalized values directly. */ export const publicResidentialInquirySchema = z.object({ firstName: z.string().min(1), lastName: z.string().min(1), email: z.string().email(), phone: z.string().min(1), /** Pre-normalized E.164 form, optional for backwards compat. */ phoneE164: optionalPhoneE164Schema.optional(), /** ISO-3166-1 alpha-2 the phone was parsed against. */ phoneCountry: optionalCountryIsoSchema.optional(), /** ISO-3166-1 alpha-2 nationality. */ nationalityIso: optionalCountryIsoSchema.optional(), /** IANA timezone. */ timezone: optionalIanaTimezoneSchema.optional(), placeOfResidence: z.string().optional(), /** ISO-3166-1 alpha-2 country of residence. */ placeOfResidenceCountryIso: optionalCountryIsoSchema.optional(), /** ISO 3166-2 subdivision code for place of residence. */ subdivisionIso: optionalSubdivisionIsoSchema.optional(), preferredContactMethod: z.enum(['email', 'phone']).optional(), notes: z.string().optional(), preferences: z.string().optional(), }); // ─── Types ──────────────────────────────────────────────────────────────────── export type CreateResidentialClientInput = z.infer; export type UpdateResidentialClientInput = z.infer; export type ListResidentialClientsInput = z.infer; export type CreateResidentialInterestInput = z.infer; export type UpdateResidentialInterestInput = z.infer; export type ListResidentialInterestsInput = z.infer; export type PublicResidentialInquiryInput = z.infer;