fix: v2 Sentry errors (#20788)
* fix: Only Team Event Types are supported for Instant Meeting * refactor: Cannot read properties of undefined (reading 'timeZone') * fix: Cannot read properties of undefined (reading 'map') * refactor: rearrange function order * refactor: booking_seats_full_error * fix: invalid_round_robin_host * refactor: booking_time_out_of_bounds_error * refactor: Attempting to book a meeting in the past. * fix: obj.phoneNumber.trim is not a function * fix: Cannot use 'in' operator to search for 'disabled' in 1
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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<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>;
|
||||
|
||||
@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<string, unknown>;
|
||||
@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 {
|
||||
|
||||
+38
-3
@@ -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",
|
||||
|
||||
+3
-3
@@ -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`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+3
-3
@@ -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`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
@@ -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()
|
||||
|
||||
+1
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user