From e3a9f54ba5966157549560cb21d868dc7d804a43 Mon Sep 17 00:00:00 2001 From: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com> Date: Tue, 24 Feb 2026 22:05:51 +0530 Subject: [PATCH] feat: Configure cancellation reason (#26872) * feat: Configure cancellation reason * fix: use enums * tests: add unit tests * fix: type error * chore: remove duplicate dialog * fix: type erro * refator: improvements * refator: improvements --------- Co-authored-by: Keith Williams Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../e2e/seated-bookings.e2e-spec.ts | 2 +- apps/web/components/booking/CancelBooking.tsx | 25 +- .../components/dialog/CancelBookingDialog.tsx | 4 + apps/web/lib/booking.ts | 1 + .../bookings/views/bookings-single-view.tsx | 1 + .../tabs/advanced/EventAdvancedTab.tsx | 89 ++- .../bookings/lib/cancellationReason.ts | 21 + .../bookings/lib/getBookingToDelete.ts | 1 + .../bookings/lib/handleCancelBooking.ts | 16 +- .../test/handleCancelBooking.test.ts | 658 +++++++++++++++++- .../features/eventtypes/lib/defaultEvents.ts | 1 + packages/features/eventtypes/lib/types.ts | 3 +- packages/i18n/locales/en/common.json | 14 +- packages/lib/test/builder.ts | 1 + .../migration.sql | 5 + packages/prisma/schema.prisma | 12 +- packages/prisma/zod-utils.ts | 2 + .../server/routers/viewer/eventTypes/types.ts | 4 + .../cancellation-reason-requirement/CLAUDE.md | 23 + .../decisions.md | 28 + .../cancellation-reason-requirement/design.md | 79 +++ .../docs/README.md | 20 + .../future-work.md | 16 + .../implementation.md | 34 + 24 files changed, 1009 insertions(+), 51 deletions(-) create mode 100644 packages/features/bookings/lib/cancellationReason.ts create mode 100644 packages/prisma/migrations/20260115111819_add_cancellation_reason_require/migration.sql create mode 100644 specs/cancellation-reason-requirement/CLAUDE.md create mode 100644 specs/cancellation-reason-requirement/decisions.md create mode 100644 specs/cancellation-reason-requirement/design.md create mode 100644 specs/cancellation-reason-requirement/docs/README.md create mode 100644 specs/cancellation-reason-requirement/future-work.md create mode 100644 specs/cancellation-reason-requirement/implementation.md diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/seated-bookings.e2e-spec.ts b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/seated-bookings.e2e-spec.ts index 6591c027b9..ac1be9af05 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/seated-bookings.e2e-spec.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/seated-bookings.e2e-spec.ts @@ -1053,7 +1053,7 @@ describe("Bookings Endpoints 2024-08-13", () => { .set("Authorization", `Bearer ${apiKeyString}`) .expect(400); - expect(response.body.message).toEqual("Cancellation reason is required when you are the host"); + expect(response.body.message).toEqual("Cancellation reason is required"); }); it("should cancel seated booking", async () => { diff --git a/apps/web/components/booking/CancelBooking.tsx b/apps/web/components/booking/CancelBooking.tsx index 68382ae43d..7c6662d817 100644 --- a/apps/web/components/booking/CancelBooking.tsx +++ b/apps/web/components/booking/CancelBooking.tsx @@ -1,17 +1,18 @@ "use client"; -import { useCallback, useState } from "react"; - import { sdkActionManager } from "@calcom/embed-core/embed-iframe"; +import { isCancellationReasonRequired } from "@calcom/features/bookings/lib/cancellationReason"; import { shouldChargeNoShowCancellationFee } from "@calcom/features/bookings/lib/payment/shouldChargeNoShowCancellationFee"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useRefreshData } from "@calcom/lib/hooks/useRefreshData"; +import type { CancellationReasonRequirement } from "@calcom/prisma/enums"; import type { RecurringEvent } from "@calcom/types/Calendar"; import classNames from "@calcom/ui/classNames"; import { Button } from "@calcom/ui/components/button"; -import { Label, Select, TextArea, CheckboxField } from "@calcom/ui/components/form"; +import { CheckboxField, Label, Select, TextArea } from "@calcom/ui/components/form"; import { showToast } from "@calcom/ui/components/toast"; import { InfoIcon, XIcon } from "@coss/ui/icons"; +import { useCallback, useState } from "react"; interface InternalNotePresetsSelectProps { internalNotePresets: { id: number; name: string }[]; @@ -110,6 +111,7 @@ type Props = { internalNotePresets: { id: number; name: string; cancellationReason: string | null }[]; renderContext: "booking-single-view" | "dialog"; eventTypeMetadata?: Record | null; + requiresCancellationReason?: CancellationReasonRequirement | null; showErrorAsToast?: boolean; onCanceled?: () => void; }; @@ -163,11 +165,18 @@ export default function CancelBooking(props: Props) { const isCancellationUserHost = props.isHost || bookingCancelledEventProps.organizer.email === currentUserEmail; - const hostMissingCancellationReason = - isCancellationUserHost && - (!cancellationReason?.trim() || (props.internalNotePresets.length > 0 && !internalNote?.id)); + const isReasonRequired = isCancellationReasonRequired( + props.requiresCancellationReason, + isCancellationUserHost + ); + + const missingRequiredReason = isReasonRequired && !cancellationReason?.trim(); + const hostMissingInternalNote = + isCancellationUserHost && props.internalNotePresets.length > 0 && !internalNote?.id; const cancellationNoShowFeeNotAcknowledged = !props.isHost && cancellationNoShowFeeWarning && !acknowledgeCancellationNoShowFee; + const canCancel = + !missingRequiredReason && !hostMissingInternalNote && !cancellationNoShowFeeNotAcknowledged; const cancelBookingRef = useCallback((node: HTMLTextAreaElement) => { if (node !== null) { // eslint-disable-next-line @calcom/eslint/no-scroll-into-view-embed -- CancelBooking is not usually used in embed mode @@ -275,7 +284,7 @@ export default function CancelBooking(props: Props) { )} - +