Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Comment on lines 148 to +150

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

correctness: 🟢 [Standard Reviewer] setInterval interval calculation is incorrect: it uses seconds instead of milliseconds, causing the reservation refresh to occur every ~16 hours instead of every 14 minutes.

🤖 AI Agent Prompt for Cursor/Windsurf

📋 Copy this prompt to your AI coding assistant (Cursor, Windsurf, etc.) to get help fixing this issue

In packages/features/bookings/Booker/components/hooks/useSlots.ts, lines 148-150, the interval for setInterval is calculated in seconds instead of milliseconds, causing the reservation refresh to happen every ~16 hours instead of every 14 minutes. Update the interval calculation to use milliseconds: change `parseInt(MINUTES_TO_BOOK) * 60 - 2` to `parseInt(MINUTES_TO_BOOK) * 60 * 1000 - 2000`.
📝 Committable Code Suggestion

‼️ Ensure you review the code suggestion before committing it to the branch. Make sure it replaces the highlighted code, contains no missing lines, and has no issues with indentation.

Suggested change
const interval = setInterval(() => {
handleReserveSlot();
}, parseInt(MINUTES_TO_BOOK) * 60 * 1000 - 2000);
}, parseInt(MINUTES_TO_BOOK) * 60 - 2);
const interval = setInterval(() => {
handleReserveSlot();
}, parseInt(MINUTES_TO_BOOK) * 60 * 1000 - 2000);


return () => {
handleRemoveSlot();
Expand Down
10 changes: 5 additions & 5 deletions packages/features/bookings/Booker/utils/isSlotEquivalent.ts
Original file line number Diff line number Diff line change
@@ -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] === ":";
};

/**
Expand All @@ -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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ type ScheduleData = {

function _isSlotPresent(slotsInIsoForDate: Maybe<SlotsInIso>, 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 });
Expand Down Expand Up @@ -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;

Expand Down
98 changes: 98 additions & 0 deletions packages/features/bookings/Booker/utils/validateBookingSlot.ts
Original file line number Diff line number Diff line change
@@ -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 };
}