import { SystemRole } from "@capakraken/shared"; import { beforeEach, describe, expect, it, vi } from "vitest"; vi.mock("@capakraken/application", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, isChargeabilityActualBooking: actual.isChargeabilityActualBooking, listAssignmentBookings: vi.fn(), }; }); import { listAssignmentBookings } from "@capakraken/application"; import { chargeabilityReportRouter } from "../router/chargeability-report.js"; import { createCallerFactory } from "../trpc.js"; const createCaller = createCallerFactory(chargeabilityReportRouter); 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, }, }); } describe("chargeability report router", () => { beforeEach(() => { vi.clearAllMocks(); }); it("excludes proposed bookings by default but includes them when requested", async () => { const db = { resource: { findMany: vi.fn().mockResolvedValue([ { id: "resource_1", eid: "E-001", displayName: "Alice", fte: 1, availability: { monday: 8, tuesday: 8, wednesday: 8, thursday: 8, friday: 8 }, countryId: "country_es", federalState: null, metroCityId: "city_1", chargeabilityTarget: 80, country: { id: "country_es", code: "ES", dailyWorkingHours: 8, scheduleRules: null, }, orgUnit: { id: "org_1", name: "CGI" }, managementLevelGroup: { id: "mgmt_1", name: "Senior", targetPercentage: 0.8 }, managementLevel: { id: "level_1", name: "L7" }, metroCity: { id: "city_1", name: "Barcelona" }, }, ]), }, project: { findMany: vi.fn().mockResolvedValue([ { id: "project_confirmed", utilizationCategory: { code: "Chg" } }, { id: "project_proposed", utilizationCategory: { code: "Chg" } }, ]), }, vacation: { findMany: vi.fn().mockResolvedValue([]), }, }; vi.mocked(listAssignmentBookings).mockResolvedValue([ { id: "assignment_confirmed", projectId: "project_confirmed", resourceId: "resource_1", startDate: new Date("2026-03-02T00:00:00.000Z"), endDate: new Date("2026-03-06T00:00:00.000Z"), hoursPerDay: 4, dailyCostCents: 0, status: "CONFIRMED", project: { id: "project_confirmed", name: "Confirmed Project", shortCode: "CP", status: "ACTIVE", orderType: "CLIENT", dynamicFields: null, }, resource: { id: "resource_1", displayName: "Alice", chapter: "CGI" }, }, { id: "assignment_proposed", projectId: "project_proposed", resourceId: "resource_1", startDate: new Date("2026-03-02T00:00:00.000Z"), endDate: new Date("2026-03-06T00:00:00.000Z"), hoursPerDay: 4, dailyCostCents: 0, status: "PROPOSED", project: { id: "project_proposed", name: "Proposed Project", shortCode: "PP", status: "ACTIVE", orderType: "CLIENT", dynamicFields: null, }, resource: { id: "resource_1", displayName: "Alice", chapter: "CGI" }, }, ]); const caller = createControllerCaller(db); const strict = await caller.getReport({ startMonth: "2026-03", endMonth: "2026-03", }); const withProposed = await caller.getReport({ startMonth: "2026-03", endMonth: "2026-03", includeProposed: true, }); const strictMonth = strict.resources[0]?.months[0]; const proposedMonth = withProposed.resources[0]?.months[0]; expect(strictMonth).toBeDefined(); expect(proposedMonth).toBeDefined(); expect(strictMonth?.chg).toBeGreaterThan(0); expect(proposedMonth?.chg).toBeGreaterThan(strictMonth?.chg ?? 0); expect(proposedMonth?.chg).toBeCloseTo((strictMonth?.chg ?? 0) * 2, 5); expect(withProposed.groupTotals[0]?.chg).toBeGreaterThan(strict.groupTotals[0]?.chg ?? 0); }); it("includes imported TBD draft work only when proposed bookings are enabled", async () => { const db = { resource: { findMany: vi.fn().mockResolvedValue([ { id: "resource_1", eid: "E-001", displayName: "Alice", fte: 1, availability: { monday: 8, tuesday: 8, wednesday: 8, thursday: 8, friday: 8 }, countryId: "country_es", federalState: null, metroCityId: "city_1", chargeabilityTarget: 80, country: { id: "country_es", code: "ES", dailyWorkingHours: 8, scheduleRules: null, }, orgUnit: { id: "org_1", name: "CGI" }, managementLevelGroup: { id: "mgmt_1", name: "Senior", targetPercentage: 0.8 }, managementLevel: { id: "level_1", name: "L7" }, metroCity: { id: "city_1", name: "Barcelona" }, }, ]), }, project: { findMany: vi.fn().mockResolvedValue([ { id: "project_tbd", utilizationCategory: { code: "Chg" } }, ]), }, vacation: { findMany: vi.fn().mockResolvedValue([]), }, }; vi.mocked(listAssignmentBookings).mockResolvedValue([ { id: "assignment_tbd", projectId: "project_tbd", resourceId: "resource_1", startDate: new Date("2026-03-02T00:00:00.000Z"), endDate: new Date("2026-03-06T00:00:00.000Z"), hoursPerDay: 4, dailyCostCents: 0, status: "PROPOSED", project: { id: "project_tbd", name: "TBD Project", shortCode: "TBD-P1", status: "DRAFT", orderType: "CLIENT", dynamicFields: { dispoImport: { isTbd: true } }, }, resource: { id: "resource_1", displayName: "Alice", chapter: "CGI" }, }, ]); const caller = createControllerCaller(db); const strict = await caller.getReport({ startMonth: "2026-03", endMonth: "2026-03", }); const withProposed = await caller.getReport({ startMonth: "2026-03", endMonth: "2026-03", includeProposed: true, }); expect(strict.resources[0]?.months[0]?.chg).toBe(0); expect(withProposed.resources[0]?.months[0]?.chg).toBeGreaterThan(0); expect(withProposed.groupTotals[0]?.chg).toBeGreaterThan(strict.groupTotals[0]?.chg ?? 0); }); it("reduces SAH for German public holidays based on the calendar", async () => { const db = { resource: { findMany: vi.fn().mockResolvedValue([ { id: "resource_de", eid: "E-001", displayName: "Alice", fte: 1, availability: { monday: 8, tuesday: 8, wednesday: 8, thursday: 8, friday: 8 }, countryId: "country_de", federalState: null, metroCityId: "city_1", chargeabilityTarget: 80, country: { id: "country_de", code: "DE", dailyWorkingHours: 8, scheduleRules: null, }, orgUnit: { id: "org_1", name: "CGI" }, managementLevelGroup: { id: "mgmt_1", name: "Senior", targetPercentage: 0.8 }, managementLevel: { id: "level_1", name: "L7" }, metroCity: { id: "city_1", name: "Munich" }, }, ]), }, project: { findMany: vi.fn().mockResolvedValue([ { id: "project_full_month", utilizationCategory: { code: "Chg" } }, ]), }, vacation: { findMany: vi.fn().mockResolvedValue([]), }, }; vi.mocked(listAssignmentBookings).mockResolvedValue([ { id: "assignment_full_month", projectId: "project_full_month", resourceId: "resource_de", startDate: new Date("2026-01-01T00:00:00.000Z"), endDate: new Date("2026-01-31T00:00:00.000Z"), hoursPerDay: 7, dailyCostCents: 0, status: "CONFIRMED", project: { id: "project_full_month", name: "Full Month Project", shortCode: "FMP", status: "ACTIVE", orderType: "CLIENT", dynamicFields: null, }, resource: { id: "resource_de", displayName: "Alice", chapter: "CGI" }, }, ]); const caller = createControllerCaller(db); const report = await caller.getReport({ startMonth: "2026-01", endMonth: "2026-01", }); const month = report.resources[0]?.months[0]; expect(month).toBeDefined(); expect(month?.sah).toBe(168); expect(month?.chg).toBeCloseTo(0.875, 5); }); it("applies city-specific public holidays to SAH", async () => { const db = { resource: { findMany: vi.fn().mockResolvedValue([ { id: "resource_augsburg", eid: "E-001", displayName: "Alice", fte: 1, availability: { monday: 8, tuesday: 8, wednesday: 8, thursday: 8, friday: 8 }, countryId: "country_de", federalState: "BY", metroCityId: "city_1", chargeabilityTarget: 80, country: { id: "country_de", code: "DE", dailyWorkingHours: 8, scheduleRules: null, }, orgUnit: { id: "org_1", name: "CGI" }, managementLevelGroup: { id: "mgmt_1", name: "Senior", targetPercentage: 0.8 }, managementLevel: { id: "level_1", name: "L7" }, metroCity: { id: "city_1", name: "Augsburg" }, }, { id: "resource_munich", eid: "E-002", displayName: "Bob", fte: 1, availability: { monday: 8, tuesday: 8, wednesday: 8, thursday: 8, friday: 8 }, countryId: "country_de", federalState: "BY", metroCityId: "city_2", chargeabilityTarget: 80, country: { id: "country_de", code: "DE", dailyWorkingHours: 8, scheduleRules: null, }, orgUnit: { id: "org_1", name: "CGI" }, managementLevelGroup: { id: "mgmt_1", name: "Senior", targetPercentage: 0.8 }, managementLevel: { id: "level_1", name: "L7" }, metroCity: { id: "city_2", name: "Munich" }, }, ]), }, project: { findMany: vi.fn().mockResolvedValue([]), }, vacation: { findMany: vi.fn().mockResolvedValue([]), }, }; vi.mocked(listAssignmentBookings).mockResolvedValue([]); const caller = createControllerCaller(db); const report = await caller.getReport({ startMonth: "2028-08", endMonth: "2028-08", }); const augsburg = report.resources.find((resource) => resource.city === "Augsburg"); const munich = report.resources.find((resource) => resource.city === "Munich"); expect(augsburg?.months[0]?.sah).toBe((munich?.months[0]?.sah ?? 0) - 8); }); it("respects individual weekday availability when computing booked hours", async () => { const db = { resource: { findMany: vi.fn().mockResolvedValue([ { id: "resource_pt", eid: "E-003", displayName: "Carla", fte: 1, availability: { monday: 8, tuesday: 8, wednesday: 8, thursday: 8, friday: 0 }, countryId: "country_de", federalState: null, metroCityId: "city_3", chargeabilityTarget: 80, country: { id: "country_de", code: "DE", dailyWorkingHours: 8, scheduleRules: null, }, orgUnit: { id: "org_1", name: "CGI" }, managementLevelGroup: { id: "mgmt_1", name: "Senior", targetPercentage: 0.8 }, managementLevel: { id: "level_1", name: "L7" }, metroCity: { id: "city_3", name: "Berlin" }, }, ]), }, project: { findMany: vi.fn().mockResolvedValue([ { id: "project_week", utilizationCategory: { code: "Chg" } }, ]), }, vacation: { findMany: vi.fn().mockResolvedValue([]), }, }; vi.mocked(listAssignmentBookings).mockResolvedValue([ { id: "assignment_week", projectId: "project_week", resourceId: "resource_pt", startDate: new Date("2026-03-02T00:00:00.000Z"), endDate: new Date("2026-03-06T00:00:00.000Z"), hoursPerDay: 4, dailyCostCents: 0, status: "CONFIRMED", project: { id: "project_week", name: "Week Project", shortCode: "WP", status: "ACTIVE", orderType: "CLIENT", dynamicFields: null, }, resource: { id: "resource_pt", displayName: "Carla", chapter: "CGI" }, }, ]); const caller = createControllerCaller(db); const report = await caller.getReport({ startMonth: "2026-03", endMonth: "2026-03", }); const month = report.resources[0]?.months[0]; expect(month).toBeDefined(); expect(month?.chg).toBeCloseTo(16 / 144, 5); }); it("returns a filtered detailed report with rounded percentages", async () => { const db = { resource: { findMany: vi.fn().mockResolvedValue([ { id: "resource_1", eid: "E-001", displayName: "Alice", fte: 1, availability: { monday: 8, tuesday: 8, wednesday: 8, thursday: 8, friday: 8 }, countryId: "country_es", federalState: null, metroCityId: "city_1", chargeabilityTarget: 80, country: { id: "country_es", code: "ES", dailyWorkingHours: 8, scheduleRules: null, }, orgUnit: { id: "org_1", name: "CGI" }, managementLevelGroup: { id: "mgmt_1", name: "Senior", targetPercentage: 0.8 }, managementLevel: { id: "level_1", name: "L7" }, metroCity: { id: "city_1", name: "Barcelona" }, }, ]), }, project: { findMany: vi.fn().mockResolvedValue([ { id: "project_confirmed", utilizationCategory: { code: "Chg" } }, ]), }, vacation: { findMany: vi.fn().mockResolvedValue([]), }, }; vi.mocked(listAssignmentBookings).mockResolvedValue([ { id: "assignment_confirmed", projectId: "project_confirmed", resourceId: "resource_1", startDate: new Date("2026-03-02T00:00:00.000Z"), endDate: new Date("2026-03-06T00:00:00.000Z"), hoursPerDay: 4, dailyCostCents: 0, status: "CONFIRMED", project: { id: "project_confirmed", name: "Confirmed Project", shortCode: "CP", status: "ACTIVE", orderType: "CLIENT", dynamicFields: null, }, resource: { id: "resource_1", displayName: "Alice", chapter: "CGI" }, }, ]); const caller = createControllerCaller(db); const result = await caller.getDetail({ startMonth: "2026-03", endMonth: "2026-03", resourceQuery: "ali", resourceLimit: 10, }); expect(result.filters).toEqual({ startMonth: "2026-03", endMonth: "2026-03", orgUnitId: null, managementLevelGroupId: null, countryId: null, includeProposed: false, resourceQuery: "ali", }); expect(result.groupTotals).toEqual([ expect.objectContaining({ monthKey: "2026-03", totalFte: 1, chargeabilityPct: expect.any(Number), targetPct: 80, }), ]); expect(result.resourceCount).toBe(1); expect(result.returnedResourceCount).toBe(1); expect(result.truncated).toBe(false); expect(result.resources).toEqual([ expect.objectContaining({ displayName: "Alice", targetPct: 80, country: "ES", city: "Barcelona", managementLevelGroup: "Senior", managementLevel: "L7", months: [ expect.objectContaining({ monthKey: "2026-03", sah: expect.any(Number), chargeabilityPct: expect.any(Number), gapPct: expect.any(Number), }), ], }), ]); }); });