name: comercialrs-whatsapp-meta-reply-update
description: Sistema bidirecional WhatsApp — recebe respostas dos corretores e atualiza tasks.status no ComercialRS via Meta Webhook
tags: [whatsapp, meta, webhook, supabase-edge-functions, comercialrs]
ComercialRS — WhatsApp Bidirecional: Resposta do Corretor Atualiza Task
Resumo
Sistema para receber respostas de WhatsApp dos corretores e atualizar automaticamente o status da task no ComercialRS. Funciona em conjunto com o dispatch-automation (envio) e a Meta WhatsApp Business API.
Arquitetura do Fluxo
`
dispatch-automation (envia)
→ Meta API → WhatsApp do corretor
→ logMessageSent() → message_log (meta_message_id + task_id + responsible_id)
Corretor responde no WhatsApp
→ Meta chama POST /functions/v1/meta-webhook
→ webhook detecta context.id (reply)
→ busca task em message_log
→ verifica responsible_id
→ parseStatus() → in_progress/completed/remarketing
→ updateTask() → tasks.status
→ logReply() → message_replies
→ sendReply() → confirmação via WhatsApp
`
Bancos de Dados
Tabelas necessárias
`sql
CREATE TABLE message_log (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
meta_message_id TEXT UNIQUE,
task_id UUID REFERENCES tasks(id) ON DELETE SET NULL,
responsible_id UUID REFERENCES team_members(id) ON DELETE SET NULL,
template_name TEXT,
sent_at TIMESTAMPTZ DEFAULT NOW(),
phone TEXT,
status TEXT DEFAULT 'sent'
);
CREATE TABLE message_replies (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
message_log_id UUID REFERENCES message_log(id) ON DELETE SET NULL,
meta_message_id TEXT,
from_phone TEXT,
response_text TEXT,
parsed_status TEXT,
task_id UUID REFERENCES tasks(id) ON DELETE SET NULL,
processed_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE incoming_messages (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
message_id TEXT UNIQUE,
from_number TEXT,
timestamp TEXT,
msg_type TEXT,
body TEXT,
source TEXT DEFAULT 'meta_webhook',
created_at TIMESTAMPTZ DEFAULT NOW()
);
ALTER TABLE tasks ADD COLUMN IF NOT EXISTS last_meta_message_id TEXT;
`
Índices
`sql
CREATE INDEX idx_message_log_meta_message_id ON message_log(meta_message_id);
CREATE INDEX idx_message_log_task_id ON message_log(task_id);
CREATE INDEX idx_message_replies_message_log_id ON message_replies(message_log_id);
CREATE INDEX idx_message_replies_task_id ON message_replies(task_id);
`
RLS Policies
`sql
ALTER TABLE message_log ENABLE ROW LEVEL SECURITY;
ALTER TABLE message_replies ENABLE ROW LEVEL SECURITY;
ALTER TABLE incoming_messages ENABLE ROW LEVEL SECURITY;
CREATE POLICY "service_role_full_message_log" ON message_log FOR ALL USING (auth.jwt() ->> 'role' = 'service_role');
CREATE POLICY "anon_read_message_log" ON message_log FOR SELECT USING (true);
CREATE POLICY "service_role_full_message_replies" ON message_replies FOR ALL USING (auth.jwt() ->> 'role' = 'service_role');
CREATE POLICY "anon_insert_message_replies" ON message_replies FOR INSERT WITH CHECK (true);
CREATE POLICY "service_role_full_incoming" ON incoming_messages FOR ALL USING (auth.jwt() ->> 'role' = 'service_role');
CREATE POLICY "anon_insert_incoming" ON incoming_messages FOR INSERT WITH CHECK (true);
`
Modificar dispatch-automation
1. sendMetaTemplate retornar messageId
`typescript
interface SendMetaResult {
ok: boolean;
messageId?: string;
error?: string;
}
async function sendMetaTemplate(...): Promise
// ... existing code ...
if (resp.ok && body.messages?.[0]?.id) {
return { ok: true, messageId: body.messages[0].id };
}
return { ok: false, error: JSON.stringify(body) };
}
`
2. Registrar envio em message_log
`typescript
async function logMessageSent(
serviceKey: string,
metaMessageId: string,
taskId: string | null,
responsibleId: string | null,
templateName: string,
phone: string
): Promise
await fetch(${supabaseUrl}/rest/v1/message_log, {
method: 'POST',
headers: {
Authorization: Bearer ${serviceKey},
apikey: serviceKey,
'Content-Type': 'application/json',
Prefer: 'resolution=merge-duplicates'
},
body: JSON.stringify([{
meta_message_id: metaMessageId,
task_id: taskId,
responsible_id: responsibleId,
template_name: templateName,
phone: phone,
status: 'sent'
}])
}).catch(err => console.error('[logMessageSent] failed:', err));
}
`
3. Chamar logMessageSent após envio
`typescript
const result = await sendMetaTemplate(config, member.phone, templateName, langCode, params);
if (result.ok && result.messageId) {
logMessageSent(
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!,
result.messageId,
taskId, // incluir taskId
responsibleId,
templateName,
member.phone
);
}
`
meta-webhook — Edge Function (Deno)
Regras CRÍTICAS
- - NÃO usar
Buffer— não existe no runtime Deno (causa BOOT_ERROR) - - NÃO usar
process.env— usarDeno.env.get() - - GET handler — retornar com
Content-Type: text/plain - - URL:
https://comercialrs.rochasalesseguros.com.br/functions/v1/meta-webhook - - Verify token: definido no código (
VERIFY_TOKEN) - - Campos:
messages,message_deliveries,message_reads - - App separado do Chatwoot (App ID diferente)
- - Usa
supabase functions deploy - - Roda no ambiente Deno do Supabase
- - Sem
Buffer, semprocess.env, sem@supabase/supabase-js - - Python 3 rodando na VPS na porta 8083
- - Conexão direta ao PostgreSQL via
psycopg2(IP172.23.0.2, porta5432) - - NÃO passa pelo PostgREST — evita problemas de SSL e JWT
- - Arquivo:
/var/www/comercialrs/data/meta_webhook_vps.py - - Nginx:
/etc/nginx/snippets/meta-webhook-proxy.conf(/meta-webhook/→127.0.0.1:8083) - - Reiniciar:
fuser -k 8083/tcp; nohup python3 -u meta_webhook_vps.py > meta_webhook_vps.log 2>&1 & - -
/var/www/comercialrs/data/meta_webhook_vps_service.py← este é o que roda em produção (systemd servicemeta-webhook-vps.service) - -
/var/www/comercialrs/data/meta_webhook_vps.py← referência, também corrigido
parseStatus()
`typescript
const STATUS_KEYWORDS: Record
'in_progress': ['iniciada', 'comecou', 'andamento', 'start', 'started'],
'completed': ['concluida', 'concluída', 'finalizada', 'done', 'fechada'],
'remarketing': ['remarketing', 'remar', 'rmk', 'remercar']
};
function parseStatus(text: string): string | null {
const normalized = text.toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').trim();
for (const [status, keywords] of Object.entries(STATUS_KEYWORDS)) {
if (keywords.some(kw => normalized.includes(kw))) return status;
}
return null;
}
`
phoneMatch() — validação flexível
`typescript
function phonesMatch(a: string, b: string): boolean {
const na = a.replace(/\D/g, '').replace(/^55/, '');
const nb = b.replace(/\D/g, '').replace(/^55/, '');
return na === nb || na.endsWith(nb) || nb.endsWith(na);
}
`
Configuração na Meta
Deploy
`bash
cd /var/www/comercialrs
SUPABASE_ACCESS_TOKEN=sbp_REDACTED \
supabase functions deploy meta-webhook --project-ref dauftiqcvgaydddoxqhh
`
Verificação
`bash
curl -s --max-time 10 "https://comercialrs.rochasalesseguros.com.br/functions/v1/meta-webhook?hub.mode=subscribe&hub.verify_token=TOKEN&hub.challenge=test" \
-H "Authorization: Bearer
-H "apikey:
Deve retornar "test" com status 200
`
Abordagem Implementada
Existem DUAS abordagens паралелas:
Abordagem A: Edge Function (Supabase Cloud)
Abordagem B: VPS Python Server (produção atual — PREFERIDA)
Configuração do Token da Meta (VPS)
O token e Phone Number ID da Meta ficam na tabela auth.automations_config:
`sql
-- Adicionar colunas se não existirem (tabela pode estar vazia — sem risco)
ALTER TABLE auth.automations_config ADD COLUMN IF NOT EXISTS meta_access_token TEXT;
ALTER TABLE auth.automations_config ADD COLUMN IF NOT EXISTS meta_phone_number_id TEXT;
ALTER TABLE auth.automations_config ADD COLUMN IF NOT EXISTS meta_webhook_verify_token TEXT;
ALTER TABLE auth.automations_config ADD COLUMN IF NOT EXISTS meta_business_account_id TEXT;
-- Inserir token (se tabela vazia)
INSERT INTO auth.automations_config
(meta_access_token, meta_phone_number_id, meta_webhook_verify_token, created_at, updated_at)
VALUES
('EAAAK...', 'PHONE_NUMBER_ID', 'VERIFY_TOKEN', NOW(), NOW())
ON CONFLICT DO NOTHING;
`
CRÍTICO: Phone Number ID e Token DEVEM pertencer ao MESMO App na Meta.
Se der erro 400 "Object with ID X does not exist" na Send API, o Phone Number ID
pertence a um App diferente do token. Verificar em developers.facebook.com → App →
WhatsApp → API Setup qual Phone Number ID aparece junto do token.
Fluxo Completo: Como as Peças se Conectam
`
dispatch-automation envia notificação
→ Meta API retorna wamid (meta_message_id)
→ dispatch-automation DEVE gravar em message_log (task_id + meta_message_id + phone + responsible_id)
Corretor responde no WhatsApp
→ Meta POST webhook (context.id = meta_message_id original)
→ webhook busca em message_log pelo meta_message_id
→ encontra task_id → atualiza tasks.status
→ Kanban atualiza (polling 15s)
`
PONTO CRÍTICO: Se o dispatch-automation NÃO gravar o meta_message_id no message_log, o webhook NUNCA vai conseguir correlacionar a resposta à task. Esse foi o bug raiz de "não atualiza" no Kanban. O logMessageSent() precisa ser chamado logo após cada sendMetaTemplate() com o messageId retornado pela Meta.
Kanban: Realtime vs Polling
Supabase Cloud Realtime (WebSocket) NÃO funciona via proxy — o endpoint
wss://dauftiqcvgaydddoxqhh.supabase.co/realtime/v1/websocket retorna HTTP 500
quando passado pelo nginx/Cloudflare. O polling de 15s é o mecanismo ativo.
Polling configurado em AppDataContext.tsx:
`typescript
const pollInterval = 15000; // 15 segundos
`
Para forçar recarga no navegador após deploy: Ctrl+Shift+R (hard refresh).
RLS: Policies em auth.tasks
Para o PostgREST conseguir ler auth.tasks (via RPC ou SELECT direto),
duas policies SELECT são obrigatórias:
`sql
-- Sem estas, o PostgREST devolve "Database error querying schema"
CREATE POLICY "authenticated_select_tasks" ON auth.tasks
FOR SELECT TO authenticated USING (true);
CREATE POLICY "anon_select_tasks" ON auth.tasks
FOR SELECT TO anon USING (true);
`
⚠️ BUG CRÍTICO: find_task() ordering —task errada atualizada
Sintoma: Vendedor clica "Iniciada" numa reunião, mas o sistema atualiza a sala errada (outra reunião mais recente em vez da mais urgente).
Causa raiz: find_task() em meta_webhook_vps_service.py usava:
`python
"ORDER BY created_at DESC LIMIT 1" # pegava a task MAIS RECENTE
`
Correção (aplicada em 2026-05-29):
`python
"ORDER BY due_date ASC NULLS LAST LIMIT 1" # pega a task mais ANTIGA por vencimento
`
Importante: existem DOIS arquivos com a mesma lógica — corrigir AMBOS:
Após editar, restartar o serviço:
`bash
cp /var/www/comercialrs/data/meta_webhook_vps_service.py \
/var/www/comercialrs/data/meta_webhook_vps_service.py.bak.$(date +%Y%m%d%H%M%S)
systemctl restart meta-webhook-vps
`
Armadilhas descobertas
1. BOOT_ERROR com Buffer — Buffer.from() não existe no Deno. Usar new TextEncoder().encode() + crypto.subtle para HMAC, ou strings puro sem crypto.
2. Meta duplica params na URL — a Meta envia hub_mode, hub_challenge, hub_verify_token juntos com os params normais. Handler deve funcionar com ambos.
3. 403 no GET após BOOT_ERROR — a Meta pode ter cacheado tentativa falhada. Clicar "Verificar" novamente no painel do Meta for Developers.
4. Confirmação via session message — funciona só dentro da janela de 24h após última msg do cliente. Usar session reply (sem template) para confirmar.
5. NLP fora do escopo — regex simples com keywords em português é suficiente para o caso de uso.
6. PostgREST SSL error na VPS — localhost:3000 dá [SSL: WRONG_VERSION_NUMBER]. O Python server deve usar psycopg2 direto (IP 172.23.0.2) em vez de chamar PostgREST.
7. Phone Number ID + Token mismatch — causa erro 400 na Send API. Token e Phone Number ID DEVEM ser do mesmo App na Meta.
8. automations_config vazio — tabela pode estar sem dados. Fazer INSERT manual com token da Meta.
9. auth.automations_config sem colunas Meta — não tem meta_access_token nem meta_phone_number_id. Adicionar com ADD COLUMN IF NOT EXISTS antes de inserir.
10. DUPLICIDADE de arquivos — meta_webhook_vps.py e meta_webhook_vps_service.py têm código duplicado. Qualquer mudança na lógica do find_task() precisa ser feita em ambos. Sugerir unificação futura.
11. Alert systems não filtravam in_progress — task_alerts.py (cron VPS) e dispatch-automation (Edge Function) enviavam alertas para tasks já iniciadas (in_progress). Correção: ambos agora filtram status = 'todo'.