# Central de Atendimento — regras de negócio e arquitetura

> Fonte da verdade do módulo de atendimento omnichannel (`/atendimento`).
> Decisões do dono estão marcadas com **[DONO]**. Regras herdadas que NÃO podem
> ser alteradas por este módulo estão marcadas com **[VIVA]** — são invariantes
> de canal/custo/compliance já verificadas em produção.

## 0. Por que este módulo existe

Antes da Central, o atendimento vivia em duas telas que liam a **mesma tabela**
(`ai_conversations`) sem se falar:

| Tela antiga | Problema |
|---|---|
| `/ai/conversations` | navegação forçada por LOTE de campanha — não existia caixa de entrada; ADMIN via todas as contas misturadas |
| `/atendimento-kanban` | board de receita sobre as mesmas conversas, com escopo por cliente (divergente) e link para a conversa **quebrado** (`/ai-conversations` não existe) |

A Central substitui as duas **[DONO]**. O app passa a ter dois modos:
**Modo Atendimento** (esta tela, ocupa a janela inteira) e **Modo Gestão** (o
restante da plataforma: campanhas, usina, faturamento, kanban de receita).
Um papel dedicado "Atendente" (só atende, não dispara nem gere) é evolução
planejada — ver §9.

## 1. Estado da conversa (derivado, não persistido)

Não existe coluna "estado da central". O estado é **derivado** dos campos que já
governam o comportamento da IA, para que tela e motor nunca divirjam.

| Estado | Predicado |
|---|---|
| **Bot atendendo** | `closed_at IS NULL AND manual_mode = false AND status = 'OPEN'` |
| **Fila / Aberta** (precisa de humano) | `closed_at IS NULL` e NÃO-bot (`manual_mode = true` OU `status IN ('TRANSFER_TO_HUMAN','LIMIT_REACHED')`) |
| **Minha** | Fila/Aberta com `assigned_user_id = <eu>` |
| **Resolvida** | `closed_at IS NOT NULL` |

- SE a conversa é fechada, ENTÃO grava-se `closed_at` **e** `status='CLOSED'`.
  O status redundante não é decoração: `CLOSED` já pertence aos `asleepStatuses`
  do agente (`server/aiAgent.ts`), então fechar pela Central faz a IA dormir
  sem nenhum código novo de gate.
- SE `closed_at` e `status` divergirem (dado legado), ENTÃO `closed_at` vence
  para exibição.
- **Aguardando resposta** (o "não lido" da v1) é derivado:
  `last_user_message_at > GREATEST(last_ai_message_at, última mensagem AGENT)`.
  Não há read-receipt por operador — ver §9.

## 2. Transições bot ↔ humano

```
              ┌──── Assumir ────►┐
   Bot atendendo              Em atendimento humano ──── Finalizar ───► Resolvida
              ◄── Religar IA ────┘        │  ▲                              │
                  (ato explícito)         │  └──── Devolver (solta o operador,
                                          │         IA CONTINUA dormindo)
                                          └──── Transferir (troca de dono) ──┘
                                                                    Reabrir ◄─┘
```

- **Assumir** — SE operador clica Assumir, ENTÃO `manual_mode = true`,
  `status = 'TRANSFER_TO_HUMAN'`, `assigned_user_id = <eu>`, e registra evento
  SYSTEM na conversa. **[VIVA]** O takeover é **permanente**: a IA não volta
  sozinha nunca, nem quando o contato manda mensagem nova.
- **Devolver** — SE operador solta a conversa, ENTÃO `manual_mode = false` e
  `assigned_user_id = NULL`, mas o `status` **permanece** `TRANSFER_TO_HUMAN`.
  A IA segue dormindo. Isso é deliberado: "não estou mais nesta conversa" não é
  a mesma decisão que "a IA pode responder de novo".
- **Religar IA** — ato **explícito e separado** **[DONO]**. SE acionado, ENTÃO
  `manual_mode = false`, `status = 'OPEN'`, `assigned_user_id = NULL` e mensagem
  SYSTEM "IA religada por {nome}".
  - SE o serviço está com `aiEnabled = false`, ENTÃO 403 (a conta inteira está
    em modo somente leitura).
  - SE `status = 'LIMIT_REACHED'`, ENTÃO 422 — o teto de respostas por contato
    é proteção de custo **[VIVA]**; religar exigiria furar o limite.
  - A UI confirma antes, explicando que a IA voltará a responder este contato.
