diff --git a/packages/trpc/server/routers/viewer/slots/util.test.ts b/packages/trpc/server/routers/viewer/slots/util.test.ts index b3ede3d45e472c..5db75874a5ec30 100644 --- a/packages/trpc/server/routers/viewer/slots/util.test.ts +++ b/packages/trpc/server/routers/viewer/slots/util.test.ts @@ -1,7 +1,10 @@ import { describe, expect, it } from "vitest"; import type { GetAvailabilityUser } from "@calcom/core/getUserAvailability"; +import dayjs from "@calcom/dayjs"; +import type { EventBusyDate } from "@calcom/types/Calendar"; +import { checkIfIsAvailable } from "./util"; import { getUsersWithCredentialsConsideringContactOwner } from "./util"; describe("getUsersWithCredentialsConsideringContactOwner", () => { @@ -88,3 +91,163 @@ describe("getUsersWithCredentialsConsideringContactOwner", () => { ]); }); }); + +describe("checkIfIsAvailable", () => { + const createTestData = (time: string) => ({ + time: dayjs(time), + eventLength: 30, + busy: [] as EventBusyDate[], + }); + + describe("currentSeats handling", () => { + it("should return true if slot exists in currentSeats", () => { + const currentSeats: CurrentSeats = [ + { + uid: "123", + startTime: dayjs.utc("2023-01-01T09:00:00Z").toDate(), + _count: { attendees: 1 }, + }, + ]; + + const result = checkIfIsAvailable({ + ...createTestData("2023-01-01T09:00:00Z"), + currentSeats, + }); + + expect(result).toBe(true); + }); + }); + + describe("busy time overlap scenarios", () => { + it("should return true when no busy periods", () => { + const result = checkIfIsAvailable(createTestData("2023-01-01T09:00:00Z")); + expect(result).toBe(true); + }); + + it("should return true when busy period ends before slot starts", () => { + const result = checkIfIsAvailable({ + ...createTestData("2023-01-01T09:00:00Z"), + busy: [ + { + start: dayjs.utc("2023-01-01T08:00:00Z").toDate(), + end: dayjs.utc("2023-01-01T08:30:00Z").toDate(), + }, + ], + }); + expect(result).toBe(true); + }); + + it("should return true when busy period starts after slot ends", () => { + const result = checkIfIsAvailable({ + ...createTestData("2023-01-01T09:00:00Z"), + busy: [ + { + start: dayjs.utc("2023-01-01T10:00:00Z").toDate(), + end: dayjs.utc("2023-01-01T10:30:00Z").toDate(), + }, + ], + }); + expect(result).toBe(true); + }); + + it("should return false when slot start falls within busy period", () => { + const result = checkIfIsAvailable({ + ...createTestData("2023-01-01T09:15:00Z"), + busy: [ + { + start: dayjs.utc("2023-01-01T09:00:00Z").toDate(), + end: dayjs.utc("2023-01-01T09:30:00Z").toDate(), + }, + ], + }); + expect(result).toBe(false); + }); + + it("should return false when slot end falls within busy period", () => { + const result = checkIfIsAvailable({ + ...createTestData("2023-01-01T08:45:00Z"), + busy: [ + { + start: dayjs.utc("2023-01-01T09:00:00Z").toDate(), + end: dayjs.utc("2023-01-01T09:30:00Z").toDate(), + }, + ], + }); + expect(result).toBe(false); + }); + + it("should return false when busy period is completely contained within slot", () => { + const result = checkIfIsAvailable({ + ...createTestData("2023-01-01T09:00:00Z"), + eventLength: 60, + busy: [ + { + start: dayjs.utc("2023-01-01T09:15:00Z").toDate(), + end: dayjs.utc("2023-01-01T09:45:00Z").toDate(), + }, + ], + }); + expect(result).toBe(false); + }); + + it("should return false when slot is completely contained within busy period", () => { + const result = checkIfIsAvailable({ + ...createTestData("2023-01-01T09:15:00Z"), + busy: [ + { + start: dayjs.utc("2023-01-01T09:00:00Z").toDate(), + end: dayjs.utc("2023-01-01T10:00:00Z").toDate(), + }, + ], + }); + expect(result).toBe(false); + }); + + it("should return true when multiple non-overlapping busy periods", () => { + const result = checkIfIsAvailable({ + ...createTestData("2023-01-01T09:30:00Z"), + busy: [ + { + start: dayjs.utc("2023-01-01T08:00:00Z").toDate(), + end: dayjs.utc("2023-01-01T09:00:00Z").toDate(), + }, + { + start: dayjs.utc("2023-01-01T10:00:00Z").toDate(), + end: dayjs.utc("2023-01-01T11:00:00Z").toDate(), + }, + ], + }); + expect(result).toBe(true); + }); + + it("should return false if any busy period overlaps", () => { + const result = checkIfIsAvailable({ + ...createTestData("2023-01-01T09:30:00Z"), + busy: [ + { + start: dayjs.utc("2023-01-01T08:00:00Z").toDate(), + end: dayjs.utc("2023-01-01T09:00:00Z").toDate(), + }, + { + start: dayjs.utc("2023-01-01T09:45:00Z").toDate(), + end: dayjs.utc("2023-01-01T10:00:00Z").toDate(), + }, + ], + }); + expect(result).toBe(false); + }); + + it("should handle exact boundary conditions", () => { + const result = checkIfIsAvailable({ + ...createTestData("2023-01-01T09:30:00Z"), + busy: [ + { + start: dayjs.utc("2023-01-01T09:00:00Z").toDate(), + end: dayjs.utc("2023-01-01T09:30:00Z").toDate(), + }, + ], + }); + expect(result).toBe(true); + }); + }); +}); diff --git a/packages/trpc/server/routers/viewer/slots/util.ts b/packages/trpc/server/routers/viewer/slots/util.ts index d9dfff3a5a897c..7d06464979465e 100644 --- a/packages/trpc/server/routers/viewer/slots/util.ts +++ b/packages/trpc/server/routers/viewer/slots/util.ts @@ -62,33 +62,38 @@ export const checkIfIsAvailable = ({ return true; } - const slotEndTime = time.add(eventLength, "minutes").utc(); - const slotStartTime = time.utc(); + const slotStartDate = time.utc().toDate(); + const slotEndDate = time.add(eventLength, "minutes").utc().toDate(); return busy.every((busyTime) => { - const startTime = dayjs.utc(busyTime.start).utc(); - const endTime = dayjs.utc(busyTime.end); + const busyStartDate = dayjs.utc(busyTime.start).toDate(); + const busyEndDate = dayjs.utc(busyTime.end).toDate(); - if (endTime.isBefore(slotStartTime) || startTime.isAfter(slotEndTime)) { + // First check if there's any overlap at all + // If busy period ends before slot starts or starts after slot ends, there's no overlap + if (busyEndDate <= slotStartDate || busyStartDate >= slotEndDate) { return true; } - if (slotStartTime.isBetween(startTime, endTime, null, "[)")) { - return false; - } else if (slotEndTime.isBetween(startTime, endTime, null, "(]")) { + // Now check all possible overlap scenarios: + + // 1. Slot start falls within busy period (inclusive start, exclusive end) + if (slotStartDate >= busyStartDate && slotStartDate < busyEndDate) { return false; } - // Check if start times are the same - if (time.utc().isBetween(startTime, endTime, null, "[)")) { + // 2. Slot end falls within busy period (exclusive start, inclusive end) + if (slotEndDate > busyStartDate && slotEndDate <= busyEndDate) { return false; } - // Check if slot end time is between start and end time - else if (slotEndTime.isBetween(startTime, endTime)) { + + // 3. Busy period completely contained within slot + if (busyStartDate >= slotStartDate && busyEndDate <= slotEndDate) { return false; } - // Check if startTime is between slot - else if (startTime.isBetween(time, slotEndTime)) { + + // 4. Slot completely contained within busy period + if (busyStartDate <= slotStartDate && busyEndDate >= slotEndDate) { return false; }