Files
cal.diy/packages/features/bookings/lib/handleCancelBooking/test/handleCancelBooking.test.ts
T
e3a9f54ba5 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 <keithwillcode@gmail.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-02-24 13:35:51 -03:00

2327 lines
68 KiB
TypeScript

import {
BookingLocations,
createBookingScenario,
getBooker,
getGoogleCalendarCredential,
getOrganizer,
getScenarioData,
mockCalendarToHaveNoBusySlots,
mockSuccessfulVideoMeetingCreation,
TestData,
getDate,
} from "@calcom/testing/lib/bookingScenario/bookingScenario";
import {
expectBookingCancelledWebhookToHaveBeenFired,
expectWorkflowToBeTriggered,
} from "@calcom/testing/lib/bookingScenario/expects";
import { setupAndTeardown } from "@calcom/testing/lib/bookingScenario/setupAndTeardown";
import { describe, expect, vi } from "vitest";
import { processPaymentRefund } from "@calcom/features/bookings/lib/payment/processPaymentRefund";
import { BookingStatus } from "@calcom/prisma/enums";
import { test } from "@calcom/testing/lib/fixtures/fixtures";
vi.mock("@calcom/features/bookings/lib/payment/processPaymentRefund", () => ({
processPaymentRefund: vi.fn(),
}));
describe("Cancel Booking", () => {
setupAndTeardown();
test("Should trigger BOOKING_CANCELLED webhook and workflow", async ({ emails }) => {
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 = "h5Wv3eHgconAED2j4gcVhP";
const idOfBookingToBeCancelled = 1020;
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
await createBookingScenario(
getScenarioData({
webhooks: [
{
userId: organizer.id,
eventTriggers: ["BOOKING_CANCELLED"],
subscriberUrl: "http://my-webhook.example.com",
active: true,
eventTypeId: 1,
appId: null,
},
],
workflows: [
{
userId: organizer.id,
trigger: "EVENT_CANCELLED",
action: "EMAIL_HOST",
template: "REMINDER",
activeOn: [1],
},
],
eventTypes: [
{
id: 1,
slotInterval: 30,
length: 30,
users: [
{
id: 101,
},
],
},
],
bookings: [
{
id: idOfBookingToBeCancelled,
uid: uidOfBookingToBeCancelled,
attendees: [
{
email: booker.email,
timeZone: "Asia/Kolkata",
},
],
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:15:00.000Z`,
metadata: {
videoCallUrl: "https://existing-daily-video-call-url.example.com",
},
},
],
organizer,
apps: [TestData.apps["daily-video"]],
})
);
mockSuccessfulVideoMeetingCreation({
metadataLookupKey: "dailyvideo",
videoMeetingData: {
id: "MOCK_ID",
password: "MOCK_PASS",
url: `http://mock-dailyvideo.example.com/meeting-1`,
},
});
mockCalendarToHaveNoBusySlots("googlecalendar", {
create: {
id: "MOCKED_GOOGLE_CALENDAR_EVENT_ID",
},
});
await handleCancelBooking({
bookingData: {
id: idOfBookingToBeCancelled,
uid: uidOfBookingToBeCancelled,
cancelledBy: organizer.email,
cancellationReason: "No reason",
},
actionSource: "WEBAPP",
});
expectBookingCancelledWebhookToHaveBeenFired({
booker,
organizer,
location: BookingLocations.CalVideo,
subscriberUrl: "http://my-webhook.example.com",
payload: {
cancelledBy: organizer.email,
organizer: {
id: organizer.id,
username: organizer.username,
email: organizer.email,
name: organizer.name,
timeZone: organizer.timeZone,
},
},
});
expectWorkflowToBeTriggered({ emailsToReceive: [organizer.email], emails });
});
test("Should call processPaymentRefund", 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 = "h5Wv3eHgconAED2j4gcVhP";
const idOfBookingToBeCancelled = 1020;
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
const booking = {
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:15:00.000Z`,
metadata: {
videoCallUrl: "https://existing-daily-video-call-url.example.com",
},
attendees: [
{
timeZone: "Asia/Kolkata",
email: booker.email,
},
],
};
await createBookingScenario(
getScenarioData({
webhooks: [
{
userId: organizer.id,
eventTriggers: ["BOOKING_CANCELLED"],
subscriberUrl: "http://my-webhook.example.com",
active: true,
eventTypeId: 1,
appId: null,
},
],
eventTypes: [
{
id: 1,
slotInterval: 30,
length: 30,
users: [
{
id: 101,
},
],
},
],
payment: [
{
amount: 12,
bookingId: idOfBookingToBeCancelled,
currency: "usd",
data: {},
externalId: "ext_id",
fee: 12,
refunded: false,
success: true,
uid: uidOfBookingToBeCancelled,
},
],
bookings: [booking],
organizer,
apps: [TestData.apps["daily-video"]],
})
);
mockSuccessfulVideoMeetingCreation({
metadataLookupKey: "dailyvideo",
videoMeetingData: {
id: "MOCK_ID",
password: "MOCK_PASS",
url: `http://mock-dailyvideo.example.com/meeting-1`,
},
});
mockCalendarToHaveNoBusySlots("googlecalendar", {
create: {
id: "MOCKED_GOOGLE_CALENDAR_EVENT_ID",
},
});
await handleCancelBooking({
bookingData: {
id: idOfBookingToBeCancelled,
uid: uidOfBookingToBeCancelled,
cancelledBy: organizer.email,
cancellationReason: "No reason",
},
actionSource: "WEBAPP",
});
expectBookingCancelledWebhookToHaveBeenFired({
booker,
organizer,
location: BookingLocations.CalVideo,
subscriberUrl: "http://my-webhook.example.com",
payload: {
cancelledBy: organizer.email,
organizer: {
id: organizer.id,
username: organizer.username,
email: organizer.email,
name: organizer.name,
timeZone: organizer.timeZone,
},
},
});
expect(processPaymentRefund).toHaveBeenCalled();
});
test("Should successfully cancel round robin team event when host is also an attendee with workflow emails", async () => {
const handleCancelBooking = (await import("@calcom/features/bookings/lib/handleCancelBooking")).default;
const organizer = getOrganizer({
name: "Host Organizer",
email: "host-organizer@example.com",
id: 101,
schedules: [TestData.schedules.IstWorkHours],
credentials: [getGoogleCalendarCredential()],
selectedCalendars: [TestData.selectedCalendars.google],
});
const hostAttendee = getOrganizer({
name: "Host Attendee",
email: "host-attendee@example.com",
id: 102,
schedules: [TestData.schedules.IstWorkHours],
credentials: [getGoogleCalendarCredential()],
selectedCalendars: [TestData.selectedCalendars.google],
});
const uidOfBookingToBeCancelled = "round-robin-booking-uid";
const idOfBookingToBeCancelled = 2030;
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
await createBookingScenario(
getScenarioData({
webhooks: [
{
userId: organizer.id,
eventTriggers: ["BOOKING_CANCELLED"],
subscriberUrl: "http://my-webhook.example.com",
active: true,
eventTypeId: 2,
appId: null,
},
],
eventTypes: [
{
id: 2,
slotInterval: 30,
length: 30,
schedulingType: "ROUND_ROBIN",
teamId: 1,
users: [
{
id: 101,
},
{
id: 102,
},
],
hosts: [
{
userId: 101,
isFixed: false,
},
{
userId: 102,
isFixed: false,
},
],
},
],
workflows: [
{
id: 1,
name: "Cancellation Email Workflow",
teamId: 1,
trigger: "EVENT_CANCELLED",
action: "EMAIL_ATTENDEE",
template: "REMINDER",
activeOn: [2],
},
],
bookings: [
{
id: idOfBookingToBeCancelled,
uid: uidOfBookingToBeCancelled,
eventTypeId: 2,
userId: 101,
responses: {
email: hostAttendee.email,
name: hostAttendee.name,
location: { optionValue: "", value: BookingLocations.CalVideo },
},
status: BookingStatus.ACCEPTED,
startTime: `${plus1DateString}T05:00:00.000Z`,
endTime: `${plus1DateString}T05:30:00.000Z`,
attendees: [
{
email: hostAttendee.email,
timeZone: "Asia/Kolkata",
locale: "en",
},
],
},
],
users: [organizer, hostAttendee],
apps: [TestData.apps["daily-video"]],
})
);
mockSuccessfulVideoMeetingCreation({
metadataLookupKey: "dailyvideo",
videoMeetingData: {
id: "MOCK_ID",
password: "MOCK_PASS",
url: `http://mock-dailyvideo.example.com/meeting-2`,
},
});
mockCalendarToHaveNoBusySlots("googlecalendar", {
create: {
id: "MOCKED_GOOGLE_CALENDAR_EVENT_ID_2",
},
});
const result = await handleCancelBooking({
bookingData: {
id: idOfBookingToBeCancelled,
uid: uidOfBookingToBeCancelled,
cancelledBy: organizer.email,
cancellationReason: "Testing round robin cancellation with host as attendee",
},
actionSource: "WEBAPP",
});
expect(result.success).toBe(true);
expect(result.bookingId).toBe(idOfBookingToBeCancelled);
expect(result.bookingUid).toBe(uidOfBookingToBeCancelled);
expect(result.onlyRemovedAttendee).toBe(false);
expectBookingCancelledWebhookToHaveBeenFired({
booker: hostAttendee,
organizer,
location: BookingLocations.CalVideo,
subscriberUrl: "http://my-webhook.example.com",
payload: {
cancelledBy: organizer.email,
organizer: {
id: organizer.id,
username: organizer.username,
email: organizer.email,
name: organizer.name,
timeZone: organizer.timeZone,
},
},
});
});
test("Should send EMAIL_HOST cancel workflow notification to both primary and secondary hosts in round robin events", async ({
emails,
}) => {
const handleCancelBooking = (await import("@calcom/features/bookings/lib/handleCancelBooking")).default;
const booker = getBooker({
email: "booker@example.com",
name: "Booker",
});
const primaryHost = getOrganizer({
name: "Primary Host",
email: "primary-host@example.com",
id: 101,
schedules: [TestData.schedules.IstWorkHours],
credentials: [getGoogleCalendarCredential()],
selectedCalendars: [TestData.selectedCalendars.google],
});
const secondaryHost = getOrganizer({
name: "Secondary Host",
email: "secondary-host@example.com",
id: 102,
schedules: [TestData.schedules.IstWorkHours],
credentials: [getGoogleCalendarCredential()],
selectedCalendars: [TestData.selectedCalendars.google],
});
const uidOfBookingToBeCancelled = "round-robin-email-host-workflow-uid";
const idOfBookingToBeCancelled = 2040;
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
await createBookingScenario(
getScenarioData({
eventTypes: [
{
id: 2,
slotInterval: 30,
length: 30,
schedulingType: "ROUND_ROBIN",
teamId: 1,
users: [
{
id: 101,
},
{
id: 102,
},
],
hosts: [
{
userId: 101,
isFixed: false,
},
{
userId: 102,
isFixed: false,
},
],
},
],
workflows: [
{
id: 1,
name: "Cancel Email Host Workflow",
teamId: 1,
trigger: "EVENT_CANCELLED",
action: "EMAIL_HOST",
template: "REMINDER",
activeOn: [2],
},
],
bookings: [
{
id: idOfBookingToBeCancelled,
uid: uidOfBookingToBeCancelled,
eventTypeId: 2,
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`,
attendees: [
{
email: booker.email,
timeZone: "Asia/Kolkata",
locale: "en",
},
{
email: secondaryHost.email,
timeZone: "Asia/Kolkata",
locale: "en",
},
],
},
],
users: [primaryHost, secondaryHost],
apps: [TestData.apps["daily-video"]],
})
);
mockSuccessfulVideoMeetingCreation({
metadataLookupKey: "dailyvideo",
videoMeetingData: {
id: "MOCK_ID",
password: "MOCK_PASS",
url: `http://mock-dailyvideo.example.com/meeting-3`,
},
});
mockCalendarToHaveNoBusySlots("googlecalendar", {
create: {
id: "MOCKED_GOOGLE_CALENDAR_EVENT_ID_3",
},
});
const result = await handleCancelBooking({
bookingData: {
id: idOfBookingToBeCancelled,
uid: uidOfBookingToBeCancelled,
cancelledBy: primaryHost.email,
cancellationReason: "Testing EMAIL_HOST workflow sends to secondary host in round robin",
},
actionSource: "WEBAPP",
});
expect(result.success).toBe(true);
expect(result.bookingId).toBe(idOfBookingToBeCancelled);
expect(result.bookingUid).toBe(uidOfBookingToBeCancelled);
expectWorkflowToBeTriggered({
emailsToReceive: [primaryHost.email, secondaryHost.email],
emails,
});
});
test("Should block cancelling past bookings", 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 = "past-booking";
const idOfBookingToBeCancelled = 3040;
const { dateString: minus1DateString } = 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: `${minus1DateString}T05:00:00.000Z`,
endTime: `${minus1DateString}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,
cancellationReason: "Testing past booking cancellation",
},
actionSource: "WEBAPP",
})
).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,
},
actionSource: "WEBAPP",
})
).rejects.toThrow("Cancellation reason is required");
});
test("Should not charge cancellation fee when organizer cancels booking", 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 = "cancellation-fee-organizer-test";
const idOfBookingToBeCancelled = 4050;
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
await createBookingScenario(
getScenarioData({
eventTypes: [
{
id: 1,
slotInterval: 30,
length: 30,
users: [{ id: 101 }],
metadata: {
apps: {
stripe: {
enabled: true,
paymentOption: "HOLD",
price: 1000,
currency: "usd",
cancellationFeeEnabled: true,
cancellationFeeTimeValue: 2,
cancellationFeeTimeUnit: "hours",
},
},
},
},
],
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`,
attendees: [
{
email: booker.email,
timeZone: "Asia/Kolkata",
},
],
},
],
payment: [
{
amount: 1000,
bookingId: idOfBookingToBeCancelled,
currency: "usd",
data: {},
externalId: "ext_id_cancellation",
fee: 0,
refunded: false,
success: false,
uid: uidOfBookingToBeCancelled,
paymentOption: "HOLD",
},
],
organizer,
apps: [TestData.apps["daily-video"]],
})
);
mockSuccessfulVideoMeetingCreation({
metadataLookupKey: "dailyvideo",
videoMeetingData: {
id: "MOCK_ID",
password: "MOCK_PASS",
url: `http://mock-dailyvideo.example.com/meeting-cancellation`,
},
});
mockCalendarToHaveNoBusySlots("googlecalendar", {
create: {
id: "MOCKED_GOOGLE_CALENDAR_EVENT_ID_CANCELLATION",
},
});
const result = await handleCancelBooking({
bookingData: {
id: idOfBookingToBeCancelled,
uid: uidOfBookingToBeCancelled,
cancelledBy: organizer.email,
cancellationReason: "No reason",
},
actionSource: "WEBAPP",
});
expect(result.success).toBe(true);
});
test("Should not charge cancellation fee when team admin cancels booking", async () => {
const handleCancelBooking = (await import("@calcom/features/bookings/lib/handleCancelBooking")).default;
const booker = getBooker({
email: "booker@example.com",
name: "Booker",
});
const organizer = getOrganizer({
name: "Team Admin",
email: "admin@example.com",
id: 101,
schedules: [TestData.schedules.IstWorkHours],
credentials: [getGoogleCalendarCredential()],
selectedCalendars: [TestData.selectedCalendars.google],
});
const teamMember = getOrganizer({
name: "Team Member",
email: "member@example.com",
id: 102,
schedules: [TestData.schedules.IstWorkHours],
credentials: [getGoogleCalendarCredential()],
selectedCalendars: [TestData.selectedCalendars.google],
});
const uidOfBookingToBeCancelled = "cancellation-fee-team-admin-test";
const idOfBookingToBeCancelled = 4060;
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
await createBookingScenario(
getScenarioData({
eventTypes: [
{
id: 2,
slotInterval: 30,
length: 30,
teamId: 1,
users: [{ id: 101 }, { id: 102 }],
hosts: [
{ userId: 101, isFixed: false },
{ userId: 102, isFixed: false },
],
metadata: {
apps: {
stripe: {
enabled: true,
paymentOption: "HOLD",
price: 1000,
currency: "usd",
cancellationFeeEnabled: true,
cancellationFeeTimeValue: 1,
cancellationFeeTimeUnit: "hours",
},
},
},
},
],
bookings: [
{
id: idOfBookingToBeCancelled,
uid: uidOfBookingToBeCancelled,
eventTypeId: 2,
userId: 102,
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`,
attendees: [
{
email: booker.email,
timeZone: "Asia/Kolkata",
},
],
},
],
payment: [
{
amount: 1000,
bookingId: idOfBookingToBeCancelled,
currency: "usd",
data: {},
externalId: "ext_id_team_cancellation",
fee: 0,
refunded: false,
success: false,
uid: uidOfBookingToBeCancelled,
paymentOption: "HOLD",
},
],
users: [organizer, teamMember],
apps: [TestData.apps["daily-video"]],
})
);
mockSuccessfulVideoMeetingCreation({
metadataLookupKey: "dailyvideo",
videoMeetingData: {
id: "MOCK_ID",
password: "MOCK_PASS",
url: `http://mock-dailyvideo.example.com/meeting-team-cancellation`,
},
});
mockCalendarToHaveNoBusySlots("googlecalendar", {
create: {
id: "MOCKED_GOOGLE_CALENDAR_EVENT_ID_TEAM_CANCELLATION",
},
});
const result = await handleCancelBooking({
bookingData: {
id: idOfBookingToBeCancelled,
uid: uidOfBookingToBeCancelled,
cancelledBy: organizer.email,
cancellationReason: "No reason",
},
actionSource: "WEBAPP",
});
expect(result.success).toBe(true);
});
test("Should charge cancellation fee when attendee cancels within time threshold", 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 = "cancellation-fee-attendee-test";
const idOfBookingToBeCancelled = 4070;
const now = new Date();
const startTime = new Date(now.getTime() + 30 * 60 * 1000);
const endTime = new Date(startTime.getTime() + 30 * 60 * 1000);
await createBookingScenario(
getScenarioData({
eventTypes: [
{
id: 1,
slotInterval: 30,
length: 30,
users: [{ id: 101 }],
metadata: {
apps: {
stripe: {
enabled: true,
paymentOption: "HOLD",
price: 1000,
currency: "usd",
cancellationFeeEnabled: true,
cancellationFeeTimeValue: 1,
cancellationFeeTimeUnit: "hours",
},
},
},
},
],
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: startTime.toISOString(),
endTime: endTime.toISOString(),
attendees: [
{
email: booker.email,
timeZone: "Asia/Kolkata",
},
],
},
],
payment: [
{
amount: 1000,
bookingId: idOfBookingToBeCancelled,
currency: "usd",
data: {},
externalId: "ext_id_attendee_cancellation",
fee: 0,
refunded: false,
success: false,
uid: uidOfBookingToBeCancelled,
paymentOption: "HOLD",
appId: "stripe",
},
],
organizer,
apps: [TestData.apps["daily-video"]],
})
);
mockSuccessfulVideoMeetingCreation({
metadataLookupKey: "dailyvideo",
videoMeetingData: {
id: "MOCK_ID",
password: "MOCK_PASS",
url: `http://mock-dailyvideo.example.com/meeting-attendee-cancellation`,
},
});
mockCalendarToHaveNoBusySlots("googlecalendar", {
create: {
id: "MOCKED_GOOGLE_CALENDAR_EVENT_ID_ATTENDEE_CANCELLATION",
},
});
const result = await handleCancelBooking({
bookingData: {
id: idOfBookingToBeCancelled,
uid: uidOfBookingToBeCancelled,
cancelledBy: booker.email,
cancellationReason: "Attendee cancelled within time threshold",
},
userId: 999,
actionSource: "WEBAPP",
});
expect(result.success).toBe(true);
});
test("Should trigger BOOKING_CANCELLED webhook with username and usernameInOrg for organization bookings", 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,
username: "organizer-username",
schedules: [TestData.schedules.IstWorkHours],
credentials: [getGoogleCalendarCredential()],
selectedCalendars: [TestData.selectedCalendars.google],
});
const uidOfBookingToBeCancelled = "org-booking-uid";
const idOfBookingToBeCancelled = 5080;
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
await createBookingScenario(
getScenarioData(
{
webhooks: [
{
userId: organizer.id,
eventTriggers: ["BOOKING_CANCELLED"],
subscriberUrl: "http://my-webhook.example.com",
active: true,
eventTypeId: 1,
appId: null,
},
],
eventTypes: [
{
id: 1,
slotInterval: 30,
length: 30,
users: [
{
id: 101,
},
],
},
],
bookings: [
{
id: idOfBookingToBeCancelled,
uid: uidOfBookingToBeCancelled,
attendees: [
{
email: booker.email,
},
],
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:15:00.000Z`,
metadata: {
videoCallUrl: "https://existing-daily-video-call-url.example.com",
},
},
],
organizer,
apps: [TestData.apps["daily-video"]],
},
{
id: 1,
profileUsername: "username-in-org",
}
)
);
mockSuccessfulVideoMeetingCreation({
metadataLookupKey: "dailyvideo",
videoMeetingData: {
id: "MOCK_ID",
password: "MOCK_PASS",
url: `http://mock-dailyvideo.example.com/meeting-org`,
},
});
mockCalendarToHaveNoBusySlots("googlecalendar", {
create: {
id: "MOCKED_GOOGLE_CALENDAR_EVENT_ID_ORG",
},
});
await handleCancelBooking({
bookingData: {
id: idOfBookingToBeCancelled,
uid: uidOfBookingToBeCancelled,
cancelledBy: organizer.email,
cancellationReason: "Organization booking cancellation test",
},
actionSource: "WEBAPP",
});
expectBookingCancelledWebhookToHaveBeenFired({
booker,
organizer: {
...organizer,
usernameInOrg: "username-in-org",
},
location: BookingLocations.CalVideo,
subscriberUrl: "http://my-webhook.example.com",
payload: {
cancelledBy: organizer.email,
organizer: {
id: organizer.id,
username: organizer.username,
email: organizer.email,
name: organizer.name,
timeZone: organizer.timeZone,
},
},
});
});
test("Should cancel seated event and delete all attendees when seatsPerTimeSlot is enabled", async () => {
const handleCancelBooking = (await import("@calcom/features/bookings/lib/handleCancelBooking")).default;
const booker = getBooker({
email: "booker@example.com",
name: "Booker",
});
const attendee2 = getBooker({
email: "attendee2@example.com",
name: "Attendee 2",
});
const organizer = getOrganizer({
name: "Organizer",
email: "organizer@example.com",
id: 101,
schedules: [TestData.schedules.IstWorkHours],
credentials: [getGoogleCalendarCredential()],
selectedCalendars: [TestData.selectedCalendars.google],
});
const uidOfBookingToBeCancelled = "seated-event-booking";
const idOfBookingToBeCancelled = 4050;
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
await createBookingScenario(
getScenarioData({
eventTypes: [
{
id: 1,
slotInterval: 30,
length: 30,
seatsPerTimeSlot: 5, // Enable seated events
users: [
{
id: 101,
},
],
hosts: [
{
id: 101,
userId: 101,
},
],
},
],
bookings: [
{
id: idOfBookingToBeCancelled,
uid: uidOfBookingToBeCancelled,
attendees: [
{
email: booker.email,
},
{
email: attendee2.email,
},
],
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"]],
})
);
mockSuccessfulVideoMeetingCreation({
metadataLookupKey: "dailyvideo",
videoMeetingData: {
id: "MOCK_ID",
password: "MOCK_PASS",
url: `http://mock-dailyvideo.example.com/meeting-seated`,
},
});
mockCalendarToHaveNoBusySlots("googlecalendar", {
create: {
id: "MOCKED_GOOGLE_CALENDAR_EVENT_ID_SEATED",
},
});
const result = await handleCancelBooking({
bookingData: {
id: idOfBookingToBeCancelled,
uid: uidOfBookingToBeCancelled,
cancelledBy: organizer.email,
cancellationReason: "Cancelling seated event",
},
userId: organizer.id,
actionSource: "WEBAPP",
});
expect(result.success).toBe(true);
expect(result.onlyRemovedAttendee).toBe(false);
expect(result.bookingId).toBe(idOfBookingToBeCancelled);
});
test("Should cancel all remaining recurring bookings when allRemainingBookings is true", 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 = "recurring-booking-1";
const idOfBookingToBeCancelled = 5060;
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
const { dateString: plus2DateString } = getDate({ dateIncrement: 2 });
const recurringEventId = "recurring-event-123";
await createBookingScenario(
getScenarioData({
eventTypes: [
{
id: 1,
slotInterval: 30,
length: 30,
recurringEvent: {
freq: 2, // weekly
count: 3,
interval: 1,
},
users: [
{
id: 101,
},
],
},
],
bookings: [
{
id: idOfBookingToBeCancelled,
uid: uidOfBookingToBeCancelled,
recurringEventId,
attendees: [
{
email: booker.email,
},
],
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`,
},
// Additional recurring booking instance
{
id: idOfBookingToBeCancelled + 1,
uid: "recurring-booking-2",
recurringEventId,
attendees: [
{
email: booker.email,
},
],
eventTypeId: 1,
userId: 101,
responses: {
email: booker.email,
name: booker.name,
location: { optionValue: "", value: BookingLocations.CalVideo },
},
status: BookingStatus.ACCEPTED,
startTime: `${plus2DateString}T05:00:00.000Z`,
endTime: `${plus2DateString}T05:30:00.000Z`,
},
],
organizer,
apps: [TestData.apps["daily-video"]],
})
);
mockSuccessfulVideoMeetingCreation({
metadataLookupKey: "dailyvideo",
videoMeetingData: {
id: "MOCK_ID",
password: "MOCK_PASS",
url: `http://mock-dailyvideo.example.com/meeting-recurring`,
},
});
mockCalendarToHaveNoBusySlots("googlecalendar", {
create: {
id: "MOCKED_GOOGLE_CALENDAR_EVENT_ID_RECURRING",
},
});
const result = await handleCancelBooking({
bookingData: {
id: idOfBookingToBeCancelled,
uid: uidOfBookingToBeCancelled,
cancelledBy: organizer.email,
cancellationReason: "Cancelling all remaining recurring bookings",
allRemainingBookings: true,
},
userId: organizer.id,
actionSource: "WEBAPP",
});
expect(result.success).toBe(true);
expect(result.onlyRemovedAttendee).toBe(false);
expect(result.bookingId).toBe(idOfBookingToBeCancelled);
});
test("Should cancel subsequent bookings when cancelSubsequentBookings is true", 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 = "recurring-subsequent-1";
const idOfBookingToBeCancelled = 6070;
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
const { dateString: plus2DateString } = getDate({ dateIncrement: 2 });
const recurringEventId = "recurring-subsequent-456";
await createBookingScenario(
getScenarioData({
eventTypes: [
{
id: 1,
slotInterval: 30,
length: 30,
recurringEvent: {
freq: 2, // weekly
count: 3,
interval: 1,
},
users: [
{
id: 101,
},
],
},
],
bookings: [
{
id: idOfBookingToBeCancelled,
uid: uidOfBookingToBeCancelled,
recurringEventId,
attendees: [
{
email: booker.email,
},
],
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`,
},
// Subsequent booking that should be cancelled
{
id: idOfBookingToBeCancelled + 1,
uid: "recurring-subsequent-2",
recurringEventId,
attendees: [
{
email: booker.email,
},
],
eventTypeId: 1,
userId: 101,
responses: {
email: booker.email,
name: booker.name,
location: { optionValue: "", value: BookingLocations.CalVideo },
},
status: BookingStatus.ACCEPTED,
startTime: `${plus2DateString}T05:00:00.000Z`,
endTime: `${plus2DateString}T05:30:00.000Z`,
},
],
organizer,
apps: [TestData.apps["daily-video"]],
})
);
mockSuccessfulVideoMeetingCreation({
metadataLookupKey: "dailyvideo",
videoMeetingData: {
id: "MOCK_ID",
password: "MOCK_PASS",
url: `http://mock-dailyvideo.example.com/meeting-subsequent`,
},
});
mockCalendarToHaveNoBusySlots("googlecalendar", {
create: {
id: "MOCKED_GOOGLE_CALENDAR_EVENT_ID_SUBSEQUENT",
},
});
const result = await handleCancelBooking({
bookingData: {
id: idOfBookingToBeCancelled,
uid: uidOfBookingToBeCancelled,
cancelledBy: organizer.email,
cancellationReason: "Cancelling this and all subsequent bookings",
cancelSubsequentBookings: true,
},
userId: organizer.id,
actionSource: "WEBAPP",
});
expect(result.success).toBe(true);
expect(result.onlyRemovedAttendee).toBe(false);
expect(result.bookingId).toBe(idOfBookingToBeCancelled);
});
test("Should handle booking reference cleanup during cancellation", 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 = "booking-with-references";
const idOfBookingToBeCancelled = 7080;
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
await createBookingScenario(
getScenarioData({
eventTypes: [
{
id: 1,
slotInterval: 30,
length: 30,
users: [
{
id: 101,
},
],
},
],
bookings: [
{
id: idOfBookingToBeCancelled,
uid: uidOfBookingToBeCancelled,
attendees: [
{
email: booker.email,
},
],
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`,
references: [
{
id: 1,
type: "daily_video",
uid: "daily-meeting-ref",
meetingId: "daily123",
meetingUrl: "https://daily.co/meeting123",
meetingPassword: "pass123",
credentialId: 1,
deleted: null,
},
{
id: 2,
type: "google_calendar",
uid: "gcal-event-ref",
meetingId: "gcal456",
meetingUrl: null,
meetingPassword: null,
credentialId: 2,
deleted: null,
},
],
},
],
organizer,
apps: [TestData.apps["daily-video"], TestData.apps["google-calendar"]],
})
);
mockSuccessfulVideoMeetingCreation({
metadataLookupKey: "dailyvideo",
videoMeetingData: {
id: "MOCK_ID",
password: "MOCK_PASS",
url: `http://mock-dailyvideo.example.com/meeting-references`,
},
});
mockCalendarToHaveNoBusySlots("googlecalendar", {
create: {
id: "MOCKED_GOOGLE_CALENDAR_EVENT_ID_REFERENCES",
},
});
const result = await handleCancelBooking({
bookingData: {
id: idOfBookingToBeCancelled,
uid: uidOfBookingToBeCancelled,
cancelledBy: organizer.email,
cancellationReason: "Testing booking reference cleanup",
},
userId: organizer.id,
actionSource: "WEBAPP",
});
expect(result.success).toBe(true);
expect(result.onlyRemovedAttendee).toBe(false);
expect(result.bookingId).toBe(idOfBookingToBeCancelled);
});
describe("Cancellation Reason Requirement", () => {
test("Should block host cancellation without reason when requiresCancellationReason is MANDATORY_BOTH", 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 = "mandatory-both-host-test";
const idOfBookingToBeCancelled = 8001;
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
await createBookingScenario(
getScenarioData({
eventTypes: [
{
id: 1,
slotInterval: 30,
length: 30,
requiresCancellationReason: "MANDATORY_BOTH",
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"]],
})
);
await expect(
handleCancelBooking({
bookingData: {
id: idOfBookingToBeCancelled,
uid: uidOfBookingToBeCancelled,
cancelledBy: organizer.email,
},
})
).rejects.toThrow("Cancellation reason is required");
});
test("Should block attendee cancellation without reason when requiresCancellationReason is MANDATORY_BOTH", 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 = "mandatory-both-attendee-test";
const idOfBookingToBeCancelled = 8002;
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
await createBookingScenario(
getScenarioData({
eventTypes: [
{
id: 1,
slotInterval: 30,
length: 30,
requiresCancellationReason: "MANDATORY_BOTH",
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"]],
})
);
await expect(
handleCancelBooking({
bookingData: {
id: idOfBookingToBeCancelled,
uid: uidOfBookingToBeCancelled,
cancelledBy: booker.email,
},
})
).rejects.toThrow("Cancellation reason is required");
});
test("Should block attendee cancellation without reason when requiresCancellationReason is MANDATORY_ATTENDEE_ONLY", 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 = "mandatory-attendee-only-test";
const idOfBookingToBeCancelled = 8003;
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
await createBookingScenario(
getScenarioData({
eventTypes: [
{
id: 1,
slotInterval: 30,
length: 30,
requiresCancellationReason: "MANDATORY_ATTENDEE_ONLY",
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"]],
})
);
await expect(
handleCancelBooking({
bookingData: {
id: idOfBookingToBeCancelled,
uid: uidOfBookingToBeCancelled,
cancelledBy: booker.email,
},
})
).rejects.toThrow("Cancellation reason is required");
});
test("Should allow host cancellation without reason when requiresCancellationReason is MANDATORY_ATTENDEE_ONLY", 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 = "mandatory-attendee-host-allowed-test";
const idOfBookingToBeCancelled = 8004;
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
await createBookingScenario(
getScenarioData({
eventTypes: [
{
id: 1,
slotInterval: 30,
length: 30,
requiresCancellationReason: "MANDATORY_ATTENDEE_ONLY",
users: [{ id: 101 }],
},
],
bookings: [
{
id: idOfBookingToBeCancelled,
uid: uidOfBookingToBeCancelled,
eventTypeId: 1,
userId: 101,
attendees: [{ email: booker.email, timeZone: "Asia/Kolkata" }],
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"]],
})
);
mockSuccessfulVideoMeetingCreation({
metadataLookupKey: "dailyvideo",
videoMeetingData: {
id: "MOCK_ID",
password: "MOCK_PASS",
url: `http://mock-dailyvideo.example.com/meeting-attendee-only`,
},
});
mockCalendarToHaveNoBusySlots("googlecalendar", {
create: {
id: "MOCKED_GOOGLE_CALENDAR_EVENT_ID_ATTENDEE_ONLY",
},
});
const result = await handleCancelBooking({
bookingData: {
id: idOfBookingToBeCancelled,
uid: uidOfBookingToBeCancelled,
cancelledBy: organizer.email,
},
});
expect(result.success).toBe(true);
});
test("Should allow host cancellation without reason when requiresCancellationReason is OPTIONAL_BOTH", 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 = "optional-both-host-test";
const idOfBookingToBeCancelled = 8005;
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
await createBookingScenario(
getScenarioData({
eventTypes: [
{
id: 1,
slotInterval: 30,
length: 30,
requiresCancellationReason: "OPTIONAL_BOTH",
users: [{ id: 101 }],
},
],
bookings: [
{
id: idOfBookingToBeCancelled,
uid: uidOfBookingToBeCancelled,
eventTypeId: 1,
userId: 101,
attendees: [{ email: booker.email, timeZone: "Asia/Kolkata" }],
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"]],
})
);
mockSuccessfulVideoMeetingCreation({
metadataLookupKey: "dailyvideo",
videoMeetingData: {
id: "MOCK_ID",
password: "MOCK_PASS",
url: `http://mock-dailyvideo.example.com/meeting-optional-host`,
},
});
mockCalendarToHaveNoBusySlots("googlecalendar", {
create: {
id: "MOCKED_GOOGLE_CALENDAR_EVENT_ID_OPTIONAL_HOST",
},
});
const result = await handleCancelBooking({
bookingData: {
id: idOfBookingToBeCancelled,
uid: uidOfBookingToBeCancelled,
cancelledBy: organizer.email,
},
});
expect(result.success).toBe(true);
});
test("Should allow attendee cancellation without reason when requiresCancellationReason is OPTIONAL_BOTH", 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 = "optional-both-attendee-test";
const idOfBookingToBeCancelled = 8006;
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
await createBookingScenario(
getScenarioData({
eventTypes: [
{
id: 1,
slotInterval: 30,
length: 30,
requiresCancellationReason: "OPTIONAL_BOTH",
users: [{ id: 101 }],
},
],
bookings: [
{
id: idOfBookingToBeCancelled,
uid: uidOfBookingToBeCancelled,
eventTypeId: 1,
userId: 101,
attendees: [{ email: booker.email, timeZone: "Asia/Kolkata" }],
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"]],
})
);
mockSuccessfulVideoMeetingCreation({
metadataLookupKey: "dailyvideo",
videoMeetingData: {
id: "MOCK_ID",
password: "MOCK_PASS",
url: `http://mock-dailyvideo.example.com/meeting-optional-attendee`,
},
});
mockCalendarToHaveNoBusySlots("googlecalendar", {
create: {
id: "MOCKED_GOOGLE_CALENDAR_EVENT_ID_OPTIONAL_ATTENDEE",
},
});
const result = await handleCancelBooking({
bookingData: {
id: idOfBookingToBeCancelled,
uid: uidOfBookingToBeCancelled,
cancelledBy: booker.email,
},
});
expect(result.success).toBe(true);
});
test("Should allow attendee cancellation without reason when requiresCancellationReason is MANDATORY_HOST_ONLY", 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 = "mandatory-host-attendee-allowed-test";
const idOfBookingToBeCancelled = 8007;
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
await createBookingScenario(
getScenarioData({
eventTypes: [
{
id: 1,
slotInterval: 30,
length: 30,
requiresCancellationReason: "MANDATORY_HOST_ONLY",
users: [{ id: 101 }],
},
],
bookings: [
{
id: idOfBookingToBeCancelled,
uid: uidOfBookingToBeCancelled,
eventTypeId: 1,
userId: 101,
attendees: [{ email: booker.email, timeZone: "Asia/Kolkata" }],
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"]],
})
);
mockSuccessfulVideoMeetingCreation({
metadataLookupKey: "dailyvideo",
videoMeetingData: {
id: "MOCK_ID",
password: "MOCK_PASS",
url: `http://mock-dailyvideo.example.com/meeting-host-only-attendee`,
},
});
mockCalendarToHaveNoBusySlots("googlecalendar", {
create: {
id: "MOCKED_GOOGLE_CALENDAR_EVENT_ID_HOST_ONLY_ATTENDEE",
},
});
const result = await handleCancelBooking({
bookingData: {
id: idOfBookingToBeCancelled,
uid: uidOfBookingToBeCancelled,
cancelledBy: booker.email,
},
});
expect(result.success).toBe(true);
});
test("Should block host cancellation without reason when requiresCancellationReason is null (default behavior)", 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 = "null-default-host-test";
const idOfBookingToBeCancelled = 8008;
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"]],
})
);
await expect(
handleCancelBooking({
bookingData: {
id: idOfBookingToBeCancelled,
uid: uidOfBookingToBeCancelled,
cancelledBy: organizer.email,
},
})
).rejects.toThrow("Cancellation reason is required");
});
test("Should allow attendee cancellation without reason when requiresCancellationReason is null (default behavior)", 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 = "null-default-attendee-test";
const idOfBookingToBeCancelled = 8009;
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,
attendees: [{ email: booker.email, timeZone: "Asia/Kolkata" }],
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"]],
})
);
mockSuccessfulVideoMeetingCreation({
metadataLookupKey: "dailyvideo",
videoMeetingData: {
id: "MOCK_ID",
password: "MOCK_PASS",
url: `http://mock-dailyvideo.example.com/meeting-null-default`,
},
});
mockCalendarToHaveNoBusySlots("googlecalendar", {
create: {
id: "MOCKED_GOOGLE_CALENDAR_EVENT_ID_NULL_DEFAULT",
},
});
const result = await handleCancelBooking({
bookingData: {
id: idOfBookingToBeCancelled,
uid: uidOfBookingToBeCancelled,
cancelledBy: booker.email,
},
});
expect(result.success).toBe(true);
});
});
});