mirror of
https://github.com/hanzoai/calendar.git
synced 2026-10-30 14:46:43 +00:00
* fix(bookings): refund all seat payments when cancelling a paid seated booking A paid seated booking creates one Payment row per seat on the same bookingId (each seat's payment_intent.succeeded webhook flips only its own payment via handlePaymentSuccess). processPaymentRefund only refunded payment.find((p) => p.success) — the first successful payment — so fully cancelling a multi-seat paid booking with an auto-refund policy refunded a single seat and left the other attendees charged with no error surfaced. Refund every successful, not-yet-refunded payment instead. Tests: unit regression in processPaymentRefund.test.ts (all payments refunded, verified red before the fix); integration test in handleCancelBooking.test.ts proving a full cancel of a seated multi-payment booking hands all payments to the refund path. * fix(bookings): make seat refunds resilient and drop dayjs in test Address CodeRabbit review on #29685: - processPaymentRefund now attempts every seat's refund independently and throws an aggregate error, so one provider failure no longer skips the remaining seats. - Add a regression test for that resilience, and use native Date instead of dayjs for fixed-offset test timestamps.
2129 lines
63 KiB
TypeScript
2129 lines
63 KiB
TypeScript
import {
|
|
BookingLocations,
|
|
createBookingScenario,
|
|
getBooker,
|
|
getGoogleCalendarCredential,
|
|
getOrganizer,
|
|
getScenarioData,
|
|
mockCalendarToHaveNoBusySlots,
|
|
mockSuccessfulVideoMeetingCreation,
|
|
TestData,
|
|
getDate,
|
|
} from "@calcom/testing/lib/bookingScenario/bookingScenario";
|
|
import {
|
|
expectBookingCancelledWebhookToHaveBeenFired,
|
|
} 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", async ({ emails }) => {
|
|
const handleCancelBooking = (await import("@calcom/features/bookings/lib/handleCancelBooking")).default;
|
|
|
|
const booker = getBooker({
|
|
email: "[email protected]",
|
|
name: "Booker",
|
|
});
|
|
|
|
const organizer = getOrganizer({
|
|
name: "Organizer",
|
|
email: "[email protected]",
|
|
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,
|
|
},
|
|
],
|
|
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",
|
|
},
|
|
});
|
|
|
|
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,
|
|
},
|
|
},
|
|
});
|
|
});
|
|
|
|
test("Should throw when cancelling a non-existing booking", async () => {
|
|
const handleCancelBooking = (await import("@calcom/features/bookings/lib/handleCancelBooking")).default;
|
|
|
|
await expect(
|
|
handleCancelBooking({
|
|
bookingData: {
|
|
id: 99999,
|
|
uid: "non-existing-uid",
|
|
cancelledBy: "[email protected]",
|
|
cancellationReason: "No reason",
|
|
},
|
|
})
|
|
).rejects.toThrow("Booking not found.");
|
|
});
|
|
|
|
test("Should call processPaymentRefund", async () => {
|
|
const handleCancelBooking = (await import("@calcom/features/bookings/lib/handleCancelBooking")).default;
|
|
|
|
const booker = getBooker({
|
|
email: "[email protected]",
|
|
name: "Booker",
|
|
});
|
|
|
|
const organizer = getOrganizer({
|
|
name: "Organizer",
|
|
email: "[email protected]",
|
|
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",
|
|
},
|
|
});
|
|
|
|
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 pass all seat payments to processPaymentRefund when fully cancelling a paid seated booking", async () => {
|
|
const handleCancelBooking = (await import("@calcom/features/bookings/lib/handleCancelBooking")).default;
|
|
|
|
const booker = getBooker({
|
|
email: "[email protected]",
|
|
name: "Booker",
|
|
});
|
|
|
|
const organizer = getOrganizer({
|
|
name: "Organizer",
|
|
email: "[email protected]",
|
|
id: 101,
|
|
schedules: [TestData.schedules.IstWorkHours],
|
|
credentials: [getGoogleCalendarCredential()],
|
|
selectedCalendars: [TestData.selectedCalendars.google],
|
|
});
|
|
|
|
const uidOfBookingToBeCancelled = "seated-multi-pay-cancel";
|
|
const idOfBookingToBeCancelled = 1099;
|
|
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({
|
|
eventTypes: [
|
|
{
|
|
id: 1,
|
|
slotInterval: 30,
|
|
length: 30,
|
|
seatsPerTimeSlot: 3,
|
|
users: [{ id: 101 }],
|
|
},
|
|
],
|
|
// Two seats on the same booking → two Payment rows sharing one bookingId.
|
|
payment: [
|
|
{
|
|
amount: 1000,
|
|
bookingId: idOfBookingToBeCancelled,
|
|
currency: "usd",
|
|
data: {},
|
|
externalId: "ext_seat_1",
|
|
fee: 0,
|
|
refunded: false,
|
|
success: true,
|
|
uid: "seat-1-pay",
|
|
},
|
|
{
|
|
amount: 5000,
|
|
bookingId: idOfBookingToBeCancelled,
|
|
currency: "usd",
|
|
data: {},
|
|
externalId: "ext_seat_2",
|
|
fee: 0,
|
|
refunded: false,
|
|
success: true,
|
|
uid: "seat-2-pay",
|
|
},
|
|
],
|
|
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" },
|
|
});
|
|
|
|
// Full cancel (no seatReferenceUid) by the host. Clear first so the count is order-independent.
|
|
vi.mocked(processPaymentRefund).mockClear();
|
|
await handleCancelBooking({
|
|
bookingData: {
|
|
id: idOfBookingToBeCancelled,
|
|
uid: uidOfBookingToBeCancelled,
|
|
cancelledBy: organizer.email,
|
|
cancellationReason: "No reason",
|
|
},
|
|
});
|
|
|
|
expect(processPaymentRefund).toHaveBeenCalledTimes(1);
|
|
const refundArg = vi.mocked(processPaymentRefund).mock.calls[0][0];
|
|
expect(refundArg.booking.payment).toHaveLength(2);
|
|
expect(refundArg.booking.payment.map((p) => p.externalId).sort()).toEqual(["ext_seat_1", "ext_seat_2"]);
|
|
});
|
|
|
|
test("Should block cancelling past bookings", async () => {
|
|
const handleCancelBooking = (await import("@calcom/features/bookings/lib/handleCancelBooking")).default;
|
|
|
|
const booker = getBooker({
|
|
email: "[email protected]",
|
|
name: "Booker",
|
|
});
|
|
|
|
const organizer = getOrganizer({
|
|
name: "Organizer",
|
|
email: "[email protected]",
|
|
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",
|
|
},
|
|
})
|
|
).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: "[email protected]",
|
|
name: "Booker",
|
|
});
|
|
|
|
const organizer = getOrganizer({
|
|
name: "Organizer",
|
|
email: "[email protected]",
|
|
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");
|
|
});
|
|
|
|
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: "[email protected]",
|
|
name: "Booker",
|
|
});
|
|
|
|
const organizer = getOrganizer({
|
|
name: "Organizer",
|
|
email: "[email protected]",
|
|
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",
|
|
},
|
|
});
|
|
|
|
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: "[email protected]",
|
|
name: "Booker",
|
|
});
|
|
|
|
const organizer = getOrganizer({
|
|
name: "Team Admin",
|
|
email: "[email protected]",
|
|
id: 101,
|
|
schedules: [TestData.schedules.IstWorkHours],
|
|
credentials: [getGoogleCalendarCredential()],
|
|
selectedCalendars: [TestData.selectedCalendars.google],
|
|
});
|
|
|
|
const teamMember = getOrganizer({
|
|
name: "Team Member",
|
|
email: "[email protected]",
|
|
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",
|
|
},
|
|
});
|
|
|
|
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: "[email protected]",
|
|
name: "Booker",
|
|
});
|
|
|
|
const organizer = getOrganizer({
|
|
name: "Organizer",
|
|
email: "[email protected]",
|
|
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,
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
});
|
|
|
|
test("Should trigger BOOKING_CANCELLED webhook with username for bookings", async () => {
|
|
const handleCancelBooking = (await import("@calcom/features/bookings/lib/handleCancelBooking")).default;
|
|
|
|
const booker = getBooker({
|
|
email: "[email protected]",
|
|
name: "Booker",
|
|
});
|
|
|
|
const organizer = getOrganizer({
|
|
name: "Organizer",
|
|
email: "[email protected]",
|
|
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",
|
|
},
|
|
});
|
|
|
|
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,
|
|
},
|
|
},
|
|
});
|
|
});
|
|
|
|
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: "[email protected]",
|
|
name: "Booker",
|
|
});
|
|
|
|
const attendee2 = getBooker({
|
|
email: "[email protected]",
|
|
name: "Attendee 2",
|
|
});
|
|
|
|
const organizer = getOrganizer({
|
|
name: "Organizer",
|
|
email: "[email protected]",
|
|
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,
|
|
});
|
|
|
|
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: "[email protected]",
|
|
name: "Booker",
|
|
});
|
|
|
|
const organizer = getOrganizer({
|
|
name: "Organizer",
|
|
email: "[email protected]",
|
|
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,
|
|
});
|
|
|
|
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: "[email protected]",
|
|
name: "Booker",
|
|
});
|
|
|
|
const organizer = getOrganizer({
|
|
name: "Organizer",
|
|
email: "[email protected]",
|
|
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,
|
|
});
|
|
|
|
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: "[email protected]",
|
|
name: "Booker",
|
|
});
|
|
|
|
const organizer = getOrganizer({
|
|
name: "Organizer",
|
|
email: "[email protected]",
|
|
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,
|
|
});
|
|
|
|
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: "[email protected]",
|
|
name: "Booker",
|
|
});
|
|
|
|
const organizer = getOrganizer({
|
|
name: "Organizer",
|
|
email: "[email protected]",
|
|
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: "[email protected]",
|
|
name: "Booker",
|
|
});
|
|
|
|
const organizer = getOrganizer({
|
|
name: "Organizer",
|
|
email: "[email protected]",
|
|
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: "[email protected]",
|
|
name: "Booker",
|
|
});
|
|
|
|
const organizer = getOrganizer({
|
|
name: "Organizer",
|
|
email: "[email protected]",
|
|
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: "[email protected]",
|
|
name: "Booker",
|
|
});
|
|
|
|
const organizer = getOrganizer({
|
|
name: "Organizer",
|
|
email: "[email protected]",
|
|
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: "[email protected]",
|
|
name: "Booker",
|
|
});
|
|
|
|
const organizer = getOrganizer({
|
|
name: "Organizer",
|
|
email: "[email protected]",
|
|
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: "[email protected]",
|
|
name: "Booker",
|
|
});
|
|
|
|
const organizer = getOrganizer({
|
|
name: "Organizer",
|
|
email: "[email protected]",
|
|
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: "[email protected]",
|
|
name: "Booker",
|
|
});
|
|
|
|
const organizer = getOrganizer({
|
|
name: "Organizer",
|
|
email: "[email protected]",
|
|
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: "[email protected]",
|
|
name: "Booker",
|
|
});
|
|
|
|
const organizer = getOrganizer({
|
|
name: "Organizer",
|
|
email: "[email protected]",
|
|
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: "[email protected]",
|
|
name: "Booker",
|
|
});
|
|
|
|
const organizer = getOrganizer({
|
|
name: "Organizer",
|
|
email: "[email protected]",
|
|
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);
|
|
});
|
|
});
|
|
});
|