import { SystemRole } from "@capakraken/shared"; import { describe, expect, it, vi } from "vitest"; import { entitlementRouter } from "../router/entitlement.js"; import { createCallerFactory } from "../trpc.js"; // Mock @capakraken/db to provide the enums used in the router vi.mock("@capakraken/db", () => ({ VacationType: { ANNUAL: "ANNUAL", SICK: "SICK", OTHER: "OTHER", PUBLIC_HOLIDAY: "PUBLIC_HOLIDAY" }, VacationStatus: { APPROVED: "APPROVED", PENDING: "PENDING", REJECTED: "REJECTED" }, })); const createCaller = createCallerFactory(entitlementRouter); // ── Caller factories ───────────────────────────────────────────────────────── /** Injects a default resource ownership mock so the ownership check in getBalance passes */ function createProtectedCaller(db: Record) { const withResourceOwnership = { resource: { findUnique: vi.fn().mockImplementation(async (args?: { select?: Record }) => { const select = args?.select ?? {}; return { ...(select.userId ? { userId: "user_1" } : {}), ...(select.federalState ? { federalState: "BY" } : {}), ...(select.country ? { country: { code: "DE" } } : {}), ...(select.metroCity ? { metroCity: null } : {}), }; }), }, ...db, }; return createCaller({ session: { user: { email: "user@example.com", name: "User", image: null }, expires: "2099-01-01T00:00:00.000Z", }, db: withResourceOwnership as never, dbUser: { id: "user_1", systemRole: SystemRole.USER, permissionOverrides: null, }, }); } function createManagerCaller(db: Record) { return createCaller({ session: { user: { email: "mgr@example.com", name: "Manager", image: null }, expires: "2099-01-01T00:00:00.000Z", }, db: db as never, dbUser: { id: "user_mgr", systemRole: SystemRole.MANAGER, permissionOverrides: null, }, }); } function createAdminCaller(db: Record) { return createCaller({ session: { user: { email: "admin@example.com", name: "Admin", image: null }, expires: "2099-01-01T00:00:00.000Z", }, db: db as never, dbUser: { id: "user_admin", systemRole: SystemRole.ADMIN, permissionOverrides: null, }, }); } // ── Sample data ────────────────────────────────────────────────────────────── function sampleEntitlement(overrides: Record = {}) { return { id: "ent_1", resourceId: "res_1", year: 2026, entitledDays: 30, carryoverDays: 2, usedDays: 5, pendingDays: 3, createdAt: new Date(), updatedAt: new Date(), ...overrides, }; } function mockEntitlementFindUniqueByYear( entitlementsByYear: Record | null>, ) { return vi.fn().mockImplementation(async ({ where }: { where: { resourceId_year: { year: number } } }) => ( entitlementsByYear[where.resourceId_year.year] ?? null )); } // ─── getBalance ────────────────────────────────────────────────────────────── describe("entitlement.getBalance", () => { it("returns vacation balance for a resource and year", async () => { const entitlement = sampleEntitlement(); const db = { systemSettings: { findUnique: vi.fn().mockResolvedValue({ id: "singleton", vacationDefaultDays: 28 }), }, vacationEntitlement: { findUnique: mockEntitlementFindUniqueByYear({ 2026: entitlement }), update: vi.fn().mockResolvedValue(entitlement), }, vacation: { findMany: vi.fn().mockResolvedValue([]), }, }; const caller = createProtectedCaller(db); const result = await caller.getBalance({ resourceId: "res_1", year: 2026 }); expect(result.year).toBe(2026); expect(result.resourceId).toBe("res_1"); expect(result.entitledDays).toBe(30); expect(result.remainingDays).toBe(22); // 30 - 5 - 3 expect(result).toHaveProperty("sickDays"); }); it("creates entitlement with carryover when none exists", async () => { const prevEntitlement = sampleEntitlement({ id: "ent_prev", year: 2025, entitledDays: 28, usedDays: 20, pendingDays: 0, }); const createdEntitlement = sampleEntitlement({ year: 2026, entitledDays: 36, // 28 default + 8 carryover carryoverDays: 8, usedDays: 0, pendingDays: 0, }); const db = { systemSettings: { findUnique: vi.fn().mockResolvedValue({ id: "singleton", vacationDefaultDays: 28 }), }, vacationEntitlement: { findUnique: mockEntitlementFindUniqueByYear({ 2025: prevEntitlement, }), create: vi.fn().mockResolvedValue(createdEntitlement), update: vi.fn().mockResolvedValue(createdEntitlement), }, vacation: { findMany: vi.fn().mockResolvedValue([]), }, }; const caller = createProtectedCaller(db); const result = await caller.getBalance({ resourceId: "res_1", year: 2026 }); expect(result.entitledDays).toBe(36); expect(result.carryoverDays).toBe(8); expect(db.vacationEntitlement.create).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ resourceId: "res_1", year: 2026, carryoverDays: 8, }), }), ); }); it("uses default of 28 days when no system settings exist", async () => { const entitlement = sampleEntitlement({ entitledDays: 28, carryoverDays: 0 }); const db = { systemSettings: { findUnique: vi.fn().mockResolvedValue(null), }, vacationEntitlement: { findUnique: mockEntitlementFindUniqueByYear({ 2026: entitlement }), update: vi.fn().mockResolvedValue(entitlement), }, vacation: { findMany: vi.fn().mockResolvedValue([]), }, }; const caller = createProtectedCaller(db); const result = await caller.getBalance({ resourceId: "res_1", year: 2026 }); expect(result.entitledDays).toBe(28); }); it("counts sick days separately", async () => { const entitlement = sampleEntitlement({ usedDays: 0, pendingDays: 0 }); const vacationFindMany = vi.fn().mockImplementation(async ({ where }: { where?: { type?: unknown } } = {}) => { if (where?.type === "SICK") { return [ { startDate: new Date("2026-03-10"), endDate: new Date("2026-03-12"), isHalfDay: false, }, ]; } return []; }); const db = { systemSettings: { findUnique: vi.fn().mockResolvedValue(null), }, vacationEntitlement: { findUnique: mockEntitlementFindUniqueByYear({ 2026: entitlement }), update: vi.fn().mockResolvedValue(entitlement), }, vacation: { findMany: vacationFindMany, }, }; const caller = createProtectedCaller(db); const result = await caller.getBalance({ resourceId: "res_1", year: 2026 }); expect(result.sickDays).toBe(3); }); it("does not deduct city-specific public holidays from leave balance", async () => { const entitlement = sampleEntitlement({ usedDays: 0, pendingDays: 0, entitledDays: 30, carryoverDays: 0 }); const db = { systemSettings: { findUnique: vi.fn().mockResolvedValue(null), }, resource: { findUnique: vi.fn().mockResolvedValue({ userId: "user_1", federalState: "BY", country: { code: "DE" }, metroCity: { name: "Augsburg" }, }), }, vacationEntitlement: { findUnique: mockEntitlementFindUniqueByYear({ 2028: entitlement }), update: vi.fn().mockImplementation(async ({ data }) => ({ ...entitlement, ...data, })), }, vacation: { findMany: vi .fn() .mockResolvedValueOnce([]) .mockResolvedValueOnce([ { startDate: new Date("2028-08-08T00:00:00.000Z"), endDate: new Date("2028-08-08T00:00:00.000Z"), status: "APPROVED", isHalfDay: false, }, ]) .mockResolvedValueOnce([]) .mockResolvedValueOnce([]), }, }; const caller = createProtectedCaller(db); const result = await caller.getBalance({ resourceId: "res_1", year: 2028 }); expect(result.usedDays).toBe(0); expect(result.remainingDays).toBe(30); }); it("recomputes carryover from the previous year when the next year already exists", async () => { const entitlements = new Map([ [2025, sampleEntitlement({ id: "ent_2025", year: 2025, entitledDays: 28, carryoverDays: 0, usedDays: 8, pendingDays: 0, })], [2026, sampleEntitlement({ id: "ent_2026", year: 2026, entitledDays: 28, carryoverDays: 0, usedDays: 0, pendingDays: 0, })], ]); const db = { systemSettings: { findUnique: vi.fn().mockResolvedValue({ id: "singleton", vacationDefaultDays: 28 }), }, resource: { findUnique: vi.fn().mockResolvedValue({ userId: "user_1", federalState: "BY", countryId: "country_de", metroCityId: null, country: { code: "DE" }, metroCity: null, }), }, holidayCalendar: { findMany: vi.fn().mockResolvedValue([]), }, vacationEntitlement: { findUnique: vi.fn().mockImplementation(async ({ where }: { where: { resourceId_year: { year: number } } }) => ( entitlements.get(where.resourceId_year.year) ?? null )), create: vi.fn(), update: vi.fn().mockImplementation(async ({ where, data }: { where: { id: string }; data: Record; }) => { const current = [...entitlements.values()].find((entry) => entry.id === where.id); if (!current) { throw new Error(`Unknown entitlement ${where.id}`); } const updated = { ...current, ...data }; entitlements.set(updated.year, updated); return updated; }), }, vacation: { findMany: vi.fn().mockImplementation(async ({ where }: { where?: { type?: unknown; startDate?: { lte?: Date }; endDate?: { gte?: Date } }; } = {}) => { if (where?.type === "SICK" || where?.type === "PUBLIC_HOLIDAY") { return []; } if (where?.startDate?.lte?.toISOString().startsWith("2025-12-31")) { return [ { startDate: new Date("2025-06-10T00:00:00.000Z"), endDate: new Date("2025-06-17T00:00:00.000Z"), status: "APPROVED", isHalfDay: false, }, ]; } return []; }), }, }; const caller = createProtectedCaller(db); const result = await caller.getBalance({ resourceId: "res_1", year: 2026 }); expect(result.carryoverDays).toBe(20); expect(result.entitledDays).toBe(48); expect(db.vacationEntitlement.update).toHaveBeenCalledWith( expect.objectContaining({ where: { id: "ent_2026" }, data: expect.objectContaining({ carryoverDays: 20, entitledDays: 48, }), }), ); }); it("deducts only the non-holiday days from mixed vacation ranges", async () => { const entitlement = sampleEntitlement({ usedDays: 0, pendingDays: 0, entitledDays: 30, carryoverDays: 0 }); const db = { systemSettings: { findUnique: vi.fn().mockResolvedValue(null), }, resource: { findUnique: vi.fn().mockResolvedValue({ userId: "user_1", countryId: "country_de", metroCityId: "city_muc", federalState: "BY", country: { code: "DE" }, metroCity: { name: "Muenchen" }, }), }, holidayCalendar: { findMany: vi.fn().mockResolvedValue([ { id: "cal_muc", name: "Muenchen lokal", scopeType: "CITY", priority: 10, createdAt: new Date("2026-01-01T00:00:00.000Z"), entries: [ { date: new Date("2020-11-15T00:00:00.000Z"), name: "Lokaler Stadtfeiertag", isRecurringAnnual: true, }, ], }, ]), }, vacationEntitlement: { findUnique: mockEntitlementFindUniqueByYear({ 2026: entitlement }), update: vi.fn().mockImplementation(async ({ data }) => ({ ...entitlement, ...data, })), }, vacation: { findMany: vi.fn().mockImplementation(async ({ where }: { where?: { type?: unknown } } = {}) => { if (where?.type === "PUBLIC_HOLIDAY" || where?.type === "SICK") { return []; } return [ { startDate: new Date("2026-11-14T00:00:00.000Z"), endDate: new Date("2026-11-16T00:00:00.000Z"), status: "APPROVED", isHalfDay: false, }, ]; }), }, }; const caller = createProtectedCaller(db); const result = await caller.getBalance({ resourceId: "res_1", year: 2026 }); expect(result.usedDays).toBe(2); expect(result.remainingDays).toBe(28); }); it("prefers persisted holiday snapshots over the current holiday context", async () => { const entitlement = sampleEntitlement({ usedDays: 0, pendingDays: 0, entitledDays: 30, carryoverDays: 0 }); const resourceFindUnique = vi.fn().mockImplementation(async ({ select }: { select?: Record } = {}) => ({ ...(select?.userId ? { userId: "user_1" } : {}), ...(select?.federalState ? { federalState: "BY" } : {}), ...(select?.country ? { country: { code: "DE" } } : {}), ...(select?.metroCity ? { metroCity: { name: "Augsburg" } } : {}), })); const db = { systemSettings: { findUnique: vi.fn().mockResolvedValue(null), }, resource: { findUnique: resourceFindUnique, }, holidayCalendar: { findMany: vi.fn().mockResolvedValue([ { id: "cal_augsburg", name: "Augsburg lokal", scopeType: "CITY", priority: 10, createdAt: new Date("2026-01-01T00:00:00.000Z"), entries: [ { date: new Date("2020-08-08T00:00:00.000Z"), name: "Hohes Friedensfest", isRecurringAnnual: true, }, ], }, ]), }, vacationEntitlement: { findUnique: mockEntitlementFindUniqueByYear({ 2028: entitlement }), update: vi.fn().mockImplementation(async ({ data }) => ({ ...entitlement, ...data, })), }, vacation: { findMany: vi .fn() .mockResolvedValueOnce([ { startDate: new Date("2028-08-08T00:00:00.000Z"), endDate: new Date("2028-08-08T00:00:00.000Z"), status: "APPROVED", isHalfDay: false, deductedDays: 1, holidayCountryCode: "DE", holidayFederalState: "BY", holidayMetroCityName: "Augsburg", holidayCalendarDates: [], holidayLegacyPublicHolidayDates: [], }, ]) .mockResolvedValueOnce([]), }, }; const caller = createProtectedCaller(db); const result = await caller.getBalance({ resourceId: "res_1", year: 2028 }); expect(result.usedDays).toBe(1); expect(result.remainingDays).toBe(29); expect(db.holidayCalendar.findMany).not.toHaveBeenCalled(); }); it("falls back to the current holiday context for legacy vacations without snapshots", async () => { const entitlement = sampleEntitlement({ usedDays: 0, pendingDays: 0, entitledDays: 30, carryoverDays: 0 }); const db = { systemSettings: { findUnique: vi.fn().mockResolvedValue(null), }, resource: { findUnique: vi.fn().mockResolvedValue({ userId: "user_1", countryId: "country_de", metroCityId: "city_aug", federalState: "BY", country: { code: "DE" }, metroCity: { name: "Augsburg" }, }), }, holidayCalendar: { findMany: vi.fn().mockResolvedValue([ { id: "cal_augsburg", name: "Augsburg lokal", scopeType: "CITY", priority: 10, createdAt: new Date("2026-01-01T00:00:00.000Z"), entries: [ { date: new Date("2020-08-08T00:00:00.000Z"), name: "Hohes Friedensfest", isRecurringAnnual: true, }, ], }, ]), }, vacationEntitlement: { findUnique: mockEntitlementFindUniqueByYear({ 2028: entitlement }), update: vi.fn().mockImplementation(async ({ data }) => ({ ...entitlement, ...data, })), }, vacation: { findMany: vi .fn() .mockResolvedValueOnce([ { startDate: new Date("2028-08-08T00:00:00.000Z"), endDate: new Date("2028-08-08T00:00:00.000Z"), status: "APPROVED", isHalfDay: false, deductedDays: null, holidayCountryCode: null, holidayFederalState: null, holidayMetroCityName: null, holidayCalendarDates: null, holidayLegacyPublicHolidayDates: null, }, ]) .mockResolvedValueOnce([]) .mockResolvedValueOnce([]), }, }; const caller = createProtectedCaller(db); const result = await caller.getBalance({ resourceId: "res_1", year: 2028 }); expect(result.usedDays).toBe(0); expect(result.remainingDays).toBe(30); expect(db.holidayCalendar.findMany).toHaveBeenCalled(); }); }); describe("entitlement.getBalanceDetail", () => { it("returns assistant-friendly balance detail from the canonical balance workflow", async () => { const entitlement = sampleEntitlement({ carryoverDays: 0, usedDays: 1, pendingDays: 0.5 }); const db = { systemSettings: { findUnique: vi.fn().mockResolvedValue({ id: "singleton", vacationDefaultDays: 28 }), }, resource: { findUnique: vi.fn().mockImplementation(async ({ select }: { select?: Record } = {}) => ({ ...(select?.userId ? { userId: "user_1" } : {}), ...(select?.federalState ? { federalState: "BY" } : {}), ...(select?.country ? { country: { code: "DE" } } : {}), ...(select?.metroCity ? { metroCity: null } : {}), ...(select?.displayName ? { displayName: "Alice Example" } : {}), ...(select?.eid ? { eid: "EMP-001" } : {}), })), }, vacationEntitlement: { findUnique: mockEntitlementFindUniqueByYear({ 2026: entitlement }), update: vi.fn().mockResolvedValue(entitlement), }, vacation: { findMany: vi.fn().mockImplementation(async ({ where }: { where?: { type?: string } } = {}) => { if (where?.type === "SICK") { return [{ startDate: new Date("2026-02-01"), endDate: new Date("2026-02-01"), isHalfDay: false }]; } return []; }), }, }; const caller = createProtectedCaller(db); const result = await caller.getBalanceDetail({ resourceId: "res_1", year: 2026 }); expect(result).toEqual({ resource: "Alice Example", eid: "EMP-001", year: 2026, entitlement: 30, carryOver: 0, taken: 1, pending: 0.5, remaining: 28.5, sickDays: 1, deductionSummary: { formula: "remaining = entitlement - taken - pending", approvedVacationCount: 0, pendingVacationCount: 0, approvedRequestedDays: 0, pendingRequestedDays: 0, approvedDeductedDays: 0, pendingDeductedDays: 0, excludedHolidayDates: [], holidayBasisVariants: [], sources: { hasCalendarHolidays: false, hasLegacyPublicHolidayEntries: false, }, }, vacations: [], }); }); it("includes holiday-adjusted vacation breakdown for explainability", async () => { const entitlement = sampleEntitlement({ carryoverDays: 1, usedDays: 1, pendingDays: 0 }); const db = { systemSettings: { findUnique: vi.fn().mockResolvedValue({ id: "singleton", vacationDefaultDays: 28 }), }, resource: { findUnique: vi.fn().mockImplementation(async ({ select }: { select?: Record } = {}) => ({ ...(select?.userId ? { userId: "user_1" } : {}), ...(select?.displayName ? { displayName: "Alice Example" } : {}), ...(select?.eid ? { eid: "EMP-001" } : {}), })), }, vacationEntitlement: { findUnique: mockEntitlementFindUniqueByYear({ 2026: entitlement }), update: vi.fn().mockResolvedValue(entitlement), }, vacation: { findMany: vi.fn().mockImplementation(async ({ where }: { where?: { type?: string | { in?: string[] } } } = {}) => { if (where?.type === "SICK") { return []; } if (typeof where?.type === "object" && Array.isArray(where.type.in)) { return [ { type: "ANNUAL", startDate: new Date("2026-05-01T00:00:00.000Z"), endDate: new Date("2026-05-02T00:00:00.000Z"), status: "APPROVED", isHalfDay: false, deductedDays: 1, holidayCountryCode: "DE", holidayCountryName: "Germany", holidayFederalState: "BY", holidayMetroCityName: null, holidayCalendarDates: ["2026-05-01"], holidayLegacyPublicHolidayDates: [], }, ]; } return []; }), }, }; const caller = createProtectedCaller(db); const result = await caller.getBalanceDetail({ resourceId: "res_1", year: 2026 }); expect(result.deductionSummary).toEqual({ formula: "remaining = entitlement - taken - pending", approvedVacationCount: 1, pendingVacationCount: 0, approvedRequestedDays: 2, pendingRequestedDays: 0, approvedDeductedDays: 1, pendingDeductedDays: 0, excludedHolidayDates: ["2026-05-01"], holidayBasisVariants: ["Germany / BY"], sources: { hasCalendarHolidays: true, hasLegacyPublicHolidayEntries: false, }, }); expect(result.vacations).toEqual([ { type: "ANNUAL", status: "APPROVED", startDate: "2026-05-01", endDate: "2026-05-02", isHalfDay: false, requestedDays: 2, deductedDays: 1, holidayCountryCode: "DE", holidayCountryName: "Germany", holidayFederalState: "BY", holidayMetroCityName: null, holidayCalendarDates: ["2026-05-01"], holidayLegacyPublicHolidayDates: [], holidayDetails: [ { date: "2026-05-01", source: "CALENDAR" }, ], holidayContext: { countryCode: "DE", countryName: "Germany", federalState: "BY", metroCityName: null, sources: { hasCalendarHolidays: true, hasLegacyPublicHolidayEntries: false, }, }, }, ]); }); }); // ─── get ───────────────────────────────────────────────────────────────────── describe("entitlement.get", () => { it("returns existing entitlement (manager role)", async () => { const entitlement = sampleEntitlement({ entitledDays: 30, carryoverDays: 0, usedDays: 0, pendingDays: 0, }); const db = { systemSettings: { findUnique: vi.fn().mockResolvedValue({ id: "singleton", vacationDefaultDays: 30 }), }, vacationEntitlement: { findUnique: mockEntitlementFindUniqueByYear({ 2026: entitlement }), update: vi.fn().mockImplementation(async ({ data }: { data: Record }) => ({ ...entitlement, ...data, })), }, vacation: { findMany: vi.fn().mockResolvedValue([]), }, }; const caller = createManagerCaller(db); const result = await caller.get({ resourceId: "res_1", year: 2026 }); expect(result.id).toBe("ent_1"); expect(result.entitledDays).toBe(30); }); it("rejects access by a regular user (FORBIDDEN)", async () => { const db = { systemSettings: { findUnique: vi.fn(), }, vacationEntitlement: { findUnique: vi.fn(), }, }; const caller = createProtectedCaller(db); await expect(caller.get({ resourceId: "res_1", year: 2026 })).rejects.toThrow(); }); }); // ─── set ───────────────────────────────────────────────────────────────────── describe("entitlement.set", () => { it("updates existing entitlement", async () => { const existing = sampleEntitlement(); const updated = { ...existing, entitledDays: 35 }; const db = { vacationEntitlement: { findUnique: vi.fn().mockResolvedValue(existing), update: vi.fn().mockResolvedValue(updated), create: vi.fn(), }, auditLog: { create: vi.fn().mockResolvedValue({}) }, }; const caller = createManagerCaller(db); const result = await caller.set({ resourceId: "res_1", year: 2026, entitledDays: 35, }); expect(result.entitledDays).toBe(35); expect(db.vacationEntitlement.update).toHaveBeenCalledWith( expect.objectContaining({ where: { id: "ent_1" }, data: { entitledDays: 35 }, }), ); expect(db.vacationEntitlement.create).not.toHaveBeenCalled(); }); it("creates new entitlement when none exists", async () => { const created = sampleEntitlement({ entitledDays: 30, carryoverDays: 0 }); const db = { vacationEntitlement: { findUnique: vi.fn().mockResolvedValue(null), update: vi.fn(), create: vi.fn().mockResolvedValue(created), }, auditLog: { create: vi.fn().mockResolvedValue({}) }, }; const caller = createManagerCaller(db); const result = await caller.set({ resourceId: "res_1", year: 2026, entitledDays: 30, }); expect(result.entitledDays).toBe(30); expect(db.vacationEntitlement.create).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ resourceId: "res_1", year: 2026, entitledDays: 30, carryoverDays: 0, usedDays: 0, pendingDays: 0, }), }), ); expect(db.vacationEntitlement.update).not.toHaveBeenCalled(); }); }); // ─── bulkSet ───────────────────────────────────────────────────────────────── describe("entitlement.bulkSet", () => { it("upserts entitlements for all active resources (admin role)", async () => { const resources = [{ id: "res_1" }, { id: "res_2" }, { id: "res_3" }]; const db = { resource: { findMany: vi.fn().mockResolvedValue(resources), }, vacationEntitlement: { upsert: vi.fn().mockResolvedValue(sampleEntitlement()), }, auditLog: { create: vi.fn().mockResolvedValue({}) }, }; const caller = createAdminCaller(db); const result = await caller.bulkSet({ year: 2026, entitledDays: 30, }); expect(result.updated).toBe(3); expect(db.vacationEntitlement.upsert).toHaveBeenCalledTimes(3); expect(db.resource.findMany).toHaveBeenCalledWith( expect.objectContaining({ where: expect.objectContaining({ isActive: true }), }), ); }); it("filters by resourceIds when provided", async () => { const resources = [{ id: "res_1" }]; const db = { resource: { findMany: vi.fn().mockResolvedValue(resources), }, vacationEntitlement: { upsert: vi.fn().mockResolvedValue(sampleEntitlement()), }, auditLog: { create: vi.fn().mockResolvedValue({}) }, }; const caller = createAdminCaller(db); await caller.bulkSet({ year: 2026, entitledDays: 30, resourceIds: ["res_1"], }); expect(db.resource.findMany).toHaveBeenCalledWith( expect.objectContaining({ where: expect.objectContaining({ isActive: true, id: { in: ["res_1"] }, }), }), ); }); it("rejects bulk set by a manager (admin only)", async () => { const db = { resource: { findMany: vi.fn() }, vacationEntitlement: { upsert: vi.fn() }, }; const caller = createManagerCaller(db); await expect( caller.bulkSet({ year: 2026, entitledDays: 30 }), ).rejects.toThrow(); }); }); // ─── getYearSummary ────────────────────────────────────────────────────────── describe("entitlement.getYearSummary", () => { it("returns summary for all active resources (manager role)", async () => { const resources = [ { id: "res_1", displayName: "Alice", eid: "alice", chapter: "VFX", federalState: "BY", country: { code: "DE", name: "Germany" }, metroCity: { name: "Muenchen" }, }, { id: "res_2", displayName: "Bob", eid: "bob", chapter: "Animation", federalState: "HH", country: { code: "DE", name: "Germany" }, metroCity: { name: "Hamburg" }, }, ]; const entitlement = sampleEntitlement({ usedDays: 5, pendingDays: 2 }); const db = { systemSettings: { findUnique: vi.fn().mockResolvedValue({ id: "singleton", vacationDefaultDays: 28 }), }, resource: { findUnique: vi.fn().mockResolvedValue({ federalState: "BY", country: { code: "DE" }, metroCity: null, }), findMany: vi.fn().mockResolvedValue(resources), }, vacationEntitlement: { findUnique: mockEntitlementFindUniqueByYear({ 2026: entitlement }), update: vi.fn().mockResolvedValue(entitlement), }, vacation: { findMany: vi.fn().mockResolvedValue([]), }, }; const caller = createManagerCaller(db); const result = await caller.getYearSummary({ year: 2026 }); expect(result).toHaveLength(2); expect(result[0]).toHaveProperty("resourceId"); expect(result[0]).toHaveProperty("displayName"); expect(result[0]).toHaveProperty("remainingDays"); expect(result[0]).toMatchObject({ countryCode: "DE", countryName: "Germany", federalState: "BY", metroCityName: "Muenchen", }); }); it("filters by chapter when provided", async () => { const db = { systemSettings: { findUnique: vi.fn().mockResolvedValue(null), }, resource: { findMany: vi.fn().mockResolvedValue([]), }, vacationEntitlement: { findUnique: vi.fn(), update: vi.fn(), }, vacation: { findMany: vi.fn(), }, }; const caller = createManagerCaller(db); await caller.getYearSummary({ year: 2026, chapter: "VFX" }); expect(db.resource.findMany).toHaveBeenCalledWith( expect.objectContaining({ where: expect.objectContaining({ isActive: true, chapter: "VFX", }), }), ); }); }); describe("entitlement.getYearSummaryDetail", () => { it("returns assistant-friendly year summary detail from the canonical summary workflow", async () => { const resources = [ { id: "res_1", displayName: "Alice Example", eid: "EMP-001", chapter: "Delivery", federalState: "BY", country: { code: "DE", name: "Germany" }, metroCity: { name: "Muenchen" }, }, { id: "res_2", displayName: "Bob Example", eid: "EMP-002", chapter: "CGI", federalState: "HH", country: { code: "DE", name: "Germany" }, metroCity: { name: "Hamburg" }, }, ]; const db = { systemSettings: { findUnique: vi.fn().mockResolvedValue({ id: "singleton", vacationDefaultDays: 28 }), }, resource: { findMany: vi.fn().mockResolvedValue(resources), findUnique: vi.fn().mockResolvedValue({ federalState: "BY", country: { code: "DE" }, metroCity: null, }), }, vacationEntitlement: { findUnique: vi.fn().mockImplementation(async ({ where }: { where: { resourceId_year: { resourceId: string; year: number } } }) => { if (where.resourceId_year.year !== 2026) { return null; } return sampleEntitlement({ id: `ent_${where.resourceId_year.resourceId}`, resourceId: where.resourceId_year.resourceId, year: 2026, entitledDays: 28, carryoverDays: 0, usedDays: 0, pendingDays: 0, }); }), create: vi.fn(), update: vi.fn().mockImplementation(async (args?: { data?: Record; where?: { id?: string } }) => ({ ...sampleEntitlement({ entitledDays: 28, carryoverDays: 0, usedDays: 0, pendingDays: 0 }), id: args?.where?.id ?? "ent_updated", ...(args?.data ?? {}), })), }, vacation: { findMany: vi.fn().mockResolvedValue([]), }, }; const caller = createManagerCaller(db); const result = await caller.getYearSummaryDetail({ year: 2026, resourceName: "alice" }); expect(result).toEqual([ { resource: "Alice Example", eid: "EMP-001", chapter: "Delivery", countryCode: "DE", countryName: "Germany", federalState: "BY", metroCityName: "Muenchen", year: 2026, entitled: 28, carryover: 0, used: 0, pending: 0, remaining: 28, }, ]); }); });