Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
2997679
Update util.ts
bandhan-majumder Aug 28, 2025
a33f5ee
update query
bandhan-majumder Aug 28, 2025
a6b8311
limiting query scope
bandhan-majumder Aug 29, 2025
de93601
organizer is an attendee case handled
bandhan-majumder Aug 29, 2025
cbd3956
tests
bandhan-majumder Aug 30, 2025
8987bda
final tests
bandhan-majumder Aug 30, 2025
8924e84
Merge branch 'main' into fix-23069
kart1ka Sep 4, 2025
0386e7d
Merge branch 'main' into fix-23069
bandhan-majumder Sep 4, 2025
7e3c130
remove comments
bandhan-majumder Sep 4, 2025
895c137
Remove comments from booking.ts
bandhan-majumder Sep 4, 2025
79d29a0
Remove commented parameter from booking method
bandhan-majumder Sep 4, 2025
66b53bf
Merge branch 'main' into fix-23069
kart1ka Sep 9, 2025
15dd6a0
Merge branch 'main' into fix-23069
kart1ka Sep 22, 2025
dad5dcf
Merge branch 'main' into fix-23069
bandhan-majumder Sep 26, 2025
0bd71a7
add: improved text on advanced settings
bandhan-majumder Sep 26, 2025
e9a2cc3
Merge branch 'main' into fix-23069
bandhan-majumder Sep 29, 2025
a5769da
better troubleshooting for pending blockslot bookings
bandhan-majumder Sep 29, 2025
72f95f6
Merge branch 'main' into fix-23069
bandhan-majumder Sep 29, 2025
6c288b5
type fix
bandhan-majumder Sep 29, 2025
a938618
Merge branch 'calcom:main' into fix-23069
bandhan-majumder Oct 6, 2025
7f53ecd
updated tooltips for advanced settings and troubleshooter
bandhan-majumder Oct 6, 2025
4e5a74f
updated title in troubleshooter tooltip
bandhan-majumder Oct 6, 2025
e177708
Merge remote-tracking branch 'origin/main' into fix-23069
bandhan-majumder Oct 10, 2025
313f488
Merge branch 'main' into fix-23069
bandhan-majumder Oct 16, 2025
0bb6a47
suggestions
bandhan-majumder Oct 28, 2025
25cae71
Merge branch 'main' into fix-23069
bandhan-majumder Oct 31, 2025
1f8c5ca
Merge branch 'main' into fix-23069
bandhan-majumder Nov 10, 2025
886b1d4
Merge branch 'main' into fix-23069
bandhan-majumder Nov 24, 2025
4ea2bb4
Merge branch 'main' into fix-23069
bandhan-majumder Nov 28, 2025
51dc181
Delete packages/features/bookings/repositories/BookingRepository.test.ts
bandhan-majumder Dec 12, 2025
724fa6e
Merge branch 'main' into fix-23069
bandhan-majumder Dec 12, 2025
fc22cee
add: tests in new existing file
bandhan-majumder Dec 12, 2025
607e77c
translations revert
bandhan-majumder Dec 12, 2025
cd29824
Merge branch 'main' into fix-23069
bandhan-majumder Dec 16, 2025
4840bb3
Merge branch 'main' into fix-23069
bandhan-majumder Jan 2, 2026
494cdb5
Merge branch 'main' into fix-23069
bandhan-majumder Feb 8, 2026
dd6ea86
Merge branch 'main' into fix-23069
bandhan-majumder Feb 10, 2026
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 @@ -17,7 +17,9 @@ import { Select } from "@calcom/ui/components/form";
import { CheckboxField } from "@calcom/ui/components/form";
import { Input } from "@calcom/ui/components/form";
import { SettingsToggle } from "@calcom/ui/components/form";
import { Icon } from "@calcom/ui/components/icon";
import { RadioField } from "@calcom/ui/components/radio";
import { Tooltip } from "@calcom/ui/components/tooltip";

