Re_Backend/HOLIDAY_EXPRESS_TAT.md

517 lines
13 KiB
Markdown

# Holiday Handling & EXPRESS Mode TAT Calculation
## Overview
The TAT (Turn Around Time) system now supports:
1. **Holiday Exclusions** - Configured holidays are excluded from STANDARD priority TAT calculations
2. **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**
```sql
INSERT INTO holidays (holiday_date, holiday_name, holiday_type, is_active)
VALUES ('2025-12-25', 'Christmas Day', 'PUBLIC_HOLIDAY', true);
```
### **3. Holiday Cache Updated**
```typescript
// Holidays are cached in memory for 6 hours
await loadHolidaysCache();
// → holidaysCache = Set(['2025-12-25', '2025-01-01', ...])
```
### **4. TAT Calculation Uses Holiday Cache**
```typescript
// 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)**
```typescript
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)**
```typescript
export function addCalendarHours(start: Date, hoursToAdd: number): Dayjs {
// Simple addition - no checks ✅
return dayjs(start).add(hoursToAdd, 'hour');
}
```
---
## TAT Scheduler Integration
### **Updated Method Signature:**
```typescript
async scheduleTatJobs(
requestId: string,
levelId: string,
approverId: string,
tatDurationHours: number,
startTime?: Date,
priority: Priority = Priority.STANDARD // ✅ New parameter
): Promise<void>
```
### **Priority-Based Calculation:**
```typescript
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):**
```typescript
// 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):**
```typescript
// 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:**
```sql
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:**
```sql
-- 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**
```bash
# 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**
```bash
# 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**
```bash
# 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:**
1. ✅ Added `addCalendarHours()` for EXPRESS mode (24/7 calculation)
2. ✅ Updated `addWorkingHours()` to check holidays from admin settings
3. ✅ Added `priority` parameter to `scheduleTatJobs()`
4. ✅ Updated workflow/approval services to pass priority
5. ✅ 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:**
1.**Accurate TAT for STANDARD** - Respects holidays, no false breaches
2.**Fast EXPRESS** - True 24/7 calculation for urgent requests
3.**Centralized Holiday Management** - Admin can add/edit holidays
4.**Performance** - Holiday cache prevents repeated DB queries
5.**Flexible** - Priority can be changed per request
---
## Files Modified
1. `Re_Backend/src/utils/tatTimeUtils.ts` - Added `addCalendarHours()` for EXPRESS mode
2. `Re_Backend/src/services/tatScheduler.service.ts` - Added priority parameter and logic
3. `Re_Backend/src/services/workflow.service.ts` - Pass priority when scheduling TAT
4. `Re_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.