-
Notifications
You must be signed in to change notification settings - Fork 12k
fix: Prevent double bookings with recurring booking links #22803
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,6 +2,10 @@ | |
| import type { BookingResponse } from "@calcom/features/bookings/types"; | ||
| import { SchedulingType } from "@calcom/prisma/client"; | ||
| import type { AppsStatus } from "@calcom/types/Calendar"; | ||
| import { HttpError } from "@calcom/lib/http-error"; | ||
| import { ErrorCode } from "@calcom/lib/errorCodes"; | ||
| import prisma from "@calcom/prisma"; | ||
| import { BookingStatus } from "@calcom/prisma/enums"; | ||
|
|
||
| export type PlatformParams = { | ||
| platformClientId?: string; | ||
|
|
@@ -21,6 +25,74 @@ | |
| noEmail?: boolean; | ||
| } & PlatformParams; | ||
|
|
||
| /** | ||
| * Check for overlapping bookings across all recurring dates | ||
| */ | ||
| async function checkForOverlappingRecurringBookings({ | ||
| eventTypeId, | ||
| recurringDates, | ||
| rescheduleUid, | ||
| }: { | ||
| eventTypeId: number; | ||
| recurringDates: { start: string | undefined; end: string | undefined }[]; | ||
| rescheduleUid?: string; | ||
| }) { | ||
| for (const date of recurringDates) { | ||
| if (!date.start || !date.end) continue; | ||
|
|
||
| const startTime = new Date(date.start); | ||
| const endTime = new Date(date.end); | ||
|
|
||
| const overlappingBookings = await prisma.booking.findFirst({ | ||
| where: { | ||
| eventTypeId, | ||
| status: { | ||
| in: [BookingStatus.ACCEPTED, BookingStatus.PENDING], | ||
| }, | ||
| // Check for overlapping time ranges | ||
| OR: [ | ||
| // New booking starts during an existing booking | ||
| { | ||
| startTime: { lte: startTime }, | ||
| endTime: { gt: startTime }, | ||
| }, | ||
| // New booking ends during an existing booking | ||
| { | ||
| startTime: { lt: endTime }, | ||
| endTime: { gte: endTime }, | ||
| }, | ||
| // New booking completely contains an existing booking | ||
| { | ||
| startTime: { gte: startTime }, | ||
| endTime: { lte: endTime }, | ||
| }, | ||
| // New booking is completely contained within an existing booking | ||
| { | ||
| startTime: { lte: startTime }, | ||
| endTime: { gte: endTime }, | ||
| }, | ||
| ], | ||
| // Exclude the booking being rescheduled | ||
| ...(rescheduleUid && { uid: { not: rescheduleUid } }), | ||
| }, | ||
| select: { | ||
| id: true, | ||
| uid: true, | ||
| startTime: true, | ||
| endTime: true, | ||
| status: true, | ||
| }, | ||
| }); | ||
|
|
||
| if (overlappingBookings) { | ||
| throw new HttpError({ | ||
|
Check failure on line 88 in packages/features/bookings/lib/handleNewRecurringBooking.ts
|
||
| statusCode: 409, | ||
| message: ErrorCode.BookingConflict, | ||
| }); | ||
| } | ||
| } | ||
| } | ||
|
Comment on lines
+28
to
+94
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Optimize database queries for better performance. While the logic is correct, the current implementation makes a separate database query for each recurring date, which could be inefficient for bookings with many recurrences. Consider optimizing with a single query that checks all dates at once: -async function checkForOverlappingRecurringBookings({
- eventTypeId,
- recurringDates,
- rescheduleUid,
-}: {
- eventTypeId: number;
- recurringDates: { start: string | undefined; end: string | undefined }[];
- rescheduleUid?: string;
-}) {
- for (const date of recurringDates) {
- if (!date.start || !date.end) continue;
-
- const startTime = new Date(date.start);
- const endTime = new Date(date.end);
-
- const overlappingBookings = await prisma.booking.findFirst({
- where: {
- eventTypeId,
- status: {
- in: [BookingStatus.ACCEPTED, BookingStatus.PENDING],
- },
- // Check for overlapping time ranges
- OR: [
- // ... overlap conditions
- ],
- // Exclude the booking being rescheduled
- ...(rescheduleUid && { uid: { not: rescheduleUid } }),
- },
- select: {
- id: true,
- uid: true,
- startTime: true,
- endTime: true,
- status: true,
- },
- });
-
- if (overlappingBookings) {
- throw new HttpError({
- statusCode: 409,
- message: ErrorCode.BookingConflict,
- });
- }
- }
-}
+async function checkForOverlappingRecurringBookings({
+ eventTypeId,
+ recurringDates,
+ rescheduleUid,
+}: {
+ eventTypeId: number;
+ recurringDates: { start: string | undefined; end: string | undefined }[];
+ rescheduleUid?: string;
+}) {
+ const validDates = recurringDates
+ .filter(date => date.start && date.end)
+ .map(date => ({
+ startTime: new Date(date.start!),
+ endTime: new Date(date.end!)
+ }));
+
+ if (validDates.length === 0) return;
+
+ const dateConditions = validDates.flatMap(({ startTime, endTime }) => [
+ { startTime: { lte: startTime }, endTime: { gt: startTime } },
+ { startTime: { lt: endTime }, endTime: { gte: endTime } },
+ { startTime: { gte: startTime }, endTime: { lte: endTime } },
+ { startTime: { lte: startTime }, endTime: { gte: endTime } },
+ ]);
+
+ const overlappingBookings = await prisma.booking.findFirst({
+ where: {
+ eventTypeId,
+ status: {
+ in: [BookingStatus.ACCEPTED, BookingStatus.PENDING],
+ },
+ OR: dateConditions,
+ ...(rescheduleUid && { uid: { not: rescheduleUid } }),
+ },
+ select: {
+ id: true,
+ uid: true,
+ startTime: true,
+ endTime: true,
+ status: true,
+ },
+ });
+
+ if (overlappingBookings) {
+ throw new HttpError({
+ statusCode: 409,
+ message: ErrorCode.BookingConflict,
+ });
+ }
+}🤖 Prompt for AI Agents |
||
|
|
||
| export const handleNewRecurringBooking = async (input: BookingHandlerInput): Promise<BookingResponse[]> => { | ||
| const data = input.bookingData; | ||
| const createdBookings: BookingResponse[] = []; | ||
|
|
@@ -49,6 +121,13 @@ | |
| areCalendarEventsEnabled: input.areCalendarEventsEnabled, | ||
| }; | ||
|
|
||
| // Check for overlapping bookings before processing any recurring bookings | ||
| await checkForOverlappingRecurringBookings({ | ||
| eventTypeId: firstBooking.eventTypeId, | ||
| recurringDates: allRecurringDates, | ||
| rescheduleUid: firstBooking.rescheduleUid, | ||
| }); | ||
|
|
||
| if (isRoundRobin) { | ||
| const recurringEventData = { | ||
| ...firstBooking, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
LGTM! Proper integration into the transaction flow.
The overlap check is correctly placed at the beginning of the transaction before any database modifications. The parameters are properly extracted from the booking data.
Verify that
eventTypeIdis always non-null at this point to ensure the non-null assertion is safe:🏁 Script executed:
Length of output: 99
🏁 Script executed:
Length of output: 6345
🏁 Script executed:
Length of output: 2792
Ensure the correct eventTypeId is used for overlap checking
The current call uses
newBookingData.eventTypeId!, butbuildNewBookingDatanever assigns aneventTypeIdproperty—so it will always beundefinedat runtime. Please update the overlap check to use the actualeventType.idfrom the function parameters (or explicitly addeventTypeIdtonewBookingData).Suggested change:
• File: packages/features/bookings/lib/handleNewBooking/createBooking.ts
• Lines: 241–247
📝 Committable suggestion
🤖 Prompt for AI Agents