Skip to content

Commit 12e07f2

Browse files
authored
feat: Add Holidays feature to block availability on public holidays (calcom#25561)
* feat: add holidays feature for automatic availability blocking- Add UserHolidaySettings model for storing user preferences- Generate static holiday data for 20 countries using date-holidays- Create HolidayService for runtime holiday queries- Add TRPC router with endpoints for country selection and holiday toggles- Create Holidays tab UI in Availability page with conflict warnings- Integrate holiday blocking into getUserAvailability calculation- Show holiday indicator on blocked dates in booker page- Add warning in OOO modal when dates overlap with holidays * feat: add optimizations, tests, and code quality improvements - Add memoization/caching to HolidayService for better performance - Optimize checkConflicts DB query with OR conditions for specific dates - Add HolidayService unit tests (10 tests) - Add error handling with Alert component for failed queries - Memoize HolidayListItem component to prevent unnecessary re-renders - Extract magic numbers into constants.ts - Use TRPCError consistently in handlers - Add missing i18n keys for error messages - Update handlers to follow Cal.com patterns (default exports, minimal comments) - Add regeneration instructions in constants * refactor: replace static JSON with Google Calendar API integration - Add GoogleHolidayService to fetch holidays from Google Calendar public calendars - Add HolidayCache Prisma model for caching API responses - Add GOOGLE_CALENDAR_API_KEY and HOLIDAY_CACHE_DAYS env variables - Support 38 countries via Google Calendar holiday calendars - Update HolidayService methods to async with database caching - Update all TRPC handlers for async holiday methods - Fix UI to display holiday dates correctly - Remove static holidays.json and generate script * use we instead of calcom in i18n message * address cubics comments * move holidays from availability to ooo * public holidays filter for holidays * follow i18n _one and _other pattern * remove holiday feature flag * revert lint command code change * revert lint command code change 2.0 * revert lint command code change 3.0 * bye bye my christmas emoji :crying-emoji * remove comments * refactor(holidays): add repository pattern, split services, and add tests - Create HolidayRepository for database operations - Split GoogleHolidayService into GoogleCalendarClient and HolidayCacheService - Add dependency injection to HolidayService and HolidayCacheService - Update TRPC handlers to use HolidayRepository - Add tests for HolidayRepository and calculateHolidayBlockedDates - Update calendar IDs to use official holiday format (244 countries + religions) * fix: address PR review feedback - Remove unused date-holidays package - Add pluralization for and_more_holidays_with_conflicts translation - DRY: spread GOOGLE_RELIGIOUS_HOLIDAY_CALENDARS into GOOGLE_HOLIDAY_CALENDARS - Add select to userHolidaySettings query in getUserAvailability - Optimize checkConflicts with pre-computed timestamps * refactor(holidays): apply proxy pattern and move logic to service - Rename HolidayCacheService to HolidayServiceCachingProxy (proxy pattern) - Remove HOLIDAY_CACHE_DAYS env var, use constant directly - Add isSupportedCountry() method to HolidayService - Add getUserSettings() and updateSettings() to HolidayService - Move toggleHoliday logic from handler to service - Move checkConflicts logic from handler to service - Add findBookingsInDateRanges() to HolidayRepository - Simplify all handlers to just call service methods * feat(bookings): add backend validation to prevent booking on holidays Adds explicit holiday conflict validation during booking creation to handle the race condition where a host enables a holiday after a guest selects a date but before they submit the booking. Changes: - Add checkHolidayConflict validation with HolidayRepository integration - Integrate ensureNoHolidayConflict in RegularBookingService - Add BookingOnHoliday error code with proper HTTP 400 response - Handle holiday error display in BookEventForm with name interpolation - Hide trace ID for expected validation errors - Add unit tests for holiday conflict validation * fix failing type check * fix: address PR review comments for holiday feature - Change error code from BAD_REQUEST to INTERNAL_SERVER_ERROR in toggleHoliday handler (errors are internal failures, not bad input) - Refactor ensureNoHolidayConflict to use Promise.all for parallel user checking instead of sequential loop * refactor: use Promise.all with logging in loop for holiday check - Check all users in parallel using Promise.all - Log conflicts inside the loop as they are detected - Wait for all checks to complete before throwing error * fix(holidays): use host timezone for holiday conflict checks The holiday feature was checking booking dates against holidays using server/local timezone instead of the host's timezone. This caused bookings near midnight boundaries to incorrectly pass or fail the holiday check. Example: A booking at Dec 24th 8PM UTC (which is Dec 25th in IST) was not being blocked for an Indian host with Christmas as a holiday. Changes: - Add .utc() to holiday date formatting for consistency - Fetch host's timezone in checkHolidayConflict - Convert booking time to host's timezone before comparison - Add findUserSettingsWithTimezone to HolidayRepository - Update error message to clarify it's the host's local time - Add timezone edge case tests * fix(holidays): align holiday blocking with OOO pattern for consistent timezone handling - Simplify calculateHolidayBlockedDates to match OOO pattern using dayjs.utc() - Fix date range query to use full day bounds (startOfDay/endOfDay) so holidays stored at midnight UTC are correctly found during booking validation - Remove separate checkHolidayConflict booking-time validation - holidays now block through oooExcludedDateRanges like OOO does - Remove getHolidayOnDate from HolidayService (no longer needed) - Remove findUserSettingsWithTimezone from HolidayRepository (no longer needed) - Clean up related tests Holiday blocking now works exactly like OOO: 1. Holidays are added to datesOutOfOffice in calculateHolidayBlockedDates 2. buildDateRanges processes them via processOOO with .tz(timeZone, true) 3. oooExcludedDateRanges excludes those dates from availability 4. ensureAvailableUsers uses oooExcludedDateRanges to block bookings This ensures consistent timezone handling where Dec 25th blocks all hours of Dec 25th in the host's timezone, regardless of booker's timezone. * update test * update .env.example file
1 parent 18415e3 commit 12e07f2

41 files changed

Lines changed: 2559 additions & 35 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.env.example

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,14 @@ NEXT_PUBLIC_HELPSCOUT_KEY=
127127
NEXT_PUBLIC_FRESHCHAT_TOKEN=
128128
NEXT_PUBLIC_FRESHCHAT_HOST=
129129

130+
# For holiday feature:
131+
# Step-by-step: Get a Google Calendar API Key
132+
# 1. Go to Google Cloud Console: https://console.cloud.google.com/
133+
# 2. Select or Create a Project
134+
# 3. Enable Google Calendar API (APIs & Services → Library , Search for Google Calendar API)
135+
# 4. Create the API Key (APIs & Services → Credentials)
136+
GOOGLE_CALENDAR_API_KEY=
137+
130138
# Google OAuth credentials
131139
# To enable Login with Google you need to:
132140
# 1. Set `GOOGLE_API_CREDENTIALS` below

apps/web/modules/settings/my-account/out-of-office-view.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@ import { useEffect, useState } from "react";
44

55
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
66

7-
import OutOfOfficeEntriesList from "@calcom/features/settings/outOfOffice/OutOfOfficeEntriesList";
87
import { CreateOrEditOutOfOfficeEntryModal } from "@calcom/features/settings/outOfOffice/CreateOrEditOutOfOfficeModal";
98
import type { BookingRedirectForm } from "@calcom/features/settings/outOfOffice/CreateOrEditOutOfOfficeModal";
9+
import { HolidaysView } from "@calcom/features/settings/outOfOffice/HolidaysView";
10+
import OutOfOfficeEntriesList from "@calcom/features/settings/outOfOffice/OutOfOfficeEntriesList";
11+
import { OutOfOfficeTab } from "@calcom/features/settings/outOfOffice/OutOfOfficeToggleGroup";
1012

1113
export default function OutOfOfficeView() {
1214
const [openModal, setOpenModal] = useState(false);
@@ -15,6 +17,7 @@ export default function OutOfOfficeView() {
1517

1618
const params = useCompatSearchParams();
1719
const openModalOnStart = !!params?.get("om");
20+
const selectedTab = params?.get("type") ?? OutOfOfficeTab.MINE;
1821

1922
useEffect(() => {
2023
if (openModalOnStart) {
@@ -37,6 +40,11 @@ export default function OutOfOfficeView() {
3740
setCurrentlyEditingOutOfOfficeEntry(null);
3841
};
3942

43+
// Show HolidaysView when holidays tab is selected
44+
if (selectedTab === OutOfOfficeTab.HOLIDAYS) {
45+
return <HolidaysView />;
46+
}
47+
4048
return (
4149
<>
4250
<OutOfOfficeEntriesList
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import { createNextApiHandler } from "@calcom/trpc/server/createNextApiHandler";
2+
import { holidaysRouter } from "@calcom/trpc/server/routers/viewer/holidays/_router";
3+
4+
export default createNextApiHandler(holidaysRouter);

apps/web/public/static/locales/en/common.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4110,6 +4110,26 @@
41104110
"booking_response": "Booking Response",
41114111
"list_view": "List view",
41124112
"calendar_view": "Calendar view",
4113+
"holidays": "Holidays",
4114+
"holidays_description": "We will automatically mark you as unavailable for the selected holidays",
4115+
"country_for_holidays": "Country for holidays",
4116+
"select_country": "Select country",
4117+
"no_holidays": "No holidays",
4118+
"holidays_list": "Holidays",
4119+
"no_holidays_selected": "No holidays selected",
4120+
"select_country_to_see_holidays": "Select a country to see available holidays",
4121+
"holiday_settings_updated": "Holiday settings updated",
4122+
"holiday_no_availability": "No availability on this holiday",
4123+
"holiday_booking_conflict_warning_one": "You have {{count}} booking on holidays",
4124+
"holiday_booking_conflict_warning_other": "You have {{count}} bookings on holidays",
4125+
"holiday_overlap_info": "Holiday notice",
4126+
"holiday_overlap_message_single": "{{date}} is {{holiday}}, which is already blocked as a holiday",
4127+
"holiday_overlap_message_multiple": "{{count}} days in this range are holidays ({{holidays}})",
4128+
"no_holidays_found_for_country": "No holidays found for this country.",
4129+
"booking_count_one": "{{count}} booking",
4130+
"booking_count_other": "{{count}} bookings",
4131+
"and_more_holidays_with_conflicts_one": "... and {{count}} more holiday with conflicts",
4132+
"and_more_holidays_with_conflicts_other": "... and {{count}} more holidays with conflicts",
41134133
"assignment_reason": "Assignment Reason",
41144134
"saved": "Saved",
41154135
"booking_history": "Booking History",

0 commit comments

Comments
 (0)