refactor(api): extract timeline project load support
This commit is contained in:
@@ -0,0 +1,165 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
vi.mock("@capakraken/application", () => ({
|
||||||
|
listAssignmentBookings: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("../router/project-planning-read-model.js", () => ({
|
||||||
|
loadProjectPlanningReadModel: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("../router/timeline-project-query-support.js", () => ({
|
||||||
|
findTimelineProjectOrThrow: vi.fn(),
|
||||||
|
projectShiftContextSelect: { id: true },
|
||||||
|
timelineProjectContextSelect: { id: true },
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("../router/timeline-shift-planning.js", () => ({
|
||||||
|
buildTimelineShiftPlan: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("../router/timeline-project-read-support.js", () => ({
|
||||||
|
buildTimelineShiftValidationBookings: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("../router/timeline-read-shared.js", async () => {
|
||||||
|
const actual = await vi.importActual<typeof import("../router/timeline-read-shared.js")>(
|
||||||
|
"../router/timeline-read-shared.js",
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
getAssignmentResourceIds: vi.fn(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.mock("../router/timeline-shift-support.js", () => ({
|
||||||
|
buildTimelineProjectShiftValidation: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { listAssignmentBookings } from "@capakraken/application";
|
||||||
|
import { loadProjectPlanningReadModel } from "../router/project-planning-read-model.js";
|
||||||
|
import {
|
||||||
|
loadProjectShiftContext,
|
||||||
|
loadTimelineProjectContext,
|
||||||
|
previewTimelineProjectShift,
|
||||||
|
} from "../router/timeline-project-load-support.js";
|
||||||
|
import { findTimelineProjectOrThrow } from "../router/timeline-project-query-support.js";
|
||||||
|
import { buildTimelineShiftPlan } from "../router/timeline-shift-planning.js";
|
||||||
|
import { buildTimelineShiftValidationBookings } from "../router/timeline-project-read-support.js";
|
||||||
|
import { getAssignmentResourceIds } from "../router/timeline-read-shared.js";
|
||||||
|
import { buildTimelineProjectShiftValidation } from "../router/timeline-shift-support.js";
|
||||||
|
|
||||||
|
const findTimelineProjectOrThrowMock = vi.mocked(findTimelineProjectOrThrow);
|
||||||
|
const loadProjectPlanningReadModelMock = vi.mocked(loadProjectPlanningReadModel);
|
||||||
|
const listAssignmentBookingsMock = vi.mocked(listAssignmentBookings);
|
||||||
|
const getAssignmentResourceIdsMock = vi.mocked(getAssignmentResourceIds);
|
||||||
|
const buildTimelineShiftValidationBookingsMock = vi.mocked(buildTimelineShiftValidationBookings);
|
||||||
|
const buildTimelineShiftPlanMock = vi.mocked(buildTimelineShiftPlan);
|
||||||
|
const buildTimelineProjectShiftValidationMock = vi.mocked(buildTimelineProjectShiftValidation);
|
||||||
|
|
||||||
|
describe("timeline project load support", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("loads shift context with booking windows when assignment resources exist", async () => {
|
||||||
|
findTimelineProjectOrThrowMock.mockResolvedValueOnce({ id: "project_1" } as never);
|
||||||
|
loadProjectPlanningReadModelMock.mockResolvedValueOnce({
|
||||||
|
demandRequirements: [{ id: "demand_1" }],
|
||||||
|
assignments: [{ id: "assignment_1" }],
|
||||||
|
readModel: {
|
||||||
|
allocations: [],
|
||||||
|
demands: [],
|
||||||
|
assignments: [{ resourceId: "resource_1" }],
|
||||||
|
},
|
||||||
|
} as never);
|
||||||
|
getAssignmentResourceIdsMock.mockReturnValueOnce(["resource_1"]);
|
||||||
|
listAssignmentBookingsMock.mockResolvedValueOnce([{ id: "booking_1" }] as never);
|
||||||
|
buildTimelineShiftValidationBookingsMock.mockReturnValueOnce([{ id: "window_1" }] as never);
|
||||||
|
buildTimelineShiftPlanMock.mockReturnValueOnce({ shifts: 1 } as never);
|
||||||
|
|
||||||
|
await expect(loadProjectShiftContext({} as never, "project_1")).resolves.toEqual({
|
||||||
|
project: { id: "project_1" },
|
||||||
|
demandRequirements: [{ id: "demand_1" }],
|
||||||
|
assignments: [{ id: "assignment_1" }],
|
||||||
|
shiftPlan: { shifts: 1 },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(listAssignmentBookingsMock).toHaveBeenCalledWith({}, {
|
||||||
|
resourceIds: ["resource_1"],
|
||||||
|
});
|
||||||
|
expect(buildTimelineShiftValidationBookingsMock).toHaveBeenCalledWith([{ id: "booking_1" }]);
|
||||||
|
expect(buildTimelineShiftPlanMock).toHaveBeenCalledWith({
|
||||||
|
demandRequirements: [{ id: "demand_1" }],
|
||||||
|
assignments: [{ id: "assignment_1" }],
|
||||||
|
allAssignmentWindows: [{ id: "window_1" }],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("loads project context and skips booking lookup without assignment resources", async () => {
|
||||||
|
findTimelineProjectOrThrowMock.mockResolvedValueOnce({
|
||||||
|
id: "project_1",
|
||||||
|
startDate: new Date("2026-04-01T00:00:00.000Z"),
|
||||||
|
endDate: new Date("2026-04-10T00:00:00.000Z"),
|
||||||
|
} as never);
|
||||||
|
loadProjectPlanningReadModelMock.mockResolvedValueOnce({
|
||||||
|
readModel: {
|
||||||
|
allocations: [{ id: "allocation_1" }],
|
||||||
|
demands: [{ id: "demand_1" }],
|
||||||
|
assignments: [{ id: "assignment_1" }],
|
||||||
|
},
|
||||||
|
} as never);
|
||||||
|
getAssignmentResourceIdsMock.mockReturnValueOnce([]);
|
||||||
|
|
||||||
|
await expect(loadTimelineProjectContext({} as never, "project_1")).resolves.toEqual({
|
||||||
|
project: {
|
||||||
|
id: "project_1",
|
||||||
|
startDate: new Date("2026-04-01T00:00:00.000Z"),
|
||||||
|
endDate: new Date("2026-04-10T00:00:00.000Z"),
|
||||||
|
},
|
||||||
|
allocations: [{ id: "allocation_1" }],
|
||||||
|
demands: [{ id: "demand_1" }],
|
||||||
|
assignments: [{ id: "assignment_1" }],
|
||||||
|
allResourceAllocations: [],
|
||||||
|
resourceIds: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(listAssignmentBookingsMock).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("builds project shift previews from loaded context", async () => {
|
||||||
|
findTimelineProjectOrThrowMock.mockResolvedValueOnce({ id: "project_1" } as never);
|
||||||
|
loadProjectPlanningReadModelMock.mockResolvedValueOnce({
|
||||||
|
demandRequirements: [{ id: "demand_1" }],
|
||||||
|
assignments: [{ id: "assignment_1" }],
|
||||||
|
readModel: {
|
||||||
|
allocations: [],
|
||||||
|
demands: [],
|
||||||
|
assignments: [{ resourceId: null }],
|
||||||
|
},
|
||||||
|
} as never);
|
||||||
|
getAssignmentResourceIdsMock.mockReturnValueOnce([]);
|
||||||
|
buildTimelineShiftPlanMock.mockReturnValueOnce({ shifts: [] } as never);
|
||||||
|
buildTimelineProjectShiftValidationMock.mockReturnValueOnce({ ok: true } as never);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
previewTimelineProjectShift({} as never, {
|
||||||
|
projectId: "project_1",
|
||||||
|
newStartDate: new Date("2026-04-02T00:00:00.000Z"),
|
||||||
|
newEndDate: new Date("2026-04-08T00:00:00.000Z"),
|
||||||
|
}),
|
||||||
|
).resolves.toEqual({ ok: true });
|
||||||
|
|
||||||
|
expect(buildTimelineProjectShiftValidationMock).toHaveBeenCalledWith({
|
||||||
|
context: {
|
||||||
|
project: { id: "project_1" },
|
||||||
|
demandRequirements: [{ id: "demand_1" }],
|
||||||
|
assignments: [{ id: "assignment_1" }],
|
||||||
|
shiftPlan: { shifts: [] },
|
||||||
|
},
|
||||||
|
newStartDate: new Date("2026-04-02T00:00:00.000Z"),
|
||||||
|
newEndDate: new Date("2026-04-08T00:00:00.000Z"),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
import { listAssignmentBookings } from "@capakraken/application";
|
||||||
|
import { loadProjectPlanningReadModel } from "./project-planning-read-model.js";
|
||||||
|
import {
|
||||||
|
findTimelineProjectOrThrow,
|
||||||
|
projectShiftContextSelect,
|
||||||
|
timelineProjectContextSelect,
|
||||||
|
} from "./timeline-project-query-support.js";
|
||||||
|
import { buildTimelineShiftPlan } from "./timeline-shift-planning.js";
|
||||||
|
import { getAssignmentResourceIds, ShiftDbClient } from "./timeline-read-shared.js";
|
||||||
|
import { buildTimelineProjectShiftValidation } from "./timeline-shift-support.js";
|
||||||
|
import { buildTimelineShiftValidationBookings } from "./timeline-project-read-support.js";
|
||||||
|
|
||||||
|
export async function loadProjectShiftContext(db: ShiftDbClient, projectId: string) {
|
||||||
|
const [project, planningRead] = await Promise.all([
|
||||||
|
findTimelineProjectOrThrow(db, {
|
||||||
|
projectId,
|
||||||
|
select: projectShiftContextSelect,
|
||||||
|
}),
|
||||||
|
loadProjectPlanningReadModel(db, { projectId, activeOnly: true }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const { demandRequirements, assignments, readModel: projectReadModel } = planningRead;
|
||||||
|
|
||||||
|
const resourceIds = getAssignmentResourceIds(projectReadModel);
|
||||||
|
const allAssignmentWindows =
|
||||||
|
resourceIds.length === 0
|
||||||
|
? []
|
||||||
|
: buildTimelineShiftValidationBookings(
|
||||||
|
await listAssignmentBookings(db, {
|
||||||
|
resourceIds,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const shiftPlan = buildTimelineShiftPlan({
|
||||||
|
demandRequirements,
|
||||||
|
assignments,
|
||||||
|
allAssignmentWindows,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
project,
|
||||||
|
demandRequirements,
|
||||||
|
assignments,
|
||||||
|
shiftPlan,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadTimelineProjectContext(db: ShiftDbClient, projectId: string) {
|
||||||
|
const [project, planningRead] = await Promise.all([
|
||||||
|
findTimelineProjectOrThrow(db, {
|
||||||
|
projectId,
|
||||||
|
select: timelineProjectContextSelect,
|
||||||
|
}),
|
||||||
|
loadProjectPlanningReadModel(db, {
|
||||||
|
projectId,
|
||||||
|
activeOnly: true,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const resourceIds = getAssignmentResourceIds(planningRead.readModel);
|
||||||
|
const allResourceAllocations =
|
||||||
|
resourceIds.length === 0
|
||||||
|
? []
|
||||||
|
: await listAssignmentBookings(db, {
|
||||||
|
resourceIds,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
project,
|
||||||
|
allocations: planningRead.readModel.allocations,
|
||||||
|
demands: planningRead.readModel.demands,
|
||||||
|
assignments: planningRead.readModel.assignments,
|
||||||
|
allResourceAllocations,
|
||||||
|
resourceIds,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function previewTimelineProjectShift(
|
||||||
|
db: ShiftDbClient,
|
||||||
|
input: {
|
||||||
|
projectId: string;
|
||||||
|
newStartDate: Date;
|
||||||
|
newEndDate: Date;
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
const context = await loadProjectShiftContext(db, input.projectId);
|
||||||
|
|
||||||
|
return buildTimelineProjectShiftValidation({
|
||||||
|
context,
|
||||||
|
newStartDate: input.newStartDate,
|
||||||
|
newEndDate: input.newEndDate,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -4,10 +4,13 @@ import { ShiftProjectSchema } from "@capakraken/shared";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { getAnonymizationDirectory } from "../lib/anonymization.js";
|
import { getAnonymizationDirectory } from "../lib/anonymization.js";
|
||||||
import { controllerProcedure } from "../trpc.js";
|
import { controllerProcedure } from "../trpc.js";
|
||||||
import { loadProjectPlanningReadModel } from "./project-planning-read-model.js";
|
|
||||||
import {
|
import {
|
||||||
loadTimelineProjectContextDetailArtifacts,
|
loadTimelineProjectContextDetailArtifacts,
|
||||||
} from "./timeline-project-context-support.js";
|
} from "./timeline-project-context-support.js";
|
||||||
|
import {
|
||||||
|
loadTimelineProjectContext,
|
||||||
|
previewTimelineProjectShift,
|
||||||
|
} from "./timeline-project-load-support.js";
|
||||||
import {
|
import {
|
||||||
buildTimelineProjectContextDetailResponse,
|
buildTimelineProjectContextDetailResponse,
|
||||||
buildTimelineProjectContextResponse,
|
buildTimelineProjectContextResponse,
|
||||||
@@ -16,100 +19,12 @@ import {
|
|||||||
buildTimelineBudgetStatusAllocations,
|
buildTimelineBudgetStatusAllocations,
|
||||||
buildTimelineBudgetStatusResponse,
|
buildTimelineBudgetStatusResponse,
|
||||||
buildTimelineShiftPreviewDetailResponse,
|
buildTimelineShiftPreviewDetailResponse,
|
||||||
buildTimelineShiftValidationBookings,
|
|
||||||
} from "./timeline-project-read-support.js";
|
} from "./timeline-project-read-support.js";
|
||||||
import {
|
import {
|
||||||
findTimelineProjectOrThrow,
|
findTimelineProjectOrThrow,
|
||||||
projectShiftContextSelect,
|
|
||||||
timelineBudgetStatusProjectSelect,
|
timelineBudgetStatusProjectSelect,
|
||||||
timelineProjectContextSelect,
|
|
||||||
timelineShiftPreviewProjectSelect,
|
timelineShiftPreviewProjectSelect,
|
||||||
} from "./timeline-project-query-support.js";
|
} from "./timeline-project-query-support.js";
|
||||||
import { buildTimelineShiftPlan } from "./timeline-shift-planning.js";
|
|
||||||
import { getAssignmentResourceIds, ShiftDbClient } from "./timeline-read-shared.js";
|
|
||||||
import { buildTimelineProjectShiftValidation } from "./timeline-shift-support.js";
|
|
||||||
|
|
||||||
export async function loadProjectShiftContext(db: ShiftDbClient, projectId: string) {
|
|
||||||
const [project, planningRead] = await Promise.all([
|
|
||||||
findTimelineProjectOrThrow(db, {
|
|
||||||
projectId,
|
|
||||||
select: projectShiftContextSelect,
|
|
||||||
}),
|
|
||||||
loadProjectPlanningReadModel(db, { projectId, activeOnly: true }),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const { demandRequirements, assignments, readModel: projectReadModel } = planningRead;
|
|
||||||
|
|
||||||
const resourceIds = getAssignmentResourceIds(projectReadModel);
|
|
||||||
const allAssignmentWindows =
|
|
||||||
resourceIds.length === 0
|
|
||||||
? []
|
|
||||||
: buildTimelineShiftValidationBookings(
|
|
||||||
await listAssignmentBookings(db, {
|
|
||||||
resourceIds,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const shiftPlan = buildTimelineShiftPlan({
|
|
||||||
demandRequirements,
|
|
||||||
assignments,
|
|
||||||
allAssignmentWindows,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
project,
|
|
||||||
demandRequirements,
|
|
||||||
assignments,
|
|
||||||
shiftPlan,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function loadTimelineProjectContext(db: ShiftDbClient, projectId: string) {
|
|
||||||
const [project, planningRead] = await Promise.all([
|
|
||||||
findTimelineProjectOrThrow(db, {
|
|
||||||
projectId,
|
|
||||||
select: timelineProjectContextSelect,
|
|
||||||
}),
|
|
||||||
loadProjectPlanningReadModel(db, {
|
|
||||||
projectId,
|
|
||||||
activeOnly: true,
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const resourceIds = getAssignmentResourceIds(planningRead.readModel);
|
|
||||||
const allResourceAllocations =
|
|
||||||
resourceIds.length === 0
|
|
||||||
? []
|
|
||||||
: await listAssignmentBookings(db, {
|
|
||||||
resourceIds,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
project,
|
|
||||||
allocations: planningRead.readModel.allocations,
|
|
||||||
demands: planningRead.readModel.demands,
|
|
||||||
assignments: planningRead.readModel.assignments,
|
|
||||||
allResourceAllocations,
|
|
||||||
resourceIds,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function previewTimelineProjectShift(
|
|
||||||
db: ShiftDbClient,
|
|
||||||
input: {
|
|
||||||
projectId: string;
|
|
||||||
newStartDate: Date;
|
|
||||||
newEndDate: Date;
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
const context = await loadProjectShiftContext(db, input.projectId);
|
|
||||||
|
|
||||||
return buildTimelineProjectShiftValidation({
|
|
||||||
context,
|
|
||||||
newStartDate: input.newStartDate,
|
|
||||||
newEndDate: input.newEndDate,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export const timelineProjectReadProcedures = {
|
export const timelineProjectReadProcedures = {
|
||||||
getProjectContext: controllerProcedure
|
getProjectContext: controllerProcedure
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { emitProjectShifted } from "../sse/event-bus.js";
|
|||||||
import { createTRPCRouter, managerProcedure, requirePermission } from "../trpc.js";
|
import { createTRPCRouter, managerProcedure, requirePermission } from "../trpc.js";
|
||||||
import { timelineAllocationMutationProcedures } from "./timeline-allocation-mutations.js";
|
import { timelineAllocationMutationProcedures } from "./timeline-allocation-mutations.js";
|
||||||
import { timelineReadProcedures } from "./timeline-read.js";
|
import { timelineReadProcedures } from "./timeline-read.js";
|
||||||
import { loadProjectShiftContext } from "./timeline-project-read.js";
|
import { loadProjectShiftContext } from "./timeline-project-load-support.js";
|
||||||
import { applyTimelineProjectShift } from "./timeline-shift-support.js";
|
import { applyTimelineProjectShift } from "./timeline-shift-support.js";
|
||||||
|
|
||||||
export const timelineRouter = createTRPCRouter({
|
export const timelineRouter = createTRPCRouter({
|
||||||
|
|||||||
Reference in New Issue
Block a user