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
This commit is contained in:
Anik Dhabal Babu authored and GitHub committed 2025-09-12 18:28:24 +01:00
1 parent 1a8b5d145f
commit a62c74f79e
3 files changed
+87 -7

No files matched your search

+11 -6
View File
@@ -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) {
</>
)}
<Label>{props.isHost ? t("cancellation_reason_host") : t("cancellation_reason")}</Label>
<Label>{isCancellationUserHost ? t("cancellation_reason_host") : t("cancellation_reason")}</Label>
<TextArea
data-testid="cancel_reason"
@@ -222,7 +225,7 @@ export default function CancelBooking(props: Props) {
className="mb-4 mt-2 w-full "
rows={3}
/>
{props.isHost ? (
{isCancellationUserHost ? (
<div className="-mt-2 mb-4 flex items-center gap-2">
<Icon name="info" className="text-subtle h-4 w-4" />
<p className="text-default text-subtle text-sm leading-none">
@@ -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"
)}`
);
}
}}
@@ -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",
@@ -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",
},
});