feat(cdr): call detail records, relatorios e disposicoes
Fecha agente.md secao 152-160. Registro duravel de chamadas — ate aqui o estado de uma chamada so' vivia transitoriamente no canal Redis b2bcall:events (pub/sub sem historico). ## Modelo calls/call_legs/call_events (secao 153, tenant-scoped, RLS) + dispositions (secao 89, "Call Center -> Disposicoes", personalizavel por tenant, mesmo padrao de PauseReason). dial_attempts da especificacao nao virou tabela nova — CallAttempt (fase Predictive Engine) ja cobre esse conceito; Call.attemptId liga um Call a' sua tentativa de discagem. Call.id = o proprio freeswitch_uuid da perna principal (sem suporte a transferencia entre uuids nesta fase). ## apps/freeswitch-events/src/cdr.ts Cada NormalizedEvent relevante faz upsert em Call + insere em call_events (a trilha bruta). CALL_ENDED calcula os agregados em segundos (secao 155-156): ringTime/waitTime/talkTime/durationSeconds/billableSeconds. ## Dois bugs reais achados e corrigidos testando esta fase - AGENT_OFFERED_CALL/AGENT_BRIDGE_FAILED disparam de uma thread interna do mod_callcenter (outbound_agent_thread_run), sem contexto de channel — nao tem header Unique-ID, entao callUuid ficava undefined e os dois eram descartados silenciosamente (Call.queueId/agentId nunca preenchidos mesmo com bridge/falha de bridge reais). Corrigido com fallback pro CC-Member-Session-UUID (data.memberSessionUuid), mesmo identificador ja usado pra correlacao equivalente no predictive-dialer. - Corrida entre CALL_CREATED/CALL_ANSWERED (persistCallEvent roda sem await, cada evento abre sua propria transacao) podia fazer answerAt aparecer antes de createdAt quando o upsert que criava a linha usava now() do momento errado (nao do occurredAt do evento real). Corrigido setando createdAt explicito a partir de normalized.occurredAt. ## Relatorios (apps/api/src/reports) GET /reports/queues (secao 159): recebidas/atendidas/abandonadas/TME/TMA/ Service Level/Abandon Rate por fila. GET /reports/agents (secao 158): tempo logado/pausado/por estado (AgentStateEvent pareado) + chamadas atendidas/TMA. GET /reports/campaigns (secao 160): leads/attempts/ answered/agent connected/busy/no answer/failed/callbacks/rates/TME/TMA — "Valor Telefonia"/"Valor IA" ficam null (dependem de Billing, fase propria). ## GET /calls e disposicao Secao 157: filtros por data/ramal/agente/fila/campanha/trunk/telefone/ hangup cause/disposicao, sempre escopado ao tenant do JWT. PATCH /calls/:id/disposition (secao 89): o proprio agente que atendeu marca (compara Call.agentId contra o Agent do usuario autenticado, nunca um agentId vindo do client), supervisor (agents.manage) pode marcar em nome de outro agente. Verificado ponta a ponta: campanha com 5 leads, 3 ANSWERED simulados entrando na fila real, Call.queueId/agentId/hangupCause corretos (confirmando a correcao da correlacao), createdAt<=answerAt em todos, durationSeconds batendo com discard_abandoned_after; os 3 relatorios com numeros internamente consistentes entre si e com os logs do discador (received:3/abandoned:3/abandonRate:1, leads:5/attempts:5/answered:3/ answerRate:0.6); disposicao gravada com ownership check correto; queue list do FreeSWITCH confirmou calls_abandoned=4 real ao final. typecheck do workspace inteiro limpo. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X1HxY46WGU4G1zmVDNKcWw
This commit is contained in:
54
TODO.md
54
TODO.md
@@ -376,10 +376,58 @@
|
||||
agente (secao 89) — fora do escopo, ficam pra fase CDR
|
||||
- [ ] CPS a nível de trunk — só aplica quando o caminho PSTN real rodar
|
||||
- [ ] Relatório de campanha (secao 160), TME/TMA/Service Level/Abandon
|
||||
Rate agregados — dependem de CDR
|
||||
Rate agregados — implementado na PHASE 17
|
||||
|
||||
## PHASE 17+ — ver `agente.md` seções 87 em diante (CDR, Recordings, AI,
|
||||
Billing, Frontend, Reports, Security, Tests)
|
||||
## PHASE 17 — CDR (agente.md secao 152-160)
|
||||
- [x] `calls`/`call_legs`/`call_events` (tenant-scoped, RLS) +
|
||||
`dispositions` (secao 89). `dial_attempts` da especificação não
|
||||
virou tabela nova — `CallAttempt` (fase Predictive Engine) já cobre
|
||||
o conceito; `Call.attemptId` liga um Call à sua tentativa
|
||||
- [x] `apps/freeswitch-events/src/cdr.ts`: cada NormalizedEvent relevante
|
||||
faz upsert em Call + insere em call_events; CALL_ENDED calcula os
|
||||
agregados em segundos (secao 155-156: ringTime/waitTime/talkTime/
|
||||
durationSeconds/billableSeconds)
|
||||
- [x] **Bug real, achado no teste desta fase**: `AGENT_OFFERED_CALL`/
|
||||
`AGENT_BRIDGE_FAILED` disparam de uma thread interna do
|
||||
mod_callcenter sem contexto de channel — não têm header Unique-ID,
|
||||
então `callUuid` ficava undefined e os dois eram descartados
|
||||
silenciosamente (Call.queueId/agentId nunca preenchidos mesmo com
|
||||
bridge/falha de bridge reais). Corrigido com fallback pro
|
||||
`CC-Member-Session-UUID` (data.memberSessionUuid).
|
||||
- [x] **Bug real, achado no teste desta fase**: corrida entre
|
||||
CALL_CREATED/CALL_ANSWERED (persistCallEvent roda sem await, cada
|
||||
evento abre sua própria transação) podia fazer `answerAt` aparecer
|
||||
antes de `createdAt` quando o upsert que criava a linha usava
|
||||
`now()` do momento errado. Corrigido setando `createdAt` explicito
|
||||
a partir de `normalized.occurredAt` no branch de criação.
|
||||
- [x] `GET /reports/queues` (secao 159): recebidas/atendidas/abandonadas/
|
||||
TME/TMA/Service Level/Abandon Rate por fila
|
||||
- [x] `GET /reports/agents` (secao 158): tempo logado/pausado/por estado
|
||||
(AgentStateEvent pareado) + chamadas atendidas/TMA
|
||||
- [x] `GET /reports/campaigns` (secao 160): leads/attempts/answered/agent
|
||||
connected/busy/no answer/failed/callbacks/rates/TME/TMA —
|
||||
"Valor Telefonia"/"Valor IA" ficam null (dependem de Billing)
|
||||
- [x] `GET /calls` (secao 157, filtros por data/ramal/agente/fila/
|
||||
campanha/trunk/telefone/hangup cause/disposição) + `PATCH /calls/
|
||||
:id/disposition` (secao 89, o próprio agente que atendeu marca, ou
|
||||
supervisor com agents.manage)
|
||||
- [x] Testado ponta a ponta: campanha com 5 leads, 3 ANSWERED simulados
|
||||
entrando na fila real, Call.queueId/agentId/hangupCause corretos
|
||||
(confirmando a correção da correlação), createdAt<=answerAt em
|
||||
todos, durationSeconds batendo com discard_abandoned_after; os 3
|
||||
relatórios com números internamente consistentes entre si e com os
|
||||
logs do discador; disposição gravada com ownership check correto
|
||||
- [ ] extensionId/sipCallId/callerNumber/calledNumber — não populados
|
||||
ainda (nenhum evento atual carrega esses dados de forma confiável)
|
||||
- [ ] direction: só distingue OUTBOUND (tem campanha) de INTERNAL —
|
||||
detecção de INBOUND de verdade não implementada
|
||||
- [ ] call_legs — schema existe, nada escreve ainda (só faz sentido com
|
||||
transferência entre uuids)
|
||||
- [ ] Cross-tenant reports pra platform admin (secao 157) — sem console
|
||||
de plataforma ainda
|
||||
|
||||
## PHASE 18+ — ver `agente.md` seções 90 em diante (Recordings, AI,
|
||||
Billing, Frontend, Security, Tests)
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user