Skip to content

Commit 4a8db7b

Browse files
committed
feat(reschedule): check guest availability when host reschedules
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
1 parent 17af50b commit 4a8db7b

File tree

3 files changed

+88
-0
lines changed

3 files changed

+88
-0
lines changed

packages/features/availability/lib/getUserAvailability.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,12 @@ export type GetUserAvailabilityInitialData = {
148148
})[];
149149
busyTimesFromLimitsBookings?: EventBusyDetails[];
150150
busyTimesFromLimits?: Map<number, EventBusyDetails[]>;
151+
/**
152+
* Busy times of guests (attendees who are not the host) from the booking being rescheduled.
153+
* Injected by the slots service when `rescheduleUid` is present so that the availability
154+
* calculation blocks any slot that conflicts with a guest's existing accepted booking.
155+
*/
156+
guestBusyTimes?: EventBusyDetails[];
151157
eventTypeForLimits?: {
152158
id: number;
153159
bookingLimits?: unknown;
@@ -622,6 +628,8 @@ export class UserAvailabilityService {
622628
})),
623629
...busyTimesFromLimits,
624630
...busyTimesFromTeamLimits,
631+
// Guest busy times from the booking being rescheduled: prevents double-booking guests.
632+
...(initialData?.guestBusyTimes ?? []),
625633
];
626634

627635
log.debug(

packages/features/bookings/repositories/BookingRepository.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2220,4 +2220,45 @@ export class BookingRepository implements IBookingRepository {
22202220
},
22212221
});
22222222
}
2223+
2224+
/**
2225+
* Returns accepted bookings whose attendee list contains at least one of the given emails,
2226+
* within the provided date range and excluding the booking with `excludedUid` (the booking
2227+
* being rescheduled). Used to surface guest conflicts when a host reschedules.
2228+
*/
2229+
async getAcceptedBookingsByAttendeeEmails({
2230+
attendeeEmails,
2231+
startDate,
2232+
endDate,
2233+
excludedUid,
2234+
}: {
2235+
attendeeEmails: string[];
2236+
startDate: Date;
2237+
endDate: Date;
2238+
excludedUid?: string | null;
2239+
}) {
2240+
if (attendeeEmails.length === 0) return [];
2241+
return this.prismaClient.booking.findMany({
2242+
where: {
2243+
status: BookingStatus.ACCEPTED,
2244+
// Use overlap semantics: any booking whose time range intersects [startDate, endDate].
2245+
startTime: { lt: endDate },
2246+
endTime: { gt: startDate },
2247+
...(excludedUid ? { uid: { not: excludedUid } } : {}),
2248+
attendees: {
2249+
some: {
2250+
email: { in: attendeeEmails },
2251+
},
2252+
},
2253+
},
2254+
select: {
2255+
id: true,
2256+
startTime: true,
2257+
endTime: true,
2258+
title: true,
2259+
uid: true,
2260+
userId: true,
2261+
},
2262+
});
2263+
}
22232264
}

packages/trpc/server/routers/viewer/slots/util.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -839,6 +839,44 @@ export class AvailableSlotsService {
839839
const allUserIds = Array.from(userIdAndEmailMap.keys());
840840

841841
const bookingRepo = this.dependencies.bookingRepo;
842+
843+
// When rescheduling, collect accepted bookings of all guests (attendees who are not
844+
// the host) so we can block slots that would double-book them.
845+
let guestBusyTimes: EventBusyDetails[] = [];
846+
if (input.rescheduleUid) {
847+
const rescheduleBooking = await bookingRepo.findByUidIncludeEventTypeAttendeesAndUser({
848+
bookingUid: input.rescheduleUid,
849+
});
850+
if (rescheduleBooking) {
851+
// Host emails: the booking's user + any event-type host emails
852+
const hostEmails = new Set<string>();
853+
if (rescheduleBooking.user?.email) hostEmails.add(rescheduleBooking.user.email);
854+
rescheduleBooking.eventType?.hosts?.forEach((h) => {
855+
if (h.user.email) hostEmails.add(h.user.email);
856+
});
857+
858+
// Guest emails = attendees that are NOT hosts
859+
const guestEmails = rescheduleBooking.attendees
860+
.map((a) => a.email)
861+
.filter((email) => !hostEmails.has(email));
862+
863+
if (guestEmails.length > 0) {
864+
const guestBookings = await bookingRepo.getAcceptedBookingsByAttendeeEmails({
865+
attendeeEmails: guestEmails,
866+
startDate: startTimeDate,
867+
endDate: endTimeDate,
868+
excludedUid: input.rescheduleUid,
869+
});
870+
guestBusyTimes = guestBookings.map((b) => ({
871+
start: b.startTime.toISOString(),
872+
end: b.endTime.toISOString(),
873+
title: b.title ?? undefined,
874+
source: "guest",
875+
}));
876+
}
877+
}
878+
}
879+
842880
const [currentBookingsAllUsers, outOfOfficeDaysAllUsers] = await Promise.all([
843881
bookingRepo.findAllExistingBookingsForEventTypeBetween({
844882
startDate: startTimeDate,
@@ -962,6 +1000,7 @@ export class AvailableSlotsService {
9621000
eventTypeForLimits: eventType && (bookingLimits || durationLimits) ? eventType : null,
9631001
teamBookingLimits: teamBookingLimitsMap,
9641002
teamForBookingLimits: teamForBookingLimits,
1003+
guestBusyTimes,
9651004
},
9661005
});
9671006
/* We get all users working hours and busy slots */

0 commit comments

Comments
 (0)