feat(reschedule): check guest availability when host reschedules#28229
feat(reschedule): check guest availability when host reschedules#28229sirrodgepodge wants to merge 1 commit intocalcom:mainfrom
Conversation
When a host reschedules a booking, the slot picker now filters out time slots that conflict with the guests' (attendees') existing accepted bookings — preventing double-booking guests. Changes: - BookingRepository: add getAcceptedBookingsByAttendeeEmails() to query bookings where any attendee email appears in a given list, within a date range, excluding the booking being rescheduled - AvailableSlotsService.calculateHostsAndAvailabilities(): when rescheduleUid is present, look up the original booking's attendees, filter out host emails to get guest-only emails, fetch their accepted bookings in the slot search window, and pass them as guestBusyTimes - GetUserAvailabilityInitialData: add optional guestBusyTimes field - getUserAvailability(): spread guestBusyTimes into detailedBusyTimes so guest conflicts are treated as busy time during availability calculation Fixes calcom#16378
Graphite Automations"Send notification to Community team when bounty PR opened" took an action on this PR • (03/01/26)2 teammates were notified to this PR based on Keith Williams's automation. |
There was a problem hiding this comment.
Pull request overview
Adds guest-conflict awareness to the reschedule slot picker so hosts can’t reschedule into times where non-host attendees already have an accepted booking.
Changes:
- Add
BookingRepository.getAcceptedBookingsByAttendeeEmails()to fetch accepted bookings for a set of attendee emails in a time window (with overlap + excluded booking semantics). - In slots availability calculation, fetch the original booking’s guest emails on reschedule and inject their accepted bookings as
guestBusyTimes. - Extend
getUserAvailabilityinitialData to acceptguestBusyTimesand merge them intodetailedBusyTimesto block conflicting slots.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 6 comments.
| File | Description |
|---|---|
packages/trpc/server/routers/viewer/slots/util.ts |
Collect guest attendee emails during reschedule and pass guest-derived busy intervals into availability calculation. |
packages/features/bookings/repositories/BookingRepository.ts |
New Prisma query to find accepted bookings by attendee emails with overlap/exclusion filtering. |
packages/features/availability/lib/getUserAvailability.ts |
Accept guestBusyTimes in initialData and include them in busy-time subtraction. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const rescheduleBooking = await bookingRepo.findByUidIncludeEventTypeAttendeesAndUser({ | ||
| bookingUid: input.rescheduleUid, | ||
| }); | ||
| if (rescheduleBooking) { |
There was a problem hiding this comment.
getSchedule is a public endpoint, and this code uses the client-provided rescheduleUid to look up a booking and then query other accepted bookings for the booking’s attendee emails. Without an explicit authorization/gating check (e.g., only run when the requester is the booking owner/host), this can become a privacy leak (inferring attendees’ busy times) if a rescheduleUid is disclosed. Consider gating guest-conflict lookups to authenticated host reschedules and/or verifying the requester is allowed to access the booking before querying attendee-based conflicts.
| if (rescheduleBooking) { | |
| // Only consider guest conflicts if the reschedule booking belongs to one of the | |
| // current hosts/users participating in this scheduling flow. This prevents | |
| // unauthorized callers from probing guest availability using arbitrary UIDs. | |
| if (rescheduleBooking && rescheduleBooking.userId && allUserIds.includes(rescheduleBooking.userId)) { |
| guestBusyTimes = guestBookings.map((b) => ({ | ||
| start: b.startTime.toISOString(), | ||
| end: b.endTime.toISOString(), | ||
| title: b.title ?? undefined, |
There was a problem hiding this comment.
guestBusyTimes is populated with title from bookings that belong to the guests. This will flow into getUserAvailability's busy output and debug logs, and may expose private meeting titles unrelated to the host/reschedule flow. Consider omitting the title (or replacing with a generic label) for guest-derived busy times.
| title: b.title ?? undefined, | |
| // Do not leak guest-owned meeting titles; use a generic label instead. | |
| title: "Busy", |
| // Guest emails = attendees that are NOT hosts | ||
| const guestEmails = rescheduleBooking.attendees | ||
| .map((a) => a.email) | ||
| .filter((email) => !hostEmails.has(email)); | ||
|
|
||
| if (guestEmails.length > 0) { | ||
| const guestBookings = await bookingRepo.getAcceptedBookingsByAttendeeEmails({ | ||
| attendeeEmails: guestEmails, | ||
| startDate: startTimeDate, |
There was a problem hiding this comment.
guestEmails can contain duplicates (and potentially empty strings, depending on upstream data). Deduplicating/filtering before calling getAcceptedBookingsByAttendeeEmails will reduce the size of the SQL IN (...) list and avoid unnecessary DB work.
| busyTimesFromLimits: busyTimesFromLimitsMap, | ||
| eventTypeForLimits: eventType && (bookingLimits || durationLimits) ? eventType : null, | ||
| teamBookingLimits: teamBookingLimitsMap, | ||
| teamForBookingLimits: teamForBookingLimits, | ||
| guestBusyTimes, | ||
| }, |
There was a problem hiding this comment.
guestBusyTimes is passed via initialData and then appended into every host's detailedBusyTimes. For multi-host event types this duplicates the same guest busy intervals across all users (and into debug logs), which can inflate memory/CPU for large teams or wide date windows. Consider applying guest conflicts once at the aggregated availability stage (global blocker) or otherwise avoiding per-user duplication.
| async getAcceptedBookingsByAttendeeEmails({ | ||
| attendeeEmails, | ||
| startDate, | ||
| endDate, | ||
| excludedUid, | ||
| }: { | ||
| attendeeEmails: string[]; | ||
| startDate: Date; | ||
| endDate: Date; | ||
| excludedUid?: string | null; | ||
| }) { | ||
| if (attendeeEmails.length === 0) return []; | ||
| return this.prismaClient.booking.findMany({ | ||
| where: { | ||
| status: BookingStatus.ACCEPTED, | ||
| // Use overlap semantics: any booking whose time range intersects [startDate, endDate]. | ||
| startTime: { lt: endDate }, | ||
| endTime: { gt: startDate }, | ||
| ...(excludedUid ? { uid: { not: excludedUid } } : {}), | ||
| attendees: { | ||
| some: { | ||
| email: { in: attendeeEmails }, | ||
| }, | ||
| }, | ||
| }, | ||
| select: { | ||
| id: true, | ||
| startTime: true, | ||
| endTime: true, | ||
| title: true, | ||
| uid: true, | ||
| userId: true, | ||
| }, | ||
| }); |
There was a problem hiding this comment.
This new query method introduces important overlap/exclusion semantics but doesn't appear to be covered by repository tests. Since BookingRepository.integration-test.ts already sets up real bookings/attendees, adding coverage for overlap boundaries (touching endpoints), multiple emails, and excludedUid would help prevent regressions.
| const detailedBusyTimes: EventBusyDetails[] = [ | ||
| ...busyTimes.map((a) => ({ | ||
| ...a, | ||
| start: dayjs(a.start).toISOString(), | ||
| end: dayjs(a.end).toISOString(), | ||
| title: a.title, | ||
| source: params.withSource ? a.source : undefined, | ||
| })), | ||
| ...busyTimesFromLimits, | ||
| ...busyTimesFromTeamLimits, | ||
| // Guest busy times from the booking being rescheduled: prevents double-booking guests. | ||
| ...(initialData?.guestBusyTimes ?? []), | ||
| ]; |
There was a problem hiding this comment.
guestBusyTimes is now merged into detailedBusyTimes, but there are no tests asserting that guest conflicts actually remove slots during rescheduling. Given the existing test suite around availability calculations, adding a unit test that injects initialData.guestBusyTimes and verifies dateRanges subtraction/slot filtering would improve confidence in this behavior.
|
Hi team, I'd like to contribute. Please assign! |
Hey @Moses-main stop commenting on PRs, you can pick up any issue form https://github.com/calcom/cal.com/issues and raise a PR for fixing that issue, avoid issue with "needs approval" label. |
|
Hi @romitg2, you're absolutely right! Thanks for the guidance. I'll directly pick up an open issue, implement the fix, and create a PR instead of asking to be assigned. Going forward, I'll:
Appreciate the feedback! 🙏 |
When a host reschedules a booking, the slot picker now filters out time slots that conflict with the guests' (attendees') existing accepted bookings — preventing double-booking guests.
Changes
packages/features/bookings/repositories/BookingRepository.tsgetAcceptedBookingsByAttendeeEmails()to query bookings where any attendee email appears in a given list, within a date range, excluding the booking being rescheduledstartTime < windowEnd AND endTime > windowStart) for correct boundary detectionpackages/trpc/server/routers/viewer/slots/util.tscalculateHostsAndAvailabilities(): whenrescheduleUidis present, look up the original booking's attendees viaBookingRepositoryguestBusyTimesthroughinitialDatapackages/features/availability/lib/getUserAvailability.tsguestBusyTimesfield toGetUserAvailabilityInitialDataguestBusyTimesintodetailedBusyTimesso guest conflicts block slot availabilityHow it works
getSchedulewithrescheduleUidFixes #16378
/claim #28164