feat: add outbound routes (Issabel-style dial pattern masking)

Nova aba 'Dialplan -> Rotas de Saída': abstração amigável sobre o dialplan
bruto, no mesmo espírito das Outbound Routes do Issabel/FreePBX. O usuário
informa (prepend) + prefix | match pattern (sintaxe de padrão do Asterisk:
X/Z/N/faixas/coringas, sem o '_' inicial) e escolhe o tronco, sem escrever
exten=>/Dial() à mão.

- packages/database: model OutboundRoute + migration
- apps/api/src/outbound-routes: gerador determinístico (contexto
  b2bcall-outbound-routes, incluído em b2bcall-agents — contexto padrão de
  Extension.context), service/controller CRUD reaproveitando as permissões
  dialplans.*, 6 testes unitários
- apps/api/src/dialplan/dialplan.service.ts: publish() agora gera e
  verifica também o contexto das rotas, no mesmo pipeline de
  versionamento/rollback automático das entradas de dialplan brutas
- apps/frontend: aba com formulário (prepend/prefix/padrão/tronco) e
  preview ao vivo da máscara resultante

O prefixo é sempre removido do número antes de discar e substituído pelo
prepend — o tronco nunca vê o prefixo digitado pelo agente, só o número já
mascarado. Não usado pelo discador preditivo (campanhas já sabem o tronco
via Campaign.trunkId diretamente).

Build/lint/testes (api+frontend) verificados; teste end-to-end contra o
Asterisk real ficou pendente porque a senha do super_admin foi trocada
durante a sessão (acesso legítimo do usuário) — validação via UI delegada
ao usuário.
This commit is contained in:
2026-08-27 20:13:21 -03:00
parent 80e72881b2
commit 9857904c6d
16 changed files with 885 additions and 4 deletions

View File

@@ -181,6 +181,7 @@ model Trunk {
updatedAt DateTime @updatedAt @map("updated_at")
campaigns Campaign[]
outboundRoutes OutboundRoute[]
@@map("trunks")
}
@@ -238,6 +239,34 @@ model DialplanEntry {
@@map("dialplan_entries")
}
// Rota de saída (agente.md seção 21) — abstração amigável sobre o dialplan
// bruto, no mesmo espírito de "Outbound Routes" do Issabel/FreePBX: o
// usuário informa prefixo + padrão de casamento (sintaxe de padrão do
// Asterisk: X/Z/N/faixas/coringas, sem o "_" inicial) + dígitos a
// prepender, e escolhe o tronco — sem escrever `exten =>`/`Dial()` à mão.
// O prefixo é sempre removido do número antes de discar (mascarado),
// substituído pelo prepend, exatamente como no Issabel. Compilada pelo
// DialplanService (junto com dialplan_entries) para o contexto gerado
// "b2bcall-outbound-routes", incluído em "b2bcall-agents".
model OutboundRoute {
id String @id @default(uuid())
name String @unique
description String?
prefix String @default("")
matchPattern String @map("match_pattern")
prepend String?
trunkId String @map("trunk_id")
order Int @default(0)
enabled Boolean @default(true)
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
trunk Trunk @relation(fields: [trunkId], references: [id], onDelete: Restrict)
@@index([order])
@@map("outbound_routes")
}
enum DialplanVersionStatus {
APPLIED
FAILED