mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage-server
synced 2026-05-18 20:08:19 +00:00
Defect 5: Worklist, missed-call-webhook, missed-queue, ai-chat, and rules-engine all used legacy lowercase field names (callbackstatus, callsourcenumber, missedcallcount, callbackattemptedat) from the old VPS schema. Fixed to camelCase (callbackStatus, callSourceNumber, missedCallCount, callbackAttemptedAt) matching the current SDK sync. Defect 6: Dial endpoint used global defaults (OZONETEL_AGENT_ID env var) instead of the logged-in agent's config. Now accepts agentId and campaignName from the frontend request body. Falls back to telephony config → DID-derived campaign name → explicit error. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
53 lines
2.4 KiB
TypeScript
53 lines
2.4 KiB
TypeScript
// src/rules-engine/facts/call-facts.provider.ts
|
|
|
|
import type { FactProvider, FactValue } from '../types/fact.types';
|
|
import type { PriorityConfig } from '../types/rule.types';
|
|
|
|
export class CallFactsProvider implements FactProvider {
|
|
name = 'call';
|
|
|
|
async resolveFacts(call: any, priorityConfig?: PriorityConfig): Promise<Record<string, FactValue>> {
|
|
const taskType = this.inferTaskType(call);
|
|
const slaMinutes = priorityConfig?.taskWeights[taskType]?.slaMinutes ?? 1440;
|
|
const createdAt = call.createdAt ? new Date(call.createdAt).getTime() : Date.now();
|
|
const elapsedMinutes = Math.round((Date.now() - createdAt) / 60000);
|
|
const slaElapsedPercent = Math.round((elapsedMinutes / slaMinutes) * 100);
|
|
|
|
return {
|
|
'call.direction': call.callDirection ?? call.direction ?? null,
|
|
'call.status': call.callStatus ?? null,
|
|
'call.disposition': call.disposition ?? null,
|
|
'call.durationSeconds': call.durationSeconds ?? call.durationSec ?? 0,
|
|
'call.callbackStatus': call.callbackStatus ?? call.callbackStatus ?? null,
|
|
'call.slaElapsedPercent': slaElapsedPercent,
|
|
'call.slaBreached': slaElapsedPercent > 100,
|
|
'call.missedCount': call.missedCallCount ?? call.missedCount ?? 0,
|
|
'call.taskType': taskType,
|
|
};
|
|
}
|
|
|
|
private inferTaskType(call: any): string {
|
|
if (call.callStatus === 'MISSED' || call.type === 'missed') return 'missed_call';
|
|
if (call.followUpType === 'CALLBACK' || call.type === 'callback') return 'follow_up';
|
|
if (call.type === 'follow-up') return 'follow_up';
|
|
if (call.contactAttempts >= 3) return 'attempt_3';
|
|
if (call.contactAttempts >= 2) return 'attempt_2';
|
|
if (call.campaignId || call.type === 'lead') return 'campaign_lead';
|
|
return 'campaign_lead';
|
|
}
|
|
}
|
|
|
|
// Exported scoring functions — used by both sidecar and frontend (via scoring.ts)
|
|
export function computeSlaMultiplier(slaElapsedPercent: number): number {
|
|
const elapsed = slaElapsedPercent / 100;
|
|
if (elapsed > 1) return 1.0 + (elapsed - 1) * 0.5;
|
|
return Math.pow(elapsed, 1.6);
|
|
}
|
|
|
|
export function computeSlaStatus(slaElapsedPercent: number): 'low' | 'medium' | 'high' | 'critical' {
|
|
if (slaElapsedPercent > 100) return 'critical';
|
|
if (slaElapsedPercent >= 80) return 'high';
|
|
if (slaElapsedPercent >= 50) return 'medium';
|
|
return 'low';
|
|
}
|