diff --git a/packages/features/calendar-subscription/lib/sync/CalendarSyncService.ts b/packages/features/calendar-subscription/lib/sync/CalendarSyncService.ts index 3d4d0b75a4..7525d954b1 100644 --- a/packages/features/calendar-subscription/lib/sync/CalendarSyncService.ts +++ b/packages/features/calendar-subscription/lib/sync/CalendarSyncService.ts @@ -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 = 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: { diff --git a/packages/features/calendar-subscription/lib/sync/__tests__/CalendarSyncService.test.ts b/packages/features/calendar-subscription/lib/sync/__tests__/CalendarSyncService.test.ts index 859912e18c..f260b6b7a2 100644 --- a/packages/features/calendar-subscription/lib/sync/__tests__/CalendarSyncService.test.ts +++ b/packages/features/calendar-subscription/lib/sync/__tests__/CalendarSyncService.test.ts @@ -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();