13 KiB
13 KiB
Holiday Handling & EXPRESS Mode TAT Calculation
Overview
The TAT (Turn Around Time) system now supports:
- Holiday Exclusions - Configured holidays are excluded from STANDARD priority TAT calculations
- EXPRESS Mode - EXPRESS priority requests use 24/7 calculation (no exclusions)
How It Works
STANDARD Priority (Default)
Calculation:
- ✅ Excludes weekends (Saturday, Sunday)
- ✅ Excludes non-working hours (9 AM - 6 PM by default)
- ✅ Excludes holidays configured in Admin Settings
Example:
TAT = 16 working hours
Start: Monday 2:00 PM
Calculation:
Monday 2:00 PM - 6:00 PM = 4 hours (remaining: 12h)
Tuesday 9:00 AM - 6:00 PM = 9 hours (remaining: 3h)
Wednesday 9:00 AM - 12:00 PM = 3 hours (remaining: 0h)
If Wednesday is a HOLIDAY → Skip to Thursday:
Wednesday (HOLIDAY) = 0 hours (skipped)
Thursday 9:00 AM - 12:00 PM = 3 hours (remaining: 0h)
Final deadline: Thursday 12:00 PM ✅
EXPRESS Priority
Calculation:
- ✅ Counts ALL hours (24/7)
- ✅ No weekend exclusion
- ✅ No non-working hours exclusion
- ✅ No holiday exclusion
Example:
TAT = 16 hours
Start: Monday 2:00 PM
Calculation:
Simply add 16 hours:
Monday 2:00 PM + 16 hours = Tuesday 6:00 AM
Final deadline: Tuesday 6:00 AM ✅
(Even if Tuesday is a holiday, it still counts)
Holiday Configuration Flow
1. Admin Adds Holiday
Settings Page → Holiday Manager → Add Holiday
Name: "Christmas Day"
Date: 2025-12-25
Type: Public Holiday
✅ Save
2. Holiday Stored in Database
INSERT INTO holidays (holiday_date, holiday_name, holiday_type, is_active)
VALUES ('2025-12-25', 'Christmas Day', 'PUBLIC_HOLIDAY', true);
3. Holiday Cache Updated
// Holidays are cached in memory for 6 hours
await loadHolidaysCache();
// → holidaysCache = Set(['2025-12-25', '2025-01-01', ...])
4. TAT Calculation Uses Holiday Cache
// When scheduling TAT jobs
if (priority === 'STANDARD') {
// Working hours calculation - checks holidays
const threshold1 = await addWorkingHours(start, hours * 0.55);
// → If date is in holidaysCache, it's skipped ✅
} else {
// EXPRESS: 24/7 calculation - ignores holidays
const threshold1 = addCalendarHours(start, hours * 0.55);
// → Adds hours directly, no checks ✅
}
Implementation Details
Function: addWorkingHours() (STANDARD Mode)
export async function addWorkingHours(start: Date, hoursToAdd: number): Promise<Dayjs> {
let current = dayjs(start);
// Load holidays from database (cached)
await loadHolidaysCache();
let remaining = hoursToAdd;
while (remaining > 0) {
current = current.add(1, 'hour');
// Check if current hour is working time
if (isWorkingTime(current)) { // ✅ Checks holidays here
remaining -= 1;
}
}
return current;
}
function isWorkingTime(date: Dayjs): boolean {
// Check weekend
if (date.day() === 0 || date.day() === 6) return false;
// Check working hours
if (date.hour() < 9 || date.hour() >= 18) return false;
// Check if holiday ✅
if (isHoliday(date)) return false;
return true;
}
function isHoliday(date: Dayjs): boolean {
const dateStr = date.format('YYYY-MM-DD');
return holidaysCache.has(dateStr); // ✅ Checks cached holidays
}
Function: addCalendarHours() (EXPRESS Mode)
export function addCalendarHours(start: Date, hoursToAdd: number): Dayjs {
// Simple addition - no checks ✅
return dayjs(start).add(hoursToAdd, 'hour');
}
TAT Scheduler Integration
Updated Method Signature:
async scheduleTatJobs(
requestId: string,
levelId: string,
approverId: string,
tatDurationHours: number,
startTime?: Date,
priority: Priority = Priority.STANDARD // ✅ New parameter
): Promise<void>
Priority-Based Calculation:
const isExpress = priority === Priority.EXPRESS;
if (isExpress) {
// EXPRESS: 24/7 calculation
threshold1Time = addCalendarHours(now, hours * 0.55).toDate();
threshold2Time = addCalendarHours(now, hours * 0.80).toDate();
breachTime = addCalendarHours(now, hours).toDate();
logger.info('Using EXPRESS mode (24/7) - no holiday/weekend exclusions');
} else {
// STANDARD: Working hours, exclude holidays
const t1 = await addWorkingHours(now, hours * 0.55);
const t2 = await addWorkingHours(now, hours * 0.80);
const tBreach = await addWorkingHours(now, hours);
threshold1Time = t1.toDate();
threshold2Time = t2.toDate();
breachTime = tBreach.toDate();
logger.info('Using STANDARD mode - excludes holidays, weekends, non-working hours');
}
Example Scenarios
Scenario 1: STANDARD with Holiday
Request Details:
- Priority: STANDARD
- TAT: 16 working hours
- Start: Monday 2:00 PM
- Holiday: Wednesday (Christmas)
Calculation:
Monday 2:00 PM - 6:00 PM = 4 hours (12h remaining)
Tuesday 9:00 AM - 6:00 PM = 9 hours (3h remaining)
Wednesday (HOLIDAY) = SKIPPED ✅
Thursday 9:00 AM - 12:00 PM = 3 hours (0h remaining)
TAT Milestones:
- Threshold 1 (55%): Tuesday 4:40 PM (8.8 working hours)
- Threshold 2 (80%): Thursday 10:48 AM (12.8 working hours)
- Breach (100%): Thursday 12:00 PM (16 working hours)
Scenario 2: EXPRESS with Holiday
Request Details:
- Priority: EXPRESS
- TAT: 16 hours
- Start: Monday 2:00 PM
- Holiday: Wednesday (Christmas) - IGNORED ✅
Calculation:
Monday 2:00 PM + 16 hours = Tuesday 6:00 AM
TAT Milestones:
- Threshold 1 (55%): Monday 10:48 PM (8.8 hours)
- Threshold 2 (80%): Tuesday 2:48 AM (12.8 hours)
- Breach (100%): Tuesday 6:00 AM (16 hours)
Note: Even though Wednesday is a holiday, EXPRESS doesn't care ✅
Scenario 3: Multiple Holidays
Request Details:
- Priority: STANDARD
- TAT: 40 working hours
- Start: Friday 10:00 AM
- Holidays: Monday (New Year), Tuesday (Day After)
Calculation:
Friday 10:00 AM - 6:00 PM = 8 hours (32h remaining)
Saturday-Sunday = SKIPPED (weekend)
Monday (HOLIDAY) = SKIPPED ✅
Tuesday (HOLIDAY) = SKIPPED ✅
Wednesday 9:00 AM - 6:00 PM = 9 hours (23h remaining)
Thursday 9:00 AM - 6:00 PM = 9 hours (14h remaining)
Friday 9:00 AM - 6:00 PM = 9 hours (5h remaining)
Monday 9:00 AM - 2:00 PM = 5 hours (0h remaining)
Final deadline: Next Monday 2:00 PM ✅
(Skipped 2 weekends + 2 holidays)
Holiday Cache Management
Cache Lifecycle:
1. Server Startup
→ initializeHolidaysCache() called
→ Holidays loaded into memory
2. Cache Valid for 6 Hours
→ holidaysCacheExpiry = now + 6 hours
→ Subsequent calls use cached data (fast)
3. Cache Expires After 6 Hours
→ Next TAT calculation reloads cache from DB
→ New cache expires in 6 hours
4. Manual Cache Refresh (Optional)
→ Admin adds/updates holiday
→ Call initializeHolidaysCache() to refresh immediately
Cache Performance:
Without Cache:
- Every TAT calculation → DB query → SLOW ❌
- 100 requests/hour → 100 DB queries
With Cache:
- Load once per 6 hours → DB query → FAST ✅
- 100 requests/hour → 0 DB queries (use cache)
- Cache refresh: Every 6 hours or on-demand
Priority Detection in Services
Workflow Service (Submission):
// When submitting workflow
const workflowPriority = (updated as any).priority || 'STANDARD';
await tatSchedulerService.scheduleTatJobs(
requestId,
levelId,
approverId,
tatHours,
now,
workflowPriority // ✅ Pass priority
);
Approval Service (Next Level):
// When moving to next approval level
const workflowPriority = (wf as any)?.priority || 'STANDARD';
await tatSchedulerService.scheduleTatJobs(
requestId,
nextLevelId,
nextApproverId,
tatHours,
now,
workflowPriority // ✅ Pass priority
);
Database Schema
Holidays Table:
CREATE TABLE holidays (
holiday_id UUID PRIMARY KEY,
holiday_date DATE NOT NULL,
holiday_name VARCHAR(255) NOT NULL,
holiday_type VARCHAR(50),
description TEXT,
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
-- Example data
INSERT INTO holidays (holiday_date, holiday_name, holiday_type)
VALUES
('2025-12-25', 'Christmas Day', 'PUBLIC_HOLIDAY'),
('2025-01-01', 'New Year''s Day', 'PUBLIC_HOLIDAY'),
('2025-07-04', 'Independence Day', 'PUBLIC_HOLIDAY');
Workflow Request Priority:
-- WorkflowRequest table already has priority field
SELECT request_id, priority, tat_hours
FROM workflow_requests
WHERE priority = 'EXPRESS'; -- 24/7 calculation
-- OR
WHERE priority = 'STANDARD'; -- Working hours + holiday exclusion
Testing Scenarios
Test 1: Add Holiday, Create STANDARD Request
# 1. Add holiday for tomorrow
curl -X POST http://localhost:5000/api/v1/admin/holidays \
-H "Authorization: Bearer TOKEN" \
-d '{
"holidayDate": "2025-11-06",
"holidayName": "Test Holiday",
"holidayType": "PUBLIC_HOLIDAY"
}'
# 2. Create STANDARD request with 24h TAT
curl -X POST http://localhost:5000/api/v1/workflows \
-d '{
"priority": "STANDARD",
"tatHours": 24
}'
# 3. Check scheduled TAT jobs in logs
# → Should show deadline skipping the holiday ✅
Test 2: Same Holiday, EXPRESS Request
# 1. Holiday still exists (tomorrow)
# 2. Create EXPRESS request with 24h TAT
curl -X POST http://localhost:5000/api/v1/workflows \
-d '{
"priority": "EXPRESS",
"tatHours": 24
}'
# 3. Check scheduled TAT jobs in logs
# → Should show deadline NOT skipping the holiday ✅
# → Exactly 24 hours from now (includes holiday)
Test 3: Verify Holiday Exclusion
# Create request on Friday afternoon
# With 16 working hours TAT
# Should skip weekend and land on Monday/Tuesday
# If Monday is a holiday:
# → STANDARD: Should land on Tuesday ✅
# → EXPRESS: Should land on Sunday ✅
Logging Examples
STANDARD Mode Log:
[TAT Scheduler] Using STANDARD mode - excludes holidays, weekends, non-working hours
[TAT Scheduler] Calculating TAT milestones for request REQ-123, level LEVEL-456
[TAT Scheduler] Priority: STANDARD, TAT Hours: 16
[TAT Scheduler] Start: 2025-11-05 14:00
[TAT Scheduler] Threshold 1 (55%): 2025-11-07 11:48 (skipped 1 holiday)
[TAT Scheduler] Threshold 2 (80%): 2025-11-08 09:48
[TAT Scheduler] Breach (100%): 2025-11-08 14:00
EXPRESS Mode Log:
[TAT Scheduler] Using EXPRESS mode (24/7) - no holiday/weekend exclusions
[TAT Scheduler] Calculating TAT milestones for request REQ-456, level LEVEL-789
[TAT Scheduler] Priority: EXPRESS, TAT Hours: 16
[TAT Scheduler] Start: 2025-11-05 14:00
[TAT Scheduler] Threshold 1 (55%): 2025-11-05 22:48 (8.8 hours)
[TAT Scheduler] Threshold 2 (80%): 2025-11-06 02:48 (12.8 hours)
[TAT Scheduler] Breach (100%): 2025-11-06 06:00 (16 hours)
Summary
What Changed:
- ✅ Added
addCalendarHours()for EXPRESS mode (24/7 calculation) - ✅ Updated
addWorkingHours()to check holidays from admin settings - ✅ Added
priorityparameter toscheduleTatJobs() - ✅ Updated workflow/approval services to pass priority
- ✅ Holiday cache for performance (6-hour expiry)
How Holidays Are Used:
| Priority | Calculation Method | Holidays | Weekends | Non-Working Hours |
|---|---|---|---|---|
| STANDARD | Working hours only | ✅ Excluded | ✅ Excluded | ✅ Excluded |
| EXPRESS | 24/7 calendar hours | ❌ Counted | ❌ Counted | ❌ Counted |
Benefits:
- ✅ Accurate TAT for STANDARD - Respects holidays, no false breaches
- ✅ Fast EXPRESS - True 24/7 calculation for urgent requests
- ✅ Centralized Holiday Management - Admin can add/edit holidays
- ✅ Performance - Holiday cache prevents repeated DB queries
- ✅ Flexible - Priority can be changed per request
Files Modified
Re_Backend/src/utils/tatTimeUtils.ts- AddedaddCalendarHours()for EXPRESS modeRe_Backend/src/services/tatScheduler.service.ts- Added priority parameter and logicRe_Backend/src/services/workflow.service.ts- Pass priority when scheduling TATRe_Backend/src/services/approval.service.ts- Pass priority for next level TAT
Configuration Keys
| Config Key | Default | Description |
|---|---|---|
WORK_START_HOUR |
9 | Working hours start (STANDARD mode only) |
WORK_END_HOUR |
18 | Working hours end (STANDARD mode only) |
WORK_START_DAY |
1 | Monday (STANDARD mode only) |
WORK_END_DAY |
5 | Friday (STANDARD mode only) |
Note: EXPRESS mode ignores all these configurations and uses 24/7 calculation.