200 lines
7.4 KiB
TypeScript
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();
|
|
|