mirror of
https://github.com/hanzoai/calendar.git
synced 2026-10-30 14:46:43 +00:00
Signed-off-by: Chayan Das <[email protected]> Co-authored-by: Bandhan Majumder <[email protected]>
553 lines
20 KiB
TypeScript
553 lines
20 KiB
TypeScript
import { DailyLocationType } from "@calcom/app-store/constants";
|
|
import { FAKE_DAILY_CREDENTIAL } from "@calcom/app-store/dailyvideo/lib/VideoApiAdapter";
|
|
import { eventTypeMetaDataSchemaWithTypedApps } from "@calcom/app-store/zod-utils";
|
|
import dayjs from "@calcom/dayjs";
|
|
import { sendCancelledEmailsAndSMS } from "@calcom/emails/email-manager";
|
|
import { BookingReferenceRepository } from "@calcom/features/bookingReference/repositories/BookingReferenceRepository";
|
|
import EventManager from "@calcom/features/bookings/lib/EventManager";
|
|
import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses";
|
|
import { processNoShowFeeOnCancellation } from "@calcom/features/bookings/lib/payment/processNoShowFeeOnCancellation";
|
|
import { processPaymentRefund } from "@calcom/features/bookings/lib/payment/processPaymentRefund";
|
|
import {
|
|
type EventTypeBrandingData,
|
|
getEventTypeService,
|
|
} from "@calcom/features/eventtypes/di/EventTypeService.container";
|
|
import { UserRepository } from "@calcom/features/users/repositories/UserRepository";
|
|
import type { GetSubscriberOptions } from "@calcom/features/webhooks/lib/getWebhooks";
|
|
import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks";
|
|
import {
|
|
cancelNoShowTasksForBooking,
|
|
deleteWebhookScheduledTriggers,
|
|
} from "@calcom/features/webhooks/lib/scheduleTrigger";
|
|
import sendPayload from "@calcom/features/webhooks/lib/sendOrSchedulePayload";
|
|
import type { EventTypeInfo } from "@calcom/features/webhooks/lib/sendPayload";
|
|
import { HttpError } from "@calcom/lib/http-error";
|
|
import { isPrismaObjOrUndefined } from "@calcom/lib/isPrismaObj";
|
|
import { parseRecurringEvent } from "@calcom/lib/isRecurringEvent";
|
|
import logger from "@calcom/lib/logger";
|
|
import { safeStringify } from "@calcom/lib/safeStringify";
|
|
import { getTranslation } from "@calcom/i18n/server";
|
|
import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat";
|
|
// TODO: Prisma import would be used from DI in a followup PR when we remove `handler` export
|
|
import prisma from "@calcom/prisma";
|
|
import type { WebhookTriggerEvents } from "@calcom/prisma/enums";
|
|
import { BookingStatus } from "@calcom/prisma/enums";
|
|
|
|
import { isCancellationReasonRequired } from "./cancellationReason";
|
|
import type { EventTypeMetadata } from "@calcom/prisma/zod-utils";
|
|
import { bookingCancelInput } from "@calcom/prisma/zod-utils";
|
|
import type { CalendarEvent } from "@calcom/types/Calendar";
|
|
import type { z } from "zod";
|
|
import { BookingRepository } from "../repositories/BookingRepository";
|
|
import { PrismaBookingAttendeeRepository } from "../repositories/PrismaBookingAttendeeRepository";
|
|
import type {
|
|
CancelBookingMeta,
|
|
CancelRegularBookingData,
|
|
HandleCancelBookingResponse,
|
|
} from "./dto/BookingCancel";
|
|
import { getAllCredentialsIncludeServiceAccountKey } from "./getAllCredentialsForUsersOnEvent/getAllCredentials";
|
|
import { getBookingToDelete } from "./getBookingToDelete";
|
|
import cancelAttendeeSeat from "./handleSeats/cancel/cancelAttendeeSeat";
|
|
import type { IBookingCancelService } from "./interfaces/IBookingCancelService";
|
|
import { isPrismaError } from "@calcom/lib/server/getServerErrorFromUnknown";
|
|
|
|
const log = logger.getSubLogger({ prefix: ["handleCancelBooking"] });
|
|
|
|
type PlatformParams = {
|
|
platformClientId?: string;
|
|
platformRescheduleUrl?: string;
|
|
platformCancelUrl?: string;
|
|
platformBookingUrl?: string;
|
|
arePlatformEmailsEnabled?: boolean;
|
|
};
|
|
|
|
export type BookingToDelete = Awaited<ReturnType<typeof getBookingToDelete>>;
|
|
|
|
export type CancelBookingInput = {
|
|
userId?: number;
|
|
userUuid?: string;
|
|
bookingData: z.infer<typeof bookingCancelInput>;
|
|
actionSource?: string;
|
|
actor?: unknown;
|
|
} & PlatformParams;
|
|
|
|
type Dependencies = {
|
|
userRepository: UserRepository;
|
|
bookingRepository: BookingRepository;
|
|
bookingReferenceRepository: BookingReferenceRepository;
|
|
attendeeRepository: PrismaBookingAttendeeRepository;
|
|
};
|
|
|
|
async function handler(input: CancelBookingInput, dependencies?: Dependencies) {
|
|
const prismaClient = prisma;
|
|
const {
|
|
userRepository,
|
|
bookingRepository,
|
|
bookingReferenceRepository,
|
|
attendeeRepository,
|
|
} = dependencies || {
|
|
userRepository: new UserRepository(prismaClient),
|
|
bookingRepository: new BookingRepository(prismaClient),
|
|
bookingReferenceRepository: new BookingReferenceRepository({ prismaClient }),
|
|
attendeeRepository: new PrismaBookingAttendeeRepository(prismaClient),
|
|
};
|
|
const body = input.bookingData;
|
|
const {
|
|
id,
|
|
uid,
|
|
allRemainingBookings,
|
|
cancellationReason,
|
|
seatReferenceUid,
|
|
cancelledBy,
|
|
cancelSubsequentBookings,
|
|
skipCancellationReasonValidation = false,
|
|
skipCalendarSyncTaskCancellation = false,
|
|
} = bookingCancelInput.parse(body);
|
|
let bookingToDelete: BookingToDelete
|
|
try {
|
|
bookingToDelete = await getBookingToDelete(id, uid);
|
|
} catch (error) {
|
|
if (isPrismaError(error) && error.code === "P2025") // Record not found
|
|
{
|
|
throw new HttpError({
|
|
statusCode: 404,
|
|
message: "Booking not found.",
|
|
});
|
|
}
|
|
throw error;
|
|
}
|
|
const {
|
|
userId,
|
|
platformBookingUrl,
|
|
platformCancelUrl,
|
|
platformClientId,
|
|
platformRescheduleUrl,
|
|
arePlatformEmailsEnabled,
|
|
} = input;
|
|
|
|
|
|
/**
|
|
* Important: We prevent cancelling an already cancelled booking.
|
|
* A booking could have been CANCELLED due to a reschedule,
|
|
* in which case we simply update the existing calendar event and meeting.
|
|
* We want to avoid deleting them by a subsequent cancellation attempt.
|
|
*/
|
|
if (bookingToDelete.status === BookingStatus.CANCELLED) {
|
|
throw new HttpError({
|
|
statusCode: 400,
|
|
message: "This booking has already been cancelled.",
|
|
});
|
|
}
|
|
|
|
if (!bookingToDelete.userId || !bookingToDelete.user) {
|
|
throw new HttpError({ statusCode: 400, message: "User not found" });
|
|
}
|
|
|
|
if (bookingToDelete.eventType?.disableCancelling) {
|
|
throw new HttpError({
|
|
statusCode: 400,
|
|
message: "This event type does not allow cancellations",
|
|
});
|
|
}
|
|
|
|
const isCancellationUserHost =
|
|
bookingToDelete.userId === userId || bookingToDelete.user.email === cancelledBy;
|
|
|
|
const isReasonRequired = isCancellationReasonRequired(
|
|
bookingToDelete.eventType?.requiresCancellationReason,
|
|
isCancellationUserHost
|
|
);
|
|
|
|
if (!platformClientId && !cancellationReason?.trim() && isReasonRequired && !skipCancellationReasonValidation) {
|
|
throw new HttpError({
|
|
statusCode: 400,
|
|
message: "Cancellation reason is required",
|
|
});
|
|
}
|
|
|
|
if (bookingToDelete.endTime && new Date() > new Date(bookingToDelete.endTime)) {
|
|
throw new HttpError({
|
|
statusCode: 400,
|
|
message: "Cannot cancel a booking that has already ended",
|
|
});
|
|
}
|
|
|
|
// If the booking is a seated event and there is no seatReferenceUid we should validate that logged in user is host
|
|
if (bookingToDelete.eventType?.seatsPerTimeSlot && !seatReferenceUid) {
|
|
const userIsHost = bookingToDelete.eventType.hosts.find((host) => {
|
|
if (host.user.id === userId) return true;
|
|
});
|
|
|
|
const userIsOwnerOfEventType = bookingToDelete.eventType.owner?.id === userId;
|
|
|
|
if (!userIsHost && !userIsOwnerOfEventType) {
|
|
throw new HttpError({
|
|
statusCode: 401,
|
|
message: "User not a host of this event",
|
|
});
|
|
}
|
|
}
|
|
|
|
// get webhooks
|
|
const eventTrigger: WebhookTriggerEvents = "BOOKING_CANCELLED";
|
|
|
|
const subscriberOptions: GetSubscriberOptions = {
|
|
userId: bookingToDelete.userId,
|
|
eventTypeId: bookingToDelete.eventTypeId as number,
|
|
triggerEvent: eventTrigger,
|
|
teamId: null,
|
|
orgId: undefined,
|
|
oAuthClientId: platformClientId,
|
|
};
|
|
|
|
const eventTypeInfo: EventTypeInfo = {
|
|
eventTitle: bookingToDelete?.eventType?.title || null,
|
|
eventDescription: bookingToDelete?.eventType?.description || null,
|
|
requiresConfirmation: bookingToDelete?.eventType?.requiresConfirmation || null,
|
|
price: bookingToDelete?.eventType?.price || null,
|
|
currency: bookingToDelete?.eventType?.currency || null,
|
|
length: bookingToDelete?.eventType?.length || null,
|
|
};
|
|
|
|
const webhooks = await getWebhooks(subscriberOptions);
|
|
|
|
const organizer = await userRepository.findByIdOrThrow({
|
|
id: bookingToDelete.userId,
|
|
});
|
|
|
|
const attendeesListPromises = [];
|
|
|
|
for (const attendee of bookingToDelete.attendees) {
|
|
attendeesListPromises.push({
|
|
name: attendee.name,
|
|
email: attendee.email,
|
|
timeZone: attendee.timeZone,
|
|
phoneNumber: attendee.phoneNumber,
|
|
language: {
|
|
translate: await getTranslation(attendee.locale ?? "en", "common"),
|
|
locale: attendee.locale ?? "en",
|
|
},
|
|
});
|
|
}
|
|
|
|
const attendeesList = await Promise.all(attendeesListPromises);
|
|
const tOrganizer = await getTranslation(organizer.locale ?? "en", "common");
|
|
|
|
const bookerUrl = process.env.NEXT_PUBLIC_WEBAPP_URL || "https://app.cal.com";
|
|
|
|
const evt: CalendarEvent = {
|
|
bookerUrl,
|
|
title: bookingToDelete?.title,
|
|
length: bookingToDelete?.eventType?.length,
|
|
type: bookingToDelete?.eventType?.slug as string,
|
|
additionalNotes: bookingToDelete?.description,
|
|
description: bookingToDelete.eventType?.description,
|
|
customInputs: isPrismaObjOrUndefined(bookingToDelete.customInputs),
|
|
eventTypeId: bookingToDelete.eventTypeId as number,
|
|
...getCalEventResponses({
|
|
bookingFields: bookingToDelete.eventType?.bookingFields ?? null,
|
|
booking: bookingToDelete,
|
|
}),
|
|
startTime: bookingToDelete?.startTime ? dayjs(bookingToDelete.startTime).format() : "",
|
|
endTime: bookingToDelete?.endTime ? dayjs(bookingToDelete.endTime).format() : "",
|
|
organizer: {
|
|
id: organizer.id,
|
|
username: organizer.username || undefined,
|
|
email: bookingToDelete?.userPrimaryEmail ?? organizer.email,
|
|
name: organizer.name ?? "Nameless",
|
|
timeZone: organizer.timeZone,
|
|
timeFormat: getTimeFormatStringFromUserTimeFormat(organizer.timeFormat),
|
|
language: { translate: tOrganizer, locale: organizer.locale ?? "en" },
|
|
},
|
|
attendees: attendeesList,
|
|
uid: bookingToDelete?.uid,
|
|
bookingId: bookingToDelete?.id,
|
|
/* Include recurringEvent information only when cancelling all bookings */
|
|
recurringEvent: allRemainingBookings
|
|
? parseRecurringEvent(bookingToDelete.eventType?.recurringEvent)
|
|
: undefined,
|
|
location: bookingToDelete?.location,
|
|
destinationCalendar: bookingToDelete?.destinationCalendar
|
|
? [bookingToDelete?.destinationCalendar]
|
|
: bookingToDelete?.user.destinationCalendar
|
|
? [bookingToDelete?.user.destinationCalendar]
|
|
: [],
|
|
cancellationReason: cancellationReason,
|
|
seatsPerTimeSlot: bookingToDelete.eventType?.seatsPerTimeSlot,
|
|
seatsShowAttendees: bookingToDelete.eventType?.seatsShowAttendees,
|
|
iCalUID: bookingToDelete.iCalUID,
|
|
iCalSequence: bookingToDelete.iCalSequence + 1,
|
|
platformClientId,
|
|
platformRescheduleUrl,
|
|
platformCancelUrl,
|
|
hideOrganizerEmail: bookingToDelete.eventType?.hideOrganizerEmail,
|
|
platformBookingUrl,
|
|
customReplyToEmail: bookingToDelete.eventType?.customReplyToEmail,
|
|
schedulingType: bookingToDelete.eventType?.schedulingType,
|
|
hideBranding: bookingToDelete.eventTypeId
|
|
? await getEventTypeService().shouldHideBrandingForEventType(bookingToDelete.eventTypeId, {
|
|
team: bookingToDelete.eventType?.team
|
|
? {
|
|
hideBranding: bookingToDelete.eventType.team.hideBranding,
|
|
parent: bookingToDelete.eventType.team.parent,
|
|
}
|
|
: null,
|
|
owner: bookingToDelete.user
|
|
? {
|
|
id: bookingToDelete.user.id,
|
|
hideBranding: bookingToDelete.user.hideBranding,
|
|
profiles: bookingToDelete.user.profiles ?? [],
|
|
}
|
|
: null,
|
|
} satisfies EventTypeBrandingData)
|
|
: false,
|
|
};
|
|
|
|
const dataForWebhooks = { evt, webhooks, eventTypeInfo };
|
|
|
|
// If it's just an attendee of a booking then just remove them from that booking
|
|
const result = await cancelAttendeeSeat(
|
|
{
|
|
seatReferenceUid: seatReferenceUid,
|
|
bookingToDelete,
|
|
},
|
|
dataForWebhooks,
|
|
bookingToDelete?.eventType?.metadata as EventTypeMetadata
|
|
);
|
|
if (result)
|
|
return {
|
|
success: true,
|
|
onlyRemovedAttendee: true,
|
|
bookingId: bookingToDelete.id,
|
|
bookingUid: bookingToDelete.uid,
|
|
message: "Attendee successfully removed.",
|
|
isPlatformManagedUserBooking: bookingToDelete.user.isPlatformManaged,
|
|
} satisfies HandleCancelBookingResponse;
|
|
|
|
const promises = webhooks.map((webhook) =>
|
|
sendPayload(webhook.secret, eventTrigger, new Date().toISOString(), webhook, {
|
|
...evt,
|
|
...eventTypeInfo,
|
|
status: "CANCELLED",
|
|
smsReminderNumber: bookingToDelete.smsReminderNumber || undefined,
|
|
cancelledBy: cancelledBy,
|
|
requestReschedule: false,
|
|
}).catch((e) => {
|
|
logger.error(
|
|
`Error executing webhook for event: ${eventTrigger}, URL: ${webhook.subscriberUrl}, bookingId: ${evt.bookingId}, bookingUid: ${evt.uid}`,
|
|
safeStringify(e)
|
|
);
|
|
})
|
|
);
|
|
await Promise.all(promises);
|
|
|
|
let updatedBookings: {
|
|
id: number;
|
|
uid: string;
|
|
references: {
|
|
type: string;
|
|
credentialId: number | null;
|
|
uid: string;
|
|
externalCalendarId: string | null;
|
|
}[];
|
|
startTime: Date;
|
|
endTime: Date;
|
|
}[] = [];
|
|
|
|
// by cancelling first, and blocking whilst doing so; we can ensure a cancel
|
|
// action always succeeds even if subsequent integrations fail cancellation.
|
|
if (
|
|
bookingToDelete.eventType?.recurringEvent &&
|
|
bookingToDelete.recurringEventId &&
|
|
(allRemainingBookings || cancelSubsequentBookings)
|
|
) {
|
|
const recurringEventId = bookingToDelete.recurringEventId;
|
|
const gte = cancelSubsequentBookings ? bookingToDelete.startTime : new Date();
|
|
// Proceed to mark as cancelled all remaining recurring events instances (greater than or equal to right now)
|
|
await bookingRepository.updateMany({
|
|
where: {
|
|
recurringEventId,
|
|
startTime: {
|
|
gte,
|
|
},
|
|
},
|
|
data: {
|
|
status: BookingStatus.CANCELLED,
|
|
cancellationReason: cancellationReason,
|
|
cancelledBy: cancelledBy,
|
|
},
|
|
});
|
|
const allUpdatedBookings = await bookingRepository.findManyIncludeReferences({
|
|
where: {
|
|
recurringEventId: bookingToDelete.recurringEventId,
|
|
startTime: {
|
|
gte: new Date(),
|
|
},
|
|
},
|
|
});
|
|
updatedBookings = updatedBookings.concat(allUpdatedBookings);
|
|
} else {
|
|
if (bookingToDelete?.eventType?.seatsPerTimeSlot) {
|
|
await attendeeRepository.deleteManyByBookingId(bookingToDelete.id);
|
|
}
|
|
|
|
const updatedBooking = await bookingRepository.updateIncludeReferences({
|
|
where: {
|
|
uid: bookingToDelete.uid,
|
|
},
|
|
data: {
|
|
status: BookingStatus.CANCELLED,
|
|
cancellationReason: cancellationReason,
|
|
cancelledBy: cancelledBy,
|
|
// Assume that canceling the booking is the last action
|
|
iCalSequence: evt.iCalSequence || 100,
|
|
},
|
|
});
|
|
|
|
updatedBookings.push(updatedBooking);
|
|
|
|
if (bookingToDelete.payment.some((payment) => payment.paymentOption === "ON_BOOKING")) {
|
|
try {
|
|
await processPaymentRefund({
|
|
booking: bookingToDelete,
|
|
});
|
|
} catch (error) {
|
|
log.error(`Error processing payment refund for booking ${bookingToDelete.uid}:`, error);
|
|
}
|
|
} else if (bookingToDelete.payment.some((payment) => payment.paymentOption === "HOLD")) {
|
|
try {
|
|
await processNoShowFeeOnCancellation({
|
|
booking: bookingToDelete,
|
|
payments: bookingToDelete.payment,
|
|
cancelledByUserId: userId,
|
|
});
|
|
} catch (error) {
|
|
log.error(`Error processing no-show fee for booking ${bookingToDelete.uid}:`, error);
|
|
}
|
|
}
|
|
}
|
|
|
|
/** TODO: Remove this without breaking functionality */
|
|
if (bookingToDelete.location === DailyLocationType) {
|
|
bookingToDelete.user.credentials.push({
|
|
...FAKE_DAILY_CREDENTIAL,
|
|
teamId: null,
|
|
});
|
|
}
|
|
|
|
const isBookingInRecurringSeries = !!(
|
|
bookingToDelete.eventType?.recurringEvent &&
|
|
bookingToDelete.recurringEventId &&
|
|
allRemainingBookings
|
|
);
|
|
|
|
// Skip calendar event deletion when cancellation comes from a calendar subscription webhook
|
|
// to avoid infinite loops (Google/Office365 → Cal.diy → Google/Office365 → ...)
|
|
if (!skipCalendarSyncTaskCancellation) {
|
|
try {
|
|
const bookingToDeleteEventTypeMetadataParsed = eventTypeMetaDataSchemaWithTypedApps.safeParse(
|
|
bookingToDelete.eventType?.metadata || null
|
|
);
|
|
|
|
if (!bookingToDeleteEventTypeMetadataParsed.success) {
|
|
log.error(
|
|
`Error parsing metadata`,
|
|
safeStringify({ error: bookingToDeleteEventTypeMetadataParsed?.error })
|
|
);
|
|
throw new Error("Error parsing metadata");
|
|
}
|
|
|
|
const bookingToDeleteEventTypeMetadata = bookingToDeleteEventTypeMetadataParsed.data;
|
|
|
|
const credentials = await getAllCredentialsIncludeServiceAccountKey(bookingToDelete.user, {
|
|
...bookingToDelete.eventType,
|
|
metadata: bookingToDeleteEventTypeMetadata,
|
|
});
|
|
|
|
const eventManager = new EventManager(
|
|
{ ...bookingToDelete.user, credentials },
|
|
bookingToDeleteEventTypeMetadata?.apps
|
|
);
|
|
|
|
await eventManager.cancelEvent(evt, bookingToDelete.references, isBookingInRecurringSeries);
|
|
} catch (error) {
|
|
log.error(`Error deleting integrations`, safeStringify({ error }));
|
|
}
|
|
}
|
|
|
|
// Always mark booking references as deleted for data consistency
|
|
// (even when skipCalendarSyncTaskCancellation is true, since the external event is already deleted)
|
|
try {
|
|
await bookingReferenceRepository.updateManyByBookingId(bookingToDelete.id, { deleted: true });
|
|
} catch (error) {
|
|
log.error(`Error marking booking references as deleted`, safeStringify({ error }));
|
|
}
|
|
|
|
try {
|
|
const webhookTriggerPromises = [];
|
|
for (const booking of updatedBookings) {
|
|
webhookTriggerPromises.push(deleteWebhookScheduledTriggers({ booking }));
|
|
webhookTriggerPromises.push(cancelNoShowTasksForBooking({ bookingUid: booking.uid }));
|
|
}
|
|
|
|
await Promise.allSettled(webhookTriggerPromises).then((results) => {
|
|
const rejectedReasons = results
|
|
.filter((result): result is PromiseRejectedResult => result.status === "rejected")
|
|
.map((result) => result.reason);
|
|
|
|
if (rejectedReasons.length > 0) {
|
|
log.error("An error occurred when deleting webhook triggers", rejectedReasons);
|
|
}
|
|
});
|
|
} catch (error) {
|
|
log.error("Error deleting scheduled webhooks", safeStringify({ error }));
|
|
}
|
|
|
|
try {
|
|
// TODO: if emails fail try to requeue them
|
|
if (!platformClientId || (platformClientId && arePlatformEmailsEnabled))
|
|
await sendCancelledEmailsAndSMS(
|
|
evt,
|
|
{ eventName: bookingToDelete?.eventType?.eventName },
|
|
bookingToDelete?.eventType?.metadata as EventTypeMetadata
|
|
);
|
|
} catch (error) {
|
|
log.error("Error deleting event", error);
|
|
}
|
|
return {
|
|
success: true,
|
|
message: "Booking successfully cancelled.",
|
|
onlyRemovedAttendee: false,
|
|
bookingId: bookingToDelete.id,
|
|
bookingUid: bookingToDelete.uid,
|
|
isPlatformManagedUserBooking: bookingToDelete.user.isPlatformManaged,
|
|
} satisfies HandleCancelBookingResponse;
|
|
}
|
|
|
|
type BookingCancelServiceDependencies = {
|
|
userRepository: UserRepository;
|
|
bookingRepository: BookingRepository;
|
|
profileRepository?: unknown;
|
|
bookingReferenceRepository: BookingReferenceRepository;
|
|
attendeeRepository: PrismaBookingAttendeeRepository;
|
|
};
|
|
|
|
/**
|
|
* Takes care of cancelling bookings. This includes regular bookings, recurring bookings, seated bookings, etc.
|
|
* Handles both individual booking cancellations and bulk cancellations for recurring events.
|
|
*/
|
|
export class BookingCancelService implements IBookingCancelService {
|
|
constructor(private readonly deps: BookingCancelServiceDependencies) {}
|
|
|
|
async cancelBooking(input: { bookingData: CancelRegularBookingData; bookingMeta?: CancelBookingMeta }) {
|
|
const cancelBookingInput: CancelBookingInput = {
|
|
bookingData: input.bookingData,
|
|
...(input.bookingMeta || {}),
|
|
};
|
|
|
|
return handler(cancelBookingInput, this.deps);
|
|
}
|
|
}
|
|
|
|
export default handler;
|