fbca017eaa
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>
307 lines
9.6 KiB
TypeScript
307 lines
9.6 KiB
TypeScript
/**
|
||
* 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);
|
||
});
|
||
});
|