diff --git a/packages/features/bookings/lib/getLuckyUser.integration-test.ts b/packages/features/bookings/lib/getLuckyUser.integration-test.ts deleted file mode 100644 index 3571d0be00..0000000000 --- a/packages/features/bookings/lib/getLuckyUser.integration-test.ts +++ /dev/null @@ -1,792 +0,0 @@ -import { getLuckyUserService } from "@calcom/features/di/containers/LuckyUser"; -import prisma from "@calcom/prisma"; -import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; - -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: [], - }); - - 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: [], - }) - ).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: [], - }) - ).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: [], - }) - ).resolves.toStrictEqual(userWithBookingThatHappenedEarlier); - }); - }); -}); - -describe("getOrderedListOfLuckyUsers Integration tests", () => { - beforeEach(() => { - vi.setSystemTime("2024-11-14T00:00:13Z"); - }); - - it("should sort by user id if no other criteria like weight/priority/calibration", 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, - }); - - const expectedOrder = [user1, user2, user3].sort((a, b) => a.id - b.id) - - expectLuckyUsers(luckyUsers, expectedOrder); - - const { users: luckyUsers2 } = await luckyUserService.getOrderedListOfLuckyUsers({ - availableUsers: [user3, user1, user2], - eventType: { - id: commonEventTypeId, - isRRWeightsEnabled: false, - team: {}, - }, - allRRHosts: [], - routingFormResponse: null, - }); - expectLuckyUsers(luckyUsers2, expectedOrder); - }); - - 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, - }); - - 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, - }); - 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, - ], - }; - - 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, - ], - }; - - 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], - }; - - 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]); - }); - }); -});