Files
agenciapsilmno/WHATSAPP_SETUP.md
T
Leonardo 2644e60bb6 CRM WhatsApp Grupo 3 completo + Marco A/B (Asaas) + admin SaaS + refactors polimórficos
Sessão 11+: fechamento do CRM de WhatsApp com dois providers (Evolution/Twilio),
sistema de créditos com Asaas/PIX, polimorfismo de telefones/emails, e integração
admin SaaS no /saas/addons existente.

═══════════════════════════════════════════════════════════════════════════
GRUPO 3 — WORKFLOW / CRM (completo)
═══════════════════════════════════════════════════════════════════════════

3.1 Tags · migration conversation_tags + seed de 5 system tags · composable
useConversationTags.js · popover + pills no drawer e nos cards do Kanban.

3.2 Atribuição de conversa a terapeuta · migration 20260421000012 com PK
(tenant_id, thread_key), UPSERT, RLS que valida assignee como membro ativo
do mesmo tenant · view conversation_threads expandida com assigned_to +
assigned_at · composable useConversationAssignment.js · drawer com Select
filtrável + botão "Assumir" · inbox com filtro aside (Todas/Minhas/Não
atribuídas) e chip do responsável em cada card (destaca "Eu" em azul).

3.3 Notas internas · migration conversation_notes · composable + seção
colapsável no drawer · apenas o criador pode editar/apagar (RLS).

3.5 Converter desconhecido em paciente · botão + dialog quick-cadastro ·
"Vincular existente" com Select filter de até 500 pacientes · cria
telefone WhatsApp (vinculado) via upsertWhatsappForExisting.

3.6 Histórico de conversa no prontuário · nova aba "Conversas" em
PatientProntuario.vue · PatientConversationsTab.vue com stats (total /
recebidas / enviadas / primeira / última), SelectButton de filtro, timeline
com bolhas por direção, mídia inline (imagem/áudio/vídeo/doc via signed
URL), indicadores ✓ ✓✓ de delivery, botão "Abrir no CRM".

═══════════════════════════════════════════════════════════════════════════
MARCO A — UNIFICAÇÃO WHATSAPP (dois providers mutuamente exclusivos)
═══════════════════════════════════════════════════════════════════════════

- Página chooser ConfiguracoesWhatsappChooserPage.vue com 2 cards (Pessoal/
  Oficial), deactivate via edge function deactivate-notification-channel
- send-whatsapp-message refatorada com roteamento por provider; Twilio deduz
  1 crédito antes do envio e refunda em falha
- Paridade Twilio (novo): módulo compartilhado supabase/functions/_shared/
  whatsapp-hooks.ts com lógica provider-agnóstica (opt-in, opt-out, auto-
  reply, schedule helpers em TZ São Paulo, makeTwilioCreditedSendFn que
  envolve envio em dedução atômica + rollback). Consumido por Evolution E
  Twilio inbound. Evolution refatorado (~290 linhas duplicadas removidas).
- Bucket privado whatsapp-media · decrypt via Evolution getBase64From
  MediaMessage · upload com path tenant/yyyy/mm · signed URLs on-demand

═══════════════════════════════════════════════════════════════════════════
MARCO B — SISTEMA DE CRÉDITOS WHATSAPP + ASAAS
═══════════════════════════════════════════════════════════════════════════

Banco:
- Migration 20260421000007_whatsapp_credits (4 tabelas: balance,
  transactions, packages, purchases) + RPCs add_whatsapp_credits e
  deduct_whatsapp_credits (atômicas com SELECT FOR UPDATE)
- Migration 20260421000013_tenant_cpf_cnpj (coluna em tenants com CHECK
  de 11 ou 14 dígitos)

Edge functions:
- create-whatsapp-credit-charge · Asaas v3 (sandbox + prod) · PIX com
  QR code · getOrCreateAsaasCustomer patcha customer existente com CPF
  quando está faltando
- asaas-webhook · recebe PAYMENT_RECEIVED/CONFIRMED e credita balance

