517 lines
13 KiB
Markdown
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.
|
|
|