From 7b4c659922fd2c78e79d6734586a653f35aacdc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hartmut=20N=C3=B6renberg?= Date: Tue, 31 Mar 2026 18:08:19 +0200 Subject: [PATCH] refactor(api): extract timeline allocation assignment procedures --- ...ation-assignment-procedure-support.test.ts | 128 ++++++++++++++++++ ...eline-allocation-procedure-support.test.ts | 122 +---------------- ...allocation-assignment-procedure-support.ts | 69 ++++++++++ .../router/timeline-allocation-mutations.ts | 4 +- .../timeline-allocation-procedure-support.ts | 72 +--------- 5 files changed, 202 insertions(+), 193 deletions(-) create mode 100644 packages/api/src/__tests__/timeline-allocation-assignment-procedure-support.test.ts create mode 100644 packages/api/src/router/timeline-allocation-assignment-procedure-support.ts diff --git a/packages/api/src/__tests__/timeline-allocation-assignment-procedure-support.test.ts b/packages/api/src/__tests__/timeline-allocation-assignment-procedure-support.test.ts new file mode 100644 index 0000000..f9414fc --- /dev/null +++ b/packages/api/src/__tests__/timeline-allocation-assignment-procedure-support.test.ts @@ -0,0 +1,128 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +vi.mock("@capakraken/application", () => ({ + buildSplitAllocationReadModel: vi.fn(), + createAssignment: vi.fn(), +})); + +vi.mock("../sse/event-bus.js", () => ({ + emitAllocationCreated: vi.fn(), +})); + +import { + buildSplitAllocationReadModel, + createAssignment, +} from "@capakraken/application"; +import { emitAllocationCreated } from "../sse/event-bus.js"; +import { + createTimelineBatchQuickAssignments, + createTimelineQuickAssignment, +} from "../router/timeline-allocation-assignment-procedure-support.js"; + +const buildSplitAllocationReadModelMock = vi.mocked(buildSplitAllocationReadModel); +const createAssignmentMock = vi.mocked(createAssignment); +const emitAllocationCreatedMock = vi.mocked(emitAllocationCreated); + +describe("timeline allocation assignment procedure support", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("creates a quick assignment and emits the allocation created event", async () => { + const db = { + $transaction: vi.fn(async (callback: (tx: unknown) => unknown) => callback(db)), + } as never; + + createAssignmentMock.mockResolvedValueOnce({ id: "assignment_1" } as never); + buildSplitAllocationReadModelMock.mockReturnValueOnce({ + allocations: [ + { + id: "allocation_1", + projectId: "project_1", + resourceId: "resource_1", + }, + ], + } as never); + + await expect( + createTimelineQuickAssignment(db, { + resourceId: "resource_1", + projectId: "project_1", + startDate: new Date("2026-04-01T00:00:00.000Z"), + endDate: new Date("2026-04-05T00:00:00.000Z"), + hoursPerDay: 8, + role: "Team Member", + status: "PROPOSED", + source: "quickAssign", + }), + ).resolves.toEqual({ + id: "allocation_1", + projectId: "project_1", + resourceId: "resource_1", + }); + + expect(createAssignmentMock).toHaveBeenCalledOnce(); + expect(emitAllocationCreatedMock).toHaveBeenCalledWith({ + id: "allocation_1", + projectId: "project_1", + resourceId: "resource_1", + }); + }); + + it("creates batch quick assignments and emits events for each result", async () => { + const db = { + $transaction: vi.fn(async (callback: (tx: unknown) => unknown) => callback(db)), + } as never; + + createAssignmentMock + .mockResolvedValueOnce({ + id: "assignment_1", + projectId: "project_1", + resourceId: "resource_1", + } as never) + .mockResolvedValueOnce({ + id: "assignment_2", + projectId: "project_2", + resourceId: "resource_2", + } as never); + + await expect( + createTimelineBatchQuickAssignments(db, { + assignments: [ + { + resourceId: "resource_1", + projectId: "project_1", + startDate: new Date("2026-04-01T00:00:00.000Z"), + endDate: new Date("2026-04-05T00:00:00.000Z"), + hoursPerDay: 8, + role: "Team Member", + status: "PROPOSED", + source: "batchQuickAssign", + }, + { + resourceId: "resource_2", + projectId: "project_2", + startDate: new Date("2026-04-06T00:00:00.000Z"), + endDate: new Date("2026-04-10T00:00:00.000Z"), + hoursPerDay: 6, + role: "Lead", + status: "PROPOSED", + source: "batchQuickAssign", + }, + ], + }), + ).resolves.toEqual({ count: 2 }); + + expect(createAssignmentMock).toHaveBeenCalledTimes(2); + expect(emitAllocationCreatedMock).toHaveBeenNthCalledWith(1, { + id: "assignment_1", + projectId: "project_1", + resourceId: "resource_1", + }); + expect(emitAllocationCreatedMock).toHaveBeenNthCalledWith(2, { + id: "assignment_2", + projectId: "project_2", + resourceId: "resource_2", + }); + }); +}); diff --git a/packages/api/src/__tests__/timeline-allocation-procedure-support.test.ts b/packages/api/src/__tests__/timeline-allocation-procedure-support.test.ts index f710666..166d7b0 100644 --- a/packages/api/src/__tests__/timeline-allocation-procedure-support.test.ts +++ b/packages/api/src/__tests__/timeline-allocation-procedure-support.test.ts @@ -1,12 +1,6 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; -vi.mock("@capakraken/application", () => ({ - buildSplitAllocationReadModel: vi.fn(), - createAssignment: vi.fn(), -})); - vi.mock("../sse/event-bus.js", () => ({ - emitAllocationCreated: vi.fn(), emitAllocationUpdated: vi.fn(), })); @@ -14,24 +8,10 @@ vi.mock("../router/timeline-allocation-shift-support.js", () => ({ applyTimelineBatchAllocationShift: vi.fn(), })); -import { - buildSplitAllocationReadModel, - createAssignment, -} from "@capakraken/application"; -import { - emitAllocationCreated, - emitAllocationUpdated, -} from "../sse/event-bus.js"; -import { - createTimelineBatchQuickAssignments, - createTimelineQuickAssignment, - shiftTimelineAllocations, -} from "../router/timeline-allocation-procedure-support.js"; +import { emitAllocationUpdated } from "../sse/event-bus.js"; +import { shiftTimelineAllocations } from "../router/timeline-allocation-procedure-support.js"; import { applyTimelineBatchAllocationShift } from "../router/timeline-allocation-shift-support.js"; -const buildSplitAllocationReadModelMock = vi.mocked(buildSplitAllocationReadModel); -const createAssignmentMock = vi.mocked(createAssignment); -const emitAllocationCreatedMock = vi.mocked(emitAllocationCreated); const emitAllocationUpdatedMock = vi.mocked(emitAllocationUpdated); const applyTimelineBatchAllocationShiftMock = vi.mocked(applyTimelineBatchAllocationShift); @@ -40,104 +20,6 @@ describe("timeline allocation procedure support", () => { vi.clearAllMocks(); }); - it("creates a quick assignment and emits the allocation created event", async () => { - const db = { - $transaction: vi.fn(async (callback: (tx: unknown) => unknown) => callback(db)), - } as never; - - createAssignmentMock.mockResolvedValueOnce({ id: "assignment_1" } as never); - buildSplitAllocationReadModelMock.mockReturnValueOnce({ - allocations: [ - { - id: "allocation_1", - projectId: "project_1", - resourceId: "resource_1", - }, - ], - } as never); - - await expect( - createTimelineQuickAssignment(db, { - resourceId: "resource_1", - projectId: "project_1", - startDate: new Date("2026-04-01T00:00:00.000Z"), - endDate: new Date("2026-04-05T00:00:00.000Z"), - hoursPerDay: 8, - role: "Team Member", - status: "PROPOSED", - source: "quickAssign", - }), - ).resolves.toEqual({ - id: "allocation_1", - projectId: "project_1", - resourceId: "resource_1", - }); - - expect(createAssignmentMock).toHaveBeenCalledOnce(); - expect(emitAllocationCreatedMock).toHaveBeenCalledWith({ - id: "allocation_1", - projectId: "project_1", - resourceId: "resource_1", - }); - }); - - it("creates batch quick assignments and emits events for each result", async () => { - const db = { - $transaction: vi.fn(async (callback: (tx: unknown) => unknown) => callback(db)), - } as never; - - createAssignmentMock - .mockResolvedValueOnce({ - id: "assignment_1", - projectId: "project_1", - resourceId: "resource_1", - } as never) - .mockResolvedValueOnce({ - id: "assignment_2", - projectId: "project_2", - resourceId: "resource_2", - } as never); - - await expect( - createTimelineBatchQuickAssignments(db, { - assignments: [ - { - resourceId: "resource_1", - projectId: "project_1", - startDate: new Date("2026-04-01T00:00:00.000Z"), - endDate: new Date("2026-04-05T00:00:00.000Z"), - hoursPerDay: 8, - role: "Team Member", - status: "PROPOSED", - source: "batchQuickAssign", - }, - { - resourceId: "resource_2", - projectId: "project_2", - startDate: new Date("2026-04-06T00:00:00.000Z"), - endDate: new Date("2026-04-10T00:00:00.000Z"), - hoursPerDay: 6, - role: "Lead", - status: "PROPOSED", - source: "batchQuickAssign", - }, - ], - }), - ).resolves.toEqual({ count: 2 }); - - expect(createAssignmentMock).toHaveBeenCalledTimes(2); - expect(emitAllocationCreatedMock).toHaveBeenNthCalledWith(1, { - id: "assignment_1", - projectId: "project_1", - resourceId: "resource_1", - }); - expect(emitAllocationCreatedMock).toHaveBeenNthCalledWith(2, { - id: "assignment_2", - projectId: "project_2", - resourceId: "resource_2", - }); - }); - it("applies allocation shifts and emits update events", async () => { applyTimelineBatchAllocationShiftMock.mockResolvedValueOnce([ { diff --git a/packages/api/src/router/timeline-allocation-assignment-procedure-support.ts b/packages/api/src/router/timeline-allocation-assignment-procedure-support.ts new file mode 100644 index 0000000..838f3bb --- /dev/null +++ b/packages/api/src/router/timeline-allocation-assignment-procedure-support.ts @@ -0,0 +1,69 @@ +import { + buildSplitAllocationReadModel, + createAssignment, +} from "@capakraken/application"; +import type { PrismaClient } from "@capakraken/db"; +import { emitAllocationCreated } from "../sse/event-bus.js"; +import { + assertTimelineDateRangeValid, + validateTimelineAllocationDateRanges, +} from "./timeline-allocation-mutation-support.js"; +import { buildTimelineQuickAssignAssignmentInput } from "./timeline-allocation-quick-assign-support.js"; + +export async function createTimelineQuickAssignment( + db: PrismaClient, + input: Parameters[0], +) { + assertTimelineDateRangeValid(input.startDate, input.endDate); + + const allocation = await db.$transaction(async (tx) => { + const assignment = await createAssignment( + tx as unknown as Parameters[0], + buildTimelineQuickAssignAssignmentInput(input), + ); + + return buildSplitAllocationReadModel({ + demandRequirements: [], + assignments: [assignment], + }).allocations[0]!; + }); + + emitAllocationCreated({ + id: allocation.id, + projectId: allocation.projectId, + resourceId: allocation.resourceId, + }); + + return allocation; +} + +export async function createTimelineBatchQuickAssignments( + db: PrismaClient, + input: { + assignments: Array[0]>; + }, +) { + validateTimelineAllocationDateRanges(input.assignments); + + const results = await db.$transaction(async (tx) => { + const created = []; + for (const assignment of input.assignments) { + const createdAssignment = await createAssignment( + tx as unknown as Parameters[0], + buildTimelineQuickAssignAssignmentInput(assignment), + ); + created.push(createdAssignment); + } + return created; + }); + + for (const assignment of results) { + emitAllocationCreated({ + id: assignment.id, + projectId: assignment.projectId, + resourceId: assignment.resourceId, + }); + } + + return { count: results.length }; +} diff --git a/packages/api/src/router/timeline-allocation-mutations.ts b/packages/api/src/router/timeline-allocation-mutations.ts index 6936f3f..130e5b6 100644 --- a/packages/api/src/router/timeline-allocation-mutations.ts +++ b/packages/api/src/router/timeline-allocation-mutations.ts @@ -5,8 +5,8 @@ import { managerProcedure, requirePermission } from "../trpc.js"; import { createTimelineBatchQuickAssignments, createTimelineQuickAssignment, - shiftTimelineAllocations, -} from "./timeline-allocation-procedure-support.js"; +} from "./timeline-allocation-assignment-procedure-support.js"; +import { shiftTimelineAllocations } from "./timeline-allocation-procedure-support.js"; import { UpdateAllocationHoursSchema, timelineBatchQuickAssignInputSchema, diff --git a/packages/api/src/router/timeline-allocation-procedure-support.ts b/packages/api/src/router/timeline-allocation-procedure-support.ts index c0aa181..7068f71 100644 --- a/packages/api/src/router/timeline-allocation-procedure-support.ts +++ b/packages/api/src/router/timeline-allocation-procedure-support.ts @@ -1,77 +1,7 @@ -import { - buildSplitAllocationReadModel, - createAssignment, -} from "@capakraken/application"; import type { PrismaClient } from "@capakraken/db"; -import { - emitAllocationCreated, - emitAllocationUpdated, -} from "../sse/event-bus.js"; -import { - assertTimelineDateRangeValid, - validateTimelineAllocationDateRanges, -} from "./timeline-allocation-mutation-support.js"; -import { buildTimelineQuickAssignAssignmentInput } from "./timeline-allocation-quick-assign-support.js"; +import { emitAllocationUpdated } from "../sse/event-bus.js"; import { applyTimelineBatchAllocationShift } from "./timeline-allocation-shift-support.js"; -export async function createTimelineQuickAssignment( - db: PrismaClient, - input: Parameters[0], -) { - assertTimelineDateRangeValid(input.startDate, input.endDate); - - const allocation = await db.$transaction(async (tx) => { - const assignment = await createAssignment( - tx as unknown as Parameters[0], - buildTimelineQuickAssignAssignmentInput(input), - ); - - return buildSplitAllocationReadModel({ - demandRequirements: [], - assignments: [assignment], - }).allocations[0]!; - }); - - emitAllocationCreated({ - id: allocation.id, - projectId: allocation.projectId, - resourceId: allocation.resourceId, - }); - - return allocation; -} - -export async function createTimelineBatchQuickAssignments( - db: PrismaClient, - input: { - assignments: Array[0]>; - }, -) { - validateTimelineAllocationDateRanges(input.assignments); - - const results = await db.$transaction(async (tx) => { - const created = []; - for (const assignment of input.assignments) { - const createdAssignment = await createAssignment( - tx as unknown as Parameters[0], - buildTimelineQuickAssignAssignmentInput(assignment), - ); - created.push(createdAssignment); - } - return created; - }); - - for (const assignment of results) { - emitAllocationCreated({ - id: assignment.id, - projectId: assignment.projectId, - resourceId: assignment.resourceId, - }); - } - - return { count: results.length }; -} - export async function shiftTimelineAllocations( db: PrismaClient, input: Omit[0], "db">,