319 lines
9.7 KiB
TypeScript
319 lines
9.7 KiB
TypeScript
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<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,
|
|
};
|
|
}
|
|
|
|
function createProjectDb(projectFindImpl: ReturnType<typeof vi.fn>) {
|
|
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<string, unknown> = {}) {
|
|
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);
|
|
});
|
|
});
|