diff --git a/TODO.md b/TODO.md
index 762afe3..f525cce 100644
--- a/TODO.md
+++ b/TODO.md
@@ -2417,6 +2417,45 @@ já em produção] que depois vai vir via sip comum ao b2bcall")
`sip-user`/`sip-domain`/`sip-server` corretos e a senha presente
(não vazia) assim que o agente com ramal vinculado abre `/app`.
+## PHASE 67 — Dropdown de destino (fila/outro IVR) numa opção de IVR
+(pedido do usuário: "dentro do IVR tem que dar a opção de eu colocar como
+destino não apenas os ramais mas tb as filas e outra IVR, como feito na
+rota de entrada")
+- [x] Mesmo gap que existia em Rotas de Entrada até a PHASE 62:
+ `IvrMenuOption` só tinha `destinationNumber` (sempre interpretado
+ como ramal), sem tipo nenhum. Novo enum próprio
+ `IvrOptionDestinationType` (EXTENSION/QUEUE/IVR) — SEM
+ `CALL_GROUP` de propósito: diferente de `InboundRoute` (resolvido
+ por chamada, sempre fresco), o dialplan de um IVR é compilado UMA
+ VEZ ao salvar o menu — "quem está no grupo agora" ficaria
+ desatualizado até a próxima edição, comportamento silenciosamente
+ stale que ninguém pediu.
+- [x] `buildIvrDialplanExtensions` (packages/telephony) — cada branch de
+ dígito decide a action pelo `destinationType`, mesmo padrão de
+ `buildInboundRouteXml`: EXTENSION continua `bridge` direto;
+ QUEUE vira `answer` + `callcenter`; IVR vira `transfer` pro
+ `IVR_ENTRY_DESTINATION` do contexto do menu alvo — reaproveita a
+ MESMA entrada que uma `InboundRoute` usa pra entrar naquele menu.
+- [x] "Outro IVR" cria um grafo entre menus que nunca existia antes (uma
+ `InboundRoute` não é ela mesma um menu, nunca podia formar ciclo).
+ `assertNoIvrCycle` (novo, `ivr-menus.controller.ts`) monta o grafo
+ com TODOS os menus do tenant antes de compilar e rejeita
+ (409) qualquer combinação que criaria um ciclo — A→B→A ou até
+ A→A — checado tanto em `create` quanto `update`.
+- [x] Frontend: os dois editores de IVR (form clássico `ivr-view.tsx` e
+ o editor visual `ivr-flow-editor.tsx`, PHASE 60/61) ganharam o
+ mesmo dropdown "Tipo" + "Destino" condicional já usado em Rotas de
+ Entrada — precisou buscar `queues` na `page.tsx` do IVR (só
+ `extensions` era buscado antes) e passar a lista de OUTROS menus
+ (excluindo o próprio, de propósito, pra reduzir a chance de
+ self-reference direto na UI — o backend segue sendo quem
+ garante isso de verdade).
+- [x] Testado ponta a ponta com Playwright, tenant/fila/2 menus de IVR
+ reais criados na hora: Menu A com opção tipo Fila (aponta pra
+ "Fila QA"), Menu B com opção tipo Outro IVR (aponta pro Menu A) —
+ os dois persistiram com o tipo certo, sobrevivendo a um reload
+ completo da página.
+
---
## Riscos conhecidos
diff --git a/apps/api/src/ivr/dto/create-ivr-menu.dto.ts b/apps/api/src/ivr/dto/create-ivr-menu.dto.ts
index abcf794..ff75328 100644
--- a/apps/api/src/ivr/dto/create-ivr-menu.dto.ts
+++ b/apps/api/src/ivr/dto/create-ivr-menu.dto.ts
@@ -3,10 +3,18 @@ import { ArrayMaxSize, ArrayMinSize, IsArray, IsIn, IsNumber, IsOptional, IsStri
import { ALLOWED_IVR_DIGITS } from "@b2bcall/telephony";
import { IsSafeDialplanData } from "../../dialplan/dto/safe-dialplan-data.validator";
+const IVR_OPTION_DESTINATION_TYPES = ["EXTENSION", "QUEUE", "IVR"] as const;
+
export class IvrMenuOptionDto {
@IsIn(ALLOWED_IVR_DIGITS)
digit!: string;
+ // PHASE 67 — decide como destinationNumber/destinationContext são
+ // interpretados (ver comentário em IvrMenuOption no schema).
+ @IsOptional()
+ @IsIn(IVR_OPTION_DESTINATION_TYPES)
+ destinationType?: (typeof IVR_OPTION_DESTINATION_TYPES)[number];
+
@IsString()
@Matches(/^[a-zA-Z0-9_-]{1,40}$/, { message: "destinationNumber deve ser alfanumérico (1 a 40 caracteres)" })
destinationNumber!: string;
diff --git a/apps/api/src/ivr/ivr-menus.controller.ts b/apps/api/src/ivr/ivr-menus.controller.ts
index 80253e5..fc4bd44 100644
--- a/apps/api/src/ivr/ivr-menus.controller.ts
+++ b/apps/api/src/ivr/ivr-menus.controller.ts
@@ -49,6 +49,53 @@ function promptHostPath(tenantId: string, menuId: string): string {
return join(IVR_PROMPTS_HOST_ROOT, tenantId, `${menuId}.wav`);
}
+/**
+ * PHASE 67 — destino "Outro IVR" cria um grafo entre menus (algo que
+ * InboundRoute->IVR nunca tinha, já que uma rota de entrada não é ela
+ * mesma um menu). Sem essa checagem, dois menus apontando um pro outro
+ * (ou um menu apontando pra si mesmo) compilam sem erro nenhum e só
+ * travam a experiência do CHAMADOR depois (fica indo de um prompt pro
+ * outro pra sempre) — pega isso antes de compilar, não depois de alguém
+ * reclamar. Monta o grafo com TODOS os menus do tenant (não só os que já
+ * existiam antes desta operação), substituindo as arestas do menu sendo
+ * salvo pelas opções novas — cobre tanto create quanto update.
+ */
+async function assertNoIvrCycle(
+ tx: Prisma.TransactionClient,
+ tenantId: string,
+ menuContext: string,
+ newOptions: IvrMenuOptionInput[],
+): Promise {
+ const menus = await tx.ivrMenu.findMany({
+ where: { tenantId, deletedAt: null },
+ select: { context: true, options: { select: { destinationType: true, destinationContext: true } } },
+ });
+
+ const edges = new Map();
+ for (const m of menus) {
+ edges.set(
+ m.context,
+ m.options.filter((o) => o.destinationType === "IVR").map((o) => o.destinationContext),
+ );
+ }
+ edges.set(
+ menuContext,
+ newOptions.filter((o) => o.destinationType === "IVR").map((o) => o.destinationContext),
+ );
+
+ const visited = new Set();
+ const stack = [...(edges.get(menuContext) ?? [])];
+ while (stack.length > 0) {
+ const next = stack.pop()!;
+ if (next === menuContext) {
+ throw new ConflictException(`Esse destino criaria um ciclo entre menus de IVR (via "${next}")`);
+ }
+ if (visited.has(next)) continue;
+ visited.add(next);
+ stack.push(...(edges.get(next) ?? []));
+ }
+}
+
/**
* Tela de autoria de IVR (PHASE 58, docs/INBOUND_ROUTES.md) — por cima do
* editor genérico de dialplan (PHASE 56/57): criar/editar um `IvrMenu`
@@ -133,22 +180,31 @@ export class IvrMenusController {
const prisma = getPrismaClient();
const tenantId = user.tenantId!;
+ const createOptions: IvrMenuOptionInput[] = dto.options.map((o) => ({
+ digit: o.digit,
+ destinationType: o.destinationType ?? "EXTENSION",
+ destinationNumber: o.destinationNumber,
+ destinationContext: o.destinationContext ?? "default",
+ }));
+
let menu;
try {
menu = await withTenantContext(prisma, tenantId, async (tx) => {
+ await assertNoIvrCycle(tx, tenantId, dto.context, createOptions);
const created = await tx.ivrMenu.create({
data: { tenantId, name: dto.name, context: dto.context, greeting: dto.greeting },
});
await tx.ivrMenuOption.createMany({
- data: dto.options.map((opt) => ({
+ data: createOptions.map((opt, i) => ({
tenantId,
ivrMenuId: created.id,
digit: opt.digit,
+ destinationType: opt.destinationType,
destinationNumber: opt.destinationNumber,
- destinationContext: opt.destinationContext ?? "default",
- label: opt.label,
- positionX: opt.positionX,
- positionY: opt.positionY,
+ destinationContext: opt.destinationContext,
+ label: dto.options[i].label,
+ positionX: dto.options[i].positionX,
+ positionY: dto.options[i].positionY,
})),
});
return created;
@@ -160,11 +216,6 @@ export class IvrMenusController {
throw err;
}
- const createOptions: IvrMenuOptionInput[] = dto.options.map((o) => ({
- digit: o.digit,
- destinationNumber: o.destinationNumber,
- destinationContext: o.destinationContext ?? "default",
- }));
await compileAndActivateIvrDialplan(prisma, tenantId, user.sub, menu, createOptions);
await recordAuditEvent(prisma, {
@@ -219,7 +270,24 @@ export class IvrMenusController {
);
if (!existing) throw new NotFoundException();
+ const options: IvrMenuOptionInput[] = dto.options
+ ? dto.options.map((o) => ({
+ digit: o.digit,
+ destinationType: o.destinationType ?? "EXTENSION",
+ destinationNumber: o.destinationNumber,
+ destinationContext: o.destinationContext ?? "default",
+ }))
+ : existing.options.map((o) => ({
+ digit: o.digit,
+ destinationType: o.destinationType,
+ destinationNumber: o.destinationNumber,
+ destinationContext: o.destinationContext,
+ }));
+
const menu = await withTenantContext(prisma, tenantId, async (tx) => {
+ if (dto.options) {
+ await assertNoIvrCycle(tx, tenantId, existing.context, options);
+ }
const updated = await tx.ivrMenu.update({
where: { id },
data: {
@@ -232,12 +300,13 @@ export class IvrMenusController {
if (dto.options) {
await tx.ivrMenuOption.deleteMany({ where: { ivrMenuId: id } });
await tx.ivrMenuOption.createMany({
- data: dto.options.map((opt) => ({
+ data: dto.options.map((opt, i) => ({
tenantId,
ivrMenuId: id,
digit: opt.digit,
+ destinationType: options[i].destinationType,
destinationNumber: opt.destinationNumber,
- destinationContext: opt.destinationContext ?? "default",
+ destinationContext: options[i].destinationContext,
label: opt.label,
positionX: opt.positionX,
positionY: opt.positionY,
@@ -247,10 +316,6 @@ export class IvrMenusController {
return updated;
});
- const options: IvrMenuOptionInput[] = dto.options
- ? dto.options.map((o) => ({ digit: o.digit, destinationNumber: o.destinationNumber, destinationContext: o.destinationContext ?? "default" }))
- : existing.options.map((o) => ({ digit: o.digit, destinationNumber: o.destinationNumber, destinationContext: o.destinationContext }));
-
await compileAndActivateIvrDialplan(prisma, tenantId, user.sub, menu, options);
await recordAuditEvent(prisma, {
@@ -342,6 +407,7 @@ export class IvrMenusController {
const options: IvrMenuOptionInput[] = menu.options.map((o) => ({
digit: o.digit,
+ destinationType: o.destinationType,
destinationNumber: o.destinationNumber,
destinationContext: o.destinationContext,
}));
@@ -380,6 +446,7 @@ export class IvrMenusController {
const options: IvrMenuOptionInput[] = menu.options.map((o) => ({
digit: o.digit,
+ destinationType: o.destinationType,
destinationNumber: o.destinationNumber,
destinationContext: o.destinationContext,
}));
diff --git a/apps/frontend/src/app/app/telefonia/ivr/actions.ts b/apps/frontend/src/app/app/telefonia/ivr/actions.ts
index 2f00c66..57ad898 100644
--- a/apps/frontend/src/app/app/telefonia/ivr/actions.ts
+++ b/apps/frontend/src/app/app/telefonia/ivr/actions.ts
@@ -3,7 +3,7 @@
import { revalidatePath } from "next/cache";
import { requireSession } from "@/lib/session";
import { apiFetch, ApiError, API_BASE_URL } from "@/lib/api";
-import type { IvrMenu } from "@/lib/callcenter-types";
+import type { IvrMenu, IvrOptionDestinationType } from "@/lib/callcenter-types";
function extractErrorMessage(err: unknown): string {
if (err instanceof ApiError) {
@@ -21,7 +21,9 @@ function extractErrorMessage(err: unknown): string {
export interface IvrMenuOptionInput {
digit: string;
+ destinationType?: IvrOptionDestinationType;
destinationNumber: string;
+ destinationContext?: string;
label?: string;
positionX?: number;
positionY?: number;
diff --git a/apps/frontend/src/app/app/telefonia/ivr/ivr-flow-editor.tsx b/apps/frontend/src/app/app/telefonia/ivr/ivr-flow-editor.tsx
index a12b1bc..167f656 100644
--- a/apps/frontend/src/app/app/telefonia/ivr/ivr-flow-editor.tsx
+++ b/apps/frontend/src/app/app/telefonia/ivr/ivr-flow-editor.tsx
@@ -19,14 +19,23 @@ import "@xyflow/react/dist/style.css";
import { PhoneIncoming, Plus, Save, Trash2 } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input, Select } from "@/components/ui/input";
-import { ALLOWED_IVR_DIGITS, type IvrMenu } from "@/lib/callcenter-types";
+import {
+ ALLOWED_IVR_DIGITS,
+ IVR_ENTRY_DESTINATION,
+ IVR_OPTION_DESTINATION_TYPES,
+ IVR_OPTION_DESTINATION_TYPE_LABELS,
+ type IvrMenu,
+ type IvrOptionDestinationType,
+ type Queue,
+} from "@/lib/callcenter-types";
import type { Extension } from "@/lib/extension-types";
import { updateIvrMenuOptions, type IvrMenuOptionInput } from "./actions";
interface OptionState {
key: string;
digit: string;
- destinationNumber: string;
+ destinationType: IvrOptionDestinationType;
+ destinationValue: string;
label: string;
positionX: number | null;
positionY: number | null;
@@ -37,6 +46,8 @@ interface EntryNodeData extends Record {}
interface OptionNodeData extends Record {
option: OptionState;
extensions: Extension[];
+ queues: Queue[];
+ menus: IvrMenu[];
usedDigits: string[];
onChange: (key: string, patch: Partial) => void;
onRemove: (key: string) => void;
@@ -56,7 +67,7 @@ function EntryNode(_props: NodeProps>) {
}
function OptionNode({ data }: NodeProps>) {
- const { option, extensions, usedDigits, onChange, onRemove } = data;
+ const { option, extensions, queues, menus, usedDigits, onChange, onRemove } = data;
return (
@@ -85,16 +96,40 @@ function OptionNode({ data }: NodeProps>) {
Crie outro menu de IVR primeiro pra poder apontar pra ele.
+ )}
+
Descrição (opcional)
("/ivr-menus", session.accessToken),
apiFetch("/extensions", session.accessToken),
+ apiFetch("/queues", session.accessToken),
]);
- return ;
+ return ;
}
diff --git a/apps/frontend/src/lib/callcenter-types.ts b/apps/frontend/src/lib/callcenter-types.ts
index fd76606..d816afe 100644
--- a/apps/frontend/src/lib/callcenter-types.ts
+++ b/apps/frontend/src/lib/callcenter-types.ts
@@ -74,9 +74,19 @@ export const ALLOWED_IVR_DIGITS = ["1", "2", "3", "4", "5", "6", "7", "8", "9",
* aponta pra um IvrMenu usa isso como destinationNumber (packages/telephony). */
export const IVR_ENTRY_DESTINATION = "ivr_entry";
+export const IVR_OPTION_DESTINATION_TYPES = ["EXTENSION", "QUEUE", "IVR"] as const;
+export type IvrOptionDestinationType = (typeof IVR_OPTION_DESTINATION_TYPES)[number];
+
+export const IVR_OPTION_DESTINATION_TYPE_LABELS: Record = {
+ EXTENSION: "Ramal",
+ QUEUE: "Fila",
+ IVR: "Outro IVR",
+};
+
export interface IvrMenuOption {
id: string;
digit: string;
+ destinationType: IvrOptionDestinationType;
destinationNumber: string;
destinationContext: string;
label: string | null;
diff --git a/docs/INBOUND_ROUTES.md b/docs/INBOUND_ROUTES.md
index c14287d..9bbc884 100644
--- a/docs/INBOUND_ROUTES.md
+++ b/docs/INBOUND_ROUTES.md
@@ -255,13 +255,48 @@ ramais) + um segundo dropdown listando as opções reais do tenant pra
cada tipo (ramais cadastrados, menus de IVR, filas, ou os valores
distintos de `callGroup` já usados em algum ramal).
+## Dropdown de destino numa opção de IVR: ramal, fila ou outro IVR (PHASE 67)
+
+Mesmo gap da seção acima, um nível abaixo: até aqui, uma OPÇÃO dentro de
+um menu de IVR só sabia apontar pra ramal (`bridge` fixo). Rota de
+entrada já suportava fila/grupo pra ENTRAR num menu, mas depois de
+entrar, cada dígito só discava um ramal.
+
+`IvrMenuOption.destinationType` (novo enum próprio,
+`IvrOptionDestinationType`: `EXTENSION`/`QUEUE`/`IVR`) decide a action de
+cada branch em `buildIvrDialplanExtensions` (packages/telephony), mesmo
+padrão do `InboundRoute` acima:
+
+- **EXTENSION**: inalterado — `bridge` direto pro ramal.
+- **QUEUE**: `answer` + `callcenter data="@"`.
+- **IVR**: `transfer` pro `IVR_ENTRY_DESTINATION` do contexto do MENU
+ ALVO — reaproveita a mesma entrada que uma `InboundRoute` usa pra
+ entrar num IVR, então um menu vira efetivamente um sub-menu de outro.
+
+**Sem `CALL_GROUP` aqui, de propósito** (diferente de `InboundRoute`,
+que tem os 4 tipos): o dialplan de um IVR é compilado UMA VEZ, ao salvar
+o menu (`compileAndActivateIvrDialplan`) — não resolvido a cada chamada
+como a resolução de `InboundRoute`. Um destino `CALL_GROUP` ficaria
+"quem estava no grupo no momento em que o menu foi salvo por último",
+uma defasagem silenciosa que ninguém pediu — melhor não oferecer do que
+oferecer errado.
+
+**Ciclos**: "Outro IVR" cria a primeira forma de um menu apontar pra
+outro (uma `InboundRoute` nunca é ela mesma um menu, então nunca podia
+formar ciclo). `assertNoIvrCycle` (`ivr-menus.controller.ts`) monta o
+grafo com todos os menus do tenant antes de compilar e rejeita (409)
+qualquer save que criaria um ciclo (A→B→A, ou até A apontando pra si
+mesmo) — checado tanto em `create` quanto `update`.
+
+Os dois editores de IVR (form clássico e o editor visual de nós, PHASE
+60/61) ganharam o mesmo par de dropdowns "Tipo"/"Destino" já usado em
+Rotas de Entrada. Testado ponta a ponta: menu A com opção tipo Fila,
+menu B com opção tipo Outro IVR apontando pro menu A — os dois
+persistiram certo depois de um reload completo da página.
+
## O que falta
- Sem TTS (texto→voz) — só upload de arquivo WAV já gravado.
-- Menu de IVR não suporta sub-menus (uma opção levando a OUTRO IVR) nem
- destino "fila" — só ramal, dentro do contexto `default`. Rota de
- entrada já suporta fila/grupo, mas um MENU de IVR ainda só bridge pra
- ramal.
- Grupo de ramais só é alcançável por Rota de Entrada — não existe (e
não foi pedido) um jeito de discar um grupo de dentro do próprio
dialplan "default" via feature code.
diff --git a/packages/database/prisma/migrations/20260831120000_ivr_option_destination_type/migration.sql b/packages/database/prisma/migrations/20260831120000_ivr_option_destination_type/migration.sql
new file mode 100644
index 0000000..4aae318
--- /dev/null
+++ b/packages/database/prisma/migrations/20260831120000_ivr_option_destination_type/migration.sql
@@ -0,0 +1,5 @@
+-- PHASE 67: dropdown de destino (ramal/fila/outro IVR) numa opção de IVR,
+-- mesmo gap que existia em Rotas de Entrada até a PHASE 62.
+CREATE TYPE "ivr_option_destination_type" AS ENUM ('EXTENSION', 'QUEUE', 'IVR');
+
+ALTER TABLE "ivr_menu_options" ADD COLUMN "destination_type" "ivr_option_destination_type" NOT NULL DEFAULT 'EXTENSION';
diff --git a/packages/database/prisma/schema.prisma b/packages/database/prisma/schema.prisma
index 6d8ed31..f9b3620 100644
--- a/packages/database/prisma/schema.prisma
+++ b/packages/database/prisma/schema.prisma
@@ -545,6 +545,14 @@ model IvrMenu {
@@map("ivr_menus")
}
+enum IvrOptionDestinationType {
+ EXTENSION
+ QUEUE
+ IVR
+
+ @@map("ivr_option_destination_type")
+}
+
model IvrMenuOption {
id String @id @default(uuid()) @db.Uuid
tenantId String @map("tenant_id") @db.Uuid
@@ -552,8 +560,27 @@ model IvrMenuOption {
digit String // "0".."9", "*" ou "#" — validado na API, é o que vira o regex da extension de branching
- destinationNumber String @map("destination_number") // ramal real dentro do contexto abaixo
- destinationContext String @default("default") @map("destination_context")
+ // PHASE 67 — achado real reportado pelo usuário: o destino de uma opção
+ // de IVR só podia ser ramal, sem dropdown de fila/outro menu, igual o
+ // gap que existia em Rotas de Entrada até a PHASE 62. Mesmo `destination
+ // Type` decide como `buildIvrDialplanExtensions` (packages/telephony)
+ // interpreta os 2 campos abaixo:
+ // EXTENSION -> destinationNumber é o número do ramal (bridge, igual
+ // sempre foi); destinationContext não usado
+ // QUEUE -> destinationNumber é o Queue.id (answer + callcenter,
+ // mesmo padrão de InboundRoute); destinationContext não usado
+ // IVR -> destinationNumber vira sempre IVR_ENTRY_DESTINATION;
+ // destinationContext é o IvrMenu.context do menu alvo
+ // (transfer pro próprio contexto, igual InboundRoute
+ // encaminha pra um IVR). Sem CALL_GROUP aqui de propósito:
+ // diferente de InboundRoute, o dialplan de um IVR é
+ // compilado uma vez ao salvar (não resolvido por chamada),
+ // então "quem está no grupo agora" ficaria desatualizado
+ // até a próxima edição — não pedido pelo usuário, não
+ // implementado.
+ destinationType IvrOptionDestinationType @default(EXTENSION) @map("destination_type")
+ destinationNumber String @map("destination_number") // ramal real dentro do contexto abaixo
+ destinationContext String @default("default") @map("destination_context")
label String?
diff --git a/packages/telephony/src/ivr-xml.ts b/packages/telephony/src/ivr-xml.ts
index 08070b5..e68843d 100644
--- a/packages/telephony/src/ivr-xml.ts
+++ b/packages/telephony/src/ivr-xml.ts
@@ -23,6 +23,7 @@ function escapeRegexLiteral(digit: string): string {
export interface IvrMenuOptionInput {
digit: string;
+ destinationType: "EXTENSION" | "QUEUE" | "IVR";
destinationNumber: string;
destinationContext: string;
}
@@ -38,6 +39,14 @@ export interface IvrMenuOptionInput {
* por uma extension nunca é enxergada por OUTRA extension na mesma
* passada; só um `transfer` (nova consulta de dialplan) resolve isso —
* e uma extension por dígito, casando por `destination_number` normal.
+ *
+ * PHASE 67 (dropdown de destino real: ramal/fila/outro IVR) — cada branch
+ * decide a action pelo `destinationType`, mesmo padrão de
+ * `buildInboundRouteXml`: EXTENSION continua `bridge` direto pro ramal;
+ * QUEUE vira `answer` + `callcenter`; IVR vira `transfer` pro
+ * `IVR_ENTRY_DESTINATION` do contexto alvo (reaproveita a MESMA entrada
+ * que qualquer chamada de fora usa pra entrar naquele menu). Sem
+ * CALL_GROUP aqui — motivo no comentário do schema (`IvrMenuOption`).
*/
export function buildIvrDialplanExtensions(
menu: { context: string; greeting?: string | null },
@@ -61,14 +70,27 @@ export function buildIvrDialplanExtensions(
],
};
- const branches: DialplanExtensionInput[] = options.map((opt, i) => ({
- name: `IVR opção ${opt.digit}`,
- conditionField: "destination_number",
- conditionExpr: `^${escapeRegexLiteral(opt.digit)}$`,
- continueOnFalse: false,
- order: 10 + i,
- actions: [{ application: "bridge", data: `user/${opt.destinationNumber}@\${domain_name}` }],
- }));
+ const branches: DialplanExtensionInput[] = options.map((opt, i) => {
+ let actions: DialplanExtensionInput["actions"];
+ if (opt.destinationType === "QUEUE") {
+ actions = [
+ { application: "answer" },
+ { application: "callcenter", data: `${opt.destinationNumber}@\${domain_name}` },
+ ];
+ } else if (opt.destinationType === "IVR") {
+ actions = [{ application: "transfer", data: `${IVR_ENTRY_DESTINATION} XML ${opt.destinationContext}` }];
+ } else {
+ actions = [{ application: "bridge", data: `user/${opt.destinationNumber}@\${domain_name}` }];
+ }
+ return {
+ name: `IVR opção ${opt.digit}`,
+ conditionField: "destination_number",
+ conditionExpr: `^${escapeRegexLiteral(opt.digit)}$`,
+ continueOnFalse: false,
+ order: 10 + i,
+ actions,
+ };
+ });
return [entry, ...branches];
}