Skip to content

fix: handle collective multiple host on destinationCalendar#8

Open
akshayutture-augment wants to merge 1 commit into
enhance-collective-scheduling-foundationfrom
fix/handle-collective-multiple-host-destinations
Open

fix: handle collective multiple host on destinationCalendar#8
akshayutture-augment wants to merge 1 commit into
enhance-collective-scheduling-foundationfrom
fix/handle-collective-multiple-host-destinations

Conversation

@akshayutture-augment

@akshayutture-augment akshayutture-augment commented Nov 14, 2025

Copy link
Copy Markdown

Test 4

Summary by CodeRabbit

  • New Features

    • Added support for multiple destination calendars per booking.
    • Enhanced team scheduling with per-member calendar selection for collective bookings.
    • Improved calendar credential resolution with fallback logic.
  • Bug Fixes

    • Standardized calendar data format across all calendar integrations.
  • Chores

    • Refactored calendar handling to use array-based format for consistent processing.

Replicated from ai-code-review-evaluation/cal.com-coderabbit#4

@akshayutture-augment

Copy link
Copy Markdown
Author

@coderabbitai review

@akshayutture-augment

Copy link
Copy Markdown
Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Nov 18, 2025

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Nov 18, 2025

Copy link
Copy Markdown

Walkthrough

This refactor converts destination calendar handling from single-value to array-based architecture across the codebase. Calendar services now extract the first array element as the primary calendar, credential IDs are propagated through event creation methods, and team booking logic accumulates member destination calendars for collective scheduling scenarios.

Changes

Cohort / File(s) Summary
Type Definitions
packages/types/Calendar.d.ts, packages/types/EventManager.d.ts, packages/core/builders/CalendarEvent/class.ts
destinationCalendar type widened from DestinationCalendar | null to DestinationCalendar[] | null; Calendar.createEvent signature updated to require credentialId: number; SchedulingType field added to CalendarEvent; EventResult augmented with optional externalId property.
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
All services extract first array element as mainHostDestinationCalendar and use its externalId for calendar operations; Google Calendar createEvent now accepts credentialId parameter for credential-matched calendar selection; consistent fallback to "primary" calendar when destination not found.
Core Event Management
packages/core/CalendarManager.ts, packages/core/EventManager.ts
CalendarManager.createEvent extended with externalId parameter and credential ID propagation; error handling refined (404 returns undefined); EventManager refactored for multi-reference calendar handling with per-reference credential resolution, parallel calendar updates, and per-reference error mapping.
Booking Handlers
packages/features/bookings/lib/handleNewBooking.ts, packages/features/bookings/lib/handleCancelBooking.ts
destinationCalendar wrapped in arrays; handleNewBooking introduces dynamic user loading with robust error handling, accumulates team member destination calendars for collective scheduling; handleCancelBooking loops over multi-reference credentials with DB fallback lookup and parallel promise error handling.
Event Data Construction
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
All handlers normalize destinationCalendar to single-element or empty array format, selecting from booking or user sources; consistent fallback behavior.
Payment & Notification Webhooks
packages/features/ee/payments/api/webhook.ts, packages/features/ee/payments/api/paypal-webhook.ts, apps/web/pages/api/cron/bookingReminder.ts, packages/emails/src/templates/BrokenIntegrationEmail.tsx
Destination calendar assignments wrapped in arrays; email templates extract first array element for integration property access.
Infrastructure & Tests
packages/core/builders/CalendarEvent/builder.ts, packages/trpc/server/routers/viewer/organizations/create.handler.ts, apps/web/playwright/webhook.e2e.ts
Import refactored to type-only for Booking; organization slug/requestedSlug logic inverted to check IS_TEAM_BILLING_ENABLED; webhook test expectation updated with empty array for destinationCalendar.

Sequence Diagram(s)

