/** * Brazilian ID validation utilities (CPF/CNPJ) */ /** * Validate CPF (Cadastro de Pessoa Física) format and checksum * CPF format: XXX.XXX.XXX-XX (11 digits) */ export function validateCPF(cpf: string): { valid: boolean; formatted?: string; error?: string } { // Remove non-numeric characters const cleaned = cpf.replace(/\D/g, ''); // Check length if (cleaned.length !== 11) { return { valid: false, error: 'CPF must have 11 digits' }; } // Check for invalid patterns (all same digits) if (/^(\d)\1{10}$/.test(cleaned)) { return { valid: false, error: 'CPF cannot have all same digits' }; } // Validate checksum digits let sum = 0; let remainder: number; // Validate first check digit for (let i = 1; i <= 9; i++) { sum += parseInt(cleaned.substring(i - 1, i)) * (11 - i); } remainder = (sum * 10) % 11; if (remainder === 10 || remainder === 11) remainder = 0; if (remainder !== parseInt(cleaned.substring(9, 10))) { return { valid: false, error: 'Invalid CPF checksum (first digit)' }; } // Validate second check digit sum = 0; for (let i = 1; i <= 10; i++) { sum += parseInt(cleaned.substring(i - 1, i)) * (12 - i); } remainder = (sum * 10) % 11; if (remainder === 10 || remainder === 11) remainder = 0; if (remainder !== parseInt(cleaned.substring(10, 11))) { return { valid: false, error: 'Invalid CPF checksum (second digit)' }; } // Format CPF const formatted = `${cleaned.substring(0, 3)}.${cleaned.substring(3, 6)}.${cleaned.substring(6, 9)}-${cleaned.substring(9, 11)}`; return { valid: true, formatted }; } /** * Validate CNPJ (Cadastro Nacional da Pessoa Jurídica) format and checksum * CNPJ format: XX.XXX.XXX/XXXX-XX (14 digits) */ export function validateCNPJ(cnpj: string): { valid: boolean; formatted?: string; error?: string } { // Remove non-numeric characters const cleaned = cnpj.replace(/\D/g, ''); // Check length if (cleaned.length !== 14) { return { valid: false, error: 'CNPJ must have 14 digits' }; } // Check for invalid patterns (all same digits) if (/^(\d)\1{13}$/.test(cleaned)) { return { valid: false, error: 'CNPJ cannot have all same digits' }; } // Validate checksum digits let length = cleaned.length - 2; let numbers = cleaned.substring(0, length); const digits = cleaned.substring(length); let sum = 0; let pos = length - 7; // Validate first check digit for (let i = length; i >= 1; i--) { sum += parseInt(numbers.charAt(length - i)) * pos--; if (pos < 2) pos = 9; } let result = sum % 11 < 2 ? 0 : 11 - (sum % 11); if (result !== parseInt(digits.charAt(0))) { return { valid: false, error: 'Invalid CNPJ checksum (first digit)' }; } // Validate second check digit length = length + 1; numbers = cleaned.substring(0, length); sum = 0; pos = length - 7; for (let i = length; i >= 1; i--) { sum += parseInt(numbers.charAt(length - i)) * pos--; if (pos < 2) pos = 9; } result = sum % 11 < 2 ? 0 : 11 - (sum % 11); if (result !== parseInt(digits.charAt(1))) { return { valid: false, error: 'Invalid CNPJ checksum (second digit)' }; } // Format CNPJ const formatted = `${cleaned.substring(0, 2)}.${cleaned.substring(2, 5)}.${cleaned.substring(5, 8)}/${cleaned.substring(8, 12)}-${cleaned.substring(12, 14)}`; return { valid: true, formatted }; } /** * Validate Brazilian tax ID (CPF or CNPJ) */ export function validateBrazilianTaxId(taxId: string): { valid: boolean; type?: 'CPF' | 'CNPJ'; formatted?: string; error?: string; } { const cleaned = taxId.replace(/\D/g, ''); if (cleaned.length === 11) { const cpfResult = validateCPF(taxId); return { ...cpfResult, type: cpfResult.valid ? 'CPF' : undefined, }; } else if (cleaned.length === 14) { const cnpjResult = validateCNPJ(taxId); return { ...cnpjResult, type: cnpjResult.valid ? 'CNPJ' : undefined, }; } return { valid: false, error: 'Tax ID must be 11 digits (CPF) or 14 digits (CNPJ)', }; } /** * Format tax ID for display (CPF or CNPJ) */ export function formatTaxId(taxId: string): string { const cleaned = taxId.replace(/\D/g, ''); if (cleaned.length === 11) { return `${cleaned.substring(0, 3)}.${cleaned.substring(3, 6)}.${cleaned.substring(6, 9)}-${cleaned.substring(9, 11)}`; } else if (cleaned.length === 14) { return `${cleaned.substring(0, 2)}.${cleaned.substring(2, 5)}.${cleaned.substring(5, 8)}/${cleaned.substring(8, 12)}-${cleaned.substring(12, 14)}`; } return taxId; }