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:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user