diff --git a/packages/api/src/__tests__/timeline-project-context-procedure-support.test.ts b/packages/api/src/__tests__/timeline-project-context-procedure-support.test.ts new file mode 100644 index 0000000..51a39a4 --- /dev/null +++ b/packages/api/src/__tests__/timeline-project-context-procedure-support.test.ts @@ -0,0 +1,140 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +vi.mock("../lib/anonymization.js", () => ({ + getAnonymizationDirectory: vi.fn(), +})); + +vi.mock("../router/timeline-project-load-support.js", () => ({ + loadTimelineProjectContext: vi.fn(), +})); + +vi.mock("../router/timeline-project-context-support.js", () => ({ + loadTimelineProjectContextDetailArtifacts: vi.fn(), +})); + +vi.mock("../router/timeline-project-context-response-support.js", () => ({ + buildTimelineProjectContextResponse: vi.fn(), + buildTimelineProjectContextDetailResponse: vi.fn(), +})); + +import { getAnonymizationDirectory } from "../lib/anonymization.js"; +import { + loadTimelineProjectContextDetailArtifacts, +} from "../router/timeline-project-context-support.js"; +import { + buildTimelineProjectContextDetailResponse, + buildTimelineProjectContextResponse, +} from "../router/timeline-project-context-response-support.js"; +import { loadTimelineProjectContext } from "../router/timeline-project-load-support.js"; +import { + readTimelineProjectContextDetailResponse, + readTimelineProjectContextResponse, +} from "../router/timeline-project-context-procedure-support.js"; + +const getAnonymizationDirectoryMock = vi.mocked(getAnonymizationDirectory); +const loadTimelineProjectContextMock = vi.mocked(loadTimelineProjectContext); +const loadTimelineProjectContextDetailArtifactsMock = vi.mocked(loadTimelineProjectContextDetailArtifacts); +const buildTimelineProjectContextResponseMock = vi.mocked(buildTimelineProjectContextResponse); +const buildTimelineProjectContextDetailResponseMock = vi.mocked(buildTimelineProjectContextDetailResponse); + +describe("timeline project context procedure support", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("builds project context responses from loaded context and anonymization directory", async () => { + loadTimelineProjectContextMock.mockResolvedValueOnce({ + project: { id: "project_1" }, + allocations: [{ id: "allocation_1" }], + demands: [{ id: "demand_1" }], + assignments: [{ id: "assignment_1" }], + allResourceAllocations: [{ id: "booking_1" }], + resourceIds: ["resource_1"], + } as never); + getAnonymizationDirectoryMock.mockResolvedValueOnce({ enabled: true } as never); + buildTimelineProjectContextResponseMock.mockReturnValueOnce({ ok: true } as never); + + await expect(readTimelineProjectContextResponse({} as never, "project_1")).resolves.toEqual({ + ok: true, + }); + + expect(loadTimelineProjectContextMock).toHaveBeenCalledWith({}, "project_1"); + expect(getAnonymizationDirectoryMock).toHaveBeenCalledWith({}); + expect(buildTimelineProjectContextResponseMock).toHaveBeenCalledWith({ + project: { id: "project_1" }, + allocations: [{ id: "allocation_1" }], + demands: [{ id: "demand_1" }], + assignments: [{ id: "assignment_1" }], + allResourceAllocations: [{ id: "booking_1" }], + resourceIds: ["resource_1"], + directory: { enabled: true }, + }); + }); + + it("builds project context detail responses from loaded artifacts", async () => { + loadTimelineProjectContextMock.mockResolvedValueOnce({ + 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", startDate: new Date("2026-04-01T00:00:00.000Z") }], + assignments: [{ id: "assignment_1", startDate: new Date("2026-04-02T00:00:00.000Z") }], + allResourceAllocations: [{ id: "booking_1" }], + resourceIds: ["resource_1"], + } as never); + getAnonymizationDirectoryMock.mockResolvedValueOnce({ enabled: true } as never); + loadTimelineProjectContextDetailArtifactsMock.mockResolvedValueOnce({ + period: { + startDate: new Date("2026-04-01T00:00:00.000Z"), + endDate: new Date("2026-04-10T00:00:00.000Z"), + }, + holidayOverlays: [{ id: "overlay_1" }], + assignmentConflicts: [{ assignmentId: "assignment_1" }], + } as never); + buildTimelineProjectContextDetailResponseMock.mockReturnValueOnce({ detail: true } as never); + + await expect( + readTimelineProjectContextDetailResponse({} as never, { + projectId: "project_1", + startDate: "2026-04-03", + endDate: "2026-04-12", + durationDays: 10, + }), + ).resolves.toEqual({ detail: true }); + + expect(loadTimelineProjectContextDetailArtifactsMock).toHaveBeenCalledWith({}, { + projectId: "project_1", + requestedStartDate: "2026-04-03", + requestedEndDate: "2026-04-12", + durationDays: 10, + projectStartDate: new Date("2026-04-01T00:00:00.000Z"), + projectEndDate: new Date("2026-04-10T00:00:00.000Z"), + firstAssignmentStartDate: new Date("2026-04-02T00:00:00.000Z"), + firstDemandStartDate: new Date("2026-04-01T00:00:00.000Z"), + assignments: [{ id: "assignment_1", startDate: new Date("2026-04-02T00:00:00.000Z") }], + allResourceAllocations: [{ id: "booking_1" }], + resourceIds: ["resource_1"], + }); + expect(buildTimelineProjectContextDetailResponseMock).toHaveBeenCalledWith({ + project: { + id: "project_1", + startDate: new Date("2026-04-01T00:00:00.000Z"), + endDate: new Date("2026-04-10T00:00:00.000Z"), + }, + period: { + 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", startDate: new Date("2026-04-01T00:00:00.000Z") }], + assignments: [{ id: "assignment_1", startDate: new Date("2026-04-02T00:00:00.000Z") }], + allResourceAllocations: [{ id: "booking_1" }], + resourceIds: ["resource_1"], + assignmentConflicts: [{ assignmentId: "assignment_1" }], + holidayOverlays: [{ id: "overlay_1" }], + directory: { enabled: true }, + }); + }); +}); diff --git a/packages/api/src/__tests__/timeline-project-procedure-support.test.ts b/packages/api/src/__tests__/timeline-project-procedure-support.test.ts index c6d3e2c..96f5ec1 100644 --- a/packages/api/src/__tests__/timeline-project-procedure-support.test.ts +++ b/packages/api/src/__tests__/timeline-project-procedure-support.test.ts @@ -1,23 +1,9 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; -vi.mock("../lib/anonymization.js", () => ({ - getAnonymizationDirectory: vi.fn(), -})); - vi.mock("../router/timeline-project-load-support.js", () => ({ - loadTimelineProjectContext: vi.fn(), previewTimelineProjectShift: vi.fn(), })); -vi.mock("../router/timeline-project-context-support.js", () => ({ - loadTimelineProjectContextDetailArtifacts: vi.fn(), -})); - -vi.mock("../router/timeline-project-context-response-support.js", () => ({ - buildTimelineProjectContextResponse: vi.fn(), - buildTimelineProjectContextDetailResponse: vi.fn(), -})); - vi.mock("@capakraken/application", () => ({ listAssignmentBookings: vi.fn(), })); @@ -40,16 +26,7 @@ vi.mock("../router/timeline-project-read-support.js", () => ({ import { listAssignmentBookings } from "@capakraken/application"; import { computeBudgetStatus } from "@capakraken/engine"; -import { getAnonymizationDirectory } from "../lib/anonymization.js"; import { - loadTimelineProjectContextDetailArtifacts, -} from "../router/timeline-project-context-support.js"; -import { - buildTimelineProjectContextDetailResponse, - buildTimelineProjectContextResponse, -} from "../router/timeline-project-context-response-support.js"; -import { - loadTimelineProjectContext, previewTimelineProjectShift, } from "../router/timeline-project-load-support.js"; import { @@ -64,17 +41,10 @@ import { } from "../router/timeline-project-query-support.js"; import { readTimelineProjectBudgetStatusResponse, - readTimelineProjectContextDetailResponse, - readTimelineProjectContextResponse, readTimelineProjectShiftPreviewDetail, } from "../router/timeline-project-procedure-support.js"; -const getAnonymizationDirectoryMock = vi.mocked(getAnonymizationDirectory); -const loadTimelineProjectContextMock = vi.mocked(loadTimelineProjectContext); const previewTimelineProjectShiftMock = vi.mocked(previewTimelineProjectShift); -const loadTimelineProjectContextDetailArtifactsMock = vi.mocked(loadTimelineProjectContextDetailArtifacts); -const buildTimelineProjectContextResponseMock = vi.mocked(buildTimelineProjectContextResponse); -const buildTimelineProjectContextDetailResponseMock = vi.mocked(buildTimelineProjectContextDetailResponse); const listAssignmentBookingsMock = vi.mocked(listAssignmentBookings); const computeBudgetStatusMock = vi.mocked(computeBudgetStatus); const findTimelineProjectOrThrowMock = vi.mocked(findTimelineProjectOrThrow); @@ -87,102 +57,6 @@ describe("timeline project procedure support", () => { vi.clearAllMocks(); }); - it("builds project context responses from loaded context and anonymization directory", async () => { - loadTimelineProjectContextMock.mockResolvedValueOnce({ - project: { id: "project_1" }, - allocations: [{ id: "allocation_1" }], - demands: [{ id: "demand_1" }], - assignments: [{ id: "assignment_1" }], - allResourceAllocations: [{ id: "booking_1" }], - resourceIds: ["resource_1"], - } as never); - getAnonymizationDirectoryMock.mockResolvedValueOnce({ enabled: true } as never); - buildTimelineProjectContextResponseMock.mockReturnValueOnce({ ok: true } as never); - - await expect(readTimelineProjectContextResponse({} as never, "project_1")).resolves.toEqual({ - ok: true, - }); - - expect(loadTimelineProjectContextMock).toHaveBeenCalledWith({}, "project_1"); - expect(getAnonymizationDirectoryMock).toHaveBeenCalledWith({}); - expect(buildTimelineProjectContextResponseMock).toHaveBeenCalledWith({ - project: { id: "project_1" }, - allocations: [{ id: "allocation_1" }], - demands: [{ id: "demand_1" }], - assignments: [{ id: "assignment_1" }], - allResourceAllocations: [{ id: "booking_1" }], - resourceIds: ["resource_1"], - directory: { enabled: true }, - }); - }); - - it("builds project context detail responses from loaded artifacts", async () => { - loadTimelineProjectContextMock.mockResolvedValueOnce({ - 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", startDate: new Date("2026-04-01T00:00:00.000Z") }], - assignments: [{ id: "assignment_1", startDate: new Date("2026-04-02T00:00:00.000Z") }], - allResourceAllocations: [{ id: "booking_1" }], - resourceIds: ["resource_1"], - } as never); - getAnonymizationDirectoryMock.mockResolvedValueOnce({ enabled: true } as never); - loadTimelineProjectContextDetailArtifactsMock.mockResolvedValueOnce({ - period: { - startDate: new Date("2026-04-01T00:00:00.000Z"), - endDate: new Date("2026-04-10T00:00:00.000Z"), - }, - holidayOverlays: [{ id: "overlay_1" }], - assignmentConflicts: [{ assignmentId: "assignment_1" }], - } as never); - buildTimelineProjectContextDetailResponseMock.mockReturnValueOnce({ detail: true } as never); - - await expect( - readTimelineProjectContextDetailResponse({} as never, { - projectId: "project_1", - startDate: "2026-04-03", - endDate: "2026-04-12", - durationDays: 10, - }), - ).resolves.toEqual({ detail: true }); - - expect(loadTimelineProjectContextDetailArtifactsMock).toHaveBeenCalledWith({}, { - projectId: "project_1", - requestedStartDate: "2026-04-03", - requestedEndDate: "2026-04-12", - durationDays: 10, - projectStartDate: new Date("2026-04-01T00:00:00.000Z"), - projectEndDate: new Date("2026-04-10T00:00:00.000Z"), - firstAssignmentStartDate: new Date("2026-04-02T00:00:00.000Z"), - firstDemandStartDate: new Date("2026-04-01T00:00:00.000Z"), - assignments: [{ id: "assignment_1", startDate: new Date("2026-04-02T00:00:00.000Z") }], - allResourceAllocations: [{ id: "booking_1" }], - resourceIds: ["resource_1"], - }); - expect(buildTimelineProjectContextDetailResponseMock).toHaveBeenCalledWith({ - project: { - id: "project_1", - startDate: new Date("2026-04-01T00:00:00.000Z"), - endDate: new Date("2026-04-10T00:00:00.000Z"), - }, - period: { - 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", startDate: new Date("2026-04-01T00:00:00.000Z") }], - assignments: [{ id: "assignment_1", startDate: new Date("2026-04-02T00:00:00.000Z") }], - allResourceAllocations: [{ id: "booking_1" }], - resourceIds: ["resource_1"], - assignmentConflicts: [{ assignmentId: "assignment_1" }], - holidayOverlays: [{ id: "overlay_1" }], - directory: { enabled: true }, - }); - }); - it("builds shift preview detail responses", async () => { findTimelineProjectOrThrowMock.mockResolvedValueOnce({ id: "project_1" } as never); previewTimelineProjectShiftMock.mockResolvedValueOnce({ valid: true } as never); diff --git a/packages/api/src/router/timeline-project-context-procedure-support.ts b/packages/api/src/router/timeline-project-context-procedure-support.ts new file mode 100644 index 0000000..991f7a7 --- /dev/null +++ b/packages/api/src/router/timeline-project-context-procedure-support.ts @@ -0,0 +1,79 @@ +import { getAnonymizationDirectory } from "../lib/anonymization.js"; +import { + loadTimelineProjectContextDetailArtifacts, +} from "./timeline-project-context-support.js"; +import { + buildTimelineProjectContextDetailResponse, + buildTimelineProjectContextResponse, +} from "./timeline-project-context-response-support.js"; +import { loadTimelineProjectContext } from "./timeline-project-load-support.js"; + +type TimelineProjectContextProcedureDb = + & Parameters[0] + & Parameters[0] + & Parameters[0]; + +export async function readTimelineProjectContextResponse( + db: TimelineProjectContextProcedureDb, + projectId: string, +) { + const { + project, + allocations, + demands, + assignments, + allResourceAllocations, + resourceIds, + } = await loadTimelineProjectContext(db, projectId); + const directory = await getAnonymizationDirectory(db); + + return buildTimelineProjectContextResponse({ + project, + allocations, + demands, + assignments, + allResourceAllocations, + resourceIds, + directory, + }); +} + +export async function readTimelineProjectContextDetailResponse( + db: TimelineProjectContextProcedureDb, + input: { + projectId: string; + startDate?: string | undefined; + endDate?: string | undefined; + durationDays?: number | undefined; + }, +) { + const projectContext = await loadTimelineProjectContext(db, input.projectId); + const directory = await getAnonymizationDirectory(db); + const { period, holidayOverlays, assignmentConflicts } = + await loadTimelineProjectContextDetailArtifacts(db, { + projectId: input.projectId, + requestedStartDate: input.startDate, + requestedEndDate: input.endDate, + durationDays: input.durationDays, + projectStartDate: projectContext.project.startDate, + projectEndDate: projectContext.project.endDate, + firstAssignmentStartDate: projectContext.assignments[0]?.startDate, + firstDemandStartDate: projectContext.demands[0]?.startDate, + assignments: projectContext.assignments, + allResourceAllocations: projectContext.allResourceAllocations, + resourceIds: projectContext.resourceIds, + }); + + return buildTimelineProjectContextDetailResponse({ + project: projectContext.project, + period, + allocations: projectContext.allocations, + demands: projectContext.demands, + assignments: projectContext.assignments, + allResourceAllocations: projectContext.allResourceAllocations, + resourceIds: projectContext.resourceIds, + assignmentConflicts, + holidayOverlays, + directory, + }); +} diff --git a/packages/api/src/router/timeline-project-procedure-support.ts b/packages/api/src/router/timeline-project-procedure-support.ts index 497e6f9..d036de1 100644 --- a/packages/api/src/router/timeline-project-procedure-support.ts +++ b/packages/api/src/router/timeline-project-procedure-support.ts @@ -1,15 +1,6 @@ import { listAssignmentBookings } from "@capakraken/application"; import { computeBudgetStatus } from "@capakraken/engine"; -import { getAnonymizationDirectory } from "../lib/anonymization.js"; import { - loadTimelineProjectContextDetailArtifacts, -} from "./timeline-project-context-support.js"; -import { - buildTimelineProjectContextDetailResponse, - buildTimelineProjectContextResponse, -} from "./timeline-project-context-response-support.js"; -import { - loadTimelineProjectContext, previewTimelineProjectShift, } from "./timeline-project-load-support.js"; import { @@ -24,77 +15,10 @@ import { } from "./timeline-project-query-support.js"; type TimelineProjectProcedureDb = - & Parameters[0] - & Parameters[0] - & Parameters[0] + & Parameters[0] & Parameters[0] & Parameters[0]; -export async function readTimelineProjectContextResponse( - db: TimelineProjectProcedureDb, - projectId: string, -) { - const { - project, - allocations, - demands, - assignments, - allResourceAllocations, - resourceIds, - } = await loadTimelineProjectContext(db, projectId); - const directory = await getAnonymizationDirectory(db); - - return buildTimelineProjectContextResponse({ - project, - allocations, - demands, - assignments, - allResourceAllocations, - resourceIds, - directory, - }); -} - -export async function readTimelineProjectContextDetailResponse( - db: TimelineProjectProcedureDb, - input: { - projectId: string; - startDate?: string | undefined; - endDate?: string | undefined; - durationDays?: number | undefined; - }, -) { - const projectContext = await loadTimelineProjectContext(db, input.projectId); - const directory = await getAnonymizationDirectory(db); - const { period, holidayOverlays, assignmentConflicts } = - await loadTimelineProjectContextDetailArtifacts(db, { - projectId: input.projectId, - requestedStartDate: input.startDate, - requestedEndDate: input.endDate, - durationDays: input.durationDays, - projectStartDate: projectContext.project.startDate, - projectEndDate: projectContext.project.endDate, - firstAssignmentStartDate: projectContext.assignments[0]?.startDate, - firstDemandStartDate: projectContext.demands[0]?.startDate, - assignments: projectContext.assignments, - allResourceAllocations: projectContext.allResourceAllocations, - resourceIds: projectContext.resourceIds, - }); - - return buildTimelineProjectContextDetailResponse({ - project: projectContext.project, - period, - allocations: projectContext.allocations, - demands: projectContext.demands, - assignments: projectContext.assignments, - allResourceAllocations: projectContext.allResourceAllocations, - resourceIds: projectContext.resourceIds, - assignmentConflicts, - holidayOverlays, - directory, - }); -} - export async function readTimelineProjectShiftPreviewDetail( db: TimelineProjectProcedureDb, input: { diff --git a/packages/api/src/router/timeline-project-read.ts b/packages/api/src/router/timeline-project-read.ts index c574104..21f62c0 100644 --- a/packages/api/src/router/timeline-project-read.ts +++ b/packages/api/src/router/timeline-project-read.ts @@ -6,9 +6,11 @@ import { TimelineProjectContextDetailSchema, } from "./timeline-read-shared.js"; import { - readTimelineProjectBudgetStatusResponse, readTimelineProjectContextDetailResponse, readTimelineProjectContextResponse, +} from "./timeline-project-context-procedure-support.js"; +import { + readTimelineProjectBudgetStatusResponse, readTimelineProjectShiftPreviewDetail, } from "./timeline-project-procedure-support.js";