import { SystemRole } from "@capakraken/shared"; import { beforeEach, describe, expect, it, vi } from "vitest"; vi.mock("@capakraken/application", () => ({ isChargeabilityActualBooking: vi.fn(() => false), isChargeabilityRelevantProject: vi.fn(() => false), listAssignmentBookings: vi.fn().mockResolvedValue([]), })); vi.mock("../lib/resource-capacity.js", () => ({ calculateEffectiveAvailableHours: vi.fn(({ context }: { context?: unknown }) => (context ? 156 : 168)), calculateEffectiveBookedHours: vi.fn(() => 0), countEffectiveWorkingDays: vi.fn(({ context }: { context?: unknown }) => (context ? 19.5 : 21)), getAvailabilityHoursForDate: vi.fn(() => 8), loadResourceDailyAvailabilityContexts: vi.fn().mockResolvedValue(new Map([ [ "res_1", { holidayDates: new Set(["2026-04-10"]), vacationFractionsByDate: new Map([["2026-04-14", 0.5]]), }, ], ])), })); import { reportRouter } from "../router/report.js"; import { createCallerFactory } from "../trpc.js"; const createCaller = createCallerFactory(reportRouter); function createControllerCaller(db: Record) { return createCaller({ session: { user: { email: "controller@example.com", name: "Controller", image: null }, expires: "2099-01-01T00:00:00.000Z", }, db: db as never, dbUser: { id: "user_controller", systemRole: SystemRole.CONTROLLER, permissionOverrides: null, }, }); } describe("report router", () => { beforeEach(() => { vi.clearAllMocks(); }); it("lists the new resource month transparency columns", async () => { const caller = createControllerCaller({}); const columns = await caller.getAvailableColumns({ entity: "resource_month" }); expect(columns).toEqual(expect.arrayContaining([ expect.objectContaining({ key: "monthlyPublicHolidayCount", label: "Holiday Dates" }), expect.objectContaining({ key: "monthlyTargetHours", label: "Target Hours" }), expect.objectContaining({ key: "monthlyUnassignedHours", label: "Unassigned Hours" }), ])); }); it("exports resource month basis and computed columns in CSV", async () => { const db = { resource: { findMany: vi.fn().mockResolvedValue([ { id: "res_1", eid: "alice", displayName: "Alice", email: "alice@example.com", chapter: "VFX", resourceType: "EMPLOYEE", isActive: true, chgResponsibility: false, rolledOff: false, departed: false, lcrCents: 7500, ucrCents: 10000, currency: "EUR", fte: 1, availability: { monday: 8, tuesday: 8, wednesday: 8, thursday: 8, friday: 8 }, chargeabilityTarget: 80, federalState: "BY", countryId: "country_de", metroCityId: null, country: { code: "DE", name: "Germany" }, metroCity: null, orgUnit: { name: "Delivery" }, managementLevelGroup: null, managementLevel: { name: "Senior" }, }, ]), }, }; const caller = createControllerCaller(db); const result = await caller.exportReport({ entity: "resource_month", columns: [ "displayName", "countryCode", "monthlyPublicHolidayCount", "monthlyPublicHolidayHoursDeduction", "monthlyAbsenceHoursDeduction", "monthlySahHours", "monthlyTargetHours", "monthlyUnassignedHours", ], filters: [], periodMonth: "2026-04", limit: 100, }); expect(result.rowCount).toBe(1); expect(result.csv).toContain("Name,Country Code,Holiday Dates,Holiday Hours Deduction,Absence Hours Deduction,SAH,Target Hours,Unassigned Hours"); expect(result.csv).toContain("Alice,DE,1,8,4,156,124.8,156"); }); it("rejects invalid resource_month period months instead of silently normalizing them", async () => { const caller = createControllerCaller({}); await expect(caller.getReportData({ entity: "resource_month", columns: ["displayName"], filters: [], periodMonth: "2026-13", limit: 10, offset: 0, })).rejects.toMatchObject({ code: "BAD_REQUEST", message: expect.stringContaining("Invalid"), }); }); it("rejects unknown columns instead of silently dropping them", async () => { const caller = createControllerCaller({ resource: { findMany: vi.fn(), count: vi.fn(), }, }); await expect(caller.getReportData({ entity: "resource", columns: ["displayName", "unknownColumn"], filters: [], limit: 10, offset: 0, })).rejects.toMatchObject({ code: "BAD_REQUEST", message: expect.stringContaining("unknownColumn"), }); }); it("rejects unsupported relation filters instead of silently ignoring them", async () => { const caller = createControllerCaller({ assignment: { findMany: vi.fn(), count: vi.fn(), }, }); await expect(caller.getReportData({ entity: "assignment", columns: ["id", "resource.displayName"], filters: [{ field: "resource.displayName", op: "contains", value: "Alice" }], limit: 10, offset: 0, })).rejects.toMatchObject({ code: "BAD_REQUEST", message: expect.stringContaining("resource.displayName"), }); }); it("rejects invalid numeric filter values instead of silently dropping them", async () => { const caller = createControllerCaller({ resource: { findMany: vi.fn(), count: vi.fn(), }, }); await expect(caller.getReportData({ entity: "resource", columns: ["displayName"], filters: [{ field: "lcrCents", op: "gte", value: "not-a-number" }], limit: 10, offset: 0, })).rejects.toMatchObject({ code: "BAD_REQUEST", message: expect.stringContaining("lcrCents"), }); }); });