feat: Kookoo IVR endpoint — outbound calls now bridge to agent SIP

When customer answers outbound call, Kookoo hits /kookoo/ivr which
returns <dial record="true">523590</dial> 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) <noreply@anthropic.com>
This commit is contained in:
2026-03-19 18:19:50 +05:30
parent 6812006b53
commit d0cb68d8d7
3 changed files with 54 additions and 8 deletions

View File

@@ -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, any>): 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 `<?xml version="1.0" encoding="UTF-8"?>
<response>
<dial record="true" timeout="30" moh="ring">${this.sipId}</dial>
</response>`;
}
// Dial event — call to agent finished
if (event === 'Dial') {
this.logger.log(`Dial completed: status=${status} data=${data}`);
return `<?xml version="1.0" encoding="UTF-8"?>
<response>
<hangup/>
</response>`;
}
// Hangup or any other event
this.logger.log(`Call ended: event=${event}`);
return `<?xml version="1.0" encoding="UTF-8"?>
<response>
<hangup/>
</response>`;
}
}

View File

@@ -1,9 +1,10 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { OzonetelAgentController } from './ozonetel-agent.controller'; import { OzonetelAgentController } from './ozonetel-agent.controller';
import { OzonetelAgentService } from './ozonetel-agent.service'; import { OzonetelAgentService } from './ozonetel-agent.service';
import { KookooIvrController } from './kookoo-ivr.controller';
@Module({ @Module({
controllers: [OzonetelAgentController], controllers: [OzonetelAgentController, KookooIvrController],
providers: [OzonetelAgentService], providers: [OzonetelAgentService],
exports: [OzonetelAgentService], exports: [OzonetelAgentService],
}) })

View File

@@ -82,13 +82,9 @@ export class OzonetelAgentService {
callback_url: params.callbackUrl ?? `${callbackBase}/webhooks/kookoo/callback`, callback_url: params.callbackUrl ?? `${callbackBase}/webhooks/kookoo/callback`,
}); });
// If IVR URL provided, Kookoo will hit it when call connects // When customer answers, Kookoo hits our IVR endpoint which dials the agent's SIP
// Otherwise use extra_data for simple TTS + hangup const ivrUrl = params.ivrUrl ?? `${callbackBase}/kookoo/ivr`;
if (params.ivrUrl) { queryParams.set('url', ivrUrl);
queryParams.set('url', params.ivrUrl);
} else {
queryParams.set('extra_data', '<response><playtext>Connecting you now</playtext><hangup/></response>');
}
const response = await axios.get( const response = await axios.get(
`https://in1-cpaas.ozonetel.com/outbound/outbound.php?${queryParams.toString()}`, `https://in1-cpaas.ozonetel.com/outbound/outbound.php?${queryParams.toString()}`,