# Campanhas, Leads e Lista de Bloqueio Agente.md secao 63-71. Modelo de dados, CRUD, máquina de estados da campanha e importação de leads — o motor que de fato origina chamadas (`PredictiveDialerEngine`, secao 72-86) é uma fase à parte, deliberadamente não implementada aqui (é um sistema grande o suficiente — pacing por EWMA, previsão de liberação de agentes, CPS distribuído, reserva atômica de leads — pra merecer o próprio ciclo de design/teste, ver agente.md secao 72 "Não implementar apenas `for lead -> originate`. Isso não é discador preditivo."). ## Modelo - `campaigns` (tenant-scoped, RLS): liga `Queue`+`Trunk`, janela de funcionamento (`timezone`/`daysOfWeek`/`startTime`/`endTime`/`startDate`/ `endDate`), limites (`maxCps`/`maxConcurrentCalls`, além dos globais do Plan), parâmetros de pacing (`pacingInitial`/`Min`/`Max`, `targetAbandonRate` — consumidos pela fase Predictive Engine, só armazenados aqui), `maxAttempts`/`ringTimeout`, flags de gravação/AVMD/IA. `daysOfWeek` é `Int[]` ISO-8601 (1=segunda...7=domingo); `startTime`/ `endTime` são `"HH:MM"` string — sem tipo TIME nativo nesta fase (o scheduler de verdade avalia isso na timezone da campanha, é trabalho da fase Predictive Engine). - `leads` (tenant-scoped, RLS): `campaignId`, `phoneOriginal`/ `phoneNormalized` (E.164), `status` (enum de 16 valores, secao 68), `attemptCount`/`lastAttemptAt`/`nextAttemptAt`/`lastResult`, `customFields` JSONB livre. Único por `(campaignId, phoneNormalized)` — o mesmo telefone pode existir em campanhas diferentes, nunca duas vezes na mesma. - `suppression_entries` (tenant-scoped, RLS): lista de bloqueio (secao 71), único por `(tenantId, phoneNormalized)`. ## Máquina de estados da campanha (secao 64-66) ``` start: DRAFT|READY|WAITING_SCHEDULE|PAUSED -> RUNNING pause: RUNNING -> PAUSED drain: RUNNING|PAUSED -> DRAINING (não origina novas, deixa terminar as existentes) stop: qualquer não-terminal -> STOPPED ``` Cada transição valida o estado atual contra uma tabela de transições permitidas (`ALLOWED_TRANSITIONS` em `campaigns.controller.ts`) — tentar `pause` numa campanha `DRAFT` retorna 400, não silenciosamente ignora. Apagar (soft delete) é bloqueado enquanto `RUNNING`/`DRAINING` — precisa parar primeiro. `COMPLETED`/`ERROR` não são alcançáveis via API nesta fase — são estados que só o motor de discagem (fase Predictive Engine) vai setar sozinho (leads esgotados / falha operacional). ## `packages/shared/src/phone.ts` — normalização (secao 70) Serviço dedicado, `normalizePhone(raw, country = "BR")`. Assinatura já preparada pra outros países (E.164 completo) — só o normalizador BR está implementado. Aceita com/sem "55" na frente, DDD 2 dígitos + 8 ou 9 dígitos de número; retorna `null` (nunca lança) pra entrada inválida, já que "número inválido" é um resultado esperado de uma importação de CSV. ## Importação CSV (secao 69) `POST /campaigns/:id/leads/import`, corpo `{ csv: string }` — a versão desta fase recebe o CSV como texto no corpo JSON, não upload de arquivo multipart; o wizard visual (Upload → Preview → Mapeamento → Validação → Normalização → Duplicados → Importação) descrito na especificação é UI, fase Frontend. Formato mínimo: cabeçalho com colunas `nome`/`telefone` (aceita `name`/`phone` também). Processamento em streaming/batches (`csv-import.ts`): parseia linha a linha (parser CSV mínimo, aceita campos entre aspas), valida telefone, detecta duplicado (dentro do próprio CSV E contra leads já existentes na campanha, num único `Set` carregado uma vez do banco — não uma query por linha), checa contra a lista de bloqueio do tenant, insere em lotes de 1000 (`createMany`). Lead que bate na lista de bloqueio é importado como `DO_NOT_CALL`, não descartado — fica registrado, só não é discado depois (quando o motor existir, ele só vai selecionar `status = READY`). Retorna o resumo pedido pela secao 69: `{ total, valid, invalid, duplicates, imported, suppressed }`. ## Lista de bloqueio (secao 71) CRUD simples (`/suppression`), tenant-scoped. A checagem obrigatória "antes de qualquer chamada" é responsabilidade de quem de fato origina (Predictive Engine) — aqui ela já é aplicada no momento da importação/criação de lead (marca como `DO_NOT_CALL` na hora, não pré-filtra o import). ## Verificado ponta a ponta ``` POST /campaigns {queueId, trunkId, maxCps:2, daysOfWeek:[1..5], ...} -> pacingMin > pacingMax rejeitado (400) -> queueId inexistente no tenant rejeitado (400) POST /suppression {phone: "11988887777"} POST /campaigns/:id/leads/import (5 linhas: 1 invalida, 1 duplicada, 1 bloqueada) -> {"total":5,"valid":3,"invalid":1,"duplicates":1,"imported":3,"suppressed":1} -> lead bloqueado importado com status DO_NOT_CALL pause numa campanha DRAFT -> 400 "Nao e' possivel "pause" ... em status DRAFT" start -> RUNNING -> pause -> PAUSED -> drain -> DRAINING -> stop -> STOPPED start numa campanha STOPPED -> 400 (transição não permitida) delete numa campanha RUNNING -> 400 "Pare a campanha antes de apaga-la" 3a campanha (plano trial, max_campaigns=2) -> 403 Quota excedida ``` ## O que falta - ~~`PredictiveDialerEngine`~~ — implementado na fase CPS Limiter/ Predictive Engine (ver docs/PREDICTIVE_DIALER.md). - Wizard visual de importação (upload de arquivo de verdade) — fase Frontend. - ~~Relatório de campanha~~ — implementado na fase CDR (ver docs/CDR.md). - Quota de leads por campanha — não existe campo `max_leads` no Plan (agente.md secao 56 não lista um); se vier a ser necessário, é uma adição pequena ao Plan + `assertQuota`.