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) <noreply@anthropic.com>
This commit is contained in:
2026-03-18 18:34:32 +05:30
parent 9688d5144e
commit a0df752cfd
3 changed files with 85 additions and 4 deletions

View File

@@ -1,11 +1,23 @@
import { Controller, Post, Body, Logger, HttpException } from '@nestjs/common'; import { Controller, Post, Body, Logger, HttpException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { OzonetelAgentService } from './ozonetel-agent.service'; import { OzonetelAgentService } from './ozonetel-agent.service';
@Controller('api/ozonetel') @Controller('api/ozonetel')
export class OzonetelAgentController { export class OzonetelAgentController {
private readonly logger = new Logger(OzonetelAgentController.name); 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<string>('OZONETEL_AGENT_ID') ?? 'agent3';
this.defaultAgentPassword = config.get<string>('OZONETEL_AGENT_PASSWORD') ?? '';
this.defaultSipId = config.get<string>('OZONETEL_SIP_ID') ?? '521814';
}
@Post('agent-login') @Post('agent-login')
async agentLogin( 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);
}
}
} }

View File

@@ -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: { async logoutAgent(params: {
agentId: string; agentId: string;
password: string; password: string;

View File

@@ -59,7 +59,7 @@ export class WorklistService {
id name createdAt id name createdAt
typeCustom status scheduledAt completedAt typeCustom status scheduledAt completedAt
priority assignedAgent priority assignedAgent
patientId callId patientId
} } } }`, } } } }`,
undefined, undefined,
authHeader, authHeader,
@@ -77,9 +77,9 @@ export class WorklistService {
private async getMissedCalls(agentName: string, authHeader: string): Promise<any[]> { private async getMissedCalls(agentName: string, authHeader: string): Promise<any[]> {
try { try {
const data = await this.platform.queryWithAuth<any>( const data = await this.platform.queryWithAuth<any>(
`{ 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 id name createdAt
direction callStatus callerNumber agentName direction callStatus agentName
startedAt endedAt durationSec startedAt endedAt durationSec
disposition leadId disposition leadId
} } } }`, } } } }`,