Files
agenciapsilmno/docs/architecture/notification-reminders-system.md

24 KiB
Raw Permalink Blame History

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
  2. Modelagem do Banco de Dados
  3. Lógica de Agendamento (Cron + Edge Functions)
  4. Integração Evolution API (MVP)
  5. Integração API Oficial Meta (Escala)
  6. Frontend: Telas de Configuração
  7. LGPD e Boas Práticas
  8. Diagrama do Fluxo Completo
  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$ 99299/mês ~R$ 0.30/msg (conversa) €49/mês + msg R$ 0.150.40/msg
Setup 24h (Docker) 48h 30min (SaaS) 1h (SaaS) 12 semanas (aprovação) 35 dias 35 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 (0100 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.250.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

# 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

// 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

// 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:

{
  "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)

// 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.