Skip to content

Conversation

@dhairyashiil
Copy link
Member

@dhairyashiil dhairyashiil commented Dec 2, 2025

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

  • Country-based holidays: Support for 36 countries via Google Calendar public holiday calendars
  • Per-holiday toggles: Users can enable/disable specific holidays
  • Booking conflict detection: Warns users if they have existing bookings on holidays they're enabling
  • OOO integration: Shows warning when creating OOO entries that overlap with holidays

Implementation

  • Google Calendar API integration: Fetches holidays from Google's public holiday calendars (no OAuth required, uses API key)
  • Database caching: HolidayCache Prisma model stores fetched holidays with configurable TTL (default 7 days via HOLIDAY_CACHE_DAYS env)
  • New UserHolidaySettings Prisma model for user preferences (country selection, disabled holidays)
  • TRPC endpoints for settings management and conflict checking
  • Holidays tab in OOO page
  • Integration with getUserAvailability for blocking time slots

Environment Variables:

  • GOOGLE_CALENDAR_API_KEY(Required) - Google Calendar API key for fetching public holiday calendars
  • HOLIDAY_CACHE_DAYS(Not Required) - Cache duration in days (default: 7)

Screenshots/Videos

you can also see booking conflict warning

Screen.Recording.2025-12-03.at.8.25.36.PM.mov

ooo warning:

Screenshot 2025-12-03 at 8 09 25 PM

Updated holiday emoji

light:

Screenshot 2025-12-03 at 10 15 20 PM

Dark:

Screenshot 2025-12-03 at 10 14 58 PM

Booking page:

Screenshot 2025-12-03 at 10 21 51 PM

Test coverage

  • Unit tests for HolidayService

Supported 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

…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
@vercel
Copy link

vercel bot commented Dec 3, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
cal-companion Ready Ready Preview, Comment Dec 15, 2025 3:24pm
2 Skipped Deployments
Project Deployment Review Updated (UTC)
cal Ignored Ignored Dec 15, 2025 3:24pm
cal-eu Ignored Ignored Dec 15, 2025 3:24pm

@dhairyashiil dhairyashiil marked this pull request as ready for review December 3, 2025 01:01
@dhairyashiil dhairyashiil requested a review from a team as a code owner December 3, 2025 01:01
@graphite-app graphite-app bot added the community Created by Linear-GitHub Sync label Dec 3, 2025
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a 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 &#39;we&#39;ll use stale cache if available&#39; 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&#39;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&#39;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

Copy link
Member

@CarinaWolli CarinaWolli left a 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

@github-actions github-actions bot marked this pull request as draft December 3, 2025 09:38
@github-actions github-actions bot added app-store area: app store, apps, calendar integrations, google calendar, outlook, lark, apple calendar Medium priority Created by Linear-GitHub Sync ✨ feature New feature or request 🎨 needs design Before engineering kick-off, a designer needs to submit a mockup labels Dec 3, 2025
@linear
Copy link

linear bot commented Dec 3, 2025

… 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",
Copy link
Member

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

Copy link
Member Author

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=

Copy link
Contributor

@eunjae-lee eunjae-lee left a 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

@CarinaWolli CarinaWolli dismissed their stale review December 15, 2025 10:54

addressed requested changes to move to OOO section

@CarinaWolli CarinaWolli self-requested a review December 15, 2025 10:55
import logger from "@calcom/lib/logger";
import { safeStringify } from "@calcom/lib/safeStringify";
import { withReporting } from "@calcom/lib/sentryWrapper";
import prisma from "@calcom/prisma";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
import prisma from "@calcom/prisma";
import { prisma } from "@calcom/prisma";

Comment on lines +1 to +15
"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";
Copy link
Contributor

@hbjORbj hbjORbj Dec 15, 2025

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

@github-actions github-actions bot marked this pull request as draft December 15, 2025 15:07
Copy link
Contributor

@hbjORbj hbjORbj left a 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

@hbjORbj hbjORbj marked this pull request as ready for review December 15, 2025 15:14
@hbjORbj hbjORbj enabled auto-merge (squash) December 15, 2025 15:14
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a 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 (&quot;No holiday country selected&quot;, &quot;Holiday not found for this country&quot;). 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(&#39;day&#39;)` 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";
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Dec 15, 2025

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 &quot;@calcom/features/schedules/lib/date-r
 import logger from &quot;@calcom/lib/logger&quot;;
 import { safeStringify } from &quot;@calcom/lib/safeStringify&quot;;
 import { withReporting } from &quot;@calcom/lib/sentryWrapper&quot;;
+import prisma from &quot;@calcom/prisma&quot;;
 import type {
   Booking,
</file context>
Fix with Cubic

throw new Error("No holiday country selected");
}

const holidays = await this.getHolidaysForCountry(settings.countryCode);
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Dec 15, 2025

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(&quot;No holiday country selected&quot;);
+    }
+
+    const holidays = await this.getHolidaysForCountry(settings.countryCode);
+    if (!holidays.some((h) =&gt; h.id === holidayId)) {
+      throw new Error(&quot;Holiday not found for this country&quot;);
</file context>
Fix with Cubic

}

const dateRanges = holidayDates.map((h) => ({
start: dayjs(h.date).startOf("day").toDate(),
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Dec 15, 2025

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) =&gt; ({
+      start: dayjs(h.date).startOf(&quot;day&quot;).toDate(),
+      end: dayjs(h.date).endOf(&quot;day&quot;).toDate(),
+    }));
</file context>
Fix with Cubic

@hbjORbj hbjORbj merged commit 12e07f2 into main Dec 15, 2025
60 of 64 checks passed
@hbjORbj hbjORbj deleted the feat/holidays-tab branch December 15, 2025 16:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

app-store area: app store, apps, calendar integrations, google calendar, outlook, lark, apple calendar community Created by Linear-GitHub Sync ❗️ .env changes contains changes to env variables ✨ feature New feature or request High priority Created by Linear-GitHub Sync ❗️ migrations contains migration files 🎨 needs design Before engineering kick-off, a designer needs to submit a mockup ready-for-e2e size/XXL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support Blocking Holiday Calendars

8 participants