feat(planning): ship holiday-aware planning and assistant upgrades

This commit is contained in:
2026-03-28 22:49:28 +01:00
parent 2a005794e7
commit 4f48afe7b4
151 changed files with 17738 additions and 1940 deletions
@@ -0,0 +1,195 @@
import { SystemRole } from "@capakraken/shared";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { computationGraphRouter } from "../router/computation-graph.js";
import { createCallerFactory } from "../trpc.js";
const createCaller = createCallerFactory(computationGraphRouter);
type ResourceGraphMeta = {
countryCode: string | null;
countryName: string | null;
federalState: string | null;
metroCityName: string | null;
resolvedHolidays: Array<{
date: string;
name: string;
scope: "COUNTRY" | "STATE" | "CITY";
calendarName: string | null;
}>;
factors: {
baseAvailableHours: number;
effectiveAvailableHours: number;
publicHolidayCount: number;
publicHolidayWorkdayCount: number;
publicHolidayHoursDeduction: number;
absenceDayCount: number;
absenceHoursDeduction: number;
};
};
function createControllerCaller(db: Record<string, unknown>) {
return createCaller({
session: {
user: { email: "controller@example.com", name: "Controller", image: null },
expires: "2026-03-14T00:00:00.000Z",
},
db: db as never,
dbUser: {
id: "user_controller",
systemRole: SystemRole.CONTROLLER,
permissionOverrides: null,
},
});
}
function createDb(resourceFindImpl: ReturnType<typeof vi.fn>) {
return {
resource: {
findUniqueOrThrow: resourceFindImpl,
},
assignment: {
findMany: vi.fn().mockResolvedValue([]),
},
vacation: {
findMany: vi.fn().mockResolvedValue([]),
},
holidayCalendar: {
findMany: vi.fn().mockResolvedValue([]),
},
calculationRule: {
findMany: vi.fn().mockResolvedValue([]),
},
};
}
function buildResource(overrides: Record<string, unknown> = {}) {
return {
id: "resource_1",
displayName: "Bruce Banner",
eid: "bruce.banner",
fte: 1,
lcrCents: 5_000,
chargeabilityTarget: 80,
countryId: "country_de",
federalState: "BY",
metroCityId: null,
availability: {
monday: 8,
tuesday: 8,
wednesday: 8,
thursday: 8,
friday: 8,
saturday: 0,
sunday: 0,
},
country: {
id: "country_de",
code: "DE",
name: "Deutschland",
dailyWorkingHours: 8,
scheduleRules: null,
},
metroCity: null,
managementLevelGroup: {
id: "mlg_1",
name: "Senior",
targetPercentage: 0.8,
},
...overrides,
};
}
describe("computation graph router", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("exposes location context and city-local holidays in the resource graph", async () => {
const db = createDb(vi.fn().mockResolvedValue(buildResource({
id: "resource_augsburg",
metroCityId: "city_augsburg",
metroCity: { id: "city_augsburg", name: "Augsburg" },
})));
const caller = createControllerCaller(db);
const result = await caller.getResourceData({
resourceId: "resource_augsburg",
month: "2026-08",
});
const meta = result.meta as ResourceGraphMeta;
const nodeIds = result.nodes.map((node) => node.id);
const holidayExamples = result.nodes.find((node) => node.id === "input.holidayExamples");
expect(new Set(nodeIds).size).toBe(nodeIds.length);
expect(nodeIds).toEqual(expect.arrayContaining([
"input.country",
"input.state",
"input.city",
"input.holidayContext",
"input.holidayExamples",
"sah.baseHours",
"sah.publicHolidayHours",
"sah.absenceHours",
]));
expect(meta).toMatchObject({
countryCode: "DE",
countryName: "Deutschland",
federalState: "BY",
metroCityName: "Augsburg",
});
expect(meta.resolvedHolidays).toEqual(expect.arrayContaining([
expect.objectContaining({
date: "2026-08-08",
name: "Augsburger Friedensfest",
scope: "CITY",
}),
]));
expect(meta.factors.publicHolidayCount).toBeGreaterThan(0);
expect(meta.factors.publicHolidayWorkdayCount).toBe(0);
expect(holidayExamples?.value).toEqual(expect.stringContaining("Augsburger Friedensfest"));
});
it("derives different effective SAH values for Bavaria and Hamburg", async () => {
const db = createDb(vi.fn()
.mockResolvedValueOnce(buildResource({
id: "resource_by",
federalState: "BY",
managementLevelGroup: null,
}))
.mockResolvedValueOnce(buildResource({
id: "resource_hh",
federalState: "HH",
managementLevelGroup: null,
})));
const caller = createControllerCaller(db);
const bavaria = await caller.getResourceData({
resourceId: "resource_by",
month: "2026-01",
});
const hamburg = await caller.getResourceData({
resourceId: "resource_hh",
month: "2026-01",
});
const bavariaMeta = bavaria.meta as ResourceGraphMeta;
const hamburgMeta = hamburg.meta as ResourceGraphMeta;
expect(bavariaMeta.federalState).toBe("BY");
expect(hamburgMeta.federalState).toBe("HH");
expect(bavariaMeta.factors.baseAvailableHours).toBe(176);
expect(hamburgMeta.factors.baseAvailableHours).toBe(176);
expect(bavariaMeta.factors.effectiveAvailableHours).toBe(160);
expect(hamburgMeta.factors.effectiveAvailableHours).toBe(168);
expect(bavariaMeta.factors.publicHolidayWorkdayCount).toBe(2);
expect(hamburgMeta.factors.publicHolidayWorkdayCount).toBe(1);
expect(bavariaMeta.factors.publicHolidayHoursDeduction).toBe(16);
expect(hamburgMeta.factors.publicHolidayHoursDeduction).toBe(8);
expect(bavariaMeta.resolvedHolidays).toEqual(expect.arrayContaining([
expect.objectContaining({ name: "Heilige Drei Könige", date: "2026-01-06", scope: "STATE" }),
]));
expect(hamburgMeta.resolvedHolidays).not.toEqual(expect.arrayContaining([
expect.objectContaining({ name: "Heilige Drei Könige", date: "2026-01-06" }),
]));
});
});