diff --git a/packages/api/src/__tests__/timeline-shift-router-support.test.ts b/packages/api/src/__tests__/timeline-shift-router-support.test.ts new file mode 100644 index 0000000..3e04b3d --- /dev/null +++ b/packages/api/src/__tests__/timeline-shift-router-support.test.ts @@ -0,0 +1,59 @@ +import { describe, expect, it, vi } from "vitest"; + +vi.mock("../sse/event-bus.js", () => ({ + emitProjectShifted: vi.fn(), +})); + +vi.mock("../router/timeline-project-load-support.js", () => ({ + loadProjectShiftContext: vi.fn(), +})); + +vi.mock("../router/timeline-shift-procedure-support.js", () => ({ + applyTimelineProjectShift: vi.fn(), +})); + +import { emitProjectShifted } from "../sse/event-bus.js"; +import { loadProjectShiftContext } from "../router/timeline-project-load-support.js"; +import { applyTimelineProjectShiftMutation } from "../router/timeline-shift-router-support.js"; +import { applyTimelineProjectShift } from "../router/timeline-shift-procedure-support.js"; + +const emitProjectShiftedMock = vi.mocked(emitProjectShifted); +const loadProjectShiftContextMock = vi.mocked(loadProjectShiftContext); +const applyTimelineProjectShiftMock = vi.mocked(applyTimelineProjectShift); + +describe("timeline shift router support", () => { + it("loads context, applies the shift, emits the event, and returns the response payload", async () => { + const db = {} as never; + const context = { project: { id: "project_1" } } as never; + const result = { + project: { id: "project_1" }, + validation: { valid: true }, + event: { projectId: "project_1" }, + } as never; + + loadProjectShiftContextMock.mockResolvedValueOnce(context); + applyTimelineProjectShiftMock.mockResolvedValueOnce(result); + + await expect( + applyTimelineProjectShiftMutation({ + db, + projectId: "project_1", + newStartDate: new Date("2026-04-03T00:00:00.000Z"), + newEndDate: new Date("2026-04-12T00:00:00.000Z"), + }), + ).resolves.toEqual({ + project: { id: "project_1" }, + validation: { valid: true }, + }); + + expect(loadProjectShiftContextMock).toHaveBeenCalledWith(db, "project_1"); + expect(applyTimelineProjectShiftMock).toHaveBeenCalledWith({ + db, + projectId: "project_1", + newStartDate: new Date("2026-04-03T00:00:00.000Z"), + newEndDate: new Date("2026-04-12T00:00:00.000Z"), + context, + }); + expect(emitProjectShiftedMock).toHaveBeenCalledWith({ projectId: "project_1" }); + }); +}); diff --git a/packages/api/src/router/timeline-mutations.ts b/packages/api/src/router/timeline-mutations.ts new file mode 100644 index 0000000..b8876a0 --- /dev/null +++ b/packages/api/src/router/timeline-mutations.ts @@ -0,0 +1,21 @@ +import { PermissionKey, ShiftProjectSchema } from "@capakraken/shared"; +import { managerProcedure, requirePermission } from "../trpc.js"; +import { timelineAllocationMutationProcedures } from "./timeline-allocation-mutations.js"; +import { applyTimelineProjectShiftMutation } from "./timeline-shift-router-support.js"; + +export const timelineMutationProcedures = { + ...timelineAllocationMutationProcedures, + + applyShift: managerProcedure + .input(ShiftProjectSchema) + .mutation(async ({ ctx, input }) => { + requirePermission(ctx, PermissionKey.MANAGE_ALLOCATIONS); + + return applyTimelineProjectShiftMutation({ + db: ctx.db, + projectId: input.projectId, + newStartDate: input.newStartDate, + newEndDate: input.newEndDate, + }); + }), +}; diff --git a/packages/api/src/router/timeline-shift-router-support.ts b/packages/api/src/router/timeline-shift-router-support.ts new file mode 100644 index 0000000..f360fa5 --- /dev/null +++ b/packages/api/src/router/timeline-shift-router-support.ts @@ -0,0 +1,27 @@ +import type { PrismaClient } from "@capakraken/db"; +import { emitProjectShifted } from "../sse/event-bus.js"; +import { loadProjectShiftContext } from "./timeline-project-load-support.js"; +import { applyTimelineProjectShift } from "./timeline-shift-procedure-support.js"; + +export async function applyTimelineProjectShiftMutation(input: { + db: PrismaClient; + projectId: string; + newStartDate: Date; + newEndDate: Date; +}) { + const context = await loadProjectShiftContext(input.db, input.projectId); + const result = await applyTimelineProjectShift({ + db: input.db, + projectId: input.projectId, + newStartDate: input.newStartDate, + newEndDate: input.newEndDate, + context, + }); + + emitProjectShifted(result.event); + + return { + project: result.project, + validation: result.validation, + }; +} diff --git a/packages/api/src/router/timeline.ts b/packages/api/src/router/timeline.ts index e70cb6a..d0934bb 100644 --- a/packages/api/src/router/timeline.ts +++ b/packages/api/src/router/timeline.ts @@ -1,38 +1,8 @@ -import { PermissionKey, ShiftProjectSchema } from "@capakraken/shared"; -import { emitProjectShifted } from "../sse/event-bus.js"; -import { createTRPCRouter, managerProcedure, requirePermission } from "../trpc.js"; -import { timelineAllocationMutationProcedures } from "./timeline-allocation-mutations.js"; +import { createTRPCRouter } from "../trpc.js"; +import { timelineMutationProcedures } from "./timeline-mutations.js"; import { timelineReadProcedures } from "./timeline-read.js"; -import { loadProjectShiftContext } from "./timeline-project-load-support.js"; -import { applyTimelineProjectShift } from "./timeline-shift-procedure-support.js"; export const timelineRouter = createTRPCRouter({ ...timelineReadProcedures, - ...timelineAllocationMutationProcedures, - - /** - * Apply a project shift: validate, then commit all allocation date changes. - * Reads includeSaturday from each allocation's metadata. - */ - applyShift: managerProcedure - .input(ShiftProjectSchema) - .mutation(async ({ ctx, input }) => { - requirePermission(ctx, PermissionKey.MANAGE_ALLOCATIONS); - const { projectId, newStartDate, newEndDate } = input; - const context = await loadProjectShiftContext(ctx.db, projectId); - const result = await applyTimelineProjectShift({ - db: ctx.db, - projectId, - newStartDate, - newEndDate, - context, - }); - - emitProjectShifted(result.event); - - return { - project: result.project, - validation: result.validation, - }; - }), + ...timelineMutationProcedures, });