From f1139e8b3adf5ccb4bd71a4a78dde3faaedfe981 Mon Sep 17 00:00:00 2001 From: Gustavo Date: Thu, 13 Aug 2026 10:25:20 -0300 Subject: [PATCH] feat: rotas de CLIENTE_CONTATO na tela de detalhe do cliente --- app/routers/contatos.py | 69 ++++++++++++++++++++++++++++++ app/templates/cliente_detalhe.html | 20 ++++++++- tests/test_router_contatos.py | 51 ++++++++++++++++++++++ 3 files changed, 138 insertions(+), 2 deletions(-) create mode 100644 app/routers/contatos.py create mode 100644 tests/test_router_contatos.py diff --git a/app/routers/contatos.py b/app/routers/contatos.py new file mode 100644 index 0000000..008d4e2 --- /dev/null +++ b/app/routers/contatos.py @@ -0,0 +1,69 @@ +""" +Rotas de gestão de CLIENTE_CONTATO — sempre no contexto de um cliente +(cliente_id na URL). Redireciona de volta para /clientes/{cliente_id} +(a tela de detalhe do cliente, montada na Task 8). +""" +from urllib.parse import urlencode + +from fastapi import APIRouter, Depends, Form +from fastapi.responses import RedirectResponse + +from ..auth import exigir_login +from ..db import clientes as db_clientes +from ..db import contatos as db_contatos +from ..normalize import normalizar_telefone + +router = APIRouter(prefix="/clientes") + + +def _redirect_com_mensagem(cliente_id: int, mensagem: str, tipo: str = "success") -> RedirectResponse: + query = urlencode({"mensagem": mensagem, "mensagem_tipo": tipo}) + return RedirectResponse(f"/clientes/{cliente_id}?{query}", status_code=303) + + +@router.post("/{cliente_id}/contatos") +def adicionar( + cliente_id: int, + nome: str = Form(""), + telefone: str = Form(...), + email: str = Form(""), + usuario: str = Depends(exigir_login), +): + if db_clientes.obter_cliente(cliente_id) is None: + return _redirect_com_mensagem(cliente_id, "Cliente não encontrado.", "danger") + + telefone_normalizado = normalizar_telefone(telefone) + if not telefone_normalizado: + return _redirect_com_mensagem(cliente_id, "Telefone obrigatório.", "danger") + + if db_contatos.contato_duplicado(cliente_id, telefone_normalizado): + return _redirect_com_mensagem(cliente_id, f"O telefone {telefone_normalizado} já está cadastrado para este cliente.", "warning") + + db_contatos.inserir_contato(cliente_id, nome.strip() or None, telefone_normalizado, email.strip() or None) + return _redirect_com_mensagem(cliente_id, "Contato adicionado.") + + +@router.post("/{cliente_id}/contatos/{contato_id}/editar") +def editar( + cliente_id: int, + contato_id: int, + nome: str = Form(""), + telefone: str = Form(...), + email: str = Form(""), + usuario: str = Depends(exigir_login), +): + telefone_normalizado = normalizar_telefone(telefone) + if not telefone_normalizado: + return _redirect_com_mensagem(cliente_id, "Telefone obrigatório.", "danger") + + if db_contatos.contato_duplicado(cliente_id, telefone_normalizado, ignorar_id=contato_id): + return _redirect_com_mensagem(cliente_id, f"O telefone {telefone_normalizado} já está cadastrado para este cliente.", "warning") + + db_contatos.atualizar_contato(contato_id, cliente_id, nome.strip() or None, telefone_normalizado, email.strip() or None) + return _redirect_com_mensagem(cliente_id, "Contato atualizado.") + + +@router.post("/{cliente_id}/contatos/{contato_id}/excluir") +def excluir(cliente_id: int, contato_id: int, usuario: str = Depends(exigir_login)): + db_contatos.excluir_contato(contato_id, cliente_id) + return _redirect_com_mensagem(cliente_id, "Contato excluído.") diff --git a/app/templates/cliente_detalhe.html b/app/templates/cliente_detalhe.html index 6aae177..057bbfb 100644 --- a/app/templates/cliente_detalhe.html +++ b/app/templates/cliente_detalhe.html @@ -13,11 +13,27 @@

Contatos

- + {% for ct in contatos %} - + + + + {% endfor %}
NomeTelefoneE-mail
NomeTelefoneE-mail
{{ ct.NOME }}{{ ct.TELEFONE }}{{ ct.EMAIL or "" }}
{{ ct.NOME }}{{ ct.TELEFONE }}{{ ct.EMAIL or "" }} +
+ +
+
+
+ + + + + + + +

Status em Clientes_Chaves

{% if status_chaves %} diff --git a/tests/test_router_contatos.py b/tests/test_router_contatos.py new file mode 100644 index 0000000..af3e724 --- /dev/null +++ b/tests/test_router_contatos.py @@ -0,0 +1,51 @@ +from fastapi import FastAPI +from fastapi.testclient import TestClient + +from app.auth import exigir_login +from app.routers import contatos as router_contatos +from app.routers import clientes as router_clientes + +app_teste = FastAPI() +app_teste.include_router(router_clientes.router) +app_teste.include_router(router_contatos.router) +app_teste.dependency_overrides[exigir_login] = lambda: "teste" +client = TestClient(app_teste, follow_redirects=False) + + +def test_adicionar_contato_com_sucesso(monkeypatch): + monkeypatch.setattr(router_contatos.db_clientes, "obter_cliente", lambda cid: {"ID": cid, "CODIGO": "000001"}) + monkeypatch.setattr(router_contatos.db_contatos, "contato_duplicado", lambda cid, tel, ignorar_id=None: False) + chamadas = {} + monkeypatch.setattr( + router_contatos.db_contatos, + "inserir_contato", + lambda cid, nome, tel, email=None: chamadas.setdefault("dados", (cid, nome, tel, email)), + ) + + resposta = client.post("/clientes/1/contatos", data={"nome": "Fulano", "telefone": "(75) 99909-0001"}) + assert resposta.status_code == 303 + assert chamadas["dados"] == (1, "Fulano", "75999090001", None) + + +def test_adicionar_contato_duplicado_avisa(monkeypatch): + monkeypatch.setattr(router_contatos.db_clientes, "obter_cliente", lambda cid: {"ID": cid, "CODIGO": "000001"}) + monkeypatch.setattr(router_contatos.db_contatos, "contato_duplicado", lambda cid, tel, ignorar_id=None: True) + # Also mock the clientes router's db functions for the redirect + monkeypatch.setattr(router_clientes.db_clientes, "obter_cliente", lambda cid: {"ID": cid, "CODIGO": "000001", "RAZAO": "Cliente", "FANTASIA": "Cliente"}) + monkeypatch.setattr(router_clientes.db_contatos, "listar_contatos_por_cliente", lambda cid: []) + monkeypatch.setattr(router_clientes.db_chaves, "status_chaves", lambda codigo: None) + + resposta = client.post("/clientes/1/contatos", data={"nome": "Fulano", "telefone": "75999090001"}, follow_redirects=True) + assert "já está cadastrado" in resposta.text + + +def test_excluir_contato(monkeypatch): + monkeypatch.setattr(router_contatos.db_clientes, "obter_cliente", lambda cid: {"ID": cid, "CODIGO": "000001"}) + chamadas = {} + monkeypatch.setattr( + router_contatos.db_contatos, "excluir_contato", lambda contato_id, cid: chamadas.setdefault("dados", (contato_id, cid)) + ) + + resposta = client.post("/clientes/1/contatos/5/excluir") + assert resposta.status_code == 303 + assert chamadas["dados"] == (5, 1)