mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage-server
synced 2026-05-18 20:08:19 +00:00
fix+feat: morning QA fixes, worklist pagination, misc sidecar improvements
- caller-resolution: drop cache, use indexed phone filter (lead.contactPhone.primaryPhoneNumber.like) - worklist: externalize page size (WORKLIST_PAGE_SIZE × WORKLIST_MAX_PAGES), paginate getMissedCalls/getAssignedLeads/getPendingFollowUps - maint: unlock-agent, force-ready, backfill-caller-resolution, clear-analysis-cache, fix-timestamps - ozonetel agent.service: force logout+re-login on "already logged in" - ai chat: context expansion - livekit-agent: updates - widget: session handling - masterdata: clinic list cache Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -317,4 +317,108 @@ export class MaintController {
|
||||
this.logger.log(`[MAINT] Backfill complete: ${linked} linked, ${created} patients created, ${apptLinked} appointments linked, ${skipped} skipped`);
|
||||
return { status: 'ok', leads: { total: leads.length, linked, created, skipped }, appointments: { total: appointments.length, linked: apptLinked } };
|
||||
}
|
||||
|
||||
// Backfill Call records that lost their identity at ingest (missed-call
|
||||
// webhook / poller / dispose flow before the caller-resolution wiring).
|
||||
// Routes each phone through CallerResolutionService so the same code
|
||||
// path the live system uses also fixes historical rows. Idempotent —
|
||||
// safe to re-run; only patches calls that are currently missing
|
||||
// leadName / patientId / leadId.
|
||||
@Post('backfill-caller-resolution')
|
||||
async backfillCallerResolution() {
|
||||
this.logger.log('[MAINT] Backfill caller resolution — patching Calls + Leads via resolver');
|
||||
|
||||
const apiKey = process.env.PLATFORM_API_KEY ?? '';
|
||||
const auth = apiKey ? `Bearer ${apiKey}` : '';
|
||||
if (!auth) throw new HttpException('PLATFORM_API_KEY not configured', 500);
|
||||
|
||||
let callsScanned = 0;
|
||||
let callsPatched = 0;
|
||||
let callsSkipped = 0;
|
||||
let leadsResolved = 0;
|
||||
let resolveErrors = 0;
|
||||
|
||||
// Phone → resolved cache so multiple calls from the same number
|
||||
// only resolve once during this run.
|
||||
const resolvedByPhone = new Map<string, { leadId: string; patientId: string; firstName: string; lastName: string }>();
|
||||
|
||||
// Page through all calls in chunks of 200. We're after rows where
|
||||
// leadName is empty OR leadId is null OR patientId is missing.
|
||||
let cursor: string | null = null;
|
||||
let hasNext = true;
|
||||
while (hasNext) {
|
||||
const pageQuery = cursor
|
||||
? `{ calls(first: 200, after: "${cursor}") { edges { cursor node { id leadId leadName callerNumber { primaryPhoneNumber } } } pageInfo { hasNextPage endCursor } } }`
|
||||
: `{ calls(first: 200) { edges { cursor node { id leadId leadName callerNumber { primaryPhoneNumber } } } pageInfo { hasNextPage endCursor } } }`;
|
||||
let page: any;
|
||||
try {
|
||||
page = await this.platform.query<any>(pageQuery);
|
||||
} catch (err) {
|
||||
this.logger.warn(`[MAINT] calls page query failed: ${err}`);
|
||||
break;
|
||||
}
|
||||
const edges = page?.calls?.edges ?? [];
|
||||
hasNext = page?.calls?.pageInfo?.hasNextPage ?? false;
|
||||
cursor = page?.calls?.pageInfo?.endCursor ?? null;
|
||||
|
||||
for (const edge of edges) {
|
||||
const call = edge.node;
|
||||
callsScanned++;
|
||||
|
||||
const phoneRaw = call.callerNumber?.primaryPhoneNumber ?? '';
|
||||
const phone10 = phoneRaw.replace(/\D/g, '').slice(-10);
|
||||
const needsName = !call.leadName || call.leadName === '';
|
||||
const needsLead = !call.leadId;
|
||||
|
||||
if (!phone10 || phone10.length < 10) { callsSkipped++; continue; }
|
||||
if (!needsName && !needsLead) { callsSkipped++; continue; }
|
||||
|
||||
let resolved = resolvedByPhone.get(phone10) ?? null;
|
||||
if (!resolved) {
|
||||
try {
|
||||
const r = await this.callerResolution.resolve(phone10, auth);
|
||||
resolved = {
|
||||
leadId: r.leadId,
|
||||
patientId: r.patientId,
|
||||
firstName: r.firstName,
|
||||
lastName: r.lastName,
|
||||
};
|
||||
resolvedByPhone.set(phone10, resolved);
|
||||
leadsResolved++;
|
||||
} catch (err) {
|
||||
this.logger.warn(`[MAINT] resolve failed for ${phone10}: ${err}`);
|
||||
resolveErrors++;
|
||||
callsSkipped++;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const fullName = `${resolved.firstName} ${resolved.lastName}`.trim();
|
||||
const updateParts: string[] = [];
|
||||
if (needsLead && resolved.leadId) updateParts.push(`leadId: "${resolved.leadId}"`);
|
||||
if (needsName && fullName) updateParts.push(`leadName: "${fullName.replace(/"/g, '\\"')}"`);
|
||||
if (updateParts.length === 0) { callsSkipped++; continue; }
|
||||
|
||||
try {
|
||||
await this.platform.query<any>(
|
||||
`mutation { updateCall(id: "${call.id}", data: { ${updateParts.join(', ')} }) { id } }`,
|
||||
);
|
||||
callsPatched++;
|
||||
} catch (err) {
|
||||
this.logger.warn(`[MAINT] updateCall failed for ${call.id}: ${err}`);
|
||||
callsSkipped++;
|
||||
}
|
||||
|
||||
// Throttle so the platform isn't hammered
|
||||
await new Promise((r) => setTimeout(r, 100));
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.log(`[MAINT] Backfill caller resolution complete: scanned=${callsScanned} patched=${callsPatched} skipped=${callsSkipped} uniquePhones=${resolvedByPhone.size} leadsResolved=${leadsResolved} resolveErrors=${resolveErrors}`);
|
||||
return {
|
||||
status: 'ok',
|
||||
calls: { scanned: callsScanned, patched: callsPatched, skipped: callsSkipped },
|
||||
phones: { unique: resolvedByPhone.size, resolved: leadsResolved, errors: resolveErrors },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user