Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions packages/features/bookings/repositories/BookingRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1303,6 +1303,93 @@ export class BookingRepository implements IBookingRepository {
];
}

/**
* Finds all accepted bookings for a team within a date range.
* This method queries by eventType.teamId (indexed) instead of attendee emails,
* providing better performance for large teams.
*
* The caller is responsible for filtering results by user involvement
* (as organizer or attendee) in application code.
*/
async findByTeamIdAndDateRangeIncludeAttendees({
teamId,
startDate,
endDate,
excludedUid,
includeManagedEvents,
}: {
teamId: number;
startDate: Date;
endDate: Date;
excludedUid?: string | null;
includeManagedEvents: boolean;
}) {
const baseWhere: Prisma.BookingWhereInput = {
status: BookingStatus.ACCEPTED,
startTime: {
gte: startDate,
},
endTime: {
lte: endDate,
},
...(excludedUid && {
uid: {
not: excludedUid,
},
}),
};

const whereTeamEventTypes: Prisma.BookingWhereInput = {
...baseWhere,
eventType: {
teamId,
},
};

const whereManagedEventTypes: Prisma.BookingWhereInput = {
...baseWhere,
eventType: {
parent: {
teamId,
},
},
};

const select = {
id: true,
startTime: true,
endTime: true,
title: true,
eventTypeId: true,
userId: true,
attendees: {
select: {
email: true,
},
},
};

if (!includeManagedEvents) {
return this.prismaClient.booking.findMany({
where: whereTeamEventTypes,
select,
});
}

const [teamBookings, managedBookings] = await Promise.all([
this.prismaClient.booking.findMany({
where: whereTeamEventTypes,
select,
}),
this.prismaClient.booking.findMany({
where: whereManagedEventTypes,
select,
}),
]);

return [...teamBookings, ...managedBookings];
}

async getValidBookingFromEventTypeForAttendee({
eventTypeId,
bookerEmail,
Expand Down
22 changes: 19 additions & 3 deletions packages/trpc/server/routers/viewer/slots/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -634,16 +634,32 @@ export class AvailableSlotsService {
);

const bookingRepo = this.dependencies.bookingRepo;
const bookings = await bookingRepo.getAllAcceptedTeamBookingsOfUsers({
users,

// Query all team bookings by eventType.teamId (indexed) instead of attendee emails
// Then filter in application code to find bookings where user is organizer or attendee
const allTeamBookings = await bookingRepo.findByTeamIdAndDateRangeIncludeAttendees({
teamId,
startDate: limitDateFrom.toDate(),
endDate: limitDateTo.toDate(),
excludedUid: rescheduleUid,
includeManagedEvents,
});

const busyTimes = bookings.map(({ id, startTime, endTime, eventTypeId, title, userId }) => ({
// Build a Set of user emails for O(1) lookup
const userEmailSet = new Set(users.map((user) => user.email));
const userIdSet = new Set(users.map((user) => user.id));

// Filter bookings where user is either the organizer (userId) or an attendee (email match)
const relevantBookings = allTeamBookings.filter((booking) => {
// Check if user is the organizer
if (booking.userId && userIdSet.has(booking.userId)) {
return true;
}
// Check if any user is an attendee
return booking.attendees.some((attendee) => userEmailSet.has(attendee.email));
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The new attendee-email filter applies to all bookings (including managed events), but the previous query only counted managed bookings when the organizer userId matched. This changes booking-limit behavior for managed events and can over-count limits. Consider distinguishing managed bookings and only applying attendee matching to non-managed bookings to preserve prior behavior.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/trpc/server/routers/viewer/slots/util.ts, line 659:

<comment>The new attendee-email filter applies to all bookings (including managed events), but the previous query only counted managed bookings when the organizer userId matched. This changes booking-limit behavior for managed events and can over-count limits. Consider distinguishing managed bookings and only applying attendee matching to non-managed bookings to preserve prior behavior.</comment>

<file context>
@@ -634,16 +634,32 @@ export class AvailableSlotsService {
+        return true;
+      }
+      // Check if any user is an attendee
+      return booking.attendees.some((attendee) => userEmailSet.has(attendee.email));
+    });
+
</file context>
Fix with Cubic

});

const busyTimes = relevantBookings.map(({ id, startTime, endTime, eventTypeId, title, userId }) => ({
start: dayjs(startTime).toDate(),
end: dayjs(endTime).toDate(),
title,
Expand Down
Loading