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",