Frontend (tenant):
- Página /configuracoes/creditos-whatsapp com saldo + loja + histórico
- Dialog de confirmação com CPF/CNPJ (validação via isValidCPF/CNPJ de
  utils/validators, formatação on-blur, pré-fill de tenants.cpf_cnpj,
  persiste no primeiro uso) · fallback sandbox 24971563792 REMOVIDO
- Composable useWhatsappCredits extrai erros amigáveis via
  error.context.json()

Frontend (SaaS admin):
- Em /saas/addons (reuso do pattern existente, não criou página paralela):
  - Aba 4 "Pacotes WhatsApp" — CRUD whatsapp_credit_packages com DataTable,
    toggle is_active inline, dialog de edição com validação
  - Aba 5 "Topup WhatsApp" — tenant Select com saldo ao vivo · RPC
    add_whatsapp_credits com p_admin_id = auth.uid() (auditoria) · histórico
    das últimas 20 transações topup/adjustment/refund

═══════════════════════════════════════════════════════════════════════════
GRUPO 2 — AUTOMAÇÃO
═══════════════════════════════════════════════════════════════════════════

2.3 Auto-reply · conversation_autoreply_settings + conversation_autoreply_
log · 3 modos de schedule (agenda das regras semanais, business_hours
custom, custom_window) · cooldown por thread · respeita opt-out · agora
funciona em Evolution E Twilio (hooks compartilhados)

2.4 Lembretes de sessão · conversation_session_reminders_settings +
_logs · edge send-session-reminders (cron) · janelas 24h e 2h antes ·
Twilio deduz crédito com rollback em falha

═══════════════════════════════════════════════════════════════════════════
GRUPO 5 — COMPLIANCE (LGPD Art. 18 §2)
═══════════════════════════════════════════════════════════════════════════

5.2 Opt-out · conversation_optouts + conversation_optout_keywords (10 system
seed + custom por tenant) · detecção por regex word-boundary e normalização
(lowercase + strip acentos + pontuação) · ack automático (deduz crédito em
Twilio) · opt-in via "voltar", "retornar", "reativar", "restart" ·
página /configuracoes/conversas-optouts com CRUD de keywords

═══════════════════════════════════════════════════════════════════════════
REFACTOR POLIMÓRFICO — TELEFONES + EMAILS
═══════════════════════════════════════════════════════════════════════════

- contact_types + contact_phones (entity_type + entity_id) — migration
  20260421000008 · contact_email_types + contact_emails — 20260421000011
- Componentes ContactPhonesEditor.vue e ContactEmailsEditor.vue (add/edit/
  remove com confirm, primary selector, WhatsApp linked badge)
- Composables useContactPhones.js + useContactEmails.js com
  unsetOtherPrimaries() e validação
- Trocado em PatientsCadastroPage.vue e MedicosPage.vue (removidos campos
  legados telefone/telefone_alternativo e email_principal/email_alternativo)
- Migration retroativa v2 (20260421000010) detecta conversation_messages
  e cria/atualiza phone como WhatsApp vinculado

═══════════════════════════════════════════════════════════════════════════
POLIMENTO VISUAL + INFRA
═══════════════════════════════════════════════════════════════════════════

- Skeletons simplificados no dashboard do terapeuta
- Animações fade-up com stagger via [--delay:Xms] (fix specificity sobre
  .dash-card box-shadow transition)
- ConfirmDialog com group="conversation-drawer" (evita montagem duplicada)
- Image preview PrimeVue com botão de download injetado via MutationObserver
  (fetch + blob para funcionar cross-origin)
- Áudio/vídeo com preload="metadata" e controles de velocidade do browser
- friendlySendError() mapeia códigos do edge pra mensagens pt-BR via
  error.context.json()
- Teleport #cfg-page-actions para ações globais de Configurações
- Brotli/Gzip + auto-import Vue/PrimeVue + bundle analyzer
- AppLayout consolidado (removidas duplicatas por área) + RouterPassthrough
- Removido console.trace debug que estava em watch de router e queries
  Supabase (degradava perf pra todos)
