import type { ShiftValidationResult } from "@capakraken/shared"; import { AllocationStatus } from "@capakraken/shared"; import { describe, expect, it, vi } from "vitest"; const { calculateAllocationMock, updateAssignmentMock, updateDemandRequirementMock, validateShiftMock, } = vi.hoisted(() => ({ validateShiftMock: vi.fn(), calculateAllocationMock: vi.fn(), updateAssignmentMock: vi.fn(), updateDemandRequirementMock: vi.fn(), })); vi.mock("@capakraken/engine", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, validateShift: validateShiftMock, calculateAllocation: calculateAllocationMock, }; }); vi.mock("@capakraken/application", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, updateAssignment: updateAssignmentMock, updateDemandRequirement: updateDemandRequirementMock, }; }); import { buildTimelineProjectShiftEventPayload } from "../router/timeline-shift-support.js"; import { applyTimelineProjectShift } from "../router/timeline-shift-procedure-support.js"; function createValidValidationResult( overrides: Partial = {}, ): ShiftValidationResult { return { valid: true, errors: [], warnings: [], conflictDetails: [], costImpact: { currentTotalCents: 100_000, newTotalCents: 112_000, deltaCents: 12_000, budgetCents: 200_000, budgetUtilizationBefore: 50, budgetUtilizationAfter: 56, wouldExceedBudget: false, }, ...overrides, }; } describe("timeline shift procedure support", () => { it("applies project shift updates and recalculates only staffed assignments", async () => { const validation = createValidValidationResult(); validateShiftMock.mockReturnValue(validation); calculateAllocationMock.mockReturnValue({ dailyCostCents: 54_321 }); updateDemandRequirementMock.mockResolvedValue({}); updateAssignmentMock.mockResolvedValue({}); const updatedProject = { id: "project_1", startDate: new Date("2026-04-03"), endDate: new Date("2026-04-12"), }; const db = { project: { update: vi.fn().mockResolvedValue(updatedProject), }, auditLog: { create: vi.fn().mockResolvedValue({}), }, vacation: { findMany: vi.fn().mockResolvedValue([]), }, calculationRule: { findMany: vi.fn().mockResolvedValue([{ id: "rule_1", priority: 1 }]), }, $transaction: vi.fn(async (callback: (tx: typeof db) => unknown) => callback(db)), }; const context = { project: { id: "project_1", budgetCents: 200_000, winProbability: 80, startDate: new Date("2026-04-01"), endDate: new Date("2026-04-10"), }, demandRequirements: [ { id: "demand_1", }, ], assignments: [ { id: "assignment_staffed", resourceId: "resource_1", hoursPerDay: 8, metadata: { includeSaturday: true }, resource: { lcrCents: 5_000, availability: { monday: 8, tuesday: 8, wednesday: 8, thursday: 8, friday: 8, }, }, }, { id: "assignment_placeholder", resourceId: null, hoursPerDay: 4, metadata: {}, resource: null, }, ], shiftPlan: { validationAllocations: [ { id: "assignment_1", resourceId: "resource_1", startDate: new Date("2026-04-01"), endDate: new Date("2026-04-05"), hoursPerDay: 8, percentage: 100, role: "Compositing", dailyCostCents: 40_000, status: AllocationStatus.ACTIVE, resource: { id: "resource_1", displayName: "Alice", lcrCents: 5_000, availability: { monday: 8, tuesday: 8, wednesday: 8, thursday: 8, friday: 8, }, }, allAllocationsForResource: [], }, ], }, }; const result = await applyTimelineProjectShift({ db: db as never, projectId: "project_1", newStartDate: new Date("2026-04-03"), newEndDate: new Date("2026-04-12"), context: context as never, }); expect(db.project.update).toHaveBeenCalledWith({ where: { id: "project_1" }, data: { startDate: new Date("2026-04-03"), endDate: new Date("2026-04-12"), }, }); expect(updateDemandRequirementMock).toHaveBeenCalledWith( db, "demand_1", { startDate: new Date("2026-04-03"), endDate: new Date("2026-04-12"), }, ); expect(db.vacation.findMany).toHaveBeenCalledTimes(1); expect(db.calculationRule.findMany).toHaveBeenCalledTimes(1); expect(calculateAllocationMock).toHaveBeenCalledTimes(1); expect(updateAssignmentMock).toHaveBeenNthCalledWith( 1, db, "assignment_staffed", { startDate: new Date("2026-04-03"), endDate: new Date("2026-04-12"), dailyCostCents: 54_321, }, ); expect(updateAssignmentMock).toHaveBeenNthCalledWith( 2, db, "assignment_placeholder", { startDate: new Date("2026-04-03"), endDate: new Date("2026-04-12"), }, ); expect(db.auditLog.create).toHaveBeenCalledWith({ data: { entityType: "Project", entityId: "project_1", action: "SHIFT", changes: expect.objectContaining({ before: { startDate: new Date("2026-04-01"), endDate: new Date("2026-04-10"), }, after: { startDate: new Date("2026-04-03"), endDate: new Date("2026-04-12"), }, costImpact: validation.costImpact, }), }, }); expect(result).toEqual({ project: updatedProject, validation, event: buildTimelineProjectShiftEventPayload({ projectId: "project_1", newStartDate: new Date("2026-04-03"), newEndDate: new Date("2026-04-12"), validation, assignments: context.assignments as never, }), }); }); });