refactor(api): extract allocation router support modules

This commit is contained in:
2026-03-31 10:46:25 +02:00
parent 8fdc83aea1
commit daed9c2d16
5 changed files with 667 additions and 627 deletions
@@ -0,0 +1,178 @@
import { buildSplitAllocationReadModel, loadAllocationEntry } from "@capakraken/application";
import { AllocationStatus, CreateDemandRequirementSchema } from "@capakraken/shared";
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { findUniqueOrThrow } from "../db/helpers.js";
import { anonymizeResource, getAnonymizationDirectory } from "../lib/anonymization.js";
import { ASSIGNMENT_INCLUDE, type AllocationEntryUpdateInput, type AllocationListFilters, type AssignmentResolutionInput, type CreateDemandDraftInput, DEMAND_INCLUDE, toIsoDate } from "./allocation-shared.js";
export function toDemandRequirementUpdateInput(input: AllocationEntryUpdateInput) {
return {
...(input.projectId !== undefined ? { projectId: input.projectId } : {}),
...(input.startDate !== undefined ? { startDate: input.startDate } : {}),
...(input.endDate !== undefined ? { endDate: input.endDate } : {}),
...(input.hoursPerDay !== undefined ? { hoursPerDay: input.hoursPerDay } : {}),
...(input.percentage !== undefined ? { percentage: input.percentage } : {}),
...(input.role !== undefined ? { role: input.role } : {}),
...(input.roleId !== undefined ? { roleId: input.roleId } : {}),
...(input.headcount !== undefined ? { headcount: input.headcount } : {}),
...(input.budgetCents !== undefined ? { budgetCents: input.budgetCents } : {}),
...(input.status !== undefined ? { status: input.status } : {}),
...(input.metadata !== undefined ? { metadata: input.metadata } : {}),
};
}
export function toAssignmentUpdateInput(input: AllocationEntryUpdateInput) {
return {
...(input.resourceId !== undefined ? { resourceId: input.resourceId } : {}),
...(input.projectId !== undefined ? { projectId: input.projectId } : {}),
...(input.startDate !== undefined ? { startDate: input.startDate } : {}),
...(input.endDate !== undefined ? { endDate: input.endDate } : {}),
...(input.hoursPerDay !== undefined ? { hoursPerDay: input.hoursPerDay } : {}),
...(input.percentage !== undefined ? { percentage: input.percentage } : {}),
...(input.role !== undefined ? { role: input.role } : {}),
...(input.roleId !== undefined ? { roleId: input.roleId } : {}),
...(input.status !== undefined ? { status: input.status } : {}),
...(input.metadata !== undefined ? { metadata: input.metadata } : {}),
};
}
export async function loadAllocationReadModel(
db: Pick<import("@capakraken/db").PrismaClient, "demandRequirement" | "assignment" | "systemSettings" | "resource">,
input: AllocationListFilters,
) {
const [demandRequirements, assignments] = await Promise.all([
input.resourceId
? Promise.resolve([])
: db.demandRequirement.findMany({
where: {
...(input.projectId ? { projectId: input.projectId } : {}),
...(input.status ? { status: input.status } : {}),
},
include: DEMAND_INCLUDE,
orderBy: { startDate: "asc" },
}),
db.assignment.findMany({
where: {
...(input.projectId ? { projectId: input.projectId } : {}),
...(input.resourceId ? { resourceId: input.resourceId } : {}),
...(input.status ? { status: input.status } : {}),
},
include: ASSIGNMENT_INCLUDE,
orderBy: { startDate: "asc" },
}),
]);
const readModel = buildSplitAllocationReadModel({ demandRequirements, assignments });
const directory = await getAnonymizationDirectory(db as import("@capakraken/db").PrismaClient);
if (!directory) {
return readModel;
}
function anonymizeAllocation<T extends { resource?: { id: string; eid?: string | null; displayName?: string | null; email?: string | null } | null }>(allocation: T): T {
if (!allocation.resource) {
return allocation;
}
return { ...allocation, resource: anonymizeResource(allocation.resource, directory) };
}
return {
...readModel,
allocations: readModel.allocations.map(anonymizeAllocation),
demands: readModel.demands.map(anonymizeAllocation),
assignments: readModel.assignments.map(anonymizeAllocation),
};
}
export async function findAllocationEntryOrNull(
db: Pick<import("@capakraken/db").PrismaClient, "demandRequirement" | "assignment">,
id: string,
) {
try {
return await loadAllocationEntry(db, id);
} catch (error) {
if (error instanceof TRPCError && error.code === "NOT_FOUND") {
return null;
}
throw error;
}
}
export function buildCreateDemandRequirementInput(input: CreateDemandDraftInput): z.infer<typeof CreateDemandRequirementSchema> {
return {
projectId: input.projectId,
startDate: input.startDate,
endDate: input.endDate,
hoursPerDay: input.hoursPerDay,
percentage: (input.hoursPerDay / 8) * 100,
status: AllocationStatus.PROPOSED,
headcount: input.headcount ?? 1,
budgetCents: input.budgetCents ?? 0,
metadata: input.metadata ?? {},
...(input.role ? { role: input.role } : {}),
...(input.roleId ? { roleId: input.roleId } : {}),
};
}
export async function getDemandRequirementByIdOrThrow(
db: Pick<import("@capakraken/db").PrismaClient, "demandRequirement">,
id: string,
) {
return findUniqueOrThrow(
db.demandRequirement.findUnique({
where: { id },
include: DEMAND_INCLUDE,
}),
"Demand requirement",
);
}
export async function resolveAssignmentBySelection(
db: Pick<import("@capakraken/db").PrismaClient, "assignment">,
input: AssignmentResolutionInput,
) {
if (input.assignmentId) {
return findUniqueOrThrow(
db.assignment.findUnique({
where: { id: input.assignmentId },
include: ASSIGNMENT_INCLUDE,
}),
"Assignment",
);
}
if (!input.resourceId || !input.projectId) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "resourceId and projectId are required when assignmentId is not provided",
});
}
const assignments = await db.assignment.findMany({
where: {
resourceId: input.resourceId,
projectId: input.projectId,
...(input.excludeCancelled ? { status: { not: AllocationStatus.CANCELLED } } : {}),
},
include: ASSIGNMENT_INCLUDE,
orderBy: { startDate: "asc" },
});
const matchingAssignment = assignments
.filter((assignment) => {
if (input.selectionMode === "WINDOW") {
return (!input.startDate || assignment.startDate >= input.startDate)
&& (!input.endDate || assignment.endDate <= input.endDate);
}
return !input.startDate || toIsoDate(assignment.startDate) === toIsoDate(input.startDate);
})
.sort((left, right) => right.startDate.getTime() - left.startDate.getTime())[0] ?? null;
if (!matchingAssignment) {
throw new TRPCError({ code: "NOT_FOUND", message: "Assignment not found" });
}
return matchingAssignment;
}