Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> git-subtree-dir: gti-maquina-comercial git-subtree-mainline:93ca82979egit-subtree-split:b77484946f
10 KiB
Campanhas de Outreach — GTI Comercial
Data: 2026-08-01 Status: aprovado pelo usuário, pendente de plano de implementação
Contexto e motivação
A Máquina Comercial Pessoal tem um subsistema de campanhas de outreach (fila com
preview/aprovação, variantes A/B/C, escalonamento semanal de limite diário, pausa
automática por saúde, classificação de estágio comercial, agendamento de
demonstração) construído sobre o envio direto via Baileys. A GTI Comercial já tem
sua própria automação de outreach (src/outreach/run.mjs + scheduler.mjs),
que dispara um lote a cada 20 minutos, respeitando produto_principal de cada
lead, canal oficial (WABA/template aprovado) ou não-oficial (texto livre via
Z-PRO), e grava tudo em outreach_sends.
O pedido: portar a mesma metodologia de campanhas da Pessoal para a Comercial, mas adaptada a duas diferenças reais entre os projetos:
- Meio de conexão: a Comercial não fala com o WhatsApp diretamente (Baileys);
ela passa pelo Z-PRO (
src/outreach/zpro-client.mjs), que oferece dois canais distintos — não-oficial (sendText/bulkIndividual, texto livre) e oficial (sendTemplateWaba, só templates pré-aprovados no Meta Business Manager, sem texto livre nem variáveis além das que o próprio template já define). - Catálogo de negócio: a Pessoal tem 2-3 campanhas fixas, hardcoded em
campaign-catalog.mjs, cada uma amarrada a um segmento específico. A Comercial tem 57 segmentos e 4 produtos (G-Chat, Sistema de Ponto, ERP, G-Delivery) — catálogo fixo não cabe nessa escala. Aqui, campanha precisa ser um registro que o operador cria pela tela (produto + segmentos + textos), não código.
Decisões já validadas com o usuário:
- Campanhas rodam ao lado do
runOutreachBatchexistente, sem substituí-lo. - Variantes A/B/C só funcionam no canal não-oficial (o oficial não aceita texto livre, então não há o que variar).
- Pacote completo: escalonamento semanal, fila/preview/aprovação, variantes com eleição de vencedora, pausa automática por saúde, classificação de estágio comercial, agendamento de demonstração.
- Classificação de estágio e registro de resposta são manuais por enquanto — não existe endpoint de webhook de resposta do Z-PRO implementado nesta base.
Arquitetura
Novo diretório src/outreach/campaign-*.mjs, paralelo a run.mjs/scheduler.mjs
— nenhum dos dois é modificado. Único ponto de integração: outreach_sends
ganha um novo valor de origem ('campanha', além dos já existentes
'automatico'/'manual') e a seleção de candidatos de campanha exclui leads
recentemente contatados para o mesmo produto por qualquer origem, evitando
contato duplicado entre a automação existente e as campanhas novas.
src/outreach/
run.mjs (existente, intocado)
scheduler.mjs (existente, intocado)
zpro-client.mjs (existente, intocado — reusado)
templates.mjs (existente, intocado — renderTemplate reusado)
campaign-catalog.mjs (novo — CRUD de outreach_campaigns/variants)
campaign-queue.mjs (novo — seleção de candidatos, preview, aprovação)
campaign-worker.mjs (novo — envio das linhas aprovadas)
campaign-metrics.mjs (novo — saúde, auto-pausa, comparação de variantes)
campaign-scheduler.mjs (novo — timer próprio, chama campaign-worker)
Modelo de dados
Migração nova em src/db.mjs (mesmo padrão das migrações v5-v7 já existentes:
ALTER TABLE/CREATE TABLE IF NOT EXISTS guardado por versão do schema).
CREATE TABLE outreach_campaigns (
id INTEGER PRIMARY KEY,
nome TEXT NOT NULL,
produto TEXT NOT NULL,
segmentos_json TEXT NOT NULL, -- ["Oficina", "Restaurante", ...]
status TEXT NOT NULL DEFAULT 'rascunho', -- rascunho|rodando|pausado|concluido
pause_reason TEXT,
limite_diario_atual INTEGER NOT NULL DEFAULT 10,
semana_iniciada_em TEXT, -- controla quando escalar o limite
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE outreach_campaign_variants (
id INTEGER PRIMARY KEY,
campaign_id INTEGER NOT NULL REFERENCES outreach_campaigns(id),
code TEXT NOT NULL, -- A|B|C
mensagem TEXT NOT NULL,
ativo INTEGER NOT NULL DEFAULT 1
);
CREATE TABLE outreach_campaign_queue (
id INTEGER PRIMARY KEY,
campaign_id INTEGER NOT NULL REFERENCES outreach_campaigns(id),
lead_id INTEGER NOT NULL REFERENCES leads(id),
variant_id INTEGER NOT NULL REFERENCES outreach_campaign_variants(id),
segmento TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'preview', -- preview|approved|cancelled|reserved|sent|unknown|error
outreach_send_id INTEGER REFERENCES outreach_sends(id),
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE outreach_lead_states (
lead_id INTEGER PRIMARY KEY REFERENCES leads(id),
stage TEXT NOT NULL, -- interessado|sem_interesse|opt_out|venda_fechada|...
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE outreach_stage_events (
id INTEGER PRIMARY KEY,
lead_id INTEGER NOT NULL REFERENCES leads(id),
campaign_id INTEGER REFERENCES outreach_campaigns(id),
stage TEXT NOT NULL,
note TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE outreach_appointments (
id INTEGER PRIMARY KEY,
lead_id INTEGER NOT NULL REFERENCES leads(id),
kind TEXT NOT NULL, -- demonstracao|teste_g_delivery|...
scheduled_at TEXT,
note TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE outreach_opt_out (
numero TEXT PRIMARY KEY,
motivo TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
ALTER TABLE outreach_sends ADD COLUMN campaign_queue_id INTEGER REFERENCES outreach_campaign_queue(id);
outreach_opt_out não existe hoje na Comercial (a Pessoal tem um equivalente
mais complexo por causa da equivalência de números 8/9 dígitos). Aqui basta uma
tabela simples por numero já normalizado (onlyDigits), sem a complexidade de
variantes de número da Pessoal — a Comercial já normaliza números na validação
(validatePhone/onlyDigits). Está incluída porque uma fila que recontata o
mesmo lead ao longo de semanas sem checar opt-out é um risco real de spam/banimento,
não porque foi pedida explicitamente — sinalizando aqui para confirmação.
Fluxo
- Criar campanha: operador escolhe produto, segmentos (lista vindo de
SELECT DISTINCT segmento, COUNT(*) FROM leads WHERE produto_principal = ? GROUP BY segmento— mesmo padrão de facet já usado em outros lugares do projeto), escreve 1-3 variantes de mensagem (texto livre, mesmas variáveis detemplates.mjs:{empresa},{produto},{cidade},{segmento}), define limite diário inicial. - Gerar fila: candidatos = leads do produto/segmentos escolhidos, com
telefone_valido=1, sem envio recente (outreach_sends, qualquer origem) para o mesmo produto, sem opt-out. Distribuição round-robin entre variantes ativas. Linhas entram comopreview— nada é enviado. - Revisar e aprovar: tela lista a fila em preview (empresa, telefone,
variante, mensagem já renderizada); operador aprova em lote ou individualmente
→ status
approved. Só linhas aprovadas entram no worker. - Envio:
campaign-worker.mjs, rodando nocampaign-scheduler.mjs(timer próprio, mesmo padrão doscheduler.mjsexistente —setIntervalcom.unref()), pega linhasapprovedrespeitandolimite_diario_atualda campanha, revalida o lead na hora (ainda não opt-out, ainda no segmento), envia viazproClient.sendText, grava emoutreach_sends(origem='campanha',campaign_queue_idpreenchido), atualiza a linha da fila parasent/error. - Escalonamento semanal: a cada 7 dias corridos sem pausa por saúde desde
semana_iniciada_em,limite_diario_atualdobra (10→20→40...), até um teto configurável. - Saúde/auto-pausa (
campaign-metrics.mjs, mesmos 3 critérios da Pessoal, adaptados às tabelas daqui):- taxa de erro de envio > 10% com pelo menos 10 tentativas no dia → pausa
high_failure_rate - taxa de estágio negativo/opt-out > 10% com pelo menos 20 contatos → pausa
high_negative_rate - taxa de resposta < 5% com pelo menos 50 contatos → pausa
low_reply_rate(mesmos limiares numéricos já usados emcampaign-metrics.mjsda Pessoal) Pausa mudastatusparapausado; o worker para de pegar novas linhas dessa campanha até o operador retomar manualmente.
- taxa de erro de envio > 10% com pelo menos 10 tentativas no dia → pausa
- Variantes e eleição de vencedora: após um mínimo de 40 envios por variante, a tela mostra taxa de resposta por variante e sinaliza a de melhor desempenho — indicativo, não desliga variantes sozinho.
- Classificação de estágio e agendamento: 100% manual por enquanto —
operador marca o estágio do lead pela tela (gera
outreach_stage_events+ atualizaoutreach_lead_states) e cria agendamentos (outreach_appointments) manualmente. Sem webhook de resposta do Z-PRO nesta fase.
Prevenção de contato duplicado com a automação existente
runOutreachBatch (run.mjs) já verifica lastEnviado antes de mandar. A
seleção de candidatos de campanha faz a checagem simétrica: exclui qualquer
lead com outreach_sends para o mesmo produto nos últimos N dias, indepen-
dente de origem. Isso garante que uma campanha nunca contata quem o lote
automático já contatou recentemente para aquele produto, e vice-versa — os
dois sistemas competem pelo mesmo "direito de contato" por produto, não
disparam em paralelo para o mesmo lead.
Fora de escopo (explicitamente adiado)
- Webhook de resposta do Z-PRO / classificação automática de estágio.
- Envio de variantes pelo canal oficial (impossível hoje — WABA não aceita texto livre).
- Equivalência de números 8/9 dígitos ou detecção de número compartilhado (não existe na Comercial hoje e não foi pedido).
Testes
Mesma disciplina da Pessoal: um arquivo de teste por módulo novo
(campaign-catalog, campaign-queue, campaign-worker, campaign-metrics),
cobrindo pelo menos: geração de fila respeita segmentos/produto/exclusões,
aprovação só libera linhas aprovadas para o worker, worker respeita limite
diário e revalida antes de enviar, auto-pausa dispara nos 3 critérios e
bloqueia novas linhas, escalonamento semanal avança e não avança durante
pausa, prevenção de contato duplicado entre origem='automatico' e
origem='campanha'.