# WhatsApp Setup — CRM de Conversas + Créditos + Automações Guia end-to-end do subsistema de WhatsApp do AgenciaPSI. Cobre **WhatsApp Pessoal** (Evolution, gratuito), **WhatsApp Oficial AgenciaPSI** (Twilio com créditos), **Asaas** (gateway de pagamento), e todas as automações (auto-reply, lembretes, opt-out, tags, notas). --- ## 🎯 Arquitetura ### Dois provedores, escolha exclusiva por tenant ``` ┌─────────────────────────────────────────────────┐ │ Tenant escolhe 1 canal em /configuracoes/whatsapp │ └─────────────────────────────────────────────────┘ │ │ ▼ ▼ ┌────────────────────┐ ┌──────────────────────┐ │ WhatsApp Pessoal │ │ WhatsApp Oficial │ │ (Evolution) │ │ AgenciaPSI (Twilio) │ │ │ │ │ │ • Gratuito │ │ • Consome créditos │ │ • QR code │ │ • API oficial Meta │ │ • Celular real │ │ • Zero ban risk │ │ • Docker self-host │ │ • Cloud gerenciado │ │ • Tier free do SaaS │ │ • Tier pago do SaaS │ └────────────────────┘ └──────────────────────┘ │ │ └───────────┬────────────────┘ ▼ ┌─────────────────────────┐ │ Edge Functions │ │ │ │ • send-whatsapp-message │ ← rota por provider │ • send-session-reminders │ ← idem │ • evolution-whatsapp-inbound (auto-reply, opt-out) │ • twilio-whatsapp-inbound (⚠ sem auto-reply ainda) │ • create-whatsapp-credit-charge (Asaas PIX) │ • asaas-webhook (credita saldo) └─────────────────────────┘ │ ▼ ┌─────────────────────────┐ │ PostgreSQL │ │ │ │ conversation_messages │ │ conversation_notes │ │ conversation_tags │ │ conversation_optouts │ │ conversation_autoreply_* │ │ session_reminder_* │ │ whatsapp_credits_* │ │ whatsapp_credit_packages │ └─────────────────────────┘ ``` ### Dedução de créditos ``` Usuário envia pelo drawer / lembrete dispara / auto-reply ↓ Edge function detecta provider do canal ativo ↓ Evolution? Twilio? ↓ ↓ Envia direto deduct_whatsapp_credits(1) ← atômico, lock, valida saldo ↓ ↓ Registra msg ┌──── OK ────┐ ┌── insufficient ──┐ ↓ ↓ send Twilio return 402 ↓ ┌── ok ──┐ ┌── fail ──┐ ↓ ↓ Registra add_whatsapp_credits(1, 'refund') msg ``` --- ## 🔧 Setup completo (dev local) ### 1. Supabase local + edge functions ```bash # Subir stack Supabase local (Postgres + Auth + Storage + etc) npx supabase start # Em outro terminal: rodar edge functions supabase functions serve --no-verify-jwt --env-file supabase/functions/.env ``` **Funções carregadas:** | Função | URL | Uso | |---|---|---| | `evolution-whatsapp-inbound` | `?tenant_id=` | Webhook Evolution: inbound msgs + auto-reply + opt-out | | `evolution-webhook-provision` | — | Configura webhook na Evolution | | `twilio-whatsapp-inbound` | `?tenant_id=` | Webhook Twilio (inbound only; sem auto-reply ainda) | | `send-whatsapp-message` | — | Envio unificado: detecta provider, deduz crédito se Twilio | | `send-session-reminders` | — | Cron/manual: dispara lembretes 24h e 2h antes | | `create-whatsapp-credit-charge` | — | Cria PIX Asaas pra compra de créditos | | `asaas-webhook` | — | Recebe eventos Asaas e credita saldo | | `deactivate-notification-channel` | — | Desativa canal (usado ao trocar provider) | **Flag `--no-verify-jwt`:** necessária porque webhooks externos (Twilio, Evolution, Asaas) não mandam JWT. ### 2. Evolution API (WhatsApp Pessoal — tier gratuito) ```bash # Subir Evolution + Postgres + Redis docker compose -f evolution-api/docker-compose.yml up -d # Verificar status docker ps --filter name=evolution_api ``` Evolution roda em `http://localhost:8080` com API key `minha_chave_123` (ver `evolution-api/docker-compose.yml`). ### 3. Asaas (pagamentos — tier pago) Ativa só em prod ou quando quiser testar compra de créditos end-to-end. **Passo 1 — Criar conta sandbox:** 1. https://sandbox.asaas.com (gratuito, CPF qualquer) 2. Menu → Integrações → Integrações Avançadas → API → copia a API key (começa com `$aact_...`) **Passo 2 — Configurar env:** Edita `supabase/functions/.env`: ```env ASAAS_API_KEY=$aact_sua_chave_aqui ASAAS_API_URL=https://sandbox.asaas.com/api/v3 ASAAS_WEBHOOK_TOKEN= # opcional, pra autenticar webhook ``` **Passo 3 — Reiniciar functions serve:** ```bash # Ctrl+C no terminal do serve supabase functions serve --no-verify-jwt --env-file supabase/functions/.env ``` **Passo 4 — (Opcional) Expor webhook via ngrok pro Asaas alcançar:** ```bash # Outro terminal ngrok http 54321 # Copia a URL (ex: https://abc123.ngrok.app) ``` Configura no Asaas: - Dashboard → Integrações → Webhooks → **Adicionar** - URL: `https://abc123.ngrok.app/functions/v1/asaas-webhook` - Eventos: marca **Cobranças** (PAYMENT_RECEIVED, PAYMENT_CONFIRMED, PAYMENT_OVERDUE, PAYMENT_DELETED, PAYMENT_REFUNDED) - Token (opcional): cadastra o mesmo valor de `ASAAS_WEBHOOK_TOKEN` **Em produção:** ```bash supabase secrets set ASAAS_API_KEY="$aact_prod_key" supabase secrets set ASAAS_API_URL="https://api.asaas.com/v3" supabase secrets set ASAAS_WEBHOOK_TOKEN="token_seguro" ``` Webhook da prod aponta pro URL real do Supabase cloud (sem ngrok). --- ## 📋 Features & como testar ### A. Envio manual via drawer **Onde:** drawer de qualquer conversa (clica no card do Kanban em `/therapist/conversas`) **Fluxo:** 1. Compose no drawer → `store.sendMessage()` → chama `send-whatsapp-message` edge function 2. Function detecta provider ativo em `notification_channels` 3. Evolution: envia direto via `/message/sendText/{instance}` 4. Twilio: `deduct_whatsapp_credits(1)` → se OK envia via Twilio API → se falhar, refunda 5. Registra em `conversation_messages` (direction=outbound, delivery_status=sent/queued) **Testar sem Twilio real** (valida dedução + rollback): - Topup 100 créditos via SQL (ver seção 🧪 mais abaixo) - Criar canal Twilio fake via SQL - Enviar msg → deduz, tenta enviar, falha com 401, refunda → saldo volta ao original ### B. Lembretes automáticos de sessão (2.4) **Onde:** `/configuracoes/lembretes-sessao` **Config:** - Toggle on/off - Ativa lembretes 24h e/ou 2h antes da sessão - Templates com variáveis: `{{nome_paciente}}`, `{{data_sessao}}`, `{{hora_sessao}}`, `{{modalidade}}`, `{{nome_clinica}}` - Quiet hours (default 22h-8h SP) - Respeitar opt-out (LGPD — recomendado ON) **Como dispara:** - Cron hit `send-session-reminders` a cada 15min (pg_cron comentado na migration; em prod configure via Supabase Dashboard → Database → Cron Jobs) - Em dev: botão **"Testar agora"** na página dispara manualmente **Query do worker:** busca `agenda_eventos` com `status='agendado'` dentro de: - Janela 24h: `inicio_em` entre `now+23h45min` e `now+24h15min` - Janela 2h: `inicio_em` entre `now+1h45min` e `now+2h15min` **Anti-dup:** UNIQUE `(event_id, reminder_type)` no `session_reminder_logs`. **Testar:** ```sql -- Cria evento daqui a ~2h (no horário de SP) -- Pelo UI da agenda é mais fácil; via SQL: INSERT INTO agenda_eventos (tenant_id, owner_id, patient_id, inicio_em, fim_em, status, modalidade, tipo, titulo) VALUES ( '', '', (SELECT id FROM patients WHERE tenant_id='' LIMIT 1), now() + interval '2 hours', now() + interval '3 hours', 'agendado', 'presencial', 'session', 'Teste lembrete' ); ``` Depois clica **"Testar agora"** em `/configuracoes/lembretes-sessao`. ### C. Auto-reply fora do horário (2.3) **Onde:** `/configuracoes/conversas-autoreply` **Config:** - Toggle on/off + mensagem + cooldown (minutos entre auto-replies pra mesma thread) - 3 modos: - **Seguir agenda** — usa `agenda_regras_semanais` dos membros ativos do tenant - **Horário de funcionamento** — janela semanal editável (armazena em JSONB `business_hours`) - **Custom** — janela específica pro auto-reply (`custom_window`) **Como dispara:** - Webhook Evolution `evolution-whatsapp-inbound` recebe msg - Depois de inserir msg, chama `maybeSendAutoReply()` - Checa: enabled, não está em horário útil, não está em cooldown - Se OK → envia via Evolution (futuro: rotear pra Twilio se provider='twilio') **⚠ Limitação:** atualmente **só funciona com Evolution**. Pra Twilio precisa implementar a mesma lógica em `twilio-whatsapp-inbound` (dívida técnica). **Testar:** - Ativa feature + define janela custom (ex: seg-sex 9h-18h) - Fora dessa janela, paciente manda msg → chega no inbox + auto-reply é enviado de volta em ~1s ### D. Opt-out LGPD (5.2) **Onde:** `/configuracoes/conversas-optouts` **Como funciona:** - Paciente envia "PARAR", "SAIR", "CANCELAR", "STOP", etc (keyword match case-insensitive sem acentos) - Edge function `evolution-whatsapp-inbound` detecta → registra em `conversation_optouts` → envia msg de confirmação - Paciente envia "VOLTAR" / "RETORNAR" → reativa (opted_back_in_at preenchido) - Auto-reply e lembretes **respeitam opt-out** automaticamente (skip + log `opted_out`) - Envio manual do terapeuta NÃO é bloqueado (relação terapêutica existe) **Keywords padrão:** 10 palavras (configuráveis na página — pode adicionar custom do tenant). **Testar:** - Manda mensagem com "parar" pelo WhatsApp conectado - Volta em `/configuracoes/conversas-optouts` → número aparece na lista - Nova mensagem que dispararia auto-reply → não dispara mais ### E. Notas internas (3.3) **Onde:** dentro do drawer de conversa, seção "Notas internas" collapsible **Como funciona:** - CRUD simples por thread - Visível apenas pra membros ativos do tenant - Edição/remoção só pelo criador (ou SaaS admin) - Soft delete (`deleted_at`) - **NÃO vai pro paciente** — apenas anotação interna da equipe ### F. Tags na conversa (3.1) **Onde:** - **Gestão:** `/configuracoes/conversas-tags` (CRUD de tags custom; system tags são read-only) - **Aplicação:** dentro do drawer de conversa + pills visíveis nos cards do Kanban **Tags system (seedadas):** Urgente (🔴), Primeira consulta (🔵), Remarcação (🟡), Confirmada (🟢), Follow-up (🟣) **Custom:** tenant cria suas próprias com nome + slug + cor + ícone (primeicons). ### G. Mídia (áudio / imagem / vídeo / documento) **Arquitetura:** - Evolution manda URLs encriptadas do Meta CDN (não tocam direto) - Edge function `evolution-whatsapp-inbound` chama `/chat/getBase64FromMediaMessage/{instance}` do Evolution → decripta - Decoda base64 → faz upload no bucket **privado `whatsapp-media`** - Path: `///_.` - Salva apenas o PATH em `media_url`, NÃO URL pública - Frontend (`ConversationDrawer`) gera **signed URL on-demand** (1h TTL) ao renderizar **LGPD:** bucket privado, RLS só permite membros ativos do tenant; path tenant-scoped; signed URLs expiram. **Player de áudio:** `