fix: handle collective multiple host on destinationCalendar - #8
Conversation
There was a problem hiding this comment.
🛡️ re-entry.ai Code Review
@CodingKylo — Please review the following assessment:
🚨 Risk Score: 83/100 · CRITICAL
| Dimension | Level |
|---|---|
| Likelihood | 🔴 Critical |
| Impact | 🔴 Critical |
| Detectability | 🟡 Medium |
Intent
Update booking/event handling to support multiple destination calendars for collective/team bookings, and ensure calendar integrations (Google/Lark/Office365) and reminder emails use the correct destination calendar.
Summary
The PR changes CalendarEvent.destinationCalendar from a single destination calendar shape to an array, and updates some integrations/reminder cron to wrap or select a “main host” destination calendar. However, multiple call sites still persist or consume only destinationCalendar[0], which can silently drop additional destination calendars for collective bookings. There is also a contract mismatch risk: GoogleCalendarService.createEvent now takes credentialId, but the call chain and the selection logic must be aligned with the same destination calendar used for the integration. You should verify that every booking flow (create/cancel/reschedule/reminders) normalizes destinationCalendar consistently and that the integration selection uses the correct credentialId/calendar pair.
🎯 Review Focus
Verify end-to-end contract consistency for CalendarEvent.destinationCalendar (array vs scalar) across: booking creation persistence (handleNewBooking), cancellation/reschedule flows, reminder cron (bookingReminder), and each calendar integration’s selection logic (especially Google’s new createEvent(calEventRaw, credentialId) path).
Key Findings
- 🚨 [packages/app-store/googlecalendar/lib/CalendarService.ts:L84] CRITICAL:
createEventsignature changed to requirecredentialId, but the service also derivesmainHostDestinationCalendarfromcalEventRaw.destinationCalendarand then selects organizer email/calendar from that element. If the caller passes a credentialId that doesn’t correspond to the same destination calendar chosen asmainHostDestinationCalendar, you can create events in the wrong host calendar or with the wrong organizer identity. Fix: make the selection deterministic from the same inputs: use the passedcredentialIdto pick the destination calendar (and error if not found), e.g.const mainHostDestinationCalendar = calEventRaw.destinationCalendar?.find(c => c.credentialId === credentialId); if (!mainHostDestinationCalendar) throw ...;and then use onlymainHostDestinationCalendarfor organizer email and calendar selection.
✅ Action Checklist
- [ ] CRITICAL [packages/app-store/googlecalendar/lib/CalendarService.ts:L84] —
createEventsignature changed to requirecredentialId, but the service also derivesmainHostDestinationCalendarfromcalEventRaw.destinationCalendarand then selects organizer email/calendar from that element. If the caller passes a credentialId that doesn’t correspond to the same destination calendar chosen asmainHostDestinationCalendar, you can create events in the wrong host calendar or with the wrong organizer identity. Fix: make the selection deterministic from the same inputs: use the passedcredentialIdto pick the destination calendar (and error if not found), e.g.const mainHostDestinationCalendar = calEventRaw.destinationCalendar?.find(c => c.credentialId === credentialId); if (!mainHostDestinationCalendar) throw ...;and then use onlymainHostDestinationCalendarfor organizer email and calendar selection. - [ ] SUGGESTION — Normalize at the boundary and delete ad-hoc shape handling. For example, in
apps/web/pages/api/cron/bookingReminder.ts:L104-L127, replacebooking.destinationCalendar || user.destinationCalendarwith a normalization helper used everywhere:const destinationCalendars = normalizeDestinationCalendars(booking.destinationCalendar, user.destinationCalendar);then setdestinationCalendar: destinationCalendars;. Apply the same helper in all booking flows (new/cancel/reschedule) and in all integration services. - [ ] SUGGESTION — Fix the systemic “main host only” workaround consistently. In
packages/app-store/larkcalendar/lib/CalendarService.ts:L125-L189andpackages/app-store/office365calendar/lib/CalendarService.ts:L70-L70, you doconst [mainHostDestinationCalendar] = event.destinationCalendar ?? [];. If the product requirement is to create events for multiple destination calendars, these services must iterate overevent.destinationCalendarand create/update/delete per destination calendar. If the requirement is truly “only one host calendar is supported for creation,” then enforce it explicitly by validatingevent.destinationCalendar.length <= 1(or selecting by credentialId) and logging/throwing when multiple are provided. - [ ] SUGGESTION — In
packages/app-store/googlecalendar/lib/CalendarService.ts:L138-L142(truncated in diff), ensure theselectedCalendarlookup is actually implemented correctly and uses the passedcredentialId(not justexternalId). Add a hard failure when no matching destination calendar is found for the credentialId to prevent silent misrouting. - [ ] SUGGESTION — In
packages/features/bookings/lib/handleNewBooking.tsaround theisTeamEventTypechange, verify thateventType.schedulingTypeis always present for the relevant event types. The new logic!!eventType.schedulingType && ["COLLECTIVE", "ROUND_ROBIN"].includes(eventType.schedulingType)changes behavior for unexpected/undefined scheduling types; add a test covering the previous behavior to avoid regressions in collective booking detection.
Suggestions
- Normalize at the boundary and delete ad-hoc shape handling. For example, in
apps/web/pages/api/cron/bookingReminder.ts:L104-L127, replacebooking.destinationCalendar || user.destinationCalendarwith a normalization helper used everywhere:const destinationCalendars = normalizeDestinationCalendars(booking.destinationCalendar, user.destinationCalendar);then setdestinationCalendar: destinationCalendars;. Apply the same helper in all booking flows (new/cancel/reschedule) and in all integration services. - Fix the systemic “main host only” workaround consistently. In
packages/app-store/larkcalendar/lib/CalendarService.ts:L125-L189andpackages/app-store/office365calendar/lib/CalendarService.ts:L70-L70, you doconst [mainHostDestinationCalendar] = event.destinationCalendar ?? [];. If the product requirement is to create events for multiple destination calendars, these services must iterate overevent.destinationCalendarand create/update/delete per destination calendar. If the requirement is truly “only one host calendar is supported for creation,” then enforce it explicitly by validatingevent.destinationCalendar.length <= 1(or selecting by credentialId) and logging/throwing when multiple are provided. - In
packages/app-store/googlecalendar/lib/CalendarService.ts:L138-L142(truncated in diff), ensure theselectedCalendarlookup is actually implemented correctly and uses the passedcredentialId(not justexternalId). Add a hard failure when no matching destination calendar is found for the credentialId to prevent silent misrouting. - In
packages/features/bookings/lib/handleNewBooking.tsaround theisTeamEventTypechange, verify thateventType.schedulingTypeis always present for the relevant event types. The new logic!!eventType.schedulingType && ["COLLECTIVE", "ROUND_ROBIN"].includes(eventType.schedulingType)changes behavior for unexpected/undefined scheduling types; add a test covering the previous behavior to avoid regressions in collective booking detection.
📝 This review includes 3 inline comments (3 warnings)
Posted by re-entry.ai · Risk governance for autonomous engineering teams
| @@ -138,13 +142,16 @@ export default class GoogleCalendarService implements Calendar { | |||
| const calendar = google.calendar({ | |||
There was a problem hiding this comment.
The new lookup only works when destinationCalendar is an array and a matching credentialId exists, but the fallback path no longer defaults to `
| @@ -1843,11 +1871,12 @@ async function handler( | |||
| id: organizerUser.id, | |||
There was a problem hiding this comment.
This persists only evt.destinationCalendar[0] even though the diff changes CalendarEvent.destinationCalendar to an array. For collective/team bookings with multiple destination calendars, the booking record will now lose the additional calendars and downstream cancellation/update flows will only know about the first one. The root cause is that the persistence layer still assumes a single destination calendar while the event model has been widened to many.
| @@ -127,7 +127,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) | |||
| attendees: attendeesList, | |||
There was a problem hiding this comment.
destinationCalendar is now wrapped in an array, but this endpoint previously passed a single calendar object. If sendOrganizerRequestReminderEmail or its template logic still expects a single DestinationCalendar, this changes the contract and can break calendar-name/integration rendering. The root cause is an API shape change without a corresponding consumer update.
There was a problem hiding this comment.
🛡️ re-entry.ai Code Review
@CodingKylo — Please review the following assessment:
🚨 Risk Score: 83/100 · CRITICAL
| Dimension | Level |
|---|---|
| Likelihood | 🔴 Critical |
| Impact | 🔴 Critical |
| Detectability | 🟡 Medium |
Intent
Update booking/event creation and calendar integrations to support multiple destination calendars per event, and fix host selection when multiple calendars are present.
Summary
The PR changes the destinationCalendar shape to support multiple destination calendars and updates several calendar services/entrypoints to select a “main host” calendar (often via array index 0). The highest risk is correctness: multiple layers now derive organizer email and calendar IDs from the first element or from a credentialId match, which can silently pick the wrong calendar if ordering/selection is inconsistent. There are also likely runtime issues around nullish calendars and credential lookup/usage that can cause missed deletions or failed updates. Reviewers must verify that all call sites pass a consistent DestinationCalendar[] payload and that every integration deterministically selects the intended primary calendar (not positional ordering).
🎯 Review Focus
Verify deterministic primary destination-calendar selection end-to-end (cron/webhook -> CalendarEvent payload -> each calendar integration) and eliminate any reliance on destinationCalendar[0] unless it is explicitly guaranteed by contract; ensure organizer email/calendarId/credential mapping all use the same selection logic.
Key Findings
- 🚨 [packages/app-store/googlecalendar/lib/CalendarService.ts:L97] CRITICAL:
createEventnow takescredentialIdbut the code still derives organizer email and calendar selection fromdestinationCalendarwithout a guaranteed match. Excerpt:const [mainHostDestinationCalendar] = calEventRaw?.destinationCalendar && calEventRaw?.destinationCalendar.length > 0 ? calEventRaw.destinationCalendar : [];andemail: mainHostDestinationCalendar?.externalId ? mainHostDestinationCalendar.externalId : calEventRaw.organizer.email,— This will silently use the first destination calendar even if it does not correspond to the provided credentialId, causing events to be created under the wrong organizer identity/calendar. Fix: deterministically select the calendar bycredentialId(or an explicitisPrimary/priority field) and use that selection for both organizer email and calendarId. Example:const main = calEventRaw.destinationCalendar?.find(c => c.credentialId === credentialId) ?? calEventRaw.destinationCalendar?.[0];and then usemainfor organizer email and calendar selection; if no match, throw a 4xx/5xx with actionable logging rather than falling back to index 0. ⚠️ [apps/web/pages/api/cron/bookingReminder.ts:L104] WARNING: Cron reminder buildsdestinationCalendaras an array but does not ensure it matches the intended credential/primary host. Excerpt:const selectedDestinationCalendar = booking.destinationCalendar || user.destinationCalendar;anddestinationCalendar: selectedDestinationCalendar ? [selectedDestinationCalendar] : [],— Ifbooking.destinationCalendaris already aDestinationCalendar[](new contract) or ifuser.destinationCalendaris not the correct primary for the booking, this will produce a nested/incorrect shape or pick the wrong host. Fix: normalize based on the actual type and select the primary deterministically. Example:const dest = booking.destinationCalendar ?? user.destinationCalendar; const destinationCalendars = Array.isArray(dest) ? dest : dest ? [dest] : []; const primary = destinationCalendars.find(c => c.credentialId === booking.credentialId) ?? destinationCalendars[0]; evt.destinationCalendar = primary ? [primary] : [];(and ensurebooking.credentialIdexists in this handler).
✅ Action Checklist
- [ ] CRITICAL [packages/app-store/googlecalendar/lib/CalendarService.ts:L97] —
createEventnow takescredentialIdbut the code still derives organizer email and calendar selection fromdestinationCalendarwithout a guaranteed match. Excerpt:const [mainHostDestinationCalendar] = calEventRaw?.destinationCalendar && calEventRaw?.destinationCalendar.length > 0 ? calEventRaw.destinationCalendar : [];andemail: mainHostDestinationCalendar?.externalId ? mainHostDestinationCalendar.externalId : calEventRaw.organizer.email,— This will silently use the first destination calendar even if it does not correspond to the provided credentialId, causing events to be created under the wrong organizer identity/calendar. Fix: deterministically select the calendar bycredentialId(or an explicitisPrimary/priority field) and use that selection for both organizer email and calendarId. Example:const main = calEventRaw.destinationCalendar?.find(c => c.credentialId === credentialId) ?? calEventRaw.destinationCalendar?.[0];and then usemainfor organizer email and calendar selection; if no match, throw a 4xx/5xx with actionable logging rather than falling back to index 0. - [ ] WARNING [apps/web/pages/api/cron/bookingReminder.ts:L104] — Cron reminder builds
destinationCalendaras an array but does not ensure it matches the intended credential/primary host. Excerpt:const selectedDestinationCalendar = booking.destinationCalendar || user.destinationCalendar;anddestinationCalendar: selectedDestinationCalendar ? [selectedDestinationCalendar] : [],— Ifbooking.destinationCalendaris already aDestinationCalendar[](new contract) or ifuser.destinationCalendaris not the correct primary for the booking, this will produce a nested/incorrect shape or pick the wrong host. Fix: normalize based on the actual type and select the primary deterministically. Example:const dest = booking.destinationCalendar ?? user.destinationCalendar; const destinationCalendars = Array.isArray(dest) ? dest : dest ? [dest] : []; const primary = destinationCalendars.find(c => c.credentialId === booking.credentialId) ?? destinationCalendars[0]; evt.destinationCalendar = primary ? [primary] : [];(and ensurebooking.credentialIdexists in this handler). - [ ] SUGGESTION — Centralize destination-calendar normalization to avoid drift across handlers/services. Add a helper like
normalizeDestinationCalendars(input): DestinationCalendar[]andselectPrimaryDestinationCalendar(destinationCalendars, credentialId): DestinationCalendar | undefined, then use it in all booking flows and in each calendar service. Example touchpoints:apps/web/pages/api/cron/bookingReminder.tsandpackages/app-store/*/lib/CalendarService.ts(where you currently doconst [mainHostDestinationCalendar] = ...). - [ ] SUGGESTION — Remove positional semantics (
destinationCalendar[0]) from all integrations. Inpackages/app-store/larkcalendar/lib/CalendarService.tsandpackages/app-store/office365calendar/lib/CalendarService.ts, you currently doconst [mainHostDestinationCalendar] = event.destinationCalendar ?? [];and then use it forcalendarId. Fix by selecting bycredentialId(or explicit primary marker) and throw if not found, rather than defaulting to index 0. - [ ] SUGGESTION — In
packages/app-store/googlecalendar/lib/CalendarService.ts, ensure the newcredentialIdparameter is actually used for selection. Right now the excerpt shows a fallback to first element and then a truncatedfindby credentialId. Fix by using thefindresult as the single source of truth for both organizer email and calendar selection; do not computemainHostDestinationCalendarfrom index 0 at all. - [ ] SUGGESTION — Add runtime shape validation at API boundaries where
destinationCalendaris constructed (e.g., cron handler). Ensure you never accidentally convertDestinationCalendar[]into[DestinationCalendar[]]or otherwise nest arrays. Example: checkArray.isArray(booking.destinationCalendar)before wrapping.
Suggestions
- Centralize destination-calendar normalization to avoid drift across handlers/services. Add a helper like
normalizeDestinationCalendars(input): DestinationCalendar[]andselectPrimaryDestinationCalendar(destinationCalendars, credentialId): DestinationCalendar | undefined, then use it in all booking flows and in each calendar service. Example touchpoints:apps/web/pages/api/cron/bookingReminder.tsandpackages/app-store/*/lib/CalendarService.ts(where you currently doconst [mainHostDestinationCalendar] = ...). - Remove positional semantics (
destinationCalendar[0]) from all integrations. Inpackages/app-store/larkcalendar/lib/CalendarService.tsandpackages/app-store/office365calendar/lib/CalendarService.ts, you currently doconst [mainHostDestinationCalendar] = event.destinationCalendar ?? [];and then use it forcalendarId. Fix by selecting bycredentialId(or explicit primary marker) and throw if not found, rather than defaulting to index 0. - In
packages/app-store/googlecalendar/lib/CalendarService.ts, ensure the newcredentialIdparameter is actually used for selection. Right now the excerpt shows a fallback to first element and then a truncatedfindby credentialId. Fix by using thefindresult as the single source of truth for both organizer email and calendar selection; do not computemainHostDestinationCalendarfrom index 0 at all. - Add runtime shape validation at API boundaries where
destinationCalendaris constructed (e.g., cron handler). Ensure you never accidentally convertDestinationCalendar[]into[DestinationCalendar[]]or otherwise nest arrays. Example: checkArray.isArray(booking.destinationCalendar)before wrapping.
📝 This review includes 3 inline comments (3 warnings)
Posted by re-entry.ai · Risk governance for autonomous engineering teams
| @@ -97,6 +97,10 @@ export default class GoogleCalendarService implements Calendar { | |||
| responseStatus: "accepted", | |||
There was a problem hiding this comment.
Valid concern: this code now treats destinationCalendar as an array and blindly takes the first entry as the “main host” calendar. That only works if callers always preserve host ordering; if the array is reordered or contains multiple calendars, the organizer email and selected calendar can be derived from the wrong host. The root cause is that the new array shape encodes multiple calendars but this service still relies on positional semantics instead of an explicit host/primary marker.
| } 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") |
There was a problem hiding this comment.
This is a real correctness issue if calendar can be nullish: calendar?.deleteEvent(...) yields undefined, and the cast to Promise<unknown> does not make it awaitable. Pushing undefined into apiDeletes will later break Promise.all/async handling or silently skip the deletion. The underlying problem is that the code assumes calendar lookup always succeeds, but the new multi-calendar flow makes that assumption less safe.
| @@ -468,33 +493,62 @@ export default class EventManager { | |||
| } | |||
There was a problem hiding this comment.
The reviewer’s symptom is real only if the credential lookup can fail. In this diff, credential is still obtained via a filtered lookup and then passed into updateEvent without a guard, so a missing match would propagate undefined into calendar updates. The root cause is that the new destinationCalendar/credential mapping is not validated before use, so the event manager can still attempt updates with an unresolved credential.
Martian Code Review Benchmark PR (mirrored from source #4)