fix: dispose creates inbound Call record, webhook enriches — eliminates UCID mismatch + timing race
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed

Root cause: CDR webhook fires 5s after dispose, stores monitorUCID.
Frontend has agent-side UCID from SIP. They never matched → disposition
not persisted, agent call history empty.

Fix:
- Dispose endpoint now creates Call records for ALL answered calls
  (inbound + outbound), not just outbound. Record gets agent-side UCID
  + correct disposition immediately.
- Webhook checks if Call record already exists (by agent UCID via
  monitorUCID→agentUCID mapping). If found, enriches with recording
  URL, agent chain name, CDR timing. If not found, creates as fallback.
- SupervisorService stores UCID mapping from real-time events (which
  carry both UCIDs). Auto-expires after 10 minutes.

Verified: UCID-MAP logged, dispose creates record, webhook enriches
without duplicating. DB shows correct agent UCID + disposition + recording.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-18 08:31:07 +05:30
parent 8c8b1e78b0
commit 3f22166ac0
3 changed files with 83 additions and 54 deletions

View File

@@ -50,7 +50,15 @@ export class MissedCallWebhookController {
const duration = this.parseDuration(payload.CallDuration ?? '00:00:00');
const agentName = payload.AgentName ?? null;
const recordingUrl = payload.AudioFile ?? null;
const ucid = payload.monitorUCID ?? null;
const monitorUcid = payload.monitorUCID ?? null;
// Resolve agent-side UCID from real-time event mapping.
// The dispose endpoint creates Call records with the agent UCID;
// this lets us find and enrich that record instead of duplicating.
const agentUcid = monitorUcid ? this.supervisor.resolveAgentUcid(monitorUcid) : null;
const ucid = agentUcid ?? monitorUcid;
if (agentUcid) {
this.logger.log(`[WEBHOOK] Resolved monitorUCID ${monitorUcid} → agent UCID ${agentUcid}`);
}
const disposition = payload.Disposition ?? null;
const hangupBy = payload.HangupBy ?? null;
@@ -109,24 +117,54 @@ export class MissedCallWebhookController {
this.logger.warn(`[WEBHOOK] Caller resolution failed for ${callerPhone}: ${err}`);
}
// Step 2: Create call record with leadId + leadName baked in so
// the worklist row renders the patient name immediately.
const callId = await this.createCall({
callerPhone,
direction,
callStatus,
agentName,
startTime,
endTime,
duration,
recordingUrl,
disposition,
ucid,
leadId: resolved.leadId || null,
leadName: resolved.leadName,
}, authHeader);
this.logger.log(`Created call record: ${callId} (${callStatus})${resolved.leadName ? ` linked to ${resolved.leadName}` : ''}`);
// Step 2: For answered calls, the dispose endpoint creates the
// Call record ~5s before this webhook fires. Check if it already
// exists and enrich it instead of creating a duplicate.
let callId: string;
if (callStatus === 'COMPLETED' && ucid) {
const existing = await this.platform.queryWithAuth<any>(
`{ calls(first: 1, filter: { ucid: { eq: "${ucid}" } }) { edges { node { id } } } }`,
undefined, authHeader,
).catch(() => null);
const existingId = existing?.calls?.edges?.[0]?.node?.id;
if (existingId) {
// Enrich existing record with webhook data (recording, chain name, timing)
const enrichData: Record<string, any> = {};
if (agentName) enrichData.agentName = agentName;
if (recordingUrl) enrichData.recording = { primaryLinkUrl: recordingUrl, primaryLinkLabel: 'Recording' };
if (resolved.leadId) enrichData.leadId = resolved.leadId;
if (resolved.leadName) enrichData.leadName = resolved.leadName;
if (startTime) enrichData.startedAt = istToUtc(startTime);
if (endTime) enrichData.endedAt = istToUtc(endTime);
if (duration) enrichData.durationSec = duration;
if (Object.keys(enrichData).length > 0) {
await this.platform.queryWithAuth<any>(
`mutation($id: UUID!, $data: CallUpdateInput!) { updateCall(id: $id, data: $data) { id } }`,
{ id: existingId, data: enrichData },
authHeader,
).catch(err => this.logger.warn(`[WEBHOOK] Failed to enrich call ${existingId}: ${err}`));
}
callId = existingId;
this.logger.log(`[WEBHOOK] Enriched existing call ${callId} with recording=${recordingUrl ? 'yes' : 'no'} agentName=${agentName}`);
} else {
// Fallback: dispose didn't create it (edge case) — create normally
this.logger.log(`[WEBHOOK] No existing call found for ucid=${ucid} — creating new record`);
callId = await this.createCall({
callerPhone, direction, callStatus, agentName,
startTime, endTime, duration, recordingUrl, disposition, ucid,
leadId: resolved.leadId || null, leadName: resolved.leadName,
}, authHeader);
this.logger.log(`Created call record: ${callId} (${callStatus})${resolved.leadName ? ` linked to ${resolved.leadName}` : ''}`);
}
} else {
// Missed calls — always create (no dispose fires for unanswered)
callId = await this.createCall({
callerPhone, direction, callStatus, agentName,
startTime, endTime, duration, recordingUrl, disposition, ucid,
leadId: resolved.leadId || null, leadName: resolved.leadName,
}, authHeader);
this.logger.log(`Created call record: ${callId} (${callStatus})${resolved.leadName ? ` linked to ${resolved.leadName}` : ''}`);
}
// Push worklist SSE so agents see new calls instantly
// instead of waiting for the 30s frontend poll.