first commit

This commit is contained in:
2026-08-14 17:35:24 -03:00
commit 04472fcd2b
118 changed files with 19173 additions and 0 deletions
+57
View File
@@ -0,0 +1,57 @@
# SeConectai Vendas — Prompts de Build Faseado
Este pacote contém 8 prompts sequenciais e autocontidos para entregar a uma
IA de geração de código (Claude Code, Cursor, Windsurf etc). Cada arquivo
pode ser colado como um prompt único — eles já trazem o contexto necessário
para não depender de memória de conversa anterior.
## Ordem obrigatória
1. `01-setup-projeto.md` — estrutura base, Docker Compose, Postgres, Prisma
2. `02-autenticacao-rls.md` — login master/tenant, JWT, RLS
3. `03-painel-master-tenants.md` — CRUD de tenants, planos, faturas
4. `04-integracoes-evolution-email.md` — Evolution API, instâncias WhatsApp, SMTP
5. `05-painel-tenant-customers-produtos.md` — CRUD de clientes e produtos/serviços
6. `06-vendas-dashboards.md` — vendas, parcelas, resumos
7. `07-motor-de-alertas.md` — BullMQ, templates, disparo e logs
8. `08-personalizacao-whitelabel.md` — logo/cor por tenant, ajustes finais
## Arquivo de controle de progresso (obrigatório)
`09-controle-de-progresso.md` é o estado oficial do projeto. Regras:
- **Sempre envie esse arquivo junto com o prompt de cada etapa.**
- A IA deve lê-lo antes de começar e recusar iniciar uma etapa se a
anterior não estiver 100% concluída na checklist.
- Ao final de cada etapa, a IA deve devolver o arquivo atualizado (itens
marcados, status geral e observações preenchidas) — é esse arquivo
atualizado que você guarda e reenvia na próxima etapa/sessão.
- Sem esse arquivo, uma IA nova (em outra sessão) não tem como saber o
que já foi feito — por isso ele substitui a necessidade de repetir todo
o histórico da conversa.
## Como usar cada prompt
- Rode nessa ordem — cada etapa assume que a anterior está pronta e
funcionando (migrations aplicadas, testes passando).
- Ao final de cada etapa, confira o checklist de "Critérios de aceite"
antes de avançar para a próxima. Se algo não for atendido, peça correção
antes de seguir.
- O arquivo `seconectai-vendas-schema.sql` é a fonte de verdade do modelo
de dados — qualquer etapa que crie migration deve refletir exatamente
essas tabelas (a IA pode adaptar para sintaxe Prisma, mas a estrutura,
nomes de campo e tipos devem ser preservados).
- Cada prompt já repete o essencial do contexto (stack, convenções), então
eles funcionam mesmo em uma conversa nova/sessão nova com a IA.
## Stack fixada em todas as etapas
- Node.js 20+ / TypeScript
- NestJS (modular por domínio)
- PostgreSQL 16 + Prisma ORM
- Redis + BullMQ (a partir da etapa 7)
- Docker + Docker Compose
- Autenticação: JWT (access curto) + refresh token com rotação, senha com
argon2id
- Multi-tenancy: banco único, isolamento por `tenant_id` + Row Level
Security do Postgres
+115
View File
@@ -0,0 +1,115 @@
# Etapa 1/8 — Setup do Projeto
## Contexto do projeto
Estou construindo o **SeConectai Vendas**: um CRM/controle de vendas
multi-tenant. Existe um painel **master** (eu, dono da plataforma, que
vendo o acesso ao sistema) e painéis de **tenant** (meus clientes, que
usam o sistema para cadastrar os próprios clientes/vendas).
Stack definida:
- Node.js 20+ / TypeScript
- NestJS (organizado em módulos por domínio)
- PostgreSQL 16 + Prisma ORM
- Redis (usado a partir de uma etapa futura, mas já deve subir no compose)
- Docker + Docker Compose para todo o ambiente (dev e produção)
- Multi-tenancy: banco único, isolamento por `tenant_id` + Row Level
Security (RLS) do Postgres
Esta é a **Etapa 1 de 8** de um build faseado. Cada etapa será entregue
separadamente. Nesta etapa, quero **apenas a fundação do projeto** —
nenhuma feature de negócio ainda.
## Objetivo desta etapa
Criar a estrutura base do repositório, ambiente Docker funcional, conexão
com banco de dados via Prisma, e o schema inicial do banco já migrado.
## Tarefas
1. **Estrutura do projeto NestJS**
- Criar projeto NestJS em TypeScript com estrutura modular:
`src/modules/<dominio>/...` (os domínios virão nas próximas etapas —
por ora, criar só `src/modules/health/` com um endpoint
`GET /health` que retorna status da API e status da conexão com o
banco).
- Configurar ESLint + Prettier com regras razoáveis para TypeScript.
- Configurar variáveis de ambiente via `.env` (usar `@nestjs/config`),
com um `.env.example` documentando todas as chaves necessárias.
2. **Docker Compose**
- Criar `docker-compose.yml` com os serviços:
- `app` (build a partir de um `Dockerfile` multi-stage — stage de
build e stage de produção enxuto)
- `postgres` (Postgres 16, volume nomeado para persistência, healthcheck)
- `redis` (Redis 7, volume nomeado, healthcheck) — não será usado
ainda, mas deixar disponível
- Criar `docker-compose.override.yml` (ou perfil `dev`) com hot-reload
do backend para desenvolvimento local.
- Garantir que `docker compose up` sobe tudo e a API responde em
`GET /health` com status 200.
3. **Banco de dados e Prisma**
- Inicializar Prisma (`prisma/schema.prisma`) e traduzir **exatamente**
o schema SQL abaixo para o formato Prisma, preservando nomes de
tabelas/campos, tipos, enums, defaults, unique constraints e índices.
- Não implemente ainda a lógica de RLS na aplicação (isso é da Etapa
2) — mas a migration gerada deve incluir os comandos
`ALTER TABLE ... ENABLE ROW LEVEL SECURITY` e as `CREATE POLICY`
definidos no schema, via uma migration SQL raw do Prisma (Prisma não
suporta RLS nativamente, então usar `prisma migrate dev` seguido de
uma migration manual `.sql` para as policies).
- Rodar a migration inicial e confirmar que todas as tabelas, enums,
views e policies foram criadas corretamente no Postgres do
`docker-compose`.
Schema de referência (colar aqui o conteúdo completo do arquivo
`seconectai-vendas-schema.sql` fornecido separadamente — use-o como
fonte de verdade).
4. **Scripts utilitários**
- `package.json` com scripts: `dev`, `build`, `start:prod`,
`lint`, `test`, `prisma:migrate`, `prisma:generate`, `prisma:studio`.
- `README.md` inicial explicando como subir o projeto localmente
(`docker compose up`, variáveis de ambiente necessárias, como rodar
migrations).
5. **Testes**
- Configurar Jest (já vem com NestJS) e criar um teste simples do
endpoint `/health`.
## Estrutura de pastas esperada
```
seconectai-vendas/
├── docker-compose.yml
├── docker-compose.override.yml
├── Dockerfile
├── .env.example
├── package.json
├── prisma/
│ ├── schema.prisma
│ └── migrations/
├── src/
│ ├── main.ts
│ ├── app.module.ts
│ ├── config/
│ └── modules/
│ └── health/
│ ├── health.module.ts
│ ├── health.controller.ts
│ └── health.service.ts
└── test/
```
## Critérios de aceite
- [ ] `docker compose up` sobe `app`, `postgres` e `redis` sem erro
- [ ] `GET /health` retorna 200 com status da API e status da conexão com
o banco
- [ ] Todas as tabelas, enums, views e índices do schema fornecido existem
no banco após a migration (conferir com `\dt` e `\dv` no psql)
- [ ] As policies de RLS estão criadas (`\d+ customers` mostra a policy)
- [ ] `.env.example` documenta todas as variáveis necessárias
- [ ] `npm run lint` e `npm run test` rodam sem erro
- [ ] README explica como subir o ambiente do zero
+115
View File
@@ -0,0 +1,115 @@
# Etapa 2/8 — Autenticação, Autorização e Row Level Security
## Contexto do projeto
Continuação do **SeConectai Vendas** (CRM/vendas multi-tenant). A Etapa 1
já entregou: projeto NestJS + Docker Compose + Postgres/Prisma com o
schema completo migrado (incluindo as tabelas `master_admins`,
`tenant_users`, `auth_refresh_tokens`, `password_reset_tokens`,
`login_attempts` e as policies de RLS).
Existem dois tipos de usuário:
- **master_admins**: você, dono da plataforma — acesso a todos os tenants.
- **tenant_users**: usuários de cada tenant (cliente seu), com papéis
(`tenant_admin`, `vendedor`, `financeiro`, `visualizador`), restritos
aos próprios dados via RLS.
## Objetivo desta etapa
Implementar login, emissão/renovação de tokens, reset de senha,
mecanismo de definição do `tenant_id` da sessão no Postgres (RLS) e
proteção contra força bruta — para ambos os tipos de usuário.
## Tarefas
1. **Módulo de autenticação (`src/modules/auth/`)**
- `POST /auth/master/login` — email + senha → valida contra
`master_admins.senha_hash` (argon2id) → retorna access token (JWT,
expiração curta, ex: 15 min) + refresh token (opaco, guardado como
hash em `auth_refresh_tokens`, expiração longa, ex: 30 dias).
- `POST /auth/tenant/login` — email + senha + (implícito) tenant
resolvido pelo email único dentro do tenant → mesma lógica, usando
`tenant_users`.
- `POST /auth/refresh` — recebe refresh token, valida hash e
expiração/revogação, **rotaciona** (invalida o antigo, emite um
novo) e retorna novo par de tokens.
- `POST /auth/logout` — revoga o refresh token atual.
- `POST /auth/password/forgot` — gera token em
`password_reset_tokens` (hash, expiração curta, ex: 1h) e dispara
e-mail (por ora, apenas logar o link/token no console — o envio real
de e-mail é de uma etapa futura).
- `POST /auth/password/reset` — valida token, atualiza `senha_hash`,
marca token como usado, revoga todos os refresh tokens ativos do
usuário.
2. **JWT payload**
- Incluir no access token: `sub` (id do usuário), `tipo`
(`master`|`tenant_user`), `tenant_id` (null para master), `role`
(para tenant_user).
- Implementar `JwtStrategy` (Passport) validando assinatura e
expiração.
3. **Guards e decorators**
- `@CurrentUser()` — decorator para extrair o usuário autenticado do
request.
- `MasterGuard` — permite apenas `tipo === 'master'`.
- `TenantGuard` — permite apenas `tipo === 'tenant_user'`, e injeta
`tenant_id` no contexto da request.
- `RolesGuard` + `@Roles('tenant_admin', ...)` — restringe endpoints
por papel dentro do tenant.
4. **Aplicação do RLS via Prisma**
- Implementar um mecanismo (middleware/interceptor do Nest, executado
por request) que, para requests autenticadas como `tenant_user`,
abre uma transação Prisma e executa
`SET LOCAL app.current_tenant_id = '<tenant_id>'` antes de qualquer
query — garantindo que a policy de RLS do Postgres filtre os dados
automaticamente.
- Para requests como `master`, usar uma conexão/role de banco com
`BYPASS ROW LEVEL SECURITY` (criar essa role na migration, se ainda
não existir, com `CREATE ROLE app_master BYPASSRLS LOGIN ...` —
documentar isso no `.env.example` como uma segunda connection string
opcional, `DATABASE_URL_MASTER`).
- Escrever um teste de integração que prove o isolamento: criar dois
tenants, um usuário em cada, confirmar que a query de um nunca
retorna dado do outro mesmo manipulando o `tenant_id` de outras
formas (ex: tentando passar um `tenant_id` diferente no body de uma
requisição — o `tenant_id` da sessão deve **sempre** vir do token,
nunca de input do usuário).
5. **Proteção contra força bruta**
- Toda tentativa de login (sucesso ou falha) grava em
`login_attempts`.
- Após N tentativas falhas (configurável, ex: 5) para o mesmo e-mail
em uma janela de tempo (ex: 15 min), bloquear novas tentativas com
429 e mensagem clara, mesmo que a senha esteja correta.
- Rate limiting geral no endpoint de login (ex: `@nestjs/throttler`).
6. **Segurança de senha**
- Hash com argon2id (biblioteca `argon2`), parâmetros de custo
documentados no código.
- Validação de força mínima da senha no cadastro/reset (tamanho
mínimo, não permitir senhas triviais).
- Refresh tokens e reset tokens **nunca** armazenados em texto puro —
sempre hash (ex: sha256) antes de gravar no banco.
## Critérios de aceite
- [ ] Login master e login tenant funcionam e retornam access+refresh
token válidos
- [ ] Refresh token rotaciona corretamente; token antigo usado após
rotação é rejeitado
- [ ] Logout revoga o refresh token
- [ ] Fluxo de esqueci minha senha gera token, valida expiração/uso único
- [ ] `MasterGuard` e `TenantGuard` bloqueiam corretamente rotas erradas
(testar com token de tenant tentando acessar rota master e
vice-versa)
- [ ] `RolesGuard` bloqueia usuário `vendedor` tentando acessar rota
restrita a `tenant_admin`
- [ ] Teste de integração prova isolamento entre tenants via RLS (dado de
um tenant nunca vaza para outro, mesmo forçando `tenant_id` no
payload da requisição)
- [ ] Após 5 tentativas de login falhas, novas tentativas são bloqueadas
temporariamente
- [ ] Todas as senhas no banco estão como hash argon2id (nenhuma em texto
puro em nenhuma tabela)
+98
View File
@@ -0,0 +1,98 @@
# Etapa 3/8 — Painel Master: Tenants, Planos e Faturas
## Contexto do projeto
Continuação do **SeConectai Vendas**. Etapas anteriores entregaram: setup
do projeto (Docker/Prisma/schema) e autenticação completa (login
master/tenant, JWT, RLS, guards `MasterGuard`/`TenantGuard`/`RolesGuard`).
Esta etapa implementa a parte do **painel master** relacionada à gestão
comercial da própria plataforma: cadastro dos tenants (seus clientes),
planos vendidos, e cobrança da mensalidade deles.
Todos os endpoints desta etapa são protegidos por `MasterGuard`.
## Objetivo desta etapa
CRUD completo de `planos` e `tenants`, geração e controle de
`tenant_faturas` (mensalidades), e endpoints de consulta para o dashboard
do master (quantos tenants ativos, inadimplentes, faturamento previsto
etc).
## Tarefas
1. **Módulo `planos` (`src/modules/planos/`)**
- CRUD completo (`GET/POST/PUT/DELETE /master/planos`).
- Validação: `valor_mensal` > 0; limites (`limite_customers`,
`limite_usuarios`, `limite_instancias_whatsapp`) opcionais, mas se
informados devem ser > 0.
- Não permitir excluir plano em uso por algum tenant ativo — apenas
desativar (`ativo = false`).
2. **Módulo `tenants` (`src/modules/tenants/`)**
- `POST /master/tenants` — cria tenant com status inicial `trial`,
associa a um plano, define `trial_expira_em` (ex: hoje + 14 dias,
configurável), e **cria automaticamente o primeiro usuário
`tenant_admin`** desse tenant (nome, e-mail e senha provisória
informados no payload, ou senha gerada e enviada por e-mail — por
ora, retornar a senha provisória na resposta, o envio de e-mail é de
etapa futura).
- `GET /master/tenants` — listagem com filtros por `status`, busca por
nome/documento, paginação.
- `GET /master/tenants/:id` — detalhe do tenant, incluindo plano,
últimas faturas, quantidade de usuários/customers/vendas
(agregados).
- `PUT /master/tenants/:id` — atualizar dados cadastrais, plano,
status.
- `PATCH /master/tenants/:id/status` — transição explícita de status
(`ativo`, `suspenso`, `cancelado`, `inadimplente`), registrando em
`audit_logs`. Regra: mudar para `suspenso` ou `cancelado` deve
também **bloquear login** dos `tenant_users` daquele tenant (checar
status do tenant no login e no `TenantGuard`).
3. **Módulo `tenant_faturas` (dentro de `tenants` ou módulo próprio)**
- Geração automática mensal: um job (pode ser um endpoint manual por
enquanto, `POST /master/faturas/gerar-mes`, já preparando estrutura
para virar cron na Etapa 7) que cria, para cada tenant `ativo`, uma
linha em `tenant_faturas` referente ao mês corrente, com
`vencimento` calculado a partir de `dia_vencimento_mensalidade`,
evitando duplicar (respeitar o `UNIQUE(tenant_id, referencia)`).
- `GET /master/faturas` — listagem com filtros por status, tenant,
período.
- `PATCH /master/faturas/:id/marcar-pago` — marca fatura como paga
(`pago_em = now()`), e se o tenant estava `inadimplente`, volta para
`ativo`.
- Regra: se uma fatura passa do vencimento sem pagamento, marcar como
`atrasado` (pode ser endpoint manual por ora, `POST
/master/faturas/atualizar-atrasadas`, que também vira job na Etapa
7) e mudar `tenants.status` para `inadimplente`.
4. **Dashboard do master**
- `GET /master/dashboard` retornando: total de tenants por status,
MRR (soma de `valor_mensal` dos planos dos tenants ativos), total
recebido no mês corrente, total em aberto (`tenant_faturas` com
status `pendente` ou `atrasado`).
5. **Auditoria**
- Toda criação/alteração de tenant, plano ou fatura grava uma linha em
`audit_logs` (`usuario_tipo='master'`, `acao`, `entidade`,
`entidade_id`, `detalhes` em JSON com o diff relevante).
## Critérios de aceite
- [ ] CRUD de planos funcional e validado
- [ ] Criar tenant também cria o primeiro `tenant_admin` funcional (login
testado)
- [ ] Listagem de tenants com filtro por status e busca funciona
- [ ] Mudar status do tenant para `suspenso`/`cancelado` bloqueia login
dos usuários daquele tenant (testar tentativa de login após
suspensão)
- [ ] Geração de faturas do mês não duplica fatura já existente para o
mesmo tenant/referência
- [ ] Marcar fatura como paga reverte tenant `inadimplente` → `ativo`
- [ ] Marcar fatura como atrasada muda tenant para `inadimplente`
- [ ] Dashboard retorna números corretos (conferir manualmente com dados
de teste)
- [ ] Todas as ações relevantes aparecem em `audit_logs`
- [ ] Todos os endpoints desta etapa são inacessíveis com token de
tenant_user (testar)
+85
View File
@@ -0,0 +1,85 @@
# Etapa 4/8 — Integrações: Evolution API (WhatsApp) e E-mail
## Contexto do projeto
Continuação do **SeConectai Vendas**. Já existem: setup do projeto, auth
completa com RLS, e o CRUD de tenants/planos/faturas no painel master.
Esta etapa conecta a infraestrutura de mensageria: cadastro da Evolution
API (nível master), criação de instâncias de WhatsApp por tenant, e
configuração de SMTP (padrão da plataforma e por tenant). **O disparo
automático de alertas fica para a Etapa 7** — aqui o objetivo é deixar a
infraestrutura de envio pronta e testável manualmente.
## Tarefas
1. **Módulo `evolution` — nível master (`src/modules/evolution/`)**
- CRUD de `evolution_api_config` (`POST/GET/PUT/DELETE
/master/evolution-config`), protegido por `MasterGuard`.
- **Nunca** retornar `api_key_cripto` em texto puro nas respostas da
API — mascarar (ex: mostrar só os últimos 4 caracteres).
- Implementar criptografia AES-256-GCM para `api_key_cripto` (chave
vinda de variável de ambiente `ENCRYPTION_KEY`, nunca hardcoded).
Criar um `EncryptionService` reutilizável (vai servir também para
`smtp_senha_cripto`).
- `POST /master/evolution-config/:id/testar-conexao` — faz uma chamada
simples à Evolution API (ex: endpoint de status/versão) para
validar que a `base_url`/`api_key` cadastradas funcionam.
2. **Módulo `whatsapp` — nível tenant**
- `POST /tenant/whatsapp/instancias` (`TenantGuard`, role
`tenant_admin`) — cria uma instância na Evolution API (nome único
sugerido automaticamente, ex: `tenant-<slug>-<uuid curto>`), grava
em `tenant_whatsapp_instances` com status `desconectado`.
- `GET /tenant/whatsapp/instancias/:id/qrcode` — solicita o QR code
de conexão à Evolution API e retorna para o front exibir; atualiza
`qr_code_gerado_em`.
- `GET /tenant/whatsapp/instancias/:id/status` — consulta status atual
na Evolution API e sincroniza com o campo `status` local
(desconectado/conectando/conectado/erro).
- `DELETE /tenant/whatsapp/instancias/:id` — desconecta e remove a
instância na Evolution API e localmente.
- `POST /tenant/whatsapp/instancias/:id/testar-envio` — envia uma
mensagem de teste para um número informado, útil para o tenant
validar a conexão antes de confiar no sistema.
- Respeitar `limite_instancias_whatsapp` do plano do tenant ao criar
nova instância (retornar erro claro se o limite foi atingido).
3. **Módulo `email-config`**
- `master_email_config`: CRUD simples (`MasterGuard`) — geralmente só
um registro ativo, mas manter como tabela para permitir trocar
provedor sem downtime.
- `tenant_email_config`: `GET/PUT /tenant/email-config`
(`TenantGuard`, role `tenant_admin`) — se `usar_config_padrao=true`,
ocultar/ignorar os campos de SMTP próprio; se `false`, exigir os
campos de SMTP preenchidos.
- `POST /tenant/email-config/testar-envio` — envia e-mail de teste
para um endereço informado, usando a config resolvida (própria ou
padrão).
- Implementar `EmailService` central que resolve automaticamente qual
SMTP usar (do tenant ou o padrão) e usa `nodemailer`.
4. **Serviço central de envio (preparando a Etapa 7)**
- Criar `AlertDispatchService` com dois métodos:
`enviarWhatsapp(tenantId, numero, mensagem)` e
`enviarEmail(tenantId, destinatario, assunto, corpo)`. Ambos apenas
executam o envio (sem lógica de "quando enviar" — isso é da Etapa
7) e retornam sucesso/erro. Usar esse service nos endpoints de
"testar envio" acima, para já validar que funciona ponta a ponta.
## Critérios de aceite
- [ ] Master consegue cadastrar config da Evolution API e testar conexão
- [ ] `api_key` e senha SMTP nunca aparecem em texto puro em nenhuma
resposta de API (conferir payloads)
- [ ] Tenant consegue criar instância WhatsApp, obter QR code, escanear e
ver status mudar para `conectado`
- [ ] Criar instância além do limite do plano retorna erro claro
- [ ] Teste de envio de WhatsApp funciona (mensagem chega no número de
teste)
- [ ] Tenant consegue configurar SMTP próprio ou usar o padrão da
plataforma
- [ ] Teste de envio de e-mail funciona nos dois modos (SMTP próprio e
padrão)
- [ ] `EncryptionService` usado de forma consistente para todos os
segredos armazenados
@@ -0,0 +1,69 @@
# Etapa 5/8 — Painel Tenant: Clientes, Produtos e Serviços
## Contexto do projeto
Continuação do **SeConectai Vendas**. Já existem: setup, auth com RLS,
painel master (tenants/planos/faturas), e integrações de WhatsApp/e-mail.
Esta etapa é o núcleo operacional do **painel do tenant**: cadastro dos
clientes dele (empresas que ele atende) e do catálogo de produtos e
serviços que ele vende. Todos os endpoints usam `TenantGuard` — o
`tenant_id` vem sempre do token, nunca de input do usuário.
## Tarefas
1. **Módulo `customers` (`src/modules/customers/`)**
- CRUD completo (`GET/POST/PUT/DELETE /tenant/customers`), com
`TenantGuard`.
- Campos obrigatórios: `razao_social`. Demais opcionais conforme
schema (`documento`, `email`, `telefone_whatsapp`, endereço etc).
- Validar `documento` como CPF ou CNPJ (formato, não precisa validar
dígito verificador na primeira versão — deixar comentário no código
indicando que pode evoluir).
- `GET /tenant/customers` — listagem com busca por nome/documento,
filtro por `ativo`, paginação.
- Respeitar `limite_customers` do plano do tenant (bloquear criação
além do limite, com mensagem clara).
- Soft delete: `DELETE` não remove a linha, apenas seta `ativo=false`
(histórico de vendas depende desses registros).
2. **Submódulo `customer_contacts`**
- CRUD aninhado em `customers` (`/tenant/customers/:customerId/contatos`).
- Regra: apenas um contato pode ser `principal=true` por customer —
ao marcar um novo como principal, desmarcar o anterior
automaticamente (numa transação).
3. **Módulo `products_services` (`src/modules/products/`)**
- CRUD completo (`GET/POST/PUT/DELETE /tenant/produtos`), com
`TenantGuard`.
- `tipo` obrigatório (`produto` ou `servico`); `valor_venda`
obrigatório e > 0.
- `sku` único por tenant quando informado (respeitar constraint do
banco, retornar erro 409 amigável em caso de conflito).
- `GET /tenant/produtos` — listagem com filtro por `tipo`, `ativo`,
busca por nome/sku.
- Soft delete igual a customers (`ativo=false`) — produto usado em
venda antiga não pode sumir do histórico.
4. **Testes de RLS aplicados a esta etapa**
- Confirmar (teste automatizado) que um tenant não consegue, por
nenhum endpoint criado aqui, ler/editar/excluir customer ou produto
de outro tenant — mesmo sabendo o UUID exato do registro alheio
(deve retornar 404, não 403, para não vazar existência do recurso).
## Critérios de aceite
- [ ] CRUD de customers funcional, com soft delete e busca
- [ ] Criar customer além do `limite_customers` do plano retorna erro
claro
- [ ] CRUD de contatos aninhado funciona, e a regra de contato principal
único é respeitada
- [ ] CRUD de produtos/serviços funcional, com soft delete e busca
- [ ] SKU duplicado no mesmo tenant retorna erro 409 amigável; SKU igual
em tenants diferentes é permitido
- [ ] Teste automatizado confirma que tentar acessar customer/produto de
outro tenant retorna 404
- [ ] Todos os endpoints desta etapa são inacessíveis com token master
"puro" sem contexto de tenant (ou documentar explicitamente se o
master deve conseguir acessar em modo de suporte — decisão a
confirmar antes de implementar)
+85
View File
@@ -0,0 +1,85 @@
# Etapa 6/8 — Vendas, Parcelas e Dashboards
## Contexto do projeto
Continuação do **SeConectai Vendas**. Já existem: setup, auth com RLS,
painel master, integrações Evolution/e-mail, e CRUD de customers e
produtos/serviços no painel do tenant.
Esta etapa implementa o fluxo de vendas propriamente dito: registrar uma
venda com itens, gerar parcelas a receber, e expor os dashboards de
acompanhamento (resumo semanal/mensal, total recebido, total a receber).
## Tarefas
1. **Módulo `sales` (`src/modules/sales/`)**
- `POST /tenant/vendas` — cria venda (`sales`) com seus itens
(`sale_items`) em uma única transação:
- Recebe `customer_id`, lista de itens
(`product_service_id`, `quantidade`, `valor_unitario` — permitir
sobrescrever o valor do catálogo caso tenha desconto pontual),
`desconto` geral, `observacoes`, `data_venda`.
- Calcula `valor_total` de cada item e da venda (soma dos itens
menos desconto).
- Recebe também a definição das parcelas (`sale_payments`): número
de parcelas, ou lista explícita de `{valor, vencimento}` — validar
que a soma das parcelas bate com `valor_total`.
- Validar que `customer_id` e todos os `product_service_id`
pertencem ao tenant autenticado (RLS já ajuda, mas validar
explicitamente para retornar erro de negócio claro em vez de erro
de banco).
- `GET /tenant/vendas` — listagem com filtros por `status`,
`customer_id`, período (`data_venda`), paginação.
- `GET /tenant/vendas/:id` — detalhe completo (itens + parcelas).
- `PATCH /tenant/vendas/:id/status` — transição de status
(`orcamento` → `confirmada` → `concluida`, ou `cancelada`).
Cancelar uma venda deve cancelar também as parcelas pendentes
(`sale_payments.status = 'cancelado'`).
- `PUT /tenant/vendas/:id` — editar venda em status `orcamento`
apenas (venda `confirmada` não deve ser editável em itens/valores —
só cancelada e recriada, para preservar histórico financeiro).
2. **Módulo `sale_payments` (parcelas)**
- `GET /tenant/recebimentos` — listagem de parcelas com filtros por
`status`, período de vencimento, `customer_id`; usar a view
`vw_recebimentos` como base.
- `PATCH /tenant/recebimentos/:id/marcar-pago` — marca parcela como
paga (`pago_em = now()`, `forma_pagamento`).
- Endpoint (preparando Etapa 7): `POST
/tenant/recebimentos/atualizar-atrasadas` — marca como `atrasado`
toda parcela `pendente` com `vencimento` no passado (vira job
agendado na Etapa 7, aqui só o endpoint manual).
3. **Dashboard do tenant**
- `GET /tenant/dashboard/resumo-semanal` e
`GET /tenant/dashboard/resumo-mensal` — usando as views
`vw_resumo_vendas_semanal` / `vw_resumo_vendas_mensal`, com filtro
de período opcional.
- `GET /tenant/dashboard/financeiro` — retorna: total recebido no mês
corrente, total a receber (pendente + atrasado), total atrasado
isolado (para destacar visualmente), próximas N parcelas a vencer.
4. **Regras de consistência**
- Toda alteração relevante em `sales`/`sale_payments` grava em
`audit_logs` (`usuario_tipo='tenant_user'`, incluindo `usuario_id` e
`tenant_id`).
- Usar transações do Prisma em toda operação que mexe em mais de uma
tabela (venda + itens + parcelas) para evitar estado inconsistente.
## Critérios de aceite
- [ ] Criar venda com itens e parcelas funciona em uma única transação;
falha em qualquer parte reverte tudo
- [ ] Soma das parcelas é validada contra o valor total da venda
- [ ] Cancelar venda cancela as parcelas pendentes associadas
- [ ] Venda confirmada não pode ser editada em itens/valores (só via
cancelamento)
- [ ] Marcar parcela como paga reflete corretamente no dashboard
financeiro
- [ ] Dashboards semanal/mensal retornam números batendo com os dados de
teste inseridos manualmente
- [ ] Endpoint de atualizar atrasadas muda status corretamente das
parcelas vencidas
- [ ] Todas as queries usam RLS corretamente — venda de um tenant nunca
aparece para outro (reforçar teste de isolamento desta etapa
também)
+89
View File
@@ -0,0 +1,89 @@
# Etapa 7/8 — Motor de Alertas (BullMQ)
## Contexto do projeto
Continuação do **SeConectai Vendas**. Já existem: setup, auth com RLS,
painel master completo, integrações Evolution API/e-mail com serviço
central de envio (`AlertDispatchService`), CRUD de customers/produtos, e
o módulo completo de vendas/parcelas/dashboards.
Esta é a etapa que **automatiza** o que até agora só existia como
endpoint manual: verificação de vencimentos e disparo de alertas, nos
dois níveis do sistema (master→tenant e tenant→customer).
## Tarefas
1. **Infraestrutura de filas**
- Configurar `@nestjs/bullmq` conectado ao Redis já disponível no
`docker-compose`.
- Criar duas filas: `alertas-mensalidade` (master→tenant) e
`alertas-cobranca` (tenant→customer).
- Criar um processo/worker separado do processo web principal (pode
ser o mesmo container em dev, mas documentar como se separaria em
produção — ex: `main.ts` vs `worker.ts`, ou um segundo serviço no
`docker-compose` chamado `worker` rodando o mesmo build com um
comando diferente).
2. **Módulo `alert-templates`**
- CRUD de `alert_templates`:
- Nível master (`MasterGuard`): templates padrão da plataforma
(`tenant_id = null`), tipo `mensalidade_tenant`.
- Nível tenant (`TenantGuard`, role `tenant_admin`): templates
próprios, tipo `cobranca_cliente` (e outros tipos que fizerem
sentido, ex: `venda_confirmada`, `lembrete_vencimento`). Se o
tenant não tiver template próprio para um tipo/canal, usar o
template padrão do master como fallback.
- Implementar renderização de placeholders (`{{nome}}`, `{{valor}}`,
`{{vencimento}}`, etc — listar todos os placeholders suportados por
tipo de template em um arquivo de constantes, documentado).
3. **Job diário: `mensalidade_tenant`**
- Cron (ex: todo dia às 8h, horário configurável via env) que:
1. Roda a lógica equivalente ao endpoint
`POST /master/faturas/atualizar-atrasadas` (marca faturas
vencidas como `atrasado`, tenant vira `inadimplente`).
2. Para toda `tenant_faturas` com vencimento em N dias (configurável,
ex: 3 dias antes) ou já vencida e ainda sem alerta hoje, enfileira
um job na fila `alertas-mensalidade` com os dados necessários.
- Worker da fila `alertas-mensalidade`: busca template
(`mensalidade_tenant`, canal `whatsapp` e/ou `email` conforme
configurado), renderiza, chama `AlertDispatchService`, grava
resultado em `alert_logs`, atualiza `tenant_faturas.alerta_enviado_em`.
4. **Job diário: `cobranca_cliente`** (por tenant)
- Mesmo padrão do job acima, mas iterando `sale_payments` de todos os
tenants ativos: marca atrasadas, enfileira alerta para parcelas
próximas do vencimento ou já atrasadas (respeitando
`alerta_enviado_em` para não notificar duas vezes no mesmo dia).
- Usa a instância WhatsApp e config de e-mail **do tenant dono da
venda**, não a do master.
5. **Idempotência e resiliência**
- Garantir que um mesmo job não dispare o mesmo alerta duas vezes
(checar `alerta_enviado_em` antes de enfileirar, e novamente antes
de enviar, dentro do worker, para lidar com concorrência).
- Configurar retry com backoff exponencial no BullMQ para falhas
transitórias de envio (ex: Evolution API fora do ar), com limite de
tentativas (ex: 3) e log do erro final em `alert_logs.erro`.
- Endpoint de observabilidade: `GET /master/alertas/logs` e
`GET /tenant/alertas/logs` para consultar `alert_logs` com filtros
(canal, status, período).
## Critérios de aceite
- [ ] Worker processa as duas filas de forma independente do processo
web (documentado como escalar workers separadamente)
- [ ] Template do tenant sobrepõe o padrão do master quando existe; usa
fallback quando não existe
- [ ] Placeholders são substituídos corretamente na renderização
- [ ] Job diário marca faturas/parcelas atrasadas corretamente
- [ ] Alerta de mensalidade chega no WhatsApp/e-mail do tenant em teste
controlado (vencimento simulado para "hoje")
- [ ] Alerta de cobrança chega no WhatsApp/e-mail do customer, usando a
instância/config do tenant correto (testar com 2 tenants
diferentes)
- [ ] Mesmo alerta não é disparado duas vezes no mesmo dia (testar
rodando o job duas vezes seguidas)
- [ ] Falha de envio é reprocessada com retry e, após esgotar tentativas,
fica registrada em `alert_logs` com erro
- [ ] Endpoints de consulta de logs funcionam com os filtros esperados
+87
View File
@@ -0,0 +1,87 @@
# Etapa 8/8 — Personalização (Whitelabel) e Ajustes Finais
## Contexto do projeto
Etapa final do **SeConectai Vendas**. Todo o backend de negócio já está
pronto: auth com RLS, painel master (tenants/planos/faturas), integrações
WhatsApp/e-mail, customers/produtos, vendas/parcelas/dashboards, e o
motor de alertas automatizado.
Esta etapa cobre a personalização visual por tenant (whitelabel) e uma
revisão geral de segurança/qualidade antes de considerar o backend pronto
para receber um frontend.
## Tarefas
1. **Upload e gestão de logo**
- `POST /tenant/perfil/logo` (`TenantGuard`, role `tenant_admin`) —
recebe upload de imagem (multipart), valida tipo (`png`/`jpg`/`svg`)
e tamanho máximo (ex: 2MB), armazena (definir estratégia: volume
Docker local para MVP, com estrutura pronta para trocar por S3/
object storage depois — abstrair atrás de um `StorageService` com
interface clara) e atualiza `tenants.logo_url`.
- `DELETE /tenant/perfil/logo` — remove logo, volta ao padrão da
plataforma.
- `PUT /tenant/perfil/cor` — atualiza `tenants.cor_primaria` (validar
formato hex `#RRGGBB`).
- `GET /tenant/perfil` — retorna dados de personalização atuais
(logo_url, cor_primaria, nome_fantasia) para o frontend montar o
tema.
- Endpoint público (sem auth, ou com auth leve): `GET
/public/tenants/:slugOuId/tema` — para a tela de login do tenant já
carregar logo/cor antes do usuário autenticar (avaliar se precisa
de um campo `slug` único e amigável em `tenants`; se sim, criar
migration incremental para adicionar essa coluna).
2. **Revisão de segurança geral**
- Checklist a validar/corrigir no código existente:
- Rate limiting está ativo em todos os endpoints de auth e nos
endpoints públicos.
- CORS configurado explicitamente (não `*`) — origem(ns) do
frontend via variável de ambiente.
- Helmet (`@nestjs/helmet` ou `helmet` direto) configurado para
headers de segurança padrão.
- Nenhum segredo (API keys, senhas SMTP, `ENCRYPTION_KEY`,
`JWT_SECRET`) hardcoded ou logado em texto puro em nenhum lugar do
código.
- Todos os inputs de API validados via DTOs com `class-validator`
(checar que nenhum endpoint aceita payload sem validação).
- Paginação obrigatória (com limite máximo) em todas as listagens,
para evitar consultas sem limite.
3. **Documentação da API**
- Configurar Swagger/OpenAPI (`@nestjs/swagger`) documentando todos os
endpoints das 8 etapas, agrupados por módulo/tag, com exemplos de
request/response e indicação clara de quais exigem `MasterGuard` vs
`TenantGuard`.
4. **Ambiente de produção**
- Revisar `Dockerfile` e `docker-compose.yml` para produção:
- Imagem final enxuta (multi-stage, sem devDependencies).
- Variáveis sensíveis via secrets/env, nunca commitadas.
- Healthchecks configurados em todos os serviços.
- Documentar no README o processo de deploy (build, migrations em
produção, rodar migration de policies RLS, subir worker
separadamente do web).
5. **Testes finais de ponta a ponta**
- Escrever um teste E2E que cobre o fluxo completo mínimo: master
cria tenant → tenant faz login → cadastra customer e produto → faz
uma venda com parcelas → marca uma parcela como paga → confere
dashboard financeiro refletindo o pagamento.
## Critérios de aceite
- [ ] Upload/remoção de logo e alteração de cor funcionam e refletem em
`GET /tenant/perfil`
- [ ] Endpoint público de tema retorna logo/cor sem exigir autenticação
- [ ] CORS, Helmet e rate limiting revisados e ativos
- [ ] Nenhum segredo aparece em log ou resposta de API (revisão manual)
- [ ] Swagger disponível (ex: `/docs`) cobrindo todos os módulos, com
guards indicados
- [ ] Build de produção do Docker gera imagem sem dependências de
desenvolvimento
- [ ] Teste E2E do fluxo completo passa do início ao fim
- [ ] README final consolidado: como rodar em dev, como fazer deploy em
produção, como rodar migrations (incluindo policies de RLS) em um
ambiente novo
+601
View File
@@ -0,0 +1,601 @@
# 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) | [ ] Não iniciada | |
| 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
- [ ] 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
**Observações:**
---
## 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.
+118
View File
@@ -0,0 +1,118 @@
# SeConectai Vendas — Arquitetura
CRM/controle de vendas multi-tenant, com um painel **master** (você, dono da
plataforma) e painéis de **tenant** (seus clientes, que por sua vez cadastram
os próprios clientes/vendas).
## 1. Modelo de dados (resumo)
```
master_admins → você e sua equipe (login do painel master)
evolution_api_config → conexão(ões) com a Evolution API 2.3
master_email_config → SMTP padrão da plataforma
planos → planos de assinatura que você vende
tenants → seus clientes (quem compra a plataforma)
tenant_faturas → mensalidades que o tenant te deve
tenant_users → usuários/login de cada tenant
tenant_whatsapp_instances → instância Evolution API de cada tenant
tenant_email_config → SMTP próprio do tenant (opcional)
alert_templates → mensagens (WhatsApp/e-mail) customizáveis
alert_logs → auditoria de todo alerta enviado
customers → clientes (empresas) de cada tenant
customer_contacts → contatos dentro dessas empresas
products_services → catálogo de produto/serviço por tenant
sales / sale_items / sale_payments → vendas, itens e parcelas a receber
audit_logs → trilha de auditoria geral
```
Arquivo completo: `seconectai-vendas-schema.sql`.
## 2. Estratégia de multi-tenancy
Banco único, todas as tabelas de dado operacional carregam `tenant_id`, com
**Row Level Security (RLS)** do Postgres isolando cada tenant. A aplicação
define `SET app.current_tenant_id = '<uuid>'` na conexão/transação do
usuário logado; o Postgres bloqueia qualquer leitura/escrita fora do tenant
automaticamente — mesmo que haja um bug na query da aplicação.
Por que essa abordagem em vez de "um schema por tenant" ou "um banco por
tenant": com um único Postgres em Docker, migrations, backup e escala
operacional ficam muito mais simples, e RLS já dá o isolamento de segurança
que schema-per-tenant tentaria resolver com mais complexidade operacional.
O usuário master (você) usa uma role de banco separada com `BYPASS ROW LEVEL
SECURITY`, para poder ver/gerenciar todos os tenants.
## 3. Stack sugerida
- **Backend:** Node.js + TypeScript, com NestJS (organização em módulos por
domínio: auth, tenants, customers, products, sales, alerts) ou Fastify se
quiser algo mais enxuto. Node se encaixa bem no seu workflow atual
(n8n, Evolution API, JS/TS já no dia a dia).
- **ORM:** Prisma — migrations versionadas, tipagem forte, e dá pra injetar
o `SET app.current_tenant_id` por transação com `$transaction`.
- **Banco:** PostgreSQL 16.
- **Fila/agendamento:** Redis + BullMQ, para dois tipos de job recorrente:
- varredura diária de vencimentos (`tenant_faturas` e `sale_payments`)
disparando alertas antes/no dia do vencimento;
- envio assíncrono de mensagens (não travar a request do usuário
esperando resposta da Evolution API/SMTP).
- **Autenticação:** JWT de curta duração (access token) + refresh token com
rotação, guardado com hash (nunca em texto puro) na tabela
`auth_refresh_tokens`. Senha com `argon2id` (preferível a bcrypt hoje).
- **Deploy:** Docker Compose com serviços `app`, `postgres`, `redis` — a
Evolution API pode rodar no mesmo compose ou em servidor separado (você
já mantém instância própria, então só precisa cadastrar a `base_url` +
`api_key` em `evolution_api_config`).
## 4. Segurança — pontos que valem atenção na implementação
- Hash de senha: `argon2id`, nunca MD5/SHA puro.
- Segredos (API key da Evolution, senha SMTP) criptografados em repouso
(AES-256-GCM) com chave fora do banco (variável de ambiente/secret
manager), não só "hash" — porque esses precisam ser recuperados em texto
puro para uso.
- Rate limiting no login (a tabela `login_attempts` já dá suporte a isso) e
bloqueio temporário após N tentativas falhas.
- 2FA (TOTP) pelo menos para `master_admins`, opcional para `tenant_admin`.
- RLS como camada de defesa adicional — mesmo com bug de aplicação, tenant A
não acessa dado de tenant B.
- HTTPS obrigatório, cookies de refresh token com `httpOnly` + `Secure` +
`SameSite=Strict`.
- Logs de auditoria (`audit_logs`, `alert_logs`) para rastrear quem fez o
quê — importante já que múltiplos tenants e usuários mexem no sistema.
## 5. Fluxo de alertas (o motor comercial do sistema)
Dois níveis, mesma mecânica:
1. **Master → Tenant:** job diário verifica `tenant_faturas` com vencimento
próximo/vencido → gera mensagem a partir de `alert_templates`
(`tipo='mensalidade_tenant'`) → envia via WhatsApp (Evolution API,
instância própria do master) e/ou e-mail (`master_email_config`) →
grava em `alert_logs`.
2. **Tenant → Customer:** mesmo job (ou outro, escopado por tenant) verifica
`sale_payments` pendentes/atrasados de cada tenant → usa a instância
Evolution do próprio tenant (`tenant_whatsapp_instances`) e o SMTP dele
(ou o padrão da plataforma, se `usar_config_padrao=true`) → envia e
registra em `alert_logs`.
Isso te dá, de graça, o mecanismo pra cobrar a própria mensalidade da
plataforma (item que você pediu explicitamente) usando a mesma
infraestrutura que o tenant usa pra cobrar os clientes dele.
## 6. Próximos passos sugeridos
Seguindo seu formato usual de build faseado (prompts sequenciais com
critério de aceite por etapa), a ordem natural seria:
1. Setup do projeto (Docker Compose, Postgres, Prisma, estrutura NestJS)
2. Auth (master + tenant_user, JWT/refresh, RLS aplicado)
3. CRUD de tenants + planos + faturas (painel master)
4. Cadastro Evolution API + instâncias WhatsApp por tenant
5. CRUD de customers, products_services (painel tenant)
6. Vendas (sales/sale_items/sale_payments) + dashboards (views já prontas)
7. Motor de alertas (BullMQ + templates + logs)
8. Personalização visual (logo/cor por tenant)
Posso detalhar qualquer uma dessas fases em um prompt de vibe coding
próprio, se quiser seguir esse fluxo.
+451
View File
@@ -0,0 +1,451 @@
-- ============================================================
-- SeConectAI Vendas — Schema PostgreSQL (multi-tenant CRM/Vendas)
-- ============================================================
-- Estratégia de multi-tenancy: banco único, isolado por tenant_id
-- + Row Level Security (RLS). Mais simples de operar em Docker/
-- Docker Compose do que "schema por tenant" e escala bem até
-- centenas de tenants num único Postgres.
-- ============================================================
CREATE EXTENSION IF NOT EXISTS "pgcrypto"; -- gen_random_uuid()
-- ============================================================
-- 1. NÍVEL MASTER (você — dono da plataforma)
-- ============================================================
CREATE TABLE master_admins (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
nome VARCHAR(150) NOT NULL,
email VARCHAR(150) UNIQUE NOT NULL,
senha_hash TEXT NOT NULL, -- argon2id ou bcrypt cost>=12
ativo BOOLEAN NOT NULL DEFAULT TRUE,
mfa_secret TEXT, -- TOTP opcional, recomendado p/ master
ultimo_login TIMESTAMPTZ,
criado_em TIMESTAMPTZ NOT NULL DEFAULT now(),
atualizado_em TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- Conexão(ões) com a Evolution API 2.3, cadastrada pelo master.
-- Pode haver mais de um servidor Evolution (ex: para balancear carga).
CREATE TABLE evolution_api_config (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
nome VARCHAR(100) NOT NULL, -- "Servidor Principal"
base_url VARCHAR(255) NOT NULL,
api_key_cripto TEXT NOT NULL, -- criptografar em repouso (app-level, ex: AES-256-GCM)
versao VARCHAR(20) NOT NULL DEFAULT '2.3',
ativo BOOLEAN NOT NULL DEFAULT TRUE,
criado_em TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- Config SMTP padrão da plataforma (fallback quando o tenant não
-- tem SMTP próprio cadastrado)
CREATE TABLE master_email_config (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
smtp_host VARCHAR(150) NOT NULL,
smtp_porta INTEGER NOT NULL,
smtp_usuario VARCHAR(150) NOT NULL,
smtp_senha_cripto TEXT NOT NULL,
email_remetente VARCHAR(150) NOT NULL,
ativo BOOLEAN NOT NULL DEFAULT TRUE,
criado_em TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE planos (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
nome VARCHAR(100) NOT NULL,
descricao TEXT,
valor_mensal NUMERIC(10,2) NOT NULL,
limite_customers INTEGER, -- NULL = ilimitado
limite_usuarios INTEGER,
limite_instancias_whatsapp INTEGER NOT NULL DEFAULT 1,
ativo BOOLEAN NOT NULL DEFAULT TRUE,
criado_em TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- ============================================================
-- 2. TENANTS — seus clientes que compram/usam a plataforma
-- ============================================================
CREATE TYPE tenant_status AS ENUM ('trial', 'ativo', 'inadimplente', 'suspenso', 'cancelado');
CREATE TABLE tenants (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
razao_social VARCHAR(200) NOT NULL,
nome_fantasia VARCHAR(200),
documento VARCHAR(20), -- CNPJ/CPF
email_contato VARCHAR(150) NOT NULL,
telefone VARCHAR(20),
logo_url TEXT, -- personalização visual (whitelabel)
cor_primaria VARCHAR(7), -- "#1a73e8"
plano_id UUID REFERENCES planos(id),
status tenant_status NOT NULL DEFAULT 'trial',
dia_vencimento_mensalidade SMALLINT NOT NULL DEFAULT 10
CHECK (dia_vencimento_mensalidade BETWEEN 1 AND 28),
trial_expira_em TIMESTAMPTZ,
criado_por UUID REFERENCES master_admins(id),
criado_em TIMESTAMPTZ NOT NULL DEFAULT now(),
atualizado_em TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_tenants_status ON tenants(status);
-- Faturas da mensalidade (você cobrando o tenant)
CREATE TYPE cobranca_status AS ENUM ('pendente', 'pago', 'atrasado', 'cancelado');
CREATE TABLE tenant_faturas (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
referencia VARCHAR(7) NOT NULL, -- '2026-08'
valor NUMERIC(10,2) NOT NULL,
vencimento DATE NOT NULL,
pago_em TIMESTAMPTZ,
status cobranca_status NOT NULL DEFAULT 'pendente',
alerta_enviado_em TIMESTAMPTZ,
criado_em TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE(tenant_id, referencia)
);
CREATE INDEX idx_tenant_faturas_venc
ON tenant_faturas(vencimento) WHERE status IN ('pendente', 'atrasado');
-- ============================================================
-- 3. USUÁRIOS E AUTENTICAÇÃO
-- ============================================================
CREATE TYPE user_role AS ENUM ('tenant_admin', 'vendedor', 'financeiro', 'visualizador');
CREATE TABLE tenant_users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
nome VARCHAR(150) NOT NULL,
email VARCHAR(150) NOT NULL,
senha_hash TEXT NOT NULL,
role user_role NOT NULL DEFAULT 'vendedor',
ativo BOOLEAN NOT NULL DEFAULT TRUE,
mfa_secret TEXT,
ultimo_login TIMESTAMPTZ,
criado_em TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE(tenant_id, email)
);
CREATE INDEX idx_tenant_users_tenant ON tenant_users(tenant_id);
-- Refresh tokens (JWT access token curto + refresh token com rotação)
CREATE TABLE auth_refresh_tokens (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
usuario_tipo VARCHAR(20) NOT NULL, -- 'master' | 'tenant_user'
usuario_id UUID NOT NULL,
token_hash TEXT NOT NULL, -- nunca guardar o token em texto puro
expira_em TIMESTAMPTZ NOT NULL,
revogado BOOLEAN NOT NULL DEFAULT FALSE,
ip_origem INET,
user_agent TEXT,
criado_em TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_refresh_tokens_usuario ON auth_refresh_tokens(usuario_tipo, usuario_id);
CREATE TABLE password_reset_tokens (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
usuario_tipo VARCHAR(20) NOT NULL,
usuario_id UUID NOT NULL,
token_hash TEXT NOT NULL,
expira_em TIMESTAMPTZ NOT NULL,
usado BOOLEAN NOT NULL DEFAULT FALSE,
criado_em TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- Registro de tentativas de login (para rate-limit/bloqueio de força bruta)
CREATE TABLE login_attempts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email VARCHAR(150) NOT NULL,
usuario_tipo VARCHAR(20) NOT NULL,
sucesso BOOLEAN NOT NULL,
ip_origem INET,
criado_em TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_login_attempts_email_data ON login_attempts(email, criado_em);
-- ============================================================
-- 4. INTEGRAÇÕES POR TENANT (WhatsApp / E-mail / Templates)
-- ============================================================
CREATE TABLE tenant_whatsapp_instances (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
evolution_config_id UUID REFERENCES evolution_api_config(id),
instance_name VARCHAR(100) NOT NULL UNIQUE, -- nome único na Evolution API
numero_conectado VARCHAR(20),
status VARCHAR(30) NOT NULL DEFAULT 'desconectado',
-- desconectado | conectando | conectado | erro
qr_code_gerado_em TIMESTAMPTZ,
criado_em TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE tenant_email_config (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL UNIQUE REFERENCES tenants(id) ON DELETE CASCADE,
smtp_host VARCHAR(150),
smtp_porta INTEGER,
smtp_usuario VARCHAR(150),
smtp_senha_cripto TEXT,
email_remetente VARCHAR(150),
usar_config_padrao BOOLEAN NOT NULL DEFAULT TRUE, -- usa master_email_config se true
criado_em TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- Templates de mensagens (nível master = padrão global; nível tenant = customizado)
CREATE TABLE alert_templates (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID REFERENCES tenants(id) ON DELETE CASCADE, -- NULL = template padrão da plataforma
tipo VARCHAR(50) NOT NULL,
-- 'mensalidade_tenant' | 'cobranca_cliente' | 'venda_confirmada' | 'lembrete_vencimento'
canal VARCHAR(20) NOT NULL, -- 'whatsapp' | 'email'
assunto VARCHAR(200), -- só para email
corpo TEXT NOT NULL, -- placeholders: {{nome}}, {{valor}}, {{vencimento}}
ativo BOOLEAN NOT NULL DEFAULT TRUE,
criado_em TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- Log/auditoria de todo alerta disparado (master->tenant e tenant->customer)
CREATE TABLE alert_logs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID REFERENCES tenants(id) ON DELETE CASCADE,
destinatario_tipo VARCHAR(20) NOT NULL, -- 'tenant' | 'customer'
destinatario_id UUID NOT NULL, -- id do tenant ou do customer
canal VARCHAR(20) NOT NULL, -- whatsapp | email
template_id UUID REFERENCES alert_templates(id),
conteudo_enviado TEXT,
status VARCHAR(20) NOT NULL DEFAULT 'enviado', -- enviado | falhou | entregue | lido
erro TEXT,
enviado_em TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_alert_logs_tenant ON alert_logs(tenant_id, enviado_em);
-- ============================================================
-- 5. CLIENTES DO TENANT ("customers" — empresas atendidas)
-- ============================================================
CREATE TABLE customers (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
razao_social VARCHAR(200) NOT NULL,
nome_fantasia VARCHAR(200),
documento VARCHAR(20),
email VARCHAR(150),
telefone_whatsapp VARCHAR(20),
endereco TEXT,
cidade VARCHAR(100),
uf CHAR(2),
observacoes TEXT,
ativo BOOLEAN NOT NULL DEFAULT TRUE,
criado_em TIMESTAMPTZ NOT NULL DEFAULT now(),
atualizado_em TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_customers_tenant ON customers(tenant_id);
CREATE TABLE customer_contacts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
customer_id UUID NOT NULL REFERENCES customers(id) ON DELETE CASCADE,
nome VARCHAR(150) NOT NULL,
cargo VARCHAR(100),
email VARCHAR(150),
telefone_whatsapp VARCHAR(20),
principal BOOLEAN NOT NULL DEFAULT FALSE,
criado_em TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- ============================================================
-- 6. PRODUTOS E SERVIÇOS DO TENANT
-- ============================================================
CREATE TYPE item_tipo AS ENUM ('produto', 'servico');
CREATE TABLE products_services (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
tipo item_tipo NOT NULL,
nome VARCHAR(200) NOT NULL,
descricao TEXT,
sku VARCHAR(50),
valor_venda NUMERIC(12,2) NOT NULL,
valor_custo NUMERIC(12,2),
unidade VARCHAR(20) NOT NULL DEFAULT 'un', -- un, hora, mês...
recorrente BOOLEAN NOT NULL DEFAULT FALSE, -- ex: serviço de mensalidade
ativo BOOLEAN NOT NULL DEFAULT TRUE,
criado_em TIMESTAMPTZ NOT NULL DEFAULT now(),
atualizado_em TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE(tenant_id, sku)
);
CREATE INDEX idx_products_tenant ON products_services(tenant_id);
-- ============================================================
-- 7. VENDAS
-- ============================================================
CREATE TYPE venda_status AS ENUM ('orcamento', 'confirmada', 'cancelada', 'concluida');
CREATE TABLE sales (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
customer_id UUID NOT NULL REFERENCES customers(id),
vendedor_id UUID REFERENCES tenant_users(id),
numero_venda SERIAL,
status venda_status NOT NULL DEFAULT 'confirmada',
valor_total NUMERIC(12,2) NOT NULL DEFAULT 0,
desconto NUMERIC(12,2) NOT NULL DEFAULT 0,
observacoes TEXT,
data_venda DATE NOT NULL DEFAULT CURRENT_DATE,
criado_em TIMESTAMPTZ NOT NULL DEFAULT now(),
atualizado_em TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_sales_tenant_data ON sales(tenant_id, data_venda);
CREATE INDEX idx_sales_customer ON sales(customer_id);
CREATE TABLE sale_items (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
sale_id UUID NOT NULL REFERENCES sales(id) ON DELETE CASCADE,
product_service_id UUID NOT NULL REFERENCES products_services(id),
quantidade NUMERIC(10,2) NOT NULL DEFAULT 1,
valor_unitario NUMERIC(12,2) NOT NULL,
valor_total NUMERIC(12,2) NOT NULL
);
CREATE INDEX idx_sale_items_sale ON sale_items(sale_id);
-- Parcelas/recebimentos: contas a receber do tenant junto ao customer
CREATE TYPE parcela_status AS ENUM ('pendente', 'pago', 'atrasado', 'cancelado');
CREATE TABLE sale_payments (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
sale_id UUID NOT NULL REFERENCES sales(id) ON DELETE CASCADE,
numero_parcela SMALLINT NOT NULL DEFAULT 1,
valor NUMERIC(12,2) NOT NULL,
vencimento DATE NOT NULL,
pago_em TIMESTAMPTZ,
forma_pagamento VARCHAR(30), -- pix, boleto, cartao, dinheiro
status parcela_status NOT NULL DEFAULT 'pendente',
alerta_enviado_em TIMESTAMPTZ,
criado_em TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_sale_payments_venc
ON sale_payments(vencimento) WHERE status IN ('pendente', 'atrasado');
-- ============================================================
-- 8. AUDITORIA GERAL
-- ============================================================
CREATE TABLE audit_logs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID REFERENCES tenants(id) ON DELETE SET NULL,
usuario_tipo VARCHAR(20) NOT NULL, -- master | tenant_user
usuario_id UUID,
acao VARCHAR(100) NOT NULL, -- login, criar_venda, excluir_cliente...
entidade VARCHAR(50),
entidade_id UUID,
detalhes JSONB,
ip_origem INET,
criado_em TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_audit_tenant_data ON audit_logs(tenant_id, criado_em);
-- ============================================================
-- 9. VIEWS DE DASHBOARD (usadas no painel do tenant)
-- ============================================================
CREATE VIEW vw_resumo_vendas_semanal AS
SELECT
s.tenant_id,
date_trunc('week', s.data_venda)::date AS semana,
COUNT(*) AS qtd_vendas,
SUM(s.valor_total) AS total_vendido
FROM sales s
WHERE s.status IN ('confirmada', 'concluida')
GROUP BY s.tenant_id, date_trunc('week', s.data_venda);
CREATE VIEW vw_resumo_vendas_mensal AS
SELECT
s.tenant_id,
date_trunc('month', s.data_venda)::date AS mes,
COUNT(*) AS qtd_vendas,
SUM(s.valor_total) AS total_vendido
FROM sales s
WHERE s.status IN ('confirmada', 'concluida')
GROUP BY s.tenant_id, date_trunc('month', s.data_venda);
CREATE VIEW vw_recebimentos AS
SELECT
sp.id,
s.tenant_id,
s.customer_id,
sp.valor,
sp.vencimento,
sp.status,
sp.pago_em
FROM sale_payments sp
JOIN sales s ON s.id = sp.sale_id;
-- ============================================================
-- 10. ROW LEVEL SECURITY — isolamento entre tenants
-- ============================================================
-- A aplicação deve definir, por conexão/transação:
-- SET app.current_tenant_id = '<uuid-do-tenant-logado>';
-- Usuários master (que enxergam todos os tenants) usam uma role
-- de banco separada (ex: app_master) que faz BYPASS RLS, ou uma
-- policy adicional que libera quando app.is_master = 'true'.
ALTER TABLE customers ENABLE ROW LEVEL SECURITY;
ALTER TABLE customer_contacts ENABLE ROW LEVEL SECURITY;
ALTER TABLE products_services ENABLE ROW LEVEL SECURITY;
ALTER TABLE sales ENABLE ROW LEVEL SECURITY;
ALTER TABLE sale_items ENABLE ROW LEVEL SECURITY;
ALTER TABLE sale_payments ENABLE ROW LEVEL SECURITY;
ALTER TABLE tenant_users ENABLE ROW LEVEL SECURITY;
ALTER TABLE tenant_whatsapp_instances ENABLE ROW LEVEL SECURITY;
ALTER TABLE tenant_email_config ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation_customers ON customers
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
CREATE POLICY tenant_isolation_products ON products_services
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
CREATE POLICY tenant_isolation_sales ON sales
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
CREATE POLICY tenant_isolation_tenant_users ON tenant_users
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
CREATE POLICY tenant_isolation_whatsapp ON tenant_whatsapp_instances
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
CREATE POLICY tenant_isolation_email_config ON tenant_email_config
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
-- sale_items e customer_contacts não têm tenant_id direto: isolar via join
CREATE POLICY tenant_isolation_sale_items ON sale_items
USING (sale_id IN (
SELECT id FROM sales
WHERE tenant_id = current_setting('app.current_tenant_id', true)::uuid
));
CREATE POLICY tenant_isolation_sale_payments ON sale_payments
USING (sale_id IN (
SELECT id FROM sales
WHERE tenant_id = current_setting('app.current_tenant_id', true)::uuid
));
CREATE POLICY tenant_isolation_customer_contacts ON customer_contacts
USING (customer_id IN (
SELECT id FROM customers
WHERE tenant_id = current_setting('app.current_tenant_id', true)::uuid
));
-- Fim do schema