diff --git a/src/ozonetel/cdr-enrichment.service.ts b/src/ozonetel/cdr-enrichment.service.ts index 19cc7be..b0014d7 100644 --- a/src/ozonetel/cdr-enrichment.service.ts +++ b/src/ozonetel/cdr-enrichment.service.ts @@ -63,10 +63,18 @@ export class CdrEnrichmentService implements OnModuleInit, OnModuleDestroy { if (cdrRows.length === 0) continue; // Build UCID → cdr-row map so we can O(1) join per Call. + // Ozonetel emits two identifiers per call — `UCID` (caller-leg) + // and `monitorUCID` (agent-leg). The webhook stores `monitorUCID`, + // but the bulk CDR rows are keyed on caller-leg `UCID`. Index + // both so the lookup at line ~79 finds the row regardless of + // which side was persisted. Without this, transferred inbound + // calls never get their agent relation enriched. const byUcid = new Map(); for (const row of cdrRows) { const ucid = String(row.UCID ?? '').trim(); + const monitorUcid = String(row.monitorUCID ?? '').trim(); if (ucid) byUcid.set(ucid, row); + if (monitorUcid && monitorUcid !== ucid) byUcid.set(monitorUcid, row); } if (byUcid.size === 0) continue; @@ -80,9 +88,25 @@ export class CdrEnrichmentService implements OnModuleInit, OnModuleDestroy { if (!cdrRow) { skipped++; continue; } const patch: Record = {}; - const cdrAgentId = cdrRow.AgentID; - if (cdrAgentId && !call.agentId) { - const uuid = await this.agentLookup.resolveByOzonetelId(cdrAgentId); + if (!call.agentId) { + // Primary resolution: use AgentID from CDR (unique lowercase id). + const cdrAgentId = cdrRow.AgentID; + let uuid = cdrAgentId + ? await this.agentLookup.resolveByOzonetelId(cdrAgentId) + : null; + // Fallback: CDR AgentName may be a chain ("A -> B") for + // transferred calls. Pick the final handler (last segment) + // and look it up by display name or ozonetelId. Matches + // the write-time resolution in missed-call-webhook. + if (!uuid && cdrRow.AgentName) { + const segments = String(cdrRow.AgentName).split('->').map((s) => s.trim()).filter(Boolean); + const finalHandler = segments[segments.length - 1]; + if (finalHandler) { + uuid = + (await this.agentLookup.resolveByOzonetelId(finalHandler)) ?? + (await this.agentLookup.resolveByDisplayName(finalHandler)); + } + } if (uuid) patch.agentId = uuid; if (cdrRow.AgentName) patch.agentName = cdrRow.AgentName; } diff --git a/src/worklist/missed-call-webhook.controller.ts b/src/worklist/missed-call-webhook.controller.ts index bbce33c..e596d5b 100644 --- a/src/worklist/missed-call-webhook.controller.ts +++ b/src/worklist/missed-call-webhook.controller.ts @@ -1,5 +1,6 @@ import { Controller, Post, Body, Headers, Logger } from '@nestjs/common'; import { PlatformGraphqlService } from '../platform/platform-graphql.service'; +import { AgentLookupService } from '../platform/agent-lookup.service'; import { CallerResolutionService } from '../caller/caller-resolution.service'; import { ConfigService } from '@nestjs/config'; @@ -22,6 +23,7 @@ export class MissedCallWebhookController { private readonly platform: PlatformGraphqlService, private readonly config: ConfigService, private readonly caller: CallerResolutionService, + private readonly agentLookup: AgentLookupService, ) { this.apiKey = config.get('platform.apiKey') ?? ''; } @@ -197,6 +199,25 @@ export class MissedCallWebhookController { callData.recording = { primaryLinkUrl: data.recordingUrl, primaryLinkLabel: 'Recording' }; } + // Resolve agent relation at write-time so the supervisor dashboard + // can bucket the row immediately. Ozonetel sends transferred calls + // with a chain-style AgentName like "RamaiahAdmin -> GlobalHealthX" — + // the final handler is the last segment, so split on " -> " and + // resolve that. Try both ozonetelAgentId (lowercase unique) and + // ozonetelDisplayName (mixed-case human label) since Ozonetel mixes + // formats across webhook payloads. Leaves agentId null on miss so + // the cdr-enrichment cron can still attempt a match by UCID later. + if (data.agentName) { + const segments = data.agentName.split('->').map((s) => s.trim()).filter(Boolean); + const finalHandler = segments[segments.length - 1]; + if (finalHandler) { + const uuid = + (await this.agentLookup.resolveByOzonetelId(finalHandler)) ?? + (await this.agentLookup.resolveByDisplayName(finalHandler)); + if (uuid) callData.agentId = uuid; + } + } + const result = await this.platform.queryWithAuth( `mutation($data: CallCreateInput!) { createCall(data: $data) { id } }`, { data: callData }, diff --git a/src/worklist/missed-call-webhook.spec.ts b/src/worklist/missed-call-webhook.spec.ts index c7d8f60..8722375 100644 --- a/src/worklist/missed-call-webhook.spec.ts +++ b/src/worklist/missed-call-webhook.spec.ts @@ -11,6 +11,8 @@ import { Test } from '@nestjs/testing'; import { ConfigService } from '@nestjs/config'; import { MissedCallWebhookController } from './missed-call-webhook.controller'; import { PlatformGraphqlService } from '../platform/platform-graphql.service'; +import { AgentLookupService } from '../platform/agent-lookup.service'; +import { CallerResolutionService } from '../caller/caller-resolution.service'; import { WEBHOOK_INBOUND_ANSWERED, WEBHOOK_INBOUND_MISSED, @@ -48,11 +50,28 @@ describe('MissedCallWebhookController', () => { }), }; + const mockCaller = { + resolve: jest.fn().mockResolvedValue({ + leadId: '', + firstName: '', + lastName: '', + patientId: '', + isNew: true, + }), + }; + + const mockAgentLookup = { + resolveByOzonetelId: jest.fn().mockResolvedValue(null), + resolveByDisplayName: jest.fn().mockResolvedValue(null), + }; + const module = await Test.createTestingModule({ controllers: [MissedCallWebhookController], providers: [ { provide: PlatformGraphqlService, useValue: mockPlatformGql }, { provide: ConfigService, useValue: mockConfig }, + { provide: CallerResolutionService, useValue: mockCaller }, + { provide: AgentLookupService, useValue: mockAgentLookup }, ], }).compile();