From 5b048dd86825ea5d7d00299ce69b540e8fcf885b Mon Sep 17 00:00:00 2001 From: Morgan <33722304+ThyMinimalDev@users.noreply.github.com> Date: Thu, 29 Jan 2026 10:23:52 +0200 Subject: [PATCH] fix: exclude third-party video apps from areCalendarEventsEnabled setting (#27230) * fix: exclude third-party video apps from areCalendarEventsEnabled setting When areCalendarEventsEnabled is false, video meeting creation for third-party video apps (like Daily.co) was being skipped, causing the meetingUrl field in webhook payloads to contain the location identifier (integrations:daily) instead of the actual video meeting URL. This fix adds a skipCalendarEvent option to EventManager.create() that: - Skips calendar event creation when true - Skips CRM event creation when true (CRM events are tied to calendar events) - Still creates video meetings for third-party video apps The calling code in handleConfirmation.ts and RegularBookingService.ts now uses this option when areCalendarEventsEnabled is false, ensuring video meetings are still created while respecting the platform customer's preference to manage their own calendar events. Co-Authored-By: morgan@cal.com * test: update E2E tests to verify skipCalendarEvent option is passed when areCalendarEventsEnabled is false Co-Authored-By: morgan@cal.com * refactor: address PR review feedback - remove comments and update E2E tests to spy on createAllCalendarEvents Co-Authored-By: morgan@cal.com * test: add spy for createAllCRMEvents in e2e tests when areCalendarEventsEnabled is false Co-Authored-By: lauris@cal.com * test: add missing spy for createAllCRMEvents in beforeEach hook Co-Authored-By: morgan@cal.com * fix: use videoCallUrl from metadata for webhook payload location Co-Authored-By: lauris@cal.com * fix: simplify webhook location to use metadata?.videoCallUrl || evt.location Co-Authored-By: lauris@cal.com * docs: add description about video call link limitations when calendar events disabled Co-Authored-By: lauris@cal.com * i18n: add translation key for calendar events disabled video limitation Co-Authored-By: lauris@cal.com * fix: update translation text to accurately reflect video app behavior Co-Authored-By: lauris@cal.com * Update common.json --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: Lauris Skraucis --- .../controllers/e2e/user-bookings.e2e-spec.ts | 18 ++++++++++++++---- .../oauth-clients/oauth-client-form/index.tsx | 1 + apps/web/public/static/locales/en/common.json | 1 + packages/features/bookings/lib/EventManager.ts | 15 ++++++++++++--- .../bookings/lib/handleConfirmation.ts | 2 +- .../lib/service/RegularBookingService.ts | 10 +++++----- 6 files changed, 34 insertions(+), 13 deletions(-) diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/user-bookings.e2e-spec.ts b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/user-bookings.e2e-spec.ts index 1c61125d8b..2792214c8d 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/user-bookings.e2e-spec.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/user-bookings.e2e-spec.ts @@ -2667,6 +2667,12 @@ describe("Bookings Endpoints 2024-08-13", () => { jest .spyOn(EventManager.prototype, "create") .mockImplementation(() => Promise.resolve({ results: [], referencesToCreate: [] })); + jest + .spyOn(EventManager.prototype, "createAllCalendarEvents") + .mockImplementation(() => Promise.resolve([])); + jest + .spyOn(EventManager.prototype, "createAllCRMEvents") + .mockImplementation(() => Promise.resolve([])); }); describe("platform oAuth client has calendar events enabled", () => { @@ -2837,7 +2843,8 @@ describe("Bookings Endpoints 2024-08-13", () => { const data: BookingOutput_2024_08_13 = responseBody.data; expect(data.id).toBeDefined(); expect(data.uid).toBeDefined(); - expect(EventManager.prototype.create).toHaveBeenCalledTimes(0); + expect(EventManager.prototype.createAllCalendarEvents).toHaveBeenCalledTimes(0); + expect(EventManager.prototype.createAllCRMEvents).toHaveBeenCalledTimes(0); } else { throw new Error( "Invalid response data - expected booking but received array of possibly recurring bookings" @@ -2867,7 +2874,8 @@ describe("Bookings Endpoints 2024-08-13", () => { const responseBody: CreateBookingOutput_2024_08_13 = response.body; expect(responseBody.status).toEqual(SUCCESS_STATUS); expect(responseBody.data).toBeDefined(); - expect(EventManager.prototype.create).toHaveBeenCalledTimes(0); + expect(EventManager.prototype.createAllCalendarEvents).toHaveBeenCalledTimes(0); + expect(EventManager.prototype.createAllCRMEvents).toHaveBeenCalledTimes(0); }); }); @@ -2901,7 +2909,8 @@ describe("Bookings Endpoints 2024-08-13", () => { const data: BookingOutput_2024_08_13 = responseBody.data; expect(data.id).toBeDefined(); expect(data.uid).toBeDefined(); - expect(EventManager.prototype.create).toHaveBeenCalledTimes(0); + expect(EventManager.prototype.createAllCalendarEvents).toHaveBeenCalledTimes(0); + expect(EventManager.prototype.createAllCRMEvents).toHaveBeenCalledTimes(0); bookingThatRequiresConfirmationUid = data.uid; } else { throw new Error( @@ -2926,7 +2935,8 @@ describe("Bookings Endpoints 2024-08-13", () => { const data: BookingOutput_2024_08_13 = responseBody.data; expect(data.id).toBeDefined(); expect(data.uid).toBeDefined(); - expect(EventManager.prototype.create).toHaveBeenCalledTimes(0); + expect(EventManager.prototype.createAllCalendarEvents).toHaveBeenCalledTimes(0); + expect(EventManager.prototype.createAllCRMEvents).toHaveBeenCalledTimes(0); } else { throw new Error( "Invalid response data - expected booking but received array of possibly recurring bookings" diff --git a/apps/web/components/settings/platform/oauth-clients/oauth-client-form/index.tsx b/apps/web/components/settings/platform/oauth-clients/oauth-client-form/index.tsx index 220b217a34..06faf2c0ec 100644 --- a/apps/web/components/settings/platform/oauth-clients/oauth-client-form/index.tsx +++ b/apps/web/components/settings/platform/oauth-clients/oauth-client-form/index.tsx @@ -231,6 +231,7 @@ export const OAuthClientForm = ({ +

{t("calendar_events_disabled_video_limitation")}

diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index 59a8a17d91..9f3bef3843 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -4517,5 +4517,6 @@ "error_enabling_feature": "Error enabling feature. Please try again.", "set_organizer_as_contact_owner": "Set booking organizer as contact owner", "overwrite_existing_contact_owner": "Overwrite existing contact owner", + "calendar_events_disabled_video_limitation": "When calendar events are disabled, meeting links cannot be generated for apps whose link generation is tied to calendar event creation, for example, Google Meet and Microsoft Teams. Other video apps like Cal Video will continue to work.", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS":"↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } diff --git a/packages/features/bookings/lib/EventManager.ts b/packages/features/bookings/lib/EventManager.ts index 9c216e0005..eebd9807b2 100644 --- a/packages/features/bookings/lib/EventManager.ts +++ b/packages/features/bookings/lib/EventManager.ts @@ -280,8 +280,15 @@ export default class EventManager { * event will be scheduled for it as well. * * @param event + * @param options.skipCalendarEvent - When true, skips calendar event creation but still creates video meetings. + * This is useful for platform customers who manage their own calendar events but still want Cal.com to create + * video meetings for third-party video apps like Daily.co. */ - public async create(event: CalendarEvent): Promise { + public async create( + event: CalendarEvent, + options?: { skipCalendarEvent?: boolean } + ): Promise { + const { skipCalendarEvent = false } = options ?? {}; // TODO this method shouldn't be modifying the event object that's passed in const evt = processLocation(event); @@ -357,7 +364,9 @@ export default class EventManager { // Some calendar libraries may edit the original event so let's clone it const clonedCalEvent = cloneDeep(event); // Create the calendar event with the proper video call data - results.push(...(await this.createAllCalendarEvents(clonedCalEvent))); + if (!skipCalendarEvent) { + results.push(...(await this.createAllCalendarEvents(clonedCalEvent))); + } if (evt.location === MSTeamsLocationType) { this.updateMSTeamsVideoCallData(evt, results); @@ -371,7 +380,7 @@ export default class EventManager { return result.type.includes("_calendar"); }; - const createdCRMEvents = await this.createAllCRMEvents(evt); + const createdCRMEvents = skipCalendarEvent ? [] : await this.createAllCRMEvents(evt); results.push(...createdCRMEvents); diff --git a/packages/features/bookings/lib/handleConfirmation.ts b/packages/features/bookings/lib/handleConfirmation.ts index 6f36f94264..f12a7dacf5 100644 --- a/packages/features/bookings/lib/handleConfirmation.ts +++ b/packages/features/bookings/lib/handleConfirmation.ts @@ -151,7 +151,7 @@ export async function handleConfirmation(args: { const apps = eventTypeAppMetadataOptionalSchema.parse(eventTypeMetadata?.apps); const eventManager = new EventManager(user, apps); const areCalendarEventsEnabled = platformClientParams?.areCalendarEventsEnabled ?? true; - const scheduleResult = areCalendarEventsEnabled ? await eventManager.create(evt) : placeholderCreatedEvent; + const scheduleResult = await eventManager.create(evt, { skipCalendarEvent: !areCalendarEventsEnabled }); const results = scheduleResult.results; const metadata: AdditionalInformation = {}; const workflows = await getAllWorkflowsFromEventType(eventType, booking.userId); diff --git a/packages/features/bookings/lib/service/RegularBookingService.ts b/packages/features/bookings/lib/service/RegularBookingService.ts index 6ce8b12347..e6431fce26 100644 --- a/packages/features/bookings/lib/service/RegularBookingService.ts +++ b/packages/features/bookings/lib/service/RegularBookingService.ts @@ -2290,11 +2290,8 @@ async function handler( // If it's not a reschedule, doesn't require confirmation and there's no price, // Create a booking } else if (isConfirmedByDefault) { - // Use EventManager to conditionally use all needed integrations. - const createManager = - areCalendarEventsEnabled && !skipCalendarSyncTaskCreation - ? await eventManager.create(evt) - : placeholderCreatedEvent; + const shouldSkipCalendarEvents = !areCalendarEventsEnabled || skipCalendarSyncTaskCreation; + const createManager = await eventManager.create(evt, { skipCalendarEvent: shouldSkipCalendarEvents }); if (evt.location) { booking.location = evt.location; } @@ -2479,6 +2476,8 @@ async function handler( tracingLogger, }); + const webhookLocation = metadata?.videoCallUrl || evt.location; + const webhookData: EventPayloadType = { ...evt, ...eventTypeInfo, @@ -2496,6 +2495,7 @@ async function handler( status: "ACCEPTED", smsReminderNumber: booking?.smsReminderNumber || undefined, rescheduledBy: reqBody.rescheduledBy, + location: webhookLocation, ...(assignmentReason ? { assignmentReason: [assignmentReason] } : {}), };