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({}),
execute: async () => {
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,
);
const breached = data.calls.edges

View File

@@ -157,7 +157,7 @@ export class OzonetelAgentController {
if (newStatus) {
try {
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) {
this.logger.warn(`Failed to update missed call status: ${err}`);
@@ -191,19 +191,28 @@ export class OzonetelAgentController {
@Post('dial')
async dial(
@Body() body: { phoneNumber: string; campaignName?: string; leadId?: string },
@Body() body: { phoneNumber: string; agentId?: string; campaignName?: string; leadId?: string },
) {
if (!body.phoneNumber) {
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 {
const result = await this.ozonetelAgent.manualDial({
agentId: this.defaultAgentId,
agentId,
campaignName,
customerNumber: body.phoneNumber,
});

View File

@@ -18,10 +18,10 @@ export class CallFactsProvider implements FactProvider {
'call.status': call.callStatus ?? null,
'call.disposition': call.disposition ?? null,
'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.slaBreached': slaElapsedPercent > 100,
'call.missedCount': call.missedcallcount ?? call.missedCount ?? 0,
'call.missedCount': call.missedCallCount ?? call.missedCount ?? 0,
'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
if (data.callStatus === 'MISSED') {
callData.callbackstatus = 'PENDING_CALLBACK';
callData.missedcallcount = 1;
callData.callbackStatus = 'PENDING_CALLBACK';
callData.missedCallCount = 1;
}
if (data.recordingUrl) {
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>(
`{ calls(first: 1, filter: {
callbackstatus: { eq: PENDING_CALLBACK },
callbackStatus: { eq: PENDING_CALLBACK },
callerNumber: { primaryPhoneNumber: { eq: "${phone}" } }
}) { edges { node { id missedcallcount } } } }`,
}) { edges { node { id missedCallCount } } } }`,
);
const existingNode = existing?.calls?.edges?.[0]?.node;
if (existingNode) {
const newCount = (existingNode.missedcallcount || 1) + 1;
const newCount = (existingNode.missedCallCount || 1) + 1;
const updateParts = [
`missedcallcount: ${newCount}`,
`missedCallCount: ${newCount}`,
`startedAt: "${callTime}"`,
`callsourcenumber: "${did}"`,
`callSourceNumber: "${did}"`,
];
if (leadId) updateParts.push(`leadId: "${leadId}"`);
if (leadName) updateParts.push(`leadName: "${leadName}"`);
@@ -123,9 +123,9 @@ export class MissedQueueService implements OnModuleInit {
`callStatus: MISSED`,
`direction: INBOUND`,
`callerNumber: { primaryPhoneNumber: "${phone}", primaryPhoneCallingCode: "+91" }`,
`callsourcenumber: "${did}"`,
`callbackstatus: PENDING_CALLBACK`,
`missedcallcount: 1`,
`callSourceNumber: "${did}"`,
`callbackStatus: PENDING_CALLBACK`,
`missedCallCount: 1`,
`startedAt: "${callTime}"`,
];
if (leadId) dataParts.push(`leadId: "${leadId}"`);
@@ -160,12 +160,12 @@ export class MissedQueueService implements OnModuleInit {
// Find oldest unassigned PENDING_CALLBACK call (empty agentName)
let result = await this.platform.query<any>(
`{ calls(first: 1, filter: {
callbackstatus: { eq: PENDING_CALLBACK },
callbackStatus: { eq: PENDING_CALLBACK },
agentName: { eq: "" }
}, orderBy: [{ startedAt: AscNullsLast }]) {
edges { node {
id callerNumber { primaryPhoneNumber }
startedAt callsourcenumber missedcallcount
startedAt callSourceNumber missedCallCount
} }
} }`,
);
@@ -176,12 +176,12 @@ export class MissedQueueService implements OnModuleInit {
if (!call) {
result = await this.platform.query<any>(
`{ calls(first: 1, filter: {
callbackstatus: { eq: PENDING_CALLBACK },
callbackStatus: { eq: PENDING_CALLBACK },
agentName: { is: NULL }
}, orderBy: [{ startedAt: AscNullsLast }]) {
edges { node {
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(', ')}`);
}
const dataParts: string[] = [`callbackstatus: ${status}`];
const dataParts: string[] = [`callbackStatus: ${status}`];
if (status === 'CALLBACK_ATTEMPTED') {
dataParts.push(`callbackattemptedat: "${new Date().toISOString()}"`);
dataParts.push(`callbackAttemptedAt: "${new Date().toISOString()}"`);
}
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,
authHeader,
);
@@ -230,12 +230,12 @@ export class MissedQueueService implements OnModuleInit {
const fields = `id name createdAt direction callStatus agentName
callerNumber { primaryPhoneNumber }
startedAt endedAt durationSec disposition leadId
callbackstatus callsourcenumber missedcallcount callbackattemptedat`;
callbackStatus callSourceNumber missedCallCount callbackAttemptedAt`;
const buildQuery = (status: string) => `{ calls(first: 50, filter: {
agentName: { eq: "${agentName}" },
callStatus: { eq: MISSED },
callbackstatus: { eq: ${status} }
callbackStatus: { eq: ${status} }
}, orderBy: [{ startedAt: AscNullsLast }]) { edges { node { ${fields} } } } }`;
try {

View File

@@ -97,13 +97,13 @@ export class WorklistService {
try {
// FIFO ordering (AscNullsLast) — oldest first. No agentName filter — missed calls are a shared queue.
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
direction callStatus agentName
callerNumber { primaryPhoneNumber }
startedAt endedAt durationSec
disposition leadId
callbackstatus callsourcenumber missedcallcount callbackattemptedat
callbackStatus callSourceNumber missedCallCount callbackAttemptedAt
} } } }`,
undefined,
authHeader,