diff --git a/apps/web/test/lib/getSchedule/expects.ts b/apps/web/test/lib/getSchedule/expects.ts index 7bfb369948..168564c818 100644 --- a/apps/web/test/lib/getSchedule/expects.ts +++ b/apps/web/test/lib/getSchedule/expects.ts @@ -37,7 +37,10 @@ declare global { namespace jest { interface Matchers { toHaveTimeSlots(expectedSlots: string[], date: { dateString: string; doExactMatch?: boolean }): R; - toHaveNoTimeSlots(date: { dateString: string }): R; + /** + * Explicitly checks if the date is disabled and fails if date is marked as OOO + */ + toHaveDateDisabled(date: { dateString: string }): R; } } } @@ -55,18 +58,28 @@ expect.extend({ }; } + const expectedSlotHasFullTimestamp = expectedSlots[0].split("-").length === 3; + if ( !schedule.slots[`${dateString}`] .map((slot) => slot.time) .every((actualSlotTime, index) => { - return `${dateString}T${expectedSlots[index]}` === actualSlotTime; + const expectedSlotTime = expectedSlotHasFullTimestamp + ? expectedSlots[index] + : `${dateString}T${expectedSlots[index]}`; + return expectedSlotTime === actualSlotTime; }) ) { return { pass: false, message: () => `has incorrect timeslots for ${dateString}.\n\r ${diff( - expectedSlots.map((expectedSlot) => `${dateString}T${expectedSlot}`), + expectedSlots.map((expectedSlot) => { + if (expectedSlotHasFullTimestamp) { + return expectedSlot; + } + return `${dateString}T${expectedSlot}`; + }), schedule.slots[`${dateString}`].map((slot) => slot.time) )}`, }; @@ -88,11 +101,19 @@ expect.extend({ }; }, - toHaveNoTimeSlots(schedule: { slots: Record }, { dateString }: { dateString: string }) { - if (!schedule.slots[`${dateString}`] || schedule.slots[`${dateString}`].length === 0) { + toHaveDateDisabled(schedule: { slots: Record }, { dateString }: { dateString: string }) { + // Frontend requires that the date must not be set for that date to be shown as disabled.Because weirdly, if an empty array is provided the date itself isn't shown which we don't want + if (!schedule.slots[`${dateString}`]) { return { pass: true, - message: () => `has no timeslots for ${dateString}`, + message: () => `is not disabled for ${dateString}`, + }; + } + + if (schedule.slots[`${dateString}`].length === 0) { + return { + pass: false, + message: () => `is all day OOO for ${dateString}.`, }; } return { diff --git a/apps/web/test/lib/getSchedule/futureLimit.timezone.test.ts b/apps/web/test/lib/getSchedule/futureLimit.timezone.test.ts index f76ab2ad63..4de779c4a6 100644 --- a/apps/web/test/lib/getSchedule/futureLimit.timezone.test.ts +++ b/apps/web/test/lib/getSchedule/futureLimit.timezone.test.ts @@ -1,10 +1,10 @@ -import type { ScenarioData } from "../../utils/bookingScenario/bookingScenario"; import { TestData, Timezones, - getDate, createBookingScenario, + replaceDates, } from "../../utils/bookingScenario/bookingScenario"; +import type { ScenarioData } from "../../utils/bookingScenario/bookingScenario"; import { describe, expect, vi, test } from "vitest"; @@ -13,7 +13,6 @@ import { getAvailableSlots as getSchedule } from "@calcom/trpc/server/routers/vi import { expectedSlotsForSchedule } from "./expects"; import { setupAndTeardown } from "./setupAndTeardown"; -import { timeTravelToTheBeginningOfToday } from "./utils"; function getPeriodTypeData({ type, @@ -29,7 +28,7 @@ function getPeriodTypeData({ periodEndDate?: Date; }) { if (type === PeriodType.ROLLING) { - if (!periodCountCalendarDays || !periodDays) { + if (periodCountCalendarDays === undefined || !periodDays) { throw new Error("periodCountCalendarDays and periodDays are required for ROLLING period type"); } return { @@ -40,7 +39,7 @@ function getPeriodTypeData({ } if (type === PeriodType.ROLLING_WINDOW) { - if (!periodCountCalendarDays || !periodDays) { + if (periodCountCalendarDays === undefined || !periodDays) { throw new Error("periodCountCalendarDays and periodDays are required for ROLLING period type"); } return { @@ -73,15 +72,16 @@ describe("getSchedule", () => { setupAndTeardown(); describe("Future Limits", () => { describe("PeriodType=ROLLING", () => { - test("Basic test", async () => { - const { dateString: yesterdayDateString } = getDate({ dateIncrement: -1 }); - const { dateString: todayDateString } = getDate(); - 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 }); - timeTravelToTheBeginningOfToday({ utcOffsetInHours: 5.5 }); + test("When the time of the first slot of current day hasn't reached", async () => { + // In IST timezone, it is 2024-05-31T07:00:00 + vi.setSystemTime("2024-05-31T01:30:00Z"); + const yesterdayDateString = "2024-05-30"; + const todayDateString = "2024-05-31"; + const plus1DateString = "2024-06-01"; + const plus2DateString = "2024-06-02"; + const plus3DateString = "2024-06-03"; + const plus4DateString = "2024-06-04"; + const plus5DateString = "2024-06-05"; const scenarioData = { eventTypes: [ @@ -125,6 +125,7 @@ describe("getSchedule", () => { }); expect(scheduleForEvent).toHaveTimeSlots( + // All slots on current day are available expectedSlotsForSchedule["IstWorkHours"].interval["1hr"].allPossibleSlotsStartingAt430, { dateString: todayDateString, @@ -149,26 +150,590 @@ describe("getSchedule", () => { ); // No Timeslots beyond plus2Date as that is beyond the rolling period - expect(scheduleForEvent).toHaveNoTimeSlots({ + expect(scheduleForEvent).toHaveDateDisabled({ dateString: plus3DateString, }); - expect(scheduleForEvent).toHaveNoTimeSlots({ + expect(scheduleForEvent).toHaveDateDisabled({ dateString: plus4DateString, }); }); + + test("When the time of the last slot of current day has passed", async () => { + // In IST timezone, it is 2024-05-31T07:00:00 + vi.setSystemTime("2024-05-31T11:30:00Z"); + const yesterdayDateString = "2024-05-30"; + const todayDateString = "2024-05-31"; + const plus1DateString = "2024-06-01"; + const plus2DateString = "2024-06-02"; + const plus3DateString = "2024-06-03"; + const plus4DateString = "2024-06-04"; + const plus5DateString = "2024-06-05"; + + const scenarioData = { + eventTypes: [ + { + id: 1, + length: 60, + ...getPeriodTypeData({ + type: "ROLLING", + periodDays: 2, + periodCountCalendarDays: true, + }), + users: [ + { + id: 101, + }, + ], + }, + ], + users: [ + { + ...TestData.users.example, + id: 101, + schedules: [TestData.schedules.IstWorkHours], + }, + ], + } satisfies ScenarioData; + + await createBookingScenario(scenarioData); + + const scheduleForEvent = await getSchedule({ + input: { + eventTypeId: 1, + eventTypeSlug: "", + usernameList: [], + // Because this time is in GMT, it will be 00:00 in IST with todayDateString + startTime: `${yesterdayDateString}T18:30:00.000Z`, + endTime: `${plus5DateString}T18:29:59.999Z`, + timeZone: Timezones["+5:30"], + isTeamEvent: false, + }, + }); + + // No timeslots of current day available + expect(scheduleForEvent).toHaveDateDisabled({ + dateString: todayDateString, + }); + + expect(scheduleForEvent).toHaveTimeSlots( + expectedSlotsForSchedule["IstWorkHours"].interval["1hr"].allPossibleSlotsStartingAt430, + { + dateString: plus1DateString, + doExactMatch: true, + } + ); + + expect(scheduleForEvent).toHaveTimeSlots( + expectedSlotsForSchedule["IstWorkHours"].interval["1hr"].allPossibleSlotsStartingAt430, + { + dateString: plus2DateString, + doExactMatch: true, + } + ); + + // No Timeslots beyond plus2Date as that is beyond the rolling period + expect(scheduleForEvent).toHaveDateDisabled({ + dateString: plus3DateString, + }); + + expect(scheduleForEvent).toHaveDateDisabled({ + dateString: plus4DateString, + }); + }); + + test("When the first timeslot of current day has passed", async () => { + // In IST timezone, it is 2024-05-31T10:00:00 + vi.setSystemTime("2024-05-31T04:30:00Z"); + const yesterdayDateString = "2024-05-30"; + const todayDateString = "2024-05-31"; + const plus1DateString = "2024-06-01"; + const plus2DateString = "2024-06-02"; + const plus3DateString = "2024-06-03"; + const plus4DateString = "2024-06-04"; + const plus5DateString = "2024-06-05"; + + const scenarioData = { + eventTypes: [ + { + id: 1, + length: 60, + ...getPeriodTypeData({ + type: "ROLLING", + periodDays: 2, + periodCountCalendarDays: true, + }), + users: [ + { + id: 101, + }, + ], + }, + ], + users: [ + { + ...TestData.users.example, + id: 101, + schedules: [TestData.schedules.IstWorkHours], + }, + ], + } satisfies ScenarioData; + + await createBookingScenario(scenarioData); + + const scheduleForEvent = await getSchedule({ + input: { + eventTypeId: 1, + eventTypeSlug: "", + usernameList: [], + // Because this time is in GMT, it will be 00:00 in IST with todayDateString + startTime: `${yesterdayDateString}T18:30:00.000Z`, + endTime: `${plus5DateString}T18:29:59.999Z`, + timeZone: Timezones["+5:30"], + isTeamEvent: false, + }, + }); + + // No timeslots of current day available + expect(scheduleForEvent).toHaveTimeSlots( + // First timeslot not available + expectedSlotsForSchedule["IstWorkHours"].interval["1hr"].allPossibleSlotsStartingAt430.slice(1), + { + doExactMatch: true, + dateString: todayDateString, + } + ); + + expect(scheduleForEvent).toHaveTimeSlots( + expectedSlotsForSchedule["IstWorkHours"].interval["1hr"].allPossibleSlotsStartingAt430, + { + dateString: plus1DateString, + doExactMatch: true, + } + ); + + expect(scheduleForEvent).toHaveTimeSlots( + expectedSlotsForSchedule["IstWorkHours"].interval["1hr"].allPossibleSlotsStartingAt430, + { + dateString: plus2DateString, + doExactMatch: true, + } + ); + + // No Timeslots beyond plus2Date as that is beyond the rolling period + expect(scheduleForEvent).toHaveDateDisabled({ + dateString: plus3DateString, + }); + + expect(scheduleForEvent).toHaveDateDisabled({ + dateString: plus4DateString, + }); + }); + + test("When the time of the first slot of current day hasn't reached and there is a day fully booked in between. Also, we are counting only business days.", async () => { + // In IST timezone, it is 2024-05-31T07:00:00 + vi.setSystemTime("2024-05-31T01:30:00Z"); + const yesterdayDateString = "2024-05-30"; + // Friday + const todayDateString = "2024-05-31"; + // Saturday + const plus1DateString = "2024-06-01"; + // Sunday + const plus2DateString = "2024-06-02"; + // Monday + const plus3DateString = "2024-06-03"; + // Tuesday + const plus4DateString = "2024-06-04"; + // Wednesday + const plus5DateString = "2024-06-05"; + + const scenarioData = { + eventTypes: [ + { + id: 1, + length: 60, + ...getPeriodTypeData({ + type: "ROLLING", + periodDays: 2, + periodCountCalendarDays: false, + }), + users: [ + { + id: 101, + }, + ], + }, + ], + users: [ + { + ...TestData.users.example, + id: 101, + schedules: [TestData.schedules.IstWorkHours], + }, + ], + } satisfies ScenarioData; + + await createBookingScenario(scenarioData); + + const scheduleForEvent = await getSchedule({ + input: { + eventTypeId: 1, + eventTypeSlug: "", + usernameList: [], + // Because this time is in GMT, it will be 00:00 in IST with todayDateString + startTime: `${yesterdayDateString}T18:30:00.000Z`, + endTime: `${plus5DateString}T18:29:59.999Z`, + timeZone: Timezones["+5:30"], + isTeamEvent: false, + }, + }); + + expect(scheduleForEvent).toHaveTimeSlots( + // All slots on current day are available + expectedSlotsForSchedule["IstWorkHours"].interval["1hr"].allPossibleSlotsStartingAt430, + { + dateString: todayDateString, + doExactMatch: true, + } + ); + + // Being a Saturday, plus1Date is available as per Availability but not counted in periodDays + expect(scheduleForEvent).toHaveTimeSlots( + expectedSlotsForSchedule["IstWorkHours"].interval["1hr"].allPossibleSlotsStartingAt430, + { + dateString: plus1DateString, + doExactMatch: true, + } + ); + + // Being a Saturday, plus2Date is available as per Availability but not counted in periodDays + expect(scheduleForEvent).toHaveTimeSlots( + expectedSlotsForSchedule["IstWorkHours"].interval["1hr"].allPossibleSlotsStartingAt430, + { + dateString: plus2DateString, + doExactMatch: true, + } + ); + + // Day1 of periodDays + expect(scheduleForEvent).toHaveTimeSlots( + expectedSlotsForSchedule["IstWorkHours"].interval["1hr"].allPossibleSlotsStartingAt430, + { + dateString: plus3DateString, + doExactMatch: true, + } + ); + + // Day2 of periodDays + expect(scheduleForEvent).toHaveTimeSlots( + expectedSlotsForSchedule["IstWorkHours"].interval["1hr"].allPossibleSlotsStartingAt430, + { + dateString: plus4DateString, + doExactMatch: true, + } + ); + + expect(scheduleForEvent).toHaveDateDisabled({ + dateString: plus5DateString, + }); + }); + + describe("Borderline cases", () => { + test("When it is the very first second of the day", async () => { + // In IST timezone, it is 2024-05-31T00:00:00 + vi.setSystemTime("2024-05-30T18:30:00Z"); + const yesterdayDateString = "2024-05-30"; + const todayDateString = "2024-05-31"; + const plus1DateString = "2024-06-01"; + const plus2DateString = "2024-06-02"; + const plus3DateString = "2024-06-03"; + const plus4DateString = "2024-06-04"; + const plus5DateString = "2024-06-05"; + + const scenarioData = { + eventTypes: [ + { + id: 1, + length: 60, + ...getPeriodTypeData({ + type: "ROLLING", + periodDays: 2, + periodCountCalendarDays: true, + }), + users: [ + { + id: 101, + }, + ], + }, + ], + users: [ + { + ...TestData.users.example, + id: 101, + schedules: [TestData.schedules.IstWorkHours], + }, + ], + } satisfies ScenarioData; + + await createBookingScenario(scenarioData); + + const scheduleForEvent = await getSchedule({ + input: { + eventTypeId: 1, + eventTypeSlug: "", + usernameList: [], + // Because this time is in GMT, it will be 00:00 in IST with todayDateString + startTime: `${yesterdayDateString}T18:30:00.000Z`, + endTime: `${plus5DateString}T18:29:59.999Z`, + timeZone: Timezones["+5:30"], + isTeamEvent: false, + }, + }); + + console.log({ scheduleForEvent }); + + expect(scheduleForEvent).toHaveTimeSlots( + // All slots on current day are available + expectedSlotsForSchedule["IstWorkHours"].interval["1hr"].allPossibleSlotsStartingAt430, + { + dateString: todayDateString, + doExactMatch: true, + } + ); + + expect(scheduleForEvent).toHaveTimeSlots( + expectedSlotsForSchedule["IstWorkHours"].interval["1hr"].allPossibleSlotsStartingAt430, + { + dateString: plus1DateString, + doExactMatch: true, + } + ); + + expect(scheduleForEvent).toHaveTimeSlots( + expectedSlotsForSchedule["IstWorkHours"].interval["1hr"].allPossibleSlotsStartingAt430, + { + dateString: plus2DateString, + doExactMatch: true, + } + ); + + // No Timeslots beyond plus2Date as that is beyond the rolling period + expect(scheduleForEvent).toHaveDateDisabled({ + dateString: plus3DateString, + }); + + expect(scheduleForEvent).toHaveDateDisabled({ + dateString: plus4DateString, + }); + }); + + test("When it is the very last second of the day", async () => { + // In IST timezone, it is 2024-05-31T23:59:00 + vi.setSystemTime("2024-05-31T18:29:00Z"); + const yesterdayDateString = "2024-05-30"; + const todayDateString = "2024-05-31"; + const plus1DateString = "2024-06-01"; + const plus2DateString = "2024-06-02"; + const plus3DateString = "2024-06-03"; + const plus4DateString = "2024-06-04"; + const plus5DateString = "2024-06-05"; + + const scenarioData = { + eventTypes: [ + { + id: 1, + length: 60, + ...getPeriodTypeData({ + type: "ROLLING", + periodDays: 2, + periodCountCalendarDays: true, + }), + users: [ + { + id: 101, + }, + ], + }, + ], + users: [ + { + ...TestData.users.example, + id: 101, + schedules: [TestData.schedules.IstWorkHours], + }, + ], + } satisfies ScenarioData; + + await createBookingScenario(scenarioData); + + const scheduleForEvent = await getSchedule({ + input: { + eventTypeId: 1, + eventTypeSlug: "", + usernameList: [], + // Because this time is in GMT, it will be 00:00 in IST with todayDateString + startTime: `${yesterdayDateString}T18:30:00.000Z`, + endTime: `${plus5DateString}T18:29:59.999Z`, + timeZone: Timezones["+5:30"], + isTeamEvent: false, + }, + }); + + expect(scheduleForEvent).toHaveDateDisabled({ + dateString: todayDateString, + }); + + expect(scheduleForEvent).toHaveTimeSlots( + expectedSlotsForSchedule["IstWorkHours"].interval["1hr"].allPossibleSlotsStartingAt430, + { + dateString: plus1DateString, + doExactMatch: true, + } + ); + + expect(scheduleForEvent).toHaveTimeSlots( + expectedSlotsForSchedule["IstWorkHours"].interval["1hr"].allPossibleSlotsStartingAt430, + { + dateString: plus2DateString, + doExactMatch: true, + } + ); + + // No Timeslots beyond plus2Date as that is beyond the rolling period + expect(scheduleForEvent).toHaveDateDisabled({ + dateString: plus3DateString, + }); + + expect(scheduleForEvent).toHaveDateDisabled({ + dateString: plus4DateString, + }); + }); + }); + + describe("GMT-11 Browsing", () => { + test("When the time of the first slot of current day hasn't reached", async () => { + // In IST timezone, it is 2024-05-31T07:00:00 + vi.setSystemTime("2024-05-31T01:30:00Z"); + const yesterdayDateString = "2024-05-30"; + const todayDateString = "2024-05-31"; + const plus1DateString = "2024-06-01"; + const plus2DateString = "2024-06-02"; + const plus3DateString = "2024-06-03"; + const plus5DateString = "2024-06-05"; + + const scenarioData = { + eventTypes: [ + { + id: 1, + length: 60, + ...getPeriodTypeData({ + type: "ROLLING", + periodDays: 2, + periodCountCalendarDays: true, + }), + users: [ + { + id: 101, + }, + ], + }, + ], + users: [ + { + ...TestData.users.example, + id: 101, + schedules: [TestData.schedules.IstWorkHours], + }, + ], + } satisfies ScenarioData; + + await createBookingScenario(scenarioData); + + const scheduleForEvent = await getSchedule({ + input: { + eventTypeId: 1, + eventTypeSlug: "", + usernameList: [], + // Because this time is in GMT, it will be 00:00 in IST with todayDateString + startTime: `${yesterdayDateString}T18:30:00.000Z`, + endTime: `${plus5DateString}T18:29:59.999Z`, + timeZone: Timezones["-11:00"], + isTeamEvent: false, + }, + }); + + const allTimeSlotsForToday = [ + "2024-05-31T11:30:00.000Z", + "2024-06-01T04:30:00.000Z", + "2024-06-01T05:30:00.000Z", + "2024-06-01T06:30:00.000Z", + "2024-06-01T07:30:00.000Z", + "2024-06-01T08:30:00.000Z", + "2024-06-01T09:30:00.000Z", + "2024-06-01T10:30:00.000Z", + ]; + + expect(scheduleForEvent).toHaveTimeSlots( + // All slots on current day are available + [ + // "2024-05-30T04:30:00.000Z", // Not available as before the start of the range + "2024-05-31T04:30:00.000Z", + "2024-05-31T05:30:00.000Z", + "2024-05-31T06:30:00.000Z", + "2024-05-31T07:30:00.000Z", + "2024-05-31T08:30:00.000Z", + "2024-05-31T09:30:00.000Z", + "2024-05-31T10:30:00.000Z", + ], + { + dateString: yesterdayDateString, + doExactMatch: true, + } + ); + + expect(scheduleForEvent).toHaveTimeSlots( + // All slots on current day are available + allTimeSlotsForToday, + { + dateString: todayDateString, + doExactMatch: true, + } + ); + + expect(scheduleForEvent).toHaveTimeSlots( + replaceDates(allTimeSlotsForToday, { + "2024-05-31": "2024-06-01", + "2024-06-01": "2024-06-02", + }), + { + dateString: plus1DateString, + doExactMatch: true, + } + ); + + //No Timeslots beyond plus2Date as that is beyond the rolling period + expect(scheduleForEvent).toHaveDateDisabled({ + dateString: plus2DateString, + }); + + expect(scheduleForEvent).toHaveDateDisabled({ + dateString: plus3DateString, + }); + }); + }); }); describe("PeriodType=ROLLING_WINDOW", () => { - test("Basic test", async () => { - const { dateString: yesterdayDateString } = getDate({ dateIncrement: -1 }); - const { dateString: todayDateString } = getDate(); - 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 }); - timeTravelToTheBeginningOfToday({ utcOffsetInHours: 5.5 }); + test("When the time of the first slot of current day hasn't reached and there is a day fully booked in between. It makes `periodDays` available", async () => { + vi.setSystemTime("2024-05-31T01:30:00Z"); + const yesterdayDateString = "2024-05-30"; + const todayDateString = "2024-05-31"; + const plus1DateString = "2024-06-01"; + const plus2DateString = "2024-06-02"; + const plus3DateString = "2024-06-03"; + const plus4DateString = "2024-06-04"; + const plus5DateString = "2024-06-05"; const scenarioData = { eventTypes: [ @@ -221,7 +786,6 @@ describe("getSchedule", () => { }, }); - console.log({ scheduleForEvent }); expect(scheduleForEvent).toHaveTimeSlots( expectedSlotsForSchedule["IstWorkHours"].interval["1hr"].allPossibleSlotsStartingAt430, { @@ -239,7 +803,7 @@ describe("getSchedule", () => { ); // plus2Date is fully booked. So, instead we will have timeslots one day later - expect(scheduleForEvent).toHaveNoTimeSlots({ + expect(scheduleForEvent).toHaveDateDisabled({ dateString: plus2DateString, }); @@ -252,22 +816,658 @@ describe("getSchedule", () => { ); // No Timeslots on plus4Date as beyond the rolling period - expect(scheduleForEvent).toHaveNoTimeSlots({ + expect(scheduleForEvent).toHaveDateDisabled({ dateString: plus4DateString, }); }); + + test("When the time of the last slot of current day has passed and there is a day fully booked in between. It makes `periodDays` available", async () => { + // In IST timezone, it is 2024-05-31T07:00:00 + vi.setSystemTime("2024-05-31T11:30:00Z"); + const yesterdayDateString = "2024-05-30"; + const todayDateString = "2024-05-31"; + const plus1DateString = "2024-06-01"; + const plus2DateString = "2024-06-02"; + const plus3DateString = "2024-06-03"; + const plus4DateString = "2024-06-04"; + const plus5DateString = "2024-06-05"; + + const scenarioData = { + eventTypes: [ + { + id: 1, + length: 60, + ...getPeriodTypeData({ + type: "ROLLING_WINDOW", + periodDays: 3, + periodCountCalendarDays: true, + }), + users: [ + { + id: 101, + }, + ], + }, + ], + users: [ + { + ...TestData.users.example, + id: 101, + schedules: [TestData.schedules.IstWorkHours], + }, + ], + bookings: [ + { + userId: 101, + eventTypeId: 1, + status: "ACCEPTED", + // Fully book plus2 Date + startTime: `${plus1DateString}T18:30:00.000Z`, + endTime: `${plus2DateString}T18:30:00.000Z`, + }, + ], + } satisfies ScenarioData; + + await createBookingScenario(scenarioData); + + const scheduleForEvent = await getSchedule({ + input: { + eventTypeId: 1, + eventTypeSlug: "", + usernameList: [], + // Because this time is in GMT, it will be 00:00 in IST with todayDateString + startTime: `${yesterdayDateString}T18:30:00.000Z`, + endTime: `${plus5DateString}T18:29:59.999Z`, + timeZone: Timezones["+5:30"], + isTeamEvent: false, + }, + }); + + expect(scheduleForEvent).toHaveDateDisabled({ + dateString: todayDateString, + }); + + expect(scheduleForEvent).toHaveTimeSlots( + expectedSlotsForSchedule["IstWorkHours"].interval["1hr"].allPossibleSlotsStartingAt430, + { + dateString: plus1DateString, + doExactMatch: true, + } + ); + + // plus2Date is fully booked. So, instead we will have timeslots one day later + expect(scheduleForEvent).toHaveDateDisabled({ + dateString: plus2DateString, + }); + + expect(scheduleForEvent).toHaveTimeSlots( + expectedSlotsForSchedule["IstWorkHours"].interval["1hr"].allPossibleSlotsStartingAt430, + { + dateString: plus3DateString, + doExactMatch: true, + } + ); + + expect(scheduleForEvent).toHaveTimeSlots( + expectedSlotsForSchedule["IstWorkHours"].interval["1hr"].allPossibleSlotsStartingAt430, + { + dateString: plus4DateString, + doExactMatch: true, + } + ); + + expect(scheduleForEvent).toHaveDateDisabled({ + dateString: plus5DateString, + }); + }); + + test("When the first timeslot of current day has passed and there is a day fully booked in between. It makes `periodDays` available", async () => { + // In IST timezone, it is 2024-05-31T10:00 + vi.setSystemTime("2024-05-31T04:30:00Z"); + const yesterdayDateString = "2024-05-30"; + const todayDateString = "2024-05-31"; + const plus1DateString = "2024-06-01"; + const plus2DateString = "2024-06-02"; + const plus3DateString = "2024-06-03"; + const plus4DateString = "2024-06-04"; + const plus5DateString = "2024-06-05"; + + const scenarioData = { + eventTypes: [ + { + id: 1, + length: 60, + ...getPeriodTypeData({ + type: "ROLLING_WINDOW", + periodDays: 3, + periodCountCalendarDays: true, + }), + users: [ + { + id: 101, + }, + ], + }, + ], + users: [ + { + ...TestData.users.example, + id: 101, + schedules: [TestData.schedules.IstWorkHours], + }, + ], + bookings: [ + { + userId: 101, + eventTypeId: 1, + status: "ACCEPTED", + // Fully book plus2 Date + startTime: `${plus1DateString}T18:30:00.000Z`, + endTime: `${plus2DateString}T18:30:00.000Z`, + }, + ], + } satisfies ScenarioData; + + await createBookingScenario(scenarioData); + + const scheduleForEvent = await getSchedule({ + input: { + eventTypeId: 1, + eventTypeSlug: "", + usernameList: [], + // Because this time is in GMT, it will be 00:00 in IST with todayDateString + startTime: `${yesterdayDateString}T18:30:00.000Z`, + endTime: `${plus5DateString}T18:29:59.999Z`, + timeZone: Timezones["+5:30"], + isTeamEvent: false, + }, + }); + + expect(scheduleForEvent).toHaveTimeSlots( + expectedSlotsForSchedule["IstWorkHours"].interval["1hr"].allPossibleSlotsStartingAt430.slice(1), + { + dateString: todayDateString, + doExactMatch: true, + } + ); + + expect(scheduleForEvent).toHaveTimeSlots( + expectedSlotsForSchedule["IstWorkHours"].interval["1hr"].allPossibleSlotsStartingAt430, + { + dateString: plus1DateString, + doExactMatch: true, + } + ); + + // plus2Date is fully booked. So, instead we will have timeslots one day later + expect(scheduleForEvent).toHaveDateDisabled({ + dateString: plus2DateString, + }); + + expect(scheduleForEvent).toHaveTimeSlots( + expectedSlotsForSchedule["IstWorkHours"].interval["1hr"].allPossibleSlotsStartingAt430, + { + dateString: plus3DateString, + doExactMatch: true, + } + ); + + expect(scheduleForEvent).toHaveDateDisabled({ + dateString: plus4DateString, + }); + + expect(scheduleForEvent).toHaveDateDisabled({ + dateString: plus5DateString, + }); + }); + + test("When the time of the first slot of current day hasn't reached and there is a day fully booked in between. Also, we are counting only business days. It makes weekends + `periodDays` available", async () => { + // In Ist timezone, it is 2024-05-31T01:30:00 which is a Friday + vi.setSystemTime("2024-05-31T01:30:00Z"); + const yesterdayDateString = "2024-05-30"; + const todayDateString = "2024-05-31"; + // Saturday + const plus1DateString = "2024-06-01"; + // Sunday + const plus2DateString = "2024-06-02"; + // Monday + const plus3DateString = "2024-06-03"; + // Tuesday + const plus4DateString = "2024-06-04"; + // Wednesday + const plus5DateString = "2024-06-05"; + // Thursday + const plus6DateString = "2024-06-06"; + + const scenarioData = { + eventTypes: [ + { + id: 1, + length: 60, + ...getPeriodTypeData({ + type: "ROLLING_WINDOW", + periodDays: 3, + periodCountCalendarDays: false, + }), + users: [ + { + id: 101, + }, + ], + }, + ], + users: [ + { + ...TestData.users.example, + id: 101, + schedules: [TestData.schedules.IstWorkHours], + }, + ], + bookings: [ + { + userId: 101, + eventTypeId: 1, + status: "ACCEPTED", + // Fully book plus3 Date + startTime: `${plus2DateString}T18:30:00.000Z`, + endTime: `${plus3DateString}T18:30:00.000Z`, + }, + ], + } satisfies ScenarioData; + + await createBookingScenario(scenarioData); + + const scheduleForEvent = await getSchedule({ + input: { + eventTypeId: 1, + eventTypeSlug: "", + usernameList: [], + // Because this time is in GMT, it will be 00:00 in IST with todayDateString + startTime: `${yesterdayDateString}T18:30:00.000Z`, + endTime: `${plus5DateString}T18:29:59.999Z`, + timeZone: Timezones["+5:30"], + isTeamEvent: false, + }, + }); + + // Day1 of periodDays + expect(scheduleForEvent).toHaveTimeSlots( + expectedSlotsForSchedule["IstWorkHours"].interval["1hr"].allPossibleSlotsStartingAt430, + { + dateString: todayDateString, + doExactMatch: true, + } + ); + + // plus1Date is a Saturday and available as per Availability but not counted in periodDays + expect(scheduleForEvent).toHaveTimeSlots( + expectedSlotsForSchedule["IstWorkHours"].interval["1hr"].allPossibleSlotsStartingAt430, + { + dateString: plus1DateString, + doExactMatch: true, + } + ); + + // plus2Date is a Sunday and available as per Availability but not counted in periodDays + expect(scheduleForEvent).toHaveTimeSlots( + expectedSlotsForSchedule["IstWorkHours"].interval["1hr"].allPossibleSlotsStartingAt430, + { + dateString: plus2DateString, + doExactMatch: true, + } + ); + + // plus3Date is fully booked. So, instead we will have timeslots one day later + expect(scheduleForEvent).toHaveDateDisabled({ + dateString: plus3DateString, + }); + + // Day2 of periodDays + expect(scheduleForEvent).toHaveTimeSlots( + expectedSlotsForSchedule["IstWorkHours"].interval["1hr"].allPossibleSlotsStartingAt430, + { + dateString: plus4DateString, + doExactMatch: true, + } + ); + + // Day3 of periodDays + expect(scheduleForEvent).toHaveTimeSlots( + expectedSlotsForSchedule["IstWorkHours"].interval["1hr"].allPossibleSlotsStartingAt430, + { + dateString: plus5DateString, + doExactMatch: true, + } + ); + + // Beyond periodDays + expect(scheduleForEvent).toHaveDateDisabled({ + dateString: plus6DateString, + }); + }); + + describe("Borderline cases", () => { + test("When it is the very first second of the day", async () => { + // In IST timezone, it is 2024-05-31T00:00 + vi.setSystemTime("2024-05-30T18:30:00Z"); + const yesterdayDateString = "2024-05-30"; + const todayDateString = "2024-05-31"; + const plus1DateString = "2024-06-01"; + const plus2DateString = "2024-06-02"; + const plus3DateString = "2024-06-03"; + const plus4DateString = "2024-06-04"; + const plus5DateString = "2024-06-05"; + + const scenarioData = { + eventTypes: [ + { + id: 1, + length: 60, + ...getPeriodTypeData({ + type: "ROLLING_WINDOW", + periodDays: 3, + periodCountCalendarDays: true, + }), + users: [ + { + id: 101, + }, + ], + }, + ], + users: [ + { + ...TestData.users.example, + id: 101, + schedules: [TestData.schedules.IstWorkHours], + }, + ], + bookings: [ + { + userId: 101, + eventTypeId: 1, + status: "ACCEPTED", + // Fully book plus2 Date + startTime: `${plus1DateString}T18:30:00.000Z`, + endTime: `${plus2DateString}T18:30:00.000Z`, + }, + ], + } satisfies ScenarioData; + + await createBookingScenario(scenarioData); + + const scheduleForEvent = await getSchedule({ + input: { + eventTypeId: 1, + eventTypeSlug: "", + usernameList: [], + // Because this time is in GMT, it will be 00:00 in IST with todayDateString + startTime: `${yesterdayDateString}T18:30:00.000Z`, + endTime: `${plus5DateString}T18:29:59.999Z`, + timeZone: Timezones["+5:30"], + isTeamEvent: false, + }, + }); + + expect(scheduleForEvent).toHaveTimeSlots( + expectedSlotsForSchedule["IstWorkHours"].interval["1hr"].allPossibleSlotsStartingAt430, + { + dateString: todayDateString, + doExactMatch: true, + } + ); + + expect(scheduleForEvent).toHaveTimeSlots( + expectedSlotsForSchedule["IstWorkHours"].interval["1hr"].allPossibleSlotsStartingAt430, + { + dateString: plus1DateString, + doExactMatch: true, + } + ); + + // plus2Date is fully booked. So, instead we will have timeslots one day later + expect(scheduleForEvent).toHaveDateDisabled({ + dateString: plus2DateString, + }); + + expect(scheduleForEvent).toHaveTimeSlots( + expectedSlotsForSchedule["IstWorkHours"].interval["1hr"].allPossibleSlotsStartingAt430, + { + dateString: plus3DateString, + doExactMatch: true, + } + ); + + // No Timeslots on plus4Date as beyond the rolling period + expect(scheduleForEvent).toHaveDateDisabled({ + dateString: plus4DateString, + }); + }); + test("When it is the very last second of the day", async () => { + // In IST timezone, it is 2024-05-31T23:59 + vi.setSystemTime("2024-05-31T18:29:00Z"); + const yesterdayDateString = "2024-05-30"; + const todayDateString = "2024-05-31"; + const plus1DateString = "2024-06-01"; + const plus2DateString = "2024-06-02"; + const plus3DateString = "2024-06-03"; + const plus4DateString = "2024-06-04"; + const plus5DateString = "2024-06-05"; + + const scenarioData = { + eventTypes: [ + { + id: 1, + length: 60, + ...getPeriodTypeData({ + type: "ROLLING_WINDOW", + periodDays: 3, + periodCountCalendarDays: true, + }), + users: [ + { + id: 101, + }, + ], + }, + ], + users: [ + { + ...TestData.users.example, + id: 101, + schedules: [TestData.schedules.IstWorkHours], + }, + ], + bookings: [ + { + userId: 101, + eventTypeId: 1, + status: "ACCEPTED", + // Fully book plus2 Date + startTime: `${plus1DateString}T18:30:00.000Z`, + endTime: `${plus2DateString}T18:30:00.000Z`, + }, + ], + } satisfies ScenarioData; + + await createBookingScenario(scenarioData); + + const scheduleForEvent = await getSchedule({ + input: { + eventTypeId: 1, + eventTypeSlug: "", + usernameList: [], + // Because this time is in GMT, it will be 00:00 in IST with todayDateString + startTime: `${yesterdayDateString}T18:30:00.000Z`, + endTime: `${plus5DateString}T18:29:59.999Z`, + timeZone: Timezones["+5:30"], + isTeamEvent: false, + }, + }); + + expect(scheduleForEvent).toHaveDateDisabled({ + dateString: todayDateString, + }); + + expect(scheduleForEvent).toHaveTimeSlots( + expectedSlotsForSchedule["IstWorkHours"].interval["1hr"].allPossibleSlotsStartingAt430, + { + dateString: plus1DateString, + doExactMatch: true, + } + ); + + // plus2Date is fully booked. So, instead we will have timeslots one day later + expect(scheduleForEvent).toHaveDateDisabled({ + dateString: plus2DateString, + }); + + expect(scheduleForEvent).toHaveTimeSlots( + expectedSlotsForSchedule["IstWorkHours"].interval["1hr"].allPossibleSlotsStartingAt430, + { + dateString: plus3DateString, + doExactMatch: true, + } + ); + + expect(scheduleForEvent).toHaveTimeSlots( + expectedSlotsForSchedule["IstWorkHours"].interval["1hr"].allPossibleSlotsStartingAt430, + { + dateString: plus4DateString, + doExactMatch: true, + } + ); + + expect(scheduleForEvent).toHaveDateDisabled({ + dateString: plus5DateString, + }); + }); + }); + + test("When the time of the first slot of current day hasn't reached and there is a day fully booked in between. Also, weekend availability is not there and one of the days is on a weekend. It makes `periodDays` available", async () => { + // In Ist timezone, it is 2024-05-31T01:30:00 which is a Friday + vi.setSystemTime("2024-05-31T01:30:00Z"); + const yesterdayDateString = "2024-05-30"; + const todayDateString = "2024-05-31"; + // Saturday + const plus1DateString = "2024-06-01"; + // Sunday + const plus2DateString = "2024-06-02"; + // Monday + const plus3DateString = "2024-06-03"; + // Tuesday + const plus4DateString = "2024-06-04"; + // Wednesday + const plus5DateString = "2024-06-05"; + // Thursday + const plus6DateString = "2024-06-06"; + + const scenarioData = { + eventTypes: [ + { + id: 1, + length: 60, + ...getPeriodTypeData({ + type: "ROLLING_WINDOW", + periodDays: 3, + periodCountCalendarDays: true, + }), + users: [ + { + id: 101, + }, + ], + }, + ], + users: [ + { + ...TestData.users.example, + id: 101, + schedules: [TestData.schedules.IstWorkHoursNoWeekends], + }, + ], + bookings: [ + { + userId: 101, + eventTypeId: 1, + status: "ACCEPTED", + // Fully book plus3 Date + startTime: `${plus2DateString}T18:30:00.000Z`, + endTime: `${plus3DateString}T18:30:00.000Z`, + }, + ], + } satisfies ScenarioData; + + await createBookingScenario(scenarioData); + + const scheduleForEvent = await getSchedule({ + input: { + eventTypeId: 1, + eventTypeSlug: "", + usernameList: [], + // Because this time is in GMT, it will be 00:00 in IST with todayDateString + startTime: `${yesterdayDateString}T18:30:00.000Z`, + endTime: `${plus5DateString}T18:29:59.999Z`, + timeZone: Timezones["+5:30"], + isTeamEvent: false, + }, + }); + + expect(scheduleForEvent).toHaveTimeSlots( + expectedSlotsForSchedule["IstWorkHours"].interval["1hr"].allPossibleSlotsStartingAt430, + { + dateString: todayDateString, + doExactMatch: true, + } + ); + + // plus1Date is a Saturday and not available + expect(scheduleForEvent).toHaveDateDisabled({ + dateString: plus1DateString, + }); + + // plus2Date is a Sunday and not available + expect(scheduleForEvent).toHaveDateDisabled({ + dateString: plus2DateString, + }); + + // plus3Date is fully booked. So, instead we will have timeslots one day later + expect(scheduleForEvent).toHaveDateDisabled({ + dateString: plus3DateString, + }); + + expect(scheduleForEvent).toHaveTimeSlots( + expectedSlotsForSchedule["IstWorkHours"].interval["1hr"].allPossibleSlotsStartingAt430, + { + dateString: plus4DateString, + doExactMatch: true, + } + ); + + expect(scheduleForEvent).toHaveTimeSlots( + expectedSlotsForSchedule["IstWorkHours"].interval["1hr"].allPossibleSlotsStartingAt430, + { + dateString: plus5DateString, + doExactMatch: true, + } + ); + + expect(scheduleForEvent).toHaveDateDisabled({ + dateString: plus6DateString, + }); + }); }); describe("PeriodType=RANGE", () => { test("Basic test", async () => { - const { dateString: yesterdayDateString } = getDate({ dateIncrement: -1 }); - const { dateString: todayDateString } = getDate(); - 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 }); - timeTravelToTheBeginningOfToday({ utcOffsetInHours: 5.5 }); + vi.setSystemTime("2024-05-31T01:30:00Z"); + const yesterdayDateString = "2024-05-30"; + const todayDateString = "2024-05-31"; + const plus1DateString = "2024-06-01"; + const plus2DateString = "2024-06-02"; + const plus3DateString = "2024-06-03"; + const plus4DateString = "2024-06-04"; + const plus5DateString = "2024-06-05"; const scenarioData = { eventTypes: [ @@ -314,9 +1514,8 @@ describe("getSchedule", () => { }, }); - console.log({ scheduleForEvent }); - - expect(scheduleForEvent).toHaveNoTimeSlots({ + // Before the start of the range + expect(scheduleForEvent).toHaveDateDisabled({ dateString: todayDateString, }); @@ -336,15 +1535,16 @@ describe("getSchedule", () => { } ); - expect(scheduleForEvent).toHaveNoTimeSlots({ + // After the range ends + expect(scheduleForEvent).toHaveDateDisabled({ dateString: plus3DateString, }); - expect(scheduleForEvent).toHaveNoTimeSlots({ + expect(scheduleForEvent).toHaveDateDisabled({ dateString: plus4DateString, }); - expect(scheduleForEvent).toHaveNoTimeSlots({ + expect(scheduleForEvent).toHaveDateDisabled({ dateString: plus4DateString, }); }); diff --git a/apps/web/test/utils/bookingScenario/bookingScenario.ts b/apps/web/test/utils/bookingScenario/bookingScenario.ts index c7e1f9acf3..d6b33e7ce6 100644 --- a/apps/web/test/utils/bookingScenario/bookingScenario.ts +++ b/apps/web/test/utils/bookingScenario/bookingScenario.ts @@ -166,6 +166,7 @@ type InputBooking = Partial> & Whit export const Timezones = { "+5:30": "Asia/Kolkata", "+6:00": "Asia/Dhaka", + "-11:00": "Pacific/Pago_Pago", }; async function addHostsToDb(eventTypes: InputEventType[]) { @@ -928,6 +929,21 @@ export const TestData = { ], timeZone: Timezones["+5:30"], }), + IstWorkHoursNoWeekends: { + id: 1, + name: "9:30AM to 6PM in India - 4:00AM to 12:30PM in GMT", + availability: [ + { + // userId: null, + // eventTypeId: null, + days: [/*0*/ 1, 2, 3, 4, 5 /*6*/], + startTime: new Date("1970-01-01T09:30:00.000Z"), + endTime: new Date("1970-01-01T18:00:00.000Z"), + date: null, + }, + ], + timeZone: Timezones["+5:30"], + }, }, users: { example: { @@ -1669,3 +1685,9 @@ export const getMockFailingAppStatus = ({ slug }: { slug: string }) => { export const getMockPassingAppStatus = ({ slug, overrideName }: { slug: string; overrideName?: string }) => { return getMockAppStatus({ slug, overrideName, failures: 0, success: 1 }); }; + +export const replaceDates = (dates: string[], replacement: Record) => { + return dates.map((date) => { + return date.replace(/(.*)T/, (_, group1) => `${replacement[group1]}T`); + }); +}; diff --git a/packages/lib/isOutOfBounds.tsx b/packages/lib/isOutOfBounds.tsx index c2b0b4f759..9e9d421027 100644 --- a/packages/lib/isOutOfBounds.tsx +++ b/packages/lib/isOutOfBounds.tsx @@ -51,13 +51,25 @@ export function calculatePeriodLimits({ }): PeriodLimits { const currentTime = dayjs().utcOffset(utcOffset); periodDays = periodDays || 0; + const log = logger.getSubLogger({ prefix: ["calculatePeriodLimits"] }); + log.debug( + safeStringify({ + periodType, + periodDays, + periodCountCalendarDays, + periodStartDate: periodStartDate, + periodEndDate: periodEndDate, + currentTime: currentTime.format(), + }) + ); switch (periodType) { case PeriodType.ROLLING: { const rollingEndDay = periodCountCalendarDays - ? currentTime.add(periodDays, "days").endOf("day") - : currentTime.businessDaysAdd(periodDays).endOf("day"); - return { rollingEndDay, rangeStartDay: null, rangeEndDay: null }; + ? currentTime.add(periodDays, "days") + : currentTime.businessDaysAdd(periodDays); + // The future limit talks in terms of days so we take the end of the day here to consider the entire day + return { rollingEndDay: rollingEndDay.endOf("day"), rangeStartDay: null, rangeEndDay: null }; } case PeriodType.ROLLING_WINDOW: { @@ -80,6 +92,7 @@ export function calculatePeriodLimits({ } case PeriodType.RANGE: { + // The future limit talks in terms of days so we take the start of the day for starting range and endOf the day for ending range const rangeStartDay = dayjs(periodStartDate).utcOffset(utcOffset).startOf("day"); const rangeEndDay = dayjs(periodEndDate).utcOffset(utcOffset).endOf("day"); @@ -159,7 +172,7 @@ export function getRollingWindowEndDate({ const rollingEndDayOrLastPossibleDayAsPerLimit = rollingEndDay ?? currentDate; log.debug("Returning rollingEndDay", rollingEndDayOrLastPossibleDayAsPerLimit.format()); - // Return endOfDay so that any timeslot in the last day is considered within bounds + // The future limit talks in terms of days so we take the end of the day here to consider the entire day return rollingEndDayOrLastPossibleDayAsPerLimit.endOf("day"); } @@ -198,34 +211,19 @@ type PeriodLimits = { * To be used when we work on just Dates(and not specific timeslots) to check boundaries * e.g. It checks for Future Limits which operate on dates and not times. */ -export function isDateOutOfBounds({ - dateString, +export function isTimeViolatingFutureLimit({ + time, periodLimits, }: { - dateString: dayjs.ConfigType; + time: dayjs.ConfigType; periodLimits: PeriodLimits; - _skipRollingWindowCheck?: boolean; }) { - const log = logger.getSubLogger({ prefix: ["isDateOutOfBounds"] }); - const date = dayjs(dateString); - - log.debug( - safeStringify({ - dateString, - endOfDay: date.format(), - periodLimits: { - rollingEndDay: periodLimits.rollingEndDay?.format(), - rangeStartDay: periodLimits.rangeStartDay?.format(), - rangeEndDay: periodLimits.rangeEndDay?.format(), - }, - }) - ); - + const log = logger.getSubLogger({ prefix: ["isTimeViolatingFutureLimit"] }); + const date = dayjs(time); if (periodLimits.rollingEndDay) { const isAfterRollingEndDay = date.isAfter(periodLimits.rollingEndDay); log.debug({ - dateString, - endOfDay: date.format(), + formattedDate: date.format(), isAfterRollingEndDay, rollingEndDay: periodLimits.rollingEndDay.format(), }); @@ -257,8 +255,8 @@ export default function isOutOfBounds( ) { return ( isTimeOutOfBounds({ time, minimumBookingNotice }) || - isDateOutOfBounds({ - dateString: time, + isTimeViolatingFutureLimit({ + time, periodLimits: calculatePeriodLimits({ periodType, periodDays, diff --git a/packages/trpc/server/routers/viewer/slots/util.ts b/packages/trpc/server/routers/viewer/slots/util.ts index 0afa92d2c2..d8e2571813 100644 --- a/packages/trpc/server/routers/viewer/slots/util.ts +++ b/packages/trpc/server/routers/viewer/slots/util.ts @@ -16,7 +16,11 @@ import { parseBookingLimit, parseDurationLimit } from "@calcom/lib"; import { RESERVED_SUBDOMAINS } from "@calcom/lib/constants"; import { getUTCOffsetByTimezone } from "@calcom/lib/date-fns"; import { getDefaultEvent } from "@calcom/lib/defaultEvents"; -import { isDateOutOfBounds, isTimeOutOfBounds, calculatePeriodLimits } from "@calcom/lib/isOutOfBounds"; +import { + isTimeOutOfBounds, + calculatePeriodLimits, + isTimeViolatingFutureLimit, +} from "@calcom/lib/isOutOfBounds"; import logger from "@calcom/lib/logger"; import { safeStringify } from "@calcom/lib/safeStringify"; import { performance } from "@calcom/lib/server/perfObserver"; @@ -761,6 +765,7 @@ export async function getAvailableSlots({ input, ctx }: GetScheduleOptions): Pro const allDatesWithBookabilityStatus = getAllDatesWithBookabilityStatus(availableDates); loggerWithEventDetails.debug(safeStringify({ availableDates })); + const utcOffset = input.timeZone ? getUTCOffsetByTimezone(input.timeZone) ?? 0 : 0; const periodLimits = calculatePeriodLimits({ periodType: eventType.periodType, periodDays: eventType.periodDays, @@ -768,27 +773,37 @@ export async function getAvailableSlots({ input, ctx }: GetScheduleOptions): Pro periodStartDate: eventType.periodStartDate, periodEndDate: eventType.periodEndDate, allDatesWithBookabilityStatus, - utcOffset: input.timeZone ? getUTCOffsetByTimezone(input.timeZone) ?? 0 : 0, + utcOffset, }); - - let foundALimitViolation = false; + let foundAFutureLimitViolation = false; const withinBoundsSlotsMappedToDate = Object.entries(slotsMappedToDate).reduce( (withinBoundsSlotsMappedToDate, [date, slots]) => { - // Computation Optimization: If a limit violation has been found, we just consider all slots to be out of bounds beyond that date. + // Computation Optimization: If a future limit violation has been found, we just consider all slots to be out of bounds beyond that slot. // We can't do the same for periodType=RANGE because it can start from a day other than today and today will hit the violation then. - if (foundALimitViolation && doesRangeStartFromToday(eventType.periodType)) { + if (foundAFutureLimitViolation && doesRangeStartFromToday(eventType.periodType)) { return withinBoundsSlotsMappedToDate; } - const isDateWithinBound = !isDateOutOfBounds({ dateString: date, periodLimits }); - if (isDateWithinBound) { - // TODO: Slots calculation logic already seems to consider the minimum booking notice and past booking time and thus there shouldn't be need to filter out slots here. - withinBoundsSlotsMappedToDate[date] = slots.filter( - (slot) => - !isTimeOutOfBounds({ time: slot.time, minimumBookingNotice: eventType.minimumBookingNotice }) + const filteredSlots = slots.filter((slot) => { + const isFutureLimitViolationForTheSlot = isTimeViolatingFutureLimit({ + time: slot.time, + periodLimits, + }); + if (isFutureLimitViolationForTheSlot) { + foundAFutureLimitViolation = true; + } + return ( + !isFutureLimitViolationForTheSlot && + // TODO: Perf Optmization: Slots calculation logic already seems to consider the minimum booking notice and past booking time and thus there shouldn't be need to filter out slots here. + !isTimeOutOfBounds({ time: slot.time, minimumBookingNotice: eventType.minimumBookingNotice }) ); - } else { - foundALimitViolation = true; + }); + + if (!filteredSlots.length) { + // If there are no slots available, we don't set that date, otherwise having an empty slots array makes frontend consider it as an all day OOO case + return withinBoundsSlotsMappedToDate; } + + withinBoundsSlotsMappedToDate[date] = filteredSlots; return withinBoundsSlotsMappedToDate; }, {} as typeof slotsMappedToDate