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 7a2d375d28..8e4244c2fb 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`); + } + + 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; } - - 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 || ""); } 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 2d59fdd640..97c335f356 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 d4321e13bf..87d4a4e626 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 0e1887c81d..b584d66101 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) - @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.`, + @ApiProperty({ + type: Attendee, + description: "The attendee's details.", }) - lengthInMinutes?: number; + @ValidateNested() + @Type(() => Attendee) + attendee!: Attendee; + + @ApiPropertyOptional({ + 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, + }) + @IsObject() + @IsOptional() + bookingFieldsResponses?: Record; @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; - @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, - }) - @IsObject() @IsOptional() - bookingFieldsResponses?: Record; + @IsInt() + @Min(1) + @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.`, + }) + 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 44315a1208..3664e52943 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 7203feb02d..f3eb2f1b7c 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 13ad1ef03d..f521eb50ee 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 be7db511ef..cf67e2168b 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 e883b9d333..4a45d0cc83 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()