# 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: 1. **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). 2. **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 `runOutreachBatch` existente, 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). ```sql 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 1. **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 de `templates.mjs`: `{empresa}`, `{produto}`, `{cidade}`, `{segmento}`), define limite diário inicial. 2. **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 como `preview` — nada é enviado. 3. **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. 4. **Envio**: `campaign-worker.mjs`, rodando no `campaign-scheduler.mjs` (timer próprio, mesmo padrão do `scheduler.mjs` existente — `setInterval` com `.unref()`), pega linhas `approved` respeitando `limite_diario_atual` da campanha, revalida o lead na hora (ainda não opt-out, ainda no segmento), envia via `zproClient.sendText`, grava em `outreach_sends` (`origem='campanha'`, `campaign_queue_id` preenchido), atualiza a linha da fila para `sent`/`error`. 5. **Escalonamento semanal**: a cada 7 dias corridos sem pausa por saúde desde `semana_iniciada_em`, `limite_diario_atual` dobra (10→20→40...), até um teto configurável. 6. **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 em `campaign-metrics.mjs` da Pessoal) Pausa muda `status` para `pausado`; o worker para de pegar novas linhas dessa campanha até o operador retomar manualmente. 7. **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. 8. **Classificação de estágio e agendamento**: 100% manual por enquanto — operador marca o estágio do lead pela tela (gera `outreach_stage_events` + atualiza `outreach_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'`.