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>
705 lines
46 KiB
Markdown
705 lines
46 KiB
Markdown
# SeConectai Vendas — Controle de Progresso
|
|
|
|
Este arquivo é o **estado oficial do projeto**. Ele deve ser mantido
|
|
atualizado pela IA que está construindo o sistema e consultado antes de
|
|
qualquer nova etapa começar.
|
|
|
|
## Instruções obrigatórias para a IA
|
|
|
|
1. **Antes de iniciar qualquer etapa**, peça este arquivo ao usuário (se
|
|
ele não for fornecido junto com o prompt da etapa) e leia a seção
|
|
"Status Geral" para confirmar que a etapa anterior está 100% concluída.
|
|
2. **Não inicie uma etapa** se a etapa anterior tiver qualquer item de
|
|
critério de aceite sem marcar. Se estiver incompleta, avise o usuário
|
|
e pergunte se deve terminar a etapa pendente antes de avançar.
|
|
3. **Ao concluir uma etapa**, marque com `[x]` cada item da checklist
|
|
correspondente (só marque o que foi de fato implementado e testado —
|
|
não marque por otimismo) e atualize:
|
|
- A seção "Status Geral" (etapa concluída, data)
|
|
- O campo "Observações" da etapa, se houver algo relevante para as
|
|
próximas etapas (ex: decisões tomadas, desvios do prompt original,
|
|
pendências técnicas deixadas para depois)
|
|
4. **Devolva este arquivo atualizado** ao usuário ao final de cada etapa,
|
|
junto com o restante da entrega, para que ele possa repassar na
|
|
próxima sessão/etapa.
|
|
5. Se o usuário pedir para pular uma etapa ou mudar a ordem, isso deve
|
|
ficar explicitamente registrado na seção "Observações" da etapa
|
|
afetada, com o motivo.
|
|
|
|
## Status Geral
|
|
|
|
| # | Etapa | Status | Concluída em |
|
|
|---|-------|--------|---------------|
|
|
| 1 | Setup do Projeto | Concluída | 2026-08-12 |
|
|
| 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) | 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 | |
|
|
|
|
Valores possíveis para "Status": `Não iniciada`, `Em andamento`,
|
|
`Concluída`, `Concluída com pendências` (usar este último se algo ficou
|
|
propositalmente para depois — detalhar em Observações).
|
|
|
|
---
|
|
|
|
## Etapa 1 — Setup do Projeto
|
|
|
|
- [x] `docker compose up` sobe `app`, `postgres` e `redis` sem erro
|
|
- [x] `GET /health` retorna 200 com status da API e status da conexão com o banco
|
|
- [x] Todas as tabelas, enums, views e índices do schema fornecido existem no banco após a migration
|
|
- [x] As policies de RLS estão criadas
|
|
- [x] `.env.example` documenta todas as variáveis necessárias
|
|
- [x] `npm run lint` e `npm run test` rodam sem erro
|
|
- [x] README explica como subir o ambiente do zero
|
|
|
|
**Observações:**
|
|
|
|
- Estrutura NestJS, `ConfigModule`/validação de env, `PrismaService`/`PrismaModule`
|
|
e o módulo `health` (controller + service + spec) já existiam de uma sessão
|
|
anterior; esta sessão completou o restante da etapa: `Dockerfile` multi-stage
|
|
(stages `deps`, `dev`, `build`, `prod-deps`, `production`), `docker-compose.yml`,
|
|
`docker-compose.override.yml` (hot-reload dev), `.env.example`, `README.md` e as
|
|
migrations do Prisma.
|
|
- `prisma/schema.prisma` já era a tradução 1:1 do schema SQL fornecido. Construtos
|
|
que o Prisma DSL não expressa nativamente (views de dashboard, `CHECK` constraint
|
|
de `dia_vencimento_mensalidade`, índices parciais e as `CREATE POLICY` de RLS)
|
|
foram implementados em uma migration manual separada
|
|
(`prisma/migrations/20260812161100_rls_views_constraints/`), aplicada com
|
|
`prisma migrate deploy` logo após a migration inicial gerada automaticamente
|
|
(`prisma migrate dev --name init`). Confirmado via `\dt`, `\dv`, `\d+ tenants` e
|
|
`\d+ customers` no psql: 22 tabelas, 6 enums, 3 views, o `CHECK` e as 9 policies
|
|
de isolamento por tenant.
|
|
- As imagens Alpine (`node:20-alpine`) precisaram de `apk add openssl` em todos os
|
|
estágios que rodam `prisma generate`/`node`, pois o Prisma não detecta
|
|
libssl/openssl no Alpine puro e cai num engine binário incompatível (aviso
|
|
"Prisma failed to detect the libssl/openssl version").
|
|
- `@types/supertest` estava fixado em `^6.0.2` no `package.json`, mas o `npm
|
|
install` resolveu `supertest@7.2.2` (com `superagent@10.x` como dependência) —
|
|
uma combinação incompatível com os tipos da v6, quebrando `npm run lint` com
|
|
erros de "Unsafe call"/"not callable" em `test/health.e2e-spec.ts`. Corrigido
|
|
atualizando para `@types/supertest@^7.0.3` e trocando `import * as request from
|
|
'supertest'` por `import request, { Response } from 'supertest'` (a v7 dos tipos
|
|
exige import default por causa da interop com `esModuleInterop`).
|
|
- `package-lock.json` foi gerado nesta sessão (não existia antes); `npm ci` no
|
|
Dockerfile depende dele.
|
|
- Ambiente de execução não tinha Node/npm no host, e os containers Docker deste
|
|
sandbox não têm acesso de rede via bridge padrão (bloqueado no nível do host) —
|
|
só o modo `--network host` alcança a internet. Isso só afetou os comandos usados
|
|
para *verificar* a etapa nesta sessão (`docker build --network host`, `docker
|
|
run --network host ... npm install`); `Dockerfile`/`docker-compose.yml`
|
|
entregues não dependem disso e devem funcionar normalmente em qualquer ambiente
|
|
Docker com rede padrão.
|
|
|
|
---
|
|
|
|
## Etapa 2 — Autenticação, Autorização e Row Level Security
|
|
|
|
- [x] Login master e login tenant funcionam e retornam access+refresh token válidos
|
|
- [x] Refresh token rotaciona corretamente; token antigo rejeitado após rotação
|
|
- [x] Logout revoga o refresh token
|
|
- [x] Fluxo de esqueci minha senha gera token, valida expiração/uso único
|
|
- [x] `MasterGuard` e `TenantGuard` bloqueiam corretamente rotas erradas
|
|
- [x] `RolesGuard` bloqueia usuário sem permissão de papel
|
|
- [x] Teste de integração prova isolamento entre tenants via RLS
|
|
- [x] Bloqueio temporário após tentativas de login falhas
|
|
- [x] Todas as senhas no banco estão como hash argon2id
|
|
|
|
**Observações:**
|
|
|
|
- Módulo `src/modules/auth/` completo: `AuthController`/`AuthService`
|
|
(`master/login`, `tenant/login`, `refresh`, `logout`, `password/forgot`,
|
|
`password/reset`), `JwtStrategy` (passport-jwt), guards (`JwtAuthGuard`,
|
|
`MasterGuard`, `TenantGuard`, `RolesGuard`), decorators (`@CurrentUser()`,
|
|
`@Roles(...)`), `password.util.ts` (argon2id, parâmetros OWASP 2024:
|
|
memoryCost 19456/timeCost 2/parallelism 1, + validação de força mínima) e
|
|
`token.util.ts` (refresh/reset tokens opacos, guardados só como hash
|
|
sha256 — nunca em texto puro).
|
|
- **Desvio do prompt original, registrado em detalhe no histórico abaixo:**
|
|
o prompt só previa uma segunda connection string (`DATABASE_URL_MASTER`,
|
|
role `app_master BYPASSRLS`) para o master. Foi necessário criar também
|
|
uma terceira role/connection string (`app_tenant`/`DATABASE_URL_TENANT`)
|
|
para que o RLS realmente filtre as queries de `tenant_user` — a role de
|
|
`DATABASE_URL` (`seconectai`) é a superuser bootstrap do Postgres
|
|
(criada pelo `POSTGRES_USER` do docker-compose desde a Etapa 1), e
|
|
superusers sempre ignoram RLS independente das policies existirem.
|
|
Ambas as roles são criadas na migration manual
|
|
`prisma/migrations/20260813120000_auth_app_roles/`.
|
|
- Mecanismo de RLS por request: `TenantRlsInterceptor` (registrado
|
|
globalmente via `APP_INTERCEPTOR`) detecta requests autenticadas como
|
|
`tenant_user` e, através de `TenantContextService`, abre uma transação
|
|
Prisma na conexão `app_tenant` executando
|
|
`SELECT set_config('app.current_tenant_id', '<tenant_id_do_jwt>', true)`
|
|
(equivalente a `SET LOCAL`, mas parametrizável — evita interpolar string
|
|
na query). O `tenant_id` usado vem **sempre** do JWT da sessão
|
|
(`request.user.tenant_id`, populado pelo `JwtStrategy`/`TenantGuard`),
|
|
nunca de body/query — verificado no teste de isolamento (endpoint
|
|
`GET /auth/tenant/users` ignora um `?tenant_id=<outro-tenant>` forçado
|
|
na query string).
|
|
- Requests master usam `PrismaMasterService` (conexão `app_master`,
|
|
bypassa RLS) — não passam pelo `TenantRlsInterceptor`.
|
|
- Endpoints de demonstração/utilidade adicionados para exercitar os guards
|
|
e o RLS de ponta a ponta (não fazem parte do prompt original, mas são
|
|
necessários para provar os critérios de aceite via HTTP real, e ficam
|
|
como funcionalidade útil para as próximas etapas): `GET /auth/me`
|
|
(qualquer usuário autenticado), `GET /auth/master/admins` (só master),
|
|
`GET /auth/tenant/users` (só `tenant_admin`, lista usuários do próprio
|
|
tenant via RLS).
|
|
- `email` de `tenant_users` é único só por tenant (`UNIQUE(tenant_id,
|
|
email)`, não globalmente) — login/forgot-password por e-mail, sem mais
|
|
contexto, não conseguem resolver com segurança um e-mail que colida
|
|
entre dois tenants distintos. Decisão: nesse caso (0 ou >1 tenant_user
|
|
com o e-mail informado), tratar como credencial inválida em vez de
|
|
arriscar autenticar na conta errada (`AuthService.uniqueTenantUserByEmail`).
|
|
Fica registrado como limitação conhecida para uma etapa futura resolver
|
|
(ex: login por subdomínio/slug do tenant).
|
|
- `POST /auth/password/forgot` sempre responde com a mesma mensagem
|
|
genérica, exista ou não a conta — prática padrão para não vazar quais
|
|
e-mails estão cadastrados (não pedido explicitamente no prompt, mas não
|
|
conflita com nenhum critério de aceite).
|
|
- Reuso de um refresh token já rotacionado (revogado) é tratado como
|
|
indício de roubo de token: além de rejeitar a tentativa, revoga toda a
|
|
família de tokens ativos daquele usuário (defesa em profundidade, além
|
|
do pedido mínimo do prompt).
|
|
- `argon2` é um módulo nativo (binding C++) sem prebuild musl/Alpine para
|
|
esta versão — precisou adicionar `python3 make g++` aos estágios `deps`
|
|
e `prod-deps` do `Dockerfile` (ausentes na imagem final `production`,
|
|
só usados para compilar durante o build).
|
|
- Teste e2e `test/auth.e2e-spec.ts` cobre todos os critérios de aceite
|
|
contra Postgres real (via `docker compose`): login (master/tenant,
|
|
sucesso/falha), rotação de refresh token (incluindo o cenário de reuso
|
|
de token revogado revogando a família toda), logout, forgot/reset
|
|
(token único-uso, expiração, senha fraca rejeitada), os quatro guards
|
|
(`JwtAuthGuard`/`MasterGuard`/`TenantGuard`/`RolesGuard`) nos dois
|
|
sentidos, isolamento por RLS (dois tenants reais, tentativa de forçar
|
|
`tenant_id` de outro tenant via query string) e bloqueio por força
|
|
bruta (5 tentativas falhas → 429 mesmo com senha correta na 6ª).
|
|
Rodado com `docker compose exec app npx prisma migrate deploy` seguido
|
|
de `npx jest --config ./test/jest-e2e.json` — 21/21 testes passando.
|
|
`npm run lint`, `tsc --noEmit` e `npm run build` (produção) também
|
|
passam sem erros; smoke test manual via `curl` contra a imagem
|
|
`production` real confirmou login + guards funcionando fim a fim.
|
|
|
|
---
|
|
|
|
## Etapa 3 — Painel Master: Tenants, Planos e Faturas
|
|
|
|
- [x] CRUD de planos funcional e validado
|
|
- [x] Criar tenant também cria o primeiro `tenant_admin` funcional
|
|
- [x] Listagem de tenants com filtro por status e busca funciona
|
|
- [x] Suspender/cancelar tenant bloqueia login dos usuários dele
|
|
- [x] Geração de faturas do mês não duplica fatura existente
|
|
- [x] Marcar fatura como paga reverte tenant `inadimplente` → `ativo`
|
|
- [x] Marcar fatura como atrasada muda tenant para `inadimplente`
|
|
- [x] Dashboard master retorna números corretos
|
|
- [x] Ações relevantes aparecem em `audit_logs`
|
|
- [x] Endpoints inacessíveis com token de tenant_user
|
|
|
|
**Observações:**
|
|
|
|
- Módulo `src/modules/planos/` (`PlanosController`/`PlanosService`, CRUD completo em
|
|
`/master/planos`) e módulo `src/modules/tenants/` (`TenantsController`/`TenantsService`
|
|
em `/master/tenants`, `FaturasController`/`FaturasService` em `/master/faturas`,
|
|
`DashboardController`/`DashboardService` em `/master/dashboard`, todos agrupados no
|
|
mesmo `TenantsModule`). Todos os controllers desta etapa usam
|
|
`@UseGuards(JwtAuthGuard, MasterGuard)` a nível de classe e a conexão `PrismaMasterService`
|
|
(`app_master`, BYPASSRLS) — `tenants`/`planos`/`tenant_faturas`/`audit_logs` não têm RLS
|
|
habilitado (só tabelas por-tenant têm), mas a conexão master é usada por consistência
|
|
arquitetural com a Etapa 2.
|
|
- Auditoria centralizada em `src/common/audit-log.util.ts` (`writeAuditLog(client, params)`,
|
|
função simples — não um serviço injetável — para poder receber tanto `PrismaMasterService`
|
|
quanto o `tx` de uma transação em andamento, seguindo o mesmo padrão de utilitário puro já
|
|
usado em `auth/password.util.ts`/`token.util.ts`). Toda operação de escrita desta etapa
|
|
(criar/atualizar plano, excluir/desativar plano, criar tenant, atualizar tenant, mudar
|
|
status, gerar faturas do mês, marcar fatura paga, atualizar atrasadas) grava a auditoria
|
|
**dentro da mesma transação** da operação principal, para que auditoria e efeito nunca
|
|
fiquem dessincronizados.
|
|
- **Desvio do prompt original:** o prompt lista `status` como um dos campos que o `PUT
|
|
/master/tenants/:id` atualiza. Implementado com `status` **fora** do `UpdateTenantDto` —
|
|
toda transição de status passa exclusivamente por `PATCH /master/tenants/:id/status`
|
|
(`TenantsService.changeStatus`), que é o único caminho que grava a auditoria da mudança
|
|
(`de`/`para`) e aplica o bloqueio de acesso. Deixar `status` editável também via PUT
|
|
abriria um caminho para mudar o status sem passar por essa lógica.
|
|
- **Desvio/extensão do prompt original, relevante para segurança:** o prompt pedia bloquear
|
|
login após suspender/cancelar um tenant ("checar status do tenant no login e no
|
|
`TenantGuard`"). Login já checava (Etapa 2); nesta etapa o `TenantGuard`
|
|
(`auth/guards/tenant.guard.ts`) passou a consultar o status do tenant a cada request
|
|
(antes só populava `request.tenant_id`, sem checar nada) — um access token emitido antes
|
|
da suspensão continua criptograficamente válido até expirar (15 min por padrão), então sem
|
|
essa checagem o usuário suspenso continuaria acessando a API normalmente até o token
|
|
expirar. Isso adiciona uma consulta a `tenants` por request autenticada como tenant_user
|
|
(não cacheada por ora — aceitável no volume atual, pode virar ponto de otimização numa
|
|
etapa futura). Como defesa em profundidade adicional (mesmo espírito da revogação de
|
|
família de refresh tokens da Etapa 2), `changeStatus` também revoga todos os refresh
|
|
tokens ativos dos `tenant_users` do tenant ao suspender/cancelar.
|
|
- `TENANT_BLOCKED_STATUSES` (antes uma constante privada em `auth.service.ts`) foi exportado
|
|
para ser a única fonte de verdade de quais status bloqueiam acesso, reusada tanto no login
|
|
(Etapa 2) quanto no `TenantGuard` (Etapa 3) — evita as duas listas divergirem no futuro.
|
|
- Exclusão de plano (`DELETE /master/planos/:id`): interpretado "em uso por algum tenant
|
|
ativo" como qualquer tenant com `plano_id` apontando pra ele e `status != 'cancelado'`
|
|
(não só `status = 'ativo'`) — um tenant `trial`/`inadimplente`/`suspenso` também depende do
|
|
plano estar íntegro. Nesse caso a exclusão é convertida em desativação (`ativo = false`,
|
|
plano continua existindo e servindo os tenants já associados) e a resposta indica
|
|
`{deletado:false, desativado:true}`; sem tenants associados (exceto cancelados), a linha é
|
|
excluída de fato (`{deletado:true, desativado:false}`). Um plano desativado não pode ser
|
|
associado a tenants novos (`create`/`update` de tenant checam `plano.ativo`), mas tenants
|
|
já associados a ele continuam funcionando normalmente (não há "downgrade forçado").
|
|
- Senha provisória do primeiro `tenant_admin`: se o payload de `POST /master/tenants` não
|
|
informar `admin.senha`, uma é gerada (`tenants/provisional-password.util.ts`,
|
|
`randomBytes(9).toString('base64url')`) e devolvida em texto puro **só na resposta desse
|
|
request** (nunca fica logada nem persistida em texto puro — só o hash argon2id via
|
|
`password.util.ts`, igual ao resto do sistema). Envio por e-mail é a Etapa 4
|
|
(Integrações), como o prompt já previa.
|
|
- `POST /master/faturas/gerar-mes` usa `createMany({ skipDuplicates: true })` em vez de
|
|
checar linha a linha se já existe fatura pro `(tenant_id, referencia)` — o
|
|
`UNIQUE(tenant_id, referencia)` do schema garante a idempotência a nível de banco; chamar
|
|
de novo no mesmo mês (ou re-chamar manualmente antes da automação virar cron na Etapa 7) é
|
|
seguro.
|
|
- `POST /master/faturas/atualizar-atrasadas` só rebaixa pra `inadimplente` tenants que
|
|
estejam `ativo` no momento — tenants já `suspenso`/`cancelado` com fatura vencida viram
|
|
`atrasado` na fatura, mas não voltam a virar `inadimplente` (não faria sentido reativar
|
|
parcialmente um tenant cancelado só por causa de uma fatura antiga).
|
|
- Dashboard (`GET /master/dashboard`): `mrr` calculado somando `planos.valor_mensal` dos
|
|
tenants `ativo` em memória com `Prisma.Decimal` (não dá pra fazer um `groupBy`/`aggregate`
|
|
do Prisma somando um campo de tabela relacionada em uma única query) — evita o erro de
|
|
arredondamento de ponto flutuante que uma soma em `number` teria com valores monetários.
|
|
`total_recebido_mes`/`total_em_aberto` usam `aggregate({_sum})` diretamente. Todos os
|
|
valores voltam formatados com `.toFixed(2)`.
|
|
- Variável de ambiente nova: `TENANT_TRIAL_DIAS` (default 14, dias de trial concedidos na
|
|
criação do tenant), adicionada a `env.validation.ts` e `.env.example`, mesmo padrão das
|
|
demais variáveis opcionais com default.
|
|
- `package.json`: `test:e2e` passou a rodar com `--runInBand`. Motivo: a partir desta etapa
|
|
os testes e2e leem estado **global** do painel master (contagem de tenants por status,
|
|
MRR, faturas do mês corrente) — com múltiplos arquivos de teste (`auth.e2e-spec.ts` e o
|
|
novo `master.e2e-spec.ts`) rodando em paralelo (padrão do Jest), um teste que cria um
|
|
tenant `ativo` num arquivo poderia contaminar uma asserção de contagem global no outro.
|
|
`--runInBand` roda os arquivos em série, eliminando essa fonte de flakiness — mesmo assim,
|
|
as asserções mais sensíveis (dashboard) comparam o valor antes/depois da operação testada
|
|
(delta), em vez do valor absoluto, para não depender de nenhum estado pré-existente no
|
|
banco.
|
|
- Teste e2e novo, `test/master.e2e-spec.ts`, cobrindo todos os critérios de aceite: CRUD de
|
|
planos (validação de `valor_mensal`/limites, exclusão bloqueada por tenant em uso vs.
|
|
exclusão real quando não usado), criação de tenant com login do `tenant_admin` gerado
|
|
testado de ponta a ponta, listagem com filtro por `status`/busca, mudança de status
|
|
bloqueando tanto login novo quanto um token de acesso já emitido (via `TenantGuard`),
|
|
geração de faturas idempotente, marcar-pago revertendo `inadimplente`→`ativo` (incluindo
|
|
rejeição 409 ao tentar marcar pago de novo), atualizar-atrasadas propagando pra
|
|
`tenants.status`, dashboard (com asserções por delta) e bloqueio de todos os grupos de
|
|
endpoint com token de `tenant_user`. Rodado com `docker compose exec`-equivalente (`docker
|
|
run` num container a partir da imagem `dev` do `Dockerfile`, ligado à mesma rede do
|
|
`docker-compose.yml`, já que a imagem `dev` compila os bindings nativos do `argon2` pra
|
|
musl/Alpine — o host deste ambiente é glibc e não tem acesso de rede pra build de imagem
|
|
sem `--network host`) — `npx prisma migrate status` confirmou que as migrations da Etapa 1
|
|
já cobrem todo o schema desta etapa (`planos`/`tenants`/`tenant_faturas`/`audit_logs` já
|
|
existiam), então não houve migration nova. `npm run lint`, `npx tsc --noEmit`, `npm run
|
|
test` (unit) e `npm run build` passam sem erros; `npx jest --config ./test/jest-e2e.json
|
|
--runInBand` — 43/43 testes passando (os 21 da Etapa 2 + 1 do health + 21 novos desta
|
|
etapa). Build de produção (`docker build --target production`) também verificado, com
|
|
smoke test manual via `curl` confirmando `/health` (200) e bloqueio de `/master/planos`
|
|
sem token (401) contra a imagem `production` real.
|
|
|
|
---
|
|
|
|
## Etapa 4 — Integrações: Evolution API e E-mail
|
|
|
|
- [x] Master cadastra config da Evolution API e testa conexão
|
|
- [x] `api_key`/senha SMTP nunca aparecem em texto puro nas respostas
|
|
- [x] Tenant cria instância WhatsApp, obtém QR code, conecta
|
|
- [x] Criar instância além do limite do plano retorna erro claro
|
|
- [x] Teste de envio de WhatsApp funciona
|
|
- [x] Tenant configura SMTP próprio ou usa o padrão da plataforma
|
|
- [x] Teste de envio de e-mail funciona nos dois modos
|
|
- [x] `EncryptionService` usado de forma consistente para segredos
|
|
|
|
**Observações:**
|
|
|
|
- Módulos novos: `src/modules/evolution/` (`EvolutionController`/`EvolutionService`/`EvolutionApiClient`,
|
|
CRUD de `evolution_api_config` em `/master/evolution-config` + `testar-conexao`),
|
|
`src/modules/whatsapp/` (`WhatsappController`/`WhatsappService`, instâncias em
|
|
`/tenant/whatsapp/instancias`), `src/modules/email-config/` (`MasterEmailConfigController`/`Service`
|
|
em `/master/email-config`, `TenantEmailConfigController`/`Service` em `/tenant/email-config`,
|
|
`EmailService` com `nodemailer`) e `src/modules/alert-dispatch/` (`AlertDispatchService`, preparando a
|
|
Etapa 7). Todos os controllers master usam `@UseGuards(JwtAuthGuard, MasterGuard)` +
|
|
`PrismaMasterService`, mesmo padrão da Etapa 3; os controllers tenant usam
|
|
`@UseGuards(JwtAuthGuard, TenantGuard, RolesGuard)` + `@Roles('tenant_admin')`.
|
|
- `EncryptionService` novo em `src/common/encryption.service.ts` (AES-256-GCM, `node:crypto` nativo, sem
|
|
dependência extra), registrado em `EncryptionModule` (`@Global()`, mesmo padrão do `PrismaModule`).
|
|
Formato gravado nas colunas `*_cripto`: `iv:authTag:conteudo` (hex, `:`-separado). A chave de
|
|
`ENCRYPTION_KEY` (env, nova variável obrigatória, `@MinLength(32)` em `env.validation.ts`, mesmo padrão
|
|
de `JWT_SECRET`) é reduzida a 32 bytes via SHA-256 antes de virar a chave AES-256 — assim qualquer
|
|
string longa o suficiente serve, sem exigir um valor hex/base64 de tamanho exato. Mascaramento para
|
|
exibição (`src/common/mask.util.ts`, `maskSecret`) sempre usa largura fixa de asteriscos + últimos 4
|
|
caracteres do valor decifrado — nunca revela o tamanho real do segredo. `api_key_cripto`/
|
|
`smtp_senha_cripto` são explicitamente excluídos do objeto de resposta (destructuring) em todo
|
|
endpoint que retorna essas entidades — conferido em teste e2e (`api_key_cripto`/`smtp_senha_cripto`
|
|
sempre `undefined` no payload).
|
|
- **Decisão de arquitetura, para registrar com destaque:** `AlertDispatchService` (e, por baixo dele,
|
|
`EmailService`) usam `PrismaMasterService` com filtro manual de `tenant_id` em toda query, em vez de
|
|
`TenantContextService`/RLS — diferente do resto do código tenant desta etapa
|
|
(`WhatsappService`/`TenantEmailConfigService`, que usam `tenantContext.getTx()` normalmente, já que
|
|
`tenant_whatsapp_instances`/`tenant_email_config` têm RLS habilitado desde a migration da Etapa 2).
|
|
Motivo: `AlertDispatchService` é explicitamente "o serviço central de envio, preparando a Etapa 7" — o
|
|
worker BullMQ da Etapa 7 vai chamá-lo fora de qualquer request HTTP, sem JWT/tenant autenticado e,
|
|
portanto, sem nenhum contexto de `AsyncLocalStorage` ativo (`TenantContextService.getTx()` lançaria
|
|
"chamado fora de um contexto de tenant ativo"). Usar `PrismaMasterService` desde já evita ter que
|
|
reescrever esse service na Etapa 7. Como esse service nunca recebe `tenant_id` de input do usuário
|
|
final (sempre do JWT via controller, e futuramente do job da fila), o filtro manual é seguro, mas fica
|
|
registrado aqui porque é a única exceção ao padrão RLS-first do resto do projeto.
|
|
- **Decisão de wiring do Nest, sem impacto de comportamento:** `EmailService` (o `nodemailer`
|
|
propriamente dito) vive num módulo próprio, `EmailModule` (`src/modules/email-config/email.module.ts`),
|
|
separado do `EmailConfigModule` (CRUD). Motivo: `AlertDispatchModule` precisa do `EmailService`, e
|
|
`EmailConfigModule` precisa do `AlertDispatchService` (endpoint `testar-envio` do tenant) — um único
|
|
módulo cobrindo as duas coisas criaria um ciclo de imports (`EmailConfigModule` → `AlertDispatchModule`
|
|
→ `EmailConfigModule`). Grafo final, sem ciclos: `EvolutionModule`/`EmailModule` → `AlertDispatchModule`
|
|
→ `WhatsappModule`/`EmailConfigModule`.
|
|
- **Desvio do prompt original, pequeno e documentado:** `AlertDispatchService.enviarWhatsapp` recebeu um
|
|
quarto parâmetro opcional, `instanceId?: string` (assinatura do prompt previa só
|
|
`enviarWhatsapp(tenantId, numero, mensagem)`). Sem `instanceId`, resolve a instância `conectado` mais
|
|
recente do tenant (comportamento que a Etapa 7 vai usar). Com `instanceId`, envia especificamente por
|
|
aquela instância — usado por `POST /tenant/whatsapp/instancias/:id/testar-envio`, que por natureza
|
|
precisa testar uma instância específica (não "uma instância qualquer do tenant"), mantendo ainda assim
|
|
o pedido do prompt de "usar esse service nos endpoints de testar envio".
|
|
- `EvolutionApiClient` (`src/modules/evolution/evolution-api.client.ts`) implementado contra a
|
|
especificação pública REST da Evolution API v2 (`apikey` no header, `/instance/create`,
|
|
`/instance/connect/:name`, `/instance/connectionState/:name`, `/instance/logout|delete/:name`,
|
|
`/message/sendText/:name`) — mesma versão (`2.3`) já assumida como default no schema
|
|
(`evolution_api_config.versao`). **Não há uma instância real da Evolution API disponível neste ambiente
|
|
de desenvolvimento/CI** (sem internet dedicada a isso nem servidor de referência) para validar contra o
|
|
serviço de verdade — mitigado escrevendo, no teste e2e, um servidor HTTP real e local
|
|
(`FakeEvolutionServer`, `node:http` puro, sem dependência nova) que implementa o mesmo contrato e roda
|
|
na porta efêmera do próprio processo de teste, exercitando o `EvolutionApiClient` real ponta a ponta
|
|
(não mockado) contra um servidor de verdade. Fica como pendência natural confirmar contra uma instância
|
|
real da Evolution API antes de operar em produção — mesmo espírito da limitação de rede Docker já
|
|
registrada na Etapa 1.
|
|
- `tenant_whatsapp_instances.status` é `String` livre no schema (não enum do Prisma) — validado/mapeado a
|
|
nível de aplicação em `src/modules/whatsapp/whatsapp-status.util.ts`
|
|
(`WhatsappInstanceStatus = 'desconectado' | 'conectando' | 'conectado' | 'erro'`,
|
|
`mapEvolutionState()` traduz o `state` da Evolution — `open`/`connecting`/`close` — pros valores em
|
|
português pedidos no critério de aceite).
|
|
- Tenants não têm campo `slug` no schema. Nome de instância (`tenant-<slug>-<sufixo>`) gerado em
|
|
`src/modules/whatsapp/instance-name.util.ts` a partir de `nome_fantasia` (ou `razao_social` como
|
|
fallback), com uma tabela de substituição de acentos comuns em português (não
|
|
`normalize('NFD')`/regex de marcas diacríticas — evitado por instabilidade de encoding observada ao
|
|
gravar esse regex específico neste ambiente) + sufixo hex aleatório de 4 bytes. Unicidade é checada
|
|
antes do insert (até 5 tentativas) e reforçada pela constraint `@unique` do schema como último recurso.
|
|
- Ao criar uma instância, o tenant não escolhe qual `evolution_api_config` usar (a etapa não previu esse
|
|
fluxo) — `WhatsappService` seleciona automaticamente a config `ativo=true` mais antiga
|
|
(`orderBy: criado_em asc`) e grava o vínculo em `evolution_config_id`. Assume-se, na prática, uma única
|
|
config ativa por vez (mesmo espírito documentado no prompt para `master_email_config`: "geralmente só
|
|
um registro ativo, mas mantido como tabela"). Fica como ponto a revisitar se o produto precisar de
|
|
múltiplas Evolution APIs simultâneas roteadas por critério de negócio.
|
|
- `EmailService.resolverConfig` (usado por `AlertDispatchService.enviarEmail` e, portanto, pelos dois
|
|
endpoints de `testar-envio`) resolve o SMTP do tenant quando existe `tenant_email_config` com
|
|
`usar_config_padrao=false` (e todos os campos preenchidos — senão `BadRequestException` explícito);
|
|
caso contrário, usa o `master_email_config` `ativo=true` mais recente
|
|
(`orderBy: criado_em desc` — intencionalmente diferente do `asc` da Evolution config, para permitir
|
|
trocar o SMTP padrão da plataforma sem precisar excluir o registro antigo, só desativando o velho e
|
|
criando um novo).
|
|
- `PUT /tenant/email-config` com `usar_config_padrao=false` exige todos os campos de SMTP via
|
|
`@ValidateIf` declarativo no DTO (`UpdateTenantEmailConfigDto`); com `usar_config_padrao=true`, os
|
|
campos de SMTP são ignorados pelo service mesmo se enviados (não sobrescreve nem apaga o que já estava
|
|
salvo) — permite ao tenant alternar entre padrão/próprio sem perder a config própria já cadastrada.
|
|
`GET /tenant/email-config` some com os campos de SMTP próprio quando `usar_config_padrao=true` (só
|
|
devolve `{usar_config_padrao:true}`), como pedido no prompt ("ocultar/ignorar os campos").
|
|
- Teste e2e novo, `test/integracoes.e2e-spec.ts` (11 testes), cobrindo todos os critérios de aceite:
|
|
CRUD de `evolution_api_config` com `api_key`/`smtp_senha` nunca em texto puro (conferido tanto na
|
|
resposta HTTP quanto decifrando o valor cru do banco via `EncryptionService` e comparando com o
|
|
original), `testar-conexao` contra o `FakeEvolutionServer`, criação de instância WhatsApp + QR code +
|
|
mudança de status para `conectado`, limite do plano bloqueando a instância excedente, teste de envio de
|
|
WhatsApp nos dois cenários (sucesso e falha simulada pela Evolution API, sempre HTTP 201 com
|
|
`{sucesso, erro?}` — nunca lança erro HTTP, como pedido no prompt), remoção de instância, isolamento
|
|
por RLS entre tenants (acessar instância de outro tenant retorna 404), CRUD de `master_email_config`,
|
|
tenant usando padrão vs. SMTP próprio (incluindo rejeição 400 sem os campos obrigatórios), teste de
|
|
e-mail nos dois modos (`nodemailer` mockado via `jest.mock('nodemailer')` — sem SMTP real disponível
|
|
neste ambiente — verificando que o `from` usado bate com a config resolvida em cada modo) e bloqueio de
|
|
todos os grupos de endpoint com o token errado (master vs. tenant). Rodado com `docker compose up -d
|
|
postgres redis` + `prisma migrate deploy` + `jest --config ./test/jest-e2e.json --runInBand` apontando
|
|
pro Postgres via `localhost` (porta mapeada do compose) — **54/54 testes e2e passando** (os 43 das
|
|
Etapas 1-3 + os 11 novos). `npm run lint`, `npx tsc --noEmit`, `npm run test` (unit, incluindo novo
|
|
`src/common/encryption.service.spec.ts`) e `npm run build` também passam sem erros. Build de produção
|
|
Docker (`docker build --target production`) verificado com sucesso, com smoke test manual confirmando
|
|
todas as rotas novas mapeadas no boot e `/health` (200) / endpoints protegidos sem token (401) contra a
|
|
imagem real.
|
|
- Duas dependências novas: `nodemailer`/`@types/nodemailer` (envio de e-mail) e `axios` (cliente HTTP
|
|
para a Evolution API) — adicionadas via `npm install`, sem `@nestjs/axios` (o projeto não usa nenhum
|
|
wrapper de HTTP client até agora; `axios` puro, encapsulado só dentro de `EvolutionApiClient`, seguiu o
|
|
mesmo espírito minimalista das dependências já escolhidas nas etapas anteriores).
|
|
- Nada relacionado ao código desta etapa, mas registrado por transparência: o ambiente local usado nesta
|
|
sessão tinha `node_modules/@types`, `dist/` (dono `root`, resíduo de builds Docker rodados como root em
|
|
sessão anterior) e um binário nativo de `argon2` compilado para `musl` em `node_modules/argon2/build`
|
|
(também resíduo de build Docker anterior, teve precedência indevida sobre o prebuild `glibc` correto
|
|
deste host) bloqueando `npm install`/`tsc`/os testes locais. Corrigido com `chown` (autorizado
|
|
interativamente pelo usuário) e removendo o `build/` nativo obsoleto — nenhuma mudança de código, só
|
|
limpeza de artefatos de build. Também diferente da Etapa 1: `docker build` funcionou nesta sessão com a
|
|
rede bridge padrão, sem precisar de `--network host`.
|
|
|
|
---
|
|
|
|
## Etapa 5 — Painel Tenant: Clientes, Produtos e Serviços
|
|
|
|
- [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
|
|
|
|
- [ ] Criar venda com itens e parcelas funciona em transação única
|
|
- [ ] Soma das parcelas validada contra o valor total da venda
|
|
- [ ] Cancelar venda cancela as parcelas pendentes associadas
|
|
- [ ] Venda confirmada não editável em itens/valores
|
|
- [ ] Marcar parcela como paga reflete no dashboard financeiro
|
|
- [ ] Dashboards semanal/mensal batem com dados de teste
|
|
- [ ] Endpoint de atualizar atrasadas funciona
|
|
- [ ] Isolamento por RLS reforçado e testado nesta etapa
|
|
|
|
**Observações:**
|
|
|
|
---
|
|
|
|
## Etapa 7 — Motor de Alertas (BullMQ)
|
|
|
|
- [ ] Worker processa as duas filas independente do processo web
|
|
- [ ] Template do tenant sobrepõe padrão do master; fallback funciona
|
|
- [ ] Placeholders substituídos corretamente
|
|
- [ ] Job diário marca faturas/parcelas atrasadas corretamente
|
|
- [ ] Alerta de mensalidade chega no WhatsApp/e-mail do tenant (teste controlado)
|
|
- [ ] Alerta de cobrança chega no WhatsApp/e-mail do customer, com config correta do tenant
|
|
- [ ] Mesmo alerta não disparado duas vezes no mesmo dia
|
|
- [ ] Falha de envio reprocessada com retry; erro final registrado
|
|
- [ ] Endpoints de consulta de logs funcionam com filtros
|
|
|
|
**Observações:**
|
|
|
|
---
|
|
|
|
## Etapa 8 — Personalização Whitelabel e Ajustes Finais
|
|
|
|
- [ ] Upload/remoção de logo e alteração de cor funcionam
|
|
- [ ] Endpoint público de tema retorna logo/cor sem autenticação
|
|
- [ ] CORS, Helmet e rate limiting revisados e ativos
|
|
- [ ] Nenhum segredo aparece em log ou resposta de API
|
|
- [ ] Swagger disponível cobrindo todos os módulos, com guards indicados
|
|
- [ ] Build de produção do Docker sem dependências de desenvolvimento
|
|
- [ ] Teste E2E do fluxo completo passa do início ao fim
|
|
- [ ] README final consolidado (dev, deploy, migrations, RLS)
|
|
|
|
**Observações:**
|
|
|
|
---
|
|
|
|
## Histórico de decisões e desvios do plano original
|
|
|
|
Use esta seção para registrar qualquer decisão tomada durante a
|
|
implementação que se desvie do que foi descrito nos prompts originais
|
|
(ex: mudança de biblioteca, ajuste de schema, corte de escopo
|
|
combinado com o usuário). Formato sugerido:
|
|
|
|
```
|
|
[DATA] Etapa X — descrição da decisão/desvio e motivo
|
|
```
|
|
|
|
[2026-08-12] Etapa 1 — `@types/supertest` atualizado de `^6.0.2` para `^7.0.3`
|
|
para casar com a versão de `supertest`/`superagent` de fato instalada pelo npm;
|
|
sem isso `npm run lint` falhava com erros de tipo em `test/health.e2e-spec.ts`.
|
|
Nenhum comportamento de runtime muda, só a versão dos `@types`.
|
|
|
|
[2026-08-13] Etapa 2 — Adicionada uma terceira connection string,
|
|
`DATABASE_URL_TENANT` (role `app_tenant`, sem `BYPASSRLS`), além da
|
|
`DATABASE_URL_MASTER` já prevista no prompt original. Motivo: a role de
|
|
`DATABASE_URL` (`seconectai`) é a superuser bootstrap do Postgres (criada
|
|
pelo `POSTGRES_USER` do docker-compose desde a Etapa 1) — e superusers
|
|
sempre ignoram Row Level Security, mesmo com as policies criadas na Etapa
|
|
1. Sem uma role de aplicação não-superuser dedicada (`app_tenant`), toda
|
|
query de `tenant_user` continuaria vendo dados de todos os tenants e o
|
|
isolamento por RLS seria só decorativo. As duas roles (`app_master` e
|
|
`app_tenant`) são criadas na migration manual
|
|
`prisma/migrations/20260813120000_auth_app_roles/`, com senhas
|
|
placeholder de dev documentadas em `.env.example` (mesmo padrão já usado
|
|
para `POSTGRES_PASSWORD`/`JWT_SECRET` — trocar em produção).
|
|
|
|
[2026-08-13] Etapa 2 — `email` de `tenant_users` é único só por tenant, não
|
|
globalmente (`UNIQUE(tenant_id, email)` no schema original). Login e
|
|
forgot-password por e-mail, sem mais contexto (ex: subdomínio/slug do
|
|
tenant), não têm como resolver com segurança um e-mail que colida entre
|
|
dois tenants diferentes. Decisão: tratar 0 ou >1 `tenant_users` com o
|
|
mesmo e-mail como credencial inválida, em vez de arriscar autenticar/
|
|
resetar a senha da conta errada. Fica como limitação conhecida — uma
|
|
etapa futura pode introduzir um identificador de tenant explícito no
|
|
login (subdomínio, slug) para eliminar essa ambiguidade.
|
|
|
|
[2026-08-13] Etapa 3 — `TenantGuard` passou a checar o status do tenant a
|
|
cada request (consulta a `tenants` via `PrismaMasterService`), não só no
|
|
login. O prompt desta etapa já pedia isso explicitamente ("checar status
|
|
do tenant no login e no TenantGuard"), mas vale registrar: sem essa
|
|
checagem, suspender/cancelar um tenant não bloqueava de fato quem já
|
|
tinha um access token válido (até 15 min de janela, com a config
|
|
padrão). `changeStatus` também revoga os refresh tokens ativos dos
|
|
usuários do tenant ao suspender/cancelar, como defesa em profundidade
|
|
extra (não pedida explicitamente, mesmo espírito da revogação de família
|
|
de tokens da Etapa 2).
|
|
|
|
[2026-08-13] Etapa 3 — `status` retirado do `UpdateTenantDto` (`PUT
|
|
/master/tenants/:id`), mesmo o prompt original listando `status` entre os
|
|
campos que esse endpoint atualiza. Toda transição de status passa
|
|
exclusivamente por `PATCH /master/tenants/:id/status`, o único caminho
|
|
que grava auditoria (`de`/`para`) e aplica bloqueio de acesso — permitir
|
|
mudar `status` também via PUT abriria um caminho para contornar essa
|
|
lógica.
|
|
|
|
[2026-08-13] Etapa 3 — `package.json`: `test:e2e` passou a rodar com
|
|
`--runInBand`. A partir desta etapa os testes e2e leem estado global do
|
|
painel master (contagem de tenants por status, MRR, faturas do mês
|
|
corrente); com mais de um arquivo de teste, o Jest roda os arquivos em
|
|
paralelo por padrão, o que criaria uma fonte real de flakiness entre
|
|
`auth.e2e-spec.ts` e `master.e2e-spec.ts`. Rodar em série elimina isso.
|
|
|
|
[2026-08-14] Etapa 4 — `AlertDispatchService` (e o `EmailService`/
|
|
`EvolutionApiClient` por baixo dele) usam `PrismaMasterService` com filtro
|
|
manual de `tenant_id`, não `TenantContextService`/RLS — única exceção ao
|
|
padrão RLS-first do resto do projeto. Motivo: este service é o "serviço
|
|
central de envio, preparando a Etapa 7" e vai ser chamado pelo worker
|
|
BullMQ da Etapa 7 fora de qualquer request HTTP/contexto de tenant
|
|
autenticado, onde `TenantContextService.getTx()` não tem como funcionar
|
|
(não há `AsyncLocalStorage` ativo sem uma request em andamento). `tenant_id`
|
|
nunca vem de input do usuário final nesse service (sempre do JWT via
|
|
controller, e futuramente do job da fila), então o filtro manual continua
|
|
seguro.
|
|
|
|
[2026-08-14] Etapa 4 — `AlertDispatchService.enviarWhatsapp` ganhou um
|
|
quarto parâmetro opcional, `instanceId?: string`, além dos três previstos
|
|
no prompt (`tenantId, numero, mensagem`). Sem ele, resolve a instância
|
|
`conectado` mais recente do tenant (uso futuro da Etapa 7); com ele, envia
|
|
por uma instância específica — necessário para
|
|
`POST /tenant/whatsapp/instancias/:id/testar-envio`, que testa uma
|
|
instância específica, não "uma instância qualquer do tenant", mantendo
|
|
ainda assim o pedido do prompt de reusar esse service nos endpoints de
|
|
teste de envio.
|
|
|
|
[2026-08-14] Etapa 4 — `ENCRYPTION_KEY` (nova variável de ambiente
|
|
obrigatória) é reduzida a 32 bytes via SHA-256 antes de virar a chave
|
|
AES-256-GCM usada pelo `EncryptionService`, em vez de exigir um valor
|
|
hex/base64 de exatamente 32 bytes. Mesma lógica de validação de
|
|
`JWT_SECRET` (`@MinLength(32)`, qualquer string forte serve) — evita o
|
|
usuário ter que gerar/colar uma chave num formato binário específico.
|
|
|
|
[2026-08-14] Etapa 4 — Não há instância real da Evolution API disponível
|
|
neste ambiente de desenvolvimento/CI. `EvolutionApiClient` foi implementado
|
|
contra a especificação pública documentada da API v2 e validado
|
|
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.
|