import { VacationStatus, VacationType } from "@capakraken/db"; import { TRPCError } from "@trpc/server"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { createVacationRequest, CreateVacationRequestSchema, } from "../router/vacation-create-support.js"; const { emitVacationCreated, createAuditEntry, createVacationApprovalTasks, getAnonymizationDirectory, resolveVacationCreationChargeability, } = vi.hoisted(() => ({ emitVacationCreated: vi.fn(), createAuditEntry: vi.fn(), createVacationApprovalTasks: vi.fn(), getAnonymizationDirectory: vi.fn(), resolveVacationCreationChargeability: vi.fn(), })); vi.mock("../sse/event-bus.js", () => ({ emitVacationCreated, })); vi.mock("../lib/audit.js", () => ({ createAuditEntry, })); vi.mock("../lib/anonymization.js", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, anonymizeResource: vi.fn((value) => value), anonymizeUser: vi.fn((value) => value), getAnonymizationDirectory, }; }); vi.mock("../router/vacation-chargeability.js", () => ({ resolveVacationCreationChargeability, })); vi.mock("../router/vacation-side-effects.js", () => ({ createVacationApprovalTasks, })); function createContext(overrides: Record = {}) { return { session: { user: { email: "user@example.com", name: "User", image: null }, expires: "2099-01-01T00:00:00.000Z", }, db: { user: { findUnique: vi.fn().mockResolvedValue({ id: "user_1", systemRole: "USER" }), }, resource: { findUnique: vi.fn().mockResolvedValue({ userId: "user_1" }), }, vacation: { findFirst: vi.fn().mockResolvedValue(null), create: vi.fn().mockResolvedValue({ id: "vac_1", resourceId: "res_1", type: VacationType.ANNUAL, status: VacationStatus.PENDING, startDate: new Date("2026-06-01T00:00:00.000Z"), endDate: new Date("2026-06-03T00:00:00.000Z"), isHalfDay: false, halfDayPart: null, resource: { id: "res_1", displayName: "Alice", eid: "E-001" }, requestedBy: { id: "user_1", name: "User", email: "user@example.com" }, }), }, }, ...overrides, }; } describe("vacation create support", () => { beforeEach(() => { emitVacationCreated.mockReset(); createAuditEntry.mockReset(); createVacationApprovalTasks.mockReset(); getAnonymizationDirectory.mockReset(); resolveVacationCreationChargeability.mockReset(); getAnonymizationDirectory.mockResolvedValue({ resourcesById: new Map(), usersById: new Map(), }); resolveVacationCreationChargeability.mockResolvedValue({ effectiveDays: 3, deductionSnapshotWriteData: { deductedDays: 3, deductionSnapshot: { source: "calendar" } }, }); }); it("validates half-day requests against cross-day ranges", () => { expect(() => CreateVacationRequestSchema.parse({ resourceId: "res_1", type: VacationType.ANNUAL, startDate: "2026-06-01", endDate: "2026-06-02", isHalfDay: true, halfDayPart: "MORNING", })).toThrowError(/Half-day requests must start and end on the same day/); }); it("creates pending vacations for regular users and schedules approval tasks", async () => { const ctx = createContext(); const result = await createVacationRequest(ctx as never, { resourceId: "res_1", type: VacationType.ANNUAL, startDate: new Date("2026-06-01T00:00:00.000Z"), endDate: new Date("2026-06-03T00:00:00.000Z"), note: "Summer", isHalfDay: false, }); expect(ctx.db.vacation.create).toHaveBeenCalledWith(expect.objectContaining({ data: expect.objectContaining({ status: VacationStatus.PENDING, requestedById: "user_1", deductedDays: 3, }), })); expect(createVacationApprovalTasks).toHaveBeenCalledWith(expect.objectContaining({ submittedByUserId: "user_1", vacationId: "vac_1", })); expect(emitVacationCreated).toHaveBeenCalledWith({ id: "vac_1", resourceId: "res_1", status: VacationStatus.PENDING, }); expect(result).toMatchObject({ id: "vac_1", effectiveDays: 3, }); }); it("auto-approves manager-created vacations without approval tasks", async () => { const ctx = createContext({ db: { user: { findUnique: vi.fn().mockResolvedValue({ id: "mgr_1", systemRole: "MANAGER" }), }, resource: { findUnique: vi.fn(), }, vacation: { findFirst: vi.fn().mockResolvedValue(null), create: vi.fn().mockResolvedValue({ id: "vac_mgr", resourceId: "res_2", type: VacationType.ANNUAL, status: VacationStatus.APPROVED, startDate: new Date("2026-07-01T00:00:00.000Z"), endDate: new Date("2026-07-02T00:00:00.000Z"), isHalfDay: false, halfDayPart: null, resource: { id: "res_2", displayName: "Bob", eid: "E-002" }, requestedBy: { id: "mgr_1", name: "Manager", email: "user@example.com" }, }), }, }, }); await createVacationRequest(ctx as never, { resourceId: "res_2", type: VacationType.ANNUAL, startDate: new Date("2026-07-01T00:00:00.000Z"), endDate: new Date("2026-07-02T00:00:00.000Z"), isHalfDay: false, }); expect(ctx.db.resource.findUnique).not.toHaveBeenCalled(); expect(ctx.db.vacation.create).toHaveBeenCalledWith(expect.objectContaining({ data: expect.objectContaining({ status: VacationStatus.APPROVED, approvedById: "mgr_1", }), })); expect(createVacationApprovalTasks).not.toHaveBeenCalled(); }); it("rejects manual public holiday creation before hitting the database", async () => { const ctx = createContext(); await expect(createVacationRequest(ctx as never, { resourceId: "res_1", type: VacationType.PUBLIC_HOLIDAY, startDate: new Date("2026-12-25T00:00:00.000Z"), endDate: new Date("2026-12-25T00:00:00.000Z"), isHalfDay: false, })).rejects.toThrowError(new TRPCError({ code: "BAD_REQUEST", message: "Public holidays must be managed via Holiday Calendars or the legacy holiday import, not via manual vacation requests", })); expect(ctx.db.user.findUnique).not.toHaveBeenCalled(); expect(ctx.db.vacation.create).not.toHaveBeenCalled(); }); });