From 9ceb5ea3c50876a608b0f0edfe198d9a33a79b5e Mon Sep 17 00:00:00 2001 From: Hariom Balhara Date: Tue, 2 Sep 2025 16:36:31 +0530 Subject: [PATCH] Skeleton for Booking flow refactor using Factory and services (#23277) --- apps/web/pages/api/book/event.ts | 19 ++++++- apps/web/pages/api/book/instant-event.ts | 2 +- apps/web/pages/api/book/recurring-event.ts | 2 +- .../InstantBookingCreateServiceContainer.ts | 13 +++++ .../RecurringBookingServiceContainer.ts | 28 ++++++++++ .../RegularBookingServiceContainer.ts | 20 ++++++++ .../InstantBookingCreateServiceModule.ts | 11 ++++ .../modules/RecurringBookingServiceModule.ts | 13 +++++ .../di/modules/RegularBookingServiceModule.ts | 39 ++++++++++++++ packages/features/bookings/lib/dto/types.d.ts | 51 +++++++++++++++++++ .../features/bookings/lib/handleNewBooking.ts | 47 +++++++++++++++-- .../bookings/lib/handleNewRecurringBooking.ts | 51 +++++++++++++------ .../lib/interfaces/IBookingCreateService.ts | 7 +++ .../lib/interfaces/IBookingService.ts | 9 ++++ .../instant-meeting/handleInstantMeeting.ts | 44 +++++++++------- packages/lib/di/tokens.ts | 6 +++ packages/prisma/zod/custom/booking.ts | 4 +- 17 files changed, 325 insertions(+), 41 deletions(-) create mode 100644 packages/features/bookings/lib/di/containers/InstantBookingCreateServiceContainer.ts create mode 100644 packages/features/bookings/lib/di/containers/RecurringBookingServiceContainer.ts create mode 100644 packages/features/bookings/lib/di/containers/RegularBookingServiceContainer.ts create mode 100644 packages/features/bookings/lib/di/modules/InstantBookingCreateServiceModule.ts create mode 100644 packages/features/bookings/lib/di/modules/RecurringBookingServiceModule.ts create mode 100644 packages/features/bookings/lib/di/modules/RegularBookingServiceModule.ts create mode 100644 packages/features/bookings/lib/dto/types.d.ts create mode 100644 packages/features/bookings/lib/interfaces/IBookingCreateService.ts create mode 100644 packages/features/bookings/lib/interfaces/IBookingService.ts diff --git a/apps/web/pages/api/book/event.ts b/apps/web/pages/api/book/event.ts index b60363fd7d..1aa0fb10c0 100644 --- a/apps/web/pages/api/book/event.ts +++ b/apps/web/pages/api/book/event.ts @@ -4,10 +4,10 @@ import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; import handleNewBooking from "@calcom/features/bookings/lib/handleNewBooking"; import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError"; import getIP from "@calcom/lib/getIP"; +import { piiHasher } from "@calcom/lib/server/PiiHasher"; import { checkCfTurnstileToken } from "@calcom/lib/server/checkCfTurnstileToken"; import { defaultResponder } from "@calcom/lib/server/defaultResponder"; import { CreationSource } from "@calcom/prisma/enums"; -import { piiHasher } from "@calcom/lib/server/PiiHasher"; async function handler(req: NextApiRequest & { userId?: number }) { const userIp = getIP(req); @@ -37,7 +37,24 @@ async function handler(req: NextApiRequest & { userId?: number }) { hostname: req.headers.host || "", forcedSlug: req.headers["x-cal-force-slug"] as string | undefined, }); + // const booking = await createBookingThroughFactory(); return booking; + + // To be added in the follow-up PR + // async function createBookingThroughFactory() { + // console.log("Creating booking through factory"); + // const regularBookingService = getRegularBookingService(); + + // const booking = await regularBookingService.createBooking({ + // bookingData: req.body, + // bookingMeta: { + // userId: session?.user?.id || -1, + // hostname: req.headers.host || "", + // forcedSlug: req.headers["x-cal-force-slug"] as string | undefined, + // }, + // }); + // return booking; + // } } export default defaultResponder(handler, "/api/book/event"); diff --git a/apps/web/pages/api/book/instant-event.ts b/apps/web/pages/api/book/instant-event.ts index 91bb97747a..82079bbd46 100644 --- a/apps/web/pages/api/book/instant-event.ts +++ b/apps/web/pages/api/book/instant-event.ts @@ -4,9 +4,9 @@ import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; import handleInstantMeeting from "@calcom/features/instant-meeting/handleInstantMeeting"; import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError"; import getIP from "@calcom/lib/getIP"; +import { piiHasher } from "@calcom/lib/server/PiiHasher"; import { defaultResponder } from "@calcom/lib/server/defaultResponder"; import { CreationSource } from "@calcom/prisma/enums"; -import { piiHasher } from "@calcom/lib/server/PiiHasher"; async function handler(req: NextApiRequest & { userId?: number }) { const userIp = getIP(req); diff --git a/apps/web/pages/api/book/recurring-event.ts b/apps/web/pages/api/book/recurring-event.ts index 69150c016b..d7dae48827 100644 --- a/apps/web/pages/api/book/recurring-event.ts +++ b/apps/web/pages/api/book/recurring-event.ts @@ -5,9 +5,9 @@ import { handleNewRecurringBooking } from "@calcom/features/bookings/lib/handleN import type { BookingResponse } from "@calcom/features/bookings/types"; import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError"; import getIP from "@calcom/lib/getIP"; +import { piiHasher } from "@calcom/lib/server/PiiHasher"; import { checkCfTurnstileToken } from "@calcom/lib/server/checkCfTurnstileToken"; import { defaultResponder } from "@calcom/lib/server/defaultResponder"; -import { piiHasher } from "@calcom/lib/server/PiiHasher"; // @TODO: Didn't look at the contents of this function in order to not break old booking page. diff --git a/packages/features/bookings/lib/di/containers/InstantBookingCreateServiceContainer.ts b/packages/features/bookings/lib/di/containers/InstantBookingCreateServiceContainer.ts new file mode 100644 index 0000000000..79715e4d20 --- /dev/null +++ b/packages/features/bookings/lib/di/containers/InstantBookingCreateServiceContainer.ts @@ -0,0 +1,13 @@ +import { createContainer } from "@evyweb/ioctopus"; + +import { DI_TOKENS } from "@calcom/lib/di/tokens"; + +import type { InstantBookingCreateService } from "../modules/InstantBookingCreateServiceModule"; +import { instantBookingCreateServiceModule } from "../modules/InstantBookingCreateServiceModule"; + +const container = createContainer(); +container.load(DI_TOKENS.INSTANT_BOOKING_CREATE_SERVICE_MODULE, instantBookingCreateServiceModule); + +export function getInstantBookingCreateService(): InstantBookingCreateService { + return container.get(DI_TOKENS.INSTANT_BOOKING_CREATE_SERVICE); +} diff --git a/packages/features/bookings/lib/di/containers/RecurringBookingServiceContainer.ts b/packages/features/bookings/lib/di/containers/RecurringBookingServiceContainer.ts new file mode 100644 index 0000000000..90bb64829d --- /dev/null +++ b/packages/features/bookings/lib/di/containers/RecurringBookingServiceContainer.ts @@ -0,0 +1,28 @@ +import { createContainer } from "@evyweb/ioctopus"; + +import { bookingRepositoryModule } from "@calcom/lib/di/modules/Booking"; +import { cacheModule } from "@calcom/lib/di/modules/Cache"; +import { checkBookingAndDurationLimitsModule } from "@calcom/lib/di/modules/CheckBookingAndDurationLimits"; +import { checkBookingLimitsModule } from "@calcom/lib/di/modules/CheckBookingLimits"; +import { featuresRepositoryModule } from "@calcom/lib/di/modules/Features"; +import { DI_TOKENS } from "@calcom/lib/di/tokens"; +import { prismaModule } from "@calcom/prisma/prisma.module"; + +import type { RecurringBookingService } from "../modules/RecurringBookingServiceModule"; +import { recurringBookingServiceModule } from "../modules/RecurringBookingServiceModule"; + +const container = createContainer(); +container.load(DI_TOKENS.PRISMA_MODULE, prismaModule); +container.load(DI_TOKENS.BOOKING_REPOSITORY_MODULE, bookingRepositoryModule); +container.load(DI_TOKENS.CACHE_SERVICE_MODULE, cacheModule); +container.load(DI_TOKENS.CHECK_BOOKING_LIMITS_SERVICE_MODULE, checkBookingLimitsModule); +container.load(DI_TOKENS.FEATURES_REPOSITORY_MODULE, featuresRepositoryModule); +container.load( + DI_TOKENS.CHECK_BOOKING_AND_DURATION_LIMITS_SERVICE_MODULE, + checkBookingAndDurationLimitsModule +); +container.load(DI_TOKENS.RECURRING_BOOKING_SERVICE_MODULE, recurringBookingServiceModule); + +export function getRecurringBookingService(): RecurringBookingService { + return container.get(DI_TOKENS.RECURRING_BOOKING_SERVICE); +} diff --git a/packages/features/bookings/lib/di/containers/RegularBookingServiceContainer.ts b/packages/features/bookings/lib/di/containers/RegularBookingServiceContainer.ts new file mode 100644 index 0000000000..3bea0eec2c --- /dev/null +++ b/packages/features/bookings/lib/di/containers/RegularBookingServiceContainer.ts @@ -0,0 +1,20 @@ +import { createContainer } from "@evyweb/ioctopus"; + +import { DI_TOKENS } from "@calcom/lib/di/tokens"; + +import type { RegularBookingService } from "../modules/RegularBookingServiceModule"; +import { + regularBookingServiceModule, + loadModuleDeps, + moduleToken, +} from "../modules/RegularBookingServiceModule"; + +const regularBookingServiceContainer = createContainer(); + +regularBookingServiceContainer.load(DI_TOKENS.REGULAR_BOOKING_SERVICE_MODULE, regularBookingServiceModule); + +export function getRegularBookingService(): RegularBookingService { + loadModuleDeps(regularBookingServiceContainer); + + return regularBookingServiceContainer.get(moduleToken); +} diff --git a/packages/features/bookings/lib/di/modules/InstantBookingCreateServiceModule.ts b/packages/features/bookings/lib/di/modules/InstantBookingCreateServiceModule.ts new file mode 100644 index 0000000000..57b2cc72f6 --- /dev/null +++ b/packages/features/bookings/lib/di/modules/InstantBookingCreateServiceModule.ts @@ -0,0 +1,11 @@ +import { createModule } from "@evyweb/ioctopus"; + +import { InstantBookingCreateService } from "@calcom/features/instant-meeting/handleInstantMeeting"; +import { DI_TOKENS } from "@calcom/lib/di/tokens"; + +export const instantBookingCreateServiceModule = createModule(); + +instantBookingCreateServiceModule + .bind(DI_TOKENS.INSTANT_BOOKING_CREATE_SERVICE) + .toClass(InstantBookingCreateService); +export type { InstantBookingCreateService }; diff --git a/packages/features/bookings/lib/di/modules/RecurringBookingServiceModule.ts b/packages/features/bookings/lib/di/modules/RecurringBookingServiceModule.ts new file mode 100644 index 0000000000..c8350cdb94 --- /dev/null +++ b/packages/features/bookings/lib/di/modules/RecurringBookingServiceModule.ts @@ -0,0 +1,13 @@ +import { createModule } from "@evyweb/ioctopus"; + +import { DI_TOKENS } from "@calcom/lib/di/tokens"; + +import { RecurringBookingService } from "../../handleNewRecurringBooking"; + +export const recurringBookingServiceModule = createModule(); + +recurringBookingServiceModule.bind(DI_TOKENS.RECURRING_BOOKING_SERVICE).toClass(RecurringBookingService, { + regularBookingService: DI_TOKENS.REGULAR_BOOKING_SERVICE, +}); + +export type { RecurringBookingService }; diff --git a/packages/features/bookings/lib/di/modules/RegularBookingServiceModule.ts b/packages/features/bookings/lib/di/modules/RegularBookingServiceModule.ts new file mode 100644 index 0000000000..7b9ec65ef4 --- /dev/null +++ b/packages/features/bookings/lib/di/modules/RegularBookingServiceModule.ts @@ -0,0 +1,39 @@ +import type { Container } from "@evyweb/ioctopus"; +import { createModule } from "@evyweb/ioctopus"; + +import { bookingRepositoryModule } from "@calcom/lib/di/modules/Booking"; +import { cacheModule } from "@calcom/lib/di/modules/Cache"; +import { checkBookingAndDurationLimitsModule } from "@calcom/lib/di/modules/CheckBookingAndDurationLimits"; +import { checkBookingLimitsModule } from "@calcom/lib/di/modules/CheckBookingLimits"; +import { featuresRepositoryModule } from "@calcom/lib/di/modules/Features"; +import { DI_TOKENS } from "@calcom/lib/di/tokens"; +import { prismaModule } from "@calcom/prisma/prisma.module"; + +import { RegularBookingService } from "../../handleNewBooking"; + +export const regularBookingServiceModule = createModule(); +const moduleToken = DI_TOKENS.REGULAR_BOOKING_SERVICE; +regularBookingServiceModule.bind(moduleToken).toClass(RegularBookingService, { + cacheService: DI_TOKENS.CACHE_SERVICE, + checkBookingAndDurationLimitsService: DI_TOKENS.CHECK_BOOKING_AND_DURATION_LIMITS_SERVICE, + prismaClient: DI_TOKENS.PRISMA_CLIENT, + bookingRepository: DI_TOKENS.BOOKING_REPOSITORY, + featuresRepository: DI_TOKENS.FEATURES_REPOSITORY, + checkBookingLimitsService: DI_TOKENS.CHECK_BOOKING_LIMITS_SERVICE, +}); + +// Load the dependencies defined for the module above +function loadModuleDeps(container: Container) { + container.load(DI_TOKENS.CACHE_SERVICE_MODULE, cacheModule); + container.load( + DI_TOKENS.CHECK_BOOKING_AND_DURATION_LIMITS_SERVICE_MODULE, + checkBookingAndDurationLimitsModule + ); + container.load(DI_TOKENS.PRISMA_MODULE, prismaModule); + container.load(DI_TOKENS.BOOKING_REPOSITORY_MODULE, bookingRepositoryModule); + container.load(DI_TOKENS.FEATURES_REPOSITORY_MODULE, featuresRepositoryModule); + container.load(DI_TOKENS.CHECK_BOOKING_LIMITS_SERVICE_MODULE, checkBookingLimitsModule); +} + +export { loadModuleDeps, moduleToken }; +export type { RegularBookingService }; diff --git a/packages/features/bookings/lib/dto/types.d.ts b/packages/features/bookings/lib/dto/types.d.ts new file mode 100644 index 0000000000..e79f6bed0c --- /dev/null +++ b/packages/features/bookings/lib/dto/types.d.ts @@ -0,0 +1,51 @@ +/** + * Domain types for BookingCreateService + * These types are framework-agnostic and contain only the data required for booking operations + */ +import type { z } from "zod"; + +import type getBookingDataSchema from "@calcom/features/bookings/lib/getBookingDataSchema"; +import type getBookingDataSchemaForApi from "@calcom/features/bookings/lib/getBookingDataSchemaForApi"; +import type { BookingCreateBody as BaseCreateBookingData } from "@calcom/prisma/zod/custom/booking"; +import type { extendedBookingCreateBody } from "@calcom/prisma/zod/custom/booking"; + +export type ExtendedBookingCreateData = z.input; +export type BookingDataSchemaGetter = typeof getBookingDataSchema | typeof getBookingDataSchemaForApi; + +export type CreateRegularBookingData = BaseCreateBookingData; + +export type CreateInstantBookingData = BaseCreateBookingData; + +export type CreateRecurringBookingData = (BaseCreateBookingData & { + schedulingType?: SchedulingType; +})[]; + +export type PlatformParams = { + platformClientId?: string; + platformCancelUrl?: string; + platformBookingUrl?: string; + platformRescheduleUrl?: string; + platformBookingLocation?: string; + areCalendarEventsEnabled?: boolean; +}; + +export type CreateBookingMeta = { + userId?: number; + // These used to come from headers but now we're passing them as params + hostname?: string; + forcedSlug?: string; + noEmail?: boolean; +} & PlatformParams; + +export type BookingHandlerInput = { + bookingData: CreateRegularBookingData; +} & CreateBookingMeta; + +export type CreateInstantBookingResponse = { + message: string; + meetingTokenId: number; + bookingId: number; + bookingUid: string; + expires: Date; + userId: number | null; +}; diff --git a/packages/features/bookings/lib/handleNewBooking.ts b/packages/features/bookings/lib/handleNewBooking.ts index eed351c7b9..890cd7fa71 100644 --- a/packages/features/bookings/lib/handleNewBooking.ts +++ b/packages/features/bookings/lib/handleNewBooking.ts @@ -25,14 +25,19 @@ import { } from "@calcom/emails"; import getICalUID from "@calcom/emails/lib/getICalUID"; import { CalendarEventBuilder } from "@calcom/features/CalendarEventBuilder"; +import type { BookingDataSchemaGetter } from "@calcom/features/bookings/lib/dto/types"; +import type { CreateRegularBookingData, CreateBookingMeta } from "@calcom/features/bookings/lib/dto/types"; +import type { CheckBookingAndDurationLimitsService } from "@calcom/features/bookings/lib/handleNewBooking/checkBookingAndDurationLimits"; import { handleWebhookTrigger } from "@calcom/features/bookings/lib/handleWebhookTrigger"; import { isEventTypeLoggingEnabled } from "@calcom/features/bookings/lib/isEventTypeLoggingEnabled"; +import type { CacheService } from "@calcom/features/calendar-cache/lib/getShouldServeCache"; import AssignmentReasonRecorder from "@calcom/features/ee/round-robin/assignmentReason/AssignmentReasonRecorder"; import { allowDisablingAttendeeConfirmationEmails, allowDisablingHostConfirmationEmails, } from "@calcom/features/ee/workflows/lib/allowDisablingStandardEmails"; import { scheduleWorkflowReminders } from "@calcom/features/ee/workflows/lib/reminders/reminderScheduler"; +import type { FeaturesRepository } from "@calcom/features/flags/features.repository"; import { getFullName } from "@calcom/features/form-builder/utils"; import { UsersRepository } from "@calcom/features/users/users.repository"; import type { GetSubscriberOptions } from "@calcom/features/webhooks/lib/getWebhooks"; @@ -64,6 +69,7 @@ import getOrgIdFromMemberOrTeamId from "@calcom/lib/getOrgIdFromMemberOrTeamId"; import { getPaymentAppData } from "@calcom/lib/getPaymentAppData"; import { getTeamIdFromEventType } from "@calcom/lib/getTeamIdFromEventType"; import { HttpError } from "@calcom/lib/http-error"; +import type { CheckBookingLimitsService } from "@calcom/lib/intervalLimits/server/checkBookingLimits"; import logger from "@calcom/lib/logger"; import { handlePayment } from "@calcom/lib/payment/handlePayment"; import { getPiiFreeCalendarEvent, getPiiFreeEventType } from "@calcom/lib/piiFreeData"; @@ -73,6 +79,7 @@ import { BookingRepository } from "@calcom/lib/server/repository/booking"; import { WorkflowRepository } from "@calcom/lib/server/repository/workflow"; import { HashedLinkService } from "@calcom/lib/server/service/hashedLinkService"; import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat"; +import type { PrismaClient } from "@calcom/prisma"; import prisma from "@calcom/prisma"; import type { AssignmentReasonEnum } from "@calcom/prisma/enums"; import { BookingStatus, SchedulingType, WebhookTriggerEvents } from "@calcom/prisma/enums"; @@ -121,6 +128,7 @@ import type { IEventTypePaymentCredentialType, Invitee, IsFixedAwareUser } from import { validateBookingTimeIsNotOutOfBounds } from "./handleNewBooking/validateBookingTimeIsNotOutOfBounds"; import { validateEventLength } from "./handleNewBooking/validateEventLength"; import handleSeats from "./handleSeats/handleSeats"; +import type { IBookingService } from "./interfaces/IBookingService"; const translator = short(); const log = logger.getSubLogger({ prefix: ["[api] book:user"] }); @@ -150,10 +158,6 @@ function getICalSequence(originalRescheduledBooking: BookingType | null) { return originalRescheduledBooking.iCalSequence + 1; } -type BookingDataSchemaGetter = - | typeof getBookingDataSchema - | typeof import("@calcom/features/bookings/lib/getBookingDataSchemaForApi").default; - type CreatedBooking = Booking & { appsStatus?: AppsStatus[]; paymentUid?: string; paymentId?: number }; type ReturnTypeCreateBooking = Awaited>; export const buildDryRunBooking = ({ @@ -2455,3 +2459,38 @@ async function handler( } export default handler; + +export interface IBookingServiceDependencies { + cacheService: CacheService; + checkBookingAndDurationLimitsService: CheckBookingAndDurationLimitsService; + prismaClient: PrismaClient; + bookingRepository: BookingRepository; + featuresRepository: FeaturesRepository; + checkBookingLimitsService: CheckBookingLimitsService; +} + +/** + * Takes care of creating/rescheduling non-recurring, non-instant bookings. Such bookings could be TeamBooking, UserBooking, SeatedUserBooking, SeatedTeamBooking, etc. + * We can't name it CoreBookingService because non-instant booking also creates a booking but it is entirely different from the regular booking. + * We are open to renaming it to something more descriptive. + */ +export class RegularBookingService implements IBookingService { + constructor(private readonly deps: IBookingServiceDependencies) {} + + async createBooking(input: { bookingData: CreateRegularBookingData; bookingMeta?: CreateBookingMeta }) { + // deps to be passed to handler in follow-up PR + return handler({ bookingData: input.bookingData, ...input.bookingMeta }); + } + + async rescheduleBooking(input: { bookingData: CreateRegularBookingData; bookingMeta?: CreateBookingMeta }) { + return handler({ bookingData: input.bookingData, ...input.bookingMeta }); + } + + async rescheduleBookingForApiV1(input: { + bookingData: CreateRegularBookingData; + bookingMeta?: CreateBookingMeta; + bookingDataSchemaGetter: BookingDataSchemaGetter; + }) { + return handler({ bookingData: input.bookingData, ...input.bookingMeta }, input.bookingDataSchemaGetter); + } +} diff --git a/packages/features/bookings/lib/handleNewRecurringBooking.ts b/packages/features/bookings/lib/handleNewRecurringBooking.ts index e9db6bbb49..c2f0eb7f65 100644 --- a/packages/features/bookings/lib/handleNewRecurringBooking.ts +++ b/packages/features/bookings/lib/handleNewRecurringBooking.ts @@ -1,25 +1,15 @@ +import type { CreateBookingMeta, CreateRecurringBookingData } from "@calcom/features/bookings/lib/dto/types"; import handleNewBooking from "@calcom/features/bookings/lib/handleNewBooking"; import type { BookingResponse } from "@calcom/features/bookings/types"; import { SchedulingType } from "@calcom/prisma/client"; import type { AppsStatus } from "@calcom/types/Calendar"; -export type PlatformParams = { - platformClientId?: string; - platformCancelUrl?: string; - platformBookingUrl?: string; - platformRescheduleUrl?: string; - platformBookingLocation?: string; - areCalendarEventsEnabled?: boolean; -}; +import type { RegularBookingService } from "./handleNewBooking"; +import type { IBookingService } from "./interfaces/IBookingService"; export type BookingHandlerInput = { - bookingData: Record[]; - userId?: number; - // These used to come from headers but now we're passing them as params - hostname?: string; - forcedSlug?: string; - noEmail?: boolean; -} & PlatformParams; + bookingData: CreateRecurringBookingData; +} & CreateBookingMeta; export const handleNewRecurringBooking = async (input: BookingHandlerInput): Promise => { const data = input.bookingData; @@ -115,7 +105,7 @@ export const handleNewRecurringBooking = async (input: BookingHandlerInput): Pro if (!thirdPartyRecurringEventId) { if (eachRecurringBooking.references && eachRecurringBooking.references.length > 0) { - for (const reference of eachRecurringBooking.references!) { + for (const reference of eachRecurringBooking.references) { if (reference.thirdPartyRecurringEventId) { thirdPartyRecurringEventId = reference.thirdPartyRecurringEventId; break; @@ -126,3 +116,32 @@ export const handleNewRecurringBooking = async (input: BookingHandlerInput): Pro } return createdBookings; }; + +export interface IRecurringBookingServiceDependencies { + regularBookingService: RegularBookingService; +} + +/** + * Recurring Booking Service takes care of creating/rescheduling recurring bookings. + */ +export class RecurringBookingService implements IBookingService { + constructor(private readonly deps: IRecurringBookingServiceDependencies) {} + + async createBooking(input: { + bookingData: CreateRecurringBookingData; + bookingMeta?: CreateBookingMeta; + }): Promise { + const handlerInput = { bookingData: input.bookingData, ...(input.bookingMeta || {}) }; + // FOLLOW-UP: Pass on dependencies to the handler + return handleNewRecurringBooking(handlerInput); + } + + async rescheduleBooking(input: { + bookingData: CreateRecurringBookingData; + bookingMeta?: CreateBookingMeta; + }): Promise { + const handlerInput = { bookingData: input.bookingData, ...(input.bookingMeta || {}) }; + // FOLLOW-UP: Pass on dependencies to the handler + return handleNewRecurringBooking(handlerInput); + } +} diff --git a/packages/features/bookings/lib/interfaces/IBookingCreateService.ts b/packages/features/bookings/lib/interfaces/IBookingCreateService.ts new file mode 100644 index 0000000000..fb7f8beb1d --- /dev/null +++ b/packages/features/bookings/lib/interfaces/IBookingCreateService.ts @@ -0,0 +1,7 @@ +// Defines an interface that could be used by any type of Booking Service +// "Any" types are used because this inteface is used by RecurringBookingService which accepts an array of bookingData and RegularBookingService which accepts a single bookingData +// So, this interface enforces just the methods that must be present but not their parameters and return types +export interface IBookingCreateService { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + createBooking: (...args: any[]) => Promise; +} diff --git a/packages/features/bookings/lib/interfaces/IBookingService.ts b/packages/features/bookings/lib/interfaces/IBookingService.ts new file mode 100644 index 0000000000..e1b89aa1f4 --- /dev/null +++ b/packages/features/bookings/lib/interfaces/IBookingService.ts @@ -0,0 +1,9 @@ +// Defines an interface that could be used by any type of Booking Service +// "Any" types are used because this inteface is used by RecurringBookingService which accepts an array of bookingData and RegularBookingService which accepts a single bookingData +// So, this interface enforces just the methods that must be present but not their parameters and return types +export interface IBookingService { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + createBooking: (...args: any[]) => Promise; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + rescheduleBooking: (...args: any[]) => Promise; +} diff --git a/packages/features/instant-meeting/handleInstantMeeting.ts b/packages/features/instant-meeting/handleInstantMeeting.ts index a9528bd2d4..65b36fd07c 100644 --- a/packages/features/instant-meeting/handleInstantMeeting.ts +++ b/packages/features/instant-meeting/handleInstantMeeting.ts @@ -5,11 +5,16 @@ import short from "short-uuid"; import { v5 as uuidv5 } from "uuid"; import dayjs from "@calcom/dayjs"; +import type { + CreateInstantBookingData, + CreateInstantBookingResponse, +} from "@calcom/features/bookings/lib/dto/types"; import getBookingDataSchema from "@calcom/features/bookings/lib/getBookingDataSchema"; import { getBookingFieldsWithSystemFields } from "@calcom/features/bookings/lib/getBookingFields"; import { getBookingData } from "@calcom/features/bookings/lib/handleNewBooking/getBookingData"; import { getCustomInputsResponses } from "@calcom/features/bookings/lib/handleNewBooking/getCustomInputsResponses"; import { getEventTypesFromDB } from "@calcom/features/bookings/lib/handleNewBooking/getEventTypesFromDB"; +import type { IBookingCreateService } from "@calcom/features/bookings/lib/interfaces/IBookingCreateService"; import { getFullName } from "@calcom/features/form-builder/utils"; import { sendNotification } from "@calcom/features/notifications/sendNotification"; import { sendGenericWebhookPayload } from "@calcom/features/webhooks/lib/sendPayload"; @@ -151,17 +156,8 @@ const triggerBrowserNotifications = async (args: { await Promise.allSettled(promises); }; -export type HandleInstantMeetingResponse = { - message: string; - meetingTokenId: number; - bookingId: number; - bookingUid: string; - expires: Date; - userId: number | null; -}; - -async function handler(req: NextApiRequest) { - let eventType = await getEventTypesFromDB(req.body.eventTypeId); +async function _handler(bookingData: CreateInstantBookingData) { + let eventType = await getEventTypesFromDB(bookingData.eventTypeId); const isOrgTeamEvent = !!eventType?.team && !!eventType?.team?.parentId; eventType = { ...eventType, @@ -173,11 +169,11 @@ async function handler(req: NextApiRequest) { } const schema = getBookingDataSchema({ - view: req.body?.rescheduleUid ? "reschedule" : "booking", + view: bookingData?.rescheduleUid ? "reschedule" : "booking", bookingFields: eventType.bookingFields, }); const reqBody = await getBookingData({ - reqBody: req.body, + reqBody: bookingData, eventType, schema, }); @@ -257,7 +253,7 @@ async function handler(req: NextApiRequest) { data: attendeesList, }, }, - creationSource: req.body.creationSource, + creationSource: bookingData.creationSource, }; const createBookingObj = { @@ -275,7 +271,7 @@ async function handler(req: NextApiRequest) { const eventTypeWithExpiryTimeOffset = await prisma.eventType.findUniqueOrThrow({ where: { - id: req.body.eventTypeId, + id: bookingData.eventTypeId, }, select: { instantMeetingExpiryTimeOffsetInSeconds: true, @@ -334,7 +330,21 @@ async function handler(req: NextApiRequest) { bookingUid: newBooking.uid, expires: instantMeetingToken.expires, userId: newBooking.userId, - } satisfies HandleInstantMeetingResponse; + } satisfies CreateInstantBookingResponse; } -export default handler; +/** + * Instant booking service that handles instant/immediate bookings + */ +export class InstantBookingCreateService implements IBookingCreateService { + async createBooking(input: { + bookingData: CreateInstantBookingData; + }): Promise { + return _handler(input.bookingData); + } +} + +// TODO: Remove it in a follow-up PR +export default async function handler(req: NextApiRequest) { + return _handler(req.body); +} diff --git a/packages/lib/di/tokens.ts b/packages/lib/di/tokens.ts index 2e4cb14461..0f2b0a601e 100644 --- a/packages/lib/di/tokens.ts +++ b/packages/lib/di/tokens.ts @@ -51,4 +51,10 @@ export const DI_TOKENS = { HOST_REPOSITORY_MODULE: Symbol("HostRepositoryModule"), ATTRIBUTE_REPOSITORY: Symbol("AttributeRepository"), ATTRIBUTE_REPOSITORY_MODULE: Symbol("AttributeRepositoryModule"), + REGULAR_BOOKING_SERVICE: Symbol("RegularBookingService"), + REGULAR_BOOKING_SERVICE_MODULE: Symbol("RegularBookingServiceModule"), + RECURRING_BOOKING_SERVICE: Symbol("RecurringBookingService"), + RECURRING_BOOKING_SERVICE_MODULE: Symbol("RecurringBookingServiceModule"), + INSTANT_BOOKING_CREATE_SERVICE: Symbol("InstantBookingCreateService"), + INSTANT_BOOKING_CREATE_SERVICE_MODULE: Symbol("InstantBookingCreateServiceModule"), }; diff --git a/packages/prisma/zod/custom/booking.ts b/packages/prisma/zod/custom/booking.ts index 36484405f3..b2986cd8fc 100644 --- a/packages/prisma/zod/custom/booking.ts +++ b/packages/prisma/zod/custom/booking.ts @@ -3,6 +3,7 @@ import z, { ZodNullable, ZodObject, ZodOptional } from "zod"; import { timeZoneSchema } from "@calcom/lib/dayjs/timeZone.schema"; // TODO: Move this out of here. Importing from app-store is a circular package dependency. import { routingFormResponseInDbSchema } from "@calcom/app-store/routing-forms/zod"; +import { CreationSource } from "@calcom/prisma/enums"; export const bookingCreateBodySchema = z.object({ end: z.string().optional(), @@ -47,7 +48,8 @@ export const bookingCreateBodySchema = z.object({ utm_term: z.string().optional(), utm_content: z.string().optional(), }).optional(), - dub_id: z.string().nullish() + dub_id: z.string().nullish(), + creationSource: z.nativeEnum(CreationSource).optional(), }); export type BookingCreateBody = z.input;