2023-02-16 22:39:57 +00:00
|
|
|
import type { User } from "@prisma/client";
|
2022-08-15 19:52:01 +00:00
|
|
|
|
|
|
|
import prisma from "@calcom/prisma";
|
|
|
|
|
2023-07-20 21:51:15 +00:00
|
|
|
async function leastRecentlyBookedUser<T extends Pick<User, "id" | "email">>({
|
2022-08-15 19:52:01 +00:00
|
|
|
availableUsers,
|
|
|
|
eventTypeId,
|
|
|
|
}: {
|
|
|
|
availableUsers: T[];
|
|
|
|
eventTypeId: number;
|
|
|
|
}) {
|
2023-07-20 21:51:15 +00:00
|
|
|
// First we get all organizers (fixed host/single round robin user)
|
|
|
|
const organizersWithLastCreated = await prisma.user.findMany({
|
2022-08-15 19:52:01 +00:00
|
|
|
where: {
|
|
|
|
id: {
|
|
|
|
in: availableUsers.map((user) => user.id),
|
|
|
|
},
|
|
|
|
},
|
|
|
|
select: {
|
|
|
|
id: true,
|
|
|
|
bookings: {
|
|
|
|
select: {
|
|
|
|
createdAt: true,
|
|
|
|
},
|
|
|
|
where: {
|
|
|
|
eventTypeId,
|
|
|
|
},
|
|
|
|
orderBy: {
|
|
|
|
createdAt: "desc",
|
|
|
|
},
|
|
|
|
take: 1,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
2023-07-20 21:51:15 +00:00
|
|
|
const organizerIdAndAtCreatedPair = organizersWithLastCreated.reduce(
|
|
|
|
(keyValuePair: { [userId: number]: Date }, user) => {
|
2023-01-10 16:06:53 +00:00
|
|
|
keyValuePair[user.id] = user.bookings[0]?.createdAt || new Date(0);
|
2022-08-15 19:52:01 +00:00
|
|
|
return keyValuePair;
|
|
|
|
},
|
|
|
|
{}
|
|
|
|
);
|
|
|
|
|
2023-07-20 21:51:15 +00:00
|
|
|
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.
|
|
|
|
}
|
|
|
|
|
2022-08-15 19:52:01 +00:00
|
|
|
const leastRecentlyBookedUser = availableUsers.sort((a, b) => {
|
|
|
|
return userIdAndAtCreatedPair[a.id] > userIdAndAtCreatedPair[b.id] ? 1 : -1;
|
|
|
|
})[0];
|
|
|
|
|
|
|
|
return leastRecentlyBookedUser;
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: Configure distributionAlgorithm from the event type configuration
|
|
|
|
// TODO: Add 'MAXIMIZE_FAIRNESS' algorithm.
|
2023-07-20 21:51:15 +00:00
|
|
|
export async function getLuckyUser<T extends Pick<User, "id" | "email">>(
|
2022-08-15 19:52:01 +00:00
|
|
|
distributionAlgorithm: "MAXIMIZE_AVAILABILITY" = "MAXIMIZE_AVAILABILITY",
|
|
|
|
{ availableUsers, eventTypeId }: { availableUsers: T[]; eventTypeId: number }
|
|
|
|
) {
|
|
|
|
if (availableUsers.length === 1) {
|
|
|
|
return availableUsers[0];
|
|
|
|
}
|
|
|
|
switch (distributionAlgorithm) {
|
|
|
|
case "MAXIMIZE_AVAILABILITY":
|
|
|
|
return leastRecentlyBookedUser<T>({ availableUsers, eventTypeId });
|
|
|
|
}
|
|
|
|
}
|