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

105 lines
5.2 KiB
Markdown

# 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).