chore: import gti-maquina-comercial history via subtree

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

git-subtree-dir: gti-maquina-comercial
git-subtree-mainline: 93ca82979e
git-subtree-split: b77484946f
This commit is contained in:
Gustavo
2026-09-28 15:50:08 -03:00
co-authored by Claude Sonnet 5
110 changed files with 27501 additions and 0 deletions
@@ -0,0 +1,62 @@
# Visão Geral
O Grupo GTI opera uma **máquina comercial local**: um aplicativo (GTI-Comercial.exe)
que captura leads B2B das fontes públicas — principalmente a base oficial de CNPJs
da Receita Federal — e ajuda a tratá-los e exportá-los para as campanhas de
WhatsApp no Z-PRO.
## Soluções vendidas
- **ERP e sistemas de gestão** para comércio, serviços, restaurantes, mercados e distribuidoras.
- **Gestão de TI** para empresas com suporte, infraestrutura, servidores, redes, backup e segurança.
- **Autoatendimento para WhatsApp** com triagem, atendimento automático, vendas e agendamento.
Cada nicho do catálogo (57 nichos) já vem associado ao interesse provável e às dores
típicas, que alimentam a personalização das mensagens.
## Arquitetura (simples de propósito)
```
GTI-Comercial.exe → servidor Node local (porta 3900) → navegador
│
├── Captura: base CNPJ por UF + OpenStreetMap + Wikidata (+ Google opcional)
├── Banco SQLite local (data/gti.db): leads, score, dedup, histórico
└── Tratamento: dashboard, filtros, relatórios e exportações Z-PRO
```
Sem n8n, sem serviços externos obrigatórios: tudo roda no computador da GTI hoje
(e vai rodar num container Docker no Proxmox da empresa no futuro, sem mudança de
código). O primeiro contato via WhatsApp pode ser automático (ver
[08-automacao-envio.md](08-automacao-envio.md)); campanhas maiores e o
acompanhamento seguem por fora, no Z-PRO, a partir dos arquivos gerados pelo
módulo de tratamento.
## Premissas
- Foco inicial na Bahia (Santo Antônio de Jesus e região + capitais), mas o app
funciona para **qualquer UF/cidade do Brasil** sem mudança de interface.
- A equipe não tem braço para operação manual intensa: o app faz a triagem
(score de qualidade dos dados + score comercial por nicho, combinados numa
prioridade P1-P4) e evita retrabalho (leads exportados ficam marcados).
- O WhatsApp é o canal central de conversão — leads com telefone válido têm prioridade.
## Resultado esperado
- Base grande e organizada de leads com WhatsApp (97%+ de cobertura via base CNPJ).
- Relatórios por nicho/cidade prontos para campanhas segmentadas.
- Histórico de tudo que já foi exportado/enviado, sem duplicar contato.
- Dashboard de gestão comercial na tela de Tratamento: percentual da base
pronta para abordagem imediata (Potencial Comercial), cobertura da base
já trabalhada, distribuição visual por prioridade, produtos recomendados
por cidade e meta diária de exportações da equipe.
- Interface explica o "porquê" de cada score (composição detalhada no modal
do lead), destaca a cidade+produto de maior oportunidade, mostra a
evolução diária de exportações e oferece atalhos de um clique para gerar
listas de prospecção (só P1, só um produto, só não exportados etc.).
- Acesso protegido por login (usuário/senha): o primeiro acesso vem com
`admin`/`admin`, que pode cadastrar novos usuários (comercial ou admin)
pela tela de Usuários.
- Automação de primeiro contato via WhatsApp (opcional, desligada por padrão):
manda uma mensagem com o template do produto recomendado pra cada lead ainda
não exportado, registra o que foi mandado e aplica tags no contato do Z-PRO —
ver [08-automacao-envio.md](08-automacao-envio.md).
@@ -0,0 +1,83 @@
# Playbook Comercial
## Oferta De Entrada
Nome: Diagnostico GTI de Gestao, TI e Atendimento.
Promessa: em 15 minutos, identificar gargalos em sistema, atendimento, infraestrutura ou processos manuais da empresa.
Essa oferta reduz resistencia porque nao comeca vendendo ERP, contrato de TI ou automacao diretamente.
## ICP Principal
Priorizar empresas de Santo Antonio de Jesus e regiao com qualquer uma destas caracteristicas:
- Controle por planilha ou papel.
- Sistema atual ruim ou insatisfatorio.
- Alto volume de WhatsApp.
- Muitos computadores ou usuarios.
- Problemas frequentes com suporte, rede, backup ou servidor.
- Dificuldade com estoque, financeiro, fiscal ou vendas.
## Segmentos Prioritarios
- Mercados e supermercados.
- Farmacias e drogarias.
- Restaurantes e delivery.
- Distribuidoras.
- Autopecas.
- Material de construcao.
- Clinicas.
- Oficinas.
- Escolas.
- Lojas de varejo.
- Empresas de servicos.
## Metodo De Atendimento
Usar SPIN de forma simples:
- Situacao: entender como a empresa funciona hoje.
- Problema: descobrir gargalos.
- Implicacao: mostrar o impacto de continuar assim.
- Necessidade: conectar a dor a uma solucao GTI.
## Perguntas Do Diagnostico
- Como voces controlam vendas, estoque e financeiro hoje?
- Qual processo mais toma tempo ou gera retrabalho?
- Hoje voces usam sistema, planilha ou controle manual?
- Quando da problema de TI, quem resolve?
- Existe rotina de backup?
- Quantas mensagens chegam por dia no WhatsApp?
- Qual o impacto se esse problema continuar por mais 6 meses?
- Se a GTI resolver isso, qual resultado seria mais importante?
## Roteiro Para Lead Quente
```text
Ola, [nome]. Vi aqui seu contato sobre [servico].
Pelo que voce informou, parece que existe uma oportunidade clara de melhorar [dor].
Quero fazer um diagnostico rapido de 15 minutos para entender sua operacao e te dizer o melhor caminho.
Pode ser hoje pela tarde ou amanha pela manha?
```
## Follow-up Manual Em Proposta
Dia 1:
```text
Oi, [nome]. Conseguiu avaliar a proposta da GTI? Posso te ajudar com alguma duvida tecnica ou comercial?
```
Dia 3:
```text
[nome], queria confirmar se esse projeto ainda faz sentido para agora ou se prefere que eu reprograme o contato para outro momento.
```
Dia 7:
```text
Vou deixar seu atendimento em aberto para retomarmos quando fizer sentido. Se quiser avancar, me responda por aqui.
```
@@ -0,0 +1,48 @@
# LGPD E Regras De WhatsApp
Este projeto deve ser operado com cuidado para evitar bloqueio de WhatsApp e problemas de privacidade.
## Regras Basicas
- Guardar origem do lead.
- Registrar consentimento quando existir.
- Evitar disparo massivo sem opt-in.
- Permitir descadastro ou parada de contato.
- Nao comprar listas frias para disparo automatico.
- Usar abordagem consultiva, nao agressiva.
- Manter historico de data, origem e mensagem enviada.
## Mensagem De Descadastro
Toda sequencia pode aceitar palavras como:
```text
PARAR
CANCELAR
SAIR
NAO QUERO
```
Quando uma dessas respostas chegar, o lead deve ser marcado como `Opt-out` no CRM e sair das automacoes.
## Prospecao Ativa Segura
Para listas frias locais, a abordagem deve ser manual ou semi-automatizada, com baixo volume e personalizacao.
Exemplo:
```text
Ola, tudo bem? Aqui e do Grupo GTI, de Santo Antonio de Jesus.
Vi que sua empresa atua em [segmento] e queria entender se hoje voces ja usam algum sistema para vendas, estoque ou atendimento.
Se nao fizer sentido, pode me avisar que nao envio novas mensagens.
```
## Dados Minimos No CRM
- Nome.
- Empresa.
- WhatsApp.
- Origem.
- Data do primeiro contato.
- Consentimento.
- Status de opt-out.
@@ -0,0 +1,75 @@
# Captura de Leads
A captura procura empresas por **UF + cidades + nichos** nas fontes selecionadas,
pontua cada lead (score/classificação) e grava tudo no banco local com
deduplicação automática — um lead capturado uma vez nunca é duplicado, em nenhuma
execução futura.
O jeito normal de usar é pela **aba "Capturar Leads" do aplicativo**
(GTI-Comercial.exe). Este documento cobre os detalhes e o uso por linha de comando.
## Fontes
| Fonte | Custo | Observações |
|---|---|---|
| `cnpj-base` | grátis | **Principal.** Base oficial da Receita Federal extraída por UF (~97% com telefone). |
| `brasilapi` | grátis | Mesma base CNPJ, enriquecida online via BrasilAPI (mais lenta). |
| `overpass` | grátis | OpenStreetMap por área da cidade (bounding box automática via geocode). |
| `nominatim` | grátis | Busca textual no OpenStreetMap. |
| `wikidata` | grátis | Empresas registradas no Wikidata (volume baixo). |
| `google-places` | paga | Exige `GOOGLE_MAPS_API_KEY` no `.env`. |
| `mock` | — | 3 leads fictícios para testar o fluxo. |
## Base CNPJ por estado
Cada UF precisa de uma extração única (~40–60 min) que gera
`prospeccao/cnpj/estabelecimentos-<UF>.csv` a partir dos zips brutos da Receita
(`prospeccao/cnpj/rfb-raw/`, Brasil inteiro, já baixados). A interface oferece o
botão **"Extrair base agora"** quando a UF escolhida ainda não tem base; na linha
de comando:
```bash
node scripts/extrair-base-cnpj.mjs --uf=SP
```
A extração cobre **todos os municípios da UF** — depois dela, qualquer cidade do
estado pode ser capturada na hora, sem re-extração. Um checkpoint em disco permite
retomar de onde parou se o processo for interrompido.
## Deduplicação e pontuação
- Chave de duplicidade: **CNPJ válido → WhatsApp válido → empresa+cidade**.
- **Score de Qualidade** (0-100, "os dados são confiáveis?"): +30 WhatsApp válido,
+20 CNPJ válido, +20 empresa válida, +5 cidade, +10 nicho, +10 CNAE, +5 website.
- **Score Comercial** (0-100, "qual a chance de comprar?"): aderência do nicho
(alta 40 / média 30 / baixa 20, configurada por nicho em `prospeccao/config.json`)
+ website 15 + e-mail 10 + WhatsApp válido 15 + CNPJ válido 20.
- **Score Geral** = 60% comercial + 40% qualidade — é o valor usado para ordenar
a tela de Tratamento.
- **Classificação**: 90-100 Excelente, 75-89 Bom, 55-74 Médio, 35-54 Baixo,
0-34 Muito Baixo. A **Prioridade comercial** (P1-P4, mostrada na tela) deriva
direto dela: Excelente/Bom ⇒ P1 (atacar já), Médio ⇒ P2 (na semana), Baixo ⇒ P3,
Muito Baixo ⇒ P4 (só em campanhas específicas).
- Cada nicho tem produtos GTI recomendados automaticamente (ERP, G-Delivery,
G-Chat, Sistema de Ponto) que aparecem no card do lead. Se o catálogo de
aderência/produtos do `config.json` mudar, rode `npm run recalc-scores` para
recalcular todos os leads já capturados.
## Linha de comando (uso avançado)
```bash
node scripts/capturar-leads.mjs --sources=mock
node scripts/capturar-leads.mjs --sources=cnpj-base --uf=BA --cities="Ilheus,Itabuna"
node scripts/capturar-leads.mjs --sources=cnpj-base,overpass --uf=BA --cities=all --segments=all
node scripts/capturar-leads.mjs --help
```
Parâmetros úteis: `--segments=a,b,c` (nichos pelo nome), `--segment-limit=N`,
`--city-limit=N`, `--max=N` (resultados por consulta nas fontes de API),
`--query-limit=N` (teto total de consultas).
## Ritmo e boas práticas (fontes OpenStreetMap)
Os servidores públicos do Overpass/Nominatim têm limites de uso. O app já aplica
espera entre consultas (`OVERPASS_REQUEST_DELAY_MS`), failover entre endpoints e
cooldown após falhas — falha de uma cidade não derruba a captura, só pula.
@@ -0,0 +1,133 @@
# Automação de envio WhatsApp (Z-PRO)
Manda automaticamente a primeira mensagem de WhatsApp pro produto/serviço
recomendado de cada lead, usando um template por produto, e registra tudo
(quem recebeu, qual produto, quando) pra nunca mandar a mesma coisa duas
vezes pro mesmo lead.
## Como funciona
- Roda **sozinha, dentro do próprio servidor**, a cada 20 minutos, enquanto
o app estiver ligado — não depende de Agendador de Tarefas do Windows nem
de cron. Isso é de propósito: o projeto hoje roda local, mas vai ser
migrado pra um container Docker no Proxmox da empresa, e um agendador do
sistema operacional teria que ser reconfigurado nesse dia. O timer interno
funciona idêntico nos dois ambientes.
- Só considera leads **ainda não exportados** (`exported_at` vazio). Assim
que um lead vira oportunidade manual no Z-PRO (exportação normal da tela
de Tratamento), a automação para de mandar mensagem pra ele — quem assumiu
o lead foi um vendedor, não o robô.
- Cada lead tem uma lista de produtos recomendados por nicho (ex.: ERP 5⭐,
G-Delivery 3⭐). Hoje a automação só tenta o **produto principal** (o de
maior estrela), uma única vez por lead. Existe uma segunda etapa pronta
(mas desligada) pra tentar o próximo produto da lista se não houver
resposta depois de alguns dias — ver `outreach.tentarProximoProduto`
abaixo.
- Manda uma mensagem por número, com controle nosso de quem já recebeu o
quê — não pelo `bulkSendMessageWithVariable` usado na exportação manual
em massa, que depende de um CSV com substituição posicional de variável.
Cada produto tem um **canal** próprio (ver seção abaixo).
- Depois de mandar, aplica duas tags no contato do Z-PRO:
`GTI P1`/`GTI P2`/`GTI P3`/`GTI P4` (prioridade do lead) e
`GTI Oferta: <produto>` (o que foi ofertado) — pra quem for atender depois
já ver esse contexto direto na conversa.
## Canal oficial x não-oficial
Cada produto/template tem um seletor de canal em Configurações:
- **Não-oficial (texto livre)** — usa `bulkIndividual`, funciona numa
conexão QR Code/Baileys do Z-PRO. Manda o texto do template com
`{empresa}`/`{cidade}`/`{segmento}` substituídos. É o padrão, e o risco
aqui é o de sempre com WhatsApp não-oficial: possibilidade de bloqueio do
número por comportamento automatizado (por isso o limite diário e o
intervalo aleatório entre envios).
- **Oficial (Meta, template aprovado)** — usa a API oficial do WhatsApp
(Cloud API/WABA) via Z-PRO. Aqui **não dá pra mandar texto livre**: o
WhatsApp só permite iniciar uma conversa com um **template pré-aprovado**
pelo Meta Business Manager (fora da janela de 24h de uma conversa já
iniciada pelo cliente). Por isso o campo vira "Nome do template aprovado"
(tem que ser **exatamente** o nome cadastrado no Meta) + "Idioma" (ex.:
`pt_BR`).
**A aprovação do template em si é feita fora deste app** — no Meta
Business Manager (ou no painel do Z-PRO, se ele tiver essa tela) — o
Z-PRO só expõe o *envio* usando um template que já existe, não a criação.
Cadastre e espere aprovar lá antes de configurar o nome aqui.
Limitação atual: o envio oficial aqui é só nome+idioma, sem variáveis
dentro do corpo do template aprovado (a API do Z-PRO tem endpoints
`templateBody`/`templateMarketingBody` que parecem aceitar variáveis, mas
o formato exato não está documentado na collection — se for necessário,
precisa testar contra a instância real antes de automatizar).
## Configurar pela primeira vez
1. **Credenciais do Z-PRO**: no painel do Z-PRO, vá em Admin > API e gere um
token (Base URL, API ID, Token e WhatsApp ID). Na tela, abra
**⚙️ Configurações > Credenciais Z-PRO (WhatsApp)**, preencha os 4 campos
e clique em **Testar conexão** (salva e faz uma chamada real e barata pra
confirmar que as credenciais estão certas, sem mandar nenhuma mensagem).
Sem essas 4 credenciais a automação não manda nada, mesmo ligada.
Alternativa pra quem prefere não guardar o token no banco local (ex.: em
produção no Docker/Proxmox, gerenciando segredo por variável de
ambiente): deixe os 4 campos da tela em branco e defina
`ZPRO_BASE_URL`/`ZPRO_API_ID`/`ZPRO_BEARER_TOKEN`/`ZPRO_WHATSAPP_ID` no
`.env` — a tela vazia cai automaticamente pro `.env`. Se a tela tiver
algum valor preenchido, ele tem prioridade sobre o `.env`.
2. Na mesma tela, em **Automação de envio (WhatsApp)**: revise o texto de
cada template (um por produto — `{empresa}`, `{cidade}` e `{segmento}`
são substituídos automaticamente), ajuste o **limite por dia** se
quiser, e marque **Ativada**. Salve.
Antes de ativar, vale mandar um teste manual (próxima seção) pro seu
próprio WhatsApp.
## Configurações (`settings`, chave → o que faz)
| Chave | Padrão | O que faz |
|---|---|---|
| `outreach.ativo` | `false` | Interruptor geral. Começa desligado de propósito. |
| `outreach.mensagensPorDia` | `10` | Máximo de envios automáticos por dia (soma de todos os produtos). |
| `outreach.diasEntreProdutos` | `5` | Dias de espera antes de tentar o próximo produto (só usado se a cascata estiver ligada). |
| `outreach.tentarProximoProduto` | `false` | Liga a cascata: quando `false`, só tenta o produto principal uma vez. |
| `outreach.horaInicio` / `outreach.horaFim` | `08:00` / `18:00` | Só manda mensagem dentro desse horário. |
Tudo isso é editável pela tela de Configurações (os dois primeiros) ou
diretamente via `PUT /api/settings` (todos).
## Disparo manual (fora do ciclo automático)
Pra testar ou forçar um lote sem esperar o próximo ciclo de 20 min:
```bash
npm run outreach
```
Respeita as mesmas regras do ciclo automático (`outreach.ativo`, horário
comercial, limite diário) — não manda nada que a tela não mostraria.
## Enviar pra um número específico
Em **Configurações > Enviar mensagem manual**: digite o número (com DDI+DDD,
ex.: `5511999999999`), escolha o canal (texto livre ou template oficial já
aprovado) e clique em Enviar. Não depende de lead cadastrado nem de produto.
No canal **Não-oficial (texto livre)**, o app usa `createTicket` para criar uma
conversa visível no painel do Z-PRO. O ticket usa o canal informado em
`WhatsApp ID` e o usuário de `Responsável` nas configurações.
No canal **Oficial**, o app usa o endpoint de template aprovado (`template`) e
também espera um `ticketId` na resposta.
Toda tentativa fica registrada no mesmo histórico dos envios automáticos
(marcado como origem "manual") e **conta pro limite diário**, já que o risco de
bloqueio é do número de WhatsApp usado, não de quem disparou a mensagem.
## Onde fica o histórico
Toda tentativa de envio (sucesso ou erro) vira uma linha na tabela
`outreach_sends` (lead, produto, mensagem exata mandada, status, quando).
Um resumo do dia (enviados hoje, por produto, últimos 20 envios) fica
disponível em `GET /api/outreach/status`.
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,213 @@
# 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'`.