sequenceDiagram
    participant Booking as Booking Handler
    participant EM as EventManager
    participant CM as CalendarManager
    participant CS as Calendar Service
    participant DB as Database

    Note over Booking,DB: Old Flow: Single Destination Calendar
    Booking->>EM: createAllCalendarEvents(evt: destinationCalendar?)
    EM->>CM: createEvent(evt, externalId?)
    CM->>CS: createEvent(evt)
    CS->>CS: Use destinationCalendar.externalId
    CS-->>CM: Result
    CM-->>EM: EventResult
    EM-->>Booking: SingleResult

    Note over Booking,DB: New Flow: Multi-Destination Calendar Array
    Booking->>EM: createAllCalendarEvents(evt: destinationCalendar[])
    EM->>EM: For each destination in array
    alt destination.credentialId exists
        EM->>DB: Fetch credential if missing
        EM->>CM: createEvent(evt, externalId, credentialId)
    else No credentialId
        EM->>DB: Find all credentials for integration
        EM->>CM: createEvent(evt, externalId) for each
    end
    CM->>CS: createEvent(evt, credentialId)
    CS->>CS: Match calendar by credentialId
    CS-->>CM: Result + credentialId + externalId
    CM-->>EM: EventResult with metadata
    EM->>EM: Collect per-reference results
    EM-->>Booking: ResultArray[]
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Areas requiring close attention:

  • packages/core/EventManager.ts — Major refactor with multi-reference iteration, credential resolution, parallel Promise handling, and per-reference error mapping; ensure array index handling and credential fallback logic are correct
  • packages/features/bookings/lib/handleNewBooking.ts — Extensive loadUsers refactor with dynamic team member handling, error guarding, and accumulation of team destination calendars; verify collective scheduling logic and DestinationCalendar type integration
  • packages/features/bookings/lib/handleCancelBooking.ts — Multi-reference iteration with DB credential lookup fallback and parallel calendar updates; test edge cases around missing credentials and error propagation
  • packages/app-store/googlecalendar/lib/CalendarService.ts — New credentialId parameter propagation and calendar matching logic; validate that credential-matched calendar selection doesn't break existing single-calendar scenarios
  • Type changes — Verify all call sites passing destinationCalendar as arrays and that array-to-single-element extraction (mainHostDestinationCalendar) is handled consistently across all services

Poem

🐰 Hops of joy for calendars now many,
Where once was one, arrays overflow with any!
Team members gather their dates so bright,
Credentials match up, the booking's just right! ✨📅

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the main objective: refactoring destinationCalendar handling to support multiple hosts in collective scheduling, which aligns with the core changes across the codebase.
✨ 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

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

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

⚠️ Outside diff range comments (4)
packages/lib/CalendarService.ts (1)

125-198: mainHostDestinationCalendar extraction is correct; guard against “no matching calendar” edge case

Deriving mainHostDestinationCalendar via event.destinationCalendar ?? [] and using its externalId both to filter target calendars and to mark primary in listCalendars correctly adapts this CalDAV service to the new array form without changing intent.

One edge case to consider: if mainHostDestinationCalendar?.externalId is set but does not match any fetched calendar URLs, the .filter in createEvent will yield an empty array, so no createCalendarObject calls are made while the function still returns a “successful” UID. It may be worth logging or surfacing a warning/error when mainHostDestinationCalendar.externalId is defined but no calendars match, to make silent misconfiguration easier to detect.

Also applies to: 496-521

packages/core/EventManager.ts (1)

116-123: Null-safety bug in Google Meet fallback when no destinationCalendar is set

Here:

const [mainHostDestinationCalendar] = evt.destinationCalendar ?? [];
if (evt.location === MeetLocationType && mainHostDestinationCalendar.integration !== "google_calendar") {
  evt["location"] = "integrations:daily";
}

If evt.destinationCalendar is null or an empty array (which is valid with the new types), mainHostDestinationCalendar is undefined. Accessing .integration on undefined will throw at runtime when evt.location === MeetLocationType.

This needs an optional chain or equivalent guard:

