feat: normalizacao de documento + validacao de DV (CPF/CNPJ alfanumerico)
This commit is contained in:
@@ -0,0 +1,84 @@
|
|||||||
|
"""
|
||||||
|
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
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
"""
|
||||||
|
Fixtures de CPF/CNPJ verificadas manualmente:
|
||||||
|
- CPF "11144477735": DV clássico módulo 11, exemplo público amplamente usado
|
||||||
|
em validadores de CPF.
|
||||||
|
- CNPJ numérico "11222333000181": exemplo público amplamente usado em
|
||||||
|
validadores de CNPJ.
|
||||||
|
- CNPJ alfanumérico "00000000E08G12": primeiro CNPJ alfanumérico realmente
|
||||||
|
emitido no Brasil (Banco do Bradil, 02/08/2026) — confirma que o algoritmo
|
||||||
|
bate com um caso real, não só um exemplo didático.
|
||||||
|
"""
|
||||||
|
from app.normalize import (
|
||||||
|
normalizar_documento,
|
||||||
|
normalizar_telefone,
|
||||||
|
validar_dv_cpf,
|
||||||
|
validar_dv_cnpj,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_normaliza_cpf_com_mascara():
|
||||||
|
doc, valido, tipo = normalizar_documento("111.444.777-35")
|
||||||
|
assert doc == "11144477735"
|
||||||
|
assert valido is True
|
||||||
|
assert tipo == "CPF"
|
||||||
|
|
||||||
|
|
||||||
|
def test_normaliza_cnpj_numerico_com_mascara():
|
||||||
|
doc, valido, tipo = normalizar_documento("11.222.333/0001-81")
|
||||||
|
assert doc == "11222333000181"
|
||||||
|
assert valido is True
|
||||||
|
assert tipo == "CNPJ"
|
||||||
|
|
||||||
|
|
||||||
|
def test_normaliza_cnpj_alfanumerico_com_mascara():
|
||||||
|
doc, valido, tipo = normalizar_documento("00.000.000/E08G-12")
|
||||||
|
assert doc == "00000000E08G12"
|
||||||
|
assert valido is True
|
||||||
|
assert tipo == "CNPJ"
|
||||||
|
|
||||||
|
|
||||||
|
def test_documento_invalido_tamanho_errado():
|
||||||
|
doc, valido, tipo = normalizar_documento("123")
|
||||||
|
assert valido is False
|
||||||
|
assert tipo is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_documento_none():
|
||||||
|
doc, valido, tipo = normalizar_documento(None)
|
||||||
|
assert doc == ""
|
||||||
|
assert valido is False
|
||||||
|
assert tipo is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_normaliza_telefone():
|
||||||
|
assert normalizar_telefone("(75) 99909-1780") == "75999091780"
|
||||||
|
assert normalizar_telefone(None) == ""
|
||||||
|
|
||||||
|
|
||||||
|
def test_validar_dv_cpf_correto():
|
||||||
|
assert validar_dv_cpf("11144477735") is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_validar_dv_cpf_incorreto():
|
||||||
|
assert validar_dv_cpf("11144477734") is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_validar_dv_cnpj_numerico_correto():
|
||||||
|
assert validar_dv_cnpj("11222333000181") is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_validar_dv_cnpj_numerico_incorreto():
|
||||||
|
assert validar_dv_cnpj("11222333000180") is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_validar_dv_cnpj_alfanumerico_correto():
|
||||||
|
# Primeiro CNPJ alfanumérico real emitido no Brasil.
|
||||||
|
assert validar_dv_cnpj("00000000E08G12") is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_validar_dv_cnpj_alfanumerico_incorreto():
|
||||||
|
assert validar_dv_cnpj("00000000E08G13") is False
|
||||||
Reference in New Issue
Block a user