From 856b36ae1fd7b0f2b09eb5b87db14f56d224709a Mon Sep 17 00:00:00 2001 From: 285729101 <285729101@qq.com> Date: Tue, 17 Feb 2026 21:19:41 +0800 Subject: [PATCH] feat: take into account guest availability when rescheduling When a host reschedules a booking, the system now checks if any attendees/guests are also Cal.com users (by looking up their email). If a guest is a Cal.com user, their availability (busy times from calendar events and bookings) is fetched and used to filter out time slots where the guest would be unavailable. This ensures that only slots where ALL participants (host + Cal.com user guests) are available are shown during rescheduling. Fixes calcom/cal.com#16378 --- .../repositories/BookingRepository.ts | 19 ++++ .../users/repositories/UserRepository.ts | 23 +++++ .../trpc/server/routers/viewer/slots/util.ts | 96 +++++++++++++++++++ 3 files changed, 138 insertions(+) diff --git a/packages/features/bookings/repositories/BookingRepository.ts b/packages/features/bookings/repositories/BookingRepository.ts index cbf6a66896adcf..ef1bccacf6ca2b 100644 --- a/packages/features/bookings/repositories/BookingRepository.ts +++ b/packages/features/bookings/repositories/BookingRepository.ts @@ -2189,4 +2189,23 @@ export class BookingRepository implements IBookingRepository { }, }); } + + /** + * Finds attendee emails for a booking by its UID. + * Used during rescheduling to check guest availability. + */ + async findAttendeeEmailsByUid({ bookingUid }: { bookingUid: string }) { + const booking = await this.prismaClient.booking.findUnique({ + where: { uid: bookingUid }, + select: { + userId: true, + attendees: { + select: { + email: true, + }, + }, + }, + }); + return booking; + } } diff --git a/packages/features/users/repositories/UserRepository.ts b/packages/features/users/repositories/UserRepository.ts index 2cb3e61ffb264f..f4f966c9efc03e 100644 --- a/packages/features/users/repositories/UserRepository.ts +++ b/packages/features/users/repositories/UserRepository.ts @@ -1456,4 +1456,27 @@ export class UserRepository { return { email: user.email, username: user.username }; } + + /** + * Finds Cal.com users by their email addresses, returning data needed for availability checking. + * Used during rescheduling to check guest availability. + */ + async findManyByEmailsForAvailability({ emails }: { emails: string[] }) { + if (!emails.length) return []; + const normalizedEmails = emails.map((e) => e.toLowerCase()); + + return this.prismaClient.user.findMany({ + where: { + email: { in: normalizedEmails }, + emailVerified: { not: null }, + locked: false, + }, + select: { + ...availabilityUserSelect, + credentials: { + select: credentialForCalendarServiceSelect, + }, + }, + }); + } } diff --git a/packages/trpc/server/routers/viewer/slots/util.ts b/packages/trpc/server/routers/viewer/slots/util.ts index 0260b13d8bf0a3..b77de917f303f2 100644 --- a/packages/trpc/server/routers/viewer/slots/util.ts +++ b/packages/trpc/server/routers/viewer/slots/util.ts @@ -1033,6 +1033,88 @@ export class AvailableSlotsService { "getRegularOrDynamicEventType" ); + /** + * When a host reschedules a booking, checks if any attendees/guests are Cal.com users + * and filters out time slots where those guests are busy. + * This ensures only mutually available times are shown during rescheduling. + */ + private async filterSlotsByGuestAvailability({ + rescheduleUid, + availableTimeSlots, + eventLength, + startTime, + endTime, + eventTypeId, + loggerWithEventDetails, + }: { + rescheduleUid: string; + availableTimeSlots: { time: Dayjs; userIds?: number[] }[]; + eventLength: number; + startTime: Dayjs; + endTime: Dayjs; + eventTypeId: number; + loggerWithEventDetails: Logger; + }) { + const bookingRepo = this.dependencies.bookingRepo; + const booking = await bookingRepo.findAttendeeEmailsByUid({ bookingUid: rescheduleUid }); + + if (!booking?.attendees?.length) { + return availableTimeSlots; + } + + const attendeeEmails = booking.attendees.map((a) => a.email); + + const userRepo = this.dependencies.userRepo; + const guestCalUsers = await userRepo.findManyByEmailsForAvailability({ emails: attendeeEmails }); + + if (!guestCalUsers.length) { + return availableTimeSlots; + } + + loggerWithEventDetails.info("Found Cal.com user guests for reschedule availability check", { + guestCount: guestCalUsers.length, + guestEmails: guestCalUsers.map((u) => u.email), + }); + + const guestUsersWithCalendars = guestCalUsers.map((user) => withSelectedCalendars(user)); + + // Get availability for each guest Cal.com user + const guestAvailabilities = await this.dependencies.userAvailabilityService.getUsersAvailability({ + users: guestUsersWithCalendars, + query: { + dateFrom: startTime.format(), + dateTo: endTime.format(), + eventTypeId, + afterEventBuffer: 0, + beforeEventBuffer: 0, + duration: 0, + returnDateOverrides: false, + bypassBusyCalendarTimes: false, + }, + initialData: { + currentSeats: undefined, + rescheduleUid, + busyTimesFromLimitsBookings: [], + }, + }); + + // Collect all busy times from all guest Cal.com users + const allGuestBusyTimes: EventBusyDate[] = guestAvailabilities.flatMap((availability) => availability.busy); + + if (!allGuestBusyTimes.length) { + return availableTimeSlots; + } + + // Filter out slots that conflict with any guest's busy times + return availableTimeSlots.filter((slot) => { + return !checkForConflicts({ + time: slot.time, + busy: allGuestBusyTimes, + eventLength, + }); + }); + } + getAvailableSlots = withReporting( withSlotsCache(this.dependencies.redisClient, this._getAvailableSlots.bind(this)), "getAvailableSlots" @@ -1406,6 +1488,20 @@ export class AvailableSlotsService { ); } + // When rescheduling, check if any attendees/guests are Cal.com users + // and filter out slots where they are busy + if (input.rescheduleUid) { + availableTimeSlots = await this.filterSlotsByGuestAvailability({ + rescheduleUid: input.rescheduleUid, + availableTimeSlots, + eventLength: input.duration || eventType.length, + startTime, + endTime, + eventTypeId: eventType.id, + loggerWithEventDetails, + }); + } + // fr-CA uses YYYY-MM-DD const formatter = new Intl.DateTimeFormat("fr-CA", { year: "numeric",