- **Finalizar** — SE acionado, ENTÃO `closed_at = now()`, `closed_by_user_id`,
  `status='CLOSED'` + SYSTEM "Finalizada por {nome}".
- **Reabrir** — SE acionado, ENTÃO `closed_at = NULL` e `status='TRANSFER_TO_HUMAN'`.
  Reabrir **nunca** religa a IA (quem quiser IA usa Religar depois).
- **Echo de Coexistência** **[VIVA]** — SE o dono responde o contato pelo app
  WhatsApp Business do celular (webhook `smb_message_echoes`), ENTÃO a mensagem
  entra como `AGENT` e a conversa sofre takeover permanente automático. Na
  Central isso aparece como conversa "Em atendimento humano" sem operador
  atribuído.

## 3. Colaboração e anti-colisão

**v1 = atribuição + aviso visual. Sem lock duro.** **[DONO: v1]**

- SE a conversa tem `assigned_user_id` de OUTRO usuário, ENTÃO o chat mostra
  banner "Em atendimento por {nome}" e pede confirmação antes de enviar. O
  servidor **não bloqueia** — bloquear geraria conversas órfãs quando alguém sai
  para o almoço com uma conversa presa.
- **Transferir** = `assign` para outro usuário do mesmo escopo (o CLIENTE dono e
  seus USUARIOs). Alvo fora do escopo → 400. Toda transferência registra SYSTEM.
- **"Digitando…"** existe apenas **entre operadores** (via SSE, §6). As APIs do
  WhatsApp **não** entregam o "digitando" do contato — qualquer indicador nesse
  sentido seria mentira de interface.
- **Notas internas** (`role = 'NOTE'`): SE a mensagem é nota, ENTÃO ela **nunca**
  passa por transporte de canal, não conta na janela, não altera
  `last_ai_message_at`, e é visível a todos os operadores do escopo. Carrega
  `author_user_id`. A UI destaca em âmbar com o rótulo "invisível ao cliente".

## 4. Envio de mensagem (gates, na ordem)

Ordem preservada do endpoint legado — o servidor é a barreira autoritativa, a UI
apenas espelha:

1. **Escopo** — conversa fora do escopo do solicitante → 403.
2. **Serviço desligado** — `services.aiEnabled = false` → 403 "somente leitura"
   (o botão de IA por serviço comanda TODO o atendimento, inclusive manual).
3. **Não assumida** — `manual_mode = false` → 403 "Assuma a conversa antes de
   responder". Assumir é ato explícito.
4. **Janela** **[VIVA]** — `canSendAutomaticFreeForm`:
   - canal **cloud**: janela conta da **última mensagem do contato**
     (modelo de atendimento da Meta);
   - demais canais: janela conta do **disparo** (`dispatch_sent_at`);
   - fora da janela → 422 "apenas um novo disparo reabre".
5. Só então o transporte resolve o canal e envia.

Notas internas pulam os gates 3 e 4 (nunca saem para o canal). Mídia enviada
pelo operador segue exatamente os mesmos gates do texto.

## 4b. Black-list (opt-out) no atendimento — **[DONO, 2026-08-06]**

**No chat NÃO existe bloqueio automático nem aviso.** Quem julga é o atendente,
que está lendo a conversa.

- SE o contato está na black-list, ENTÃO o operador **continua podendo
  responder** normalmente. Bloquear criaria mais problema do que resolve: o
  contato que escreve merece resposta, e "sair" no meio de uma frase
  ("vou sair agora, me manda depois") não é um pedido de descadastro.
- SE o atendente conclui que o contato quer sair, ENTÃO ele **insere
  manualmente** pela ficha (botão Descadastrar), o que aciona a supressão
  cross-canal padrão: black-list + `consentStatus=descadastrado` + evento +
  saída das rotinas. O evento de sistema na conversa registra **quem** fez.
- SE reativa, ENTÃO sai da black-list e o `consentStatus` volta para
  `sem_registro` — **não** para `ativo`: tirar o bloqueio não fabrica um
  consentimento que nunca existiu. `optOutAt` fica como histórico.
- ⚠ O opt-out **automático** por palavra-gatilho continua existindo no caminho
  da MO (`registrarOptOutSeGatilho`) — é ele que protege as **campanhas** de
  denúncia de spam. Esta seção trata só do que o atendimento humano faz.
- O **teto semanal de frequência** também não se aplica ao atendimento:
  mensagem de atendimento não é marketing (regra A1.5).

## 5. Escopo por papel

