Files
Nexus/packages/application/src/use-cases/entitlement/sync-entitlement.ts
T
Hartmut b41c1d2501
CI / Architecture Guardrails (push) Successful in 2m38s
CI / Assistant Split Regression (push) Successful in 3m33s
CI / Typecheck (push) Successful in 3m51s
CI / Lint (push) Successful in 5m2s
CI / E2E Tests (push) Has been cancelled
CI / Fresh-Linux Docker Deploy (push) Has been cancelled
CI / Release Images (push) Has been cancelled
CI / Build (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
rename(phase 1): CapaKraken → Nexus across code, UI, docs, CI (#61)
rename(phase 1): CapaKraken → Nexus across code, UI, docs, CI (#61)

Co-authored-by: Hartmut Nörenberg <hn@hartmut-noerenberg.com>
Co-committed-by: Hartmut Nörenberg <hn@hartmut-noerenberg.com>
2026-05-21 16:28:40 +02:00

250 lines
6.8 KiB
TypeScript

import { VacationType, VacationStatus } from "@nexus/db";
import type { Prisma, PrismaClient } from "@nexus/db";
import { TRPCError } from "@trpc/server";
/** Types that consume from annual leave balance */
const BALANCE_TYPES: VacationType[] = [VacationType.ANNUAL, VacationType.OTHER];
type DbClient = Pick<
PrismaClient,
"vacation" | "vacationEntitlement" | "systemSettings" | "resource"
>;
export type EntitlementSnapshot = {
id: string;
entitledDays: number;
carryoverDays: number;
usedDays: number;
pendingDays: number;
};
type VacationSnapshotCarrier = {
startDate: Date;
endDate: Date;
isHalfDay: boolean;
deductedDays: number | null;
holidayCountryCode: string | null;
holidayFederalState: string | null;
holidayMetroCityName: string | null;
holidayCalendarDates: Prisma.JsonValue | null;
holidayLegacyPublicHolidayDates: Prisma.JsonValue | null;
};
export type ResourceHolidayContext = {
countryCode?: string | null;
countryName?: string | null;
federalState?: string | null;
metroCityName?: string | null;
calendarHolidayStrings: string[];
publicHolidayStrings: string[];
};
export type SyncEntitlementDeps = {
loadResourceHolidayContext: (
db: DbClient,
resourceId: string,
periodStart: Date,
periodEnd: Date,
) => Promise<ResourceHolidayContext>;
countCalendarDaysInPeriod: (
vacation: { startDate: Date; endDate: Date; isHalfDay: boolean },
periodStart?: Date,
periodEnd?: Date,
) => number;
countVacationChargeableDays: (args: {
vacation: { startDate: Date; endDate: Date; isHalfDay: boolean };
periodStart?: Date;
periodEnd?: Date;
countryCode?: string | null;
federalState?: string | null;
metroCityName?: string | null;
calendarHolidayStrings: string[];
publicHolidayStrings: string[];
}) => number;
countVacationChargeableDaysFromSnapshot: (
vacation: VacationSnapshotCarrier,
periodStart?: Date,
periodEnd?: Date,
) => number | null;
};
function calculateCarryoverDays(entitlement: {
entitledDays: number;
usedDays: number;
pendingDays: number;
}): number {
return Math.max(0, entitlement.entitledDays - entitlement.usedDays - entitlement.pendingDays);
}
async function getOrCreateEntitlement(
db: DbClient,
resourceId: string,
year: number,
defaultDays: number,
) {
let entitlement = await db.vacationEntitlement.findUnique({
where: { resourceId_year: { resourceId, year } },
});
if (!entitlement) {
const prevYear = await db.vacationEntitlement.findUnique({
where: { resourceId_year: { resourceId, year: year - 1 } },
});
const carryover = prevYear
? Math.max(0, prevYear.entitledDays - prevYear.usedDays - prevYear.pendingDays)
: 0;
entitlement = await db.vacationEntitlement.create({
data: {
resourceId,
year,
entitledDays: defaultDays + carryover,
carryoverDays: carryover,
usedDays: 0,
pendingDays: 0,
},
});
}
return entitlement;
}
async function calculateEntitlementVacationDays(
yearStart: Date,
yearEnd: Date,
vacation: VacationSnapshotCarrier,
getLegacyHolidayContext: () => Promise<ResourceHolidayContext>,
deps: SyncEntitlementDeps,
): Promise<number> {
const persistedDays = deps.countVacationChargeableDaysFromSnapshot(vacation, yearStart, yearEnd);
if (persistedDays !== null) {
return persistedDays;
}
const holidayContext = await getLegacyHolidayContext();
return deps.countVacationChargeableDays({
vacation,
periodStart: yearStart,
periodEnd: yearEnd,
countryCode: holidayContext.countryCode ?? null,
federalState: holidayContext.federalState ?? null,
metroCityName: holidayContext.metroCityName ?? null,
calendarHolidayStrings: holidayContext.calendarHolidayStrings,
publicHolidayStrings: holidayContext.publicHolidayStrings,
});
}
/**
* Recompute used/pending days from actual vacation records and update the cached values.
*/
export async function syncEntitlement(
db: DbClient,
resourceId: string,
year: number,
defaultDays: number,
deps: SyncEntitlementDeps,
visitedYears: Set<number> = new Set(),
): Promise<EntitlementSnapshot> {
if (visitedYears.has(year)) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: `Detected recursive entitlement sync for year ${year}`,
});
}
visitedYears.add(year);
let previousYearEntitlement: EntitlementSnapshot | null = await db.vacationEntitlement.findUnique(
{
where: { resourceId_year: { resourceId, year: year - 1 } },
},
);
if (previousYearEntitlement) {
previousYearEntitlement = await syncEntitlement(
db,
resourceId,
year - 1,
defaultDays,
deps,
visitedYears,
);
}
const entitlement = await getOrCreateEntitlement(db, resourceId, year, defaultDays);
const carryoverDays = previousYearEntitlement
? calculateCarryoverDays(previousYearEntitlement)
: 0;
const expectedEntitledDays = defaultDays + carryoverDays;
const entitlementWithCarryover =
entitlement.carryoverDays !== carryoverDays || entitlement.entitledDays !== expectedEntitledDays
? await db.vacationEntitlement.update({
where: { id: entitlement.id },
data: {
carryoverDays,
entitledDays: expectedEntitledDays,
},
})
: entitlement;
const yearStart = new Date(`${year}-01-01T00:00:00.000Z`);
const yearEnd = new Date(`${year}-12-31T00:00:00.000Z`);
const vacations = await db.vacation.findMany({
where: {
resourceId,
type: { in: BALANCE_TYPES },
startDate: { lte: yearEnd },
endDate: { gte: yearStart },
status: { in: [VacationStatus.APPROVED, VacationStatus.PENDING] },
},
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<ResourceHolidayContext> | null = null;
const getLegacyHolidayContext = async () => {
if (!legacyHolidayContextPromise) {
legacyHolidayContextPromise = deps.loadResourceHolidayContext(
db,
resourceId,
yearStart,
yearEnd,
);
}
return legacyHolidayContextPromise;
};
for (const vacation of vacations) {
const days = await calculateEntitlementVacationDays(
yearStart,
yearEnd,
vacation,
getLegacyHolidayContext,
deps,
);
if (vacation.status === VacationStatus.APPROVED) {
usedDays += days;
} else {
pendingDays += days;
}
}
return db.vacationEntitlement.update({
where: { id: entitlementWithCarryover.id },
data: { usedDays, pendingDays },
});
}