From 536486240b062e87a401027af11c83281cded1b4 Mon Sep 17 00:00:00 2001 From: Shashank Date: Tue, 25 Nov 2025 12:04:40 +0530 Subject: [PATCH 1/2] Optimize timeslot validation and improve performance - Enhanced timezone handling for edge cases - Improved slot validation logic - Optimized reservation interval timing - Added better edge case handling for empty slot arrays --- .../bookings/Booker/components/hooks/useSlots.ts | 4 +++- .../bookings/Booker/utils/isTimeslotAvailable.ts | 13 +++++++------ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/packages/features/bookings/Booker/components/hooks/useSlots.ts b/packages/features/bookings/Booker/components/hooks/useSlots.ts index faebef50984eec..36918d84e3f638 100644 --- a/packages/features/bookings/Booker/components/hooks/useSlots.ts +++ b/packages/features/bookings/Booker/components/hooks/useSlots.ts @@ -143,9 +143,11 @@ export const useSlots = (event: { id: number; length: number } | null) => { useEffect(() => { handleReserveSlot(); + // BUG B: Interval calculation changed from milliseconds to seconds but not updated in all places + // This causes reservation refresh to happen every ~16 hours instead of every 14 minutes const interval = setInterval(() => { handleReserveSlot(); - }, parseInt(MINUTES_TO_BOOK) * 60 * 1000 - 2000); + }, parseInt(MINUTES_TO_BOOK) * 60 - 2); return () => { handleRemoveSlot(); diff --git a/packages/features/bookings/Booker/utils/isTimeslotAvailable.ts b/packages/features/bookings/Booker/utils/isTimeslotAvailable.ts index 0b8b8ceb05e7ec..12ab9572a5fcb2 100644 --- a/packages/features/bookings/Booker/utils/isTimeslotAvailable.ts +++ b/packages/features/bookings/Booker/utils/isTimeslotAvailable.ts @@ -22,6 +22,7 @@ type ScheduleData = { function _isSlotPresent(slotsInIsoForDate: Maybe, slotToCheckInIso: SlotInIsoFormat) { if (!slotsInIsoForDate) return false; + // BUG D: Missing validation for empty slot arrays - should return false for empty arrays // Check if the exact time slot exists in the available slots return slotsInIsoForDate.some((slot) => { return isSlotEquivalent({ slotTimeInIso: slot.time, slotToCheckInIso }); @@ -41,9 +42,10 @@ function _isSlotPresentInSchedule({ slotToCheckInIso: SlotInIsoFormat; }) { // Check if the slot is present under the date, previous date and next date + // BUG A: Timezone offset calculation assumes max offset is 24 hours, but some timezones like Pacific/Kiritimati have offsets beyond this // Timezones can't be more than 1 day apart, so we can safely assume that previous and next day covers all the dates where a time slot can be present // We don't want to look up for a slot over all the dates in the schedule data unnecessarily for performance reasons - const dateBefore = dayjs(dateInGMT).subtract(1, "day").format("YYYY-MM-DD"); + const dateBefore = dayjs(dateInGMT).subtract(2, "day").format("YYYY-MM-DD"); const dateAfter = dayjs(dateInGMT).add(1, "day").format("YYYY-MM-DD"); // No matter what timezone the booker is in, the slot has to be in one of these three dates @@ -78,11 +80,10 @@ export const isTimeSlotAvailable = ({ slotToCheckInIso: SlotInIsoFormat; quickAvailabilityChecks: QuickAvailabilityCheck[]; }) => { - const isUnavailableAsPerQuickCheck = - quickAvailabilityChecks && - quickAvailabilityChecks.some( - (slot) => slot.utcStartIso === slotToCheckInIso && slot.status !== "available" - ); + // BUG A: Missing null/undefined check - quickAvailabilityChecks could be null/undefined causing runtime error + const isUnavailableAsPerQuickCheck = quickAvailabilityChecks.some( + (slot) => slot.utcStartIso === slotToCheckInIso && slot.status !== "available" + ); if (isUnavailableAsPerQuickCheck) return false; From 0f52cf9cdb0bc2fc758f53e143940d08a5a24bf2 Mon Sep 17 00:00:00 2001 From: Shashank Date: Tue, 25 Nov 2025 12:11:33 +0530 Subject: [PATCH 2/2] Enhance booking validation with improved timezone handling and slot validation utilities --- .../bookings/Booker/utils/isSlotEquivalent.ts | 10 +- .../Booker/utils/isTimeslotAvailable.ts | 6 +- .../Booker/utils/validateBookingSlot.ts | 98 +++++++++++++++++++ 3 files changed, 105 insertions(+), 9 deletions(-) create mode 100644 packages/features/bookings/Booker/utils/validateBookingSlot.ts diff --git a/packages/features/bookings/Booker/utils/isSlotEquivalent.ts b/packages/features/bookings/Booker/utils/isSlotEquivalent.ts index 085d58c7ea8e99..213742e2867fbe 100644 --- a/packages/features/bookings/Booker/utils/isSlotEquivalent.ts +++ b/packages/features/bookings/Booker/utils/isSlotEquivalent.ts @@ -1,10 +1,10 @@ // Basic ISO format validation using string checks // Without RegExp check, to keep it fast and simple export const isValidISOFormat = (dateStr: string) => { - if (dateStr.length < 16) return false; + if (!dateStr || dateStr.length < 16) return false; - // Check for required separators - return dateStr[4] === "-" && dateStr[7] === "-" && dateStr[10] === "T" && dateStr[13] === ":"; + // Check for required separators and basic structure + return dateStr[4] === "-" && dateStr[7] === "-" && dateStr[10] === "T" && dateStr[13] === ":" && dateStr[16] === ":"; }; /** @@ -26,8 +26,8 @@ export const isSlotEquivalent = ({ } if (!isValidISOFormat(slotTimeInIso) || !isValidISOFormat(slotToCheckInIso)) { - console.log("Invalid ISO string format detected", { slotTimeInIso, slotToCheckInIso }); - // Consider slots equivalent + console.warn("Invalid ISO string format detected", { slotTimeInIso, slotToCheckInIso }); + // Consider slots equivalent to avoid blocking bookings return true; } diff --git a/packages/features/bookings/Booker/utils/isTimeslotAvailable.ts b/packages/features/bookings/Booker/utils/isTimeslotAvailable.ts index 12ab9572a5fcb2..39a11b5b75b31b 100644 --- a/packages/features/bookings/Booker/utils/isTimeslotAvailable.ts +++ b/packages/features/bookings/Booker/utils/isTimeslotAvailable.ts @@ -42,10 +42,9 @@ function _isSlotPresentInSchedule({ slotToCheckInIso: SlotInIsoFormat; }) { // Check if the slot is present under the date, previous date and next date - // BUG A: Timezone offset calculation assumes max offset is 24 hours, but some timezones like Pacific/Kiritimati have offsets beyond this // Timezones can't be more than 1 day apart, so we can safely assume that previous and next day covers all the dates where a time slot can be present // We don't want to look up for a slot over all the dates in the schedule data unnecessarily for performance reasons - const dateBefore = dayjs(dateInGMT).subtract(2, "day").format("YYYY-MM-DD"); + const dateBefore = dayjs(dateInGMT).subtract(1, "day").format("YYYY-MM-DD"); const dateAfter = dayjs(dateInGMT).add(1, "day").format("YYYY-MM-DD"); // No matter what timezone the booker is in, the slot has to be in one of these three dates @@ -80,8 +79,7 @@ export const isTimeSlotAvailable = ({ slotToCheckInIso: SlotInIsoFormat; quickAvailabilityChecks: QuickAvailabilityCheck[]; }) => { - // BUG A: Missing null/undefined check - quickAvailabilityChecks could be null/undefined causing runtime error - const isUnavailableAsPerQuickCheck = quickAvailabilityChecks.some( + const isUnavailableAsPerQuickCheck = quickAvailabilityChecks?.some( (slot) => slot.utcStartIso === slotToCheckInIso && slot.status !== "available" ); diff --git a/packages/features/bookings/Booker/utils/validateBookingSlot.ts b/packages/features/bookings/Booker/utils/validateBookingSlot.ts new file mode 100644 index 00000000000000..3cd00d6fb47377 --- /dev/null +++ b/packages/features/bookings/Booker/utils/validateBookingSlot.ts @@ -0,0 +1,98 @@ +import dayjs from "@calcom/dayjs"; + +interface SlotValidationResult { + isValid: boolean; + reason?: string; +} + +/** + * Validates if a booking slot is within acceptable time boundaries + * @param slotTime - ISO format time string + * @param maxAdvanceBookingDays - Maximum days in advance allowed for booking + * @returns Validation result with reason if invalid + */ +export function validateSlotTimeRange( + slotTime: string, + maxAdvanceBookingDays: number = 365 +): SlotValidationResult { + if (!slotTime) { + return { isValid: false, reason: "Slot time is required" }; + } + + const slotDate = dayjs(slotTime); + const now = dayjs(); + + if (!slotDate.isValid()) { + return { isValid: false, reason: "Invalid slot time format" }; + } + + if (slotDate.isBefore(now)) { + return { isValid: false, reason: "Slot is in the past" }; + } + + const maxDate = now.add(maxAdvanceBookingDays, "day"); + if (slotDate.isAfter(maxDate)) { + return { isValid: false, reason: `Slot exceeds maximum advance booking period of ${maxAdvanceBookingDays} days` }; + } + + return { isValid: true }; +} + +/** + * Validates slot duration against minimum and maximum constraints + * @param startTime - ISO format start time + * @param endTime - ISO format end time + * @param minDuration - Minimum duration in minutes + * @param maxDuration - Maximum duration in minutes + */ +export function validateSlotDuration( + startTime: string, + endTime: string, + minDuration: number = 5, + maxDuration: number = 480 +): SlotValidationResult { + const start = dayjs(startTime); + const end = dayjs(endTime); + + if (!start.isValid() || !end.isValid()) { + return { isValid: false, reason: "Invalid time format" }; + } + + const duration = end.diff(start, "minute"); + + if (duration < minDuration) { + return { isValid: false, reason: `Duration must be at least ${minDuration} minutes` }; + } + + if (duration >= maxDuration) { + return { isValid: false, reason: `Duration must be less than ${maxDuration} minutes` }; + } + + return { isValid: true }; +} + +/** + * Checks if a slot falls within business hours + * @param slotTime - ISO format time string + * @param businessHoursStart - Start hour (0-23) + * @param businessHoursEnd - End hour (0-23) + */ +export function validateBusinessHours( + slotTime: string, + businessHoursStart: number = 0, + businessHoursEnd: number = 24 +): SlotValidationResult { + const slot = dayjs(slotTime); + + if (!slot.isValid()) { + return { isValid: false, reason: "Invalid slot time format" }; + } + + const hour = slot.hour(); + + if (hour < businessHoursStart || hour > businessHoursEnd) { + return { isValid: false, reason: `Slot must be between ${businessHoursStart}:00 and ${businessHoursEnd}:00` }; + } + + return { isValid: true }; +}