import dayjs from "@calcom/dayjs"; import { HolidayRepository } from "@calcom/features/holidays/repositories/HolidayRepository"; import { GOOGLE_HOLIDAY_CALENDARS, HOLIDAY_CACHE_DAYS } from "./constants"; import { getGoogleCalendarClient, type GoogleCalendarClient, type GoogleCalendarHoliday, } from "./GoogleCalendarClient"; export interface CachedHoliday { id: string; countryCode: string; eventId: string; name: string; date: Date; year: number; } export class HolidayServiceCachingProxy { private calendarClient: GoogleCalendarClient; constructor(calendarClient?: GoogleCalendarClient) { this.calendarClient = calendarClient || getGoogleCalendarClient(); } private async isCacheStale(countryCode: string, year: number): Promise { const staleDate = dayjs().subtract(HOLIDAY_CACHE_DAYS, "days").toDate(); const cachedEntry = await HolidayRepository.findFirstCacheEntry({ countryCode, year }); if (!cachedEntry) { return true; } return cachedEntry.updatedAt < staleDate; } private async refreshCache(countryCode: string, year: number): Promise { const calendarConfig = GOOGLE_HOLIDAY_CALENDARS[countryCode]; if (!calendarConfig) { return; } try { const holidays = await this.calendarClient.fetchHolidays(countryCode, year); await HolidayRepository.refreshCache({ countryCode, year, holidays: holidays.map((h: GoogleCalendarHoliday) => ({ countryCode: h.countryCode, calendarId: calendarConfig.calendarId, eventId: h.eventId, name: h.name, date: h.date, year: h.year, })), }); } catch (error) { console.error(`Failed to refresh holiday cache for ${countryCode}:`, error); } } async getHolidaysForCountry(countryCode: string, year: number): Promise { const isStale = await this.isCacheStale(countryCode, year); if (isStale) { await this.refreshCache(countryCode, year); } const cached = await HolidayRepository.findManyCachedHolidays({ countryCode, year }); return cached.map( (h): CachedHoliday => ({ id: h.id, countryCode: h.countryCode, eventId: h.eventId, name: h.name, date: h.date, year: h.year, }) ); } async getHolidaysInRange(countryCode: string, startDate: Date, endDate: Date): Promise { const startYear = dayjs(startDate).year(); const endYear = dayjs(endDate).year(); for (let year = startYear; year <= endYear; year++) { const isStale = await this.isCacheStale(countryCode, year); if (isStale) { await this.refreshCache(countryCode, year); } } const cached = await HolidayRepository.findCachedHolidaysInRange({ countryCode, startDate, endDate, }); return cached.map( (h): CachedHoliday => ({ id: h.id, countryCode: h.countryCode, eventId: h.eventId, name: h.name, date: h.date, year: h.year, }) ); } } let defaultProxy: HolidayServiceCachingProxy | null = null; export function getHolidayServiceCachingProxy(): HolidayServiceCachingProxy { if (!defaultProxy) { defaultProxy = new HolidayServiceCachingProxy(); } return defaultProxy; }