# Agentes, Tiers e Pausas Agente.md secao 45-49 e 52. Completa o Call Center: depois desta fase, um usuário autenticado consegue logar como agente, entrar numa fila real, se pausar e voltar — tudo refletido de verdade no FreeSWITCH. ## Modelo - `agents` (tenant-scoped, RLS): liga `User` (login) → `Extension` (ramal SIP) → identidade de agente (`User`/`Agent`/`Extension` separados, agente.md secao 45). `state` espelha o estado real (secao 46), nunca escrito direto pela API — só via login/logout/pause/resume. - `tiers`: Queue↔Agent (secao 52) — level/position espelham 1:1 o mod_callcenter. - `agent_sessions`: uma linha por ciclo login→logout. - `agent_state_events`: histórico de transições de estado (auditoria). - `pause_reasons`/`agent_pause_events` (secao 48). ## Mecanismo: 100% dinâmico, sem arquivo Confirmado na fase anterior (Queues) via `help callcenter_config`: agentes e tiers não têm representação em XML — só comandos ESL diretos (`agent add/del/set status/set contact`, `tier add/del`). Isso muda o padrão de sincronização: em vez de "regenerar todos os arquivos" (Trunks/Queues), `apps/api` publica uma mensagem **por ação, com payload** (`{tenantId, agentId, action}` em `b2bcall:agents:sync`; `{tenantId, queueId, agentId, level, position, action}` em `b2bcall:tiers:sync`) — `b2bcall-fs-config` recebe e aplica o comando correspondente. ## Achados reais confirmados manualmente antes de codar Rodei cada comando contra o FreeSWITCH real antes de escrever qualquer lógica de sync (mesmo método usado nas fases anteriores): - `agent add` em cima de um agente que já existe: **erro** ("Agent already exist!") — não é idempotente. `tier add` duplicado: mesmo erro ("Tier already exist!"). Ambos capturados e ignorados no sync (`.catch(() => undefined)`), já que essa condição é esperada em qualquer resync. - `tier del`/`agent del` em algo que não existe: **não dá erro** (+OK) — seguro chamar sem checar existência antes. - `agent set status` só aceita 3 valores exatos: `Available`, `On Break`, `Logged Out` — qualquer outro string dá `-ERR Invalid Agent Status!` (testado deliberadamente). Os estados derivados de chamada do nosso enum (RESERVED/RINGING/IN_CALL/WRAP_UP) não têm status próprio no mod_callcenter — mapeiam pra "Available" (só o `state`, campo separado, muda sozinho conforme a chamada progride — não escrevemos isso). - **Achado de corrida real**, visto ao testar o fluxo completo: como um agente só passa a existir no FreeSWITCH no login (`agent add` roda ali, não na criação do registro `Agent`), atribuir um tier (`POST /queues/:id/agents`) **antes** do primeiro login do agente falha silenciosamente do lado do FreeSWITCH (`-ERR Agent not found!`, logado mas não propagado como erro HTTP). Por isso o login **sempre re-sincroniza todos os tiers do agente** depois de garantir que ele existe — a auto-correção aconteceu exatamente assim no teste real. ## Fluxo de login (agente.md secao 47) ``` POST /agents/me/login → valida usuário (JWT) e ramal (Agent.extensionId precisa existir) → cria agent_session → agent add (idempotente via catch) + set contact (dial-string do ramal) → set status Available → re-sincroniza todos os tiers do agente → Agent.state = AVAILABLE ``` `POST /agents/me/logout|pause|resume` seguem o mesmo padrão — sempre sobre o agente do **próprio usuário autenticado** (nunca um `agentId` arbitrário do client, mesmo princípio de nunca confiar em tenant_id/ids sensíveis vindo do frontend sem checar contra o JWT). ## Verificado ponta a ponta ``` POST /extensions {"number":"2000",...} POST /agents {userId, extensionId, name} POST /queues {"name":"Suporte"} POST /queues/:queueId/agents {agentId} → tier add falha (agente ainda nao existe) POST /agents/me/login → agent add + contact + status Available → tier re-sync (dessa vez funciona) callcenter_config agent list → status=Available, contact={ignore_early_media=true}user/2000@b2bcall.local ✓ callcenter_config tier list → queue|agent com state=Ready ✓ POST /agents/me/pause {pauseReasonId} → status=On Break ✓ POST /agents/me/resume → status=Available ✓ POST /agents/me/logout → status=Logged Out ✓ ``` Todos os 4 estados confirmados batendo entre o banco (`Agent.state`) e o FreeSWITCH (`agent list`). ## O que falta - Estados derivados de chamada (RINGING, IN_CALL, WRAP_UP, RESERVED) — o mecanismo de entrega do `callcenter::info` foi corrigido e confirmado funcionando (ver seção abaixo), mas gravar esses estados de volta em `Agent.state` ainda não foi implementado — hoje o campo só muda via login/logout/pause/resume. O estado real do mod_callcenter (`CC-Agent-State`: `Waiting`/`Receiving`/...) já está disponível em tempo real via WebSocket (docs/REALTIME.md); persistir isso em `Agent.state` fica pra quando o Predictive Engine/CDR precisarem consultar esse histórico. - Tela do agente (secao 49) — fase Frontend. - Quota de agentes (`max_agents`) — depende de Plans/Entitlements. - `PauseReason.maxDuration` existe no modelo mas não é aplicado automaticamente ainda (ninguém força o fim da pausa ao expirar). ## Correção: eventos CUSTOM (`callcenter::info`) nunca chegavam Achado ao testar esta fase ponta a ponta com uma chamada real de teste (`originate null/dummy &callcenter(fila@dominio)`): nenhum evento CUSTOM aparecia em `b2bcall-fs-events`, apesar do agente ser ofertado a chamada de verdade (`agent list` mostrava `last_offered_call` mudando). Causa raiz: `event_json(...SUBSCRIBED_EVENTS)` mandava `"CUSTOM"` como último argumento do comando `event json`, sem nenhum subclass depois — o mod_event_socket do FreeSWITCH exige que os subclasses (`callcenter::info`, `sofia::register`, `sofia::gateway_state`, ...) venham logo depois do token `CUSTOM` no mesmo comando, senão zero eventos CUSTOM são entregues (de qualquer subclass). Afetava também `sofia::gateway_state` (ver correção equivalente em docs/TRUNKS.md). Corrigido separando `PLAIN_EVENTS` (nomes normais, cada um vira um listener `.on()`) de `CUSTOM_SUBCLASSES` (só compõem o comando `event json`, nunca viram listener — o client ESL sempre emite `"CUSTOM"` como nome de evento, com o subclass real no header `Event-Subclass`). De quebra, corrigido também o nome de campo errado em `normalizeCustomEvent`: o código lia `CC-Agent-Status`, que não existe — o campo real é `CC-Agent-State`. Os `CC-Action` reais observados no teste (úteis pro monitoramento de filas, agente.md secao 54): `agent-offering`, `agent-state-change`, `bridge-agent-fail`, `member-queue-end` (com `CC-Cause`/`CC-Cancel-Reason` e timestamps de entrada/saída — abandono vs. atendida), `members-count` (contagem ao vivo de chamadas esperando por fila). Ver `packages/telephony/src/normalize-event.ts`.