diff --git a/apps/web/test/lib/team-event-types.test.ts b/apps/web/test/lib/team-event-types.test.ts index 84275f4c42..4e5506f5c0 100644 --- a/apps/web/test/lib/team-event-types.test.ts +++ b/apps/web/test/lib/team-event-types.test.ts @@ -33,6 +33,7 @@ it("can find lucky user with maximize availability", async () => { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore prismaMock.user.findMany.mockResolvedValue(users); + prismaMock.booking.findMany.mockResolvedValue([]); await expect( getLuckyUser("MAXIMIZE_AVAILABILITY", { diff --git a/packages/lib/server/getLuckyUser.ts b/packages/lib/server/getLuckyUser.ts index 5d0be1969d..bfc798b555 100644 --- a/packages/lib/server/getLuckyUser.ts +++ b/packages/lib/server/getLuckyUser.ts @@ -2,14 +2,15 @@ import type { User } from "@prisma/client"; import prisma from "@calcom/prisma"; -async function leastRecentlyBookedUser>({ +async function leastRecentlyBookedUser>({ availableUsers, eventTypeId, }: { availableUsers: T[]; eventTypeId: number; }) { - const usersWithLastCreated = await prisma.user.findMany({ + // First we get all organizers (fixed host/single round robin user) + const organizersWithLastCreated = await prisma.user.findMany({ where: { id: { in: availableUsers.map((user) => user.id), @@ -32,18 +33,64 @@ async function leastRecentlyBookedUser>({ }, }); - if (!usersWithLastCreated) { - throw new Error("Unable to find users by availableUser ids."); // should never happen. - } - - const userIdAndAtCreatedPair = usersWithLastCreated.reduce( - (keyValuePair: { [key: number]: Date }, user) => { + const organizerIdAndAtCreatedPair = organizersWithLastCreated.reduce( + (keyValuePair: { [userId: number]: Date }, user) => { keyValuePair[user.id] = user.bookings[0]?.createdAt || new Date(0); return keyValuePair; }, {} ); + const bookings = await prisma.booking.findMany({ + where: { + AND: [ + { + eventTypeId, + }, + { + attendees: { + some: { + email: { + in: availableUsers.map((user) => user.email), + }, + }, + }, + }, + ], + }, + select: { + id: true, + createdAt: true, + attendees: { + select: { + email: true, + }, + }, + }, + orderBy: { + createdAt: "desc", + }, + }); + + const attendeeUserIdAndAtCreatedPair = bookings.reduce((aggregate: { [userId: number]: Date }, booking) => { + availableUsers.forEach((user) => { + if (aggregate[user.id]) return; // Bookings are ordered DESC, so if the reducer aggregate + // contains the user id, it's already got the most recent booking marked. + if (!booking.attendees.map((attendee) => attendee.email).includes(user.email)) return; + aggregate[user.id] = booking.createdAt; + }); + return aggregate; + }, {}); + + const userIdAndAtCreatedPair = { + ...organizerIdAndAtCreatedPair, + ...attendeeUserIdAndAtCreatedPair, + }; + + if (!userIdAndAtCreatedPair) { + throw new Error("Unable to find users by availableUser ids."); // should never happen. + } + const leastRecentlyBookedUser = availableUsers.sort((a, b) => { return userIdAndAtCreatedPair[a.id] > userIdAndAtCreatedPair[b.id] ? 1 : -1; })[0]; @@ -53,7 +100,7 @@ async function leastRecentlyBookedUser>({ // TODO: Configure distributionAlgorithm from the event type configuration // TODO: Add 'MAXIMIZE_FAIRNESS' algorithm. -export async function getLuckyUser>( +export async function getLuckyUser>( distributionAlgorithm: "MAXIMIZE_AVAILABILITY" = "MAXIMIZE_AVAILABILITY", { availableUsers, eventTypeId }: { availableUsers: T[]; eventTypeId: number } ) {