Skip to content

Conversation

admin-coderabbit
Copy link
Owner

@admin-coderabbit admin-coderabbit commented Feb 4, 2026

This pull request was automatically created by @coderabbitai/e2e-reviewer.

Batch created pull request.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added support for multiple destination calendars per booking event.
    • Enhanced team member calendar handling for collective scheduling workflows.
  • Bug Fixes

    • Improved calendar event deletion resilience with enhanced error handling.
    • Fixed credential-based calendar targeting for event creation and updates.
  • Refactor

    • Refactored internal calendar service architecture to improve multi-calendar support and reference handling.

@coderabbit-eval
Copy link

coderabbit-eval bot commented Feb 4, 2026

📝 Walkthrough

Walkthrough

This PR refactors destination calendar handling across the codebase to use arrays instead of single values, updating type definitions, calendar service implementations, event managers, and booking workflows. It also adds credential ID parameters to calendar operations and adjusts email template rendering for the new structure.

Changes

Cohort / File(s) Summary
Type Definitions & Core Interfaces
packages/types/Calendar.d.ts, packages/types/EventManager.d.ts, packages/core/builders/CalendarEvent/class.ts, packages/core/builders/CalendarEvent/builder.ts
destinationCalendar changed from single value to array type; Calendar.createEvent now requires credentialId parameter; EventResult gains externalId field; Booking import converted to type-only.
Calendar Service Implementations
packages/app-store/googlecalendar/lib/CalendarService.ts, packages/app-store/larkcalendar/lib/CalendarService.ts, packages/app-store/office365calendar/lib/CalendarService.ts, packages/lib/CalendarService.ts
Calendar selection logic updated to extract first destination calendar and use its externalId; credential ID now used to match correct calendar for insertion; fallback to primary calendar preserved when destination not found.
Core Event Management
packages/core/CalendarManager.ts, packages/core/EventManager.ts
createEvent passes credential.id to calendar services; returned EventResult includes externalId and credentialId; destination calendar treated as array with first element extraction; credential resolution added for multi-calendar support; error handling refined for 404 responses.
Booking & Event Creation
packages/features/bookings/lib/handleNewBooking.ts, packages/features/bookings/lib/handleCancelBooking.ts, packages/features/ee/payments/api/webhook.ts, packages/features/ee/payments/api/paypal-webhook.ts
destinationCalendar wrapped in arrays with computed fallback logic; handleNewBooking adds team destination calendars for COLLECTIVE scheduling; handleCancelBooking refactored to iterate over calendar references with per-reference credential resolution and DB fallback; dynamic user loading restructured with error handling.
TRPC Handlers
packages/trpc/server/routers/viewer/bookings/confirm.handler.ts, packages/trpc/server/routers/viewer/bookings/editLocation.handler.ts, packages/trpc/server/routers/viewer/bookings/requestReschedule.handler.ts, packages/trpc/server/routers/loggedInViewer/deleteCredential.handler.ts, packages/trpc/server/routers/viewer/organizations/create.handler.ts
destinationCalendar normalized to array form in event payload construction; slug/requestedSlug toggling inverted for IS_TEAM_BILLING_ENABLED condition.
Email & Templates
packages/emails/src/templates/BrokenIntegrationEmail.tsx
Destination calendar extracted as array with first element fallback for integration resolution.
Testing & API
apps/web/pages/api/cron/bookingReminder.ts, apps/web/playwright/webhook.e2e.ts
destinationCalendar converted to array form in booking reminder; test expectation updated from null to [].

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~65 minutes

Poem

🐰 Arrays of calendars, what a delight!
No more singular, we embrace the might
Credentials march through, each one finds its way
Destinations multiply, hooray, hooray!
From booking to bell, each calendar rings true

🚥 Pre-merge checks | ✅ 1 | ❌ 1
❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Title check ❓ Inconclusive The PR title is vague and uses the branch name format; it does not clearly communicate the main purpose or change being made. Use a descriptive title that summarizes the primary change, such as 'Support multiple destination calendars for collective scheduling' or 'Convert destinationCalendar to array-based structure'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/handle-collective-multiple-host-destinations

Comment @coderabbit eval help to get the list of available commands and usage tips.

@coderabbit-eval coderabbit-eval bot closed this Feb 4, 2026
Copy link

@coderabbit-eval coderabbit-eval bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
packages/app-store/office365calendar/lib/CalendarService.ts (1)

94-109: ⚠️ Potential issue | 🟠 Major

Apply the same dynamic calendar selection logic to updateEvent that createEvent uses.

The updateEvent method receives the event parameter containing destinationCalendar but uses the hardcoded path /me/calendar/events/${uid}, while createEvent correctly selects the calendar endpoint based on mainHostDestinationCalendar?.externalId. If an event was created in a non-default calendar, updating it via the hardcoded default calendar path will fail or update the wrong event. Extract mainHostDestinationCalendar from event.destinationCalendar and construct the path as:

const eventsUrl = mainHostDestinationCalendar?.externalId
  ? `/me/calendars/${mainHostDestinationCalendar?.externalId}/events/${uid}`
  : `/me/calendar/events/${uid}`;
packages/lib/CalendarService.ts (1)

