Etapa 5 — Painel Tenant: clientes, contatos e produtos/serviços

Adiciona os módulos customers (com contatos aninhados e regra de contato
principal único) e products, ambos com TenantGuard/RLS, soft delete,
limite de clientes do plano e SKU único por tenant (409 amigável).
Atualiza o controle de progresso e cobre os critérios de aceite com
test/tenant-customers-produtos.e2e-spec.ts.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-14 21:44:01 -03:00
co-authored by Claude Sonnet 5
parent 04472fcd2b
commit 24b3bb9ae4
20 changed files with 1523 additions and 8 deletions
+111 -8
View File
@@ -34,7 +34,7 @@ qualquer nova etapa começar.
| 2 | Autenticação e RLS | Concluída | 2026-08-13 |
| 3 | Painel Master (tenants/planos/faturas) | Concluída | 2026-08-13 |
| 4 | Integrações Evolution API + E-mail | Concluída | 2026-08-14 |
| 5 | Painel Tenant (clientes/produtos) | [ ] Não iniciada | |
| 5 | Painel Tenant (clientes/produtos) | Concluída | 2026-08-14 |
| 6 | Vendas, Parcelas e Dashboards | [ ] Não iniciada | |
| 7 | Motor de Alertas (BullMQ) | [ ] Não iniciada | |
| 8 | Personalização Whitelabel e Ajustes Finais | [ ] Não iniciada | |
@@ -439,16 +439,108 @@ propositalmente para depois — detalhar em Observações).
## Etapa 5 — Painel Tenant: Clientes, Produtos e Serviços
- [ ] CRUD de customers funcional, com soft delete e busca
- [ ] Criar customer além do limite do plano retorna erro claro
- [ ] CRUD de contatos aninhado funciona; contato principal único respeitado
- [ ] CRUD de produtos/serviços funcional, com soft delete e busca
- [ ] SKU duplicado no mesmo tenant retorna erro 409; permitido entre tenants
- [ ] Teste confirma que acessar recurso de outro tenant retorna 404
- [ ] Regra de acesso master a dados de tenant definida e documentada
- [x] CRUD de customers funcional, com soft delete e busca
- [x] Criar customer além do limite do plano retorna erro claro
- [x] CRUD de contatos aninhado funciona; contato principal único respeitado
- [x] CRUD de produtos/serviços funcional, com soft delete e busca
- [x] SKU duplicado no mesmo tenant retorna erro 409; permitido entre tenants
- [x] Teste confirma que acessar recurso de outro tenant retorna 404
- [x] Regra de acesso master a dados de tenant definida e documentada
**Observações:**
- Módulos novos: `src/modules/customers/` (`CustomersController`/`Service` em
`/tenant/customers`, `CustomerContactsController`/`Service` aninhado em
`/tenant/customers/:customerId/contatos`) e `src/modules/products/`
(`ProductsController`/`Service` em `/tenant/produtos`). Todos os
controllers usam só `@UseGuards(JwtAuthGuard, TenantGuard)` — sem
`RolesGuard`/`@Roles`, como o prompt desta etapa pedia ("todos os
endpoints usam TenantGuard") — qualquer papel de `tenant_user`
(`tenant_admin`/`vendedor`/`financeiro`/`visualizador`) acessa; papéis
mais granulares por endpoint ficam para uma etapa futura, se necessário.
Todos usam `TenantContextService.getTx()` (conexão `app_tenant`, RLS
ativo) — nenhuma query manual filtrando por `tenant_id`.
- **Decisão que o prompt pedia confirmar antes de implementar** (critério
de aceite "master deve conseguir acessar em modo de suporte — decisão a
confirmar"): **não** — o master "puro" (sem estar logado como um tenant
específico) não tem nenhum acesso aos endpoints desta etapa — o mesmo
critério que já valia nas etapas anteriores (`TenantGuard` rejeita
qualquer token que não seja `tenant_user`, 403). Confirmado com o
usuário nesta sessão. Se um modo de suporte for necessário no futuro, deve virar uma
etapa própria com endpoints paralelos (ex: `GET
/master/tenants/:tenantId/customers`), guardados por `MasterGuard` e com
log de auditoria de cada acesso — não reaproveitar as rotas `/tenant/*`.
- Limite de `limite_customers` do plano: contado só entre customers
`ativo=true` (`tx.customers.count({ where: { ativo: true } })`) — um
cliente desativado (soft delete) libera espaço pra cadastrar outro,
mesmo espírito de "soft delete não deve punir o tenant". `plano` é
resolvido via `PrismaMasterService` (`tenants.findUnique({include:
{plano:true}})`), mesmo padrão já usado em `WhatsappService` pro limite
de instâncias (Etapa 4) — `limite_customers` `NULL` = ilimitado, não
bloqueia.
- Regra de contato principal único (`customer_contacts.principal`):
aplicada com um `updateMany` desmarcando o(s) principal(is) anterior(es)
do mesmo `customer_id`, seguido do `create`/`update` do novo — como o
interceptor de RLS (`TenantRlsInterceptor`, Etapa 2) já envolve a
request inteira numa única transação Prisma, essas duas operações já são
atômicas por construção, sem precisar abrir uma `$transaction` aninhada
(que nem seria possível: `tx` aqui já é um `Prisma.TransactionClient`,
que não expõe `$transaction`).
Comportamento explícito verificado em teste: criar dois contatos
seguidos com `principal:true` deixa só o segundo como principal.
- SKU duplicado no mesmo tenant → 409: `products_services` tem
`@@unique([tenant_id, sku])` no schema; o service tenta o `create`/
`update` dentro de um `try/catch` e traduz
`Prisma.PrismaClientKnownRequestError` com `code === 'P2002'` para
`ConflictException` com mensagem citando o SKU. Não filtra por
`error.meta.target` incluir `'sku'` — testado nesta sessão que o driver
Postgres do Prisma nem sempre popula esse campo de forma previsível (às
vezes só o nome da constraint, às vezes indisponível); como `sku` é a
única constraint `UNIQUE` da tabela além da chave primária, checar só
`code === 'P2002'` é suficiente e mais robusto.
- Validação de `documento` (customers): formato de CPF (11 dígitos) ou
CNPJ (14 dígitos), puro ou com a pontuação usual — **não** confere
dígito verificador, como o prompt desta etapa já autorizava
("não precisa validar dígito verificador na primeira versão"). Comentário
no código (`src/modules/customers/documento.util.ts`) registrando que
pode evoluir para validação completa (módulo 11) numa etapa futura.
- Isolamento por RLS desta etapa não precisou de nenhuma policy nova — já
criadas na migration da Etapa 1/2
(`tenant_isolation_customers`/`tenant_isolation_products`/
`tenant_isolation_customer_contacts`, essa última via subquery em
`customer_id` já que `customer_contacts` não tem `tenant_id` direto).
Testado nesta etapa (não só confiado na policy): tenant B tentando
ler/editar/excluir customer, produto e lista de contatos do tenant A
pelo UUID exato — sempre 404 (nunca 403), já que a linha simplesmente
não existe do ponto de vista da conexão `app_tenant` com
`app.current_tenant_id` do tenant B.
- Teste e2e novo, `test/tenant-customers-produtos.e2e-spec.ts` (20 testes),
cobrindo todos os critérios de aceite: CRUD de customers (campo mínimo
obrigatório, validação de formato de documento, busca por nome/
documento, paginação, update, soft delete confirmando a linha
permanecer no banco com `ativo=false` via query direta ao Prisma),
bloqueio ao estourar `limite_customers` do plano com mensagem clara,
CRUD de contatos aninhado com a regra de principal único, contato de
customer inexistente retornando 404, CRUD de produtos/serviços
(validação de `tipo`/`valor_venda > 0`, SKU duplicado 409 vs. SKU igual
permitido entre tenants diferentes, filtro por `tipo`/`ativo`/busca,
soft delete), isolamento RLS entre dois tenants reais (customer, produto
e lista de contatos de outro tenant sempre 404) e bloqueio de todos os
endpoints desta etapa com token master "puro" (403). Rodado com `docker
compose up -d postgres redis` + `prisma migrate deploy` (nenhuma
migration nova — `customers`/`customer_contacts`/`products_services` já
existiam desde a Etapa 1) + `jest --config ./test/jest-e2e.json
--runInBand` apontando pro Postgres via `localhost` — **74/74 testes e2e
passando** (os 54 das Etapas 1-4 + os 20 novos desta etapa). `npm run
lint`, `npm run test` (unit) e `npm run build` também passam sem erros.
Ambiente deste host não tem Node/npm nativo (só Docker) — dependências
instaladas via `docker run node:20-alpine` com `python3 make g++`
adicionados (necessários pro binding nativo do `argon2`, mesmo motivo já
documentado na Etapa 1/4); rede bridge padrão do Docker funcionou nesta
sessão (sem precisar `--network host` pra `npm install`/pull de imagens,
só pro container de teste alcançar o Postgres publicado pelo
`docker-compose` via `localhost`).
---
## Etapa 6 — Vendas, Parcelas e Dashboards
@@ -599,3 +691,14 @@ ponta a ponta no teste e2e contra um servidor HTTP local que simula esse
contrato (`FakeEvolutionServer`, em `test/integracoes.e2e-spec.ts`), não
contra o serviço real. Confirmar contra uma instância real antes de operar
em produção.
[2026-08-14] Etapa 5 — Decisão explicitamente pedida pelo critério de
aceite ("master deve conseguir acessar em modo de suporte — decisão a
confirmar antes de implementar"), confirmada com o usuário: **o master
"puro" não acessa `/tenant/customers`/`/tenant/produtos`/contatos de
nenhum tenant.** Todos os controllers desta etapa usam só `TenantGuard`
(sem endpoint paralelo pro master). Se um modo de suporte para
diagnóstico/atendimento vier a ser necessário, deve ser uma etapa própria
(endpoints `/master/tenants/:tenantId/...` read-only, `MasterGuard` +
auditoria de cada acesso), não uma extensão das rotas `/tenant/*`
existentes.