Re_Backend/HOLIDAY_EXPRESS_TAT.md

13 KiB

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

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:

  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.