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>
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
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 };
|
||||
}
|
||||
Reference in New Issue
Block a user