import { z } from 'zod'; import { stripHtml } from '../services/sanitize.js'; const panPattern = /^[A-Z]{5}[0-9]{4}[A-Z]$/; const ifscPattern = /^[A-Z]{4}0[A-Z0-9]{6}$/; const gstinPattern = /^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z][1-9A-Z]Z[0-9A-Z]$/; const accountPattern = /^[0-9]{9,18}$/; const emptyToUndefined = (value) => (value === '' || value === null ? undefined : value); /** * @param {z.ZodTypeAny} schema */ function text(schema) { return z.preprocess((value) => (typeof value === 'string' ? stripHtml(value) : value), schema); } export const applicationSchema = z.object({ agreement_date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/), full_name: text(z.string().trim().min(1)), age: z.number().int().min(21).max(100), address: text(z.string().trim().min(1)), registration_no: text(z.string().trim().min(1)), medical_council: text(z.string().trim().min(1)), pan: text(z.string().regex(panPattern)), gstin: z.preprocess(emptyToUndefined, text(z.string().regex(gstinPattern)).optional()), bank_name: text(z.string().trim().min(1)), account_holder: text(z.string().trim().min(1)), account_number: text(z.string().regex(accountPattern)), ifsc: text(z.string().regex(ifscPattern)), }); const emptyToNull = (value) => (value === '' ? null : value); const optionalText = (schema) => z.preprocess(emptyToNull, schema.nullable().optional()); export const draftApplicationSchema = z .object({ agreement_date: optionalText(z.string().regex(/^\d{4}-\d{2}-\d{2}$/)), full_name: optionalText(text(z.string().trim().min(1))), age: z.preprocess((value) => { if (value === '' || value === null) { return null; } if (typeof value === 'string' && /^\d+$/.test(value)) { return Number(value); } return value; }, z.number().int().min(21).max(100).nullable().optional()), address: optionalText(text(z.string().trim().min(1))), registration_no: optionalText(text(z.string().trim().min(1))), medical_council: optionalText(text(z.string().trim().min(1))), pan: optionalText(text(z.string().regex(panPattern))), gstin: optionalText(text(z.string().regex(gstinPattern))), bank_name: optionalText(text(z.string().trim().min(1))), account_holder: optionalText(text(z.string().trim().min(1))), account_number: optionalText(text(z.string().regex(accountPattern))), ifsc: optionalText(text(z.string().regex(ifscPattern))), accepted: z.boolean().optional(), /** Optional cancelled-cheque image as a data URL, or null to remove. */ cancelled_cheque: z.union([z.string().min(1), z.null()]).optional(), }) .strict(); export const finalizeApplicationSchema = applicationSchema.extend({ accepted: z.literal(true), });