# 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.*