feat: add advanced tab attributes to api/v2/event-types (#16314)

* added bookerLayouts

* added requiresConfirmation

* added tests

* undo extra changes

* undo extra change

* fixed imports

* added requiresBookerEmailVerification

* added hideCalendarNotes

* added lockTimeZoneToggleOnBookingPage

* added eventTypeColor

* undo extra changes

* added seats

* added requiresConfirmationWillBlockSlot to requiresConfirmation

* refactor: Implement BaseEventType class to avoid duplication across EventType input/output classes

## D-R-Y ##

* temp-fix: will revert later

* Correct placement of users array from EventTypeOutput to TeamEventTypeOutput

* Refactor: Shifted transformation logic to controllers using interceptors and pipes for a cleaner service layer.

* small fix

* Refactor: Moved transformation logic from service to controller using interceptors and pipes

** organisations/event-types

* feat: adde tests for `validateEventTypeInputs`

* fix: failing test

* fix: layout validator

* renamed blockCalendarForUnconfirmedBookings to blockUnconfirmedBookingsInBooker

* testing changes

* added disabled state

* fix merege conflict

* renamed requiresConfirmation to confirmationPolicy

* renamed OutputEventTypesResponseInterceptor to OutputTeamEventTypesResponseInterceptor

* renamed `darkThemeColor` -> `darkThemeHex` and `lightThemeColor` -> `lightThemeHex`

* renamed `eventTypeColor` -> `color`

* changed `requiresConfirmation` -> `confirmationPolicy`

* added tests for disabled state

* added class-validators to `disabled?: false`

* Revert: Split create and update input/output classes

* feat: Refactor responses to use DTOs with plainToClass for explicit property control

	•	Updated all endpoints to return response DTOs (e.g., CreateTeamEventTypeOutput, GetTeamEventTypeOutput) to ensure consistent Swagger documentation generation.
	•	Introduced plainToClass for transforming responses, enabling explicit control over which properties are returned in the response, following Morgan’s recommendation.
	•	Replaced generic HandlerResponse type with class-validator based DTOs for each endpoint, ensuring TypeScript enforces correct response structure.
	•	Applied strategy: "excludeAll" to limit response properties to only those explicitly defined in the DTO, preventing unintended fields from being included in the API responses.

* added pipes to transform output data

* add clean script to platform-types package.json

* added customName

* added destination calendar

* fixed type errors

* added useDestinationCalendarEmail

* refactor: api-reqest and api-response

* Update event-type.output.ts

* fixed errors

* Update yarn.lock

* added some missed properties to output

* Update constants.ts

* removed return type

* fixed some type errors

* reuse types

* Improve readability of validation functions

* Update CHANGELOG.md

* chore: post publish platform libraries

* Update documentation.json

* fix: reset platform libraries to 0.0.0

* fixup! fix: reset platform libraries to 0.0.0

* Update event-type.tranformed.ts

---------

Co-authored-by: supalarry <laurisskraucis@gmail.com>
Co-authored-by: Morgan Vernay <morgan@cal.com>
This commit is contained in:
Somay Chauhan
2024-09-24 13:36:05 +03:00
committed by GitHub
co-authored by supalarry Morgan Vernay
parent 6de454b071
commit 2e2a6c73b1
68 changed files with 2859 additions and 6305 deletions
+1 -1
View File
@@ -28,7 +28,7 @@
"dependencies": {
"@calcom/platform-constants": "*",
"@calcom/platform-enums": "*",
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.37",
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.39",
"@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2",
"@calcom/platform-types": "*",
"@calcom/platform-utils": "*",
@@ -198,7 +198,7 @@ class DestinationCalendar {
integrationTitle?: string;
}
class ConnectedCalendarsData {
export class ConnectedCalendarsData {
@ValidateNested({ each: true })
@IsArray()
connectedCalendars!: ConnectedCalendar[];
@@ -4,7 +4,6 @@ import {
CredentialsRepository,
CredentialsWithUserEmail,
} from "@/modules/credentials/credentials.repository";
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
import { UsersRepository } from "@/modules/users/users.repository";
@@ -14,7 +13,6 @@ import {
UnauthorizedException,
NotFoundException,
} from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { User } from "@prisma/client";
import { Prisma } from "@prisma/client";
import { DateTime } from "luxon";
@@ -34,9 +32,7 @@ export class CalendarsService {
private readonly credentialsRepository: CredentialsRepository,
private readonly appsRepository: AppsRepository,
private readonly calendarsRepository: CalendarsRepository,
private readonly dbRead: PrismaReadService,
private readonly dbWrite: PrismaWriteService,
private readonly config: ConfigService,
private readonly selectedCalendarsRepository: SelectedCalendarsRepository
) {}
@@ -42,7 +42,7 @@ export class GoogleCalendarService implements OAuthCalendarApp {
): Promise<{ status: typeof SUCCESS_STATUS; data: { authUrl: string } }> {
const accessToken = authorization.replace("Bearer ", "");
const origin = req.get("origin") ?? req.get("host");
const redirectUrl = await await this.getCalendarRedirectUrl(accessToken, origin ?? "", redir);
const redirectUrl = await this.getCalendarRedirectUrl(accessToken, origin ?? "", redir);
return { status: SUCCESS_STATUS, data: { authUrl: redirectUrl } };
}
@@ -1,16 +1,44 @@
import { IntegrationLocation } from "@calcom/lib";
type BaseEventType = {
length: number;
slug: string;
title: string;
};
type EventTypeWithLocation = BaseEventType & {
locations: IntegrationLocation[];
};
const thirtyMinutes: BaseEventType = {
length: 30,
slug: "thirty-minutes",
title: "30 Minutes",
};
const thirtyMinutesVideo: EventTypeWithLocation = {
length: 30,
slug: "thirty-minutes-video",
title: "30 Minutes",
locations: [{ type: "integrations:daily" }],
};
const sixtyMinutes: BaseEventType = {
length: 60,
slug: "sixty-minutes",
title: "60 Minutes",
};
const sixtyMinutesVideo: EventTypeWithLocation = {
length: 60,
slug: "sixty-minutes-video",
title: "60 Minutes",
locations: [{ type: "integrations:daily" }],
};
export const DEFAULT_EVENT_TYPES = {
thirtyMinutes: { length: 30, slug: "thirty-minutes", title: "30 Minutes" },
thirtyMinutesVideo: {
length: 30,
slug: "thirty-minutes-video",
title: "30 Minutes",
locations: [{ type: "integrations:daily" }],
},
sixtyMinutes: { length: 60, slug: "sixty-minutes", title: "60 Minutes" },
sixtyMinutesVideo: {
length: 60,
slug: "sixty-minutes-video",
title: "60 Minutes",
locations: [{ type: "integrations:daily" }],
},
thirtyMinutes,
thirtyMinutesVideo,
sixtyMinutes,
sixtyMinutesVideo,
};
@@ -20,7 +20,13 @@ import { UserRepositoryFixture } from "test/fixtures/repository/users.repository
import { withApiAuth } from "test/utils/withApiAuth";
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_06_14 } from "@calcom/platform-constants";
import { BookingWindowPeriodInputTypeEnum_2024_06_14, FrequencyInput } from "@calcom/platform-enums";
import {
BookingWindowPeriodInputTypeEnum_2024_06_14,
BookerLayoutsInputEnum_2024_06_14,
ConfirmationPolicyEnum,
NoticeThresholdUnitEnum,
FrequencyInput,
} from "@calcom/platform-enums";
import {
ApiSuccessResponse,
CreateEventTypeInput_2024_06_14,
@@ -230,11 +236,36 @@ describe("Event types Endpoints", () => {
value: 30,
rolling: true,
},
bookerLayouts: {
enabledLayouts: [
BookerLayoutsInputEnum_2024_06_14.column,
BookerLayoutsInputEnum_2024_06_14.month,
BookerLayoutsInputEnum_2024_06_14.week,
],
defaultLayout: BookerLayoutsInputEnum_2024_06_14.month,
},
confirmationPolicy: {
type: ConfirmationPolicyEnum.TIME,
noticeThreshold: {
count: 60,
unit: NoticeThresholdUnitEnum.MINUTES,
},
blockUnconfirmedBookingsInBooker: true,
},
recurrence: {
frequency: FrequencyInput.weekly,
interval: 2,
occurrences: 10,
disabled: false,
},
requiresBookerEmailVerification: false,
hideCalendarNotes: false,
lockTimeZoneToggleOnBookingPage: true,
color: {
darkThemeHex: "#292929",
lightThemeHex: "#fafafa",
},
customName: `{Event type title} between {Organiser} and {Scheduler}`,
};
return request(app.getHttpServer())
@@ -257,7 +288,19 @@ describe("Event types Endpoints", () => {
expect(createdEventType.bookingLimitsDuration).toEqual(body.bookingLimitsDuration);
expect(createdEventType.offsetStart).toEqual(body.offsetStart);
expect(createdEventType.bookingWindow).toEqual(body.bookingWindow);
expect(createdEventType.bookerLayouts).toEqual(body.bookerLayouts);
expect(createdEventType.confirmationPolicy).toEqual(body.confirmationPolicy);
expect(createdEventType.recurrence).toEqual(body.recurrence);
expect(createdEventType.customName).toEqual(body.customName);
expect(createdEventType.requiresBookerEmailVerification).toEqual(
body.requiresBookerEmailVerification
);
expect(createdEventType.hideCalendarNotes).toEqual(body.hideCalendarNotes);
expect(createdEventType.lockTimeZoneToggleOnBookingPage).toEqual(
body.lockTimeZoneToggleOnBookingPage
);
expect(createdEventType.color).toEqual(body.color);
const responseBookingFields = body.bookingFields || [];
const expectedBookingFields = [
@@ -272,6 +315,317 @@ describe("Event types Endpoints", () => {
});
});
it(`/GET/even-types by username`, async () => {
const response = await request(app.getHttpServer())
.get(`/api/v2/event-types?username=${username}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
// note: bearer token value mocked using "withAccessTokenAuth" for user which id is used when creating event type above
.set("Authorization", `Bearer whatever`)
.expect(200);
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14[]> = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseBody.data?.length).toEqual(1);
const fetchedEventType = responseBody.data?.[0];
expect(fetchedEventType?.id).toEqual(eventType.id);
expect(fetchedEventType?.title).toEqual(eventType.title);
expect(fetchedEventType?.description).toEqual(eventType.description);
expect(fetchedEventType?.lengthInMinutes).toEqual(eventType.lengthInMinutes);
expect(fetchedEventType?.locations).toEqual(eventType.locations);
expect(fetchedEventType?.bookingFields).toEqual(eventType.bookingFields);
expect(fetchedEventType?.ownerId).toEqual(user.id);
expect(fetchedEventType.bookingLimitsCount).toEqual(eventType.bookingLimitsCount);
expect(fetchedEventType.onlyShowFirstAvailableSlot).toEqual(eventType.onlyShowFirstAvailableSlot);
expect(fetchedEventType.bookingLimitsDuration).toEqual(eventType.bookingLimitsDuration);
expect(fetchedEventType.offsetStart).toEqual(eventType.offsetStart);
expect(fetchedEventType.bookingWindow).toEqual(eventType.bookingWindow);
expect(fetchedEventType.bookerLayouts).toEqual(eventType.bookerLayouts);
expect(fetchedEventType.confirmationPolicy).toEqual(eventType.confirmationPolicy);
expect(fetchedEventType.recurrence).toEqual(eventType.recurrence);
expect(fetchedEventType.customName).toEqual(eventType.customName);
expect(fetchedEventType.requiresBookerEmailVerification).toEqual(
eventType.requiresBookerEmailVerification
);
expect(fetchedEventType.hideCalendarNotes).toEqual(eventType.hideCalendarNotes);
expect(fetchedEventType.lockTimeZoneToggleOnBookingPage).toEqual(
eventType.lockTimeZoneToggleOnBookingPage
);
expect(fetchedEventType.color).toEqual(eventType.color);
});
it("should return an error when creating an event type with seats enabled and multiple locations", async () => {
const body: CreateEventTypeInput_2024_06_14 = {
title: "Coding class 2",
slug: "coding-class-2",
description: "Let's learn how to code like a pro.",
lengthInMinutes: 60,
locations: [
{
type: "integration",
integration: "cal-video",
},
{
type: "phone",
phone: "+37120993151",
public: true,
},
],
scheduleId: firstSchedule.id,
seats: {
seatsPerTimeSlot: 4,
showAttendeeInfo: true,
showAvailabilityCount: true,
},
};
await request(app.getHttpServer())
.post("/api/v2/event-types")
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.send(body)
.expect(400);
});
it("should return an error when trying to enable seats for an event type with multiple locations", async () => {
const body: CreateEventTypeInput_2024_06_14 = {
title: "Coding class 3",
slug: "coding-class-3",
description: "Let's learn how to code like a pro.",
lengthInMinutes: 60,
locations: [
{
type: "integration",
integration: "cal-video",
},
{
type: "phone",
phone: "+37120993151",
public: true,
},
],
scheduleId: firstSchedule.id,
};
const createResponse = await request(app.getHttpServer())
.post("/api/v2/event-types")
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.send(body)
.expect(201);
const createdEventType = createResponse.body.data;
expect(createdEventType).toMatchObject({
id: expect.any(Number),
title: body.title,
description: body.description,
lengthInMinutes: body.lengthInMinutes,
locations: body.locations,
scheduleId: firstSchedule.id,
});
return request(app.getHttpServer())
.patch(`/api/v2/event-types/${createdEventType.id}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.send({
seats: {
seatsPerTimeSlot: 4,
showAttendeeInfo: true,
showAvailabilityCount: true,
},
})
.expect(400);
});
it("should return an error when creating an event type with seats enabled and confirmationPolicy enabled", async () => {
const body: CreateEventTypeInput_2024_06_14 = {
title: "Coding class 4",
slug: "coding-class-4",
description: "Let's learn how to code like a pro.",
lengthInMinutes: 60,
scheduleId: firstSchedule.id,
confirmationPolicy: {
type: ConfirmationPolicyEnum.ALWAYS,
blockUnconfirmedBookingsInBooker: false,
},
seats: {
seatsPerTimeSlot: 4,
showAttendeeInfo: true,
showAvailabilityCount: true,
},
};
await request(app.getHttpServer())
.post("/api/v2/event-types")
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.send(body)
.expect(400);
});
it("should return an error when trying to enable seats for an event type with confirmationPolicy enabled", async () => {
const body: CreateEventTypeInput_2024_06_14 = {
title: "Coding class 5",
slug: "coding-class-5",
description: "Let's learn how to code like a pro.",
lengthInMinutes: 60,
confirmationPolicy: {
type: ConfirmationPolicyEnum.ALWAYS,
blockUnconfirmedBookingsInBooker: false,
},
scheduleId: firstSchedule.id,
};
const createResponse = await request(app.getHttpServer())
.post("/api/v2/event-types")
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.send(body)
.expect(201);
const createdEventType = createResponse.body.data;
console.log("createdEventType: ", createdEventType);
expect(createdEventType).toMatchObject({
id: expect.any(Number),
title: body.title,
description: body.description,
lengthInMinutes: body.lengthInMinutes,
confirmationPolicy: body.confirmationPolicy,
scheduleId: firstSchedule.id,
});
return request(app.getHttpServer())
.patch(`/api/v2/event-types/${createdEventType.id}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.send({
seats: {
seatsPerTimeSlot: 4,
showAttendeeInfo: true,
showAvailabilityCount: true,
},
})
.expect(400);
});
it("should return an error when trying to set multiple locations for an event type with seats enabled", async () => {
const body: CreateEventTypeInput_2024_06_14 = {
title: "Coding class 6",
slug: "coding-class-6",
description: "Let's learn how to code like a pro.",
lengthInMinutes: 60,
scheduleId: firstSchedule.id,
seats: {
seatsPerTimeSlot: 4,
showAttendeeInfo: true,
showAvailabilityCount: true,
},
};
const createResponse = await request(app.getHttpServer())
.post("/api/v2/event-types")
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.send(body)
.expect(201);
const createdEventType = createResponse.body.data;
expect(createdEventType).toMatchObject({
id: expect.any(Number),
title: body.title,
description: body.description,
lengthInMinutes: body.lengthInMinutes,
seats: body.seats,
scheduleId: firstSchedule.id,
});
return request(app.getHttpServer())
.patch(`/api/v2/event-types/${createdEventType.id}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.send({
locations: [
{
type: "integration",
integration: "cal-video",
},
{
type: "phone",
phone: "+37120993151",
public: true,
},
],
})
.expect(400);
});
it("should return an error when creating an event type with confirmationPolicy enabled and seats enabled", async () => {
const body: CreateEventTypeInput_2024_06_14 = {
title: "Coding class 7",
slug: "coding-class-7",
description: "Let's learn how to code like a pro.",
lengthInMinutes: 60,
confirmationPolicy: {
type: ConfirmationPolicyEnum.ALWAYS,
blockUnconfirmedBookingsInBooker: false,
},
scheduleId: firstSchedule.id,
seats: {
seatsPerTimeSlot: 4,
showAttendeeInfo: true,
showAvailabilityCount: true,
},
};
await request(app.getHttpServer())
.post("/api/v2/event-types")
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.send(body)
.expect(400);
});
it("should return an error when trying to enable confirmationPolicy for an event type with seats enabled", async () => {
const body: CreateEventTypeInput_2024_06_14 = {
title: "Coding class 8",
slug: "coding-class-8",
description: "Let's learn how to code like a pro.",
lengthInMinutes: 60,
seats: {
seatsPerTimeSlot: 4,
showAttendeeInfo: true,
showAvailabilityCount: true,
},
scheduleId: firstSchedule.id,
};
const createResponse = await request(app.getHttpServer())
.post("/api/v2/event-types")
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.send(body)
.expect(201);
const createdEventType = createResponse.body.data;
expect(createdEventType).toMatchObject({
id: expect.any(Number),
title: body.title,
description: body.description,
lengthInMinutes: body.lengthInMinutes,
seats: body.seats,
scheduleId: firstSchedule.id,
});
return request(app.getHttpServer())
.patch(`/api/v2/event-types/${createdEventType.id}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.send({
confirmationPolicy: {
type: ConfirmationPolicyEnum.ALWAYS,
blockUnconfirmedBookingsInBooker: false,
},
})
.expect(400);
});
it("should update event type", async () => {
const newTitle = "Coding class in Italian!";
@@ -293,11 +647,32 @@ describe("Event types Endpoints", () => {
value: 40,
rolling: false,
},
bookerLayouts: {
enabledLayouts: [
BookerLayoutsInputEnum_2024_06_14.column,
BookerLayoutsInputEnum_2024_06_14.month,
BookerLayoutsInputEnum_2024_06_14.week,
],
defaultLayout: BookerLayoutsInputEnum_2024_06_14.month,
},
confirmationPolicy: {
type: ConfirmationPolicyEnum.ALWAYS,
blockUnconfirmedBookingsInBooker: false,
},
recurrence: {
frequency: FrequencyInput.monthly,
interval: 4,
occurrences: 10,
disabled: false,
},
requiresBookerEmailVerification: true,
hideCalendarNotes: true,
lockTimeZoneToggleOnBookingPage: true,
color: {
darkThemeHex: "#292929",
lightThemeHex: "#fafafa",
},
customName: `{Event type title} betweennnnnnnnnnn {Organiser} and {Scheduler}`,
};
return request(app.getHttpServer())
@@ -323,7 +698,18 @@ describe("Event types Endpoints", () => {
expect(updatedEventType.bookingLimitsDuration).toEqual(body.bookingLimitsDuration);
expect(updatedEventType.offsetStart).toEqual(body.offsetStart);
expect(updatedEventType.bookingWindow).toEqual(body.bookingWindow);
expect(updatedEventType.bookerLayouts).toEqual(body.bookerLayouts);
expect(updatedEventType.confirmationPolicy).toEqual(body.confirmationPolicy);
expect(updatedEventType.recurrence).toEqual(body.recurrence);
expect(updatedEventType.customName).toEqual(body.customName);
expect(updatedEventType.requiresBookerEmailVerification).toEqual(
body.requiresBookerEmailVerification
);
expect(updatedEventType.hideCalendarNotes).toEqual(body.hideCalendarNotes);
expect(updatedEventType.lockTimeZoneToggleOnBookingPage).toEqual(
body.lockTimeZoneToggleOnBookingPage
);
expect(updatedEventType.color).toEqual(body.color);
eventType.title = newTitle;
eventType.scheduleId = secondSchedule.id;
@@ -332,7 +718,14 @@ describe("Event types Endpoints", () => {
eventType.bookingLimitsDuration = updatedEventType.bookingLimitsDuration;
eventType.offsetStart = updatedEventType.offsetStart;
eventType.bookingWindow = updatedEventType.bookingWindow;
eventType.bookerLayouts = updatedEventType.bookerLayouts;
eventType.confirmationPolicy = updatedEventType.confirmationPolicy;
eventType.recurrence = updatedEventType.recurrence;
eventType.customName = updatedEventType.customName;
eventType.requiresBookerEmailVerification = updatedEventType.requiresBookerEmailVerification;
eventType.hideCalendarNotes = updatedEventType.hideCalendarNotes;
eventType.lockTimeZoneToggleOnBookingPage = updatedEventType.lockTimeZoneToggleOnBookingPage;
eventType.color = updatedEventType.color;
});
});
@@ -373,38 +766,18 @@ describe("Event types Endpoints", () => {
expect(fetchedEventType.bookingLimitsDuration).toEqual(eventType.bookingLimitsDuration);
expect(fetchedEventType.offsetStart).toEqual(eventType.offsetStart);
expect(fetchedEventType.bookingWindow).toEqual(eventType.bookingWindow);
expect(fetchedEventType.bookerLayouts).toEqual(eventType.bookerLayouts);
expect(fetchedEventType.confirmationPolicy).toEqual(eventType.confirmationPolicy);
expect(fetchedEventType.recurrence).toEqual(eventType.recurrence);
});
it(`/GET/even-types by username`, async () => {
const response = await request(app.getHttpServer())
.get(`/api/v2/event-types?username=${username}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
// note: bearer token value mocked using "withAccessTokenAuth" for user which id is used when creating event type above
.set("Authorization", `Bearer whatever`)
.expect(200);
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14[]> = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseBody.data?.length).toEqual(1);
const fetchedEventType = responseBody.data?.[0];
expect(fetchedEventType?.id).toEqual(eventType.id);
expect(fetchedEventType?.title).toEqual(eventType.title);
expect(fetchedEventType?.description).toEqual(eventType.description);
expect(fetchedEventType?.lengthInMinutes).toEqual(eventType.lengthInMinutes);
expect(fetchedEventType?.locations).toEqual(eventType.locations);
expect(fetchedEventType?.bookingFields).toEqual(eventType.bookingFields);
expect(fetchedEventType?.ownerId).toEqual(user.id);
expect(fetchedEventType.bookingLimitsCount).toEqual(eventType.bookingLimitsCount);
expect(fetchedEventType.onlyShowFirstAvailableSlot).toEqual(eventType.onlyShowFirstAvailableSlot);
expect(fetchedEventType.bookingLimitsDuration).toEqual(eventType.bookingLimitsDuration);
expect(fetchedEventType.offsetStart).toEqual(eventType.offsetStart);
expect(fetchedEventType.bookingWindow).toEqual(eventType.bookingWindow);
expect(fetchedEventType.recurrence).toEqual(eventType.recurrence);
expect(fetchedEventType.customName).toEqual(eventType.customName);
expect(fetchedEventType.requiresBookerEmailVerification).toEqual(
eventType.requiresBookerEmailVerification
);
expect(fetchedEventType.hideCalendarNotes).toEqual(eventType.hideCalendarNotes);
expect(fetchedEventType.lockTimeZoneToggleOnBookingPage).toEqual(
eventType.lockTimeZoneToggleOnBookingPage
);
expect(fetchedEventType.color).toEqual(eventType.color);
});
it(`/GET/event-types by username and eventSlug`, async () => {
@@ -430,7 +803,18 @@ describe("Event types Endpoints", () => {
expect(fetchedEventType.bookingLimitsDuration).toEqual(eventType.bookingLimitsDuration);
expect(fetchedEventType.offsetStart).toEqual(eventType.offsetStart);
expect(fetchedEventType.bookingWindow).toEqual(eventType.bookingWindow);
expect(fetchedEventType.bookerLayouts).toEqual(eventType.bookerLayouts);
expect(fetchedEventType.confirmationPolicy).toEqual(eventType.confirmationPolicy);
expect(fetchedEventType.recurrence).toEqual(eventType.recurrence);
expect(fetchedEventType.customName).toEqual(eventType.customName);
expect(fetchedEventType.requiresBookerEmailVerification).toEqual(
eventType.requiresBookerEmailVerification
);
expect(fetchedEventType.hideCalendarNotes).toEqual(eventType.hideCalendarNotes);
expect(fetchedEventType.lockTimeZoneToggleOnBookingPage).toEqual(
eventType.lockTimeZoneToggleOnBookingPage
);
expect(fetchedEventType.color).toEqual(eventType.color);
});
it(`/GET/:id not existing`, async () => {
@@ -3,7 +3,9 @@ import { DeleteEventTypeOutput_2024_06_14 } from "@/ee/event-types/event-types_2
import { GetEventTypeOutput_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/outputs/get-event-type.output";
import { GetEventTypesOutput_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/outputs/get-event-types.output";
import { UpdateEventTypeOutput_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/outputs/update-event-type.output";
import { EventTypeResponseTransformPipe } from "@/ee/event-types/event-types_2024_06_14/pipes/event-type-response.transformer";
import { EventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/event-types.service";
import { InputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/input-event-types.service";
import { VERSION_2024_06_14_VALUE } from "@/lib/api-versions";
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
@@ -23,14 +25,15 @@ import {
HttpStatus,
Delete,
Query,
ParseIntPipe,
} from "@nestjs/common";
import { ApiHeader, ApiTags as DocsTags } from "@nestjs/swagger";
import { EVENT_TYPE_READ, EVENT_TYPE_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
import {
CreateEventTypeInput_2024_06_14,
UpdateEventTypeInput_2024_06_14,
GetEventTypesQuery_2024_06_14,
CreateEventTypeInput_2024_06_14,
} from "@calcom/platform-types";
@Controller({
@@ -45,7 +48,11 @@ import {
required: true,
})
export class EventTypesController_2024_06_14 {
constructor(private readonly eventTypesService: EventTypesService_2024_06_14) {}
constructor(
private readonly eventTypesService: EventTypesService_2024_06_14,
private readonly inputEventTypesService: InputEventTypesService_2024_06_14,
private readonly eventTypeResponseTransformPipe: EventTypeResponseTransformPipe
) {}
@Post("/")
@Permissions([EVENT_TYPE_WRITE])
@@ -60,11 +67,16 @@ export class EventTypesController_2024_06_14 {
@Body() body: CreateEventTypeInput_2024_06_14,
@GetUser() user: UserWithProfile
): Promise<CreateEventTypeOutput_2024_06_14> {
const eventType = await this.eventTypesService.createUserEventType(user, body);
const transformedBody = await this.inputEventTypesService.transformAndValidateCreateEventTypeInput(
user.id,
body
);
const eventType = await this.eventTypesService.createUserEventType(user, transformedBody);
return {
status: SUCCESS_STATUS,
data: eventType,
data: this.eventTypeResponseTransformPipe.transform(eventType),
};
}
@@ -89,7 +101,7 @@ export class EventTypesController_2024_06_14 {
return {
status: SUCCESS_STATUS,
data: eventType,
data: this.eventTypeResponseTransformPipe.transform(eventType),
};
}
@@ -101,7 +113,7 @@ export class EventTypesController_2024_06_14 {
return {
status: SUCCESS_STATUS,
data: eventTypes,
data: this.eventTypeResponseTransformPipe.transform(eventTypes),
};
}
@@ -116,15 +128,21 @@ export class EventTypesController_2024_06_14 {
})
@HttpCode(HttpStatus.OK)
async updateEventType(
@Param("eventTypeId") eventTypeId: number,
@Param("eventTypeId", ParseIntPipe) eventTypeId: number,
@Body() body: UpdateEventTypeInput_2024_06_14,
@GetUser() user: UserWithProfile
): Promise<UpdateEventTypeOutput_2024_06_14> {
const eventType = await this.eventTypesService.updateEventType(eventTypeId, body, user);
const transformedBody = await this.inputEventTypesService.transformAndValidateUpdateEventTypeInput(
body,
user.id,
eventTypeId
);
const eventType = await this.eventTypesService.updateEventType(eventTypeId, transformedBody, user);
return {
status: SUCCESS_STATUS,
data: eventType,
data: this.eventTypeResponseTransformPipe.transform(eventType),
};
}
@@ -1,9 +1,14 @@
import { CalendarsRepository } from "@/ee/calendars/calendars.repository";
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
import { EventTypesController_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/controllers/event-types.controller";
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
import { EventTypeResponseTransformPipe } from "@/ee/event-types/event-types_2024_06_14/pipes/event-type-response.transformer";
import { EventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/event-types.service";
import { InputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/input-event-types.service";
import { OutputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/output-event-types.service";
import { SchedulesRepository_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/schedules.repository";
import { AppsRepository } from "@/modules/apps/apps.repository";
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
import { MembershipsModule } from "@/modules/memberships/memberships.module";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { SelectedCalendarsModule } from "@/modules/selected-calendars/selected-calendars.module";
@@ -22,6 +27,11 @@ import { Module } from "@nestjs/common";
UsersRepository,
UsersService,
SchedulesRepository_2024_06_11,
EventTypeResponseTransformPipe,
CalendarsService,
CredentialsRepository,
AppsRepository,
CalendarsRepository,
],
controllers: [EventTypesController_2024_06_14],
exports: [
@@ -4,36 +4,10 @@ import { UsersService } from "@/modules/users/services/users.service";
import { UserWithProfile } from "@/modules/users/users.repository";
import { Injectable } from "@nestjs/common";
import {
getEventTypeById,
transformBookingFieldsApiToInternal,
transformLocationsApiToInternal,
} from "@calcom/platform-libraries";
import { CreateEventTypeInput_2024_06_14 } from "@calcom/platform-types";
import { getEventTypeById } from "@calcom/platform-libraries";
import { InputEventTransformed_2024_06_14 } from "@calcom/platform-types";
import type { PrismaClient } from "@calcom/prisma";
type InputEventTransformed = Omit<
CreateEventTypeInput_2024_06_14,
| "lengthInMinutes"
| "locations"
| "bookingFields"
| "bookingLimitsCount"
| "onlyShowFirstAvailableSlot"
| "bookingLimitsDuration"
| "offsetStart"
| "periodType"
| "periodDays"
| "periodCountCalendarDays"
| "periodStartDate"
| "periodEndDate"
| "recurrence"
> & {
length: number;
slug: string;
locations?: ReturnType<typeof transformLocationsApiToInternal>;
bookingFields?: ReturnType<typeof transformBookingFieldsApiToInternal>;
};
@Injectable()
export class EventTypesRepository_2024_06_14 {
constructor(
@@ -42,7 +16,10 @@ export class EventTypesRepository_2024_06_14 {
private usersService: UsersService
) {}
async createUserEventType(userId: number, body: InputEventTransformed) {
async createUserEventType(
userId: number,
body: Omit<InputEventTransformed_2024_06_14, "destinationCalendar">
) {
return this.dbWrite.prisma.eventType.create({
data: {
...body,
@@ -57,7 +34,19 @@ export class EventTypesRepository_2024_06_14 {
async getEventTypeWithSeats(eventTypeId: number) {
return this.dbRead.prisma.eventType.findUnique({
where: { id: eventTypeId },
select: { users: { select: { id: true } }, seatsPerTimeSlot: true },
select: {
users: { select: { id: true } },
seatsPerTimeSlot: true,
locations: true,
requiresConfirmation: true,
},
});
}
async getEventTypeWithMetaData(eventTypeId: number) {
return this.dbRead.prisma.eventType.findUnique({
where: { id: eventTypeId },
select: { metadata: true },
});
}
@@ -67,7 +56,7 @@ export class EventTypesRepository_2024_06_14 {
id: eventTypeId,
userId,
},
include: { users: true, schedule: true },
include: { users: true, schedule: true, destinationCalendar: true },
});
}
@@ -76,7 +65,7 @@ export class EventTypesRepository_2024_06_14 {
where: {
userId,
},
include: { users: true, schedule: true },
include: { users: true, schedule: true, destinationCalendar: true },
});
}
@@ -98,7 +87,7 @@ export class EventTypesRepository_2024_06_14 {
async getEventTypeById(eventTypeId: number) {
return this.dbRead.prisma.eventType.findUnique({
where: { id: eventTypeId },
include: { users: true, schedule: true },
include: { users: true, schedule: true, destinationCalendar: true },
});
}
@@ -117,7 +106,7 @@ export class EventTypesRepository_2024_06_14 {
slug: slug,
},
},
include: { users: true, schedule: true },
include: { users: true, schedule: true, destinationCalendar: true },
});
}
@@ -0,0 +1,39 @@
import { Injectable, PipeTransform } from "@nestjs/common";
import { plainToClass } from "class-transformer";
import { EventTypeOutput_2024_06_14 } from "@calcom/platform-types";
import {
DatabaseEventType,
OutputEventTypesService_2024_06_14,
} from "../services/output-event-types.service";
type EventTypeResponse = DatabaseEventType & { ownerId: number };
@Injectable()
export class EventTypeResponseTransformPipe implements PipeTransform {
constructor(private readonly outputEventTypesService: OutputEventTypesService_2024_06_14) {}
private transformEventType(eventType: EventTypeResponse): EventTypeOutput_2024_06_14 {
return plainToClass(
EventTypeOutput_2024_06_14,
this.outputEventTypesService.getResponseEventType(eventType.ownerId, eventType),
{ strategy: "exposeAll" }
);
}
// Implementing function overloading to ensure correct return types based on input type:
transform(value: EventTypeResponse[]): EventTypeOutput_2024_06_14[];
transform(value: EventTypeResponse): EventTypeOutput_2024_06_14;
transform(
value: EventTypeResponse | EventTypeResponse[]
): EventTypeOutput_2024_06_14 | EventTypeOutput_2024_06_14[] {
if (Array.isArray(value)) {
return value.map((item) => this.transformEventType(item));
} else {
return this.transformEventType(value);
}
}
}
@@ -1,7 +1,5 @@
import { DEFAULT_EVENT_TYPES } from "@/ee/event-types/event-types_2024_06_14/constants/constants";
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
import { InputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/input-event-types.service";
import { OutputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/output-event-types.service";
import { SchedulesRepository_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/schedules.repository";
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
@@ -13,20 +11,13 @@ import { BadRequestException, ForbiddenException, Injectable, NotFoundException
import { createEventType, updateEventType } from "@calcom/platform-libraries";
import { getEventTypesPublic, EventTypesPublic } from "@calcom/platform-libraries";
import { dynamicEvent } from "@calcom/platform-libraries";
import {
CreateEventTypeInput_2024_06_14,
UpdateEventTypeInput_2024_06_14,
GetEventTypesQuery_2024_06_14,
EventTypeOutput_2024_06_14,
} from "@calcom/platform-types";
import { GetEventTypesQuery_2024_06_14, InputEventTransformed_2024_06_14 } from "@calcom/platform-types";
import { EventType } from "@calcom/prisma/client";
@Injectable()
export class EventTypesService_2024_06_14 {
constructor(
private readonly eventTypesRepository: EventTypesRepository_2024_06_14,
private readonly inputEventTypesService: InputEventTypesService_2024_06_14,
private readonly outputEventTypesService: OutputEventTypesService_2024_06_14,
private readonly membershipsRepository: MembershipsRepository,
private readonly usersRepository: UsersRepository,
private readonly usersService: UsersService,
@@ -35,22 +26,13 @@ export class EventTypesService_2024_06_14 {
private readonly schedulesRepository: SchedulesRepository_2024_06_11
) {}
async createUserEventType(user: UserWithProfile, body: CreateEventTypeInput_2024_06_14) {
async createUserEventType(user: UserWithProfile, body: InputEventTransformed_2024_06_14) {
await this.checkCanCreateEventType(user.id, body);
const eventTypeUser = await this.getUserToCreateEvent(user);
const {
bookingLimits,
durationLimits,
periodType = undefined,
periodDays = undefined,
periodCountCalendarDays = undefined,
periodStartDate = undefined,
periodEndDate = undefined,
recurrence = undefined,
...bodyTransformed
} = this.inputEventTypesService.transformInputCreateEventType(body);
const { destinationCalendar: _destinationCalendar, ...rest } = body;
const { eventType: eventTypeCreated } = await createEventType({
input: bodyTransformed,
input: rest,
ctx: {
user: eventTypeUser,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
@@ -62,15 +44,7 @@ export class EventTypesService_2024_06_14 {
await updateEventType({
input: {
id: eventTypeCreated.id,
bookingLimits,
durationLimits,
periodType,
periodDays,
periodCountCalendarDays,
periodStartDate,
periodEndDate,
recurrence,
...bodyTransformed,
...body,
},
ctx: {
user: eventTypeUser,
@@ -86,10 +60,13 @@ export class EventTypesService_2024_06_14 {
throw new NotFoundException(`Event type with id ${eventTypeCreated.id} not found`);
}
return this.outputEventTypesService.getResponseEventType(user.id, eventType);
return {
ownerId: user.id,
...eventType,
};
}
async checkCanCreateEventType(userId: number, body: CreateEventTypeInput_2024_06_14) {
async checkCanCreateEventType(userId: number, body: InputEventTransformed_2024_06_14) {
const existsWithSlug = await this.eventTypesRepository.getUserEventTypeBySlug(userId, body.slug);
if (existsWithSlug) {
throw new BadRequestException("User already has an event type with this slug.");
@@ -109,7 +86,10 @@ export class EventTypesService_2024_06_14 {
return null;
}
return this.outputEventTypesService.getResponseEventType(user.id, eventType);
return {
ownerId: user.id,
...eventType,
};
}
async getEventTypesByUsername(username: string) {
@@ -117,7 +97,7 @@ export class EventTypesService_2024_06_14 {
if (!user) {
return [];
}
return this.getUserEventTypes(user.id);
return await this.getUserEventTypes(user.id);
}
async getUserToCreateEvent(user: UserWithProfile) {
@@ -147,17 +127,19 @@ export class EventTypesService_2024_06_14 {
}
this.checkUserOwnsEventType(userId, eventType);
return this.outputEventTypesService.getResponseEventType(userId, eventType);
return {
ownerId: userId,
...eventType,
};
}
async getUserEventTypes(userId: number) {
const eventTypes = await this.eventTypesRepository.getUserEventTypes(userId);
const eventTypePromises = eventTypes.map(async (eventType) => {
return await this.outputEventTypesService.getResponseEventType(userId, eventType);
return eventTypes.map((eventType) => {
return { ownerId: userId, ...eventType };
});
return await Promise.all(eventTypePromises);
}
async getUserEventTypeForAtom(user: UserWithProfile, eventTypeId: number) {
@@ -190,7 +172,7 @@ export class EventTypesService_2024_06_14 {
return await getEventTypesPublic(user.id);
}
async getEventTypes(queryParams: GetEventTypesQuery_2024_06_14): Promise<EventTypeOutput_2024_06_14[]> {
async getEventTypes(queryParams: GetEventTypesQuery_2024_06_14) {
const { username, eventSlug, usernames } = queryParams;
if (username && eventSlug) {
@@ -218,12 +200,12 @@ export class EventTypesService_2024_06_14 {
usersFiltered.push(user);
}
}
return this.outputEventTypesService.getResponseEventType(0, {
return {
ownerId: 0,
...dynamicEvent,
users: usersFiltered,
isInstantEvent: false,
});
};
}
async createUserDefaultEventTypes(userId: number) {
@@ -239,12 +221,12 @@ export class EventTypesService_2024_06_14 {
return defaultEventTypes;
}
async updateEventType(eventTypeId: number, body: UpdateEventTypeInput_2024_06_14, user: UserWithProfile) {
async updateEventType(eventTypeId: number, body: InputEventTransformed_2024_06_14, user: UserWithProfile) {
await this.checkCanUpdateEventType(user.id, eventTypeId, body.scheduleId);
const eventTypeUser = await this.getUserToUpdateEvent(user);
const bodyTransformed = this.inputEventTypesService.transformInputUpdateEventType(body);
await updateEventType({
input: { id: eventTypeId, ...bodyTransformed },
input: { id: eventTypeId, ...body },
ctx: {
user: eventTypeUser,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
@@ -259,7 +241,10 @@ export class EventTypesService_2024_06_14 {
throw new NotFoundException(`Event type with id ${eventTypeId} not found`);
}
return this.outputEventTypesService.getResponseEventType(user.id, eventType);
return {
ownerId: user.id,
...eventType,
};
}
async checkCanUpdateEventType(userId: number, eventTypeId: number, scheduleId: number | undefined) {
@@ -1,4 +1,8 @@
import { Injectable } from "@nestjs/common";
import { ConnectedCalendarsData } from "@/ee/calendars/outputs/connected-calendars.output";
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
import { UserWithProfile } from "@/modules/users/users.repository";
import { Injectable, BadRequestException } from "@nestjs/common";
import {
transformBookingFieldsApiToInternal,
@@ -9,12 +13,85 @@ import {
systemBeforeFieldName,
systemBeforeFieldEmail,
systemAfterFieldRescheduleReason,
EventTypeMetaDataSchema,
systemBeforeFieldLocation,
transformBookerLayoutsApiToInternal,
transformConfirmationPolicyApiToInternal,
transformEventColorsApiToInternal,
validateCustomEventName,
transformSeatsApiToInternal,
} from "@calcom/platform-libraries";
import { systemBeforeFieldLocation } from "@calcom/platform-libraries";
import { CreateEventTypeInput_2024_06_14, UpdateEventTypeInput_2024_06_14 } from "@calcom/platform-types";
import {
CreateEventTypeInput_2024_06_14,
DestinationCalendar_2024_06_14,
InputEventTransformed_2024_06_14,
UpdateEventTypeInput_2024_06_14,
} from "@calcom/platform-types";
import { OutputEventTypesService_2024_06_14 } from "./output-event-types.service";
interface ValidationContext {
eventTypeId?: number;
seatsPerTimeSlot?: number | null;
locations?: InputEventTransformed_2024_06_14["locations"];
requiresConfirmation?: boolean;
eventName?: string;
}
@Injectable()
export class InputEventTypesService_2024_06_14 {
constructor(
private readonly eventTypesRepository: EventTypesRepository_2024_06_14,
private readonly outputEventTypesService: OutputEventTypesService_2024_06_14,
private readonly calendarsService: CalendarsService
) {}
async transformAndValidateCreateEventTypeInput(
userId: UserWithProfile["id"],
inputEventType: CreateEventTypeInput_2024_06_14
) {
const transformedBody = this.transformInputCreateEventType(inputEventType);
await this.validateEventTypeInputs({
seatsPerTimeSlot: transformedBody.seatsPerTimeSlot,
locations: transformedBody.locations,
requiresConfirmation: transformedBody.requiresConfirmation,
eventName: transformedBody.eventName,
});
transformedBody.destinationCalendar &&
(await this.validateInputDestinationCalendar(userId, transformedBody.destinationCalendar));
transformedBody.useEventTypeDestinationCalendarEmail &&
(await this.validateInputUseDestinationCalendarEmail(userId));
return transformedBody;
}
async transformAndValidateUpdateEventTypeInput(
inputEventType: UpdateEventTypeInput_2024_06_14,
userId: UserWithProfile["id"],
eventTypeId: number
) {
const transformedBody = await this.transformInputUpdateEventType(inputEventType, eventTypeId);
await this.validateEventTypeInputs({
eventTypeId: eventTypeId,
seatsPerTimeSlot: transformedBody.seatsPerTimeSlot,
locations: transformedBody.locations,
requiresConfirmation: transformedBody.requiresConfirmation,
eventName: transformedBody.eventName,
});
transformedBody.destinationCalendar &&
(await this.validateInputDestinationCalendar(userId, transformedBody.destinationCalendar));
transformedBody.useEventTypeDestinationCalendarEmail &&
(await this.validateInputUseDestinationCalendarEmail(userId));
return transformedBody;
}
transformInputCreateEventType(inputEventType: CreateEventTypeInput_2024_06_14) {
const defaultLocations: CreateEventTypeInput_2024_06_14["locations"] = [
{
@@ -30,9 +107,16 @@ export class InputEventTypesService_2024_06_14 {
bookingLimitsCount,
bookingLimitsDuration,
bookingWindow,
bookerLayouts,
confirmationPolicy,
color,
recurrence,
seats,
customName,
useDestinationCalendarEmail,
...rest
} = inputEventType;
const confirmationPolicyTransformed = this.transformInputConfirmationPolicy(confirmationPolicy);
const hasMultipleLocations = (locations || defaultLocations).length > 1;
const eventType = {
@@ -45,25 +129,47 @@ export class InputEventTypesService_2024_06_14 {
? this.transformInputIntervalLimits(bookingLimitsDuration)
: undefined,
...this.transformInputBookingWindow(bookingWindow),
metadata: {
bookerLayouts: this.transformInputBookerLayouts(bookerLayouts),
requiresConfirmationThreshold:
confirmationPolicyTransformed?.requiresConfirmationThreshold ?? undefined,
},
requiresConfirmation: confirmationPolicyTransformed?.requiresConfirmation ?? undefined,
requiresConfirmationWillBlockSlot:
confirmationPolicyTransformed?.requiresConfirmationWillBlockSlot ?? undefined,
eventTypeColor: this.transformInputEventTypeColor(color),
recurringEvent: recurrence ? this.transformInputRecurrignEvent(recurrence) : undefined,
...this.transformInputSeatOptions(seats),
eventName: customName,
useEventTypeDestinationCalendarEmail: useDestinationCalendarEmail,
};
return eventType;
}
transformInputUpdateEventType(inputEventType: UpdateEventTypeInput_2024_06_14) {
async transformInputUpdateEventType(inputEventType: UpdateEventTypeInput_2024_06_14, eventTypeId: number) {
const {
lengthInMinutes,
locations,
bookingFields,
scheduleId,
bookingLimitsCount,
bookingLimitsDuration,
bookingWindow,
bookerLayouts,
confirmationPolicy,
color,
recurrence,
seats,
customName,
useDestinationCalendarEmail,
...rest
} = inputEventType;
const eventTypeDb = await this.eventTypesRepository.getEventTypeWithMetaData(eventTypeId);
const metadataTransformed = !!eventTypeDb?.metadata
? EventTypeMetaDataSchema.parse(eventTypeDb.metadata)
: {};
const confirmationPolicyTransformed = this.transformInputConfirmationPolicy(confirmationPolicy);
const hasMultipleLocations = !!(locations && locations?.length > 1);
const eventType = {
@@ -73,13 +179,25 @@ export class InputEventTypesService_2024_06_14 {
bookingFields: bookingFields
? this.transformInputBookingFields(bookingFields, hasMultipleLocations)
: undefined,
schedule: scheduleId,
bookingLimits: bookingLimitsCount ? this.transformInputIntervalLimits(bookingLimitsCount) : undefined,
durationLimits: bookingLimitsDuration
? this.transformInputIntervalLimits(bookingLimitsDuration)
: undefined,
...this.transformInputBookingWindow(bookingWindow),
metadata: {
...metadataTransformed,
bookerLayouts: this.transformInputBookerLayouts(bookerLayouts),
requiresConfirmationThreshold:
confirmationPolicyTransformed?.requiresConfirmationThreshold ?? undefined,
},
recurringEvent: recurrence ? this.transformInputRecurrignEvent(recurrence) : undefined,
requiresConfirmation: confirmationPolicyTransformed?.requiresConfirmation ?? undefined,
requiresConfirmationWillBlockSlot:
confirmationPolicyTransformed?.requiresConfirmationWillBlockSlot ?? undefined,
eventTypeColor: this.transformInputEventTypeColor(color),
...this.transformInputSeatOptions(seats),
eventName: customName,
useEventTypeDestinationCalendarEmail: useDestinationCalendarEmail,
};
return eventType;
@@ -114,7 +232,142 @@ export class InputEventTypesService_2024_06_14 {
return !!res ? res : {};
}
transformInputBookerLayouts(inputBookerLayouts: CreateEventTypeInput_2024_06_14["bookerLayouts"]) {
return transformBookerLayoutsApiToInternal(inputBookerLayouts);
}
transformInputConfirmationPolicy(
requiresConfirmation: CreateEventTypeInput_2024_06_14["confirmationPolicy"]
) {
return transformConfirmationPolicyApiToInternal(requiresConfirmation);
}
transformInputRecurrignEvent(recurrence: CreateEventTypeInput_2024_06_14["recurrence"]) {
return transformRecurrenceApiToInternal(recurrence);
}
transformInputEventTypeColor(color: CreateEventTypeInput_2024_06_14["color"]) {
return transformEventColorsApiToInternal(color);
}
transformInputSeatOptions(seats: CreateEventTypeInput_2024_06_14["seats"]) {
return transformSeatsApiToInternal(seats);
}
async validateEventTypeInputs({
eventTypeId,
seatsPerTimeSlot,
locations,
requiresConfirmation,
eventName,
}: ValidationContext) {
let seatsPerTimeSlotDb: number | null = null;
let locationsDb: ReturnType<typeof this.transformInputLocations> = [];
let requiresConfirmationDb = false;
if (eventTypeId != null) {
const eventTypeDb = await this.eventTypesRepository.getEventTypeWithSeats(eventTypeId);
seatsPerTimeSlotDb = eventTypeDb?.seatsPerTimeSlot ?? null;
locationsDb = this.outputEventTypesService.transformLocations(eventTypeDb?.locations) ?? [];
requiresConfirmationDb = eventTypeDb?.requiresConfirmation ?? false;
}
const seatsPerTimeSlotFinal = seatsPerTimeSlot !== undefined ? seatsPerTimeSlot : seatsPerTimeSlotDb;
const seatsEnabledFinal = seatsPerTimeSlotFinal != null && seatsPerTimeSlotFinal > 0;
const locationsFinal = locations !== undefined ? locations : locationsDb;
const requiresConfirmationFinal =
requiresConfirmation !== undefined ? requiresConfirmation : requiresConfirmationDb;
this.validateSeatsSingleLocationRule(seatsEnabledFinal, locationsFinal);
this.validateSeatsRequiresConfirmationFalseRule(seatsEnabledFinal, requiresConfirmationFinal);
this.validateMultipleLocationsSeatsDisabledRule(locationsFinal, seatsEnabledFinal);
this.validateRequiresConfirmationSeatsDisabledRule(requiresConfirmationFinal, seatsEnabledFinal);
if (eventName) {
await this.validateCustomEventNameInput(eventName);
}
}
validateSeatsSingleLocationRule(
seatsEnabled: boolean,
locations: ReturnType<typeof this.transformInputLocations>
) {
if (seatsEnabled && locations.length > 1) {
throw new BadRequestException(
"Seats Validation failed: Seats are enabled but more than one location provided."
);
}
}
validateSeatsRequiresConfirmationFalseRule(seatsEnabled: boolean, requiresConfirmation: boolean) {
if (seatsEnabled && requiresConfirmation) {
throw new BadRequestException(
"Seats Validation failed: Seats are enabled but requiresConfirmation is true."
);
}
}
validateMultipleLocationsSeatsDisabledRule(
locations: ReturnType<typeof this.transformInputLocations>,
seatsEnabled: boolean
) {
if (locations.length > 1 && seatsEnabled) {
throw new BadRequestException("Locations Validation failed: Multiple locations but seats are enabled.");
}
}
validateRequiresConfirmationSeatsDisabledRule(requiresConfirmation: boolean, seatsEnabled: boolean) {
if (requiresConfirmation && seatsEnabled) {
throw new BadRequestException(
"RequiresConfirmation Validation failed: Seats are enabled but requiresConfirmation is true."
);
}
}
async validateCustomEventNameInput(value: string) {
const validationResult = validateCustomEventName(value);
if (validationResult !== true) {
throw new BadRequestException(`Invalid event name variables: ${validationResult}`);
}
return;
}
async validateInputDestinationCalendar(
userId: number,
destinationCalendar: DestinationCalendar_2024_06_14
) {
const calendars: ConnectedCalendarsData = await this.calendarsService.getCalendars(userId);
const allCals = calendars.connectedCalendars.map((cal) => cal.calendars ?? []).flat();
const matchedCalendar = allCals.find(
(cal) =>
cal.externalId === destinationCalendar.externalId &&
cal.integration === destinationCalendar.integration
);
if (!matchedCalendar) {
throw new BadRequestException("Invalid destinationCalendarId: Calendar does not exist");
}
if (matchedCalendar.readOnly) {
throw new BadRequestException("Invalid destinationCalendarId: Calendar does not have write permission");
}
return;
}
async validateInputUseDestinationCalendarEmail(userId: number) {
const calendars: ConnectedCalendarsData = await this.calendarsService.getCalendars(userId);
const allCals = calendars.connectedCalendars.map((cal) => cal.calendars ?? []).flat();
const primaryCalendar = allCals.find((cal) => cal.primary);
if (!primaryCalendar) {
throw new BadRequestException(
"Validation failed: A primary connected calendar is required to set useDestinationCalendarEmail"
);
}
return;
}
}
@@ -1,5 +1,5 @@
import { Injectable } from "@nestjs/common";
import type { EventType, User, Schedule } from "@prisma/client";
import type { EventType, User, Schedule, DestinationCalendar } from "@prisma/client";
import {
EventTypeMetaDataSchema,
@@ -15,11 +15,25 @@ import {
transformIntervalLimitsInternalToApi,
transformFutureBookingLimitsInternalToApi,
transformRecurrenceInternalToApi,
transformBookerLayoutsInternalToApi,
transformRequiresConfirmationInternalToApi,
transformEventTypeColorsInternalToApi,
parseEventTypeColor,
transformSeatsInternalToApi,
} from "@calcom/platform-libraries";
import { TransformFutureBookingsLimitSchema_2024_06_14 } from "@calcom/platform-types";
import {
TransformFutureBookingsLimitSchema_2024_06_14,
BookerLayoutsTransformedSchema,
NoticeThresholdTransformedSchema,
EventTypeOutput_2024_06_14,
} from "@calcom/platform-types";
type EventTypeRelations = { users: User[]; schedule: Schedule | null };
type DatabaseEventType = EventType & EventTypeRelations;
type EventTypeRelations = {
users: User[];
schedule: Schedule | null;
destinationCalendar?: DestinationCalendar | null;
};
export type DatabaseEventType = EventType & EventTypeRelations;
type Input = Pick<
DatabaseEventType,
@@ -58,11 +72,19 @@ type Input = Pick<
| "periodCountCalendarDays"
| "periodStartDate"
| "periodEndDate"
| "requiresBookerEmailVerification"
| "hideCalendarNotes"
| "eventTypeColor"
| "seatsShowAttendees"
| "requiresConfirmationWillBlockSlot"
| "eventName"
| "destinationCalendar"
| "useEventTypeDestinationCalendarEmail"
>;
@Injectable()
export class OutputEventTypesService_2024_06_14 {
async getResponseEventType(ownerId: number, databaseEventType: Input) {
getResponseEventType(ownerId: number, databaseEventType: Input): EventTypeOutput_2024_06_14 {
const {
id,
length,
@@ -74,8 +96,6 @@ export class OutputEventTypesService_2024_06_14 {
beforeEventBuffer,
afterEventBuffer,
slug,
schedulingType,
requiresConfirmation,
price,
currency,
lockTimeZoneToggleOnBookingPage,
@@ -87,17 +107,34 @@ export class OutputEventTypesService_2024_06_14 {
scheduleId,
onlyShowFirstAvailableSlot,
offsetStart,
requiresBookerEmailVerification,
hideCalendarNotes,
seatsShowAttendees,
useEventTypeDestinationCalendarEmail,
} = databaseEventType;
const locations = this.transformLocations(databaseEventType.locations);
const customName = databaseEventType?.eventName ?? undefined;
const bookingFields = databaseEventType.bookingFields
? this.transformBookingFields(BookingFieldsSchema.parse(databaseEventType.bookingFields))
: [];
const recurrence = this.transformRecurringEvent(databaseEventType.recurringEvent);
const metadata = this.transformMetadata(databaseEventType.metadata) || {};
const users = this.transformUsers(databaseEventType.users);
const users = this.transformUsers(databaseEventType.users || []);
const bookingLimitsCount = this.transformIntervalLimits(databaseEventType.bookingLimits);
const bookingLimitsDuration = this.transformIntervalLimits(databaseEventType.durationLimits);
const color = this.transformEventTypeColor(databaseEventType.eventTypeColor);
const bookerLayouts = this.transformBookerLayouts(
metadata.bookerLayouts as unknown as BookerLayoutsTransformedSchema
);
const confirmationPolicy = this.transformRequiresConfirmation(
!!databaseEventType.requiresConfirmation,
!!databaseEventType.requiresConfirmationWillBlockSlot,
metadata.requiresConfirmationThreshold as NoticeThresholdTransformedSchema
);
delete metadata["bookerLayouts"];
delete metadata["requiresConfirmationThreshold"];
const seats = this.transformSeats(seatsPerTimeSlot, seatsShowAttendees, seatsShowAvailabilityCount);
const bookingWindow = this.transformBookingWindow({
periodType: databaseEventType.periodType,
periodDays: databaseEventType.periodDays,
@@ -105,6 +142,7 @@ export class OutputEventTypesService_2024_06_14 {
periodStartDate: databaseEventType.periodStartDate,
periodEndDate: databaseEventType.periodEndDate,
} as TransformFutureBookingsLimitSchema_2024_06_14);
const destinationCalendar = this.transformDestinationCalendar(databaseEventType.destinationCalendar);
return {
id,
@@ -121,16 +159,12 @@ export class OutputEventTypesService_2024_06_14 {
minimumBookingNotice,
beforeEventBuffer,
afterEventBuffer,
schedulingType,
metadata,
requiresConfirmation,
price,
currency,
lockTimeZoneToggleOnBookingPage,
seatsPerTimeSlot,
forwardParamsSuccessRedirect,
successRedirectUrl,
seatsShowAvailabilityCount,
isInstantEvent,
users,
scheduleId,
@@ -139,6 +173,15 @@ export class OutputEventTypesService_2024_06_14 {
onlyShowFirstAvailableSlot,
offsetStart,
bookingWindow,
bookerLayouts,
confirmationPolicy,
requiresBookerEmailVerification,
hideCalendarNotes,
color,
seats,
customName,
destinationCalendar,
useDestinationCalendarEmail: useEventTypeDestinationCalendarEmail,
};
}
@@ -147,6 +190,14 @@ export class OutputEventTypesService_2024_06_14 {
return transformLocationsInternalToApi(TransformedLocationsSchema.parse(locations));
}
transformDestinationCalendar(destinationCalendar?: DestinationCalendar | null) {
if (!destinationCalendar) return undefined;
return {
integration: destinationCalendar.integration,
externalId: destinationCalendar.externalId,
};
}
transformBookingFields(bookingFields: (SystemField | CustomField)[] | null) {
if (!bookingFields) return [];
@@ -189,4 +240,39 @@ export class OutputEventTypesService_2024_06_14 {
transformBookingWindow(bookingLimits: TransformFutureBookingsLimitSchema_2024_06_14) {
return transformFutureBookingLimitsInternalToApi(bookingLimits);
}
transformBookerLayouts(bookerLayouts: BookerLayoutsTransformedSchema) {
if (!bookerLayouts) return undefined;
return transformBookerLayoutsInternalToApi(bookerLayouts);
}
transformRequiresConfirmation(
requiresConfirmation: boolean,
requiresConfirmationWillBlockSlot: boolean,
requiresConfirmationThreshold?: NoticeThresholdTransformedSchema
) {
return transformRequiresConfirmationInternalToApi(
requiresConfirmation,
requiresConfirmationWillBlockSlot,
requiresConfirmationThreshold
);
}
transformEventTypeColor(eventTypeColor: any) {
if (!eventTypeColor) return undefined;
const parsedeventTypeColor = parseEventTypeColor(eventTypeColor);
return transformEventTypeColorsInternalToApi(parsedeventTypeColor);
}
transformSeats(
seatsPerTimeSlot: number | null,
seatsShowAttendees: boolean | null,
seatsShowAvailabilityCount: boolean | null
) {
return transformSeatsInternalToApi({
seatsPerTimeSlot,
seatsShowAttendees: !!seatsShowAttendees,
seatsShowAvailabilityCount: !!seatsShowAvailabilityCount,
});
}
}
@@ -1,5 +1,6 @@
import { CreatePhoneCallInput } from "@/ee/event-types/event-types_2024_06_14/inputs/create-phone-call.input";
import { CreatePhoneCallOutput } from "@/ee/event-types/event-types_2024_06_14/outputs/create-phone-call.output";
import { InputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/input-event-types.service";
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator";
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
@@ -15,7 +16,10 @@ import { DeleteTeamEventTypeOutput } from "@/modules/organizations/controllers/e
import { GetTeamEventTypeOutput } from "@/modules/organizations/controllers/event-types/outputs/teams/get-team-event-type.output";
import { GetTeamEventTypesOutput } from "@/modules/organizations/controllers/event-types/outputs/teams/get-team-event-types.output";
import { UpdateTeamEventTypeOutput } from "@/modules/organizations/controllers/event-types/outputs/teams/update-team-event-type.output";
import { OutputTeamEventTypesResponsePipe } from "@/modules/organizations/controllers/pipes/event-types/team-event-types-response.transformer";
import { InputOrganizationsEventTypesService } from "@/modules/organizations/services/event-types/input.service";
import { OrganizationsEventTypesService } from "@/modules/organizations/services/event-types/organizations-event-types.service";
import { DatabaseTeamEventType } from "@/modules/organizations/services/event-types/output.service";
import { UserWithProfile } from "@/modules/users/users.repository";
import {
Controller,
@@ -34,22 +38,33 @@ import {
} from "@nestjs/common";
import { ApiTags as DocsTags } from "@nestjs/swagger";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import { ERROR_STATUS, SUCCESS_STATUS } from "@calcom/platform-constants";
import { handleCreatePhoneCall } from "@calcom/platform-libraries";
import {
CreateTeamEventTypeInput_2024_06_14,
GetTeamEventTypesQuery_2024_06_14,
SkipTakePagination,
TeamEventTypeOutput_2024_06_14,
UpdateTeamEventTypeInput_2024_06_14,
} from "@calcom/platform-types";
export type EventTypeHandlerResponse = {
data: DatabaseTeamEventType[] | DatabaseTeamEventType;
status: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
};
@Controller({
path: "/v2/organizations/:orgId",
version: API_VERSIONS_VALUES,
})
@DocsTags("Organizations Event Types")
export class OrganizationsEventTypesController {
constructor(private readonly organizationsEventTypesService: OrganizationsEventTypesService) {}
constructor(
private readonly organizationsEventTypesService: OrganizationsEventTypesService,
private readonly inputService: InputOrganizationsEventTypesService,
private readonly inputUserEventTypesService: InputEventTypesService_2024_06_14,
private readonly outputTeamEventTypesResponsePipe: OutputTeamEventTypesResponsePipe
) {}
@Roles("TEAM_ADMIN")
@PlatformPlan("ESSENTIALS")
@@ -61,16 +76,22 @@ export class OrganizationsEventTypesController {
@Param("orgId", ParseIntPipe) orgId: number,
@Body() bodyEventType: CreateTeamEventTypeInput_2024_06_14
): Promise<CreateTeamEventTypeOutput> {
const transformedBody = await this.inputService.transformAndValidateCreateTeamEventTypeInput(
user.id,
teamId,
bodyEventType
);
const eventType = await this.organizationsEventTypesService.createTeamEventType(
user,
teamId,
orgId,
bodyEventType
transformedBody
);
return {
status: SUCCESS_STATUS,
data: eventType,
data: await this.outputTeamEventTypesResponsePipe.transform(eventType),
};
}
@@ -90,7 +111,9 @@ export class OrganizationsEventTypesController {
return {
status: SUCCESS_STATUS,
data: eventType,
data: (await this.outputTeamEventTypesResponsePipe.transform(
eventType
)) as TeamEventTypeOutput_2024_06_14,
};
}
@@ -125,12 +148,13 @@ export class OrganizationsEventTypesController {
@Query() queryParams: GetTeamEventTypesQuery_2024_06_14
): Promise<GetTeamEventTypesOutput> {
const { eventSlug } = queryParams;
if (eventSlug) {
const eventType = await this.organizationsEventTypesService.getTeamEventTypeBySlug(teamId, eventSlug);
return {
status: SUCCESS_STATUS,
data: eventType ? [eventType] : [],
data: await this.outputTeamEventTypesResponsePipe.transform(eventType ? [eventType] : []),
};
}
@@ -138,7 +162,7 @@ export class OrganizationsEventTypesController {
return {
status: SUCCESS_STATUS,
data: eventTypes,
data: await this.outputTeamEventTypesResponsePipe.transform(eventTypes),
};
}
@@ -155,7 +179,7 @@ export class OrganizationsEventTypesController {
return {
status: SUCCESS_STATUS,
data: eventTypes,
data: await this.outputTeamEventTypesResponsePipe.transform(eventTypes),
};
}
@@ -169,16 +193,23 @@ export class OrganizationsEventTypesController {
@GetUser() user: UserWithProfile,
@Body() bodyEventType: UpdateTeamEventTypeInput_2024_06_14
): Promise<UpdateTeamEventTypeOutput> {
const transformedBody = await this.inputService.transformAndValidateUpdateTeamEventTypeInput(
user.id,
eventTypeId,
teamId,
bodyEventType
);
const eventType = await this.organizationsEventTypesService.updateTeamEventType(
eventTypeId,
teamId,
bodyEventType,
transformedBody,
user
);
return {
status: SUCCESS_STATUS,
data: eventType,
data: await this.outputTeamEventTypesResponsePipe.transform(eventType),
};
}
@@ -189,7 +220,7 @@ export class OrganizationsEventTypesController {
@HttpCode(HttpStatus.OK)
async deleteTeamEventType(
@Param("teamId", ParseIntPipe) teamId: number,
@Param("eventTypeId") eventTypeId: number
@Param("eventTypeId", ParseIntPipe) eventTypeId: number
): Promise<DeleteTeamEventTypeOutput> {
const eventType = await this.organizationsEventTypesService.deleteTeamEventType(teamId, eventTypeId);
@@ -17,7 +17,12 @@ import { UserRepositoryFixture } from "test/fixtures/repository/users.repository
import { withApiAuth } from "test/utils/withApiAuth";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import { BookingWindowPeriodInputTypeEnum_2024_06_14 } from "@calcom/platform-enums";
import {
BookingWindowPeriodInputTypeEnum_2024_06_14,
BookerLayoutsInputEnum_2024_06_14,
ConfirmationPolicyEnum,
NoticeThresholdUnitEnum,
} from "@calcom/platform-enums";
import {
ApiSuccessResponse,
CreateTeamEventTypeInput_2024_06_14,
@@ -276,6 +281,30 @@ describe("Organizations Event Types Endpoints", () => {
value: 30,
rolling: true,
},
bookerLayouts: {
enabledLayouts: [
BookerLayoutsInputEnum_2024_06_14.column,
BookerLayoutsInputEnum_2024_06_14.month,
BookerLayoutsInputEnum_2024_06_14.week,
],
defaultLayout: BookerLayoutsInputEnum_2024_06_14.month,
},
confirmationPolicy: {
type: ConfirmationPolicyEnum.TIME,
noticeThreshold: {
count: 60,
unit: NoticeThresholdUnitEnum.MINUTES,
},
blockUnconfirmedBookingsInBooker: true,
},
requiresBookerEmailVerification: true,
hideCalendarNotes: true,
lockTimeZoneToggleOnBookingPage: true,
color: {
darkThemeHex: "#292929",
lightThemeHex: "#fafafa",
},
};
return request(app.getHttpServer())
@@ -297,6 +326,12 @@ describe("Organizations Event Types Endpoints", () => {
expect(data.bookingLimitsDuration).toEqual(body.bookingLimitsDuration);
expect(data.offsetStart).toEqual(body.offsetStart);
expect(data.bookingWindow).toEqual(body.bookingWindow);
expect(data.bookerLayouts).toEqual(body.bookerLayouts);
expect(data.confirmationPolicy).toEqual(body.confirmationPolicy);
expect(data.requiresBookerEmailVerification).toEqual(body.requiresBookerEmailVerification);
expect(data.hideCalendarNotes).toEqual(body.hideCalendarNotes);
expect(data.lockTimeZoneToggleOnBookingPage).toEqual(body.lockTimeZoneToggleOnBookingPage);
expect(data.color).toEqual(body.color);
collectiveEventType = responseBody.data;
});
@@ -444,7 +479,7 @@ describe("Organizations Event Types Endpoints", () => {
return request(app.getHttpServer())
.patch(`/v2/organizations/${org.id}/teams/${team.id}/event-types/999999`)
.send(body)
.expect(404);
.expect(400);
});
it("should update collective event-type", async () => {
@@ -0,0 +1,38 @@
import { OutputOrganizationsEventTypesService } from "@/modules/organizations/services/event-types/output.service";
import { DatabaseTeamEventType } from "@/modules/organizations/services/event-types/output.service";
import { Injectable, PipeTransform } from "@nestjs/common";
import { plainToClass } from "class-transformer";
import { TeamEventTypeOutput_2024_06_14 } from "@calcom/platform-types";
@Injectable()
export class OutputTeamEventTypesResponsePipe implements PipeTransform {
constructor(private readonly outputOrganizationsEventTypesService: OutputOrganizationsEventTypesService) {}
private async transformEventType(item: DatabaseTeamEventType): Promise<TeamEventTypeOutput_2024_06_14> {
return plainToClass(
TeamEventTypeOutput_2024_06_14,
await this.outputOrganizationsEventTypesService.getResponseTeamEventType(item),
{ strategy: "exposeAll" }
);
}
// Implementing function overloading to ensure correct return types based on input type:
async transform(value: DatabaseTeamEventType[]): Promise<TeamEventTypeOutput_2024_06_14[]>;
async transform(value: DatabaseTeamEventType): Promise<TeamEventTypeOutput_2024_06_14>;
async transform(
value: DatabaseTeamEventType | DatabaseTeamEventType[]
): Promise<TeamEventTypeOutput_2024_06_14 | TeamEventTypeOutput_2024_06_14[]>;
async transform(
value: DatabaseTeamEventType | DatabaseTeamEventType[]
): Promise<TeamEventTypeOutput_2024_06_14 | TeamEventTypeOutput_2024_06_14[]> {
if (Array.isArray(value)) {
return await Promise.all(value.map((item) => this.transformEventType(item)));
} else {
return await this.transformEventType(value);
}
}
}
@@ -7,6 +7,7 @@ import { OrganizationsOptionsAttributesController } from "@/modules/organization
import { OrganizationsAttributesController } from "@/modules/organizations/controllers/attributes/organizations-attributes.controller";
import { OrganizationsEventTypesController } from "@/modules/organizations/controllers/event-types/organizations-event-types.controller";
import { OrganizationsMembershipsController } from "@/modules/organizations/controllers/memberships/organizations-membership.controller";
import { OutputTeamEventTypesResponsePipe } from "@/modules/organizations/controllers/pipes/event-types/team-event-types-response.transformer";
import { OrganizationsSchedulesController } from "@/modules/organizations/controllers/schedules/organizations-schedules.controller";
import { OrganizationsTeamsMembershipsController } from "@/modules/organizations/controllers/teams/memberships/organizations-teams-memberships.controller";
import { OrganizationsTeamsController } from "@/modules/organizations/controllers/teams/organizations-teams.controller";
@@ -79,6 +80,7 @@ import { Module } from "@nestjs/common";
OrganizationsWebhooksService,
WebhooksRepository,
WebhooksService,
OutputTeamEventTypesResponsePipe,
],
exports: [
OrganizationsService,
@@ -12,7 +12,7 @@ export class OrganizationsEventTypesRepository {
id: eventTypeId,
teamId,
},
include: { users: true, schedule: true, hosts: true },
include: { users: true, schedule: true, hosts: true, destinationCalendar: true },
});
}
@@ -24,7 +24,7 @@ export class OrganizationsEventTypesRepository {
slug: eventTypeSlug,
},
},
include: { users: true, schedule: true, hosts: true },
include: { users: true, schedule: true, hosts: true, destinationCalendar: true },
});
}
@@ -33,21 +33,21 @@ export class OrganizationsEventTypesRepository {
where: {
teamId,
},
include: { users: true, schedule: true, hosts: true },
include: { users: true, schedule: true, hosts: true, destinationCalendar: true },
});
}
async getEventTypeById(eventTypeId: number) {
return this.dbRead.prisma.eventType.findUnique({
where: { id: eventTypeId },
include: { users: true, schedule: true, hosts: true },
include: { users: true, schedule: true, hosts: true, destinationCalendar: true },
});
}
async getEventTypeChildren(eventTypeId: number) {
return this.dbRead.prisma.eventType.findMany({
where: { parentId: eventTypeId },
include: { users: true, schedule: true, hosts: true },
include: { users: true, schedule: true, hosts: true, destinationCalendar: true },
});
}
@@ -60,7 +60,7 @@ export class OrganizationsEventTypesRepository {
},
skip,
take,
include: { users: true, schedule: true, hosts: true },
include: { users: true, schedule: true, hosts: true, destinationCalendar: true },
});
}
@@ -2,7 +2,7 @@ import { InputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types_
import { OrganizationsEventTypesRepository } from "@/modules/organizations/repositories/organizations-event-types.repository";
import { OrganizationsTeamsRepository } from "@/modules/organizations/repositories/organizations-teams.repository";
import { UsersRepository } from "@/modules/users/users.repository";
import { BadRequestException, Injectable } from "@nestjs/common";
import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common";
import {
CreateTeamEventTypeInput_2024_06_14,
@@ -19,37 +19,89 @@ export class InputOrganizationsEventTypesService {
private readonly usersRepository: UsersRepository,
private readonly orgEventTypesRepository: OrganizationsEventTypesRepository
) {}
async transformAndValidateCreateTeamEventTypeInput(
userId: number,
teamId: number,
inputEventType: CreateTeamEventTypeInput_2024_06_14
) {
await this.validateHosts(teamId, inputEventType.hosts);
const transformedBody = await this.transformInputCreateTeamEventType(teamId, inputEventType);
await this.inputEventTypesService.validateEventTypeInputs({
seatsPerTimeSlot: transformedBody.seatsPerTimeSlot,
locations: transformedBody.locations,
requiresConfirmation: transformedBody.requiresConfirmation,
eventName: transformedBody.eventName,
});
transformedBody.destinationCalendar &&
(await this.inputEventTypesService.validateInputDestinationCalendar(
userId,
transformedBody.destinationCalendar
));
transformedBody.useEventTypeDestinationCalendarEmail &&
(await this.inputEventTypesService.validateInputUseDestinationCalendarEmail(userId));
return transformedBody;
}
async transformAndValidateUpdateTeamEventTypeInput(
userId: number,
eventTypeId: number,
teamId: number,
inputEventType: UpdateTeamEventTypeInput_2024_06_14
) {
await this.validateHosts(teamId, inputEventType.hosts);
const transformedBody = await this.transformInputUpdateTeamEventType(eventTypeId, teamId, inputEventType);
await this.inputEventTypesService.validateEventTypeInputs({
eventTypeId: eventTypeId,
seatsPerTimeSlot: transformedBody.seatsPerTimeSlot,
locations: transformedBody.locations,
requiresConfirmation: transformedBody.requiresConfirmation,
eventName: transformedBody.eventName,
});
transformedBody.destinationCalendar &&
(await this.inputEventTypesService.validateInputDestinationCalendar(
userId,
transformedBody.destinationCalendar
));
transformedBody.useEventTypeDestinationCalendarEmail &&
(await this.inputEventTypesService.validateInputUseDestinationCalendarEmail(userId));
return transformedBody;
}
async transformInputCreateTeamEventType(
teamId: number,
inputEventType: CreateTeamEventTypeInput_2024_06_14
) {
const {
hosts,
assignAllTeamMembers,
bookingLimitsCount,
bookingLimitsDuration,
bookingWindow,
bookingFields,
recurrence,
...rest
} = inputEventType;
const { hosts, assignAllTeamMembers, ...rest } = inputEventType;
const eventType = this.inputEventTypesService.transformInputCreateEventType(rest);
const children = await this.getChildEventTypesForManagedEventType(null, inputEventType, teamId);
const metadata = rest.schedulingType === "MANAGED" ? { managedEventConfig: {} } : undefined;
const metadata =
rest.schedulingType === "MANAGED"
? { managedEventConfig: {}, ...eventType.metadata }
: eventType.metadata;
const teamEventType = {
...eventType,
hosts: assignAllTeamMembers
? await this.getAllTeamMembers(teamId, inputEventType.schedulingType)
: this.transformInputHosts(hosts, inputEventType.schedulingType),
// note(Lauris): we don't populate hosts for managed event-types because they are handled by the children
hosts: !(rest.schedulingType === "MANAGED")
? assignAllTeamMembers
? await this.getAllTeamMembers(teamId, inputEventType.schedulingType)
: this.transformInputHosts(hosts, inputEventType.schedulingType)
: undefined,
assignAllTeamMembers,
bookingLimitsCount,
bookingLimitsDuration,
bookingWindow,
bookingFields,
recurrence,
metadata,
children,
};
return teamEventType;
@@ -62,7 +114,7 @@ export class InputOrganizationsEventTypesService {
) {
const { hosts, assignAllTeamMembers, ...rest } = inputEventType;
const eventType = this.inputEventTypesService.transformInputUpdateEventType(rest);
const eventType = await this.inputEventTypesService.transformInputUpdateEventType(rest, eventTypeId);
const dbEventType = await this.orgEventTypesRepository.getTeamEventType(teamId, eventTypeId);
if (!dbEventType) {
@@ -86,14 +138,16 @@ export class InputOrganizationsEventTypesService {
}
async getChildEventTypesForManagedEventType(
eventTypeId: number,
eventTypeId: number | null,
inputEventType: UpdateTeamEventTypeInput_2024_06_14,
teamId: number
) {
const eventType = await this.orgEventTypesRepository.getEventTypeByIdWithChildren(eventTypeId);
if (!eventType || eventType.schedulingType !== "MANAGED") {
return undefined;
let eventType = null;
if (eventTypeId) {
eventType = await this.orgEventTypesRepository.getEventTypeByIdWithChildren(eventTypeId);
if (!eventType || eventType.schedulingType !== "MANAGED") {
return undefined;
}
}
const ownersIds = await this.getOwnersIdsForManagedEventType(teamId, inputEventType, eventType);
@@ -110,7 +164,7 @@ export class InputOrganizationsEventTypesService {
async getOwnersIdsForManagedEventType(
teamId: number,
inputEventType: UpdateTeamEventTypeInput_2024_06_14,
eventType: { children: { userId: number | null }[] }
eventType: { children: { userId: number | null }[] } | null
) {
if (inputEventType.assignAllTeamMembers) {
return await this.organizationsTeamsRepository.getTeamMembersIds(teamId);
@@ -122,7 +176,7 @@ export class InputOrganizationsEventTypesService {
}
// note(Lauris): when API user DOES NOT update managed event type users, but we still need existing managed event type users to know which event-types to update
return eventType.children.map((child) => child.userId).filter((id) => !!id) as number[];
return eventType?.children.map((child) => child.userId).filter((id) => !!id) as number[];
}
async getOwnersForManagedEventType(userIds: number[]) {
@@ -170,6 +224,16 @@ export class InputOrganizationsEventTypesService {
),
}));
}
async validateHosts(teamId: number, hosts: CreateTeamEventTypeInput_2024_06_14["hosts"] | undefined) {
if (hosts && hosts.length) {
const membersIds = await this.organizationsTeamsRepository.getTeamMembersIds(teamId);
const invalidHosts = hosts.filter((host) => !membersIds.includes(host.userId));
if (invalidHosts.length) {
throw new NotFoundException(`Invalid hosts: ${invalidHosts.join(", ")}`);
}
}
}
}
function getPriorityValue(priority: keyof typeof HostPriority): number {
@@ -2,31 +2,23 @@ import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_20
import { EventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/event-types.service";
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
import { OrganizationsEventTypesRepository } from "@/modules/organizations/repositories/organizations-event-types.repository";
import { OrganizationsTeamsRepository } from "@/modules/organizations/repositories/organizations-teams.repository";
import { InputOrganizationsEventTypesService } from "@/modules/organizations/services/event-types/input.service";
import { OutputOrganizationsEventTypesService } from "@/modules/organizations/services/event-types/output.service";
import { DatabaseTeamEventType } from "@/modules/organizations/services/event-types/output.service";
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { UsersService } from "@/modules/users/services/users.service";
import { UserWithProfile } from "@/modules/users/users.repository";
import { Injectable, NotFoundException } from "@nestjs/common";
import { createEventType, updateEventType } from "@calcom/platform-libraries";
import {
CreateTeamEventTypeInput_2024_06_14,
UpdateTeamEventTypeInput_2024_06_14,
} from "@calcom/platform-types";
import { InputTeamEventTransformed_2024_06_14 } from "@calcom/platform-types";
@Injectable()
export class OrganizationsEventTypesService {
constructor(
private readonly inputService: InputOrganizationsEventTypesService,
private readonly eventTypesService: EventTypesService_2024_06_14,
private readonly dbWrite: PrismaWriteService,
private readonly organizationEventTypesRepository: OrganizationsEventTypesRepository,
private readonly eventTypesRepository: EventTypesRepository_2024_06_14,
private readonly outputService: OutputOrganizationsEventTypesService,
private readonly membershipsRepository: MembershipsRepository,
private readonly organizationsTeamsRepository: OrganizationsTeamsRepository,
private readonly usersService: UsersService
) {}
@@ -34,24 +26,14 @@ export class OrganizationsEventTypesService {
user: UserWithProfile,
teamId: number,
orgId: number,
body: CreateTeamEventTypeInput_2024_06_14
) {
await this.validateHosts(teamId, body.hosts);
body: InputTeamEventTransformed_2024_06_14
): Promise<DatabaseTeamEventType | DatabaseTeamEventType[]> {
const eventTypeUser = await this.getUserToCreateTeamEvent(user, orgId);
const {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
hosts,
assignAllTeamMembers,
locations,
bookingLimitsCount,
bookingLimitsDuration,
bookingWindow,
bookingFields,
recurrence,
...rest
} = await this.inputService.transformInputCreateTeamEventType(teamId, body);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { hosts, children, destinationCalendar, ...rest } = body;
const { eventType: eventTypeCreated } = await createEventType({
input: { teamId: teamId, locations, ...rest },
input: { teamId: teamId, ...rest },
ctx: {
user: eventTypeUser,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
@@ -60,31 +42,7 @@ export class OrganizationsEventTypesService {
},
});
return await this.updateTeamEventType(
eventTypeCreated.id,
teamId,
{
hosts: body.hosts,
assignAllTeamMembers,
bookingLimitsCount,
bookingLimitsDuration,
bookingWindow,
bookingFields,
recurrence,
...rest,
},
user
);
}
async validateHosts(teamId: number, hosts: CreateTeamEventTypeInput_2024_06_14["hosts"] | undefined) {
if (hosts && hosts.length) {
const membersIds = await this.organizationsTeamsRepository.getTeamMembersIds(teamId);
const invalidHosts = hosts.filter((host) => !membersIds.includes(host.userId));
if (invalidHosts.length) {
throw new NotFoundException(`Invalid hosts: ${invalidHosts.join(", ")}`);
}
}
return this.updateTeamEventType(eventTypeCreated.id, teamId, body, user);
}
async validateEventTypeExists(teamId: number, eventTypeId: number) {
@@ -110,17 +68,17 @@ export class OrganizationsEventTypesService {
};
}
async getTeamEventType(teamId: number, eventTypeId: number) {
async getTeamEventType(teamId: number, eventTypeId: number): Promise<DatabaseTeamEventType | null> {
const eventType = await this.organizationEventTypesRepository.getTeamEventType(teamId, eventTypeId);
if (!eventType) {
return null;
}
return this.outputService.getResponseTeamEventType(eventType);
return eventType;
}
async getTeamEventTypeBySlug(teamId: number, eventTypeSlug: string) {
async getTeamEventTypeBySlug(teamId: number, eventTypeSlug: string): Promise<DatabaseTeamEventType | null> {
const eventType = await this.organizationEventTypesRepository.getTeamEventTypeBySlug(
teamId,
eventTypeSlug
@@ -130,46 +88,28 @@ export class OrganizationsEventTypesService {
return null;
}
return this.outputService.getResponseTeamEventType(eventType);
return eventType;
}
async getTeamEventTypes(teamId: number) {
const eventTypes = await this.organizationEventTypesRepository.getTeamEventTypes(teamId);
const eventTypePromises = eventTypes.map(async (eventType) => {
return await this.outputService.getResponseTeamEventType(eventType);
});
return await Promise.all(eventTypePromises);
async getTeamEventTypes(teamId: number): Promise<DatabaseTeamEventType[]> {
return await this.organizationEventTypesRepository.getTeamEventTypes(teamId);
}
async getTeamsEventTypes(orgId: number, skip = 0, take = 250) {
const eventTypes = await this.organizationEventTypesRepository.getTeamsEventTypes(orgId, skip, take);
const eventTypePromises = eventTypes.map(async (eventType) => {
return await this.outputService.getResponseTeamEventType(eventType);
});
return await Promise.all(eventTypePromises);
async getTeamsEventTypes(orgId: number, skip = 0, take = 250): Promise<DatabaseTeamEventType[]> {
return await this.organizationEventTypesRepository.getTeamsEventTypes(orgId, skip, take);
}
async updateTeamEventType(
eventTypeId: number,
teamId: number,
body: UpdateTeamEventTypeInput_2024_06_14,
body: InputTeamEventTransformed_2024_06_14,
user: UserWithProfile
) {
): Promise<DatabaseTeamEventType | DatabaseTeamEventType[]> {
await this.validateEventTypeExists(teamId, eventTypeId);
await this.validateHosts(teamId, body.hosts);
const eventTypeUser = await this.eventTypesService.getUserToUpdateEvent(user);
const bodyTransformed = await this.inputService.transformInputUpdateTeamEventType(
eventTypeId,
teamId,
body
);
await updateEventType({
input: { id: eventTypeId, ...bodyTransformed },
input: { id: eventTypeId, ...body },
ctx: {
user: eventTypeUser,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
@@ -185,18 +125,12 @@ export class OrganizationsEventTypesService {
}
if (eventType.schedulingType !== "MANAGED") {
return this.outputService.getResponseTeamEventType(eventType);
return eventType;
}
const children = await this.organizationEventTypesRepository.getEventTypeChildren(eventType.id);
const childrenEventTypes = await this.organizationEventTypesRepository.getEventTypeChildren(eventType.id);
const eventTypes = [eventType, ...children];
const eventTypePromises = eventTypes.map(async (e) => {
return await this.outputService.getResponseTeamEventType(e);
});
return await Promise.all(eventTypePromises);
return [eventType, ...childrenEventTypes];
}
async deleteTeamEventType(teamId: number, eventTypeId: number) {
@@ -2,16 +2,21 @@ import { OutputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types
import { OrganizationsEventTypesRepository } from "@/modules/organizations/repositories/organizations-event-types.repository";
import { UsersRepository } from "@/modules/users/users.repository";
import { Injectable } from "@nestjs/common";
import type { EventType, User, Schedule, Host } from "@prisma/client";
import type { EventType, User, Schedule, Host, DestinationCalendar } from "@prisma/client";
import { SchedulingType } from "@prisma/client";
import { HostPriority, TeamEventTypeResponseHost } from "@calcom/platform-types";
type EventTypeRelations = { users: User[]; schedule: Schedule | null; hosts: Host[] };
type DatabaseEventType = EventType & EventTypeRelations;
type EventTypeRelations = {
users: User[];
schedule: Schedule | null;
hosts: Host[];
destinationCalendar?: DestinationCalendar | null;
};
export type DatabaseTeamEventType = EventType & EventTypeRelations;
type Input = Pick<
DatabaseEventType,
DatabaseTeamEventType,
| "id"
| "length"
| "title"
@@ -52,6 +57,14 @@ type Input = Pick<
| "periodCountCalendarDays"
| "periodStartDate"
| "periodEndDate"
| "requiresBookerEmailVerification"
| "hideCalendarNotes"
| "lockTimeZoneToggleOnBookingPage"
| "eventTypeColor"
| "seatsShowAttendees"
| "requiresConfirmationWillBlockSlot"
| "eventName"
| "useEventTypeDestinationCalendarEmail"
>;
@Injectable()
@@ -65,7 +78,7 @@ export class OutputOrganizationsEventTypesService {
async getResponseTeamEventType(databaseEventType: Input) {
const { teamId, userId, parentId, assignAllTeamMembers } = databaseEventType;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { ownerId, users, ...rest } = await this.outputEventTypesService.getResponseEventType(
const { ownerId, users, ...rest } = this.outputEventTypesService.getResponseEventType(
0,
databaseEventType
);
@@ -80,6 +93,7 @@ export class OutputOrganizationsEventTypesService {
teamId,
ownerId: userId,
parentEventTypeId: parentId,
schedulingType: databaseEventType.schedulingType,
assignAllTeamMembers: teamId ? assignAllTeamMembers : undefined,
};
}
+263 -195
View File
@@ -4923,6 +4923,11 @@
"nullable": true,
"example": "alice"
},
"name": {
"type": "string",
"nullable": true,
"example": "alice"
},
"timeZone": {
"type": "string",
"example": "America/New_York"
@@ -4998,6 +5003,7 @@
"id",
"email",
"username",
"name",
"timeZone",
"weekStart",
"createdDate",
@@ -6079,94 +6085,6 @@
"value"
]
},
"BookingLimitsCount_2024_06_14": {
"type": "object",
"properties": {
"day": {
"type": "number",
"description": "The number of bookings per day",
"example": 1
},
"week": {
"type": "number",
"description": "The number of bookings per week",
"example": 2
},
"month": {
"type": "number",
"description": "The number of bookings per month",
"example": 3
},
"year": {
"type": "number",
"description": "The number of bookings per year",
"example": 4
}
},
"required": [
"day",
"week",
"month",
"year"
]
},
"BookingLimitsDuration_2024_06_14": {
"type": "object",
"properties": {
"day": {
"type": "number",
"minimum": 15,
"description": "The duration of bookings per day (must be a multiple of 15)",
"example": 60
},
"week": {
"type": "number",
"minimum": 15,
"description": "The duration of bookings per week (must be a multiple of 15)",
"example": 120
},
"month": {
"type": "number",
"minimum": 15,
"description": "The duration of bookings per month (must be a multiple of 15)",
"example": 180
},
"year": {
"type": "number",
"minimum": 15,
"description": "The duration of bookings per year (must be a multiple of 15)",
"example": 240
}
}
},
"Recurrence_2024_06_14": {
"type": "object",
"properties": {
"interval": {
"type": "number",
"example": 10,
"description": "Repeats every {count} week | month | year"
},
"occurrences": {
"type": "number",
"example": 10,
"description": "Repeats for a maximum of {count} events"
},
"frequency": {
"type": "string",
"enum": [
"yearly",
"monthly",
"weekly"
]
}
},
"required": [
"interval",
"occurrences",
"frequency"
]
},
"CreateEventTypeInput_2024_06_14": {
"type": "object",
"properties": {
@@ -6272,24 +6190,16 @@
"description": "If you want that this event has different schedule than user's default one you can specify it here."
},
"bookingLimitsCount": {
"description": "Limit how many times this event can be booked",
"allOf": [
{
"$ref": "#/components/schemas/BookingLimitsCount_2024_06_14"
}
]
"type": "object",
"description": "Limit how many times this event can be booked"
},
"onlyShowFirstAvailableSlot": {
"type": "boolean",
"description": "This will limit your availability for this event type to one slot per day, scheduled at the earliest available time."
},
"bookingLimitsDuration": {
"description": "Limit total amount of time that this event can be booked",
"allOf": [
{
"$ref": "#/components/schemas/BookingLimitsDuration_2024_06_14"
}
]
"type": "object",
"description": "Limit total amount of time that this event can be booked"
},
"bookingWindow": {
"type": "array",
@@ -6312,13 +6222,10 @@
"type": "number",
"description": "Offset timeslots shown to bookers by a specified number of minutes"
},
"recurrence": {
"description": "Create a recurring event that can be booked once but will occur multiple times",
"allOf": [
{
"$ref": "#/components/schemas/Recurrence_2024_06_14"
}
]
"customName": {
"type": "string",
"description": "Customizable event name with valid variables: \n {Event type title}, {Organiser}, {Scheduler}, {Location}, {Organiser first name}, \n {Scheduler first name}, {Scheduler last name}, {Event duration}, {LOCATION}, \n {HOST/ATTENDEE}, {HOST}, {ATTENDEE}, {USER}",
"example": "{Event type title} between {Organiser} and {Scheduler}"
}
},
"required": [
@@ -6339,7 +6246,7 @@
"bookingLimitsDuration",
"bookingWindow",
"offsetStart",
"recurrence"
"customName"
]
},
"EmailDefaultFieldOutput_2024_06_14": {
@@ -7136,6 +7043,104 @@
"isDefault"
]
},
"BaseBookingLimitsDuration_2024_06_14": {
"type": "object",
"properties": {
"day": {
"type": "number",
"minimum": 15,
"description": "The duration of bookings per day (must be a multiple of 15)",
"example": 60
},
"week": {
"type": "number",
"minimum": 15,
"description": "The duration of bookings per week (must be a multiple of 15)",
"example": 120
},
"month": {
"type": "number",
"minimum": 15,
"description": "The duration of bookings per month (must be a multiple of 15)",
"example": 180
},
"year": {
"type": "number",
"minimum": 15,
"description": "The duration of bookings per year (must be a multiple of 15)",
"example": 240
},
"disabled": {
"type": "boolean",
"default": false
}
}
},
"BookerLayouts_2024_06_14": {
"type": "object",
"properties": {}
},
"EventTypeColor_2024_06_14": {
"type": "object",
"properties": {
"lightThemeHex": {
"type": "string",
"description": "Color used for event types in light theme",
"example": "#292929"
},
"darkThemeHex": {
"type": "string",
"description": "Color used for event types in dark theme",
"example": "#fafafa"
}
},
"required": [
"lightThemeHex",
"darkThemeHex"
]
},
"Seats_2024_06_14": {
"type": "object",
"properties": {
"seatsPerTimeSlot": {
"type": "number",
"description": "Number of seats available per time slot",
"example": 4
},
"showAttendeeInfo": {
"type": "boolean",
"description": "Show attendee information to other guests",
"example": true
},
"showAvailabilityCount": {
"type": "boolean",
"description": "Display the count of available seats",
"example": true
}
},
"required": [
"seatsPerTimeSlot",
"showAttendeeInfo",
"showAvailabilityCount"
]
},
"DestinationCalendar_2024_06_14": {
"type": "object",
"properties": {
"integration": {
"type": "string",
"description": "The integration type of the destination calendar. Refer to the /api/v2/calendars endpoint to retrieve the integration type of your connected calendars."
},
"externalId": {
"type": "string",
"description": "The external ID of the destination calendar. Refer to the /api/v2/calendars endpoint to retrieve the external IDs of your connected calendars."
}
},
"required": [
"integration",
"externalId"
]
},
"EventTypeOutput_2024_06_14": {
"type": "object",
"properties": {
@@ -7157,7 +7162,7 @@
},
"description": {
"type": "string",
"example": "Discover the culinary wonders of the Argentina by making the best flan ever!"
"example": "Discover the culinary wonders of Argentina by making the best flan ever!"
},
"locations": {
"type": "array",
@@ -7264,9 +7269,6 @@
"metadata": {
"type": "object"
},
"requiresConfirmation": {
"type": "boolean"
},
"price": {
"type": "number"
},
@@ -7285,6 +7287,9 @@
"successRedirectUrl": {
"type": "object"
},
"isInstantEvent": {
"type": "boolean"
},
"seatsShowAvailabilityCount": {
"type": "object"
},
@@ -7292,13 +7297,13 @@
"type": "object"
},
"bookingLimitsCount": {
"$ref": "#/components/schemas/BookingLimitsCount_2024_06_14"
"type": "object"
},
"onlyShowFirstAvailableSlot": {
"type": "boolean"
},
"bookingLimitsDuration": {
"$ref": "#/components/schemas/BookingLimitsDuration_2024_06_14"
"type": "object"
},
"bookingWindow": {
"type": "array",
@@ -7317,9 +7322,36 @@
]
}
},
"bookerLayouts": {
"$ref": "#/components/schemas/BookerLayouts_2024_06_14"
},
"confirmationPolicy": {
"type": "object"
},
"requiresBookerEmailVerification": {
"type": "boolean"
},
"hideCalendarNotes": {
"type": "boolean"
},
"color": {
"$ref": "#/components/schemas/EventTypeColor_2024_06_14"
},
"seats": {
"$ref": "#/components/schemas/Seats_2024_06_14"
},
"offsetStart": {
"type": "number"
},
"customName": {
"type": "string"
},
"destinationCalendar": {
"$ref": "#/components/schemas/DestinationCalendar_2024_06_14"
},
"useDestinationCalendarEmail": {
"type": "boolean"
},
"ownerId": {
"type": "number",
"example": 10
@@ -7346,20 +7378,29 @@
"afterEventBuffer",
"recurrence",
"metadata",
"requiresConfirmation",
"price",
"currency",
"lockTimeZoneToggleOnBookingPage",
"seatsPerTimeSlot",
"forwardParamsSuccessRedirect",
"successRedirectUrl",
"isInstantEvent",
"seatsShowAvailabilityCount",
"scheduleId",
"bookingLimitsCount",
"onlyShowFirstAvailableSlot",
"bookingLimitsDuration",
"bookingWindow",
"bookerLayouts",
"confirmationPolicy",
"requiresBookerEmailVerification",
"hideCalendarNotes",
"color",
"seats",
"offsetStart",
"customName",
"destinationCalendar",
"useDestinationCalendarEmail",
"ownerId",
"users"
]
@@ -7537,24 +7578,16 @@
"description": "If you want that this event has different schedule than user's default one you can specify it here."
},
"bookingLimitsCount": {
"description": "Limit how many times this event can be booked",
"allOf": [
{
"$ref": "#/components/schemas/BookingLimitsCount_2024_06_14"
}
]
"type": "object",
"description": "Limit how many times this event can be booked"
},
"onlyShowFirstAvailableSlot": {
"type": "boolean",
"description": "This will limit your availability for this event type to one slot per day, scheduled at the earliest available time."
},
"bookingLimitsDuration": {
"description": "Limit total amount of time that this event can be booked",
"allOf": [
{
"$ref": "#/components/schemas/BookingLimitsDuration_2024_06_14"
}
]
"type": "object",
"description": "Limit total amount of time that this event can be booked"
},
"bookingWindow": {
"type": "array",
@@ -7577,13 +7610,10 @@
"type": "number",
"description": "Offset timeslots shown to bookers by a specified number of minutes"
},
"recurrence": {
"description": "Create a recurring event that can be booked once but will occur multiple times",
"allOf": [
{
"$ref": "#/components/schemas/Recurrence_2024_06_14"
}
]
"customName": {
"type": "string",
"description": "Customizable event name with valid variables: \n {Event type title}, {Organiser}, {Scheduler}, {Location}, {Organiser first name}, \n {Scheduler first name}, {Scheduler last name}, {Event duration}, {LOCATION}, \n {HOST/ATTENDEE}, {HOST}, {ATTENDEE}, {USER}",
"example": "{Event type title} between {Organiser} and {Scheduler}"
}
},
"required": [
@@ -7604,7 +7634,7 @@
"bookingLimitsDuration",
"bookingWindow",
"offsetStart",
"recurrence"
"customName"
]
},
"UpdateEventTypeOutput_2024_06_14": {
@@ -7938,6 +7968,9 @@
"weekStart": {
"type": "string",
"default": "Sunday"
},
"bookingLimits": {
"type": "string"
}
}
},
@@ -8930,24 +8963,16 @@
"description": "If you want that this event has different schedule than user's default one you can specify it here."
},
"bookingLimitsCount": {
"description": "Limit how many times this event can be booked",
"allOf": [
{
"$ref": "#/components/schemas/BookingLimitsCount_2024_06_14"
}
]
"type": "object",
"description": "Limit how many times this event can be booked"
},
"onlyShowFirstAvailableSlot": {
"type": "boolean",
"description": "This will limit your availability for this event type to one slot per day, scheduled at the earliest available time."
},
"bookingLimitsDuration": {
"description": "Limit total amount of time that this event can be booked",
"allOf": [
{
"$ref": "#/components/schemas/BookingLimitsDuration_2024_06_14"
}
]
"type": "object",
"description": "Limit total amount of time that this event can be booked"
},
"bookingWindow": {
"type": "array",
@@ -8970,13 +8995,10 @@
"type": "number",
"description": "Offset timeslots shown to bookers by a specified number of minutes"
},
"recurrence": {
"description": "Create a recurring event that can be booked once but will occur multiple times",
"allOf": [
{
"$ref": "#/components/schemas/Recurrence_2024_06_14"
}
]
"customName": {
"type": "string",
"description": "Customizable event name with valid variables: \n {Event type title}, {Organiser}, {Scheduler}, {Location}, {Organiser first name}, \n {Scheduler first name}, {Scheduler last name}, {Event duration}, {LOCATION}, \n {HOST/ATTENDEE}, {HOST}, {ATTENDEE}, {USER}",
"example": "{Event type title} between {Organiser} and {Scheduler}"
},
"schedulingType": {
"type": "object"
@@ -9010,7 +9032,7 @@
"bookingLimitsDuration",
"bookingWindow",
"offsetStart",
"recurrence",
"customName",
"schedulingType",
"hosts",
"assignAllTeamMembers"
@@ -9046,6 +9068,38 @@
"data"
]
},
"Recurrence_2024_06_14": {
"type": "object",
"properties": {
"interval": {
"type": "number",
"example": 10,
"description": "Repeats every {count} week | month | year"
},
"occurrences": {
"type": "number",
"example": 10,
"description": "Repeats for a maximum of {count} events"
},
"frequency": {
"enum": [
"yearly",
"monthly",
"weekly"
],
"type": "string"
},
"disabled": {
"type": "boolean",
"default": false
}
},
"required": [
"interval",
"occurrences",
"frequency"
]
},
"TeamEventTypeResponseHost": {
"type": "object",
"properties": {
@@ -9101,7 +9155,7 @@
},
"description": {
"type": "string",
"example": "Discover the culinary wonders of the Argentina by making the best flan ever!"
"example": "Discover the culinary wonders of Argentina by making the best flan ever!"
},
"locations": {
"type": "array",
@@ -9215,9 +9269,6 @@
"metadata": {
"type": "object"
},
"requiresConfirmation": {
"type": "boolean"
},
"price": {
"type": "number"
},
@@ -9239,6 +9290,9 @@
"type": "string",
"nullable": true
},
"isInstantEvent": {
"type": "boolean"
},
"seatsShowAvailabilityCount": {
"type": "boolean",
"nullable": true
@@ -9248,13 +9302,13 @@
"nullable": true
},
"bookingLimitsCount": {
"$ref": "#/components/schemas/BookingLimitsCount_2024_06_14"
"type": "object"
},
"onlyShowFirstAvailableSlot": {
"type": "boolean"
},
"bookingLimitsDuration": {
"$ref": "#/components/schemas/BookingLimitsDuration_2024_06_14"
"type": "object"
},
"bookingWindow": {
"type": "array",
@@ -9273,18 +9327,36 @@
]
}
},
"bookerLayouts": {
"$ref": "#/components/schemas/BookerLayouts_2024_06_14"
},
"confirmationPolicy": {
"type": "object"
},
"requiresBookerEmailVerification": {
"type": "boolean"
},
"hideCalendarNotes": {
"type": "boolean"
},
"color": {
"$ref": "#/components/schemas/EventTypeColor_2024_06_14"
},
"seats": {
"$ref": "#/components/schemas/Seats_2024_06_14"
},
"offsetStart": {
"type": "number",
"minimum": 1
},
"schedulingType": {
"type": "string",
"nullable": true,
"enum": [
"ROUND_ROBIN",
"COLLECTIVE",
"MANAGED"
]
"customName": {
"type": "string"
},
"destinationCalendar": {
"$ref": "#/components/schemas/DestinationCalendar_2024_06_14"
},
"useDestinationCalendarEmail": {
"type": "boolean"
},
"teamId": {
"type": "number",
@@ -9297,7 +9369,7 @@
"parentEventTypeId": {
"type": "number",
"nullable": true,
"description": "For managed event types parent event type is the event type that this event type is based on"
"description": "For managed event types, parent event type is the event type that this event type is based on"
},
"hosts": {
"type": "array",
@@ -9307,6 +9379,15 @@
},
"assignAllTeamMembers": {
"type": "boolean"
},
"schedulingType": {
"type": "string",
"nullable": true,
"enum": [
"ROUND_ROBIN",
"COLLECTIVE",
"MANAGED"
]
}
},
"required": [
@@ -9320,17 +9401,15 @@
"disableGuests",
"recurrence",
"metadata",
"requiresConfirmation",
"price",
"currency",
"lockTimeZoneToggleOnBookingPage",
"seatsPerTimeSlot",
"forwardParamsSuccessRedirect",
"successRedirectUrl",
"seatsShowAvailabilityCount",
"isInstantEvent",
"scheduleId",
"schedulingType",
"hosts"
"hosts",
"schedulingType"
]
},
"GetTeamEventTypeOutput": {
@@ -9580,24 +9659,16 @@
"description": "If you want that this event has different schedule than user's default one you can specify it here."
},
"bookingLimitsCount": {
"description": "Limit how many times this event can be booked",
"allOf": [
{
"$ref": "#/components/schemas/BookingLimitsCount_2024_06_14"
}
]
"type": "object",
"description": "Limit how many times this event can be booked"
},
"onlyShowFirstAvailableSlot": {
"type": "boolean",
"description": "This will limit your availability for this event type to one slot per day, scheduled at the earliest available time."
},
"bookingLimitsDuration": {
"description": "Limit total amount of time that this event can be booked",
"allOf": [
{
"$ref": "#/components/schemas/BookingLimitsDuration_2024_06_14"
}
]
"type": "object",
"description": "Limit total amount of time that this event can be booked"
},
"bookingWindow": {
"type": "array",
@@ -9620,13 +9691,10 @@
"type": "number",
"description": "Offset timeslots shown to bookers by a specified number of minutes"
},
"recurrence": {
"description": "Create a recurring event that can be booked once but will occur multiple times",
"allOf": [
{
"$ref": "#/components/schemas/Recurrence_2024_06_14"
}
]
"customName": {
"type": "string",
"description": "Customizable event name with valid variables: \n {Event type title}, {Organiser}, {Scheduler}, {Location}, {Organiser first name}, \n {Scheduler first name}, {Scheduler last name}, {Event duration}, {LOCATION}, \n {HOST/ATTENDEE}, {HOST}, {ATTENDEE}, {USER}",
"example": "{Event type title} between {Organiser} and {Scheduler}"
},
"hosts": {
"type": "array",
@@ -9657,7 +9725,7 @@
"bookingLimitsDuration",
"bookingWindow",
"offsetStart",
"recurrence",
"customName",
"hosts",
"assignAllTeamMembers"
]
@@ -1,6 +1,5 @@
import type { Locator, Page } from "@playwright/test";
import { expect } from "@playwright/test";
import { apiLogin } from "playwright/fixtures/users";
import { SchedulingType } from "@calcom/prisma/enums";
-1
View File
@@ -6,7 +6,6 @@ import { MembershipRole } from "@calcom/prisma/client";
import { BookingStatus } from "@calcom/prisma/enums";
import { bookingMetadataSchema } from "@calcom/prisma/zod-utils";
import { apiLogin } from "./fixtures/users";
import { test } from "./lib/fixtures";
import {
bookTimeSlot,
@@ -332,7 +332,7 @@ const ZoomVideoApiAdapter = (credential: CredentialPayload): VideoApiAdapter =>
},
body: JSON.stringify(await translateEvent(event)),
});
const updatedMeeting = await fetchZoomApi(`meetings/${bookingRef.uid}`);
const result = zoomEventResultSchema.parse(updatedMeeting);
+2 -2
View File
@@ -4,7 +4,7 @@ import { useFormContext } from "react-hook-form";
import z from "zod";
import { HOSTED_CAL_FEATURES } from "@calcom/lib/constants";
import { useLastUsed, LastUsed } from "@calcom/lib/hooks/useLastUsed";
import { useLastUsed } from "@calcom/lib/hooks/useLastUsed";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import { Button } from "@calcom/ui";
@@ -38,7 +38,7 @@ export function SAMLLogin({ samlTenantID, samlProductID, setErrorMessage }: Prop
<Button
color="minimal"
data-testid="saml"
className="text-brand-500 font-medium p-0 h-auto"
className="text-brand-500 h-auto p-0 font-medium"
onClick={async (event) => {
event.preventDefault();
@@ -1,12 +1,24 @@
import { describe, expect, it } from "vitest";
import { FrequencyInput } from "@calcom/platform-enums/monorepo";
import {
BookerLayoutsInputEnum_2024_06_14,
BookerLayoutsOutputEnum_2024_06_14,
ConfirmationPolicyEnum,
NoticeThresholdUnitEnum,
FrequencyInput,
} from "@calcom/platform-enums/monorepo";
import type {
InputBookingField_2024_06_14,
Location_2024_06_14,
BookingLimitsCount_2024_06_14,
BookingWindow_2024_06_14,
BookerLayouts_2024_06_14,
ConfirmationPolicy_2024_06_14,
EventTypeColor_2024_06_14,
Recurrence_2024_06_14,
CreateEventTypeInput_2024_06_14,
SeatOptionsTransformedSchema,
SeatOptionsDisabledSchema,
} from "@calcom/platform-types";
import type { CustomField } from "../internal-to-api/booking-fields";
@@ -17,6 +29,10 @@ import {
transformIntervalLimitsApiToInternal,
transformFutureBookingLimitsApiToInternal,
transformRecurrenceApiToInternal,
transformSeatsApiToInternal,
transformEventColorsApiToInternal,
transformBookerLayoutsApiToInternal,
transformConfirmationPolicyApiToInternal,
} from "./index";
describe("transformLocationsApiToInternal", () => {
@@ -585,6 +601,152 @@ describe("transformFutureBookingLimitsApiToInternal", () => {
expect(result).toEqual(expectedOutput);
});
it("should transform disabled", () => {
const input: BookingWindow_2024_06_14 = {
disabled: true,
};
const expectedOutput = {
periodType: "UNLIMITED",
};
const result = transformFutureBookingLimitsApiToInternal(input);
expect(result).toEqual(expectedOutput);
});
});
describe("transformBookerLayoutsApiToInternal", () => {
it("should transform booker layouts", () => {
const input: BookerLayouts_2024_06_14 = {
enabledLayouts: [
BookerLayoutsInputEnum_2024_06_14.column,
BookerLayoutsInputEnum_2024_06_14.month,
BookerLayoutsInputEnum_2024_06_14.week,
],
defaultLayout: BookerLayoutsInputEnum_2024_06_14.week,
};
const expectedOutput = {
enabledLayouts: [
BookerLayoutsOutputEnum_2024_06_14.column_view,
BookerLayoutsOutputEnum_2024_06_14.month_view,
BookerLayoutsOutputEnum_2024_06_14.week_view,
],
defaultLayout: BookerLayoutsOutputEnum_2024_06_14.week_view,
};
const result = transformBookerLayoutsApiToInternal(input);
expect(result).toEqual(expectedOutput);
});
});
describe("transformConfirmationPolicyApiToInternal", () => {
it("should transform requires confirmation - time", () => {
const input: ConfirmationPolicy_2024_06_14 = {
type: ConfirmationPolicyEnum.TIME,
noticeThreshold: {
count: 60,
unit: NoticeThresholdUnitEnum.MINUTES,
},
blockUnconfirmedBookingsInBooker: true,
};
const expectedOutput = {
requiresConfirmation: true,
requiresConfirmationThreshold: {
time: 60,
unit: NoticeThresholdUnitEnum.MINUTES,
},
requiresConfirmationWillBlockSlot: true,
};
const result = transformConfirmationPolicyApiToInternal(input);
expect(result).toEqual(expectedOutput);
});
it("should transform requires confirmation - always", () => {
const input: ConfirmationPolicy_2024_06_14 = {
type: ConfirmationPolicyEnum.ALWAYS,
blockUnconfirmedBookingsInBooker: true,
};
const expectedOutput = {
requiresConfirmation: true,
requiresConfirmationWillBlockSlot: true,
};
const result = transformConfirmationPolicyApiToInternal(input);
expect(result).toEqual(expectedOutput);
});
it("should transform requires confirmation - disabled", () => {
const input: ConfirmationPolicy_2024_06_14 = {
disabled: true,
};
const expectedOutput = {
requiresConfirmation: false,
requiresConfirmationWillBlockSlot: false,
requiresConfirmationThreshold: undefined,
};
const result = transformConfirmationPolicyApiToInternal(input);
expect(result).toEqual(expectedOutput);
});
});
describe("transformEventColorsApiToInternal", () => {
it("should transform event type colors", () => {
const input: EventTypeColor_2024_06_14 = {
darkThemeHex: "#292929",
lightThemeHex: "#fafafa",
};
const expectedOutput = {
darkEventTypeColor: "#292929",
lightEventTypeColor: "#fafafa",
};
const result = transformEventColorsApiToInternal(input);
expect(result).toEqual(expectedOutput);
});
});
describe("transformSeatsApiToInternal", () => {
it("should transform seat options", () => {
const input: CreateEventTypeInput_2024_06_14["seats"] = {
seatsPerTimeSlot: 20,
showAttendeeInfo: true,
showAvailabilityCount: false,
};
const expectedOutput: SeatOptionsTransformedSchema = {
seatsPerTimeSlot: 20,
seatsShowAttendees: true,
seatsShowAvailabilityCount: false,
};
const result = transformSeatsApiToInternal(input);
expect(result).toEqual(expectedOutput);
});
it("should transform seat options - disabled", () => {
const input: CreateEventTypeInput_2024_06_14["seats"] = {
disabled: true,
};
const expectedOutput: SeatOptionsDisabledSchema = {
seatsPerTimeSlot: null,
};
const result = transformSeatsApiToInternal(input);
expect(result).toEqual(expectedOutput);
});
});
describe("transformRecurrenceApiToInternal", () => {
@@ -600,7 +762,15 @@ describe("transformRecurrenceApiToInternal", () => {
freq: 2,
};
const result = transformRecurrenceApiToInternal(input);
expect(result).toEqual(expectedOutput);
});
it("should transform recurrence - disabled", () => {
const input: CreateEventTypeInput_2024_06_14["recurrence"] = {
disabled: true,
};
const expectedOutput = undefined;
const result = transformRecurrenceApiToInternal(input);
expect(result).toEqual(expectedOutput);
});
});
@@ -0,0 +1,22 @@
import {
BookerLayoutsInputEnum_2024_06_14,
BookerLayoutsOutputEnum_2024_06_14,
} from "@calcom/platform-enums/monorepo";
import { type CreateEventTypeInput_2024_06_14 } from "@calcom/platform-types";
export function transformBookerLayoutsApiToInternal(
inputBookerLayout: CreateEventTypeInput_2024_06_14["bookerLayouts"]
) {
if (!inputBookerLayout) return undefined;
const inputToOutputMap = {
[BookerLayoutsInputEnum_2024_06_14.month]: BookerLayoutsOutputEnum_2024_06_14.month_view,
[BookerLayoutsInputEnum_2024_06_14.week]: BookerLayoutsOutputEnum_2024_06_14.week_view,
[BookerLayoutsInputEnum_2024_06_14.column]: BookerLayoutsOutputEnum_2024_06_14.column_view,
};
return {
defaultLayout: inputToOutputMap[inputBookerLayout.defaultLayout],
enabledLayouts: inputBookerLayout.enabledLayouts.map((layout) => inputToOutputMap[layout]),
};
}
@@ -0,0 +1,36 @@
import { ConfirmationPolicyEnum } from "@calcom/platform-enums/monorepo";
import type {
ConfirmationPolicyTransformedSchema,
NoticeThresholdTransformedSchema,
CreateEventTypeInput_2024_06_14,
} from "@calcom/platform-types";
export function transformConfirmationPolicyApiToInternal(
inputConfirmationPolicy: CreateEventTypeInput_2024_06_14["confirmationPolicy"]
): ConfirmationPolicyTransformedSchema | undefined {
if (!inputConfirmationPolicy) return undefined;
if (inputConfirmationPolicy.disabled) {
return {
requiresConfirmation: false,
requiresConfirmationWillBlockSlot: false,
requiresConfirmationThreshold: undefined,
};
}
switch (inputConfirmationPolicy.type) {
case ConfirmationPolicyEnum.ALWAYS:
return {
requiresConfirmation: true,
requiresConfirmationThreshold: undefined,
requiresConfirmationWillBlockSlot: inputConfirmationPolicy.blockUnconfirmedBookingsInBooker,
};
case ConfirmationPolicyEnum.TIME:
return {
requiresConfirmation: true,
requiresConfirmationThreshold: {
unit: inputConfirmationPolicy.noticeThreshold?.unit,
time: inputConfirmationPolicy.noticeThreshold?.count,
} as NoticeThresholdTransformedSchema,
requiresConfirmationWillBlockSlot: inputConfirmationPolicy.blockUnconfirmedBookingsInBooker,
};
}
}
@@ -0,0 +1,15 @@
import type {
CreateEventTypeInput_2024_06_14,
EventTypeColorsTransformedSchema,
} from "@calcom/platform-types";
export function transformEventColorsApiToInternal(
inputEventTypeColors: CreateEventTypeInput_2024_06_14["color"]
): EventTypeColorsTransformedSchema | undefined {
if (!inputEventTypeColors) return undefined;
return {
darkEventTypeColor: inputEventTypeColors.darkThemeHex,
lightEventTypeColor: inputEventTypeColors.lightThemeHex,
};
}
@@ -12,6 +12,14 @@ import {
export function transformFutureBookingLimitsApiToInternal(
inputBookingLimits: CreateEventTypeInput_2024_06_14["bookingWindow"]
): TransformFutureBookingsLimitSchema_2024_06_14 | undefined {
if (!inputBookingLimits) {
return;
}
if (inputBookingLimits.disabled) {
return {
periodType: BookingWindowPeriodOutputTypeEnum_2024_06_14.UNLIMITED,
};
}
switch (inputBookingLimits?.type) {
case BookingWindowPeriodInputTypeEnum_2024_06_14.businessDays:
return {
@@ -3,3 +3,7 @@ export * from "./locations";
export * from "./recurrence";
export * from "./interval-limits";
export * from "./future-booking-limits";
export * from "./booker-layouts";
export * from "./confirmation-policy";
export * from "./event-colors";
export * from "./seats";
@@ -9,6 +9,9 @@ export function transformIntervalLimitsApiToInternal(
inputBookingLimits: CreateEventTypeInput_2024_06_14["bookingLimitsCount"]
) {
const res: TransformBookingLimitsSchema_2024_06_14 = {};
if (inputBookingLimits?.disabled) {
return res;
}
inputBookingLimits &&
Object.entries(inputBookingLimits).map(([key, value]) => {
const outputKey: BookingLimitsKeyOutputType_2024_06_14 = BookingLimitsEnum_2024_06_14[
@@ -71,7 +71,7 @@ const UserPhoneSchema = z.object({
type InPersonLocation = z.infer<typeof InPersonSchema>;
type LinkLocation = z.infer<typeof LinkSchema>;
type IntegrationLocation = z.infer<typeof IntegrationSchema>;
export type IntegrationLocation = z.infer<typeof IntegrationSchema>;
type UserPhoneLocation = z.infer<typeof UserPhoneSchema>;
const TransformedLocationSchema = z.union([InPersonSchema, LinkSchema, IntegrationSchema, UserPhoneSchema]);
@@ -7,7 +7,8 @@ import {
export function transformRecurrenceApiToInternal(
recurrence: CreateEventTypeInput_2024_06_14["recurrence"]
): TransformRecurringEventSchema_2024_06_14 | undefined {
if (!recurrence) return undefined;
if (!recurrence || recurrence.disabled) return undefined;
return {
interval: recurrence.interval,
count: recurrence.occurrences,
@@ -0,0 +1,22 @@
import type {
CreateEventTypeInput_2024_06_14,
SeatOptionsTransformedSchema,
SeatOptionsDisabledSchema,
} from "@calcom/platform-types";
export function transformSeatsApiToInternal(
inputSeats: CreateEventTypeInput_2024_06_14["seats"]
): SeatOptionsTransformedSchema | SeatOptionsDisabledSchema | undefined {
if (!inputSeats) return undefined;
if (inputSeats.disabled)
return {
seatsPerTimeSlot: null,
};
return {
seatsPerTimeSlot: inputSeats.seatsPerTimeSlot,
seatsShowAttendees: inputSeats.showAttendeeInfo,
seatsShowAvailabilityCount: inputSeats.showAvailabilityCount,
};
}
@@ -0,0 +1,20 @@
import {
BookerLayoutsInputEnum_2024_06_14,
BookerLayoutsOutputEnum_2024_06_14,
} from "@calcom/platform-enums/monorepo";
import type { BookerLayoutsTransformedSchema } from "@calcom/platform-types";
export function transformBookerLayoutsInternalToApi(
transformedBookerLayouts: BookerLayoutsTransformedSchema
) {
const outputToInputMap = {
[BookerLayoutsOutputEnum_2024_06_14.month_view]: BookerLayoutsInputEnum_2024_06_14.month,
[BookerLayoutsOutputEnum_2024_06_14.week_view]: BookerLayoutsInputEnum_2024_06_14.week,
[BookerLayoutsOutputEnum_2024_06_14.column_view]: BookerLayoutsInputEnum_2024_06_14.column,
};
return {
defaultLayout: outputToInputMap[transformedBookerLayouts.defaultLayout],
enabledLayouts: transformedBookerLayouts.enabledLayouts.map((layout) => outputToInputMap[layout]),
};
}
@@ -0,0 +1,10 @@
import type { EventTypeColorsTransformedSchema, EventTypeColor_2024_06_14 } from "@calcom/platform-types";
export function transformEventTypeColorsInternalToApi(
transformedColors: EventTypeColorsTransformedSchema
): EventTypeColor_2024_06_14 {
return {
darkThemeHex: transformedColors.darkEventTypeColor,
lightThemeHex: transformedColors.lightEventTypeColor,
};
}
@@ -8,6 +8,7 @@ import type {
RangeWindow_2024_06_14,
CalendarDaysWindow_2024_06_14,
BusinessDaysWindow_2024_06_14,
Disabled_2024_06_14,
} from "@calcom/platform-types";
export function transformFutureBookingLimitsInternalToApi(
@@ -38,6 +39,11 @@ export function transformFutureBookingLimitsInternalToApi(
value: transformedFutureBookingsLimitsFields.periodDays,
rolling: false,
} as CalendarDaysWindow_2024_06_14 | BusinessDaysWindow_2024_06_14;
case BookingWindowPeriodOutputTypeEnum_2024_06_14.UNLIMITED:
return {
disabled: true,
} as Disabled_2024_06_14;
default:
return undefined;
}
@@ -3,3 +3,7 @@ export * from "./locations";
export * from "./recurrence";
export * from "./interval-limits";
export * from "./future-booking-limits";
export * from "./booker-layouts";
export * from "./event-type-colors";
export * from "./requires-confirmation";
export * from "./seats";
@@ -1,5 +1,11 @@
import { describe, expect, it } from "vitest";
import {
BookerLayoutsInputEnum_2024_06_14,
BookerLayoutsOutputEnum_2024_06_14,
ConfirmationPolicyEnum,
NoticeThresholdUnitEnum,
} from "@calcom/platform-enums/monorepo";
import type {
AddressLocation_2024_06_14,
LinkLocation_2024_06_14,
@@ -7,7 +13,11 @@ import type {
IntegrationLocation_2024_06_14,
TransformBookingLimitsSchema_2024_06_14,
TransformFutureBookingsLimitSchema_2024_06_14,
BookerLayoutsTransformedSchema,
EventTypeColorsTransformedSchema,
TransformRecurringEventSchema_2024_06_14,
SeatOptionsTransformedSchema,
SeatOptionsDisabledSchema,
} from "@calcom/platform-types";
import {
@@ -16,6 +26,10 @@ import {
transformIntervalLimitsInternalToApi,
transformFutureBookingLimitsInternalToApi,
transformRecurrenceInternalToApi,
transformBookerLayoutsInternalToApi,
transformRequiresConfirmationInternalToApi,
transformEventTypeColorsInternalToApi,
transformSeatsInternalToApi,
} from ".";
import type { CustomField } from "./booking-fields";
@@ -621,6 +635,162 @@ describe("transformFutureBookingLimitsInternalToApi", () => {
expect(result).toEqual(expectedOutput);
});
it("should reverse transform disabled", () => {
const transformedField: TransformFutureBookingsLimitSchema_2024_06_14 = {
periodType: "UNLIMITED",
};
const expectedOutput = {
disabled: true,
};
const result = transformFutureBookingLimitsInternalToApi(transformedField);
expect(result).toEqual(expectedOutput);
});
});
describe("transformBookerLayoutsInternalToApi", () => {
it("should reverse transform booker layout", () => {
const transformedField: BookerLayoutsTransformedSchema = {
enabledLayouts: [
BookerLayoutsOutputEnum_2024_06_14.column_view,
BookerLayoutsOutputEnum_2024_06_14.month_view,
BookerLayoutsOutputEnum_2024_06_14.week_view,
],
defaultLayout: BookerLayoutsOutputEnum_2024_06_14.week_view,
};
const expectedOutput = {
enabledLayouts: [
BookerLayoutsInputEnum_2024_06_14.column,
BookerLayoutsInputEnum_2024_06_14.month,
BookerLayoutsInputEnum_2024_06_14.week,
],
defaultLayout: BookerLayoutsInputEnum_2024_06_14.week,
};
const result = transformBookerLayoutsInternalToApi(transformedField);
expect(result).toEqual(expectedOutput);
});
});
describe("transformRequiresConfirmationInternalToApi", () => {
it("should reverse transform requires confirmation - time", () => {
const transformedField = {
requiresConfirmation: true,
requiresConfirmationThreshold: {
time: 60,
unit: NoticeThresholdUnitEnum.MINUTES,
},
requiresConfirmationWillBlockSlot: true,
};
const expectedOutput = {
type: ConfirmationPolicyEnum.TIME,
noticeThreshold: {
count: 60,
unit: NoticeThresholdUnitEnum.MINUTES,
},
blockUnconfirmedBookingsInBooker: true,
};
const result = transformRequiresConfirmationInternalToApi(
transformedField.requiresConfirmation,
transformedField.requiresConfirmationWillBlockSlot,
transformedField.requiresConfirmationThreshold
);
expect(result).toEqual(expectedOutput);
});
it("should reverse transform requires confirmation - always", () => {
const transformedField = {
requiresConfirmation: true,
requiresConfirmationWillBlockSlot: true,
};
const expectedOutput = {
type: ConfirmationPolicyEnum.ALWAYS,
blockUnconfirmedBookingsInBooker: true,
};
const result = transformRequiresConfirmationInternalToApi(
transformedField.requiresConfirmation,
transformedField.requiresConfirmationWillBlockSlot,
undefined
);
expect(result).toEqual(expectedOutput);
});
it("should reverse transform requires confirmation - disabled", () => {
const transformedField = {
requiresConfirmation: false,
};
const expectedOutput = {
disabled: true,
};
const result = transformRequiresConfirmationInternalToApi(
transformedField.requiresConfirmation,
!!undefined,
undefined
);
expect(result).toEqual(expectedOutput);
});
});
describe("transformEventTypeColorsInternalToApi", () => {
it("should reverse transform event type colors", () => {
const transformedField: EventTypeColorsTransformedSchema = {
darkEventTypeColor: "#292929",
lightEventTypeColor: "#fafafa",
};
const expectedOutput = {
darkThemeHex: "#292929",
lightThemeHex: "#fafafa",
};
const result = transformEventTypeColorsInternalToApi(transformedField);
expect(result).toEqual(expectedOutput);
});
});
describe("transformSeatsInternalToApi", () => {
it("should reverse transform event type seats", () => {
const transformedSeats: SeatOptionsTransformedSchema = {
seatsPerTimeSlot: 10,
seatsShowAttendees: true,
seatsShowAvailabilityCount: false,
};
const expectedOutput = {
seatsPerTimeSlot: 10,
showAttendeeInfo: true,
showAvailabilityCount: false,
};
const result = transformSeatsInternalToApi(transformedSeats);
expect(result).toEqual(expectedOutput);
});
it("should reverse transform event type seats - disabled", () => {
const transformedSeats: SeatOptionsDisabledSchema = {
seatsPerTimeSlot: null,
};
const expectedOutput = {
disabled: true,
};
const result = transformSeatsInternalToApi(transformedSeats);
expect(result).toEqual(expectedOutput);
});
});
describe("transformRecurrenceInternalToApi", () => {
@@ -0,0 +1,29 @@
import { ConfirmationPolicyEnum } from "@calcom/platform-enums/monorepo";
import type { NoticeThresholdTransformedSchema, ConfirmationPolicy_2024_06_14 } from "@calcom/platform-types";
export function transformRequiresConfirmationInternalToApi(
requiresConfirmation: boolean,
requiresConfirmationWillBlockSlot: boolean,
requiresConfirmationThreshold?: NoticeThresholdTransformedSchema
): ConfirmationPolicy_2024_06_14 | undefined {
if (requiresConfirmationThreshold?.unit) {
return {
type: ConfirmationPolicyEnum.TIME,
noticeThreshold: {
unit: requiresConfirmationThreshold.unit,
count: requiresConfirmationThreshold.time,
},
blockUnconfirmedBookingsInBooker: requiresConfirmationWillBlockSlot,
};
} else if (requiresConfirmation) {
return {
type: ConfirmationPolicyEnum.ALWAYS,
blockUnconfirmedBookingsInBooker: requiresConfirmationWillBlockSlot,
};
} else if (!requiresConfirmation) {
return {
disabled: true,
};
}
return undefined;
}
@@ -0,0 +1,20 @@
import type {
SeatOptionsTransformedSchema,
SeatOptionsDisabledSchema,
CreateEventTypeInput_2024_06_14,
} from "@calcom/platform-types";
export function transformSeatsInternalToApi(
transformedSeats: SeatOptionsTransformedSchema | SeatOptionsDisabledSchema
): CreateEventTypeInput_2024_06_14["seats"] {
if (transformedSeats.seatsPerTimeSlot == null) {
return {
disabled: true,
};
}
return {
seatsPerTimeSlot: transformedSeats.seatsPerTimeSlot,
showAttendeeInfo: transformedSeats.seatsShowAttendees,
showAvailabilityCount: transformedSeats.seatsShowAvailabilityCount,
};
}
@@ -0,0 +1,11 @@
export enum BookerLayoutsInputEnum_2024_06_14 {
month = "month",
week = "week",
column = "column",
}
export enum BookerLayoutsOutputEnum_2024_06_14 {
month_view = "month_view",
week_view = "week_view",
column_view = "column_view",
}
@@ -8,4 +8,5 @@ export enum BookingWindowPeriodOutputTypeEnum_2024_06_14 {
RANGE = "RANGE",
ROLLING_WINDOW = "ROLLING_WINDOW",
ROLLING = "ROLLING",
UNLIMITED = "UNLIMITED",
}
@@ -0,0 +1,9 @@
export enum ConfirmationPolicyEnum {
ALWAYS = "always",
TIME = "time",
}
export enum NoticeThresholdUnitEnum {
MINUTES = "minutes",
HOURS = "hours",
}
+2
View File
@@ -5,3 +5,5 @@ export * from "./event-types/interval-limits.enum";
export * from "./event-types/period-type";
export * from "./event-types/scheduling-type";
export * from "./event-types/frequency";
export * from "./event-types/booker-layouts.enum";
export * from "./event-types/confirmation-policy.enum";
+2
View File
@@ -5,3 +5,5 @@ export * from "./event-types/interval-limits.enum";
export * from "./event-types/period-type";
export * from "./event-types/scheduling-type";
export * from "./event-types/frequency";
export * from "./event-types/booker-layouts.enum";
export * from "./event-types/confirmation-policy.enum";
+18
View File
@@ -1,3 +1,21 @@
## 0.0.38
#### Feature: Added Support for AdvancedTab Event-Type Attributes in API
- **Booker Layouts**:
- Added `transformBookerLayoutsApiToInternal` translator to enable the `bookerLayouts` attribute in the event-type API.
- Added `transformBookerLayoutsInternalToApi` translator to improve clarity and readability of `bookerLayouts` response data.
- **Event-Type Colors**:
- Added `transformEventColorsApiToInternal` translator to enable the `color` attribute in the event-type API.
- Added `transformEventTypeColorsInternalToApi` translator to enhance the readability of the `color` attribute in the response.
- **Confirmation Policy**:
- Added `transformConfirmationPolicyApiToInternal` translator to enable the `confirmationPolicy` attribute in the event-type API.
- Added `transformRequiresConfirmationInternalToApi` translator to improve the readability of `requiresConfirmation` data in the response.
- **Seats**:
- Added `transformSeatsApiToInternal` translator to enable the `seats` attribute in the event-type API.
- Added `transformSeatsInternalToApi` translator to enhance readability and clarity of the `seats` data.
## 0.0.37
Released to support PR https://github.com/calcom/cal.com/pull/16200
+10 -1
View File
@@ -100,12 +100,20 @@ export {
transformIntervalLimitsApiToInternal,
transformFutureBookingLimitsApiToInternal,
transformRecurrenceApiToInternal,
transformBookerLayoutsApiToInternal,
transformConfirmationPolicyApiToInternal,
transformEventColorsApiToInternal,
transformSeatsApiToInternal,
// note(Lauris): Internal to api
transformBookingFieldsInternalToApi,
transformLocationsInternalToApi,
transformIntervalLimitsInternalToApi,
transformFutureBookingLimitsInternalToApi,
transformRecurrenceInternalToApi,
transformBookerLayoutsInternalToApi,
transformRequiresConfirmationInternalToApi,
transformEventTypeColorsInternalToApi,
transformSeatsInternalToApi,
// note(Lauris): schemas
TransformedLocationsSchema,
BookingFieldsSchema,
@@ -118,7 +126,7 @@ export {
export type { SystemField, CustomField } from "@calcom/lib/event-types/transformers";
export { parseBookingLimit } from "@calcom/lib";
export { parseBookingLimit, parseEventTypeColor } from "@calcom/lib";
export { parseRecurringEvent } from "@calcom/lib/isRecurringEvent";
export { dynamicEvent } from "@calcom/lib/defaultEvents";
@@ -133,3 +141,4 @@ export { getTranslation };
export { updateNewTeamMemberEventTypes } from "@calcom/lib/server/queries";
export { ErrorCode } from "@calcom/lib/errorCodes";
export { validateCustomEventName } from "@calcom/core/event";
@@ -1,2 +1,3 @@
export * from "./inputs";
export * from "./outputs";
export * from "./transformed";
@@ -0,0 +1,52 @@
import type { ValidatorConstraintInterface, ValidationOptions } from "class-validator";
import { ValidatorConstraint, registerDecorator } from "class-validator";
import {
BookerLayoutsInputEnum_2024_06_14,
type BookerLayoutsOutputEnum_2024_06_14,
} from "@calcom/platform-enums";
export type BookerLayoutsTransformedSchema = {
defaultLayout: BookerLayoutsOutputEnum_2024_06_14;
enabledLayouts: BookerLayoutsOutputEnum_2024_06_14[];
};
@ValidatorConstraint({ name: "LayoutValidator", async: false })
export class LayoutValidator implements ValidatorConstraintInterface {
validate(value: BookerLayoutsInputEnum_2024_06_14 | BookerLayoutsInputEnum_2024_06_14[]) {
const validValues = Object.values(BookerLayoutsInputEnum_2024_06_14);
// If the value is an array, check if every item in the array is valid
if (Array.isArray(value)) {
return value.every((layout) => validValues.includes(layout));
}
// If the value is a single layout, check if it is valid
return validValues.includes(value);
}
defaultMessage() {
const validValues = Object.values(BookerLayoutsInputEnum_2024_06_14).join(", ");
return `Invalid layout. Valid options are: ${validValues}`;
}
}
function IsValidLayout(validationOptions?: ValidationOptions) {
return function (object: any, propertyName: string) {
registerDecorator({
name: "IsValidLayout",
target: object.constructor,
propertyName: propertyName,
options: validationOptions,
validator: new LayoutValidator(),
});
};
}
export class BookerLayouts_2024_06_14 {
@IsValidLayout({ message: "defaultLayout must be one of the valid layouts." })
defaultLayout!: BookerLayoutsInputEnum_2024_06_14;
@IsValidLayout({ message: "enabledLayouts must be one of the valid layouts." })
enabledLayouts!: BookerLayoutsInputEnum_2024_06_14[];
}
@@ -1,6 +1,8 @@
import { ApiProperty } from "@nestjs/swagger";
import type { ValidatorConstraintInterface, ValidationOptions } from "class-validator";
import { IsInt, IsOptional, Min, ValidatorConstraint, registerDecorator } from "class-validator";
import { IsBoolean, IsInt, IsOptional, Min, ValidatorConstraint, registerDecorator } from "class-validator";
import type { Disabled_2024_06_14 } from "./disabled.input";
export type BookingLimitsKeyOutputType_2024_06_14 = "PER_DAY" | "PER_WEEK" | "PER_MONTH" | "PER_YEAR";
export type BookingLimitsKeysInputType = "day" | "week" | "month" | "year";
@@ -8,7 +10,7 @@ export type TransformBookingLimitsSchema_2024_06_14 = {
[K in BookingLimitsKeyOutputType_2024_06_14]?: number;
};
export class BookingLimitsCount_2024_06_14 {
export class BaseBookingLimitsCount_2024_06_14 {
@IsOptional()
@IsInt()
@Min(1)
@@ -44,8 +46,13 @@ export class BookingLimitsCount_2024_06_14 {
example: 4,
})
year?: number;
@IsOptional()
@IsBoolean()
disabled?: boolean = false;
}
export type BookingLimitsCount_2024_06_14 = BaseBookingLimitsCount_2024_06_14 | Disabled_2024_06_14;
// Custom validator to handle the union type
@ValidatorConstraint({ name: "BookingLimitsCountValidator", async: false })
class BookingLimitsCountValidator implements ValidatorConstraintInterface {
@@ -55,6 +62,9 @@ class BookingLimitsCountValidator implements ValidatorConstraintInterface {
} = {};
validate(value: BookingLimitsCount_2024_06_14) {
if (!value) return false;
if ("disabled" in value) {
return true;
}
const { day, week, month, year } = value;
@@ -84,7 +94,10 @@ class BookingLimitsCountValidator implements ValidatorConstraintInterface {
defaultMessage() {
const { invalidLimit, comparedLimit } = this.errorDetails;
return `Invalid booking limits: The number of bookings for ${invalidLimit} cannot exceed the number of bookings for ${comparedLimit}.`;
if (invalidLimit && comparedLimit) {
return `Invalid booking limits: The number of bookings for ${invalidLimit} cannot exceed the number of bookings for ${comparedLimit}.`;
}
return `Invalid booking limits count structure`;
}
}
@@ -1,8 +1,10 @@
import { ApiProperty } from "@nestjs/swagger";
import type { ValidatorConstraintInterface, ValidationOptions } from "class-validator";
import { IsInt, IsOptional, Min, ValidatorConstraint, registerDecorator } from "class-validator";
import { IsBoolean, IsInt, IsOptional, Min, ValidatorConstraint, registerDecorator } from "class-validator";
export class BookingLimitsDuration_2024_06_14 {
import type { Disabled_2024_06_14 } from "./disabled.input";
export class BaseBookingLimitsDuration_2024_06_14 {
@IsOptional()
@IsInt()
@Min(15)
@@ -38,8 +40,13 @@ export class BookingLimitsDuration_2024_06_14 {
example: 240,
})
year?: number;
@IsOptional()
@IsBoolean()
disabled?: boolean = false;
}
export type BookingLimitsDuration_2024_06_14 = BaseBookingLimitsDuration_2024_06_14 | Disabled_2024_06_14;
@ValidatorConstraint({ name: "BookingLimitsDurationValidator", async: false })
class BookingLimitsDurationValidator implements ValidatorConstraintInterface {
private errorDetails: {
@@ -48,6 +55,9 @@ class BookingLimitsDurationValidator implements ValidatorConstraintInterface {
} = {};
validate(value: BookingLimitsDuration_2024_06_14) {
if (!value) return false;
if ("disabled" in value) {
return true;
}
const { day, week, month, year } = value;
@@ -77,7 +87,10 @@ class BookingLimitsDurationValidator implements ValidatorConstraintInterface {
defaultMessage() {
const { invalidLimit, comparedLimit } = this.errorDetails;
return `Invalid booking durations: The duration of bookings for ${invalidLimit} cannot exceed the duration of bookings for ${comparedLimit}.`;
if (invalidLimit && comparedLimit) {
return `Invalid booking durations: The duration of bookings for ${invalidLimit} cannot exceed the duration of bookings for ${comparedLimit}.`;
}
return `Invalid booking durations structure`;
}
}
@@ -1,4 +1,3 @@
import { BadRequestException } from "@nestjs/common";
import { ApiProperty } from "@nestjs/swagger";
import type { ValidatorConstraintInterface, ValidationOptions } from "class-validator";
import {
@@ -16,8 +15,10 @@ import {
import { BookingWindowPeriodInputTypeEnum_2024_06_14 } from "@calcom/platform-enums";
import type { Disabled_2024_06_14 } from "./disabled.input";
export type BookingWindowPeriodInputType_2024_06_14 = "businessDays" | "calendarDays" | "range";
export type BookingWindowPeriodOutputType_2024_06_14 = "RANGE" | "ROLLING_WINDOW" | "ROLLING";
export type BookingWindowPeriodOutputType_2024_06_14 = "RANGE" | "ROLLING_WINDOW" | "ROLLING" | "UNLIMITED";
export type TransformFutureBookingsLimitSchema_2024_06_14 = {
periodType: BookingWindowPeriodOutputType_2024_06_14;
@@ -52,6 +53,10 @@ export class BusinessDaysWindow_2024_06_14 extends BookingWindowBase {
"If true, the window will be rolling aka from the moment that someone is trying to book this event. Otherwise it will be specified amount of days from the current date.",
})
rolling?: boolean;
@IsOptional()
@IsBoolean()
disabled?: boolean = false;
}
export class CalendarDaysWindow_2024_06_14 extends BookingWindowBase {
@@ -68,6 +73,10 @@ export class CalendarDaysWindow_2024_06_14 extends BookingWindowBase {
"If true, the window will be rolling aka from the moment that someone is trying to book this event. Otherwise it will be specified amount of days from the current date.",
})
rolling?: boolean;
@IsOptional()
@IsBoolean()
disabled?: boolean = false;
}
export class RangeWindow_2024_06_14 extends BookingWindowBase {
@@ -81,39 +90,48 @@ export class RangeWindow_2024_06_14 extends BookingWindowBase {
description: "Date range for when this event can be booked.",
})
value!: string[];
@IsOptional()
@IsBoolean()
disabled?: boolean = false;
}
export type BookingWindow_2024_06_14 =
| BusinessDaysWindow_2024_06_14
| CalendarDaysWindow_2024_06_14
| RangeWindow_2024_06_14;
| RangeWindow_2024_06_14
| Disabled_2024_06_14;
// Custom validator to handle the union type
@ValidatorConstraint({ name: "bookingWindowValidator", async: false })
class BookingWindowValidator implements ValidatorConstraintInterface {
validate(value: BookingWindow_2024_06_14) {
if (!value || !value.type) {
throw new BadRequestException(`'BookingWindow' must have a type`);
if ("disabled" in value && value.disabled === true) {
return true;
}
switch (value.type) {
case BookingWindowPeriodInputTypeEnum_2024_06_14.businessDays:
case BookingWindowPeriodInputTypeEnum_2024_06_14.calendarDays:
return (
typeof value.value === "number" &&
(typeof (value as BusinessDaysWindow_2024_06_14 | CalendarDaysWindow_2024_06_14).rolling ===
"undefined" ||
typeof (value as BusinessDaysWindow_2024_06_14 | CalendarDaysWindow_2024_06_14).rolling ===
"boolean")
);
case BookingWindowPeriodInputTypeEnum_2024_06_14.range:
return (
Array.isArray(value.value) &&
value.value.every((date: any) => typeof date === "string" && !isNaN(Date.parse(date)))
);
default:
return false;
if ("type" in value) {
switch (value.type) {
case BookingWindowPeriodInputTypeEnum_2024_06_14.businessDays:
case BookingWindowPeriodInputTypeEnum_2024_06_14.calendarDays:
return (
typeof value.value === "number" &&
(typeof (value as BusinessDaysWindow_2024_06_14 | CalendarDaysWindow_2024_06_14).rolling ===
"undefined" ||
typeof (value as BusinessDaysWindow_2024_06_14 | CalendarDaysWindow_2024_06_14).rolling ===
"boolean")
);
case BookingWindowPeriodInputTypeEnum_2024_06_14.range:
return (
Array.isArray(value.value) &&
value.value.every((date: any) => typeof date === "string" && !isNaN(Date.parse(date)))
);
default:
return false;
}
}
return false;
}
defaultMessage() {
@@ -0,0 +1,108 @@
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
import { Type } from "class-transformer";
import { IsEnum, IsOptional, IsInt, ValidateNested, IsBoolean } from "class-validator";
import type { ValidatorConstraintInterface, ValidationOptions } from "class-validator";
import { ValidatorConstraint, registerDecorator } from "class-validator";
import { ConfirmationPolicyEnum, NoticeThresholdUnitEnum } from "@calcom/platform-enums";
import type { Disabled_2024_06_14 } from "./disabled.input";
// Class representing the notice threshold
export class NoticeThreshold_2024_06_14 {
@IsEnum(NoticeThresholdUnitEnum)
@ApiProperty({
description: "The unit of time for the notice threshold (e.g., minutes, hours)",
example: NoticeThresholdUnitEnum.MINUTES,
})
unit!: NoticeThresholdUnitEnum;
@IsInt()
@ApiProperty({
description: "The time value for the notice threshold",
example: 30,
})
count!: number;
}
// Class representing the confirmation requirements
export class BaseConfirmationPolicy_2024_06_14 {
@IsEnum(ConfirmationPolicyEnum)
@ApiProperty({
description: "The policy that determines when confirmation is required",
example: ConfirmationPolicyEnum.ALWAYS,
})
type!: ConfirmationPolicyEnum;
@IsOptional()
@ValidateNested()
@Type(() => NoticeThreshold_2024_06_14)
@ApiPropertyOptional({
description: "The notice threshold required before confirmation is needed. Required when type is 'time'.",
type: NoticeThreshold_2024_06_14,
})
noticeThreshold?: NoticeThreshold_2024_06_14;
@IsBoolean()
blockUnconfirmedBookingsInBooker!: boolean;
@IsOptional()
@IsBoolean()
disabled?: boolean = false;
}
export type ConfirmationPolicy_2024_06_14 = BaseConfirmationPolicy_2024_06_14 | Disabled_2024_06_14;
// Validator for confirmation settings
@ValidatorConstraint({ name: "ConfirmationPolicyValidator", async: false })
export class ConfirmationPolicyValidator implements ValidatorConstraintInterface {
validate(value: ConfirmationPolicy_2024_06_14): boolean {
if (value.disabled) {
return true;
}
const { type, noticeThreshold } = value;
if (!type) return false;
if (type === ConfirmationPolicyEnum.ALWAYS) {
return true;
}
if (type === ConfirmationPolicyEnum.TIME) {
return !!(
noticeThreshold &&
typeof noticeThreshold.count === "number" &&
typeof noticeThreshold.unit === "string"
);
}
return false;
}
defaultMessage(): string {
return `Invalid requiresConfirmation structure. Use "type": "always" or provide a valid time and unit in "noticeThreshold" for "type": "time".`;
}
}
// Custom decorator for confirmation validation
export function ValidateConfirmationPolicy(validationOptions?: ValidationOptions) {
return function (object: any, propertyName: string) {
registerDecorator({
name: "ValidateConfirmationPolicy",
target: object.constructor,
propertyName,
options: validationOptions,
validator: ConfirmationPolicyValidator,
});
};
}
export type NoticeThresholdTransformedSchema = {
unit: NoticeThresholdUnitEnum;
time: number;
};
export type ConfirmationPolicyTransformedSchema = {
requiresConfirmation: boolean;
requiresConfirmationThreshold?: NoticeThresholdTransformedSchema;
requiresConfirmationWillBlockSlot: boolean;
};
@@ -1,5 +1,5 @@
import { ApiProperty as DocsProperty, getSchemaPath, ApiExtraModels } from "@nestjs/swagger";
import { Type, Transform } from "class-transformer";
import { Type, Transform, Expose } from "class-transformer";
import {
IsString,
IsInt,
@@ -13,6 +13,7 @@ import {
import { SchedulingType } from "@calcom/platform-enums";
import { BookerLayouts_2024_06_14 } from "./booker-layouts.input";
import {
AddressFieldInput_2024_06_14,
BooleanFieldInput_2024_06_14,
@@ -28,11 +29,10 @@ import {
} from "./booking-fields.input";
import type { InputBookingField_2024_06_14 } from "./booking-fields.input";
import { ValidateInputBookingFields_2024_06_14 } from "./booking-fields.input";
import { BookingLimitsCount_2024_06_14, ValidateBookingLimistsCount } from "./booking-limits-count.input";
import {
BookingLimitsDuration_2024_06_14,
ValidateBookingLimistsDuration,
} from "./booking-limits-duration.input";
import type { BookingLimitsCount_2024_06_14 } from "./booking-limits-count.input";
import { ValidateBookingLimistsCount } from "./booking-limits-count.input";
import type { BookingLimitsDuration_2024_06_14 } from "./booking-limits-duration.input";
import { ValidateBookingLimistsDuration } from "./booking-limits-duration.input";
import type { BookingWindow_2024_06_14 } from "./booking-window.input";
import {
BusinessDaysWindow_2024_06_14,
@@ -40,6 +40,11 @@ import {
RangeWindow_2024_06_14,
ValidateBookingWindow,
} from "./booking-window.input";
import type { ConfirmationPolicy_2024_06_14 } from "./confirmation-policy.input";
import { ValidateConfirmationPolicy } from "./confirmation-policy.input";
import { DestinationCalendar_2024_06_14 } from "./destination-calendar.input";
import { Disabled_2024_06_14 } from "./disabled.input";
import { EventTypeColor_2024_06_14 } from "./event-type-color.input";
import {
AddressLocation_2024_06_14,
IntegrationLocation_2024_06_14,
@@ -49,6 +54,7 @@ import {
} from "./locations.input";
import type { Location_2024_06_14 } from "./locations.input";
import { Recurrence_2024_06_14 } from "./recurrence.input";
import { Seats_2024_06_14 } from "./seats.input";
export const CREATE_EVENT_LENGTH_EXAMPLE = 60;
export const CREATE_EVENT_TITLE_EXAMPLE = "Learn the secrets of masterchief!";
@@ -177,7 +183,6 @@ export class CreateEventTypeInput_2024_06_14 {
scheduleId?: number;
@IsOptional()
@Type(() => BookingLimitsCount_2024_06_14)
@ValidateBookingLimistsCount()
@DocsProperty({ description: "Limit how many times this event can be booked" })
bookingLimitsCount?: BookingLimitsCount_2024_06_14;
@@ -191,7 +196,6 @@ export class CreateEventTypeInput_2024_06_14 {
onlyShowFirstAvailableSlot?: boolean;
@IsOptional()
@Type(() => BookingLimitsDuration_2024_06_14)
@ValidateBookingLimistsDuration()
@DocsProperty({ description: "Limit total amount of time that this event can be booked" })
bookingLimitsDuration?: BookingLimitsDuration_2024_06_14;
@@ -217,12 +221,75 @@ export class CreateEventTypeInput_2024_06_14 {
offsetStart?: number;
@IsOptional()
@Type(() => BookerLayouts_2024_06_14)
bookerLayouts?: BookerLayouts_2024_06_14;
@IsOptional()
@ValidateConfirmationPolicy()
confirmationPolicy?: ConfirmationPolicy_2024_06_14;
@ValidateNested()
@Type(() => Recurrence_2024_06_14)
@DocsProperty({
description: "Create a recurring event that can be booked once but will occur multiple times",
@Transform(({ value }) => {
if (value && typeof value === "object") {
if ("interval" in value) {
return Object.assign(new Recurrence_2024_06_14(), value);
} else if ("disabled" in value) {
return Object.assign(new Disabled_2024_06_14(), value);
}
}
return value;
})
recurrence?: Recurrence_2024_06_14;
@ValidateNested()
recurrence?: Recurrence_2024_06_14 | Disabled_2024_06_14;
@IsOptional()
@IsBoolean()
requiresBookerEmailVerification?: boolean;
@IsOptional()
@IsBoolean()
hideCalendarNotes?: boolean;
@IsOptional()
@IsBoolean()
lockTimeZoneToggleOnBookingPage?: boolean;
@IsOptional()
@Type(() => EventTypeColor_2024_06_14)
color?: EventTypeColor_2024_06_14;
@IsOptional()
@Transform(({ value }) => {
if (value && typeof value === "object") {
if ("seatsPerTimeSlot" in value) {
return Object.assign(new Seats_2024_06_14(), value);
} else if ("disabled" in value) {
return Object.assign(new Disabled_2024_06_14(), value);
}
}
return value;
})
@ValidateNested()
seats?: Seats_2024_06_14 | Disabled_2024_06_14;
@IsOptional()
@IsString()
@DocsProperty({
description: `Customizable event name with valid variables:
{Event type title}, {Organiser}, {Scheduler}, {Location}, {Organiser first name},
{Scheduler first name}, {Scheduler last name}, {Event duration}, {LOCATION},
{HOST/ATTENDEE}, {HOST}, {ATTENDEE}, {USER}`,
example: "{Event type title} between {Organiser} and {Scheduler}",
})
customName?: string;
@IsOptional()
@Type(() => DestinationCalendar_2024_06_14)
destinationCalendar?: DestinationCalendar_2024_06_14;
@IsOptional()
@IsBoolean()
useDestinationCalendarEmail?: boolean;
}
export enum HostPriority {
@@ -233,11 +300,13 @@ export enum HostPriority {
highest = "highest",
}
export class Host {
@Expose()
@IsInt()
@DocsProperty({ description: "Which user is the host of this event" })
userId!: number;
@IsOptional()
@Expose()
@IsBoolean()
@DocsProperty({
description:
@@ -246,6 +315,7 @@ export class Host {
mandatory?: boolean = false;
@IsEnum(HostPriority)
@Expose()
@IsOptional()
@DocsProperty({ enum: HostPriority })
priority?: keyof typeof HostPriority = "medium";
@@ -0,0 +1,18 @@
import { ApiProperty } from "@nestjs/swagger";
import { IsString } from "class-validator";
export class DestinationCalendar_2024_06_14 {
@ApiProperty({
description:
"The integration type of the destination calendar. Refer to the /api/v2/calendars endpoint to retrieve the integration type of your connected calendars.",
})
@IsString()
integration!: string;
@ApiProperty({
description:
"The external ID of the destination calendar. Refer to the /api/v2/calendars endpoint to retrieve the external IDs of your connected calendars.",
})
@IsString()
externalId!: string;
}
@@ -0,0 +1,13 @@
import { ApiProperty } from "@nestjs/swagger";
import { IsBoolean, IsOptional } from "class-validator";
export class Disabled_2024_06_14 {
@IsOptional()
@IsBoolean()
@ApiProperty({
description: "Indicates if the option is disabled",
example: true,
default: false,
})
disabled!: true;
}
@@ -0,0 +1,26 @@
import { ApiProperty } from "@nestjs/swagger";
import { IsHexColor, IsString } from "class-validator";
// Class representing the event type colors
export class EventTypeColor_2024_06_14 {
@IsHexColor()
@IsString()
@ApiProperty({
description: "Color used for event types in light theme",
example: "#292929",
})
lightThemeHex!: string;
@IsHexColor()
@IsString()
@ApiProperty({
description: "Color used for event types in dark theme",
example: "#fafafa",
})
darkThemeHex!: string;
}
export type EventTypeColorsTransformedSchema = {
darkEventTypeColor: string;
lightEventTypeColor: string;
};
@@ -7,3 +7,9 @@ export * from "./booking-fields.input";
export * from "./recurrence.input";
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 "./event-type-color.input";
export * from "./seats.input";
export * from "./destination-calendar.input";
export * from "./disabled.input";
@@ -1,5 +1,5 @@
import { ApiProperty } from "@nestjs/swagger";
import { IsInt, IsEnum } from "class-validator";
import { IsInt, IsEnum, IsOptional, IsBoolean } from "class-validator";
import { FrequencyInput } from "@calcom/platform-enums";
@@ -21,4 +21,8 @@ export class Recurrence_2024_06_14 {
@IsEnum(FrequencyInput)
@ApiProperty({ enum: FrequencyInput })
frequency!: FrequencyInput;
@IsOptional()
@IsBoolean()
disabled?: boolean = false;
}
@@ -0,0 +1,41 @@
import { ApiProperty } from "@nestjs/swagger";
import { IsBoolean, IsInt, IsOptional, Min } from "class-validator";
// Class representing the seat options
export class Seats_2024_06_14 {
@IsInt()
@Min(1)
@ApiProperty({
description: "Number of seats available per time slot",
example: 4,
})
seatsPerTimeSlot!: number;
@IsBoolean()
@ApiProperty({
description: "Show attendee information to other guests",
example: true,
})
showAttendeeInfo!: boolean;
@IsBoolean()
@ApiProperty({
description: "Display the count of available seats",
example: true,
})
showAvailabilityCount!: boolean;
@IsOptional()
@IsBoolean()
disabled?: boolean = false;
}
export type SeatOptionsTransformedSchema = {
seatsPerTimeSlot: number;
seatsShowAttendees: boolean;
seatsShowAvailabilityCount: boolean;
};
export type SeatOptionsDisabledSchema = {
seatsPerTimeSlot: null;
};
@@ -1,7 +1,8 @@
import { ApiProperty as DocsProperty, getSchemaPath, ApiExtraModels } from "@nestjs/swagger";
import { Type } from "class-transformer";
import { Type, Transform } from "class-transformer";
import { IsString, IsInt, IsBoolean, IsOptional, Min, ValidateNested, IsArray } from "class-validator";
import { BookerLayouts_2024_06_14 } from "./booker-layouts.input";
import type { InputBookingField_2024_06_14 } from "./booking-fields.input";
import {
AddressFieldInput_2024_06_14,
@@ -17,11 +18,10 @@ import {
TextFieldInput_2024_06_14,
ValidateInputBookingFields_2024_06_14,
} from "./booking-fields.input";
import { BookingLimitsCount_2024_06_14, ValidateBookingLimistsCount } from "./booking-limits-count.input";
import {
BookingLimitsDuration_2024_06_14,
ValidateBookingLimistsDuration,
} from "./booking-limits-duration.input";
import type { BookingLimitsCount_2024_06_14 } from "./booking-limits-count.input";
import { ValidateBookingLimistsCount } from "./booking-limits-count.input";
import type { BookingLimitsDuration_2024_06_14 } from "./booking-limits-duration.input";
import { ValidateBookingLimistsDuration } from "./booking-limits-duration.input";
import {
BusinessDaysWindow_2024_06_14,
CalendarDaysWindow_2024_06_14,
@@ -29,6 +29,8 @@ import {
ValidateBookingWindow,
type BookingWindow_2024_06_14,
} from "./booking-window.input";
import type { ConfirmationPolicy_2024_06_14 } from "./confirmation-policy.input";
import { ValidateConfirmationPolicy } from "./confirmation-policy.input";
import {
CREATE_EVENT_DESCRIPTION_EXAMPLE,
CREATE_EVENT_LENGTH_EXAMPLE,
@@ -36,6 +38,9 @@ import {
CREATE_EVENT_TITLE_EXAMPLE,
Host,
} from "./create-event-type.input";
import { DestinationCalendar_2024_06_14 } from "./destination-calendar.input";
import { Disabled_2024_06_14 } from "./disabled.input";
import { EventTypeColor_2024_06_14 } from "./event-type-color.input";
import {
AddressLocation_2024_06_14,
IntegrationLocation_2024_06_14,
@@ -45,6 +50,7 @@ import {
} from "./locations.input";
import type { Location_2024_06_14 } from "./locations.input";
import { Recurrence_2024_06_14 } from "./recurrence.input";
import { Seats_2024_06_14 } from "./seats.input";
@ApiExtraModels(
AddressLocation_2024_06_14,
@@ -78,8 +84,8 @@ export class UpdateEventTypeInput_2024_06_14 {
@DocsProperty({ example: CREATE_EVENT_TITLE_EXAMPLE })
title?: string;
@IsString()
@IsOptional()
@IsString()
@DocsProperty({ example: CREATE_EVENT_SLUG_EXAMPLE })
slug?: string;
@@ -170,7 +176,6 @@ export class UpdateEventTypeInput_2024_06_14 {
scheduleId?: number;
@IsOptional()
@Type(() => BookingLimitsCount_2024_06_14)
@ValidateBookingLimistsCount()
@DocsProperty({ description: "Limit how many times this event can be booked" })
bookingLimitsCount?: BookingLimitsCount_2024_06_14;
@@ -184,7 +189,6 @@ export class UpdateEventTypeInput_2024_06_14 {
onlyShowFirstAvailableSlot?: boolean;
@IsOptional()
@Type(() => BookingLimitsDuration_2024_06_14)
@ValidateBookingLimistsDuration()
@DocsProperty({ description: "Limit total amount of time that this event can be booked" })
bookingLimitsDuration?: BookingLimitsDuration_2024_06_14;
@@ -210,12 +214,74 @@ export class UpdateEventTypeInput_2024_06_14 {
offsetStart?: number;
@IsOptional()
@ValidateNested()
@Type(() => Recurrence_2024_06_14)
@DocsProperty({
description: "Create a recurring event that can be booked once but will occur multiple times",
@Type(() => BookerLayouts_2024_06_14)
bookerLayouts?: BookerLayouts_2024_06_14;
@IsOptional()
@ValidateConfirmationPolicy()
confirmationPolicy?: ConfirmationPolicy_2024_06_14;
@Transform(({ value }) => {
if (value && typeof value === "object") {
if ("interval" in value) {
return Object.assign(new Recurrence_2024_06_14(), value);
} else if ("disabled" in value) {
return Object.assign(new Disabled_2024_06_14(), value);
}
}
return value;
})
recurrence?: Recurrence_2024_06_14;
@ValidateNested()
recurrence?: Recurrence_2024_06_14 | Disabled_2024_06_14;
@IsOptional()
@IsBoolean()
requiresBookerEmailVerification?: boolean;
@IsOptional()
@IsBoolean()
hideCalendarNotes?: boolean;
@IsOptional()
@IsBoolean()
lockTimeZoneToggleOnBookingPage?: boolean;
@IsOptional()
@Type(() => EventTypeColor_2024_06_14)
color?: EventTypeColor_2024_06_14;
@IsOptional()
@Transform(({ value }) => {
if (value && typeof value === "object") {
if ("seatsPerTimeSlot" in value) {
return Object.assign(new Seats_2024_06_14(), value);
} else if ("disabled" in value) {
return Object.assign(new Disabled_2024_06_14(), value);
}
}
return value;
})
@ValidateNested()
seats?: Seats_2024_06_14 | Disabled_2024_06_14;
@IsOptional()
@IsString()
@DocsProperty({
description: `Customizable event name with valid variables:
{Event type title}, {Organiser}, {Scheduler}, {Location}, {Organiser first name},
{Scheduler first name}, {Scheduler last name}, {Event duration}, {LOCATION},
{HOST/ATTENDEE}, {HOST}, {ATTENDEE}, {USER}`,
example: "{Event type title} between {Organiser} and {Scheduler}",
})
customName?: string;
@IsOptional()
@Type(() => DestinationCalendar_2024_06_14)
destinationCalendar?: DestinationCalendar_2024_06_14;
@IsOptional()
@IsBoolean()
useDestinationCalendarEmail?: boolean;
}
@ApiExtraModels(
@@ -246,8 +312,8 @@ export class UpdateTeamEventTypeInput_2024_06_14 extends UpdateEventTypeInput_20
@DocsProperty()
hosts?: Host[];
@IsBoolean()
@IsOptional()
@IsBoolean()
@DocsProperty()
@DocsProperty({
description: "If true, all current and future team members will be assigned to this event type",
@@ -1,4 +1,4 @@
import { ApiProperty as DocsProperty, getSchemaPath, ApiExtraModels } from "@nestjs/swagger";
import { ApiProperty as DocsProperty, ApiExtraModels, getSchemaPath } from "@nestjs/swagger";
import { Type } from "class-transformer";
import {
IsArray,
@@ -11,16 +11,25 @@ import {
ValidateNested,
} from "class-validator";
import type { Location_2024_06_14, BookingWindow_2024_06_14 } from "../inputs";
import {
Host as TeamEventTypeHostInput,
import type {
Location_2024_06_14,
BookingWindow_2024_06_14,
BookingLimitsDuration_2024_06_14,
} from "../inputs";
import {
EventTypeColor_2024_06_14,
Seats_2024_06_14,
Host as TeamEventTypeHostInput,
BaseBookingLimitsDuration_2024_06_14,
BusinessDaysWindow_2024_06_14,
CalendarDaysWindow_2024_06_14,
RangeWindow_2024_06_14,
} from "../inputs";
import { Recurrence_2024_06_14 } from "../inputs";
import { BookingLimitsCount_2024_06_14 } from "../inputs/booking-limits-count.input";
import { BookerLayouts_2024_06_14 } from "../inputs/booker-layouts.input";
import type { BookingLimitsCount_2024_06_14 } from "../inputs/booking-limits-count.input";
import type { ConfirmationPolicy_2024_06_14 } from "../inputs/confirmation-policy.input";
import { DestinationCalendar_2024_06_14 } from "../inputs/destination-calendar.input";
import {
AddressLocation_2024_06_14,
IntegrationLocation_2024_06_14,
@@ -81,6 +90,7 @@ class User_2024_06_14 {
@IsString()
darkBrandColor!: string | null;
@Type(() => Object)
metadata!: Record<string, unknown>;
}
@@ -107,7 +117,7 @@ class User_2024_06_14 {
SelectFieldOutput_2024_06_14,
TextAreaFieldOutput_2024_06_14,
TextFieldOutput_2024_06_14,
BookingLimitsDuration_2024_06_14,
BaseBookingLimitsDuration_2024_06_14,
BusinessDaysWindow_2024_06_14,
CalendarDaysWindow_2024_06_14,
RangeWindow_2024_06_14
@@ -132,7 +142,7 @@ class BaseEventTypeOutput_2024_06_14 {
@IsString()
@DocsProperty({
example: "Discover the culinary wonders of the Argentina by making the best flan ever!",
example: "Discover the culinary wonders of Argentina by making the best flan ever!",
})
description!: string;
@@ -150,6 +160,7 @@ class BaseEventTypeOutput_2024_06_14 {
locations!: Location_2024_06_14[];
@ValidateOutputBookingFields_2024_06_14()
@DocsProperty()
@DocsProperty({
oneOf: [
{ $ref: getSchemaPath(NameDefaultFieldOutput_2024_06_14) },
@@ -209,10 +220,6 @@ class BaseEventTypeOutput_2024_06_14 {
@DocsProperty()
metadata!: Record<string, unknown>;
@IsBoolean()
@DocsProperty()
requiresConfirmation!: boolean;
@IsInt()
@DocsProperty()
price!: number;
@@ -227,7 +234,8 @@ class BaseEventTypeOutput_2024_06_14 {
@IsInt()
@DocsProperty()
seatsPerTimeSlot!: number | null;
@IsOptional()
seatsPerTimeSlot?: number | null;
@IsBoolean()
@DocsProperty()
@@ -239,7 +247,12 @@ class BaseEventTypeOutput_2024_06_14 {
@IsBoolean()
@DocsProperty()
seatsShowAvailabilityCount!: boolean | null;
isInstantEvent!: boolean;
@IsOptional()
@IsBoolean()
@DocsProperty()
seatsShowAvailabilityCount?: boolean | null;
@IsInt()
@DocsProperty()
@@ -247,7 +260,6 @@ class BaseEventTypeOutput_2024_06_14 {
@IsOptional()
@DocsProperty()
@Type(() => BookingLimitsCount_2024_06_14)
bookingLimitsCount?: BookingLimitsCount_2024_06_14;
@IsOptional()
@@ -256,7 +268,6 @@ class BaseEventTypeOutput_2024_06_14 {
onlyShowFirstAvailableSlot?: boolean;
@IsOptional()
@Type(() => BookingLimitsDuration_2024_06_14)
@DocsProperty()
bookingLimitsDuration?: BookingLimitsDuration_2024_06_14;
@@ -273,11 +284,55 @@ class BaseEventTypeOutput_2024_06_14 {
@Type(() => Object)
bookingWindow?: BookingWindow_2024_06_14;
@IsOptional()
@Type(() => BookerLayouts_2024_06_14)
@DocsProperty()
bookerLayouts?: BookerLayouts_2024_06_14;
@IsOptional()
@DocsProperty()
confirmationPolicy?: ConfirmationPolicy_2024_06_14;
@IsOptional()
@IsBoolean()
@DocsProperty()
requiresBookerEmailVerification?: boolean;
@IsOptional()
@IsBoolean()
@DocsProperty()
hideCalendarNotes?: boolean;
@IsOptional()
@Type(() => EventTypeColor_2024_06_14)
@DocsProperty()
color?: EventTypeColor_2024_06_14;
@IsOptional()
@Type(() => Seats_2024_06_14)
@DocsProperty()
seats?: Seats_2024_06_14;
@IsOptional()
@IsInt()
@Min(1)
@DocsProperty()
offsetStart?: number;
@IsOptional()
@IsString()
@DocsProperty()
customName?: string;
@IsOptional()
@Type(() => DestinationCalendar_2024_06_14)
@DocsProperty()
destinationCalendar?: DestinationCalendar_2024_06_14;
@IsOptional()
@IsBoolean()
@DocsProperty()
useDestinationCalendarEmail?: boolean;
}
export class TeamEventTypeResponseHost extends TeamEventTypeHostInput {
@@ -298,10 +353,6 @@ export class EventTypeOutput_2024_06_14 extends BaseEventTypeOutput_2024_06_14 {
}
export class TeamEventTypeOutput_2024_06_14 extends BaseEventTypeOutput_2024_06_14 {
@IsEnum(SchedulingTypeEnum)
@DocsProperty({ enum: SchedulingTypeEnum })
schedulingType!: EventTypesOutputSchedulingType | null;
@IsInt()
@IsOptional()
@DocsProperty()
@@ -316,7 +367,7 @@ export class TeamEventTypeOutput_2024_06_14 extends BaseEventTypeOutput_2024_06_
@IsOptional()
@DocsProperty({
description:
"For managed event types parent event type is the event type that this event type is based on",
"For managed event types, parent event type is the event type that this event type is based on",
})
parentEventTypeId?: number | null;
@@ -330,4 +381,8 @@ export class TeamEventTypeOutput_2024_06_14 extends BaseEventTypeOutput_2024_06_
@IsOptional()
@DocsProperty()
assignAllTeamMembers?: boolean;
@IsEnum(SchedulingTypeEnum)
@DocsProperty({ enum: SchedulingTypeEnum })
schedulingType!: EventTypesOutputSchedulingType | null;
}
@@ -0,0 +1,59 @@
import type { z } from "zod";
import type {
transformEventColorsApiToInternal,
transformFutureBookingLimitsApiToInternal,
transformIntervalLimitsApiToInternal,
transformRecurrenceApiToInternal,
transformSeatsApiToInternal,
transformBookingFieldsApiToInternal,
TransformedLocationsSchema,
} from "@calcom/lib/event-types/transformers";
import type { CreateEventTypeInput_2024_06_14, ConfirmationPolicyTransformedSchema } from "../inputs";
export type InputEventTransformed_2024_06_14 = Omit<
CreateEventTypeInput_2024_06_14,
| "lengthInMinutes"
| "locations"
| "bookingFields"
| "bookingLimitsCount"
| "bookingLimitsDuration"
| "bookingWindow"
| "bookerLayouts"
| "confirmationPolicy"
| "recurrence"
| "color"
| "seats"
| "customName"
| "useDestinationCalendarEmail"
> & {
length: number;
slug: string;
eventName?: string;
bookingLimits?: ReturnType<typeof transformIntervalLimitsApiToInternal>;
locations?: z.infer<typeof TransformedLocationsSchema>;
bookingFields?: ReturnType<typeof transformBookingFieldsApiToInternal>;
durationLimits?: ReturnType<typeof transformIntervalLimitsApiToInternal>;
recurringEvent?: ReturnType<typeof transformRecurrenceApiToInternal>;
eventTypeColor?: ReturnType<typeof transformEventColorsApiToInternal>;
useEventTypeDestinationCalendarEmail?: boolean;
} & Partial<
Pick<ConfirmationPolicyTransformedSchema, "requiresConfirmation" | "requiresConfirmationWillBlockSlot">
> &
Partial<ReturnType<typeof transformSeatsApiToInternal>> &
Partial<ReturnType<typeof transformFutureBookingLimitsApiToInternal>>;
export type InputTeamEventTransformed_2024_06_14 = InputEventTransformed_2024_06_14 & {
hosts: {
userId: number;
isFixed: boolean;
priority: number;
}[];
children: {
id: number;
name: string;
email: string;
eventTypeSlugs: string[];
}[];
};
@@ -0,0 +1 @@
export * from "./event-type.tranformed";
+147 -5721
View File
File diff suppressed because it is too large Load Diff