From 58225b79436dd3d17131c1ac2c84905a56c8c256 Mon Sep 17 00:00:00 2001 From: saridsa2 Date: Fri, 20 Mar 2026 20:22:47 +0530 Subject: [PATCH] feat: webhook field fixes, Force Ready endpoint, improved error logging - Fix Call record field names (recording, callerNumber, durationSec) - Add POST /api/ozonetel/agent-ready using logout+login for Force Ready - Add callerNumber to kookoo callback - Better error logging with response body Co-Authored-By: Claude Opus 4.6 (1M context) --- src/ozonetel/kookoo-ivr.controller.ts | 6 ++-- src/ozonetel/ozonetel-agent.controller.ts | 23 ++++++++++++ src/worklist/kookoo-callback.controller.ts | 6 ++-- .../missed-call-webhook.controller.ts | 36 ++++++++++--------- 4 files changed, 49 insertions(+), 22 deletions(-) diff --git a/src/ozonetel/kookoo-ivr.controller.ts b/src/ozonetel/kookoo-ivr.controller.ts index 9c2e542..b33e56d 100644 --- a/src/ozonetel/kookoo-ivr.controller.ts +++ b/src/ozonetel/kookoo-ivr.controller.ts @@ -25,12 +25,10 @@ export class KookooIvrController { // New outbound call — customer answered, put them in a conference room // The room ID is based on the call SID so we can join from the browser if (event === 'NewCall') { - // Try dialing the SIP extension with 0 prefix (internal routing) - const ext = query.ext ?? `0${this.sipId}`; - this.logger.log(`Customer ${cid} answered — dialing agent at ${ext}`); + this.logger.log(`Customer ${cid} answered — dialing DID ${this.callerId} to route to agent`); return ` -${ext} +${this.callerId} `; } diff --git a/src/ozonetel/ozonetel-agent.controller.ts b/src/ozonetel/ozonetel-agent.controller.ts index 762e57d..73308e5 100644 --- a/src/ozonetel/ozonetel-agent.controller.ts +++ b/src/ozonetel/ozonetel-agent.controller.ts @@ -53,6 +53,29 @@ export class OzonetelAgentController { } } + @Post('agent-ready') + async agentReady() { + this.logger.log(`Force ready: logging out and back in agent ${this.defaultAgentId}`); + + try { + await this.ozonetelAgent.logoutAgent({ + agentId: this.defaultAgentId, + password: this.defaultAgentPassword, + }); + const result = await this.ozonetelAgent.loginAgent({ + agentId: this.defaultAgentId, + password: this.defaultAgentPassword, + phoneNumber: this.defaultSipId, + mode: 'blended', + }); + return result; + } catch (error: any) { + const message = error.response?.data?.message ?? error.message ?? 'Force ready failed'; + this.logger.error(`Force ready failed: ${message}`); + throw new HttpException(message, error.response?.status ?? 502); + } + } + @Post('dispose') async dispose( @Body() body: { diff --git a/src/worklist/kookoo-callback.controller.ts b/src/worklist/kookoo-callback.controller.ts index 932e0c2..9b7b60e 100644 --- a/src/worklist/kookoo-callback.controller.ts +++ b/src/worklist/kookoo-callback.controller.ts @@ -49,6 +49,7 @@ export class KookooCallbackController { name: `Outbound — ${phoneNumber}`, direction: 'OUTBOUND', callStatus, + callerNumber: { primaryPhoneNumber: `+91${phoneNumber}` }, startedAt: startTime ? new Date(startTime).toISOString() : new Date().toISOString(), endedAt: endTime ? new Date(endTime).toISOString() : null, durationSec: duration, @@ -82,8 +83,9 @@ export class KookooCallbackController { } return { received: true, processed: true, callId: callResult.createCall.id }; - } catch (err) { - this.logger.error(`Kookoo callback processing failed: ${err}`); + } catch (err: any) { + const responseData = err?.response?.data ? JSON.stringify(err.response.data) : ''; + this.logger.error(`Kookoo callback processing failed: ${err.message} ${responseData}`); return { received: true, processed: false }; } } diff --git a/src/worklist/missed-call-webhook.controller.ts b/src/worklist/missed-call-webhook.controller.ts index df5a37c..6fa3bb5 100644 --- a/src/worklist/missed-call-webhook.controller.ts +++ b/src/worklist/missed-call-webhook.controller.ts @@ -105,8 +105,9 @@ export class MissedCallWebhookController { } return { received: true, processed: true, callId, leadId: lead?.id ?? null }; - } catch (err) { - this.logger.error(`Webhook processing failed: ${err}`); + } catch (err: any) { + const responseData = err?.response?.data ? JSON.stringify(err.response.data) : ''; + this.logger.error(`Webhook processing failed: ${err.message} ${responseData}`); return { received: true, processed: false, error: String(err) }; } } @@ -123,21 +124,24 @@ export class MissedCallWebhookController { disposition: string | null; ucid: string | null; }, authHeader: string): Promise { + const callData: Record = { + name: `${data.direction === 'INBOUND' ? 'Inbound' : 'Outbound'} — ${data.callerPhone}`, + direction: data.direction, + callStatus: data.callStatus, + callerNumber: { primaryPhoneNumber: `+91${data.callerPhone}` }, + agentName: data.agentName, + startedAt: data.startTime ? new Date(data.startTime).toISOString() : null, + endedAt: data.endTime ? new Date(data.endTime).toISOString() : null, + durationSec: data.duration, + disposition: this.mapDisposition(data.disposition), + }; + if (data.recordingUrl) { + callData.recording = { primaryLinkUrl: data.recordingUrl, primaryLinkLabel: 'Recording' }; + } + const result = await this.platform.queryWithAuth( `mutation($data: CallCreateInput!) { createCall(data: $data) { id } }`, - { - data: { - name: `${data.direction === 'INBOUND' ? 'Inbound' : 'Outbound'} — ${data.callerPhone}`, - direction: data.direction, - callStatus: data.callStatus, - agentName: data.agentName, - startedAt: data.startTime ? new Date(data.startTime).toISOString() : null, - endedAt: data.endTime ? new Date(data.endTime).toISOString() : null, - durationSec: data.duration, - disposition: this.mapDisposition(data.disposition), - recordingUrl: data.recordingUrl ? { primaryLinkUrl: data.recordingUrl, primaryLinkLabel: 'Recording' } : undefined, - }, - }, + { data: callData }, authHeader, ); return result.createCall.id; @@ -185,7 +189,7 @@ export class MissedCallWebhookController { occurredAt: new Date().toISOString(), performedBy: data.performedBy, channel: data.channel, - durationSeconds: data.durationSeconds, + durationSec: data.durationSeconds, outcome: data.outcome, leadId: data.leadId, },