/** * Vacation overlap edge-case tests for analyzeUtilization and findCapacityWindows. * * The capacity analyzer itself does NOT have direct vacation knowledge — vacations * are represented as allocations (with hoursPerDay equal to the resource's full * availability) from the caller's perspective. These tests verify that the * analyzeUtilization and findCapacityWindows functions correctly handle the * resulting utilization when vacation blocks are modelled as full-day allocations. */ import { AllocationStatus } from "@capakraken/shared"; import { describe, expect, it } from "vitest"; import { analyzeUtilization, findCapacityWindows } from "../capacity-analyzer.js"; import type { CapacityAnalysisInput } from "../capacity-analyzer.js"; const standardAvailability = { sunday: 0, monday: 8, tuesday: 8, wednesday: 8, thursday: 8, friday: 8, saturday: 0, }; const resource = { id: "res-vacation", displayName: "Vacation Tester", chargeabilityTarget: 80, availability: standardAvailability, }; const simpleResource = { id: "res-vacation", displayName: "Vacation Tester", availability: standardAvailability, }; // Monday 2026-03-09 … Friday 2026-03-13 (5 working days) const MON = new Date("2026-03-09"); const FRI = new Date("2026-03-13"); describe("analyzeUtilization — vacation overlap", () => { it("resource with vacation covering the entire allocation period results in 0 free chargeable hours", () => { // Vacation modelled as a full-day non-chargeable allocation for the whole week const input: CapacityAnalysisInput = { resource, allocations: [ { startDate: MON, endDate: FRI, hoursPerDay: 8, status: AllocationStatus.CONFIRMED, projectName: "Vacation", isChargeable: false, }, ], analysisStart: MON, analysisEnd: FRI, }; const result = analyzeUtilization(input); // No chargeable hours, but all available hours are consumed by vacation expect(result.currentChargeability).toBe(0); // All 5 days are allocated at 8/8 — not overallocated, not underutilized expect(result.overallocatedDays).toHaveLength(0); // Each day: allocatedHours (8) === availableHours (8), which is NOT < 50%, so no underutilized expect(result.underutilizedDays).toHaveLength(0); }); it("resource with partial vacation overlap has correctly reduced available chargeable hours", () => { // 2-day vacation Mon-Tue, then project allocation Wed-Fri const input: CapacityAnalysisInput = { resource, allocations: [ { startDate: MON, endDate: new Date("2026-03-10"), // Mon–Tue hoursPerDay: 8, status: AllocationStatus.CONFIRMED, projectName: "Vacation", isChargeable: false, }, { startDate: new Date("2026-03-11"), // Wed–Fri endDate: FRI, hoursPerDay: 8, status: AllocationStatus.CONFIRMED, projectName: "Project A", isChargeable: true, }, ], analysisStart: MON, analysisEnd: FRI, }; const result = analyzeUtilization(input); // 5 working days × 8 h = 40 h total available // Chargeable hours: 3 days × 8 h = 24 h → 24/40 = 60% expect(result.currentChargeability).toBeCloseTo(60, 5); expect(result.overallocatedDays).toHaveLength(0); expect(result.underutilizedDays).toHaveLength(0); }); it("multiple overlapping vacation blocks are each counted separately without double-counting overallocation when summed hours exceed availability", () => { // Two full-day vacation records on the same Monday (simulating duplicate entries) const input: CapacityAnalysisInput = { resource, allocations: [ { startDate: MON, endDate: MON, hoursPerDay: 5, status: AllocationStatus.CONFIRMED, projectName: "Vacation A", isChargeable: false, }, { startDate: MON, endDate: MON, hoursPerDay: 5, status: AllocationStatus.CONFIRMED, projectName: "Vacation B", isChargeable: false, }, ], analysisStart: MON, analysisEnd: MON, }; const result = analyzeUtilization(input); // 10 h allocated on a day with 8 h availability → overallocated expect(result.overallocatedDays).toHaveLength(1); // Date string contains the correct date components (timezone-safe check) expect(result.overallocatedDays[0]).toMatch(/2026-03-0[89]/); expect(result.currentChargeability).toBe(0); // all non-chargeable }); it("vacation on Saturday/Sunday does not reduce working hours (availability = 0 on weekends)", () => { // Vacation over a weekend — should have zero effect on computed utilization const input: CapacityAnalysisInput = { resource, allocations: [ { startDate: new Date("2026-03-07"), // Saturday endDate: new Date("2026-03-08"), // Sunday hoursPerDay: 8, status: AllocationStatus.CONFIRMED, projectName: "Weekend Vacation", isChargeable: false, }, ], analysisStart: new Date("2026-03-07"), analysisEnd: new Date("2026-03-08"), }; const result = analyzeUtilization(input); // Weekends skipped entirely — totalWorkingDays = 0, no days affected expect(result.overallocatedDays).toHaveLength(0); expect(result.underutilizedDays).toHaveLength(0); expect(result.currentChargeability).toBe(0); // allocations list still includes the weekend entry (it's just not active on working days) // but chargeability stays 0 because there are no working days in that window }); it("vacation on weekend days within a full-week analysis window leaves weekday capacity intact", () => { // Analysis covers Mon–Sun. Vacation is Sat–Sun only. const input: CapacityAnalysisInput = { resource, allocations: [ { startDate: new Date("2026-03-14"), // Saturday endDate: new Date("2026-03-15"), // Sunday hoursPerDay: 8, status: AllocationStatus.CONFIRMED, projectName: "Weekend Vacation", isChargeable: false, }, ], analysisStart: MON, analysisEnd: new Date("2026-03-15"), // Mon–Sun }; const result = analyzeUtilization(input); // All 5 weekdays have zero allocation → all underutilized (0 < 50% of 8) expect(result.underutilizedDays).toHaveLength(5); expect(result.overallocatedDays).toHaveLength(0); expect(result.currentChargeability).toBe(0); }); }); describe("findCapacityWindows — vacation overlap", () => { it("vacation covering entire search period leaves no capacity windows (full-day blocking)", () => { const windows = findCapacityWindows( simpleResource, [ { startDate: MON, endDate: FRI, hoursPerDay: 8, status: AllocationStatus.CONFIRMED, }, ], MON, FRI, ); expect(windows).toHaveLength(0); }); it("partial vacation overlap leaves capacity only on non-vacation days", () => { // Mon–Wed booked as vacation (8 h/day), Thu–Fri free const windows = findCapacityWindows( simpleResource, [ { startDate: MON, endDate: new Date("2026-03-11"), // Mon–Wed hoursPerDay: 8, status: AllocationStatus.CONFIRMED, }, ], MON, FRI, ); expect(windows).toHaveLength(1); expect(windows[0]!.availableDays).toBe(2); // Thu + Fri expect(windows[0]!.availableHoursPerDay).toBe(8); expect(windows[0]!.totalAvailableHours).toBe(16); }); it("multiple non-overlapping vacation blocks create gaps in capacity", () => { // Mon booked, Tue free, Wed booked, Thu–Fri free const windows = findCapacityWindows( simpleResource, [ { startDate: MON, endDate: MON, hoursPerDay: 8, status: AllocationStatus.CONFIRMED, }, { startDate: new Date("2026-03-11"), // Wed endDate: new Date("2026-03-11"), hoursPerDay: 8, status: AllocationStatus.CONFIRMED, }, ], MON, FRI, ); // Tue alone, then Thu–Fri expect(windows).toHaveLength(2); expect(windows[0]!.availableDays).toBe(1); // Tue expect(windows[1]!.availableDays).toBe(2); // Thu–Fri }); it("vacation on weekend days does not affect capacity windows for weekdays", () => { // Vacation on Sat–Sun should not close any weekday windows const windows = findCapacityWindows( simpleResource, [ { startDate: new Date("2026-03-07"), // Saturday endDate: new Date("2026-03-08"), // Sunday hoursPerDay: 8, status: AllocationStatus.CONFIRMED, }, ], MON, FRI, ); // The weekend vacation falls outside the Mon–Fri window, so full week is free expect(windows).toHaveLength(1); expect(windows[0]!.availableDays).toBe(5); expect(windows[0]!.totalAvailableHours).toBe(40); }); it("cancelled vacation has no effect on available capacity", () => { const windows = findCapacityWindows( simpleResource, [ { startDate: MON, endDate: FRI, hoursPerDay: 8, status: AllocationStatus.CANCELLED, }, ], MON, FRI, ); expect(windows).toHaveLength(1); expect(windows[0]!.availableDays).toBe(5); expect(windows[0]!.totalAvailableHours).toBe(40); }); });