import { beforeEach, describe, expect, it, vi } from "vitest"; import { PermissionKey } from "@capakraken/shared"; import { listAssignmentBookings } from "@capakraken/application"; vi.mock("@capakraken/application", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, approveEstimateVersion: vi.fn(), cloneEstimate: vi.fn(), commitDispoImportBatch: vi.fn(), countPlanningEntries: vi.fn().mockResolvedValue({ countsByRoleId: new Map() }), createEstimateExport: vi.fn(), createEstimatePlanningHandoff: vi.fn(), createEstimateRevision: vi.fn(), assessDispoImportReadiness: vi.fn(), loadResourceDailyAvailabilityContexts: vi.fn().mockResolvedValue(new Map()), getDashboardDemand: vi.fn().mockResolvedValue([]), getDashboardBudgetForecast: vi.fn().mockResolvedValue([]), getDashboardOverview: vi.fn(), getDashboardSkillGapSummary: vi.fn().mockResolvedValue({ roleGaps: [], totalOpenPositions: 0, skillSupplyTop10: [], resourcesByRole: [], }), getDashboardProjectHealth: vi.fn().mockResolvedValue([]), getDashboardPeakTimes: vi.fn().mockResolvedValue([]), getDashboardTopValueResources: vi.fn().mockResolvedValue([]), getEstimateById: vi.fn(), listAssignmentBookings: vi.fn().mockResolvedValue([]), stageDispoImportBatch: vi.fn(), submitEstimateVersion: vi.fn(), updateEstimateDraft: vi.fn(), }; }); import { executeTool } from "../router/assistant-tools.js"; import { createToolContext } from "./assistant-tools-report-test-helpers.js"; describe("assistant chargeability report tools", () => { beforeEach(() => { vi.clearAllMocks(); }); it("returns the chargeability report readmodel through the assistant", async () => { 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 ctx = createToolContext( { 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([]), }, holidayCalendar: { findMany: vi.fn().mockResolvedValue([]), }, }, { permissions: [PermissionKey.VIEW_COSTS, PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS], }, ); const result = await executeTool( "get_chargeability_report", JSON.stringify({ startMonth: "2026-03", endMonth: "2026-03", resourceLimit: 10, }), ctx, ); const parsed = JSON.parse(result.content) as { monthKeys: string[]; groupTotals: Array<{ monthKey: string; chargeabilityPct: number; targetPct: number }>; resourceCount: number; returnedResourceCount: number; truncated: boolean; explainability: { locationFields: string[]; monthDerivationFields: string[]; activeFilters: string[]; formulas: { sah: string; chargeabilityPct: string; targetHours: string; gapHours: string; }; notes: string[]; }; resources: Array<{ displayName: string; targetPct: number; months: Array<{ monthKey: string; sah: number; chargeabilityPct: number }>; }>; }; expect(parsed.monthKeys).toEqual(["2026-03"]); expect(parsed.groupTotals).toEqual([ expect.objectContaining({ monthKey: "2026-03", chargeabilityPct: expect.any(Number), targetPct: 80, }), ]); expect(parsed.resourceCount).toBe(1); expect(parsed.returnedResourceCount).toBe(1); expect(parsed.truncated).toBe(false); expect(parsed.explainability).toEqual({ locationFields: [ "country", "federalState", "city", "orgUnit", "managementLevelGroup", "managementLevel", ], monthDerivationFields: [ "baseAvailableHours", "publicHolidayCount", "publicHolidayWorkdayCount", "publicHolidayHoursDeduction", "absenceDayEquivalent", "absenceHoursDeduction", "effectiveAvailableHours", ], activeFilters: [], formulas: { sah: "baseAvailableHours - publicHolidayHoursDeduction - absenceHoursDeduction = effectiveAvailableHours", chargeabilityPct: "chargeabilityHours / sahHours", targetHours: "sahHours * targetPct", gapHours: "chargeabilityHours - targetHours", }, notes: [ "Location fields explain why two resources can have different SAH in the same month because country, federal state, and city holidays may differ.", "Holiday deductions and absence deductions are tracked separately; absence does not deduct days that are already public holidays.", "Include proposed work changes chargeability ratios and hours, but it does not change holiday or absence-based SAH derivation.", ], }); expect(parsed.resources).toEqual([ expect.objectContaining({ displayName: "Alice", targetPct: 80, months: [ expect.objectContaining({ monthKey: "2026-03", sah: expect.any(Number), chargeabilityPct: expect.any(Number), }), ], }), ]); }); });