import { getPublicHolidays, type AbsenceDay } from "@capakraken/shared"; type VacationLike = { startDate: Date; endDate: Date; type: string; isHalfDay: boolean; }; type HolidayAvailabilityInput = { vacations: VacationLike[]; periodStart: Date; periodEnd: Date; countryCode?: string | null | undefined; federalState?: string | null | undefined; metroCityName?: string | null | undefined; resolvedHolidayStrings?: string[] | undefined; }; type HolidayAvailabilityResult = { absenceDateStrings: string[]; publicHolidayStrings: string[]; absenceDays: AbsenceDay[]; }; export type CalendarHoliday = { date: string; name: string; scope: "COUNTRY" | "STATE" | "CITY"; }; type CalendarScope = CalendarHoliday["scope"]; type HolidayCalendarEntryRecord = { date: Date; name: string; isRecurringAnnual: boolean; }; type HolidayCalendarRecord = { id: string; name: string; scopeType: CalendarScope; priority: number; createdAt?: Date; entries: HolidayCalendarEntryRecord[]; }; type HolidayResolverDb = { [key: string]: unknown; country?: { findUnique: (args: any) => any; }; metroCity?: { findUnique: (args: any) => any; }; holidayCalendar?: { findMany: (args: any) => any; }; }; type ResolvedHoliday = CalendarHoliday & { calendarName: string; priority: number; sourceType: "BUILTIN" | "CUSTOM"; }; export function asHolidayResolverDb(db: unknown): HolidayResolverDb { return db as HolidayResolverDb; } export function toIsoDate(value: Date): string { return value.toISOString().slice(0, 10); } type CityHolidayRule = { countryCode: string; cityName: string; resolveDates: (year: number) => string[]; }; const CITY_HOLIDAY_RULES: CityHolidayRule[] = [ { countryCode: "DE", cityName: "Augsburg", resolveDates: (year) => [`${year}-08-08`], }, ]; const SCOPE_WEIGHT: Record = { COUNTRY: 1, STATE: 2, CITY: 3, }; function normalizeCityName(cityName?: string | null): string | null { const normalized = cityName?.trim().toLowerCase(); return normalized && normalized.length > 0 ? normalized : null; } function normalizeStateCode(stateCode?: string | null): string | null { const normalized = stateCode?.trim().toUpperCase(); return normalized && normalized.length > 0 ? normalized : null; } function resolveCalendarEntries( calendars: HolidayCalendarRecord[], periodStart: Date, periodEnd: Date, ): ResolvedHoliday[] { const startYear = periodStart.getUTCFullYear(); const endYear = periodEnd.getUTCFullYear(); const startIso = toIsoDate(periodStart); const endIso = toIsoDate(periodEnd); const resolved = new Map(); for (const calendar of calendars) { for (const entry of calendar.entries) { const baseDate = new Date(entry.date); for (let year = startYear; year <= endYear; year += 1) { const effectiveDate = entry.isRecurringAnnual ? new Date(Date.UTC(year, baseDate.getUTCMonth(), baseDate.getUTCDate())) : baseDate; const key = toIsoDate(effectiveDate); if (key < startIso || key > endIso) { if (!entry.isRecurringAnnual) { break; } continue; } const candidate: ResolvedHoliday = { date: key, name: entry.name, scope: calendar.scopeType, calendarName: calendar.name, priority: calendar.priority, sourceType: "CUSTOM", }; const existing = resolved.get(key); if ( !existing || SCOPE_WEIGHT[candidate.scope] > SCOPE_WEIGHT[existing.scope] || ( SCOPE_WEIGHT[candidate.scope] === SCOPE_WEIGHT[existing.scope] && candidate.priority > existing.priority ) || ( SCOPE_WEIGHT[candidate.scope] === SCOPE_WEIGHT[existing.scope] && candidate.priority === existing.priority && existing.sourceType === "BUILTIN" ) ) { resolved.set(key, candidate); } if (!entry.isRecurringAnnual) { break; } } } } return [...resolved.values()].sort((left, right) => left.date.localeCompare(right.date)); } function mergeResolvedHolidays( builtInHolidays: CalendarHoliday[], customHolidays: ResolvedHoliday[], ): ResolvedHoliday[] { const merged = new Map(); for (const holiday of builtInHolidays) { merged.set(holiday.date, { ...holiday, calendarName: "System", priority: Number.MIN_SAFE_INTEGER, sourceType: "BUILTIN", }); } for (const holiday of customHolidays) { const existing = merged.get(holiday.date); if ( !existing || SCOPE_WEIGHT[holiday.scope] > SCOPE_WEIGHT[existing.scope] || ( SCOPE_WEIGHT[holiday.scope] === SCOPE_WEIGHT[existing.scope] && holiday.priority >= existing.priority ) ) { merged.set(holiday.date, holiday); } } return [...merged.values()].sort((left, right) => left.date.localeCompare(right.date)); } async function loadScopedHolidayCalendars( db: HolidayResolverDb, input: { countryId?: string | null | undefined; stateCode?: string | null | undefined; metroCityId?: string | null | undefined; }, ): Promise { if (!input.countryId || typeof db.holidayCalendar?.findMany !== "function") { return []; } const stateCode = normalizeStateCode(input.stateCode); const metroCityId = input.metroCityId?.trim() || null; return db.holidayCalendar.findMany({ where: { isActive: true, countryId: input.countryId, OR: [ { scopeType: "COUNTRY" }, ...(stateCode ? [{ scopeType: "STATE" as const, stateCode }] : []), ...(metroCityId ? [{ scopeType: "CITY" as const, metroCityId }] : []), ], }, include: { entries: true }, orderBy: [{ priority: "asc" }, { createdAt: "asc" }], }); } export function getCalendarHolidayStrings( periodStart: Date, periodEnd: Date, countryCode?: string | null, federalState?: string | null, metroCityName?: string | null, ): string[] { return getCalendarHolidays( periodStart, periodEnd, countryCode, federalState, metroCityName, ).map((holiday) => holiday.date); } export function getCalendarHolidays( periodStart: Date, periodEnd: Date, countryCode?: string | null, federalState?: string | null, metroCityName?: string | null, ): CalendarHoliday[] { const startYear = periodStart.getUTCFullYear(); const endYear = periodEnd.getUTCFullYear(); const holidays = new Map(); if (countryCode === "DE") { for (let year = startYear; year <= endYear; year += 1) { for (const holiday of getPublicHolidays(year, federalState ?? undefined)) { if (holiday.date >= toIsoDate(periodStart) && holiday.date <= toIsoDate(periodEnd)) { holidays.set(holiday.date, { date: holiday.date, name: holiday.name, scope: holiday.federal ? "COUNTRY" : "STATE", }); } } } } const normalizedCityName = normalizeCityName(metroCityName); if (countryCode && normalizedCityName) { for (const rule of CITY_HOLIDAY_RULES) { if ( rule.countryCode === countryCode && normalizeCityName(rule.cityName) === normalizedCityName ) { for (let year = startYear; year <= endYear; year += 1) { for (const holidayDate of rule.resolveDates(year)) { if (holidayDate >= toIsoDate(periodStart) && holidayDate <= toIsoDate(periodEnd)) { holidays.set(holidayDate, { date: holidayDate, name: "Augsburger Friedensfest", scope: "CITY", }); } } } } } } return [...holidays.values()].sort((left, right) => left.date.localeCompare(right.date)); } export async function getResolvedCalendarHolidays( db: HolidayResolverDb, input: { periodStart: Date; periodEnd: Date; countryId?: string | null | undefined; countryCode?: string | null | undefined; federalState?: string | null | undefined; metroCityId?: string | null | undefined; metroCityName?: string | null | undefined; }, ): Promise { let countryCode = input.countryCode ?? null; if (!countryCode && input.countryId && typeof db.country?.findUnique === "function") { const country = await db.country.findUnique({ where: { id: input.countryId }, select: { code: true }, }); countryCode = country?.code ?? null; } let metroCityName = input.metroCityName ?? null; if (!metroCityName && input.metroCityId && typeof db.metroCity?.findUnique === "function") { const metroCity = await db.metroCity.findUnique({ where: { id: input.metroCityId }, select: { name: true }, }); metroCityName = metroCity?.name ?? null; } const builtIn = getCalendarHolidays( input.periodStart, input.periodEnd, countryCode, input.federalState, metroCityName, ); const calendars = await loadScopedHolidayCalendars(db, { countryId: input.countryId, stateCode: input.federalState, metroCityId: input.metroCityId, }); const custom = resolveCalendarEntries(calendars, input.periodStart, input.periodEnd); return mergeResolvedHolidays(builtIn, custom); } export async function getResolvedCalendarHolidayStrings( db: HolidayResolverDb, input: { periodStart: Date; periodEnd: Date; countryId?: string | null | undefined; countryCode?: string | null | undefined; federalState?: string | null | undefined; metroCityId?: string | null | undefined; metroCityName?: string | null | undefined; }, ): Promise { const holidays = await getResolvedCalendarHolidays(db, input); return holidays.map((holiday) => holiday.date); } export function collectHolidayAvailability( input: HolidayAvailabilityInput, ): HolidayAvailabilityResult { const periodStartIso = toIsoDate(input.periodStart); const periodEndIso = toIsoDate(input.periodEnd); const publicHolidaySet = new Set( input.resolvedHolidayStrings ? input.resolvedHolidayStrings.filter((date) => date >= periodStartIso && date <= periodEndIso) : getCalendarHolidayStrings( input.periodStart, input.periodEnd, input.countryCode, input.federalState, input.metroCityName, ), ); const absenceDateSet = new Set(); const absenceDayMap = new Map(); for (const isoDate of publicHolidaySet) { absenceDayMap.set(isoDate, { date: new Date(`${isoDate}T00:00:00.000Z`), type: "PUBLIC_HOLIDAY", }); } for (const vacation of input.vacations) { if (vacation.type !== "PUBLIC_HOLIDAY") { continue; } const overlapStart = new Date( Math.max(vacation.startDate.getTime(), input.periodStart.getTime()), ); const overlapEnd = new Date( Math.min(vacation.endDate.getTime(), input.periodEnd.getTime()), ); if (overlapStart > overlapEnd) { continue; } const cursor = new Date(overlapStart); cursor.setUTCHours(0, 0, 0, 0); const end = new Date(overlapEnd); end.setUTCHours(0, 0, 0, 0); while (cursor <= end) { const isoDate = toIsoDate(cursor); publicHolidaySet.add(isoDate); absenceDayMap.set(isoDate, { date: new Date(cursor), type: "PUBLIC_HOLIDAY", ...(vacation.isHalfDay ? { isHalfDay: true } : {}), }); cursor.setUTCDate(cursor.getUTCDate() + 1); } } for (const vacation of input.vacations) { if (vacation.type === "PUBLIC_HOLIDAY") { continue; } const overlapStart = new Date( Math.max(vacation.startDate.getTime(), input.periodStart.getTime()), ); const overlapEnd = new Date( Math.min(vacation.endDate.getTime(), input.periodEnd.getTime()), ); if (overlapStart > overlapEnd) { continue; } const cursor = new Date(overlapStart); cursor.setUTCHours(0, 0, 0, 0); const end = new Date(overlapEnd); end.setUTCHours(0, 0, 0, 0); const triggerType = vacation.type === "SICK" ? "SICK" : "VACATION"; while (cursor <= end) { const isoDate = toIsoDate(cursor); if (!publicHolidaySet.has(isoDate)) { absenceDateSet.add(isoDate); absenceDayMap.set(isoDate, { date: new Date(cursor), type: triggerType, ...(vacation.isHalfDay ? { isHalfDay: true } : {}), }); } cursor.setUTCDate(cursor.getUTCDate() + 1); } } return { absenceDateStrings: [...absenceDateSet].sort(), publicHolidayStrings: [...publicHolidaySet].sort(), absenceDays: [...absenceDayMap.values()], }; }