mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage-server
synced 2026-05-18 20:08:19 +00:00
fix: disposition for answered inbound calls + SLA timing wiring + backfill
Three related fixes:
1. Disposition for answered inbound calls
Previously the dispose endpoint sent the agent's choice to Ozonetel
but never wrote it back to the platform Call record. The webhook's
pre-disposition value ("General Enquiry" → INFO_PROVIDED) persisted.
Now: dispose endpoint finds the Call by UCID and updates disposition
to the agent's actual selection.
2. SLA timing wiring (assignedAt / answeredAt / responseTimeS)
patchCallTiming() existed but was never called. Now wired into
handleCallEvent:
- "Calling" event → writes assignedAt (ring start)
- "Answered" event → writes answeredAt + computes responseTimeS
(answeredAt - startedAt = caller wait time)
Uses patchCallTimingByUcid helper that looks up Call by UCID.
3. Backfill maint endpoint: POST /api/maint/backfill-call-disposition-timing
Walks calls for a given date, joins to CDR by UCID (both legs),
patches disposition (from CDR's mapped value, always overwrites),
timing fields (answeredAt, assignedAt, responseTimeS from CDR),
and CDR-specific durations (handlingTimeS, acwDurationS, holdDurationS).
Idempotent — safe to run multiple times.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -101,6 +101,22 @@ export class SupervisorService implements OnModuleInit {
|
||||
eventType: 'CALL_START',
|
||||
eventAt: iso,
|
||||
}).catch(() => {});
|
||||
|
||||
// Write answeredAt + responseTimeS to the Call record.
|
||||
// Look up the Call by UCID, then patch. The "Calling" event
|
||||
// sets assignedAt (ring start); "Answered" computes response
|
||||
// time as answered - assigned (queue wait time).
|
||||
this.patchCallTimingByUcid(ucid, {
|
||||
answeredAt: iso,
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
||||
// "Calling" = agent's phone is ringing → write assignedAt
|
||||
// (the moment the call was routed to this agent).
|
||||
if (action === 'Calling') {
|
||||
this.patchCallTimingByUcid(ucid, {
|
||||
assignedAt: iso,
|
||||
}).catch(() => {});
|
||||
}
|
||||
} else if (action === 'Disconnect') {
|
||||
const wasActive = this.activeCalls.get(ucid);
|
||||
@@ -306,6 +322,50 @@ export class SupervisorService implements OnModuleInit {
|
||||
return Array.from(this.activeCalls.values());
|
||||
}
|
||||
|
||||
// Look up a Call by UCID and patch its timing fields. Used by
|
||||
// handleCallEvent to write assignedAt/answeredAt in real-time.
|
||||
// Also computes responseTimeS when answeredAt is written and
|
||||
// the Call already has a startedAt.
|
||||
private async patchCallTimingByUcid(ucid: string, fields: {
|
||||
assignedAt?: string;
|
||||
answeredAt?: string;
|
||||
}): Promise<void> {
|
||||
try {
|
||||
const data = await this.platform.query<any>(
|
||||
`{ calls(first: 1, filter: { ucid: { eq: "${ucid}" } }) { edges { node { id startedAt assignedAt } } } }`,
|
||||
);
|
||||
const call = data?.calls?.edges?.[0]?.node;
|
||||
if (!call) {
|
||||
this.logger.warn(`[SLA] No Call for ucid=${ucid} — timing not written`);
|
||||
return;
|
||||
}
|
||||
|
||||
const patch: Record<string, any> = {};
|
||||
if (fields.assignedAt) patch.assignedAt = fields.assignedAt;
|
||||
if (fields.answeredAt) {
|
||||
patch.answeredAt = fields.answeredAt;
|
||||
// Compute response time: answered - started (how long the
|
||||
// caller waited from call creation to agent pickup).
|
||||
const start = call.startedAt ? new Date(call.startedAt).getTime() : null;
|
||||
const answered = new Date(fields.answeredAt).getTime();
|
||||
if (start && !isNaN(start) && !isNaN(answered)) {
|
||||
const responseS = Math.max(0, Math.round((answered - start) / 1000));
|
||||
patch.responseTimeS = responseS;
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(patch).length > 0) {
|
||||
await this.platform.query<any>(
|
||||
`mutation($id: UUID!, $data: CallUpdateInput!) { updateCall(id: $id, data: $data) { id } }`,
|
||||
{ id: call.id, data: patch },
|
||||
);
|
||||
this.logger.log(`[SLA] Patched call ${call.id} — ${Object.entries(patch).map(([k, v]) => `${k}=${v}`).join(' ')}`);
|
||||
}
|
||||
} catch (err: any) {
|
||||
this.logger.warn(`[SLA] patchCallTimingByUcid failed for ${ucid}: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async getTeamPerformance(date: string): Promise<any> {
|
||||
// Get all agents from platform. Field names are label-derived
|
||||
// camelCase on the current platform schema — see
|
||||
|
||||
Reference in New Issue
Block a user