// src/rules-engine/rules-engine.service.ts import { Injectable, Logger } from '@nestjs/common'; import { Engine } from 'json-rules-engine'; import { RulesStorageService } from './rules-storage.service'; import { LeadFactsProvider } from './facts/lead-facts.provider'; import { CallFactsProvider, computeSlaMultiplier, computeSlaStatus } from './facts/call-facts.provider'; import { AgentFactsProvider } from './facts/agent-facts.provider'; import { ScoreActionHandler } from './actions/score.action'; import { AssignActionHandler } from './actions/assign.action'; import { EscalateActionHandler } from './actions/escalate.action'; import type { Rule, ScoredItem, ScoreBreakdown, PriorityConfig } from './types/rule.types'; import type { ActionHandler } from './types/action.types'; @Injectable() export class RulesEngineService { private readonly logger = new Logger(RulesEngineService.name); private readonly leadFacts = new LeadFactsProvider(); private readonly callFacts = new CallFactsProvider(); private readonly agentFacts = new AgentFactsProvider(); private readonly actionHandlers: Map; constructor( private readonly storage: RulesStorageService, private readonly escalateHandler: EscalateActionHandler, ) { this.actionHandlers = new Map([ ['score', new ScoreActionHandler()], ['assign', new AssignActionHandler()], ['escalate', this.escalateHandler], ]); } async evaluate(triggerType: string, triggerValue: string, factContext: Record): Promise<{ rulesApplied: string[]; results: any[] }> { const rules = await this.storage.getByTrigger(triggerType, triggerValue); if (rules.length === 0) return { rulesApplied: [], results: [] }; const engine = new Engine(); const ruleMap = new Map(); for (const rule of rules) { engine.addRule({ conditions: rule.conditions as any, event: { type: rule.action.type, params: { ruleId: rule.id, ...rule.action.params as any } }, priority: rule.priority, }); ruleMap.set(rule.id, rule); } for (const [key, value] of Object.entries(factContext)) { engine.addFact(key, value); } const { events } = await engine.run(); const results: any[] = []; const rulesApplied: string[] = []; for (const event of events) { const ruleId = event.params?.ruleId; const rule = ruleMap.get(ruleId); if (!rule) continue; const handler = this.actionHandlers.get(event.type); if (handler) { const result = await handler.execute(rule.action, factContext); results.push({ ruleId, ruleName: rule.name, ...result }); rulesApplied.push(rule.name); } } return { rulesApplied, results }; } async scoreWorklistItem(item: any, priorityConfig: PriorityConfig): Promise { const leadFacts = await this.leadFacts.resolveFacts(item.originalLead ?? item); const callFacts = await this.callFacts.resolveFacts(item, priorityConfig); const taskType = callFacts['call.taskType'] as string; // Inject priority config weights into context for the score action const campaignWeight = item.campaignId ? (priorityConfig.campaignWeights[item.campaignId] ?? 5) : 5; const sourceWeight = priorityConfig.sourceWeights[leadFacts['lead.source'] as string] ?? 5; const allFacts: Record = { ...leadFacts, ...callFacts, '_campaignWeight': campaignWeight, '_sourceWeight': sourceWeight, }; const { rulesApplied, results } = await this.evaluate('on_request', 'worklist', allFacts); let totalScore = 0; let slaMultiplierVal = 1; let campaignMultiplierVal = 1; for (const result of results) { if (result.success && result.data?.score != null) { totalScore += result.data.score; if (result.data.slaApplied) slaMultiplierVal = computeSlaMultiplier((allFacts['call.slaElapsedPercent'] as number) ?? 0); if (result.data.campaignApplied) campaignMultiplierVal = (campaignWeight / 10) * (sourceWeight / 10); } } const slaElapsedPercent = (allFacts['call.slaElapsedPercent'] as number) ?? 0; return { id: item.id, score: Math.round(totalScore * 100) / 100, scoreBreakdown: { baseScore: totalScore, slaMultiplier: Math.round(slaMultiplierVal * 100) / 100, campaignMultiplier: Math.round(campaignMultiplierVal * 100) / 100, rulesApplied, }, slaStatus: computeSlaStatus(slaElapsedPercent), slaElapsedPercent, }; } async scoreWorklist(items: any[]): Promise<(any & ScoredItem)[]> { const priorityConfig = await this.storage.getPriorityConfig(); const scored = await Promise.all( items.map(async (item) => { const scoreData = await this.scoreWorklistItem(item, priorityConfig); return { ...item, ...scoreData }; }), ); scored.sort((a, b) => b.score - a.score); return scored; } async previewScoring(items: any[], config: PriorityConfig): Promise<(any & ScoredItem)[]> { // Same as scoreWorklist but uses provided config (for live preview) const scored = await Promise.all( items.map(async (item) => { const scoreData = await this.scoreWorklistItem(item, config); return { ...item, ...scoreData }; }), ); scored.sort((a, b) => b.score - a.score); return scored; } }