import { Job } from 'bullmq'; import { notificationMongoService } from '../services/notification.service'; import { ApprovalLevelModel } from '../models/mongoose/ApprovalLevel.schema'; import { WorkflowRequestModel } from '../models/mongoose/WorkflowRequest.schema'; import { TatAlertModel } from '../models/mongoose/TatAlert.schema'; import { activityMongoService } from '../services/activity.service'; import logger from '../utils/logger'; import { calculateElapsedWorkingHours, addWorkingHours, addWorkingHoursExpress } from '../utils/tatTimeUtils'; interface TatJobData { type: 'threshold1' | 'threshold2' | 'breach'; threshold: number; requestId: string; levelId: string; approverId: string; } /** * Handle TAT notification jobs (MongoDB Version) */ export async function handleTatJob(job: Job) { const { requestId, levelId, approverId, type, threshold } = job.data; logger.info(`[TAT Processor] Processing ${type} (${threshold}%) for request ${requestId}`); try { // Get approval level const approvalLevel = await ApprovalLevelModel.findById(levelId); if (!approvalLevel) { logger.warn(`[TAT Processor] Approval level ${levelId} not found - likely already approved/rejected`); return; } // Check if level is still pending if (approvalLevel.status !== 'PENDING' && approvalLevel.status !== 'IN_PROGRESS') { logger.info(`[TAT Processor] Level ${levelId} is already ${approvalLevel.status}. Skipping notification.`); return; } // Get workflow - Try finding by UUID (requestId) first let workflow: any = await WorkflowRequestModel.findOne({ requestId: requestId }); if (!workflow) { // Fallback to requestNumber workflow = await WorkflowRequestModel.findOne({ requestNumber: requestId }); } if (!workflow) { // Fallback to _id workflow = await WorkflowRequestModel.findById(requestId); } if (!workflow) { logger.warn(`[TAT Processor] Workflow ${requestId} not found`); return; } const requestNumber = workflow.requestNumber; const title = workflow.title; let message = ''; let activityDetails = ''; let thresholdPercentage: number = threshold; let alertType: 'TAT_50' | 'TAT_75' | 'TAT_100' = 'TAT_50'; // Check if level is paused if (approvalLevel.paused?.isPaused) { logger.info(`[TAT Processor] Skipping ${type} notification - level ${levelId} is paused`); return; } const tatHours = Number(approvalLevel.tat?.assignedHours || 0); const levelStartTime = approvalLevel.createdAt || new Date(); // Fallback // Or check if approvalLevel has a specific tatStartTime // Schema has 'tat.startTime' const actualStartTime = approvalLevel.tat?.startTime || levelStartTime; const now = new Date(); const priority = (workflow.priority || 'STANDARD').toString().toLowerCase(); // Check pause info const isCurrentlyPaused = approvalLevel.paused?.isPaused === true; const wasResumed = !isCurrentlyPaused && (approvalLevel.paused?.elapsedHoursBeforePause !== undefined && approvalLevel.paused?.elapsedHoursBeforePause !== null) && (approvalLevel.paused?.resumedAt !== undefined && approvalLevel.paused?.resumedAt !== null); const pauseInfo = isCurrentlyPaused ? { isPaused: true, pausedAt: approvalLevel.paused?.pausedAt, pauseElapsedHours: approvalLevel.paused?.elapsedHoursBeforePause, pauseResumeDate: approvalLevel.paused?.resumedAt // Might be null } : wasResumed ? { isPaused: false, pausedAt: null, pauseElapsedHours: Number(approvalLevel.paused?.elapsedHoursBeforePause), pauseResumeDate: approvalLevel.paused?.resumedAt } : undefined; const elapsedHours = await calculateElapsedWorkingHours(approvalLevel.createdAt, now, priority, pauseInfo); let remainingHours = Math.max(0, tatHours - elapsedHours); const expectedCompletionTime = priority === 'express' ? (await addWorkingHoursExpress(actualStartTime, tatHours)).toDate() : (await addWorkingHours(actualStartTime, tatHours)).toDate(); switch (type) { case 'threshold1': alertType = 'TAT_50'; thresholdPercentage = threshold; message = `${threshold}% of TAT elapsed for Request ${requestNumber}: ${title}`; activityDetails = `${threshold}% of TAT time has elapsed`; await ApprovalLevelModel.updateOne( { _id: levelId }, { 'alerts.fiftyPercentSent': true, // We can store generic TAT stats here if schema supports it, for now rely on alerts flag 'tat.actualParams.elapsedHours': elapsedHours } ); break; case 'threshold2': alertType = 'TAT_75'; thresholdPercentage = threshold; message = `${threshold}% of TAT elapsed for Request ${requestNumber}: ${title}. Please take action soon.`; activityDetails = `${threshold}% of TAT time has elapsed - Escalation warning`; await ApprovalLevelModel.updateOne( { _id: levelId }, { 'alerts.seventyFivePercentSent': true, 'tat.actualParams.elapsedHours': elapsedHours } ); break; case 'breach': alertType = 'TAT_100'; thresholdPercentage = 100; message = `TAT breached for Request ${requestNumber}: ${title}. Immediate action required!`; activityDetails = 'TAT deadline reached - Breach notification'; remainingHours = 0; await ApprovalLevelModel.updateOne( { _id: levelId }, { 'tat.isBreached': true, 'tat.actualParams.elapsedHours': elapsedHours } ); break; } // Create TAT Alert (Mongo) try { await TatAlertModel.create({ requestId: workflow.requestId, // Standardized to UUID levelId, approverId, alertType, thresholdPercentage, tatHoursAllocated: tatHours, tatHoursElapsed: elapsedHours, tatHoursRemaining: remainingHours, levelStartTime: actualStartTime, alertSentAt: now, expectedCompletionTime, alertMessage: message, notificationSent: true, notificationChannels: ['push'], isBreached: type === 'breach', metadata: { requestNumber, requestTitle: title, approverName: approvalLevel.approver?.name, priority: priority, levelNumber: approvalLevel.levelNumber } }); logger.info(`[TAT Processor] ✅ Alert created: ${type} (${threshold}%)`); } catch (alertError: any) { logger.error(`[TAT Processor] ❌ Alert creation failed: ${alertError.message}`); } const notificationPriority = type === 'breach' ? 'URGENT' : type === 'threshold2' ? 'HIGH' : 'MEDIUM'; const timeRemainingText = remainingHours > 0 ? `${remainingHours.toFixed(1)} hours remaining` : type === 'breach' ? `${Math.abs(remainingHours).toFixed(1)} hours overdue` : 'Time exceeded'; // Notification try { await notificationMongoService.sendToUsers([approverId], { title: type === 'breach' ? 'TAT Breach Alert' : 'TAT Reminder', body: message, requestId: workflow.requestId, // Standardized to UUID requestNumber, url: `/request/${requestNumber}`, type: type, priority: notificationPriority as any, actionRequired: type === 'breach' || type === 'threshold2', metadata: { thresholdPercentage, tatInfo: { thresholdPercentage, timeRemaining: timeRemainingText, tatDeadline: expectedCompletionTime, assignedDate: actualStartTime, timeOverdue: type === 'breach' ? timeRemainingText : undefined } } }); logger.info(`[TAT Processor] ✅ Notification sent to approver ${approverId}`); } catch (notificationError: any) { logger.error(`[TAT Processor] ❌ Failed to send notification: ${notificationError.message}`); } // Breach initiator notification if (type === 'breach') { const initiatorId = workflow.initiator?.userId; if (initiatorId && initiatorId !== approverId) { try { await notificationMongoService.sendToUsers([initiatorId], { title: 'TAT Breach - Request Delayed', body: `Your request ${requestNumber}: "${title}" has exceeded its TAT.`, requestId: workflow.requestId, // Standardized to UUID requestNumber, type: 'tat_breach_initiator', priority: 'HIGH' }); } catch (e) { logger.error('Initiator notification failed', e); } } } // Activity Log try { // System user handling might differ in Mongo logic. Passing userId: 'system' is fine usually. await activityMongoService.log({ requestId: workflow.requestId, // Standardized to UUID type: 'sla_warning', user: { userId: 'system', name: 'System' }, timestamp: new Date().toISOString(), action: type === 'breach' ? 'TAT Breached' : 'TAT Warning', details: activityDetails, category: 'SYSTEM', severity: type === 'breach' ? 'ERROR' : 'WARNING' }); } catch (e) { logger.warn('Activity log failed', e); } // Socket Emit try { const { emitToRequestRoom } = require('../realtime/socket'); if (emitToRequestRoom) { // Fetch latest alert const newAlert = await TatAlertModel.findOne({ requestId: workflow.requestId, levelId: levelId, alertType }).sort({ createdAt: -1 }); if (newAlert) { emitToRequestRoom(workflow.requestId, 'tat:alert', { alert: newAlert.toJSON(), requestId: workflow.requestId, levelId, type, thresholdPercentage, message }); } } } catch (e) { logger.warn('Socket emit failed', e); } logger.info(`[TAT Processor] ✅ ${type} processed`); } catch (error) { logger.error(`[TAT Processor] Failed to process ${type}:`, error); throw error; } }