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; }; }; type ResourceGraphDetail = { resource: { id: string; eid: string; displayName: string }; availableDomains: string[]; requestedDomain: string | null; totalNodeCount: number; totalLinkCount: number; selectedNodeCount: number; selectedLinkCount: number; nodes: Array<{ id: string; domain: string }>; }; type ProjectGraphDetail = { project: { id: string; shortCode: string; name: string }; availableDomains: string[]; requestedDomain: string | null; totalNodeCount: number; selectedNodeCount: number; selectedLinkCount: number; nodes: Array<{ id: string; domain: string }>; links?: Array<{ source: string; target: string }>; meta: { projectName: string; projectCode: string; }; }; function createControllerCaller(db: Record) { 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) { 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 = {}) { 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, }; } function createProjectDb(projectFindImpl: ReturnType) { return { project: { findUniqueOrThrow: projectFindImpl, }, estimate: { findFirst: vi.fn().mockResolvedValue(null), }, assignment: { findMany: vi.fn().mockResolvedValue([ { status: "CONFIRMED", dailyCostCents: 4_000, startDate: new Date("2026-01-05T00:00:00.000Z"), endDate: new Date("2026-01-30T00:00:00.000Z"), hoursPerDay: 4, }, ]), }, effortRule: { count: vi.fn().mockResolvedValue(0), }, experienceMultiplierRule: { count: vi.fn().mockResolvedValue(0), }, }; } function buildProject(overrides: Record = {}) { return { id: "project_1", name: "Gelddruckmaschine", shortCode: "GDM", budgetCents: 100_000, winProbability: 75, startDate: new Date("2026-01-05T00:00:00.000Z"), endDate: new Date("2026-02-28T00:00:00.000Z"), ...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" }), ])); }); it("returns a filtered resource detail graph with canonical selection metadata", 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.getResourceDataDetail({ resourceId: "resource_augsburg", month: "2026-08", domain: "SAH", }) as ResourceGraphDetail; expect(result.resource).toEqual({ id: "resource_augsburg", eid: "bruce.banner", displayName: "Bruce Banner", }); expect(result.availableDomains).toEqual(expect.arrayContaining(["INPUT", "SAH", "ALLOCATION", "CHARGEABILITY"])); expect(result.requestedDomain).toBe("SAH"); expect(result.totalNodeCount).toBeGreaterThan(result.selectedNodeCount); expect(result.totalLinkCount).toBeGreaterThan(0); expect(result.selectedNodeCount).toBeGreaterThan(0); expect(result.selectedLinkCount).toBe(0); expect(result.nodes.every((node) => node.domain === "SAH")).toBe(true); }); it("returns a filtered project detail graph with canonical project identity", async () => { const db = createProjectDb(vi.fn().mockResolvedValue(buildProject())); const caller = createControllerCaller(db); const result = await caller.getProjectDataDetail({ projectId: "project_1", domain: "BUDGET", includeLinks: true, }) as ProjectGraphDetail; expect(result.project).toEqual({ id: "project_1", shortCode: "GDM", name: "Gelddruckmaschine", }); expect(result.meta).toEqual({ projectName: "Gelddruckmaschine", projectCode: "GDM", }); expect(result.availableDomains).toEqual(expect.arrayContaining(["INPUT", "BUDGET"])); expect(result.requestedDomain).toBe("BUDGET"); expect(result.totalNodeCount).toBeGreaterThan(result.selectedNodeCount); expect(result.selectedNodeCount).toBeGreaterThan(0); expect(result.selectedLinkCount).toBeGreaterThan(0); expect(result.nodes.every((node) => node.domain === "BUDGET")).toBe(true); expect(result.links?.length).toBe(result.selectedLinkCount); }); });