refactor(api): extract timeline allocation mutation payload helpers
This commit is contained in:
@@ -2,11 +2,14 @@ import { TRPCError } from "@trpc/server";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
assertTimelineDateRangeValid,
|
||||
buildTimelineAllocationEntryUpdate,
|
||||
buildTimelineAllocationMetadata,
|
||||
buildTimelineBatchShiftAuditChanges,
|
||||
buildTimelineQuickAssignAssignmentInput,
|
||||
buildTimelineQuickAssignMetadata,
|
||||
calculateTimelineAllocationPercentage,
|
||||
shiftTimelineAllocationWindow,
|
||||
validateTimelineAllocationDateRanges,
|
||||
} from "../router/timeline-allocation-mutation-support.js";
|
||||
|
||||
describe("timeline allocation mutation support", () => {
|
||||
@@ -46,6 +49,33 @@ describe("timeline allocation mutation support", () => {
|
||||
expect(buildTimelineQuickAssignMetadata("batchQuickAssign")).toEqual({ source: "batchQuickAssign" });
|
||||
});
|
||||
|
||||
it("builds assignment create payloads for quick assign flows", () => {
|
||||
expect(
|
||||
buildTimelineQuickAssignAssignmentInput({
|
||||
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: 6,
|
||||
role: "Engineer",
|
||||
roleId: "role_1",
|
||||
status: "PROPOSED",
|
||||
source: "quickAssign",
|
||||
}),
|
||||
).toEqual({
|
||||
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: 6,
|
||||
percentage: 75,
|
||||
role: "Engineer",
|
||||
roleId: "role_1",
|
||||
status: "PROPOSED",
|
||||
metadata: { source: "quickAssign" },
|
||||
});
|
||||
});
|
||||
|
||||
it("shifts and clamps allocation windows for each batch-shift mode", () => {
|
||||
expect(
|
||||
shiftTimelineAllocationWindow({
|
||||
@@ -98,4 +128,47 @@ describe("timeline allocation mutation support", () => {
|
||||
count: 2,
|
||||
});
|
||||
});
|
||||
|
||||
it("validates date ranges in bulk", () => {
|
||||
expect(() =>
|
||||
validateTimelineAllocationDateRanges([
|
||||
{
|
||||
startDate: new Date("2026-04-10T00:00:00.000Z"),
|
||||
endDate: new Date("2026-04-11T00:00:00.000Z"),
|
||||
},
|
||||
{
|
||||
startDate: new Date("2026-04-12T00:00:00.000Z"),
|
||||
endDate: new Date("2026-04-09T00:00:00.000Z"),
|
||||
},
|
||||
])).toThrowError(TRPCError);
|
||||
});
|
||||
|
||||
it("builds shared update payloads for demand and assignment changes", () => {
|
||||
expect(
|
||||
buildTimelineAllocationEntryUpdate({
|
||||
hoursPerDay: 7.5,
|
||||
startDate: new Date("2026-04-01T00:00:00.000Z"),
|
||||
endDate: new Date("2026-04-10T00:00:00.000Z"),
|
||||
metadata: { includeSaturday: true },
|
||||
dailyCostCents: 48000,
|
||||
role: "Architect",
|
||||
}),
|
||||
).toEqual({
|
||||
demandRequirementUpdate: {
|
||||
hoursPerDay: 7.5,
|
||||
startDate: new Date("2026-04-01T00:00:00.000Z"),
|
||||
endDate: new Date("2026-04-10T00:00:00.000Z"),
|
||||
metadata: { includeSaturday: true },
|
||||
role: "Architect",
|
||||
},
|
||||
assignmentUpdate: {
|
||||
hoursPerDay: 7.5,
|
||||
startDate: new Date("2026-04-01T00:00:00.000Z"),
|
||||
endDate: new Date("2026-04-10T00:00:00.000Z"),
|
||||
dailyCostCents: 48000,
|
||||
metadata: { includeSaturday: true },
|
||||
role: "Architect",
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Prisma } from "@capakraken/db";
|
||||
import { AllocationStatus } from "@capakraken/shared";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
export type TimelineBatchShiftMode = "move" | "resize-start" | "resize-end";
|
||||
@@ -39,6 +40,39 @@ export function buildTimelineQuickAssignMetadata(source: "quickAssign" | "batchQ
|
||||
return { source } satisfies Record<string, unknown>;
|
||||
}
|
||||
|
||||
export function buildTimelineQuickAssignAssignmentInput(input: {
|
||||
resourceId: string;
|
||||
projectId: string;
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
hoursPerDay: number;
|
||||
role: string;
|
||||
roleId?: string | undefined;
|
||||
status: AllocationStatus;
|
||||
source: "quickAssign" | "batchQuickAssign";
|
||||
}) {
|
||||
return {
|
||||
resourceId: input.resourceId,
|
||||
projectId: input.projectId,
|
||||
startDate: input.startDate,
|
||||
endDate: input.endDate,
|
||||
hoursPerDay: input.hoursPerDay,
|
||||
percentage: calculateTimelineAllocationPercentage(input.hoursPerDay),
|
||||
role: input.role,
|
||||
...(input.roleId !== undefined ? { roleId: input.roleId } : {}),
|
||||
status: input.status,
|
||||
metadata: buildTimelineQuickAssignMetadata(input.source),
|
||||
};
|
||||
}
|
||||
|
||||
export function validateTimelineAllocationDateRanges(
|
||||
ranges: Array<{ startDate: Date; endDate: Date }>,
|
||||
): void {
|
||||
for (const range of ranges) {
|
||||
assertTimelineDateRangeValid(range.startDate, range.endDate);
|
||||
}
|
||||
}
|
||||
|
||||
export function shiftTimelineAllocationWindow(input: {
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
@@ -98,6 +132,33 @@ export function buildTimelineAllocationUpdateAuditChanges(input: {
|
||||
} as unknown as Prisma.InputJsonValue;
|
||||
}
|
||||
|
||||
export function buildTimelineAllocationEntryUpdate(input: {
|
||||
hoursPerDay: number;
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
metadata: Record<string, unknown>;
|
||||
dailyCostCents: number;
|
||||
role?: string | undefined;
|
||||
}) {
|
||||
return {
|
||||
demandRequirementUpdate: {
|
||||
hoursPerDay: input.hoursPerDay,
|
||||
startDate: input.startDate,
|
||||
endDate: input.endDate,
|
||||
metadata: input.metadata,
|
||||
...(input.role !== undefined ? { role: input.role } : {}),
|
||||
},
|
||||
assignmentUpdate: {
|
||||
hoursPerDay: input.hoursPerDay,
|
||||
startDate: input.startDate,
|
||||
endDate: input.endDate,
|
||||
dailyCostCents: input.dailyCostCents,
|
||||
metadata: input.metadata,
|
||||
...(input.role !== undefined ? { role: input.role } : {}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function buildTimelineBatchShiftAuditChanges(input: {
|
||||
mode: TimelineBatchShiftMode;
|
||||
daysDelta: number;
|
||||
|
||||
@@ -22,12 +22,13 @@ import {
|
||||
import { managerProcedure, requirePermission } from "../trpc.js";
|
||||
import {
|
||||
assertTimelineDateRangeValid,
|
||||
buildTimelineAllocationEntryUpdate,
|
||||
buildTimelineAllocationMetadata,
|
||||
buildTimelineAllocationUpdateAuditChanges,
|
||||
buildTimelineBatchShiftAuditChanges,
|
||||
buildTimelineQuickAssignMetadata,
|
||||
calculateTimelineAllocationPercentage,
|
||||
buildTimelineQuickAssignAssignmentInput,
|
||||
shiftTimelineAllocationWindow,
|
||||
validateTimelineAllocationDateRanges,
|
||||
} from "./timeline-allocation-mutation-support.js";
|
||||
import { calculateTimelineAllocationDailyCost } from "./timeline-cost-support.js";
|
||||
|
||||
@@ -81,21 +82,14 @@ export const timelineAllocationMutationProcedures = {
|
||||
tx as unknown as Parameters<typeof updateAllocationEntry>[0],
|
||||
{
|
||||
id: input.allocationId,
|
||||
demandRequirementUpdate: {
|
||||
...buildTimelineAllocationEntryUpdate({
|
||||
hoursPerDay: newHoursPerDay,
|
||||
startDate: newStartDate,
|
||||
endDate: newEndDate,
|
||||
metadata: newMeta,
|
||||
...(input.role !== undefined ? { role: input.role } : {}),
|
||||
},
|
||||
assignmentUpdate: {
|
||||
hoursPerDay: newHoursPerDay,
|
||||
startDate: newStartDate,
|
||||
endDate: newEndDate,
|
||||
dailyCostCents: newDailyCostCents,
|
||||
metadata: newMeta,
|
||||
...(input.role !== undefined ? { role: input.role } : {}),
|
||||
},
|
||||
role: input.role,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
@@ -147,24 +141,13 @@ export const timelineAllocationMutationProcedures = {
|
||||
requirePermission(ctx, PermissionKey.MANAGE_ALLOCATIONS);
|
||||
assertTimelineDateRangeValid(input.startDate, input.endDate);
|
||||
|
||||
const percentage = calculateTimelineAllocationPercentage(input.hoursPerDay);
|
||||
const metadata = buildTimelineQuickAssignMetadata("quickAssign");
|
||||
|
||||
const allocation = await ctx.db.$transaction(async (tx) => {
|
||||
const assignment = await createAssignment(
|
||||
tx as unknown as Parameters<typeof createAssignment>[0],
|
||||
{
|
||||
resourceId: input.resourceId,
|
||||
projectId: input.projectId,
|
||||
startDate: input.startDate,
|
||||
endDate: input.endDate,
|
||||
hoursPerDay: input.hoursPerDay,
|
||||
percentage,
|
||||
role: input.role,
|
||||
roleId: input.roleId ?? undefined,
|
||||
status: input.status,
|
||||
metadata,
|
||||
},
|
||||
buildTimelineQuickAssignAssignmentInput({
|
||||
...input,
|
||||
source: "quickAssign",
|
||||
}),
|
||||
);
|
||||
|
||||
return buildSplitAllocationReadModel({
|
||||
@@ -205,30 +188,17 @@ export const timelineAllocationMutationProcedures = {
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
requirePermission(ctx, PermissionKey.MANAGE_ALLOCATIONS);
|
||||
|
||||
for (const assignment of input.assignments) {
|
||||
assertTimelineDateRangeValid(assignment.startDate, assignment.endDate);
|
||||
}
|
||||
validateTimelineAllocationDateRanges(input.assignments);
|
||||
|
||||
const results = await ctx.db.$transaction(async (tx) => {
|
||||
const created = [];
|
||||
for (const assignment of input.assignments) {
|
||||
const percentage = calculateTimelineAllocationPercentage(assignment.hoursPerDay);
|
||||
const metadata = buildTimelineQuickAssignMetadata("batchQuickAssign");
|
||||
|
||||
const createdAssignment = await createAssignment(
|
||||
tx as unknown as Parameters<typeof createAssignment>[0],
|
||||
{
|
||||
resourceId: assignment.resourceId,
|
||||
projectId: assignment.projectId,
|
||||
startDate: assignment.startDate,
|
||||
endDate: assignment.endDate,
|
||||
hoursPerDay: assignment.hoursPerDay,
|
||||
percentage,
|
||||
role: assignment.role,
|
||||
status: assignment.status,
|
||||
metadata,
|
||||
},
|
||||
buildTimelineQuickAssignAssignmentInput({
|
||||
...assignment,
|
||||
source: "batchQuickAssign",
|
||||
}),
|
||||
);
|
||||
created.push(createdAssignment);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user