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-1", displayName: "Jane Doe", chargeabilityTarget: 80, availability: standardAvailability, }; describe("analyzeUtilization", () => { it("computes chargeability for a single chargeable allocation", () => { const input: CapacityAnalysisInput = { resource, allocations: [ { startDate: new Date("2026-03-09"), // Monday endDate: new Date("2026-03-13"), // Friday hoursPerDay: 8, status: AllocationStatus.CONFIRMED, projectName: "Project A", isChargeable: true, }, ], analysisStart: new Date("2026-03-09"), analysisEnd: new Date("2026-03-13"), }; const result = analyzeUtilization(input); expect(result.resourceId).toBe("res-1"); expect(result.currentChargeability).toBe(100); expect(result.chargeabilityGap).toBe(-20); // target 80, actual 100 expect(result.overallocatedDays).toHaveLength(0); expect(result.underutilizedDays).toHaveLength(0); }); it("detects overallocated days", () => { const input: CapacityAnalysisInput = { resource, allocations: [ { startDate: new Date("2026-03-09"), endDate: new Date("2026-03-09"), hoursPerDay: 5, status: AllocationStatus.CONFIRMED, projectName: "A", isChargeable: true, }, { startDate: new Date("2026-03-09"), endDate: new Date("2026-03-09"), hoursPerDay: 5, status: AllocationStatus.CONFIRMED, projectName: "B", isChargeable: true, }, ], analysisStart: new Date("2026-03-09"), analysisEnd: new Date("2026-03-09"), }; const result = analyzeUtilization(input); expect(result.overallocatedDays.length).toBeGreaterThanOrEqual(1); }); it("detects underutilized days (below 50%)", () => { const input: CapacityAnalysisInput = { resource, allocations: [ { startDate: new Date("2026-03-09"), endDate: new Date("2026-03-09"), hoursPerDay: 2, status: AllocationStatus.CONFIRMED, projectName: "A", isChargeable: true, }, ], analysisStart: new Date("2026-03-09"), analysisEnd: new Date("2026-03-09"), }; const result = analyzeUtilization(input); expect(result.underutilizedDays).toHaveLength(1); }); it("ignores allocations with inactive status", () => { const input: CapacityAnalysisInput = { resource, allocations: [ { startDate: new Date("2026-03-09"), endDate: new Date("2026-03-13"), hoursPerDay: 8, status: AllocationStatus.CANCELLED, projectName: "Cancelled", isChargeable: true, }, ], analysisStart: new Date("2026-03-09"), analysisEnd: new Date("2026-03-13"), }; const result = analyzeUtilization(input); expect(result.currentChargeability).toBe(0); expect(result.allocations).toHaveLength(0); }); it("separates chargeable from non-chargeable hours", () => { const input: CapacityAnalysisInput = { resource, allocations: [ { startDate: new Date("2026-03-09"), endDate: new Date("2026-03-09"), hoursPerDay: 4, status: AllocationStatus.CONFIRMED, projectName: "Chargeable", isChargeable: true, }, { startDate: new Date("2026-03-09"), endDate: new Date("2026-03-09"), hoursPerDay: 4, status: AllocationStatus.CONFIRMED, projectName: "Internal", isChargeable: false, }, ], analysisStart: new Date("2026-03-09"), analysisEnd: new Date("2026-03-09"), }; const result = analyzeUtilization(input); expect(result.currentChargeability).toBe(50); expect(result.overallocatedDays).toHaveLength(0); }); it("skips weekends", () => { const input: CapacityAnalysisInput = { resource, allocations: [], analysisStart: new Date("2026-03-07"), // Saturday analysisEnd: new Date("2026-03-08"), // Sunday }; const result = analyzeUtilization(input); expect(result.currentChargeability).toBe(0); expect(result.overallocatedDays).toHaveLength(0); expect(result.underutilizedDays).toHaveLength(0); }); }); describe("findCapacityWindows", () => { const simpleResource = { id: "res-1", displayName: "Jane Doe", availability: standardAvailability, }; it("finds a full-week window with no allocations", () => { const windows = findCapacityWindows( simpleResource, [], new Date("2026-03-09"), // Monday new Date("2026-03-13"), // Friday ); expect(windows).toHaveLength(1); expect(windows[0]!.availableDays).toBe(5); expect(windows[0]!.availableHoursPerDay).toBe(8); expect(windows[0]!.totalAvailableHours).toBe(40); }); it("reduces available hours when partially allocated", () => { const windows = findCapacityWindows( simpleResource, [ { startDate: new Date("2026-03-09"), endDate: new Date("2026-03-13"), hoursPerDay: 4, status: AllocationStatus.CONFIRMED, }, ], new Date("2026-03-09"), new Date("2026-03-13"), ); expect(windows).toHaveLength(1); expect(windows[0]!.availableHoursPerDay).toBe(4); expect(windows[0]!.totalAvailableHours).toBe(20); }); it("returns no windows when fully allocated", () => { const windows = findCapacityWindows( simpleResource, [ { startDate: new Date("2026-03-09"), endDate: new Date("2026-03-13"), hoursPerDay: 8, status: AllocationStatus.CONFIRMED, }, ], new Date("2026-03-09"), new Date("2026-03-13"), ); expect(windows).toHaveLength(0); }); it("ignores cancelled allocations", () => { const windows = findCapacityWindows( simpleResource, [ { startDate: new Date("2026-03-09"), endDate: new Date("2026-03-13"), hoursPerDay: 8, status: AllocationStatus.CANCELLED, }, ], new Date("2026-03-09"), new Date("2026-03-13"), ); expect(windows).toHaveLength(1); expect(windows[0]!.availableDays).toBe(5); }); it("splits windows around gaps", () => { const windows = findCapacityWindows( simpleResource, [ { startDate: new Date("2026-03-11"), // Wed only fully booked endDate: new Date("2026-03-11"), hoursPerDay: 8, status: AllocationStatus.CONFIRMED, }, ], new Date("2026-03-09"), // Mon new Date("2026-03-13"), // Fri ); // Two windows: Mon-Tue, Thu-Fri expect(windows).toHaveLength(2); expect(windows[0]!.availableDays).toBe(2); expect(windows[1]!.availableDays).toBe(2); }); it("respects minAvailableHoursPerDay threshold", () => { const windows = findCapacityWindows( simpleResource, [ { startDate: new Date("2026-03-09"), endDate: new Date("2026-03-09"), hoursPerDay: 6, status: AllocationStatus.CONFIRMED, }, ], new Date("2026-03-09"), new Date("2026-03-09"), 4, // need at least 4 hours free ); // Only 2 hours free (8-6), threshold is 4 → no window expect(windows).toHaveLength(0); }); });