feat(telefonia): rotas de entrada por DID — fundação real pro IVR
Pedido do usuário: "pode iniciar a montar o IVR e as rotas de entrada". Investigando antes de escrever qualquer XML de IVR, achei que NENHUMA chamada de entrada por tronco tinha como funcionar hoje, IVR ou não: nenhuma carregava `b2bcall_tenant_id` (só REGISTER de ramal e discagem de saída setam essa variable), e mesmo corrigindo isso, o profile "external" apontava pro contexto "public" vanilla — um arquivo ESTÁTICO, que sempre ganha de uma consulta ao mod_xml_curl, então nunca seria dinâmico enquanto se chamasse "public". Perguntei ao usuário a granularidade certa (por DID ou por tronco) antes de desenhar o schema — escolheu por DID, mais flexível (um tronco pode carregar vários números com destinos diferentes). `InboundRoute` nova (RLS real, FORCE ROW LEVEL SECURITY): `didNumber` único GLOBAL entre tenants (mesma exceção já aceita em Tenant.telephonyDomain) — é a ÚNICA forma de descobrir de qual tenant é uma chamada de entrada ANTES de identificar o tenant. Resolvido por fan-out sobre tenants ativos, nunca uma query sem contexto de RLS. Dockerfile repontou o profile external pra context="inbound" (sem arquivo estático, cai no mod_xml_curl de verdade). O XML gerado pra esse contexto injeta b2bcall_tenant_id + domain_name (achado real: sem setar domain_name explicitamente, o bridge da "Discagem interna" resolvia pro domínio GLOBAL default, nunca pro do tenant) e transfere pro dialplan real do tenant — reaproveita 100% do que já existe, inclusive pickup de grupo (PHASE 53). CRUD completo (InboundRoutesController, permissions novas no seed) + tela "Telefonia > Rotas de Entrada" no frontend. Testado com uma chamada REAL: um softphone registrado como ramal normal, outro discando direto pro profile external (porta 5080, sem registrar — exatamente como um provedor de tronco manda) um DID cadastrado. `show channels` confirma: tenant certo, contexto certo, domínio certo no bridge, codec PCMU negociado nos dois lados, ramal tocou e atendeu de verdade. Detalhes completos, inclusive uma tentativa de teste que falhou por limitação do canal `loopback` (não um bug), em docs/INBOUND_ROUTES.md. O IVR em si (menu com play_and_get_digits) fica pra próxima fase — esta é a fundação sem a qual nada de chamada de entrada funcionava. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BFaBaBSQGhyXGEgtTYZGV8
This commit is contained in:
75
TODO.md
75
TODO.md
@@ -1996,6 +1996,81 @@ de fora da rede Docker e não conseguiu)
|
||||
precisam estar liberadas lá também — só o `iptables` local foi
|
||||
confirmado
|
||||
|
||||
## PHASE 55 — Status de registro de ramal em tempo real (monitoramento)
|
||||
(pedido do usuário: "antes de ir para ivr no monitoramento tem que
|
||||
mostrar quantos ramais estao online e o status de cada ramal criado")
|
||||
- [x] `Extension.registeredAt` (novo, nullable) — preenchido pelo
|
||||
`apps/freeswitch-events` a cada `sofia::register`/`sofia::unregister`/
|
||||
`sofia::expire` (já consumidos por esse serviço pra outros fins),
|
||||
mais uma reconciliação completa (`show registrations`) ao
|
||||
conectar/reconectar no ESL. `apps/api` roda no host e não alcança
|
||||
`freeswitch:8021` diretamente (rede interna do Docker), por isso a
|
||||
escrita vem de fs-events, mesmo padrão já usado em `Trunk.status`
|
||||
- [x] `GET /extensions` devolve `registeredAt`; `/app/monitoramento`
|
||||
ganhou painel "Ramais" com status ao vivo (online/offline, desde
|
||||
quando, grupo de captura) via os mesmos eventos SSE
|
||||
`EXTENSION_REGISTERED`/`EXTENSION_UNREGISTERED` que já existiam
|
||||
(só apareciam no log de eventos, nunca como estado atual)
|
||||
- [x] Testado contra o container real: reconciliação confirmada marcando
|
||||
online os 2 ramais que já estavam registrados antes do restart do
|
||||
fs-events; `GET /extensions` confirmado devolvendo `registeredAt`
|
||||
correto (null em ramal recém-criado sem registro)
|
||||
|
||||
## PHASE 56 — Rotas de entrada por DID (fundação pro IVR, docs/INBOUND_ROUTES.md)
|
||||
(pedido do usuário: "pode iniciar a montar o IVR e as rotas de entrada")
|
||||
- [x] achado real (bloqueava TUDO de chamada de entrada, não só IVR):
|
||||
nenhuma chamada que chega por um tronco carregava
|
||||
`b2bcall_tenant_id` — só REGISTER de ramal e discagem de saída
|
||||
setam essa variable. Sem ela, `resolveDialplanXml` sempre devolvia
|
||||
"not found" pra qualquer chamada de entrada
|
||||
- [x] achado real #2: mesmo corrigindo isso, o profile `external` (onde
|
||||
trunks recebem chamada) apontava pro contexto `public` vanilla —
|
||||
um ARQUIVO ESTÁTICO (`dialplan/public.xml`). Config estática sempre
|
||||
ganha de uma consulta ao `mod_xml_curl`, então esse contexto nunca
|
||||
seria dinâmico enquanto se chamasse "public". Repontado pra
|
||||
`context="inbound"` (sem arquivo estático nenhum) no Dockerfile
|
||||
- [x] `InboundRoute` (nova tabela, RLS real com FORCE) — decisão do
|
||||
usuário: granularidade por DID/número (não por tronco), pra um
|
||||
tronco poder carregar vários números com destinos diferentes.
|
||||
`didNumber` é `@unique` GLOBAL de propósito (mesma exceção já
|
||||
aceita em `Tenant.telephonyDomain`) — é a ÚNICA forma de descobrir
|
||||
de qual tenant é uma chamada de entrada ANTES de identificar o
|
||||
tenant. Resolvido por fan-out sobre tenants ativos em
|
||||
`apps/freeswitch-config` (nunca uma query sem contexto de RLS)
|
||||
- [x] `buildInboundRouteXml` (`packages/telephony`) gera o XML mínimo do
|
||||
contexto `inbound`: injeta `b2bcall_tenant_id` + `domain_name`
|
||||
(achado real #3: sem setar `domain_name` explicitamente, o
|
||||
`bridge data="user/${destination_number}@${domain_name}"` da regra
|
||||
"Discagem interna" resolvia pro domínio GLOBAL default, não pro do
|
||||
tenant — a leg de entrada não é ramal registrado, nada preenche
|
||||
essa variable sozinho) e transfere pro contexto/destino do tenant,
|
||||
reaproveitando 100% do dialplan já existente (inclusive pickup de
|
||||
grupo, PHASE 53)
|
||||
- [x] `InboundRoutesController` (CRUD completo, permissions
|
||||
`inbound_routes.view`/`.manage` novas no seed de RBAC) + tela
|
||||
"Telefonia > Rotas de Entrada" no frontend (mesmo padrão de
|
||||
Troncos: lista + form inline + remoção com confirmação de 2
|
||||
cliques)
|
||||
- [x] Testado ponta a ponta com uma chamada REAL (não só leitura de
|
||||
código): softphone registrado como ramal normal + um segundo
|
||||
softphone discando DIRETO pro profile `external` (porta 5080, sem
|
||||
registrar — exatamente como um provedor de tronco manda) um DID
|
||||
cadastrado numa `InboundRoute`. Confirmado via `show channels`: a
|
||||
chamada resolveu o tenant certo, transferiu pro contexto certo,
|
||||
bridged com o domínio certo do tenant (não o global), codec PCMU
|
||||
negociado nos dois lados, ramal tocou e atendeu de verdade.
|
||||
(Uma tentativa inicial com `originate loopback/.../inbound` deu
|
||||
`INCOMPATIBLE_DESTINATION` — isolado como limitação do canal
|
||||
`loopback` sem SDP real, não um bug da resolução; ver
|
||||
docs/INBOUND_ROUTES.md pro isolamento completo)
|
||||
- [ ] IVR em si (menu com `play_and_get_digits` + branching por dígito
|
||||
coletado) ainda não construído — é a próxima fase. Precisa
|
||||
widening de `ALLOWED_CONDITION_FIELDS` (hoje um enum fixo) pra
|
||||
aceitar `${variavel}` com a MESMA proteção anti-RCE já aplicada em
|
||||
`data` (secao 180), já que `field` também é expandido pelo
|
||||
FreeSWITCH em tempo de chamada. Ver "O que falta" em
|
||||
docs/INBOUND_ROUTES.md
|
||||
|
||||
---
|
||||
|
||||
## Riscos conhecidos
|
||||
|
||||
Reference in New Issue
Block a user