""" Funções de normalização e validação de documento (CPF/CNPJ) e telefone. Formato de CNPJ alfanumérico vigente desde 31/07/2026 (Instrução Normativa RFB nº 2.229/2024, Nota Técnica COCAD/SUARA/RFB nº 49/2024): 14 posições, sendo as 12 primeiras alfanuméricas (0-9, A-Z — raiz + filial) e as 2 últimas numéricas (dígitos verificadores, módulo 11 sobre valor ASCII − 48 de cada caractere). CNPJs numéricos antigos continuam válidos e geram o mesmo DV pela fórmula nova (retrocompatível). `normalizar_documento` valida só o FORMATO (tamanho/charset) — é o mesmo contrato já usado no workflow n8n (node "05 - Normalizar CNPJ") e não deve divergir dele. `validar_dv_cpf`/`validar_dv_cnpj` são checagens ADICIONAIS de dígito verificador, usadas pela UI como aviso (não bloqueio) — o legado aceita documentos com DV matematicamente inválido. """ import re _RE_NAO_ALFANUM = re.compile(r"[^0-9A-Z]") _RE_CPF = re.compile(r"^[0-9]{11}$") _RE_CNPJ = re.compile(r"^[0-9A-Z]{12}[0-9]{2}$") _RE_NAO_DIGITO = re.compile(r"[^0-9]") def normalizar_documento(bruto: str | None) -> tuple[str, bool, str | None]: """ Normaliza CPF/CNPJ: maiúsculas, só dígitos e letras A-Z (remove pontos, barras, hífens, espaços). CPF: 11 dígitos numéricos. CNPJ: 12 caracteres alfanuméricos (0-9, A-Z) + 2 dígitos verificadores numéricos. Retorna (documento_normalizado, formato_valido, tipo) onde tipo é "CPF", "CNPJ" ou None se inválido. """ limpo = _RE_NAO_ALFANUM.sub("", (bruto or "").upper()) eh_cpf = bool(_RE_CPF.match(limpo)) eh_cnpj = bool(_RE_CNPJ.match(limpo)) tipo = "CPF" if eh_cpf else ("CNPJ" if eh_cnpj else None) return limpo, (eh_cpf or eh_cnpj), tipo def normalizar_telefone(bruto: str | None) -> str: """Remove tudo que não for dígito (espaços, parênteses, traços, "+").""" return _RE_NAO_DIGITO.sub("", bruto or "") _PESOS_CPF_DV1 = [10, 9, 8, 7, 6, 5, 4, 3, 2] _PESOS_CPF_DV2 = [11, 10, 9, 8, 7, 6, 5, 4, 3, 2] _PESOS_CNPJ_DV1 = [5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2] _PESOS_CNPJ_DV2 = [6, 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2] def _digito_verificador(valores: list[int], pesos: list[int]) -> int: soma = sum(v * p for v, p in zip(valores, pesos)) resto = soma % 11 return 0 if resto < 2 else 11 - resto def validar_dv_cpf(cpf_normalizado: str) -> bool: """Confere os 2 dígitos verificadores de um CPF já normalizado (11 dígitos).""" digitos = [int(c) for c in cpf_normalizado] dv1 = _digito_verificador(digitos[:9], _PESOS_CPF_DV1) dv2 = _digito_verificador(digitos[:9] + [dv1], _PESOS_CPF_DV2) return digitos[9] == dv1 and digitos[10] == dv2 def _valor_caractere(c: str) -> int: """Valor ASCII − 48: dígitos 0-9 valem 0-9, letras A-Z valem 17-42.""" return ord(c) - 48 def validar_dv_cnpj(cnpj_normalizado: str) -> bool: """ Confere os 2 dígitos verificadores de um CNPJ já normalizado (12 alfanuméricos + 2 numéricos). Funciona tanto para CNPJ numérico antigo quanto para o novo formato alfanumérico (mesma fórmula, retrocompatível). """ base = [_valor_caractere(c) for c in cnpj_normalizado[:12]] dv_informado = [int(c) for c in cnpj_normalizado[12:14]] dv1 = _digito_verificador(base, _PESOS_CNPJ_DV1) dv2 = _digito_verificador(base + [dv1], _PESOS_CNPJ_DV2) return dv_informado[0] == dv1 and dv_informado[1] == dv2