- Realtime em conversation_messages via publication supabase_realtime
- Notifier global flutuante com beep + toggle mute (4 camadas: badge +
  sino + popup + browser notification)

═══════════════════════════════════════════════════════════════════════════
MIGRATIONS NOVAS (13)
═══════════════════════════════════════════════════════════════════════════

20260420000001_patient_intake_invite_info_rpc
20260420000002_audit_logs_lgpd
20260420000003_audit_logs_unified_view
20260420000004_lgpd_export_patient_rpc
20260420000005_conversation_messages
20260420000005_search_global_rpc
20260420000006_conv_messages_notifications
20260420000007_notif_channels_saas_admin_insert
20260420000008_conv_messages_realtime
20260420000009_conv_messages_delivery_status
20260421000001_whatsapp_media_bucket
20260421000002_conversation_notes
20260421000003_conversation_tags
20260421000004_conversation_autoreply
20260421000005_conversation_optouts
20260421000006_session_reminders
20260421000007_whatsapp_credits
20260421000008_contact_phones
20260421000009_retroactive_whatsapp_link
20260421000010_retroactive_whatsapp_link_v2
20260421000011_contact_emails
20260421000012_conversation_assignments
20260421000013_tenant_cpf_cnpj

═══════════════════════════════════════════════════════════════════════════
EDGE FUNCTIONS NOVAS / MODIFICADAS
═══════════════════════════════════════════════════════════════════════════

Novas:
- _shared/whatsapp-hooks.ts (módulo compartilhado)
- asaas-webhook
- create-whatsapp-credit-charge
- deactivate-notification-channel
- evolution-webhook-provision
- evolution-whatsapp-inbound
- get-intake-invite-info
- notification-webhook
- send-session-reminders
- send-whatsapp-message
- submit-patient-intake
- twilio-whatsapp-inbound

═══════════════════════════════════════════════════════════════════════════
FRONTEND — RESUMO
═══════════════════════════════════════════════════════════════════════════

Composables novos: useAddonExtrato, useAuditoria, useAutoReplySettings,
useClinicKPIs, useContactEmails, useContactPhones, useConversationAssignment,
useConversationNotes, useConversationOptouts, useConversationTags,
useConversations, useLgpdExport, useSessionReminders, useWhatsappCredits

Stores: conversationDrawerStore

Componentes novos: ConversationDrawer, GlobalInboundNotifier, GlobalSearch,
ContactEmailsEditor, ContactPhonesEditor

Páginas novas: CRMConversasPage, PatientConversationsTab, AddonsExtratoPage,
AuditoriaPage, NotificationsHistoryPage, ConfiguracoesWhatsappChooserPage,
ConfiguracoesConversasAutoreplyPage, ConfiguracoesConversasOptoutsPage,
ConfiguracoesConversasTagsPage, ConfiguracoesCreditosWhatsappPage,
ConfiguracoesLembretesSessaoPage

Utils novos: addonExtratoExport, auditoriaExport, excelExport,
lgpdExportFormats

Páginas existentes alteradas: ClinicDashboard, PatientsCadastroPage,
PatientCadastroDialog, PatientsListPage, MedicosPage, PatientProntuario,
ConfiguracoesWhatsappPage, SaasWhatsappPage, ConfiguracoesRecursosExtrasPage,
ConfiguracoesPage, AgendaTerapeutaPage, AgendaClinicaPage, NotificationItem,
NotificationDrawer, AppLayout, AppTopbar, useMenuBadges,
patientsRepository, SaasAddonsPage (aba 4 + 5 WhatsApp)

Routes: routes.clinic, routes.configs, routes.therapist atualizados
Menus: clinic.menu, therapist.menu, saas.menu atualizados

═══════════════════════════════════════════════════════════════════════════
NOTAS

- Após subir, rodar supabase functions serve --no-verify-jwt
  --env-file supabase/functions/.env pra carregar o módulo _shared
