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

@@ -14,6 +14,10 @@ import { TELEPHONY_PROVIDER } from '../telephony/telephony.module';
import { CreateDialplanEntryDto } from './dto/create-dialplan-entry.dto';
import { UpdateDialplanEntryDto } from './dto/update-dialplan-entry.dto';
import { generateDialplanConfig } from './dialplan-generator';
import {
generateOutboundRoutesConfig,
OUTBOUND_ROUTES_CONTEXT,
} from '../outbound-routes/outbound-route-generator';
const DIALPLAN_FILE_PATH = '/etc/asterisk-generated/b2bcall-dialplan.conf';
@@ -155,7 +159,19 @@ export class DialplanService {
const entries = await this.prisma.dialplanEntry.findMany({
where: { enabled: true },
});
const newConfig = generateDialplanConfig(entries);
const outboundRoutes = await this.prisma.outboundRoute.findMany({
where: { enabled: true },
include: { trunk: { select: { name: true } } },
});
const newConfig =
generateDialplanConfig(entries) +
'\n' +
generateOutboundRoutesConfig(outboundRoutes);
const verifyContexts = [
...entries.map((e) => ({ context: e.context })),
{ context: OUTBOUND_ROUTES_CONTEXT },
{ context: 'b2bcall-agents' },
];
const previous = await this.prisma.dialplanVersion.findFirst({
where: { status: DialplanVersionStatus.APPLIED },
@@ -173,7 +189,7 @@ export class DialplanService {
);
}
const failureReason = await this.verify(entries);
const failureReason = await this.verify(verifyContexts);
if (failureReason) {
// Rollback automático — nunca deixa uma configuração inválida ativa.