mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage-server
synced 2026-05-18 20:08:19 +00:00
Audited all 23 sidecar create-mutation call sites; 7 were missing the top-level data.name field that the platform uses as record title: - caller-resolution.service.ts createPatient — full name from first/last - maint.controller.ts createPatient (backfill-lead-patient-links) — same - widget.service.ts createPatient (chat path + booking path) — full name - widget.service.ts createAppointment — "<Patient> — <date>" - worklist/missed-queue.service.ts createCall — "Missed — <phone>" - rules-engine/actions/escalate.action.ts createPerformanceAlert — "<agent>: <message> (<value>)" - supervisor/agent-history.service.ts createAgentEvent / createAgentSession Cosmetic only — the app fetches fullName/agentName for display, so end users never saw "Untitled". Fixes platform-side admin browsing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
96 lines
4.6 KiB
TypeScript
96 lines
4.6 KiB
TypeScript
import { Injectable, Logger } from '@nestjs/common';
|
|
import { PlatformGraphqlService } from '../../platform/platform-graphql.service';
|
|
import type { ActionHandler, ActionResult } from '../types/action.types';
|
|
import type { RuleAction, EscalateActionParams } from '../types/rule.types';
|
|
|
|
/**
|
|
* Persists a PerformanceAlert when a rule's escalate action fires.
|
|
*
|
|
* Dedupes by (agentId, alertType, IST date) — a single rule firing every
|
|
* 5 min should only produce ONE alert per day per agent until dismissed.
|
|
* If a row already exists for that key today and is not dismissed, the
|
|
* action is a no-op (returns the existing id). If the existing row was
|
|
* dismissed earlier today, we don't re-fire — supervisor explicitly
|
|
* acknowledged.
|
|
*/
|
|
@Injectable()
|
|
export class EscalateActionHandler implements ActionHandler {
|
|
type = 'escalate';
|
|
private readonly logger = new Logger(EscalateActionHandler.name);
|
|
|
|
constructor(private readonly platform: PlatformGraphqlService) {}
|
|
|
|
async execute(action: RuleAction, context: Record<string, any>): Promise<ActionResult> {
|
|
const params = action.params as EscalateActionParams & { ruleId?: string; alertType?: string };
|
|
const agentId = context['agent.id'] as string | undefined;
|
|
const agentName = (context['agent.name'] as string | undefined) ?? '';
|
|
const valueRaw = context['_alertValue'];
|
|
const valueText = valueRaw != null ? String(valueRaw) : null;
|
|
|
|
if (!agentId) {
|
|
return { success: false, error: 'agent.id missing from facts' };
|
|
}
|
|
|
|
const alertType = params.alertType ?? this.inferAlertType(params.message);
|
|
const severity = (params.severity ?? 'warning').toUpperCase(); // INFO | WARNING | CRITICAL
|
|
const today = this.todayIst();
|
|
|
|
// Dedupe: any non-dismissed alert today for this agent + type?
|
|
try {
|
|
const existing = await this.platform.query<any>(
|
|
`{ performanceAlerts(first: 1, filter: {
|
|
agentId: { eq: "${agentId}" },
|
|
alertType: { eq: ${alertType} },
|
|
firedAt: { gte: "${today}T00:00:00+05:30", lte: "${today}T23:59:59+05:30" }
|
|
}) { edges { node { id dismissedAt value } } } }`,
|
|
);
|
|
const existingNode = existing?.performanceAlerts?.edges?.[0]?.node;
|
|
if (existingNode) {
|
|
// Already fired today. If value changed, update it; otherwise no-op.
|
|
if (!existingNode.dismissedAt && existingNode.value !== valueText) {
|
|
await this.platform.query<any>(
|
|
`mutation($id: UUID!, $data: PerformanceAlertUpdateInput!) { updatePerformanceAlert(id: $id, data: $data) { id } }`,
|
|
{ id: existingNode.id, data: { value: valueText } },
|
|
);
|
|
}
|
|
return { success: true, data: { id: existingNode.id, deduped: true, agentId, alertType } };
|
|
}
|
|
|
|
const created = await this.platform.query<any>(
|
|
`mutation($data: PerformanceAlertCreateInput!) { createPerformanceAlert(data: $data) { id } }`,
|
|
{
|
|
data: {
|
|
name: `${agentName || agentId}: ${params.message ?? alertType}${valueText ? ` (${valueText})` : ''}`,
|
|
agentId,
|
|
alertType,
|
|
severity,
|
|
message: params.message ?? alertType,
|
|
value: valueText,
|
|
ruleId: params.ruleId ?? null,
|
|
firedAt: new Date().toISOString(),
|
|
},
|
|
},
|
|
);
|
|
const id = created?.createPerformanceAlert?.id;
|
|
this.logger.log(`[ESCALATE] Created alert ${id} agent=${agentName ?? agentId} type=${alertType} value=${valueText}`);
|
|
return { success: true, data: { id, agentId, alertType, severity, message: params.message } };
|
|
} catch (err: any) {
|
|
this.logger.warn(`[ESCALATE] Failed for agent=${agentId}: ${err?.message ?? err}`);
|
|
return { success: false, error: String(err?.message ?? err) };
|
|
}
|
|
}
|
|
|
|
private inferAlertType(message: string | undefined): string {
|
|
const m = (message ?? '').toLowerCase();
|
|
if (m.includes('idle')) return 'EXCESSIVE_IDLE';
|
|
if (m.includes('nps')) return 'LOW_NPS';
|
|
if (m.includes('conversion')) return 'LOW_CONVERSION';
|
|
return 'OTHER';
|
|
}
|
|
|
|
private todayIst(): string {
|
|
const ist = new Date(Date.now() + 5.5 * 60 * 60 * 1000);
|
|
return ist.toISOString().slice(0, 10);
|
|
}
|
|
}
|