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/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 0b8b8ceb05e7ec..39a11b5b75b31b 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 }); @@ -78,11 +79,9 @@ export const isTimeSlotAvailable = ({ slotToCheckInIso: SlotInIsoFormat; quickAvailabilityChecks: QuickAvailabilityCheck[]; }) => { - const isUnavailableAsPerQuickCheck = - quickAvailabilityChecks && - quickAvailabilityChecks.some( - (slot) => slot.utcStartIso === slotToCheckInIso && slot.status !== "available" - ); + const isUnavailableAsPerQuickCheck = quickAvailabilityChecks?.some( + (slot) => slot.utcStartIso === slotToCheckInIso && slot.status !== "available" + ); if (isUnavailableAsPerQuickCheck) return false; 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 }; +}