176 lines
5.4 KiB
TypeScript
176 lines
5.4 KiB
TypeScript
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<typeof import("@capakraken/application")>();
|
|
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.",
|
|
}),
|
|
);
|
|
});
|
|
});
|