- WHATSAPP_SETUP.md reescrito (~400 linhas) com setup completo dos 3
  providers + troubleshooting + LGPD
- HANDOFF.md atualizado com estado atual e próximos passos

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 07:05:24 -03:00

22 KiB

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

# 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=<uuid> Webhook Evolution: inbound msgs + auto-reply + opt-out
evolution-webhook-provision Configura webhook na Evolution
twilio-whatsapp-inbound ?tenant_id=<uuid> 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)

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

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:

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

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

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:

-- 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 (
  '<tenant>',
  '<user>',
  (SELECT id FROM patients WHERE tenant_id='<tenant>' 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: <tenant_id>/<yyyy>/<mm>/<msg_id>_<timestamp>.<ext>
  • 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: <audio min-w-[260px] controls> + preload="metadata" pra mostrar duração sem baixar tudo.

Preview de imagem: <Image preview> do PrimeVue (fullscreen com zoom/rotate) + botão download injetado via MutationObserver (fetch blob → download attr → force download com nome do arquivo).

H. Créditos WhatsApp (Marco B)

Onde: /configuracoes/creditos-whatsapp

Fluxo de compra:

  1. User clica "Comprar" num pacote → create-whatsapp-credit-charge cria:
    • Customer Asaas (ou reutiliza via externalReference=tenant_id)
    • Pagamento PIX (billingType=PIX, value, dueDate)
    • Busca QR Code em /payments/{id}/pixQrCode
  2. Dialog mostra QR + copia-cola + link cartão
  3. User paga
  4. Asaas → webhook asaas-webhook recebe PAYMENT_RECEIVED/CONFIRMED
  5. Webhook chama add_whatsapp_credits(tenant, credits, 'purchase') → saldo atualiza

Pacotes seedados: Iniciante (100/R$49,90), Profissional (500/R$199,90, featured), Clínica (1500/R$499,90), Enterprise (5000/R$1499,90) — editáveis via DB.

RPCs atômicas (SECURITY DEFINER):

  • add_whatsapp_credits(tenant, amount, kind, purchase_id, admin_id, note) → retorna novo saldo
  • deduct_whatsapp_credits(tenant, amount, msg_id, note) → atômico com SELECT FOR UPDATE; lança insufficient_credits se saldo < amount

CPF: fallback hardcoded 24971563792 em sandbox. Em produção, frontend deve coletar CPF real do user (TODO — adicionar input no dialog + salvar em profile/tenant).


🧪 Testar sem provedor real

Pré-requisito: creditar saldo

No Supabase Studio (http://localhost:54323) → SQL Editor (desativa LIMIT 100 no dropdown):

SELECT public.add_whatsapp_credits(
    tm.tenant_id,
    100,
    'topup_manual',
    NULL,
    auth.uid(),
    'Topup de teste'
) AS novo_saldo
FROM public.tenant_members tm
JOIN auth.users u ON u.id = tm.user_id
WHERE u.email = 'SEU_EMAIL'
  AND tm.status = 'active'
LIMIT 1;

Simular canal Twilio fake (pra testar dedução)

-- Desativa Evolution
UPDATE public.notification_channels
SET is_active = false, deleted_at = now()
WHERE tenant_id = (
    SELECT tm.tenant_id FROM public.tenant_members tm
    JOIN auth.users u ON u.id = tm.user_id
    WHERE u.email = 'SEU_EMAIL' AND tm.status = 'active' LIMIT 1
)
AND channel = 'whatsapp' AND deleted_at IS NULL;

-- Cria Twilio fake
INSERT INTO public.notification_channels (
    tenant_id, owner_id, channel, provider, is_active,
    twilio_subaccount_sid, twilio_phone_number, credentials
)
SELECT
    tm.tenant_id, u.id, 'whatsapp', 'twilio', true,
    'ACfake0000000000000000000000000000',
    '+15557775555',
    '{"subaccount_auth_token": "fake_token"}'::jsonb
FROM public.tenant_members tm
JOIN auth.users u ON u.id = tm.user_id
WHERE u.email = 'SEU_EMAIL' AND tm.status = 'active'
LIMIT 1;

Agora qualquer envio:

  • Deduz 1 crédito (usage -1)
  • Twilio API retorna 401 (creds fake)
  • Refunda automaticamente (refund +1)
  • Saldo final: igual ao inicial

Resultado esperado no extrato:

+1  Estorno   — Refund envio falhou: Twilio 401: ...
-1  Uso       — Envio manual WhatsApp
+100 Topup manual — Topup de teste

Simular mensagem inbound via curl

curl -X POST "http://localhost:54321/functions/v1/evolution-whatsapp-inbound?tenant_id=<uuid>" \
  -H "Content-Type: application/json" \
  -d '{
    "event": "messages.upsert",
    "data": {
      "key": { "remoteJid": "5516912345678@s.whatsapp.net", "fromMe": false, "id": "MSG_TEST" },
      "message": { "conversation": "parar" },
      "messageTimestamp": '"$(date +%s)"'
    }
  }'

Isso vai testar detecção de opt-out ("parar" é keyword) → registra na lista + envia ACK.

Testar Twilio sandbox real (opcional)

  1. Cria conta trial em https://www.twilio.com/try-twilio (US$15 grátis)
  2. Dashboard → Messaging → Try it out → Send a WhatsApp Message
  3. Segue instruções pra join sandbox (join <frase> do seu celular pro número Twilio)
  4. Pega Account SID + Auth Token + From number do sandbox
  5. Atualiza o canal Twilio no SQL com valores reais
  6. Envia mensagem do drawer → chega no seu celular

🆘 Troubleshooting

Sintoma Causa provável Fix
404 no webhook supabase functions serve não rodando OU function não carregada Reinicia serve com --env-file
502 edge function Crash antes do console.error — erro de sintaxe, env var faltando, ou API externa timeout Olha logs do serve; adiciona console.log em cada etapa
Asaas 400 "CPF obrigatório" Customer existente sem CPF Fix em getOrCreateAsaasCustomer faz PATCH com CPF quando falta
QR code PIX não aparece Asaas sandbox sem PIX habilitado OU endpoint /pixQrCode falhou Ativa PIX em Asaas → Recebimentos → Chaves PIX
Webhook Asaas não chega URL não pública (localhost) Usa ngrok OU deploy em prod
"insufficient_credits" no envio Saldo zerado Topup via /configuracoes/creditos-whatsapp ou SQL manual
Auto-reply não dispara Tenant fora do modo horário configurado OU em cooldown OU opted-out Checa banner de status em /configuracoes/conversas-autoreply; olha conversation_autoreply_log
Lembrete duplicado Não acontece — UNIQUE (event_id, reminder_type) previne
Audio não toca (era o ponto inicial do Marco A-I) URL encriptada do Meta salva direto Fix: edge function agora decripta via getBase64FromMediaMessage + upload pro bucket
Mime audio/ogg; codecs=opus rejeitado pelo bucket allowed_mime_types faz match exato Fix: strip ;codecs=... antes do upload
Dialog de confirmação aparece 2x Dois <ConfirmDialog> montados (página + drawer global) Fix: drawer usa group="conversation-drawer" pra isolar

🔒 Segurança & Compliance

RLS de conversation_messages

  • SELECT: tenant members ativos OU saas_admin
  • INSERT direto: bloqueado (só service_role via edge function)
  • UPDATE: tenant members podem mudar kanban_status, read_at
  • DELETE: bloqueado

RLS de whatsapp-media bucket

  • Privado
  • Read: membros ativos do tenant cujo id é o primeiro segmento do path
  • Write: apenas service_role
  • Delete: apenas saas_admin

RLS de whatsapp_credits_*

  • Balance/transactions: tenant members leem
  • Escrita: via RPCs add_whatsapp_credits / deduct_whatsapp_credits (SECURITY DEFINER)
  • Packages: tenant members leem os ativos; saas_admin gerencia

LGPD

  • Art. 18 §2 (direito de oposição): opt-out implementado. Auto-reply + lembretes respeitam.
  • Dados clínicos: canal Oficial (Twilio) recomendado em prod. Pessoal (Evolution) é uso informal, sem SLA, tenant assume risco.
  • Audit log: toda transação de crédito fica em whatsapp_credits_transactions (append-only).

Bot defense / Rate limiting

  • public_submission_attempts, submission_rate_limits e math_challenges são tabelas do sistema de bot defense pro cadastro externo. Não relacionado ao WhatsApp, mas protege fluxo paralelo.

📚 Referência de arquivos

Edge functions (supabase/functions/)

  • evolution-whatsapp-inbound/ — webhook Evolution (msgs inbound + auto-reply + opt-out + media)
  • evolution-webhook-provision/ — configura webhook na Evolution
  • twilio-whatsapp-inbound/ — webhook Twilio (inbound only, sem automações ainda)
  • twilio-whatsapp-provision/ — provisiona subconta Twilio (SaaS admin only)
  • send-whatsapp-message/ — envio unificado (Evolution ou Twilio com dedução)
  • send-session-reminders/ — worker de lembretes (chamado por cron)
  • create-whatsapp-credit-charge/ — cria PIX Asaas
  • asaas-webhook/ — recebe eventos Asaas e credita saldo
  • deactivate-notification-channel/ — soft-delete de canal (via service_role)

Composables (src/composables/)

  • useConversations.js — Kanban threads
  • useConversationNotes.js — notas internas
  • useConversationTags.js — tags CRUD + apply
  • useConversationOptouts.js — opt-outs + keywords
  • useAutoReplySettings.js — config auto-reply
  • useSessionReminders.js — config lembretes + logs
  • useWhatsappCredits.js — saldo + loja + extrato

Páginas principais (src/layout/configuracoes/)

  • ConfiguracoesWhatsappChooserPage.vue — landing/chooser
  • ConfiguracoesWhatsappPage.vue — setup Evolution (Pessoal)
  • ConfiguracoesTwilioWhatsappPage.vue — setup Twilio (Oficial, rebrandeado)
  • ConfiguracoesConversasTagsPage.vue — CRUD tags
  • ConfiguracoesConversasOptoutsPage.vue — lista opt-outs + keywords
  • ConfiguracoesConversasAutoreplyPage.vue — auto-reply
  • ConfiguracoesLembretesSessaoPage.vue — lembretes
  • ConfiguracoesCreditosWhatsappPage.vue — saldo + loja + histórico

Tabelas principais (database-novo/schema/)

Ver dashboard interativo: database-novo/agenciapsi-db-dashboard.html (regenerado via node db.cjs dashboard).

Domínios no dashboard:

  • CRM Conversas (WhatsApp) — todas as tabelas de conversas, notas, tags, opt-outs, auto-reply, lembretes
  • Addons / Créditos — créditos WhatsApp (balance, transactions, packages, purchases)
  • Comunicação / Notificações — canais, templates, queue

📌 Dívidas técnicas conhecidas

  1. Auto-reply via Twilio — hoje só funciona com Evolution. Precisa portar lógica pra twilio-whatsapp-inbound.
  2. Opt-out via Twilio — idem (keyword detection no inbound Twilio).
  3. Admin SaaS UI de créditos — topup manual + gestão de pacotes (hoje via SQL).
  4. Input de CPF real no dialog de compra (hoje fallback hardcoded em sandbox).
  5. Alerta de saldo baixo — estrutura DB pronta (low_balance_threshold, low_balance_alerted_at), falta trigger/notification.
  6. Reconnect Evolution automático — Grupo 6.3 do roadmap (heartbeat + reconnect).
  7. pg_cron em prod — migration tem bloco comentado; ativar via Supabase Dashboard → Database → Cron Jobs ou descomentar após setar app.settings.service_role_key.

Última atualização: 2026-04-21 (sessão de features CRM WhatsApp + Marco B Créditos)