import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; vi.mock("@capakraken/application", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, isChargeabilityActualBooking: actual.isChargeabilityActualBooking, listAssignmentBookings: vi.fn(), }; }); import { listAssignmentBookings } from "@capakraken/application"; import { checkChargeabilityAlerts } from "../lib/chargeability-alerts.js"; describe("chargeability alerts", () => { beforeEach(() => { vi.clearAllMocks(); vi.useFakeTimers(); vi.setSystemTime(new Date("2026-01-15T12:00:00.000Z")); }); afterEach(() => { vi.useRealTimers(); }); it("creates an alert when a regional holiday reduces booked hours below threshold", async () => { const notifications: Array<{ userId: string; title: string; body?: string }> = []; const db = { resource: { findMany: vi.fn().mockResolvedValue([ { id: "res_1", displayName: "Bruce Banner", fte: 1, availability: { monday: 8, tuesday: 8, wednesday: 8, thursday: 8, friday: 8 }, countryId: "country_de", metroCityId: null, federalState: "BY", chargeabilityTarget: 21, country: { id: "country_de", code: "DE", dailyWorkingHours: 8, scheduleRules: null, }, managementLevelGroup: { targetPercentage: 0.21 }, metroCity: null, }, ]), }, vacation: { findMany: vi.fn().mockResolvedValue([]), }, notification: { findFirst: vi.fn().mockResolvedValue(null), create: vi.fn().mockImplementation(async ({ data }) => { notifications.push(data); return { id: `notification_${notifications.length}`, userId: data.userId }; }), }, user: { findMany: vi.fn().mockResolvedValue([{ id: "manager_1" }]), }, }; vi.mocked(listAssignmentBookings).mockResolvedValue([ { id: "assignment_1", projectId: "project_1", resourceId: "res_1", startDate: new Date("2026-01-05T00:00:00.000Z"), endDate: new Date("2026-01-06T00:00:00.000Z"), hoursPerDay: 8, dailyCostCents: 0, status: "CONFIRMED", project: { id: "project_1", name: "Gamma", shortCode: "GAM", status: "ACTIVE", orderType: "CLIENT", dynamicFields: null, }, resource: { id: "res_1", displayName: "Bruce Banner", chapter: "CGI" }, }, ]); const alertCount = await checkChargeabilityAlerts(db); expect(alertCount).toBe(1); expect(notifications).toHaveLength(1); expect(notifications[0]?.title).toContain("Bruce Banner"); expect(notifications[0]?.body).toContain("gap: 16pp"); }); });