Re_Backend/src/queues/tatProcessor.mongo.ts

291 lines
12 KiB
TypeScript

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<TatJobData>) {
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;
}
}