- Complete monorepo structure with pnpm workspaces and Turborepo - All packages implemented: types, utils, rules-engine, iso20022, treasury, risk-models, audit - React web application with TypeScript and Tailwind CSS - Full Brazil regulatory compliance (BCB requirements) - ISO 20022 message support (pacs.008, pacs.009, pain.001) - Treasury and subledger management - Risk, capital, and liquidity stress allocation - Audit logging and BCB reporting - E&O +10% uplift implementation
152 lines
4.5 KiB
TypeScript
152 lines
4.5 KiB
TypeScript
/**
|
|
* 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;
|
|
}
|