refactor(api): extract vacation chargeability helpers

This commit is contained in:
2026-03-31 11:08:58 +02:00
parent 3e0d9d9af7
commit 656c3d0ee5
3 changed files with 219 additions and 156 deletions
+37 -156
View File
@@ -9,15 +9,17 @@ import { createTRPCRouter, adminProcedure, managerProcedure, protectedProcedure
import { getAnonymizationDirectory } from "../lib/anonymization.js";
import { checkVacationConflicts, checkBatchVacationConflicts } from "../lib/vacation-conflicts.js";
import { createAuditEntry } from "../lib/audit.js";
import { asHolidayResolverDb, getResolvedCalendarHolidays } from "../lib/holiday-availability.js";
import { countCalendarDaysInPeriod, countVacationChargeableDays } from "../lib/vacation-day-count.js";
import {
buildVacationApprovalWriteData,
resolveVacationCreationChargeability,
assertVacationStillChargeable,
} from "./vacation-chargeability.js";
import {
batchCreatePublicHolidayVacations,
} from "./vacation-public-holidays.js";
import {
VACATION_BALANCE_TYPES,
buildVacationDeductionSnapshotWriteData,
calculateVacationDeductionSnapshot,
type VacationChargeableInput,
} from "../lib/vacation-deduction-snapshot.js";
import type { TRPCContext } from "../trpc.js";
import { anonymizeVacationRecord, isSameUtcDay, vacationReadProcedures } from "./vacation-read.js";
import {
completeVacationApprovalTasks,
@@ -26,35 +28,6 @@ import {
notifyVacationStatusInBackground,
} from "./vacation-side-effects.js";
async function calculateVacationEffectiveDays(
db: TRPCContext["db"],
vacation: VacationChargeableInput,
): Promise<number> {
if (!VACATION_BALANCE_TYPES.has(vacation.type)) {
return countCalendarDaysInPeriod(vacation);
}
const snapshot = await calculateVacationDeductionSnapshot(db, vacation);
return snapshot.deductedDays;
}
async function assertVacationStillChargeable(
db: TRPCContext["db"],
vacation: VacationChargeableInput,
): Promise<void> {
if (!VACATION_BALANCE_TYPES.has(vacation.type)) {
return;
}
const effectiveDays = await calculateVacationEffectiveDays(db, vacation);
if (effectiveDays <= 0) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Vacation no longer deducts any vacation days for the current holiday calendar and cannot be approved",
});
}
}
const CreateVacationRequestSchema = z.object({
resourceId: z.string(),
type: z.nativeEnum(VacationType),
@@ -158,26 +131,13 @@ export const vacationRouter = createTRPCRouter({
});
}
let effectiveDays: number | null = null;
let deductionSnapshotWriteData: ReturnType<typeof buildVacationDeductionSnapshotWriteData> | null = null;
if (VACATION_BALANCE_TYPES.has(input.type)) {
const deductionSnapshot = await calculateVacationDeductionSnapshot(ctx.db, {
resourceId: input.resourceId,
type: input.type,
startDate: input.startDate,
endDate: input.endDate,
isHalfDay: input.isHalfDay ?? false,
});
effectiveDays = deductionSnapshot.deductedDays;
deductionSnapshotWriteData = buildVacationDeductionSnapshotWriteData(deductionSnapshot);
if (effectiveDays <= 0) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Selected vacation period only contains public holidays and does not deduct any vacation days",
});
}
}
const { effectiveDays, deductionSnapshotWriteData } = await resolveVacationCreationChargeability(ctx.db, {
resourceId: input.resourceId,
type: input.type,
startDate: input.startDate,
endDate: input.endDate,
isHalfDay: input.isHalfDay ?? false,
});
const status = isManager ? VacationStatus.APPROVED : VacationStatus.PENDING;
@@ -257,17 +217,13 @@ export const vacationRouter = createTRPCRouter({
endDate: existing.endDate,
isHalfDay: existing.isHalfDay,
});
const deductionSnapshotWriteData = VACATION_BALANCE_TYPES.has(existing.type)
? buildVacationDeductionSnapshotWriteData(
await calculateVacationDeductionSnapshot(ctx.db, {
resourceId: existing.resourceId,
type: existing.type,
startDate: existing.startDate,
endDate: existing.endDate,
isHalfDay: existing.isHalfDay,
}),
)
: { deductedDays: 0 };
const deductionSnapshotWriteData = await buildVacationApprovalWriteData(ctx.db, {
resourceId: existing.resourceId,
type: existing.type,
startDate: existing.startDate,
endDate: existing.endDate,
isHalfDay: existing.isHalfDay,
});
const userRecord = await ctx.db.user.findUnique({
where: { email: ctx.session.user?.email ?? "" },
@@ -412,17 +368,13 @@ export const vacationRouter = createTRPCRouter({
);
for (const v of vacations) {
const deductionSnapshotWriteData = VACATION_BALANCE_TYPES.has(v.type)
? buildVacationDeductionSnapshotWriteData(
await calculateVacationDeductionSnapshot(ctx.db, {
resourceId: v.resourceId,
type: v.type,
startDate: v.startDate,
endDate: v.endDate,
isHalfDay: v.isHalfDay,
}),
)
: { deductedDays: 0 };
const deductionSnapshotWriteData = await buildVacationApprovalWriteData(ctx.db, {
resourceId: v.resourceId,
type: v.type,
startDate: v.startDate,
endDate: v.endDate,
isHalfDay: v.isHalfDay,
});
const updated = await ctx.db.vacation.update({
where: { id: v.id },
data: {
@@ -609,88 +561,17 @@ export const vacationRouter = createTRPCRouter({
}),
)
.mutation(async ({ ctx, input }) => {
const resources = await ctx.db.resource.findMany({
where: {
isActive: true,
...(input.chapter ? { chapter: input.chapter } : {}),
},
select: {
id: true,
federalState: true,
countryId: true,
metroCityId: true,
country: { select: { code: true } },
metroCity: { select: { name: true } },
},
});
if (resources.length === 0) {
return { created: 0 };
}
const adminUser = await ctx.db.user.findUnique({
where: { email: ctx.session.user?.email ?? "" },
select: { id: true },
});
if (!adminUser) throw new TRPCError({ code: "UNAUTHORIZED" });
let created = 0;
let holidayCount = 0;
for (const resource of resources) {
const holidays = await getResolvedCalendarHolidays(asHolidayResolverDb(ctx.db), {
periodStart: new Date(`${input.year}-01-01T00:00:00.000Z`),
periodEnd: new Date(`${input.year}-12-31T00:00:00.000Z`),
countryId: resource.countryId,
countryCode: resource.country?.code,
federalState: input.federalState ?? resource.federalState,
metroCityId: resource.metroCityId,
metroCityName: resource.metroCity?.name,
});
holidayCount += holidays.length;
for (const holiday of holidays) {
const startDate = new Date(holiday.date);
const endDate = new Date(holiday.date);
if (input.replaceExisting) {
// Remove any existing public holiday on this exact date for this resource
await ctx.db.vacation.deleteMany({
where: {
resourceId: resource.id,
type: VacationType.PUBLIC_HOLIDAY,
startDate,
endDate,
},
});
}
// Check if one already exists
const exists = await ctx.db.vacation.findFirst({
where: {
resourceId: resource.id,
type: VacationType.PUBLIC_HOLIDAY,
startDate,
endDate,
},
});
if (exists) continue;
await ctx.db.vacation.create({
data: {
resourceId: resource.id,
type: VacationType.PUBLIC_HOLIDAY,
status: VacationStatus.APPROVED,
startDate,
endDate,
note: holiday.name,
requestedById: adminUser.id,
approvedById: adminUser.id,
approvedAt: new Date(),
},
});
created++;
}
}
const { created, holidays: holidayCount, resources } = await batchCreatePublicHolidayVacations(
ctx.db,
input,
adminUser.id,
);
void createAuditEntry({
db: ctx.db,
@@ -699,12 +580,12 @@ export const vacationRouter = createTRPCRouter({
entityName: `Public Holidays ${input.year}${input.federalState ? ` (${input.federalState})` : ""}`,
action: "CREATE",
userId: adminUser.id,
after: { created, holidays: holidayCount, resources: resources.length, year: input.year, federalState: input.federalState } as unknown as Record<string, unknown>,
after: { created, holidays: holidayCount, resources, year: input.year, federalState: input.federalState } as unknown as Record<string, unknown>,
source: "ui",
summary: `Batch created ${created} public holidays for ${resources.length} resources (${input.year})`,
summary: `Batch created ${created} public holidays for ${resources} resources (${input.year})`,
});
return { created, holidays: holidayCount, resources: resources.length };
return { created, holidays: holidayCount, resources };
}),
/**