import CalendarManagerMock from "../../../../tests/libs/__mocks__/CalendarManager"; import prismaMock from "../../../../tests/libs/__mocks__/prisma"; import { diff } from "jest-diff"; import { describe, expect, vi, beforeEach, afterEach, test } from "vitest"; import prisma from "@calcom/prisma"; import type { BookingStatus } from "@calcom/prisma/enums"; import type { Slot } from "@calcom/trpc/server/routers/viewer/slots/types"; import { getAvailableSlots as getSchedule } from "@calcom/trpc/server/routers/viewer/slots/util"; import { getDate, getGoogleCalendarCredential, createBookingScenario} from "../utils/bookingScenario"; // TODO: Mock properly prismaMock.eventType.findUnique.mockResolvedValue(null); prismaMock.user.findMany.mockResolvedValue([]); vi.mock("@calcom/lib/constants", () => ({ IS_PRODUCTION: true, WEBAPP_URL: "http://localhost:3000" })); declare global { // eslint-disable-next-line @typescript-eslint/no-namespace namespace jest { interface Matchers { toHaveTimeSlots(expectedSlots: string[], date: { dateString: string }): R; } } } expect.extend({ toHaveTimeSlots( schedule: { slots: Record }, expectedSlots: string[], { dateString }: { dateString: string } ) { if (!schedule.slots[`${dateString}`]) { return { pass: false, message: () => `has no timeslots for ${dateString}`, }; } if ( !schedule.slots[`${dateString}`] .map((slot) => slot.time) .every((actualSlotTime, index) => { return `${dateString}T${expectedSlots[index]}` === actualSlotTime; }) ) { return { pass: false, message: () => `has incorrect timeslots for ${dateString}.\n\r ${diff( expectedSlots.map((expectedSlot) => `${dateString}T${expectedSlot}`), schedule.slots[`${dateString}`].map((slot) => slot.time) )}`, }; } return { pass: true, message: () => "has correct timeslots ", }; }, }); const Timezones = { "+5:30": "Asia/Kolkata", "+6:00": "Asia/Dhaka", }; const TestData = { selectedCalendars: { google: { integration: "google_calendar", externalId: "john@example.com", }, }, credentials: { google: getGoogleCalendarCredential(), }, schedules: { IstWorkHours: { id: 1, name: "9:30AM to 6PM in India - 4:00AM to 12:30PM in GMT", availability: [ { userId: null, eventTypeId: null, days: [0, 1, 2, 3, 4, 5, 6], startTime: new Date("1970-01-01T09:30:00.000Z"), endTime: new Date("1970-01-01T18:00:00.000Z"), date: null, }, ], timeZone: Timezones["+5:30"], }, IstWorkHoursWithDateOverride: (dateString: string) => ({ id: 1, name: "9:30AM to 6PM in India - 4:00AM to 12:30PM in GMT but with a Date Override for 2PM to 6PM IST(in GST time it is 8:30AM to 12:30PM)", availability: [ { userId: null, eventTypeId: null, days: [0, 1, 2, 3, 4, 5, 6], startTime: new Date("1970-01-01T09:30:00.000Z"), endTime: new Date("1970-01-01T18:00:00.000Z"), date: null, }, { userId: null, eventTypeId: null, days: [0, 1, 2, 3, 4, 5, 6], startTime: new Date("1970-01-01T14:00:00.000Z"), endTime: new Date("1970-01-01T18:00:00.000Z"), date: dateString, }, ], timeZone: Timezones["+5:30"], }), }, users: { example: { name: "Example", username: "example", defaultScheduleId: 1, email: "example@example.com", timeZone: Timezones["+5:30"], }, }, apps: { googleCalendar: { slug: "google-calendar", dirName: "whatever", // eslint-disable-next-line @typescript-eslint/ban-ts-comment //@ts-ignore keys: { expiry_date: Infinity, client_id: "client_id", client_secret: "client_secret", redirect_uris: ["http://localhost:3000/auth/callback"], }, }, }, }; const cleanup = async () => { await prisma.eventType.deleteMany(); await prisma.user.deleteMany(); await prisma.schedule.deleteMany(); await prisma.selectedCalendar.deleteMany(); await prisma.credential.deleteMany(); await prisma.booking.deleteMany(); await prisma.app.deleteMany(); }; beforeEach(async () => { await cleanup(); }); afterEach(async () => { await cleanup(); }); describe("getSchedule", () => { describe("Calendar event", () => { test("correctly identifies unavailable slots from calendar", async () => { const { dateString: plus1DateString } = getDate({ dateIncrement: 1 }); const { dateString: plus2DateString } = getDate({ dateIncrement: 2 }); CalendarManagerMock.getBusyCalendarTimes.mockResolvedValue([ { start: `${plus2DateString}T04:45:00.000Z`, end: `${plus2DateString}T23:00:00.000Z`, }, ]); const scenarioData = { eventTypes: [ { id: 1, slotInterval: 45, length: 45, users: [ { id: 101, }, ], }, ], users: [ { ...TestData.users.example, id: 101, schedules: [TestData.schedules.IstWorkHours], credentials: [getGoogleCalendarCredential()], selectedCalendars: [TestData.selectedCalendars.google], }, ], apps: [TestData.apps.googleCalendar], }; // An event with one accepted booking createBookingScenario(scenarioData); const scheduleForDayWithAGoogleCalendarBooking = await getSchedule({ input: { eventTypeId: 1, eventTypeSlug: "", startTime: `${plus1DateString}T18:30:00.000Z`, endTime: `${plus2DateString}T18:29:59.999Z`, timeZone: Timezones["+5:30"], isTeamEvent: false, }, }); // As per Google Calendar Availability, only 4PM(4-4:45PM) GMT slot would be available expect(scheduleForDayWithAGoogleCalendarBooking).toHaveTimeSlots([`04:00:00.000Z`], { dateString: plus2DateString, }); }); }); describe("User Event", () => { test("correctly identifies unavailable slots from Cal Bookings in different status", async () => { const { dateString: plus1DateString } = getDate({ dateIncrement: 1 }); const { dateString: plus2DateString } = getDate({ dateIncrement: 2 }); const { dateString: plus3DateString } = getDate({ dateIncrement: 3 }); // An event with one accepted booking createBookingScenario({ // An event with length 30 minutes, slotInterval 45 minutes, and minimumBookingNotice 1440 minutes (24 hours) eventTypes: [ { id: 1, // If `slotInterval` is set, it supersedes `length` slotInterval: 45, length: 45, users: [ { id: 101, }, ], }, ], users: [ { ...TestData.users.example, id: 101, schedules: [TestData.schedules.IstWorkHours], }, ], bookings: [ // That event has one accepted booking from 4:00 to 4:15 in GMT on Day + 3 which is 9:30 to 9:45 in IST { eventTypeId: 1, userId: 101, status: "ACCEPTED", // Booking Time is stored in GMT in DB. So, provide entry in GMT only. startTime: `${plus3DateString}T04:00:00.000Z`, endTime: `${plus3DateString}T04:15:00.000Z`, }, { eventTypeId: 1, userId: 101, status: "REJECTED", // Booking Time is stored in GMT in DB. So, provide entry in GMT only. startTime: `${plus2DateString}T04:00:00.000Z`, endTime: `${plus2DateString}T04:15:00.000Z`, }, { eventTypeId: 1, userId: 101, status: "CANCELLED", // Booking Time is stored in GMT in DB. So, provide entry in GMT only. startTime: `${plus2DateString}T05:00:00.000Z`, endTime: `${plus2DateString}T05:15:00.000Z`, }, { eventTypeId: 1, userId: 101, status: "PENDING", // Booking Time is stored in GMT in DB. So, provide entry in GMT only. startTime: `${plus2DateString}T06:00:00.000Z`, endTime: `${plus2DateString}T06:15:00.000Z`, }, ], }); // Day Plus 2 is completely free - It only has non accepted bookings const scheduleOnCompletelyFreeDay = await getSchedule({ input: { eventTypeId: 1, // EventTypeSlug doesn't matter for non-dynamic events eventTypeSlug: "", startTime: `${plus1DateString}T18:30:00.000Z`, endTime: `${plus2DateString}T18:29:59.999Z`, timeZone: Timezones["+5:30"], isTeamEvent: false, } }); // getSchedule returns timeslots in GMT expect(scheduleOnCompletelyFreeDay).toHaveTimeSlots( [ "04:00:00.000Z", "04:45:00.000Z", "05:30:00.000Z", "06:15:00.000Z", "07:00:00.000Z", "07:45:00.000Z", "08:30:00.000Z", "09:15:00.000Z", "10:00:00.000Z", "10:45:00.000Z", "11:30:00.000Z", ], { dateString: plus2DateString, } ); // Day plus 3 const scheduleForDayWithOneBooking = await getSchedule({ input: { eventTypeId: 1, eventTypeSlug: "", startTime: `${plus2DateString}T18:30:00.000Z`, endTime: `${plus3DateString}T18:29:59.999Z`, timeZone: Timezones["+5:30"], isTeamEvent: false, } }); expect(scheduleForDayWithOneBooking).toHaveTimeSlots( [ // "04:00:00.000Z", - This slot is unavailable because of the booking from 4:00 to 4:15 `04:15:00.000Z`, `05:00:00.000Z`, `05:45:00.000Z`, `06:30:00.000Z`, `07:15:00.000Z`, `08:00:00.000Z`, `08:45:00.000Z`, `09:30:00.000Z`, `10:15:00.000Z`, `11:00:00.000Z`, `11:45:00.000Z`, ], { dateString: plus3DateString, } ); }); test("slots are available as per `length`, `slotInterval` of the event", async () => { createBookingScenario({ eventTypes: [ { id: 1, length: 30, users: [ { id: 101, }, ], }, { id: 2, length: 30, slotInterval: 120, users: [ { id: 101, }, ], }, ], users: [ { ...TestData.users.example, id: 101, schedules: [TestData.schedules.IstWorkHours], }, ], }); const { dateString: plus1DateString } = getDate({ dateIncrement: 1 }); const { dateString: plus2DateString } = getDate({ dateIncrement: 2 }); const scheduleForEventWith30Length = await getSchedule({ input: { eventTypeId: 1, eventTypeSlug: "", startTime: `${plus1DateString}T18:30:00.000Z`, endTime: `${plus2DateString}T18:29:59.999Z`, timeZone: Timezones["+5:30"], isTeamEvent: false, } }); expect(scheduleForEventWith30Length).toHaveTimeSlots( [ `04:00:00.000Z`, `04:30:00.000Z`, `05:00:00.000Z`, `05:30:00.000Z`, `06:00:00.000Z`, `06:30:00.000Z`, `07:00:00.000Z`, `07:30:00.000Z`, `08:00:00.000Z`, `08:30:00.000Z`, `09:00:00.000Z`, `09:30:00.000Z`, `10:00:00.000Z`, `10:30:00.000Z`, `11:00:00.000Z`, `11:30:00.000Z`, `12:00:00.000Z`, ], { dateString: plus2DateString, } ); const scheduleForEventWith30minsLengthAndSlotInterval2hrs = await getSchedule({ input: { eventTypeId: 2, eventTypeSlug: "", startTime: `${plus1DateString}T18:30:00.000Z`, endTime: `${plus2DateString}T18:29:59.999Z`, timeZone: Timezones["+5:30"], isTeamEvent: false, } }); // `slotInterval` takes precedence over `length` // 4:30 is utc so it is 10:00 in IST expect(scheduleForEventWith30minsLengthAndSlotInterval2hrs).toHaveTimeSlots( [`04:30:00.000Z`, `06:30:00.000Z`, `08:30:00.000Z`, `10:30:00.000Z`, `12:30:00.000Z`], { dateString: plus2DateString, } ); }); // FIXME: Fix minimumBookingNotice is respected test // eslint-disable-next-line playwright/no-skipped-test test.skip("minimumBookingNotice is respected", async () => { vi.useFakeTimers().setSystemTime( (() => { const today = new Date(); // Beginning of the day in current timezone of the system return new Date(today.getFullYear(), today.getMonth(), today.getDate()); })() ); createBookingScenario({ eventTypes: [ { id: 1, length: 120, minimumBookingNotice: 13 * 60, // Would take the minimum bookable time to be 18:30UTC+13 = 7:30AM UTC users: [ { id: 101, }, ], }, { id: 2, length: 120, minimumBookingNotice: 10 * 60, // Would take the minimum bookable time to be 18:30UTC+10 = 4:30AM UTC users: [ { id: 101, }, ], }, ], users: [ { ...TestData.users.example, id: 101, schedules: [TestData.schedules.IstWorkHours], }, ], }); const { dateString: todayDateString } = getDate(); const { dateString: minus1DateString } = getDate({ dateIncrement: -1 }); const scheduleForEventWithBookingNotice13Hrs = await getSchedule({ input: { eventTypeId: 1, eventTypeSlug: "", startTime: `${minus1DateString}T18:30:00.000Z`, endTime: `${todayDateString}T18:29:59.999Z`, timeZone: Timezones["+5:30"], isTeamEvent: false, } }); expect(scheduleForEventWithBookingNotice13Hrs).toHaveTimeSlots( [ /*`04:00:00.000Z`, `06:00:00.000Z`, - Minimum time slot is 07:30 UTC*/ `08:00:00.000Z`, `10:00:00.000Z`, `12:00:00.000Z`, ], { dateString: todayDateString, } ); const scheduleForEventWithBookingNotice10Hrs = await getSchedule({ input: { eventTypeId: 2, eventTypeSlug: "", startTime: `${minus1DateString}T18:30:00.000Z`, endTime: `${todayDateString}T18:29:59.999Z`, timeZone: Timezones["+5:30"], isTeamEvent: false, } }); expect(scheduleForEventWithBookingNotice10Hrs).toHaveTimeSlots( [ /*`04:00:00.000Z`, - Minimum bookable time slot is 04:30 UTC but next available is 06:00*/ `06:00:00.000Z`, `08:00:00.000Z`, `10:00:00.000Z`, `12:00:00.000Z`, ], { dateString: todayDateString, } ); vi.useRealTimers(); }); test("afterBuffer and beforeBuffer tests - Non Cal Busy Time", async () => { const { dateString: plus2DateString } = getDate({ dateIncrement: 2 }); const { dateString: plus3DateString } = getDate({ dateIncrement: 3 }); CalendarManagerMock.getBusyCalendarTimes.mockResolvedValue([ { start: `${plus3DateString}T04:00:00.000Z`, end: `${plus3DateString}T05:59:59.000Z`, }, ]); const scenarioData = { eventTypes: [ { id: 1, length: 120, beforeEventBuffer: 120, afterEventBuffer: 120, users: [ { id: 101, }, ], }, ], users: [ { ...TestData.users.example, id: 101, schedules: [TestData.schedules.IstWorkHours], credentials: [getGoogleCalendarCredential()], selectedCalendars: [TestData.selectedCalendars.google], }, ], apps: [TestData.apps.googleCalendar], }; createBookingScenario(scenarioData); const scheduleForEventOnADayWithNonCalBooking = await getSchedule({ input: { eventTypeId: 1, eventTypeSlug: "", startTime: `${plus2DateString}T18:30:00.000Z`, endTime: `${plus3DateString}T18:29:59.999Z`, timeZone: Timezones["+5:30"], isTeamEvent: false, } }); expect(scheduleForEventOnADayWithNonCalBooking).toHaveTimeSlots( [ // `04:00:00.000Z`, // - 4 AM is booked // `06:00:00.000Z`, // - 6 AM is not available because 08:00AM slot has a `beforeEventBuffer` `08:00:00.000Z`, // - 8 AM is available because of availability of 06:00 - 07:59 `10:00:00.000Z`, `12:00:00.000Z`, ], { dateString: plus3DateString, } ); }); test("afterBuffer and beforeBuffer tests - Cal Busy Time", async () => { const { dateString: plus1DateString } = getDate({ dateIncrement: 1 }); const { dateString: plus2DateString } = getDate({ dateIncrement: 2 }); const { dateString: plus3DateString } = getDate({ dateIncrement: 3 }); CalendarManagerMock.getBusyCalendarTimes.mockResolvedValue([ { start: `${plus3DateString}T04:00:00.000Z`, end: `${plus3DateString}T05:59:59.000Z`, }, ]); const scenarioData = { eventTypes: [ { id: 1, length: 120, beforeEventBuffer: 120, afterEventBuffer: 120, users: [ { id: 101, }, ], }, ], users: [ { ...TestData.users.example, id: 101, schedules: [TestData.schedules.IstWorkHours], credentials: [getGoogleCalendarCredential()], selectedCalendars: [TestData.selectedCalendars.google], }, ], bookings: [ { userId: 101, eventTypeId: 1, startTime: `${plus2DateString}T04:00:00.000Z`, endTime: `${plus2DateString}T05:59:59.000Z`, status: "ACCEPTED" as BookingStatus, }, ], apps: [TestData.apps.googleCalendar], }; createBookingScenario(scenarioData); const scheduleForEventOnADayWithCalBooking = await getSchedule({ input: { eventTypeId: 1, eventTypeSlug: "", startTime: `${plus1DateString}T18:30:00.000Z`, endTime: `${plus2DateString}T18:29:59.999Z`, timeZone: Timezones["+5:30"], isTeamEvent: false, } }); expect(scheduleForEventOnADayWithCalBooking).toHaveTimeSlots( [ // `04:00:00.000Z`, // - 4 AM is booked // `06:00:00.000Z`, // - 6 AM is not available because of afterBuffer(120 mins) of the existing booking(4-5:59AM slot) // `08:00:00.000Z`, // - 8 AM is not available because of beforeBuffer(120mins) of possible booking at 08:00 `10:00:00.000Z`, `12:00:00.000Z`, ], { dateString: plus2DateString, } ); }); test("Start times are offset (offsetStart)", async () => { const { dateString: plus1DateString } = getDate({ dateIncrement: 1 }); const { dateString: plus2DateString } = getDate({ dateIncrement: 2 }); CalendarManagerMock.getBusyCalendarTimes.mockResolvedValue([]); const scenarioData = { eventTypes: [ { id: 1, length: 25, offsetStart: 5, users: [ { id: 101, }, ], }, ], users: [ { ...TestData.users.example, id: 101, schedules: [TestData.schedules.IstWorkHours], credentials: [getGoogleCalendarCredential()], selectedCalendars: [TestData.selectedCalendars.google], }, ], apps: [TestData.apps.googleCalendar], }; createBookingScenario(scenarioData); const schedule = await getSchedule({ input: { eventTypeId: 1, eventTypeSlug: "", startTime: `${plus1DateString}T18:30:00.000Z`, endTime: `${plus2DateString}T18:29:59.999Z`, timeZone: Timezones["+5:30"], isTeamEvent: false, } }); expect(schedule).toHaveTimeSlots( [ `04:05:00.000Z`, `04:35:00.000Z`, `05:05:00.000Z`, `05:35:00.000Z`, `06:05:00.000Z`, `06:35:00.000Z`, `07:05:00.000Z`, `07:35:00.000Z`, `08:05:00.000Z`, `08:35:00.000Z`, `09:05:00.000Z`, `09:35:00.000Z`, `10:05:00.000Z`, `10:35:00.000Z`, `11:05:00.000Z`, `11:35:00.000Z`, `12:05:00.000Z`, ], { dateString: plus2DateString, } ); }); test("Check for Date overrides", async () => { const { dateString: plus1DateString } = getDate({ dateIncrement: 1 }); const { dateString: plus2DateString } = getDate({ dateIncrement: 2 }); const scenarioData = { eventTypes: [ { id: 1, length: 60, users: [ { id: 101, }, ], }, ], users: [ { ...TestData.users.example, id: 101, schedules: [TestData.schedules.IstWorkHoursWithDateOverride(plus2DateString)], }, ], }; createBookingScenario(scenarioData); const scheduleForEventOnADayWithDateOverride = await getSchedule({ input: { eventTypeId: 1, eventTypeSlug: "", startTime: `${plus1DateString}T18:30:00.000Z`, endTime: `${plus2DateString}T18:29:59.999Z`, timeZone: Timezones["+5:30"], isTeamEvent: false, } }); expect(scheduleForEventOnADayWithDateOverride).toHaveTimeSlots( ["08:30:00.000Z", "09:30:00.000Z", "10:30:00.000Z", "11:30:00.000Z"], { dateString: plus2DateString, } ); }); test("that a user is considered busy when there's a booking they host", async () => { const { dateString: plus1DateString } = getDate({ dateIncrement: 1 }); const { dateString: plus2DateString } = getDate({ dateIncrement: 2 }); createBookingScenario({ eventTypes: [ // A Collective Event Type hosted by this user { id: 1, slotInterval: 45, schedulingType: "COLLECTIVE", hosts: [ { id: 101, }, { id: 102, }, ], }, // A default Event Type which this user owns { id: 2, length: 15, slotInterval: 45, users: [{ id: 101 }], }, ], users: [ { ...TestData.users.example, id: 101, schedules: [TestData.schedules.IstWorkHours], }, { ...TestData.users.example, id: 102, schedules: [TestData.schedules.IstWorkHours], }, ], bookings: [ // Create a booking on our Collective Event Type { userId: 101, attendees: [ { email: "IntegrationTestUser102@example.com", }, ], eventTypeId: 1, status: "ACCEPTED", startTime: `${plus2DateString}T04:00:00.000Z`, endTime: `${plus2DateString}T04:15:00.000Z`, }, ], }); // Requesting this user's availability for their // individual Event Type const thisUserAvailability = await getSchedule({ input: { eventTypeId: 2, eventTypeSlug: "", startTime: `${plus1DateString}T18:30:00.000Z`, endTime: `${plus2DateString}T18:29:59.999Z`, timeZone: Timezones["+5:30"], isTeamEvent: false, } }); expect(thisUserAvailability).toHaveTimeSlots( [ // `04:00:00.000Z`, // <- This slot should be occupied by the Collective Event `04:15:00.000Z`, `05:00:00.000Z`, `05:45:00.000Z`, `06:30:00.000Z`, `07:15:00.000Z`, `08:00:00.000Z`, `08:45:00.000Z`, `09:30:00.000Z`, `10:15:00.000Z`, `11:00:00.000Z`, `11:45:00.000Z`, ], { dateString: plus2DateString, } ); }); }); describe("Team Event", () => { test("correctly identifies unavailable slots from calendar for all users in collective scheduling, considers bookings of users in other events as well", async () => { const { dateString: todayDateString } = getDate(); const { dateString: plus1DateString } = getDate({ dateIncrement: 1 }); const { dateString: plus2DateString } = getDate({ dateIncrement: 2 }); createBookingScenario({ eventTypes: [ // An event having two users with one accepted booking { id: 1, slotInterval: 45, schedulingType: "COLLECTIVE", length: 45, users: [ { id: 101, }, { id: 102, }, ], }, { id: 2, slotInterval: 45, length: 45, users: [ { id: 102, }, ], }, ], users: [ { ...TestData.users.example, id: 101, schedules: [TestData.schedules.IstWorkHours], }, { ...TestData.users.example, id: 102, schedules: [TestData.schedules.IstWorkHours], }, ], bookings: [ { userId: 101, eventTypeId: 1, status: "ACCEPTED", startTime: `${plus2DateString}T04:00:00.000Z`, endTime: `${plus2DateString}T04:15:00.000Z`, }, { userId: 102, eventTypeId: 2, status: "ACCEPTED", startTime: `${plus2DateString}T05:30:00.000Z`, endTime: `${plus2DateString}T05:45:00.000Z`, }, ], }); const scheduleForTeamEventOnADayWithNoBooking = await getSchedule({ input: { eventTypeId: 1, eventTypeSlug: "", startTime: `${todayDateString}T18:30:00.000Z`, endTime: `${plus1DateString}T18:29:59.999Z`, timeZone: Timezones["+5:30"], isTeamEvent: true, } }); expect(scheduleForTeamEventOnADayWithNoBooking).toHaveTimeSlots( [ `04:00:00.000Z`, `04:45:00.000Z`, `05:30:00.000Z`, `06:15:00.000Z`, `07:00:00.000Z`, `07:45:00.000Z`, `08:30:00.000Z`, `09:15:00.000Z`, `10:00:00.000Z`, `10:45:00.000Z`, `11:30:00.000Z`, ], { dateString: plus1DateString, } ); const scheduleForTeamEventOnADayWithOneBookingForEachUser = await getSchedule({ input: { eventTypeId: 1, eventTypeSlug: "", startTime: `${plus1DateString}T18:30:00.000Z`, endTime: `${plus2DateString}T18:29:59.999Z`, timeZone: Timezones["+5:30"], isTeamEvent: true, } }); // A user with blocked time in another event, still affects Team Event availability // It's a collective availability, so both user 101 and 102 are considered for timeslots expect(scheduleForTeamEventOnADayWithOneBookingForEachUser).toHaveTimeSlots( [ //`04:00:00.000Z`, - Blocked with User 101 `04:15:00.000Z`, //`05:00:00.000Z`, - Blocked with User 102 in event 2 `05:45:00.000Z`, `06:30:00.000Z`, `07:15:00.000Z`, `08:00:00.000Z`, `08:45:00.000Z`, `09:30:00.000Z`, `10:15:00.000Z`, `11:00:00.000Z`, `11:45:00.000Z`, ], { dateString: plus2DateString } ); }); test("correctly identifies unavailable slots from calendar for all users in Round Robin scheduling, considers bookings of users in other events as well", async () => { const { dateString: plus1DateString } = getDate({ dateIncrement: 1 }); const { dateString: plus2DateString } = getDate({ dateIncrement: 2 }); const { dateString: plus3DateString } = getDate({ dateIncrement: 3 }); createBookingScenario({ eventTypes: [ // An event having two users with one accepted booking { id: 1, slotInterval: 45, length: 45, users: [ { id: 101, }, { id: 102, }, ], schedulingType: "ROUND_ROBIN", }, { id: 2, slotInterval: 45, length: 45, users: [ { id: 102, }, ], }, ], users: [ { ...TestData.users.example, id: 101, schedules: [TestData.schedules.IstWorkHours], }, { ...TestData.users.example, id: 102, schedules: [TestData.schedules.IstWorkHours], }, ], bookings: [ { userId: 101, eventTypeId: 1, status: "ACCEPTED", startTime: `${plus2DateString}T04:00:00.000Z`, endTime: `${plus2DateString}T04:15:00.000Z`, }, { userId: 102, eventTypeId: 2, status: "ACCEPTED", startTime: `${plus2DateString}T05:30:00.000Z`, endTime: `${plus2DateString}T05:45:00.000Z`, }, { userId: 101, eventTypeId: 1, status: "ACCEPTED", startTime: `${plus3DateString}T04:00:00.000Z`, endTime: `${plus3DateString}T04:15:00.000Z`, }, { userId: 102, eventTypeId: 2, status: "ACCEPTED", startTime: `${plus3DateString}T04:00:00.000Z`, endTime: `${plus3DateString}T04:15:00.000Z`, }, ], }); const scheduleForTeamEventOnADayWithOneBookingForEachUserButOnDifferentTimeslots = await getSchedule({ input: { eventTypeId: 1, eventTypeSlug: "", startTime: `${plus1DateString}T18:30:00.000Z`, endTime: `${plus2DateString}T18:29:59.999Z`, timeZone: Timezones["+5:30"], isTeamEvent: true, } }); // A user with blocked time in another event, still affects Team Event availability expect(scheduleForTeamEventOnADayWithOneBookingForEachUserButOnDifferentTimeslots).toHaveTimeSlots( [ `04:00:00.000Z`, // - Blocked with User 101 but free with User 102. Being RoundRobin it is still bookable `04:45:00.000Z`, `05:30:00.000Z`, // - Blocked with User 102 but free with User 101. Being RoundRobin it is still bookable `06:15:00.000Z`, `07:00:00.000Z`, `07:45:00.000Z`, `08:30:00.000Z`, `09:15:00.000Z`, `10:00:00.000Z`, `10:45:00.000Z`, `11:30:00.000Z`, ], { dateString: plus2DateString } ); const scheduleForTeamEventOnADayWithOneBookingForEachUserOnSameTimeSlot = await getSchedule({ input: { eventTypeId: 1, eventTypeSlug: "", startTime: `${plus2DateString}T18:30:00.000Z`, endTime: `${plus3DateString}T18:29:59.999Z`, timeZone: Timezones["+5:30"], isTeamEvent: true, } }); // A user with blocked time in another event, still affects Team Event availability expect(scheduleForTeamEventOnADayWithOneBookingForEachUserOnSameTimeSlot).toHaveTimeSlots( [ //`04:00:00.000Z`, // - Blocked with User 101 as well as User 102, so not available in Round Robin `04:15:00.000Z`, `05:00:00.000Z`, `05:45:00.000Z`, `06:30:00.000Z`, `07:15:00.000Z`, `08:00:00.000Z`, `08:45:00.000Z`, `09:30:00.000Z`, `10:15:00.000Z`, `11:00:00.000Z`, `11:45:00.000Z`, ], { dateString: plus3DateString } ); }); }); });