import { describe, expect, it, vi } from "vitest"; vi.mock("../lib/resource-capacity.js", () => ({ getAvailabilityHoursForDate: vi.fn().mockReturnValue(8), calculateEffectiveDayAvailability: vi.fn().mockReturnValue(8), })); import { getAvailabilityHoursForDate, calculateEffectiveDayAvailability, } from "../lib/resource-capacity.js"; import { ACTIVE_STATUSES, createDateRange, getBaseDayAvailability, getEffectiveDayAvailability, createLocationLabel, calculateAllocatedHoursForDay, } from "../router/staffing-shared.js"; // ─── ACTIVE_STATUSES ────────────────────────────────────────────────────────── describe("ACTIVE_STATUSES", () => { it("contains PROPOSED, CONFIRMED and ACTIVE", () => { expect(ACTIVE_STATUSES.has("PROPOSED")).toBe(true); expect(ACTIVE_STATUSES.has("CONFIRMED")).toBe(true); expect(ACTIVE_STATUSES.has("ACTIVE")).toBe(true); }); it("does not contain DRAFT, CANCELLED or COMPLETED", () => { expect(ACTIVE_STATUSES.has("DRAFT")).toBe(false); expect(ACTIVE_STATUSES.has("CANCELLED")).toBe(false); expect(ACTIVE_STATUSES.has("COMPLETED")).toBe(false); }); }); // ─── createDateRange ────────────────────────────────────────────────────────── describe("createDateRange", () => { it("uses startDate and endDate when both provided", () => { const start = new Date("2026-04-10"); const end = new Date("2026-04-20"); const result = createDateRange({ startDate: start, endDate: end }); expect(result.startDate.toISOString()).toBe("2026-04-10T00:00:00.000Z"); expect(result.endDate.toISOString()).toBe("2026-04-20T00:00:00.000Z"); }); it("defaults endDate to startDate + 20 days when only startDate given (21-day duration)", () => { const start = new Date("2026-04-10"); const result = createDateRange({ startDate: start }); expect(result.startDate.toISOString()).toBe("2026-04-10T00:00:00.000Z"); expect(result.endDate.toISOString()).toBe("2026-04-30T00:00:00.000Z"); }); it("defaults startDate to today when not provided", () => { const todayIso = new Date().toISOString().slice(0, 10); const result = createDateRange({}); expect(result.startDate.toISOString().startsWith(todayIso)).toBe(true); }); it("applies custom durationDays so that endDate = startDate + durationDays - 1", () => { const start = new Date("2026-04-10"); const result = createDateRange({ startDate: start, durationDays: 10 }); expect(result.startDate.toISOString()).toBe("2026-04-10T00:00:00.000Z"); expect(result.endDate.toISOString()).toBe("2026-04-19T00:00:00.000Z"); }); it("throws when endDate is before startDate", () => { const start = new Date("2026-04-20"); const end = new Date("2026-04-10"); expect(() => createDateRange({ startDate: start, endDate: end })).toThrow( "endDate must be on or after startDate.", ); }); }); // ─── getBaseDayAvailability ─────────────────────────────────────────────────── describe("getBaseDayAvailability", () => { const availability = { monday: 8, tuesday: 8, wednesday: 8, thursday: 8, friday: 8, }; const date = new Date("2026-04-10"); it("calls getAvailabilityHoursForDate with the correct arguments", () => { getBaseDayAvailability(availability, date); expect(getAvailabilityHoursForDate).toHaveBeenCalledWith(availability, date); }); it("returns the value from the mock", () => { const result = getBaseDayAvailability(availability, date); expect(result).toBe(8); }); }); // ─── getEffectiveDayAvailability ────────────────────────────────────────────── describe("getEffectiveDayAvailability", () => { const availability = { monday: 8, tuesday: 8, wednesday: 8, thursday: 8, friday: 8, }; const date = new Date("2026-04-10"); const context = { absenceFractionsByDate: new Map(), holidayDates: new Set(), vacationFractionsByDate: new Map(), }; it("calls calculateEffectiveDayAvailability with the correct arguments", () => { getEffectiveDayAvailability(availability, date, context); expect(calculateEffectiveDayAvailability).toHaveBeenCalledWith({ availability, date, context, }); }); it("passes the context parameter through to the underlying function", () => { getEffectiveDayAvailability(availability, date, undefined); expect(calculateEffectiveDayAvailability).toHaveBeenCalledWith({ availability, date, context: undefined, }); }); }); // ─── createLocationLabel ────────────────────────────────────────────────────── describe("createLocationLabel", () => { it("returns all three parts joined with ' / ' when all values are present", () => { expect( createLocationLabel({ countryCode: "DE", federalState: "Bavaria", metroCityName: "Munich" }), ).toBe("DE / Bavaria / Munich"); }); it("returns only countryCode when federalState and metroCityName are absent", () => { expect(createLocationLabel({ countryCode: "DE" })).toBe("DE"); }); it("returns empty string when all parts are null or undefined", () => { expect( createLocationLabel({ countryCode: null, federalState: null, metroCityName: null }), ).toBe(""); expect(createLocationLabel({})).toBe(""); }); it("omits entries that consist entirely of whitespace", () => { expect( createLocationLabel({ countryCode: "DE", federalState: " ", metroCityName: "Munich" }), ).toBe("DE / Munich"); }); }); // ─── calculateAllocatedHoursForDay ──────────────────────────────────────────── describe("calculateAllocatedHoursForDay", () => { const date = new Date("2026-04-10T00:00:00.000Z"); const emptyContext = { absenceFractionsByDate: new Map(), holidayDates: new Set(), vacationFractionsByDate: new Map(), }; it("sums hoursPerDay for all active bookings that overlap the date", () => { const bookings = [ { startDate: new Date("2026-04-01"), endDate: new Date("2026-04-30"), hoursPerDay: 6, status: "CONFIRMED", }, { startDate: new Date("2026-04-01"), endDate: new Date("2026-04-30"), hoursPerDay: 2, status: "ACTIVE", }, ]; const result = calculateAllocatedHoursForDay({ bookings, date, context: emptyContext }); expect(result.allocatedHours).toBe(8); }); it("ignores bookings with non-active statuses such as DRAFT and CANCELLED", () => { const bookings = [ { startDate: new Date("2026-04-01"), endDate: new Date("2026-04-30"), hoursPerDay: 8, status: "DRAFT", }, { startDate: new Date("2026-04-01"), endDate: new Date("2026-04-30"), hoursPerDay: 4, status: "CANCELLED", }, { startDate: new Date("2026-04-01"), endDate: new Date("2026-04-30"), hoursPerDay: 3, status: "CONFIRMED", }, ]; const result = calculateAllocatedHoursForDay({ bookings, date, context: emptyContext }); expect(result.allocatedHours).toBe(3); }); it("ignores bookings whose date range does not overlap the target date", () => { const bookings = [ { startDate: new Date("2026-03-01"), endDate: new Date("2026-04-09"), hoursPerDay: 8, status: "ACTIVE", }, { startDate: new Date("2026-04-11"), endDate: new Date("2026-04-30"), hoursPerDay: 8, status: "ACTIVE", }, { startDate: new Date("2026-04-10"), endDate: new Date("2026-04-10"), hoursPerDay: 4, status: "CONFIRMED", }, ]; const result = calculateAllocatedHoursForDay({ bookings, date, context: emptyContext }); expect(result.allocatedHours).toBe(4); }); it("separates chargeable hours from non-chargeable hours", () => { const bookings = [ { startDate: new Date("2026-04-01"), endDate: new Date("2026-04-30"), hoursPerDay: 6, status: "CONFIRMED", isChargeable: true, }, { startDate: new Date("2026-04-01"), endDate: new Date("2026-04-30"), hoursPerDay: 2, status: "ACTIVE", isChargeable: false, }, ]; const result = calculateAllocatedHoursForDay({ bookings, date, context: emptyContext }); expect(result.allocatedHours).toBe(8); expect(result.chargeableHours).toBe(6); }); it("applies absenceFraction as dayFraction so that effective hours are scaled accordingly", () => { const context = { absenceFractionsByDate: new Map([["2026-04-10", 0.5]]), holidayDates: new Set(), vacationFractionsByDate: new Map(), }; const bookings = [ { startDate: new Date("2026-04-01"), endDate: new Date("2026-04-30"), hoursPerDay: 8, status: "CONFIRMED", isChargeable: true, }, ]; const result = calculateAllocatedHoursForDay({ bookings, date, context }); // dayFraction = 1 - 0.5 = 0.5 → 8 * 0.5 = 4 expect(result.allocatedHours).toBe(4); expect(result.chargeableHours).toBe(4); }); });