feat(platform): Clientes > Tenants e Planos — CRUD real (backend novo)
Não existia NENHUM endpoint pra criar/listar/editar tenant nem plano até aqui — só via script/seed ad hoc. Dois controllers novos: TenantsController (/tenants) e PlansController (/plans), platform-only. POST /tenants cria o tenant E o primeiro usuário (Tenant Admin) numa transação só — sem esse usuário o tenant fica inacessível. Senha gerada e devolvida em texto puro só na resposta de criação (revela uma vez, mesmo padrão de Ramais/SIP). Bug real achado testando o próprio endpoint: GET /tenants calculava memberCount sem contexto de RLS — tenant_memberships tem FORCE RLS, então nem platform admin enxerga linha nenhuma sem app.current_tenant_id setado, o campo sempre voltava 0. Corrigido abrindo o contexto de cada tenant um de cada vez. Frontend: /platform/clientes/tenants (lista+busca), /tenants/new (cria tenant+admin, revela senha), /tenants/:id (troca status/plano, preview dos limites ao vivo), /platform/clientes/planos (CRUD completo dos limites). Testado ponta a ponta: plano novo -> tenant novo com admin real -> troca de plano persistida, confirmada via API. Smoke test nas 19 telas anteriores, todas 200. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BFaBaBSQGhyXGEgtTYZGV8
This commit is contained in:
55
TODO.md
55
TODO.md
@@ -1176,6 +1176,61 @@ Troncos, Discador > Lista de Bloqueio (agente.md secao 50-51, 41-42, 71,
|
||||
reconexão do socket.io real antes de fechar o stream, mas não há
|
||||
backoff exponencial nem um teste de queda de rede prolongada
|
||||
|
||||
## PHASE 33 — Platform: Clientes > Tenants e Planos (CRUD real, backend
|
||||
novo) (agente.md secao 29, 56, 126, 141, 168)
|
||||
- [x] **Lacuna de backend fechada primeiro**: não existia NENHUM endpoint
|
||||
pra criar/listar/editar tenant nem plano até aqui — só via script/
|
||||
seed ad hoc (a própria Acme foi criada assim numa sessão anterior,
|
||||
sem deixar rastro reproduzível). Dois controllers novos:
|
||||
`TenantsController` (`/tenants`, `tenants.manage`/`.view`) e
|
||||
`PlansController` (`/plans`, `pricing.manage`), ambos platform-only
|
||||
(`isPlatformUser`, mesmo padrão de billing).
|
||||
- [x] `POST /tenants` cria o tenant **e** o primeiro usuário (Tenant
|
||||
Admin) numa transação só (secao 141: sem esse usuário o tenant fica
|
||||
inacessível) — `Tenant.create` + `User.create` + `TenantMembership`
|
||||
+ `UserRole(tenant_admin)`, senha gerada
|
||||
(`generateStrongPassword`) e devolvida em texto puro só nesta
|
||||
resposta (mesmo padrão de "revela uma vez" de Ramais/SIP).
|
||||
`telephonyDomain` fixo em `b2bcall.local` (decisão já tomada na
|
||||
PHASE 08 — sem multi-domínio real ainda).
|
||||
- [x] **Bug real, achado testando o próprio endpoint antes de expor no
|
||||
frontend**: `GET /tenants` calculava `memberCount` com
|
||||
`tenantMembership.groupBy` numa query sem contexto de tenant
|
||||
nenhum — `tenant_memberships` tem FORCE RLS (secao 32), então
|
||||
**nem platform admin** enxerga uma linha sequer sem
|
||||
`app.current_tenant_id` setado; o campo sempre voltava `0`.
|
||||
Corrigido abrindo o contexto de RLS de cada tenant um de cada vez
|
||||
(`Promise.all` de `withTenantContext` por tenant) — aceitável numa
|
||||
tela de administração, não é hot path.
|
||||
- [x] Frontend: `/platform/clientes/tenants` (lista com busca, badge de
|
||||
status próprio — não reusa `StatusBadge`, mesma cautela de gênero
|
||||
já aplicada em `AgentStateBadge`: "Ativo" de tenant não é "Ativa"
|
||||
de campanha), `/tenants/new` (cria tenant+admin, revela senha
|
||||
temporária uma vez), `/tenants/:id` (troca status/plano, mostra os
|
||||
limites do plano selecionado ao vivo antes de salvar).
|
||||
`/platform/clientes/planos` (cards com todos os limites, criar e
|
||||
editar inline — campo de limite em branco = sem limite, nunca
|
||||
zero).
|
||||
- [x] Testado ponta a ponta contra a API real: criado plano "Profissional"
|
||||
(50 ramais/agentes, resto sem limite) → criado tenant "Beta Corp
|
||||
Telecom LTDA" com admin `admin@beta.b2bcall.local`, senha revelada
|
||||
uma vez → detalhe do tenant → trocado plano pra "Profissional" →
|
||||
salvo → confirmado via API que persistiu (`GET /tenants/:id`
|
||||
retornando o plano novo). Smoke test de regressão nas 19 telas do
|
||||
tenant + tarifas, todas 200.
|
||||
- [ ] "Clientes > Assinaturas" e "> Quotas" continuam "em breve" —
|
||||
`SubscriptionsController`/`PlanVersionsController` já existem
|
||||
(PHASE 22) mas sem tela; Quotas ficou parcialmente coberta pelos
|
||||
limites do plano já visíveis no detalhe do tenant, sem uma tela
|
||||
dedicada de consumo-vs-limite ainda
|
||||
(esperando **Billing > Consumo**, que depende do mesmo seletor de
|
||||
tenant)
|
||||
- [ ] Sem exclusão/soft-delete de tenant nem de plano pela UI (só
|
||||
suspender via status) — decisão deliberada, mesma cautela de
|
||||
qualquer ação destrutiva em dado de cliente real
|
||||
- [ ] `Plan.key` não pode ser editado depois de criado (só os limites) —
|
||||
não implementado, `key` normalmente não deveria mudar mesmo
|
||||
|
||||
---
|
||||
|
||||
## Riscos conhecidos
|
||||
|
||||
Reference in New Issue
Block a user