Skip to content

Commit 5242e41

Browse files
volneidevin-ai-integration[bot]emrysalkeithwillcodecoderabbitai[bot]
authored
feat: Calendar Sync (calcom#24124)
* feat: calendar cache and sync - wip * Add env.example * refactor on CalendarCacheEventService * remove test console.log * Fix type checks errors * chore: remove pt comment * add route.ts * chore: fix tests * Improve cache impl * chore: update recurring event id * chore: small improvements * calendar cache improvements * Fix remove dynamic imports * Add cleanup stale cache * Fix tests * add event update * type fixes * feat: add comprehensive tests for new calendar subscription API routes - Add tests for /api/cron/calendar-subscriptions-cleanup route (9 tests) - Add tests for /api/cron/calendar-subscriptions route (10 tests) - Add tests for /api/webhooks/calendar-subscription/[provider] route (11 tests) - Add missing feature flags for calendar-subscription-cache and calendar-subscription-sync - All 30 tests pass with comprehensive coverage of authentication, feature flags, error handling, and service instantiation Tests cover: - Authentication scenarios (API key validation, Bearer tokens, query parameters) - Feature flag combinations (cache/sync enabled/disabled states) - Success and error handling (including non-Error exceptions) - Service instantiation with proper dependency injection - Provider validation for webhook endpoints Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com> * feat: add comprehensive tests for calendar subscription services, repositories, and adapters - Add unit tests for CalendarSubscriptionService with subscription, webhook, and event processing - Add unit tests for CalendarCacheEventService with cache operations and cleanup - Add unit tests for CalendarSyncService with Cal.com event filtering and booking operations - Add unit tests for CalendarCacheEventRepository with CRUD operations - Add unit tests for SelectedCalendarRepository with calendar selection management - Add unit tests for GoogleCalendarSubscriptionAdapter with subscription and event fetching - Add unit tests for Office365CalendarSubscriptionAdapter with placeholder implementation - Add unit tests for AdaptersFactory with provider management and adapter creation - Fix lint issues by removing explicit 'any' type casting and unused variables - All tests follow Cal.com conventions using Vitest framework with proper mocking Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com> * fix: improve calendar-subscriptions-cleanup test performance by adding missing mocks - Add comprehensive mocks for defaultResponderForAppDir, logger, performance monitoring, and Sentry - Fix slow test execution (933ms -> <100ms) caused by missing dependency mocks - Ensure consistent test performance across different environments Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com> * Fix tests * Fix tests * type fix * Fix coderabbit comments * Fix types * Fix test * Update apps/web/app/api/cron/calendar-subscriptions/route.ts Co-authored-by: Alex van Andel <me@alexvanandel.com> * Fixes by first review * feat: add database migrations for calendar cache and sync fields - Add CalendarCacheEventStatus enum with confirmed, tentative, cancelled values - Add new fields to SelectedCalendar: channelId, channelKind, channelResourceId, channelResourceUri, channelExpiration, syncSubscribedAt, syncToken, syncedAt, syncErrorAt, syncErrorCount - Create CalendarCacheEvent table with foreign key to SelectedCalendar - Add necessary indexes and constraints for performance and data integrity Fixes database schema issues causing e2e test failures with 'column does not exist' errors. Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com> * only google-calendar for now * docs: add Calendar Cache and Sync feature documentation - Add comprehensive feature overview and motivation - Document feature flags with SQL examples - Include SQL examples for enabling features for users and teams - Reference technical documentation files Addresses PR calcom#23876 documentation requirements Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com> * docs: update calendar subscription README with comprehensive documentation - Undo incorrect changes to main README.md - Update packages/features/calendar-subscription/README.md with: - Feature overview and motivation - Environment variables section - Complete feature flags documentation with SQL examples - SQL examples for enabling features for users and teams - Detailed architecture documentation Addresses PR calcom#23876 documentation requirements Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com> * fix docs * Fix test to available calendars * Fix test to available calendars * add migration and sync boilerplate * fix typo * remove double log * sync boilerplate * remove console.log * only subscribe for google calendar * adjust for 3 months fetch * only subscribe for teams that have feature enabled * adjust tests * chore: safe increment error count Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * calendar sync * test: add comprehensive tests for CalendarSyncService - Add comprehensive test coverage for CalendarSyncService methods - Test handleEvents, cancelBooking, and rescheduleBooking functionality - Cover edge cases like missing UIDs, malformed UIDs, and error handling - Fix dynamic import usage in CalendarSyncService to use .default - Remove invalid properties from handleNewBooking call to fix type errors Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com> * fix: add skipCalendarSyncTaskCreation flag to handleCancelBooking to prevent infinite loops - Add skipCalendarSyncTaskCreation field to bookingCancelSchema in zod-utils.ts - Update handleCancelBooking to skip EventManager.cancelEvent when flag is true - Update CalendarSyncService.cancelBooking to pass skipCalendarSyncTaskCreation: true - Update CalendarSyncService.rescheduleBooking to pass skipCalendarSyncTaskCreation: true - Update tests to verify the flags are passed correctly This prevents infinite loops when calendar events are cancelled/rescheduled from external calendars (Google/Office365) which trigger webhooks to Cal.com, which would otherwise try to update the external calendar again. Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com> * refactor: rename flag to skipCalendarSyncTaskCancellation and use static imports - Rename skipCalendarSyncTaskCreation to skipCalendarSyncTaskCancellation in handleCancelBooking - Convert dynamic imports to static imports in CalendarSyncService - Update tests to use vi.hoisted for proper mock hoisting with static imports Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com> * feat: add error handling and static import for handleNewBooking - Create index.ts entry point for handleNewBooking directory - Add try-catch error handling to cancelBooking and rescheduleBooking - Log errors but don't block calendar sync operations - Update tests to verify errors are caught and logged, not thrown Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com> * refactor: use RegularBookingService directly and fix safeStringify usage - Remove handleNewBooking/index.ts and call getRegularBookingService().createBooking() directly in CalendarSyncService - Fix safeStringify usage: apply to error itself, not wrapper object - Update tests to mock getRegularBookingService instead of handleNewBooking Addresses PR comments from Volnei and Cubic-dev-ai Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com> * fix: restore handleNewBooking/index.ts and fix bookingData structure for calendar sync - Restore handleNewBooking/index.ts as entry point for static imports - Fix CalendarSyncService.rescheduleBooking to use correct bookingData structure with required fields (eventTypeId, start, end, timeZone, language, metadata) - Use rescheduleUid to indicate this is a reschedule operation - Fix safeStringify usage in error logging (wrap only the error, not the whole object) - Update tests to match new bookingData structure Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com> * refactor: address PR comments - use RegularBookingService directly, remove unnecessary flags, fix booking reference update - Remove handleNewBooking/index.ts wrapper and use getRegularBookingService().createBooking() directly in CalendarSyncService - Remove allRemainingBookings and cancelSubsequentBookings flags from cancelBooking (only cancel the specific booking, not the entire series) - Move bookingReference update outside skipCalendarSyncTaskCancellation block for data consistency - Update tests to match new implementation Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com> * chore: remove unnecessary comment from zod-utils.ts Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com> * feat: add Sentry metrics telemetry and fix null assertions in CalendarSyncService Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com> * fix: use dynamic import for getRegularBookingService to avoid RAQB import in server context Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com> * fix: use dynamic import for findTeamMembersMatchingAttributeLogic to avoid RAQB import in server context Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com> * improve booking update * add more tests and edge cases * fix: add required actionSource and Sentry mocks to calendar sync Add actionSource: "SYSTEM" to handleCancelBooking call after it became required, and mock @sentry/nextjs in test files. --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: Alex van Andel <me@alexvanandel.com> Co-authored-by: Keith Williams <keithwillcode@gmail.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
1 parent 5d65a0f commit 5242e41

6 files changed

Lines changed: 1877 additions & 42 deletions

File tree

packages/features/bookings/lib/handleCancelBooking.ts

Lines changed: 33 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,7 @@ async function handler(input: CancelBookingInput, dependencies?: Dependencies) {
166166
cancelSubsequentBookings,
167167
internalNote,
168168
skipCancellationReasonValidation = false,
169+
skipCalendarSyncTaskCancellation = false,
169170
} = bookingCancelInput.parse(body);
170171
const bookingToDelete = await getBookingToDelete(id, uid);
171172
const {
@@ -630,36 +631,46 @@ async function handler(input: CancelBookingInput, dependencies?: Dependencies) {
630631
allRemainingBookings
631632
);
632633

633-
try {
634-
const bookingToDeleteEventTypeMetadataParsed = eventTypeMetaDataSchemaWithTypedApps.safeParse(
635-
bookingToDelete.eventType?.metadata || null
636-
);
637-
638-
if (!bookingToDeleteEventTypeMetadataParsed.success) {
639-
log.error(
640-
`Error parsing metadata`,
641-
safeStringify({ error: bookingToDeleteEventTypeMetadataParsed?.error })
634+
// Skip calendar event deletion when cancellation comes from a calendar subscription webhook
635+
// to avoid infinite loops (Google/Office365 → Cal.com → Google/Office365 → ...)
636+
if (!skipCalendarSyncTaskCancellation) {
637+
try {
638+
const bookingToDeleteEventTypeMetadataParsed = eventTypeMetaDataSchemaWithTypedApps.safeParse(
639+
bookingToDelete.eventType?.metadata || null
642640
);
643-
throw new Error("Error parsing metadata");
644-
}
645641

646-
const bookingToDeleteEventTypeMetadata = bookingToDeleteEventTypeMetadataParsed.data;
642+
if (!bookingToDeleteEventTypeMetadataParsed.success) {
643+
log.error(
644+
`Error parsing metadata`,
645+
safeStringify({ error: bookingToDeleteEventTypeMetadataParsed?.error })
646+
);
647+
throw new Error("Error parsing metadata");
648+
}
647649

648-
const credentials = await getAllCredentialsIncludeServiceAccountKey(bookingToDelete.user, {
649-
...bookingToDelete.eventType,
650-
metadata: bookingToDeleteEventTypeMetadata,
651-
});
650+
const bookingToDeleteEventTypeMetadata = bookingToDeleteEventTypeMetadataParsed.data;
652651

653-
const eventManager = new EventManager(
654-
{ ...bookingToDelete.user, credentials },
655-
bookingToDeleteEventTypeMetadata?.apps
656-
);
652+
const credentials = await getAllCredentialsIncludeServiceAccountKey(bookingToDelete.user, {
653+
...bookingToDelete.eventType,
654+
metadata: bookingToDeleteEventTypeMetadata,
655+
});
656+
657+
const eventManager = new EventManager(
658+
{ ...bookingToDelete.user, credentials },
659+
bookingToDeleteEventTypeMetadata?.apps
660+
);
657661

658-
await eventManager.cancelEvent(evt, bookingToDelete.references, isBookingInRecurringSeries);
662+
await eventManager.cancelEvent(evt, bookingToDelete.references, isBookingInRecurringSeries);
663+
} catch (error) {
664+
log.error(`Error deleting integrations`, safeStringify({ error }));
665+
}
666+
}
659667

668+
// Always mark booking references as deleted for data consistency
669+
// (even when skipCalendarSyncTaskCancellation is true, since the external event is already deleted)
670+
try {
660671
await bookingReferenceRepository.updateManyByBookingId(bookingToDelete.id, { deleted: true });
661672
} catch (error) {
662-
log.error(`Error deleting integrations`, safeStringify({ error }));
673+
log.error(`Error marking booking references as deleted`, safeStringify({ error }));
663674
}
664675

665676
try {

0 commit comments

Comments
 (0)