first commit
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user