feat: api v2 event type settings - booker bookings limit, emails, round robin reschedule (#24169)
* feat: event type bookerActiveBookingsLimit * test: booking with bookerActiveBookingsLimit * fix: typecheck * feat: org team event type emailSettings & rescheduleWithSameRoundRobinHost * docs: regenerate swagger * chore: default booker name and email in examples app * refactor: email settings description * refactor: rename email setting properties * refactor: function to add email settings to metadata * fix: try to fix e2e * fix: dont return input bookerActiveBookingsLimit in transformed object * fix: dont allow recurrence and bookerActiveBookingsLimit * fix: output service return bookerActiveBookingsLimit
This commit is contained in:
@@ -2494,7 +2494,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
.expect(400);
|
||||
|
||||
expect(response.body.error.message).toEqual(
|
||||
`Missing attendee phone number - it is required by the event type. Pass it as \"attendee.phoneNumber\" string in the request.`
|
||||
`Missing attendee phone number - it is required by the event type. Pass it as "attendee.phoneNumber" string in the request.`
|
||||
);
|
||||
});
|
||||
|
||||
@@ -3026,13 +3026,154 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("event type with max bookings count per booker", () => {
|
||||
it("should not allow booking more than maximumActiveBookings per attendee", async () => {
|
||||
const eventTypeIdWithMaxBookerBookings = await eventTypesRepositoryFixture.create(
|
||||
{
|
||||
slug: `max-bookings-count-per-booker-${randomString(10)}`,
|
||||
length: 60,
|
||||
title: "Event Type with max bookings count per booker",
|
||||
maxActiveBookingsPerBooker: 1,
|
||||
},
|
||||
user.id
|
||||
);
|
||||
|
||||
const body: CreateBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2040, 0, 13, 10, 0, 0)).toISOString(),
|
||||
eventTypeId: eventTypeIdWithMaxBookerBookings.id,
|
||||
attendee: {
|
||||
name: "alice",
|
||||
email: "alice@gmail.com",
|
||||
timeZone: "Europe/Rome",
|
||||
language: "it",
|
||||
},
|
||||
};
|
||||
|
||||
const response = await request(app.getHttpServer())
|
||||
.post("/v2/bookings")
|
||||
.send(body)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(201);
|
||||
|
||||
const responseBody: CreateBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseDataIsBooking(responseBody.data)).toBe(true);
|
||||
|
||||
if (responseDataIsBooking(responseBody.data)) {
|
||||
const data: BookingOutput_2024_08_13 = responseBody.data;
|
||||
expect(data.id).toBeDefined();
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected booking but received array of possibly recurring bookings"
|
||||
);
|
||||
}
|
||||
|
||||
const response2 = await request(app.getHttpServer())
|
||||
.post("/v2/bookings")
|
||||
.send(body)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(400);
|
||||
|
||||
expect(response2.body.error.message).toBe(
|
||||
"Attendee with this email can't book because the maximum number of active bookings has been reached."
|
||||
);
|
||||
|
||||
const body2: CreateBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2040, 0, 13, 12, 0, 0)).toISOString(),
|
||||
eventTypeId: eventTypeIdWithMaxBookerBookings.id,
|
||||
attendee: {
|
||||
name: "bob",
|
||||
email: "bob@gmail.com",
|
||||
timeZone: "Europe/Rome",
|
||||
language: "it",
|
||||
},
|
||||
};
|
||||
|
||||
const response3 = await request(app.getHttpServer())
|
||||
.post("/v2/bookings")
|
||||
.send(body2)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(201);
|
||||
|
||||
const responseBody2: CreateBookingOutput_2024_08_13 = response3.body;
|
||||
expect(responseBody2.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody2.data).toBeDefined();
|
||||
expect(responseDataIsBooking(responseBody2.data)).toBe(true);
|
||||
|
||||
if (responseDataIsBooking(responseBody2.data)) {
|
||||
const data: BookingOutput_2024_08_13 = responseBody2.data;
|
||||
expect(data.id).toBeDefined();
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected booking but received array of possibly recurring bookings"
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("should not allow booking more than maximumActiveBookings per attendee and offer rescheduling", async () => {
|
||||
const eventTypeIdWithMaxBookerBookings = await eventTypesRepositoryFixture.create(
|
||||
{
|
||||
slug: `max-bookings-count-per-booker-with-reschedule-${randomString(10)}`,
|
||||
length: 60,
|
||||
title: "Event Type with max bookings count per booker with reschedule",
|
||||
maxActiveBookingsPerBooker: 1,
|
||||
maxActiveBookingPerBookerOfferReschedule: true,
|
||||
},
|
||||
user.id
|
||||
);
|
||||
|
||||
const body: CreateBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2040, 0, 13, 14, 0, 0)).toISOString(),
|
||||
eventTypeId: eventTypeIdWithMaxBookerBookings.id,
|
||||
attendee: {
|
||||
name: "charlie",
|
||||
email: "charlie@gmail.com",
|
||||
timeZone: "Europe/Rome",
|
||||
language: "it",
|
||||
},
|
||||
};
|
||||
|
||||
const response = await request(app.getHttpServer())
|
||||
.post("/v2/bookings")
|
||||
.send(body)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(201);
|
||||
|
||||
const responseBody: CreateBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseDataIsBooking(responseBody.data)).toBe(true);
|
||||
|
||||
if (responseDataIsBooking(responseBody.data)) {
|
||||
const data: BookingOutput_2024_08_13 = responseBody.data;
|
||||
expect(data.id).toBeDefined();
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected booking but received array of possibly recurring bookings"
|
||||
);
|
||||
}
|
||||
|
||||
const response2 = await request(app.getHttpServer())
|
||||
.post("/v2/bookings")
|
||||
.send(body)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(400);
|
||||
|
||||
expect(response2.body.error.message).toBe(
|
||||
`Attendee with this email can't book because the maximum number of active bookings has been reached. You can reschedule your existing booking (${responseBody.data.uid}) to a new timeslot instead.`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function responseDataIsRecurranceBooking(data: any): data is RecurringBookingOutput_2024_08_13 {
|
||||
function responseDataIsRecurranceBooking(data: unknown): data is RecurringBookingOutput_2024_08_13 {
|
||||
return (
|
||||
!Array.isArray(data) &&
|
||||
typeof data === "object" &&
|
||||
data !== null &&
|
||||
data &&
|
||||
"id" in data &&
|
||||
"recurringBookingUid" in data
|
||||
@@ -3048,11 +3189,11 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
});
|
||||
});
|
||||
|
||||
function responseDataIsBooking(data: any): data is BookingOutput_2024_08_13 {
|
||||
return !Array.isArray(data) && typeof data === "object" && data && "id" in data;
|
||||
function responseDataIsBooking(data: unknown): data is BookingOutput_2024_08_13 {
|
||||
return !Array.isArray(data) && typeof data === "object" && data !== null && data && "id" in data;
|
||||
}
|
||||
|
||||
function responseDataIsRecurringBooking(data: any): data is RecurringBookingOutput_2024_08_13[] {
|
||||
function responseDataIsRecurringBooking(data: unknown): data is RecurringBookingOutput_2024_08_13[] {
|
||||
return Array.isArray(data);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -48,6 +48,19 @@ export class ErrorsBookingsService_2024_08_13 {
|
||||
throw new BadRequestException("Attempting to book a meeting in the past.");
|
||||
} else if (error.message === "hosts_unavailable_for_booking") {
|
||||
throw new BadRequestException(hostsUnavaile);
|
||||
} else if (error.message === "booker_limit_exceeded_error") {
|
||||
throw new BadRequestException(
|
||||
"Attendee with this email can't book because the maximum number of active bookings has been reached."
|
||||
);
|
||||
} else if (error.message === "booker_limit_exceeded_error_reschedule") {
|
||||
const errorData =
|
||||
"data" in error ? (error.data as { rescheduleUid: string }) : { rescheduleUid: undefined };
|
||||
let message =
|
||||
"Attendee with this email can't book because the maximum number of active bookings has been reached.";
|
||||
if (errorData?.rescheduleUid) {
|
||||
message += ` You can reschedule your existing booking (${errorData.rescheduleUid}) to a new timeslot instead.`;
|
||||
}
|
||||
throw new BadRequestException(message);
|
||||
}
|
||||
}
|
||||
throw error;
|
||||
|
||||
+182
-9
@@ -31,16 +31,18 @@ import {
|
||||
FrequencyInput,
|
||||
} from "@calcom/platform-enums";
|
||||
import { SchedulingType } from "@calcom/platform-libraries";
|
||||
import type {
|
||||
ApiSuccessResponse,
|
||||
CreateEventTypeInput_2024_06_14,
|
||||
EventTypeOutput_2024_06_14,
|
||||
GuestsDefaultFieldOutput_2024_06_14,
|
||||
NameDefaultFieldInput_2024_06_14,
|
||||
NotesDefaultFieldInput_2024_06_14,
|
||||
SplitNameDefaultFieldOutput_2024_06_14,
|
||||
UpdateEventTypeInput_2024_06_14,
|
||||
import {
|
||||
type ApiSuccessResponse,
|
||||
type CreateEventTypeInput_2024_06_14,
|
||||
type EventTypeOutput_2024_06_14,
|
||||
type GuestsDefaultFieldOutput_2024_06_14,
|
||||
type NameDefaultFieldInput_2024_06_14,
|
||||
type NotesDefaultFieldInput_2024_06_14,
|
||||
type SplitNameDefaultFieldOutput_2024_06_14,
|
||||
type UpdateEventTypeInput_2024_06_14,
|
||||
} from "@calcom/platform-types";
|
||||
import { FAILED_RECURRING_EVENT_TYPE_WITH_BOOKER_LIMITS_ERROR_MESSAGE } from "@calcom/platform-types/event-types/event-types_2024_06_14/inputs/validators/CantHaveRecurrenceAndBookerActiveBookingsLimit";
|
||||
import { REQUIRES_AT_LEAST_ONE_PROPERTY_ERROR } from "@calcom/platform-types/utils/RequiresOneOfPropertiesWhenNotDisabled";
|
||||
import type { PlatformOAuthClient, Team, User, Schedule, EventType } from "@calcom/prisma/client";
|
||||
|
||||
const orderBySlug = (a: { slug: string }, b: { slug: string }) => {
|
||||
@@ -1442,6 +1444,177 @@ describe("Event types Endpoints", () => {
|
||||
.expect(200);
|
||||
});
|
||||
|
||||
describe("bookerActiveBookingsLimit", () => {
|
||||
describe("negative tests", () => {
|
||||
it("should not create an event type with bookerActiveBookingsLimit and recurrence", async () => {
|
||||
const body: CreateEventTypeInput_2024_06_14 = {
|
||||
title: "Coding class with bookerActiveBookingsLimit",
|
||||
slug: "coding-class-booker-active-bookings-limit",
|
||||
description: "Let's learn how to code like a pro.",
|
||||
lengthInMinutes: 60,
|
||||
bookerActiveBookingsLimit: {
|
||||
maximumActiveBookings: 2,
|
||||
offerReschedule: true,
|
||||
},
|
||||
recurrence: {
|
||||
frequency: FrequencyInput.weekly,
|
||||
interval: 2,
|
||||
occurrences: 10,
|
||||
},
|
||||
};
|
||||
|
||||
const response = await request(app.getHttpServer())
|
||||
.post("/api/v2/event-types")
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||
.set("Authorization", `Bearer ${apiKeyString}`)
|
||||
.send(body)
|
||||
.expect(400);
|
||||
|
||||
expect(
|
||||
response.body.error.message.includes(FAILED_RECURRING_EVENT_TYPE_WITH_BOOKER_LIMITS_ERROR_MESSAGE)
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("should not allow creating an event type with bookerActiveBookingsLimit disabled:false", async () => {
|
||||
const body: CreateEventTypeInput_2024_06_14 = {
|
||||
title: "Coding class with bookerActiveBookingsLimit disabled:false",
|
||||
slug: "coding-class-booker-active-bookings-limit-disabled",
|
||||
description: "Let's learn how to code like a pro.",
|
||||
lengthInMinutes: 60,
|
||||
// note(Lauris): disabled false means that it is enabled so it should have maximumActiveBookings and / or offerReschedule provided
|
||||
bookerActiveBookingsLimit: {
|
||||
disabled: false,
|
||||
},
|
||||
};
|
||||
|
||||
const response = await request(app.getHttpServer())
|
||||
.post("/api/v2/event-types")
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||
.set("Authorization", `Bearer ${apiKeyString}`)
|
||||
.send(body)
|
||||
.expect(400);
|
||||
|
||||
expect(response.body.error.message.includes(REQUIRES_AT_LEAST_ONE_PROPERTY_ERROR)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("positive tests", () => {
|
||||
let eventTypeWithBookerActiveBookingsLimitId: number;
|
||||
|
||||
it("should create an event type with bookerActiveBookingsLimit", async () => {
|
||||
const body: CreateEventTypeInput_2024_06_14 = {
|
||||
title: "Coding class with bookerActiveBookingsLimit",
|
||||
slug: "coding-class-booker-active-bookings-limit",
|
||||
description: "Let's learn how to code like a pro.",
|
||||
lengthInMinutes: 60,
|
||||
bookerActiveBookingsLimit: {
|
||||
maximumActiveBookings: 2,
|
||||
offerReschedule: true,
|
||||
},
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post("/api/v2/event-types")
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||
.set("Authorization", `Bearer ${apiKeyString}`)
|
||||
.send(body)
|
||||
.expect(201)
|
||||
.then(async (response) => {
|
||||
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
|
||||
const createdEventType = responseBody.data;
|
||||
expect(createdEventType).toHaveProperty("id");
|
||||
expect(createdEventType.title).toEqual(body.title);
|
||||
expect(createdEventType.bookerActiveBookingsLimit).toEqual(body.bookerActiveBookingsLimit);
|
||||
eventTypeWithBookerActiveBookingsLimitId = createdEventType.id;
|
||||
});
|
||||
});
|
||||
|
||||
it("should update an event type with bookerActiveBookingsLimit", async () => {
|
||||
const body: UpdateEventTypeInput_2024_06_14 = {
|
||||
bookerActiveBookingsLimit: {
|
||||
disabled: true,
|
||||
},
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.patch(`/api/v2/event-types/${eventTypeWithBookerActiveBookingsLimitId}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||
.set("Authorization", `Bearer ${apiKeyString}`)
|
||||
.send(body)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
|
||||
const updatedEventType = responseBody.data;
|
||||
expect(updatedEventType.bookerActiveBookingsLimit).toEqual(body.bookerActiveBookingsLimit);
|
||||
eventTypeWithBookerActiveBookingsLimitId = updatedEventType.id;
|
||||
});
|
||||
});
|
||||
|
||||
it("should create an event type with bookerActiveBookingsLimit and recurrence disabled", async () => {
|
||||
const body: CreateEventTypeInput_2024_06_14 = {
|
||||
title: "Coding class with bookerActiveBookingsLimit and recurrence disabled",
|
||||
slug: "coding-class-booker-active-bookings-limit-recurrence-disabled",
|
||||
description: "Let's learn how to code like a pro.",
|
||||
lengthInMinutes: 60,
|
||||
bookerActiveBookingsLimit: {
|
||||
maximumActiveBookings: 2,
|
||||
offerReschedule: true,
|
||||
},
|
||||
recurrence: {
|
||||
disabled: true,
|
||||
},
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post("/api/v2/event-types")
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||
.set("Authorization", `Bearer ${apiKeyString}`)
|
||||
.send(body)
|
||||
.expect(201)
|
||||
.then(async (response) => {
|
||||
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
|
||||
const createdEventType = responseBody.data;
|
||||
expect(createdEventType).toHaveProperty("id");
|
||||
expect(createdEventType.title).toEqual(body.title);
|
||||
expect(createdEventType.bookerActiveBookingsLimit).toEqual(body.bookerActiveBookingsLimit);
|
||||
eventTypeWithBookerActiveBookingsLimitId = createdEventType.id;
|
||||
});
|
||||
});
|
||||
|
||||
it("should create an event type with recurrence and bookerActiveBookingsLimit disabled", async () => {
|
||||
const body: CreateEventTypeInput_2024_06_14 = {
|
||||
title: "Coding class with bookerActiveBookingsLimit disabled and recurrence",
|
||||
slug: "coding-class-booker-active-bookings-limit-disabled-and-recurrence",
|
||||
description: "Let's learn how to code like a pro.",
|
||||
lengthInMinutes: 60,
|
||||
bookerActiveBookingsLimit: {
|
||||
disabled: true,
|
||||
},
|
||||
recurrence: {
|
||||
frequency: FrequencyInput.weekly,
|
||||
interval: 2,
|
||||
occurrences: 10,
|
||||
},
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post("/api/v2/event-types")
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||
.set("Authorization", `Bearer ${apiKeyString}`)
|
||||
.send(body)
|
||||
.expect(201)
|
||||
.then(async (response) => {
|
||||
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
|
||||
const createdEventType = responseBody.data;
|
||||
expect(createdEventType).toHaveProperty("id");
|
||||
expect(createdEventType.title).toEqual(body.title);
|
||||
expect(createdEventType.bookerActiveBookingsLimit).toEqual(body.bookerActiveBookingsLimit);
|
||||
eventTypeWithBookerActiveBookingsLimitId = createdEventType.id;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await oauthClientRepositoryFixture.delete(oAuthClient.id);
|
||||
await teamRepositoryFixture.delete(organization.id);
|
||||
|
||||
+65
-25
@@ -28,7 +28,11 @@ import { UserWithProfile } from "@/modules/users/users.repository";
|
||||
import { Injectable, BadRequestException } from "@nestjs/common";
|
||||
|
||||
import { getApps, getUsersCredentialsIncludeServiceAccountKey } from "@calcom/platform-libraries/app-store";
|
||||
import { validateCustomEventName, EventTypeMetaDataSchema } from "@calcom/platform-libraries/event-types";
|
||||
import {
|
||||
validateCustomEventName,
|
||||
EventTypeMetaDataSchema,
|
||||
EventTypeMetadata,
|
||||
} from "@calcom/platform-libraries/event-types";
|
||||
import {
|
||||
CreateEventTypeInput_2024_06_14,
|
||||
DestinationCalendar_2024_06_14,
|
||||
@@ -67,11 +71,13 @@ export class InputEventTypesService_2024_06_14 {
|
||||
eventName: transformedBody.eventName,
|
||||
});
|
||||
|
||||
transformedBody.destinationCalendar &&
|
||||
(await this.validateInputDestinationCalendar(user.id, transformedBody.destinationCalendar));
|
||||
if (transformedBody.destinationCalendar) {
|
||||
await this.validateInputDestinationCalendar(user.id, transformedBody.destinationCalendar);
|
||||
}
|
||||
|
||||
transformedBody.useEventTypeDestinationCalendarEmail &&
|
||||
(await this.validateInputUseDestinationCalendarEmail(user.id));
|
||||
if (transformedBody.useEventTypeDestinationCalendarEmail) {
|
||||
await this.validateInputUseDestinationCalendarEmail(user.id);
|
||||
}
|
||||
|
||||
return transformedBody;
|
||||
}
|
||||
@@ -93,11 +99,13 @@ export class InputEventTypesService_2024_06_14 {
|
||||
eventName: transformedBody.eventName,
|
||||
});
|
||||
|
||||
transformedBody.destinationCalendar &&
|
||||
(await this.validateInputDestinationCalendar(user.id, transformedBody.destinationCalendar));
|
||||
if (transformedBody.destinationCalendar) {
|
||||
await this.validateInputDestinationCalendar(user.id, transformedBody.destinationCalendar);
|
||||
}
|
||||
|
||||
transformedBody.useEventTypeDestinationCalendarEmail &&
|
||||
(await this.validateInputUseDestinationCalendarEmail(user.id));
|
||||
if (transformedBody.useEventTypeDestinationCalendarEmail) {
|
||||
await this.validateInputUseDestinationCalendarEmail(user.id);
|
||||
}
|
||||
|
||||
return transformedBody;
|
||||
}
|
||||
@@ -119,6 +127,7 @@ export class InputEventTypesService_2024_06_14 {
|
||||
customName,
|
||||
useDestinationCalendarEmail,
|
||||
disableGuests,
|
||||
bookerActiveBookingsLimit,
|
||||
...rest
|
||||
} = inputEventType;
|
||||
const confirmationPolicyTransformed = this.transformInputConfirmationPolicy(confirmationPolicy);
|
||||
@@ -130,6 +139,17 @@ export class InputEventTypesService_2024_06_14 {
|
||||
? this.getBookingFieldsWithGuestsToggled(bookingFields, disableGuests)
|
||||
: bookingFields;
|
||||
|
||||
const maxActiveBookingsPerBooker = bookerActiveBookingsLimit
|
||||
? this.transformInputBookerActiveBookingsLimit(bookerActiveBookingsLimit)
|
||||
: {};
|
||||
|
||||
const metadata: EventTypeMetadata = {
|
||||
bookerLayouts: this.transformInputBookerLayouts(bookerLayouts),
|
||||
requiresConfirmationThreshold:
|
||||
confirmationPolicyTransformed?.requiresConfirmationThreshold ?? undefined,
|
||||
multipleDuration: lengthInMinutesOptions,
|
||||
};
|
||||
|
||||
const eventType = {
|
||||
...rest,
|
||||
length: lengthInMinutes,
|
||||
@@ -140,12 +160,7 @@ export class InputEventTypesService_2024_06_14 {
|
||||
? this.transformInputIntervalLimits(bookingLimitsDuration)
|
||||
: undefined,
|
||||
...this.transformInputBookingWindow(bookingWindow),
|
||||
metadata: {
|
||||
bookerLayouts: this.transformInputBookerLayouts(bookerLayouts),
|
||||
requiresConfirmationThreshold:
|
||||
confirmationPolicyTransformed?.requiresConfirmationThreshold ?? undefined,
|
||||
multipleDuration: lengthInMinutesOptions,
|
||||
},
|
||||
metadata,
|
||||
requiresConfirmation: confirmationPolicyTransformed?.requiresConfirmation ?? undefined,
|
||||
requiresConfirmationWillBlockSlot:
|
||||
confirmationPolicyTransformed?.requiresConfirmationWillBlockSlot ?? undefined,
|
||||
@@ -154,11 +169,27 @@ export class InputEventTypesService_2024_06_14 {
|
||||
...this.transformInputSeatOptions(seats),
|
||||
eventName: customName,
|
||||
useEventTypeDestinationCalendarEmail: useDestinationCalendarEmail,
|
||||
...maxActiveBookingsPerBooker,
|
||||
};
|
||||
|
||||
return eventType;
|
||||
}
|
||||
|
||||
transformInputBookerActiveBookingsLimit(
|
||||
bookerActiveBookingsLimit: CreateEventTypeInput_2024_06_14["bookerActiveBookingsLimit"]
|
||||
) {
|
||||
if (!bookerActiveBookingsLimit || bookerActiveBookingsLimit.disabled) {
|
||||
return {
|
||||
maxActiveBookingsPerBooker: null,
|
||||
maxActiveBookingPerBookerOfferReschedule: false,
|
||||
};
|
||||
}
|
||||
return {
|
||||
maxActiveBookingsPerBooker: bookerActiveBookingsLimit?.maximumActiveBookings,
|
||||
maxActiveBookingPerBookerOfferReschedule: bookerActiveBookingsLimit?.offerReschedule,
|
||||
};
|
||||
}
|
||||
|
||||
async transformInputUpdateEventType(inputEventType: UpdateEventTypeInput_2024_06_14, eventTypeId: number) {
|
||||
const {
|
||||
lengthInMinutes,
|
||||
@@ -176,10 +207,11 @@ export class InputEventTypesService_2024_06_14 {
|
||||
customName,
|
||||
useDestinationCalendarEmail,
|
||||
disableGuests,
|
||||
bookerActiveBookingsLimit,
|
||||
...rest
|
||||
} = inputEventType;
|
||||
const eventTypeDb = await this.eventTypesRepository.getEventTypeWithMetaData(eventTypeId);
|
||||
const metadataTransformed = !!eventTypeDb?.metadata
|
||||
const metadataTransformed = eventTypeDb?.metadata
|
||||
? EventTypeMetaDataSchema.parse(eventTypeDb.metadata)
|
||||
: {};
|
||||
|
||||
@@ -190,6 +222,18 @@ export class InputEventTypesService_2024_06_14 {
|
||||
? this.getBookingFieldsWithGuestsToggled(bookingFields, disableGuests)
|
||||
: bookingFields;
|
||||
|
||||
const maxActiveBookingsPerBooker = bookerActiveBookingsLimit
|
||||
? this.transformInputBookerActiveBookingsLimit(bookerActiveBookingsLimit)
|
||||
: {};
|
||||
|
||||
const metadata: EventTypeMetadata = {
|
||||
...metadataTransformed,
|
||||
bookerLayouts: this.transformInputBookerLayouts(bookerLayouts),
|
||||
requiresConfirmationThreshold:
|
||||
confirmationPolicyTransformed?.requiresConfirmationThreshold ?? undefined,
|
||||
multipleDuration: lengthInMinutesOptions,
|
||||
};
|
||||
|
||||
const eventType = {
|
||||
...rest,
|
||||
length: lengthInMinutes,
|
||||
@@ -202,13 +246,7 @@ export class InputEventTypesService_2024_06_14 {
|
||||
? this.transformInputIntervalLimits(bookingLimitsDuration)
|
||||
: undefined,
|
||||
...this.transformInputBookingWindow(bookingWindow),
|
||||
metadata: {
|
||||
...metadataTransformed,
|
||||
bookerLayouts: this.transformInputBookerLayouts(bookerLayouts),
|
||||
requiresConfirmationThreshold:
|
||||
confirmationPolicyTransformed?.requiresConfirmationThreshold ?? undefined,
|
||||
multipleDuration: lengthInMinutesOptions,
|
||||
},
|
||||
metadata,
|
||||
recurringEvent: recurrence ? this.transformInputRecurrignEvent(recurrence) : undefined,
|
||||
requiresConfirmation: confirmationPolicyTransformed?.requiresConfirmation ?? undefined,
|
||||
requiresConfirmationWillBlockSlot:
|
||||
@@ -217,6 +255,7 @@ export class InputEventTypesService_2024_06_14 {
|
||||
...this.transformInputSeatOptions(seats),
|
||||
eventName: customName,
|
||||
useEventTypeDestinationCalendarEmail: useDestinationCalendarEmail,
|
||||
...maxActiveBookingsPerBooker,
|
||||
};
|
||||
|
||||
return eventType;
|
||||
@@ -319,7 +358,7 @@ export class InputEventTypesService_2024_06_14 {
|
||||
|
||||
transformInputBookingWindow(inputBookingWindow: CreateEventTypeInput_2024_06_14["bookingWindow"]) {
|
||||
const res = transformFutureBookingLimitsApiToInternal(inputBookingWindow);
|
||||
return !!res ? res : {};
|
||||
return res ? res : {};
|
||||
}
|
||||
|
||||
transformInputBookerLayouts(inputBookerLayouts: CreateEventTypeInput_2024_06_14["bookerLayouts"]) {
|
||||
@@ -369,7 +408,7 @@ export class InputEventTypesService_2024_06_14 {
|
||||
requiresConfirmationDb = eventTypeDb?.requiresConfirmation ?? false;
|
||||
}
|
||||
|
||||
const seatsPerTimeSlotFinal = !!seatsPerTimeSlot ? seatsPerTimeSlot : seatsPerTimeSlotDb;
|
||||
const seatsPerTimeSlotFinal = seatsPerTimeSlot ? seatsPerTimeSlot : seatsPerTimeSlotDb;
|
||||
const seatsEnabledFinal = !!seatsPerTimeSlotFinal && seatsPerTimeSlotFinal > 0;
|
||||
|
||||
const locationsFinal = locations !== undefined ? locations : locationsDb;
|
||||
@@ -394,6 +433,7 @@ export class InputEventTypesService_2024_06_14 {
|
||||
);
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
transformLocations(locations: any) {
|
||||
if (!locations) return [];
|
||||
|
||||
|
||||
+28
@@ -92,6 +92,8 @@ type Input = Pick<
|
||||
| "calVideoSettings"
|
||||
| "hidden"
|
||||
| "bookingRequiresAuthentication"
|
||||
| "maxActiveBookingsPerBooker"
|
||||
| "maxActiveBookingPerBookerOfferReschedule"
|
||||
>;
|
||||
|
||||
@Injectable()
|
||||
@@ -165,6 +167,7 @@ export class OutputEventTypesService_2024_06_14 {
|
||||
periodEndDate: databaseEventType.periodEndDate,
|
||||
} as TransformFutureBookingsLimitSchema_2024_06_14);
|
||||
const destinationCalendar = this.transformDestinationCalendar(databaseEventType.destinationCalendar);
|
||||
const bookerActiveBookingsLimit = this.transformBookerActiveBookingsLimit(databaseEventType);
|
||||
|
||||
return {
|
||||
id,
|
||||
@@ -210,9 +213,29 @@ export class OutputEventTypesService_2024_06_14 {
|
||||
calVideoSettings,
|
||||
hidden,
|
||||
bookingRequiresAuthentication,
|
||||
bookerActiveBookingsLimit,
|
||||
};
|
||||
}
|
||||
|
||||
transformBookerActiveBookingsLimit(databaseEventType: Input) {
|
||||
const noMaxActiveBookingsPerBooker =
|
||||
!databaseEventType.maxActiveBookingsPerBooker && databaseEventType.maxActiveBookingsPerBooker !== 0;
|
||||
const noMaxActiveBookingPerBookerOfferReschedule =
|
||||
!databaseEventType.maxActiveBookingPerBookerOfferReschedule;
|
||||
|
||||
if (noMaxActiveBookingsPerBooker && noMaxActiveBookingPerBookerOfferReschedule) {
|
||||
return {
|
||||
disabled: true,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
maximumActiveBookings: databaseEventType.maxActiveBookingsPerBooker ?? undefined,
|
||||
offerReschedule: databaseEventType.maxActiveBookingPerBookerOfferReschedule ?? undefined,
|
||||
};
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
transformLocations(locationDb: any) {
|
||||
if (!locationDb) return [];
|
||||
|
||||
@@ -239,6 +262,7 @@ export class OutputEventTypesService_2024_06_14 {
|
||||
};
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
transformBookingFields(bookingFields: any) {
|
||||
if (!bookingFields) return [];
|
||||
|
||||
@@ -273,6 +297,7 @@ export class OutputEventTypesService_2024_06_14 {
|
||||
return this.transformBookingFields(defaultBookingFields);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
transformRecurringEvent(recurringEvent: any) {
|
||||
if (!recurringEvent) return null;
|
||||
const recurringEventParsed = parseRecurringEvent(recurringEvent);
|
||||
@@ -280,6 +305,7 @@ export class OutputEventTypesService_2024_06_14 {
|
||||
return transformRecurrenceInternalToApi(recurringEventParsed);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
transformMetadata(metadata: any) {
|
||||
if (!metadata) return {};
|
||||
return EventTypeMetaDataSchema.parse(metadata);
|
||||
@@ -301,6 +327,7 @@ export class OutputEventTypesService_2024_06_14 {
|
||||
});
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
transformIntervalLimits(bookingLimits: any) {
|
||||
const bookingLimitsParsed = parseBookingLimit(bookingLimits);
|
||||
return transformIntervalLimitsInternalToApi(bookingLimitsParsed);
|
||||
@@ -327,6 +354,7 @@ export class OutputEventTypesService_2024_06_14 {
|
||||
);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
transformEventTypeColor(eventTypeColor: any) {
|
||||
if (!eventTypeColor) return undefined;
|
||||
const parsedeventTypeColor = parseEventTypeColor(eventTypeColor);
|
||||
|
||||
+2
@@ -38,6 +38,8 @@ export type InputEventTransformed_2024_06_14 = Omit<
|
||||
bookingFields?: ReturnType<typeof transformBookingFieldsApiToInternal>;
|
||||
durationLimits?: ReturnType<typeof transformIntervalLimitsApiToInternal>;
|
||||
recurringEvent?: ReturnType<typeof transformRecurrenceApiToInternal>;
|
||||
maxActiveBookingsPerBooker?: number;
|
||||
maxActiveBookingPerBookerOfferReschedule?: boolean;
|
||||
eventTypeColor?: ReturnType<typeof transformEventColorsApiToInternal>;
|
||||
useEventTypeDestinationCalendarEmail?: boolean;
|
||||
} & Partial<
|
||||
|
||||
+13
@@ -355,6 +355,11 @@ describe("Organizations Event Types Endpoints", () => {
|
||||
darkThemeHex: "#292929",
|
||||
lightThemeHex: "#fafafa",
|
||||
},
|
||||
emailSettings: {
|
||||
disableEmailsToAttendees: true,
|
||||
disableEmailsToHosts: true,
|
||||
},
|
||||
rescheduleWithSameRoundRobinHost: true,
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
@@ -385,6 +390,7 @@ describe("Organizations Event Types Endpoints", () => {
|
||||
expect(data.lockTimeZoneToggleOnBookingPage).toEqual(body.lockTimeZoneToggleOnBookingPage);
|
||||
expect(data.color).toEqual(body.color);
|
||||
expect(data.successRedirectUrl).toEqual("https://masterchief.com/argentina/flan/video/1234");
|
||||
expect(data.emailSettings).toEqual(body.emailSettings);
|
||||
collectiveEventType = responseBody.data;
|
||||
});
|
||||
});
|
||||
@@ -600,6 +606,11 @@ describe("Organizations Event Types Endpoints", () => {
|
||||
const body: UpdateTeamEventTypeInput_2024_06_14 = {
|
||||
hosts: newHosts,
|
||||
successRedirectUrl: "https://new-url-success.com",
|
||||
emailSettings: {
|
||||
disableEmailsToAttendees: false,
|
||||
disableEmailsToHosts: false,
|
||||
},
|
||||
rescheduleWithSameRoundRobinHost: false,
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
@@ -614,6 +625,8 @@ describe("Organizations Event Types Endpoints", () => {
|
||||
expect(eventType.successRedirectUrl).toEqual("https://new-url-success.com");
|
||||
expect(eventType.title).toEqual(collectiveEventType.title);
|
||||
expect(eventType.hosts.length).toEqual(1);
|
||||
expect(eventType.emailSettings).toEqual(body.emailSettings);
|
||||
expect(eventType.rescheduleWithSameRoundRobinHost).toEqual(body.rescheduleWithSameRoundRobinHost);
|
||||
evaluateHost(eventType.hosts[0], newHosts[0]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,10 +8,12 @@ import { UsersRepository } from "@/modules/users/users.repository";
|
||||
import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common";
|
||||
|
||||
import { SchedulingType } from "@calcom/platform-libraries";
|
||||
import { EventTypeMetadata } from "@calcom/platform-libraries/event-types";
|
||||
import {
|
||||
CreateTeamEventTypeInput_2024_06_14,
|
||||
UpdateTeamEventTypeInput_2024_06_14,
|
||||
HostPriority,
|
||||
EmailSettings_2024_06_14,
|
||||
} from "@calcom/platform-types";
|
||||
|
||||
export type TransformedCreateTeamEventTypeInput = Awaited<
|
||||
@@ -49,14 +51,16 @@ export class InputOrganizationsEventTypesService {
|
||||
eventName: transformedBody.eventName,
|
||||
});
|
||||
|
||||
transformedBody.destinationCalendar &&
|
||||
(await this.inputEventTypesService.validateInputDestinationCalendar(
|
||||
if (transformedBody.destinationCalendar) {
|
||||
await this.inputEventTypesService.validateInputDestinationCalendar(
|
||||
userId,
|
||||
transformedBody.destinationCalendar
|
||||
));
|
||||
);
|
||||
}
|
||||
|
||||
transformedBody.useEventTypeDestinationCalendarEmail &&
|
||||
(await this.inputEventTypesService.validateInputUseDestinationCalendarEmail(userId));
|
||||
if (transformedBody.useEventTypeDestinationCalendarEmail) {
|
||||
await this.inputEventTypesService.validateInputUseDestinationCalendarEmail(userId);
|
||||
}
|
||||
|
||||
return transformedBody;
|
||||
}
|
||||
@@ -83,14 +87,16 @@ export class InputOrganizationsEventTypesService {
|
||||
eventName: transformedBody.eventName,
|
||||
});
|
||||
|
||||
transformedBody.destinationCalendar &&
|
||||
(await this.inputEventTypesService.validateInputDestinationCalendar(
|
||||
if (transformedBody.destinationCalendar) {
|
||||
await this.inputEventTypesService.validateInputDestinationCalendar(
|
||||
userId,
|
||||
transformedBody.destinationCalendar
|
||||
));
|
||||
);
|
||||
}
|
||||
|
||||
transformedBody.useEventTypeDestinationCalendarEmail &&
|
||||
(await this.inputEventTypesService.validateInputUseDestinationCalendarEmail(userId));
|
||||
if (transformedBody.useEventTypeDestinationCalendarEmail) {
|
||||
await this.inputEventTypesService.validateInputUseDestinationCalendarEmail(userId);
|
||||
}
|
||||
|
||||
return transformedBody;
|
||||
}
|
||||
@@ -110,7 +116,7 @@ export class InputOrganizationsEventTypesService {
|
||||
teamId: number,
|
||||
inputEventType: CreateTeamEventTypeInput_2024_06_14
|
||||
) {
|
||||
const { hosts, assignAllTeamMembers, locations, ...rest } = inputEventType;
|
||||
const { hosts, assignAllTeamMembers, locations, emailSettings, ...rest } = inputEventType;
|
||||
|
||||
const eventType = this.inputEventTypesService.transformInputCreateEventType(rest);
|
||||
|
||||
@@ -123,11 +129,15 @@ export class InputOrganizationsEventTypesService {
|
||||
|
||||
const children = await this.getChildEventTypesForManagedEventTypeCreate(inputEventType, teamId);
|
||||
|
||||
const metadata =
|
||||
let metadata =
|
||||
rest.schedulingType === "MANAGED"
|
||||
? { managedEventConfig: {}, ...eventType.metadata }
|
||||
: eventType.metadata;
|
||||
|
||||
if (emailSettings) {
|
||||
metadata = this.addEmailSettingsToMetadata(emailSettings, metadata);
|
||||
}
|
||||
|
||||
const teamEventType = {
|
||||
...eventType,
|
||||
// note(Lauris): we don't populate hosts for managed event-types because they are handled by the children
|
||||
@@ -145,12 +155,42 @@ export class InputOrganizationsEventTypesService {
|
||||
return teamEventType;
|
||||
}
|
||||
|
||||
private addEmailSettingsToMetadata(
|
||||
emailSettings: EmailSettings_2024_06_14,
|
||||
metadata: NonNullable<EventTypeMetadata>
|
||||
) {
|
||||
if (
|
||||
emailSettings?.disableEmailsToAttendees === undefined &&
|
||||
emailSettings?.disableEmailsToHosts === undefined
|
||||
) {
|
||||
return metadata;
|
||||
}
|
||||
|
||||
const clonedMetadata = structuredClone(metadata);
|
||||
|
||||
if (!clonedMetadata.disableStandardEmails) {
|
||||
clonedMetadata.disableStandardEmails = {};
|
||||
}
|
||||
if (!clonedMetadata.disableStandardEmails.all) {
|
||||
clonedMetadata.disableStandardEmails.all = {};
|
||||
}
|
||||
|
||||
if (emailSettings?.disableEmailsToAttendees !== undefined) {
|
||||
clonedMetadata.disableStandardEmails.all.attendee = emailSettings.disableEmailsToAttendees;
|
||||
}
|
||||
if (emailSettings?.disableEmailsToHosts !== undefined) {
|
||||
clonedMetadata.disableStandardEmails.all.host = emailSettings.disableEmailsToHosts;
|
||||
}
|
||||
|
||||
return clonedMetadata;
|
||||
}
|
||||
|
||||
async transformInputUpdateTeamEventType(
|
||||
eventTypeId: number,
|
||||
teamId: number,
|
||||
inputEventType: UpdateTeamEventTypeInput_2024_06_14
|
||||
) {
|
||||
const { hosts, assignAllTeamMembers, locations, ...rest } = inputEventType;
|
||||
const { hosts, assignAllTeamMembers, locations, emailSettings, ...rest } = inputEventType;
|
||||
|
||||
const eventType = await this.inputEventTypesService.transformInputUpdateEventType(rest, eventTypeId);
|
||||
const dbEventType = await this.teamsEventTypesRepository.getTeamEventType(teamId, eventTypeId);
|
||||
@@ -164,6 +204,12 @@ export class InputOrganizationsEventTypesService {
|
||||
? await this.getChildEventTypesForManagedEventTypeUpdate(eventTypeId, inputEventType, teamId)
|
||||
: undefined;
|
||||
|
||||
let metadata = eventType.metadata;
|
||||
|
||||
if (emailSettings) {
|
||||
metadata = this.addEmailSettingsToMetadata(emailSettings, metadata);
|
||||
}
|
||||
|
||||
const teamEventType = {
|
||||
...eventType,
|
||||
// note(Lauris): we don't populate hosts for managed event-types because they are handled by the children
|
||||
@@ -175,6 +221,7 @@ export class InputOrganizationsEventTypesService {
|
||||
assignAllTeamMembers,
|
||||
children,
|
||||
locations: locations ? this.transformInputTeamLocations(locations) : undefined,
|
||||
metadata,
|
||||
};
|
||||
|
||||
return teamEventType;
|
||||
|
||||
@@ -4,6 +4,7 @@ import { UsersRepository } from "@/modules/users/users.repository";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
import { SchedulingType } from "@calcom/platform-libraries";
|
||||
import { EventTypeMetadata } from "@calcom/platform-libraries/event-types";
|
||||
import type { HostPriority, TeamEventTypeResponseHost } from "@calcom/platform-types";
|
||||
import type {
|
||||
Team,
|
||||
@@ -84,6 +85,9 @@ type Input = Pick<
|
||||
| "calVideoSettings"
|
||||
| "hidden"
|
||||
| "bookingRequiresAuthentication"
|
||||
| "rescheduleWithSameRoundRobinHost"
|
||||
| "maxActiveBookingPerBookerOfferReschedule"
|
||||
| "maxActiveBookingsPerBooker"
|
||||
>;
|
||||
|
||||
@Injectable()
|
||||
@@ -95,7 +99,12 @@ export class OutputOrganizationsEventTypesService {
|
||||
) {}
|
||||
|
||||
async getResponseTeamEventType(databaseEventType: Input, isOrgTeamEvent: boolean) {
|
||||
const { teamId, userId, parentId, assignAllTeamMembers } = databaseEventType;
|
||||
const metadata = this.outputEventTypesService.transformMetadata(databaseEventType.metadata);
|
||||
|
||||
const emailSettings = this.transformEmailSettings(metadata);
|
||||
|
||||
const { teamId, userId, parentId, assignAllTeamMembers, rescheduleWithSameRoundRobinHost } =
|
||||
databaseEventType;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { ownerId, users, ...rest } = this.outputEventTypesService.getResponseEventType(
|
||||
0,
|
||||
@@ -117,6 +126,7 @@ export class OutputOrganizationsEventTypesService {
|
||||
? this.getResponseSchedulingType(databaseEventType.schedulingType)
|
||||
: databaseEventType.schedulingType,
|
||||
assignAllTeamMembers: teamId ? assignAllTeamMembers : undefined,
|
||||
emailSettings,
|
||||
team: {
|
||||
id: teamId,
|
||||
name: databaseEventType?.team?.name,
|
||||
@@ -128,6 +138,7 @@ export class OutputOrganizationsEventTypesService {
|
||||
darkBrandColor: databaseEventType?.team?.darkBrandColor,
|
||||
theme: databaseEventType?.team?.theme,
|
||||
},
|
||||
rescheduleWithSameRoundRobinHost,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -194,6 +205,23 @@ export class OutputOrganizationsEventTypesService {
|
||||
|
||||
return transformedHosts;
|
||||
}
|
||||
|
||||
private transformEmailSettings(metadata: EventTypeMetadata) {
|
||||
if (!metadata?.disableStandardEmails?.all) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const { attendee, host } = metadata.disableStandardEmails.all;
|
||||
|
||||
if (attendee !== undefined || host !== undefined) {
|
||||
return {
|
||||
disableEmailsToAttendees: attendee ?? false,
|
||||
disableEmailsToHosts: host ?? false,
|
||||
};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function getPriorityLabel(priority: number): keyof typeof HostPriority {
|
||||
|
||||
@@ -15649,6 +15649,20 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"BookerActiveBookingsLimit_2024_06_14": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"maximumActiveBookings": {
|
||||
"type": "number",
|
||||
"description": "The maximum number of active bookings a booker can have for this event type.",
|
||||
"example": 3
|
||||
},
|
||||
"offerReschedule": {
|
||||
"type": "boolean",
|
||||
"description": "Whether to offer rescheduling the last active booking to the chosen time slot when limit is reached."
|
||||
}
|
||||
}
|
||||
},
|
||||
"Disabled_2024_06_14": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -15986,6 +16000,19 @@
|
||||
},
|
||||
"required": ["type"]
|
||||
},
|
||||
"EmailSettings_2024_06_14": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"disableEmailsToAttendees": {
|
||||
"type": "boolean",
|
||||
"description": "Disables all email communication to attendees for this event type, including booking confirmations, reminders, and cancellations. This DOES NOT include emails sent by custom email workflows."
|
||||
},
|
||||
"disableEmailsToHosts": {
|
||||
"type": "boolean",
|
||||
"description": "Disables all email communication to hosts for this event type, including booking confirmations, reminders, and cancellations. This DOES NOT include emails sent by custom email workflows."
|
||||
}
|
||||
}
|
||||
},
|
||||
"CalVideoSettings": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -16146,6 +16173,17 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"bookerActiveBookingsLimit": {
|
||||
"description": "Limit the number of active bookings a booker can make for this event type.",
|
||||
"oneOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/BookerActiveBookingsLimit_2024_06_14"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/Disabled_2024_06_14"
|
||||
}
|
||||
]
|
||||
},
|
||||
"onlyShowFirstAvailableSlot": {
|
||||
"type": "boolean",
|
||||
"description": "This will limit your availability for this event type to one slot per day, scheduled at the earliest available time."
|
||||
@@ -17389,6 +17427,9 @@
|
||||
"bookingLimitsCount": {
|
||||
"type": "object"
|
||||
},
|
||||
"bookerActiveBookingsLimit": {
|
||||
"$ref": "#/components/schemas/BookerActiveBookingsLimit_2024_06_14"
|
||||
},
|
||||
"onlyShowFirstAvailableSlot": {
|
||||
"type": "boolean"
|
||||
},
|
||||
@@ -17673,6 +17714,17 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"bookerActiveBookingsLimit": {
|
||||
"description": "Limit the number of active bookings a booker can make for this event type.",
|
||||
"oneOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/BookerActiveBookingsLimit_2024_06_14"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/Disabled_2024_06_14"
|
||||
}
|
||||
]
|
||||
},
|
||||
"onlyShowFirstAvailableSlot": {
|
||||
"type": "boolean",
|
||||
"description": "This will limit your availability for this event type to one slot per day, scheduled at the earliest available time."
|
||||
@@ -19244,6 +19296,17 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"bookerActiveBookingsLimit": {
|
||||
"description": "Limit the number of active bookings a booker can make for this event type.",
|
||||
"oneOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/BookerActiveBookingsLimit_2024_06_14"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/Disabled_2024_06_14"
|
||||
}
|
||||
]
|
||||
},
|
||||
"onlyShowFirstAvailableSlot": {
|
||||
"type": "boolean",
|
||||
"description": "This will limit your availability for this event type to one slot per day, scheduled at the earliest available time."
|
||||
@@ -19420,6 +19483,18 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"emailSettings": {
|
||||
"description": "Email settings for this event type - only available for organization team event types.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/EmailSettings_2024_06_14"
|
||||
}
|
||||
]
|
||||
},
|
||||
"rescheduleWithSameRoundRobinHost": {
|
||||
"type": "boolean",
|
||||
"description": "Rescheduled events will be assigned to the same host as initially scheduled."
|
||||
}
|
||||
},
|
||||
"required": ["lengthInMinutes", "title", "slug", "schedulingType"]
|
||||
@@ -19653,6 +19728,9 @@
|
||||
"bookingLimitsCount": {
|
||||
"type": "object"
|
||||
},
|
||||
"bookerActiveBookingsLimit": {
|
||||
"$ref": "#/components/schemas/BookerActiveBookingsLimit_2024_06_14"
|
||||
},
|
||||
"onlyShowFirstAvailableSlot": {
|
||||
"type": "boolean"
|
||||
},
|
||||
@@ -19755,6 +19833,18 @@
|
||||
},
|
||||
"team": {
|
||||
"$ref": "#/components/schemas/EventTypeTeam"
|
||||
},
|
||||
"emailSettings": {
|
||||
"description": "Email settings for this event type. Only available for organization team event types.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/EmailSettings_2024_06_14"
|
||||
}
|
||||
]
|
||||
},
|
||||
"rescheduleWithSameRoundRobinHost": {
|
||||
"type": "boolean",
|
||||
"description": "Rescheduled events will be assigned to the same host as initially scheduled."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -20043,6 +20133,17 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"bookerActiveBookingsLimit": {
|
||||
"description": "Limit the number of active bookings a booker can make for this event type.",
|
||||
"oneOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/BookerActiveBookingsLimit_2024_06_14"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/Disabled_2024_06_14"
|
||||
}
|
||||
]
|
||||
},
|
||||
"onlyShowFirstAvailableSlot": {
|
||||
"type": "boolean",
|
||||
"description": "This will limit your availability for this event type to one slot per day, scheduled at the earliest available time."
|
||||
@@ -20213,6 +20314,18 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"emailSettings": {
|
||||
"description": "Email settings for this event type. Only available for organization team event types.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/EmailSettings_2024_06_14"
|
||||
}
|
||||
]
|
||||
},
|
||||
"rescheduleWithSameRoundRobinHost": {
|
||||
"type": "boolean",
|
||||
"description": "Rescheduled events will be assigned to the same host as initially scheduled."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Navbar } from "@/components/Navbar";
|
||||
import { Inter } from "next/font/google";
|
||||
// eslint-disable-next-line @calcom/eslint/deprecated-imports-next-router
|
||||
import { useRouter } from "next/router";
|
||||
import { useState } from "react";
|
||||
|
||||
@@ -95,6 +94,7 @@ export default function Bookings(props: { calUsername: string; calEmail: string
|
||||
// timeZones={["Europe/London", "Asia/Kolkata"]}
|
||||
// isBookingDryRun={true}
|
||||
// roundRobinHideOrgAndTeam={true}
|
||||
defaultFormValues={{ name: "Bob the booker", email: "bob@thebooker.com" }}
|
||||
bannerUrl="https://i0.wp.com/mahala.co.uk/wp-content/uploads/2014/12/img_banner-thin_mountains.jpg?fit=800%2C258&ssl=1"
|
||||
eventSlug={eventTypeSlug}
|
||||
onCreateBookingSuccess={(data) => {
|
||||
|
||||
@@ -18,6 +18,8 @@ export {
|
||||
eventTypeLocations,
|
||||
} from "@calcom/prisma/zod-utils";
|
||||
|
||||
export type { EventTypeMetadata } from "@calcom/prisma/zod-utils";
|
||||
|
||||
export { validateCustomEventName } from "@calcom/features/eventtypes/lib/eventNaming";
|
||||
export { EventManager };
|
||||
export { getEventTypeById } from "@calcom/features/eventtypes/lib/getEventTypeById";
|
||||
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
import { ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { IsBoolean, IsInt, IsOptional, Min } from "class-validator";
|
||||
|
||||
export class BookerActiveBookingsLimit_2024_06_14 {
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
@ApiPropertyOptional({
|
||||
description: "The maximum number of active bookings a booker can have for this event type.",
|
||||
example: 3,
|
||||
})
|
||||
maximumActiveBookings?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@ApiPropertyOptional({
|
||||
description:
|
||||
"Whether to offer rescheduling the last active booking to the chosen time slot when limit is reached.",
|
||||
})
|
||||
offerReschedule?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
disabled?: boolean = false;
|
||||
}
|
||||
+50
-4
@@ -19,9 +19,10 @@ import {
|
||||
ArrayUnique,
|
||||
} from "class-validator";
|
||||
|
||||
|
||||
import { SchedulingType } from "@calcom/platform-enums";
|
||||
|
||||
import { RequiresAtLeastOnePropertyWhenNotDisabled } from "../../../utils/RequiresOneOfPropertiesWhenNotDisabled";
|
||||
import { BookerActiveBookingsLimit_2024_06_14 } from "./booker-active-booking-limit.input";
|
||||
import { BookerLayouts_2024_06_14 } from "./booker-layouts.input";
|
||||
import {
|
||||
AddressFieldInput_2024_06_14,
|
||||
@@ -64,6 +65,7 @@ import type { ConfirmationPolicy_2024_06_14 } from "./confirmation-policy.input"
|
||||
import { BaseConfirmationPolicy_2024_06_14, ValidateConfirmationPolicy } from "./confirmation-policy.input";
|
||||
import { DestinationCalendar_2024_06_14 } from "./destination-calendar.input";
|
||||
import { Disabled_2024_06_14 } from "./disabled.input";
|
||||
import { EmailSettings_2024_06_14 } from "./email-settings.input";
|
||||
import { EventTypeColor_2024_06_14 } from "./event-type-color.input";
|
||||
import {
|
||||
InputAddressLocation_2024_06_14,
|
||||
@@ -80,6 +82,7 @@ import {
|
||||
import type { InputLocation_2024_06_14, InputTeamLocation_2024_06_14 } from "./locations.input";
|
||||
import { Recurrence_2024_06_14 } from "./recurrence.input";
|
||||
import { Seats_2024_06_14 } from "./seats.input";
|
||||
import { CantHaveRecurrenceAndBookerActiveBookingsLimit } from "./validators/CantHaveRecurrenceAndBookerActiveBookingsLimit";
|
||||
|
||||
export const CREATE_EVENT_LENGTH_EXAMPLE = 60;
|
||||
export const CREATE_EVENT_TITLE_EXAMPLE = "Learn the secrets of masterchief!";
|
||||
@@ -108,6 +111,7 @@ export const CREATE_EVENT_SLUG_EXAMPLE = "learn-the-secrets-of-masterchief";
|
||||
CalendarDaysWindow_2024_06_14,
|
||||
RangeWindow_2024_06_14,
|
||||
BaseBookingLimitsCount_2024_06_14,
|
||||
BookerActiveBookingsLimit_2024_06_14,
|
||||
Disabled_2024_06_14,
|
||||
BaseBookingLimitsDuration_2024_06_14,
|
||||
Recurrence_2024_06_14,
|
||||
@@ -123,7 +127,8 @@ export const CREATE_EVENT_SLUG_EXAMPLE = "learn-the-secrets-of-masterchief";
|
||||
NotesDefaultFieldInput_2024_06_14,
|
||||
GuestsDefaultFieldInput_2024_06_14,
|
||||
RescheduleReasonDefaultFieldInput_2024_06_14,
|
||||
InputOrganizersDefaultApp_2024_06_14
|
||||
InputOrganizersDefaultApp_2024_06_14,
|
||||
EmailSettings_2024_06_14
|
||||
)
|
||||
export class CalVideoSettings {
|
||||
@IsOptional()
|
||||
@@ -176,7 +181,8 @@ export class CalVideoSettings {
|
||||
disableTranscriptionForOrganizer?: boolean;
|
||||
}
|
||||
|
||||
class BaseCreateEventTypeInput {
|
||||
@CantHaveRecurrenceAndBookerActiveBookingsLimit()
|
||||
export class BaseCreateEventTypeInput {
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
@DocsProperty({ example: CREATE_EVENT_LENGTH_EXAMPLE })
|
||||
@@ -297,6 +303,29 @@ class BaseCreateEventTypeInput {
|
||||
@Type(() => Object)
|
||||
bookingLimitsCount?: BookingLimitsCount_2024_06_14;
|
||||
|
||||
@IsOptional()
|
||||
@RequiresAtLeastOnePropertyWhenNotDisabled()
|
||||
@Transform(({ value }) => {
|
||||
if (value && typeof value === "object") {
|
||||
if ("disabled" in value && value.disabled) {
|
||||
return Object.assign(new Disabled_2024_06_14(), value);
|
||||
} else {
|
||||
return Object.assign(new BookerActiveBookingsLimit_2024_06_14(), value);
|
||||
}
|
||||
}
|
||||
return value;
|
||||
})
|
||||
@ValidateNested()
|
||||
@DocsPropertyOptional({
|
||||
description: "Limit the number of active bookings a booker can make for this event type.",
|
||||
oneOf: [
|
||||
{ $ref: getSchemaPath(BookerActiveBookingsLimit_2024_06_14) },
|
||||
{ $ref: getSchemaPath(Disabled_2024_06_14) },
|
||||
],
|
||||
})
|
||||
@Type(() => Object)
|
||||
bookerActiveBookingsLimit?: BookerActiveBookingsLimit_2024_06_14 | Disabled_2024_06_14;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@DocsPropertyOptional({
|
||||
@@ -568,7 +597,8 @@ export class CreateTeamEventTypeInput_2024_06_14 extends BaseCreateEventTypeInpu
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
@DocsPropertyOptional({
|
||||
description: "If true, all current and future team members will be assigned to this event type. Provide either assignAllTeamMembers or hosts but not both",
|
||||
description:
|
||||
"If true, all current and future team members will be assigned to this event type. Provide either assignAllTeamMembers or hosts but not both",
|
||||
})
|
||||
assignAllTeamMembers?: boolean;
|
||||
|
||||
@@ -591,4 +621,20 @@ export class CreateTeamEventTypeInput_2024_06_14 extends BaseCreateEventTypeInpu
|
||||
})
|
||||
@Type(() => Object)
|
||||
locations?: InputTeamLocation_2024_06_14[];
|
||||
|
||||
@IsOptional()
|
||||
@ValidateNested()
|
||||
@DocsPropertyOptional({
|
||||
description: "Email settings for this event type - only available for organization team event types.",
|
||||
type: () => EmailSettings_2024_06_14,
|
||||
})
|
||||
@Type(() => EmailSettings_2024_06_14)
|
||||
emailSettings?: EmailSettings_2024_06_14;
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
@DocsPropertyOptional({
|
||||
description: "Rescheduled events will be assigned to the same host as initially scheduled.",
|
||||
})
|
||||
rescheduleWithSameRoundRobinHost?: boolean;
|
||||
}
|
||||
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
import { ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { IsBoolean, IsOptional } from "class-validator";
|
||||
|
||||
export class EmailSettings_2024_06_14 {
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({
|
||||
description:
|
||||
"Disables all email communication to attendees for this event type, including booking confirmations, reminders, and cancellations. This DOES NOT include emails sent by custom email workflows.",
|
||||
})
|
||||
disableEmailsToAttendees?: boolean;
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({
|
||||
description:
|
||||
"Disables all email communication to hosts for this event type, including booking confirmations, reminders, and cancellations. This DOES NOT include emails sent by custom email workflows.",
|
||||
})
|
||||
disableEmailsToHosts?: boolean;
|
||||
}
|
||||
@@ -9,6 +9,7 @@ export * from "./update-event-type.input";
|
||||
export * from "./get-event-types-query.input";
|
||||
export * from "./booker-layouts.input";
|
||||
export * from "./confirmation-policy.input";
|
||||
export * from "./email-settings.input";
|
||||
export * from "./event-type-color.input";
|
||||
export * from "./seats.input";
|
||||
export * from "./destination-calendar.input";
|
||||
|
||||
+53
-10
@@ -1,9 +1,4 @@
|
||||
import {
|
||||
ApiProperty as DocsProperty,
|
||||
ApiPropertyOptional as DocsPropertyOptional,
|
||||
getSchemaPath,
|
||||
ApiExtraModels,
|
||||
} from "@nestjs/swagger";
|
||||
import { ApiPropertyOptional as DocsPropertyOptional, getSchemaPath, ApiExtraModels } from "@nestjs/swagger";
|
||||
import { Type, Transform } from "class-transformer";
|
||||
import {
|
||||
IsString,
|
||||
@@ -18,6 +13,8 @@ import {
|
||||
IsUrl,
|
||||
} from "class-validator";
|
||||
|
||||
import { RequiresAtLeastOnePropertyWhenNotDisabled } from "../../../utils/RequiresOneOfPropertiesWhenNotDisabled";
|
||||
import { BookerActiveBookingsLimit_2024_06_14 } from "./booker-active-booking-limit.input";
|
||||
import { BookerLayouts_2024_06_14 } from "./booker-layouts.input";
|
||||
import type { InputBookingField_2024_06_14 } from "./booking-fields.input";
|
||||
import {
|
||||
@@ -67,6 +64,7 @@ import {
|
||||
} from "./create-event-type.input";
|
||||
import { DestinationCalendar_2024_06_14 } from "./destination-calendar.input";
|
||||
import { Disabled_2024_06_14 } from "./disabled.input";
|
||||
import { EmailSettings_2024_06_14 } from "./email-settings.input";
|
||||
import { EventTypeColor_2024_06_14 } from "./event-type-color.input";
|
||||
import {
|
||||
InputAddressLocation_2024_06_14,
|
||||
@@ -83,6 +81,7 @@ import {
|
||||
import type { InputLocation_2024_06_14, InputTeamLocation_2024_06_14 } from "./locations.input";
|
||||
import { Recurrence_2024_06_14 } from "./recurrence.input";
|
||||
import { Seats_2024_06_14 } from "./seats.input";
|
||||
import { CantHaveRecurrenceAndBookerActiveBookingsLimit } from "./validators/CantHaveRecurrenceAndBookerActiveBookingsLimit";
|
||||
|
||||
@ApiExtraModels(
|
||||
InputAddressLocation_2024_06_14,
|
||||
@@ -118,8 +117,11 @@ import { Seats_2024_06_14 } from "./seats.input";
|
||||
LocationDefaultFieldInput_2024_06_14,
|
||||
NotesDefaultFieldInput_2024_06_14,
|
||||
GuestsDefaultFieldInput_2024_06_14,
|
||||
RescheduleReasonDefaultFieldInput_2024_06_14
|
||||
RescheduleReasonDefaultFieldInput_2024_06_14,
|
||||
BookerActiveBookingsLimit_2024_06_14,
|
||||
EmailSettings_2024_06_14
|
||||
)
|
||||
@CantHaveRecurrenceAndBookerActiveBookingsLimit()
|
||||
class BaseUpdateEventTypeInput {
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@@ -243,6 +245,29 @@ class BaseUpdateEventTypeInput {
|
||||
@Type(() => Object)
|
||||
bookingLimitsCount?: BookingLimitsCount_2024_06_14;
|
||||
|
||||
@IsOptional()
|
||||
@Transform(({ value }) => {
|
||||
if (value && typeof value === "object") {
|
||||
if ("disabled" in value && value.disabled) {
|
||||
return Object.assign(new Disabled_2024_06_14(), value);
|
||||
} else {
|
||||
return Object.assign(new BookerActiveBookingsLimit_2024_06_14(), value);
|
||||
}
|
||||
}
|
||||
return value;
|
||||
})
|
||||
@ValidateNested()
|
||||
@RequiresAtLeastOnePropertyWhenNotDisabled()
|
||||
@DocsPropertyOptional({
|
||||
description: "Limit the number of active bookings a booker can make for this event type.",
|
||||
oneOf: [
|
||||
{ $ref: getSchemaPath(BookerActiveBookingsLimit_2024_06_14) },
|
||||
{ $ref: getSchemaPath(Disabled_2024_06_14) },
|
||||
],
|
||||
})
|
||||
@Type(() => Object)
|
||||
bookerActiveBookingsLimit?: BookerActiveBookingsLimit_2024_06_14 | Disabled_2024_06_14;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@DocsPropertyOptional({
|
||||
@@ -455,16 +480,18 @@ export class UpdateTeamEventTypeInput_2024_06_14 extends BaseUpdateEventTypeInpu
|
||||
@Type(() => Host)
|
||||
@IsArray()
|
||||
@IsOptional()
|
||||
@DocsPropertyOptional({ type: [Host],
|
||||
@DocsPropertyOptional({
|
||||
type: [Host],
|
||||
description:
|
||||
"Hosts contain specific team members you want to assign to this event type, but if you want to assign all team members, use `assignAllTeamMembers: true` instead and omit this field. For platform customers the hosts can include userIds only of managed users. Provide either hosts or assignAllTeamMembers but not both",
|
||||
})
|
||||
})
|
||||
hosts?: Host[];
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@DocsPropertyOptional({
|
||||
description: "If true, all current and future team members will be assigned to this event type. Provide either assignAllTeamMembers or hosts but not both",
|
||||
description:
|
||||
"If true, all current and future team members will be assigned to this event type. Provide either assignAllTeamMembers or hosts but not both",
|
||||
})
|
||||
assignAllTeamMembers?: boolean;
|
||||
|
||||
@@ -487,4 +514,20 @@ export class UpdateTeamEventTypeInput_2024_06_14 extends BaseUpdateEventTypeInpu
|
||||
})
|
||||
@Type(() => Object)
|
||||
locations?: InputTeamLocation_2024_06_14[];
|
||||
|
||||
@IsOptional()
|
||||
@ValidateNested()
|
||||
@DocsPropertyOptional({
|
||||
description: "Email settings for this event type. Only available for organization team event types.",
|
||||
type: () => EmailSettings_2024_06_14,
|
||||
})
|
||||
@Type(() => EmailSettings_2024_06_14)
|
||||
emailSettings?: EmailSettings_2024_06_14;
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
@DocsPropertyOptional({
|
||||
description: "Rescheduled events will be assigned to the same host as initially scheduled.",
|
||||
})
|
||||
rescheduleWithSameRoundRobinHost?: boolean;
|
||||
}
|
||||
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
import { BadRequestException } from "@nestjs/common";
|
||||
import { ValidationOptions, registerDecorator, ValidationArguments } from "class-validator";
|
||||
|
||||
import type { BaseCreateEventTypeInput } from "../create-event-type.input";
|
||||
|
||||
export const FAILED_RECURRING_EVENT_TYPE_WITH_BOOKER_LIMITS_ERROR_MESSAGE =
|
||||
"Can't have `recurrence` and `bookerActiveBookingsLimit` enabled at the same time - recurring events do not support maximum active bookings limit setting.";
|
||||
|
||||
export function CantHaveRecurrenceAndBookerActiveBookingsLimit(validationOptions?: ValidationOptions) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return function (object: any) {
|
||||
registerDecorator({
|
||||
name: "cantHaveRecurrenceAndBookerActiveBookingsLimit",
|
||||
target: object,
|
||||
propertyName: "recurrence or bookerActiveBookingsLimit",
|
||||
options: validationOptions,
|
||||
constraints: [],
|
||||
validator: {
|
||||
validate(_: unknown, args: ValidationArguments) {
|
||||
const obj = args.object as BaseCreateEventTypeInput;
|
||||
|
||||
const hasActiveRecurrence = obj?.recurrence && "interval" in obj.recurrence;
|
||||
|
||||
const hasActiveBookerLimit =
|
||||
obj?.bookerActiveBookingsLimit &&
|
||||
("maximumActiveBookings" in obj.bookerActiveBookingsLimit ||
|
||||
"offerReschedule" in obj.bookerActiveBookingsLimit);
|
||||
|
||||
if (hasActiveRecurrence && hasActiveBookerLimit) {
|
||||
throw new BadRequestException(FAILED_RECURRING_EVENT_TYPE_WITH_BOOKER_LIMITS_ERROR_MESSAGE);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
defaultMessage(): string {
|
||||
return FAILED_RECURRING_EVENT_TYPE_WITH_BOOKER_LIMITS_ERROR_MESSAGE;
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
import { ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { IsBoolean, IsInt, IsOptional, Min } from "class-validator";
|
||||
|
||||
export class BookerActiveBookingsLimitOutput_2024_06_14 {
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
@ApiPropertyOptional({
|
||||
description: "The maximum number of active bookings a booker can have for this event type.",
|
||||
example: 3,
|
||||
})
|
||||
maximumActiveBookings?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@ApiPropertyOptional({
|
||||
description:
|
||||
"Whether to offer rescheduling the last active booking to the chosen time slot when limit is reached.",
|
||||
})
|
||||
offerReschedule?: boolean;
|
||||
}
|
||||
+25
-1
@@ -35,6 +35,7 @@ import type { BookingLimitsCount_2024_06_14 } from "../inputs/booking-limits-cou
|
||||
import type { ConfirmationPolicy_2024_06_14 } from "../inputs/confirmation-policy.input";
|
||||
import { DestinationCalendar_2024_06_14 } from "../inputs/destination-calendar.input";
|
||||
import type { Disabled_2024_06_14 } from "../inputs/disabled.input";
|
||||
import { EmailSettings_2024_06_14 } from "../inputs/email-settings.input";
|
||||
import {
|
||||
EmailDefaultFieldOutput_2024_06_14,
|
||||
NameDefaultFieldOutput_2024_06_14,
|
||||
@@ -56,6 +57,7 @@ import {
|
||||
TextFieldOutput_2024_06_14,
|
||||
UrlFieldOutput_2024_06_14,
|
||||
} from "../outputs/booking-fields.output";
|
||||
import { BookerActiveBookingsLimitOutput_2024_06_14 } from "./booker-active-bookings-limit.output";
|
||||
import type { OutputBookingField_2024_06_14 } from "./booking-fields.output";
|
||||
import { ValidateOutputBookingFields_2024_06_14 } from "./booking-fields.output";
|
||||
import type { OutputLocation_2024_06_14 } from "./locations.output";
|
||||
@@ -178,7 +180,8 @@ class EventTypeTeam {
|
||||
BaseBookingLimitsDuration_2024_06_14,
|
||||
BusinessDaysWindow_2024_06_14,
|
||||
CalendarDaysWindow_2024_06_14,
|
||||
RangeWindow_2024_06_14
|
||||
RangeWindow_2024_06_14,
|
||||
EmailSettings_2024_06_14
|
||||
)
|
||||
class BaseEventTypeOutput_2024_06_14 {
|
||||
@IsInt()
|
||||
@@ -346,6 +349,11 @@ class BaseEventTypeOutput_2024_06_14 {
|
||||
@ApiPropertyOptional()
|
||||
bookingLimitsCount?: BookingLimitsCount_2024_06_14;
|
||||
|
||||
@IsOptional()
|
||||
@Type(() => BookerActiveBookingsLimitOutput_2024_06_14)
|
||||
@ApiPropertyOptional({ type: BookerActiveBookingsLimitOutput_2024_06_14 })
|
||||
bookerActiveBookingsLimit?: BookerActiveBookingsLimitOutput_2024_06_14;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@ApiPropertyOptional()
|
||||
@@ -526,4 +534,20 @@ export class TeamEventTypeOutput_2024_06_14 extends BaseEventTypeOutput_2024_06_
|
||||
@Type(() => EventTypeTeam)
|
||||
@DocsProperty()
|
||||
team!: EventTypeTeam;
|
||||
|
||||
@IsOptional()
|
||||
@ValidateNested()
|
||||
@Type(() => EmailSettings_2024_06_14)
|
||||
@ApiPropertyOptional({
|
||||
description: "Email settings for this event type. Only available for organization team event types.",
|
||||
type: () => EmailSettings_2024_06_14,
|
||||
})
|
||||
emailSettings?: EmailSettings_2024_06_14;
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({
|
||||
description: "Rescheduled events will be assigned to the same host as initially scheduled.",
|
||||
})
|
||||
rescheduleWithSameRoundRobinHost?: boolean;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { BadRequestException } from "@nestjs/common";
|
||||
import { ValidationOptions, registerDecorator } from "class-validator";
|
||||
|
||||
/**
|
||||
* note(Lauris): See example in event-types_2024_06_14/inputs/create-event-type.input.ts file bookerActiveBookingsLimit property.
|
||||
* This validator is used if type can be SomeClass | Disabled_2024_06_14 to ensure that if {disabled: false} is passed
|
||||
* meaning that the feature is enabled that other properties are provided too.
|
||||
*/
|
||||
|
||||
export const REQUIRES_AT_LEAST_ONE_PROPERTY_ERROR =
|
||||
"When disabled is false {disabled: false} at least one other property must be provided, because it means that the property is enabled but then other properties must be provided too.";
|
||||
|
||||
export function RequiresAtLeastOnePropertyWhenNotDisabled(validationOptions?: ValidationOptions) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return function (object: any, propertyName: string) {
|
||||
registerDecorator({
|
||||
name: "requiresAtLeastOnePropertyWhenNotDisabled",
|
||||
target: object.constructor,
|
||||
propertyName: propertyName,
|
||||
options: validationOptions,
|
||||
constraints: [],
|
||||
validator: {
|
||||
validate(value: unknown) {
|
||||
if (!value || typeof value !== "object") {
|
||||
return true;
|
||||
}
|
||||
|
||||
const obj = value as Record<string, unknown>;
|
||||
|
||||
if (obj.disabled !== false) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const otherProperties = Object.keys(obj).filter((key) => key !== "disabled");
|
||||
const hasAtLeastOneOtherProperty = otherProperties.length > 0;
|
||||
|
||||
if (!hasAtLeastOneOtherProperty) {
|
||||
throw new BadRequestException(REQUIRES_AT_LEAST_ONE_PROPERTY_ERROR);
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
defaultMessage(): string {
|
||||
return REQUIRES_AT_LEAST_ONE_PROPERTY_ERROR;
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user