- if (evt.location === MeetLocationType && mainHostDestinationCalendar.integration !== "google_calendar") {
+ if (
+   evt.location === MeetLocationType &&
+   mainHostDestinationCalendar?.integration !== "google_calendar"
+ ) {
    evt["location"] = "integrations:daily";
  }

Without this change, bookings with Google Meet location and no destination calendar will crash in EventManager.create.

packages/app-store/googlecalendar/lib/CalendarService.ts (2)

196-203: Fix the .find logic to properly handle missing externalCalendarId in updateEvent and deleteEvent

The review comment is confirmed. Both methods contain a critical logic flaw:

  • Lines 254-256 (updateEvent) and 315-317 (deleteEvent) search event.destinationCalendar?.find((cal) => cal.externalId === externalCalendarId) in the fallback branch.
  • When externalCalendarId is falsy (undefined/null/empty), this predicate never matches any calendar, returning undefined.
  • For deleteEvent, undefined then silently falls back to "primary", ignoring available destination calendars.
  • For updateEvent, undefined may be passed directly to the API call.

Required changes:

  • Only attempt .find() when externalCalendarId is truthy.
  • Fall back to event.destinationCalendar?.[0]?.externalId before falling back to "primary" or defaultCalendarId.

This ensures legacy bookings with missing externalCalendarId but populated destinationCalendar use the available calendar instead of silently defaulting to primary.


87-104: Calendar selection logic passes undefined calendarId to Google API calls and mishandles fallbacks

The review comment is accurate. All three issues are confirmed in the source code:

  1. createEvent patch lacks fallback (line 168): The calendar.events.patch() call uses calendarId: selectedCalendar without the || "primary" fallback that calendar.events.insert() correctly applies on line 153. If selectedCalendar is undefined (no matching destination calendar), the patch will fail.

  2. updateEvent and deleteEvent compare against undefined (lines 250–252, 315–317): Both methods use .find((cal) => cal.externalId === externalCalendarId) where externalCalendarId may be falsy/undefined. This predicate will never match any calendar, so event.destinationCalendar is ignored as a fallback. In updateEvent, selectedCalendar is then passed directly to calendar.events.update() on line 256 without a fallback, risking undefined.

  3. deleteEvent has a fallback but flawed logic: While line 322 does fall back to defaultCalendarId, the find logic is still incorrect and should not compare against undefined.

The fixes in the review comment are correct and necessary to ensure robust calendar selection across all three methods.

🧹 Nitpick comments (12)
packages/emails/src/templates/BrokenIntegrationEmail.tsx (1)

88-91: Simplify the optional chaining and ensure type consistency.

Line 90 contains redundant optional chaining (mainHostDestinationCalendar?.integration) since mainHostDestinationCalendar is already checked in the ternary condition. Additionally, the fallback value "calendar" is a string, while split("_") returns an array, creating a type inconsistency that makes the code harder to reason about.

Consider this refactor for clarity:

 const [mainHostDestinationCalendar] = calEvent.destinationCalendar ?? [];
 let calendar = mainHostDestinationCalendar
-  ? mainHostDestinationCalendar?.integration.split("_")
-  : "calendar";
+  ? mainHostDestinationCalendar.integration.split("_")
+  : ["calendar"];

This ensures calendar is always an array initially, making the subsequent Array.isArray check on line 93 more predictable.

packages/app-store/larkcalendar/lib/CalendarService.ts (1)

125-161: Array-based destinationCalendar handling is correct; logic is duplicated in a few places

Using [mainHostDestinationCalendar] = event.destinationCalendar ?? [] and then mainHostDestinationCalendar?.externalId safely adapts to the new array shape and avoids null access across create/update/delete and attendee creation. To keep behavior perfectly in sync if this ever changes, consider extracting a small helper like getMainHostDestinationCalendar(event) instead of repeating the destructuring in each method.

Also applies to: 163-182, 189-229, 236-252

packages/trpc/server/routers/viewer/bookings/editLocation.handler.ts (1)

68-92: destinationCalendar normalization to array matches the new CalendarEvent contract

Wrapping booking.destinationCalendar (or the user’s destinationCalendar fallback) in a single‑element array, and defaulting to [], cleanly aligns this handler with the updated destinationCalendar: DestinationCalendar[] shape while preserving existing selection logic. If you keep adding similar logic in other handlers, consider a shared helper (e.g. getSelectedDestinationCalendar(booking)) to avoid future drift.

packages/app-store/office365calendar/lib/CalendarService.ts (1)

72-92: Office365 createEvent correctly switches to array-based destinationCalendar

Using [mainHostDestinationCalendar] = event.destinationCalendar ?? [] and then targeting /me/calendars/{id}/events when externalId is present keeps existing behavior while supporting DestinationCalendar[], with a safe fallback to /me/calendar/events when none is selected. As a tiny optional cleanup, the second ?. in ${mainHostDestinationCalendar?.externalId} isn’t strictly needed since you already guard on it above.

packages/features/ee/payments/api/webhook.ts (1)

100-121: Stripe payment flows now normalize destinationCalendar to an array consistently

In both getBooking and handlePaymentSuccess, computing selectedDestinationCalendar = booking.destinationCalendar || user.destinationCalendar and then setting destinationCalendar: selectedDestinationCalendar ? [selectedDestinationCalendar] : [] preserves the previous fallback logic while conforming to the new DestinationCalendar[] shape used by CalendarEvent.

Since this selection rule is now duplicated (and similar patterns exist in other handlers), you might consider a small shared helper (e.g. resolveDestinationCalendar(booking, user)) to keep behavior consistent across all payment and booking flows.

Also applies to: 207-231

packages/core/CalendarManager.ts (1)

218-269: createEvent refactor cleanly propagates credentialId/externalId; error handling looks reasonable

Passing credential.id into calendar.createEvent(calEvent, credential.id) and returning both externalId and credentialId from this helper gives downstream code enough context to build accurate booking references for multi‑destination scenarios. The hideCalendarNotes behavior and updated copy (“organizer”) are also clear.

The catch handler marking success = false, special‑casing code === 404, and optionally capturing calError provides a controlled failure path without throwing for the “mismatched externalId/credential” case. Just ensure all calendar adapters’ createEvent signatures are compatible with the extra credentialId parameter (even if some ignore it), and that callers supply the appropriate externalId where you rely on it for later correlation.

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

250-256: destinationCalendar array handling is consistent; consider de-duplicating the construction

The two places where evt.destinationCalendar is built now both follow:

destinationCalendar: bookingToDelete?.destinationCalendar
  ? [bookingToDelete.destinationCalendar]
  : bookingToDelete?.user.destinationCalendar
  ? [bookingToDelete.user.destinationCalendar]
  : [];

This matches the new DestinationCalendar[] | null shape and works fine with downstream checks that look at length > 0.

If this pattern is going to be reused further, consider extracting a small helper (e.g., getBookingDestinationCalendars(booking)) to avoid subtle divergence between the paid/non-paid paths in future edits.

Also applies to: 528-533

packages/core/EventManager.ts (2)

336-383: Multi-destination create flow looks correct; consider sharing credential-fetch helper

The new createAllCalendarEvents logic:

  • Iterates event.destinationCalendar[].
  • When destination.credentialId is present, resolves that specific credential (with DB fallback if not already in this.calendarCredentials) and calls createEvent(credential, event, destination.externalId).
  • When not, falls back to all credentials whose type matches destination.integration.

This matches the new per-destination model and should behave correctly.

You may want to factor the “fetch credential from DB and build CredentialWithAppName” code into a small helper, since the same pattern also appears in updateAllCalendarEvents.


474-590: updateAllCalendarEvents correctly iterates calendar references, but error fallback loses credential context

The refactor to:

  • Derive calendarReference as an array of all _calendar references (from newBooking when merging, otherwise from booking),
  • Resolve a credential per reference (with DB fallback), and
  • Push updateEvent(credential, event, bookingRefUid, calenderExternalId),

is consistent with the new multi-reference model and should handle most cases well.

In the catch block, though:

return Promise.resolve(
  calendarReference?.map((reference) => ({
    appName: "none",
    type: reference?.type || "calendar",
    success: false,
    uid: "",
    originalEvent: event,
    credentialId: 0,
  })) ?? []
);

you intentionally drop any association with the actual credential ID that failed. If you care about observability, you could surface reference.credentialId ?? 0 instead of a hard-coded 0 to make debugging easier:

- credentialId: 0,
+ credentialId: reference?.credentialId ?? 0,

Functionally everything still works; this is just a small improvement to error reporting.

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

724-769: loadUsers refactor improves clarity and error handling

The new loadUsers encapsulation:

  • Handles dynamic groups (no eventTypeId) by loading users by username list.
  • Handles eventType-based bookings by mapping hosts into user-like objects, falling back to eventType.users when no hosts are defined.
  • Wraps Prisma and HttpError failures into consistent HttpError responses, with a generic 500 for unexpected errors.

This makes the user-loading logic easier to follow and isolates error mapping to one place. The only minor suggestion is to preserve more detail from non-Prisma errors if you care about debugging, but functionally this is solid.


987-1009: teamDestinationCalendars + evt.destinationCalendar push correctly aggregate host calendars for COLLECTIVE

You now:

  • Collect each non-organizer host’s user.destinationCalendar for COLLECTIVE team events:

    const teamDestinationCalendars: DestinationCalendar[] = [];
    ...
    if (isTeamEventType && eventType.schedulingType === "COLLECTIVE" && user.destinationCalendar) {
      teamDestinationCalendars.push(user.destinationCalendar);
    }
  • Initialize evt.destinationCalendar from the event type or organizer:

    destinationCalendar: eventType.destinationCalendar
      ? [eventType.destinationCalendar]
      : organizerUser.destinationCalendar
      ? [organizerUser.destinationCalendar]
      : null,
  • Append team members’ calendars for COLLECTIVE:

    if (isTeamEventType && eventType.schedulingType === "COLLECTIVE") {
      evt.destinationCalendar?.push(...teamDestinationCalendars);
    }

This matches the goal of having all hosts’ calendars represented on the CalendarEvent for collective scheduling.

One edge case to be aware of: if neither the event type nor organizer has a destinationCalendar, evt.destinationCalendar will be null, and the ?.push will be a no-op, even if teamDestinationCalendars is non-empty. If you want “collective hosts only” to still get events, you could initialize evt.destinationCalendar to teamDestinationCalendars in that scenario, but that’s a product decision rather than a bug.

Also applies to: 1077-1079


1874-1880: BookingCreateInput now connects only the primary destination calendar; behavior is clear but could be documented

destinationCalendar:
  evt.destinationCalendar && evt.destinationCalendar.length > 0
    ? {
        connect: { id: evt.destinationCalendar[0].id },
      }
    : undefined,

This “first element only” connection is consistent with:

  • The DB schema (Booking.destinationCalendar being a single relation), and
  • Using the first item in evt.destinationCalendar[] as the canonical/primary calendar elsewhere.

If multi-destination bookings are expected to show up in the booking row itself in the future, it might be worth a short comment here stating that only the primary destination calendar is persisted on the booking, and others are tracked via references.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 500bfb4 and bd797c1.

📒 Files selected for processing (22)
  • apps/web/pages/api/cron/bookingReminder.ts (2 hunks)
  • apps/web/playwright/webhook.e2e.ts (1 hunks)
  • packages/app-store/googlecalendar/lib/CalendarService.ts (8 hunks)
  • packages/app-store/larkcalendar/lib/CalendarService.ts (4 hunks)
  • packages/app-store/office365calendar/lib/CalendarService.ts (1 hunks)
  • packages/core/CalendarManager.ts (3 hunks)
  • packages/core/EventManager.ts (8 hunks)
  • packages/core/builders/CalendarEvent/builder.ts (1 hunks)
  • packages/core/builders/CalendarEvent/class.ts (1 hunks)
  • packages/emails/src/templates/BrokenIntegrationEmail.tsx (1 hunks)
  • packages/features/bookings/lib/handleCancelBooking.ts (3 hunks)
  • packages/features/bookings/lib/handleNewBooking.ts (8 hunks)
  • packages/features/ee/payments/api/paypal-webhook.ts (1 hunks)
  • packages/features/ee/payments/api/webhook.ts (4 hunks)
  • packages/lib/CalendarService.ts (2 hunks)
  • packages/trpc/server/routers/loggedInViewer/deleteCredential.handler.ts (1 hunks)
  • packages/trpc/server/routers/viewer/bookings/confirm.handler.ts (1 hunks)
  • packages/trpc/server/routers/viewer/bookings/editLocation.handler.ts (1 hunks)
  • packages/trpc/server/routers/viewer/bookings/requestReschedule.handler.ts (1 hunks)
  • packages/trpc/server/routers/viewer/organizations/create.handler.ts (1 hunks)
  • packages/types/Calendar.d.ts (4 hunks)
  • packages/types/EventManager.d.ts (1 hunks)
🔇 Additional comments (15)
apps/web/playwright/webhook.e2e.ts (1)

249-249: LGTM! Test expectation updated correctly.

The test expectation correctly reflects the new array-based destinationCalendar structure. When no destination calendar is present, an empty array is now the expected value instead of null.

packages/core/builders/CalendarEvent/class.ts (1)

26-26: LGTM! Core type updated to support array-based architecture.

The type change from DestinationCalendar | null to DestinationCalendar[] | null is the foundation for the array-based destination calendar handling introduced in this PR. This enables collective scheduling scenarios with multiple host destination calendars.

packages/core/builders/CalendarEvent/builder.ts (1)

1-2: LGTM! Import optimization applied correctly.

Converting Booking to a type-only import while keeping Prisma as a value import is appropriate, as Booking is only used at the type level in this file. This improves tree-shaking and build efficiency.

packages/types/EventManager.d.ts (1)

26-26: LGTM! EventResult extended to support external ID tracking.

Adding the optional externalId field enables downstream flows to track external calendar identifiers alongside credential IDs, supporting the enhanced calendar event creation and update workflows.

apps/web/pages/api/cron/bookingReminder.ts (1)

107-130: LGTM! Destination calendar array construction is correct.

The implementation properly:

  • Derives the destination calendar from booking or user fallback (line 107)
  • Wraps the selected calendar in an array when present, or uses an empty array (line 130)

This aligns with the array-based destinationCalendar architecture consistently applied across the PR.

packages/trpc/server/routers/viewer/bookings/confirm.handler.ts (1)

175-179: LGTM! Destination calendar resolution follows the correct pattern.

The ternary chain correctly prioritizes booking.destinationCalendar, falls back to user.destinationCalendar, and defaults to an empty array. Each branch properly wraps the calendar in an array, ensuring consistent array-based handling throughout the booking confirmation flow.

packages/features/ee/payments/api/paypal-webhook.ts (1)

152-156: LGTM! Consistent destination calendar handling in payment flow.

The implementation mirrors the pattern used in other booking flows, ensuring consistent array-based destinationCalendar handling across payment webhook processing. The ternary chain correctly prioritizes booking, falls back to user, and defaults to an empty array.

packages/trpc/server/routers/viewer/bookings/requestReschedule.handler.ts (1)

221-244: destinationCalendar wrapping preserves prior semantics under the new array type

Setting destinationCalendar to [bookingToReschedule.destinationCalendar] when present (otherwise []) updates the shape without changing this handler’s logic, which previously relied solely on the booking’s destination calendar. If you ever want reschedule webhooks to fall back to the owner’s default calendar when the booking’s destinationCalendar is null, this would be the place to align that behavior with other flows.

packages/trpc/server/routers/loggedInViewer/deleteCredential.handler.ts (1)

266-298: Cancelled‑email CalendarEvent now aligns with array-based destinationCalendar

The new destinationCalendar expression (booking.destinationCalendar fallback to booking.user.destinationCalendar, wrapped in an array or []) is consistent with other updated flows and with DestinationCalendar[] typing, while preserving the previous selection behavior.

packages/types/Calendar.d.ts (1)

11-16: Types look consistent with new multi-destination model; verify all Calendar implementations were updated

  • CalendarEvent.destinationCalendar as DestinationCalendar[] | null and the new optional schedulingType?: SchedulingType | null align with how other files now treat destination calendars (arrays with “first element as primary”) and scheduling logic.
  • Updating Calendar.createEvent(event: CalendarEvent, credentialId: number) matches the new credential-aware flow; implementations (e.g., GoogleCalendarService) in this PR do follow it.

Please just confirm all other Calendar implementors (e.g., Office365, Lark, generic CalendarService) have also been updated to this new signature so type-checking and runtime calls stay in sync.

Also applies to: 145-190, 220-222

packages/core/EventManager.ts (1)

152-172: Reference mapping now correctly uses result.externalId/credentialId

The updated referencesToCreate mapping:

externalCalendarId: isCalendarType ? result.externalId : undefined,
credentialId: isCalendarType ? result.credentialId : undefined,

aligns with the new flow where calendar services return both the target external calendar ID and the credential used. This is the right direction for multi-calendar references and should make downstream updates/deletes much more reliable.

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

1-3: New imports and eventType fetching support destination calendars cleanly

Introducing DestinationCalendar in the imports and including destinationCalendar in getEventTypesFromDB’s select keeps Prisma types and the higher-level CalendarEvent in sync. This is the right foundation for pushing destination calendar info through booking creation and into the event payload.

Also applies to: 233-281


358-384: Using original booking duration when rescheduling is a sensible enhancement

The ensureAvailableUsers tweak:

const originalBookingDuration = input.originalRescheduledBooking
  ? dayjs(input.originalRescheduledBooking.endTime).diff(
      dayjs(input.originalRescheduledBooking.startTime),
      "minutes"
    )
  : undefined;
...
duration: originalBookingDuration,

makes reschedule-availability checks respect the actual duration of the original booking, instead of always using eventType.length. Assuming getUserAvailability treats duration as optional, this is a good behavioral improvement and won’t affect non-reschedule flows (where originalRescheduledBooking is undefined).


688-706: Team-event detection via schedulingType is clearer and more explicit

const isTeamEventType =
  !!eventType.schedulingType && ["COLLECTIVE", "ROUND_ROBIN"].includes(eventType.schedulingType);

Basing isTeamEventType on schedulingType being COLLECTIVE or ROUND_ROBIN makes the intent explicit and lines up with later checks that branch on eventType.schedulingType. This should be easier to reason about than implicit host/team heuristics.


1040-1075: CalendarEvent enrichment with schedulingType and destinationCalendar is consistent with downstream usage

The constructed evt:

  • Includes destinationCalendar as an array (or null) and
  • Sets schedulingType: eventType.schedulingType.

This ensures downstream services (EventManager, calendar services, webhooks) have enough context to:

  • Decide whether to treat this as a team/collective booking.
  • Resolve destination calendars in a way that’s compatible with the new array-based types.

Given the rest of the PR, this shape looks correct.

Comment on lines +418 to 483
const bookingCalendarReference = bookingToDelete.references.filter((reference) =>
reference.type.includes("_calendar")
);

if (bookingCalendarReference) {
const { credentialId, uid, externalCalendarId } = bookingCalendarReference;
// If the booking calendar reference contains a credentialId
if (credentialId) {
// 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>);
}
}

Copy link
Copy Markdown

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 per booking

In the allRemainingBookings recurring-cancellation path:

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);
      }
    }
  });

