291 lines
12 KiB
TypeScript
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;
|
|
}
|
|
}
|