import { TRPCError } from "@trpc/server"; import { beforeEach, describe, expect, it, vi } from "vitest"; const { loadAllocationEntryMock, updateAssignmentMock, createAssignmentMock, deleteAllocationEntryMock, } = vi.hoisted(() => ({ loadAllocationEntryMock: vi.fn(), updateAssignmentMock: vi.fn(), createAssignmentMock: vi.fn(), deleteAllocationEntryMock: vi.fn(), })); vi.mock("@capakraken/application", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, loadAllocationEntry: loadAllocationEntryMock, updateAssignment: updateAssignmentMock, createAssignment: createAssignmentMock, deleteAllocationEntry: deleteAllocationEntryMock, }; }); import { carveTimelineAllocationRange } from "../router/timeline-allocation-fragment-support.js"; function createResolvedAssignment() { return { kind: "assignment" as const, entry: { id: "assignment_1", startDate: new Date("2026-04-06T00:00:00.000Z"), endDate: new Date("2026-04-17T00:00:00.000Z"), hoursPerDay: 8, metadata: {}, }, assignment: { id: "assignment_1", demandRequirementId: null, resourceId: "resource_1", projectId: "project_1", startDate: new Date("2026-04-06T00:00:00.000Z"), endDate: new Date("2026-04-17T00:00:00.000Z"), hoursPerDay: 8, percentage: 100, role: "Artist", roleId: "role_1", dailyCostCents: 80000, status: "ACTIVE", metadata: {}, }, projectId: "project_1", resourceId: "resource_1", }; } describe("timeline allocation fragment support", () => { beforeEach(() => { vi.clearAllMocks(); }); it("splits an assignment into left and right fragments around the carved range", async () => { loadAllocationEntryMock.mockResolvedValue(createResolvedAssignment()); updateAssignmentMock.mockResolvedValue({ id: "assignment_1" }); createAssignmentMock.mockResolvedValue({ id: "assignment_2" }); const db = { $transaction: vi.fn(async (callback: (tx: unknown) => unknown) => callback(db)), }; const result = await carveTimelineAllocationRange({ db: db as never, allocationId: "assignment_1", startDate: new Date("2026-04-09T00:00:00.000Z"), endDate: new Date("2026-04-10T00:00:00.000Z"), }); expect(result).toEqual({ action: "split", allocationGroupId: expect.any(String), updatedAllocationIds: ["assignment_1"], createdAllocationIds: ["assignment_2"], deletedAllocationIds: [], projectId: "project_1", resourceId: "resource_1", }); expect(updateAssignmentMock).toHaveBeenCalledWith( db, "assignment_1", expect.objectContaining({ startDate: new Date("2026-04-06T00:00:00.000Z"), endDate: new Date("2026-04-08T00:00:00.000Z"), }), ); expect(createAssignmentMock).toHaveBeenCalledWith( db, expect.objectContaining({ resourceId: "resource_1", projectId: "project_1", startDate: new Date("2026-04-11T00:00:00.000Z"), endDate: new Date("2026-04-17T00:00:00.000Z"), }), ); }); it("shrinks the existing assignment when carving from the start edge", async () => { loadAllocationEntryMock.mockResolvedValue(createResolvedAssignment()); updateAssignmentMock.mockResolvedValue({ id: "assignment_1" }); const db = { $transaction: vi.fn(async (callback: (tx: unknown) => unknown) => callback(db)), }; const result = await carveTimelineAllocationRange({ db: db as never, allocationId: "assignment_1", startDate: new Date("2026-04-06T00:00:00.000Z"), endDate: new Date("2026-04-08T00:00:00.000Z"), }); expect(result.action).toBe("updated"); expect(createAssignmentMock).not.toHaveBeenCalled(); expect(updateAssignmentMock).toHaveBeenCalledWith( db, "assignment_1", expect.objectContaining({ startDate: new Date("2026-04-09T00:00:00.000Z"), endDate: new Date("2026-04-17T00:00:00.000Z"), }), ); }); it("deletes the assignment when the carved range covers the full interval", async () => { loadAllocationEntryMock.mockResolvedValue(createResolvedAssignment()); const db = { $transaction: vi.fn(async (callback: (tx: unknown) => unknown) => callback(db)), }; const result = await carveTimelineAllocationRange({ db: db as never, allocationId: "assignment_1", startDate: new Date("2026-04-06T00:00:00.000Z"), endDate: new Date("2026-04-17T00:00:00.000Z"), }); expect(result.action).toBe("deleted"); expect(deleteAllocationEntryMock).toHaveBeenCalledWith( db, expect.objectContaining({ assignment: expect.objectContaining({ id: "assignment_1" }), }), ); }); it("rejects carve ranges outside the allocation interval", async () => { loadAllocationEntryMock.mockResolvedValue(createResolvedAssignment()); await expect( carveTimelineAllocationRange({ db: { $transaction: vi.fn() } as never, allocationId: "assignment_1", startDate: new Date("2026-04-05T00:00:00.000Z"), endDate: new Date("2026-04-06T00:00:00.000Z"), }), ).rejects.toThrowError( new TRPCError({ code: "BAD_REQUEST", message: "The requested carve range must be fully inside the existing allocation.", }), ); }); });