From d0cb68d8d76a21819e5272a4f3970ee5013de020 Mon Sep 17 00:00:00 2001 From: saridsa2 Date: Thu, 19 Mar 2026 18:19:50 +0530 Subject: [PATCH] =?UTF-8?q?feat:=20Kookoo=20IVR=20endpoint=20=E2=80=94=20o?= =?UTF-8?q?utbound=20calls=20now=20bridge=20to=20agent=20SIP?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When customer answers outbound call, Kookoo hits /kookoo/ivr which returns 523590 to bridge the call to the agent's SIP extension. Agent's browser rings, both connect. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/ozonetel/kookoo-ivr.controller.ts | 49 ++++++++++++++++++++++++++ src/ozonetel/ozonetel-agent.module.ts | 3 +- src/ozonetel/ozonetel-agent.service.ts | 10 ++---- 3 files changed, 54 insertions(+), 8 deletions(-) create mode 100644 src/ozonetel/kookoo-ivr.controller.ts diff --git a/src/ozonetel/kookoo-ivr.controller.ts b/src/ozonetel/kookoo-ivr.controller.ts new file mode 100644 index 0000000..e5c4a1b --- /dev/null +++ b/src/ozonetel/kookoo-ivr.controller.ts @@ -0,0 +1,49 @@ +import { Controller, Get, Query, Logger, Header } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; + +@Controller('kookoo') +export class KookooIvrController { + private readonly logger = new Logger(KookooIvrController.name); + private readonly sipId: string; + + constructor(private config: ConfigService) { + this.sipId = process.env.OZONETEL_SIP_ID ?? '523590'; + } + + @Get('ivr') + @Header('Content-Type', 'application/xml') + handleIvr(@Query() query: Record): string { + const event = query.event ?? ''; + const sid = query.sid ?? ''; + const cid = query.cid ?? ''; + const data = query.data ?? ''; + const status = query.status ?? ''; + + this.logger.log(`Kookoo IVR: event=${event} sid=${sid} cid=${cid} status=${status}`); + + // New outbound call — customer answered, connect to agent's SIP + if (event === 'NewCall') { + this.logger.log(`Connecting customer ${cid} to agent SIP ${this.sipId}`); + return ` + +${this.sipId} +`; + } + + // Dial event — call to agent finished + if (event === 'Dial') { + this.logger.log(`Dial completed: status=${status} data=${data}`); + return ` + + +`; + } + + // Hangup or any other event + this.logger.log(`Call ended: event=${event}`); + return ` + + +`; + } +} diff --git a/src/ozonetel/ozonetel-agent.module.ts b/src/ozonetel/ozonetel-agent.module.ts index e592c0b..bcecdc6 100644 --- a/src/ozonetel/ozonetel-agent.module.ts +++ b/src/ozonetel/ozonetel-agent.module.ts @@ -1,9 +1,10 @@ import { Module } from '@nestjs/common'; import { OzonetelAgentController } from './ozonetel-agent.controller'; import { OzonetelAgentService } from './ozonetel-agent.service'; +import { KookooIvrController } from './kookoo-ivr.controller'; @Module({ - controllers: [OzonetelAgentController], + controllers: [OzonetelAgentController, KookooIvrController], providers: [OzonetelAgentService], exports: [OzonetelAgentService], }) diff --git a/src/ozonetel/ozonetel-agent.service.ts b/src/ozonetel/ozonetel-agent.service.ts index 5bfd5c3..7df60a1 100644 --- a/src/ozonetel/ozonetel-agent.service.ts +++ b/src/ozonetel/ozonetel-agent.service.ts @@ -82,13 +82,9 @@ export class OzonetelAgentService { callback_url: params.callbackUrl ?? `${callbackBase}/webhooks/kookoo/callback`, }); - // If IVR URL provided, Kookoo will hit it when call connects - // Otherwise use extra_data for simple TTS + hangup - if (params.ivrUrl) { - queryParams.set('url', params.ivrUrl); - } else { - queryParams.set('extra_data', 'Connecting you now'); - } + // When customer answers, Kookoo hits our IVR endpoint which dials the agent's SIP + const ivrUrl = params.ivrUrl ?? `${callbackBase}/kookoo/ivr`; + queryParams.set('url', ivrUrl); const response = await axios.get( `https://in1-cpaas.ozonetel.com/outbound/outbound.php?${queryParams.toString()}`,