From a62c74f79e20e9329ee4dd19dc3079c1e2e14881 Mon Sep 17 00:00:00 2001 From: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com> Date: Fri, 12 Sep 2025 22:58:24 +0530 Subject: [PATCH] fix: make cancellation reason mandatory when cancelledBy param is set to host email (#23764) * fix: make cancelllation Reason mandatory when cancelledBy set as the host * update * tweak * show error message --- apps/web/components/booking/CancelBooking.tsx | 17 +++-- .../bookings/lib/handleCancelBooking.ts | 5 +- .../test/handleCancelBooking.test.ts | 72 +++++++++++++++++++ 3 files changed, 87 insertions(+), 7 deletions(-) diff --git a/apps/web/components/booking/CancelBooking.tsx b/apps/web/components/booking/CancelBooking.tsx index 4591aa92b5..e53a0e7349 100644 --- a/apps/web/components/booking/CancelBooking.tsx +++ b/apps/web/components/booking/CancelBooking.tsx @@ -157,8 +157,11 @@ export default function CancelBooking(props: Props) { const cancellationNoShowFeeWarning = autoChargeNoShowFee(); + const isCancellationUserHost = + props.isHost || bookingCancelledEventProps.organizer.email === currentUserEmail; + const hostMissingCancellationReason = - props.isHost && + isCancellationUserHost && (!cancellationReason?.trim() || (props.internalNotePresets.length > 0 && !internalNote?.id)); const cancellationNoShowFeeNotAcknowledged = !props.isHost && cancellationNoShowFeeWarning && !acknowledgeCancellationNoShowFee; @@ -211,7 +214,7 @@ export default function CancelBooking(props: Props) { > )} - + - {props.isHost ? ( + {isCancellationUserHost ? (
@@ -296,11 +299,13 @@ export default function CancelBooking(props: Props) { }); refreshData(); } else { + const data = await res.json(); setLoading(false); setError( - `${t("error_with_status_code_occured", { status: res.status })} ${t( - "please_try_again" - )}` + data.message || + `${t("error_with_status_code_occured", { status: res.status })} ${t( + "please_try_again" + )}` ); } }} diff --git a/packages/features/bookings/lib/handleCancelBooking.ts b/packages/features/bookings/lib/handleCancelBooking.ts index e70423e794..92dc9f4c0e 100644 --- a/packages/features/bookings/lib/handleCancelBooking.ts +++ b/packages/features/bookings/lib/handleCancelBooking.ts @@ -114,7 +114,10 @@ async function handler(input: CancelBookingInput) { }); } - if (!platformClientId && !cancellationReason?.trim() && bookingToDelete.userId == userId) { + const isCancellationUserHost = + bookingToDelete.userId == userId || bookingToDelete.user.email === cancelledBy; + + if (!platformClientId && !cancellationReason?.trim() && isCancellationUserHost) { throw new HttpError({ statusCode: 400, message: "Cancellation reason is required when you are the host", diff --git a/packages/features/bookings/lib/handleCancelBooking/test/handleCancelBooking.test.ts b/packages/features/bookings/lib/handleCancelBooking/test/handleCancelBooking.test.ts index b8fd087057..0ad54ff65d 100644 --- a/packages/features/bookings/lib/handleCancelBooking/test/handleCancelBooking.test.ts +++ b/packages/features/bookings/lib/handleCancelBooking/test/handleCancelBooking.test.ts @@ -132,6 +132,7 @@ describe("Cancel Booking", () => { id: idOfBookingToBeCancelled, uid: uidOfBookingToBeCancelled, cancelledBy: organizer.email, + cancellationReason: "No reason", }, }); @@ -261,6 +262,7 @@ describe("Cancel Booking", () => { id: idOfBookingToBeCancelled, uid: uidOfBookingToBeCancelled, cancelledBy: organizer.email, + cancellationReason: "No reason", }, }); @@ -510,6 +512,74 @@ describe("Cancel Booking", () => { ).rejects.toThrow("Cannot cancel a booking that has already ended"); }); + test("Should block canceling bookings without a cancellation reason when cancelledBy is set to the host", async () => { + const handleCancelBooking = (await import("@calcom/features/bookings/lib/handleCancelBooking")).default; + + const booker = getBooker({ + email: "booker@example.com", + name: "Booker", + }); + + const organizer = getOrganizer({ + name: "Organizer", + email: "organizer@example.com", + id: 101, + schedules: [TestData.schedules.IstWorkHours], + credentials: [getGoogleCalendarCredential()], + selectedCalendars: [TestData.selectedCalendars.google], + }); + + const uidOfBookingToBeCancelled = "cancelled-booking"; + const idOfBookingToBeCancelled = 3040; + const { dateString: plus1DateString } = getDate({ dateIncrement: 1 }); + + await createBookingScenario( + getScenarioData({ + eventTypes: [ + { + id: 1, + slotInterval: 30, + length: 30, + users: [ + { + id: 101, + }, + ], + }, + ], + bookings: [ + { + id: idOfBookingToBeCancelled, + uid: uidOfBookingToBeCancelled, + eventTypeId: 1, + userId: 101, + responses: { + email: booker.email, + name: booker.name, + location: { optionValue: "", value: BookingLocations.CalVideo }, + }, + status: BookingStatus.ACCEPTED, + startTime: `${plus1DateString}T05:00:00.000Z`, + endTime: `${plus1DateString}T05:30:00.000Z`, + }, + ], + organizer, + apps: [TestData.apps["daily-video"]], + }) + ); + + // This should throw an error with current implementation + await expect( + handleCancelBooking({ + bookingData: { + id: idOfBookingToBeCancelled, + uid: uidOfBookingToBeCancelled, + cancelledBy: organizer.email, + }, + }) + ).rejects.toThrow("Cancellation reason is required when you are the host"); + }); + test("Should not charge cancellation fee when organizer cancels booking", async () => { const handleCancelBooking = (await import("@calcom/features/bookings/lib/handleCancelBooking")).default; @@ -615,6 +685,7 @@ describe("Cancel Booking", () => { id: idOfBookingToBeCancelled, uid: uidOfBookingToBeCancelled, cancelledBy: organizer.email, + cancellationReason: "No reason", }, }); @@ -747,6 +818,7 @@ describe("Cancel Booking", () => { id: idOfBookingToBeCancelled, uid: uidOfBookingToBeCancelled, cancelledBy: organizer.email, + cancellationReason: "No reason", }, });