Files
helix-engage-server/src/platform/agent-lookup.service.ts
saridsa2 5b40f49b65 feat(agent-lookup): resolve by Ozonetel display name too
Inbound webhook rows store agentName as Ozonetel's display string
("Ganesh Bandi", "GlobalHealthX") which doesn't match either
ozonetelAgentId or platform Agent.name. Add ozonetelDisplayName as a
third index — populated on each platform Agent with the Full Name from
the Ozonetel admin UI.

After this + setting display names on Global + Ramaiah agents:
backfill matched 122/136 (~90%) on Global; remaining are calls handled
by agents that don't exist on this workspace.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 08:25:14 +05:30

71 lines
3.0 KiB
TypeScript

import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
import { PlatformGraphqlService } from './platform-graphql.service';
/**
* Maps Ozonetel agent identifiers (unique — e.g. "ramaiahadmin",
* "globalhealthx", "global") to the platform Agent entity UUID. Used by
* ingest paths (webhook, dispose, CDR enrichment, backfill) so every Call
* ends up with the correct `agent` relation regardless of how Ozonetel
* formats the display name (AgentName collisions, transfer chains like
* "A -> B -> C", etc.).
*
* The cache is case-insensitive because Ozonetel occasionally mixes
* casing ("global" vs "Global" vs "GLOBAL") across webhook/CDR responses.
*/
@Injectable()
export class AgentLookupService implements OnModuleInit {
private readonly logger = new Logger(AgentLookupService.name);
private readonly uuidByOzonetelId = new Map<string, string>();
private readonly uuidByDisplayName = new Map<string, string>();
constructor(private readonly platform: PlatformGraphqlService) {}
async onModuleInit() {
await this.refresh();
}
async refresh(): Promise<void> {
try {
const data = await this.platform.query<any>(
`{ agents(first: 100) { edges { node { id ozonetelAgentId ozonetelDisplayName } } } }`,
);
const edges = data?.agents?.edges ?? [];
this.uuidByOzonetelId.clear();
this.uuidByDisplayName.clear();
for (const edge of edges) {
const n = edge.node;
if (n.ozonetelAgentId) {
this.uuidByOzonetelId.set(n.ozonetelAgentId.toLowerCase(), n.id);
}
if (n.ozonetelDisplayName) {
this.uuidByDisplayName.set(n.ozonetelDisplayName.toLowerCase().trim(), n.id);
}
}
this.logger.log(`[AGENT-LOOKUP] Loaded ${this.uuidByOzonetelId.size} agents (${this.uuidByDisplayName.size} with display name)`);
} catch (err) {
this.logger.warn(`[AGENT-LOOKUP] Refresh failed: ${err}`);
}
}
async resolveByOzonetelId(ozonetelId: string | null | undefined): Promise<string | null> {
if (!ozonetelId) return null;
const key = ozonetelId.toLowerCase();
const cached = this.uuidByOzonetelId.get(key);
if (cached) return cached;
// Cache miss — refresh once (handles late-provisioned agents)
await this.refresh();
return this.uuidByOzonetelId.get(key) ?? null;
}
// Resolve by Ozonetel display name (e.g. "Ganesh Bandi") — used by
// missed-call webhook backfill where only AgentName (display) is available.
async resolveByDisplayName(displayName: string | null | undefined): Promise<string | null> {
if (!displayName) return null;
const key = displayName.toLowerCase().trim();
const cached = this.uuidByDisplayName.get(key);
if (cached) return cached;
await this.refresh();
return this.uuidByDisplayName.get(key) ?? null;
}
}