import { getPublicHolidays, toIsoDate } from "@capakraken/shared"; export type HolidayCalendarSeedScope = "COUNTRY" | "STATE" | "CITY"; export type HolidayCalendarSeedEntry = { date: string; name: string; isRecurringAnnual: boolean; }; export type HolidayCalendarSeedDefinition = { name: string; scopeType: HolidayCalendarSeedScope; countryCode: string; stateCode?: string; cityName?: string; priority: number; entries: HolidayCalendarSeedEntry[]; }; type SeedContext = { availableCountryCodes: string[]; availableCitiesByCountry: Record; activeGermanStates?: string[]; years: number[]; }; type SimpleHolidayDefinition = { name: string; resolveDate: (year: number) => string; }; const COUNTRY_PRIORITY = 10; const STATE_PRIORITY = 20; const CITY_PRIORITY = 30; function dateUtc(year: number, month: number, day: number): Date { return new Date(Date.UTC(year, month - 1, day)); } function addDays(date: Date, amount: number): Date { const next = new Date(date); next.setUTCDate(next.getUTCDate() + amount); return next; } function computeEasterSunday(year: number): Date { const a = year % 19; const b = Math.floor(year / 100); const c = year % 100; const d = Math.floor(b / 4); const e = b % 4; const f = Math.floor((b + 8) / 25); const g = Math.floor((b - f + 1) / 3); const h = (19 * a + b - d - g + 15) % 30; const i = Math.floor(c / 4); const k = c % 4; const l = (32 + 2 * e + 2 * i - h - k) % 7; const m = Math.floor((a + 11 * h + 22 * l) / 451); const month = Math.floor((h + l - 7 * m + 114) / 31); const day = ((h + l - 7 * m + 114) % 31) + 1; return dateUtc(year, month, day); } function nthWeekdayOfMonth(year: number, month: number, weekday: number, occurrence: number): string { const firstDay = dateUtc(year, month, 1); const delta = (weekday - firstDay.getUTCDay() + 7) % 7; const day = 1 + delta + ((occurrence - 1) * 7); return toIsoDate(dateUtc(year, month, day)); } function lastWeekdayOfMonth(year: number, month: number, weekday: number): string { const lastDay = dateUtc(year, month + 1, 0); const delta = (lastDay.getUTCDay() - weekday + 7) % 7; lastDay.setUTCDate(lastDay.getUTCDate() - delta); return toIsoDate(lastDay); } function observedUsFixedHoliday(year: number, month: number, day: number): string { const holiday = dateUtc(year, month, day); const weekday = holiday.getUTCDay(); if (weekday === 6) { return toIsoDate(addDays(holiday, -1)); } if (weekday === 0) { return toIsoDate(addDays(holiday, 1)); } return toIsoDate(holiday); } function buildEntries(years: number[], definitions: SimpleHolidayDefinition[]): HolidayCalendarSeedEntry[] { const entries = new Map(); for (const year of years) { for (const definition of definitions) { const date = definition.resolveDate(year); entries.set(date, { date, name: definition.name, isRecurringAnnual: false, }); } } return [...entries.values()].sort((left, right) => left.date.localeCompare(right.date)); } function buildGermanCountryEntries(years: number[]): HolidayCalendarSeedEntry[] { return buildEntries( years, years.flatMap((year) => getPublicHolidays(year) .filter((holiday) => holiday.federal) .map((holiday) => ({ name: holiday.name, resolveDate: () => holiday.date, })), ), ); } function buildGermanStateEntries(years: number[], stateCode: string): HolidayCalendarSeedEntry[] { return buildEntries( years, years.flatMap((year) => getPublicHolidays(year, stateCode) .filter((holiday) => !holiday.federal) .map((holiday) => ({ name: holiday.name, resolveDate: () => holiday.date, })), ), ); } function buildSpanishCountryEntries(years: number[]): HolidayCalendarSeedEntry[] { return buildEntries(years, [ { name: "Ano Nuevo", resolveDate: (year) => toIsoDate(dateUtc(year, 1, 1)) }, { name: "Epifania del Senor", resolveDate: (year) => toIsoDate(dateUtc(year, 1, 6)) }, { name: "Viernes Santo", resolveDate: (year) => toIsoDate(addDays(computeEasterSunday(year), -2)) }, { name: "Fiesta del Trabajo", resolveDate: (year) => toIsoDate(dateUtc(year, 5, 1)) }, { name: "Asuncion de la Virgen", resolveDate: (year) => toIsoDate(dateUtc(year, 8, 15)) }, { name: "Fiesta Nacional de Espana", resolveDate: (year) => toIsoDate(dateUtc(year, 10, 12)) }, { name: "Todos los Santos", resolveDate: (year) => toIsoDate(dateUtc(year, 11, 1)) }, { name: "Dia de la Constitucion", resolveDate: (year) => toIsoDate(dateUtc(year, 12, 6)) }, { name: "Inmaculada Concepcion", resolveDate: (year) => toIsoDate(dateUtc(year, 12, 8)) }, { name: "Navidad", resolveDate: (year) => toIsoDate(dateUtc(year, 12, 25)) }, ]); } function buildIndianCountryEntries(years: number[]): HolidayCalendarSeedEntry[] { return buildEntries(years, [ { name: "Republic Day", resolveDate: (year) => toIsoDate(dateUtc(year, 1, 26)) }, { name: "Good Friday", resolveDate: (year) => toIsoDate(addDays(computeEasterSunday(year), -2)) }, { name: "Independence Day", resolveDate: (year) => toIsoDate(dateUtc(year, 8, 15)) }, { name: "Gandhi Jayanti", resolveDate: (year) => toIsoDate(dateUtc(year, 10, 2)) }, ]); } function buildUsCountryEntries(years: number[]): HolidayCalendarSeedEntry[] { return buildEntries(years, [ { name: "New Year's Day", resolveDate: (year) => observedUsFixedHoliday(year, 1, 1) }, { name: "Martin Luther King Jr. Day", resolveDate: (year) => nthWeekdayOfMonth(year, 1, 1, 3) }, { name: "Memorial Day", resolveDate: (year) => lastWeekdayOfMonth(year, 5, 1) }, { name: "Independence Day", resolveDate: (year) => observedUsFixedHoliday(year, 7, 4) }, { name: "Labor Day", resolveDate: (year) => nthWeekdayOfMonth(year, 9, 1, 1) }, { name: "Thanksgiving Day", resolveDate: (year) => nthWeekdayOfMonth(year, 11, 4, 4) }, { name: "Christmas Day", resolveDate: (year) => observedUsFixedHoliday(year, 12, 25) }, ]); } function normalizeCountryCodes(countryCodes: string[]): Set { return new Set(countryCodes.map((countryCode) => countryCode.trim().toUpperCase())); } function normalizeCityLookup(availableCitiesByCountry: Record): Map> { const lookup = new Map>(); for (const [countryCode, cityNames] of Object.entries(availableCitiesByCountry)) { lookup.set(countryCode.trim().toUpperCase(), new Set(cityNames)); } return lookup; } function hasCity(cityLookup: Map>, countryCode: string, cityName: string): boolean { return cityLookup.get(countryCode)?.has(cityName) ?? false; } function germanStateDisplayName(stateCode: string): string { switch (stateCode) { case "BW": return "Baden-Wuerttemberg"; case "BY": return "Bayern"; case "HH": return "Hamburg"; case "NW": return "Nordrhein-Westfalen"; default: return stateCode; } } function buildCityEntries(years: number[], definitions: SimpleHolidayDefinition[]): HolidayCalendarSeedEntry[] { return buildEntries(years, definitions); } export function buildHolidayCalendarSeedDefinitions( context: SeedContext, ): HolidayCalendarSeedDefinition[] { const availableCountries = normalizeCountryCodes(context.availableCountryCodes); const cityLookup = normalizeCityLookup(context.availableCitiesByCountry); const definitions: HolidayCalendarSeedDefinition[] = []; if (availableCountries.has("DE")) { definitions.push({ name: "Referenzfeiertage Deutschland 2026-2027", scopeType: "COUNTRY", countryCode: "DE", priority: COUNTRY_PRIORITY, entries: buildGermanCountryEntries(context.years), }); const germanStates = new Set( [...(context.activeGermanStates ?? []), "BY", "NW"] .map((stateCode) => stateCode.trim().toUpperCase()) .filter((stateCode) => ["BW", "BY", "HH", "NW"].includes(stateCode)), ); for (const stateCode of [...germanStates].sort()) { const entries = buildGermanStateEntries(context.years, stateCode); if (entries.length === 0) { continue; } definitions.push({ name: `Referenzfeiertage Deutschland - ${germanStateDisplayName(stateCode)} 2026-2027`, scopeType: "STATE", countryCode: "DE", stateCode, priority: STATE_PRIORITY, entries, }); } if (hasCity(cityLookup, "DE", "Augsburg")) { definitions.push({ name: "Referenzfeiertage Deutschland - Augsburg 2026-2027", scopeType: "CITY", countryCode: "DE", cityName: "Augsburg", priority: CITY_PRIORITY, entries: buildCityEntries(context.years, [ { name: "Augsburger Friedensfest", resolveDate: (year) => toIsoDate(dateUtc(year, 8, 8)) }, ]), }); } } if (availableCountries.has("ES")) { definitions.push({ name: "Referenzfeiertage Spanien 2026-2027", scopeType: "COUNTRY", countryCode: "ES", priority: COUNTRY_PRIORITY, entries: buildSpanishCountryEntries(context.years), }); if (hasCity(cityLookup, "ES", "Madrid")) { definitions.push({ name: "Referenzfeiertage Spanien - Madrid 2026-2027", scopeType: "CITY", countryCode: "ES", cityName: "Madrid", priority: CITY_PRIORITY, entries: buildEntries(context.years, [ { name: "San Isidro", resolveDate: (year) => toIsoDate(dateUtc(year, 5, 15)) }, { name: "Nuestra Senora de la Almudena", resolveDate: (year) => toIsoDate(dateUtc(year, 11, 9)) }, ]), }); } if (hasCity(cityLookup, "ES", "Barcelona")) { definitions.push({ name: "Referenzfeiertage Spanien - Barcelona 2026-2027", scopeType: "CITY", countryCode: "ES", cityName: "Barcelona", priority: CITY_PRIORITY, entries: buildEntries(context.years, [ { name: "La Merce", resolveDate: (year) => toIsoDate(dateUtc(year, 9, 24)) }, { name: "Santa Eulalia", resolveDate: (year) => toIsoDate(dateUtc(year, 2, 12)) }, ]), }); } } if (availableCountries.has("IN")) { definitions.push({ name: "Referenzfeiertage Indien 2026-2027", scopeType: "COUNTRY", countryCode: "IN", priority: COUNTRY_PRIORITY, entries: buildIndianCountryEntries(context.years), }); } if (availableCountries.has("US")) { definitions.push({ name: "Referenzfeiertage USA 2026-2027", scopeType: "COUNTRY", countryCode: "US", priority: COUNTRY_PRIORITY, entries: buildUsCountryEntries(context.years), }); } return definitions; }