refactor(api): extract vacation management support
This commit is contained in:
@@ -18,6 +18,16 @@ import {
|
||||
dispatchVacationWebhookInBackground,
|
||||
notifyVacationStatusInBackground,
|
||||
} from "./vacation-side-effects.js";
|
||||
import {
|
||||
assertVacationApprovable,
|
||||
assertVacationCancelable,
|
||||
assertVacationRejectable,
|
||||
buildApprovedVacationUpdateData,
|
||||
buildRejectedVacationUpdateData,
|
||||
buildVacationStatusUpdateData,
|
||||
canActorCancelVacation,
|
||||
isVacationManagerRole,
|
||||
} from "./vacation-management-support.js";
|
||||
|
||||
const BatchCreatePublicHolidaysSchema = z.object({
|
||||
year: z.number().int().min(2000).max(2100),
|
||||
@@ -44,10 +54,7 @@ export const vacationManagementProcedures = {
|
||||
ctx.db.vacation.findUnique({ where: { id: input.id } }),
|
||||
"Vacation",
|
||||
);
|
||||
const approvableStatuses: string[] = [VacationStatus.PENDING, VacationStatus.CANCELLED, VacationStatus.REJECTED];
|
||||
if (!approvableStatuses.includes(existing.status)) {
|
||||
throw new TRPCError({ code: "BAD_REQUEST", message: "Only PENDING, CANCELLED, or REJECTED vacations can be approved" });
|
||||
}
|
||||
assertVacationApprovable(existing.status);
|
||||
|
||||
await assertVacationStillChargeable(ctx.db, {
|
||||
resourceId: existing.resourceId,
|
||||
@@ -74,13 +81,11 @@ export const vacationManagementProcedures = {
|
||||
|
||||
const updated = await ctx.db.vacation.update({
|
||||
where: { id: input.id },
|
||||
data: {
|
||||
status: VacationStatus.APPROVED,
|
||||
rejectionReason: null,
|
||||
...deductionSnapshotWriteData,
|
||||
...(userRecord?.id ? { approvedById: userRecord.id } : {}),
|
||||
data: buildApprovedVacationUpdateData({
|
||||
deductionSnapshotWriteData,
|
||||
approvedById: userRecord?.id,
|
||||
approvedAt: new Date(),
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
emitVacationUpdated({ id: updated.id, resourceId: updated.resourceId, status: updated.status });
|
||||
@@ -121,16 +126,13 @@ export const vacationManagementProcedures = {
|
||||
ctx.db.vacation.findUnique({ where: { id: input.id } }),
|
||||
"Vacation",
|
||||
);
|
||||
if (existing.status !== VacationStatus.PENDING) {
|
||||
throw new TRPCError({ code: "BAD_REQUEST", message: "Only PENDING vacations can be rejected" });
|
||||
}
|
||||
assertVacationRejectable(existing.status);
|
||||
|
||||
const updated = await ctx.db.vacation.update({
|
||||
where: { id: input.id },
|
||||
data: {
|
||||
status: VacationStatus.REJECTED,
|
||||
...(input.rejectionReason !== undefined ? { rejectionReason: input.rejectionReason } : {}),
|
||||
},
|
||||
data: buildRejectedVacationUpdateData({
|
||||
rejectionReason: input.rejectionReason,
|
||||
}),
|
||||
});
|
||||
|
||||
emitVacationUpdated({ id: updated.id, resourceId: updated.resourceId, status: updated.status });
|
||||
@@ -200,13 +202,11 @@ export const vacationManagementProcedures = {
|
||||
});
|
||||
const updated = await ctx.db.vacation.update({
|
||||
where: { id: vacation.id },
|
||||
data: {
|
||||
status: VacationStatus.APPROVED,
|
||||
rejectionReason: null,
|
||||
...deductionSnapshotWriteData,
|
||||
...(userRecord?.id ? { approvedById: userRecord.id } : {}),
|
||||
data: buildApprovedVacationUpdateData({
|
||||
deductionSnapshotWriteData,
|
||||
approvedById: userRecord?.id,
|
||||
approvedAt: new Date(),
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
emitVacationUpdated({ id: updated.id, resourceId: updated.resourceId, status: updated.status });
|
||||
@@ -252,10 +252,9 @@ export const vacationManagementProcedures = {
|
||||
|
||||
await ctx.db.vacation.updateMany({
|
||||
where: { id: { in: vacations.map((vacation) => vacation.id) } },
|
||||
data: {
|
||||
status: VacationStatus.REJECTED,
|
||||
...(input.rejectionReason !== undefined ? { rejectionReason: input.rejectionReason } : {}),
|
||||
},
|
||||
data: buildRejectedVacationUpdateData({
|
||||
rejectionReason: input.rejectionReason,
|
||||
}),
|
||||
});
|
||||
|
||||
for (const vacation of vacations) {
|
||||
@@ -293,28 +292,29 @@ export const vacationManagementProcedures = {
|
||||
ctx.db.vacation.findUnique({ where: { id: input.id } }),
|
||||
"Vacation",
|
||||
);
|
||||
if (existing.status === VacationStatus.CANCELLED) {
|
||||
throw new TRPCError({ code: "BAD_REQUEST", message: "Already cancelled" });
|
||||
}
|
||||
assertVacationCancelable(existing.status);
|
||||
|
||||
const userRecord = await findVacationActor(ctx.db, ctx.session.user?.email);
|
||||
if (!userRecord) {
|
||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||
}
|
||||
const isManagerOrAdmin = userRecord.systemRole === "ADMIN" || userRecord.systemRole === "MANAGER";
|
||||
if (!isManagerOrAdmin) {
|
||||
if (existing.requestedById !== userRecord.id) {
|
||||
const resource = await ctx.db.resource.findUnique({
|
||||
const resource = isVacationManagerRole(userRecord.systemRole) || existing.requestedById === userRecord.id
|
||||
? null
|
||||
: await ctx.db.resource.findUnique({
|
||||
where: { id: existing.resourceId },
|
||||
select: { userId: true },
|
||||
});
|
||||
if (!resource || resource.userId !== userRecord.id) {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message: "You can only cancel your own vacation requests",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!canActorCancelVacation({
|
||||
actorId: userRecord.id,
|
||||
actorRole: userRecord.systemRole,
|
||||
requestedById: existing.requestedById,
|
||||
resourceUserId: resource?.userId,
|
||||
})) {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message: "You can only cancel your own vacation requests",
|
||||
});
|
||||
}
|
||||
|
||||
const updated = await ctx.db.vacation.update({
|
||||
@@ -396,24 +396,18 @@ export const vacationManagementProcedures = {
|
||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||
}
|
||||
|
||||
const isManager = userRecord.systemRole === "ADMIN" || userRecord.systemRole === "MANAGER";
|
||||
if (input.status !== "CANCELLED" && !isManager) {
|
||||
if (input.status !== "CANCELLED" && !isVacationManagerRole(userRecord.systemRole)) {
|
||||
throw new TRPCError({ code: "FORBIDDEN", message: "Manager role required to approve/reject" });
|
||||
}
|
||||
|
||||
const data: Record<string, unknown> = { status: input.status };
|
||||
if (input.status === "APPROVED") {
|
||||
data.approvedById = userRecord.id;
|
||||
data.approvedAt = new Date();
|
||||
data.rejectionReason = null;
|
||||
}
|
||||
if (input.note !== undefined) {
|
||||
data.note = input.note;
|
||||
}
|
||||
|
||||
const updated = await ctx.db.vacation.update({
|
||||
where: { id: input.id },
|
||||
data,
|
||||
data: buildVacationStatusUpdateData({
|
||||
status: input.status,
|
||||
note: input.note,
|
||||
approvedById: userRecord.id,
|
||||
approvedAt: new Date(),
|
||||
}),
|
||||
});
|
||||
|
||||
emitVacationUpdated({ id: updated.id, resourceId: updated.resourceId, status: updated.status });
|
||||
|
||||
Reference in New Issue
Block a user