From a0df752cfdbf14b85d3f77276fbdd1b4c9ff804a Mon Sep 17 00:00:00 2001 From: saridsa2 Date: Wed, 18 Mar 2026 18:34:32 +0530 Subject: [PATCH] feat: Ozonetel V3 dial endpoint, worklist query fixes - Switch outbound dial to /CAServicesV3/mdlConnection.php (tbManualDial) - Use both Apikey + Basic Auth headers matching Ozonetel toolbar pattern - Auto-login agent before dial attempt - Dial controller returns 502 (not 401) for Ozonetel errors to prevent session logout - Fix worklist: remove non-existent callId from followUp query - Fix worklist: use unquoted MISSED enum, remove callerNumber subfield issue - Worklist controller resolves agent name from platform currentUser API Co-Authored-By: Claude Opus 4.6 (1M context) --- src/ozonetel/ozonetel-agent.controller.ts | 44 ++++++++++++++++++++++- src/ozonetel/ozonetel-agent.service.ts | 39 ++++++++++++++++++++ src/worklist/worklist.service.ts | 6 ++-- 3 files changed, 85 insertions(+), 4 deletions(-) diff --git a/src/ozonetel/ozonetel-agent.controller.ts b/src/ozonetel/ozonetel-agent.controller.ts index c5a0ff5..5490545 100644 --- a/src/ozonetel/ozonetel-agent.controller.ts +++ b/src/ozonetel/ozonetel-agent.controller.ts @@ -1,11 +1,23 @@ import { Controller, Post, Body, Logger, HttpException } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; import { OzonetelAgentService } from './ozonetel-agent.service'; @Controller('api/ozonetel') export class OzonetelAgentController { private readonly logger = new Logger(OzonetelAgentController.name); + private readonly defaultAgentId: string; + private readonly defaultAgentPassword: string; - constructor(private readonly ozonetelAgent: OzonetelAgentService) {} + private readonly defaultSipId: string; + + constructor( + private readonly ozonetelAgent: OzonetelAgentService, + private readonly config: ConfigService, + ) { + this.defaultAgentId = config.get('OZONETEL_AGENT_ID') ?? 'agent3'; + this.defaultAgentPassword = config.get('OZONETEL_AGENT_PASSWORD') ?? ''; + this.defaultSipId = config.get('OZONETEL_SIP_ID') ?? '521814'; + } @Post('agent-login') async agentLogin( @@ -40,4 +52,34 @@ export class OzonetelAgentController { ); } } + + @Post('dial') + async dial( + @Body() body: { phoneNumber: string; leadId?: string }, + ) { + if (!body.phoneNumber) { + throw new HttpException('phoneNumber required', 400); + } + + this.logger.log(`Dial request: ${body.phoneNumber} (lead: ${body.leadId ?? 'none'})`); + + try { + // Ensure agent is logged in before dialing + await this.ozonetelAgent.loginAgent({ + agentId: this.defaultAgentId, + password: this.defaultAgentPassword, + phoneNumber: this.defaultSipId, + mode: 'blended', + }); + + const result = await this.ozonetelAgent.dialCustomer({ + agentId: this.defaultAgentId, + customerNumber: body.phoneNumber, + }); + return result; + } catch (error: any) { + const message = error.response?.data?.message ?? error.message ?? 'Dial failed'; + throw new HttpException(message, 502); + } + } } diff --git a/src/ozonetel/ozonetel-agent.service.ts b/src/ozonetel/ozonetel-agent.service.ts index b8e1170..b879997 100644 --- a/src/ozonetel/ozonetel-agent.service.ts +++ b/src/ozonetel/ozonetel-agent.service.ts @@ -63,6 +63,45 @@ export class OzonetelAgentService { } } + async dialCustomer(params: { + agentId: string; + customerNumber: string; + campaignName?: string; + }): Promise<{ type: string; agentId: string; user: string }> { + const url = `https://${this.apiDomain}/CAServicesV3/mdlConnection.php`; + const basicAuth = Buffer.from(`${this.accountId}:${this.apiKey}`).toString('base64'); + + this.logger.log(`Dialing ${params.customerNumber} for agent ${params.agentId}`); + + try { + const response = await axios.post( + url, + { + type: 'tbManualDial', + ns: 'ozonetel.cloudagent', + customer: this.accountId, + agentId: params.agentId, + custNumber: params.customerNumber, + campaignName: params.campaignName ?? 'Inbound_918062873123', + timestamp: Date.now(), + }, + { + headers: { + 'Apikey': this.apiKey, + 'Authorization': `Basic ${basicAuth}`, + 'Content-Type': 'application/json', + }, + }, + ); + + this.logger.log(`Dial response: ${JSON.stringify(response.data)}`); + return response.data; + } catch (error: any) { + this.logger.error(`Dial failed: ${error.response?.data?.message ?? error.message}`); + throw error; + } + } + async logoutAgent(params: { agentId: string; password: string; diff --git a/src/worklist/worklist.service.ts b/src/worklist/worklist.service.ts index 969a11c..6443da2 100644 --- a/src/worklist/worklist.service.ts +++ b/src/worklist/worklist.service.ts @@ -59,7 +59,7 @@ export class WorklistService { id name createdAt typeCustom status scheduledAt completedAt priority assignedAgent - patientId callId + patientId } } } }`, undefined, authHeader, @@ -77,9 +77,9 @@ export class WorklistService { private async getMissedCalls(agentName: string, authHeader: string): Promise { try { const data = await this.platform.queryWithAuth( - `{ calls(first: 20, filter: { agentName: { eq: "${agentName}" }, callStatus: { eq: "MISSED" } }, orderBy: [{ startedAt: DescNullsLast }]) { edges { node { + `{ calls(first: 20, filter: { agentName: { eq: "${agentName}" }, callStatus: { eq: MISSED } }, orderBy: [{ startedAt: DescNullsLast }]) { edges { node { id name createdAt - direction callStatus callerNumber agentName + direction callStatus agentName startedAt endedAt durationSec disposition leadId } } } }`,