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>
This commit is contained in:
@@ -0,0 +1,306 @@
|
||||
/**
|
||||
* 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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user