feat: bloqueia CNPJ/CPF duplicado ao cadastrar ou editar cliente

Mesmo padrão já usado pro CODIGO (db_clientes.codigo_em_uso): nova
db_clientes.cnpj_em_uso(documento, ignorar_id=None) checada em
criar()/editar() antes de gravar. A comparação ignora pontuação dos dois
lados (não só normaliza o valor recebido) porque CLIENTE.CNPJ tem
formatação inconsistente em registros legados (alguns com máscara,
outros sem) — sem isso, um cadastro novo com o mesmo documento de um
legado ainda mascarado passaria batido.

app/templates/cliente_form.html: nova mensagem de erro
'erro_cnpj_duplicado', mesmo estilo visual do erro de código duplicado.

6 testes novos (unitário do db + integração do router pros dois
fluxos), suíte completa (196 testes) passando.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Gustavo
2026-08-19 09:13:43 -03:00
co-authored by Claude Sonnet 5
parent eed473c5f3
commit f940ff5768
5 changed files with 109 additions and 0 deletions
+42
View File
@@ -253,6 +253,7 @@ def test_criar_cliente_codigo_duplicado_bloqueia_sem_bypass(monkeypatch):
def test_criar_cliente_campo_obrigatorio_faltando_bloqueia(monkeypatch):
monkeypatch.setattr(router_clientes.db_clientes, "codigo_em_uso", lambda codigo, ignorar_id=None: False)
monkeypatch.setattr(router_clientes.db_clientes, "cnpj_em_uso", lambda documento, ignorar_id=None: False)
criado = {}
monkeypatch.setattr(
router_clientes.db_clientes, "criar_cliente", lambda dados: criado.setdefault("ok", True) and 42
@@ -276,6 +277,7 @@ def test_criar_cliente_campos_administrativos_em_branco_usa_defaults(monkeypatch
import datetime
monkeypatch.setattr(router_clientes.db_clientes, "codigo_em_uso", lambda codigo, ignorar_id=None: False)
monkeypatch.setattr(router_clientes.db_clientes, "cnpj_em_uso", lambda documento, ignorar_id=None: False)
capturado = {}
monkeypatch.setattr(
router_clientes.db_clientes, "criar_cliente", lambda dados: capturado.setdefault("dados", dados) and 42
@@ -299,6 +301,7 @@ def test_criar_cliente_campos_administrativos_em_branco_usa_defaults(monkeypatch
def test_criar_cliente_normaliza_cnpj_e_telefones(monkeypatch):
monkeypatch.setattr(router_clientes.db_clientes, "codigo_em_uso", lambda codigo, ignorar_id=None: False)
monkeypatch.setattr(router_clientes.db_clientes, "cnpj_em_uso", lambda documento, ignorar_id=None: False)
capturado = {}
monkeypatch.setattr(
router_clientes.db_clientes, "criar_cliente", lambda dados: capturado.setdefault("dados", dados) and 42
@@ -323,6 +326,7 @@ def test_criar_cliente_normaliza_cnpj_e_telefones(monkeypatch):
def test_editar_cliente_normaliza_cnpj_e_telefones(monkeypatch):
monkeypatch.setattr(router_clientes.db_clientes, "obter_cliente", lambda cliente_id: {"ID": cliente_id})
monkeypatch.setattr(router_clientes.db_clientes, "codigo_em_uso", lambda codigo, ignorar_id=None: False)
monkeypatch.setattr(router_clientes.db_clientes, "cnpj_em_uso", lambda documento, ignorar_id=None: False)
capturado = {}
monkeypatch.setattr(
router_clientes.db_clientes,
@@ -363,6 +367,37 @@ def test_editar_cliente_codigo_duplicado_bloqueia_sem_bypass(monkeypatch):
assert "ok" not in atualizado
def test_criar_cliente_cnpj_duplicado_bloqueia(monkeypatch):
monkeypatch.setattr(router_clientes.db_clientes, "codigo_em_uso", lambda codigo, ignorar_id=None: False)
monkeypatch.setattr(router_clientes.db_clientes, "cnpj_em_uso", lambda documento, ignorar_id=None: True)
criado = {}
monkeypatch.setattr(
router_clientes.db_clientes, "criar_cliente", lambda dados: criado.setdefault("ok", True) and 42
)
resposta = client.post("/clientes/novo", data=_form_valido())
assert resposta.status_code == 200
assert "já está cadastrado" in resposta.text
assert "ok" not in criado
def test_editar_cliente_cnpj_duplicado_bloqueia(monkeypatch):
monkeypatch.setattr(router_clientes.db_clientes, "obter_cliente", lambda cliente_id: {"ID": cliente_id})
monkeypatch.setattr(router_clientes.db_clientes, "codigo_em_uso", lambda codigo, ignorar_id=None: False)
monkeypatch.setattr(router_clientes.db_clientes, "cnpj_em_uso", lambda documento, ignorar_id=None: True)
atualizado = {}
monkeypatch.setattr(
router_clientes.db_clientes,
"atualizar_cliente",
lambda cliente_id, dados: atualizado.setdefault("ok", True),
)
resposta = client.post("/clientes/1/editar", data=_form_valido())
assert resposta.status_code == 200
assert "já está cadastrado" in resposta.text
assert "ok" not in atualizado
def test_editar_cliente_inexistente_nao_finge_sucesso(monkeypatch):
# Sem a checagem de existência, isso caía direto no UPDATE (0 linhas
# afetadas, sem erro do MySQL) e ainda registrava "sucesso" no log.
@@ -437,6 +472,7 @@ def test_criar_cliente_com_data_e_decimais_vazios_cria_com_defaults():
def test_criar_cliente_com_dv_invalido_exige_confirmacao(monkeypatch):
monkeypatch.setattr(router_clientes.db_clientes, "codigo_em_uso", lambda codigo, ignorar_id=None: False)
monkeypatch.setattr(router_clientes.db_clientes, "cnpj_em_uso", lambda documento, ignorar_id=None: False)
criado = {}
monkeypatch.setattr(
router_clientes.db_clientes, "criar_cliente", lambda dados: criado.setdefault("ok", True) and 42
@@ -464,6 +500,7 @@ def test_criar_cliente_com_cnpj_dv_invalido_bloqueia_sempre(monkeypatch):
"cadastrar mesmo assim" — nem com confirmar_dv_invalido=1.
"""
monkeypatch.setattr(router_clientes.db_clientes, "codigo_em_uso", lambda codigo, ignorar_id=None: False)
monkeypatch.setattr(router_clientes.db_clientes, "cnpj_em_uso", lambda documento, ignorar_id=None: False)
criado = {}
monkeypatch.setattr(
router_clientes.db_clientes, "criar_cliente", lambda dados: criado.setdefault("ok", True) and 42
@@ -487,6 +524,7 @@ def test_criar_cliente_com_cnpj_dv_invalido_bloqueia_sempre(monkeypatch):
def test_editar_cliente_com_cnpj_dv_invalido_bloqueia(monkeypatch):
monkeypatch.setattr(router_clientes.db_clientes, "obter_cliente", lambda cliente_id: {"ID": cliente_id})
monkeypatch.setattr(router_clientes.db_clientes, "codigo_em_uso", lambda codigo, ignorar_id=None: False)
monkeypatch.setattr(router_clientes.db_clientes, "cnpj_em_uso", lambda documento, ignorar_id=None: False)
atualizado = {}
monkeypatch.setattr(
router_clientes.db_clientes,
@@ -506,6 +544,7 @@ def test_editar_cliente_com_cnpj_dv_invalido_bloqueia(monkeypatch):
def test_criar_cliente_com_cpf_dv_invalido_continua_permitindo_confirmar(monkeypatch):
"""Regressão: CPF com DV inválido não muda de comportamento."""
monkeypatch.setattr(router_clientes.db_clientes, "codigo_em_uso", lambda codigo, ignorar_id=None: False)
monkeypatch.setattr(router_clientes.db_clientes, "cnpj_em_uso", lambda documento, ignorar_id=None: False)
criado = {}
monkeypatch.setattr(
router_clientes.db_clientes, "criar_cliente", lambda dados: criado.setdefault("ok", True) and 42
@@ -527,6 +566,7 @@ def test_criar_cliente_com_cpf_dv_invalido_continua_permitindo_confirmar(monkeyp
def test_criar_cliente_grava_telefone_com_ddi_55(monkeypatch):
monkeypatch.setattr(router_clientes.db_clientes, "codigo_em_uso", lambda codigo, ignorar_id=None: False)
monkeypatch.setattr(router_clientes.db_clientes, "cnpj_em_uso", lambda documento, ignorar_id=None: False)
capturado = {}
monkeypatch.setattr(
router_clientes.db_clientes, "criar_cliente", lambda dados: capturado.setdefault("dados", dados) and 42
@@ -543,6 +583,7 @@ def test_criar_cliente_grava_telefone_com_ddi_55(monkeypatch):
def test_criar_cliente_telefone_ja_com_55_nao_duplica(monkeypatch):
monkeypatch.setattr(router_clientes.db_clientes, "codigo_em_uso", lambda codigo, ignorar_id=None: False)
monkeypatch.setattr(router_clientes.db_clientes, "cnpj_em_uso", lambda documento, ignorar_id=None: False)
capturado = {}
monkeypatch.setattr(
router_clientes.db_clientes, "criar_cliente", lambda dados: capturado.setdefault("dados", dados) and 42
@@ -558,6 +599,7 @@ def test_criar_cliente_telefone_ja_com_55_nao_duplica(monkeypatch):
def test_criar_cliente_grava_uf_maiuscula_e_cep_sem_traco(monkeypatch):
monkeypatch.setattr(router_clientes.db_clientes, "codigo_em_uso", lambda codigo, ignorar_id=None: False)
monkeypatch.setattr(router_clientes.db_clientes, "cnpj_em_uso", lambda documento, ignorar_id=None: False)
capturado = {}
monkeypatch.setattr(
router_clientes.db_clientes, "criar_cliente", lambda dados: capturado.setdefault("dados", dados) and 42