mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage-server
synced 2026-05-18 20:08:19 +00:00
feat(maint): backfill-call-agents-by-name for historical Calls
Historical Calls pre-date UCID persistence, so the CDR-join enrichment can't reach them. Fallback: parse agentName (may be "A -> B -> C" transfer chain), take the final hop, resolve to Agent by ozonetelAgentId (case-insensitive) or by Full Name. Preserve the full chain string in transferredTo when it was actually chained. No rate limit — pure platform queries, no CDR. POST /api/maint/backfill-call-agents-by-name Header: x-maint-otp: <OTP> Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -598,6 +598,104 @@ export class MaintController {
|
||||
return { status: 'ok', scanned: totalScanned, enriched: totalEnriched, skipped: totalSkipped, dates };
|
||||
}
|
||||
|
||||
// Fallback backfill for historical Calls that pre-date UCID persistence.
|
||||
// Can't join to CDR without UCID, so parse the agentName string (which
|
||||
// may be a transfer chain "A -> B -> C"), take the final segment, and
|
||||
// resolve to an Agent entity by name or ozonetelAgentId (case-insensitive).
|
||||
//
|
||||
// POST /api/maint/backfill-call-agents-by-name
|
||||
// Headers: x-maint-otp: <OTP>
|
||||
// Body: {}
|
||||
@Post('backfill-call-agents-by-name')
|
||||
async backfillCallAgentsByName() {
|
||||
this.logger.log('[MAINT] Backfill call agents by name — matching agentName last-segment to Agent entity');
|
||||
|
||||
// Pull all active agents — cheap, cached at service level but we
|
||||
// also need a name→UUID map for this pass.
|
||||
const agentData = await this.platform.query<any>(
|
||||
`{ agents(first: 100) { edges { node { id name ozonetelAgentId } } } }`,
|
||||
);
|
||||
const agentUuidByName = new Map<string, string>(); // lowercase name → UUID
|
||||
const agentUuidByOzonetelId = new Map<string, string>(); // lowercase ozonetelAgentId → UUID
|
||||
for (const edge of agentData?.agents?.edges ?? []) {
|
||||
const a = edge.node;
|
||||
if (a.name) agentUuidByName.set(a.name.toLowerCase().trim(), a.id);
|
||||
if (a.ozonetelAgentId) agentUuidByOzonetelId.set(a.ozonetelAgentId.toLowerCase().trim(), a.id);
|
||||
}
|
||||
|
||||
let scanned = 0;
|
||||
let patched = 0;
|
||||
let skipped = 0;
|
||||
let unmatched = 0;
|
||||
const unmatchedSamples = new Set<string>();
|
||||
|
||||
// Paginate through all Calls with agentId=null and agentName set.
|
||||
let after: string | null = null;
|
||||
for (let page = 0; page < 50; page++) {
|
||||
const cursorArg: string = after ? `, after: "${after}"` : '';
|
||||
const data: any = await this.platform.query<any>(
|
||||
`{ calls(first: 200${cursorArg}, filter: {
|
||||
agentId: { is: NULL },
|
||||
agentName: { is: NOT_NULL }
|
||||
}) {
|
||||
edges { node { id agentName } }
|
||||
pageInfo { hasNextPage endCursor }
|
||||
} }`,
|
||||
).catch(() => ({ calls: { edges: [], pageInfo: {} } }));
|
||||
const edges = data?.calls?.edges ?? [];
|
||||
scanned += edges.length;
|
||||
|
||||
for (const edge of edges) {
|
||||
const call = edge.node;
|
||||
if (!call.agentName || call.agentName.trim() === '') { skipped++; continue; }
|
||||
|
||||
// Take the final hop of the transfer chain, trimmed.
|
||||
const segments = call.agentName.split('->').map((s: string) => s.trim()).filter(Boolean);
|
||||
const last = segments[segments.length - 1];
|
||||
if (!last) { skipped++; continue; }
|
||||
|
||||
// Prefer ozonetelAgentId match; fall back to display name.
|
||||
const key = last.toLowerCase();
|
||||
const uuid = agentUuidByOzonetelId.get(key) ?? agentUuidByName.get(key);
|
||||
if (!uuid) {
|
||||
unmatched++;
|
||||
if (unmatchedSamples.size < 10) unmatchedSamples.add(last);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Store the raw chain on transferredTo if it was actually chained,
|
||||
// so the audit trail is preserved even without CDR data.
|
||||
const patchData: Record<string, any> = { agentId: uuid };
|
||||
if (segments.length > 1) patchData.transferredTo = call.agentName;
|
||||
|
||||
try {
|
||||
await this.platform.query<any>(
|
||||
`mutation($id: UUID!, $data: CallUpdateInput!) { updateCall(id: $id, data: $data) { id } }`,
|
||||
{ id: call.id, data: patchData },
|
||||
);
|
||||
patched++;
|
||||
await new Promise((r) => setTimeout(r, 50));
|
||||
} catch (err) {
|
||||
skipped++;
|
||||
}
|
||||
}
|
||||
|
||||
const pageInfo = data?.calls?.pageInfo ?? {};
|
||||
if (!pageInfo.hasNextPage) break;
|
||||
after = pageInfo.endCursor ?? null;
|
||||
}
|
||||
|
||||
this.logger.log(`[MAINT] Backfill by name complete: scanned=${scanned} patched=${patched} unmatched=${unmatched} skipped=${skipped}`);
|
||||
return {
|
||||
status: 'ok',
|
||||
scanned,
|
||||
patched,
|
||||
unmatched,
|
||||
skipped,
|
||||
unmatchedSamples: Array.from(unmatchedSamples),
|
||||
};
|
||||
}
|
||||
|
||||
private async enrichSingleDate(date: string): Promise<{ scanned: number; enriched: number; skipped: number }> {
|
||||
// Reuse the cdr-enrichment path via its runOnce method, but scoped.
|
||||
// For simplicity we reimplement the single-date logic here so we can
|
||||
|
||||
Reference in New Issue
Block a user