| Papel | Acesso à Central |
|---|---|
| `CLIENTE` | conversas da própria conta |
| `USUARIO` | conversas da conta do pai (`parentId`) — é membro da empresa |
| `ADMIN` / `REVENDA` | precisam **escolher um cliente** (mesmo modelo da Camada de Receita) |
| `OPERADOR` | **403** |
| `MODERADOR` | **403** |

- SE ADMIN/REVENDA não informar o cliente, ENTÃO 400. **Mudança deliberada**:
  antes o ADMIN via todas as contas misturadas em `/ai/conversations`, o que
  quebra o isolamento por conta que o resto da plataforma respeita.
- ⚠ **`OPERADOR` não é atendente.** Apesar do nome, é o operacional da OnSMS
  (pacotes ZIP, relatórios; `parentId = ADMIN`). Não reaproveitar esse papel
  para atendimento — ver §9.

## 6. Tempo real (SSE)

- Um **bus in-process** (`server/atendimentoBus.ts`) emite eventos a partir do
  ponto único de escrita das conversas (`appendAiConversationMessage` /
  `updateAiConversation` no storage). Assim TODA origem entra no stream sem
  código duplicado: IA, webhook Infobip, worker da Meta, echo de coexistência,
  envio manual, notas, eventos de sistema. Processo único é garantido pelo
  `instanceGuard`.
- `GET /api/atendimento/stream` (Server-Sent Events) entrega apenas eventos do
  escopo autenticado. Eventos: `message`, `conversation`, `typing`.
- SE o stream cair, ENTÃO o cliente reconecta com backoff e, enquanto estiver
  desconectado, faz polling de 30s (degradação, não quebra).
- Latência mínima do inbound cloud continua limitada pelo ciclo de 5s da fila
  `meta_webhook_queue` — é o piso, não um defeito da Central.

## 7. Identidade do contato e canal

- **Identidade** = `global_contacts` (único por conta + telefone). As
  **etiquetas** exibidas na Central são as do CONTATO, não da conversa — não se
  cria um segundo sistema de tags. Um mesmo contato que escreva por canais
  diferentes é a mesma ficha.
- **Canal** é **derivado**, não persistido: SE existe conexão cloud da conta cujo
  número normalizado bate com o `sender_number` da conversa, ENTÃO `wa_cloud`;
  SENÃO `services.tipoWhatsapp` (`wa_oficial`, `wa_multibm`, …). Derivar evita
  backfill e dessincronização quando um número migra de trilha.
- Canais ativos hoje: **WhatsApp Oficial (Infobip)**, **WhatsApp Multi-BM**,
  **WhatsApp Cloud (Meta direto)**. RCS não tem caminho de entrada. Instagram,
  Telegram e widget web são evolução — a arquitetura já trata canal como
  atributo, então plugam sem redesenho **[DONO]**.

## 8. Métricas do cabeçalho

| Métrica | Definição v1 |
|---|---|
| Em atendimento | conversas abertas com `manual_mode = true` |
| Na fila | conversas abertas aguardando humano sem operador atribuído |
| SLA hoje | % das conversas que entraram na fila hoje com 1ª resposta humana em ≤ 5 min |
| TMA | média de `closed_at − created_at` das resolvidas hoje |

Fuso de referência: `America/Sao_Paulo`. O limite de SLA é constante nesta
versão; virar configuração por conta é evolução.

## 9. Fora de escopo (anotado de propósito)

- **Papel "Atendente"** — perfil que só atende (não dispara, não gere). Os
  ganchos já existem (`assigned_user_id`, status do agente na tela); falta a
  decisão de permissões. **[DONO: depois]**
- **Instagram / Telegram / WebChat** — canal já é atributo; falta o conector.
- **Filas e times**, roteamento automático, lock duro anti-colisão.
- **Read-receipt por operador** (v1 usa "aguardando resposta" derivado).
- **Busca no corpo das mensagens** (v1 busca nome, telefone e etiqueta) —
  exigiria índice de texto dedicado.
- **Transcrição de áudio e leitura de imagem pela IA** **[VIVA]** — decisão de
  custo: a IA nunca interpreta mídia. O operador humano vê e ouve tudo na
  Central; a IA continua recebendo apenas o marcador textual.
- **Fluxo de nós / NPS automático** — a IA da plataforma é LLM com persona e
  base de conhecimento, não um construtor de fluxos. O painel lateral mostra o
  estado real da IA (ativa/dormindo e por quê), não uma árvore de nós.
