Repository: calcom/cal.com
Pull request #20788: fix: v2 Sentry errors
Author: login: supalarry
Labels: name: 🐛 bug
description: Something isn't working
name: platform
description: Anything related to our platform plan
name: bookings
description: area: bookings, availability, timezones, double booking
name: event-types
description: area: event types, event-types
name: core
description: area: core, team members only
name: ready-for-e2e
Created: 2025-04-21T07:40:43Z | Merged: 2025-04-21T09:12:47Z

Linear [CAL-5548](https://linear.app/calcom/issue/CAL-5548/batch-1)<!-- This is an auto-generated description by mrge. -->
---


## Summary by mrge
Improved error handling and validation for v2 bookings API to prevent Sentry errors and provide clearer feedback for invalid booking requests.

- **Bug Fixes**
  - Added specific error messages for booking time, seat availability, and round robin host validation.
  - Improved input validation for attendee contact info and booking limits.
  - Updated API docs and input types for better clarity and accuracy.

<!-- End of auto-generated description by mrge. -->




Diff:
diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts
index 7a2d375d2845a2..8e4244c2fb79d7 100644
--- a/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts
+++ b/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts
@@ -15,7 +15,7 @@ import { TeamsEventTypesRepository } from "@/modules/teams/event-types/teams-eve
 import { TeamsRepository } from "@/modules/teams/teams/teams.repository";
 import { UsersService } from "@/modules/users/services/users.service";
 import { UsersRepository, UserWithProfile } from "@/modules/users/users.repository";
-import { Injectable, Logger, NotFoundException } from "@nestjs/common";
+import { ConflictException, Injectable, Logger, NotFoundException } from "@nestjs/common";
 import { BadRequestException } from "@nestjs/common";
 import { Request } from "express";
 import { z } from "zod";
@@ -141,6 +141,12 @@ export class BookingsService_2024_08_13 {
       if (error instanceof Error) {
         if (error.message === "no_available_users_found_error") {
           throw new BadRequestException("User either already has booking at this time or is not available");
+        } else if (error.message === "booking_time_out_of_bounds_error") {
+          throw new BadRequestException(
+            `The event type can't be booked at selected time ${body.start}. This could be because it's too soon (violating the minimum booking notice) or too far in the future (outside the event's scheduling window). Try fetching available slots first using the GET /v2/slots endpoint and then make a booking with "start" time equal to one of the available slots: https://cal.com/docs/api-reference/v2/slots`
+          );
+        } else if (error.message === "Attempting to book a meeting in the past.") {
+          throw new BadRequestException("Attempting to book a meeting in the past.");
         }
       }
       throw error;
@@ -223,6 +229,12 @@ export class BookingsService_2024_08_13 {
     body: CreateInstantBookingInput_2024_08_13,
     eventType: EventTypeWithOwnerAndTeam
   ) {
+    if (!eventType.team?.id) {
+      throw new BadRequestException(
+        "Instant bookings are only supported for team event types, not individual user event types."
+      );
+    }
+
     const bookingRequest = await this.inputService.createBookingRequest(request, body, eventType);
     const booking = await handleInstantMeeting(bookingRequest);
 
@@ -313,29 +325,37 @@ export class BookingsService_2024_08_13 {
     eventType: EventTypeWithOwnerAndTeam
   ) {
     const bookingRequest = await this.inputService.createBookingRequest(request, body, eventType);
-    const booking = await handleNewBooking({
-      bookingData: bookingRequest.body,
-      userId: bookingRequest.userId,
-      hostname: bookingRequest.headers?.host || "",
-      platformClientId: bookingRequest.platformClientId,
-      platformRescheduleUrl: bookingRequest.platformRescheduleUrl,
-      platformCancelUrl: bookingRequest.platformCancelUrl,
-      platformBookingUrl: bookingRequest.platformBookingUrl,
-      platformBookingLocation: bookingRequest.platformBookingLocation,
-    });
+    try {
+      const booking = await handleNewBooking({
+        bookingData: bookingRequest.body,
+        userId: bookingRequest.userId,
+        hostname: bookingRequest.headers?.host || "",
+        platformClientId: bookingRequest.platformClientId,
+        platformRescheduleUrl: bookingRequest.platformRescheduleUrl,
+        platformCancelUrl: bookingRequest.platformCancelUrl,
+        platformBookingUrl: bookingRequest.platformBookingUrl,
+        platformBookingLocation: bookingRequest.platformBookingLocation,
+      });
 
-    if (!booking.uid) {
-      throw new Error("Booking missing uid");
-    }
+      if (!booking.uid) {
+        throw new Error("Booking missing uid");
+      }
 
-    const databaseBooking = await this.bookingsRepository.getByUidWithAttendeesWithBookingSeatAndUserAndEvent(
-      booking.uid
-    );
-    if (!databaseBooking) {
-      throw new Error(`Booking with uid=${booking.uid} was not found in the database`);
-    }
+      const databaseBooking =
+        await this.bookingsRepository.getByUidWithAttendeesWithBookingSeatAndUserAndEvent(booking.uid);
+      if (!databaseBooking) {
+        throw new Error(`Booking with uid=${booking.uid} was not found in the database`);
+      }
 
-    return this.outputService.getOutputCreateSeatedBooking(databaseBooking, booking.seatReferenceUid || "");
+      return this.outputService.getOutputCreateSeatedBooking(databaseBooking, booking.seatReferenceUid || "");
+    } catch (error) {
+      if (error instanceof Error) {
+        if (error.message === "booking_seats_full_error") {
+          throw new ConflictException("No more seats left at this seated booking.");
+        }
+      }
+      throw error;
+    }
   }
 
   async getBooking(uid: string) {
@@ -697,17 +717,28 @@ export class BookingsService_2024_08_13 {
 
     const profile = this.usersService.getUserMainProfile(user);
 
-    const reassigned = await roundRobinManualReassignment({
-      bookingId: booking.id,
-      newUserId,
-      orgId: profile?.organizationId || null,
-      reassignReason: body.reason,
-      reassignedById,
-      emailsEnabled,
-      platformClientParams,
-    });
+    try {
+      const reassigned = await roundRobinManualReassignment({
+        bookingId: booking.id,
+        newUserId,
+        orgId: profile?.organizationId || null,
+        reassignReason: body.reason,
+        reassignedById,
+        emailsEnabled,
+        platformClientParams,
+      });
 
-    return this.outputService.getOutputReassignedBooking(reassigned);
+      return this.outputService.getOutputReassignedBooking(reassigned);
+    } catch (error) {
+      if (error instanceof Error) {
+        if (error.message === "invalid_round_robin_host") {
+          throw new BadRequestException(
+            `User with id=${newUserId} is not a valid Round Robin host - the user to which you reassign this booking must be one of the booking hosts. Fetch the booking using following endpoint and select id of one of the hosts: https://cal.com/docs/api-reference/v2/bookings/get-a-booking`
+          );
+        }
+      }
+      throw error;
+    }
   }
 
   async confirmBooking(bookingUid: string, requestUser: UserWithProfile) {
diff --git a/apps/api/v2/swagger/documentation.json b/apps/api/v2/swagger/documentation.json
index 2d59fdd640a0d4..97c335f356ac7b 100644
--- a/apps/api/v2/swagger/documentation.json
+++ b/apps/api/v2/swagger/documentation.json
@@ -13337,6 +13337,7 @@
           },
           "enabledLayouts": {
             "type": "array",
+            "description": "Array of valid layouts - month, week or column",
             "items": {
               "type": "string",
               "enum": [
@@ -21103,10 +21104,20 @@
             "description": "The start time of the booking in ISO 8601 format in UTC timezone.",
             "example": "2024-08-13T09:00:00Z"
           },
-          "lengthInMinutes": {
-            "type": "number",
-            "example": 30,
-            "description": "If it is an event type that has multiple possible lengths that attendee can pick from, you can pass the desired booking length here.\n    If not provided then event type default length will be used for the booking."
+          "attendee": {
+            "description": "The attendee's details.",
+            "allOf": [
+              {
+                "$ref": "#/components/schemas/Attendee"
+              }
+            ]
+          },
+          "bookingFieldsResponses": {
+            "type": "object",
+            "description": "Booking field responses consisting of an object with booking field slug as keys and user response as values for custom booking fields added by you.",
+            "example": {
+              "customField": "customValue"
+            }
           },
           "eventTypeId": {
             "type": "number",
@@ -21133,14 +21144,6 @@
             "description": "The organization slug. Optional, only used when booking with eventTypeSlug + username or eventTypeSlug + teamSlug.",
             "example": "acme-corp"
           },
-          "attendee": {
-            "description": "The attendee's details.",
-            "allOf": [
-              {
-                "$ref": "#/components/schemas/Attendee"
-              }
-            ]
-          },
           "guests": {
             "description": "An optional list of guest emails attending the event.",
             "example": [
@@ -21194,12 +21197,10 @@
               "key": "value"
             }
           },
-          "bookingFieldsResponses": {
-            "type": "object",
-            "description": "Booking field responses consisting of an object with booking field slug as keys and user response as values.",
-            "example": {
-              "customField": "customValue"
-            }
+          "lengthInMinutes": {
+            "type": "number",
+            "example": 30,
+            "description": "If it is an event type that has multiple possible lengths that attendee can pick from, you can pass the desired booking length here.\n    If not provided then event type default length will be used for the booking."
           }
         },
         "required": [
@@ -21215,10 +21216,20 @@
             "description": "The start time of the booking in ISO 8601 format in UTC timezone.",
             "example": "2024-08-13T09:00:00Z"
           },
-          "lengthInMinutes": {
-            "type": "number",
-            "example": 30,
-            "description": "If it is an event type that has multiple possible lengths that attendee can pick from, you can pass the desired booking length here.\n    If not provided then event type default length will be used for the booking."
+          "attendee": {
+            "description": "The attendee's details.",
+            "allOf": [
+              {
+                "$ref": "#/components/schemas/Attendee"
+              }
+            ]
+          },
+          "bookingFieldsResponses": {
+            "type": "object",
+            "description": "Booking field responses consisting of an object with booking field slug as keys and user response as values for custom booking fields added by you.",
+            "example": {
+              "customField": "customValue"
+            }
           },
           "eventTypeId": {
             "type": "number",
@@ -21245,14 +21256,6 @@
             "description": "The organization slug. Optional, only used when booking with eventTypeSlug + username or eventTypeSlug + teamSlug.",
             "example": "acme-corp"
           },
-          "attendee": {
-            "description": "The attendee's details.",
-            "allOf": [
-              {
-                "$ref": "#/components/schemas/Attendee"
-              }
-            ]
-          },
           "guests": {
             "description": "An optional list of guest emails attending the event.",
             "example": [
@@ -21306,12 +21309,10 @@
               "key": "value"
             }
           },
-          "bookingFieldsResponses": {
-            "type": "object",
-            "description": "Booking field responses consisting of an object with booking field slug as keys and user response as values.",
-            "example": {
-              "customField": "customValue"
-            }
+          "lengthInMinutes": {
+            "type": "number",
+            "example": 30,
+            "description": "If it is an event type that has multiple possible lengths that attendee can pick from, you can pass the desired booking length here.\n    If not provided then event type default length will be used for the booking."
           },
           "instant": {
             "type": "boolean",
@@ -21333,10 +21334,20 @@
             "description": "The start time of the booking in ISO 8601 format in UTC timezone.",
             "example": "2024-08-13T09:00:00Z"
           },
-          "lengthInMinutes": {
-            "type": "number",
-            "example": 30,
-            "description": "If it is an event type that has multiple possible lengths that attendee can pick from, you can pass the desired booking length here.\n    If not provided then event type default length will be used for the booking."
+          "attendee": {
+            "description": "The attendee's details.",
+            "allOf": [
+              {
+                "$ref": "#/components/schemas/Attendee"
+              }
+            ]
+          },
+          "bookingFieldsResponses": {
+            "type": "object",
+            "description": "Booking field responses consisting of an object with booking field slug as keys and user response as values for custom booking fields added by you.",
+            "example": {
+              "customField": "customValue"
+            }
           },
           "eventTypeId": {
             "type": "number",
@@ -21363,14 +21374,6 @@
             "description": "The organization slug. Optional, only used when booking with eventTypeSlug + username or eventTypeSlug + teamSlug.",
             "example": "acme-corp"
           },
-          "attendee": {
-            "description": "The attendee's details.",
-            "allOf": [
-              {
-                "$ref": "#/components/schemas/Attendee"
-              }
-            ]
-          },
           "guests": {
             "description": "An optional list of guest emails attending the event.",
             "example": [
@@ -21424,12 +21427,10 @@
               "key": "value"
             }
           },
-          "bookingFieldsResponses": {
-            "type": "object",
-            "description": "Booking field responses consisting of an object with booking field slug as keys and user response as values.",
-            "example": {
-              "customField": "customValue"
-            }
+          "lengthInMinutes": {
+            "type": "number",
+            "example": 30,
+            "description": "If it is an event type that has multiple possible lengths that attendee can pick from, you can pass the desired booking length here.\n    If not provided then event type default length will be used for the booking."
           },
           "recurrenceCount": {
             "type": "number",
diff --git a/docs/api-reference/v2/openapi.json b/docs/api-reference/v2/openapi.json
index d4321e13bf56fb..87d4a4e6269c91 100644
--- a/docs/api-reference/v2/openapi.json
+++ b/docs/api-reference/v2/openapi.json
@@ -12518,6 +12518,7 @@
           },
           "enabledLayouts": {
             "type": "array",
+            "description": "Array of valid layouts - month, week or column",
             "items": {
               "type": "string",
               "enum": ["month", "week", "column"]
@@ -19268,10 +19269,20 @@
             "description": "The start time of the booking in ISO 8601 format in UTC timezone.",
             "example": "2024-08-13T09:00:00Z"
           },
-          "lengthInMinutes": {
-            "type": "number",
-            "example": 30,
-            "description": "If it is an event type that has multiple possible lengths that attendee can pick from, you can pass the desired booking length here.\n    If not provided then event type default length will be used for the booking."
+          "attendee": {
+            "description": "The attendee's details.",
+            "allOf": [
+              {
+                "$ref": "#/components/schemas/Attendee"
+              }
+            ]
+          },
+          "bookingFieldsResponses": {
+            "type": "object",
+            "description": "Booking field responses consisting of an object with booking field slug as keys and user response as values for custom booking fields added by you.",
+            "example": {
+              "customField": "customValue"
+            }
           },
           "eventTypeId": {
             "type": "number",
@@ -19298,14 +19309,6 @@
             "description": "The organization slug. Optional, only used when booking with eventTypeSlug + username or eventTypeSlug + teamSlug.",
             "example": "acme-corp"
           },
-          "attendee": {
-            "description": "The attendee's details.",
-            "allOf": [
-              {
-                "$ref": "#/components/schemas/Attendee"
-              }
-            ]
-          },
           "guests": {
             "description": "An optional list of guest emails attending the event.",
             "example": ["guest1@example.com", "guest2@example.com"],
@@ -19356,12 +19359,10 @@
               "key": "value"
             }
           },
-          "bookingFieldsResponses": {
-            "type": "object",
-            "description": "Booking field responses consisting of an object with booking field slug as keys and user response as values.",
-            "example": {
-              "customField": "customValue"
-            }
+          "lengthInMinutes": {
+            "type": "number",
+            "example": 30,
+            "description": "If it is an event type that has multiple possible lengths that attendee can pick from, you can pass the desired booking length here.\n    If not provided then event type default length will be used for the booking."
           }
         },
         "required": ["start", "attendee"]
@@ -19374,10 +19375,20 @@
             "description": "The start time of the booking in ISO 8601 format in UTC timezone.",
             "example": "2024-08-13T09:00:00Z"
           },
-          "lengthInMinutes": {
-            "type": "number",
-            "example": 30,
-            "description": "If it is an event type that has multiple possible lengths that attendee can pick from, you can pass the desired booking length here.\n    If not provided then event type default length will be used for the booking."
+          "attendee": {
+            "description": "The attendee's details.",
+            "allOf": [
+              {
+                "$ref": "#/components/schemas/Attendee"
+              }
+            ]
+          },
+          "bookingFieldsResponses": {
+            "type": "object",
+            "description": "Booking field responses consisting of an object with booking field slug as keys and user response as values for custom booking fields added by you.",
+            "example": {
+              "customField": "customValue"
+            }
           },
           "eventTypeId": {
             "type": "number",
@@ -19404,14 +19415,6 @@
             "description": "The organization slug. Optional, only used when booking with eventTypeSlug + username or eventTypeSlug + teamSlug.",
             "example": "acme-corp"
           },
-          "attendee": {
-            "description": "The attendee's details.",
-            "allOf": [
-              {
-                "$ref": "#/components/schemas/Attendee"
-              }
-            ]
-          },
           "guests": {
             "description": "An optional list of guest emails attending the event.",
             "example": ["guest1@example.com", "guest2@example.com"],
@@ -19462,12 +19465,10 @@
               "key": "value"
             }
           },
-          "bookingFieldsResponses": {
-            "type": "object",
-            "description": "Booking field responses consisting of an object with booking field slug as keys and user response as values.",
-            "example": {
-              "customField": "customValue"
-            }
+          "lengthInMinutes": {
+            "type": "number",
+            "example": 30,
+            "description": "If it is an event type that has multiple possible lengths that attendee can pick from, you can pass the desired booking length here.\n    If not provided then event type default length will be used for the booking."
           },
           "instant": {
             "type": "boolean",
@@ -19485,10 +19486,20 @@
             "description": "The start time of the booking in ISO 8601 format in UTC timezone.",
             "example": "2024-08-13T09:00:00Z"
           },
-          "lengthInMinutes": {
-            "type": "number",
-            "example": 30,
-            "description": "If it is an event type that has multiple possible lengths that attendee can pick from, you can pass the desired booking length here.\n    If not provided then event type default length will be used for the booking."
+          "attendee": {
+            "description": "The attendee's details.",
+            "allOf": [
+              {
+                "$ref": "#/components/schemas/Attendee"
+              }
+            ]
+          },
+          "bookingFieldsResponses": {
+            "type": "object",
+            "description": "Booking field responses consisting of an object with booking field slug as keys and user response as values for custom booking fields added by you.",
+            "example": {
+              "customField": "customValue"
+            }
           },
           "eventTypeId": {
             "type": "number",
@@ -19515,14 +19526,6 @@
             "description": "The organization slug. Optional, only used when booking with eventTypeSlug + username or eventTypeSlug + teamSlug.",
             "example": "acme-corp"
           },
-          "attendee": {
-            "description": "The attendee's details.",
-            "allOf": [
-              {
-                "$ref": "#/components/schemas/Attendee"
-              }
-            ]
-          },
           "guests": {
             "description": "An optional list of guest emails attending the event.",
             "example": ["guest1@example.com", "guest2@example.com"],
@@ -19573,12 +19576,10 @@
               "key": "value"
             }
           },
-          "bookingFieldsResponses": {
-            "type": "object",
-            "description": "Booking field responses consisting of an object with booking field slug as keys and user response as values.",
-            "example": {
-              "customField": "customValue"
-            }
+          "lengthInMinutes": {
+            "type": "number",
+            "example": 30,
+            "description": "If it is an event type that has multiple possible lengths that attendee can pick from, you can pass the desired booking length here.\n    If not provided then event type default length will be used for the booking."
           },
           "recurrenceCount": {
             "type": "number",
diff --git a/packages/platform/types/bookings/2024-08-13/inputs/create-booking.input.ts b/packages/platform/types/bookings/2024-08-13/inputs/create-booking.input.ts
index 0e1887c81d47d9..b584d661013907 100644
--- a/packages/platform/types/bookings/2024-08-13/inputs/create-booking.input.ts
+++ b/packages/platform/types/bookings/2024-08-13/inputs/create-booking.input.ts
@@ -40,6 +40,7 @@ export const FAILED_EVENT_TYPE_IDENTIFICATION_ERROR_MESSAGE =
   "Either eventTypeId or eventTypeSlug + username or eventTypeSlug + teamSlug must be provided";
 
 function RequireEventTypeIdentification(validationOptions?: ValidationOptions) {
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
   return function (object: any) {
     registerDecorator({
       name: "requireEventTypeIdentification",
@@ -48,7 +49,7 @@ function RequireEventTypeIdentification(validationOptions?: ValidationOptions) {
       options: validationOptions,
       constraints: [],
       validator: {
-        validate(_value: any, args: ValidationArguments) {
+        validate(_: unknown, args: ValidationArguments) {
           const obj = args.object as CreateBookingInput_2024_08_13;
 
           const hasEventTypeId = !!obj?.eventTypeId;
@@ -68,28 +69,31 @@ function RequireEventTypeIdentification(validationOptions?: ValidationOptions) {
 }
 
 function RequireEmailOrPhone(validationOptions?: ValidationOptions) {
-  return function (target: object, propertyName: string) {
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  return function (object: any) {
     registerDecorator({
       name: "requireEmailOrPhone",
-      target: target.constructor,
-      propertyName: propertyName,
+      target: object,
+      propertyName: "attendee email or phone",
       options: validationOptions,
+      constraints: [],
       validator: {
-        validate(value: any, args: ValidationArguments) {
+        validate(_: unknown, args: ValidationArguments) {
           const obj = args.object as Attendee;
 
-          const hasPhoneNumber = !!obj.phoneNumber && obj.phoneNumber.trim().length > 0;
-          const hasEmail = !!obj.email && obj.email.trim().length > 0;
+          const hasPhoneNumber = !!obj.phoneNumber;
+          const hasEmail = !!obj.email;
           return hasPhoneNumber || hasEmail;
         },
         defaultMessage(): string {
-          return "At least one contact method (email or phone number) must be provided";
+          return "Attendee must have at least one contact method (email or phone number)";
         },
       },
     });
   };
 }
 
+@RequireEmailOrPhone()
 class Attendee {
   @ApiProperty({
     type: String,
@@ -108,7 +112,6 @@ class Attendee {
   @Validate((value: string) => !value || isEmail(value), {
     message: "Invalid email format",
   })
-  @RequireEmailOrPhone()
   email?: string;
 
   @ApiProperty({
@@ -162,15 +165,24 @@ export class CreateBookingInput_2024_08_13 {
   @IsDateString()
   start!: string;
 
-  @IsOptional()
-  @IsInt()
-  @Min(1)
+  @ApiProperty({
+    type: Attendee,
+    description: "The attendee's details.",
+  })
+  @ValidateNested()
+  @Type(() => Attendee)
+  attendee!: Attendee;
+
   @ApiPropertyOptional({
-    example: 30,
-    description: `If it is an event type that has multiple possible lengths that attendee can pick from, you can pass the desired booking length here.
-    If not provided then event type default length will be used for the booking.`,
+    type: Object,
+    description:
+      "Booking field responses consisting of an object with booking field slug as keys and user response as values for custom booking fields added by you.",
+    example: { customField: "customValue" },
+    required: false,
   })
-  lengthInMinutes?: number;
+  @IsObject()
+  @IsOptional()
+  bookingFieldsResponses?: Record<string, unknown>;
 
   @ApiPropertyOptional({
     type: Number,
@@ -222,14 +234,6 @@ export class CreateBookingInput_2024_08_13 {
   @IsString()
   organizationSlug?: string;
 
-  @ApiProperty({
-    type: Attendee,
-    description: "The attendee's details.",
-  })
-  @ValidateNested()
-  @Type(() => Attendee)
-  attendee!: Attendee;
-
   @ApiPropertyOptional({
     type: [String],
     description: "An optional list of guest emails attending the event.",
@@ -287,16 +291,15 @@ export class CreateBookingInput_2024_08_13 {
   })
   metadata?: Record<string, string>;
 
+  @IsOptional()
+  @IsInt()
+  @Min(1)
   @ApiPropertyOptional({
-    type: Object,
-    description:
-      "Booking field responses consisting of an object with booking field slug as keys and user response as values.",
-    example: { customField: "customValue" },
-    required: false,
+    example: 30,
+    description: `If it is an event type that has multiple possible lengths that attendee can pick from, you can pass the desired booking length here.
+    If not provided then event type default length will be used for the booking.`,
   })
-  @IsObject()
-  @IsOptional()
-  bookingFieldsResponses?: Record<string, unknown>;
+  lengthInMinutes?: number;
 }
 
 export class CreateInstantBookingInput_2024_08_13 extends CreateBookingInput_2024_08_13 {
diff --git a/packages/platform/types/event-types/event-types_2024_06_14/inputs/booker-layouts.input.ts b/packages/platform/types/event-types/event-types_2024_06_14/inputs/booker-layouts.input.ts
index 44315a1208f908..3664e52943b95d 100644
--- a/packages/platform/types/event-types/event-types_2024_06_14/inputs/booker-layouts.input.ts
+++ b/packages/platform/types/event-types/event-types_2024_06_14/inputs/booker-layouts.input.ts
@@ -1,5 +1,5 @@
 import { ApiProperty } from "@nestjs/swagger";
-import type { ValidatorConstraintInterface, ValidationOptions } from "class-validator";
+import type { ValidatorConstraintInterface, ValidationOptions, ValidationArguments } from "class-validator";
 import { IsEnum, ValidatorConstraint, registerDecorator } from "class-validator";
 
 import {
@@ -44,14 +44,49 @@ function IsValidLayout(validationOptions?: ValidationOptions) {
   };
 }
 
+@ValidatorConstraint({ name: "isDefaultLayoutWithinEnabledLayouts", async: false })
+export class DefaultLayoutEnabledValidator implements ValidatorConstraintInterface {
+  validate(_: unknown, args: ValidationArguments) {
+    const object = args.object as BookerLayouts_2024_06_14;
+    if (object.defaultLayout && Array.isArray(object.enabledLayouts) && object.enabledLayouts.length > 0) {
+      return object.enabledLayouts.includes(object.defaultLayout);
+    }
+
+    return false;
+  }
+
+  defaultMessage() {
+    return "If you pass the optional 'bookerLayouts' property, you must provide both 'bookerLayouts.defaultLayout' and 'bookerLayouts.enabledLayouts' and 'bookerLayouts.enabledLayouts' must include 'bookerLayouts.defaultLayout'";
+  }
+}
+
+function IsDefaultLayoutWithinEnabledLayouts(validationOptions?: ValidationOptions) {
+  return function (object: any) {
+    registerDecorator({
+      name: "isDefaultLayoutWithinEnabledLayouts",
+      target: object,
+      propertyName: "defaultLayout",
+      options: validationOptions,
+      constraints: [],
+      validator: DefaultLayoutEnabledValidator,
+    });
+  };
+}
+@IsDefaultLayoutWithinEnabledLayouts()
 export class BookerLayouts_2024_06_14 {
   @IsValidLayout({ message: "defaultLayout must be one of the valid layouts - month, week or column" })
   @ApiProperty({ type: String, enum: BookerLayoutsInputEnum_2024_06_14 })
   @IsEnum(BookerLayoutsInputEnum_2024_06_14)
   defaultLayout!: BookerLayoutsInputEnum_2024_06_14;
 
-  @IsValidLayout({ message: "enabledLayouts must be one of the valid layouts." })
-  @ApiProperty({ type: [String], enum: BookerLayoutsInputEnum_2024_06_14 })
+  @IsValidLayout({
+    message: "enabledLayouts must be an array containing valid layouts - month, week or column",
+  })
+  @ApiProperty({
+    type: [String],
+    enum: BookerLayoutsInputEnum_2024_06_14,
+    description: "Array of valid layouts - month, week or column",
+  })
   @IsEnum(BookerLayoutsInputEnum_2024_06_14, {
     each: true,
     message: "enabledLayouts must contain only valid layouts - month, week or column",
diff --git a/packages/platform/types/event-types/event-types_2024_06_14/inputs/booking-limits-count.input.ts b/packages/platform/types/event-types/event-types_2024_06_14/inputs/booking-limits-count.input.ts
index 7203feb02ddadd..f3eb2f1b7cc070 100644
--- a/packages/platform/types/event-types/event-types_2024_06_14/inputs/booking-limits-count.input.ts
+++ b/packages/platform/types/event-types/event-types_2024_06_14/inputs/booking-limits-count.input.ts
@@ -63,7 +63,7 @@ class BookingLimitsCountValidator implements ValidatorConstraintInterface {
   } = {};
 
   validate(value: BookingLimitsCount_2024_06_14) {
-    if (!value) return false;
+    if (!value || typeof value !== "object") return false;
     if ("disabled" in value) {
       return true;
     }
@@ -97,9 +97,9 @@ class BookingLimitsCountValidator implements ValidatorConstraintInterface {
   defaultMessage() {
     const { invalidLimit, comparedLimit } = this.errorDetails;
     if (invalidLimit && comparedLimit) {
-      return `Invalid booking limits: The number of bookings for ${invalidLimit} cannot exceed the number of bookings for ${comparedLimit}.`;
+      return `Invalid bookingLimitsCount: The number of bookings for ${invalidLimit} cannot exceed the number of bookings for ${comparedLimit}.`;
     }
-    return `Invalid booking limits count structure`;
+    return `Invalid bookingLimitsCount`;
   }
 }
 
diff --git a/packages/platform/types/event-types/event-types_2024_06_14/inputs/booking-limits-duration.input.ts b/packages/platform/types/event-types/event-types_2024_06_14/inputs/booking-limits-duration.input.ts
index 13ad1ef03ddf37..f521eb50ee2827 100644
--- a/packages/platform/types/event-types/event-types_2024_06_14/inputs/booking-limits-duration.input.ts
+++ b/packages/platform/types/event-types/event-types_2024_06_14/inputs/booking-limits-duration.input.ts
@@ -54,7 +54,7 @@ class BookingLimitsDurationValidator implements ValidatorConstraintInterface {
     comparedLimit?: string;
   } = {};
   validate(value: BookingLimitsDuration_2024_06_14) {
-    if (!value) return false;
+    if (!value || typeof value !== "object") return false;
     if ("disabled" in value) {
       return true;
     }
@@ -88,9 +88,9 @@ class BookingLimitsDurationValidator implements ValidatorConstraintInterface {
   defaultMessage() {
     const { invalidLimit, comparedLimit } = this.errorDetails;
     if (invalidLimit && comparedLimit) {
-      return `Invalid booking durations: The duration of bookings for ${invalidLimit} cannot exceed the duration of bookings for ${comparedLimit}.`;
+      return `Invalid bookingLimitsDuration: The duration of bookings for ${invalidLimit} cannot exceed the duration of bookings for ${comparedLimit}.`;
     }
-    return `Invalid booking durations structure`;
+    return `Invalid bookingLimitsDuration`;
   }
 }
 
diff --git a/packages/platform/types/event-types/event-types_2024_06_14/inputs/create-event-type.input.ts b/packages/platform/types/event-types/event-types_2024_06_14/inputs/create-event-type.input.ts
index be7db511efa3c2..cf67e2168b48df 100644
--- a/packages/platform/types/event-types/event-types_2024_06_14/inputs/create-event-type.input.ts
+++ b/packages/platform/types/event-types/event-types_2024_06_14/inputs/create-event-type.input.ts
@@ -288,6 +288,7 @@ class BaseCreateEventTypeInput {
       "Should booker have week, month or column view. Specify default layout and enabled layouts user can pick.",
   })
   @Type(() => BookerLayouts_2024_06_14)
+  @ValidateNested()
   bookerLayouts?: BookerLayouts_2024_06_14;
 
   @IsOptional()
diff --git a/packages/platform/types/event-types/event-types_2024_06_14/inputs/update-event-type.input.ts b/packages/platform/types/event-types/event-types_2024_06_14/inputs/update-event-type.input.ts
index e883b9d3337cb8..4a45d0cc83de7b 100644
--- a/packages/platform/types/event-types/event-types_2024_06_14/inputs/update-event-type.input.ts
+++ b/packages/platform/types/event-types/event-types_2024_06_14/inputs/update-event-type.input.ts
@@ -285,6 +285,7 @@ class BaseUpdateEventTypeInput {
       "Should booker have week, month or column view. Specify default layout and enabled layouts user can pick.",
   })
   @Type(() => BookerLayouts_2024_06_14)
+  @ValidateNested()
   bookerLayouts?: BookerLayouts_2024_06_14;
 
   @IsOptional()


Reviews:
user: login: cubic-dev-ai[bot]
body: mrge found 6 issues across 9 files. View them in [mrge.io](https://mrge.io/pr/calcom/cal.com/pull/20788)
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-21T07:43:49Z
user: login: ThyMinimalDev
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-21T09:12:37Z

Review comments:
diff_hunk: @@ -285,6 +285,7 @@ class BaseUpdateEventTypeInput {
       "Should booker have week, month or column view. Specify default layout and enabled layouts user can pick.",
   })
   @Type(() => BookerLayouts_2024_06_14)
+  @ValidateNested()
path: packages/platform/types/event-types/event-types_2024_06_14/inputs/update-event-type.input.ts
user: login: cubic-dev-ai[bot]
body: Missing validation decorator for nested object
created_at: 2025-04-21T07:43:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -68,28 +69,31 @@ function RequireEventTypeIdentification(validationOptions?: ValidationOptions) {
 }
 
 function RequireEmailOrPhone(validationOptions?: ValidationOptions) {
-  return function (target: object, propertyName: string) {
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  return function (object: any) {
     registerDecorator({
       name: "requireEmailOrPhone",
-      target: target.constructor,
-      propertyName: propertyName,
+      target: object,
+      propertyName: "attendee email or phone",
       options: validationOptions,
+      constraints: [],
       validator: {
-        validate(value: any, args: ValidationArguments) {
+        validate(_: unknown, args: ValidationArguments) {
           const obj = args.object as Attendee;
 
-          const hasPhoneNumber = !!obj.phoneNumber && obj.phoneNumber.trim().length > 0;
-          const hasEmail = !!obj.email && obj.email.trim().length > 0;
+          const hasPhoneNumber = !!obj.phoneNumber;
path: packages/platform/types/bookings/2024-08-13/inputs/create-booking.input.ts
user: login: cubic-dev-ai[bot]
body: Removed string emptiness check which may allow empty strings to be considered valid phone numbers
created_at: 2025-04-21T07:43:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -68,28 +69,31 @@
 }
 
 function RequireEmailOrPhone(validationOptions?: ValidationOptions) {
-  return function (target: object, propertyName: string) {
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  return function (object: any) {
     registerDecorator({
       name: "requireEmailOrPhone",
-      target: target.constructor,
-      propertyName: propertyName,
+      target: object,
+      propertyName: "attendee email or phone",
       options: validationOptions,
+      constraints: [],
       validator: {
-        validate(value: any, args: ValidationArguments) {
+        validate(_: unknown, args: ValidationArguments) {
           const obj = args.object as Attendee;
 
-          const hasPhoneNumber = !!obj.phoneNumber && obj.phoneNumber.trim().length > 0;
-          const hasEmail = !!obj.email && obj.email.trim().length > 0;
+          const hasPhoneNumber = !!obj.phoneNumber;
+          const hasEmail = !!obj.email;
path: packages/platform/types/bookings/2024-08-13/inputs/create-booking.input.ts
user: login: cubic-dev-ai[bot]
body: Removed string emptiness check which may allow empty strings to be considered valid emails
created_at: 2025-04-21T07:43:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -40,6 +40,7 @@
   "Either eventTypeId or eventTypeSlug + username or eventTypeSlug + teamSlug must be provided";
 
 function RequireEventTypeIdentification(validationOptions?: ValidationOptions) {
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
path: packages/platform/types/bookings/2024-08-13/inputs/create-booking.input.ts
user: login: cubic-dev-ai[bot]
body: Using eslint-disable for explicit any type decreases type safety
created_at: 2025-04-21T07:43:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -68,28 +69,31 @@
 }
 
 function RequireEmailOrPhone(validationOptions?: ValidationOptions) {
-  return function (target: object, propertyName: string) {
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  return function (object: any) {
     registerDecorator({
       name: "requireEmailOrPhone",
-      target: target.constructor,
-      propertyName: propertyName,
+      target: object,
+      propertyName: "attendee email or phone",
       options: validationOptions,
+      constraints: [],
       validator: {
-        validate(value: any, args: ValidationArguments) {
+        validate(_: unknown, args: ValidationArguments) {
           const obj = args.object as Attendee;
 
-          const hasPhoneNumber = !!obj.phoneNumber && obj.phoneNumber.trim().length > 0;
-          const hasEmail = !!obj.email && obj.email.trim().length > 0;
+          const hasPhoneNumber = !!obj.phoneNumber;
+          const hasEmail = !!obj.email;
           return hasPhoneNumber || hasEmail;
         },
         defaultMessage(): string {
-          return "At least one contact method (email or phone number) must be provided";
+          return "Attendee must have at least one contact method (email or phone number)";
         },
       },
     });
   };
 }
 
+@RequireEmailOrPhone()
path: packages/platform/types/bookings/2024-08-13/inputs/create-booking.input.ts
user: login: cubic-dev-ai[bot]
body: Decorator moved from property to class level, ensure validation behavior is still correct
created_at: 2025-04-21T07:43:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -68,28 +69,31 @@
 }
 
 function RequireEmailOrPhone(validationOptions?: ValidationOptions) {
-  return function (target: object, propertyName: string) {
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  return function (object: any) {
     registerDecorator({
       name: "requireEmailOrPhone",
-      target: target.constructor,
-      propertyName: propertyName,
+      target: object,
+      propertyName: "attendee email or phone",
path: packages/platform/types/bookings/2024-08-13/inputs/create-booking.input.ts
user: login: cubic-dev-ai[bot]
body: Hard-coded propertyName may cause issues if the property name changes
created_at: 2025-04-21T07:43:49Z
author_association: CONTRIBUTOR

PR comments:
user: login: linear[bot]
created_at: 2025-04-21T07:40:46Z
body: <p><a href="https://linear.app/calcom/issue/CAL-5548/batch-1">CAL-5548 batch 1</a></p>
author_association: NONE
user: login: vercel[bot]
created_at: 2025-04-21T07:42:00Z
body: [vc]: #mZ8m89x9UxPt7qsEZxt4NdPJw7xAkfvTDKuWW0UAYY8=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJjYWxjb20td2ViLWNhbmFyeSIsInJvb3REaXJlY3RvcnkiOiJhcHBzL3dlYiIsImxpdmVGZWVkYmFjayI6eyJyZXNvbHZlZCI6MCwidW5yZXNvbHZlZCI6MCwidG90YWwiOjAsImxpbmsiOiIifSwiaW5zcGVjdG9yVXJsIjoiaHR0cHM6Ly92ZXJjZWwuY29tL2NhbC9jYWxjb20td2ViLWNhbmFyeS9Fck5TdXZHNmhvR3RFeFRTWTdxemNnM05ZWmlkIiwibmV4dENvbW1pdFN0YXR1cyI6IklHTk9SRUQifSx7Im5hbWUiOiJjYWwiLCJyb290RGlyZWN0b3J5IjoiYXBwcy93ZWIiLCJsaXZlRmVlZGJhY2siOnsicmVzb2x2ZWQiOjAsInVucmVzb2x2ZWQiOjAsInRvdGFsIjowLCJsaW5rIjoiIn0sImluc3BlY3RvclVybCI6Imh0dHBzOi8vdmVyY2VsLmNvbS9jYWwvY2FsLzdicU5ta3FrZzJDbVAyR3RGQTRFSjN0eDZrU3oiLCJuZXh0Q29tbWl0U3RhdHVzIjoiSUdOT1JFRCJ9XX0=
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)


<details><summary>2 Skipped Deployments</summary>

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **cal** | ⬜️ Ignored ([Inspect](https://vercel.com/cal/cal/7bqNmkqkg2CmP2GtFA4EJ3tx6kSz)) |  |  | Apr 21, 2025 7:42am |
| **calcom-web-canary** | ⬜️ Ignored ([Inspect](https://vercel.com/cal/calcom-web-canary/ErNSuvG6hoGtExTSY7qzcg3NYZid)) |  |  | Apr 21, 2025 7:42am |
</details>



author_association: NONE
user: login: graphite-app[bot]
created_at: 2025-04-21T07:45:11Z
body: ### Graphite Automations
<details><summary><a href="https://app.graphite.dev/automations/?org=calcom&ruleId=qoTwHtX8MnP39HKxIt4V&ref=gt-automation-github-comment">"Add consumer team as reviewer"</a> took an action on this PR • (04/21/25)</summary><p>1 reviewer was added to this PR based on Keith Williams's automation.</p></details>
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-04-21T08:07:46Z
body: <!-- GENERATED-E2E-RESULTS -->
## E2E results are ready!
- [Workflow #57395.1 latest results](https://calcom.github.io/test-results/reports/lauris/cal-5548-batch-1/14569660562/1)
author_association: CONTRIBUTOR