Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
cff3778
Fixes #24017: Allow the host to cancel the event when `Disable Resche…
GurneeshBudhiraja Sep 24, 2025
f13782b
Update: Add the test in `bookingActions.test.ts` to test that the hos…
GurneeshBudhiraja Sep 24, 2025
786b702
Fixes #24017: Updates the logic for verifying the `isHost`
GurneeshBudhiraja Sep 24, 2025
7666088
Merge branch 'main' into bug/host-unable-to-cancel-event
GurneeshBudhiraja Sep 24, 2025
35d6eb1
update `handleCancelBooking` utility to allow host cancellation when …
GurneeshBudhiraja Sep 24, 2025
dd435b5
Merge branch 'main' into bug/host-unable-to-cancel-event
GurneeshBudhiraja Sep 24, 2025
9a7316b
merge with main and resolve the merge conflicts
GurneeshBudhiraja Nov 28, 2025
635059c
prevent the host from cancelling the past meetings
GurneeshBudhiraja Nov 28, 2025
6b98b56
Changes: add more checks for the `userId` to determine the user host …
GurneeshBudhiraja Nov 28, 2025
34a5fb0
Merge upstream/main into bug/host-unable-to-cancel-event and resolve …
devin-ai-integration[bot] Jan 12, 2026
f97bb6a
Merge upstream/main into bug/host-unable-to-cancel-event and resolve …
devin-ai-integration[bot] Feb 4, 2026
18ac758
update webhook delete confirmation message
romitg2 Feb 4, 2026
0148bdb
revert
romitg2 Feb 5, 2026
9348f9c
fix: prevent hosts from cancelling already cancelled/rejected bookings
devin-ai-integration[bot] Feb 5, 2026
7fe57a9
fix: add missing isHost to BookingActionsDropdown actionContext
devin-ai-integration[bot] Feb 5, 2026
d32a8a0
Merge branch 'main' into bug/host-unable-to-cancel-event
romitg2 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
7 changes: 7 additions & 0 deletions apps/web/components/booking/BookingListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,12 @@ function BookingListItem(booking: BookingItemProps) {

const isAttendee = !!userSeat;

// Checks if the logged in user is the host of the booking
const isHost =
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 5, 2026

Choose a reason for hiding this comment

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

P2: isHost is computed using only userId equality, which doesn’t account for team host assignments that depend on attendee email. This diverges from server-side host determination and can block legitimate hosts from canceling/rescheduling when Disable Cancelling is enabled.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/web/components/booking/BookingListItem.tsx, line 189:

<comment>isHost is computed using only userId equality, which doesn’t account for team host assignments that depend on attendee email. This diverges from server-side host determination and can block legitimate hosts from canceling/rescheduling when Disable Cancelling is enabled.</comment>

<file context>
@@ -185,6 +185,12 @@ function BookingListItem(booking: BookingItemProps) {
   const isAttendee = !!userSeat;
 
+  // Checks if the logged in user is the host of the booking
+  const isHost =
+    booking.user?.id != null &&
+    booking.loggedInUser.userId != null &&
</file context>
Fix with Cubic

booking.user?.id != null &&
booking.loggedInUser.userId != null &&
booking.loggedInUser.userId === booking.user.id;

const paymentAppData = getPaymentAppData(booking.eventType);

const location = booking.location as ReturnType<typeof getEventLocationValue>;
Expand Down Expand Up @@ -234,6 +240,7 @@ function BookingListItem(booking: BookingItemProps) {
showPendingPayment: paymentAppData.enabled && booking.payment.length && !booking.paid,
isAttendee,
cardCharged,
isHost,
attendeeList,
getSeatReferenceUid,
t,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,7 @@ export function BookingActionsDropdown({
showPendingPayment,
isAttendee,
cardCharged,
isHost,
attendeeList,
getSeatReferenceUid,
t,
Expand Down
32 changes: 32 additions & 0 deletions apps/web/components/booking/actions/bookingActions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -592,6 +592,38 @@ describe("Booking Actions", () => {
expect(isActionDisabled("cancel", futureContext)).toBe(false);
});

it("should allow hosts to cancel bookings even when cancellation is disabled", () => {
const context = createMockContext({
isHost: true,
isDisabledCancelling: true,
isBookingInPast: false,
});

expect(isActionDisabled("cancel", context)).toBe(false);
});

it("should not allow hosts to cancel already cancelled bookings", () => {
const context = createMockContext({
isHost: true,
isDisabledCancelling: true,
isBookingInPast: false,
isCancelled: true,
});

expect(isActionDisabled("cancel", context)).toBe(true);
});

it("should not allow hosts to cancel rejected bookings", () => {
const context = createMockContext({
isHost: true,
isDisabledCancelling: true,
isBookingInPast: false,
isRejected: true,
});

expect(isActionDisabled("cancel", context)).toBe(true);
});

it("should disable video actions for non-past bookings", () => {
const context = createMockContext({ isBookingInPast: false });

Expand Down
4 changes: 3 additions & 1 deletion apps/web/components/booking/actions/bookingActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export interface BookingActionContext {
showPendingPayment: boolean;
isAttendee: boolean;
cardCharged: boolean;
isHost?: boolean;
attendeeList: Array<{
name: string;
email: string;
Expand Down Expand Up @@ -237,7 +238,7 @@ export function shouldShowIndividualReportButton(context: BookingActionContext):
}

export function isActionDisabled(actionId: string, context: BookingActionContext): boolean {
const { booking, isBookingInPast, isDisabledRescheduling, isDisabledCancelling, isAttendee, isCancelled, isRejected } = context;
const { booking, isBookingInPast, isDisabledRescheduling, isDisabledCancelling, isAttendee, isHost, isCancelled, isRejected } = context;

switch (actionId) {
case "reschedule":
Expand All @@ -263,6 +264,7 @@ export function isActionDisabled(actionId: string, context: BookingActionContext
isWithinMinimumNotice
);
case "cancel":
if (isHost && !isBookingInPast && !isCancelled && !isRejected) return false;
return isDisabledCancelling || isBookingInPast || isCancelled || isRejected;
case "view_recordings":
return !(isBookingInPast && booking.status === BookingStatus.ACCEPTED && context.isCalVideoLocation);
Expand Down
27 changes: 23 additions & 4 deletions packages/features/bookings/lib/handleCancelBooking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,16 +208,35 @@ async function handler(input: CancelBookingInput, dependencies?: Dependencies) {
throw new HttpError({ statusCode: 400, message: "User not found" });
}

if (bookingToDelete.eventType?.disableCancelling) {
let isCancellationUserHost = false;
if (userId) {
if (bookingToDelete.userId === userId) {
isCancellationUserHost = true;
}
else if (bookingToDelete.eventType?.hosts?.some((host) => host.user.id === userId)) {
isCancellationUserHost = true;
}
else if (bookingToDelete.eventType?.owner?.id === userId) {
isCancellationUserHost = true;
}
else if (
await PrismaOrgMembershipRepository.isLoggedInUserOrgAdminOfBookingHost(
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 4, 2026

Choose a reason for hiding this comment

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

P3: Duplicated host-authorization logic triggers the org-admin DB lookup twice for seated events, adding redundant query work and risking drift between the two authorization checks. Consider reusing the earlier computed result (e.g., cache the org-admin check or reuse isCancellationUserHost) instead of repeating the same call.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/features/bookings/lib/handleCancelBooking.ts, line 222:

<comment>Duplicated host-authorization logic triggers the org-admin DB lookup twice for seated events, adding redundant query work and risking drift between the two authorization checks. Consider reusing the earlier computed result (e.g., cache the org-admin check or reuse isCancellationUserHost) instead of repeating the same call.</comment>

<file context>
@@ -207,16 +207,35 @@ async function handler(input: CancelBookingInput, dependencies?: Dependencies) {
+      isCancellationUserHost = true;
+    }
+    else if (
+      await PrismaOrgMembershipRepository.isLoggedInUserOrgAdminOfBookingHost(
+        userId,
+        bookingToDelete.userId
</file context>
Fix with Cubic

userId,
bookingToDelete.userId
)
) {
isCancellationUserHost = true;
}
}

// Only the host can cancel the booking even when the cancellation is disabled for the event
if (!isCancellationUserHost && bookingToDelete.eventType?.disableCancelling) {
throw new HttpError({
statusCode: 400,
message: "This event type does not allow cancellations",
});
}

const isCancellationUserHost =
bookingToDelete.userId === userId || bookingToDelete.user.email === cancelledBy;

if (
!platformClientId &&
!cancellationReason?.trim() &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -700,14 +700,14 @@ describe("Cancel Booking", () => {
})
);

// This should throw an error with current implementation
await expect(
handleCancelBooking({
bookingData: {
id: idOfBookingToBeCancelled,
uid: uidOfBookingToBeCancelled,
cancelledBy: organizer.email,
},
userId: organizer.id,
})
).rejects.toThrow("Cancellation reason is required when you are the host");
});
Expand Down
Loading