-
Notifications
You must be signed in to change notification settings - Fork 11.7k
feat: Add Holidays feature to block availability on public holidays #25561
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
…serHolidaySettings 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
- 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
- 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
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
7 issues found across 37 files
Prompt for AI agents (all 7 issues)
Check if these issues are valid — if so, understand the root cause of each and fix them.
<file name="packages/prisma/migrations/20251202181340_add_user_holiday_settings/migration.sql">
<violation number="1" location="packages/prisma/migrations/20251202181340_add_user_holiday_settings/migration.sql:17">
P2: Redundant index on `userId`. The `UserHolidaySettings_userId_key` unique index already provides lookup capability for the `userId` column, making the additional `UserHolidaySettings_userId_idx` index unnecessary. This adds storage overhead and slows down inserts/updates without benefit.</violation>
</file>
<file name="packages/lib/holidays/GoogleHolidayService.ts">
<violation number="1" location="packages/lib/holidays/GoogleHolidayService.ts:161">
P1: Race condition: If `createMany` fails after `deleteMany` succeeds, the cache data is lost. The comment 'we'll use stale cache if available' is incorrect since the stale cache was already deleted. Consider using a Prisma transaction to make delete and insert atomic, or insert first and then delete old entries.</violation>
</file>
<file name="packages/features/availability/lib/getUserAvailability.ts">
<violation number="1" location="packages/features/availability/lib/getUserAvailability.ts:570">
P2: `Object.assign` overwrites existing OOO entries when a holiday falls on the same date. This loses user's custom OOO data including `toUser` redirect and custom reason/emoji. Consider preserving OOO entries by only adding holidays for dates without existing OOO.</violation>
</file>
<file name="packages/trpc/server/routers/viewer/holidays/getUserSettings.handler.ts">
<violation number="1" location="packages/trpc/server/routers/viewer/holidays/getUserSettings.handler.ts:17">
P2: Prisma query should use `select` to only fetch the fields that are actually used (`countryCode` and `disabledIds`). This improves performance and follows project guidelines about selecting only needed data.</violation>
</file>
<file name="packages/trpc/server/routers/viewer/holidays/updateSettings.handler.ts">
<violation number="1" location="packages/trpc/server/routers/viewer/holidays/updateSettings.handler.ts:31">
P2: Prisma `upsert` should use `select` to return only the needed fields (`countryCode` and `disabledIds`). This follows the project guideline to only select data you need for better performance and to avoid unnecessary data exposure.</violation>
</file>
<file name="packages/lib/holidays/constants.ts">
<violation number="1" location="packages/lib/holidays/constants.ts:29">
P2: Belgium is mapped to the Dutch holiday calendar, which will show Dutch holidays (e.g., King's Day) instead of Belgian holidays (e.g., Belgian National Day on July 21st). This could cause users in Belgium to block incorrect dates.</violation>
<violation number="2" location="packages/lib/holidays/constants.ts:31">
P2: Switzerland is mapped to the German holiday calendar, which will show German holidays (e.g., German Unity Day) instead of Swiss holidays (e.g., Swiss National Day on August 1st). This could cause users in Switzerland to block incorrect dates.</violation>
</file>
Reply to cubic to teach it or ask questions. Re-run a review with @cubic-dev-ai review this PR
packages/prisma/migrations/20251202181340_add_user_holiday_settings/migration.sql
Outdated
Show resolved
Hide resolved
packages/trpc/server/routers/viewer/holidays/getUserSettings.handler.ts
Outdated
Show resolved
Hide resolved
packages/trpc/server/routers/viewer/holidays/updateSettings.handler.ts
Outdated
Show resolved
Hide resolved
CarinaWolli
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We want to move this to /my-account/out-of-office instead and it shows too many holidays that are not real public holidays
… 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.
| "AVATARAPI_PASSWORD", | ||
| "GIPHY_API_KEY", | ||
| "GOOGLE_API_CREDENTIALS", | ||
| "GOOGLE_CALENDAR_API_KEY", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That needs to be added to the .env.example file
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
added the following in .env.example:
# For holiday feature:
# Step-by-step: Get a Google Calendar API Key
# 1. Go to Google Cloud Console: https://console.cloud.google.com/
# 2. Select or Create a Project
# 3. Enable Google Calendar API (APIs & Services → Library , Search for Google Calendar API)
# 4. Create the API Key (APIs & Services → Credentials)
GOOGLE_CALENDAR_API_KEY=
eunjae-lee
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
looks good and it's working for me
addressed requested changes to move to OOO section
| import logger from "@calcom/lib/logger"; | ||
| import { safeStringify } from "@calcom/lib/safeStringify"; | ||
| import { withReporting } from "@calcom/lib/sentryWrapper"; | ||
| import prisma from "@calcom/prisma"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| import prisma from "@calcom/prisma"; | |
| import { prisma } from "@calcom/prisma"; |
| "use client"; | ||
|
|
||
| import { memo, useMemo, useCallback } from "react"; | ||
|
|
||
| import dayjs from "@calcom/dayjs"; | ||
| import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader"; | ||
| import { useLocale } from "@calcom/lib/hooks/useLocale"; | ||
| import type { RouterOutputs } from "@calcom/trpc/react"; | ||
| import { trpc } from "@calcom/trpc/react"; | ||
| import { Alert } from "@calcom/ui/components/alert"; | ||
| import { Button } from "@calcom/ui/components/button"; | ||
| import { EmptyScreen } from "@calcom/ui/components/empty-screen"; | ||
| import { Label, Select } from "@calcom/ui/components/form"; | ||
| import { Switch } from "@calcom/ui/components/form"; | ||
| import { SkeletonContainer, SkeletonText } from "@calcom/ui/components/skeleton"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let's move this component to apps/web/modules/settings/my-account/components because I am trying to remove all usages of trpc hooks in /features
Same for all other new React components you added in /features that import trpc
hbjORbj
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please address my comments in a follow-up PR, it's important feedback. Approving because @CarinaWolli needs this merged now
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
7 issues found across 41 files
Prompt for AI agents (all 7 issues)
Check if these issues are valid — if so, understand the root cause of each and fix them.
<file name="packages/trpc/server/routers/viewer/holidays/toggleHoliday.handler.ts">
<violation number="1" location="packages/trpc/server/routers/viewer/holidays/toggleHoliday.handler.ts:29">
P2: The error handling wraps all errors as `INTERNAL_SERVER_ERROR`, but the underlying service throws specific validation errors ("No holiday country selected", "Holiday not found for this country"). Consider checking the error message to return appropriate codes like `BAD_REQUEST` for validation failures or `NOT_FOUND` for missing resources.</violation>
</file>
<file name="packages/features/availability/lib/getUserAvailability.ts">
<violation number="1" location="packages/features/availability/lib/getUserAvailability.ts:29">
P2: Direct `prisma` import breaks the dependency injection pattern used throughout this class. Other database operations use `this.dependencies.oooRepo`, `this.dependencies.bookingRepo`, etc. Consider creating a holiday settings repository and injecting it via `this.dependencies` for consistency and testability.</violation>
<violation number="2" location="packages/features/availability/lib/getUserAvailability.ts:563">
P2: Holiday fetch errors are unhandled; a failure in the holiday service will throw and fail availability instead of degrading gracefully.</violation>
</file>
<file name="packages/lib/holidays/HolidayService.ts">
<violation number="1" location="packages/lib/holidays/HolidayService.ts:178">
P2: Toggling holidays validates only the current year, so next-year holidays shown to the user will be rejected as “not found.” Include next-year holidays in the validation set.</violation>
<violation number="2" location="packages/lib/holidays/HolidayService.ts:214">
P1: Holiday conflict windows are calculated in local time, which can miss/introduce conflicts for non-UTC users. Use UTC start/end of day when building date ranges.</violation>
<violation number="3" location="packages/lib/holidays/HolidayService.ts:232">
P1: Missing `.utc()` causes timezone inconsistency. Holiday dates are stored as UTC midnight (per comment on line 93), but here dayjs parses without `.utc()`. On a non-UTC server, `startOf('day')` will compute the wrong day boundaries, causing booking conflicts to be matched against incorrect dates.</violation>
</file>
<file name="packages/lib/holidays/HolidayServiceCachingProxy.ts">
<violation number="1" location="packages/lib/holidays/HolidayServiceCachingProxy.ts:24">
P2: Cache TTL is hardcoded to 7 days; the HOLIDAY_CACHE_DAYS env var is ignored, so operators cannot tune holiday cache freshness.</violation>
</file>
Reply to cubic to teach it or ask questions. Re-run a review with @cubic-dev-ai review this PR
| import logger from "@calcom/lib/logger"; | ||
| import { safeStringify } from "@calcom/lib/safeStringify"; | ||
| import { withReporting } from "@calcom/lib/sentryWrapper"; | ||
| import prisma from "@calcom/prisma"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P2: Direct prisma import breaks the dependency injection pattern used throughout this class. Other database operations use this.dependencies.oooRepo, this.dependencies.bookingRepo, etc. Consider creating a holiday settings repository and injecting it via this.dependencies for consistency and testability.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/features/availability/lib/getUserAvailability.ts, line 29:
<comment>Direct `prisma` import breaks the dependency injection pattern used throughout this class. Other database operations use `this.dependencies.oooRepo`, `this.dependencies.bookingRepo`, etc. Consider creating a holiday settings repository and injecting it via `this.dependencies` for consistency and testability.</comment>
<file context>
@@ -18,13 +18,15 @@ import { buildDateRanges, subtract } from "@calcom/features/schedules/lib/date-r
import logger from "@calcom/lib/logger";
import { safeStringify } from "@calcom/lib/safeStringify";
import { withReporting } from "@calcom/lib/sentryWrapper";
+import prisma from "@calcom/prisma";
import type {
Booking,
</file context>
| throw new Error("No holiday country selected"); | ||
| } | ||
|
|
||
| const holidays = await this.getHolidaysForCountry(settings.countryCode); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P2: Toggling holidays validates only the current year, so next-year holidays shown to the user will be rejected as “not found.” Include next-year holidays in the validation set.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/lib/holidays/HolidayService.ts, line 178:
<comment>Toggling holidays validates only the current year, so next-year holidays shown to the user will be rejected as “not found.” Include next-year holidays in the validation set.</comment>
<file context>
@@ -0,0 +1,266 @@
+ throw new Error("No holiday country selected");
+ }
+
+ const holidays = await this.getHolidaysForCountry(settings.countryCode);
+ if (!holidays.some((h) => h.id === holidayId)) {
+ throw new Error("Holiday not found for this country");
</file context>
| } | ||
|
|
||
| const dateRanges = holidayDates.map((h) => ({ | ||
| start: dayjs(h.date).startOf("day").toDate(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P1: Holiday conflict windows are calculated in local time, which can miss/introduce conflicts for non-UTC users. Use UTC start/end of day when building date ranges.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/lib/holidays/HolidayService.ts, line 214:
<comment>Holiday conflict windows are calculated in local time, which can miss/introduce conflicts for non-UTC users. Use UTC start/end of day when building date ranges.</comment>
<file context>
@@ -0,0 +1,266 @@
+ }
+
+ const dateRanges = holidayDates.map((h) => ({
+ start: dayjs(h.date).startOf("day").toDate(),
+ end: dayjs(h.date).endOf("day").toDate(),
+ }));
</file context>
What does this PR do?
Adds a new "Holidays" feature that allows users to automatically block their availability on public holidays based on their country.
Fixes #8918
Fixes CAL-6865
Features
Implementation
Environment Variables:
Screenshots/Videos
you can also see booking conflict warning
Screen.Recording.2025-12-03.at.8.25.36.PM.mov
ooo warning:
Updated holiday emoji
light:
Dark:
Booking page:
Test coverage
HolidayServiceSupported Countries
US, UK, Canada, Australia, Germany, France, Spain, Netherlands, Italy, Brazil, Mexico, India, Ireland, New Zealand, Sweden, Norway, Denmark, Belgium, Austria, Switzerland, Japan, China, South Korea, Singapore, Hong Kong, Taiwan, Thailand, Malaysia, Indonesia, Philippines, Vietnam, Russia, Poland, Greece, Portugal, Finland, South Africa, Iran
refer: https://gist.githubusercontent.com/mattn/1438183/raw/612d3178bef9afa832adc1ff82062bb50aba152c/google-calendar-list.txt
update: used this: https://gist.github.com/dhoeric/76bd1c15168ee0ee61ad3bf1730dcb65