Files
CapaKraken/packages/api/src/router/allocation-effects.ts
T

157 lines
5.0 KiB
TypeScript

import { createDemandRequirement, fillDemandRequirement } from "@capakraken/application";
import { buildTaskAction, CreateDemandRequirementSchema, FillDemandRequirementSchema } from "@capakraken/shared";
import { z } from "zod";
import { checkBudgetThresholds } from "../lib/budget-alerts.js";
import { generateAutoSuggestions } from "../lib/auto-staffing.js";
import { invalidateDashboardCache } from "../lib/cache.js";
import { logger } from "../lib/logger.js";
import { dispatchWebhooks } from "../lib/webhook-dispatcher.js";
import { emitAllocationCreated, emitAllocationUpdated, emitNotificationCreated } from "../sse/event-bus.js";
export function runAllocationBackgroundEffect(
effectName: string,
execute: () => unknown,
metadata: Record<string, unknown> = {},
): void {
void Promise.resolve()
.then(execute)
.catch((error) => {
logger.error(
{ err: error, effectName, ...metadata },
"Allocation background side effect failed",
);
});
}
export function invalidateDashboardCacheInBackground(): void {
runAllocationBackgroundEffect("invalidateDashboardCache", () => invalidateDashboardCache());
}
export function checkBudgetThresholdsInBackground(
db: import("@capakraken/db").PrismaClient,
projectId: string,
): void {
runAllocationBackgroundEffect(
"checkBudgetThresholds",
// eslint-disable-next-line @typescript-eslint/no-explicit-any
() => checkBudgetThresholds(db as any, projectId),
{ projectId },
);
}
export function dispatchAllocationWebhookInBackground(
db: import("@capakraken/db").PrismaClient,
event: string,
payload: Record<string, unknown>,
): void {
runAllocationBackgroundEffect(
"dispatchWebhooks",
() => dispatchWebhooks(db, event, payload),
{ event },
);
}
export function generateAutoSuggestionsInBackground(
db: import("@capakraken/db").PrismaClient,
demandRequirementId: string,
): void {
runAllocationBackgroundEffect(
"generateAutoSuggestions",
// eslint-disable-next-line @typescript-eslint/no-explicit-any
() => generateAutoSuggestions(db as any, demandRequirementId),
{ demandRequirementId },
);
}
export async function createDemandRequirementWithEffects(
db: import("@capakraken/db").PrismaClient,
input: z.infer<typeof CreateDemandRequirementSchema>,
) {
const demandRequirement = await db.$transaction(async (tx) => {
return createDemandRequirement(
tx as unknown as Parameters<typeof createDemandRequirement>[0],
input,
);
});
emitAllocationCreated({
id: demandRequirement.id,
projectId: demandRequirement.projectId,
resourceId: null,
});
invalidateDashboardCacheInBackground();
const [project, roleEntity, managers] = await Promise.all([
db.project.findUnique({
where: { id: demandRequirement.projectId },
select: { name: true },
}),
demandRequirement.roleId
? db.role.findUnique({
where: { id: demandRequirement.roleId },
select: { name: true },
})
: Promise.resolve(null),
db.user.findMany({
where: { systemRole: { in: ["ADMIN", "MANAGER"] } },
select: { id: true },
}),
]);
const roleName = roleEntity?.name ?? demandRequirement.role ?? "Unspecified role";
const projectName = project?.name ?? "Unknown project";
const headcount = demandRequirement.headcount ?? 1;
for (const manager of managers) {
const task = await db.notification.create({
data: {
userId: manager.id,
category: "TASK",
type: "DEMAND_FILL",
priority: "NORMAL",
title: `Staff demand: ${roleName} for ${projectName}`,
body: `${headcount} ${roleName} needed for project ${projectName}`,
taskStatus: "OPEN",
taskAction: buildTaskAction("fill_demand", demandRequirement.id),
entityId: demandRequirement.id,
entityType: "demand",
link: `/projects/${demandRequirement.projectId}`,
channel: "in_app",
},
});
emitNotificationCreated(manager.id, task.id);
}
checkBudgetThresholdsInBackground(db, demandRequirement.projectId);
generateAutoSuggestionsInBackground(db, demandRequirement.id);
return demandRequirement;
}
export async function fillDemandRequirementWithEffects(
db: import("@capakraken/db").PrismaClient,
input: z.infer<typeof FillDemandRequirementSchema>,
) {
const result = await fillDemandRequirement(db, input);
emitAllocationCreated({
id: result.assignment.id,
projectId: result.assignment.projectId,
resourceId: result.assignment.resourceId,
});
emitAllocationUpdated({
id: result.updatedDemandRequirement.id,
projectId: result.updatedDemandRequirement.projectId,
resourceId: null,
});
invalidateDashboardCacheInBackground();
checkBudgetThresholdsInBackground(db, result.assignment.projectId);
if (result.updatedDemandRequirement.headcount > 0
&& result.updatedDemandRequirement.status !== "COMPLETED") {
generateAutoSuggestionsInBackground(db, result.updatedDemandRequirement.id);
}
return result;
}