import type { DialplanExtensionInput } from "./dialplan-xml"; /** * destination_number fixo usado pra entrar num menu de IVR (PHASE 58) — * toda `InboundRoute` que aponta pra um `IvrMenu` deve usar exatamente * este valor como `destinationNumber`, com `destinationContext` igual ao * `IvrMenu.context`. */ export const IVR_ENTRY_DESTINATION = "ivr_entry"; const DEFAULT_GREETING = "tone_stream://%(500,0,800)"; const INVALID_TONE = "tone_stream://%(500,0,400)"; /** Digits válidos num menu de IVR — os mesmos que um telefone real manda. */ export const ALLOWED_IVR_DIGITS = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "*", "#"] as const; export type AllowedIvrDigit = (typeof ALLOWED_IVR_DIGITS)[number]; function escapeRegexLiteral(digit: string): string { // Só "*" precisa de escape entre os digits permitidos (regex especial); // os demais (0-9, #) já são literais seguros. return digit === "*" ? "\\*" : digit; } export interface IvrMenuOptionInput { digit: string; destinationType: "EXTENSION" | "QUEUE" | "IVR"; destinationNumber: string; destinationContext: string; } /** * Compila um `IvrMenu` + suas opções nas mesmas 2 formas de `` * já testadas ponta a ponta com DTMF real (PHASE 56): uma entrada que * atende, toca o prompt, coleta 1 dígito com `play_and_get_digits` e * `transfer`e pro PRÓPRIO contexto usando o dígito coletado como novo * `destination_number` — achado real, documentado em * docs/INBOUND_ROUTES.md: FreeSWITCH resolve todas as condições de um * contexto ANTES de executar qualquer ação, então uma variable setada * 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 }, options: IvrMenuOptionInput[], ): DialplanExtensionInput[] { const greetingFile = menu.greeting?.trim() || DEFAULT_GREETING; const entry: DialplanExtensionInput = { name: "IVR entrada", conditionField: "destination_number", conditionExpr: `^${IVR_ENTRY_DESTINATION}$`, continueOnFalse: false, order: 1, actions: [ { application: "answer" }, { application: "play_and_get_digits", data: `1 1 3 5000 # ${greetingFile} ${INVALID_TONE} ivr_choice \\d+ 3000`, }, { application: "transfer", data: `\${ivr_choice} XML ${menu.context}` }, ], }; 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]; }