fix: camelCase field names + dial uses per-agent config

Defect 5: Worklist, missed-call-webhook, missed-queue, ai-chat, and
rules-engine all used legacy lowercase field names (callbackstatus,
callsourcenumber, missedcallcount, callbackattemptedat) from the old
VPS schema. Fixed to camelCase (callbackStatus, callSourceNumber,
missedCallCount, callbackAttemptedAt) matching the current SDK sync.

Defect 6: Dial endpoint used global defaults (OZONETEL_AGENT_ID env
var) instead of the logged-in agent's config. Now accepts agentId
and campaignName from the frontend request body. Falls back to
telephony config → DID-derived campaign name → explicit error.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-10 14:29:19 +05:30
parent 7717536622
commit 898ff65951
6 changed files with 38 additions and 29 deletions

View File

@@ -271,7 +271,7 @@ export class AiChatController {
inputSchema: z.object({}), inputSchema: z.object({}),
execute: async () => { execute: async () => {
const data = await platformService.queryWithAuth<any>( const data = await platformService.queryWithAuth<any>(
`{ calls(first: 100, filter: { callStatus: { eq: MISSED }, callbackstatus: { eq: PENDING_CALLBACK } }) { edges { node { id callerNumber { primaryPhoneNumber } startedAt agentName sla } } } }`, `{ calls(first: 100, filter: { callStatus: { eq: MISSED }, callbackStatus: { eq: PENDING_CALLBACK } }) { edges { node { id callerNumber { primaryPhoneNumber } startedAt agentName sla } } } }`,
undefined, auth, undefined, auth,
); );
const breached = data.calls.edges const breached = data.calls.edges

View File

@@ -157,7 +157,7 @@ export class OzonetelAgentController {
if (newStatus) { if (newStatus) {
try { try {
await this.platform.query<any>( await this.platform.query<any>(
`mutation { updateCall(id: "${body.missedCallId}", data: { callbackstatus: ${newStatus} }) { id } }`, `mutation { updateCall(id: "${body.missedCallId}", data: { callbackStatus: ${newStatus} }) { id } }`,
); );
} catch (err) { } catch (err) {
this.logger.warn(`Failed to update missed call status: ${err}`); this.logger.warn(`Failed to update missed call status: ${err}`);
@@ -191,19 +191,28 @@ export class OzonetelAgentController {
@Post('dial') @Post('dial')
async dial( async dial(
@Body() body: { phoneNumber: string; campaignName?: string; leadId?: string }, @Body() body: { phoneNumber: string; agentId?: string; campaignName?: string; leadId?: string },
) { ) {
if (!body.phoneNumber) { if (!body.phoneNumber) {
throw new HttpException('phoneNumber required', 400); throw new HttpException('phoneNumber required', 400);
} }
const campaignName = body.campaignName ?? this.telephony.getConfig().ozonetel.campaignName ?? 'Inbound_918041763265'; const agentId = body.agentId ?? this.defaultAgentId;
const campaignName = body.campaignName
?? this.telephony.getConfig().ozonetel.campaignName
?? this.telephony.getConfig().ozonetel.did
? `Inbound_${this.telephony.getConfig().ozonetel.did}`
: '';
this.logger.log(`[DIAL] phone=${body.phoneNumber} campaign=${campaignName} agentId=${this.defaultAgentId} lead=${body.leadId ?? 'none'}`); if (!campaignName) {
throw new HttpException('Campaign name not configured — set in Telephony settings or pass campaignName', 400);
}
this.logger.log(`[DIAL] phone=${body.phoneNumber} campaign=${campaignName} agentId=${agentId} lead=${body.leadId ?? 'none'}`);
try { try {
const result = await this.ozonetelAgent.manualDial({ const result = await this.ozonetelAgent.manualDial({
agentId: this.defaultAgentId, agentId,
campaignName, campaignName,
customerNumber: body.phoneNumber, customerNumber: body.phoneNumber,
}); });

View File

@@ -18,10 +18,10 @@ export class CallFactsProvider implements FactProvider {
'call.status': call.callStatus ?? null, 'call.status': call.callStatus ?? null,
'call.disposition': call.disposition ?? null, 'call.disposition': call.disposition ?? null,
'call.durationSeconds': call.durationSeconds ?? call.durationSec ?? 0, 'call.durationSeconds': call.durationSeconds ?? call.durationSec ?? 0,
'call.callbackStatus': call.callbackstatus ?? call.callbackStatus ?? null, 'call.callbackStatus': call.callbackStatus ?? call.callbackStatus ?? null,
'call.slaElapsedPercent': slaElapsedPercent, 'call.slaElapsedPercent': slaElapsedPercent,
'call.slaBreached': slaElapsedPercent > 100, 'call.slaBreached': slaElapsedPercent > 100,
'call.missedCount': call.missedcallcount ?? call.missedCount ?? 0, 'call.missedCount': call.missedCallCount ?? call.missedCount ?? 0,
'call.taskType': taskType, 'call.taskType': taskType,
}; };
} }

View File

@@ -147,8 +147,8 @@ export class MissedCallWebhookController {
}; };
// Set callback tracking fields for missed calls so they appear in the worklist // Set callback tracking fields for missed calls so they appear in the worklist
if (data.callStatus === 'MISSED') { if (data.callStatus === 'MISSED') {
callData.callbackstatus = 'PENDING_CALLBACK'; callData.callbackStatus = 'PENDING_CALLBACK';
callData.missedcallcount = 1; callData.missedCallCount = 1;
} }
if (data.recordingUrl) { if (data.recordingUrl) {
callData.recording = { primaryLinkUrl: data.recordingUrl, primaryLinkLabel: 'Recording' }; callData.recording = { primaryLinkUrl: data.recordingUrl, primaryLinkLabel: 'Recording' };

View File

@@ -97,19 +97,19 @@ export class MissedQueueService implements OnModuleInit {
const existing = await this.platform.query<any>( const existing = await this.platform.query<any>(
`{ calls(first: 1, filter: { `{ calls(first: 1, filter: {
callbackstatus: { eq: PENDING_CALLBACK }, callbackStatus: { eq: PENDING_CALLBACK },
callerNumber: { primaryPhoneNumber: { eq: "${phone}" } } callerNumber: { primaryPhoneNumber: { eq: "${phone}" } }
}) { edges { node { id missedcallcount } } } }`, }) { edges { node { id missedCallCount } } } }`,
); );
const existingNode = existing?.calls?.edges?.[0]?.node; const existingNode = existing?.calls?.edges?.[0]?.node;
if (existingNode) { if (existingNode) {
const newCount = (existingNode.missedcallcount || 1) + 1; const newCount = (existingNode.missedCallCount || 1) + 1;
const updateParts = [ const updateParts = [
`missedcallcount: ${newCount}`, `missedCallCount: ${newCount}`,
`startedAt: "${callTime}"`, `startedAt: "${callTime}"`,
`callsourcenumber: "${did}"`, `callSourceNumber: "${did}"`,
]; ];
if (leadId) updateParts.push(`leadId: "${leadId}"`); if (leadId) updateParts.push(`leadId: "${leadId}"`);
if (leadName) updateParts.push(`leadName: "${leadName}"`); if (leadName) updateParts.push(`leadName: "${leadName}"`);
@@ -123,9 +123,9 @@ export class MissedQueueService implements OnModuleInit {
`callStatus: MISSED`, `callStatus: MISSED`,
`direction: INBOUND`, `direction: INBOUND`,
`callerNumber: { primaryPhoneNumber: "${phone}", primaryPhoneCallingCode: "+91" }`, `callerNumber: { primaryPhoneNumber: "${phone}", primaryPhoneCallingCode: "+91" }`,
`callsourcenumber: "${did}"`, `callSourceNumber: "${did}"`,
`callbackstatus: PENDING_CALLBACK`, `callbackStatus: PENDING_CALLBACK`,
`missedcallcount: 1`, `missedCallCount: 1`,
`startedAt: "${callTime}"`, `startedAt: "${callTime}"`,
]; ];
if (leadId) dataParts.push(`leadId: "${leadId}"`); if (leadId) dataParts.push(`leadId: "${leadId}"`);
@@ -160,12 +160,12 @@ export class MissedQueueService implements OnModuleInit {
// Find oldest unassigned PENDING_CALLBACK call (empty agentName) // Find oldest unassigned PENDING_CALLBACK call (empty agentName)
let result = await this.platform.query<any>( let result = await this.platform.query<any>(
`{ calls(first: 1, filter: { `{ calls(first: 1, filter: {
callbackstatus: { eq: PENDING_CALLBACK }, callbackStatus: { eq: PENDING_CALLBACK },
agentName: { eq: "" } agentName: { eq: "" }
}, orderBy: [{ startedAt: AscNullsLast }]) { }, orderBy: [{ startedAt: AscNullsLast }]) {
edges { node { edges { node {
id callerNumber { primaryPhoneNumber } id callerNumber { primaryPhoneNumber }
startedAt callsourcenumber missedcallcount startedAt callSourceNumber missedCallCount
} } } }
} }`, } }`,
); );
@@ -176,12 +176,12 @@ export class MissedQueueService implements OnModuleInit {
if (!call) { if (!call) {
result = await this.platform.query<any>( result = await this.platform.query<any>(
`{ calls(first: 1, filter: { `{ calls(first: 1, filter: {
callbackstatus: { eq: PENDING_CALLBACK }, callbackStatus: { eq: PENDING_CALLBACK },
agentName: { is: NULL } agentName: { is: NULL }
}, orderBy: [{ startedAt: AscNullsLast }]) { }, orderBy: [{ startedAt: AscNullsLast }]) {
edges { node { edges { node {
id callerNumber { primaryPhoneNumber } id callerNumber { primaryPhoneNumber }
startedAt callsourcenumber missedcallcount startedAt callSourceNumber missedCallCount
} } } }
} }`, } }`,
); );
@@ -209,13 +209,13 @@ export class MissedQueueService implements OnModuleInit {
throw new Error(`Invalid status: ${status}. Must be one of: ${validStatuses.join(', ')}`); throw new Error(`Invalid status: ${status}. Must be one of: ${validStatuses.join(', ')}`);
} }
const dataParts: string[] = [`callbackstatus: ${status}`]; const dataParts: string[] = [`callbackStatus: ${status}`];
if (status === 'CALLBACK_ATTEMPTED') { if (status === 'CALLBACK_ATTEMPTED') {
dataParts.push(`callbackattemptedat: "${new Date().toISOString()}"`); dataParts.push(`callbackAttemptedAt: "${new Date().toISOString()}"`);
} }
return this.platform.queryWithAuth<any>( return this.platform.queryWithAuth<any>(
`mutation { updateCall(id: "${callId}", data: { ${dataParts.join(', ')} }) { id callbackstatus callbackattemptedat } }`, `mutation { updateCall(id: "${callId}", data: { ${dataParts.join(', ')} }) { id callbackStatus callbackAttemptedAt } }`,
undefined, undefined,
authHeader, authHeader,
); );
@@ -230,12 +230,12 @@ export class MissedQueueService implements OnModuleInit {
const fields = `id name createdAt direction callStatus agentName const fields = `id name createdAt direction callStatus agentName
callerNumber { primaryPhoneNumber } callerNumber { primaryPhoneNumber }
startedAt endedAt durationSec disposition leadId startedAt endedAt durationSec disposition leadId
callbackstatus callsourcenumber missedcallcount callbackattemptedat`; callbackStatus callSourceNumber missedCallCount callbackAttemptedAt`;
const buildQuery = (status: string) => `{ calls(first: 50, filter: { const buildQuery = (status: string) => `{ calls(first: 50, filter: {
agentName: { eq: "${agentName}" }, agentName: { eq: "${agentName}" },
callStatus: { eq: MISSED }, callStatus: { eq: MISSED },
callbackstatus: { eq: ${status} } callbackStatus: { eq: ${status} }
}, orderBy: [{ startedAt: AscNullsLast }]) { edges { node { ${fields} } } } }`; }, orderBy: [{ startedAt: AscNullsLast }]) { edges { node { ${fields} } } } }`;
try { try {

View File

@@ -97,13 +97,13 @@ export class WorklistService {
try { try {
// FIFO ordering (AscNullsLast) — oldest first. No agentName filter — missed calls are a shared queue. // FIFO ordering (AscNullsLast) — oldest first. No agentName filter — missed calls are a shared queue.
const data = await this.platform.queryWithAuth<any>( const data = await this.platform.queryWithAuth<any>(
`{ calls(first: 20, filter: { callStatus: { eq: MISSED }, callbackstatus: { in: [PENDING_CALLBACK, CALLBACK_ATTEMPTED] } }, orderBy: [{ startedAt: AscNullsLast }]) { edges { node { `{ calls(first: 20, filter: { callStatus: { eq: MISSED }, callbackStatus: { in: [PENDING_CALLBACK, CALLBACK_ATTEMPTED] } }, orderBy: [{ startedAt: AscNullsLast }]) { edges { node {
id name createdAt id name createdAt
direction callStatus agentName direction callStatus agentName
callerNumber { primaryPhoneNumber } callerNumber { primaryPhoneNumber }
startedAt endedAt durationSec startedAt endedAt durationSec
disposition leadId disposition leadId
callbackstatus callsourcenumber missedcallcount callbackattemptedat callbackStatus callSourceNumber missedCallCount callbackAttemptedAt
} } } }`, } } } }`,
undefined, undefined,
authHeader, authHeader,