import { AllocationStatus, SystemRole } from "@capakraken/shared"; import { describe, expect, it, vi } from "vitest"; import { timelineRouter } from "../router/timeline.js"; import { createCallerFactory } from "../trpc.js"; vi.mock("../sse/event-bus.js", () => ({ emitAllocationCreated: vi.fn(), emitAllocationDeleted: vi.fn(), emitAllocationUpdated: vi.fn(), emitProjectShifted: vi.fn(), })); vi.mock("../lib/budget-alerts.js", () => ({ checkBudgetThresholds: vi.fn(), })); vi.mock("../lib/cache.js", () => ({ invalidateDashboardCache: vi.fn(), })); const createCaller = createCallerFactory(timelineRouter); function createManagerCaller(db: Record) { return createCaller({ session: { user: { email: "manager@example.com", name: "Manager", image: null }, expires: "2026-03-13T00:00:00.000Z", }, db: db as never, dbUser: { id: "user_1", systemRole: SystemRole.MANAGER, permissionOverrides: null, }, }); } describe("timeline allocation entry resolution", () => { it("creates a quick assignment without dual-writing a legacy allocation row", async () => { const createdAssignment = { id: "assignment_quick_1", demandRequirementId: null, resourceId: "resource_1", projectId: "project_1", startDate: new Date("2026-03-16"), endDate: new Date("2026-03-20"), hoursPerDay: 8, percentage: 100, role: "Team Member", roleId: null, dailyCostCents: 40000, status: AllocationStatus.PROPOSED, metadata: { source: "quickAssign" }, createdAt: new Date("2026-03-13"), updatedAt: new Date("2026-03-13"), resource: { id: "resource_1", displayName: "Alice", eid: "E-001", lcrCents: 5000, }, project: { id: "project_1", name: "Project One", shortCode: "PRJ" }, roleEntity: null, demandRequirement: null, }; const db = { project: { findUnique: vi.fn().mockResolvedValue({ id: "project_1" }), }, resource: { findUnique: vi.fn().mockResolvedValue({ id: "resource_1", lcrCents: 5000, availability: { monday: 8, tuesday: 8, wednesday: 8, thursday: 8, friday: 8, saturday: 0, sunday: 0, }, }), }, allocation: { findMany: vi.fn().mockResolvedValue([]), create: vi.fn(), }, assignment: { findMany: vi.fn().mockResolvedValue([]), create: vi.fn().mockResolvedValue(createdAssignment), }, vacation: { findMany: vi.fn().mockResolvedValue([]), }, auditLog: { create: vi.fn().mockResolvedValue({}), }, $transaction: vi.fn(async (callback: (tx: unknown) => unknown) => callback(db)), }; const caller = createManagerCaller(db); const result = await caller.quickAssign({ resourceId: "resource_1", projectId: "project_1", startDate: new Date("2026-03-16"), endDate: new Date("2026-03-20"), hoursPerDay: 8, role: "Team Member", status: AllocationStatus.PROPOSED, }); expect(result.id).toBe("assignment_quick_1"); expect(result.isPlaceholder).toBe(false); expect(db.allocation.create).not.toHaveBeenCalled(); expect(db.assignment.create).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ resourceId: "resource_1", metadata: { source: "quickAssign" }, }), }), ); }); it("updates an explicit assignment through updateAllocationInline", async () => { const existingAssignment = { id: "assignment_1", demandRequirementId: null, resourceId: "resource_1", projectId: "project_1", startDate: new Date("2026-03-16"), endDate: new Date("2026-03-20"), hoursPerDay: 4, percentage: 50, role: "Compositor", roleId: "role_comp", dailyCostCents: 20000, status: AllocationStatus.PROPOSED, metadata: {}, createdAt: new Date("2026-03-13"), updatedAt: new Date("2026-03-13"), resource: { id: "resource_1", displayName: "Alice", eid: "E-001", lcrCents: 5000, availability: { monday: 8, tuesday: 8, wednesday: 8, thursday: 8, friday: 8, saturday: 0, sunday: 0, }, }, project: { id: "project_1", name: "Project One", shortCode: "PRJ" }, roleEntity: { id: "role_comp", name: "Compositor", color: "#111111" }, demandRequirement: null, }; const updatedAssignment = { ...existingAssignment, hoursPerDay: 6, endDate: new Date("2026-03-21"), percentage: 75, dailyCostCents: 30000, metadata: { includeSaturday: true }, updatedAt: new Date("2026-03-14"), }; const db = { allocation: { findUnique: vi.fn().mockResolvedValue(null), }, demandRequirement: { findUnique: vi.fn().mockResolvedValue(null), }, assignment: { findUnique: vi.fn().mockResolvedValue(existingAssignment), update: vi.fn().mockResolvedValue(updatedAssignment), }, resource: { findUnique: vi.fn().mockResolvedValue({ id: "resource_1", lcrCents: 5000, availability: { monday: 8, tuesday: 8, wednesday: 8, thursday: 8, friday: 8, saturday: 0, sunday: 0, }, }), }, vacation: { findMany: vi.fn().mockResolvedValue([]), }, auditLog: { create: vi.fn().mockResolvedValue({}), }, $transaction: vi.fn(async (callback: (tx: unknown) => unknown) => callback(db)), }; const caller = createManagerCaller(db); const result = await caller.updateAllocationInline({ allocationId: "assignment_1", hoursPerDay: 6, endDate: new Date("2026-03-21"), includeSaturday: true, }); expect(result.id).toBe("assignment_1"); expect(result.hoursPerDay).toBe(6); expect(db.assignment.update).toHaveBeenCalledWith( expect.objectContaining({ where: { id: "assignment_1" }, }), ); }); it("falls back to default rules when calculationRule and vacation tables are missing", async () => { const existingAssignment = { id: "assignment_legacy_1", demandRequirementId: null, resourceId: "resource_1", projectId: "project_1", startDate: new Date("2026-03-16"), endDate: new Date("2026-03-20"), hoursPerDay: 4, percentage: 50, role: "Compositor", roleId: "role_comp", dailyCostCents: 20000, status: AllocationStatus.PROPOSED, metadata: {}, createdAt: new Date("2026-03-13"), updatedAt: new Date("2026-03-13"), resource: { id: "resource_1", displayName: "Alice", eid: "E-001", lcrCents: 5000, availability: { monday: 8, tuesday: 8, wednesday: 8, thursday: 8, friday: 8, saturday: 0, sunday: 0, }, }, project: { id: "project_1", name: "Project One", shortCode: "PRJ" }, roleEntity: { id: "role_comp", name: "Compositor", color: "#111111" }, demandRequirement: null, }; const missingVacationTableError = { code: "P2021", message: "The table `public.vacation` does not exist in the current database.", meta: { table: "public.vacation" }, }; const missingCalculationRuleTableError = { code: "P2021", message: "The table `public.calculation_rule` does not exist in the current database.", meta: { table: "public.calculation_rule" }, }; const db = { allocation: { findUnique: vi.fn().mockResolvedValue(null), }, demandRequirement: { findUnique: vi.fn().mockResolvedValue(null), }, assignment: { findUnique: vi.fn().mockResolvedValue(existingAssignment), update: vi.fn().mockImplementation(async ({ data }: { data: Record }) => ({ ...existingAssignment, ...data, metadata: data.metadata ?? existingAssignment.metadata, updatedAt: new Date("2026-03-14"), })), }, resource: { findUnique: vi.fn().mockResolvedValue({ id: "resource_1", lcrCents: 5000, availability: { monday: 8, tuesday: 8, wednesday: 8, thursday: 8, friday: 8, saturday: 0, sunday: 0, }, }), }, vacation: { findMany: vi.fn().mockRejectedValue(missingVacationTableError), }, calculationRule: { findMany: vi.fn().mockRejectedValue(missingCalculationRuleTableError), }, auditLog: { create: vi.fn().mockResolvedValue({}), }, $transaction: vi.fn(async (callback: (tx: unknown) => unknown) => callback(db)), }; const caller = createManagerCaller(db); const result = await caller.updateAllocationInline({ allocationId: "assignment_legacy_1", hoursPerDay: 6, endDate: new Date("2026-03-21"), includeSaturday: true, }); expect(result.id).toBe("assignment_legacy_1"); expect(db.vacation.findMany).toHaveBeenCalledTimes(1); expect(db.calculationRule.findMany).toHaveBeenCalledTimes(1); expect(db.assignment.update).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ dailyCostCents: expect.any(Number), metadata: expect.objectContaining({ includeSaturday: true }), }), }), ); }); it("updates an explicit demand row through updateAllocationInline", async () => { const existingDemand = { id: "demand_1", projectId: "project_1", startDate: new Date("2026-03-16"), endDate: new Date("2026-03-20"), hoursPerDay: 4, percentage: 50, role: "FX Artist", roleId: "role_fx", headcount: 1, status: AllocationStatus.PROPOSED, metadata: {}, createdAt: new Date("2026-03-13"), updatedAt: new Date("2026-03-13"), project: { id: "project_1", name: "Project One", shortCode: "PRJ" }, roleEntity: { id: "role_fx", name: "FX Artist", color: "#222222" }, }; const updatedDemand = { ...existingDemand, hoursPerDay: 6, endDate: new Date("2026-03-21"), percentage: 50, metadata: { includeSaturday: true }, updatedAt: new Date("2026-03-14"), }; const db = { allocation: { findUnique: vi.fn().mockResolvedValue(null), }, demandRequirement: { findUnique: vi.fn().mockResolvedValue(existingDemand), update: vi.fn().mockResolvedValue(updatedDemand), }, assignment: { findUnique: vi.fn().mockResolvedValue(null), update: vi.fn(), }, resource: { findUnique: vi.fn(), }, vacation: { findMany: vi.fn().mockResolvedValue([]), }, auditLog: { create: vi.fn().mockResolvedValue({}), }, $transaction: vi.fn(async (callback: (tx: unknown) => unknown) => callback(db)), }; const caller = createManagerCaller(db); const result = await caller.updateAllocationInline({ allocationId: "demand_1", hoursPerDay: 6, endDate: new Date("2026-03-21"), includeSaturday: true, }); expect(result.id).toBe("demand_1"); expect(result.hoursPerDay).toBe(6); expect(db.resource.findUnique).not.toHaveBeenCalled(); expect(db.demandRequirement.update).toHaveBeenCalledWith( expect.objectContaining({ where: { id: "demand_1" }, }), ); }); it("returns resolved holiday overlays for assigned resources", async () => { const db = { demandRequirement: { findMany: vi.fn().mockResolvedValue([]), }, assignment: { findMany: vi.fn().mockResolvedValue([ { id: "assignment_1", kind: "assignment", resourceId: "resource_by", projectId: "project_1", startDate: new Date("2026-01-01"), endDate: new Date("2026-01-31"), hoursPerDay: 8, status: AllocationStatus.CONFIRMED, metadata: {}, project: { id: "project_1", name: "Project One", shortCode: "PRJ", status: "ACTIVE", startDate: new Date("2026-01-01"), endDate: new Date("2026-03-31"), orderType: "CHARGEABLE", clientId: null, }, resource: { id: "resource_by", displayName: "Alice", eid: "E-001", chapter: null, }, }, ]), }, resource: { findMany: vi.fn().mockResolvedValue([ { id: "resource_by", countryId: "country_de", federalState: "BY", metroCityId: null, country: { code: "DE" }, metroCity: null, }, ]), }, project: { findMany: vi.fn().mockResolvedValue([]), }, holidayCalendar: { findMany: vi.fn().mockResolvedValue([]), }, country: { findUnique: vi.fn(), }, metroCity: { findUnique: vi.fn(), }, }; const caller = createManagerCaller(db); const overlays = await caller.getHolidayOverlays({ startDate: new Date("2026-01-01"), endDate: new Date("2026-01-31"), }); expect(overlays).toEqual( expect.arrayContaining([ expect.objectContaining({ resourceId: "resource_by", type: "PUBLIC_HOLIDAY", note: "Heilige Drei Könige", }), ]), ); }); });