import { Injectable, Logger } from '@nestjs/common'; import { PlatformGraphqlService } from '../../platform/platform-graphql.service'; import type { FactProvider, FactValue } from '../types/fact.types'; /** * Resolves per-agent performance facts for the rules engine. * Used by the PerformanceConsumer to evaluate alert rules every 5 min. * * Facts exposed: * - agent.idleMinutes — from today's AgentSession.idleTimeS * - agent.busyMinutes — from AgentSession.busyTimeS * - agent.totalCallsToday — count of Calls started today * - agent.bookedCallsToday — count of Calls today with disposition=APPOINTMENT_BOOKED * - agent.conversionPercent — bookedCallsToday / totalCallsToday × 100 * - agent.id, agent.name — for routing alerts back to the right agent * * NPS deferred — no source signal exists yet. */ @Injectable() export class PerformanceFactsProvider implements FactProvider { name = 'performance'; private readonly logger = new Logger(PerformanceFactsProvider.name); constructor(private readonly platform: PlatformGraphqlService) {} /** * @param entityData { agentId: string, agentName?: string } */ async resolveFacts(entityData: { agentId: string; agentName?: string }): Promise> { const agentId = entityData.agentId; const today = this.todayIst(); const session = await this.fetchTodaySession(agentId, today); const callTotals = await this.fetchTodayCallTotals(agentId, today); const idleMinutes = Math.round((session?.idleTimeS ?? 0) / 60); const busyMinutes = Math.round((session?.busyTimeS ?? 0) / 60); const conversionPercent = callTotals.total > 0 ? Math.round((callTotals.booked / callTotals.total) * 100) : 0; return { 'agent.id': agentId, 'agent.name': entityData.agentName ?? '', 'agent.idleMinutes': idleMinutes, 'agent.busyMinutes': busyMinutes, 'agent.totalCallsToday': callTotals.total, 'agent.bookedCallsToday': callTotals.booked, 'agent.conversionPercent': conversionPercent, }; } private todayIst(): string { const ist = new Date(Date.now() + 5.5 * 60 * 60 * 1000); return ist.toISOString().slice(0, 10); } private async fetchTodaySession(agentId: string, date: string): Promise<{ idleTimeS: number; busyTimeS: number } | null> { try { const data = await this.platform.query( `{ agentSessions(first: 1, filter: { agentId: { eq: "${agentId}" }, date: { eq: "${date}" } }) { edges { node { idleTimeS busyTimeS } } } }`, ); const node = data?.agentSessions?.edges?.[0]?.node; if (!node) return null; return { idleTimeS: node.idleTimeS ?? 0, busyTimeS: node.busyTimeS ?? 0 }; } catch (err) { this.logger.warn(`[PERF-FACTS] Session fetch failed for agent=${agentId}: ${err}`); return null; } } private async fetchTodayCallTotals(agentId: string, date: string): Promise<{ total: number; booked: number }> { const gte = `${date}T00:00:00+05:30`; const lte = `${date}T23:59:59+05:30`; try { const data = await this.platform.query( `{ calls(first: 200, filter: { agentId: { eq: "${agentId}" }, startedAt: { gte: "${gte}", lte: "${lte}" } }) { edges { node { disposition } } } }`, ); const edges = data?.calls?.edges ?? []; const total = edges.length; const booked = edges.filter((e: any) => e.node.disposition === 'APPOINTMENT_BOOKED').length; return { total, booked }; } catch (err) { this.logger.warn(`[PERF-FACTS] Call totals fetch failed for agent=${agentId}: ${err}`); return { total: 0, booked: 0 }; } } }