fix(api): honor vacation deduction snapshots

This commit is contained in:
2026-03-31 21:00:11 +02:00
parent a490d68a3b
commit e8c0d3c3eb
2 changed files with 300 additions and 45 deletions
+62 -12
View File
@@ -11,6 +11,9 @@ import { createTRPCRouter, adminProcedure, managerProcedure, protectedProcedure
import { createAuditEntry } from "../lib/audit.js";
import { loadResourceHolidayContext } from "../lib/resource-holiday-context.js";
import { countCalendarDaysInPeriod, countVacationChargeableDays } from "../lib/vacation-day-count.js";
import {
countVacationChargeableDaysFromSnapshot,
} from "../lib/vacation-deduction-snapshot.js";
/** Types that consume from annual leave balance */
const BALANCE_TYPES: VacationType[] = [VacationType.ANNUAL, VacationType.OTHER];
@@ -230,6 +233,40 @@ function calculateCarryoverDays(entitlement: {
return Math.max(0, entitlement.entitledDays - entitlement.usedDays - entitlement.pendingDays);
}
async function calculateEntitlementVacationDays(
yearStart: Date,
yearEnd: Date,
vacation: {
startDate: Date;
endDate: Date;
isHalfDay: boolean;
deductedDays: number | null;
holidayCountryCode: string | null;
holidayFederalState: string | null;
holidayMetroCityName: string | null;
holidayCalendarDates: import("@capakraken/db").Prisma.JsonValue | null;
holidayLegacyPublicHolidayDates: import("@capakraken/db").Prisma.JsonValue | null;
},
getLegacyHolidayContext: () => Promise<Awaited<ReturnType<typeof loadResourceHolidayContext>>>,
): Promise<number> {
const persistedDays = countVacationChargeableDaysFromSnapshot(vacation, yearStart, yearEnd);
if (persistedDays !== null) {
return persistedDays;
}
const holidayContext = await getLegacyHolidayContext();
return countVacationChargeableDays({
vacation,
periodStart: yearStart,
periodEnd: yearEnd,
countryCode: holidayContext.countryCode,
federalState: holidayContext.federalState,
metroCityName: holidayContext.metroCityName,
calendarHolidayStrings: holidayContext.calendarHolidayStrings,
publicHolidayStrings: holidayContext.publicHolidayStrings,
});
}
/**
* Recompute used/pending days from actual vacation records and update the cached values.
*/
@@ -281,7 +318,6 @@ async function syncEntitlement(
: entitlement;
const yearStart = new Date(`${year}-01-01T00:00:00.000Z`);
const yearEnd = new Date(`${year}-12-31T00:00:00.000Z`);
const holidayContext = await loadResourceHolidayContext(db, resourceId, yearStart, yearEnd);
const vacations = await db.vacation.findMany({
where: {
@@ -291,23 +327,37 @@ async function syncEntitlement(
endDate: { gte: yearStart },
status: { in: [VacationStatus.APPROVED, VacationStatus.PENDING] },
},
select: { startDate: true, endDate: true, status: true, isHalfDay: true },
select: {
startDate: true,
endDate: true,
status: true,
isHalfDay: true,
deductedDays: true,
holidayCountryCode: true,
holidayFederalState: true,
holidayMetroCityName: true,
holidayCalendarDates: true,
holidayLegacyPublicHolidayDates: true,
},
});
let usedDays = 0;
let pendingDays = 0;
let legacyHolidayContextPromise: Promise<Awaited<ReturnType<typeof loadResourceHolidayContext>>> | null = null;
const getLegacyHolidayContext = async () => {
if (!legacyHolidayContextPromise) {
legacyHolidayContextPromise = loadResourceHolidayContext(db, resourceId, yearStart, yearEnd);
}
return legacyHolidayContextPromise;
};
for (const v of vacations) {
const days = countVacationChargeableDays({
vacation: v,
periodStart: yearStart,
periodEnd: yearEnd,
countryCode: holidayContext.countryCode,
federalState: holidayContext.federalState,
metroCityName: holidayContext.metroCityName,
calendarHolidayStrings: holidayContext.calendarHolidayStrings,
publicHolidayStrings: holidayContext.publicHolidayStrings,
});
const days = await calculateEntitlementVacationDays(
yearStart,
yearEnd,
v,
getLegacyHolidayContext,
);
if (v.status === VacationStatus.APPROVED) usedDays += days;
else pendingDays += days;
}