feat: Administração > Configurações, remover usuário do tenant, Discador > Callbacks

Fecha os últimos gaps do módulo Administração (agente.md secao 169): tela de
Configurações self-service do próprio tenant (GET/PATCH /tenant-settings, nunca
aceita tenantId arbitrário — só user.tenantId das claims), e DELETE /users/:id pra
remover alguém do tenant, com duas proteções que não existiam antes (não deixa
remover a si mesmo, não deixa remover/rebaixar o último Tenant Admin).

Corrige um bug real achado testando a remoção: o delete de TenantMembership (FORCE
RLS) rodava dentro de um prisma.$transaction([...]) em forma de array, que nunca
seta app.current_tenant_id — Prisma devolvia P2025 "not found" com a linha
existindo (500 pro cliente). Mesma classe de bug já corrigida antes em
TenantsController.create; corrigido com $transaction(async (tx) => ...) + set_config
explícito.

Adiciona Discador > Callbacks (GET/PATCH /leads/callbacks, tenant-wide): reagendar,
tentar de novo sem esperar, ou desistir de um lead que pediu retorno em outro
horário. Remove "Importações" do menu — decisão já registrada na PHASE 39 de não
duplicar uma tela pro que já existe (CSV em lote no wizard/detalhe da campanha).

Testado ponta a ponta via curl e Puppeteer contra o tenant Acme real: tenant-settings
GET/PATCH, proteção de último-admin nos dois endpoints que a usam, convite+remoção
de um admin temporário, e o fluxo completo de callback (lead forçado pra CALLBACK
via SQL, reagendar rejeitado pro passado/aceito pro futuro, requeue confirmado).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BFaBaBSQGhyXGEgtTYZGV8
This commit is contained in:
2026-08-30 08:16:38 -03:00
parent 52e1b4f3b7
commit 2fb5010283
24 changed files with 798 additions and 11 deletions

57
TODO.md
View File

@@ -1483,6 +1483,63 @@ systemd (fecha um risco documentado desde a PHASE 01)
`PredictiveDialerEngine` já popula (PHASE 16) mas sem tela nenhuma
pra visualizar/gerenciar ainda
## PHASE 40 — Administração > Configurações (tenant) + últimos gaps de
Usuários (agente.md secao 169)
- [x] `GET/PATCH /tenant-settings`: self-service do próprio tenant — só
lê/escreve `user.tenantId` das claims, nunca aceita um tenantId
arbitrário no path/body, então não existe forma de um Tenant Admin
mexer em outro tenant por aqui (diferente de `TenantsController`,
que é platform-only). Editável: nome fantasia, CNPJ/CPF, fuso,
idioma, privacidade de IA. Somente leitura: razão social, código,
plano, status, moeda, domínio de telefonia (controlados pela
plataforma)
- [x] Tela `/app/administracao/configuracoes` — formulário + bloco
read-only, mesmo padrão visual das outras telas de Administração
- [x] `DELETE /users/:id` — remove só a membership+role do tenant (nunca
a conta `User`, que pode ter acesso a outros tenants). Duas
proteções novas (nenhuma existia antes): não deixa remover a si
mesmo, e não deixa remover/rebaixar o último Tenant Admin do tenant
(`isLastTenantAdmin`, aplicado também em `PATCH /users/:id/role`) —
sem isso um tenant podia ficar sem ninguém que pudesse gerenciar
usuários
- [x] achado real: o primeiro `remove()` usava
`prisma.$transaction([...])` (forma array) pra apagar
`tenantMembership` — como essa tabela tem FORCE RLS (secao 32) e a
forma array não abre uma transação com `app.current_tenant_id`
setado, o Prisma devolvia P2025 ("not found") mesmo com a linha
existindo (500 pro cliente). Mesma classe de bug já corrigida antes
em `TenantsController.create`. Corrigido trocando pra
`$transaction(async (tx) => ...)` com `set_config` explícito antes
do delete — testado removendo de verdade um usuário de teste
(invite → demote self (2 admins) → remove → 204 → sumiu da lista)
- [x] Testado ponta a ponta: GET/PATCH tenant-settings via curl e via UI
(nome fantasia editado, sidebar atualiza na hora), proteção de
último-admin confirmada nos dois endpoints (403 nos dois), convite +
remoção de um admin temporário confirmados
## PHASE 41 — Discador > Callbacks (agente.md secao 78-79, 169)
- [x] `GET /leads/callbacks` (tenant-wide, todas as campanhas) +
`PATCH /leads/callbacks/:id` com 3 ações: `RESCHEDULE` (nova data,
rejeitada se não for no futuro), `REQUEUE` (volta pra `READY`,
dialer pega de novo sem esperar), `CANCEL` (`DO_NOT_CALL`) — só
atua sobre leads que ainda estão em `CALLBACK` (RLS +
`withTenantContext`, mesmo padrão já auditado)
- [x] Tela `/app/discador/callbacks` — lista com nome da campanha,
telefone, tentativas, "remarcado para" com date-time picker inline
- [x] "Importações" removido do menu (nunca virou tela real, decisão já
registrada na PHASE 39: CSV em lote já existe no wizard e no
detalhe da campanha, uma tela separada só faria sentido com
histórico de import persistido, que não existe) — mesmo princípio
já usado em Monitoramento (secao 168-169): não deixar link pra
"em breve" quando a funcionalidade de verdade já está em outro
lugar
- [x] Testado ponta a ponta: lead de teste forçado pra `CALLBACK` via SQL
direto (não existe fluxo de produto pra chegar nesse estado sem o
dialer rodando uma chamada real), reagendamento rejeitado pro
passado (400), aceito pro futuro (200), requeue confirmado (sai da
lista de callbacks, volta pra `READY`), lead de teste removido no
final
---
## Riscos conhecidos