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 <morgan@cal.com>

* test: update E2E tests to verify skipCalendarEvent option is passed when areCalendarEventsEnabled is false

Co-Authored-By: morgan@cal.com <morgan@cal.com>

* refactor: address PR review feedback - remove comments and update E2E tests to spy on createAllCalendarEvents

Co-Authored-By: morgan@cal.com <morgan@cal.com>

* test: add spy for createAllCRMEvents in e2e tests when areCalendarEventsEnabled is false

Co-Authored-By: lauris@cal.com <lauris.skraucis@gmail.com>

* test: add missing spy for createAllCRMEvents in beforeEach hook

Co-Authored-By: morgan@cal.com <morgan@cal.com>

* fix: use videoCallUrl from metadata for webhook payload location

Co-Authored-By: lauris@cal.com <lauris.skraucis@gmail.com>

* fix: simplify webhook location to use metadata?.videoCallUrl || evt.location

Co-Authored-By: lauris@cal.com <lauris.skraucis@gmail.com>

* docs: add description about video call link limitations when calendar events disabled

Co-Authored-By: lauris@cal.com <lauris.skraucis@gmail.com>

* i18n: add translation key for calendar events disabled video limitation

Co-Authored-By: lauris@cal.com <lauris.skraucis@gmail.com>

* fix: update translation text to accurately reflect video app behavior

Co-Authored-By: lauris@cal.com <lauris.skraucis@gmail.com>

* Update common.json

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Lauris Skraucis <lauris.skraucis@gmail.com>
This commit is contained in:
Morgan
2026-01-29 10:23:52 +02:00
committed by GitHub
co-authored by morgan@cal.com lauris@cal.com Devin AI
parent b02470e780
commit 5b048dd868
6 changed files with 34 additions and 13 deletions
@@ -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"
@@ -231,6 +231,7 @@ export const OAuthClientForm = ({
</div>
</Tooltip>
</div>
<p className="text-subtle mt-2 text-sm">{t("calendar_events_disabled_video_limitation")}</p>
</div>
<div className="mt-6">
<div className="flex items-center">
@@ -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 ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
+12 -3
View File
@@ -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<CreateUpdateResult> {
public async create(
event: CalendarEvent,
options?: { skipCalendarEvent?: boolean }
): Promise<CreateUpdateResult> {
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);
@@ -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);
@@ -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] } : {}),
};