refactor(api): extract timeline shift procedure support

This commit is contained in:
2026-03-31 17:50:36 +02:00
parent 109bf70699
commit eef91a1068
5 changed files with 378 additions and 275 deletions
@@ -0,0 +1,96 @@
import {
updateAssignment,
updateDemandRequirement,
} from "@capakraken/application";
import type { PrismaClient } from "@capakraken/db";
import {
assertTimelineProjectShiftValid,
buildTimelineProjectShiftEventPayload,
buildTimelineProjectShiftValidation,
type LoadedTimelineShiftContext,
} from "./timeline-shift-support.js";
import {
buildTimelineProjectDateRangeUpdate,
buildTimelineProjectShiftAuditChanges,
buildTimelineShiftedAssignmentUpdate,
recalculateShiftedAssignmentDailyCost,
} from "./timeline-shift-mutation-support.js";
export interface ApplyTimelineProjectShiftInput {
db: PrismaClient;
projectId: string;
newStartDate: Date;
newEndDate: Date;
context: LoadedTimelineShiftContext;
}
export async function applyTimelineProjectShift(input: ApplyTimelineProjectShiftInput) {
const validation = buildTimelineProjectShiftValidation({
context: input.context,
newStartDate: input.newStartDate,
newEndDate: input.newEndDate,
});
assertTimelineProjectShiftValid(validation);
const updatedProject = await input.db.$transaction(async (tx) => {
const projectRecord = await tx.project.update({
where: { id: input.projectId },
data: buildTimelineProjectDateRangeUpdate(input),
});
for (const demandRequirement of input.context.demandRequirements) {
await updateDemandRequirement(
tx as unknown as Parameters<typeof updateDemandRequirement>[0],
demandRequirement.id,
buildTimelineProjectDateRangeUpdate(input),
);
}
for (const assignment of input.context.assignments) {
const dailyCostCents = await recalculateShiftedAssignmentDailyCost({
db: input.db,
assignment,
newStartDate: input.newStartDate,
newEndDate: input.newEndDate,
});
await updateAssignment(
tx as unknown as Parameters<typeof updateAssignment>[0],
assignment.id,
buildTimelineShiftedAssignmentUpdate({
newStartDate: input.newStartDate,
newEndDate: input.newEndDate,
dailyCostCents,
}),
);
}
await tx.auditLog.create({
data: {
entityType: "Project",
entityId: input.projectId,
action: "SHIFT",
changes: buildTimelineProjectShiftAuditChanges({
project: input.context.project,
newStartDate: input.newStartDate,
newEndDate: input.newEndDate,
validation,
}),
},
});
return projectRecord;
});
return {
project: updatedProject,
validation,
event: buildTimelineProjectShiftEventPayload({
projectId: input.projectId,
newStartDate: input.newStartDate,
newEndDate: input.newEndDate,
validation,
assignments: input.context.assignments,
}),
};
}
@@ -1,20 +1,11 @@
import {
updateAssignment,
updateDemandRequirement,
type SplitAssignmentRecord,
type SplitDemandRequirementRecord,
import type {
SplitAssignmentRecord,
SplitDemandRequirementRecord,
} from "@capakraken/application";
import type { PrismaClient } from "@capakraken/db";
import { validateShift } from "@capakraken/engine";
import type { ShiftValidationResult } from "@capakraken/shared";
import { TRPCError } from "@trpc/server";
import type { TimelineShiftPlan } from "./timeline-shift-planning.js";
import {
buildTimelineProjectDateRangeUpdate,
buildTimelineProjectShiftAuditChanges,
buildTimelineShiftedAssignmentUpdate,
recalculateShiftedAssignmentDailyCost,
} from "./timeline-shift-mutation-support.js";
export interface TimelineShiftProjectRecord {
id: string;
@@ -31,14 +22,6 @@ export interface LoadedTimelineShiftContext {
shiftPlan: TimelineShiftPlan;
}
export interface ApplyTimelineProjectShiftInput {
db: PrismaClient;
projectId: string;
newStartDate: Date;
newEndDate: Date;
context: LoadedTimelineShiftContext;
}
export interface TimelineProjectShiftEventPayload extends Record<string, unknown> {
projectId: string;
newStartDate: string;
@@ -96,74 +79,3 @@ export function buildTimelineProjectShiftEventPayload(input: {
resourceIds: input.assignments.map((assignment) => assignment.resourceId),
};
}
export async function applyTimelineProjectShift(input: ApplyTimelineProjectShiftInput) {
const validation = buildTimelineProjectShiftValidation({
context: input.context,
newStartDate: input.newStartDate,
newEndDate: input.newEndDate,
});
assertTimelineProjectShiftValid(validation);
const updatedProject = await input.db.$transaction(async (tx) => {
const projectRecord = await tx.project.update({
where: { id: input.projectId },
data: buildTimelineProjectDateRangeUpdate(input),
});
for (const demandRequirement of input.context.demandRequirements) {
await updateDemandRequirement(
tx as unknown as Parameters<typeof updateDemandRequirement>[0],
demandRequirement.id,
buildTimelineProjectDateRangeUpdate(input),
);
}
for (const assignment of input.context.assignments) {
const dailyCostCents = await recalculateShiftedAssignmentDailyCost({
db: input.db,
assignment,
newStartDate: input.newStartDate,
newEndDate: input.newEndDate,
});
await updateAssignment(
tx as unknown as Parameters<typeof updateAssignment>[0],
assignment.id,
buildTimelineShiftedAssignmentUpdate({
newStartDate: input.newStartDate,
newEndDate: input.newEndDate,
dailyCostCents,
}),
);
}
await tx.auditLog.create({
data: {
entityType: "Project",
entityId: input.projectId,
action: "SHIFT",
changes: buildTimelineProjectShiftAuditChanges({
project: input.context.project,
newStartDate: input.newStartDate,
newEndDate: input.newEndDate,
validation,
}),
},
});
return projectRecord;
});
return {
project: updatedProject,
validation,
event: buildTimelineProjectShiftEventPayload({
projectId: input.projectId,
newStartDate: input.newStartDate,
newEndDate: input.newEndDate,
validation,
assignments: input.context.assignments,
}),
};
}
+1 -1
View File
@@ -4,7 +4,7 @@ import { createTRPCRouter, managerProcedure, requirePermission } from "../trpc.j
import { timelineAllocationMutationProcedures } from "./timeline-allocation-mutations.js";
import { timelineReadProcedures } from "./timeline-read.js";
import { loadProjectShiftContext } from "./timeline-project-load-support.js";
import { applyTimelineProjectShift } from "./timeline-shift-support.js";
import { applyTimelineProjectShift } from "./timeline-shift-procedure-support.js";
export const timelineRouter = createTRPCRouter({
...timelineReadProcedures,