feat: bloquear CNPJ com DV inválido, gravar telefone/UF/CEP normalizados

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Gustavo
2026-08-14 12:18:24 -03:00
co-authored by Claude Sonnet 5
parent 8b5e7e251e
commit 59f15d9ac0
3 changed files with 170 additions and 22 deletions
+48 -18
View File
@@ -20,7 +20,14 @@ from ..db import chaves as db_chaves
from ..db import clientes as db_clientes
from ..db import contatos as db_contatos
from ..db import log as db_log
from ..normalize import normalizar_documento, normalizar_telefone, validar_dv_cpf, validar_dv_cnpj
from ..normalize import (
normalizar_documento,
normalizar_telefone_com_ddi,
normalizar_uf,
normalizar_cep,
validar_dv_cpf,
validar_dv_cnpj,
)
router = APIRouter(prefix="/clientes")
templates = Jinja2Templates(directory="app/templates")
@@ -46,15 +53,17 @@ def _ler_form_cliente(form: dict) -> dict:
def _normalizar_documento_e_telefones(dados: dict) -> None:
"""
Grava CNPJ/CPF e telefones sempre sem máscara (só dígitos/letras),
igual ao que CLIENTE_CONTATO.TELEFONE já faz — evita que CLIENTE fique
com dois formatos diferentes convivendo. Só afeta escritas novas feitas
por este painel; registros legados só passam a ficar sem máscara quando
forem reeditados e salvos aqui.
Grava CNPJ/CPF sempre sem máscara, TELEFONE/CELULAR sempre com DDI 55
embutido, UF em maiúsculas (2 letras) e CEP só com dígitos — evita que
CLIENTE fique com formatos inconsistentes convivendo. Só afeta
escritas novas feitas por este painel; registros legados só passam a
ficar normalizados quando forem reeditados e salvos aqui.
"""
dados["CNPJ"] = normalizar_documento(dados["CNPJ"])[0]
dados["TELEFONE"] = normalizar_telefone(dados["TELEFONE"])
dados["CELULAR"] = normalizar_telefone(dados["CELULAR"])
dados["TELEFONE"] = normalizar_telefone_com_ddi(dados["TELEFONE"])
dados["CELULAR"] = normalizar_telefone_com_ddi(dados["CELULAR"])
dados["UF"] = normalizar_uf(dados["UF"])
dados["CEP"] = normalizar_cep(dados["CEP"])
def _aplicar_defaults_opcionais(dados: dict) -> None:
@@ -77,6 +86,29 @@ def _campos_obrigatorios_faltando(dados: dict) -> list[str]:
return [campo for campo in CAMPOS_OBRIGATORIOS if not dados.get(campo)]
def _validar_documento(dados: dict, confirmar_dv: bool) -> dict:
"""
Confere o dígito verificador do documento em CLIENTE.CNPJ (o mesmo
campo aceita CPF ou CNPJ, ver app/normalize.py). CNPJ com DV inválido
bloqueia sempre — não existe "cadastrar mesmo assim" para CNPJ. CPF
com DV inválido mantém o fluxo já existente: aviso + confirmação.
Retorna um dict para mesclar no contexto do template: {} se o
documento está ok (ou é indeterminado), ou uma das chaves de erro que
cliente_form.html já sabe renderizar.
"""
documento_normalizado, formato_valido, tipo = normalizar_documento(dados["CNPJ"])
if not formato_valido:
return {}
if tipo == "CNPJ":
if not validar_dv_cnpj(documento_normalizado):
return {"erro_cnpj_invalido": True}
return {}
if not validar_dv_cpf(documento_normalizado) and not confirmar_dv:
return {"aviso_dv_invalido": True}
return {}
@router.get("", response_class=HTMLResponse)
def listar(
request: Request,
@@ -151,16 +183,9 @@ async def criar(request: Request, usuario: str = Depends(exigir_login)):
request, "cliente_form.html", {**contexto_base, "erro_codigo_duplicado": True}
)
documento_normalizado, formato_valido, tipo = normalizar_documento(dados["CNPJ"])
dv_invalido = False
if formato_valido:
dv_invalido = not (
validar_dv_cpf(documento_normalizado) if tipo == "CPF" else validar_dv_cnpj(documento_normalizado)
)
if dv_invalido and not confirmar_dv:
return templates.TemplateResponse(
request, "cliente_form.html", {**contexto_base, "aviso_dv_invalido": True}
)
extra_erro = _validar_documento(dados, confirmar_dv)
if extra_erro:
return templates.TemplateResponse(request, "cliente_form.html", {**contexto_base, **extra_erro})
_aplicar_defaults_opcionais(dados)
try:
@@ -237,6 +262,7 @@ async def editar(request: Request, cliente_id: int, usuario: str = Depends(exigi
form = dict((await request.form()))
dados = _ler_form_cliente(form)
_normalizar_documento_e_telefones(dados)
confirmar_dv = form.get("confirmar_dv_invalido") == "1"
contexto_base = {
"cliente": {"ID": cliente_id}, "dados": dados, "campos": db_clientes.CAMPOS_CLIENTE,
@@ -255,6 +281,10 @@ async def editar(request: Request, cliente_id: int, usuario: str = Depends(exigi
request, "cliente_form.html", {**contexto_base, "erro_codigo_duplicado": True}
)
extra_erro = _validar_documento(dados, confirmar_dv)
if extra_erro:
return templates.TemplateResponse(request, "cliente_form.html", {**contexto_base, **extra_erro})
_aplicar_defaults_opcionais(dados)
try:
db_clientes.atualizar_cliente(cliente_id, dados)