125-125: ⚠️ Potential issue | 🔴 Critical

Interface compliance: createEvent signature mismatch.

The Calendar interface requires createEvent(event: CalendarEvent, credentialId: number), but this implementation uses only createEvent(event: CalendarEvent). This will cause a TypeScript compilation error.

🐛 Proposed fix to match interface signature
- async createEvent(event: CalendarEvent): Promise<NewCalendarEventType> {
+ async createEvent(event: CalendarEvent, credentialId: number): Promise<NewCalendarEventType> {

Note: If credentialId is not needed in this implementation, it can be prefixed with underscore: _credentialId: number

packages/features/bookings/lib/handleNewBooking.ts (1)

987-1079: ⚠️ Potential issue | 🟠 Major

Collective team destination calendars can be dropped

When neither eventType.destinationCalendar nor organizerUser.destinationCalendar exist, evt.destinationCalendar is set to null, and the optional-chained push at Line 1078 skips teamDestinationCalendars. For COLLECTIVE, this silently omits team members’ calendars.

🔧 Suggested fix
-    destinationCalendar: eventType.destinationCalendar
-      ? [eventType.destinationCalendar]
-      : organizerUser.destinationCalendar
-      ? [organizerUser.destinationCalendar]
-      : null,
+    destinationCalendar: eventType.destinationCalendar
+      ? [eventType.destinationCalendar]
+      : organizerUser.destinationCalendar
+      ? [organizerUser.destinationCalendar]
+      : [],
@@
-  if (isTeamEventType && eventType.schedulingType === "COLLECTIVE") {
-    evt.destinationCalendar?.push(...teamDestinationCalendars);
-  }
+  if (isTeamEventType && eventType.schedulingType === "COLLECTIVE") {
+    evt.destinationCalendar = [...(evt.destinationCalendar ?? []), ...teamDestinationCalendars];
+  }
packages/app-store/googlecalendar/lib/CalendarService.ts (2)

145-178: ⚠️ Potential issue | 🟡 Minor

Patch receives undefined while insert falls back to "primary"—use consistent calendarId for both calls

Line 154 uses selectedCalendar || "primary" for the insert call, but line 168 passes selectedCalendar directly to patch without the fallback. When no destination calendar matches the credentialId, the insert succeeds with the "primary" calendar, but the patch receives undefined and may fail silently. Extract the fallback once and reuse it for both operations.

🔧 Suggested fix
      const selectedCalendar = calEventRaw.destinationCalendar?.find(
        (cal) => cal.credentialId === credentialId
      )?.externalId;
+     const calendarId = selectedCalendar || "primary";

      calendar.events.insert(
        {
          auth: myGoogleAuth,
-         calendarId: selectedCalendar || "primary",
+         calendarId,
          requestBody: payload,
          conferenceDataVersion: 1,
          sendUpdates: "none",
        },
        function (error, event) {
          if (event && event.data.id && event.data.hangoutLink) {
            calendar.events.patch({
              // Update the same event but this time we know the hangout link
-             calendarId: selectedCalendar,
+             calendarId,
              auth: myGoogleAuth,
              eventId: event.data.id || "",
              requestBody: {

198-230: ⚠️ Potential issue | 🟠 Major

updateEvent can send an undefined calendarId

When externalCalendarId is falsy, the fallback logic compares cal.externalId === externalCalendarId (undefined), which never matches, leaving selectedCalendar undefined. This causes the API call to fail. Use the resolved destination calendar instead.

🔧 Suggested fix
-      const selectedCalendar = externalCalendarId
-        ? externalCalendarId
-        : event.destinationCalendar?.find((cal) => cal.externalId === externalCalendarId)?.externalId;
+      const selectedCalendar =
+        externalCalendarId ||
+        mainHostDestinationCalendar?.externalId ||
+        event.destinationCalendar?.[0]?.externalId ||
+        "primary";
🤖 Fix all issues with AI agents
In `@packages/app-store/googlecalendar/lib/CalendarService.ts`:
- Around line 315-318: The fallback for computing calendarId is incorrect:
change the logic in the delete path (where calendarId is computed using
externalCalendarId and event.destinationCalendar) so that when
externalCalendarId is null/undefined you use the first destination calendar's
externalId if present, otherwise fall back to "primary"; update the code that
computes calendarId (reference externalCalendarId, calendarId, and
event.destinationCalendar in the deleteEvent flow) to implement this new
fallback order.

In `@packages/app-store/larkcalendar/lib/CalendarService.ts`:
- Around line 125-131: The createEvent implementation in CalendarService.ts must
match the Calendar interface: change the method signature of createEvent to
include the second parameter credentialId: number (i.e., createEvent(event:
CalendarEvent, credentialId: number)), update any internal references to accept
and use credentialId as needed, and propagate this signature change to all
callers of CalendarService.createEvent (and other calendar service
implementations like Office365/Exchange/Hubspot/Salesforce) so the type contract
in packages/types/Calendar.d.ts is satisfied.

In `@packages/core/EventManager.ts`:
- Around line 116-120: Guard against missing destination calendars by ensuring
mainHostDestinationCalendar exists before reading its .integration: in
EventManager.ts replace the current conditional that checks evt.location ===
MeetLocationType && mainHostDestinationCalendar.integration !==
"google_calendar" with a check that first verifies mainHostDestinationCalendar
is defined (e.g., mainHostDestinationCalendar &&
mainHostDestinationCalendar.integration !== "google_calendar") so you don't
access .integration on undefined before setting evt.location to
"integrations:daily".
- Around line 511-543: The code may call updateEvent(credential, ...) with
credential === undefined when credential is not found in calendarCredentials or
credentialFromDB; instead, after the DB lookup in the block guarded by
reference.credentialId, check if credential is still undefined and if so push a
per-reference failure into result (marking this specific bookingRefUid/reference
as failed with an error like "missing credential") and skip calling updateEvent
for that reference, otherwise call result.push(updateEvent(...)) as before;
reference symbols: reference.credentialId, calendarCredentials,
credentialFromDB, updateEvent, bookingRefUid, calenderExternalId.

In `@packages/features/bookings/lib/handleCancelBooking.ts`:
- Around line 251-255: Compute a single normalized value for destinationCalendar
(e.g., get dest = bookingToDelete?.destinationCalendar ??
bookingToDelete?.user.destinationCalendar ?? []) and then set
destinationCalendar = Array.isArray(dest) ? dest : [dest] so you never produce
nested arrays; replace the existing inline ternary that wraps values in [] with
this normalized value and reuse it in both payloads (also update the other
occurrence referenced around lines 528-532).
- Around line 418-484: In handleCancelBooking: bookingCalendarReference
currently leads to only the first _calendar ref being used per updated booking
and the recurring-deletion logic runs for each bookingCalendarReference (causing
duplicate API calls); change the per-updated-booking lookup from find(...) to
filter(...) so you iterate all calendar references for each updated booking (use
bookingRef(s) instead of a single bookingRef), resolve calendar credentials per
reference (use reference.credentialId to fetch the specific credential via
prisma if not present in bookingToDelete.user.credentials), and move/guard the
recurring branch (the block that maps
bookingToDelete.user.credentials/getCalendar and iterates updatedBookings) so it
executes once for the recurring case rather than once per
bookingCalendarReference; keep using getCalendar and calendar.deleteEvent(uid,
evt, externalCalendarId) but collect promises into apiDeletes without
duplicating calls.

In `@packages/types/Calendar.d.ts`:
- Line 221: Update each CalendarService implementation to match the new
interface: change the createEvent signature to accept (event: CalendarEvent,
credentialId: number): Promise<NewCalendarEventType>, update any internal calls
and parameter passing to forward credentialId to downstream clients or API
helpers, and adjust any mock/tests that call createEvent; specifically edit the
createEvent method in the listed implementations (e.g., CalendarService in
packages/lib, packages/app-store/*/lib) so they accept the credentialId
parameter and use it where credentials/authorization are selected or passed
along.

Comment on lines +315 to 318
const calendarId = externalCalendarId
? externalCalendarId
: event.destinationCalendar?.find((cal) => cal.externalId === externalCalendarId)?.externalId;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cd packages/app-store/googlecalendar/lib && wc -l CalendarService.ts

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 112


🏁 Script executed:

cd packages/app-store/googlecalendar/lib && sed -n '300,330p' CalendarService.ts

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 1119


🏁 Script executed:

cd packages/app-store/googlecalendar/lib && grep -n "deleteEvent" CalendarService.ts | head -20

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 201


🏁 Script executed:

cd packages/app-store/googlecalendar && find . -name "*.ts" -type f | xargs grep -l "interface CalendarEvent\|type CalendarEvent" | head -5

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 72


🏁 Script executed:

cd packages && rg "interface CalendarEvent|type CalendarEvent" -A 10

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 1896


🏁 Script executed:

cd packages/app-store/googlecalendar/lib && sed -n '306,340p' CalendarService.ts

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 1428


🏁 Script executed:

cd packages && rg "interface CalendarEvent" -A 50 types/Calendar.d.ts | head -80

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 1752


🏁 Script executed:

cd packages/app-store/googlecalendar/lib && grep -n "destinationCalendar" CalendarService.ts | head -20

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 719


🏁 Script executed:

cd packages && rg "destinationCalendar" -B 2 -A 2 types/Calendar.d.ts

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 293


🏁 Script executed:

cd packages && rg "interface DestinationCalendar|type DestinationCalendar" -A 10 types/Calendar.d.ts

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 72


🏁 Script executed:

cd packages/app-store/googlecalendar/lib && sed -n '250,270p' CalendarService.ts

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 755


🏁 Script executed:

cd packages && rg "DestinationCalendar" types/Calendar.d.ts

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 241


🏁 Script executed:

cd packages && rg "export.*DestinationCalendar" -A 10

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 2443


🏁 Script executed:

cd packages && find . -name "*.d.ts" -type f | xargs grep -l "DestinationCalendar" | head -5

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 111


🏁 Script executed:

cd packages && rg "model DestinationCalendar" -A 15

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 1030


🏁 Script executed:

cd packages && rg "SelectedCalendar" types/Calendar.d.ts -B 2 -A 2

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 409


🏁 Script executed:

cd packages/app-store/googlecalendar/lib && sed -n '240,280p' CalendarService.ts

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 1431


🏁 Script executed:

cd packages/app-store/googlecalendar/lib && sed -n '230,260p' CalendarService.ts

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 971


🏁 Script executed:

cd packages/app-store/googlecalendar/lib && grep -B 5 "async updateEvent" CalendarService.ts

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 220


🏁 Script executed:

cd packages && rg "deleteEvent\|updateEvent" --type ts -A 3 packages/app-store/googlecalendar/lib/CalendarService.ts | head -60

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 186


🏁 Script executed:

cd packages/app-store/googlecalendar/lib && rg "async.*Event" CalendarService.ts | head -20

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 399


🏁 Script executed:

cd packages/app-store/googlecalendar/lib && sed -n '140,180p' CalendarService.ts

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 1519


🏁 Script executed:

cd packages/app-store/googlecalendar/lib && grep -n "async createEvent\|async updateEvent\|async deleteEvent" CalendarService.ts

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 410


🏁 Script executed:

cd packages/app-store/googlecalendar/lib && sed -n '87,160p' CalendarService.ts | head -60

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 2271


🏁 Script executed:

cd packages/app-store/googlecalendar/lib && sed -n '306,340p' CalendarService.ts

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 1428


Fix deleteEvent fallback logic when externalCalendarId is missing

The fallback logic on line 315-318 is broken. When externalCalendarId is undefined/null, the .find() searches for cal.externalId === undefined, which never matches any calendar. This causes calendarId to become undefined, forcing deletion to always target the "primary" calendar regardless of where the event was created. Since 404 errors are silently swallowed (line 339), deletion silently fails when the event is on another calendar.

Use the first destination calendar or primary directly:

🔧 Suggested fix
-      const calendarId = externalCalendarId
-        ? externalCalendarId
-        : event.destinationCalendar?.find((cal) => cal.externalId === externalCalendarId)?.externalId;
+      const calendarId =
+        externalCalendarId || event.destinationCalendar?.[0]?.externalId || defaultCalendarId;

       calendar.events.delete(
         {
           auth: myGoogleAuth,
-          calendarId: calendarId ? calendarId : defaultCalendarId,
+          calendarId,
🤖 Prompt for AI Agents
In `@packages/app-store/googlecalendar/lib/CalendarService.ts` around lines 315 -
318, The fallback for computing calendarId is incorrect: change the logic in the
delete path (where calendarId is computed using externalCalendarId and
event.destinationCalendar) so that when externalCalendarId is null/undefined you
use the first destination calendar's externalId if present, otherwise fall back
to "primary"; update the code that computes calendarId (reference
externalCalendarId, calendarId, and event.destinationCalendar in the deleteEvent
flow) to implement this new fallback order.

Comment on lines 125 to 131
async createEvent(event: CalendarEvent): Promise<NewCalendarEventType> {
let eventId = "";
let eventRespData;
const calendarId = event.destinationCalendar?.externalId;
const [mainHostDestinationCalendar] = event.destinationCalendar ?? [];
const calendarId = mainHostDestinationCalendar?.externalId;
if (!calendarId) {
throw new Error("no calendar id");

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Find the Calendar interface definition
fd "\.ts$" packages/app-store | xargs rg "interface Calendar" -A 20

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 1562


🏁 Script executed:

# Check the actual LarkCalendarService createEvent signature
cat packages/app-store/larkcalendar/lib/CalendarService.ts | head -150

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 5129


🏁 Script executed:

# Find all calendar services
fd -type d "packages/app-store/*/lib" | grep -E "calendar|outlook|google" | head -10

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 260


🏁 Script executed:

# Find the Calendar interface definition in types
fd -type f "Calendar.ts" packages/

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 260


🏁 Script executed:

# Search for interface Calendar in the codebase
rg "interface Calendar" --type ts -A 15

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 3906


🏁 Script executed:

# Find all calendar services and their createEvent signatures
find packages/app-store -name "CalendarService.ts" -o -name "calendar.ts" | head -20

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 919


🏁 Script executed:

# Check other calendar services
ls -la packages/app-store | grep -E "google|outlook|caldav|ical"

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 284


🏁 Script executed:

# Check Google Calendar Service createEvent signature
head -200 packages/app-store/googlecalendar/lib/CalendarService.ts | grep -A 5 "createEvent"

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 396


🏁 Script executed:

# Check Outlook Calendar Service createEvent signature
head -200 packages/app-store/office365calendar/lib/CalendarService.ts | grep -A 5 "createEvent"

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 422


🏁 Script executed:

# Check CalDAV Calendar Service createEvent signature
head -200 packages/app-store/caldavcalendar/lib/CalendarService.ts | grep -A 5 "createEvent"

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 72


🏁 Script executed:

# Count how many calendar services have the credentialId parameter
rg "async createEvent\(event: CalendarEvent, credentialId: number\)" packages/app-store

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 72


🏁 Script executed:

# Count how many calendar services still have the old signature
rg "async createEvent\(event: CalendarEvent\):" packages/app-store

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 1649


🏁 Script executed:

# Check if tsconfig enforces strict type checking
cat tsconfig.json | head -50

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 135


🏁 Script executed:

# Check if there's a build or type-check script
cat package.json | grep -A 5 '"scripts"'

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 353


🏁 Script executed:

# Look for any type errors or TODO comments about the credentialId migration
rg "credentialId" packages/app-store --type ts -B 2 -A 2 | head -100

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 7274


Interface compliance: createEvent signature mismatch with Calendar interface.

The Calendar interface (packages/types/Calendar.d.ts) requires createEvent(event: CalendarEvent, credentialId: number), but this implementation uses createEvent(event: CalendarEvent) without the credentialId parameter. This violates the interface contract and will cause a TypeScript type error.

Note: This issue affects multiple calendar services (Office 365, Exchange, Hubspot, Salesforce, and others) — only the Google Calendar service has been updated so far. Consider whether this is part of a broader migration effort.

🤖 Prompt for AI Agents
In `@packages/app-store/larkcalendar/lib/CalendarService.ts` around lines 125 -
131, The createEvent implementation in CalendarService.ts must match the
Calendar interface: change the method signature of createEvent to include the
second parameter credentialId: number (i.e., createEvent(event: CalendarEvent,
credentialId: number)), update any internal references to accept and use
credentialId as needed, and propagate this signature change to all callers of
CalendarService.createEvent (and other calendar service implementations like
Office365/Exchange/Hubspot/Salesforce) so the type contract in
packages/types/Calendar.d.ts is satisfied.

Comment on lines 116 to 120
// Fallback to Cal Video if Google Meet is selected w/o a Google Cal
if (evt.location === MeetLocationType && evt.destinationCalendar?.integration !== "google_calendar") {
// @NOTE: destinationCalendar it's an array now so as a fallback we will only check the first one
const [mainHostDestinationCalendar] = evt.destinationCalendar ?? [];
if (evt.location === MeetLocationType && mainHostDestinationCalendar.integration !== "google_calendar") {
evt["location"] = "integrations:daily";

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Guard against undefined destination calendar in Meet fallback

mainHostDestinationCalendar can be undefined when no destination calendars are set; accessing .integration will throw. Line 119 should tolerate missing values.

🔧 Suggested fix
-    const [mainHostDestinationCalendar] = evt.destinationCalendar ?? [];
-    if (evt.location === MeetLocationType && mainHostDestinationCalendar.integration !== "google_calendar") {
+    const [mainHostDestinationCalendar] = evt.destinationCalendar ?? [];
+    if (
+      evt.location === MeetLocationType &&
+      mainHostDestinationCalendar?.integration !== "google_calendar"
+    ) {
       evt["location"] = "integrations:daily";
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Fallback to Cal Video if Google Meet is selected w/o a Google Cal
if (evt.location === MeetLocationType && evt.destinationCalendar?.integration !== "google_calendar") {
// @NOTE: destinationCalendar it's an array now so as a fallback we will only check the first one
const [mainHostDestinationCalendar] = evt.destinationCalendar ?? [];
if (evt.location === MeetLocationType && mainHostDestinationCalendar.integration !== "google_calendar") {
evt["location"] = "integrations:daily";
// Fallback to Cal Video if Google Meet is selected w/o a Google Cal
// `@NOTE`: destinationCalendar it's an array now so as a fallback we will only check the first one
const [mainHostDestinationCalendar] = evt.destinationCalendar ?? [];
if (
evt.location === MeetLocationType &&
mainHostDestinationCalendar?.integration !== "google_calendar"
) {
evt["location"] = "integrations:daily";
🤖 Prompt for AI Agents
In `@packages/core/EventManager.ts` around lines 116 - 120, Guard against missing
destination calendars by ensuring mainHostDestinationCalendar exists before
reading its .integration: in EventManager.ts replace the current conditional
that checks evt.location === MeetLocationType &&
mainHostDestinationCalendar.integration !== "google_calendar" with a check that
first verifies mainHostDestinationCalendar is defined (e.g.,
mainHostDestinationCalendar && mainHostDestinationCalendar.integration !==
"google_calendar") so you don't access .integration on undefined before setting
evt.location to "integrations:daily".

Comment on lines +511 to +543
if (reference.credentialId) {
credential = this.calendarCredentials.filter(
(credential) => credential.id === reference?.credentialId
)[0];
if (!credential) {
// Fetch credential from DB
const credentialFromDB = await prisma.credential.findUnique({
include: {
app: {
select: {
slug: true,
},
},
},
where: {
id: reference.credentialId,
},
});
if (credentialFromDB && credentialFromDB.app?.slug) {
credential = {
appName: credentialFromDB?.app.slug ?? "",
id: credentialFromDB.id,
type: credentialFromDB.type,
key: credentialFromDB.key,
userId: credentialFromDB.userId,
teamId: credentialFromDB.teamId,
invalid: credentialFromDB.invalid,
appId: credentialFromDB.appId,
};
}
}
result.push(updateEvent(credential, event, bookingRefUid, calenderExternalId));
} else {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Handle missing credential before calling updateEvent

If the credential isn’t found in memory or DB, updateEvent(credential, …) receives undefined, which throws and forces the catch block to mark all references as failed. Emit a per-reference failure instead.

🔧 Suggested fix
         if (reference.credentialId) {
           credential = this.calendarCredentials.filter(
             (credential) => credential.id === reference?.credentialId
           )[0];
           if (!credential) {
             // Fetch credential from DB
             const credentialFromDB = await prisma.credential.findUnique({
               include: {
                 app: {
                   select: {
                     slug: true,
                   },
                 },
               },
               where: {
                 id: reference.credentialId,
               },
             });
             if (credentialFromDB && credentialFromDB.app?.slug) {
               credential = {
                 appName: credentialFromDB?.app.slug ?? "",
                 id: credentialFromDB.id,
                 type: credentialFromDB.type,
                 key: credentialFromDB.key,
                 userId: credentialFromDB.userId,
                 teamId: credentialFromDB.teamId,
                 invalid: credentialFromDB.invalid,
                 appId: credentialFromDB.appId,
               };
             }
           }
-          result.push(updateEvent(credential, event, bookingRefUid, calenderExternalId));
+          if (!credential) {
+            result.push(
+              Promise.resolve({
+                appName: "none",
+                type: reference.type,
+                success: false,
+                uid: "",
+                originalEvent: event,
+                credentialId: reference.credentialId ?? 0,
+              })
+            );
+            continue;
+          }
+          result.push(updateEvent(credential, event, bookingRefUid, calenderExternalId));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (reference.credentialId) {
credential = this.calendarCredentials.filter(
(credential) => credential.id === reference?.credentialId
)[0];
if (!credential) {
// Fetch credential from DB
const credentialFromDB = await prisma.credential.findUnique({
include: {
app: {
select: {
slug: true,
},
},
},
where: {
id: reference.credentialId,
},
});
if (credentialFromDB && credentialFromDB.app?.slug) {
credential = {
appName: credentialFromDB?.app.slug ?? "",
id: credentialFromDB.id,
type: credentialFromDB.type,
key: credentialFromDB.key,
userId: credentialFromDB.userId,
teamId: credentialFromDB.teamId,
invalid: credentialFromDB.invalid,
appId: credentialFromDB.appId,
};
}
}
result.push(updateEvent(credential, event, bookingRefUid, calenderExternalId));
} else {
if (reference.credentialId) {
credential = this.calendarCredentials.filter(
(credential) => credential.id === reference?.credentialId
)[0];
if (!credential) {
// Fetch credential from DB
const credentialFromDB = await prisma.credential.findUnique({
include: {
app: {
select: {
slug: true,
},
},
},
where: {
id: reference.credentialId,
},
});
if (credentialFromDB && credentialFromDB.app?.slug) {
credential = {
appName: credentialFromDB?.app.slug ?? "",
id: credentialFromDB.id,
type: credentialFromDB.type,
key: credentialFromDB.key,
userId: credentialFromDB.userId,
teamId: credentialFromDB.teamId,
invalid: credentialFromDB.invalid,
appId: credentialFromDB.appId,
};
}
}
if (!credential) {
result.push(
Promise.resolve({
appName: "none",
type: reference.type,
success: false,
uid: "",
originalEvent: event,
credentialId: reference.credentialId ?? 0,
})
);
continue;
}
result.push(updateEvent(credential, event, bookingRefUid, calenderExternalId));
} else {
🤖 Prompt for AI Agents
In `@packages/core/EventManager.ts` around lines 511 - 543, The code may call
updateEvent(credential, ...) with credential === undefined when credential is
not found in calendarCredentials or credentialFromDB; instead, after the DB
lookup in the block guarded by reference.credentialId, check if credential is
still undefined and if so push a per-reference failure into result (marking this
specific bookingRefUid/reference as failed with an error like "missing
credential") and skip calling updateEvent for that reference, otherwise call
result.push(updateEvent(...)) as before; reference symbols:
reference.credentialId, calendarCredentials, credentialFromDB, updateEvent,
bookingRefUid, calenderExternalId.

Comment on lines +251 to +255
destinationCalendar: bookingToDelete?.destinationCalendar
? [bookingToDelete?.destinationCalendar]
: bookingToDelete?.user.destinationCalendar
? [bookingToDelete?.user.destinationCalendar]
: [],

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Normalize destinationCalendar without nesting arrays.

With array-based destination calendars, wrapping the value in [] will produce [[...]] (and [[]] for empty arrays), which breaks downstream expectations. Normalize with Array.isArray and reuse the normalized value for both payloads.

🛠️ Proposed fix
@@
-    destinationCalendar: bookingToDelete?.destinationCalendar
-      ? [bookingToDelete?.destinationCalendar]
-      : bookingToDelete?.user.destinationCalendar
-      ? [bookingToDelete?.user.destinationCalendar]
-      : [],
+    destinationCalendar:
+      bookingToDelete?.destinationCalendar != null
+        ? Array.isArray(bookingToDelete.destinationCalendar)
+          ? bookingToDelete.destinationCalendar
+          : [bookingToDelete.destinationCalendar]
+        : bookingToDelete?.user.destinationCalendar != null
+        ? Array.isArray(bookingToDelete.user.destinationCalendar)
+          ? bookingToDelete.user.destinationCalendar
+          : [bookingToDelete.user.destinationCalendar]
+        : [],
@@
-      destinationCalendar: bookingToDelete?.destinationCalendar
-        ? [bookingToDelete?.destinationCalendar]
-        : bookingToDelete?.user.destinationCalendar
-        ? [bookingToDelete?.user.destinationCalendar]
-        : [],
+      destinationCalendar:
+        bookingToDelete?.destinationCalendar != null
+          ? Array.isArray(bookingToDelete.destinationCalendar)
+            ? bookingToDelete.destinationCalendar
+            : [bookingToDelete.destinationCalendar]
+          : bookingToDelete?.user.destinationCalendar != null
+          ? Array.isArray(bookingToDelete.user.destinationCalendar)
+            ? bookingToDelete.user.destinationCalendar
+            : [bookingToDelete.user.destinationCalendar]
+          : [],

Also applies to: 528-532

🤖 Prompt for AI Agents
In `@packages/features/bookings/lib/handleCancelBooking.ts` around lines 251 -
255, Compute a single normalized value for destinationCalendar (e.g., get dest =
bookingToDelete?.destinationCalendar ??
bookingToDelete?.user.destinationCalendar ?? []) and then set
destinationCalendar = Array.isArray(dest) ? dest : [dest] so you never produce
nested arrays; replace the existing inline ternary that wraps values in [] with
this normalized value and reuse it in both payloads (also update the other
occurrence referenced around lines 528-532).

Comment on lines +418 to 484
// Find the correct calendar credential under user credentials
const calendarCredential = bookingToDelete.user.credentials.find(
(credential) => credential.id === credentialId
);
if (calendarCredential) {
const calendar = await getCalendar(calendarCredential);
if (
bookingToDelete.eventType?.recurringEvent &&
bookingToDelete.recurringEventId &&
allRemainingBookings
) {
const promises = bookingToDelete.user.credentials
.filter((credential) => credential.type.endsWith("_calendar"))
.map(async (credential) => {
const calendar = await getCalendar(credential);
for (const updBooking of updatedBookings) {
const bookingRef = updBooking.references.find((ref) => ref.type.includes("_calendar"));
if (bookingRef) {
const { uid, externalCalendarId } = bookingRef;
const deletedEvent = await calendar?.deleteEvent(uid, evt, externalCalendarId);
apiDeletes.push(deletedEvent);
if (bookingCalendarReference.length > 0) {
for (const reference of bookingCalendarReference) {
const { credentialId, uid, externalCalendarId } = reference;
// If the booking calendar reference contains a credentialId
if (credentialId) {
// Find the correct calendar credential under user credentials
let calendarCredential = bookingToDelete.user.credentials.find(
(credential) => credential.id === credentialId
);
if (!calendarCredential) {
// get credential from DB
const foundCalendarCredential = await prisma.credential.findUnique({
where: {
id: credentialId,
},
});
if (foundCalendarCredential) {
calendarCredential = foundCalendarCredential;
}
}
if (calendarCredential) {
const calendar = await getCalendar(calendarCredential);
if (
bookingToDelete.eventType?.recurringEvent &&
bookingToDelete.recurringEventId &&
allRemainingBookings
) {
const promises = bookingToDelete.user.credentials
.filter((credential) => credential.type.endsWith("_calendar"))
.map(async (credential) => {
const calendar = await getCalendar(credential);
for (const updBooking of updatedBookings) {
const bookingRef = updBooking.references.find((ref) => ref.type.includes("_calendar"));
if (bookingRef) {
const { uid, externalCalendarId } = bookingRef;
const deletedEvent = await calendar?.deleteEvent(uid, evt, externalCalendarId);
apiDeletes.push(deletedEvent);
}
}
});
try {
await Promise.all(promises);
} catch (error) {
if (error instanceof Error) {
logger.error(error.message);
}
});
try {
await Promise.all(promises);
} catch (error) {
if (error instanceof Error) {
logger.error(error.message);
}
} else {
apiDeletes.push(calendar?.deleteEvent(uid, evt, externalCalendarId) as Promise<unknown>);
}
} else {
}
} else {
// For bookings made before the refactor we go through the old behavior of running through each calendar credential
const calendarCredentials = bookingToDelete.user.credentials.filter((credential) =>
credential.type.endsWith("_calendar")
);
for (const credential of calendarCredentials) {
const calendar = await getCalendar(credential);
apiDeletes.push(calendar?.deleteEvent(uid, evt, externalCalendarId) as Promise<unknown>);
}
}
} else {
// For bookings made before the refactor we go through the old behaviour of running through each calendar credential
const calendarCredentials = bookingToDelete.user.credentials.filter((credential) =>
credential.type.endsWith("_calendar")
);
for (const credential of calendarCredentials) {
const calendar = await getCalendar(credential);
apiDeletes.push(calendar?.deleteEvent(uid, evt, externalCalendarId) as Promise<unknown>);
}
}
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Recurring cancellations only delete the first calendar reference and can repeat per reference.

For multi-calendar bookings, find() removes only the first _calendar reference per updated booking, leaving other calendars untouched. Also, the recurring deletion runs once per bookingCalendarReference, which duplicates API calls. Iterate all calendar references and ensure the recurring branch runs only once.

🛠️ Proposed fix
-  if (bookingCalendarReference.length > 0) {
-    for (const reference of bookingCalendarReference) {
+  if (bookingCalendarReference.length > 0) {
+    let processedRecurring = false;
+    for (const reference of bookingCalendarReference) {
@@
-          if (
-            bookingToDelete.eventType?.recurringEvent &&
-            bookingToDelete.recurringEventId &&
-            allRemainingBookings
-          ) {
+          if (
+            bookingToDelete.eventType?.recurringEvent &&
+            bookingToDelete.recurringEventId &&
+            allRemainingBookings
+          ) {
+            if (processedRecurring) continue;
+            processedRecurring = true;
             const promises = bookingToDelete.user.credentials
               .filter((credential) => credential.type.endsWith("_calendar"))
               .map(async (credential) => {
                 const calendar = await getCalendar(credential);
                 for (const updBooking of updatedBookings) {
-                  const bookingRef = updBooking.references.find((ref) => ref.type.includes("_calendar"));
-                  if (bookingRef) {
-                    const { uid, externalCalendarId } = bookingRef;
-                    const deletedEvent = await calendar?.deleteEvent(uid, evt, externalCalendarId);
-                    apiDeletes.push(deletedEvent);
-                  }
+                  const bookingRefs = updBooking.references.filter((ref) =>
+                    ref.type.includes("_calendar")
+                  );
+                  for (const bookingRef of bookingRefs) {
+                    const { uid, externalCalendarId } = bookingRef;
+                    const deletedEvent = await calendar?.deleteEvent(uid, evt, externalCalendarId);
+                    apiDeletes.push(deletedEvent);
+                  }
                 }
               });

If each reference carries a credentialId, consider resolving the credential per reference instead of iterating all calendar credentials to avoid mismatched credential deletes.

🤖 Prompt for AI Agents
In `@packages/features/bookings/lib/handleCancelBooking.ts` around lines 418 -
484, In handleCancelBooking: bookingCalendarReference currently leads to only
the first _calendar ref being used per updated booking and the
recurring-deletion logic runs for each bookingCalendarReference (causing
duplicate API calls); change the per-updated-booking lookup from find(...) to
filter(...) so you iterate all calendar references for each updated booking (use
bookingRef(s) instead of a single bookingRef), resolve calendar credentials per
reference (use reference.credentialId to fetch the specific credential via
prisma if not present in bookingToDelete.user.credentials), and move/guard the
recurring branch (the block that maps
bookingToDelete.user.credentials/getCalendar and iterates updatedBookings) so it
executes once for the recurring case rather than once per
bookingCalendarReference; keep using getCalendar and calendar.deleteEvent(uid,
evt, externalCalendarId) but collect promises into apiDeletes without
duplicating calls.

@@ -216,7 +218,7 @@ export interface IntegrationCalendar extends Ensure<Partial<SelectedCalendar>, "
}

export interface Calendar {
createEvent(event: CalendarEvent): Promise<NewCalendarEventType>;
createEvent(event: CalendarEvent, credentialId: number): Promise<NewCalendarEventType>;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Run the provided verification script
rg -n 'async createEvent\(event: CalendarEvent\)' --type ts

# Also search for all createEvent implementations to get a complete picture
echo "=== All createEvent implementations ==="
rg -n 'createEvent\s*\(' --type ts -A 2

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 10412


Critical: 13 calendar implementations have not been updated with the new credentialId parameter.

The interface change requires all createEvent implementations to accept the credentialId parameter, but only the Google Calendar implementation has been updated. The following implementations still use the old signature and must be updated:

  • packages/lib/CalendarService.ts
  • packages/app-store/zoho-bigin/lib/CalendarService.ts
  • packages/app-store/zohocrm/lib/CalendarService.ts
  • packages/app-store/salesforce/lib/CalendarService.ts
  • packages/app-store/sendgrid/lib/CalendarService.ts
  • packages/app-store/office365calendar/lib/CalendarService.ts
  • packages/app-store/larkcalendar/lib/CalendarService.ts
  • packages/app-store/hubspot/lib/CalendarService.ts
  • packages/app-store/exchangecalendar/lib/CalendarService.ts
  • packages/app-store/exchange2016calendar/lib/CalendarService.ts
  • packages/app-store/exchange2013calendar/lib/CalendarService.ts
  • packages/app-store/closecom/lib/CalendarService.ts
  • packages/app-store/basecamp3/lib/CalendarService.ts
🤖 Prompt for AI Agents
In `@packages/types/Calendar.d.ts` at line 221, Update each CalendarService
implementation to match the new interface: change the createEvent signature to
accept (event: CalendarEvent, credentialId: number):
Promise<NewCalendarEventType>, update any internal calls and parameter passing
to forward credentialId to downstream clients or API helpers, and adjust any
mock/tests that call createEvent; specifically edit the createEvent method in
the listed implementations (e.g., CalendarService in packages/lib,
packages/app-store/*/lib) so they accept the credentialId parameter and use it
where credentials/authorization are selected or passed along.

Sign in to join this conversation on GitHub.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

1 participant