import CalendarManagerMock from "../../../../tests/libs/__mocks__/CalendarManager"; import { constantsScenarios } from "@calcom/lib/__mocks__/constants"; 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 { getAvailableSlotsService } from "@calcom/lib/di/containers/AvailableSlots"; import { SchedulingType, type BookingStatus } from "@calcom/prisma/enums"; import { expect, expectedSlotsForSchedule } from "./getSchedule/expects"; import { setupAndTeardown } from "./getSchedule/setupAndTeardown"; import { timeTravelToTheBeginningOfToday } from "./getSchedule/utils"; constantsScenarios.set({ IS_PRODUCTION: true as any, WEBAPP_URL: "http://localhost:3000", RESERVED_SUBDOMAINS: ["auth", "docs"] as any, SINGLE_ORG_SLUG: "" as any, }); describe("getSchedule", () => { const availableSlotsService = getAvailableSlotsService(); setupAndTeardown(); // TODO: Move these inside describe('Team Event') describe("Round robin lead skip(i.e. use contact owner specified by teamMemberEmail) - 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 availableSlotsService.getAvailableSlots({ input: { eventTypeId: 1, eventTypeSlug: "", startTime: `${plus1DateString}T18:30:00.000Z`, endTime: `${plus2DateString}T18:29:59.999Z`, timeZone: Timezones["+5:30"], isTeamEvent: true, teamMemberEmail: "example@example.com", orgSlug: null, }, }); // 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 availableSlotsService.getAvailableSlots({ input: { eventTypeId: 1, eventTypeSlug: "", startTime: `${plus1DateString}T18:30:00.000Z`, endTime: `${plus2DateString}T18:29:59.999Z`, timeZone: Timezones["+5:30"], isTeamEvent: true, orgSlug: null, }, }); // 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 () => { 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", }, { 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 availableSlotsService.getAvailableSlots({ input: { eventTypeId: 1, eventTypeSlug: "", startTime: `${plus1DateString}T18:30:00.000Z`, endTime: `${plus2DateString}T18:29:59.999Z`, timeZone: Timezones["+5:30"], isTeamEvent: true, teamMemberEmail: "example@example.com", orgSlug: null, }, }); // 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 availableSlotsService.getAvailableSlots({ input: { eventTypeId: 1, eventTypeSlug: "", startTime: `${plus1DateString}T18:30:00.000Z`, endTime: `${plus2DateString}T18:29:59.999Z`, timeZone: Timezones["+5:30"], isTeamEvent: true, teamMemberEmail: "example1@example.com", orgSlug: null, }, }); // 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, } ); }); test("correctly get slots when contact owner has no availability in first two weeks", async () => { vi.setSystemTime("2024-05-01T00:00:13Z"); const startTime = "2024-05-01"; const endTime = "2024-06-01"; const dateWithinTwoWeeks = "2024-05-10"; const dateAfterTwoWeeks = "2024-05-22"; 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.IstWorkHoursWithFirstTwoWeeksUnavailable(startTime)], }, { ...TestData.users.example, email: "example1@example.com", id: 102, schedules: [TestData.schedules.IstMorningShift], defaultScheduleId: 2, }, ], bookings: [], }); const scheduleWithLeadSkip = await availableSlotsService.getAvailableSlots({ input: { eventTypeId: 1, eventTypeSlug: "", startTime: `${startTime}T18:30:00.000Z`, endTime: `${endTime}T18:29:59.999Z`, timeZone: Timezones["+5:30"], isTeamEvent: true, teamMemberEmail: "example@example.com", orgSlug: null, }, }); // example@example.com isn't available in the first two weeks, so we fallback to all hosts expect(scheduleWithLeadSkip).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: dateWithinTwoWeeks, } ); // slots from from both hosts because example@example.com isn't available in the first two weeks, so we fallback to all hosts expect(scheduleWithLeadSkip).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: dateAfterTwoWeeks, } ); }); test("correctly get slots for event when contact owner has no availability for the whole month but is available the first two weeks", async () => { vi.setSystemTime("2024-05-01T00:00:13Z"); const startTime = "2024-07-01"; const endTime = "2024-07-31"; 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.IstNotAvailableForFullMonth("2024-07")], // not available full July }, { ...TestData.users.example, email: "example1@example.com", id: 102, schedules: [TestData.schedules.IstMorningShift], defaultScheduleId: 2, }, ], bookings: [], }); const scheduleWithLeadSkip = await availableSlotsService.getAvailableSlots({ input: { eventTypeId: 1, eventTypeSlug: "", startTime: `${startTime}T18:30:00.000Z`, endTime: `${endTime}T18:29:59.999Z`, timeZone: Timezones["+5:30"], isTeamEvent: true, teamMemberEmail: "example@example.com", orgSlug: null, }, }); // slots from example@example.com because even though user isn't available in July, user is available in the first two weeks expect(scheduleWithLeadSkip).toHaveDateDisabled({ dateString: "2024-07-05", }); }); test("correctly get slots for event when contact owner has no availability at all", async () => { vi.setSystemTime("2024-05-01T00:00:13Z"); const startTime = "2024-07-01"; const endTime = "2024-07-31"; 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.EmptyAvailability], }, { ...TestData.users.example, email: "example1@example.com", id: 102, schedules: [TestData.schedules.IstMorningShift], defaultScheduleId: 2, }, ], bookings: [], }); const scheduleWithLeadSkip = await availableSlotsService.getAvailableSlots({ input: { eventTypeId: 1, eventTypeSlug: "", startTime: `${startTime}T18:30:00.000Z`, endTime: `${endTime}T18:29:59.999Z`, timeZone: Timezones["+5:30"], isTeamEvent: true, teamMemberEmail: "example@example.com", orgSlug: null, }, }); // slots from example1@example.com because example@example.com isn't available also not the first two weeks expect(scheduleWithLeadSkip).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`, ], { dateString: "2024-07-05", } ); }); test("correctly gets slot for event when contact owner is available in the first two weeks and the next month is loaded (within the two weeks)", async () => { vi.setSystemTime("2024-05-23T00:00:13Z"); const startTime = "2024-06-01"; const endTime = "2024-06-31"; 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.IstNotAvailableForFullMonth("2024-06")], }, { ...TestData.users.example, email: "example1@example.com", id: 102, schedules: [TestData.schedules.IstMorningShift], defaultScheduleId: 2, }, ], bookings: [], }); const scheduleWithLeadSkip = await availableSlotsService.getAvailableSlots({ input: { eventTypeId: 1, eventTypeSlug: "", startTime: `${startTime}T18:30:00.000Z`, endTime: `${endTime}T18:29:59.999Z`, timeZone: Timezones["+5:30"], isTeamEvent: true, teamMemberEmail: "example@example.com", orgSlug: null, }, }); // slots for example@example.com, because user is available in the first two weeks (prev month) expect(scheduleWithLeadSkip).toHaveDateDisabled({ dateString: "2024-06-03", }); }); test("correctly gets slot for event when contact owner is available in the first two weeks and it's end of the month", async () => { vi.setSystemTime("2024-05-23T00:00:13Z"); const startTime = "2024-05-23"; const endTime = "2024-05-31"; 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.IstNotAvailableForFullMonth("2024-05")], }, { ...TestData.users.example, email: "example1@example.com", id: 102, schedules: [TestData.schedules.IstMorningShift], defaultScheduleId: 2, }, ], bookings: [], }); const scheduleWithLeadSkip = await availableSlotsService.getAvailableSlots({ input: { eventTypeId: 1, eventTypeSlug: "", startTime: `${startTime}T18:30:00.000Z`, endTime: `${endTime}T18:29:59.999Z`, timeZone: Timezones["+5:30"], isTeamEvent: true, teamMemberEmail: "example@example.com", orgSlug: null, }, }); // slots for example@example.com, because user is available in the first two weeks (next month) expect(scheduleWithLeadSkip).toHaveDateDisabled({ dateString: "2024-05-29", }); }); test("Negative case(i.e. skipContactOwner is true) - when teamMemberEmail is provided but not used", 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 scheduleWhenContactOwnerIsSkipped = await availableSlotsService.getAvailableSlots({ input: { eventTypeId: 1, eventTypeSlug: "", startTime: `${plus1DateString}T18:30:00.000Z`, endTime: `${plus2DateString}T18:29:59.999Z`, timeZone: Timezones["+5:30"], isTeamEvent: true, teamMemberEmail: "example@example.com", skipContactOwner: true, orgSlug: null, }, }); // Both users slot would be available as contact owner(example@example.com) is skipped and we fallback to all users of RR expect(scheduleWhenContactOwnerIsSkipped).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, } ); }); }); 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 availableSlotsService.getAvailableSlots({ 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, orgSlug: null, }, }); // 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 availableSlotsService.getAvailableSlots({ input: { eventTypeId: 1, eventTypeSlug: "", startTime: `${plus2DateString}T18:30:00.000Z`, endTime: `${plus3DateString}T18:29:59.999Z`, timeZone: Timezones["+5:30"], isTeamEvent: false, orgSlug: null, }, }); 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 availableSlotsService.getAvailableSlots({ input: { orgSlug: null, 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 availableSlotsService.getAvailableSlots({ input: { orgSlug: null, 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 availableSlotsService.getAvailableSlots({ input: { eventTypeId: 1, eventTypeSlug: "", startTime: `${minus1DateString}T18:30:00.000Z`, endTime: `${todayDateString}T18:29:59.999Z`, timeZone: Timezones["+5:30"], isTeamEvent: false, orgSlug: null, }, }); 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 availableSlotsService.getAvailableSlots({ input: { eventTypeId: 2, eventTypeSlug: "", startTime: `${minus1DateString}T18:30:00.000Z`, endTime: `${todayDateString}T18:29:59.999Z`, timeZone: Timezones["+5:30"], isTeamEvent: false, orgSlug: null, }, }); 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({ success: true, data: [ { 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 availableSlotsService.getAvailableSlots({ input: { eventTypeId: 1, eventTypeSlug: "", startTime: `${plus2DateString}T18:30:00.000Z`, endTime: `${plus3DateString}T18:29:59.999Z`, timeZone: Timezones["+5:30"], isTeamEvent: false, orgSlug: null, }, }); 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({ success: true, data: [ { 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 availableSlotsService.getAvailableSlots({ input: { eventTypeId: 1, eventTypeSlug: "", startTime: `${plus1DateString}T18:30:00.000Z`, endTime: `${plus2DateString}T18:29:59.999Z`, timeZone: Timezones["+5:30"], isTeamEvent: false, orgSlug: null, }, }); 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({ success: true, data: [] }); 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 availableSlotsService.getAvailableSlots({ input: { eventTypeId: 1, eventTypeSlug: "", startTime: `${plus1DateString}T18:30:00.000Z`, endTime: `${plus2DateString}T18:29:59.999Z`, timeZone: Timezones["+5:30"], isTeamEvent: false, orgSlug: null, }, }); 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 availableSlotsService.getAvailableSlots({ input: { eventTypeId: 1, eventTypeSlug: "", startTime: `${plus1DateString}T18:30:00.000Z`, endTime: `${plus2DateString}T18:29:59.999Z`, timeZone: Timezones["+5:30"], isTeamEvent: false, orgSlug: null, }, }); 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: 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 availableSlotsService.getAvailableSlots({ input: { eventTypeId: 2, eventTypeSlug: "", startTime: `${plus1DateString}T18:30:00.000Z`, endTime: `${plus2DateString}T18:29:59.999Z`, timeZone: Timezones["+5:30"], isTeamEvent: false, orgSlug: null, }, }); 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 availableSlotsService.getAvailableSlots({ input: { eventTypeId: 1, eventTypeSlug: "", startTime: `${plus1DateString}T00:00:00.000Z`, endTime: `${plus3DateString}T23:59:59.999Z`, timeZone: Timezones["+6:00"], isTeamEvent: false, orgSlug: null, }, }); const thisUserAvailabilityBookingLimitTwo = await availableSlotsService.getAvailableSlots({ input: { eventTypeId: 2, eventTypeSlug: "", startTime: `${plus1DateString}T00:00:00.000Z`, endTime: `${plus3DateString}T23:59:59.999Z`, timeZone: Timezones["+6:00"], isTeamEvent: false, orgSlug: null, }, }); 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 }); test("test that duration 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, durationLimits: { PER_DAY: 120, // 2 hours per day }, users: [ { id: 101, }, ], }, { id: 2, length: 60, beforeEventBuffer: 0, afterEventBuffer: 0, durationLimits: { PER_DAY: 240, // 4 hours per day }, 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"], }, ], }, ], bookings: [ { userId: 101, eventTypeId: 1, startTime: `${plus2DateString}T08:00:00.000Z`, endTime: `${plus2DateString}T09:00:00.000Z`, status: "ACCEPTED" as BookingStatus, }, { userId: 101, eventTypeId: 1, startTime: `${plus2DateString}T10:00:00.000Z`, endTime: `${plus2DateString}T11:00:00.000Z`, status: "ACCEPTED" as BookingStatus, }, { userId: 101, eventTypeId: 2, startTime: `${plus2DateString}T12:00:00.000Z`, endTime: `${plus2DateString}T13:00:00.000Z`, status: "ACCEPTED" as BookingStatus, }, ], }; await createBookingScenario(scenarioData); const thisUserAvailabilityDurationLimitReached = await availableSlotsService.getAvailableSlots({ input: { eventTypeId: 1, eventTypeSlug: "", startTime: `${plus1DateString}T00:00:00.000Z`, endTime: `${plus3DateString}T23:59:59.999Z`, timeZone: Timezones["+6:00"], isTeamEvent: false, orgSlug: null, }, }); const thisUserAvailabilityDurationLimitNotReached = await availableSlotsService.getAvailableSlots({ input: { eventTypeId: 2, eventTypeSlug: "", startTime: `${plus1DateString}T00:00:00.000Z`, endTime: `${plus3DateString}T23:59:59.999Z`, timeZone: Timezones["+6:00"], isTeamEvent: false, orgSlug: null, }, }); let availableSlotsInTz: dayjs.Dayjs[] = []; for (const date in thisUserAvailabilityDurationLimitReached.slots) { thisUserAvailabilityDurationLimitReached.slots[date].forEach((timeObj) => { availableSlotsInTz.push(dayjs(timeObj.time).tz(Timezones["+6:00"])); }); } expect(availableSlotsInTz.filter((slot) => slot.format().startsWith(plus2DateString)).length).toBe(0); availableSlotsInTz = []; for (const date in thisUserAvailabilityDurationLimitNotReached.slots) { thisUserAvailabilityDurationLimitNotReached.slots[date].forEach((timeObj) => { availableSlotsInTz.push(dayjs(timeObj.time).tz(Timezones["+6:00"])); }); } expect( availableSlotsInTz.filter((slot) => slot.format().startsWith(plus2DateString)).length ).toBeGreaterThan(0); }); test("test that duration limit is working correctly with different timezone", 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, durationLimits: { PER_DAY: 120, // 2 hours per day }, 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"], }, ], }, ], bookings: [ { userId: 101, eventTypeId: 1, startTime: `${plus2DateString}T08:00:00.000Z`, endTime: `${plus2DateString}T09:00:00.000Z`, status: "ACCEPTED" as BookingStatus, }, { userId: 101, eventTypeId: 1, startTime: `${plus2DateString}T10:00:00.000Z`, endTime: `${plus2DateString}T11:00:00.000Z`, status: "ACCEPTED" as BookingStatus, }, ], }; await createBookingScenario(scenarioData); const thisUserAvailabilityDurationLimit = await availableSlotsService.getAvailableSlots({ input: { eventTypeId: 1, eventTypeSlug: "", startTime: `${plus1DateString}T00:00:00.000Z`, endTime: `${plus3DateString}T23:59:59.999Z`, timeZone: Timezones["-11:00"], // attendee timezone isTeamEvent: false, orgSlug: null, }, }); const availableSlotsInTz: dayjs.Dayjs[] = []; for (const date in thisUserAvailabilityDurationLimit.slots) { thisUserAvailabilityDurationLimit.slots[date].forEach((timeObj) => { availableSlotsInTz.push(dayjs(timeObj.time).tz(Timezones["+6:00"])); }); } expect(availableSlotsInTz.filter((slot) => slot.format().startsWith(plus2DateString)).length).toBe(0); }); test.skip("test that PER_WEEK duration limits work correctly", async () => { const { dateString: plus1DateString } = getDate({ dateIncrement: 1 }); const { dateString: plus2DateString } = getDate({ dateIncrement: 2 }); const { dateString: plus3DateString } = getDate({ dateIncrement: 3 }); const { dateString: plus4DateString } = getDate({ dateIncrement: 4 }); const { dateString: plus5DateString } = getDate({ dateIncrement: 5 }); const scenarioData = { eventTypes: [ { id: 1, length: 60, beforeEventBuffer: 0, afterEventBuffer: 0, durationLimits: { PER_WEEK: 180, // 3 hours per week }, 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"], }, ], }, ], bookings: [ { userId: 101, eventTypeId: 1, startTime: `${plus1DateString}T08:00:00.000Z`, endTime: `${plus1DateString}T09:00:00.000Z`, status: "ACCEPTED" as BookingStatus, }, { userId: 101, eventTypeId: 1, startTime: `${plus2DateString}T10:00:00.000Z`, endTime: `${plus2DateString}T11:00:00.000Z`, status: "ACCEPTED" as BookingStatus, }, { userId: 101, eventTypeId: 1, startTime: `${plus3DateString}T12:00:00.000Z`, endTime: `${plus3DateString}T13:00:00.000Z`, status: "ACCEPTED" as BookingStatus, }, ], }; await createBookingScenario(scenarioData); const weeklyAvailability = await availableSlotsService.getAvailableSlots({ input: { eventTypeId: 1, eventTypeSlug: "", startTime: `${plus1DateString}T00:00:00.000Z`, endTime: `${plus5DateString}T23:59:59.999Z`, timeZone: Timezones["+6:00"], isTeamEvent: false, orgSlug: null, }, }); const availableSlotsInTz: dayjs.Dayjs[] = []; for (const date in weeklyAvailability.slots) { weeklyAvailability.slots[date].forEach((timeObj) => { availableSlotsInTz.push(dayjs(timeObj.time).tz(Timezones["+6:00"])); }); } expect( availableSlotsInTz.filter( (slot) => slot.format().startsWith(plus1DateString) || slot.format().startsWith(plus2DateString) || slot.format().startsWith(plus3DateString) || slot.format().startsWith(plus4DateString) ).length ).toBe(0); }); test.skip("global team duration limit blocks slots if one fixed host reached limit", 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, team: { id: 1, durationLimits: { PER_DAY: 120 }, // 2 hours per day }, schedulingType: SchedulingType.COLLECTIVE, users: [ { id: 101, }, { id: 102, }, ], }, { id: 2, length: 60, beforeEventBuffer: 0, afterEventBuffer: 0, team: { id: 1, durationLimits: { PER_DAY: 120 }, // 2 hours per day }, schedulingType: SchedulingType.COLLECTIVE, users: [ { id: 101, }, { id: 102, }, ], }, { id: 3, length: 60, beforeEventBuffer: 0, afterEventBuffer: 0, 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"], }, ], }, ], bookings: [ { userId: 101, eventTypeId: 1, startTime: `${plus2DateString}T08:00:00.000Z`, endTime: `${plus2DateString}T09:00:00.000Z`, status: "ACCEPTED" as BookingStatus, }, { userId: 101, eventTypeId: 1, startTime: `${plus2DateString}T10:00:00.000Z`, endTime: `${plus2DateString}T11:00:00.000Z`, status: "ACCEPTED" as BookingStatus, }, ], }; await createBookingScenario(scenarioData); const availabilityEventTypeOne = await availableSlotsService.getAvailableSlots({ input: { eventTypeId: 1, eventTypeSlug: "", startTime: `${plus1DateString}T00:00:00.000Z`, endTime: `${plus3DateString}T23:59:59.999Z`, timeZone: Timezones["+6:00"], isTeamEvent: false, orgSlug: null, }, }); const availableSlotsInTz: dayjs.Dayjs[] = []; for (const date in availabilityEventTypeOne.slots) { availabilityEventTypeOne.slots[date].forEach((timeObj) => { availableSlotsInTz.push(dayjs(timeObj.time).tz(Timezones["+6:00"])); }); } expect(availableSlotsInTz.filter((slot) => slot.format().startsWith(plus2DateString)).length).toBe(0); const availabilityEventTypeTwo = await availableSlotsService.getAvailableSlots({ input: { eventTypeId: 2, eventTypeSlug: "", startTime: `${plus1DateString}T00:00:00.000Z`, endTime: `${plus3DateString}T23:59:59.999Z`, timeZone: Timezones["+6:00"], isTeamEvent: false, orgSlug: null, }, }); const availableSlotsInTz2: dayjs.Dayjs[] = []; for (const date in availabilityEventTypeTwo.slots) { availabilityEventTypeTwo.slots[date].forEach((timeObj) => { availableSlotsInTz2.push(dayjs(timeObj.time).tz(Timezones["+6:00"])); }); } expect(availableSlotsInTz2.filter((slot) => slot.format().startsWith(plus2DateString)).length).toBe(0); const availabilityUserEventType = await availableSlotsService.getAvailableSlots({ input: { eventTypeId: 3, eventTypeSlug: "", startTime: `${plus1DateString}T00:00:00.000Z`, endTime: `${plus3DateString}T23:59:59.999Z`, timeZone: Timezones["+6:00"], isTeamEvent: false, orgSlug: null, }, }); const availableSlotsInTz3: dayjs.Dayjs[] = []; for (const date in availabilityUserEventType.slots) { availabilityUserEventType.slots[date].forEach((timeObj) => { availableSlotsInTz3.push(dayjs(timeObj.time).tz(Timezones["+6:00"])); }); } expect( availableSlotsInTz3.filter((slot) => slot.format().startsWith(plus2DateString)).length ).toBeGreaterThan(0); }); test.skip("test that combined booking and duration limits work correctly", 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: 3, // 3 bookings per day }, durationLimits: { PER_DAY: 120, // 2 hours per day }, users: [ { id: 101, }, ], }, { id: 2, length: 30, // 30 minute event beforeEventBuffer: 0, afterEventBuffer: 0, bookingLimits: { PER_DAY: 2, // 2 bookings per day }, durationLimits: { PER_DAY: 120, // 2 hours per day }, 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"], }, ], }, ], bookings: [ { userId: 101, eventTypeId: 1, startTime: `${plus2DateString}T08:00:00.000Z`, endTime: `${plus2DateString}T09:00:00.000Z`, status: "ACCEPTED" as BookingStatus, }, { userId: 101, eventTypeId: 1, startTime: `${plus2DateString}T10:00:00.000Z`, endTime: `${plus2DateString}T11:00:00.000Z`, status: "ACCEPTED" as BookingStatus, }, { userId: 101, eventTypeId: 2, startTime: `${plus2DateString}T12:00:00.000Z`, endTime: `${plus2DateString}T12:30:00.000Z`, status: "ACCEPTED" as BookingStatus, }, ], }; await createBookingScenario(scenarioData); const eventType1Availability = await availableSlotsService.getAvailableSlots({ input: { eventTypeId: 1, eventTypeSlug: "", startTime: `${plus1DateString}T00:00:00.000Z`, endTime: `${plus3DateString}T23:59:59.999Z`, timeZone: Timezones["+6:00"], isTeamEvent: false, orgSlug: null, }, }); const eventType2Availability = await availableSlotsService.getAvailableSlots({ input: { eventTypeId: 2, eventTypeSlug: "", startTime: `${plus1DateString}T00:00:00.000Z`, endTime: `${plus3DateString}T23:59:59.999Z`, timeZone: Timezones["+6:00"], isTeamEvent: false, orgSlug: null, }, }); let availableSlotsInTz: dayjs.Dayjs[] = []; for (const date in eventType1Availability.slots) { eventType1Availability.slots[date].forEach((timeObj) => { availableSlotsInTz.push(dayjs(timeObj.time).tz(Timezones["+6:00"])); }); } expect(availableSlotsInTz.filter((slot) => slot.format().startsWith(plus2DateString)).length).toBe(0); availableSlotsInTz = []; for (const date in eventType2Availability.slots) { eventType2Availability.slots[date].forEach((timeObj) => { availableSlotsInTz.push(dayjs(timeObj.time).tz(Timezones["+6:00"])); }); } expect( availableSlotsInTz.filter((slot) => slot.format().startsWith(plus2DateString)).length ).toBeGreaterThan(0); // Create a new booking scenario with an additional booking await createBookingScenario({ eventTypes: scenarioData.eventTypes, users: scenarioData.users, bookings: [ ...scenarioData.bookings, { userId: 101, eventTypeId: 2, startTime: `${plus2DateString}T14:00:00.000Z`, endTime: `${plus2DateString}T14:30:00.000Z`, status: "ACCEPTED" as BookingStatus, }, ], }); const eventType2AvailabilityUpdated = await availableSlotsService.getAvailableSlots({ input: { eventTypeId: 2, eventTypeSlug: "", startTime: `${plus1DateString}T00:00:00.000Z`, endTime: `${plus3DateString}T23:59:59.999Z`, timeZone: Timezones["+6:00"], isTeamEvent: false, orgSlug: null, }, }); availableSlotsInTz = []; for (const date in eventType2AvailabilityUpdated.slots) { eventType2AvailabilityUpdated.slots[date].forEach((timeObj) => { availableSlotsInTz.push(dayjs(timeObj.time).tz(Timezones["+6:00"])); }); } expect(availableSlotsInTz.filter((slot) => slot.format().startsWith(plus2DateString)).length).toBe(0); }); test("test that booking limit is working correctly if user is all day available and attendee is in different timezone than host", 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, }, ], }, ], 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, }, ], }; await createBookingScenario(scenarioData); const thisUserAvailabilityBookingLimit = await availableSlotsService.getAvailableSlots({ input: { eventTypeId: 1, eventTypeSlug: "", startTime: `${plus1DateString}T00:00:00.000Z`, endTime: `${plus3DateString}T23:59:59.999Z`, timeZone: Timezones["-11:00"], //attendee timezone isTeamEvent: false, orgSlug: null, }, }); const availableSlotsInTz: dayjs.Dayjs[] = []; for (const date in thisUserAvailabilityBookingLimit.slots) { thisUserAvailabilityBookingLimit.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 }); test("global team booking limit block slot if one fixed host reached limit", 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, team: { id: 1, bookingLimits: { PER_DAY: 1 }, }, schedulingType: SchedulingType.COLLECTIVE, users: [ { id: 101, }, { id: 102, }, ], }, { id: 2, length: 60, beforeEventBuffer: 0, afterEventBuffer: 0, team: { id: 1, bookingLimits: { PER_DAY: 1 }, }, schedulingType: SchedulingType.COLLECTIVE, users: [ { id: 101, }, { id: 102, }, ], }, { id: 3, length: 60, beforeEventBuffer: 0, afterEventBuffer: 0, 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"], }, ], }, ], bookings: [ { userId: 101, eventTypeId: 1, startTime: `${plus2DateString}T08:00:00.000Z`, endTime: `${plus2DateString}T09:00:00.000Z`, status: "ACCEPTED" as BookingStatus, }, ], }; await createBookingScenario(scenarioData); const availabilityEventTypeOne = await availableSlotsService.getAvailableSlots({ input: { eventTypeId: 1, eventTypeSlug: "", startTime: `${plus1DateString}T00:00:00.000Z`, endTime: `${plus3DateString}T23:59:59.999Z`, timeZone: Timezones["+6:00"], isTeamEvent: false, orgSlug: null, }, }); const availableSlotsInTz: dayjs.Dayjs[] = []; for (const date in availabilityEventTypeOne.slots) { availabilityEventTypeOne.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 const availabilityEventTypeTwo = await availableSlotsService.getAvailableSlots({ input: { eventTypeId: 2, eventTypeSlug: "", startTime: `${plus1DateString}T00:00:00.000Z`, endTime: `${plus3DateString}T23:59:59.999Z`, timeZone: Timezones["+6:00"], isTeamEvent: false, orgSlug: null, }, }); for (const date in availabilityEventTypeTwo.slots) { availabilityEventTypeTwo.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 const availabilityUserEventType = await availableSlotsService.getAvailableSlots({ input: { eventTypeId: 3, eventTypeSlug: "", startTime: `${plus1DateString}T00:00:00.000Z`, endTime: `${plus3DateString}T23:59:59.999Z`, timeZone: Timezones["+6:00"], isTeamEvent: false, orgSlug: null, }, }); for (const date in availabilityUserEventType.slots) { availabilityUserEventType.slots[date].forEach((timeObj) => { availableSlotsInTz.push(dayjs(timeObj.time).tz(Timezones["+6:00"])); }); } expect(availableSlotsInTz.filter((slot) => slot.format().startsWith(plus2DateString)).length).toBe(23); }); test("global team booking limit blocks correct slots if attendee and host are in different timezone", 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, team: { id: 1, bookingLimits: { PER_DAY: 1 }, }, schedulingType: SchedulingType.COLLECTIVE, users: [ { id: 101, }, { id: 102, }, ], }, ], 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"], }, ], }, ], bookings: [ { userId: 101, eventTypeId: 1, startTime: `${plus2DateString}T08:00:00.000Z`, endTime: `${plus2DateString}T09:00:00.000Z`, status: "ACCEPTED" as BookingStatus, }, ], }; await createBookingScenario(scenarioData); const thisUserAvailabilityBookingLimit = await availableSlotsService.getAvailableSlots({ input: { eventTypeId: 1, eventTypeSlug: "", startTime: `${plus1DateString}T00:00:00.000Z`, endTime: `${plus3DateString}T23:59:59.999Z`, timeZone: Timezones["-11:00"], // attendee timezone isTeamEvent: false, orgSlug: null, }, }); const availableSlotsInTz: dayjs.Dayjs[] = []; for (const date in thisUserAvailabilityBookingLimit.slots) { thisUserAvailabilityBookingLimit.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 }); test("a slot counts as being busy when the eventType is requiresConfirmation and requiresConfirmationWillBlockSlot", async () => { const { dateString: plus1DateString } = getDate({ dateIncrement: 1 }); const { dateString: plus2DateString } = getDate({ dateIncrement: 2 }); await createBookingScenario({ eventTypes: [ // A default Event Type which this user owns { id: 2, length: 15, slotInterval: 45, users: [{ id: 101 }], requiresConfirmation: true, requiresConfirmationWillBlockSlot: true, }, { id: 3, length: 15, slotInterval: 45, users: [{ id: 101 }], requiresConfirmation: true, requiresConfirmationWillBlockSlot: false, }, ], users: [ { ...TestData.users.example, id: 101, schedules: [TestData.schedules.IstWorkHours], }, ], bookings: [ { userId: 101, attendees: [ { email: "IntegrationTestUser102@example.com", }, ], eventTypeId: 2, status: "PENDING", startTime: `${plus2DateString}T04:00:00.000Z`, endTime: `${plus2DateString}T04:15:00.000Z`, }, { userId: 101, attendees: [ { email: "IntegrationTestUser103@example.com", }, ], eventTypeId: 3, status: "PENDING", startTime: `${plus2DateString}T05:00:00.000Z`, endTime: `${plus2DateString}T05:15:00.000Z`, }, ], }); // Requesting this user's availability for their // individual Event Type const thisUserAvailability = await availableSlotsService.getAvailableSlots({ input: { eventTypeId: 2, eventTypeSlug: "", startTime: `${plus1DateString}T18:30:00.000Z`, endTime: `${plus2DateString}T18:29:59.999Z`, timeZone: Timezones["+5:30"], isTeamEvent: false, orgSlug: null, }, }); expect(thisUserAvailability).toHaveTimeSlots( [ // `04:00:00.000Z`, // <- This slot should be occupied by the Pending Requires Confirmation booking blocking this slot `04:15:00.000Z`, `05:00:00.000Z`, // <- This slot should be available to book as this event type (id: 3) has requires confirmation without blocking the slot `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("Edge Case: Same day OOO entries that could accidentally be considered in past shouldn't crash the getSchedule", async () => { // Set a fixed date for testing vi.setSystemTime("2024-05-21T00:00:13Z"); const { dateString: plus1DateString } = getDate({ dateIncrement: 1 }); const { dateString: todayDateString } = getDate({ dateIncrement: 0 }); const { dateString: plus2DateString } = getDate({ dateIncrement: 2 }); await createBookingScenario({ eventTypes: [ { id: 1, slotInterval: 45, length: 45, users: [ { id: 101, }, ], }, ], users: [ { ...TestData.users.example, id: 101, schedules: [TestData.schedules.IstWorkHours], outOfOffice: { dateRanges: [ { start: `${todayDateString}T00:00:00.000Z`, end: `${todayDateString}T23:59:59.999Z`, }, ], }, }, ], }); const schedule = await availableSlotsService.getAvailableSlots({ input: { eventTypeId: 1, eventTypeSlug: "", startTime: `${plus1DateString}T18:30:00.000Z`, endTime: `${plus2DateString}T18:29:59.999Z`, timeZone: Timezones["+5:30"], isTeamEvent: false, orgSlug: null, }, }); // Verify that we get slots for the requested dates expect(schedule).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, } ); }); }); 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: 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 availableSlotsService.getAvailableSlots({ input: { eventTypeId: 1, eventTypeSlug: "", startTime: `${todayDateString}T18:30:00.000Z`, endTime: `${plus1DateString}T18:29:59.999Z`, timeZone: Timezones["+5:30"], isTeamEvent: true, orgSlug: null, }, }); 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 availableSlotsService.getAvailableSlots({ input: { eventTypeId: 1, eventTypeSlug: "", startTime: `${plus1DateString}T18:30:00.000Z`, endTime: `${plus2DateString}T18:29:59.999Z`, timeZone: Timezones["+5:30"], isTeamEvent: true, orgSlug: null, }, }); // 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 availableSlotsService.getAvailableSlots({ input: { eventTypeId: 1, eventTypeSlug: "", startTime: `${plus1DateString}T18:30:00.000Z`, endTime: `${plus2DateString}T18:29:59.999Z`, timeZone: Timezones["+5:30"], isTeamEvent: true, orgSlug: null, }, }); // 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 availableSlotsService.getAvailableSlots({ input: { eventTypeId: 1, eventTypeSlug: "", startTime: `${plus2DateString}T18:30:00.000Z`, endTime: `${plus3DateString}T18:29:59.999Z`, timeZone: Timezones["+5:30"], isTeamEvent: true, orgSlug: null, }, }); // 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, userId: 101, }, ], organizer, }, { id: org.id } ) ); const { dateString: plus1DateString } = getDate({ dateIncrement: 1 }); const { dateString: plus2DateString } = getDate({ dateIncrement: 2 }); const getScheduleRes = await availableSlotsService.getAvailableSlots({ 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 } ); }); }); describe("Round robin event", async () => { test("correctly get slots for event with round robin hosts chosen schedule (non-default)", async () => { vi.setSystemTime("2024-05-21T00:00:13Z"); const plus1DateString = "2024-05-22"; const plus2DateString = "2024-05-23"; // An event with no common schedule and hosts chosen schedule (non-default schedule) await createBookingScenario({ eventTypes: [ { id: 1, slotInterval: 60, length: 60, hosts: [ { userId: 101, isFixed: false, scheduleId: 2, }, { userId: 102, isFixed: false, scheduleId: 3, }, ], schedulingType: "ROUND_ROBIN", schedule: null, }, ], users: [ { ...TestData.users.example, email: "example@example.com", id: 101, schedules: [TestData.schedules.IstMorningShift, TestData.schedules.IstEveningShift], defaultScheduleId: 1, }, { ...TestData.users.example, email: "example1@example.com", id: 102, schedules: [TestData.schedules.IstEveningShift, TestData.schedules.IstMorningShift], defaultScheduleId: 4, }, ], bookings: [], }); const scheduleWithHostChoosenSch = await availableSlotsService.getAvailableSlots({ input: { eventTypeId: 1, eventTypeSlug: "", startTime: `${plus1DateString}T18:30:00.000Z`, endTime: `${plus2DateString}T18:29:59.999Z`, timeZone: Timezones["+5:30"], isTeamEvent: true, teamMemberEmail: "test@test.com", }, }); // expect only slots of IstEveningShift , as this is the chosen schedule for hosts expect(scheduleWithHostChoosenSch).toHaveTimeSlots( [`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 common schedule", async () => { vi.setSystemTime("2024-05-21T00:00:13Z"); const plus1DateString = "2024-05-22"; const plus2DateString = "2024-05-23"; // An event with common schedule await createBookingScenario({ eventTypes: [ { id: 1, slotInterval: 60, length: 60, hosts: [ { userId: 101, isFixed: false, }, { userId: 102, isFixed: false, }, ], schedule: TestData.schedules.IstEveningShift, schedulingType: "ROUND_ROBIN", }, ], users: [ { ...TestData.users.example, email: "example@example.com", id: 101, schedules: [TestData.schedules.IstMorningShift], defaultScheduleId: 1, }, { ...TestData.users.example, email: "example1@example.com", id: 102, schedules: [TestData.schedules.IstMidShift], defaultScheduleId: 2, }, ], bookings: [], }); const scheduleWithEventCommonSch = await availableSlotsService.getAvailableSlots({ input: { eventTypeId: 1, eventTypeSlug: "", startTime: `${plus1DateString}T18:30:00.000Z`, endTime: `${plus2DateString}T18:29:59.999Z`, timeZone: Timezones["+5:30"], isTeamEvent: true, teamMemberEmail: "test@test.com", }, }); // expect only slots of IstEveningShift , as this is the common schedule of event expect(scheduleWithEventCommonSch).toHaveTimeSlots( [`11:30:00.000Z`, `12:30:00.000Z`, `13:30:00.000Z`, `14:30:00.000Z`, `15:30:00.000Z`], { dateString: plus2DateString, } ); }); test("Reschedule: should show timeslots of the same host if rescheduleWithSameRoundRobinHost is true", async () => { vi.setSystemTime("2024-05-21T00:00:13Z"); const plus1DateString = "2024-05-22"; const plus2DateString = "2024-05-23"; // An event with common schedule await createBookingScenario({ eventTypes: [ { id: 1, slotInterval: 60, length: 60, rescheduleWithSameRoundRobinHost: true, hosts: [ { userId: 101, isFixed: false, }, { userId: 102, isFixed: false, }, ], schedulingType: "ROUND_ROBIN", }, ], users: [ { ...TestData.users.example, email: "example@example.com", id: 101, schedules: [TestData.schedules.IstEveningShift], defaultScheduleId: 1, }, { ...TestData.users.example, email: "example1@example.com", id: 102, schedules: [TestData.schedules.IstMorningShift], defaultScheduleId: 2, }, ], bookings: [ { uid: "BOOKING_TO_RESCHEDULE_UID", userId: 101, attendees: [ { email: "IntegrationTestUser102@example.com", }, ], eventTypeId: 1, status: "ACCEPTED", startTime: `${plus2DateString}T04:00:00.000Z`, endTime: `${plus2DateString}T04:15:00.000Z`, }, ], }); const schedule = await availableSlotsService.getAvailableSlots({ input: { eventTypeId: 1, eventTypeSlug: "", startTime: `${plus1DateString}T18:30:00.000Z`, endTime: `${plus2DateString}T18:29:59.999Z`, timeZone: Timezones["+5:30"], isTeamEvent: true, rescheduleUid: "BOOKING_TO_RESCHEDULE_UID", }, }); // expect only slots of IstEveningShift as this is the slots for the original host of the booking expect(schedule).toHaveTimeSlots( [`11:30:00.000Z`, `12:30:00.000Z`, `13:30:00.000Z`, `14:30:00.000Z`, `15:30:00.000Z`], { dateString: plus2DateString, } ); }); test("Reschedule: should show timeslots as per routedTeamMemberIds(instead of same host) even if rescheduleWithSameRoundRobinHost is true but it is a rerouting scenario", async () => { vi.setSystemTime("2024-05-21T00:00:13Z"); const plus1DateString = "2024-05-22"; const plus2DateString = "2024-05-23"; // An event with common schedule await createBookingScenario({ eventTypes: [ { id: 1, slotInterval: 60, length: 60, rescheduleWithSameRoundRobinHost: true, hosts: [ { userId: 101, isFixed: false, }, { userId: 102, isFixed: false, }, ], schedulingType: "ROUND_ROBIN", }, ], users: [ { ...TestData.users.example, email: "example@example.com", id: 101, schedules: [TestData.schedules.IstEveningShift], defaultScheduleId: 1, }, { ...TestData.users.example, email: "example1@example.com", id: 102, schedules: [TestData.schedules.IstMorningShift], defaultScheduleId: 2, }, ], bookings: [ { uid: "BOOKING_TO_RESCHEDULE_UID", userId: 101, attendees: [ { email: "IntegrationTestUser102@example.com", }, ], eventTypeId: 1, status: "ACCEPTED", startTime: `${plus2DateString}T04:00:00.000Z`, endTime: `${plus2DateString}T04:15:00.000Z`, }, ], }); const schedule = await availableSlotsService.getAvailableSlots({ input: { eventTypeId: 1, eventTypeSlug: "", startTime: `${plus1DateString}T18:30:00.000Z`, endTime: `${plus2DateString}T18:29:59.999Z`, timeZone: Timezones["+5:30"], isTeamEvent: true, rescheduleUid: "BOOKING_TO_RESCHEDULE_UID", routedTeamMemberIds: [102], }, }); // expect only slots of IstEveningShift as this is the slots for the original host of the booking expect(schedule).toHaveTimeSlots( expectedSlotsForSchedule.IstMorningShift.interval["1hr"].allPossibleSlotsStartingAt430, { dateString: plus2DateString, } ); }); }); });