Re_Backend/src/services/tatScheduler.service.ts

200 lines
7.4 KiB
TypeScript

import { tatQueue } from '../queues/tatQueue';
import { calculateDelay, addWorkingHours, addCalendarHours } from '@utils/tatTimeUtils';
import { getTatThresholds } from './configReader.service';
import dayjs from 'dayjs';
import logger from '@utils/logger';
import { Priority } from '../types/common.types';
export class TatSchedulerService {
/**
* Schedule TAT notification jobs for an approval level
* @param requestId - The workflow request ID
* @param levelId - The approval level ID
* @param approverId - The approver user ID
* @param tatDurationHours - TAT duration in hours
* @param startTime - Optional start time (defaults to now)
* @param priority - Request priority (EXPRESS = 24/7, STANDARD = working hours only)
*/
async scheduleTatJobs(
requestId: string,
levelId: string,
approverId: string,
tatDurationHours: number,
startTime?: Date,
priority: Priority = Priority.STANDARD
): Promise<void> {
try {
// Check if tatQueue is available
if (!tatQueue) {
logger.warn(`[TAT Scheduler] TAT queue not available (Redis not connected). Skipping TAT job scheduling.`);
return;
}
const now = startTime || new Date();
const isExpress = priority === Priority.EXPRESS;
// Get current thresholds from database configuration
const thresholds = await getTatThresholds();
// Calculate milestone times using configured thresholds
// EXPRESS mode: 24/7 calculation (includes holidays, weekends, non-working hours)
// STANDARD mode: Working hours only (excludes holidays, weekends, non-working hours)
let threshold1Time: Date;
let threshold2Time: Date;
let breachTime: Date;
if (isExpress) {
// EXPRESS: 24/7 calculation - no exclusions
threshold1Time = addCalendarHours(now, tatDurationHours * (thresholds.first / 100)).toDate();
threshold2Time = addCalendarHours(now, tatDurationHours * (thresholds.second / 100)).toDate();
breachTime = addCalendarHours(now, tatDurationHours).toDate();
logger.info(`[TAT Scheduler] Using EXPRESS mode (24/7) - no holiday/weekend exclusions`);
} else {
// STANDARD: Working hours only, excludes holidays
const t1 = await addWorkingHours(now, tatDurationHours * (thresholds.first / 100));
const t2 = await addWorkingHours(now, tatDurationHours * (thresholds.second / 100));
const tBreach = await addWorkingHours(now, tatDurationHours);
threshold1Time = t1.toDate();
threshold2Time = t2.toDate();
breachTime = tBreach.toDate();
logger.info(`[TAT Scheduler] Using STANDARD mode - excludes holidays, weekends, non-working hours`);
}
logger.info(`[TAT Scheduler] Calculating TAT milestones for request ${requestId}, level ${levelId}`);
logger.info(`[TAT Scheduler] Priority: ${priority}, TAT Hours: ${tatDurationHours}`);
logger.info(`[TAT Scheduler] Start: ${dayjs(now).format('YYYY-MM-DD HH:mm')}`);
logger.info(`[TAT Scheduler] Threshold 1 (${thresholds.first}%): ${dayjs(threshold1Time).format('YYYY-MM-DD HH:mm')}`);
logger.info(`[TAT Scheduler] Threshold 2 (${thresholds.second}%): ${dayjs(threshold2Time).format('YYYY-MM-DD HH:mm')}`);
logger.info(`[TAT Scheduler] Breach (100%): ${dayjs(breachTime).format('YYYY-MM-DD HH:mm')}`);
const jobs = [
{
type: 'threshold1' as const,
threshold: thresholds.first,
delay: calculateDelay(threshold1Time),
targetTime: threshold1Time
},
{
type: 'threshold2' as const,
threshold: thresholds.second,
delay: calculateDelay(threshold2Time),
targetTime: threshold2Time
},
{
type: 'breach' as const,
threshold: 100,
delay: calculateDelay(breachTime),
targetTime: breachTime
}
];
for (const job of jobs) {
// Skip if the time has already passed
if (job.delay === 0) {
logger.warn(`[TAT Scheduler] Skipping ${job.type} (${job.threshold}%) for level ${levelId} - time already passed`);
continue;
}
await tatQueue.add(
job.type,
{
type: job.type,
threshold: job.threshold, // Store actual threshold percentage in job data
requestId,
levelId,
approverId
},
{
delay: job.delay,
jobId: `tat-${job.type}-${requestId}-${levelId}`, // Generic job ID
removeOnComplete: true,
removeOnFail: false
}
);
logger.info(
`[TAT Scheduler] Scheduled ${job.type} (${job.threshold}%) for level ${levelId} ` +
`(delay: ${Math.round(job.delay / 1000 / 60)} minutes, ` +
`target: ${dayjs(job.targetTime).format('YYYY-MM-DD HH:mm')})`
);
}
logger.info(`[TAT Scheduler] ✅ TAT jobs scheduled for request ${requestId}, approver ${approverId}`);
} catch (error) {
logger.error(`[TAT Scheduler] Failed to schedule TAT jobs:`, error);
throw error;
}
}
/**
* Cancel TAT jobs for a specific approval level
* Useful when an approver acts before TAT expires
* @param requestId - The workflow request ID
* @param levelId - The approval level ID
*/
async cancelTatJobs(requestId: string, levelId: string): Promise<void> {
try {
// Check if tatQueue is available
if (!tatQueue) {
logger.warn(`[TAT Scheduler] TAT queue not available. Skipping job cancellation.`);
return;
}
// Use generic job names that don't depend on threshold percentages
const jobIds = [
`tat-threshold1-${requestId}-${levelId}`,
`tat-threshold2-${requestId}-${levelId}`,
`tat-breach-${requestId}-${levelId}`
];
for (const jobId of jobIds) {
try {
const job = await tatQueue.getJob(jobId);
if (job) {
await job.remove();
logger.info(`[TAT Scheduler] Cancelled job ${jobId}`);
}
} catch (error) {
// Job might not exist, which is fine
logger.debug(`[TAT Scheduler] Job ${jobId} not found (may have already been processed)`);
}
}
logger.info(`[TAT Scheduler] ✅ TAT jobs cancelled for level ${levelId}`);
} catch (error) {
logger.error(`[TAT Scheduler] Failed to cancel TAT jobs:`, error);
// Don't throw - cancellation failure shouldn't break the workflow
}
}
/**
* Cancel all TAT jobs for a workflow request
* @param requestId - The workflow request ID
*/
async cancelAllTatJobsForRequest(requestId: string): Promise<void> {
try {
// Check if tatQueue is available
if (!tatQueue) {
logger.warn(`[TAT Scheduler] TAT queue not available. Skipping job cancellation.`);
return;
}
const jobs = await tatQueue.getJobs(['delayed', 'waiting']);
const requestJobs = jobs.filter(job => job.data.requestId === requestId);
for (const job of requestJobs) {
await job.remove();
logger.info(`[TAT Scheduler] Cancelled job ${job.id}`);
}
logger.info(`[TAT Scheduler] ✅ All TAT jobs cancelled for request ${requestId}`);
} catch (error) {
logger.error(`[TAT Scheduler] Failed to cancel all TAT jobs:`, error);
// Don't throw - cancellation failure shouldn't break the workflow
}
}
}
export const tatSchedulerService = new TatSchedulerService();