Files
CapaKraken/packages/application/src/use-cases/vacation/cancel-vacation.ts
T
Hartmut dda049075f refactor(application): extract vacation management into application use-cases
Moves approve, reject, cancel, and request vacation business logic
out of the tRPC procedure layer into packages/application, matching
the pattern used by allocation use-cases.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 16:49:45 +02:00

74 lines
2.1 KiB
TypeScript

import type { PrismaClient, VacationStatus } from "@capakraken/db";
import { TRPCError } from "@trpc/server";
type DbClient = Pick<PrismaClient, "vacation" | "resource">;
export type CancelVacationDeps = {
assertVacationCancelable: (status: VacationStatus) => void;
isVacationManagerRole: (role: string | null | undefined) => boolean;
canActorCancelVacation: (input: {
actorId: string;
actorRole: string | null | undefined;
requestedById: string | null | undefined;
resourceUserId: string | null | undefined;
}) => boolean;
};
export type CancelVacationInput = {
id: string;
actorId: string;
actorRole: string | null | undefined;
};
export type CancelVacationResult = {
vacation: Awaited<ReturnType<PrismaClient["vacation"]["update"]>>;
existingStatus: VacationStatus;
};
export async function cancelVacation(
db: DbClient,
input: CancelVacationInput,
deps: CancelVacationDeps,
): Promise<CancelVacationResult> {
const existing = await db.vacation.findUnique({ where: { id: input.id } });
if (!existing) {
throw new TRPCError({ code: "NOT_FOUND", message: "Vacation not found" });
}
deps.assertVacationCancelable(existing.status);
// Only fetch the linked resource when the actor is not a manager and didn't
// originally request the vacation — we need to check resource ownership.
const needsResourceCheck =
!deps.isVacationManagerRole(input.actorRole) &&
existing.requestedById !== input.actorId;
const resource = needsResourceCheck
? await db.resource.findUnique({
where: { id: existing.resourceId },
select: { userId: true },
})
: null;
if (
!deps.canActorCancelVacation({
actorId: input.actorId,
actorRole: input.actorRole,
requestedById: existing.requestedById,
resourceUserId: resource?.userId,
})
) {
throw new TRPCError({
code: "FORBIDDEN",
message: "You can only cancel your own vacation requests",
});
}
const updated = await db.vacation.update({
where: { id: input.id },
data: { status: "CANCELLED" },
});
return { vacation: updated, existingStatus: existing.status };
}