Files
Nexus/packages/staffing/src/__tests__/capacity-analyzer-vacation.test.ts
T
Hartmut fbca017eaa test(application): add unit tests for demand fill logic and capacity vacation overlap
Covers fill-demand-requirement status validation, duplicate detection,
fill-open-demand happy path, and vacation overlap edge cases in
capacity analyzer.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 14:15:00 +02:00

307 lines
9.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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"), // MonTue
hoursPerDay: 8,
status: AllocationStatus.CONFIRMED,
projectName: "Vacation",
isChargeable: false,
},
{
startDate: new Date("2026-03-11"), // WedFri
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 MonSun. Vacation is SatSun 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"), // MonSun
};
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", () => {
// MonWed booked as vacation (8 h/day), ThuFri free
const windows = findCapacityWindows(
simpleResource,
[
{
startDate: MON,
endDate: new Date("2026-03-11"), // MonWed
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, ThuFri 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 ThuFri
expect(windows).toHaveLength(2);
expect(windows[0]!.availableDays).toBe(1); // Tue
expect(windows[1]!.availableDays).toBe(2); // ThuFri
});
it("vacation on weekend days does not affect capacity windows for weekdays", () => {
// Vacation on SatSun 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 MonFri 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);
});
});