e75872faec900d261ba98ac117f04d02db0830df
3 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| 7b62ad3d82 |
feat(entitlements,campaigns): plans/quotas + campanhas, leads, lista de bloqueio
Fecha duas fases: Plans/Entitlements (agente.md secao 56-62), que tinha
ficado pra trás desde o inicio, e Campanhas/Leads/Lista de Bloqueio (secao
63-71).
## Plans/Entitlements
A ordem de implementacao da propria especificacao (secao 232) coloca
Plans/Entitlements logo depois de PostgreSQL RLS, bem antes de FreeSWITCH
— mas o build seguiu direto sem essa peca, e toda fase desde entao
documentou "quota depende de Plans/Entitlements" como pendencia
(EXTENSIONS.md, TRUNKS.md, AGENTS.md, QUEUES.md, agora todas atualizadas).
Fechado agora porque Campanhas precisa de max_campaigns e o proximo CPS
Limiter vai precisar de max_cps/max_concurrent_calls.
- plans: catalogo compartilhado entre tenants (sem RLS, nao e' tenant-
scoped) com todos os campos de entitlement da secao 56. Campo de limite
null = "sem limite", nunca "sem plano" — tenants.plan_id e' obrigatorio,
nunca null (secao 56: nao espalhar `if plan == PRO` pelo codigo).
- Migration hand-escrita: cria plans, insere seed "trial", faz backfill de
plan_id pros tenants ja existentes, so' depois torna NOT NULL (Postgres
nao deixa NOT NULL sem default em tabela nao-vazia).
- packages/entitlements (pacote novo): assertQuota/assertFeatureEnabled,
erros mapeados pra 403 no DomainExceptionFilter.
- Retrofit em Extensions/Trunks/Agents/Queues: contam linhas ativas e
checam quota antes de criar.
## Campanhas, Leads, Lista de Bloqueio
Deliberadamente so' o modelo/CRUD/maquina de estados — o motor que de fato
origina chamadas (PredictiveDialerEngine, secao 72-86: dados em tempo
real, EWMA, CPS distribuido, reserva atomica de lead, lock de campanha,
bgapi originate, controle de abandono, retry) e' um sistema grande o
suficiente pra merecer fase propria (secao 72: "nao e' so' `for lead ->
originate`").
- campaigns/leads/suppression_entries (tenant-scoped, RLS).
- Maquina de estados da campanha (secao 64-66): start/pause/drain/stop com
tabela de transicoes validas — transicao invalida retorna 400, nunca
ignora silenciosamente. Apagar bloqueado enquanto RUNNING/DRAINING.
- packages/shared/src/phone.ts (secao 70): normalizacao dedicada,
preparada pra E.164 completo, so' BR implementado.
- Importacao CSV em batches de 1000 (secao 69): detecta duplicado (dentro
do CSV + contra leads existentes), checa lista de bloqueio (importa como
DO_NOT_CALL, nao descarta), retorna {total, valid, invalid, duplicates,
imported, suppressed}.
- Lista de bloqueio (secao 71): CRUD tenant-scoped.
Verificado ponta a ponta: campanha com queueId/trunkId invalido e
pacingMin > pacingMax rejeitados; CSV de 5 linhas (1 invalida, 1
duplicada, 1 bloqueada) importado corretamente; start->pause->drain->stop
e transicoes invalidas todas corretas; 3a campanha rejeitada por quota
(max_campaigns=2 do plano trial); 6a extensao rejeitada por quota
(max_extensions=5). Suites de teste existentes (tenant-isolation, auth)
atualizadas pro novo Tenant.planId obrigatorio e passando.
typecheck do workspace inteiro limpo.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X1HxY46WGU4G1zmVDNKcWw
|
|||
| b940ce3e63 |
fix(events): eventos CUSTOM do ESL nunca eram entregues
Achado ao iniciar a fase de Realtime Monitoring: nenhum evento CUSTOM (sofia::register, sofia::gateway_state, callcenter::info) jamais chegou em b2bcall-fs-events nesta sessao, apesar do estado real do FreeSWITCH mudar de verdade (confirmado originando uma chamada de teste pra dentro de uma fila real com agente logado). Isso deixava dois gaps documentados como "nao verificado" em docs/TRUNKS.md e docs/AGENTS.md. Causa raiz: `event_json(...SUBSCRIBED_EVENTS)` mandava "CUSTOM" como ultimo token do comando `event json`, sem nenhum subclass depois. O mod_event_socket do FreeSWITCH exige que os subclasses (callcenter::info, sofia::register, ...) venham imediatamente depois do token CUSTOM no mesmo comando — sem isso, zero eventos CUSTOM sao entregues, de qualquer subclass. Corrigido separando PLAIN_EVENTS (nomes normais, cada um vira um listener .on()) de CUSTOM_SUBCLASSES (so compoe o comando de subscricao — o client ESL sempre emite "CUSTOM" como nome de evento, com o subclass real no header Event-Subclass). Corrigido tambem um bug de nome de campo: normalizeCustomEvent lia CC-Agent-Status, que nao existe; o campo real e CC-Agent-State. Com o pipeline corrigido, chegam eventos ricos de callcenter::info nunca antes observados: agent-offering, bridge-agent-fail, members-count (fila em tempo real) e member-queue-end (com CC-Cause/CC-Cancel-Reason e timestamps de entrada/saida — atendida vs. abandonada). Adicionados como novos tipos normalizados: AGENT_OFFERED_CALL, AGENT_BRIDGE_FAILED, QUEUE_MEMBER_COUNT, QUEUE_MEMBER_LEFT. De quebra, achado e corrigido um segundo bug real ao reverificar Trunks com o pipeline de eventos funcionando: `sofia profile external rescan` nunca descarregava um gateway cujo arquivo .xml foi apagado (fica fantasma na memoria do Sofia indefinidamente). trunk-sync.ts agora roda `sofia profile external killgw <nome>` pra cada gateway removido, antes do rescan. Reverificado ponta a ponta pra ambos os bugs: - Trunk com register:true apontando pra host inexistente: Trunk.status no banco passa de UNKNOWN pra FAILED sozinho, via evento, sem polling. - Trunk apagado via API: gateway some imediatamente de `sofia status gateway`, sem esperar reinicio de profile. - Chamada de teste real numa fila com agente logado: members-count, agent-offering, agent-state-change (CC-Agent-State correto: Waiting/ Receiving), bridge-agent-fail, todos chegando certos no canal Redis b2bcall:events. typecheck do workspace inteiro limpo. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X1HxY46WGU4G1zmVDNKcWw |
|||
| 4c638ad496 |
feat: implement Trunks with real FreeSWITCH gateway sync
- trunks table (tenant-scoped, RLS): host/proxy/realm, register, username/password_enc (AES-256-GCM), dtmf_mode, ping, transport, and a status/status_updated_at pair meant to be driven by FreeSWITCH events - apps/api/src/trunks: CRUD (POST/GET/GET:id/DELETE), same RBAC/tenant pattern as Extensions, password never exposed in any GET - packages/telephony: buildGatewayXml() generates a Sofia gateway XML file - b2bcall-fs-config now writes sip_profiles/external/<trunk_id>.xml (shared Docker volume with FreeSWITCH -- the vanilla external profile already includes external/*.xml) and runs 'sofia profile external rescan' over ESL; syncs on boot and on demand via Redis pub/sub (b2bcall:trunks:sync), since apps/api runs on the host and fs-config has no port published to reach directly - added FreeSwitchTelephonyProvider.waitUntilConnected() to fix a startup race: the first sync ran before the ESL connection had settled, logging a harmless but noisy error - verified end-to-end with a fake host: create trunk -> gateway file written -> FreeSWITCH shows the real gateway (FAIL_WAIT, expected) -> delete -> file removed (cleanup also correctly swept the stale 'example.com' gateway that had been copied into the volume from the vanilla image) - apps/freeswitch-events/src/trunk-status.ts: written to update Trunk.status from sofia::gateway_state events, using the same normalizeEslEvent path already proven for CHANNEL_* events - KNOWN GAP, documented rather than glossed over: monitored fs-events for ~90s while the gateway visibly transitioned states in FreeSWITCH (FAIL_WAIT/DOWN) and no sofia::gateway_state event was observed arriving. CUSTOM/sofia::* events have not actually been proven working end-to-end in this session -- only CHANNEL_* events have been. Needs verification against a real SIP target before the status auto-update can be trusted in production. See docs/TRUNKS.md and TODO.md. - docs/TRUNKS.md |