# Sistema de Lembretes Automáticos — WhatsApp, E-mail, SMS

> Agência PSI — Arquitetura completa
> Data: 2026-03-21
> Autor: Leonardo Nohama

---

## Sumário

1. [Decisão de Provedor WhatsApp](#1-decisão-de-provedor-whatsapp)
2. [Modelagem do Banco de Dados](#2-modelagem-do-banco-de-dados)
3. [Lógica de Agendamento (Cron + Edge Functions)](#3-lógica-de-agendamento)
4. [Integração Evolution API (MVP)](#4-integração-evolution-api-mvp)
5. [Integração API Oficial Meta (Escala)](#5-integração-api-oficial-meta)
6. [Frontend: Telas de Configuração](#6-frontend-telas-de-configuração)
7. [LGPD e Boas Práticas](#7-lgpd-e-boas-práticas)
8. [Diagrama do Fluxo Completo](#8-diagrama-do-fluxo-completo)
9. [Checklist de Produção](#9-checklist-de-produção)

---

## 1. Decisão de Provedor WhatsApp

### Comparativo de Provedores

| Critério | Evolution API | WPPConnect | Z-API (grátis) | Z-API Pro | Twilio (Meta oficial) | 360dialog (Meta oficial) | Zenvia (Meta oficial) |
|----------|--------------|------------|----------------|-----------|----------------------|-------------------------|----------------------|
| **Tipo** | Não-oficial (baileys) | Não-oficial | Não-oficial | Não-oficial (infra deles) | API Oficial Meta | API Oficial Meta | API Oficial Meta |
| **Custo mensal** | R$ 0 (self-hosted) | R$ 0 (self-hosted) | R$ 0 (limite de msgs) | R$ 99–299/mês | ~R$ 0.30/msg (conversa) | €49/mês + msg | R$ 0.15–0.40/msg |
| **Setup** | 2–4h (Docker) | 4–8h | 30min (SaaS) | 1h (SaaS) | 1–2 semanas (aprovação) | 3–5 dias | 3–5 dias |
| **Risco de ban** | **MÉDIO-ALTO** | ALTO | MÉDIO | MÉDIO | **ZERO** | **ZERO** | **ZERO** |
| **Templates Meta** | ❌ Não suporta | ❌ | ❌ | ❌ | ✅ Obrigatório | ✅ Obrigatório | ✅ Obrigatório |
| **Webhooks status** | ✅ Completo | ✅ Parcial | ✅ | ✅ | ✅ Completo | ✅ Completo | ✅ Completo |
| **Multi-instância** | ✅ Nativo | ❌ Manual | ❌ | ✅ | ✅ Via WABA | ✅ | ✅ |
| **Uptime SLA** | Depende de você | Depende de você | 99.5% | 99.5% | 99.95% | 99.9% | 99.9% |
| **Escalabilidade** | ~500 msgs/dia seguro | ~300 msgs/dia | ~200 msgs/dia | ~2000 msgs/dia | Ilimitado | Ilimitado | Ilimitado |

### Recomendação

**MVP (0–100 clínicas):** Evolution API self-hosted
- Custo zero, setup rápido, suficiente para validar o produto
- Cada terapeuta conecta seu próprio número via QR Code
- Limite prático: ~500 mensagens/dia por número
- Mitigação de ban: mensagens personalizadas (não genéricas), intervalos entre envios,
  máximo 2 lembretes por sessão

**Escala (100+ clínicas):** Migrar para API Oficial da Meta via 360dialog ou Twilio
- Zero risco de banimento
- Templates aprovados pela Meta = entrega garantida
- Custo previsível por conversa (~R$ 0.25–0.40 por conversa de 24h)
- Suporte a botões interativos (confirmar/cancelar)

**Estratégia de migração:** O sistema será projetado com abstração de provedor desde o início.
A tabela `notification_channels` registra qual provedor cada tenant usa. Trocar de Evolution
para Meta oficial = mudar o `provider` e credenciais, sem alterar a fila ou templates.

---

## 2. Modelagem do Banco de Dados

> **Nota de integração:** O sistema existente já possui:
> - `email_templates_global` / `email_templates_tenant` → serão estendidos (não duplicados)
> - `notifications` → continuam para notificações in-app (realtime)
> - `profiles.notify_reminders` → será respeitado como opt-out global
> - `TEMPLATE_CHANNELS` em emailTemplateConstants.js → já prevê whatsapp/sms

### Relação entre tabelas novas e existentes

```
┌─────────────────────────────────────────────────────────────────┐
│ EXISTENTES (não alterar)                                        │
│                                                                 │
│  email_templates_global ──→ templates de email (11 seeds)       │
│  email_templates_tenant ──→ overrides por tenant/owner          │
│  notifications          ──→ notificações in-app (realtime)      │
│  profiles               ──→ notify_reminders, notify_system_email│
│  agenda_eventos         ──→ sessões com patient_id, inicio_em   │
│  patients               ──→ nome_completo, telefone, email      │
├─────────────────────────────────────────────────────────────────┤
│ NOVAS TABELAS                                                   │
│                                                                 │
│  notification_channels     ──→ config WhatsApp/SMS por tenant   │
│  notification_templates    ──→ templates multi-canal (wpp/sms)  │
│  notification_queue        ──→ fila de envio                    │
│  notification_logs         ──→ histórico completo               │
│  notification_preferences  ──→ opt-in/opt-out por paciente      │
│  notification_schedules    ──→ regras de quando disparar        │
└─────────────────────────────────────────────────────────────────┘
```

---

## 3. Lógica de Agendamento

### Fluxo Completo

```
pg_cron (a cada 5 min)
  │
  ├──→ populate_notification_queue()     ← PL/pgSQL function
  │    Busca agenda_eventos com inicio_em futuro,
  │    cruza com notification_schedules ativas,
  │    verifica notification_preferences do paciente,
  │    insere na notification_queue com idempotency_key
  │
  └──→ HTTP call → Edge Function: process-notification-queue
       │
       ├── Busca itens pendentes (status = 'pendente', scheduled_at <= now())
       ├── Marca como 'processando' (lock otimista via updated_at)
       ├── Resolve variáveis do template
       ├── Despacha para o provedor correto:
       │   ├── WhatsApp → Evolution API ou Meta API
       │   ├── Email → Resend / SendGrid / SMTP
       │   └── SMS → Zenvia / Twilio
       ├── Atualiza status → 'enviado' ou 'falhou'
       ├── Insere em notification_logs
       └── Em caso de falha: agenda retry exponencial
```

### Retry Exponencial

```
Tentativa 1: imediato
Tentativa 2: +5 minutos
Tentativa 3: +15 minutos
Tentativa 4: +60 minutos
Tentativa 5: +4 horas
Máximo: 5 tentativas → marca como 'falhou' definitivamente
```

### Prevenção de Duplicatas

1. **Idempotency key** = `{agenda_evento_id}:{schedule_key}:{canal}:{data_sessao}`
2. **UNIQUE constraint** na notification_queue sobre idempotency_key
3. **Lock otimista** no processamento: `UPDATE ... WHERE status = 'pendente' AND updated_at = ?`
4. **pg_cron não overlap**: usa `pg_try_advisory_lock()` no populate

---

## 4. Integração Evolution API (MVP)

### Setup Docker

```yaml
# docker-compose.evolution.yml
version: '3.8'
services:
  evolution-api:
    image: atendai/evolution-api:latest
    ports:
      - "8080:8080"
    environment:
      - AUTHENTICATION_API_KEY=sua_chave_global_aqui
      - DATABASE_PROVIDER=postgresql
      - DATABASE_CONNECTION_URI=postgresql://user:pass@host:5432/evolution
      - WEBHOOK_GLOBAL_URL=https://seu-dominio.com/api/webhooks/evolution
      - WEBHOOK_GLOBAL_ENABLED=true
      - WEBHOOK_EVENTS_STATUS_INSTANCE=true
      - WEBHOOK_EVENTS_MESSAGES_UPSERT=true
      - WEBHOOK_EVENTS_SEND_MESSAGE=true
    volumes:
      - evolution_data:/evolution/store
    restart: unless-stopped

volumes:
  evolution_data:
```

### Endpoints Necessários

```
Base URL: https://evolution.seudominio.com

POST /instance/create          → criar instância para o tenant
GET  /instance/connect/{name}  → obter QR Code para conectar número
GET  /instance/connectionState/{name} → verificar status da conexão
POST /message/sendText/{name}  → enviar mensagem de texto
POST /message/sendMedia/{name} → enviar com mídia (opcional)
DELETE /instance/delete/{name} → remover instância
```

### Payload de Envio

```json
// POST /message/sendText/{instance_name}
{
  "number": "5516999887766",
  "text": "Olá Ana Clara! 👋\n\nLembrete: você tem sessão amanhã, 21/03, às 14:00 com Dra. Beatriz Costa.\n\n📍 Online via Google Meet\n🔗 https://meet.google.com/abc-defg-hij\n\nPara confirmar, responda OK.\nPara cancelar, responda CANCELAR.\n\nAgência PSI"
}
```

### Webhook de Status

```json
// POST /api/webhooks/evolution (recebido do Evolution)
{
  "event": "messages.update",
  "instance": "clinica_abc",
  "data": {
    "key": {
      "remoteJid": "5516999887766@s.whatsapp.net",
      "id": "3EB0A0B6F..."
    },
    "update": {
      "status": 3  // 1=pendente, 2=enviado ao servidor, 3=entregue, 4=lido
    }
  }
}
```

### Credenciais no Supabase

Armazenadas na tabela `notification_channels.credentials` como JSONB criptografado:

```json
{
  "api_url": "https://evolution.seudominio.com",
  "api_key": "chave_global_evolution",
  "instance_name": "clinica_dr_beatriz",
  "connected_number": "5516999887766",
  "connection_status": "open"
}
```

> A criptografia das credenciais usa `pgcrypto` com chave armazenada como
> variável de ambiente do Supabase (Vault). Veja a função SQL `encrypt_credentials()`.

---

## 5. Integração API Oficial Meta

### Template de Lembrete para Aprovação

Nome: `session_reminder_v1`
Categoria: `UTILITY`
Idioma: `pt_BR`

```
HEADER: 📋 Lembrete de Sessão
BODY: Olá {{1}}! Sua sessão com {{2}} está agendada para {{3}} às {{4}}.
      Modalidade: {{5}}
BUTTONS:
  [quick_reply] ✅ Confirmar presença
  [quick_reply] ❌ Preciso cancelar
FOOTER: Agência PSI — Tecnologia aplicada à escuta
```

### Envio via Graph API

```
POST https://graph.facebook.com/v19.0/{phone_number_id}/messages
Authorization: Bearer {access_token}
Content-Type: application/json

{
  "messaging_product": "whatsapp",
  "to": "5516999887766",
  "type": "template",
  "template": {
    "name": "session_reminder_v1",
    "language": { "code": "pt_BR" },
    "components": [
      {
        "type": "body",
        "parameters": [
          { "type": "text", "text": "Ana Clara" },
          { "type": "text", "text": "Dra. Beatriz Costa" },
          { "type": "text", "text": "21/03/2026" },
          { "type": "text", "text": "14:00" },
          { "type": "text", "text": "Online" }
        ]
      }
    ]
  }
}
```

### Webhook de Status (Meta)

```json
// POST /api/webhooks/meta-whatsapp
{
  "entry": [{
    "changes": [{
      "value": {
        "statuses": [{
          "id": "wamid.HBgN...",
          "status": "delivered",  // sent, delivered, read, failed
          "timestamp": "1711036800",
          "recipient_id": "5516999887766",
          "errors": []
        }]
      }
    }]
  }]
}
```

---

## 6. Frontend: Telas de Configuração

### 6.1 Configuração de Canal (`ConfiguracoesCanaisPage.vue`)

```
┌─────────────────────────────────────────────────┐
│ 📡 Canais de Notificação                        │
├─────────────────────────────────────────────────┤
│                                                 │
│  ┌──────────────────────────────────────────┐   │
│  │ 💬 WhatsApp                    [Ativo ✅] │   │
│  │ Provedor: Evolution API                  │   │
│  │ Número: +55 16 99988-7766               │   │
│  │ Status: 🟢 Conectado                     │   │
│  │                                          │   │
│  │ [Reconectar]  [Ver QR Code]  [Testar]   │   │
│  └──────────────────────────────────────────┘   │
│                                                 │
│  ┌──────────────────────────────────────────┐   │
│  │ 📧 E-mail                     [Ativo ✅] │   │
│  │ Provedor: Resend                         │   │
│  │ Remetente: clinica@drbeat...             │   │
│  │ Status: 🟢 Verificado                    │   │
│  │                                          │   │
│  │ [Configurar SMTP]  [Testar]              │   │
│  └──────────────────────────────────────────┘   │
│                                                 │
│  ┌──────────────────────────────────────────┐   │
│  │ 📱 SMS                      [Inativo ⬜] │   │
│  │ Não configurado                          │   │
│  │                                          │   │
│  │ [Ativar]                                 │   │
│  └──────────────────────────────────────────┘   │
│                                                 │
└─────────────────────────────────────────────────┘
```

### 6.2 Templates (`ConfiguracoesNotifTemplatesPage.vue`)

- Lista de templates por domínio (Sessão, Triagem, Sistema)
- Preview ao vivo com variáveis mock (já existe no sistema)
- Cada template pode ser personalizado por canal (email / whatsapp / sms)
- Herança: se o tenant não customizou, mostra o template global com badge "Padrão"

### 6.3 Regras de Envio (`ConfiguracoesNotifRegrasPage.vue`)

```
┌──────────────────────────────────────────────┐
│ ⏰ Regras de Envio de Lembretes              │
├──────────────────────────────────────────────┤
│                                              │
│ Lembrete de Sessão                           │
│ ├── 24 horas antes    [WhatsApp ✅] [Email ✅] │
│ ├── 2 horas antes     [WhatsApp ✅] [Email ⬜] │
│ └── 30 min antes      [WhatsApp ⬜] [Email ⬜] │
│                                              │
│ Confirmação de Sessão                        │
│ ├── Imediata (ao criar) [Email ✅]            │
│ └── Imediata            [WhatsApp ✅]          │
│                                              │
│ Cancelamento                                 │
│ └── Imediata            [WhatsApp ✅] [Email ✅]│
│                                              │
│ Boas-vindas (novo paciente)                  │
│ └── Imediata            [WhatsApp ✅] [Email ✅]│
│                                              │
│ ⚙️ Horário permitido: 08:00 – 20:00          │
│ 📅 Não enviar: Domingos e feriados           │
│                                              │
│ [Salvar configurações]                       │
└──────────────────────────────────────────────┘
```

### 6.4 Logs de Envio (`ConfiguracoesNotifLogsPage.vue`)

- Tabela paginada com filtros por canal, status, período
- Detalhes por envio: template usado, variáveis resolvidas, resposta do provedor
- Estatísticas: total enviado, entregue, lido, falhou (últimos 30 dias)
- Export CSV

### 6.5 Opt-out pelo Paciente

- **WhatsApp:** Paciente responde "SAIR" → webhook captura → atualiza `notification_preferences`
- **Email:** Link "Cancelar inscrição" no rodapé → página pública de opt-out
- **Portal do paciente (futuro):** Toggle na área logada do paciente

---

## 7. LGPD e Boas Práticas

### Coleta de Consentimento

1. **Cadastro externo** (CadastroPacienteExterno.vue): já tem checkbox LGPD
2. **Cadastro pelo terapeuta**: adicionar checkbox "Paciente autoriza receber lembretes por WhatsApp/E-mail"
3. **Primeiro lembrete**: incluir mensagem "Responda SAIR a qualquer momento para parar de receber mensagens"

### Armazenamento Seguro

- Telefone do paciente: armazenado na tabela `patients.telefone` (já existe)
- Credenciais do provedor: `notification_channels.credentials` criptografado com `pgcrypto`
- Chave de criptografia: Supabase Vault (variável de ambiente, nunca no código)

### Retenção de Logs

- `notification_logs`: reter por **2 anos** (exigência legal para comprovação de comunicação)
- `notification_queue`: limpar itens processados após **90 dias** (via pg_cron)
- `notification_preferences`: manter enquanto o paciente estiver ativo

### Opt-out Imediato

- Resposta "SAIR" no WhatsApp → webhook → `notification_preferences.whatsapp_opt_in = false`
- Efeito imediato: todas as mensagens pendentes na fila para aquele paciente são canceladas
- Trigger SQL: ao atualizar opt-out, cancela itens pendentes na queue

---

## 8. Diagrama do Fluxo Completo

```
 ┌──────────────┐
 │ agenda_eventos│ ← terapeuta cria/edita sessão
 └──────┬───────┘
        │
        ▼
 ┌──────────────────────────────────────┐
 │ pg_cron: populate_notification_queue │ ← roda a cada 5 min
 │                                      │
 │ 1. Busca sessões com inicio_em       │
 │    entre agora e +48h                │
 │ 2. Cruza com notification_schedules  │
 │    (ex: 24h antes, 2h antes)         │
 │ 3. Verifica:                         │
 │    - notification_preferences (opt-in)│
 │    - notification_channels (canal ativo)│
 │    - profiles.notify_reminders       │
 │    - agenda_eventos.status ≠ cancelado│
 │ 4. Gera idempotency_key             │
 │ 5. INSERT INTO notification_queue    │
 │    ON CONFLICT DO NOTHING            │
 └──────────────┬───────────────────────┘
                │
                ▼
 ┌──────────────────────────────────────┐
 │ pg_cron: chama Edge Function         │ ← roda a cada 5 min (offset 2min)
 │ POST /functions/v1/process-notif-queue│
 └──────────────┬───────────────────────┘
                │
                ▼
 ┌──────────────────────────────────────┐
 │ Edge Function: process-notif-queue   │
 │                                      │
 │ 1. SELECT ... FROM notification_queue│
 │    WHERE status = 'pendente'         │
 │    AND scheduled_at <= now()         │
 │    LIMIT 50                          │
 │    FOR UPDATE SKIP LOCKED            │
 │                                      │
 │ 2. Para cada item:                   │
 │    a. Marca 'processando'            │
 │    b. Resolve template + variáveis   │
 │    c. Despacha para provedor:        │
 │       ┌──────────────────────┐       │
 │       │ channel = 'whatsapp' │       │
 │       │ → Evolution API      │       │
 │       │   ou Meta Graph API  │       │
 │       ├──────────────────────┤       │
 │       │ channel = 'email'    │       │
 │       │ → Resend / SMTP      │       │
 │       ├──────────────────────┤       │
 │       │ channel = 'sms'      │       │
 │       │ → Zenvia / Twilio    │       │
 │       └──────────────────────┘       │
 │    d. Atualiza status                │
 │    e. INSERT notification_logs       │
 │                                      │
 │ 3. Itens com falha:                  │
 │    attempts += 1                     │
 │    next_retry_at = exponential       │
 │    status = attempts >= 5            │
 │      ? 'falhou' : 'pendente'         │
 └──────────────────────────────────────┘
                │
                ▼
 ┌──────────────────────────────────────┐
 │ Webhook Handler                      │
 │ POST /functions/v1/notif-webhook     │
 │                                      │
 │ Recebe status do provedor:           │
 │ - Evolution: messages.update         │
 │ - Meta: webhook de status            │
 │ - Email: bounce/delivery events      │
 │                                      │
 │ Atualiza notification_logs:          │
 │ - delivered_at, read_at, failed_at   │
 │ - provider_status, provider_response │
 │                                      │
 │ Se resposta = "SAIR":               │
 │ → UPDATE notification_preferences    │
 │   SET whatsapp_opt_in = false        │
 │ → CANCEL pendentes na queue          │
 └──────────────────────────────────────┘
```

---

## 9. Checklist de Produção

### Infraestrutura
- [ ] Subir Evolution API (Docker) em VPS dedicada
- [ ] Configurar domínio + SSL para Evolution API
- [ ] Configurar Supabase Vault com chave de criptografia
- [ ] Habilitar extensão `pgcrypto` no Supabase
- [ ] Habilitar extensão `pg_cron` no Supabase
- [ ] Configurar DNS para webhook (ex: `webhooks.agenciapsi.com.br`)

### Banco de Dados
- [ ] Rodar migration: tabelas de notificação
- [ ] Rodar migration: functions PL/pgSQL (populate queue, encrypt/decrypt)
- [ ] Configurar pg_cron jobs (populate + process)
- [ ] Verificar RLS policies em todas as tabelas
- [ ] Seed: notification_schedules padrão (24h, 2h)
- [ ] Seed: notification_templates padrão (whatsapp + sms)

### Edge Functions
- [ ] Deploy: `process-notif-queue`
- [ ] Deploy: `notif-webhook`
- [ ] Configurar secrets: `EVOLUTION_API_KEY`, `ENCRYPTION_KEY`
- [ ] Testar com payload simulado

### Frontend
- [ ] Tela de configuração de canais
- [ ] Tela de templates (estender ConfiguracoesEmailTemplatesPage existente)
- [ ] Tela de regras de envio
- [ ] Tela de logs
- [ ] Adicionar consentimento no cadastro de paciente
- [ ] Adicionar opt-out no rodapé de emails

### Testes
- [ ] Teste E2E: criar sessão → verificar queue populada → verificar envio
- [ ] Teste: opt-out WhatsApp → verificar cancelamento na queue
- [ ] Teste: retry após falha → verificar exponential backoff
- [ ] Teste: idempotency → rodar populate 2x → verificar sem duplicatas
- [ ] Teste: tenant isolation → verificar RLS
- [ ] Teste de carga: 1000 mensagens na queue → medir throughput

### Monitoramento
- [ ] Alerta se queue > 500 itens pendentes
- [ ] Alerta se taxa de falha > 10% em 1h
- [ ] Dashboard de métricas (envios/dia, taxa de entrega, tempo médio de processamento)
- [ ] Log de erros no Supabase Logs

---

*Documento gerado como parte da arquitetura do sistema Agência PSI.*
*As implementações SQL e JavaScript estão nos arquivos separados referenciados abaixo.*
