dda049075f
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>
74 lines
2.1 KiB
TypeScript
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 };
|
|
}
|