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 db = { systemSettings: { findUnique: vi.fn().mockResolvedValue(null), }, vacationEntitlement: { findUnique: mockEntitlementFindUniqueByYear({ 2026: entitlement }), update: vi.fn().mockResolvedValue(entitlement), }, vacation: { findMany: vi .fn() // Public holiday vacations for holiday context .mockResolvedValueOnce([]) // First call: balance-type vacations (for syncEntitlement) .mockResolvedValueOnce([]) // Second call: sick days .mockResolvedValueOnce([ { startDate: new Date("2026-03-10"), endDate: new Date("2026-03-12"), isHalfDay: false, }, ]), }, }; 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() // 2025 holiday context .mockResolvedValueOnce([]) // 2025 balance vacations .mockResolvedValueOnce([ { startDate: new Date("2025-06-10T00:00:00.000Z"), endDate: new Date("2025-06-17T00:00:00.000Z"), status: "APPROVED", isHalfDay: false, }, ]) // 2026 holiday context .mockResolvedValueOnce([]) // 2026 balance vacations .mockResolvedValueOnce([]) // 2026 sick days .mockResolvedValueOnce([]), }, }; 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, }), }), ); }); }); 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, }); }); }); // ─── 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" }, { id: "res_2", displayName: "Bob", eid: "bob", chapter: "Animation" }, ]; 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"); }); 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" }, { id: "res_2", displayName: "Bob Example", eid: "EMP-002", chapter: "CGI" }, ]; 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", year: 2026, entitled: 28, carryover: 0, used: 0, pending: 0, remaining: 28, }, ]); }); });