refactor(api): extract vacation side effects

This commit is contained in:
2026-03-31 11:02:40 +02:00
parent 269288f5df
commit e013d1af9f
2 changed files with 170 additions and 167 deletions
@@ -0,0 +1,150 @@
import { buildTaskAction } from "@capakraken/shared";
import { VacationStatus, VacationType } from "@capakraken/db";
import { createNotification } from "../lib/create-notification.js";
import { sendEmail } from "../lib/email.js";
import { logger } from "../lib/logger.js";
import { dispatchWebhooks } from "../lib/webhook-dispatcher.js";
import { emitTaskAssigned } from "../sse/event-bus.js";
import type { TRPCContext } from "../trpc.js";
type VacationDb = TRPCContext["db"];
function runVacationBackgroundEffect(
effectName: string,
execute: () => unknown,
metadata: Record<string, unknown> = {},
): void {
void Promise.resolve()
.then(execute)
.catch((error) => {
logger.error(
{ err: error, effectName, ...metadata },
"Vacation background side effect failed",
);
});
}
async function notifyVacationStatus(
db: VacationDb,
vacationId: string,
resourceId: string,
newStatus: VacationStatus,
rejectionReason?: string | null,
) {
const resource = await db.resource.findUnique({
where: { id: resourceId },
select: {
displayName: true,
user: { select: { id: true, email: true, name: true } },
},
});
if (!resource?.user) return;
const statusLabel = newStatus === VacationStatus.APPROVED ? "approved" : "rejected";
const title = `Vacation request ${statusLabel}`;
const body = rejectionReason
? `Your vacation request was ${statusLabel}. Reason: ${rejectionReason}`
: `Your vacation request has been ${statusLabel}.`;
await createNotification({
db,
userId: resource.user.id,
type: `VACATION_${newStatus}`,
title,
body,
entityId: vacationId,
entityType: "vacation",
});
if (resource.user.email) {
void sendEmail({
to: resource.user.email,
subject: `CapaKraken — ${title}`,
text: body,
});
}
}
export function notifyVacationStatusInBackground(
db: VacationDb,
vacationId: string,
resourceId: string,
newStatus: VacationStatus,
rejectionReason?: string | null,
): void {
runVacationBackgroundEffect(
"notifyVacationStatus",
() => notifyVacationStatus(db, vacationId, resourceId, newStatus, rejectionReason),
{ vacationId, resourceId, newStatus },
);
}
export function dispatchVacationWebhookInBackground(
db: VacationDb,
event: string,
payload: Record<string, unknown>,
): void {
runVacationBackgroundEffect(
"dispatchWebhooks",
() => dispatchWebhooks(db, event, payload),
{ event },
);
}
export async function createVacationApprovalTasks(args: {
db: VacationDb;
submittedByUserId: string;
vacationId: string;
resourceName: string;
vacationType: VacationType;
startDate: Date;
endDate: Date;
}): Promise<void> {
const { db, submittedByUserId, vacationId, resourceName, vacationType, startDate, endDate } = args;
const startStr = startDate.toISOString().slice(0, 10);
const endStr = endDate.toISOString().slice(0, 10);
const managers = await db.user.findMany({
where: { systemRole: { in: ["ADMIN", "MANAGER"] } },
select: { id: true },
});
for (const manager of managers) {
if (manager.id === submittedByUserId) continue;
const taskId = await createNotification({
db,
userId: manager.id,
category: "APPROVAL",
type: "VACATION_APPROVAL",
priority: "NORMAL",
title: `Vacation approval: ${resourceName}`,
body: `${resourceName} requests ${vacationType} from ${startStr} to ${endStr}`,
taskStatus: "OPEN",
taskAction: buildTaskAction("approve_vacation", vacationId),
entityId: vacationId,
entityType: "vacation",
link: "/vacations",
senderId: submittedByUserId,
channel: "in_app",
});
emitTaskAssigned(manager.id, taskId);
}
}
export async function completeVacationApprovalTasks(
db: VacationDb,
vacationId: string,
completedById?: string,
): Promise<void> {
await db.notification.updateMany({
where: {
taskAction: buildTaskAction("approve_vacation", vacationId),
category: "APPROVAL",
taskStatus: "OPEN",
},
data: {
taskStatus: "DONE",
completedAt: new Date(),
...(completedById ? { completedBy: completedById } : {}),
},
});
}