Files
calendar/packages/lib/server/getLuckyUser.ts
T
e3861ad22a feat: No-show bookings - do not count towards a member's round robin assignment (#15323)
* feat: No-show bookings - do not count towards a member's round robin assignment

* filter no show organizer

* remove logs

* fix: filter bookings where host attendee is no show

* fix: use not condition for noShow

* chore: test

* add default false for noShowHost

* Revert "chore: test"

This reverts commit c914a8fc4a14ca9c7d8c4a08ff8b0f463789171c.

* chore: integration test

* fix: noShowHost not working for null

* Update packages/lib/server/getLuckyUser.ts

* chore: update test description

---------

Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: gh0st91848 <gh0st91848@gmail.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
2024-07-29 16:05:03 +03:00

148 lines
4.3 KiB
TypeScript

import type { User } from "@prisma/client";
import prisma from "@calcom/prisma";
async function leastRecentlyBookedUser<T extends Pick<User, "id" | "email">>({
availableUsers,
eventTypeId,
}: {
availableUsers: T[];
eventTypeId: number;
}) {
// 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),
},
},
select: {
id: true,
bookings: {
select: {
createdAt: true,
},
where: {
eventTypeId,
attendees: {
some: {
noShow: false,
},
},
// not:true won't match null, thus we need to do an OR with null case separately(for bookings that might have null value for `noShowHost` as earlier it didn't have default false)
// https://github.com/calcom/cal.com/pull/15323#discussion_r1687728207
OR: [
{
noShowHost: false,
},
{
noShowHost: null,
},
],
},
orderBy: {
createdAt: "desc",
},
take: 1,
},
},
});
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),
},
noShow: false,
},
},
},
],
},
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;
if (organizerIdAndAtCreatedPair[user.id] > booking.createdAt) return; // only consider bookings if they were created after organizer bookings
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) => {
if (userIdAndAtCreatedPair[a.id] > userIdAndAtCreatedPair[b.id]) return 1;
else if (userIdAndAtCreatedPair[a.id] < userIdAndAtCreatedPair[b.id]) return -1;
// if two (or more) dates are identical, we randomize the order
else return Math.random() > 0.5 ? 1 : -1;
})[0];
return leastRecentlyBookedUser;
}
function getUsersWithHighestPriority<T extends Pick<User, "id" | "email"> & { priority?: number | null }>({
availableUsers,
}: {
availableUsers: T[];
}) {
const highestPriority = Math.max(...availableUsers.map((user) => user.priority ?? 2));
return availableUsers.filter(
(user) => user.priority === highestPriority || (user.priority == null && highestPriority === 2)
);
}
// TODO: Configure distributionAlgorithm from the event type configuration
// TODO: Add 'MAXIMIZE_FAIRNESS' algorithm.
export async function getLuckyUser<T extends Pick<User, "id" | "email"> & { priority?: number | null }>(
distributionAlgorithm: "MAXIMIZE_AVAILABILITY" = "MAXIMIZE_AVAILABILITY",
{ availableUsers, eventTypeId }: { availableUsers: T[]; eventTypeId: number }
) {
if (availableUsers.length === 1) {
return availableUsers[0];
}
switch (distributionAlgorithm) {
case "MAXIMIZE_AVAILABILITY":
const highestPriorityUsers = getUsersWithHighestPriority({ availableUsers });
return leastRecentlyBookedUser<T>({ availableUsers: highestPriorityUsers, eventTypeId });
}
}