From 5e5e77236ad5c8c40d0954aa26c116ce56853028 Mon Sep 17 00:00:00 2001 From: Hariom Balhara Date: Tue, 21 Oct 2025 12:29:17 +0530 Subject: [PATCH] chore: Add skeleton for BookingCancelService (#24038) --- .../src/lib/modules/booking-cancel.module.ts | 10 ++++ .../lib/services/booking-cancel.service.ts | 15 ++++++ apps/web/app/api/cancel/route.ts | 9 ++++ .../di/BookingCancelService.container.ts | 14 ++++++ .../di/BookingCancelService.module.ts | 24 +++++++++ packages/features/bookings/di/tokens.ts | 2 + .../bookings/lib/dto/BookingCancel.d.ts | 35 +++++++++++++ .../bookings/lib/handleCancelBooking.ts | 50 ++++++++++++++----- .../lib/interfaces/IBookingCancelService.ts | 12 +++++ packages/features/di/tokens.ts | 1 - 10 files changed, 158 insertions(+), 14 deletions(-) create mode 100644 apps/api/v2/src/lib/modules/booking-cancel.module.ts create mode 100644 apps/api/v2/src/lib/services/booking-cancel.service.ts create mode 100644 packages/features/bookings/di/BookingCancelService.container.ts create mode 100644 packages/features/bookings/di/BookingCancelService.module.ts create mode 100644 packages/features/bookings/lib/dto/BookingCancel.d.ts create mode 100644 packages/features/bookings/lib/interfaces/IBookingCancelService.ts diff --git a/apps/api/v2/src/lib/modules/booking-cancel.module.ts b/apps/api/v2/src/lib/modules/booking-cancel.module.ts new file mode 100644 index 0000000000..4151789cae --- /dev/null +++ b/apps/api/v2/src/lib/modules/booking-cancel.module.ts @@ -0,0 +1,10 @@ +import { BookingCancelService } from "@/lib/services/booking-cancel.service"; +import { PrismaModule } from "@/modules/prisma/prisma.module"; +import { Module } from "@nestjs/common"; + +@Module({ + imports: [PrismaModule], + providers: [BookingCancelService], + exports: [BookingCancelService], +}) +export class BookingCancelModule {} diff --git a/apps/api/v2/src/lib/services/booking-cancel.service.ts b/apps/api/v2/src/lib/services/booking-cancel.service.ts new file mode 100644 index 0000000000..1457ad2468 --- /dev/null +++ b/apps/api/v2/src/lib/services/booking-cancel.service.ts @@ -0,0 +1,15 @@ +import { PrismaWriteService } from "@/modules/prisma/prisma-write.service"; +import { Injectable } from "@nestjs/common"; + +import { BookingCancelService as BaseBookingCancelService } from "@calcom/features/bookings/lib/handleCancelBooking"; + +@Injectable() +export class BookingCancelService extends BaseBookingCancelService { + constructor( + prismaWriteService: PrismaWriteService, + ) { + super({ + prismaClient: prismaWriteService.prisma, + }); + } +} diff --git a/apps/web/app/api/cancel/route.ts b/apps/web/app/api/cancel/route.ts index 46da34f7e7..c2eaabc8e5 100644 --- a/apps/web/app/api/cancel/route.ts +++ b/apps/web/app/api/cancel/route.ts @@ -26,11 +26,20 @@ async function handler(req: NextRequest) { cookieStore.delete("calcom.csrf_token"); const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) }); + const result = await handleCancelBooking({ bookingData, userId: session?.user?.id || -1, }); + // const bookingCancelService = getBookingCancelService(); + // const result = await bookingCancelService.cancelBooking({ + // bookingData: bookingData, + // bookingMeta: { + // userId: session?.user?.id || -1, + // }, + // }); + const statusCode = result.success ? 200 : 400; return NextResponse.json(result, { status: statusCode }); diff --git a/packages/features/bookings/di/BookingCancelService.container.ts b/packages/features/bookings/di/BookingCancelService.container.ts new file mode 100644 index 0000000000..6b7cabaa43 --- /dev/null +++ b/packages/features/bookings/di/BookingCancelService.container.ts @@ -0,0 +1,14 @@ +import { createContainer } from "@calcom/lib/di/di"; + +import { + type BookingCancelService, + moduleLoader as bookingCancelServiceModule, +} from "../modules/BookingCancelService.module"; + +const bookingCancelServiceContainer = createContainer(); + +export function getBookingCancelService(): BookingCancelService { + bookingCancelServiceModule.loadModule(bookingCancelServiceContainer); + + return bookingCancelServiceContainer.get(bookingCancelServiceModule.token); +} diff --git a/packages/features/bookings/di/BookingCancelService.module.ts b/packages/features/bookings/di/BookingCancelService.module.ts new file mode 100644 index 0000000000..a4e6ea5f25 --- /dev/null +++ b/packages/features/bookings/di/BookingCancelService.module.ts @@ -0,0 +1,24 @@ +import { BookingCancelService } from "@calcom/features/bookings/lib/handleCancelBooking"; +import { bindModuleToClassOnToken, createModule } from "@calcom/lib/di/di"; +import { DI_TOKENS } from "@calcom/lib/di/tokens"; +import { moduleLoader as prismaModuleLoader } from "@calcom/prisma/prisma.module"; + +const thisModule = createModule(); +const token = DI_TOKENS.BOOKING_CANCEL_SERVICE; +const moduleToken = DI_TOKENS.BOOKING_CANCEL_SERVICE_MODULE; +const loadModule = bindModuleToClassOnToken({ + module: thisModule, + moduleToken, + token, + classs: BookingCancelService, + depsMap: { + prismaClient: prismaModuleLoader, + }, +}); + +export const moduleLoader = { + token, + loadModule, +}; + +export type { BookingCancelService }; diff --git a/packages/features/bookings/di/tokens.ts b/packages/features/bookings/di/tokens.ts index 84f8827cc3..560c2663a2 100644 --- a/packages/features/bookings/di/tokens.ts +++ b/packages/features/bookings/di/tokens.ts @@ -5,4 +5,6 @@ export const BOOKING_DI_TOKENS = { RECURRING_BOOKING_SERVICE_MODULE: Symbol("RecurringBookingServiceModule"), INSTANT_BOOKING_CREATE_SERVICE: Symbol("InstantBookingCreateService"), INSTANT_BOOKING_CREATE_SERVICE_MODULE: Symbol("InstantBookingCreateServiceModule"), + BOOKING_CANCEL_SERVICE: Symbol("BookingCancelService"), + BOOKING_CANCEL_SERVICE_MODULE: Symbol("BookingCancelServiceModule"), }; diff --git a/packages/features/bookings/lib/dto/BookingCancel.d.ts b/packages/features/bookings/lib/dto/BookingCancel.d.ts new file mode 100644 index 0000000000..e341d24c26 --- /dev/null +++ b/packages/features/bookings/lib/dto/BookingCancel.d.ts @@ -0,0 +1,35 @@ +/** + * Domain types for Booking Cancellation Service + * These types are framework-agnostic and contain only the data required for booking cancellation operations + */ +import type { z } from "zod"; + +import type { bookingCancelInput } from "@calcom/prisma/zod-utils"; + +/** + * The booking data required for cancellation + */ +export type CancelRegularBookingData = z.infer; + +/** + * Additional metadata for booking cancellation, typically used for platform integrations + */ +export type CancelBookingMeta = { + userId?: number; + platformClientId?: string; + platformRescheduleUrl?: string; + platformCancelUrl?: string; + platformBookingUrl?: string; + arePlatformEmailsEnabled?: boolean; +}; + +/** + * The response returned after attempting to cancel a booking + */ +export type HandleCancelBookingResponse = { + success: boolean; + message: string; + onlyRemovedAttendee: boolean; + bookingId: number; + bookingUid: string; +}; diff --git a/packages/features/bookings/lib/handleCancelBooking.ts b/packages/features/bookings/lib/handleCancelBooking.ts index 090cb8dea0..71cfd3a9f7 100644 --- a/packages/features/bookings/lib/handleCancelBooking.ts +++ b/packages/features/bookings/lib/handleCancelBooking.ts @@ -5,10 +5,13 @@ import { FAKE_DAILY_CREDENTIAL } from "@calcom/app-store/dailyvideo/lib/VideoApi import { eventTypeMetaDataSchemaWithTypedApps } from "@calcom/app-store/zod-utils"; import dayjs from "@calcom/dayjs"; import { sendCancelledEmailsAndSMS } from "@calcom/emails"; +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 { getBookerBaseUrl } from "@calcom/features/ee/organizations/lib/getBookerUrlServer"; import { sendCancelledReminders } from "@calcom/features/ee/workflows/lib/reminders/reminderScheduler"; +import { WorkflowRepository } from "@calcom/features/ee/workflows/repositories/WorkflowRepository"; import type { GetSubscriberOptions } from "@calcom/features/webhooks/lib/getWebhooks"; import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks"; import { @@ -17,8 +20,6 @@ import { } from "@calcom/features/webhooks/lib/scheduleTrigger"; import sendPayload from "@calcom/features/webhooks/lib/sendOrSchedulePayload"; import type { EventTypeInfo } from "@calcom/features/webhooks/lib/sendPayload"; -import EventManager from "@calcom/features/bookings/lib/EventManager"; -import { getBookerBaseUrl } from "@calcom/features/ee/organizations/lib/getBookerUrlServer"; import getOrgIdFromMemberOrTeamId from "@calcom/lib/getOrgIdFromMemberOrTeamId"; import { getTeamIdFromEventType } from "@calcom/lib/getTeamIdFromEventType"; import { HttpError } from "@calcom/lib/http-error"; @@ -27,10 +28,10 @@ import { parseRecurringEvent } from "@calcom/lib/isRecurringEvent"; import logger from "@calcom/lib/logger"; import { safeStringify } from "@calcom/lib/safeStringify"; import { getTranslation } from "@calcom/lib/server/i18n"; -import { WorkflowRepository } from "@calcom/features/ee/workflows/repositories/WorkflowRepository"; 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 { Prisma, WorkflowReminder } from "@calcom/prisma/client"; +import type { Prisma, PrismaClient, WorkflowReminder } from "@calcom/prisma/client"; import type { WebhookTriggerEvents } from "@calcom/prisma/enums"; import { BookingStatus } from "@calcom/prisma/enums"; import { bookingMetadataSchema, bookingCancelInput } from "@calcom/prisma/zod-utils"; @@ -38,10 +39,16 @@ import type { EventTypeMetadata } from "@calcom/prisma/zod-utils"; import { getAllWorkflowsFromEventType } from "@calcom/trpc/server/routers/viewer/workflows/util"; import type { CalendarEvent } from "@calcom/types/Calendar"; +import type { + CancelRegularBookingData, + CancelBookingMeta, + HandleCancelBookingResponse, +} from "./dto/BookingCancel"; import { getAllCredentialsIncludeServiceAccountKey } from "./getAllCredentialsForUsersOnEvent/getAllCredentials"; import { getBookingToDelete } from "./getBookingToDelete"; import { handleInternalNote } from "./handleInternalNote"; import cancelAttendeeSeat from "./handleSeats/cancel/cancelAttendeeSeat"; +import type { IBookingCancelService } from "./interfaces/IBookingCancelService"; const log = logger.getSubLogger({ prefix: ["handleCancelBooking"] }); @@ -60,14 +67,6 @@ export type CancelBookingInput = { bookingData: z.infer; } & PlatformParams; -export type HandleCancelBookingResponse = { - success: boolean; - message: string; - onlyRemovedAttendee: boolean; - bookingId: number; - bookingUid: string; -}; - async function handler(input: CancelBookingInput) { const body = input.bookingData; const { @@ -118,7 +117,12 @@ async function handler(input: CancelBookingInput) { const isCancellationUserHost = bookingToDelete.userId == userId || bookingToDelete.user.email === cancelledBy; - if (!platformClientId && !cancellationReason?.trim() && isCancellationUserHost && !skipCancellationReasonValidation) { + if ( + !platformClientId && + !cancellationReason?.trim() && + isCancellationUserHost && + !skipCancellationReasonValidation + ) { throw new HttpError({ statusCode: 400, message: "Cancellation reason is required when you are the host", @@ -588,4 +592,24 @@ async function handler(input: CancelBookingInput) { } satisfies HandleCancelBookingResponse; } +type BookingCancelServiceDependencies = { + prismaClient: PrismaClient; +}; +/** + * 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 || {}), + }; + // TODO: Deps to be passed to it later when we stop exporting handler + return handler(cancelBookingInput); + } +} + export default handler; diff --git a/packages/features/bookings/lib/interfaces/IBookingCancelService.ts b/packages/features/bookings/lib/interfaces/IBookingCancelService.ts new file mode 100644 index 0000000000..adc99efd9d --- /dev/null +++ b/packages/features/bookings/lib/interfaces/IBookingCancelService.ts @@ -0,0 +1,12 @@ +import type { + CancelRegularBookingData, + CancelBookingMeta, + HandleCancelBookingResponse, +} from "../dto/BookingCancel"; + +export interface IBookingCancelService { + cancelBooking(input: { + bookingData: CancelRegularBookingData; + bookingMeta?: CancelBookingMeta; + }): Promise; +} diff --git a/packages/features/di/tokens.ts b/packages/features/di/tokens.ts index 673e520890..c0809b94bf 100644 --- a/packages/features/di/tokens.ts +++ b/packages/features/di/tokens.ts @@ -55,7 +55,6 @@ export const DI_TOKENS = { HOST_REPOSITORY_MODULE: Symbol("HostRepositoryModule"), ATTRIBUTE_REPOSITORY: Symbol("AttributeRepository"), ATTRIBUTE_REPOSITORY_MODULE: Symbol("AttributeRepositoryModule"), - // Booking service tokens ...BOOKING_DI_TOKENS, // Watchlist service tokens ...WATCHLIST_DI_TOKENS,