export type RequiresConfirmationCustomClassNames = SettingsToggleClassNames & {
radioGroupContainer?: string;
Expand Down Expand Up @@ -247,22 +249,30 @@ export default function RequiresConfirmationController({
id="notice"
value="notice"
/>
<div className="-ml-1 stack-y-2">
<CheckboxField
checked={requiresConfirmationWillBlockSlot}
descriptionAsLabel
description={t("requires_confirmation_will_block_slot_description")}
className={customClassNames?.conditionalConfirmationRadio?.checkbox}
descriptionClassName={
customClassNames?.conditionalConfirmationRadio?.checkboxDescription
}
onChange={(e) => {
// We set should dirty to properly detect when we can submit the form
formMethods.setValue("requiresConfirmationWillBlockSlot", e.target.checked, {
shouldDirty: true,
});
}}
/>
<div className="-ml-1 space-y-2">
<div className="flex gap-2">
<CheckboxField
checked={requiresConfirmationWillBlockSlot}
descriptionAsLabel
description={t("requires_confirmation_will_block_slot_description")}
className={customClassNames?.conditionalConfirmationRadio?.checkbox}
descriptionClassName={
customClassNames?.conditionalConfirmationRadio?.checkboxDescription
}
onChange={(e) => {
// We set should dirty to properly detect when we can submit the form
formMethods.setValue("requiresConfirmationWillBlockSlot", e.target.checked, {
shouldDirty: true,
});
}}
/>
<div className="flex flex-col items-center justify-center">
<Tooltip
content={t("requires_confirmation_will_block_slot_description_tooltip")}>
<Icon name="info" className="text-muted-foreground h-4 w-4" />
</Tooltip>
</div>
</div>
<CheckboxField
checked={requiresConfirmationForFreeEmail}
descriptionAsLabel
Expand Down
3 changes: 2 additions & 1 deletion apps/web/public/static/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -1222,8 +1222,9 @@
"disable_notes_description": "For privacy reasons, additional inputs and notes will be hidden in the calendar entry. They will still be sent to your email. <0>Learn more</0>",
"notes_hidden_by_organizer": "Notes have been hidden by the organizer",
"requires_confirmation_description": "The booking needs to be manually confirmed before it is pushed to your calendar and a confirmation is sent. <0>Learn more</0>",
"requires_confirmation_will_block_slot_description_tooltip": "When enabled, bookings will block availability across all event types",
"recurring_event": "Recurring Event",
"requires_confirmation_will_block_slot_description": "Unconfirmed bookings still block calendar slots.",
"recurring_event": "Recurring event",
"recurring_event_description": "People can subscribe for recurring events. <0>Learn more</0>",
"connect_and_join": "Connect and join",
"cannot_be_used_with_paid_event_types": "It cannot be used with paid event types",
Expand Down
10 changes: 8 additions & 2 deletions packages/features/availability/lib/getUserAvailability.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,10 +132,16 @@ export type GetUserAvailabilityInitialData = {
eventType?: EventType;
currentSeats?: CurrentSeats;
rescheduleUid?: string | null;
currentBookings?: (Pick<Booking, "id" | "uid" | "userId" | "startTime" | "endTime" | "title"> & {
currentBookings?: (Pick<Booking, "id" | "uid" | "userId" | "startTime" | "endTime" | "title" | "status"> & {
eventType: Pick<
PrismaEventType,
"id" | "beforeEventBuffer" | "afterEventBuffer" | "seatsPerTimeSlot"
| "id"
| "beforeEventBuffer"
| "afterEventBuffer"
| "title"
| "seatsPerTimeSlot"
| "requiresConfirmation"
| "requiresConfirmationWillBlockSlot"
> | null;
_count?: {
seatsReferences: number;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ import { BookingRepository } from "./BookingRepository";

// Track resources to clean up
const createdBookingIds: number[] = [];
const createdEventTypeIds: number[] = [];
let testUserId: number;
let testUser2Id: number;
let testEventTypeId: number | null = null;

async function clearTestBookings() {
Expand All @@ -23,6 +25,13 @@ async function clearTestBookings() {
});
createdBookingIds.length = 0;
}

if (createdEventTypeIds.length > 0) {
await prisma.eventType.deleteMany({
where: { id: { in: createdEventTypeIds } },
});
createdEventTypeIds.length = 0;
}
}

async function createAttendeeNoShowTestBookings() {
Expand Down Expand Up @@ -344,4 +353,269 @@ describe("BookingRepository (Integration Tests)", () => {
);
});
});
});
});

describe("_findAllExistingBookingsForEventTypeBetween", () => {
beforeAll(async () => {
const testUser = await prisma.user.findFirstOrThrow({
where: { email: "member0-acme@example.com" },
});
testUserId = testUser.id;

const testUser2 = await prisma.user.findFirstOrThrow({
where: { email: { not: "member0-acme@example.com" } },
});
testUser2Id = testUser2.id;
});

beforeEach(() => {
vi.setSystemTime(new Date("2025-05-01T12:00:00.000Z"));
vi.resetAllMocks();
});

afterEach(async () => {
await clearTestBookings();
vi.useRealTimers();
});

describe("Cross-event-type PENDING bookings with requiresConfirmationWillBlockSlot", () => {
it("PENDING booking with block slot enabled should block slots globally for all event types", async () => {
// Create Event Type A
const eventTypeA = await prisma.eventType.create({
data: {
title: "Event Type A",
slug: `event-type-a-${Date.now()}`,
description: "Event Type A with slot blocking",
userId: testUserId,
length: 60,
requiresConfirmation: true,
requiresConfirmationWillBlockSlot: true,
},
});
createdEventTypeIds.push(eventTypeA.id);

// Create Event Type B
const eventTypeB = await prisma.eventType.create({
data: {
title: "Event Type B",
slug: `event-type-b-${Date.now()}`,
description: "Event Type B",
userId: testUserId,
length: 60,
requiresConfirmation: false,
requiresConfirmationWillBlockSlot: false,
},
});
createdEventTypeIds.push(eventTypeB.id);

// create a PENDING booking for Event Type A
const booking = await prisma.booking.create({
data: {
userId: testUserId,
uid: "pending-booking-type-a",
eventTypeId: eventTypeA.id,
status: BookingStatus.PENDING,
attendees: {
create: {
email: "test1@example.com",
noShow: false,
name: "Test 1",
timeZone: "America/Toronto",
},
},
startTime: new Date("2025-05-01T14:00:00.000Z"),
endTime: new Date("2025-05-01T15:00:00.000Z"),
title: "PENDING Event Type A Booking",
},
});
createdBookingIds.push(booking.id);

const bookingRepo = new BookingRepository(prisma);
const userIdAndEmailMap = new Map([[testUserId, "test1@example.com"]]);

// finding PENDING bookings that has blocked slots globally for all event types
const bookings = await bookingRepo.findAllExistingBookingsForEventTypeBetween({
startDate: new Date("2025-05-01T13:00:00.000Z"),
endDate: new Date("2025-05-01T16:00:00.000Z"),
userIdAndEmailMap,
});

expect(bookings).toHaveLength(1);
});

it("should NOT include PENDING bookings that don't block slots", async () => {
const eventType = await prisma.eventType.create({
data: {
title: "Non-blocking Event Type",
slug: `non-blocking-event-type-${Date.now()}`,
description: "This event type doesn't block slots",
userId: testUserId,
length: 60,
requiresConfirmation: true,
requiresConfirmationWillBlockSlot: false,
},
});
createdEventTypeIds.push(eventType.id);

const booking = await prisma.booking.create({
data: {
userId: testUserId,
uid: "pending-non-blocking",
eventTypeId: eventType.id,
status: BookingStatus.PENDING,
attendees: {
create: {
email: "test1@example.com",
noShow: false,
name: "Test 1",
timeZone: "America/Toronto",
},
},
startTime: new Date("2025-05-01T14:00:00.000Z"),
endTime: new Date("2025-05-01T15:00:00.000Z"),
title: "PENDING Non-blocking Event",
},
});
createdBookingIds.push(booking.id);

const bookingRepo = new BookingRepository(prisma);
const userIdAndEmailMap = new Map([[testUserId, "test1@example.com"]]);

const bookings = await bookingRepo.findAllExistingBookingsForEventTypeBetween({
startDate: new Date("2025-05-01T13:00:00.000Z"),
endDate: new Date("2025-05-01T16:00:00.000Z"),
userIdAndEmailMap,
});

expect(bookings).toHaveLength(0);
});

it("should remove duplicate PENDING bookings when organizer books their own event type", async () => {
// Create event type with slot blocking
const eventType = await prisma.eventType.create({
data: {
title: "Self-Booking Event Type",
slug: `self-booking-event-type-${Date.now()}`,
description: "Event type for self-booking scenario",
userId: testUserId,
length: 60,
requiresConfirmation: true,
requiresConfirmationWillBlockSlot: true,
},
});
createdEventTypeIds.push(eventType.id);

// Create a PENDING booking where organizer is also an attendee
const booking = await prisma.booking.create({
data: {
userId: testUserId,
uid: "pending-self-booking",
eventTypeId: eventType.id,
status: BookingStatus.PENDING,
attendees: {
create: {
email: "organizer@example.com",
noShow: false,
name: "Organizer",
timeZone: "America/Toronto",
},
},
startTime: new Date("2025-05-01T14:00:00.000Z"),
endTime: new Date("2025-05-01T15:00:00.000Z"),
title: "PENDING Self-Booking",
},
});
createdBookingIds.push(booking.id);

const bookingRepo = new BookingRepository(prisma);
const userIdAndEmailMap = new Map([[testUserId, "organizer@example.com"]]);

const bookings = await bookingRepo.findAllExistingBookingsForEventTypeBetween({
startDate: new Date("2025-05-01T13:00:00.000Z"),
endDate: new Date("2025-05-01T16:00:00.000Z"),
userIdAndEmailMap,
});

// Should return only 1 booking, not 2
// The booking appears in both pendingAndBlockingBookingsWhereUserIsOrganizer
// and pendingAndBlockingBookingsWhereUserIsAttendee queries
expect(bookings).toHaveLength(1);
expect(bookings[0].uid).toBe("pending-self-booking");
expect(bookings[0].status).toBe(BookingStatus.PENDING);
});

it("should include separate PENDING bookings when organizer and attendee are different people", async () => {
// Create event type with slot blocking
const eventType = await prisma.eventType.create({
data: {
title: "Multi-User Event Type",
slug: `multi-user-event-type-${Date.now()}`,
description: "Event type for multi-user scenario",
userId: testUserId,
length: 60,
requiresConfirmation: true,
requiresConfirmationWillBlockSlot: true,
},
});
createdEventTypeIds.push(eventType.id);

// Create a PENDING booking where organizer hosts for a different attendee
const booking1 = await prisma.booking.create({
data: {
userId: testUserId,
uid: "pending-organizer-booking",
eventTypeId: eventType.id,
status: BookingStatus.PENDING,
attendees: {
create: {
email: "attendee@example.com",
noShow: false,
name: "Attendee",
timeZone: "America/Toronto",
},
},
startTime: new Date("2025-05-01T14:00:00.000Z"),
endTime: new Date("2025-05-01T15:00:00.000Z"),
title: "PENDING Organizer Booking",
},
});
createdBookingIds.push(booking1.id);

// Create another PENDING booking where organizer is an attendee on someone else's event
const booking2 = await prisma.booking.create({
data: {
userId: testUser2Id,
uid: "pending-attendee-booking",
eventTypeId: eventType.id,
status: BookingStatus.PENDING,
attendees: {
create: {
email: "organizer@example.com",
noShow: false,
name: "Organizer as Attendee",
timeZone: "America/Toronto",
},
},
startTime: new Date("2025-05-01T16:00:00.000Z"),
endTime: new Date("2025-05-01T17:00:00.000Z"),
title: "PENDING Attendee Booking",
},
});
createdBookingIds.push(booking2.id);

const bookingRepo = new BookingRepository(prisma);
const userIdAndEmailMap = new Map([[testUserId, "organizer@example.com"]]);

const bookings = await bookingRepo.findAllExistingBookingsForEventTypeBetween({
startDate: new Date("2025-05-01T13:00:00.000Z"),
endDate: new Date("2025-05-01T18:00:00.000Z"),
userIdAndEmailMap,
});

// Should return both bookings with no duplicates
expect(bookings).toHaveLength(2);
expect(bookings.map((b) => b.uid)).toContain("pending-organizer-booking");
expect(bookings.map((b) => b.uid)).toContain("pending-attendee-booking");
});
});
});
Loading
Loading