diff --git a/apps/web/test/lib/getSchedule.test.ts b/apps/web/test/lib/getSchedule.test.ts index 712687d90f..25bea1cb69 100644 --- a/apps/web/test/lib/getSchedule.test.ts +++ b/apps/web/test/lib/getSchedule.test.ts @@ -1542,7 +1542,7 @@ describe("getSchedule", () => { { id: 1, slotInterval: 45, - schedulingType: "COLLECTIVE", + schedulingType: SchedulingType.COLLECTIVE, hosts: [ { userId: 101, @@ -1746,6 +1746,668 @@ describe("getSchedule", () => { 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 getSchedule({ + 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 getSchedule({ + 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 getSchedule({ + 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 getSchedule({ + 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 getSchedule({ + 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 getSchedule({ + 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 getSchedule({ + 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 getSchedule({ + 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 getSchedule({ + 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 getSchedule({ + 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 }); @@ -2246,7 +2908,7 @@ describe("getSchedule", () => { { id: 1, slotInterval: 45, - schedulingType: "COLLECTIVE", + schedulingType: SchedulingType.COLLECTIVE, length: 45, users: [ {