feat(api,application): add checkConflicts query and soften overbooking block

- New allocation.checkConflicts managerProcedure: returns per-day overbooking
  breakdown (availableHours, existingHours, requestedHours, overageHours,
  maxOverbookPercent) plus vacation overlap list for the requested period.
  Read-only — used by AllocationModal for pre-submission warnings.
- createAssignment(): replace the hard >5-day overbooking block with a soft
  CONFLICT error. When allowOverbooking: true is passed the assignment is
  created and overbookingAcknowledged is set to true on the record.
- allowOverbooking field added to CreateAssignmentBaseSchema (optional)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-09 10:13:18 +02:00
parent b944a17572
commit 61e52e9995
4 changed files with 184 additions and 4 deletions
@@ -143,10 +143,10 @@ export async function createAssignment(
availabilityWindows,
);
if (!availabilityResult.valid && availabilityResult.totalConflictDays > 5) {
if (!availabilityResult.valid && !input.allowOverbooking) {
throw new TRPCError({
code: "BAD_REQUEST",
message: `Resource has availability conflicts on ${availabilityResult.totalConflictDays} days`,
code: "CONFLICT",
message: `Resource is overbooked on ${availabilityResult.totalConflictDays} day(s). Pass allowOverbooking: true to proceed.`,
});
}
@@ -179,6 +179,7 @@ export async function createAssignment(
dailyCostCents: input.dailyCostCents ?? calculation.dailyCostCents,
status: input.status,
metadata: input.metadata as unknown as Prisma.InputJsonValue,
overbookingAcknowledged: input.allowOverbooking === true && !availabilityResult.valid,
},
include: ASSIGNMENT_RELATIONS_INCLUDE,
});
@@ -194,3 +195,55 @@ export async function createAssignment(
return assignment;
}
/**
* Creates an assignment record without running availability or duplicate checks.
* Use ONLY for fragment operations (splitting/carving an existing allocation)
* where the resource's total load does not change.
*/
export async function createAssignmentFragment(
db: DbClient,
input: {
demandRequirementId?: string | null | undefined;
resourceId: string;
projectId: string;
startDate: Date;
endDate: Date;
hoursPerDay: number;
percentage: number;
role?: string | null | undefined;
roleId?: string | null | undefined;
dailyCostCents: number;
status: string;
metadata: unknown;
},
): Promise<AssignmentWithRelations> {
const assignment = await db.assignment.create({
data: {
demandRequirementId: input.demandRequirementId ?? null,
resourceId: input.resourceId,
projectId: input.projectId,
startDate: input.startDate,
endDate: input.endDate,
hoursPerDay: input.hoursPerDay,
percentage: input.percentage,
role: input.role ?? null,
roleId: input.roleId ?? null,
dailyCostCents: input.dailyCostCents,
status: input.status as import("@capakraken/db").AllocationStatus,
metadata: input.metadata as import("@capakraken/db").Prisma.InputJsonValue,
},
include: ASSIGNMENT_RELATIONS_INCLUDE,
});
await db.auditLog.create({
data: {
entityType: "Assignment",
entityId: assignment.id,
action: "CREATE",
changes: { after: assignment } as unknown as import("@capakraken/db").Prisma.InputJsonValue,
},
});
return assignment;
}