import CalendarManagerMock from "../../../../tests/libs/__mocks__/CalendarManager"; import { getDate, getGoogleCalendarCredential, createBookingScenario, createOrganization, getOrganizer, getScenarioData, Timezones, TestData, createCredentials, mockCrmApp, } from "../utils/bookingScenario/bookingScenario"; import { describe, vi, test } from "vitest"; import dayjs from "@calcom/dayjs"; import type { BookingStatus } from "@calcom/prisma/enums"; import { getAvailableSlots as getSchedule } from "@calcom/trpc/server/routers/viewer/slots/util"; import { expect } from "./getSchedule/expects"; import { setupAndTeardown } from "./getSchedule/setupAndTeardown"; import { timeTravelToTheBeginningOfToday } from "./getSchedule/utils"; vi.mock("@calcom/lib/constants", () => ({ IS_PRODUCTION: true, WEBAPP_URL: "http://localhost:3000", RESERVED_SUBDOMAINS: ["auth", "docs"], })); describe("getSchedule", () => { setupAndTeardown(); 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["google-calendar"]], }; // An event with one accepted booking await 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("Round robin lead skip - CRM", async () => { test("correctly get slots for event with only round robin hosts", async () => { vi.setSystemTime("2024-05-21T00:00:13Z"); const plus1DateString = "2024-05-22"; const plus2DateString = "2024-05-23"; const crmCredential = { id: 1, type: "salesforce_crm", key: { clientId: "test-client-id", }, userId: 1, teamId: null, appId: "salesforce", invalid: false, user: { email: "test@test.com" }, }; await createCredentials([crmCredential]); mockCrmApp("salesforce", { getContacts: [ { id: "contact-id", email: "test@test.com", ownerEmail: "example@example.com", }, ], createContacts: [{ id: "contact-id", email: "test@test.com" }], }); await createBookingScenario({ eventTypes: [ { id: 1, slotInterval: 60, length: 60, hosts: [ { userId: 101, isFixed: false, }, { userId: 102, isFixed: false, }, ], schedulingType: "ROUND_ROBIN", metadata: { apps: { salesforce: { enabled: true, appCategories: ["crm"], roundRobinLeadSkip: true, }, }, }, }, ], users: [ { ...TestData.users.example, email: "example@example.com", id: 101, schedules: [TestData.schedules.IstEveningShift], }, { ...TestData.users.example, email: "example1@example.com", id: 102, schedules: [TestData.schedules.IstMorningShift], defaultScheduleId: 2, }, ], bookings: [], }); const scheduleWithLeadSkip = await getSchedule({ input: { eventTypeId: 1, eventTypeSlug: "", startTime: `${plus1DateString}T18:30:00.000Z`, endTime: `${plus2DateString}T18:29:59.999Z`, timeZone: Timezones["+5:30"], isTeamEvent: true, bookerEmail: "test@test.com", }, }); expect(scheduleWithLeadSkip.teamMember).toBe("example@example.com"); // only slots where example@example.com is available expect(scheduleWithLeadSkip).toHaveTimeSlots( [`11:30:00.000Z`, `12:30:00.000Z`, `13:30:00.000Z`, `14:30:00.000Z`, `15:30:00.000Z`], { dateString: plus2DateString, } ); const scheduleWithoutLeadSkip = await getSchedule({ input: { eventTypeId: 1, eventTypeSlug: "", startTime: `${plus1DateString}T18:30:00.000Z`, endTime: `${plus2DateString}T18:29:59.999Z`, timeZone: Timezones["+5:30"], isTeamEvent: true, bookerEmail: "testtest@test.com", }, }); expect(scheduleWithoutLeadSkip.teamMember).toBe(undefined); // slots where either one of the rr hosts is available expect(scheduleWithoutLeadSkip).toHaveTimeSlots( [ `04:30:00.000Z`, `05:30:00.000Z`, `06:30:00.000Z`, `07:30:00.000Z`, `08:30:00.000Z`, `09:30:00.000Z`, `10:30:00.000Z`, `11:30:00.000Z`, `12:30:00.000Z`, `13:30:00.000Z`, `14:30:00.000Z`, `15:30:00.000Z`, ], { dateString: plus2DateString, } ); }); test("correctly get slots for event with round robin and fixed hosts", async () => { const { dateString: plus1DateString } = getDate({ dateIncrement: 1 }); const { dateString: plus2DateString } = getDate({ dateIncrement: 2 }); const crmCredential = { id: 1, type: "salesforce_crm", key: { clientId: "test-client-id", }, userId: 1, teamId: null, appId: "salesforce", invalid: false, user: { email: "test@test.com" }, }; await createCredentials([crmCredential]); mockCrmApp("salesforce", { getContacts: [ { id: "contact-id", email: "test@test.com", ownerEmail: "example@example.com", }, { id: "contact-id-1", email: "test1@test.com", ownerEmail: "example1@example.com", }, ], createContacts: [{ id: "contact-id", email: "test@test.com" }], }); await createBookingScenario({ eventTypes: [ { id: 1, slotInterval: 60, length: 60, hosts: [ { userId: 101, isFixed: true, }, { userId: 102, isFixed: false, }, { userId: 103, isFixed: false, }, ], schedulingType: "ROUND_ROBIN", metadata: { apps: { salesforce: { enabled: true, appCategories: ["crm"], roundRobinLeadSkip: true, }, }, }, }, ], users: [ { ...TestData.users.example, email: "example@example.com", id: 101, schedules: [TestData.schedules.IstMidShift], }, { ...TestData.users.example, email: "example1@example.com", id: 102, schedules: [TestData.schedules.IstMorningShift], defaultScheduleId: 2, }, { ...TestData.users.example, email: "example2@example.com", id: 103, schedules: [TestData.schedules.IstEveningShift], defaultScheduleId: 3, }, ], bookings: [], }); const scheduleFixedHostLead = await getSchedule({ input: { eventTypeId: 1, eventTypeSlug: "", startTime: `${plus1DateString}T18:30:00.000Z`, endTime: `${plus2DateString}T18:29:59.999Z`, timeZone: Timezones["+5:30"], isTeamEvent: true, bookerEmail: "test@test.com", }, }); expect(scheduleFixedHostLead.teamMember).toBe("example@example.com"); // show normal slots, example@example + one RR host needs to be available expect(scheduleFixedHostLead).toHaveTimeSlots( [ `07:30:00.000Z`, `08:30:00.000Z`, `09:30:00.000Z`, `10:30:00.000Z`, `11:30:00.000Z`, `12:30:00.000Z`, `13:30:00.000Z`, ], { dateString: plus2DateString, } ); const scheduleRRHostLead = await getSchedule({ input: { eventTypeId: 1, eventTypeSlug: "", startTime: `${plus1DateString}T18:30:00.000Z`, endTime: `${plus2DateString}T18:29:59.999Z`, timeZone: Timezones["+5:30"], isTeamEvent: true, bookerEmail: "test1@test.com", }, }); expect(scheduleRRHostLead.teamMember).toBe("example1@example.com"); // slots where example@example (fixed host) + example1@example.com are available together expect(scheduleRRHostLead).toHaveTimeSlots( [`07:30:00.000Z`, `08:30:00.000Z`, `09:30:00.000Z`, `10:30:00.000Z`, `11:30: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 await 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 () => { await 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, } ); }); test("minimumBookingNotice is respected", async () => { await createBookingScenario({ eventTypes: [ { id: 1, length: 2 * 60, minimumBookingNotice: 13 * 60, // Would take the minimum bookable time to be 18:30UTC+13 = 7:30AM UTC users: [ { id: 101, }, ], }, { id: 2, length: 2 * 60, 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 }); // Time Travel to the beginning of today after getting all the dates correctly. timeTravelToTheBeginningOfToday({ utcOffsetInHours: 5.5 }); 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 which is 13hrs from 18:30*/ `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 which is 10hrs from 18:30 */ `05:00:00.000Z`, `07:00:00.000Z`, `09:00:00.000Z`, ], { dateString: todayDateString, } ); }); 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["google-calendar"]], }; await 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["google-calendar"]], }; await 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["google-calendar"]], }; await 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)], }, ], }; await 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 }); await createBookingScenario({ eventTypes: [ // A Collective Event Type hosted by this user { id: 1, slotInterval: 45, schedulingType: "COLLECTIVE", hosts: [ { userId: 101, }, { userId: 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, } ); }); test("test that booking limit is working correctly if user is all day available", async () => { const { dateString: plus1DateString } = getDate({ dateIncrement: 1 }); const { dateString: plus2DateString } = getDate({ dateIncrement: 2 }); const { dateString: plus3DateString } = getDate({ dateIncrement: 3 }); const scenarioData = { eventTypes: [ { id: 1, length: 60, beforeEventBuffer: 0, afterEventBuffer: 0, bookingLimits: { PER_DAY: 1, }, users: [ { id: 101, }, ], }, { id: 2, length: 60, beforeEventBuffer: 0, afterEventBuffer: 0, bookingLimits: { PER_DAY: 2, }, users: [ { id: 101, }, ], }, ], users: [ { ...TestData.users.example, id: 101, schedules: [ { id: 1, name: "All Day available", availability: [ { userId: null, eventTypeId: null, days: [0, 1, 2, 3, 4, 5, 6], startTime: new Date("1970-01-01T00:00:00.000Z"), endTime: new Date("1970-01-01T23:59:59.999Z"), date: null, }, ], timeZone: Timezones["+6:00"], }, ], }, ], // One bookings for each(E1 and E2) on plus2Date bookings: [ { userId: 101, eventTypeId: 1, startTime: `${plus2DateString}T08:00:00.000Z`, endTime: `${plus2DateString}T09:00:00.000Z`, status: "ACCEPTED" as BookingStatus, }, { userId: 101, eventTypeId: 2, startTime: `${plus2DateString}T08:00:00.000Z`, endTime: `${plus2DateString}T09:00:00.000Z`, status: "ACCEPTED" as BookingStatus, }, ], }; await createBookingScenario(scenarioData); const thisUserAvailabilityBookingLimitOne = await getSchedule({ input: { eventTypeId: 1, eventTypeSlug: "", startTime: `${plus1DateString}T00:00:00.000Z`, endTime: `${plus3DateString}T23:59:59.999Z`, timeZone: Timezones["+6:00"], isTeamEvent: false, }, }); const thisUserAvailabilityBookingLimitTwo = await getSchedule({ input: { eventTypeId: 2, eventTypeSlug: "", startTime: `${plus1DateString}T00:00:00.000Z`, endTime: `${plus3DateString}T23:59:59.999Z`, timeZone: Timezones["+6:00"], isTeamEvent: false, }, }); let availableSlotsInTz: dayjs.Dayjs[] = []; for (const date in thisUserAvailabilityBookingLimitOne.slots) { thisUserAvailabilityBookingLimitOne.slots[date].forEach((timeObj) => { availableSlotsInTz.push(dayjs(timeObj.time).tz(Timezones["+6:00"])); }); } expect(availableSlotsInTz.filter((slot) => slot.format().startsWith(plus2DateString)).length).toBe(0); // 1 booking per day as limit availableSlotsInTz = []; for (const date in thisUserAvailabilityBookingLimitTwo.slots) { thisUserAvailabilityBookingLimitTwo.slots[date].forEach((timeObj) => { availableSlotsInTz.push(dayjs(timeObj.time).tz(Timezones["+6:00"])); }); } expect(availableSlotsInTz.filter((slot) => slot.format().startsWith(plus2DateString)).length).toBe(23); // 2 booking per day as limit, only one booking on that }); }); 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 }); await 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 }); await 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 } ); }); test("getSchedule can get slots of org's member event type when orgSlug, eventTypeSlug passed as input", async () => { const org = await createOrganization({ name: "acme", slug: "acme" }); const organizer = getOrganizer({ name: "Organizer", email: "organizer@example.com", id: 101, // So, that it picks the first schedule from the list defaultScheduleId: null, organizationId: org.id, // Has morning shift with some overlap with morning shift schedules: [TestData.schedules.IstWorkHours], }); const scenario = await createBookingScenario( getScenarioData({ eventTypes: [ { id: 1, slotInterval: 45, length: 45, users: [ { id: 101, }, ], }, ], organizer, }) ); const { dateString: plus1DateString } = getDate({ dateIncrement: 1 }); const { dateString: plus2DateString } = getDate({ dateIncrement: 2 }); const getScheduleRes = await getSchedule({ input: { eventTypeSlug: scenario.eventTypes[0]?.slug, startTime: `${plus1DateString}T18:30:00.000Z`, endTime: `${plus2DateString}T18:29:59.999Z`, timeZone: Timezones["+5:30"], isTeamEvent: false, orgSlug: "acme", usernameList: [organizer.username], }, }); expect(getScheduleRes).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 } ); }); }); });