fix(calendar): sync Cal.diy iCalUID events (#29580)

Co-authored-by: Kartik <103111467+kartik-212004@users.noreply.github.com>
This commit is contained in:
Shreyas
2026-07-07 21:53:03 +05:30
committed by GitHub
co-authored by Kartik
parent ca90ca2c94
commit 0968bcef01
2 changed files with 41 additions and 12 deletions
@@ -1,14 +1,25 @@
import type { CreateRegularBookingData } from "@calcom/features/bookings/lib/dto/types";
import { IdempotencyKeyService } from "@calcom/lib/idempotencyKey/idempotencyKeyService";
import handleCancelBooking from "@calcom/features/bookings/lib/handleCancelBooking";
import type { BookingRepository } from "@calcom/features/bookings/repositories/BookingRepository";
import type { CalendarSubscriptionEventItem } from "@calcom/features/calendar-subscription/lib/CalendarSubscriptionPort.interface";
import { APP_NAME } from "@calcom/lib/constants";
import { IdempotencyKeyService } from "@calcom/lib/idempotencyKey/idempotencyKeyService";
import logger from "@calcom/lib/logger";
import { safeStringify } from "@calcom/lib/safeStringify";
import type { SelectedCalendar } from "@calcom/prisma/client";
import { metrics } from "@sentry/nextjs";
const log = logger.getSubLogger({ prefix: ["CalendarSyncService"] });
const CAL_MANAGED_ICAL_UID_SUFFIXES: ReadonlySet<string> = new Set(
["cal.com", "cal.diy", APP_NAME].map((suffix) => suffix.toLowerCase())
);
const isCalManagedICalUID = (iCalUID?: string | null): boolean => {
const suffix = iCalUID?.split("@").at(-1)?.toLowerCase();
if (!suffix) return false;
return CAL_MANAGED_ICAL_UID_SUFFIXES.has(suffix);
};
/**
* Service to handle synchronization of calendar events.
@@ -42,10 +53,7 @@ export class CalendarSyncService {
},
});
// only process cal.com calendar events
const calEvents = calendarSubscriptionEvents.filter((e) =>
e.iCalUID?.toLowerCase()?.endsWith("@cal.com")
);
const calEvents = calendarSubscriptionEvents.filter((e) => isCalManagedICalUID(e.iCalUID));
metrics.distribution("calendar.sync.handleEvents.events_count", calEvents.length, {
attributes: {
@@ -1,9 +1,7 @@
import { beforeEach, describe, expect, test, vi } from "vitest";
import type { CalendarSubscriptionEventItem } from "@calcom/features/calendar-subscription/lib/CalendarSubscriptionPort.interface";
import type { BookingRepository } from "@calcom/lib/server/repository/booking";
import type { SelectedCalendar } from "@calcom/prisma/client";
import { beforeEach, describe, expect, test, vi } from "vitest";
import { CalendarSyncService } from "../CalendarSyncService";
const { mockHandleCancelBooking, mockCreateBooking } = vi.hoisted(() => ({
@@ -206,6 +204,21 @@ describe("CalendarSyncService", () => {
});
});
test("should handle default Cal.diy iCalUID", async () => {
const eventWithCalDiyUID: CalendarSubscriptionEventItem = {
...mockCalComEvent,
iCalUID: "test-booking-uid@Cal.diy",
};
mockBookingRepository.findBookingByUidWithEventType = vi.fn().mockResolvedValue(mockBooking);
await service.handleEvents(mockSelectedCalendar, [eventWithCalDiyUID]);
expect(mockBookingRepository.findBookingByUidWithEventType).toHaveBeenCalledWith({
bookingUid: "test-booking-uid",
});
});
test("should handle events with null iCalUID", async () => {
const eventWithNullUID: CalendarSubscriptionEventItem = {
...mockCalComEvent,
@@ -279,7 +292,9 @@ describe("CalendarSyncService", () => {
mockHandleCancelBooking.mockRejectedValue(new Error("Cancellation failed"));
// Should not throw - errors are caught and logged
await expect(service.cancelBooking(mockCancelledEvent, mockSelectedCalendar.userId)).resolves.not.toThrow();
await expect(
service.cancelBooking(mockCancelledEvent, mockSelectedCalendar.userId)
).resolves.not.toThrow();
expect(mockBookingRepository.findBookingByUidWithEventType).toHaveBeenCalled();
expect(mockHandleCancelBooking).toHaveBeenCalled();
@@ -290,7 +305,9 @@ describe("CalendarSyncService", () => {
.fn()
.mockRejectedValue(new Error("DB connection failed"));
await expect(service.cancelBooking(mockCancelledEvent, mockSelectedCalendar.userId)).resolves.not.toThrow();
await expect(
service.cancelBooking(mockCancelledEvent, mockSelectedCalendar.userId)
).resolves.not.toThrow();
expect(mockBookingRepository.findBookingByUidWithEventType).toHaveBeenCalled();
expect(mockHandleCancelBooking).not.toHaveBeenCalled();
@@ -489,7 +506,9 @@ describe("CalendarSyncService", () => {
mockCreateBooking.mockRejectedValue(new Error("Rescheduling failed"));
// Should not throw - errors are caught and logged
await expect(service.rescheduleBooking(eventWithDifferentStart, mockSelectedCalendar.userId)).resolves.not.toThrow();
await expect(
service.rescheduleBooking(eventWithDifferentStart, mockSelectedCalendar.userId)
).resolves.not.toThrow();
expect(mockBookingRepository.findBookingByUidWithEventType).toHaveBeenCalled();
expect(mockCreateBooking).toHaveBeenCalled();
@@ -500,7 +519,9 @@ describe("CalendarSyncService", () => {
.fn()
.mockRejectedValue(new Error("DB connection failed"));
await expect(service.rescheduleBooking(mockCalComEvent, mockSelectedCalendar.userId)).resolves.not.toThrow();
await expect(
service.rescheduleBooking(mockCalComEvent, mockSelectedCalendar.userId)
).resolves.not.toThrow();
expect(mockBookingRepository.findBookingByUidWithEventType).toHaveBeenCalled();
expect(mockCreateBooking).not.toHaveBeenCalled();