Ports the Máquina Comercial Pessoal's campaign methodology (queue/preview/ approval, A/B/C variants, weekly limit escalation, health-based auto-pause, stage classification, appointment scheduling), adapted for the Z-PRO/WABA send layer and a dynamic operator-defined catalog instead of a hardcoded one.
214 lines
10 KiB
Markdown
214 lines
10 KiB
Markdown
# 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'`.
|