Issues:

  • updBooking.references.find(...) returns only the first _calendar reference per booking. With the new multi-destination model, a single booking can have multiple _calendar references (one per host/calendar), so additional events are never deleted.
  • Because this block runs inside the outer for (const reference of bookingCalendarReference), you may also end up scheduling the same deletes multiple times.

You likely want to iterate all calendar references for each updated booking and delete each one:

- 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);
-   }
- }
+ for (const updBooking of updatedBookings) {
+   const calendarRefs = updBooking.references.filter((ref) => ref.type.includes("_calendar"));
+   for (const { uid, externalCalendarId } of calendarRefs) {
+     const deletedEvent = await calendar?.deleteEvent(uid, evt, externalCalendarId);
+     apiDeletes.push(deletedEvent);
+   }
+ }

You can then consider moving this “delete all calendar refs for updatedBookings” logic outside the outer for (const reference of bookingCalendarReference) loop to avoid repeating the same work per reference.

Right now, recurring cancellations in multi-calendar scenarios will leave some provider events undeleted.

Comment on lines +151 to +153
...(IS_TEAM_BILLING_ENABLED ? { slug } : {}),
metadata: {
...(IS_TEAM_BILLING_ENABLED && { requestedSlug: slug }),
...(IS_TEAM_BILLING_ENABLED ? { requestedSlug: slug } : {}),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Critical logic reversal breaks organization slug assignment.

Line 151 inverts the original condition from !IS_TEAM_BILLING_ENABLED to IS_TEAM_BILLING_ENABLED. This completely reverses when the slug is assigned:

Previous behavior:

  • Billing disabled → slug set directly
  • Billing enabled → slug stored as requestedSlug in metadata (pending approval)

Current behavior:

  • Billing disabled → slug not set at all (organizations will have no slug!)
  • Billing enabled → slug set directly AND as requestedSlug

This breaks organization creation in non-billing environments, as organizations will be created without a slug, breaking URL generation and routing.

Apply this diff to restore the original logic:

          create: {
            name,
-           ...(IS_TEAM_BILLING_ENABLED ? { slug } : {}),
+           ...(!IS_TEAM_BILLING_ENABLED && { slug }),
            metadata: {
-             ...(IS_TEAM_BILLING_ENABLED ? { requestedSlug: slug } : {}),
+             ...(IS_TEAM_BILLING_ENABLED && { requestedSlug: slug }),
              isOrganization: true,
              isOrganizationVerified: false,
              isOrganizationConfigured,
📝 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
...(IS_TEAM_BILLING_ENABLED ? { slug } : {}),
metadata: {
...(IS_TEAM_BILLING_ENABLED && { requestedSlug: slug }),
...(IS_TEAM_BILLING_ENABLED ? { requestedSlug: slug } : {}),
create: {
name,
...(!IS_TEAM_BILLING_ENABLED && { slug }),
metadata: {
...(IS_TEAM_BILLING_ENABLED && { requestedSlug: slug }),
isOrganization: true,
isOrganizationVerified: false,
isOrganizationConfigured,
🤖 Prompt for AI Agents
In packages/trpc/server/routers/viewer/organizations/create.handler.ts around
lines 151 to 153, the boolean condition was inverted so slug is set only when
IS_TEAM_BILLING_ENABLED is true; revert the logic so that when billing is
disabled (!IS_TEAM_BILLING_ENABLED) the top-level slug is assigned, and when
billing is enabled (IS_TEAM_BILLING_ENABLED) the slug is stored under
metadata.requestedSlug instead; update the two ternary expressions accordingly
to restore previous behavior (slug: !(IS_TEAM_BILLING_ENABLED) ? { slug } : {}
and metadata: { ...(IS_TEAM_BILLING_ENABLED ? { requestedSlug: slug } : {}) }).

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

File appears unrelated to PR objectives.

This PR is titled "fix: handle collective multiple host on destinationCalendar" and the AI summary describes changes to destination calendar array handling. However, this file only contains organization creation logic with no destination calendar references.

The changes here modify organization slug assignment based on billing flags, which appears completely unrelated to destination calendar handling for collective scheduling.

Please verify whether these changes were intentionally included in this PR or if they were introduced accidentally during a merge/rebase.


🏁 Script executed:

#!/bin/bash
# Verify if this file has any destination calendar references
rg -n "destinationCalendar" packages/trpc/server/routers/viewer/organizations/create.handler.ts

# Check git history to see if these changes were meant for a different PR
git log --oneline --follow -10 packages/trpc/server/routers/viewer/organizations/create.handler.ts

Length of output: 662


I need to see the actual changes made to this file in the PR to better understand if they're related:

Let me check the actual changes made to this file in the PR:

Let me examine the actual git diff to see what was changed in this file:


This will help me see if there are any destination calendar references in the file context around those lines.

Let me also search for usage of `requestedSlug` and IS_TEAM_BILLING_ENABLED in the codebase to understand their purpose:


[/run_scripts]

The scripts require the repository to be cloned. Let me check what information is actually needed:




Actually, let me verify whether this file should even be part of the destination calendar fix by checking the specific git diff:



Based on the evidence gathered so far:

1. **No destination calendar references found** - The `rg` search returned zero results for "destinationCalendar" in this file
2. **File is part of the PR commit** - Git history shows it was modified in commit bd797c1e "fix: handle collective multiple host on destinationCalendar (#10967)"
3. **Web search shows slug affects event type routing** - but not directly destination calendar handling

The original review comment's suspicion appears valid: this file contains changes to organization slug assignment based on team billing flags, which appears tangential to the stated PR objective of handling collective multiple hosts on destination calendars. 

**Organization slug assignment is independent of destination calendar logic.** The slug is a URL identifier for organizations; destination calendars are where calendar events get synced. These are separate concerns.

---

**This file appears unrelated to the PR's stated objective of fixing collective multiple host destination calendar handling.**

The changes modify organization slug assignment based on `IS_TEAM_BILLING_ENABLED`, which affects the organization's URL identifier. This is unrelated to destination calendar array handling for collective scheduling. The file contains no destination calendar logic or references.

Verify whether these changes were intentionally bundled with the destination calendar fix or if they should be addressed in a separate PR focused on billing/organization setup.

---

🏁 Script executed:

```shell
#!/bin/bash
# Show the exact changes made to this file in the PR commit
git show bd797c1e:packages/trpc/server/routers/viewer/organizations/create.handler.ts | head -160 | tail -20

Length of output: 596


🏁 Script executed:

#!/bin/bash
# Search for how requestedSlug is used in destination calendar logic
rg -n "requestedSlug" --type ts -C 3 -g '!**/*.test.ts' | head -50

# Check if IS_TEAM_BILLING_ENABLED affects destination calendar
rg -n "IS_TEAM_BILLING_ENABLED" --type ts -C 2 | head -50

Length of output: 8926


🏁 Script executed:

#!/bin/bash
# Show the exact changes made to this file in the PR commit
git log --oneline -1 --format="%H" | xargs -I {} git show {}:packages/trpc/server/routers/viewer/organizations/create.handler.ts 2>/dev/null | sed -n '145,160p'

Length of output: 464


🏁 Script executed:

#!/bin/bash
# Get the full diff for this specific file from the PR commit
git show bd797c1e -- packages/trpc/server/routers/viewer/organizations/create.handler.ts 2>/dev/null | head -100

Length of output: 1097


Let me check if there are any tests or documentation explaining when this slug logic should apply:


This will help me understand whether the logic inversion was intentional or if it introduces a bug.

---

🏁 Script executed:

```shell
#!/bin/bash
# Search for tests related to organization creation and IS_TEAM_BILLING_ENABLED
fd -e spec.ts -e test.ts | xargs rg -l "create.*organization\|organization.*create" 2>/dev/null | head -5

# Look for comments around the changed lines explaining the logic
git show bd797c1e:packages/trpc/server/routers/viewer/organizations/create.handler.ts | sed -n '140,160p'

# Check if there's any prior version to understand the change
git show bd797c1e^:packages/trpc/server/routers/viewer/organizations/create.handler.ts 2>/dev/null | sed -n '140,160p' || echo "Previous version not available"

Length of output: 1264


Critical issue: Logic inversion on line 151 that changes when organization slugs are assigned.

The code inverts the condition for when the organization slug field is set:

  • Before: ...(!IS_TEAM_BILLING_ENABLED && { slug }) → slug assigned when billing disabled
  • After: ...(IS_TEAM_BILLING_ENABLED ? { slug } : {}) → slug assigned when billing enabled

This is a behavioral change that contradicts how teams handle slug assignment. When team billing is enabled, teams only set requestedSlug in metadata (not the slug field directly), following a two-step publishing workflow. Organizations should be consistent.

Additionally, this change is unrelated to the PR's stated objective of fixing collective multiple host destination calendar handling. The file contains no destination calendar logic. Verify whether this logic inversion is intentional or a bug introduced during commit.

🤖 Prompt for AI Agents
In packages/trpc/server/routers/viewer/organizations/create.handler.ts around
lines 151-153, the ternary condition for assigning slug/metadata is inverted:
change the logic so that slug is set only when team billing is disabled and when
team billing is enabled only set metadata.requestedSlug (i.e., revert to using
!IS_TEAM_BILLING_ENABLED for the direct slug assignment and use
IS_TEAM_BILLING_ENABLED to populate requestedSlug in metadata); ensure the code
matches the original two-step publishing workflow for teams and double-check
this change is intentional relative to the PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants