Files
calendar/packages/lib/server/getLuckyUser.integration-test.ts
T
Benny JooandGitHub 96468c4083 refactor: move @calcom/lib/di folder to @calcom/features (#24199)
* mv di folder

* update imports

* fix

* fix

* fix test
2025-10-02 08:12:06 -03:00

801 lines
26 KiB
TypeScript

import { describe, it, vi, expect, afterEach, beforeEach, beforeAll, afterAll } from "vitest";
import { getLuckyUserService } from "@calcom/features/di/containers/LuckyUser";
import prisma from "@calcom/prisma";
const luckyUserService = getLuckyUserService();
let commonEventTypeId: number;
const userIds: number[] = [];
async function deleteUsers() {
await prisma.user.deleteMany({
where: {
id: {
in: userIds,
},
},
});
userIds.splice(0, userIds.length);
}
beforeAll(async () => {
const event = await prisma.eventType.create({
data: {
title: "Test Event",
slug: "test-event",
length: 15,
},
select: {
id: true,
},
});
commonEventTypeId = event.id;
});
beforeEach(() => {
// tell vitest we use mocked time
vi.useFakeTimers();
});
afterEach(async () => {
await deleteUsers();
vi.useRealTimers();
});
afterAll(async () => {
await prisma.eventType.delete({
where: {
id: commonEventTypeId,
},
});
});
type BookingPropsRelatedToLuckyUserAlgorithm = {
eventTypeId: number;
createdAt: Date;
noShowHost?: boolean | null;
};
type OptionalBookingProps = {
uid?: string;
title?: string;
startTime?: Date;
endTime?: Date;
attendees?: {
create: {
name: string;
email: string;
timeZone: string;
noShow?: boolean | null;
}[];
};
};
type UserProps = {
email: string;
createdDate?: Date;
};
const commonBookingData = {
startTime: new Date(),
endTime: new Date(),
};
const commonAttendeesData = [
{
name: "test-attendee",
email: "test-attendee@example.com",
timeZone: "Asia/Calcutta",
},
];
const createUserWithBookings = async ({
user: { email, createdDate },
bookings,
}: {
user: UserProps;
bookings: (BookingPropsRelatedToLuckyUserAlgorithm & OptionalBookingProps)[];
}) => {
const user = await prisma.user.create({
data: {
email,
createdDate,
bookings: {
create: bookings.map(({ eventTypeId, ...booking }, index) => ({
...commonBookingData,
attendees: {
create: commonAttendeesData,
},
uid: `uuid-${email}-booking${index + 1}`,
title: `${email} Booking ${index + 1}`,
eventTypeId,
...booking,
})),
},
},
include: {
bookings: true,
credentials: true,
},
});
userIds.push(user.id);
return user;
};
const createHostWithBookings = async ({
user: userData,
bookings,
priority = 2,
weight = null,
createdAt,
}: {
user: UserProps;
bookings: (BookingPropsRelatedToLuckyUserAlgorithm & OptionalBookingProps)[];
weight?: number | null;
priority?: number;
createdAt?: Date;
}) => {
const user = await createUserWithBookings({
user: {
createdDate: userData.createdDate || new Date(),
email: userData.email,
},
bookings,
});
const host = await prisma.host.create({
data: {
user: { connect: { id: user.id } },
eventType: { connect: { id: commonEventTypeId } },
weight,
priority,
createdAt: createdAt ?? new Date(),
},
include: {
user: {
include: {
bookings: true,
credentials: true,
},
},
},
});
return {
...host,
user: {
...host.user,
priority,
weight,
},
};
};
function expectLuckyUsers(luckyUsers: { email: string }[], expectedLuckyUsers: { email: string }[]) {
expect(luckyUsers.map((user) => user.email)).toEqual(expectedLuckyUsers.map((user) => user.email));
}
describe("getLuckyUser Integration tests", () => {
describe("should not consider no show bookings for round robin: ", () => {
it("When a host is no show, that is chosen when competing with another host that showed up for the booking", async () => {
const createOrganizerThatShowedUp = async (email: string) => {
return createHostWithBookings({
user: { email },
bookings: [{ eventTypeId: commonEventTypeId, createdAt: new Date("2022-01-25T05:30:00.000Z") }],
});
};
const createOrganizerThatDidntShowUp = async (email: string) => {
return createHostWithBookings({
user: { email },
bookings: [
{
eventTypeId: commonEventTypeId,
createdAt: new Date("2022-01-25T06:30:00.000Z"),
noShowHost: true,
},
],
});
};
const organizerHostThatShowedUp = await createOrganizerThatShowedUp("test-user1@example.com");
const organizerHostThatDidntShowUp = await createOrganizerThatDidntShowUp("test-user2@example.com");
const organizerThatShowedUp = organizerHostThatShowedUp.user;
const organizerThatDidntShowUp = organizerHostThatDidntShowUp.user;
const luckyUser = await luckyUserService.getLuckyUser({
availableUsers: [organizerThatShowedUp, organizerThatDidntShowUp],
eventType: {
id: commonEventTypeId,
isRRWeightsEnabled: false,
team: {},
},
allRRHosts: [],
routingFormResponse: null,
});
expect(luckyUser.email).toBe(organizerThatDidntShowUp.email);
});
it("When a attendee is a noShow for organizers booking, that organizer is competing with another host whose attendee showed up for the booking", async () => {
const organizerHostWhoseAttendeeShowedUp = await createHostWithBookings({
user: { email: "test-user1@example.com" },
bookings: [
{
eventTypeId: commonEventTypeId,
createdAt: new Date("2022-01-25T05:30:00.000Z"),
title: "Test User 1 Booking",
attendees: {
create: [
{
name: "test-attendee",
email: "test-attendee@example.com",
timeZone: "Asia/Calcutta",
},
],
},
},
],
});
const organizerWhoseAttendeeShowedUp = organizerHostWhoseAttendeeShowedUp.user;
const organizerHostWhoseAttendeeDidntShowUp = await createHostWithBookings({
user: { email: "test-user2@example.com" },
bookings: [
{
eventTypeId: commonEventTypeId,
createdAt: new Date("2022-01-25T06:30:00.000Z"),
attendees: {
create: [
{
noShow: true,
name: "test-attendee",
email: "test-attendee@example.com",
timeZone: "Asia/Calcutta",
},
],
},
},
],
});
const organizerWhoseAttendeeDidntShowUp = organizerHostWhoseAttendeeDidntShowUp.user;
expect(
luckyUserService.getLuckyUser({
availableUsers: [organizerWhoseAttendeeShowedUp, organizerWhoseAttendeeDidntShowUp],
eventType: {
id: commonEventTypeId,
isRRWeightsEnabled: false,
team: {},
},
allRRHosts: [],
routingFormResponse: null,
})
).resolves.toStrictEqual(organizerWhoseAttendeeDidntShowUp);
});
it("When a organizer is attendee (event types with fixed hosts) and no show, that organizer is competing other hosts", async () => {
const organizerHostWhoseAttendeeShowedUp = await createHostWithBookings({
user: { email: "test-user1@example.com" },
bookings: [
{
eventTypeId: commonEventTypeId,
createdAt: new Date("2022-01-25T05:30:00.000Z"),
title: "Test User 1 Booking",
attendees: {
create: [
{
name: "test-attendee",
email: "test-attendee@example.com",
timeZone: "Asia/Calcutta",
},
],
},
},
],
});
const organizerWhoseAttendeeShowedUp = organizerHostWhoseAttendeeShowedUp.user;
const fixedHostOrganizerHostWhoseAttendeeDidNotShowUp = await createHostWithBookings({
user: { email: "test-user2@example.com" },
bookings: [
{
eventTypeId: commonEventTypeId,
createdAt: new Date("2022-01-25T06:30:00.000Z"),
attendees: {
create: [
{
name: "test-attendee",
email: "test-attendee@example.com",
timeZone: "Asia/Calcutta",
},
],
},
},
// User2 is fixed user, User 3 was selected as round robin host but did not show up so this booking should be counted for User 3
{
uid: "uuid-test-user2-booking2",
title: `Test User 2 Booking 2`,
createdAt: new Date("2022-01-25T07:30:00.000Z"),
eventTypeId: commonEventTypeId,
attendees: {
create: [
{
name: "test-attendee",
email: "test-user3@example.com",
timeZone: "Asia/Calcutta",
noShow: true,
},
],
},
},
],
});
const fixedHostOrganizerWhoseAttendeeDidNotShowUp =
fixedHostOrganizerHostWhoseAttendeeDidNotShowUp.user;
const organizerHostWhoWasAttendeeAndDidntShowUp = await createHostWithBookings({
user: { email: `test-user3@example.com` },
bookings: [
{
uid: "uuid-test-user3-booking1",
title: `Test User 3 Booking`,
createdAt: new Date("2022-01-25T04:30:00.000Z"),
attendees: {
create: [
{
name: "test-attendee",
email: "test-attendee@example.com",
timeZone: "Asia/Calcutta",
},
],
},
eventTypeId: commonEventTypeId,
},
],
});
const organizerWhoWasAttendeeAndDidntShowUp = organizerHostWhoWasAttendeeAndDidntShowUp.user;
expect(
luckyUserService.getLuckyUser({
availableUsers: [
organizerWhoseAttendeeShowedUp,
fixedHostOrganizerWhoseAttendeeDidNotShowUp,
organizerWhoWasAttendeeAndDidntShowUp,
],
eventType: {
id: commonEventTypeId,
isRRWeightsEnabled: false,
team: {},
},
allRRHosts: [],
routingFormResponse: null,
})
).resolves.toStrictEqual(organizerWhoWasAttendeeAndDidntShowUp);
});
it("should consider booking when noShowHost is null", async () => {
const hostWithBookingThatHappenedLater = await createHostWithBookings({
user: { email: "test-user1@example.com" },
bookings: [
{
uid: "uuid-test-user1-booking1",
createdAt: new Date("2022-01-25T07:30:00.000Z"),
title: "Test user 1 Booking",
noShowHost: null,
eventTypeId: commonEventTypeId,
attendees: {
create: [
{
name: "test-attendee",
email: "test-attendee@example.com",
timeZone: "Asia/Calcutta",
},
],
},
},
],
});
const userWithBookingThatHappenedLater = hostWithBookingThatHappenedLater.user;
const hostWithBookingThatHappenedEarlier = await createHostWithBookings({
user: { email: "test-user2@example.com" },
bookings: [
{
uid: "uuid-test-user2-booking1",
title: "Test User 2 Booking",
createdAt: new Date("2022-01-25T06:30:00.000Z"),
noShowHost: null,
eventTypeId: commonEventTypeId,
attendees: {
create: [
{
name: "test-attendee",
email: "test-attendee@example.com",
timeZone: "Asia/Calcutta",
},
],
},
},
],
});
const userWithBookingThatHappenedEarlier = hostWithBookingThatHappenedEarlier.user;
expect(
luckyUserService.getLuckyUser({
availableUsers: [userWithBookingThatHappenedLater, userWithBookingThatHappenedEarlier],
eventType: {
id: commonEventTypeId,
isRRWeightsEnabled: false,
team: {},
},
allRRHosts: [],
routingFormResponse: null,
})
).resolves.toStrictEqual(userWithBookingThatHappenedEarlier);
});
});
});
describe("getOrderedListOfLuckyUsers Integration tests", () => {
beforeEach(() => {
vi.setSystemTime("2024-11-14T00:00:13Z");
});
it("should sort as per availableUsers if no other criteria like weight/priority/calibration (TODO: make it independent of availableUsers order)", async () => {
const [host1, host2, host3] = await Promise.all([
createHostWithBookings({
user: { email: "test-user1@example.com" },
bookings: [],
createdAt: new Date(),
}),
createHostWithBookings({
user: { email: "test-user2@example.com" },
bookings: [],
createdAt: new Date(),
}),
createHostWithBookings({
user: { email: "test-user3@example.com" },
bookings: [],
createdAt: new Date(),
}),
]);
const user1 = host1.user;
const user2 = host2.user;
const user3 = host3.user;
const { users: luckyUsers } = await luckyUserService.getOrderedListOfLuckyUsers({
availableUsers: [user2, user1, user3],
eventType: {
id: commonEventTypeId,
isRRWeightsEnabled: false,
team: {},
},
allRRHosts: [],
routingFormResponse: null,
});
expectLuckyUsers(luckyUsers, [user2, user1, user3]);
const { users: luckyUsers2 } = await luckyUserService.getOrderedListOfLuckyUsers({
availableUsers: [user3, user1, user2],
eventType: {
id: commonEventTypeId,
isRRWeightsEnabled: false,
team: {},
},
allRRHosts: [],
routingFormResponse: null,
});
expectLuckyUsers(luckyUsers2, [user3, user1, user2]);
});
describe("should sort as per weights", () => {
const isRRWeightsEnabled = true;
it("even if there are no bookings", async () => {
const [host1WithWeight100, host2WithWeight200, host3WithWeight100] = await Promise.all([
createHostWithBookings({
user: { email: "test-user1@example.com" },
bookings: [],
createdAt: new Date(),
weight: 100,
}),
createHostWithBookings({
user: { email: "test-user2@example.com" },
bookings: [],
createdAt: new Date(),
weight: 200,
}),
createHostWithBookings({
user: { email: "test-user3@example.com" },
bookings: [],
createdAt: new Date(),
weight: 100,
}),
]);
const user1WithWeight100 = host1WithWeight100.user;
const userWithHighestWeight = host2WithWeight200.user;
const user2WithWeight100 = host3WithWeight100.user;
const allRRHosts = [host1WithWeight100, host2WithWeight200, host3WithWeight100];
const { users: luckyUsers } = await luckyUserService.getOrderedListOfLuckyUsers({
availableUsers: [userWithHighestWeight, user1WithWeight100, user2WithWeight100],
eventType: {
id: commonEventTypeId,
isRRWeightsEnabled,
team: {},
},
allRRHosts,
routingFormResponse: null,
});
expectLuckyUsers(luckyUsers, [
// It has the highest weight
userWithHighestWeight,
// It has the same weight as the next one but comes earlier in availableUsers array
user1WithWeight100,
// It is the last choice
user2WithWeight100,
]);
const { users: luckyUsers2 } = await luckyUserService.getOrderedListOfLuckyUsers({
availableUsers: [user2WithWeight100, userWithHighestWeight, user1WithWeight100],
eventType: {
id: commonEventTypeId,
isRRWeightsEnabled,
team: {},
},
allRRHosts,
routingFormResponse: null,
});
expectLuckyUsers(luckyUsers2, [
// It has the highest weight and zero bookings.
userWithHighestWeight,
// It has the same weight as the next one but comes earlier in availableUsers array
user2WithWeight100,
// It is the last choice
user1WithWeight100,
]);
});
it("consider booking count for the current month", async () => {
const [
hostWithOneBookingAndWeight200,
hostWithTwoBookingsAndWeight100,
hostWithThreeBookingsAndWeight100,
] = await Promise.all([
createHostWithBookings({
user: { email: "test-user1@example.com" },
bookings: [{ eventTypeId: commonEventTypeId, createdAt: new Date() }],
createdAt: new Date(),
weight: 200,
}),
createHostWithBookings({
user: { email: "test-user2@example.com" },
bookings: [
{ eventTypeId: commonEventTypeId, createdAt: new Date() },
{ eventTypeId: commonEventTypeId, createdAt: new Date() },
],
createdAt: new Date(),
weight: 100,
}),
createHostWithBookings({
user: { email: "test-user3@example.com" },
bookings: [
{ eventTypeId: commonEventTypeId, createdAt: new Date() },
{ eventTypeId: commonEventTypeId, createdAt: new Date() },
{ eventTypeId: commonEventTypeId, createdAt: new Date() },
],
createdAt: new Date(),
weight: 100,
}),
]);
const userWithOneBookingAndWeight200 = hostWithOneBookingAndWeight200.user;
const userWithTwoBookingsAndWeight100 = hostWithTwoBookingsAndWeight100.user;
const userWithThreeBookingsAndWeight100 = hostWithThreeBookingsAndWeight100.user;
const availableUsers = [
userWithThreeBookingsAndWeight100,
userWithTwoBookingsAndWeight100,
userWithOneBookingAndWeight200,
];
const getLuckUserParams = {
availableUsers,
eventType: {
id: commonEventTypeId,
isRRWeightsEnabled,
team: null,
},
allRRHosts: [
hostWithOneBookingAndWeight200,
hostWithTwoBookingsAndWeight100,
hostWithThreeBookingsAndWeight100,
],
routingFormResponse: null,
};
const { users: luckyUsers, perUserData } = await luckyUserService.getOrderedListOfLuckyUsers({
...getLuckUserParams,
availableUsers: [getLuckUserParams.availableUsers[0], ...getLuckUserParams.availableUsers.slice(1)],
});
expectLuckyUsers(luckyUsers, [
// User with 1 booking is chosen first because it has higher weight and lesser bookings
userWithOneBookingAndWeight200,
// User with 2 bookings is chosen next because it has lesser bookings
userWithTwoBookingsAndWeight100,
// User with 3 bookings is chosen last because it has the most bookings
userWithThreeBookingsAndWeight100,
]);
if (!perUserData?.bookingShortfalls) {
throw new Error("bookingShortfalls is not defined");
}
expect(perUserData.bookingShortfalls[userWithThreeBookingsAndWeight100.id]).toBe(-1.5);
expect(perUserData.bookingShortfalls[userWithTwoBookingsAndWeight100.id]).toBe(-0.5);
expect(perUserData.bookingShortfalls[userWithOneBookingAndWeight200.id]).toBe(2);
});
it("not considering bookings that were created in previous months", async () => {
const [
hostWithOneBookingInPreviousMonthAndWeight200,
hostWithTwoBookingsInPreviousMonthAndWeight100,
hostWithThreeBookingsInPreviousMonthAndWeight100,
] = await Promise.all([
createHostWithBookings({
user: { email: "test-user1@example.com", createdDate: new Date() },
bookings: [{ eventTypeId: commonEventTypeId, createdAt: new Date("2024-10-01T00:00:00.000Z") }],
weight: 200,
createdAt: new Date(),
}),
createHostWithBookings({
user: { email: "test-user2@example.com", createdDate: new Date() },
bookings: [
{ eventTypeId: commonEventTypeId, createdAt: new Date("2024-10-01T00:00:00.000Z") },
{ eventTypeId: commonEventTypeId, createdAt: new Date("2024-10-01T00:00:00.000Z") },
],
weight: 100,
createdAt: new Date(),
}),
createHostWithBookings({
user: { email: "test-user3@example.com", createdDate: new Date() },
bookings: [
{ eventTypeId: commonEventTypeId, createdAt: new Date("2024-10-01T00:00:00.000Z") },
{ eventTypeId: commonEventTypeId, createdAt: new Date("2024-10-01T00:00:00.000Z") },
{ eventTypeId: commonEventTypeId, createdAt: new Date("2024-10-01T00:00:00.000Z") },
],
weight: 100,
createdAt: new Date(),
}),
]);
const userWithOneBookingInPreviousMonthAndWeight200 =
hostWithOneBookingInPreviousMonthAndWeight200.user;
const userWithTwoBookingsInPreviousMonthAndWeight100 =
hostWithTwoBookingsInPreviousMonthAndWeight100.user;
const userWithThreeBookingsInPreviousMonthAndWeight100 =
hostWithThreeBookingsInPreviousMonthAndWeight100.user;
const availableUsers = [
userWithThreeBookingsInPreviousMonthAndWeight100,
userWithTwoBookingsInPreviousMonthAndWeight100,
userWithOneBookingInPreviousMonthAndWeight200,
];
const getLuckUserParams = {
availableUsers,
eventType: {
id: commonEventTypeId,
isRRWeightsEnabled,
team: null,
},
allRRHosts: [
hostWithOneBookingInPreviousMonthAndWeight200,
hostWithTwoBookingsInPreviousMonthAndWeight100,
hostWithThreeBookingsInPreviousMonthAndWeight100,
],
routingFormResponse: null,
};
const { users: luckyUsers, perUserData } = await luckyUserService.getOrderedListOfLuckyUsers({
...getLuckUserParams,
availableUsers: [getLuckUserParams.availableUsers[0], ...getLuckUserParams.availableUsers.slice(1)],
});
expectLuckyUsers(luckyUsers, [
// User with 1 booking is chosen first because it has higher weight and lesser bookings
userWithOneBookingInPreviousMonthAndWeight200,
// User with 3 bookings is chosen next because it comes earlier in availableUsers array
userWithThreeBookingsInPreviousMonthAndWeight100,
// User with 2 bookings is chosen last because it comes later in availableUsers array
userWithTwoBookingsInPreviousMonthAndWeight100,
]);
if (!perUserData?.bookingShortfalls) {
throw new Error("bookingShortfalls is not defined");
}
// Because no one has any bookings in the current month, the booking shortfall should be 0 for all users
expect(perUserData.bookingShortfalls[userWithThreeBookingsInPreviousMonthAndWeight100.id]).toBe(0);
expect(perUserData.bookingShortfalls[userWithTwoBookingsInPreviousMonthAndWeight100.id]).toBe(0);
expect(perUserData.bookingShortfalls[userWithOneBookingInPreviousMonthAndWeight200.id]).toBe(0);
});
});
describe("should sort as per host creation data calibration", () => {
const isRRWeightsEnabled = true;
it("not considering bookings that were created in previous months", async () => {
const today = new Date();
const tenthOfTheMonth = new Date(today.getFullYear(), today.getMonth(), 10);
const secondsInDay = 24 * 60 * 60 * 1000;
const ninthOfTheMonth = new Date(tenthOfTheMonth.getTime() - secondsInDay);
const eighthOfTheMonth = new Date(tenthOfTheMonth.getTime() - 2 * secondsInDay);
const [host1, host2, host3] = await Promise.all([
createHostWithBookings({
user: { email: "test-user1@example.com", createdDate: tenthOfTheMonth },
bookings: [
{ eventTypeId: commonEventTypeId, createdAt: new Date(tenthOfTheMonth.getTime() + 1000) },
],
weight: 200,
createdAt: tenthOfTheMonth,
}),
createHostWithBookings({
user: { email: "test-user2@example.com", createdDate: ninthOfTheMonth },
bookings: [
{ eventTypeId: commonEventTypeId, createdAt: new Date(ninthOfTheMonth.getTime() + 1000) },
{ eventTypeId: commonEventTypeId, createdAt: new Date(ninthOfTheMonth.getTime() + 2000) },
],
weight: 100,
createdAt: ninthOfTheMonth,
}),
createHostWithBookings({
user: { email: "test-user3@example.com", createdDate: eighthOfTheMonth },
bookings: [
{ eventTypeId: commonEventTypeId, createdAt: new Date(eighthOfTheMonth.getTime() + 1000) },
{ eventTypeId: commonEventTypeId, createdAt: new Date(eighthOfTheMonth.getTime() + 2000) },
{ eventTypeId: commonEventTypeId, createdAt: new Date(eighthOfTheMonth.getTime() + 3000) },
],
weight: 100,
createdAt: eighthOfTheMonth,
}),
]);
const availableUsers = [host3.user, host2.user, host1.user];
const getLuckyUserParams = {
availableUsers,
eventType: {
id: commonEventTypeId,
isRRWeightsEnabled,
team: null,
},
allRRHosts: [host1, host2, host3],
routingFormResponse: null,
};
const { users: luckyUsers, perUserData } = await luckyUserService.getOrderedListOfLuckyUsers({
...getLuckyUserParams,
availableUsers: [getLuckyUserParams.availableUsers[0], ...getLuckyUserParams.availableUsers.slice(1)],
});
if (!perUserData?.bookingShortfalls || !perUserData?.calibrations) {
throw new Error("bookingShortfalls or calibrations is not defined");
}
expect(perUserData.calibrations[host1.user.id]).toBe(2.5);
expect(perUserData.calibrations[host2.user.id]).toBe(3);
expect(perUserData.calibrations[host3.user.id]).toBe(0);
expectLuckyUsers(luckyUsers, [host1.user, host3.user, host2.user]);
});
});
});