Files
B2BCall-dialer/docs/DIALPLAN.md
Matheus 83eb2c5aea fix(telefonia): tenant novo nasce sem conseguir discar entre ramais
Achado real reportado pelo usuário: "a ligacao entre ramais nao esta
funcionando". Não era regressão de nada desta sessão — o tenant onde o
usuário registrou os próprios ramais de teste tinha ZERO regras de
dialplan e ZERO versão ativa. Nenhum tenant nascia com a regra de
"discagem interna": só existia nos tenants onde eu mesmo configurei
manualmente durante testes anteriores (Acme). Todo tenant novo nascia
incapaz de ligar entre os próprios ramais até alguém configurar isso à
mão em Telefonia > Dialplan — um gap real de produto.

`DEFAULT_DIALPLAN_EXTENSIONS` (novo, packages/telephony) tem as mesmas 2
regras já testadas ponta a ponta com chamada real (PHASE 53): discagem
interna com pickup de grupo + `*8`. `TenantsController.create()` agora
semeia essas regras e já ativa a versão 1 do contexto default, na mesma
transação de criação do tenant.

Reparo pontual aplicado no tenant existente do usuário (mesmas 2 regras,
via SQL direto — não existe endpoint de "reparar tenant já criado").
Testado com uma chamada real: originate de um ramal pro outro tocou de
verdade no aparelho do usuário (RINGING -> NO_ANSWER, não mais dialplan
not found). Auto-seed testado criando um tenant novo de verdade via
POST /tenants — confirmado no banco que a versão 1 já nasce ACTIVE.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BFaBaBSQGhyXGEgtTYZGV8
2026-08-30 17:10:15 -03:00

5.2 KiB

Dialplan

Agente.md secao 43-44. Editor estruturado (não textarea) + versionamento com ativação/rollback explícitos.

Modelo

  • dialplan_extensions (tenant-scoped, RLS): fonte editável — context, name, conditionField/conditionExpr (uma condição por extension, simplificação deliberada em relação ao FreeSWITCH puro, que permite várias <condition> por extension), actions/antiActions (JSON, array de {application, data}), continueOnFalse, order, enabled.
  • dialplan_versions (tenant-scoped, RLS): snapshot gerado — context, version (incremental por tenant+context), generatedXml, status (DRAFT/ACTIVE/SUPERSEDED), createdByUserId, activatedAt.

Fluxo (agente.md secao 44)

POST /dialplan/extensions          -- edita as linhas (não afeta chamadas)
POST /dialplan/versions/generate   -- gera XML a partir das linhas atuais,
                                       valida bem-formação (fast-xml-parser),
                                       salva como nova versão DRAFT
POST /dialplan/versions/:id/activate -- marca ACTIVE, a anterior vira
                                       SUPERSEDED (transação atômica)

Reativar uma versão antiga é o próprio rollback — não existe endpoint separado. GET /dialplan/versions mostra o histórico completo.

Dialplan padrão pra tenant novo (PHASE 57)

Achado real reportado pelo usuário: "a ligação entre ramais não está funcionando" — nenhum tenant nascia com nenhuma regra de dialplan, então nenhum tenant novo conseguia discar entre os próprios ramais até alguém configurar manualmente em Telefonia > Dialplan. DEFAULT_DIALPLAN_EXTENSIONS (packages/telephony/src/default-dialplan.ts) tem as 2 regras mínimas (discagem interna com pickup de grupo + *8, já testadas ponta a ponta com chamada real na PHASE 53) — TenantsController.create() semeia essas regras e já ativa a versão 1 do contexto default, dentro da MESMA transação de criação do tenant. Um tenant criado ANTES desta fase precisa de reparo manual (inserir as mesmas 2 linhas + gerar/ativar uma versão).

Mecanismo: dinâmico por chamada, não arquivo+rescan

Ao contrário de Trunks (gateways carregados uma vez no boot/rescan), dialplan é resolvido pelo FreeSWITCH a cada chamada via mod_xml_curl — então não precisa de sincronização por arquivo nem de avisar o FreeSWITCH quando uma versão é ativada. b2bcall-fs-config serve generatedXml da versão ACTIVE diretamente na resposta HTTP; "ativar" uma versão tem efeito imediato na próxima chamada.

Resolução de tenant é feita pelo channel variable variable_b2bcall_tenant_id — que já vem setado desde o registro/ autenticação do ramal (buildDirectoryUserXml, fase Extensions). Diferente do directory (resolvido por domain, hoje um valor fixo compartilhado por todos os tenants — limitação conhecida), o dialplan não sofre dessa ambiguidade porque a variável já está no contexto da própria chamada.

Segurança: allowlist de applications (agente.md secao 180)

ALLOWED_DIALPLAN_APPLICATIONS (packages/telephony/src/dialplan-xml.ts) restringe action.application a um conjunto seguro (answer, bridge, playback, hangup, set, export, ...) — de propósito sem system, exec, socket/Lua-eval ou qualquer application capaz de rodar comandos no host. Sem isso, um tenant_admin malicioso ou comprometido poderia configurar uma "rota de dialplan" que executa comandos arbitrários no servidor FreeSWITCH.

Achado crítico durante os testes desta fase

Ao testar deliberadamente que a allowlist rejeitava application: "system", a API aceitou com 201 — a validação inteira de apps/api (todo @Body(), em todos os controllers, desde que a API foi criada) estava silenciosamente inoperante rodando via tsx. Causa raiz, correção e o que foi limpo: ver docs/VALIDATION_PIPE_BUG.md (achado grande o suficiente pra merecer documento próprio). Resumo da correção: apps/api agora builda com tsc de verdade antes de rodar — nunca mais tsx src/main.ts direto.

Verificado ponta a ponta (já com a validação corrigida)

POST /dialplan/extensions {"conditionExpr":"^7000$","actions":[answer,playback,hangup]}
POST /dialplan/versions/generate?context=default   → v1 DRAFT
POST /dialplan/versions/:id/activate               → v1 ACTIVE

originate {b2bcall_tenant_id=<tenant>}null/_test_ 7000 XML default
→ fs-events: CALL_CREATED → CALL_ANSWERED → CALL_ENDED (tenantId correto)

Criei uma segunda versão (v2, rota diferente), ativei — v1 virou SUPERSEDED. Reativei v1 (rollback) — v2 virou SUPERSEDED, v1 voltou pra ACTIVE. Confirmado com GET /dialplan/versions.

O que falta

  • Só uma <condition> por extension (simplificação) — FreeSWITCH suporta múltiplas em sequência; revisitar se algum caso de uso real precisar.
  • dialplan.view/dialplan.manage não existem como permissions próprias — reusei freeswitch.view/freeswitch.configure (já existentes na seção 145 do agente.md), que cobrem semanticamente "configuração do FreeSWITCH". Se precisar de granularidade maior no futuro, criar permissions dedicadas.
  • Sem UI ainda (fase Frontend).