Files
CapaKraken/packages/api/src/__tests__/staffing-shared.test.ts
T
Hartmut 2f2fe2631f test(api): add 38 tests for project read, project cost, and staffing shared utils
Project identifier: 4-step fallback lookup, search summaries with fuzzy notes.
Project cost: pagination, cost/person-day calculations, utilization percent.
Staffing shared: createDateRange, ACTIVE_STATUSES, createLocationLabel,
calculateAllocatedHoursForDay with absence fractions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-10 16:49:23 +02:00

285 lines
9.7 KiB
TypeScript

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<string, number>(),
holidayDates: new Set<string>(),
vacationFractionsByDate: new Map<string, number>(),
};
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<string, number>(),
holidayDates: new Set<string>(),
vacationFractionsByDate: new Map<string, number>(),
};
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<string>(),
vacationFractionsByDate: new Map<string, number>(),
};
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);
});
});