const PAN_RE = /^[A-Z]{5}[0-9]{4}[A-Z]$/; const IFSC_RE = /^[A-Z]{4}0[A-Z0-9]{6}$/; const GSTIN_RE = /^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z][1-9A-Z]Z[0-9A-Z]$/; const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; const PHONE_RE = /^[0-9+\-()\s]{6,16}$/; const norm = (v: string) => v.trim().toUpperCase(); export function panError(value: string): string | undefined { if (!value.trim()) return undefined; return PAN_RE.test(norm(value)) ? undefined : "Enter a valid PAN, e.g. CPOPP7388K"; } export function ifscError(value: string): string | undefined { if (!value.trim()) return undefined; return IFSC_RE.test(norm(value)) ? undefined : "Enter a valid IFSC, e.g. SBIN0017060"; } export function gstinError(value: string): string | undefined { const v = norm(value); if (!v || v === "NA") return undefined; return GSTIN_RE.test(v) ? undefined : "Enter a valid 15-character GSTIN"; } const GSTIN_CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"; /** Standard GSTIN mod-36 check digit. Returns an error message, or undefined when valid. */ export function gstinChecksumError(value: string): string | undefined { const v = norm(value); if (!GSTIN_RE.test(v)) return "Enter a valid 15-character GSTIN"; let sum = 0; for (let i = 0; i < 14; i++) { const product = GSTIN_CHARS.indexOf(v[i]) * (i % 2 === 0 ? 1 : 2); sum += Math.floor(product / 36) + (product % 36); } const expected = GSTIN_CHARS[(36 - (sum % 36)) % 36]; return v[14] === expected ? undefined : `GSTIN check digit looks wrong (expected ${expected})`; } /** * Full GSTIN check: shape, check digit and, when a PAN is given, that the GSTIN * contains that PAN. Empty (or "NA") is valid; the field is optional. */ export function gstinFullError(value: string, pan = ""): string | undefined { const v = norm(value); if (!v || v === "NA") return undefined; const base = gstinError(v) ?? gstinChecksumError(v); if (base) return base; if (pan.trim() && v.slice(2, 12) !== norm(pan)) return "GSTIN does not contain the PAN"; return undefined; } /** Supplier GSTIN rules for a registered business; an unregistered one needs none. */ export function vendorGstinError( registration: string, gstin: string, pan: string, stateCode: string, ): string | undefined { if (registration === "unregistered") return undefined; const g = norm(gstin); if (!g) return "A registered business needs a GSTIN"; const base = gstinFullError(g, pan); if (base) return base; if (g.slice(0, 2) !== stateCode) return `GSTIN starts with ${g.slice(0, 2)}, but your state code is ${stateCode}`; return undefined; } export function emailError(value: string): string | undefined { if (!value.trim()) return undefined; return EMAIL_RE.test(value.trim()) ? undefined : "Enter a valid email address"; } export function phoneError(value: string): string | undefined { if (!value.trim()) return undefined; return PHONE_RE.test(value.trim()) ? undefined : "Enter a valid phone number"; } export function accountError(value: string): string | undefined { if (!value.trim()) return undefined; return /^[0-9]{6,20}$/.test(value.trim()) ? undefined : "Account number should be 6–20 digits"; } export const isPan = (v: string) => PAN_RE.test(norm(v)); export const isIfsc = (v: string) => IFSC_RE.test(norm(v)); export const isGstin = (v: string) => GSTIN_RE.test(norm(v));