From 3519e6b2904ca1ebf1e90c05a905ca90a2b7a129 Mon Sep 17 00:00:00 2001 From: supalarry Date: Tue, 13 Aug 2024 12:11:35 +0200 Subject: [PATCH 01/96] chore: version existing bookings as 2024-04-15 --- .../{ => 2024-04-15}/bookings.module.ts | 6 +- .../bookings.controller.e2e-spec.ts | 16 +-- .../controllers/bookings.controller.ts | 40 ++++---- .../inputs/create-booking.input.ts | 2 +- .../inputs/create-recurring-booking.input.ts | 4 +- .../inputs/mark-no-show.input.ts | 2 +- .../outputs/get-booking.output.ts | 10 +- .../outputs/get-bookings.output.ts | 10 +- .../outputs/mark-no-show.output.ts | 10 +- .../v2/src/ee/platform-endpoints-module.ts | 4 +- apps/api/v2/swagger/documentation.json | 98 +++++-------------- .../platform/atoms/hooks/useGetBookings.ts | 4 +- .../types/bookings/2024-04-15/index.ts | 1 + .../2024-04-15/inputs/index.ts} | 10 +- packages/platform/types/bookings/index.ts | 1 + 15 files changed, 89 insertions(+), 129 deletions(-) rename apps/api/v2/src/ee/bookings/{ => 2024-04-15}/bookings.module.ts (78%) rename apps/api/v2/src/ee/bookings/{ => 2024-04-15}/controllers/bookings.controller.e2e-spec.ts (95%) rename apps/api/v2/src/ee/bookings/{ => 2024-04-15}/controllers/bookings.controller.ts (89%) rename apps/api/v2/src/ee/bookings/{ => 2024-04-15}/inputs/create-booking.input.ts (97%) rename apps/api/v2/src/ee/bookings/{ => 2024-04-15}/inputs/create-recurring-booking.input.ts (68%) rename apps/api/v2/src/ee/bookings/{ => 2024-04-15}/inputs/mark-no-show.input.ts (90%) rename apps/api/v2/src/ee/bookings/{ => 2024-04-15}/outputs/get-booking.output.ts (92%) rename apps/api/v2/src/ee/bookings/{ => 2024-04-15}/outputs/get-bookings.output.ts (94%) rename apps/api/v2/src/ee/bookings/{ => 2024-04-15}/outputs/mark-no-show.output.ts (78%) create mode 100644 packages/platform/types/bookings/2024-04-15/index.ts rename packages/platform/types/{bookings.ts => bookings/2024-04-15/inputs/index.ts} (87%) create mode 100644 packages/platform/types/bookings/index.ts diff --git a/apps/api/v2/src/ee/bookings/bookings.module.ts b/apps/api/v2/src/ee/bookings/2024-04-15/bookings.module.ts similarity index 78% rename from apps/api/v2/src/ee/bookings/bookings.module.ts rename to apps/api/v2/src/ee/bookings/2024-04-15/bookings.module.ts index 8e1be0f3e7c74e..bf365cec13c815 100644 --- a/apps/api/v2/src/ee/bookings/bookings.module.ts +++ b/apps/api/v2/src/ee/bookings/2024-04-15/bookings.module.ts @@ -1,4 +1,4 @@ -import { BookingsController } from "@/ee/bookings/controllers/bookings.controller"; +import { BookingsController_2024_04_15 } from "@/ee/bookings/2024-04-15/controllers/bookings.controller"; import { BillingModule } from "@/modules/billing/billing.module"; import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository"; import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service"; @@ -11,6 +11,6 @@ import { Module } from "@nestjs/common"; @Module({ imports: [PrismaModule, RedisModule, TokensModule, BillingModule], providers: [TokensRepository, OAuthFlowService, OAuthClientRepository], - controllers: [BookingsController], + controllers: [BookingsController_2024_04_15], }) -export class BookingsModule {} +export class BookingsModule_2024_04_15 {} diff --git a/apps/api/v2/src/ee/bookings/controllers/bookings.controller.e2e-spec.ts b/apps/api/v2/src/ee/bookings/2024-04-15/controllers/bookings.controller.e2e-spec.ts similarity index 95% rename from apps/api/v2/src/ee/bookings/controllers/bookings.controller.e2e-spec.ts rename to apps/api/v2/src/ee/bookings/2024-04-15/controllers/bookings.controller.e2e-spec.ts index c8731539f55d0d..33e39941813be7 100644 --- a/apps/api/v2/src/ee/bookings/controllers/bookings.controller.e2e-spec.ts +++ b/apps/api/v2/src/ee/bookings/2024-04-15/controllers/bookings.controller.e2e-spec.ts @@ -1,8 +1,8 @@ import { bootstrap } from "@/app"; import { AppModule } from "@/app.module"; -import { CreateBookingInput } from "@/ee/bookings/inputs/create-booking.input"; -import { GetBookingOutput } from "@/ee/bookings/outputs/get-booking.output"; -import { GetBookingsOutput } from "@/ee/bookings/outputs/get-bookings.output"; +import { CreateBookingInput_2024_04_15 } from "@/ee/bookings/2024-04-15/inputs/create-booking.input"; +import { GetBookingOutput_2024_04_15 } from "@/ee/bookings/2024-04-15/outputs/get-booking.output"; +import { GetBookingsOutput_2024_04_15 } from "@/ee/bookings/2024-04-15/outputs/get-bookings.output"; import { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input"; import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module"; import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service"; @@ -23,7 +23,7 @@ import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants"; import { handleNewBooking } from "@calcom/platform-libraries"; import { ApiSuccessResponse, ApiResponse } from "@calcom/platform-types"; -describe("Bookings Endpoints", () => { +describe("Bookings Endpoints 2024-04-15", () => { describe("User Authenticated", () => { let app: INestApplication; @@ -106,7 +106,7 @@ describe("Bookings Endpoints", () => { guests: [], }; - const body: CreateBookingInput = { + const body: CreateBookingInput_2024_04_15 = { start: bookingStart, end: bookingEnd, eventTypeId: bookingEventTypeId, @@ -162,7 +162,7 @@ describe("Bookings Endpoints", () => { guests: [], }; - const body: CreateBookingInput = { + const body: CreateBookingInput_2024_04_15 = { rescheduleUid: createdBooking.uid, start: newBookingStart, end: newBookingEnd, @@ -201,7 +201,7 @@ describe("Bookings Endpoints", () => { return request(app.getHttpServer()) .get("/v2/bookings?filters[status]=upcoming") .then((response) => { - const responseBody: GetBookingsOutput = response.body; + const responseBody: GetBookingsOutput_2024_04_15 = response.body; const fetchedBooking = responseBody.data.bookings[0]; expect(responseBody.data.bookings.length).toEqual(1); @@ -221,7 +221,7 @@ describe("Bookings Endpoints", () => { return request(app.getHttpServer()) .get(`/v2/bookings/${createdBooking.uid}`) .then((response) => { - const responseBody: GetBookingOutput = response.body; + const responseBody: GetBookingOutput_2024_04_15 = response.body; const bookingInfo = responseBody.data; expect(responseBody.status).toEqual(SUCCESS_STATUS); diff --git a/apps/api/v2/src/ee/bookings/controllers/bookings.controller.ts b/apps/api/v2/src/ee/bookings/2024-04-15/controllers/bookings.controller.ts similarity index 89% rename from apps/api/v2/src/ee/bookings/controllers/bookings.controller.ts rename to apps/api/v2/src/ee/bookings/2024-04-15/controllers/bookings.controller.ts index dfe7b26c70c479..18f69b49073abc 100644 --- a/apps/api/v2/src/ee/bookings/controllers/bookings.controller.ts +++ b/apps/api/v2/src/ee/bookings/2024-04-15/controllers/bookings.controller.ts @@ -1,9 +1,9 @@ -import { CreateBookingInput } from "@/ee/bookings/inputs/create-booking.input"; -import { CreateRecurringBookingInput } from "@/ee/bookings/inputs/create-recurring-booking.input"; -import { MarkNoShowInput } from "@/ee/bookings/inputs/mark-no-show.input"; -import { GetBookingOutput } from "@/ee/bookings/outputs/get-booking.output"; -import { GetBookingsOutput } from "@/ee/bookings/outputs/get-bookings.output"; -import { MarkNoShowOutput } from "@/ee/bookings/outputs/mark-no-show.output"; +import { CreateBookingInput_2024_04_15 } from "@/ee/bookings/2024-04-15/inputs/create-booking.input"; +import { CreateRecurringBookingInput_2024_04_15 } from "@/ee/bookings/2024-04-15/inputs/create-recurring-booking.input"; +import { MarkNoShowInput_2024_04_15 } from "@/ee/bookings/2024-04-15/inputs/mark-no-show.input"; +import { GetBookingOutput_2024_04_15 } from "@/ee/bookings/2024-04-15/outputs/get-booking.output"; +import { GetBookingsOutput_2024_04_15 } from "@/ee/bookings/2024-04-15/outputs/get-bookings.output"; +import { MarkNoShowOutput_2024_04_15 } from "@/ee/bookings/2024-04-15/outputs/mark-no-show.output"; import { API_VERSIONS_VALUES } from "@/lib/api-versions"; import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator"; import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator"; @@ -47,7 +47,11 @@ import { handleCancelBooking, getBookingForReschedule, } from "@calcom/platform-libraries"; -import { GetBookingsInput, CancelBookingInput, Status } from "@calcom/platform-types"; +import { + GetBookingsInput_2024_04_15, + CancelBookingInput_2024_04_15, + Status_2024_04_15, +} from "@calcom/platform-types"; import { ApiResponse } from "@calcom/platform-types"; import { PrismaClient } from "@calcom/prisma"; @@ -79,7 +83,7 @@ const DEFAULT_PLATFORM_PARAMS = { }) @UseGuards(PermissionsGuard) @DocsTags("Bookings") -export class BookingsController { +export class BookingsController_2024_04_15 { private readonly logger = new Logger("BookingsController"); constructor( @@ -92,13 +96,13 @@ export class BookingsController { @Get("/") @UseGuards(ApiAuthGuard) @Permissions([BOOKING_READ]) - @ApiQuery({ name: "filters[status]", enum: Status, required: true }) + @ApiQuery({ name: "filters[status]", enum: Status_2024_04_15, required: true }) @ApiQuery({ name: "limit", type: "number", required: false }) @ApiQuery({ name: "cursor", type: "number", required: false }) async getBookings( @GetUser() user: User, - @Query() queryParams: GetBookingsInput - ): Promise { + @Query() queryParams: GetBookingsInput_2024_04_15 + ): Promise { const { filters, cursor, limit } = queryParams; const bookings = await getAllUserBookings({ bookingListingByStatus: filters.status, @@ -118,7 +122,7 @@ export class BookingsController { } @Get("/:bookingUid") - async getBooking(@Param("bookingUid") bookingUid: string): Promise { + async getBooking(@Param("bookingUid") bookingUid: string): Promise { const { bookingInfo } = await getBookingInfo(bookingUid); if (!bookingInfo) { @@ -148,7 +152,7 @@ export class BookingsController { @Post("/") async createBooking( @Req() req: BookingRequest, - @Body() body: CreateBookingInput, + @Body() body: CreateBookingInput_2024_04_15, @Headers(X_CAL_CLIENT_ID) clientId?: string ): Promise>> { const oAuthClientId = clientId?.toString(); @@ -179,7 +183,7 @@ export class BookingsController { async cancelBooking( @Req() req: BookingRequest, @Param("bookingId") bookingId: string, - @Body() _: CancelBookingInput, + @Body() _: CancelBookingInput_2024_04_15, @Headers(X_CAL_CLIENT_ID) clientId?: string ): Promise> { const oAuthClientId = clientId?.toString(); @@ -212,9 +216,9 @@ export class BookingsController { @UseGuards(ApiAuthGuard) async markNoShow( @GetUser("id") userId: number, - @Body() body: MarkNoShowInput, + @Body() body: MarkNoShowInput_2024_04_15, @Param("bookingUid") bookingUid: string - ): Promise { + ): Promise { try { const markNoShowResponse = await handleMarkNoShow({ bookingUid: bookingUid, @@ -233,7 +237,7 @@ export class BookingsController { @Post("/recurring") async createRecurringBooking( @Req() req: BookingRequest, - @Body() _: CreateRecurringBookingInput[], + @Body() _: CreateRecurringBookingInput_2024_04_15[], @Headers(X_CAL_CLIENT_ID) clientId?: string ): Promise> { const oAuthClientId = clientId?.toString(); @@ -264,7 +268,7 @@ export class BookingsController { @Post("/instant") async createInstantBooking( @Req() req: BookingRequest, - @Body() _: CreateBookingInput, + @Body() _: CreateBookingInput_2024_04_15, @Headers(X_CAL_CLIENT_ID) clientId?: string ): Promise>>> { const oAuthClientId = clientId?.toString(); diff --git a/apps/api/v2/src/ee/bookings/inputs/create-booking.input.ts b/apps/api/v2/src/ee/bookings/2024-04-15/inputs/create-booking.input.ts similarity index 97% rename from apps/api/v2/src/ee/bookings/inputs/create-booking.input.ts rename to apps/api/v2/src/ee/bookings/2024-04-15/inputs/create-booking.input.ts index 7101bb80a2d518..8d8fc4c0148a26 100644 --- a/apps/api/v2/src/ee/bookings/inputs/create-booking.input.ts +++ b/apps/api/v2/src/ee/bookings/2024-04-15/inputs/create-booking.input.ts @@ -40,7 +40,7 @@ class Response { notes?: string; } -export class CreateBookingInput { +export class CreateBookingInput_2024_04_15 { @IsString() @IsOptional() end?: string; diff --git a/apps/api/v2/src/ee/bookings/inputs/create-recurring-booking.input.ts b/apps/api/v2/src/ee/bookings/2024-04-15/inputs/create-recurring-booking.input.ts similarity index 68% rename from apps/api/v2/src/ee/bookings/inputs/create-recurring-booking.input.ts rename to apps/api/v2/src/ee/bookings/2024-04-15/inputs/create-recurring-booking.input.ts index 471e8ab8dfa4e3..757e8282f65180 100644 --- a/apps/api/v2/src/ee/bookings/inputs/create-recurring-booking.input.ts +++ b/apps/api/v2/src/ee/bookings/2024-04-15/inputs/create-recurring-booking.input.ts @@ -1,9 +1,9 @@ -import { CreateBookingInput } from "@/ee/bookings/inputs/create-booking.input"; +import { CreateBookingInput_2024_04_15 } from "@/ee/bookings/2024-04-15/inputs/create-booking.input"; import { IsBoolean, IsNumber, IsOptional } from "class-validator"; import type { AppsStatus } from "@calcom/platform-libraries"; -export class CreateRecurringBookingInput extends CreateBookingInput { +export class CreateRecurringBookingInput_2024_04_15 extends CreateBookingInput_2024_04_15 { @IsBoolean() @IsOptional() noEmail?: boolean; diff --git a/apps/api/v2/src/ee/bookings/inputs/mark-no-show.input.ts b/apps/api/v2/src/ee/bookings/2024-04-15/inputs/mark-no-show.input.ts similarity index 90% rename from apps/api/v2/src/ee/bookings/inputs/mark-no-show.input.ts rename to apps/api/v2/src/ee/bookings/2024-04-15/inputs/mark-no-show.input.ts index 0630f08fcc3395..017f9edd78c8ce 100644 --- a/apps/api/v2/src/ee/bookings/inputs/mark-no-show.input.ts +++ b/apps/api/v2/src/ee/bookings/2024-04-15/inputs/mark-no-show.input.ts @@ -9,7 +9,7 @@ class Attendee { noShow!: boolean; } -export class MarkNoShowInput { +export class MarkNoShowInput_2024_04_15 { @IsBoolean() @IsOptional() noShowHost?: boolean; diff --git a/apps/api/v2/src/ee/bookings/outputs/get-booking.output.ts b/apps/api/v2/src/ee/bookings/2024-04-15/outputs/get-booking.output.ts similarity index 92% rename from apps/api/v2/src/ee/bookings/outputs/get-booking.output.ts rename to apps/api/v2/src/ee/bookings/2024-04-15/outputs/get-booking.output.ts index c770a583059624..07e352736e00d9 100644 --- a/apps/api/v2/src/ee/bookings/outputs/get-booking.output.ts +++ b/apps/api/v2/src/ee/bookings/2024-04-15/outputs/get-booking.output.ts @@ -88,7 +88,7 @@ class EventType { timeZone!: string | null; } -class GetBookingData { +class GetBookingData_2024_04_15 { @IsString() title!: string; @@ -159,15 +159,15 @@ class GetBookingData { eventType!: EventType | null; } -export class GetBookingOutput { +export class GetBookingOutput_2024_04_15 { @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) @IsEnum([SUCCESS_STATUS, ERROR_STATUS]) status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS; @ApiProperty({ - type: GetBookingData, + type: GetBookingData_2024_04_15, }) @ValidateNested() - @Type(() => GetBookingData) - data!: GetBookingData; + @Type(() => GetBookingData_2024_04_15) + data!: GetBookingData_2024_04_15; } diff --git a/apps/api/v2/src/ee/bookings/outputs/get-bookings.output.ts b/apps/api/v2/src/ee/bookings/2024-04-15/outputs/get-bookings.output.ts similarity index 94% rename from apps/api/v2/src/ee/bookings/outputs/get-bookings.output.ts rename to apps/api/v2/src/ee/bookings/2024-04-15/outputs/get-bookings.output.ts index 09e555fbb135f5..7e27580bc70bd0 100644 --- a/apps/api/v2/src/ee/bookings/outputs/get-bookings.output.ts +++ b/apps/api/v2/src/ee/bookings/2024-04-15/outputs/get-bookings.output.ts @@ -202,7 +202,7 @@ class GetBookingsDataEntry { rescheduled?: any; } -class GetBookingsData { +class GetBookingsData_2024_04_15 { @ValidateNested() @Type(() => GetBookingsDataEntry) @IsArray() @@ -215,15 +215,15 @@ class GetBookingsData { nextCursor!: number | null; } -export class GetBookingsOutput { +export class GetBookingsOutput_2024_04_15 { @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) @IsEnum([SUCCESS_STATUS, ERROR_STATUS]) status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS; @ApiProperty({ - type: GetBookingsData, + type: GetBookingsData_2024_04_15, }) @ValidateNested() - @Type(() => GetBookingsData) - data!: GetBookingsData; + @Type(() => GetBookingsData_2024_04_15) + data!: GetBookingsData_2024_04_15; } diff --git a/apps/api/v2/src/ee/bookings/outputs/mark-no-show.output.ts b/apps/api/v2/src/ee/bookings/2024-04-15/outputs/mark-no-show.output.ts similarity index 78% rename from apps/api/v2/src/ee/bookings/outputs/mark-no-show.output.ts rename to apps/api/v2/src/ee/bookings/2024-04-15/outputs/mark-no-show.output.ts index a321848b139bf2..c951987c7c1a47 100644 --- a/apps/api/v2/src/ee/bookings/outputs/mark-no-show.output.ts +++ b/apps/api/v2/src/ee/bookings/2024-04-15/outputs/mark-no-show.output.ts @@ -12,7 +12,7 @@ class Attendee { noShow!: boolean; } -class HandleMarkNoShowData { +class HandleMarkNoShowData_2024_04_15 { @IsString() message!: string; @@ -27,15 +27,15 @@ class HandleMarkNoShowData { attendees?: Attendee[]; } -export class MarkNoShowOutput { +export class MarkNoShowOutput_2024_04_15 { @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) @IsEnum([SUCCESS_STATUS, ERROR_STATUS]) status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS; @ApiProperty({ - type: HandleMarkNoShowData, + type: HandleMarkNoShowData_2024_04_15, }) @ValidateNested() - @Type(() => HandleMarkNoShowData) - data!: HandleMarkNoShowData; + @Type(() => HandleMarkNoShowData_2024_04_15) + data!: HandleMarkNoShowData_2024_04_15; } diff --git a/apps/api/v2/src/ee/platform-endpoints-module.ts b/apps/api/v2/src/ee/platform-endpoints-module.ts index 0e7df6730a5f31..6e99f63103b133 100644 --- a/apps/api/v2/src/ee/platform-endpoints-module.ts +++ b/apps/api/v2/src/ee/platform-endpoints-module.ts @@ -1,4 +1,4 @@ -import { BookingsModule } from "@/ee/bookings/bookings.module"; +import { BookingsModule_2024_04_15 } from "@/ee/bookings/2024-04-15/bookings.module"; import { CalendarsModule } from "@/ee/calendars/calendars.module"; import { EventTypesModule_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/event-types.module"; import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module"; @@ -21,7 +21,7 @@ import { Module } from "@nestjs/common"; EventTypesModule_2024_04_15, EventTypesModule_2024_06_14, CalendarsModule, - BookingsModule, + BookingsModule_2024_04_15, SlotsModule, ], }) diff --git a/apps/api/v2/swagger/documentation.json b/apps/api/v2/swagger/documentation.json index a293fb3d6c94f4..4a6d569563f4a1 100644 --- a/apps/api/v2/swagger/documentation.json +++ b/apps/api/v2/swagger/documentation.json @@ -2848,7 +2848,7 @@ }, "/v2/bookings": { "get": { - "operationId": "BookingsController_getBookings", + "operationId": "BookingsController_2024_04_15_getBookings", "parameters": [ { "name": "cursor", @@ -2870,16 +2870,7 @@ "name": "filters[status]", "required": true, "in": "query", - "schema": { - "enum": [ - "upcoming", - "recurring", - "past", - "cancelled", - "unconfirmed" - ], - "type": "string" - } + "schema": {} } ], "responses": { @@ -2888,7 +2879,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/GetBookingsOutput" + "$ref": "#/components/schemas/GetBookingsOutput_2024_04_15" } } } @@ -2899,7 +2890,7 @@ ] }, "post": { - "operationId": "BookingsController_createBooking", + "operationId": "BookingsController_2024_04_15_createBooking", "parameters": [ { "name": "x-cal-client-id", @@ -2915,7 +2906,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CreateBookingInput" + "$ref": "#/components/schemas/CreateBookingInput_2024_04_15" } } } @@ -2939,7 +2930,7 @@ }, "/v2/bookings/{bookingUid}": { "get": { - "operationId": "BookingsController_getBooking", + "operationId": "BookingsController_2024_04_15_getBooking", "parameters": [ { "name": "bookingUid", @@ -2956,7 +2947,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/GetBookingOutput" + "$ref": "#/components/schemas/GetBookingOutput_2024_04_15" } } } @@ -2969,7 +2960,7 @@ }, "/v2/bookings/{bookingUid}/reschedule": { "get": { - "operationId": "BookingsController_getBookingForReschedule", + "operationId": "BookingsController_2024_04_15_getBookingForReschedule", "parameters": [ { "name": "bookingUid", @@ -2999,7 +2990,7 @@ }, "/v2/bookings/{bookingId}/cancel": { "post": { - "operationId": "BookingsController_cancelBooking", + "operationId": "BookingsController_2024_04_15_cancelBooking", "parameters": [ { "name": "bookingId", @@ -3018,16 +3009,6 @@ } } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CancelBookingInput" - } - } - } - }, "responses": { "201": { "description": "", @@ -3047,7 +3028,7 @@ }, "/v2/bookings/{bookingUid}/mark-no-show": { "post": { - "operationId": "BookingsController_markNoShow", + "operationId": "BookingsController_2024_04_15_markNoShow", "parameters": [ { "name": "bookingUid", @@ -3063,7 +3044,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/MarkNoShowInput" + "$ref": "#/components/schemas/MarkNoShowInput_2024_04_15" } } } @@ -3074,7 +3055,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/MarkNoShowOutput" + "$ref": "#/components/schemas/MarkNoShowOutput_2024_04_15" } } } @@ -3087,7 +3068,7 @@ }, "/v2/bookings/recurring": { "post": { - "operationId": "BookingsController_createRecurringBooking", + "operationId": "BookingsController_2024_04_15_createRecurringBooking", "parameters": [ { "name": "x-cal-client-id", @@ -3130,7 +3111,7 @@ }, "/v2/bookings/instant": { "post": { - "operationId": "BookingsController_createInstantBooking", + "operationId": "BookingsController_2024_04_15_createInstantBooking", "parameters": [ { "name": "x-cal-client-id", @@ -3146,7 +3127,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CreateBookingInput" + "$ref": "#/components/schemas/CreateBookingInput_2024_04_15" } } } @@ -8628,7 +8609,7 @@ "user" ] }, - "GetBookingsData": { + "GetBookingsData_2024_04_15": { "type": "object", "properties": { "bookings": { @@ -8654,7 +8635,7 @@ "nextCursor" ] }, - "GetBookingsOutput": { + "GetBookingsOutput_2024_04_15": { "type": "object", "properties": { "status": { @@ -8666,7 +8647,7 @@ ] }, "data": { - "$ref": "#/components/schemas/GetBookingsData" + "$ref": "#/components/schemas/GetBookingsData_2024_04_15" } }, "required": [ @@ -8674,7 +8655,7 @@ "data" ] }, - "GetBookingData": { + "GetBookingData_2024_04_15": { "type": "object", "properties": { "title": { @@ -8784,7 +8765,7 @@ "eventType" ] }, - "GetBookingOutput": { + "GetBookingOutput_2024_04_15": { "type": "object", "properties": { "status": { @@ -8796,7 +8777,7 @@ ] }, "data": { - "$ref": "#/components/schemas/GetBookingData" + "$ref": "#/components/schemas/GetBookingData_2024_04_15" } }, "required": [ @@ -8832,7 +8813,7 @@ "guests" ] }, - "CreateBookingInput": { + "CreateBookingInput_2024_04_15": { "type": "object", "properties": { "end": { @@ -8901,34 +8882,7 @@ "responses" ] }, - "CancelBookingInput": { - "type": "object", - "properties": { - "id": { - "type": "number" - }, - "uid": { - "type": "string" - }, - "allRemainingBookings": { - "type": "boolean" - }, - "cancellationReason": { - "type": "string" - }, - "seatReferenceUid": { - "type": "string" - } - }, - "required": [ - "id", - "uid", - "allRemainingBookings", - "cancellationReason", - "seatReferenceUid" - ] - }, - "MarkNoShowInput": { + "MarkNoShowInput_2024_04_15": { "type": "object", "properties": { "noShowHost": { @@ -8942,7 +8896,7 @@ } } }, - "HandleMarkNoShowData": { + "HandleMarkNoShowData_2024_04_15": { "type": "object", "properties": { "message": { @@ -8962,7 +8916,7 @@ "message" ] }, - "MarkNoShowOutput": { + "MarkNoShowOutput_2024_04_15": { "type": "object", "properties": { "status": { @@ -8974,7 +8928,7 @@ ] }, "data": { - "$ref": "#/components/schemas/HandleMarkNoShowData" + "$ref": "#/components/schemas/HandleMarkNoShowData_2024_04_15" } }, "required": [ diff --git a/packages/platform/atoms/hooks/useGetBookings.ts b/packages/platform/atoms/hooks/useGetBookings.ts index afa8578f3dc371..70edd64c1e8dcc 100644 --- a/packages/platform/atoms/hooks/useGetBookings.ts +++ b/packages/platform/atoms/hooks/useGetBookings.ts @@ -3,13 +3,13 @@ import { useQuery } from "@tanstack/react-query"; import { SUCCESS_STATUS, V2_ENDPOINTS } from "@calcom/platform-constants"; import type { getAllUserBookings } from "@calcom/platform-libraries"; import type { ApiResponse, ApiSuccessResponse } from "@calcom/platform-types"; -import type { GetBookingsInput } from "@calcom/platform-types/bookings"; +import type { GetBookingsInput_2024_04_15 } from "@calcom/platform-types/bookings"; import http from "../lib/http"; export const QUERY_KEY = "user-bookings"; -export const useGetBookings = (input: GetBookingsInput) => { +export const useGetBookings = (input: GetBookingsInput_2024_04_15) => { const pathname = `/${V2_ENDPOINTS.bookings}`; const bookingsQuery = useQuery({ diff --git a/packages/platform/types/bookings/2024-04-15/index.ts b/packages/platform/types/bookings/2024-04-15/index.ts new file mode 100644 index 00000000000000..1ac83e88a736ab --- /dev/null +++ b/packages/platform/types/bookings/2024-04-15/index.ts @@ -0,0 +1 @@ +export * from "./inputs"; diff --git a/packages/platform/types/bookings.ts b/packages/platform/types/bookings/2024-04-15/inputs/index.ts similarity index 87% rename from packages/platform/types/bookings.ts rename to packages/platform/types/bookings/2024-04-15/inputs/index.ts index 600dee7425091c..970650e5fabad7 100644 --- a/packages/platform/types/bookings.ts +++ b/packages/platform/types/bookings/2024-04-15/inputs/index.ts @@ -12,7 +12,7 @@ import { IsString, } from "class-validator"; -export enum Status { +export enum Status_2024_04_15 { upcoming = "upcoming", recurring = "recurring", past = "past", @@ -20,7 +20,7 @@ export enum Status { unconfirmed = "unconfirmed", } -type BookingStatus = `${Status}`; +type BookingStatus = `${Status_2024_04_15}`; class Filters { @IsOptional() @@ -33,7 +33,7 @@ class Filters { @Type(() => Number) userIds?: number[]; - @IsEnum(Status) + @IsEnum(Status_2024_04_15) status!: BookingStatus; @IsOptional() @@ -42,7 +42,7 @@ class Filters { eventTypeIds?: number[]; } -export class GetBookingsInput { +export class GetBookingsInput_2024_04_15 { @ValidateNested({ each: true }) @Type(() => Filters) filters!: Filters; @@ -60,7 +60,7 @@ export class GetBookingsInput { cursor?: number | null; } -export class CancelBookingInput { +export class CancelBookingInput_2024_04_15 { @IsNumber() @IsOptional() @ApiProperty() diff --git a/packages/platform/types/bookings/index.ts b/packages/platform/types/bookings/index.ts new file mode 100644 index 00000000000000..1b7d4a1e45425f --- /dev/null +++ b/packages/platform/types/bookings/index.ts @@ -0,0 +1 @@ +export * from "./2024-04-15"; From 2c90331fb77abeeb0fc87b806aaa0b8811a7ef1e Mon Sep 17 00:00:00 2001 From: supalarry Date: Tue, 13 Aug 2024 13:13:14 +0200 Subject: [PATCH 02/96] feat: initialize bookings version 2024-08-13 --- .../controllers/bookings.controller.ts | 4 +- .../ee/bookings/2024-08-13/bookings.module.ts | 26 +++++ .../bookings.controller.e2e-spec.ts | 109 ++++++++++++++++++ .../controllers/bookings.controller.ts | 34 ++++++ .../outputs/create-booking.output.ts | 19 +++ .../2024-08-13/services/bookings.service.ts | 20 ++++ .../2024-08-13/services/input.service.ts | 10 ++ .../2024-08-13/services/output.service.ts | 10 ++ .../v2/src/ee/platform-endpoints-module.ts | 2 + apps/api/v2/src/lib/api-versions.ts | 3 + packages/platform/constants/api.ts | 8 +- .../types/bookings/2024-08-13/index.ts | 2 + .../2024-08-13/inputs/create-booking.input.ts | 6 + .../types/bookings/2024-08-13/inputs/index.ts | 1 + .../2024-08-13/outputs/booking.output.ts | 6 + .../bookings/2024-08-13/outputs/index.ts | 1 + packages/platform/types/bookings/index.ts | 1 + 17 files changed, 259 insertions(+), 3 deletions(-) create mode 100644 apps/api/v2/src/ee/bookings/2024-08-13/bookings.module.ts create mode 100644 apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.e2e-spec.ts create mode 100644 apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.ts create mode 100644 apps/api/v2/src/ee/bookings/2024-08-13/outputs/create-booking.output.ts create mode 100644 apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts create mode 100644 apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts create mode 100644 apps/api/v2/src/ee/bookings/2024-08-13/services/output.service.ts create mode 100644 packages/platform/types/bookings/2024-08-13/index.ts create mode 100644 packages/platform/types/bookings/2024-08-13/inputs/create-booking.input.ts create mode 100644 packages/platform/types/bookings/2024-08-13/inputs/index.ts create mode 100644 packages/platform/types/bookings/2024-08-13/outputs/booking.output.ts create mode 100644 packages/platform/types/bookings/2024-08-13/outputs/index.ts diff --git a/apps/api/v2/src/ee/bookings/2024-04-15/controllers/bookings.controller.ts b/apps/api/v2/src/ee/bookings/2024-04-15/controllers/bookings.controller.ts index 18f69b49073abc..95e0d35b8c2b2c 100644 --- a/apps/api/v2/src/ee/bookings/2024-04-15/controllers/bookings.controller.ts +++ b/apps/api/v2/src/ee/bookings/2024-04-15/controllers/bookings.controller.ts @@ -4,7 +4,7 @@ import { MarkNoShowInput_2024_04_15 } from "@/ee/bookings/2024-04-15/inputs/mark import { GetBookingOutput_2024_04_15 } from "@/ee/bookings/2024-04-15/outputs/get-booking.output"; import { GetBookingsOutput_2024_04_15 } from "@/ee/bookings/2024-04-15/outputs/get-bookings.output"; import { MarkNoShowOutput_2024_04_15 } from "@/ee/bookings/2024-04-15/outputs/mark-no-show.output"; -import { API_VERSIONS_VALUES } from "@/lib/api-versions"; +import { VERSION_2024_04_15_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"; import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard"; @@ -79,7 +79,7 @@ const DEFAULT_PLATFORM_PARAMS = { @Controller({ path: "/v2/bookings", - version: API_VERSIONS_VALUES, + version: VERSION_2024_04_15_VALUE, }) @UseGuards(PermissionsGuard) @DocsTags("Bookings") diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/bookings.module.ts b/apps/api/v2/src/ee/bookings/2024-08-13/bookings.module.ts new file mode 100644 index 00000000000000..05fffb8ba080f9 --- /dev/null +++ b/apps/api/v2/src/ee/bookings/2024-08-13/bookings.module.ts @@ -0,0 +1,26 @@ +import { BookingsController_2024_08_13 } from "@/ee/bookings/2024-08-13/controllers/bookings.controller"; +import { BookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/bookings.service"; +import { InputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/input.service"; +import { OutputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/output.service"; +import { BillingModule } from "@/modules/billing/billing.module"; +import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository"; +import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service"; +import { PrismaModule } from "@/modules/prisma/prisma.module"; +import { RedisModule } from "@/modules/redis/redis.module"; +import { TokensModule } from "@/modules/tokens/tokens.module"; +import { TokensRepository } from "@/modules/tokens/tokens.repository"; +import { Module } from "@nestjs/common"; + +@Module({ + imports: [PrismaModule, RedisModule, TokensModule, BillingModule], + providers: [ + TokensRepository, + OAuthFlowService, + OAuthClientRepository, + BookingsService_2024_08_13, + InputBookingsService_2024_08_13, + OutputBookingsService_2024_08_13, + ], + controllers: [BookingsController_2024_08_13], +}) +export class BookingsModule_2024_08_13 {} diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.e2e-spec.ts b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.e2e-spec.ts new file mode 100644 index 00000000000000..3f15cd1bd93914 --- /dev/null +++ b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.e2e-spec.ts @@ -0,0 +1,109 @@ +import { bootstrap } from "@/app"; +import { AppModule } from "@/app.module"; +import { CreateBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/create-booking.output"; +import { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input"; +import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module"; +import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service"; +import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard"; +import { PrismaModule } from "@/modules/prisma/prisma.module"; +import { UsersModule } from "@/modules/users/users.module"; +import { INestApplication } from "@nestjs/common"; +import { NestExpressApplication } from "@nestjs/platform-express"; +import { Test } from "@nestjs/testing"; +import { User } from "@prisma/client"; +import * as request from "supertest"; +import { BookingsRepositoryFixture } from "test/fixtures/repository/bookings.repository.fixture"; +import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture"; +import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture"; +import { withApiAuth } from "test/utils/withApiAuth"; + +import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants"; +import { CreateBookingInput_2024_08_13, BookingOutput_2024_08_13 } from "@calcom/platform-types"; + +describe("Bookings Endpoints 2024-04-15", () => { + describe("User Authenticated", () => { + let app: INestApplication; + + let userRepositoryFixture: UserRepositoryFixture; + let bookingsRepositoryFixture: BookingsRepositoryFixture; + let schedulesService: SchedulesService_2024_04_15; + let eventTypesRepositoryFixture: EventTypesRepositoryFixture; + + const userEmail = "bookings-controller-e2e@api.com"; + let user: User; + + let eventTypeId: number; + + let createdBooking: BookingOutput_2024_08_13; + + beforeAll(async () => { + const moduleRef = await withApiAuth( + userEmail, + Test.createTestingModule({ + imports: [AppModule, PrismaModule, UsersModule, SchedulesModule_2024_04_15], + }) + ) + .overrideGuard(PermissionsGuard) + .useValue({ + canActivate: () => true, + }) + .compile(); + + userRepositoryFixture = new UserRepositoryFixture(moduleRef); + bookingsRepositoryFixture = new BookingsRepositoryFixture(moduleRef); + eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef); + schedulesService = moduleRef.get(SchedulesService_2024_04_15); + + user = await userRepositoryFixture.create({ + email: userEmail, + }); + + const userSchedule: CreateScheduleInput_2024_04_15 = { + name: "working time", + timeZone: "Europe/Rome", + isDefault: true, + }; + await schedulesService.createUserSchedule(user.id, userSchedule); + const event = await eventTypesRepositoryFixture.create( + { title: "peer coding", slug: "peer-coding", length: 60 }, + user.id + ); + eventTypeId = event.id; + + app = moduleRef.createNestApplication(); + bootstrap(app as NestExpressApplication); + + await app.init(); + }); + + it("should be defined", () => { + expect(userRepositoryFixture).toBeDefined(); + expect(user).toBeDefined(); + }); + + it("should create a booking", async () => { + const body: CreateBookingInput_2024_08_13 = { + start: new Date().toISOString(), + }; + + return request(app.getHttpServer()) + .post("/v2/bookings") + .send(body) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .expect(201) + .then(async (response) => { + const responseBody: CreateBookingOutput_2024_08_13 = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + + createdBooking = responseBody.data; + }); + }); + + afterAll(async () => { + await userRepositoryFixture.deleteByEmail(user.email); + await bookingsRepositoryFixture.deleteAllBookings(user.id, user.email); + await app.close(); + }); + }); +}); diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.ts b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.ts new file mode 100644 index 00000000000000..51a22fd6ab0aca --- /dev/null +++ b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.ts @@ -0,0 +1,34 @@ +import { CreateBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/create-booking.output"; +import { BookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/bookings.service"; +import { VERSION_2024_08_13_VALUE } from "@/lib/api-versions"; +import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard"; +import { Controller, Post, Logger, Body, Headers, UseGuards } from "@nestjs/common"; +import { ApiTags as DocsTags } from "@nestjs/swagger"; + +import { X_CAL_CLIENT_ID } from "@calcom/platform-constants"; +import { CreateBookingInput_2024_08_13 } from "@calcom/platform-types"; + +@Controller({ + path: "/v2/bookings", + version: VERSION_2024_08_13_VALUE, +}) +@UseGuards(PermissionsGuard) +@DocsTags("Bookings") +export class BookingsController_2024_08_13 { + private readonly logger = new Logger("BookingsController"); + + constructor(private readonly bookingsService: BookingsService_2024_08_13) {} + + @Post("/") + async createBooking( + @Body() body: CreateBookingInput_2024_08_13, + @Headers(X_CAL_CLIENT_ID) clientId: string | undefined + ): Promise { + const booking = await this.bookingsService.createBooking(body, clientId); + + return { + status: "success", + data: booking, + }; + } +} diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/outputs/create-booking.output.ts b/apps/api/v2/src/ee/bookings/2024-08-13/outputs/create-booking.output.ts new file mode 100644 index 00000000000000..d4127de7801679 --- /dev/null +++ b/apps/api/v2/src/ee/bookings/2024-08-13/outputs/create-booking.output.ts @@ -0,0 +1,19 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { Type } from "class-transformer"; +import { IsEnum, ValidateNested } from "class-validator"; + +import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants"; +import { BookingOutput_2024_08_13 } from "@calcom/platform-types"; + +export class CreateBookingOutput_2024_08_13 { + @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) + @IsEnum([SUCCESS_STATUS, ERROR_STATUS]) + status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS; + + @ApiProperty({ + type: BookingOutput_2024_08_13, + }) + @ValidateNested() + @Type(() => BookingOutput_2024_08_13) + data!: BookingOutput_2024_08_13; +} diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts new file mode 100644 index 00000000000000..4e55314ad5dfcc --- /dev/null +++ b/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts @@ -0,0 +1,20 @@ +import { InputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/input.service"; +import { OutputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/output.service"; +import { Injectable } from "@nestjs/common"; + +import { CreateBookingInput_2024_08_13 } from "@calcom/platform-types"; + +@Injectable() +export class BookingsService_2024_08_13 { + constructor( + private readonly inputService: InputBookingsService_2024_08_13, + private readonly outputService: OutputBookingsService_2024_08_13 + ) {} + + async createBooking(body: CreateBookingInput_2024_08_13, clientId: string | undefined) { + const bodyTransformed = this.inputService.transformInputCreateBooking(body); + const booking: any = {}; + + return this.outputService.getOutputBooking(booking); + } +} diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts new file mode 100644 index 00000000000000..74e4d3ec387a74 --- /dev/null +++ b/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts @@ -0,0 +1,10 @@ +import { Injectable } from "@nestjs/common"; + +import { CreateBookingInput_2024_08_13 } from "@calcom/platform-types"; + +@Injectable() +export class InputBookingsService_2024_08_13 { + transformInputCreateBooking(inputBooking: CreateBookingInput_2024_08_13) { + return inputBooking; + } +} diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/output.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/output.service.ts new file mode 100644 index 00000000000000..fa98d62effe940 --- /dev/null +++ b/apps/api/v2/src/ee/bookings/2024-08-13/services/output.service.ts @@ -0,0 +1,10 @@ +import { Injectable } from "@nestjs/common"; + +import { Booking } from "@calcom/prisma/client"; + +@Injectable() +export class OutputBookingsService_2024_08_13 { + getOutputBooking(databaseBooking: Booking) { + return databaseBooking; + } +} diff --git a/apps/api/v2/src/ee/platform-endpoints-module.ts b/apps/api/v2/src/ee/platform-endpoints-module.ts index 6e99f63103b133..b02b611399c5cd 100644 --- a/apps/api/v2/src/ee/platform-endpoints-module.ts +++ b/apps/api/v2/src/ee/platform-endpoints-module.ts @@ -1,4 +1,5 @@ import { BookingsModule_2024_04_15 } from "@/ee/bookings/2024-04-15/bookings.module"; +import { BookingsModule_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.module"; import { CalendarsModule } from "@/ee/calendars/calendars.module"; import { EventTypesModule_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/event-types.module"; import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module"; @@ -22,6 +23,7 @@ import { Module } from "@nestjs/common"; EventTypesModule_2024_06_14, CalendarsModule, BookingsModule_2024_04_15, + BookingsModule_2024_08_13, SlotsModule, ], }) diff --git a/apps/api/v2/src/lib/api-versions.ts b/apps/api/v2/src/lib/api-versions.ts index 62a70a4b83a372..ab9fcdae091c31 100644 --- a/apps/api/v2/src/lib/api-versions.ts +++ b/apps/api/v2/src/lib/api-versions.ts @@ -5,13 +5,16 @@ import { VERSION_2024_04_15, VERSION_2024_06_11, VERSION_2024_06_14, + VERSION_2024_08_13, } from "@calcom/platform-constants"; export const API_VERSIONS_VALUES: VersionValue = API_VERSIONS as unknown as VersionValue; export const VERSION_2024_06_14_VALUE: VersionValue = VERSION_2024_06_14 as unknown as VersionValue; export const VERSION_2024_06_11_VALUE: VersionValue = VERSION_2024_06_11 as unknown as VersionValue; export const VERSION_2024_04_15_VALUE: VersionValue = VERSION_2024_04_15 as unknown as VersionValue; +export const VERSION_2024_08_13_VALUE: VersionValue = VERSION_2024_08_13 as unknown as VersionValue; export { VERSION_2024_04_15 }; export { VERSION_2024_06_11 }; export { VERSION_2024_06_14 }; +export { VERSION_2024_08_13 }; diff --git a/packages/platform/constants/api.ts b/packages/platform/constants/api.ts index 4a3e2b9c365aae..2ebbdebdd5f11e 100644 --- a/packages/platform/constants/api.ts +++ b/packages/platform/constants/api.ts @@ -54,8 +54,14 @@ export const HTTP_CODE_TOKEN_EXPIRED = 498; export const VERSION_2024_06_14 = "2024-06-14"; export const VERSION_2024_06_11 = "2024-06-11"; export const VERSION_2024_04_15 = "2024-04-15"; +export const VERSION_2024_08_13 = "2024-08-13"; -export const API_VERSIONS = [VERSION_2024_06_14, VERSION_2024_06_11, VERSION_2024_04_15] as const; +export const API_VERSIONS = [ + VERSION_2024_06_14, + VERSION_2024_06_11, + VERSION_2024_04_15, + VERSION_2024_08_13, +] as const; export type API_VERSIONS_ENUM = (typeof API_VERSIONS)[number]; export type API_VERSIONS_TYPE = typeof API_VERSIONS; diff --git a/packages/platform/types/bookings/2024-08-13/index.ts b/packages/platform/types/bookings/2024-08-13/index.ts new file mode 100644 index 00000000000000..775f90ba48c3fa --- /dev/null +++ b/packages/platform/types/bookings/2024-08-13/index.ts @@ -0,0 +1,2 @@ +export * from "./inputs"; +export * from "./outputs"; diff --git a/packages/platform/types/bookings/2024-08-13/inputs/create-booking.input.ts b/packages/platform/types/bookings/2024-08-13/inputs/create-booking.input.ts new file mode 100644 index 00000000000000..ecdf6331bdbb27 --- /dev/null +++ b/packages/platform/types/bookings/2024-08-13/inputs/create-booking.input.ts @@ -0,0 +1,6 @@ +import { IsDateString } from "class-validator"; + +export class CreateBookingInput_2024_08_13 { + @IsDateString() + start!: string; +} diff --git a/packages/platform/types/bookings/2024-08-13/inputs/index.ts b/packages/platform/types/bookings/2024-08-13/inputs/index.ts new file mode 100644 index 00000000000000..831470b0364317 --- /dev/null +++ b/packages/platform/types/bookings/2024-08-13/inputs/index.ts @@ -0,0 +1 @@ +export * from "./create-booking.input"; diff --git a/packages/platform/types/bookings/2024-08-13/outputs/booking.output.ts b/packages/platform/types/bookings/2024-08-13/outputs/booking.output.ts new file mode 100644 index 00000000000000..eb4ce4cfceb366 --- /dev/null +++ b/packages/platform/types/bookings/2024-08-13/outputs/booking.output.ts @@ -0,0 +1,6 @@ +import { IsInt } from "class-validator"; + +export class BookingOutput_2024_08_13 { + @IsInt() + id!: number; +} diff --git a/packages/platform/types/bookings/2024-08-13/outputs/index.ts b/packages/platform/types/bookings/2024-08-13/outputs/index.ts new file mode 100644 index 00000000000000..6b714c906187a3 --- /dev/null +++ b/packages/platform/types/bookings/2024-08-13/outputs/index.ts @@ -0,0 +1 @@ +export * from "./booking.output"; diff --git a/packages/platform/types/bookings/index.ts b/packages/platform/types/bookings/index.ts index 1b7d4a1e45425f..1feb2c7f74edc0 100644 --- a/packages/platform/types/bookings/index.ts +++ b/packages/platform/types/bookings/index.ts @@ -1 +1,2 @@ export * from "./2024-04-15"; +export * from "./2024-08-13"; From 90386fdaaeae72b6eccfc260ce8f5ed106a2b636 Mon Sep 17 00:00:00 2001 From: supalarry Date: Wed, 14 Aug 2024 09:36:49 +0200 Subject: [PATCH 03/96] feat: Create and reschedule booking inputs logic --- .../bookings.controller.e2e-spec.ts | 2 +- .../controllers/bookings.controller.ts | 10 +- .../2024-08-13/services/bookings.service.ts | 7 +- .../2024-08-13/services/input.service.ts | 66 ++++++++++++- apps/api/v2/swagger/documentation.json | 97 ++++++++++++++----- .../inputs/create-booking-input.pipe.ts | 58 +++++++++++ .../2024-08-13/inputs/create-booking.input.ts | 59 ++++++++++- .../types/bookings/2024-08-13/inputs/index.ts | 1 + .../bookings/2024-08-13/inputs/language.ts | 6 ++ packages/platform/types/package.json | 1 + 10 files changed, 276 insertions(+), 31 deletions(-) create mode 100644 packages/platform/types/bookings/2024-08-13/inputs/create-booking-input.pipe.ts create mode 100644 packages/platform/types/bookings/2024-08-13/inputs/language.ts diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.e2e-spec.ts b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.e2e-spec.ts index 3f15cd1bd93914..b8eee3d439e923 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.e2e-spec.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.e2e-spec.ts @@ -20,7 +20,7 @@ import { withApiAuth } from "test/utils/withApiAuth"; import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants"; import { CreateBookingInput_2024_08_13, BookingOutput_2024_08_13 } from "@calcom/platform-types"; -describe("Bookings Endpoints 2024-04-15", () => { +describe("Bookings Endpoints 2024-08-13", () => { describe("User Authenticated", () => { let app: INestApplication; diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.ts b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.ts index 51a22fd6ab0aca..4510e70963a998 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.ts @@ -6,7 +6,11 @@ import { Controller, Post, Logger, Body, Headers, UseGuards } from "@nestjs/comm import { ApiTags as DocsTags } from "@nestjs/swagger"; import { X_CAL_CLIENT_ID } from "@calcom/platform-constants"; -import { CreateBookingInput_2024_08_13 } from "@calcom/platform-types"; +import { + CreateBookingInput_2024_08_13, + CreateBookingInputPipe, + RescheduleBookingInput_2024_08_13, +} from "@calcom/platform-types"; @Controller({ path: "/v2/bookings", @@ -21,9 +25,11 @@ export class BookingsController_2024_08_13 { @Post("/") async createBooking( - @Body() body: CreateBookingInput_2024_08_13, + @Body(new CreateBookingInputPipe(CreateBookingInput_2024_08_13, RescheduleBookingInput_2024_08_13)) + body: CreateBookingInput_2024_08_13 | RescheduleBookingInput_2024_08_13, @Headers(X_CAL_CLIENT_ID) clientId: string | undefined ): Promise { + console.log("asap body", JSON.stringify(body)); const booking = await this.bookingsService.createBooking(body, clientId); return { diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts index 4e55314ad5dfcc..2a47eb1d2bda7e 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts @@ -2,7 +2,7 @@ import { InputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/servic import { OutputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/output.service"; import { Injectable } from "@nestjs/common"; -import { CreateBookingInput_2024_08_13 } from "@calcom/platform-types"; +import { CreateBookingInput_2024_08_13, RescheduleBookingInput_2024_08_13 } from "@calcom/platform-types"; @Injectable() export class BookingsService_2024_08_13 { @@ -11,7 +11,10 @@ export class BookingsService_2024_08_13 { private readonly outputService: OutputBookingsService_2024_08_13 ) {} - async createBooking(body: CreateBookingInput_2024_08_13, clientId: string | undefined) { + async createBooking( + body: CreateBookingInput_2024_08_13 | RescheduleBookingInput_2024_08_13, + clientId: string | undefined + ) { const bodyTransformed = this.inputService.transformInputCreateBooking(body); const booking: any = {}; diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts index 74e4d3ec387a74..b1440bf1cf5d83 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts @@ -1,10 +1,72 @@ +import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository"; +import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service"; import { Injectable } from "@nestjs/common"; +import { Logger } from "@nestjs/common"; +import { Request } from "express"; -import { CreateBookingInput_2024_08_13 } from "@calcom/platform-types"; +import { CreateBookingInput_2024_08_13, RescheduleBookingInput_2024_08_13 } from "@calcom/platform-types"; + +const DEFAULT_PLATFORM_PARAMS = { + platformClientId: "", + platformCancelUrl: "", + platformRescheduleUrl: "", + platformBookingUrl: "", + arePlatformEmailsEnabled: false, + platformBookingLocation: undefined, +}; @Injectable() export class InputBookingsService_2024_08_13 { - transformInputCreateBooking(inputBooking: CreateBookingInput_2024_08_13) { + private readonly logger = new Logger("InputBookingsService_2024_08_13"); + + constructor( + private readonly oAuthFlowService: OAuthFlowService, + private readonly oAuthClientRepository: OAuthClientRepository + ) {} + private async createBookingRequest(req: Request, oAuthClientId?: string, locationUrl?: string) { + const reqCopy = { ...req }; + const userId = (await this.createBookingRequestOwnerId(req)) ?? -1; + const oAuthParams = oAuthClientId + ? await this.createBookingRequestOAuthClientParams(oAuthClientId) + : DEFAULT_PLATFORM_PARAMS; + Object.assign(reqCopy, { userId, ...oAuthParams, platformBookingLocation: locationUrl }); + reqCopy.body = { ...reqCopy.body, noEmail: !oAuthParams.arePlatformEmailsEnabled }; + + return reqCopy; + } + + private async createBookingRequestOwnerId(req: Request): Promise { + try { + const accessToken = req.get("Authorization")?.replace("Bearer ", ""); + if (accessToken) { + return this.oAuthFlowService.getOwnerId(accessToken); + } + } catch (err) { + this.logger.error(err); + } + } + + private async createBookingRequestOAuthClientParams(clientId: string) { + const params = DEFAULT_PLATFORM_PARAMS; + try { + const client = await this.oAuthClientRepository.getOAuthClient(clientId); + if (client) { + params.platformClientId = clientId; + params.platformCancelUrl = client.bookingCancelRedirectUri ?? ""; + params.platformRescheduleUrl = client.bookingRescheduleRedirectUri ?? ""; + params.platformBookingUrl = client.bookingRedirectUri ?? ""; + params.arePlatformEmailsEnabled = client.areEmailsEnabled ?? false; + } + return params; + } catch (err) { + this.logger.error(err); + return params; + } + } + + transformInputCreateBooking( + inputBooking: CreateBookingInput_2024_08_13 | RescheduleBookingInput_2024_08_13 + ) { return inputBooking; } } diff --git a/apps/api/v2/swagger/documentation.json b/apps/api/v2/swagger/documentation.json index 4a6d569563f4a1..7aa053bd06346d 100644 --- a/apps/api/v2/swagger/documentation.json +++ b/apps/api/v2/swagger/documentation.json @@ -2870,7 +2870,16 @@ "name": "filters[status]", "required": true, "in": "query", - "schema": {} + "schema": { + "enum": [ + "upcoming", + "recurring", + "past", + "cancelled", + "unconfirmed" + ], + "type": "string" + } } ], "responses": { @@ -2890,34 +2899,15 @@ ] }, "post": { - "operationId": "BookingsController_2024_04_15_createBooking", - "parameters": [ - { - "name": "x-cal-client-id", - "required": true, - "in": "header", - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateBookingInput_2024_04_15" - } - } - } - }, + "operationId": "BookingsController_2024_08_13_createBooking", + "parameters": [], "responses": { "201": { "description": "", "content": { "application/json": { "schema": { - "type": "object" + "$ref": "#/components/schemas/CreateBookingOutput_2024_08_13" } } } @@ -3009,6 +2999,16 @@ } } ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CancelBookingInput_2024_04_15" + } + } + } + }, "responses": { "201": { "description": "", @@ -8882,6 +8882,33 @@ "responses" ] }, + "CancelBookingInput_2024_04_15": { + "type": "object", + "properties": { + "id": { + "type": "number" + }, + "uid": { + "type": "string" + }, + "allRemainingBookings": { + "type": "boolean" + }, + "cancellationReason": { + "type": "string" + }, + "seatReferenceUid": { + "type": "string" + } + }, + "required": [ + "id", + "uid", + "allRemainingBookings", + "cancellationReason", + "seatReferenceUid" + ] + }, "MarkNoShowInput_2024_04_15": { "type": "object", "properties": { @@ -8936,6 +8963,30 @@ "data" ] }, + "BookingOutput_2024_08_13": { + "type": "object", + "properties": {} + }, + "CreateBookingOutput_2024_08_13": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/BookingOutput_2024_08_13" + } + }, + "required": [ + "status", + "data" + ] + }, "ReserveSlotInput": { "type": "object", "properties": {} diff --git a/packages/platform/types/bookings/2024-08-13/inputs/create-booking-input.pipe.ts b/packages/platform/types/bookings/2024-08-13/inputs/create-booking-input.pipe.ts new file mode 100644 index 00000000000000..2fc36adf3bb6a3 --- /dev/null +++ b/packages/platform/types/bookings/2024-08-13/inputs/create-booking-input.pipe.ts @@ -0,0 +1,58 @@ +import type { PipeTransform } from "@nestjs/common"; +import { Injectable, BadRequestException } from "@nestjs/common"; +import type { ValidationError } from "class-validator"; +import { validateSync } from "class-validator"; + +import type { + CreateBookingInput_2024_08_13, + RescheduleBookingInput_2024_08_13, +} from "./create-booking.input"; + +@Injectable() +export class CreateBookingInputPipe implements PipeTransform { + constructor( + private readonly bookingDto: typeof CreateBookingInput_2024_08_13, + private readonly rescheduleDto: typeof RescheduleBookingInput_2024_08_13 + ) {} + + transform(value: any) { + const dtoClass = this.determineDtoClass(value); + + if (!dtoClass) { + throw new BadRequestException("Invalid request body"); + } + + const object = Object.assign(new dtoClass(), value); + const errors = validateSync(object); + + if (errors.length > 0) { + throw new BadRequestException(this.formatErrors(errors)); + } + + return object; + } + + private determineDtoClass( + value: any + ): typeof CreateBookingInput_2024_08_13 | typeof RescheduleBookingInput_2024_08_13 | null { + if (!value) { + return null; + } + + if (value.hasOwnProperty("rescheduleBookingUid")) { + return this.rescheduleDto; + } else { + return this.bookingDto; + } + } + + private formatErrors(errors: ValidationError[]): string { + return errors + .map((err) => { + return `${err.property} has wrong value ${err.value}, ${Object.values(err.constraints || {}).join( + ", " + )}`; + }) + .join(", "); + } +} diff --git a/packages/platform/types/bookings/2024-08-13/inputs/create-booking.input.ts b/packages/platform/types/bookings/2024-08-13/inputs/create-booking.input.ts index ecdf6331bdbb27..3f21ad1d4c3f88 100644 --- a/packages/platform/types/bookings/2024-08-13/inputs/create-booking.input.ts +++ b/packages/platform/types/bookings/2024-08-13/inputs/create-booking.input.ts @@ -1,6 +1,63 @@ -import { IsDateString } from "class-validator"; +import { Type } from "class-transformer"; +import { + IsInt, + IsDateString, + IsTimeZone, + IsEnum, + IsEmail, + ValidateNested, + IsArray, + IsString, + IsOptional, + IsUrl, + IsObject, +} from "class-validator"; +import type { BookingLanguageType } from "./language"; +import { BookingLanguage } from "./language"; + +class Attendee { + @IsEmail() + email!: string; + + @IsTimeZone() + // note(Lauris): setup CapitalizeTimezone + timeZone!: string; + + @IsEnum(BookingLanguage) + language!: BookingLanguageType; +} export class CreateBookingInput_2024_08_13 { @IsDateString() start!: string; + + @IsInt() + eventTypeId!: number; + + @ValidateNested() + @Type(() => Attendee) + @IsArray() + attendee!: Attendee; + + @IsArray() + @IsString({ each: true }) + @IsOptional() + guests?: string[]; + + @IsUrl() + meetingUrl!: string; + + @IsObject() + metadata!: Record; + + @IsObject() + bookingFieldsResponses!: Record; +} + +export class RescheduleBookingInput_2024_08_13 { + @IsDateString() + start!: string; + + @IsString() + rescheduleBookingUid!: string; } diff --git a/packages/platform/types/bookings/2024-08-13/inputs/index.ts b/packages/platform/types/bookings/2024-08-13/inputs/index.ts index 831470b0364317..6f24e2bf00b57b 100644 --- a/packages/platform/types/bookings/2024-08-13/inputs/index.ts +++ b/packages/platform/types/bookings/2024-08-13/inputs/index.ts @@ -1 +1,2 @@ export * from "./create-booking.input"; +export * from "./create-booking-input.pipe"; diff --git a/packages/platform/types/bookings/2024-08-13/inputs/language.ts b/packages/platform/types/bookings/2024-08-13/inputs/language.ts new file mode 100644 index 00000000000000..9a8bf5713e4e03 --- /dev/null +++ b/packages/platform/types/bookings/2024-08-13/inputs/language.ts @@ -0,0 +1,6 @@ +export enum BookingLanguage { + "en" = "en", + "lv" = "lv", +} + +export type BookingLanguageType = keyof typeof BookingLanguage; diff --git a/packages/platform/types/package.json b/packages/platform/types/package.json index 8a4ba5c01af6a0..cbbe5cb5570f8e 100644 --- a/packages/platform/types/package.json +++ b/packages/platform/types/package.json @@ -11,6 +11,7 @@ "dependencies": { "@calcom/platform-constants": "*", "@types/express": "^4.17.21", + "class-transformer": "^0.5.1", "class-validator": "^0.14.0" } } From d31440a307f34f089a0478ad4bc5817a7364e3fe Mon Sep 17 00:00:00 2001 From: supalarry Date: Wed, 14 Aug 2024 16:01:58 +0200 Subject: [PATCH 04/96] feat: create booking --- .../ee/bookings/2024-08-13/bookings.module.ts | 4 + .../2024-08-13/bookings.repository.ts | 24 ++++ .../controllers/bookings.controller.ts | 9 +- .../2024-08-13/services/bookings.service.ts | 24 +++- .../2024-08-13/services/input.service.ts | 124 ++++++++++++++++-- .../2024-08-13/services/output.service.ts | 31 ++++- .../event-types.repository.ts | 7 + .../organizations-teams.repository.ts | 8 ++ .../inputs/create-booking-input.pipe.ts | 68 ++++++---- .../2024-08-13/inputs/create-booking.input.ts | 12 +- .../2024-08-13/outputs/booking.output.ts | 55 +++++++- 11 files changed, 315 insertions(+), 51 deletions(-) create mode 100644 apps/api/v2/src/ee/bookings/2024-08-13/bookings.repository.ts diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/bookings.module.ts b/apps/api/v2/src/ee/bookings/2024-08-13/bookings.module.ts index 05fffb8ba080f9..1b5465646dccf8 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/bookings.module.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/bookings.module.ts @@ -1,7 +1,9 @@ +import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.repository"; import { BookingsController_2024_08_13 } from "@/ee/bookings/2024-08-13/controllers/bookings.controller"; import { BookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/bookings.service"; import { InputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/input.service"; import { OutputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/output.service"; +import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository"; import { BillingModule } from "@/modules/billing/billing.module"; import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository"; import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service"; @@ -20,6 +22,8 @@ import { Module } from "@nestjs/common"; BookingsService_2024_08_13, InputBookingsService_2024_08_13, OutputBookingsService_2024_08_13, + BookingsRepository_2024_08_13, + EventTypesRepository_2024_06_14, ], controllers: [BookingsController_2024_08_13], }) diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/bookings.repository.ts b/apps/api/v2/src/ee/bookings/2024-08-13/bookings.repository.ts new file mode 100644 index 00000000000000..b6b779fb2ae47f --- /dev/null +++ b/apps/api/v2/src/ee/bookings/2024-08-13/bookings.repository.ts @@ -0,0 +1,24 @@ +import { PrismaReadService } from "@/modules/prisma/prisma-read.service"; +import { PrismaWriteService } from "@/modules/prisma/prisma-write.service"; +import { Injectable } from "@nestjs/common"; + +@Injectable() +export class BookingsRepository_2024_08_13 { + constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {} + + async getById(id: number) { + return this.dbRead.prisma.booking.findUnique({ + where: { + id, + }, + }); + } + + async getByUid(bookingUid: string) { + return this.dbRead.prisma.booking.findUnique({ + where: { + uid: bookingUid, + }, + }); + } +} diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.ts b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.ts index 4510e70963a998..b08e4bc5c10a58 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.ts @@ -2,10 +2,10 @@ import { CreateBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs import { BookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/bookings.service"; import { VERSION_2024_08_13_VALUE } from "@/lib/api-versions"; import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard"; -import { Controller, Post, Logger, Body, Headers, UseGuards } from "@nestjs/common"; +import { Controller, Post, Logger, Body, UseGuards, Req } from "@nestjs/common"; import { ApiTags as DocsTags } from "@nestjs/swagger"; +import { Request } from "express"; -import { X_CAL_CLIENT_ID } from "@calcom/platform-constants"; import { CreateBookingInput_2024_08_13, CreateBookingInputPipe, @@ -27,10 +27,9 @@ export class BookingsController_2024_08_13 { async createBooking( @Body(new CreateBookingInputPipe(CreateBookingInput_2024_08_13, RescheduleBookingInput_2024_08_13)) body: CreateBookingInput_2024_08_13 | RescheduleBookingInput_2024_08_13, - @Headers(X_CAL_CLIENT_ID) clientId: string | undefined + @Req() request: Request ): Promise { - console.log("asap body", JSON.stringify(body)); - const booking = await this.bookingsService.createBooking(body, clientId); + const booking = await this.bookingsService.createBooking(request, body); return { status: "success", diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts index 2a47eb1d2bda7e..be5793a787ecae 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts @@ -1,23 +1,35 @@ +import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.repository"; import { InputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/input.service"; import { OutputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/output.service"; import { Injectable } from "@nestjs/common"; +import { Request } from "express"; +import { handleNewBooking } from "@calcom/platform-libraries"; import { CreateBookingInput_2024_08_13, RescheduleBookingInput_2024_08_13 } from "@calcom/platform-types"; @Injectable() export class BookingsService_2024_08_13 { constructor( private readonly inputService: InputBookingsService_2024_08_13, - private readonly outputService: OutputBookingsService_2024_08_13 + private readonly outputService: OutputBookingsService_2024_08_13, + private readonly bookingsRepository: BookingsRepository_2024_08_13 ) {} async createBooking( - body: CreateBookingInput_2024_08_13 | RescheduleBookingInput_2024_08_13, - clientId: string | undefined + request: Request, + body: CreateBookingInput_2024_08_13 | RescheduleBookingInput_2024_08_13 ) { - const bodyTransformed = this.inputService.transformInputCreateBooking(body); - const booking: any = {}; + const bookingRequest = await this.inputService.createBookingRequest(request, body); + const booking = await handleNewBooking(bookingRequest); + if (!booking.id) { + throw new Error("Booking was not created"); + } - return this.outputService.getOutputBooking(booking); + const databaseBooking = await this.bookingsRepository.getById(booking.id); + if (!databaseBooking) { + throw new Error(`Booking with id=${booking.id} was not found in the database`); + } + + return this.outputService.getOutputBooking(databaseBooking); } } diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts index b1440bf1cf5d83..8f4846cea64e15 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts @@ -1,11 +1,18 @@ +import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.repository"; +import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository"; import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository"; import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service"; -import { Injectable } from "@nestjs/common"; +import { Injectable, NotFoundException } from "@nestjs/common"; import { Logger } from "@nestjs/common"; import { Request } from "express"; +import { DateTime } from "luxon"; +import { NextApiRequest } from "next/types"; +import { X_CAL_CLIENT_ID } from "@calcom/platform-constants"; import { CreateBookingInput_2024_08_13, RescheduleBookingInput_2024_08_13 } from "@calcom/platform-types"; +type BookingRequest = NextApiRequest & { userId: number | undefined } & OAuthRequestParams; + const DEFAULT_PLATFORM_PARAMS = { platformClientId: "", platformCancelUrl: "", @@ -15,24 +22,44 @@ const DEFAULT_PLATFORM_PARAMS = { platformBookingLocation: undefined, }; +type OAuthRequestParams = { + platformClientId: string; + platformRescheduleUrl: string; + platformCancelUrl: string; + platformBookingUrl: string; + platformBookingLocation?: string; + arePlatformEmailsEnabled: boolean; +}; + @Injectable() export class InputBookingsService_2024_08_13 { private readonly logger = new Logger("InputBookingsService_2024_08_13"); constructor( private readonly oAuthFlowService: OAuthFlowService, - private readonly oAuthClientRepository: OAuthClientRepository + private readonly oAuthClientRepository: OAuthClientRepository, + private readonly eventTypesRepository: EventTypesRepository_2024_06_14, + private readonly bookingsRepository: BookingsRepository_2024_08_13 ) {} - private async createBookingRequest(req: Request, oAuthClientId?: string, locationUrl?: string) { - const reqCopy = { ...req }; - const userId = (await this.createBookingRequestOwnerId(req)) ?? -1; + + async createBookingRequest( + request: Request, + body: CreateBookingInput_2024_08_13 | RescheduleBookingInput_2024_08_13 + ): Promise { + // note(Lauris): update to this.transformInputCreate when rescheduling is implemented + const bodyTransformed = await this.transformInputCreateBooking(body as CreateBookingInput_2024_08_13); + const oAuthClientId = request.get(X_CAL_CLIENT_ID); + + const newRequest = { ...request }; + const userId = (await this.createBookingRequestOwnerId(request)) ?? undefined; const oAuthParams = oAuthClientId ? await this.createBookingRequestOAuthClientParams(oAuthClientId) : DEFAULT_PLATFORM_PARAMS; - Object.assign(reqCopy, { userId, ...oAuthParams, platformBookingLocation: locationUrl }); - reqCopy.body = { ...reqCopy.body, noEmail: !oAuthParams.arePlatformEmailsEnabled }; - return reqCopy; + Object.assign(newRequest, { userId, ...oAuthParams, platformBookingLocation: request.body.meetingUrl }); + newRequest.body = { ...bodyTransformed, noEmail: !oAuthParams.arePlatformEmailsEnabled }; + + return newRequest as unknown as BookingRequest; } private async createBookingRequestOwnerId(req: Request): Promise { @@ -64,9 +91,82 @@ export class InputBookingsService_2024_08_13 { } } - transformInputCreateBooking( - inputBooking: CreateBookingInput_2024_08_13 | RescheduleBookingInput_2024_08_13 - ) { - return inputBooking; + transformInputCreate(inputBooking: CreateBookingInput_2024_08_13 | RescheduleBookingInput_2024_08_13) { + const isReschedule = "rescheduleBookingUid" in inputBooking; + const isBooking = "rescheduleBookingUid" in inputBooking === false; + + if (!("rescheduleBookingUid" in inputBooking)) { + return this.transformInputCreateBooking(inputBooking); + } + } + + async transformInputCreateBooking(inputBooking: CreateBookingInput_2024_08_13) { + const eventType = await this.eventTypesRepository.getEventTypeByIdWithOwnerAndTeam( + inputBooking.eventTypeId + ); + if (!eventType) { + throw new NotFoundException(`Event type with id=${inputBooking.eventTypeId} not found`); + } + + const startTime = DateTime.fromISO(inputBooking.start, { zone: "utc" }).setZone( + inputBooking.attendee.timeZone + ); + const endTime = startTime.plus({ minutes: eventType.length }); + + return { + start: startTime.toISO(), + end: endTime.toISO(), + eventTypeId: inputBooking.eventTypeId, + eventTypeSlug: eventType.slug, + timeZone: inputBooking.attendee.timeZone, + language: inputBooking.attendee.language || "en", + metadata: inputBooking.metadata || {}, + hasHashedBookingLink: false, + guests: inputBooking.guests, + responses: inputBooking.bookingFieldsResponses + ? { + ...inputBooking.bookingFieldsResponses, + name: inputBooking.attendee.name, + email: inputBooking.attendee.email, + } + : { name: inputBooking.attendee.name, email: inputBooking.attendee.email }, + user: eventType.owner ? eventType.owner.username : eventType.team?.slug, + }; + } + + async transformInputRescheduleBooking(inputBooking: RescheduleBookingInput_2024_08_13) { + const booking = await this.bookingsRepository.getByUid(inputBooking.rescheduleBookingUid); + + if (!booking) { + throw new NotFoundException(`Booking with uid=${inputBooking.rescheduleBookingUid} not found`); + } + + if (!booking.eventTypeId) { + throw new NotFoundException( + `Booking with uid=${inputBooking.rescheduleBookingUid} is missing event type` + ); + } + + const eventType = await this.eventTypesRepository.getEventTypeByIdWithOwnerAndTeam(booking.eventTypeId); + if (!eventType) { + throw new NotFoundException(`Event type with id=${booking.eventTypeId} not found`); + } + + // note(Lauris): we need to store attendee.timeZone, language in metadata of booking, but need to wait for refactor. + return {}; + // return { + // start: inputBooking.start, + // end: this.getEndTime(inputBooking.start, eventType.length), + // eventTypeId: booking.eventTypeId, + // eventTypeSlug: eventType.slug, + // timeZone: booking.timeZone, + // language: inputBooking.attendee.language, + // metadata: inputBooking.metadata, + // hasHashedBookingLink: false, + // guests: inputBooking.guests, + // locationUrl: inputBooking.meetingUrl, + // responses: inputBooking.bookingFieldsResponses, + // user: eventType.owner ? eventType.owner.username : eventType.team?.slug + // } } } diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/output.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/output.service.ts index fa98d62effe940..8b13c6ed90c79b 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/services/output.service.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/services/output.service.ts @@ -1,10 +1,39 @@ import { Injectable } from "@nestjs/common"; +import { plainToClass } from "class-transformer"; +import { DateTime } from "luxon"; +import { z } from "zod"; +import { BookingOutput_2024_08_13 } from "@calcom/platform-types"; import { Booking } from "@calcom/prisma/client"; +export const bookingResponsesSchema = z.object({ + email: z.string(), + name: z.string(), + guests: z.array(z.string()).optional(), +}); + @Injectable() export class OutputBookingsService_2024_08_13 { getOutputBooking(databaseBooking: Booking) { - return databaseBooking; + const dateStart = DateTime.fromISO(databaseBooking.startTime.toISOString()); + const dateEnd = DateTime.fromISO(databaseBooking.endTime.toISOString()); + const duration = dateEnd.diff(dateStart, "minutes").minutes; + + const bookingResponses = bookingResponsesSchema.parse(databaseBooking.responses); + const booking = { + id: databaseBooking.id, + start: databaseBooking.startTime, + end: databaseBooking.endTime, + duration, + eventTypeId: databaseBooking.eventTypeId, + attendee: { + name: bookingResponses.name, + email: bookingResponses.email, + }, + guests: bookingResponses.guests, + meetingUrl: databaseBooking.location, + }; + + return plainToClass(BookingOutput_2024_08_13, booking, { strategy: "excludeAll" }); } } diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/event-types.repository.ts b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/event-types.repository.ts index a20b989060c0b3..80372726062659 100644 --- a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/event-types.repository.ts +++ b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/event-types.repository.ts @@ -85,6 +85,13 @@ export class EventTypesRepository_2024_06_14 { }); } + async getEventTypeByIdWithOwnerAndTeam(eventTypeId: number) { + return this.dbRead.prisma.eventType.findUnique({ + where: { id: eventTypeId }, + include: { owner: true, team: true }, + }); + } + async getUserEventTypeBySlug(userId: number, slug: string) { return this.dbRead.prisma.eventType.findUnique({ where: { diff --git a/apps/api/v2/src/modules/organizations/repositories/organizations-teams.repository.ts b/apps/api/v2/src/modules/organizations/repositories/organizations-teams.repository.ts index b81489aa6baaed..1d16e769fed62b 100644 --- a/apps/api/v2/src/modules/organizations/repositories/organizations-teams.repository.ts +++ b/apps/api/v2/src/modules/organizations/repositories/organizations-teams.repository.ts @@ -17,6 +17,14 @@ export class OrganizationsTeamsRepository { }); } + async findTeamById(teamId: number) { + return this.dbRead.prisma.team.findUnique({ + where: { + id: teamId, + }, + }); + } + async findOrgTeams(organizationId: number) { return this.dbRead.prisma.team.findMany({ where: { diff --git a/packages/platform/types/bookings/2024-08-13/inputs/create-booking-input.pipe.ts b/packages/platform/types/bookings/2024-08-13/inputs/create-booking-input.pipe.ts index 2fc36adf3bb6a3..b9987b247109aa 100644 --- a/packages/platform/types/bookings/2024-08-13/inputs/create-booking-input.pipe.ts +++ b/packages/platform/types/bookings/2024-08-13/inputs/create-booking-input.pipe.ts @@ -1,12 +1,11 @@ import type { PipeTransform } from "@nestjs/common"; import { Injectable, BadRequestException } from "@nestjs/common"; +import { plainToClass } from "class-transformer"; import type { ValidationError } from "class-validator"; import { validateSync } from "class-validator"; -import type { - CreateBookingInput_2024_08_13, - RescheduleBookingInput_2024_08_13, -} from "./create-booking.input"; +import { RescheduleBookingInput_2024_08_13 } from "./create-booking.input"; +import { CreateBookingInput_2024_08_13 } from "./create-booking.input"; @Injectable() export class CreateBookingInputPipe implements PipeTransform { @@ -15,15 +14,29 @@ export class CreateBookingInputPipe implements PipeTransform { private readonly rescheduleDto: typeof RescheduleBookingInput_2024_08_13 ) {} - transform(value: any) { - const dtoClass = this.determineDtoClass(value); + transform(value: CreateBookingInput_2024_08_13 | RescheduleBookingInput_2024_08_13) { + if (!value) { + throw new BadRequestException("Body is required"); + } + if (typeof value !== "object") { + throw new BadRequestException("Body should be an object"); + } - if (!dtoClass) { - throw new BadRequestException("Invalid request body"); + if (this.isRescheduleBookingInput(value)) { + return this.validateRescheduleBooking(value); } - const object = Object.assign(new dtoClass(), value); - const errors = validateSync(object); + return this.validateBooking(value); + } + + validateBooking(value: CreateBookingInput_2024_08_13) { + const object = plainToClass(CreateBookingInput_2024_08_13, value); + + const errors = validateSync(object, { + whitelist: true, + forbidNonWhitelisted: true, + skipMissingProperties: false, + }); if (errors.length > 0) { throw new BadRequestException(this.formatErrors(errors)); @@ -32,27 +45,36 @@ export class CreateBookingInputPipe implements PipeTransform { return object; } - private determineDtoClass( - value: any - ): typeof CreateBookingInput_2024_08_13 | typeof RescheduleBookingInput_2024_08_13 | null { - if (!value) { - return null; - } + validateRescheduleBooking(value: RescheduleBookingInput_2024_08_13) { + const object = plainToClass(RescheduleBookingInput_2024_08_13, value); - if (value.hasOwnProperty("rescheduleBookingUid")) { - return this.rescheduleDto; - } else { - return this.bookingDto; + const errors = validateSync(object, { + whitelist: true, + forbidNonWhitelisted: true, + skipMissingProperties: false, + }); + + if (errors.length > 0) { + throw new BadRequestException(this.formatErrors(errors)); } + + return object; } private formatErrors(errors: ValidationError[]): string { return errors .map((err) => { - return `${err.property} has wrong value ${err.value}, ${Object.values(err.constraints || {}).join( - ", " - )}`; + const constraints = err.constraints ? Object.values(err.constraints).join(", ") : ""; + const childrenErrors = + err.children && err.children.length > 0 ? `${this.formatErrors(err.children)}` : ""; + return `${err.property} property is wrong,${constraints} ${childrenErrors}`; }) .join(", "); } + + private isRescheduleBookingInput( + value: CreateBookingInput_2024_08_13 | RescheduleBookingInput_2024_08_13 + ): value is RescheduleBookingInput_2024_08_13 { + return value.hasOwnProperty("rescheduleBookingUid"); + } } diff --git a/packages/platform/types/bookings/2024-08-13/inputs/create-booking.input.ts b/packages/platform/types/bookings/2024-08-13/inputs/create-booking.input.ts index 3f21ad1d4c3f88..d6341559928507 100644 --- a/packages/platform/types/bookings/2024-08-13/inputs/create-booking.input.ts +++ b/packages/platform/types/bookings/2024-08-13/inputs/create-booking.input.ts @@ -17,6 +17,9 @@ import type { BookingLanguageType } from "./language"; import { BookingLanguage } from "./language"; class Attendee { + @IsString() + name!: string; + @IsEmail() email!: string; @@ -25,7 +28,8 @@ class Attendee { timeZone!: string; @IsEnum(BookingLanguage) - language!: BookingLanguageType; + @IsOptional() + language?: BookingLanguageType; } export class CreateBookingInput_2024_08_13 { @IsDateString() @@ -36,7 +40,6 @@ export class CreateBookingInput_2024_08_13 { @ValidateNested() @Type(() => Attendee) - @IsArray() attendee!: Attendee; @IsArray() @@ -45,12 +48,15 @@ export class CreateBookingInput_2024_08_13 { guests?: string[]; @IsUrl() - meetingUrl!: string; + @IsOptional() + meetingUrl?: string; @IsObject() + @IsOptional() metadata!: Record; @IsObject() + @IsOptional() bookingFieldsResponses!: Record; } diff --git a/packages/platform/types/bookings/2024-08-13/outputs/booking.output.ts b/packages/platform/types/bookings/2024-08-13/outputs/booking.output.ts index eb4ce4cfceb366..41b537f2d58904 100644 --- a/packages/platform/types/bookings/2024-08-13/outputs/booking.output.ts +++ b/packages/platform/types/bookings/2024-08-13/outputs/booking.output.ts @@ -1,6 +1,59 @@ -import { IsInt } from "class-validator"; +import { Expose, Type } from "class-transformer"; +import { + IsArray, + IsDateString, + IsEmail, + IsInt, + IsOptional, + IsString, + IsUrl, + ValidateNested, +} from "class-validator"; + +class Attendee { + @IsString() + @Expose() + name!: string; + + @IsEmail() + @Expose() + email!: string; +} export class BookingOutput_2024_08_13 { @IsInt() + @Expose() id!: number; + + @IsDateString() + @Expose() + start!: string; + + @IsDateString() + @Expose() + end!: string; + + @IsInt() + @Expose() + duration!: number; + + @IsInt() + @Expose() + eventTypeId!: number; + + @ValidateNested() + @Type(() => Attendee) + @Expose() + attendee!: Attendee; + + @IsArray() + @IsString({ each: true }) + @IsOptional() + @Expose() + guests?: string[]; + + @IsUrl() + @IsOptional() + @Expose() + meetingUrl?: string; } From b2259b9b99b89aacdc7faa57778691d3bc81c713 Mon Sep 17 00:00:00 2001 From: supalarry Date: Wed, 14 Aug 2024 18:11:14 +0200 Subject: [PATCH 05/96] refactor: create booking response --- .../bookings/2024-08-13/bookings.repository.ts | 11 +++++++++++ .../2024-08-13/services/bookings.service.ts | 2 +- .../2024-08-13/services/output.service.ts | 18 +++++++++++++++--- .../2024-08-13/outputs/booking.output.ts | 15 +++++++++++++++ 4 files changed, 42 insertions(+), 4 deletions(-) diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/bookings.repository.ts b/apps/api/v2/src/ee/bookings/2024-08-13/bookings.repository.ts index b6b779fb2ae47f..5644a84853d5ea 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/bookings.repository.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/bookings.repository.ts @@ -21,4 +21,15 @@ export class BookingsRepository_2024_08_13 { }, }); } + + async getByIdWithAttendees(id: number) { + return this.dbRead.prisma.booking.findUnique({ + where: { + id, + }, + include: { + attendees: true, + }, + }); + } } diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts index be5793a787ecae..8697d3c7303316 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts @@ -25,7 +25,7 @@ export class BookingsService_2024_08_13 { throw new Error("Booking was not created"); } - const databaseBooking = await this.bookingsRepository.getById(booking.id); + const databaseBooking = await this.bookingsRepository.getByIdWithAttendees(booking.id); if (!databaseBooking) { throw new Error(`Booking with id=${booking.id} was not found in the database`); } diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/output.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/output.service.ts index 8b13c6ed90c79b..f8cb99bfb0c128 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/services/output.service.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/services/output.service.ts @@ -14,12 +14,22 @@ export const bookingResponsesSchema = z.object({ @Injectable() export class OutputBookingsService_2024_08_13 { - getOutputBooking(databaseBooking: Booking) { + getOutputBooking( + databaseBooking: Booking & { + attendees: { name: string; email: string; timeZone: string; locale: string | null }[]; + } + ) { const dateStart = DateTime.fromISO(databaseBooking.startTime.toISOString()); const dateEnd = DateTime.fromISO(databaseBooking.endTime.toISOString()); const duration = dateEnd.diff(dateStart, "minutes").minutes; const bookingResponses = bookingResponsesSchema.parse(databaseBooking.responses); + const attendee = databaseBooking.attendees.find((attendee) => attendee.email === bookingResponses.email); + + if (!attendee) { + throw new Error("Attendee not found"); + } + const booking = { id: databaseBooking.id, start: databaseBooking.startTime, @@ -27,8 +37,10 @@ export class OutputBookingsService_2024_08_13 { duration, eventTypeId: databaseBooking.eventTypeId, attendee: { - name: bookingResponses.name, - email: bookingResponses.email, + name: attendee.name, + email: attendee.email, + timeZone: attendee.timeZone, + language: attendee.locale, }, guests: bookingResponses.guests, meetingUrl: databaseBooking.location, diff --git a/packages/platform/types/bookings/2024-08-13/outputs/booking.output.ts b/packages/platform/types/bookings/2024-08-13/outputs/booking.output.ts index 41b537f2d58904..ed8ec974e2d725 100644 --- a/packages/platform/types/bookings/2024-08-13/outputs/booking.output.ts +++ b/packages/platform/types/bookings/2024-08-13/outputs/booking.output.ts @@ -3,13 +3,18 @@ import { IsArray, IsDateString, IsEmail, + IsEnum, IsInt, IsOptional, IsString, + IsTimeZone, IsUrl, ValidateNested, } from "class-validator"; +import type { BookingLanguageType } from "../inputs/language"; +import { BookingLanguage } from "../inputs/language"; + class Attendee { @IsString() @Expose() @@ -18,6 +23,16 @@ class Attendee { @IsEmail() @Expose() email!: string; + + @IsTimeZone() + @Expose() + // note(Lauris): setup CapitalizeTimezone + timeZone!: string; + + @IsEnum(BookingLanguage) + @Expose() + @IsOptional() + language?: BookingLanguageType; } export class BookingOutput_2024_08_13 { From 82cc884456c06d71c8e189d06ddac217b8529fc6 Mon Sep 17 00:00:00 2001 From: supalarry Date: Thu, 15 Aug 2024 13:08:37 +0200 Subject: [PATCH 06/96] feat: reschedule booking --- .../2024-08-13/bookings.repository.ts | 11 +++ .../2024-08-13/services/input.service.ts | 76 ++++++++++++------- .../2024-08-13/services/output.service.ts | 1 + .../2024-08-13/outputs/booking.output.ts | 4 + 4 files changed, 65 insertions(+), 27 deletions(-) diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/bookings.repository.ts b/apps/api/v2/src/ee/bookings/2024-08-13/bookings.repository.ts index 5644a84853d5ea..5bbd01b5b37125 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/bookings.repository.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/bookings.repository.ts @@ -32,4 +32,15 @@ export class BookingsRepository_2024_08_13 { }, }); } + + async getByUidWithAttendees(uid: string) { + return this.dbRead.prisma.booking.findUnique({ + where: { + uid, + }, + include: { + attendees: true, + }, + }); + } } diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts index 8f4846cea64e15..a264f9c7616528 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts @@ -1,4 +1,5 @@ import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.repository"; +import { bookingResponsesSchema } from "@/ee/bookings/2024-08-13/services/output.service"; import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository"; import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository"; import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service"; @@ -47,7 +48,7 @@ export class InputBookingsService_2024_08_13 { body: CreateBookingInput_2024_08_13 | RescheduleBookingInput_2024_08_13 ): Promise { // note(Lauris): update to this.transformInputCreate when rescheduling is implemented - const bodyTransformed = await this.transformInputCreateBooking(body as CreateBookingInput_2024_08_13); + const bodyTransformed = await this.transformInputCreate(body); const oAuthClientId = request.get(X_CAL_CLIENT_ID); const newRequest = { ...request }; @@ -56,7 +57,8 @@ export class InputBookingsService_2024_08_13 { ? await this.createBookingRequestOAuthClientParams(oAuthClientId) : DEFAULT_PLATFORM_PARAMS; - Object.assign(newRequest, { userId, ...oAuthParams, platformBookingLocation: request.body.meetingUrl }); + const location = await this.getLocation(request, body); + Object.assign(newRequest, { userId, ...oAuthParams, platformBookingLocation: location }); newRequest.body = { ...bodyTransformed, noEmail: !oAuthParams.arePlatformEmailsEnabled }; return newRequest as unknown as BookingRequest; @@ -92,12 +94,11 @@ export class InputBookingsService_2024_08_13 { } transformInputCreate(inputBooking: CreateBookingInput_2024_08_13 | RescheduleBookingInput_2024_08_13) { - const isReschedule = "rescheduleBookingUid" in inputBooking; - const isBooking = "rescheduleBookingUid" in inputBooking === false; - - if (!("rescheduleBookingUid" in inputBooking)) { - return this.transformInputCreateBooking(inputBooking); + if ("rescheduleBookingUid" in inputBooking) { + return this.transformInputRescheduleBooking(inputBooking); } + + return this.transformInputCreateBooking(inputBooking); } async transformInputCreateBooking(inputBooking: CreateBookingInput_2024_08_13) { @@ -134,39 +135,60 @@ export class InputBookingsService_2024_08_13 { }; } - async transformInputRescheduleBooking(inputBooking: RescheduleBookingInput_2024_08_13) { - const booking = await this.bookingsRepository.getByUid(inputBooking.rescheduleBookingUid); + async getLocation( + request: Request, + body: CreateBookingInput_2024_08_13 | RescheduleBookingInput_2024_08_13 + ) { + if ("rescheduleBookingUid" in body) { + const booking = await this.bookingsRepository.getByUid(body.rescheduleBookingUid); + if (!booking) { + throw new NotFoundException(`Booking with uid=${body.rescheduleBookingUid} not found`); + } + return booking.location; + } + return request.body.meetingUrl; + } + + async transformInputRescheduleBooking(inputBooking: RescheduleBookingInput_2024_08_13) { + const booking = await this.bookingsRepository.getByUidWithAttendees(inputBooking.rescheduleBookingUid); if (!booking) { throw new NotFoundException(`Booking with uid=${inputBooking.rescheduleBookingUid} not found`); } - if (!booking.eventTypeId) { throw new NotFoundException( `Booking with uid=${inputBooking.rescheduleBookingUid} is missing event type` ); } - const eventType = await this.eventTypesRepository.getEventTypeByIdWithOwnerAndTeam(booking.eventTypeId); if (!eventType) { throw new NotFoundException(`Event type with id=${booking.eventTypeId} not found`); } - // note(Lauris): we need to store attendee.timeZone, language in metadata of booking, but need to wait for refactor. - return {}; - // return { - // start: inputBooking.start, - // end: this.getEndTime(inputBooking.start, eventType.length), - // eventTypeId: booking.eventTypeId, - // eventTypeSlug: eventType.slug, - // timeZone: booking.timeZone, - // language: inputBooking.attendee.language, - // metadata: inputBooking.metadata, - // hasHashedBookingLink: false, - // guests: inputBooking.guests, - // locationUrl: inputBooking.meetingUrl, - // responses: inputBooking.bookingFieldsResponses, - // user: eventType.owner ? eventType.owner.username : eventType.team?.slug - // } + const bookingResponses = bookingResponsesSchema.parse(booking.responses); + const attendee = booking.attendees.find((attendee) => attendee.email === bookingResponses.email); + + if (!attendee) { + throw new NotFoundException( + `Attendee with e-mail ${bookingResponses.email} for booking with uid=${inputBooking.rescheduleBookingUid} not found` + ); + } + + const startTime = DateTime.fromISO(inputBooking.start, { zone: "utc" }).setZone(attendee.timeZone); + const endTime = startTime.plus({ minutes: eventType.length }); + + return { + start: startTime.toISO(), + end: endTime.toISO(), + eventTypeId: eventType.id, + eventTypeSlug: eventType.slug, + timeZone: attendee.timeZone, + language: attendee.locale, + metadata: booking.metadata || {}, + hasHashedBookingLink: false, + guests: bookingResponses.guests, + responses: bookingResponses, + user: eventType.owner ? eventType.owner.username : eventType.team?.slug, + }; } } diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/output.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/output.service.ts index f8cb99bfb0c128..98ae3bc461d054 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/services/output.service.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/services/output.service.ts @@ -32,6 +32,7 @@ export class OutputBookingsService_2024_08_13 { const booking = { id: databaseBooking.id, + uid: databaseBooking.uid, start: databaseBooking.startTime, end: databaseBooking.endTime, duration, diff --git a/packages/platform/types/bookings/2024-08-13/outputs/booking.output.ts b/packages/platform/types/bookings/2024-08-13/outputs/booking.output.ts index ed8ec974e2d725..8f2eaf59657322 100644 --- a/packages/platform/types/bookings/2024-08-13/outputs/booking.output.ts +++ b/packages/platform/types/bookings/2024-08-13/outputs/booking.output.ts @@ -40,6 +40,10 @@ export class BookingOutput_2024_08_13 { @Expose() id!: number; + @IsString() + @Expose() + uid!: string; + @IsDateString() @Expose() start!: string; From e702c9e12c08e9938c8a8c80811463e7267ea85d Mon Sep 17 00:00:00 2001 From: supalarry Date: Thu, 15 Aug 2024 13:18:17 +0200 Subject: [PATCH 07/96] chore: update language input --- .../bookings/2024-08-13/inputs/language.ts | 42 ++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/packages/platform/types/bookings/2024-08-13/inputs/language.ts b/packages/platform/types/bookings/2024-08-13/inputs/language.ts index 9a8bf5713e4e03..c67edfb6b32ea7 100644 --- a/packages/platform/types/bookings/2024-08-13/inputs/language.ts +++ b/packages/platform/types/bookings/2024-08-13/inputs/language.ts @@ -1,6 +1,46 @@ export enum BookingLanguage { - "en" = "en", + "ar" = "ar", + "ca" = "ca", + "de" = "de", + "es" = "es", + "eu" = "eu", + "he" = "he", + "id" = "id", + "ja" = "ja", "lv" = "lv", + "pl" = "pl", + "ro" = "ro", + "sr" = "sr", + "th" = "th", + "vi" = "vi", + "az" = "az", + "cs" = "cs", + "el" = "el", + "es-419" = "es-419", + "fi" = "fi", + "hr" = "hr", + "it" = "it", + "km" = "km", + "nl" = "nl", + "pt" = "pt", + "ru" = "ru", + "sv" = "sv", + "tr" = "tr", + "zh-CN" = "zh-CN", + "bg" = "bg", + "da" = "da", + "en" = "en", + "et" = "et", + "fr" = "fr", + "hu" = "hu", + "iw" = "iw", + "ko" = "ko", + "no" = "no", + "pt-BR" = "pt-BR", + "sk" = "sk", + "ta" = "ta", + "uk" = "uk", + "zh-TW" = "zh-TW", } export type BookingLanguageType = keyof typeof BookingLanguage; From 2560939bf50f6fc91d35ded524f25ee99922314c Mon Sep 17 00:00:00 2001 From: supalarry Date: Thu, 15 Aug 2024 15:45:25 +0200 Subject: [PATCH 08/96] feat: recurring booking --- .../controllers/bookings.controller.ts | 8 +- .../outputs/create-booking.output.ts | 2 +- .../2024-08-13/services/bookings.service.ts | 41 +++++- .../2024-08-13/services/input.service.ts | 126 +++++++++++++++++- .../inputs/create-booking-input.pipe.ts | 47 ++++++- .../2024-08-13/inputs/create-booking.input.ts | 29 ++++ 6 files changed, 235 insertions(+), 18 deletions(-) diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.ts b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.ts index b08e4bc5c10a58..70850e406979de 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.ts @@ -9,6 +9,7 @@ import { Request } from "express"; import { CreateBookingInput_2024_08_13, CreateBookingInputPipe, + CreateRecurringBookingInput_2024_08_13, RescheduleBookingInput_2024_08_13, } from "@calcom/platform-types"; @@ -25,8 +26,11 @@ export class BookingsController_2024_08_13 { @Post("/") async createBooking( - @Body(new CreateBookingInputPipe(CreateBookingInput_2024_08_13, RescheduleBookingInput_2024_08_13)) - body: CreateBookingInput_2024_08_13 | RescheduleBookingInput_2024_08_13, + @Body(new CreateBookingInputPipe()) + body: + | CreateBookingInput_2024_08_13 + | RescheduleBookingInput_2024_08_13 + | CreateRecurringBookingInput_2024_08_13, @Req() request: Request ): Promise { const booking = await this.bookingsService.createBooking(request, body); diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/outputs/create-booking.output.ts b/apps/api/v2/src/ee/bookings/2024-08-13/outputs/create-booking.output.ts index d4127de7801679..b1ed8f68c2fd3e 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/outputs/create-booking.output.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/outputs/create-booking.output.ts @@ -15,5 +15,5 @@ export class CreateBookingOutput_2024_08_13 { }) @ValidateNested() @Type(() => BookingOutput_2024_08_13) - data!: BookingOutput_2024_08_13; + data!: BookingOutput_2024_08_13 | BookingOutput_2024_08_13[]; } diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts index 8697d3c7303316..d04ab22373d59b 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts @@ -4,8 +4,12 @@ import { OutputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/servi import { Injectable } from "@nestjs/common"; import { Request } from "express"; -import { handleNewBooking } from "@calcom/platform-libraries"; -import { CreateBookingInput_2024_08_13, RescheduleBookingInput_2024_08_13 } from "@calcom/platform-types"; +import { handleNewBooking, handleNewRecurringBooking } from "@calcom/platform-libraries"; +import { + CreateBookingInput_2024_08_13, + RescheduleBookingInput_2024_08_13, + CreateRecurringBookingInput_2024_08_13, +} from "@calcom/platform-types"; @Injectable() export class BookingsService_2024_08_13 { @@ -17,8 +21,17 @@ export class BookingsService_2024_08_13 { async createBooking( request: Request, - body: CreateBookingInput_2024_08_13 | RescheduleBookingInput_2024_08_13 + body: + | CreateBookingInput_2024_08_13 + | RescheduleBookingInput_2024_08_13 + | CreateRecurringBookingInput_2024_08_13 ) { + const isRecurring = "recurringEventTypeId" in body; + + if (isRecurring) { + return this.createRecurringBooking(request, body); + } + const bookingRequest = await this.inputService.createBookingRequest(request, body); const booking = await handleNewBooking(bookingRequest); if (!booking.id) { @@ -32,4 +45,26 @@ export class BookingsService_2024_08_13 { return this.outputService.getOutputBooking(databaseBooking); } + + async createRecurringBooking(request: Request, body: CreateRecurringBookingInput_2024_08_13) { + const bookingRequest = await this.inputService.createBookingRequest(request, body); + const bookings = await handleNewRecurringBooking(bookingRequest); + + const transformed = []; + + for (const booking of bookings) { + if (!booking.id) { + throw new Error("Booking was not created"); + } + + const databaseBooking = await this.bookingsRepository.getByIdWithAttendees(booking.id); + if (!databaseBooking) { + throw new Error(`Booking with id=${booking.id} was not found in the database`); + } + + transformed.push(this.outputService.getOutputBooking(databaseBooking)); + } + + return transformed; + } } diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts index a264f9c7616528..94907de3a6d63b 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts @@ -8,9 +8,14 @@ import { Logger } from "@nestjs/common"; import { Request } from "express"; import { DateTime } from "luxon"; import { NextApiRequest } from "next/types"; +import { z } from "zod"; import { X_CAL_CLIENT_ID } from "@calcom/platform-constants"; -import { CreateBookingInput_2024_08_13, RescheduleBookingInput_2024_08_13 } from "@calcom/platform-types"; +import { + CreateBookingInput_2024_08_13, + CreateRecurringBookingInput_2024_08_13, + RescheduleBookingInput_2024_08_13, +} from "@calcom/platform-types"; type BookingRequest = NextApiRequest & { userId: number | undefined } & OAuthRequestParams; @@ -32,6 +37,24 @@ type OAuthRequestParams = { arePlatformEmailsEnabled: boolean; }; +export enum Frequency { + "YEARLY", + "MONTHLY", + "WEEKLY", + "DAILY", + "HOURLY", + "MINUTELY", + "SECONDLY", +} + +const recurringEventSchema = z.object({ + dtstart: z.string().optional(), + interval: z.number().int().optional(), + count: z.number().int().optional(), + freq: z.nativeEnum(Frequency).optional(), + until: z.string().optional(), +}); + @Injectable() export class InputBookingsService_2024_08_13 { private readonly logger = new Logger("InputBookingsService_2024_08_13"); @@ -45,7 +68,10 @@ export class InputBookingsService_2024_08_13 { async createBookingRequest( request: Request, - body: CreateBookingInput_2024_08_13 | RescheduleBookingInput_2024_08_13 + body: + | CreateBookingInput_2024_08_13 + | RescheduleBookingInput_2024_08_13 + | CreateRecurringBookingInput_2024_08_13 ): Promise { // note(Lauris): update to this.transformInputCreate when rescheduling is implemented const bodyTransformed = await this.transformInputCreate(body); @@ -59,7 +85,14 @@ export class InputBookingsService_2024_08_13 { const location = await this.getLocation(request, body); Object.assign(newRequest, { userId, ...oAuthParams, platformBookingLocation: location }); - newRequest.body = { ...bodyTransformed, noEmail: !oAuthParams.arePlatformEmailsEnabled }; + + const notRecurring = !("recurringEventTypeId" in body); + newRequest.body = notRecurring + ? { ...bodyTransformed, noEmail: !oAuthParams.arePlatformEmailsEnabled } + : (bodyTransformed as any[]).map((event) => ({ + ...event, + noEmail: !oAuthParams.arePlatformEmailsEnabled, + })); return newRequest as unknown as BookingRequest; } @@ -93,14 +126,94 @@ export class InputBookingsService_2024_08_13 { } } - transformInputCreate(inputBooking: CreateBookingInput_2024_08_13 | RescheduleBookingInput_2024_08_13) { + transformInputCreate( + inputBooking: + | CreateBookingInput_2024_08_13 + | RescheduleBookingInput_2024_08_13 + | CreateRecurringBookingInput_2024_08_13 + ) { if ("rescheduleBookingUid" in inputBooking) { return this.transformInputRescheduleBooking(inputBooking); } + if ("recurringEventTypeId" in inputBooking) { + return this.transformInputCreateRecurringBooking(inputBooking); + } + return this.transformInputCreateBooking(inputBooking); } + async transformInputCreateRecurringBooking(inputBooking: CreateRecurringBookingInput_2024_08_13) { + const eventType = await this.eventTypesRepository.getEventTypeByIdWithOwnerAndTeam( + inputBooking.recurringEventTypeId + ); + if (!eventType) { + throw new NotFoundException(`Event type with id=${inputBooking.recurringEventTypeId} not found`); + } + if (!eventType.recurringEvent) { + throw new NotFoundException( + `Event type with id=${inputBooking.recurringEventTypeId} is not a recurring event` + ); + } + + const occurrance = recurringEventSchema.parse(eventType.recurringEvent); + const repeatsEvery = occurrance.interval; + const repeatsTimes = occurrance.count; + // note(Lauris): timeBetween 0=yearly, 1=monthly and 2=weekly + const timeBetween = occurrance.freq; + + if (!repeatsTimes) { + throw new Error("Repeats times is required"); + } + + const events = []; + + let startTime = DateTime.fromISO(inputBooking.start, { zone: "utc" }).setZone( + inputBooking.attendee.timeZone + ); + + for (let i = 0; i < repeatsTimes; i++) { + const endTime = startTime.plus({ minutes: eventType.length }); + + events.push({ + start: startTime.toISO(), + end: endTime.toISO(), + eventTypeId: inputBooking.recurringEventTypeId, + eventTypeSlug: eventType.slug, + timeZone: inputBooking.attendee.timeZone, + language: inputBooking.attendee.language || "en", + metadata: inputBooking.metadata || {}, + hasHashedBookingLink: false, + guests: inputBooking.guests, + responses: inputBooking.bookingFieldsResponses + ? { + ...inputBooking.bookingFieldsResponses, + name: inputBooking.attendee.name, + email: inputBooking.attendee.email, + } + : { name: inputBooking.attendee.name, email: inputBooking.attendee.email }, + user: eventType.owner ? eventType.owner.username : eventType.team?.slug, + schedulingType: eventType.schedulingType, + }); + + switch (timeBetween) { + case 0: // Yearly + startTime = startTime.plus({ years: repeatsEvery }); + break; + case 1: // Monthly + startTime = startTime.plus({ months: repeatsEvery }); + break; + case 2: // Weekly + startTime = startTime.plus({ weeks: repeatsEvery }); + break; + default: + throw new Error("Unsupported timeBetween value"); + } + } + + return events; + } + async transformInputCreateBooking(inputBooking: CreateBookingInput_2024_08_13) { const eventType = await this.eventTypesRepository.getEventTypeByIdWithOwnerAndTeam( inputBooking.eventTypeId @@ -137,7 +250,10 @@ export class InputBookingsService_2024_08_13 { async getLocation( request: Request, - body: CreateBookingInput_2024_08_13 | RescheduleBookingInput_2024_08_13 + body: + | CreateBookingInput_2024_08_13 + | RescheduleBookingInput_2024_08_13 + | CreateRecurringBookingInput_2024_08_13 ) { if ("rescheduleBookingUid" in body) { const booking = await this.bookingsRepository.getByUid(body.rescheduleBookingUid); diff --git a/packages/platform/types/bookings/2024-08-13/inputs/create-booking-input.pipe.ts b/packages/platform/types/bookings/2024-08-13/inputs/create-booking-input.pipe.ts index b9987b247109aa..fca479db411611 100644 --- a/packages/platform/types/bookings/2024-08-13/inputs/create-booking-input.pipe.ts +++ b/packages/platform/types/bookings/2024-08-13/inputs/create-booking-input.pipe.ts @@ -4,17 +4,18 @@ import { plainToClass } from "class-transformer"; import type { ValidationError } from "class-validator"; import { validateSync } from "class-validator"; +import { CreateRecurringBookingInput_2024_08_13 } from "./create-booking.input"; import { RescheduleBookingInput_2024_08_13 } from "./create-booking.input"; import { CreateBookingInput_2024_08_13 } from "./create-booking.input"; @Injectable() export class CreateBookingInputPipe implements PipeTransform { - constructor( - private readonly bookingDto: typeof CreateBookingInput_2024_08_13, - private readonly rescheduleDto: typeof RescheduleBookingInput_2024_08_13 - ) {} - - transform(value: CreateBookingInput_2024_08_13 | RescheduleBookingInput_2024_08_13) { + transform( + value: + | CreateBookingInput_2024_08_13 + | RescheduleBookingInput_2024_08_13 + | CreateRecurringBookingInput_2024_08_13 + ) { if (!value) { throw new BadRequestException("Body is required"); } @@ -26,6 +27,10 @@ export class CreateBookingInputPipe implements PipeTransform { return this.validateRescheduleBooking(value); } + if (this.isRecurringBookingInput(value)) { + return this.validateRecurringBooking(value); + } + return this.validateBooking(value); } @@ -61,6 +66,22 @@ export class CreateBookingInputPipe implements PipeTransform { return object; } + validateRecurringBooking(value: CreateRecurringBookingInput_2024_08_13) { + const object = plainToClass(CreateRecurringBookingInput_2024_08_13, value); + + const errors = validateSync(object, { + whitelist: true, + forbidNonWhitelisted: true, + skipMissingProperties: false, + }); + + if (errors.length > 0) { + throw new BadRequestException(this.formatErrors(errors)); + } + + return object; + } + private formatErrors(errors: ValidationError[]): string { return errors .map((err) => { @@ -73,8 +94,20 @@ export class CreateBookingInputPipe implements PipeTransform { } private isRescheduleBookingInput( - value: CreateBookingInput_2024_08_13 | RescheduleBookingInput_2024_08_13 + value: + | CreateBookingInput_2024_08_13 + | RescheduleBookingInput_2024_08_13 + | CreateRecurringBookingInput_2024_08_13 ): value is RescheduleBookingInput_2024_08_13 { return value.hasOwnProperty("rescheduleBookingUid"); } + + private isRecurringBookingInput( + value: + | CreateBookingInput_2024_08_13 + | RescheduleBookingInput_2024_08_13 + | CreateRecurringBookingInput_2024_08_13 + ): value is CreateRecurringBookingInput_2024_08_13 { + return value.hasOwnProperty("recurringEventTypeId"); + } } diff --git a/packages/platform/types/bookings/2024-08-13/inputs/create-booking.input.ts b/packages/platform/types/bookings/2024-08-13/inputs/create-booking.input.ts index d6341559928507..8458b059bdf069 100644 --- a/packages/platform/types/bookings/2024-08-13/inputs/create-booking.input.ts +++ b/packages/platform/types/bookings/2024-08-13/inputs/create-booking.input.ts @@ -60,6 +60,35 @@ export class CreateBookingInput_2024_08_13 { bookingFieldsResponses!: Record; } +export class CreateRecurringBookingInput_2024_08_13 { + @IsDateString() + start!: string; + + @IsInt() + recurringEventTypeId!: number; + + @ValidateNested() + @Type(() => Attendee) + attendee!: Attendee; + + @IsArray() + @IsString({ each: true }) + @IsOptional() + guests?: string[]; + + @IsUrl() + @IsOptional() + meetingUrl?: string; + + @IsObject() + @IsOptional() + metadata!: Record; + + @IsObject() + @IsOptional() + bookingFieldsResponses!: Record; +} + export class RescheduleBookingInput_2024_08_13 { @IsDateString() start!: string; From 1c44dcc69be3c8b8c7872009e7de34cd2a6aa38d Mon Sep 17 00:00:00 2001 From: supalarry Date: Thu, 15 Aug 2024 15:56:43 +0200 Subject: [PATCH 09/96] refactor: add booking status in response --- .../v2/src/ee/bookings/2024-08-13/services/output.service.ts | 1 + .../types/bookings/2024-08-13/outputs/booking.output.ts | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/output.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/output.service.ts index 98ae3bc461d054..d027d9b7716f76 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/services/output.service.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/services/output.service.ts @@ -33,6 +33,7 @@ export class OutputBookingsService_2024_08_13 { const booking = { id: databaseBooking.id, uid: databaseBooking.uid, + status: databaseBooking.status.toLowerCase(), start: databaseBooking.startTime, end: databaseBooking.endTime, duration, diff --git a/packages/platform/types/bookings/2024-08-13/outputs/booking.output.ts b/packages/platform/types/bookings/2024-08-13/outputs/booking.output.ts index 8f2eaf59657322..a907a097e33ffa 100644 --- a/packages/platform/types/bookings/2024-08-13/outputs/booking.output.ts +++ b/packages/platform/types/bookings/2024-08-13/outputs/booking.output.ts @@ -34,7 +34,6 @@ class Attendee { @IsOptional() language?: BookingLanguageType; } - export class BookingOutput_2024_08_13 { @IsInt() @Expose() @@ -44,6 +43,10 @@ export class BookingOutput_2024_08_13 { @Expose() uid!: string; + @IsEnum(["cancelled", "accepted", "rejected", "pending", "awaiting_host"]) + @Expose() + status!: "cancelled" | "accepted" | "rejected" | "pending" | "awaiting_host"; + @IsDateString() @Expose() start!: string; From dd4959db7c7ba9dd4f4cd9ba863316ef2a0442f1 Mon Sep 17 00:00:00 2001 From: supalarry Date: Thu, 15 Aug 2024 16:51:56 +0200 Subject: [PATCH 10/96] refactor: recurring bookings --- apps/api/v2/package.json | 2 +- .../controllers/bookings.controller.ts | 16 +- .../outputs/create-booking.output.ts | 8 +- .../2024-08-13/outputs/get-booking.output.ts | 19 ++ .../2024-08-13/services/bookings.service.ts | 27 +- .../2024-08-13/services/input.service.ts | 38 ++- .../2024-08-13/services/output.service.ts | 40 ++- apps/api/v2/swagger/documentation.json | 6 +- .../2024-08-13/inputs/create-booking.input.ts | 2 +- .../2024-08-13/outputs/booking.output.ts | 50 +++ yarn.lock | 315 ++++++------------ 11 files changed, 284 insertions(+), 239 deletions(-) create mode 100644 apps/api/v2/src/ee/bookings/2024-08-13/outputs/get-booking.output.ts diff --git a/apps/api/v2/package.json b/apps/api/v2/package.json index 4a1e2b362da801..7ace0c34ee7fb1 100644 --- a/apps/api/v2/package.json +++ b/apps/api/v2/package.json @@ -62,7 +62,7 @@ "reflect-metadata": "^0.1.13", "rxjs": "^7.8.1", "stripe": "^15.3.0", - "uuid": "^8.3.2", + "uuid": "^10.0.0", "winston": "^3.11.0", "zod": "^3.22.4" }, diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.ts b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.ts index 70850e406979de..18021daf9181c2 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.ts @@ -2,7 +2,7 @@ import { CreateBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs import { BookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/bookings.service"; import { VERSION_2024_08_13_VALUE } from "@/lib/api-versions"; import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard"; -import { Controller, Post, Logger, Body, UseGuards, Req } from "@nestjs/common"; +import { Controller, Post, Logger, Body, UseGuards, Req, Get } from "@nestjs/common"; import { ApiTags as DocsTags } from "@nestjs/swagger"; import { Request } from "express"; @@ -40,4 +40,18 @@ export class BookingsController_2024_08_13 { data: booking, }; } + + // @Get("/:bookingUid") + // async getBooking(@Param("bookingUid") bookingUid: string): Promise { + // const { bookingInfo } = await getBookingInfo(bookingUid); + + // if (!bookingInfo) { + // throw new NotFoundException(`Booking with UID=${bookingUid} does not exist.`); + // } + + // return { + // status: SUCCESS_STATUS, + // data: bookingInfo, + // }; + // } } diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/outputs/create-booking.output.ts b/apps/api/v2/src/ee/bookings/2024-08-13/outputs/create-booking.output.ts index b1ed8f68c2fd3e..e61ac1276aca21 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/outputs/create-booking.output.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/outputs/create-booking.output.ts @@ -3,17 +3,13 @@ import { Type } from "class-transformer"; import { IsEnum, ValidateNested } from "class-validator"; import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants"; -import { BookingOutput_2024_08_13 } from "@calcom/platform-types"; +import { BookingOutput_2024_08_13, RecurringBookingOutput_2024_08_13 } from "@calcom/platform-types"; export class CreateBookingOutput_2024_08_13 { @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) @IsEnum([SUCCESS_STATUS, ERROR_STATUS]) status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS; - @ApiProperty({ - type: BookingOutput_2024_08_13, - }) @ValidateNested() - @Type(() => BookingOutput_2024_08_13) - data!: BookingOutput_2024_08_13 | BookingOutput_2024_08_13[]; + data!: BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13[]; } diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/outputs/get-booking.output.ts b/apps/api/v2/src/ee/bookings/2024-08-13/outputs/get-booking.output.ts new file mode 100644 index 00000000000000..e3e450248618c4 --- /dev/null +++ b/apps/api/v2/src/ee/bookings/2024-08-13/outputs/get-booking.output.ts @@ -0,0 +1,19 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { Type } from "class-transformer"; +import { IsEnum, ValidateNested } from "class-validator"; + +import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants"; +import { BookingOutput_2024_08_13 } from "@calcom/platform-types"; + +export class GetBookingOutput_2024_08_13 { + @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) + @IsEnum([SUCCESS_STATUS, ERROR_STATUS]) + status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS; + + @ApiProperty({ + type: BookingOutput_2024_08_13, + }) + @ValidateNested() + @Type(() => BookingOutput_2024_08_13) + data!: BookingOutput_2024_08_13; +} diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts index d04ab22373d59b..cded96dae45656 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts @@ -1,6 +1,7 @@ import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.repository"; import { InputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/input.service"; import { OutputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/output.service"; +import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository"; import { Injectable } from "@nestjs/common"; import { Request } from "express"; @@ -16,7 +17,8 @@ export class BookingsService_2024_08_13 { constructor( private readonly inputService: InputBookingsService_2024_08_13, private readonly outputService: OutputBookingsService_2024_08_13, - private readonly bookingsRepository: BookingsRepository_2024_08_13 + private readonly bookingsRepository: BookingsRepository_2024_08_13, + private readonly eventTypesRepository: EventTypesRepository_2024_06_14 ) {} async createBooking( @@ -26,9 +28,9 @@ export class BookingsService_2024_08_13 { | RescheduleBookingInput_2024_08_13 | CreateRecurringBookingInput_2024_08_13 ) { - const isRecurring = "recurringEventTypeId" in body; + const isRecurring = await this.isRecurring(body); - if (isRecurring) { + if (!("rescheduleBookingUid" in body) && isRecurring) { return this.createRecurringBooking(request, body); } @@ -46,8 +48,23 @@ export class BookingsService_2024_08_13 { return this.outputService.getOutputBooking(databaseBooking); } + async isRecurring( + body: + | CreateBookingInput_2024_08_13 + | RescheduleBookingInput_2024_08_13 + | CreateRecurringBookingInput_2024_08_13 + ) { + if ("rescheduleBookingUid" in body) { + return false; + } + + const eventType = await this.eventTypesRepository.getEventTypeById(body.eventTypeId); + + return !!eventType?.recurringEvent; + } + async createRecurringBooking(request: Request, body: CreateRecurringBookingInput_2024_08_13) { - const bookingRequest = await this.inputService.createBookingRequest(request, body); + const bookingRequest = await this.inputService.createRecurringBookingRequest(request, body); const bookings = await handleNewRecurringBooking(bookingRequest); const transformed = []; @@ -62,7 +79,7 @@ export class BookingsService_2024_08_13 { throw new Error(`Booking with id=${booking.id} was not found in the database`); } - transformed.push(this.outputService.getOutputBooking(databaseBooking)); + transformed.push(this.outputService.getOutputRecurringBooking(databaseBooking)); } return transformed; diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts index 94907de3a6d63b..a1e6a733255ff8 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts @@ -8,6 +8,7 @@ import { Logger } from "@nestjs/common"; import { Request } from "express"; import { DateTime } from "luxon"; import { NextApiRequest } from "next/types"; +import { v4 as uuidv4 } from "uuid"; import { z } from "zod"; import { X_CAL_CLIENT_ID } from "@calcom/platform-constants"; @@ -97,6 +98,31 @@ export class InputBookingsService_2024_08_13 { return newRequest as unknown as BookingRequest; } + async createRecurringBookingRequest( + request: Request, + body: CreateRecurringBookingInput_2024_08_13 + ): Promise { + // note(Lauris): update to this.transformInputCreate when rescheduling is implemented + const bodyTransformed = await this.transformInputCreateRecurringBooking(body); + const oAuthClientId = request.get(X_CAL_CLIENT_ID); + + const newRequest = { ...request }; + const userId = (await this.createBookingRequestOwnerId(request)) ?? undefined; + const oAuthParams = oAuthClientId + ? await this.createBookingRequestOAuthClientParams(oAuthClientId) + : DEFAULT_PLATFORM_PARAMS; + + const location = await this.getLocation(request, body); + Object.assign(newRequest, { userId, ...oAuthParams, platformBookingLocation: location }); + + newRequest.body = (bodyTransformed as any[]).map((event) => ({ + ...event, + noEmail: !oAuthParams.arePlatformEmailsEnabled, + })); + + return newRequest as unknown as BookingRequest; + } + private async createBookingRequestOwnerId(req: Request): Promise { try { const accessToken = req.get("Authorization")?.replace("Bearer ", ""); @@ -145,15 +171,13 @@ export class InputBookingsService_2024_08_13 { async transformInputCreateRecurringBooking(inputBooking: CreateRecurringBookingInput_2024_08_13) { const eventType = await this.eventTypesRepository.getEventTypeByIdWithOwnerAndTeam( - inputBooking.recurringEventTypeId + inputBooking.eventTypeId ); if (!eventType) { - throw new NotFoundException(`Event type with id=${inputBooking.recurringEventTypeId} not found`); + throw new NotFoundException(`Event type with id=${inputBooking.eventTypeId} not found`); } if (!eventType.recurringEvent) { - throw new NotFoundException( - `Event type with id=${inputBooking.recurringEventTypeId} is not a recurring event` - ); + throw new NotFoundException(`Event type with id=${inputBooking.eventTypeId} is not a recurring event`); } const occurrance = recurringEventSchema.parse(eventType.recurringEvent); @@ -167,6 +191,7 @@ export class InputBookingsService_2024_08_13 { } const events = []; + const recurringEventId = uuidv4(); let startTime = DateTime.fromISO(inputBooking.start, { zone: "utc" }).setZone( inputBooking.attendee.timeZone @@ -178,8 +203,9 @@ export class InputBookingsService_2024_08_13 { events.push({ start: startTime.toISO(), end: endTime.toISO(), - eventTypeId: inputBooking.recurringEventTypeId, + eventTypeId: inputBooking.eventTypeId, eventTypeSlug: eventType.slug, + recurringEventId, timeZone: inputBooking.attendee.timeZone, language: inputBooking.attendee.language || "en", metadata: inputBooking.metadata || {}, diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/output.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/output.service.ts index d027d9b7716f76..4396c9831687d8 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/services/output.service.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/services/output.service.ts @@ -3,7 +3,7 @@ import { plainToClass } from "class-transformer"; import { DateTime } from "luxon"; import { z } from "zod"; -import { BookingOutput_2024_08_13 } from "@calcom/platform-types"; +import { BookingOutput_2024_08_13, RecurringBookingOutput_2024_08_13 } from "@calcom/platform-types"; import { Booking } from "@calcom/prisma/client"; export const bookingResponsesSchema = z.object({ @@ -50,4 +50,42 @@ export class OutputBookingsService_2024_08_13 { return plainToClass(BookingOutput_2024_08_13, booking, { strategy: "excludeAll" }); } + + getOutputRecurringBooking( + databaseBooking: Booking & { + attendees: { name: string; email: string; timeZone: string; locale: string | null }[]; + } + ) { + const dateStart = DateTime.fromISO(databaseBooking.startTime.toISOString()); + const dateEnd = DateTime.fromISO(databaseBooking.endTime.toISOString()); + const duration = dateEnd.diff(dateStart, "minutes").minutes; + + const bookingResponses = bookingResponsesSchema.parse(databaseBooking.responses); + const attendee = databaseBooking.attendees.find((attendee) => attendee.email === bookingResponses.email); + + if (!attendee) { + throw new Error("Attendee not found"); + } + + const booking = { + id: databaseBooking.id, + uid: databaseBooking.uid, + status: databaseBooking.status.toLowerCase(), + start: databaseBooking.startTime, + end: databaseBooking.endTime, + duration, + eventTypeId: databaseBooking.eventTypeId, + attendee: { + name: attendee.name, + email: attendee.email, + timeZone: attendee.timeZone, + language: attendee.locale, + }, + guests: bookingResponses.guests, + meetingUrl: databaseBooking.location, + recurringBookingUid: databaseBooking.recurringEventId, + }; + + return plainToClass(RecurringBookingOutput_2024_08_13, booking, { strategy: "excludeAll" }); + } } diff --git a/apps/api/v2/swagger/documentation.json b/apps/api/v2/swagger/documentation.json index 7aa053bd06346d..5489ba38feb9eb 100644 --- a/apps/api/v2/swagger/documentation.json +++ b/apps/api/v2/swagger/documentation.json @@ -8963,10 +8963,6 @@ "data" ] }, - "BookingOutput_2024_08_13": { - "type": "object", - "properties": {} - }, "CreateBookingOutput_2024_08_13": { "type": "object", "properties": { @@ -8979,7 +8975,7 @@ ] }, "data": { - "$ref": "#/components/schemas/BookingOutput_2024_08_13" + "type": "object" } }, "required": [ diff --git a/packages/platform/types/bookings/2024-08-13/inputs/create-booking.input.ts b/packages/platform/types/bookings/2024-08-13/inputs/create-booking.input.ts index 8458b059bdf069..a98a0e2ad21e28 100644 --- a/packages/platform/types/bookings/2024-08-13/inputs/create-booking.input.ts +++ b/packages/platform/types/bookings/2024-08-13/inputs/create-booking.input.ts @@ -65,7 +65,7 @@ export class CreateRecurringBookingInput_2024_08_13 { start!: string; @IsInt() - recurringEventTypeId!: number; + eventTypeId!: number; @ValidateNested() @Type(() => Attendee) diff --git a/packages/platform/types/bookings/2024-08-13/outputs/booking.output.ts b/packages/platform/types/bookings/2024-08-13/outputs/booking.output.ts index a907a097e33ffa..7cda0548483850 100644 --- a/packages/platform/types/bookings/2024-08-13/outputs/booking.output.ts +++ b/packages/platform/types/bookings/2024-08-13/outputs/booking.output.ts @@ -79,3 +79,53 @@ export class BookingOutput_2024_08_13 { @Expose() meetingUrl?: string; } + +export class RecurringBookingOutput_2024_08_13 { + @IsInt() + @Expose() + id!: number; + + @IsString() + @Expose() + uid!: string; + + @IsEnum(["cancelled", "accepted", "rejected", "pending", "awaiting_host"]) + @Expose() + status!: "cancelled" | "accepted" | "rejected" | "pending" | "awaiting_host"; + + @IsDateString() + @Expose() + start!: string; + + @IsDateString() + @Expose() + end!: string; + + @IsInt() + @Expose() + duration!: number; + + @IsInt() + @Expose() + eventTypeId!: number; + + @IsString() + @Expose() + recurringBookingUid!: string; + + @ValidateNested() + @Type(() => Attendee) + @Expose() + attendee!: Attendee; + + @IsArray() + @IsString({ each: true }) + @IsOptional() + @Expose() + guests?: string[]; + + @IsUrl() + @IsOptional() + @Expose() + meetingUrl?: string; +} diff --git a/yarn.lock b/yarn.lock index fdb8746c5f3f2f..f73f02b20cfb04 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4133,8 +4133,8 @@ __metadata: resolution: "@calcom/api-v2@workspace:apps/api/v2" dependencies: "@calcom/platform-constants": "*" + "@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.27" "@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2" - "@calcom/platform-libraries-0.0.26": "npm:@calcom/platform-libraries@0.0.26" "@calcom/platform-types": "*" "@calcom/platform-utils": "*" "@calcom/prisma": "*" @@ -4190,7 +4190,7 @@ __metadata: ts-node: ^10.9.1 tsconfig-paths: ^4.1.0 typescript: ^4.9.4 - uuid: ^8.3.2 + uuid: ^10.0.0 winston: ^3.11.0 zod: ^3.22.4 languageName: unknown @@ -4286,7 +4286,7 @@ __metadata: languageName: unknown linkType: soft -"@calcom/atoms@*, @calcom/atoms@workspace:^, @calcom/atoms@workspace:packages/platform/atoms": +"@calcom/atoms@*, @calcom/atoms@workspace:packages/platform/atoms": version: 0.0.0-use.local resolution: "@calcom/atoms@workspace:packages/platform/atoms" dependencies: @@ -4465,17 +4465,17 @@ __metadata: "@heroicons/react": ^1.0.6 "@prisma/client": ^5.4.2 "@tailwindcss/forms": ^0.5.2 - "@types/node": ^20.3.1 + "@types/node": 16.9.1 "@types/react": 18.0.26 - autoprefixer: ^10.4.19 + autoprefixer: ^10.4.12 chart.js: ^3.7.1 client-only: ^0.0.1 eslint: ^8.34.0 - next: ^14.1.3 + next: ^13.5.4 next-auth: ^4.22.1 next-i18next: ^13.2.2 - postcss: ^8.4.38 - prisma: ^5.7.1 + postcss: ^8.4.18 + prisma: ^5.4.2 prisma-field-encryption: ^1.4.0 react: ^18.2.0 react-chartjs-2: ^4.0.1 @@ -4484,7 +4484,7 @@ __metadata: react-live-chat-loader: ^2.8.1 swr: ^1.2.2 tailwindcss: ^3.3.3 - typescript: ^5.3.3 + typescript: ^4.9.4 zod: ^3.22.4 languageName: unknown linkType: soft @@ -4849,6 +4849,15 @@ __metadata: languageName: unknown linkType: soft +"@calcom/horizon-workrooms@workspace:packages/app-store/horizon-workrooms": + version: 0.0.0-use.local + resolution: "@calcom/horizon-workrooms@workspace:packages/app-store/horizon-workrooms" + dependencies: + "@calcom/lib": "*" + "@calcom/types": "*" + languageName: unknown + linkType: soft + "@calcom/hubspot@workspace:packages/app-store/hubspot": version: 0.0.0-use.local resolution: "@calcom/hubspot@workspace:packages/app-store/hubspot" @@ -5086,25 +5095,25 @@ __metadata: languageName: unknown linkType: soft -"@calcom/platform-libraries-0.0.26@npm:@calcom/platform-libraries@0.0.26": - version: 0.0.26 - resolution: "@calcom/platform-libraries@npm:0.0.26" +"@calcom/platform-libraries-0.0.2@npm:@calcom/platform-libraries@0.0.2, @calcom/platform-libraries@npm:0.0.2": + version: 0.0.2 + resolution: "@calcom/platform-libraries@npm:0.0.2" dependencies: "@calcom/core": "*" "@calcom/features": "*" "@calcom/lib": "*" - checksum: 95b60b6bccfecf1d75f5b37b6e2b48811d6cbdd6ccb8157fa700558f2947adefd1fa26e7ed123f72960ddbf44ead26cce6e6fbd62505d73f5490abcf7c95563d + checksum: 61b6be1b9d0be8a54ca8b1dff4b1e4db122b3c30d9203467f5347232eaf600ca3892da45a2de8abfe75327086406c33c1f6f75cd12a7147744437bc85a0ee755 languageName: node linkType: hard -"@calcom/platform-libraries-0.0.2@npm:@calcom/platform-libraries@0.0.2, @calcom/platform-libraries@npm:0.0.2": - version: 0.0.2 - resolution: "@calcom/platform-libraries@npm:0.0.2" +"@calcom/platform-libraries@npm:@calcom/platform-libraries@0.0.27": + version: 0.0.27 + resolution: "@calcom/platform-libraries@npm:0.0.27" dependencies: "@calcom/core": "*" "@calcom/features": "*" "@calcom/lib": "*" - checksum: 61b6be1b9d0be8a54ca8b1dff4b1e4db122b3c30d9203467f5347232eaf600ca3892da45a2de8abfe75327086406c33c1f6f75cd12a7147744437bc85a0ee755 + checksum: 516c347b3f71e45879925d31f067035a7c6857dc0f474a726dcb58a7949e1d6906e8e21b18a3a480052088ef742048029509aa74b6bde31b51a13fc6ea00a9f4 languageName: node linkType: hard @@ -5130,6 +5139,7 @@ __metadata: dependencies: "@calcom/platform-constants": "*" "@types/express": ^4.17.21 + class-transformer: ^0.5.1 class-validator: ^0.14.0 languageName: unknown linkType: soft @@ -5737,7 +5747,6 @@ __metadata: dependencies: "@algora/sdk": ^0.1.2 "@calcom/app-store": "*" - "@calcom/atoms": "workspace:^" "@calcom/config": "*" "@calcom/dayjs": "*" "@calcom/embed-react": "workspace:^" @@ -5768,23 +5777,22 @@ __metadata: "@radix-ui/react-tabs": ^1.0.0 "@radix-ui/react-tooltip": ^1.0.0 "@stripe/stripe-js": ^1.35.0 - "@tanstack/react-query": ^5.17.15 + "@tanstack/react-query": ^4.3.9 "@typeform/embed-react": ^1.2.4 "@types/bcryptjs": ^2.4.2 "@types/debounce": ^1.2.1 "@types/gtag.js": ^0.0.10 "@types/micro": 7.3.7 - "@types/node": ^20.3.1 + "@types/node": 16.9.1 "@types/react": 18.0.26 "@types/react-gtm-module": ^2.0.1 "@types/xml2js": ^0.4.11 "@vercel/analytics": ^0.1.6 "@vercel/edge-functions-ui": ^0.2.1 "@vercel/og": ^0.5.0 - autoprefixer: ^10.4.19 + autoprefixer: ^10.4.12 bcryptjs: ^2.4.3 - class-variance-authority: ^0.7.0 - clsx: ^2.0.0 + clsx: ^1.2.1 cobe: ^0.4.1 concurrently: ^7.6.0 cross-env: ^7.0.3 @@ -5796,7 +5804,6 @@ __metadata: env-cmd: ^10.1.0 eslint: ^8.34.0 fathom-client: ^3.5.0 - framer-motion: ^11.0.25 globby: ^13.1.3 graphql: ^16.8.0 graphql-codegen: ^0.4.0 @@ -5806,7 +5813,7 @@ __metadata: i18n-unused: ^0.13.0 iframe-resizer-react: ^1.1.0 keen-slider: ^6.8.0 - lucide-react: ^0.364.0 + lucide-react: ^0.171.0 micro: ^10.0.1 next: ^14.1.3 next-auth: ^4.22.1 @@ -5814,11 +5821,11 @@ __metadata: next-i18next: ^13.2.2 next-seo: ^6.0.0 playwright-core: ^1.38.1 - postcss: ^8.4.38 + postcss: ^8.4.18 prism-react-renderer: ^1.3.5 react: ^18.2.0 react-confetti: ^6.0.1 - react-datocms: ^5.0.3 + react-datocms: ^3.1.0 react-device-detect: ^2.2.2 react-dom: ^18.2.0 react-fast-marquee: ^1.6.4 @@ -5828,23 +5835,21 @@ __metadata: react-live-chat-loader: ^2.8.1 react-markdown: ^9.0.1 react-merge-refs: 1.1.0 - react-parallax-tilt: ^1.7.226 react-resize-detector: ^9.1.0 react-twemoji: ^0.3.0 - react-twitter-embed: ^4.0.4 react-use-measure: ^2.1.1 react-wrap-balancer: ^1.0.0 remark: ^14.0.2 remark-html: ^14.0.1 remeda: ^1.24.1 - stripe: ^15.3.0 + stripe: ^9.16.0 tailwind-merge: ^1.13.2 tailwindcss: ^3.3.3 ts-node: ^10.9.1 - typescript: ^5.3.3 + typescript: ^4.9.4 wait-on: ^7.0.1 xml2js: ^0.6.0 - zod: ^3.22.4 + zod: ^3.22.2 languageName: unknown linkType: soft @@ -9253,59 +9258,6 @@ __metadata: languageName: node linkType: hard -"@mux/mux-player-react@npm:*": - version: 2.9.1 - resolution: "@mux/mux-player-react@npm:2.9.1" - dependencies: - "@mux/mux-player": 2.9.1 - "@mux/playback-core": 0.25.2 - prop-types: ^15.7.2 - peerDependencies: - "@types/react": ^17.0.0 || ^18 || ^19 - react: ^17.0.2 || ^18 || ^19 - react-dom: ^17.0.2 || ^18 || ^19 - peerDependenciesMeta: - "@types/react": - optional: true - "@types/react-dom": - optional: true - checksum: bee10ae2a1cf3279d9180020ed4e48a878b3277db98be00c41a143bd0c046066913f030d2c0088923dbc52c2f44c4cf3b7107688caa38b5662f093397ec7ba94 - languageName: node - linkType: hard - -"@mux/mux-player@npm:2.9.1": - version: 2.9.1 - resolution: "@mux/mux-player@npm:2.9.1" - dependencies: - "@mux/mux-video": 0.20.2 - "@mux/playback-core": 0.25.2 - media-chrome: ~3.2.5 - checksum: 21315a5277f17a595741890c4b16149fb05260075f63b25a51fba568aa395de35210fda95b4321025cc3ad093507ceac2e6c4080f378a6f910cc2b37206a4160 - languageName: node - linkType: hard - -"@mux/mux-video@npm:0.20.2": - version: 0.20.2 - resolution: "@mux/mux-video@npm:0.20.2" - dependencies: - "@mux/playback-core": 0.25.2 - castable-video: ~1.0.9 - custom-media-element: ~1.3.1 - media-tracks: ~0.3.2 - checksum: 58702a90b6c75c0525ef0c533500460fe6b24e9624f4e52d9a07a8a9e2fb132d0d734e823c390fe63d220c6a7b3377422c766a17bfaafe8c9ff527dfc28a0a38 - languageName: node - linkType: hard - -"@mux/playback-core@npm:0.25.2": - version: 0.25.2 - resolution: "@mux/playback-core@npm:0.25.2" - dependencies: - hls.js: ~1.5.11 - mux-embed: ~5.2.0 - checksum: 29e1950d605f3f2cc2511f325b94d40b0467f82e7974e7619f135617e6b6dd3424da9282cb01e63a7215029dd3c9ac560b5a208b44531120835bf6244392dbb1 - languageName: node - linkType: hard - "@ndelangen/get-tarball@npm:^3.0.7": version: 3.0.9 resolution: "@ndelangen/get-tarball@npm:3.0.9" @@ -16161,6 +16113,13 @@ __metadata: languageName: node linkType: hard +"@tanstack/query-core@npm:4.36.1": + version: 4.36.1 + resolution: "@tanstack/query-core@npm:4.36.1" + checksum: 47672094da20d89402d9fe03bb7b0462be73a76ff9ca715169738bc600a719d064d106d083a8eedae22a2c22de22f87d5eb5d31ef447aba771d9190f2117ed10 + languageName: node + linkType: hard + "@tanstack/query-core@npm:5.17.19": version: 5.17.19 resolution: "@tanstack/query-core@npm:5.17.19" @@ -16168,6 +16127,25 @@ __metadata: languageName: node linkType: hard +"@tanstack/react-query@npm:^4.3.9": + version: 4.36.1 + resolution: "@tanstack/react-query@npm:4.36.1" + dependencies: + "@tanstack/query-core": 4.36.1 + use-sync-external-store: ^1.2.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-native: "*" + peerDependenciesMeta: + react-dom: + optional: true + react-native: + optional: true + checksum: 1aff0a476859386f8d32253fa0d0bde7b81769a6d4d4d9cbd78778f0f955459a3bdb7ee27a0d2ee7373090f12998b45df80db0b5b313bd0a7a39d36c6e8e51c5 + languageName: node + linkType: hard + "@tanstack/react-query@npm:^5.17.15": version: 5.17.19 resolution: "@tanstack/react-query@npm:5.17.19" @@ -21594,15 +21572,6 @@ __metadata: languageName: node linkType: hard -"castable-video@npm:~1.0.9": - version: 1.0.10 - resolution: "castable-video@npm:1.0.10" - dependencies: - custom-media-element: ~1.3.2 - checksum: 5b7a27aacf305f40c6867e96e773f4290260f6eb8b01fe1f209506f8f6dec4cd099f3e35ee57de098829e4bcc688ccdc2cbb2fc86c509712dd573eadf66d42cc - languageName: node - linkType: hard - "ccount@npm:^2.0.0": version: 2.0.1 resolution: "ccount@npm:2.0.1" @@ -22067,15 +22036,6 @@ __metadata: languageName: node linkType: hard -"class-variance-authority@npm:^0.7.0": - version: 0.7.0 - resolution: "class-variance-authority@npm:0.7.0" - dependencies: - clsx: 2.0.0 - checksum: e7fd1fab433ef06f52a1b7b241b70b4a185864deef199d3b0a2c3412f1cc179517288264c383f3b971a00d76811625fc8f7ffe709e6170219e88cd7368f08a20 - languageName: node - linkType: hard - "classnames@npm:^2.2.5, classnames@npm:^2.2.6": version: 2.3.2 resolution: "classnames@npm:2.3.2" @@ -22348,13 +22308,6 @@ __metadata: languageName: node linkType: hard -"clsx@npm:2.0.0": - version: 2.0.0 - resolution: "clsx@npm:2.0.0" - checksum: a2cfb2351b254611acf92faa0daf15220f4cd648bdf96ce369d729813b85336993871a4bf6978ddea2b81b5a130478339c20d9d0b5c6fc287e5147f0c059276e - languageName: node - linkType: hard - "clsx@npm:^1.1.1": version: 1.1.1 resolution: "clsx@npm:1.1.1" @@ -22362,6 +22315,13 @@ __metadata: languageName: node linkType: hard +"clsx@npm:^1.2.1": + version: 1.2.1 + resolution: "clsx@npm:1.2.1" + checksum: 30befca8019b2eb7dbad38cff6266cf543091dae2825c856a62a8ccf2c3ab9c2907c4d12b288b73101196767f66812365400a227581484a05f968b0307cfaf12 + languageName: node + linkType: hard + "clsx@npm:^2.0.0": version: 2.1.0 resolution: "clsx@npm:2.1.0" @@ -23655,13 +23615,6 @@ __metadata: languageName: node linkType: hard -"custom-media-element@npm:~1.3.1, custom-media-element@npm:~1.3.2": - version: 1.3.2 - resolution: "custom-media-element@npm:1.3.2" - checksum: 9ec2ff88c3c5ba7d20b9484c0359b4cfe08ad6804f3116934ffbae2f2e0e04d2a832beb6f0bec56027a969ab366727d217406f750890613c988de6de5330cb4f - languageName: node - linkType: hard - "d3-array@npm:2 - 3, d3-array@npm:2.10.0 - 3, d3-array@npm:^3.1.6": version: 3.2.3 resolution: "d3-array@npm:3.2.3" @@ -27724,26 +27677,6 @@ __metadata: languageName: node linkType: hard -"framer-motion@npm:^11.0.25": - version: 11.3.24 - resolution: "framer-motion@npm:11.3.24" - dependencies: - tslib: ^2.4.0 - peerDependencies: - "@emotion/is-prop-valid": "*" - react: ^18.0.0 - react-dom: ^18.0.0 - peerDependenciesMeta: - "@emotion/is-prop-valid": - optional: true - react: - optional: true - react-dom: - optional: true - checksum: c036284f7adb0f8fa05624e0bfb5e0000c1a3cfdc349a01797c9f8be0fadb5b9ccdbea095abfe685cc52c94f7a69a79d98569c8f74aa1ba3bc1676624f614952 - languageName: node - linkType: hard - "fresh@npm:0.5.2, fresh@npm:^0.5.2": version: 0.5.2 resolution: "fresh@npm:0.5.2" @@ -29426,13 +29359,6 @@ __metadata: languageName: node linkType: hard -"hls.js@npm:~1.5.11": - version: 1.5.14 - resolution: "hls.js@npm:1.5.14" - checksum: 7e8385dd188cc2fbf089d3daed12a4b0905f1468be1b1598ddba07edc9d398f9639a054b8f2a73c2d09a177eecc6559c0030ee051611f109e23559004d953d3e - languageName: node - linkType: hard - "hmac-drbg@npm:^1.0.1": version: 1.0.1 resolution: "hmac-drbg@npm:1.0.1" @@ -33954,12 +33880,12 @@ __metadata: languageName: node linkType: hard -"lucide-react@npm:^0.364.0": - version: 0.364.0 - resolution: "lucide-react@npm:0.364.0" +"lucide-react@npm:^0.171.0": + version: 0.171.0 + resolution: "lucide-react@npm:0.171.0" peerDependencies: react: ^16.5.1 || ^17.0.0 || ^18.0.0 - checksum: 954f6274df46ffa172130d9e60eb630a377435c7dbf0ddb5ceebf284602dde3b99755dcd5c0757e697ee734a32c808a3cd2c25ad88fec1d8f627a2317f00e3e6 + checksum: 768ffe368c52a518ee339203d86ff4479989ab4d79c0716f721900c4bb7392ef6ff7a14807f6a685abd74d27f4c1778170bff77a0ab4c3e06c17944b557d8300 languageName: node linkType: hard @@ -34501,20 +34427,6 @@ __metadata: languageName: node linkType: hard -"media-chrome@npm:~3.2.5": - version: 3.2.5 - resolution: "media-chrome@npm:3.2.5" - checksum: f1d4620d250327c0cb4872c2f7825f407b4f8086e9830a6db76f6be97d6cbed0c0779a768d1e3b2fde62a4bfa283cfadabf588985af1720af0d4c206515b3b7c - languageName: node - linkType: hard - -"media-tracks@npm:~0.3.2": - version: 0.3.3 - resolution: "media-tracks@npm:0.3.3" - checksum: 4795af3f171d7ad3a68d1ac1c1e8166a735244fe57d3fc0ec53b1c7410799e524756fc0bfb389632aebb148436b03baade36ca34a3f5377776c3968f6d2cc580 - languageName: node - linkType: hard - "media-typer@npm:0.3.0": version: 0.3.0 resolution: "media-typer@npm:0.3.0" @@ -35880,13 +35792,6 @@ __metadata: languageName: node linkType: hard -"mux-embed@npm:~5.2.0": - version: 5.2.1 - resolution: "mux-embed@npm:5.2.1" - checksum: ec34d3e003e2520be6d1386ca2383daa198a1fa3a469ca3f06c11a742d6ab136eef4944c37e8f2dcaa0585e1b0a6dfc119e3cf2302c7eb0e7f2ebc7c1fc95fbd - languageName: node - linkType: hard - "mysql2@npm:3.9.1": version: 3.9.1 resolution: "mysql2@npm:3.9.1" @@ -40203,21 +40108,20 @@ __metadata: languageName: node linkType: hard -"react-datocms@npm:^5.0.3": - version: 5.0.3 - resolution: "react-datocms@npm:5.0.3" +"react-datocms@npm:^3.1.0": + version: 3.1.4 + resolution: "react-datocms@npm:3.1.4" dependencies: - "@mux/mux-player-react": "*" datocms-listen: ^0.1.9 datocms-structured-text-generic-html-renderer: ^2.0.1 datocms-structured-text-utils: ^2.0.1 - react-intersection-observer: ^9.4.3 + react-intersection-observer: ^8.33.1 react-string-replace: ^1.1.0 universal-base64: ^2.1.0 use-deep-compare-effect: ^1.6.1 peerDependencies: react: ">= 16.12.0" - checksum: 22c20152afb54424acfe967a2c8c525cd9f132a33374f2aba0231f16ea64dade389b096e2dac8de9ffded612bc32e9891d725609ee947639fe1cef907cb143f5 + checksum: 54aba12aef4937175c2011548a8a576c96c8d8a596e84d191826910624c1d596e76a49782689dc236388a10803b02e700ac820cb7500cca7fd147a81f6c544c3 languageName: node linkType: hard @@ -40458,16 +40362,12 @@ __metadata: languageName: node linkType: hard -"react-intersection-observer@npm:^9.4.3": - version: 9.13.0 - resolution: "react-intersection-observer@npm:9.13.0" +"react-intersection-observer@npm:^8.33.1": + version: 8.34.0 + resolution: "react-intersection-observer@npm:8.34.0" peerDependencies: - react: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - react-dom: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - peerDependenciesMeta: - react-dom: - optional: true - checksum: ee2a163b078923c1556814834f83d7c3ea3e8a9d3ceef974351fd52afe12163518ec57cb2eb0f6544ac255ac4b64d2e7652bc5d60c255159c0f25bda57a565b6 + react: ^15.0.0 || ^16.0.0 || ^17.0.0|| ^18.0.0 + checksum: 7713fecfd1512c7f5a60f9f0bf15403b8f8bbd4110bcafaeaea6de36a0e0eb60368c3638f99e9c97b75ad8fc787ea48c241dcb5c694f821d7f2976f709082cc5 languageName: node linkType: hard @@ -40577,16 +40477,6 @@ __metadata: languageName: node linkType: hard -"react-parallax-tilt@npm:^1.7.226": - version: 1.7.235 - resolution: "react-parallax-tilt@npm:1.7.235" - peerDependencies: - react: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 - react-dom: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 - checksum: 3174054dc0725ed8260d592931fbef3315f6dae636a8f88c15e68be3378d9c391edb75cdf8f48430689f5fe1db09b55e3ae302889b1a9afa68a79cc394aff1ad - languageName: node - linkType: hard - "react-phone-input-2@npm:^2.15.1": version: 2.15.1 resolution: "react-phone-input-2@npm:2.15.1" @@ -41021,18 +40911,6 @@ __metadata: languageName: node linkType: hard -"react-twitter-embed@npm:^4.0.4": - version: 4.0.4 - resolution: "react-twitter-embed@npm:4.0.4" - dependencies: - scriptjs: ^2.5.9 - peerDependencies: - react: ^16.0.0 || ^17.0.0 || ^18.0.0 - react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 - checksum: cdb3c5bd04c4da0efa767476be47c0a3865fb6335f2a1b9e242170167b51615c38164223278cef60c77143c4bac27ba582cbea054d0af3f138104fa5ec537c4c - languageName: node - linkType: hard - "react-universal-interface@npm:^0.6.2": version: 0.6.2 resolution: "react-universal-interface@npm:0.6.2" @@ -42857,13 +42735,6 @@ __metadata: languageName: node linkType: hard -"scriptjs@npm:^2.5.9": - version: 2.5.9 - resolution: "scriptjs@npm:2.5.9" - checksum: fc84cb6b60b6fb9aa6f1b3bc59fc94b233bd5241ed3a04233579014382b5eb60640269c87d8657902acc09f9b785ee33230c218627cea00e653564bda8f5acb6 - languageName: node - linkType: hard - "scuid@npm:^1.1.0": version: 1.1.0 resolution: "scuid@npm:1.1.0" @@ -47375,6 +47246,15 @@ __metadata: languageName: node linkType: hard +"use-sync-external-store@npm:^1.2.0": + version: 1.2.2 + resolution: "use-sync-external-store@npm:1.2.2" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + checksum: fe07c071c4da3645f112c38c0e57beb479a8838616ff4e92598256ecce527f2888c08febc7f9b2f0ce2f0e18540ba3cde41eb2035e4fafcb4f52955037098a81 + languageName: node + linkType: hard + "utif@npm:^2.0.1": version: 2.0.1 resolution: "utif@npm:2.0.1" @@ -47436,6 +47316,15 @@ __metadata: languageName: node linkType: hard +"uuid@npm:^10.0.0": + version: 10.0.0 + resolution: "uuid@npm:10.0.0" + bin: + uuid: dist/bin/uuid + checksum: 4b81611ade2885d2313ddd8dc865d93d8dccc13ddf901745edca8f86d99bc46d7a330d678e7532e7ebf93ce616679fb19b2e3568873ac0c14c999032acb25869 + languageName: node + linkType: hard + "uuid@npm:^3.3.2": version: 3.4.0 resolution: "uuid@npm:3.4.0" From c59295d009836442294ee22d5abade8ffb7e39c0 Mon Sep 17 00:00:00 2001 From: supalarry Date: Fri, 16 Aug 2024 13:19:34 +0200 Subject: [PATCH 11/96] feat: get booking by uid --- .../2024-08-13/bookings.repository.ts | 11 +++++++ .../controllers/bookings.controller.ts | 26 ++++++++--------- .../2024-08-13/outputs/get-booking.output.ts | 4 +-- .../2024-08-13/services/bookings.service.ts | 29 ++++++++++++------- .../2024-08-13/services/output.service.ts | 26 +++++++++++++++++ apps/api/v2/swagger/documentation.json | 28 ++++++++++++++++-- 6 files changed, 96 insertions(+), 28 deletions(-) diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/bookings.repository.ts b/apps/api/v2/src/ee/bookings/2024-08-13/bookings.repository.ts index 5bbd01b5b37125..9157723592a96b 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/bookings.repository.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/bookings.repository.ts @@ -43,4 +43,15 @@ export class BookingsRepository_2024_08_13 { }, }); } + + async getRecurringByUidWithAttendees(uid: string) { + return this.dbRead.prisma.booking.findMany({ + where: { + recurringEventId: uid, + }, + include: { + attendees: true, + }, + }); + } } diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.ts b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.ts index 18021daf9181c2..ace33b3eed5336 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.ts @@ -1,11 +1,13 @@ import { CreateBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/create-booking.output"; +import { GetBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/get-booking.output"; import { BookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/bookings.service"; import { VERSION_2024_08_13_VALUE } from "@/lib/api-versions"; import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard"; -import { Controller, Post, Logger, Body, UseGuards, Req, Get } from "@nestjs/common"; +import { Controller, Post, Logger, Body, UseGuards, Req, Get, Param } from "@nestjs/common"; import { ApiTags as DocsTags } from "@nestjs/swagger"; import { Request } from "express"; +import { SUCCESS_STATUS } from "@calcom/platform-constants"; import { CreateBookingInput_2024_08_13, CreateBookingInputPipe, @@ -36,22 +38,18 @@ export class BookingsController_2024_08_13 { const booking = await this.bookingsService.createBooking(request, body); return { - status: "success", + status: SUCCESS_STATUS, data: booking, }; } - // @Get("/:bookingUid") - // async getBooking(@Param("bookingUid") bookingUid: string): Promise { - // const { bookingInfo } = await getBookingInfo(bookingUid); + @Get("/:bookingUid") + async getBooking(@Param("bookingUid") bookingUid: string): Promise { + const booking = await this.bookingsService.getBooking(bookingUid); - // if (!bookingInfo) { - // throw new NotFoundException(`Booking with UID=${bookingUid} does not exist.`); - // } - - // return { - // status: SUCCESS_STATUS, - // data: bookingInfo, - // }; - // } + return { + status: SUCCESS_STATUS, + data: booking, + }; + } } diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/outputs/get-booking.output.ts b/apps/api/v2/src/ee/bookings/2024-08-13/outputs/get-booking.output.ts index e3e450248618c4..223fb726adf25c 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/outputs/get-booking.output.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/outputs/get-booking.output.ts @@ -3,7 +3,7 @@ import { Type } from "class-transformer"; import { IsEnum, ValidateNested } from "class-validator"; import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants"; -import { BookingOutput_2024_08_13 } from "@calcom/platform-types"; +import { BookingOutput_2024_08_13, RecurringBookingOutput_2024_08_13 } from "@calcom/platform-types"; export class GetBookingOutput_2024_08_13 { @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) @@ -15,5 +15,5 @@ export class GetBookingOutput_2024_08_13 { }) @ValidateNested() @Type(() => BookingOutput_2024_08_13) - data!: BookingOutput_2024_08_13; + data!: BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13[]; } diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts index cded96dae45656..fb21c0708e2318 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts @@ -66,22 +66,31 @@ export class BookingsService_2024_08_13 { async createRecurringBooking(request: Request, body: CreateRecurringBookingInput_2024_08_13) { const bookingRequest = await this.inputService.createRecurringBookingRequest(request, body); const bookings = await handleNewRecurringBooking(bookingRequest); + const uid = bookings[0].recurringEventId; + if (!uid) { + throw new Error("Recurring booking was not created"); + } - const transformed = []; + const recurringBooking = await this.bookingsRepository.getRecurringByUidWithAttendees(uid); + return this.outputService.getOutputRecurringBookings(recurringBooking); + } - for (const booking of bookings) { - if (!booking.id) { - throw new Error("Booking was not created"); - } + async getBooking(uid: string) { + const booking = await this.bookingsRepository.getByUidWithAttendees(uid); - const databaseBooking = await this.bookingsRepository.getByIdWithAttendees(booking.id); - if (!databaseBooking) { - throw new Error(`Booking with id=${booking.id} was not found in the database`); + if (booking) { + const isRecurring = !!booking.recurringEventId; + if (isRecurring) { + return this.outputService.getOutputRecurringBooking(booking); } + return this.outputService.getOutputBooking(booking); + } - transformed.push(this.outputService.getOutputRecurringBooking(databaseBooking)); + const recurringBooking = await this.bookingsRepository.getRecurringByUidWithAttendees(uid); + if (!recurringBooking.length) { + throw new Error(`Booking with uid=${uid} was not found in the database`); } - return transformed; + return this.outputService.getOutputRecurringBookings(recurringBooking); } } diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/output.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/output.service.ts index 4396c9831687d8..16e5a6b78bd391 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/services/output.service.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/services/output.service.ts @@ -1,3 +1,4 @@ +import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.repository"; import { Injectable } from "@nestjs/common"; import { plainToClass } from "class-transformer"; import { DateTime } from "luxon"; @@ -14,6 +15,8 @@ export const bookingResponsesSchema = z.object({ @Injectable() export class OutputBookingsService_2024_08_13 { + constructor(private readonly bookingsRepository: BookingsRepository_2024_08_13) {} + getOutputBooking( databaseBooking: Booking & { attendees: { name: string; email: string; timeZone: string; locale: string | null }[]; @@ -51,6 +54,29 @@ export class OutputBookingsService_2024_08_13 { return plainToClass(BookingOutput_2024_08_13, booking, { strategy: "excludeAll" }); } + async getOutputRecurringBookings( + databaseBookings: (Booking & { + attendees: { name: string; email: string; timeZone: string; locale: string | null }[]; + })[] + ) { + const transformed = []; + + for (const booking of databaseBookings) { + if (!booking.id) { + throw new Error("Booking was not created"); + } + + const databaseBooking = await this.bookingsRepository.getByIdWithAttendees(booking.id); + if (!databaseBooking) { + throw new Error(`Booking with id=${booking.id} was not found in the database`); + } + + transformed.push(this.getOutputRecurringBooking(databaseBooking)); + } + + return transformed; + } + getOutputRecurringBooking( databaseBooking: Booking & { attendees: { name: string; email: string; timeZone: string; locale: string | null }[]; diff --git a/apps/api/v2/swagger/documentation.json b/apps/api/v2/swagger/documentation.json index 5489ba38feb9eb..04a524ea23a3f1 100644 --- a/apps/api/v2/swagger/documentation.json +++ b/apps/api/v2/swagger/documentation.json @@ -2920,7 +2920,7 @@ }, "/v2/bookings/{bookingUid}": { "get": { - "operationId": "BookingsController_2024_04_15_getBooking", + "operationId": "BookingsController_2024_08_13_getBooking", "parameters": [ { "name": "bookingUid", @@ -2937,7 +2937,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/GetBookingOutput_2024_04_15" + "$ref": "#/components/schemas/GetBookingOutput_2024_08_13" } } } @@ -8983,6 +8983,30 @@ "data" ] }, + "BookingOutput_2024_08_13": { + "type": "object", + "properties": {} + }, + "GetBookingOutput_2024_08_13": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/BookingOutput_2024_08_13" + } + }, + "required": [ + "status", + "data" + ] + }, "ReserveSlotInput": { "type": "object", "properties": {} From 1bce4d8e791ab96e172b3cf7f3b4a55af0e65ca8 Mon Sep 17 00:00:00 2001 From: supalarry Date: Mon, 19 Aug 2024 10:35:45 +0200 Subject: [PATCH 12/96] wip: get event types --- .../controllers/bookings.controller.ts | 22 +++- .../2024-08-13/outputs/get-bookings.output.ts | 14 ++ .../2024-08-13/services/bookings.service.ts | 87 +++++++++++++ apps/api/v2/swagger/documentation.json | 61 ++++----- .../2024-08-13/inputs/get-bookings.input.ts | 122 ++++++++++++++++++ .../types/bookings/2024-08-13/inputs/index.ts | 1 + 6 files changed, 270 insertions(+), 37 deletions(-) create mode 100644 apps/api/v2/src/ee/bookings/2024-08-13/outputs/get-bookings.output.ts create mode 100644 packages/platform/types/bookings/2024-08-13/inputs/get-bookings.input.ts diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.ts b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.ts index ace33b3eed5336..eca64af469d453 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.ts @@ -1,17 +1,21 @@ import { CreateBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/create-booking.output"; import { GetBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/get-booking.output"; +import { GetBookingsOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/get-bookings.output"; import { BookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/bookings.service"; import { VERSION_2024_08_13_VALUE } from "@/lib/api-versions"; +import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator"; +import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard"; import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard"; -import { Controller, Post, Logger, Body, UseGuards, Req, Get, Param } from "@nestjs/common"; +import { Controller, Post, Logger, Body, UseGuards, Req, Get, Param, Query } from "@nestjs/common"; import { ApiTags as DocsTags } from "@nestjs/swagger"; import { Request } from "express"; -import { SUCCESS_STATUS } from "@calcom/platform-constants"; +import { BOOKING_READ, SUCCESS_STATUS } from "@calcom/platform-constants"; import { CreateBookingInput_2024_08_13, CreateBookingInputPipe, CreateRecurringBookingInput_2024_08_13, + GetBookingsInput_2024_08_13, RescheduleBookingInput_2024_08_13, } from "@calcom/platform-types"; @@ -52,4 +56,18 @@ export class BookingsController_2024_08_13 { data: booking, }; } + + @Get("/") + @UseGuards(ApiAuthGuard) + @Permissions([BOOKING_READ]) + async getBookings( + @Query() queryParams: GetBookingsInput_2024_08_13 + ): Promise { + const bookings = await this.bookingsService.getBookings(queryParams); + + return { + status: SUCCESS_STATUS, + data: bookings, + }; + } } diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/outputs/get-bookings.output.ts b/apps/api/v2/src/ee/bookings/2024-08-13/outputs/get-bookings.output.ts new file mode 100644 index 00000000000000..7d5553e85a1cb5 --- /dev/null +++ b/apps/api/v2/src/ee/bookings/2024-08-13/outputs/get-bookings.output.ts @@ -0,0 +1,14 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { IsEnum, ValidateNested } from "class-validator"; + +import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants"; +import { BookingOutput_2024_08_13, RecurringBookingOutput_2024_08_13 } from "@calcom/platform-types"; + +export class GetBookingsOutput_2024_08_13 { + @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) + @IsEnum([SUCCESS_STATUS, ERROR_STATUS]) + status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS; + + @ValidateNested({ each: true }) + data!: (BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13)[]; +} diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts index fb21c0708e2318..77a465aa9e64cb 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts @@ -10,7 +10,10 @@ import { CreateBookingInput_2024_08_13, RescheduleBookingInput_2024_08_13, CreateRecurringBookingInput_2024_08_13, + GetBookingsInput_2024_08_13, } from "@calcom/platform-types"; +import { Prisma } from "@calcom/prisma/client"; +import { BookingStatus } from "@calcom/prisma/enums"; @Injectable() export class BookingsService_2024_08_13 { @@ -93,4 +96,88 @@ export class BookingsService_2024_08_13 { return this.outputService.getOutputRecurringBookings(recurringBooking); } + + async getBookings(queryParams: GetBookingsInput_2024_08_13) { + return []; + } + + async createGetBookingsWhere(queryParams: GetBookingsInput_2024_08_13): Promise { + const where: Prisma.BookingWhereInput = {}; + + if (queryParams.status) { + if (queryParams.status === "upcoming") { + where.startTime = { + gte: new Date(), + }; + } else if (queryParams.status === "past") { + where.startTime = { + lte: new Date(), + }; + } else if (queryParams.status.startsWith("!")) { + where.status = { + not: queryParams.status.substring(1) as BookingStatus, + }; + } else { + where.status = queryParams.status; + } + } + + if (queryParams.attendeeEmail) { + where.attendees = { + some: { + email: { + contains: queryParams.attendeeEmail, + mode: "insensitive", + }, + }, + }; + } + + if (queryParams.eventTypeIds) { + where.eventTypeId = { + in: queryParams.eventTypeIds, + }; + } + + if (queryParams.eventTypeId) { + where.eventTypeId = queryParams.eventTypeId; + } + + if (queryParams.teamsIds) { + where.destinationCalendar = { + teamId: { + in: queryParams.teamsIds, + }, + }; + } + + if (queryParams.teamId) { + where.destinationCalendar = { + teamId: queryParams.teamId, + }; + } + + if (queryParams.dateRange) { + const [fromDate, toDate] = queryParams.dateRange; + where.startTime = { + gte: new Date(fromDate), + lte: new Date(toDate), + }; + } else { + if (queryParams.fromDate) { + where.startTime = { + gte: new Date(queryParams.fromDate), + }; + } + + if (queryParams.toDate) { + where.startTime = { + ...where.startTime, + lte: new Date(queryParams.toDate), + }; + } + } + + return where; + } } diff --git a/apps/api/v2/swagger/documentation.json b/apps/api/v2/swagger/documentation.json index 04a524ea23a3f1..dee47f2024ad21 100644 --- a/apps/api/v2/swagger/documentation.json +++ b/apps/api/v2/swagger/documentation.json @@ -2848,47 +2848,15 @@ }, "/v2/bookings": { "get": { - "operationId": "BookingsController_2024_04_15_getBookings", - "parameters": [ - { - "name": "cursor", - "required": false, - "in": "query", - "schema": { - "type": "number" - } - }, - { - "name": "limit", - "required": false, - "in": "query", - "schema": { - "type": "number" - } - }, - { - "name": "filters[status]", - "required": true, - "in": "query", - "schema": { - "enum": [ - "upcoming", - "recurring", - "past", - "cancelled", - "unconfirmed" - ], - "type": "string" - } - } - ], + "operationId": "BookingsController_2024_08_13_getBookings", + "parameters": [], "responses": { "200": { "description": "", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/GetBookingsOutput_2024_04_15" + "$ref": "#/components/schemas/GetBookingsOutput_2024_08_13" } } } @@ -9007,6 +8975,29 @@ "data" ] }, + "GetBookingsOutput_2024_08_13": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "type": "array", + "items": { + "type": "object" + } + } + }, + "required": [ + "status", + "data" + ] + }, "ReserveSlotInput": { "type": "object", "properties": {} diff --git a/packages/platform/types/bookings/2024-08-13/inputs/get-bookings.input.ts b/packages/platform/types/bookings/2024-08-13/inputs/get-bookings.input.ts new file mode 100644 index 00000000000000..a29ce6bc4c3027 --- /dev/null +++ b/packages/platform/types/bookings/2024-08-13/inputs/get-bookings.input.ts @@ -0,0 +1,122 @@ +import { Transform, Type } from "class-transformer"; +import { + ArrayMaxSize, + ArrayMinSize, + ArrayNotEmpty, + IsArray, + IsEnum, + IsInt, + IsISO8601, + IsNumber, + IsOptional, + IsString, + Max, + Min, +} from "class-validator"; + +enum Status { + accepted = "accepted", + cancelled = "cancelled", + rejected = "rejected", + pending = "pending", + upcoming = "upcoming", + recurring = "recurring", + past = "past", + unconfirmed = "unconfirmed", + "!accepted" = "!accepted", + "!cancelled" = "!cancelled", + "!rejected" = "!rejected", + "!pending" = "!pending", + "!upcoming" = "!upcoming", + "!recurring" = "!recurring", + "!past" = "!past", + "!unconfirmed" = "!unconfirmed", +} +type StatusType = keyof typeof Status; + +enum SortOrder { + asc = "asc", + desc = "desc", +} +type SortOrderType = keyof typeof SortOrder; + +export class GetBookingsInput_2024_08_13 { + // note(Lauris): filters + @IsOptional() + @IsEnum(Status) + @IsOptional() + status?: StatusType; + + @IsString() + @IsOptional() + attendeeEmail?: string; + + @IsArray() + @Type(() => Number) + @IsOptional() + eventTypeIds?: number[]; + + @IsInt() + @IsOptional() + eventTypeId?: number; + + @IsArray() + @Type(() => Number) + @IsOptional() + teamsIds?: number[]; + + @IsInt() + @IsOptional() + teamId?: number; + + @IsOptional() + @IsArray() + @ArrayMinSize(2, { message: "dateRange must contain 2 dates - from and to." }) + @ArrayMaxSize(2, { message: "dateRange must contain 2 dates - from and to." }) + @ArrayNotEmpty({ message: "dateRange cannot be empty." }) + @IsISO8601( + { strict: true }, + { each: true, message: "Each date in dateRange must be a valid ISO 8601 date." } + ) + dateRange?: [string, string]; + + @IsOptional() + @IsISO8601({ strict: true }, { message: "fromDate must be a valid ISO 8601 date." }) + fromDate?: string; + + @IsOptional() + @IsISO8601({ strict: true }, { message: "toDate must be a valid ISO 8601 date." }) + toDate?: string; + + // note(Lauris): sort + @IsOptional() + @IsEnum(SortOrder, { + message: 'SortStart must be either "asc" or "desc".', + }) + sortStart?: SortOrderType; + + @IsOptional() + @IsEnum(SortOrder, { + message: 'SortEnd must be either "asc" or "desc".', + }) + sortEnd?: SortOrderType; + + @IsOptional() + @IsEnum(SortOrder, { + message: 'SortCreated must be either "asc" or "desc".', + }) + sortCreated?: SortOrderType; + + // note(Lauris): pagination + @Transform(({ value }: { value: string }) => value && parseInt(value)) + @IsNumber() + @Min(1) + @Max(100) + @IsOptional() + limit?: number; + + @Transform(({ value }: { value: string }) => value && parseInt(value)) + @IsNumber() + @IsOptional() + cursor?: number; +} diff --git a/packages/platform/types/bookings/2024-08-13/inputs/index.ts b/packages/platform/types/bookings/2024-08-13/inputs/index.ts index 6f24e2bf00b57b..1dc86279cf339b 100644 --- a/packages/platform/types/bookings/2024-08-13/inputs/index.ts +++ b/packages/platform/types/bookings/2024-08-13/inputs/index.ts @@ -1,2 +1,3 @@ export * from "./create-booking.input"; export * from "./create-booking-input.pipe"; +export * from "./get-bookings.input"; From 0286d50508e50449cf0969f63ad944d07536b766 Mon Sep 17 00:00:00 2001 From: supalarry Date: Wed, 21 Aug 2024 16:54:50 +0200 Subject: [PATCH 13/96] feat: fetch by multiple status filters and sort --- apps/api/v2/package.json | 1 + .../controllers/bookings.controller.ts | 7 +- .../2024-08-13/services/bookings.service.ts | 110 ++++-------------- .../2024-08-13/services/input.service.ts | 13 +++ packages/lib/bookings/getAllUserBookings.ts | 62 +++++++--- packages/platform/libraries/package.json | 2 +- .../inputs/create-booking-input.pipe.ts | 6 + .../2024-08-13/inputs/get-bookings.input.ts | 28 +++-- .../routers/viewer/bookings/get.handler.ts | 3 +- .../routers/viewer/bookings/get.schema.ts | 2 +- 10 files changed, 111 insertions(+), 123 deletions(-) diff --git a/apps/api/v2/package.json b/apps/api/v2/package.json index 7ace0c34ee7fb1..d8f4868f06ee2c 100644 --- a/apps/api/v2/package.json +++ b/apps/api/v2/package.json @@ -27,6 +27,7 @@ "@calcom/platform-constants": "*", "@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.27", "@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2", + "@calcom/platform-libraries-1.2.3": "npm:@calcom/platform-libraries@1.2.3", "@calcom/platform-types": "*", "@calcom/platform-utils": "*", "@calcom/prisma": "*", diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.ts b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.ts index eca64af469d453..8fc07d94aad70c 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.ts @@ -3,11 +3,13 @@ import { GetBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/ge import { GetBookingsOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/get-bookings.output"; import { BookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/bookings.service"; import { VERSION_2024_08_13_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"; import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard"; import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard"; import { Controller, Post, Logger, Body, UseGuards, Req, Get, Param, Query } from "@nestjs/common"; import { ApiTags as DocsTags } from "@nestjs/swagger"; +import { User } from "@prisma/client"; import { Request } from "express"; import { BOOKING_READ, SUCCESS_STATUS } from "@calcom/platform-constants"; @@ -61,9 +63,10 @@ export class BookingsController_2024_08_13 { @UseGuards(ApiAuthGuard) @Permissions([BOOKING_READ]) async getBookings( - @Query() queryParams: GetBookingsInput_2024_08_13 + @Query() queryParams: GetBookingsInput_2024_08_13, + @GetUser() user: User ): Promise { - const bookings = await this.bookingsService.getBookings(queryParams); + const bookings = await this.bookingsService.getBookings(queryParams, user); return { status: SUCCESS_STATUS, diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts index 77a465aa9e64cb..d9802f32067827 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts @@ -2,18 +2,22 @@ import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings import { InputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/input.service"; import { OutputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/output.service"; import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository"; +import { PrismaReadService } from "@/modules/prisma/prisma-read.service"; import { Injectable } from "@nestjs/common"; import { Request } from "express"; -import { handleNewBooking, handleNewRecurringBooking } from "@calcom/platform-libraries"; +import { + handleNewBooking, + handleNewRecurringBooking, + getAllUserBookings, +} from "@calcom/platform-libraries-1.2.3"; import { CreateBookingInput_2024_08_13, RescheduleBookingInput_2024_08_13, CreateRecurringBookingInput_2024_08_13, GetBookingsInput_2024_08_13, } from "@calcom/platform-types"; -import { Prisma } from "@calcom/prisma/client"; -import { BookingStatus } from "@calcom/prisma/enums"; +import { PrismaClient } from "@calcom/prisma"; @Injectable() export class BookingsService_2024_08_13 { @@ -21,7 +25,8 @@ export class BookingsService_2024_08_13 { private readonly inputService: InputBookingsService_2024_08_13, private readonly outputService: OutputBookingsService_2024_08_13, private readonly bookingsRepository: BookingsRepository_2024_08_13, - private readonly eventTypesRepository: EventTypesRepository_2024_06_14 + private readonly eventTypesRepository: EventTypesRepository_2024_06_14, + private readonly prismaReadService: PrismaReadService ) {} async createBooking( @@ -97,87 +102,20 @@ export class BookingsService_2024_08_13 { return this.outputService.getOutputRecurringBookings(recurringBooking); } - async getBookings(queryParams: GetBookingsInput_2024_08_13) { - return []; - } - - async createGetBookingsWhere(queryParams: GetBookingsInput_2024_08_13): Promise { - const where: Prisma.BookingWhereInput = {}; - - if (queryParams.status) { - if (queryParams.status === "upcoming") { - where.startTime = { - gte: new Date(), - }; - } else if (queryParams.status === "past") { - where.startTime = { - lte: new Date(), - }; - } else if (queryParams.status.startsWith("!")) { - where.status = { - not: queryParams.status.substring(1) as BookingStatus, - }; - } else { - where.status = queryParams.status; - } - } - - if (queryParams.attendeeEmail) { - where.attendees = { - some: { - email: { - contains: queryParams.attendeeEmail, - mode: "insensitive", - }, - }, - }; - } - - if (queryParams.eventTypeIds) { - where.eventTypeId = { - in: queryParams.eventTypeIds, - }; - } - - if (queryParams.eventTypeId) { - where.eventTypeId = queryParams.eventTypeId; - } - - if (queryParams.teamsIds) { - where.destinationCalendar = { - teamId: { - in: queryParams.teamsIds, - }, - }; - } - - if (queryParams.teamId) { - where.destinationCalendar = { - teamId: queryParams.teamId, - }; - } - - if (queryParams.dateRange) { - const [fromDate, toDate] = queryParams.dateRange; - where.startTime = { - gte: new Date(fromDate), - lte: new Date(toDate), - }; - } else { - if (queryParams.fromDate) { - where.startTime = { - gte: new Date(queryParams.fromDate), - }; - } - - if (queryParams.toDate) { - where.startTime = { - ...where.startTime, - lte: new Date(queryParams.toDate), - }; - } - } - - return where; + async getBookings(queryParams: GetBookingsInput_2024_08_13, user: { email: string; id: number }) { + const bookings = await getAllUserBookings({ + bookingListingByStatus: queryParams.status, + skip: queryParams.cursor ?? 0, + take: queryParams.limit ?? 10, + // todo: add filters here like by eventtype id etc + filters: undefined, + ctx: { + user, + prisma: this.prismaReadService.prisma as unknown as PrismaClient, + }, + sort: this.inputService.transformGetBookingsSort(queryParams), + }); + + return bookings; } } diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts index a1e6a733255ff8..713527fec1e0c6 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts @@ -15,6 +15,7 @@ import { X_CAL_CLIENT_ID } from "@calcom/platform-constants"; import { CreateBookingInput_2024_08_13, CreateRecurringBookingInput_2024_08_13, + GetBookingsInput_2024_08_13, RescheduleBookingInput_2024_08_13, } from "@calcom/platform-types"; @@ -333,4 +334,16 @@ export class InputBookingsService_2024_08_13 { user: eventType.owner ? eventType.owner.username : eventType.team?.slug, }; } + + transformGetBookingsSort(queryParams: GetBookingsInput_2024_08_13) { + if (!queryParams.sortStart && !queryParams.sortEnd && !queryParams.sortCreated) { + return undefined; + } + + return { + sortStart: queryParams.sortStart, + sortEnd: queryParams.sortEnd, + sortCreated: queryParams.sortCreated, + }; + } } diff --git a/packages/lib/bookings/getAllUserBookings.ts b/packages/lib/bookings/getAllUserBookings.ts index 4aa1c70cfa4540..ae3eb8b35fdd36 100644 --- a/packages/lib/bookings/getAllUserBookings.ts +++ b/packages/lib/bookings/getAllUserBookings.ts @@ -4,26 +4,32 @@ import { BookingStatus } from "@calcom/prisma/enums"; import { getBookings } from "@calcom/trpc/server/routers/viewer/bookings/get.handler"; type InputByStatus = "upcoming" | "recurring" | "past" | "cancelled" | "unconfirmed"; +type SortOptions = { + sortStart?: "asc" | "desc"; + sortEnd?: "asc" | "desc"; + sortCreated?: "asc" | "desc"; +}; type GetOptions = { ctx: { user: { id: number; email: string }; prisma: PrismaClient; }; - bookingListingByStatus: InputByStatus; + bookingListingByStatus: InputByStatus[]; take: number; skip: number; filters: { - status: InputByStatus; + status?: InputByStatus; teamIds?: number[] | undefined; userIds?: number[] | undefined; eventTypeIds?: number[] | undefined; }; + sort?: SortOptions; }; -const getAllUserBookings = async ({ ctx, filters, bookingListingByStatus, take, skip }: GetOptions) => { +const getAllUserBookings = async ({ ctx, filters, bookingListingByStatus, take, skip, sort }: GetOptions) => { const { prisma, user } = ctx; - const bookingListingFilters: Record = { + const bookingListingFilters: Record = { upcoming: { endTime: { gte: new Date() }, // These changes are needed to not show confirmed recurring events, @@ -62,24 +68,17 @@ const getAllUserBookings = async ({ ctx, filters, bookingListingByStatus, take, status: { equals: BookingStatus.PENDING }, }, }; - const bookingListingOrderby: Record< - typeof bookingListingByStatus, - Prisma.BookingOrderByWithAggregationInput - > = { - upcoming: { startTime: "asc" }, - recurring: { startTime: "asc" }, - past: { startTime: "desc" }, - cancelled: { startTime: "desc" }, - unconfirmed: { startTime: "asc" }, - }; - const passedBookingsStatusFilter = bookingListingFilters[bookingListingByStatus]; - const orderBy = bookingListingOrderby[bookingListingByStatus]; + const orderBy = getOrderBy(bookingListingByStatus, sort); + + const combinedFilters = bookingListingByStatus.map((status) => bookingListingFilters[status]); const { bookings, recurringInfo } = await getBookings({ user, prisma, - passedBookingsStatusFilter, + passedBookingsStatusFilter: { + OR: combinedFilters, + }, filters: filters, orderBy, take, @@ -101,4 +100,33 @@ const getAllUserBookings = async ({ ctx, filters, bookingListingByStatus, take, }; }; +function getOrderBy( + bookingListingByStatus: InputByStatus[], + sort?: SortOptions +): Prisma.BookingOrderByWithAggregationInput { + const bookingListingOrderby: Record = { + upcoming: { startTime: "asc" }, + recurring: { startTime: "asc" }, + past: { startTime: "desc" }, + cancelled: { startTime: "desc" }, + unconfirmed: { startTime: "asc" }, + }; + + if (bookingListingByStatus.length === 1 && !sort) { + return bookingListingOrderby[bookingListingByStatus[0]]; + } + + if (sort?.sortStart) { + return { startTime: sort.sortStart }; + } + if (sort?.sortEnd) { + return { endTime: sort.sortEnd }; + } + if (sort?.sortCreated) { + return { createdAt: sort.sortCreated }; + } + + return { startTime: "desc" }; +} + export default getAllUserBookings; diff --git a/packages/platform/libraries/package.json b/packages/platform/libraries/package.json index 2df5d3d0370e2a..f640ed918904b9 100644 --- a/packages/platform/libraries/package.json +++ b/packages/platform/libraries/package.json @@ -1,6 +1,6 @@ { "name": "@calcom/platform-libraries", - "version": "0.0.0", + "version": "1.2.3", "main": "./dist/index.cjs", "module": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/packages/platform/types/bookings/2024-08-13/inputs/create-booking-input.pipe.ts b/packages/platform/types/bookings/2024-08-13/inputs/create-booking-input.pipe.ts index fca479db411611..d6d5091992284b 100644 --- a/packages/platform/types/bookings/2024-08-13/inputs/create-booking-input.pipe.ts +++ b/packages/platform/types/bookings/2024-08-13/inputs/create-booking-input.pipe.ts @@ -10,6 +10,12 @@ import { CreateBookingInput_2024_08_13 } from "./create-booking.input"; @Injectable() export class CreateBookingInputPipe implements PipeTransform { + // note(Lauris): we need empty constructor otherwise v2 can't be started due to error: + // CreateBookingInputPipe is not a constructor + + // eslint-disable-next-line @typescript-eslint/no-empty-function + constructor() {} + transform( value: | CreateBookingInput_2024_08_13 diff --git a/packages/platform/types/bookings/2024-08-13/inputs/get-bookings.input.ts b/packages/platform/types/bookings/2024-08-13/inputs/get-bookings.input.ts index a29ce6bc4c3027..63bfdad6e804a5 100644 --- a/packages/platform/types/bookings/2024-08-13/inputs/get-bookings.input.ts +++ b/packages/platform/types/bookings/2024-08-13/inputs/get-bookings.input.ts @@ -15,22 +15,11 @@ import { } from "class-validator"; enum Status { - accepted = "accepted", - cancelled = "cancelled", - rejected = "rejected", - pending = "pending", upcoming = "upcoming", recurring = "recurring", past = "past", + cancelled = "cancelled", unconfirmed = "unconfirmed", - "!accepted" = "!accepted", - "!cancelled" = "!cancelled", - "!rejected" = "!rejected", - "!pending" = "!pending", - "!upcoming" = "!upcoming", - "!recurring" = "!recurring", - "!past" = "!past", - "!unconfirmed" = "!unconfirmed", } type StatusType = keyof typeof Status; @@ -43,9 +32,18 @@ type SortOrderType = keyof typeof SortOrder; export class GetBookingsInput_2024_08_13 { // note(Lauris): filters @IsOptional() - @IsEnum(Status) - @IsOptional() - status?: StatusType; + @Transform(({ value }) => { + if (typeof value === "string") { + return value.split(",").map((status: string) => status.trim()); + } + return value; + }) + @ArrayNotEmpty({ message: "status cannot be empty." }) + @IsEnum(Status, { + each: true, + message: "Invalid status. Allowed are upcoming, recurring, past, cancelled, unconfirmed", + }) + status?: StatusType[]; @IsString() @IsOptional() diff --git a/packages/trpc/server/routers/viewer/bookings/get.handler.ts b/packages/trpc/server/routers/viewer/bookings/get.handler.ts index 998278d0c9a646..603e0da790ca67 100644 --- a/packages/trpc/server/routers/viewer/bookings/get.handler.ts +++ b/packages/trpc/server/routers/viewer/bookings/get.handler.ts @@ -23,7 +23,8 @@ export const getHandler = async ({ ctx, input }: GetOptions) => { const take = input.limit ?? 10; const skip = input.cursor ?? 0; const { prisma, user } = ctx; - const bookingListingByStatus = input.filters.status; + const defaultStatus = "upcoming"; + const bookingListingByStatus = [input.filters.status || defaultStatus]; const { bookings, recurringInfo, nextCursor } = await getAllUserBookings({ ctx: { user: { id: user.id, email: user.email }, prisma: prisma }, diff --git a/packages/trpc/server/routers/viewer/bookings/get.schema.ts b/packages/trpc/server/routers/viewer/bookings/get.schema.ts index c06284ac0282df..acbf34a9f03ad5 100644 --- a/packages/trpc/server/routers/viewer/bookings/get.schema.ts +++ b/packages/trpc/server/routers/viewer/bookings/get.schema.ts @@ -4,7 +4,7 @@ export const ZGetInputSchema = z.object({ filters: z.object({ teamIds: z.number().array().optional(), userIds: z.number().array().optional(), - status: z.enum(["upcoming", "recurring", "past", "cancelled", "unconfirmed"]), + status: z.enum(["upcoming", "recurring", "past", "cancelled", "unconfirmed"]).optional(), eventTypeIds: z.number().array().optional(), }), limit: z.number().min(1).max(100).nullish(), From 246af06d3cd61db6669f51c333de73cb3eeaf9ac Mon Sep 17 00:00:00 2001 From: supalarry Date: Wed, 21 Aug 2024 17:00:37 +0200 Subject: [PATCH 14/96] feat: fetch by teamId, teamIds, eventTypeId, eventTypeIds --- .../ee/bookings/2024-08-13/services/bookings.service.ts | 2 +- .../src/ee/bookings/2024-08-13/services/input.service.ts | 8 ++++++++ .../bookings/2024-08-13/inputs/get-bookings.input.ts | 2 ++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts index d9802f32067827..bec7a3ae235add 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts @@ -108,7 +108,7 @@ export class BookingsService_2024_08_13 { skip: queryParams.cursor ?? 0, take: queryParams.limit ?? 10, // todo: add filters here like by eventtype id etc - filters: undefined, + filters: this.inputService.transformGetBookingsFilters(queryParams), ctx: { user, prisma: this.prismaReadService.prisma as unknown as PrismaClient, diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts index 713527fec1e0c6..e7b1966a37ca56 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts @@ -335,6 +335,14 @@ export class InputBookingsService_2024_08_13 { }; } + transformGetBookingsFilters(queryParams: GetBookingsInput_2024_08_13) { + return { + teamIds: queryParams.teamsIds || (queryParams.teamId ? [queryParams.teamId] : undefined), + eventTypeIds: + queryParams.eventTypeIds || (queryParams.eventTypeId ? [queryParams.eventTypeId] : undefined), + }; + } + transformGetBookingsSort(queryParams: GetBookingsInput_2024_08_13) { if (!queryParams.sortStart && !queryParams.sortEnd && !queryParams.sortCreated) { return undefined; diff --git a/packages/platform/types/bookings/2024-08-13/inputs/get-bookings.input.ts b/packages/platform/types/bookings/2024-08-13/inputs/get-bookings.input.ts index 63bfdad6e804a5..a58510de549a81 100644 --- a/packages/platform/types/bookings/2024-08-13/inputs/get-bookings.input.ts +++ b/packages/platform/types/bookings/2024-08-13/inputs/get-bookings.input.ts @@ -52,6 +52,7 @@ export class GetBookingsInput_2024_08_13 { @IsArray() @Type(() => Number) @IsOptional() + @ArrayMinSize(1, { message: "eventTypeIds must contain at least 1 event type id" }) eventTypeIds?: number[]; @IsInt() @@ -61,6 +62,7 @@ export class GetBookingsInput_2024_08_13 { @IsArray() @Type(() => Number) @IsOptional() + @ArrayMinSize(1, { message: "teamIds must contain at least 1 team id" }) teamsIds?: number[]; @IsInt() From 3a3848ef9194a48f3d6377290d16f549bb5edadd Mon Sep 17 00:00:00 2001 From: supalarry Date: Wed, 21 Aug 2024 17:20:02 +0200 Subject: [PATCH 15/96] wip: filter by attendee email --- .../ee/bookings/2024-08-13/services/bookings.service.ts | 2 +- .../src/ee/bookings/2024-08-13/services/input.service.ts | 1 + packages/lib/bookings/getAllUserBookings.ts | 3 ++- .../trpc/server/routers/viewer/bookings/get.handler.ts | 9 +++++++++ .../trpc/server/routers/viewer/bookings/get.schema.ts | 1 + 5 files changed, 14 insertions(+), 2 deletions(-) diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts index bec7a3ae235add..bd27b2c6f2dfb1 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts @@ -104,7 +104,7 @@ export class BookingsService_2024_08_13 { async getBookings(queryParams: GetBookingsInput_2024_08_13, user: { email: string; id: number }) { const bookings = await getAllUserBookings({ - bookingListingByStatus: queryParams.status, + bookingListingByStatus: queryParams.status || [], skip: queryParams.cursor ?? 0, take: queryParams.limit ?? 10, // todo: add filters here like by eventtype id etc diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts index e7b1966a37ca56..e31e26ca7f02d8 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts @@ -337,6 +337,7 @@ export class InputBookingsService_2024_08_13 { transformGetBookingsFilters(queryParams: GetBookingsInput_2024_08_13) { return { + attendeeEmail: queryParams.attendeeEmail, teamIds: queryParams.teamsIds || (queryParams.teamId ? [queryParams.teamId] : undefined), eventTypeIds: queryParams.eventTypeIds || (queryParams.eventTypeId ? [queryParams.eventTypeId] : undefined), diff --git a/packages/lib/bookings/getAllUserBookings.ts b/packages/lib/bookings/getAllUserBookings.ts index ae3eb8b35fdd36..e7b8ac68439eaa 100644 --- a/packages/lib/bookings/getAllUserBookings.ts +++ b/packages/lib/bookings/getAllUserBookings.ts @@ -22,6 +22,7 @@ type GetOptions = { teamIds?: number[] | undefined; userIds?: number[] | undefined; eventTypeIds?: number[] | undefined; + attendeeEmail?: string; }; sort?: SortOptions; }; @@ -112,7 +113,7 @@ function getOrderBy( unconfirmed: { startTime: "asc" }, }; - if (bookingListingByStatus.length === 1 && !sort) { + if (bookingListingByStatus?.length === 1 && !sort) { return bookingListingOrderby[bookingListingByStatus[0]]; } diff --git a/packages/trpc/server/routers/viewer/bookings/get.handler.ts b/packages/trpc/server/routers/viewer/bookings/get.handler.ts index 603e0da790ca67..7b621ed4cf8ca4 100644 --- a/packages/trpc/server/routers/viewer/bookings/get.handler.ts +++ b/packages/trpc/server/routers/viewer/bookings/get.handler.ts @@ -157,6 +157,15 @@ export async function getBookings({ ], }, }; + if (filters?.attendeeEmail) { + bookingWhereInputFilters.attendeeEmail = { + attendees: { + some: { + email: filters.attendeeEmail, + }, + }, + }; + } const filtersCombined: Prisma.BookingWhereInput[] = !filters ? [] diff --git a/packages/trpc/server/routers/viewer/bookings/get.schema.ts b/packages/trpc/server/routers/viewer/bookings/get.schema.ts index acbf34a9f03ad5..a7d20ce7ec23f0 100644 --- a/packages/trpc/server/routers/viewer/bookings/get.schema.ts +++ b/packages/trpc/server/routers/viewer/bookings/get.schema.ts @@ -6,6 +6,7 @@ export const ZGetInputSchema = z.object({ userIds: z.number().array().optional(), status: z.enum(["upcoming", "recurring", "past", "cancelled", "unconfirmed"]).optional(), eventTypeIds: z.number().array().optional(), + attendeeEmail: z.string().optional(), }), limit: z.number().min(1).max(100).nullish(), cursor: z.number().nullish(), // <-- "cursor" needs to exist when using useInfiniteQuery, but can be any type From 5a26593fed7d8b396d9ee89a0e163d22b3e7fdea Mon Sep 17 00:00:00 2001 From: supalarry Date: Thu, 22 Aug 2024 11:54:45 +0200 Subject: [PATCH 16/96] feat: filter by attendee email --- .../controllers/bookings.controller.ts | 4 +- .../oauth-client-users.controller.ts | 1 - .../2024-08-13/inputs/get-bookings.input.ts | 2 + .../routers/viewer/bookings/get.handler.ts | 40 +++++++++++-------- yarn.lock | 25 ++++++------ 5 files changed, 41 insertions(+), 31 deletions(-) diff --git a/apps/api/v2/src/ee/bookings/2024-04-15/controllers/bookings.controller.ts b/apps/api/v2/src/ee/bookings/2024-04-15/controllers/bookings.controller.ts index 95e0d35b8c2b2c..94771f203c3907 100644 --- a/apps/api/v2/src/ee/bookings/2024-04-15/controllers/bookings.controller.ts +++ b/apps/api/v2/src/ee/bookings/2024-04-15/controllers/bookings.controller.ts @@ -46,7 +46,7 @@ import { getBookingInfo, handleCancelBooking, getBookingForReschedule, -} from "@calcom/platform-libraries"; +} from "@calcom/platform-libraries-1.2.3"; import { GetBookingsInput_2024_04_15, CancelBookingInput_2024_04_15, @@ -105,7 +105,7 @@ export class BookingsController_2024_04_15 { ): Promise { const { filters, cursor, limit } = queryParams; const bookings = await getAllUserBookings({ - bookingListingByStatus: filters.status, + bookingListingByStatus: [filters.status], skip: cursor ?? 0, take: limit ?? 10, filters, diff --git a/apps/api/v2/src/modules/oauth-clients/controllers/oauth-client-users/oauth-client-users.controller.ts b/apps/api/v2/src/modules/oauth-clients/controllers/oauth-client-users/oauth-client-users.controller.ts index b81124117b8a9b..b92e22df12f96b 100644 --- a/apps/api/v2/src/modules/oauth-clients/controllers/oauth-client-users/oauth-client-users.controller.ts +++ b/apps/api/v2/src/modules/oauth-clients/controllers/oauth-client-users/oauth-client-users.controller.ts @@ -79,7 +79,6 @@ export class OAuthClientUsersController { `Creating user with data: ${JSON.stringify(body, null, 2)} for OAuth Client with ID ${oAuthClientId}` ); const client = await this.oauthRepository.getOAuthClient(oAuthClientId); - console.log("asap createUser client", JSON.stringify(client, null, 2)); const isPlatformManaged = true; const { user, tokens } = await this.oAuthClientUsersService.createOauthClientUser( diff --git a/packages/platform/types/bookings/2024-08-13/inputs/get-bookings.input.ts b/packages/platform/types/bookings/2024-08-13/inputs/get-bookings.input.ts index a58510de549a81..1eb5776e8c6b91 100644 --- a/packages/platform/types/bookings/2024-08-13/inputs/get-bookings.input.ts +++ b/packages/platform/types/bookings/2024-08-13/inputs/get-bookings.input.ts @@ -57,6 +57,7 @@ export class GetBookingsInput_2024_08_13 { @IsInt() @IsOptional() + @Type(() => Number) eventTypeId?: number; @IsArray() @@ -67,6 +68,7 @@ export class GetBookingsInput_2024_08_13 { @IsInt() @IsOptional() + @Type(() => Number) teamId?: number; @IsOptional() diff --git a/packages/trpc/server/routers/viewer/bookings/get.handler.ts b/packages/trpc/server/routers/viewer/bookings/get.handler.ts index 7b621ed4cf8ca4..e86b4f833db425 100644 --- a/packages/trpc/server/routers/viewer/bookings/get.handler.ts +++ b/packages/trpc/server/routers/viewer/bookings/get.handler.ts @@ -69,9 +69,10 @@ export async function getBookings({ take: number; skip: number; }) { - // TODO: Fix record typing - const bookingWhereInputFilters: Record = { - teamIds: { + const bookingWhereInputFilters: Record = {}; + + if (filters?.teamIds && filters.teamIds.length > 0) { + bookingWhereInputFilters.teamIds = { AND: [ { OR: [ @@ -79,7 +80,7 @@ export async function getBookings({ eventType: { team: { id: { - in: filters?.teamIds, + in: filters.teamIds, }, }, }, @@ -89,7 +90,7 @@ export async function getBookings({ parent: { team: { id: { - in: filters?.teamIds, + in: filters.teamIds, }, }, }, @@ -98,8 +99,11 @@ export async function getBookings({ ], }, ], - }, - userIds: { + }; + } + + if (filters?.userIds && filters.userIds.length > 0) { + bookingWhereInputFilters.userIds = { AND: [ { OR: [ @@ -108,7 +112,7 @@ export async function getBookings({ hosts: { some: { userId: { - in: filters?.userIds, + in: filters.userIds, }, }, }, @@ -116,7 +120,7 @@ export async function getBookings({ }, { userId: { - in: filters?.userIds, + in: filters.userIds, }, }, { @@ -124,7 +128,7 @@ export async function getBookings({ users: { some: { id: { - in: filters?.userIds, + in: filters.userIds, }, }, }, @@ -133,21 +137,24 @@ export async function getBookings({ ], }, ], - }, - eventTypeIds: { + }; + } + + if (filters?.eventTypeIds && filters.eventTypeIds.length > 0) { + bookingWhereInputFilters.eventTypeIds = { AND: [ { OR: [ { eventTypeId: { - in: filters?.eventTypeIds, + in: filters.eventTypeIds, }, }, { eventType: { parent: { id: { - in: filters?.eventTypeIds, + in: filters.eventTypeIds, }, }, }, @@ -155,8 +162,9 @@ export async function getBookings({ ], }, ], - }, - }; + }; + } + if (filters?.attendeeEmail) { bookingWhereInputFilters.attendeeEmail = { attendees: { diff --git a/yarn.lock b/yarn.lock index f73f02b20cfb04..259c76734d4169 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4135,6 +4135,7 @@ __metadata: "@calcom/platform-constants": "*" "@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.27" "@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2" + "@calcom/platform-libraries-1.2.3": "npm:@calcom/platform-libraries@1.2.3" "@calcom/platform-types": "*" "@calcom/platform-utils": "*" "@calcom/prisma": "*" @@ -5106,18 +5107,7 @@ __metadata: languageName: node linkType: hard -"@calcom/platform-libraries@npm:@calcom/platform-libraries@0.0.27": - version: 0.0.27 - resolution: "@calcom/platform-libraries@npm:0.0.27" - dependencies: - "@calcom/core": "*" - "@calcom/features": "*" - "@calcom/lib": "*" - checksum: 516c347b3f71e45879925d31f067035a7c6857dc0f474a726dcb58a7949e1d6906e8e21b18a3a480052088ef742048029509aa74b6bde31b51a13fc6ea00a9f4 - languageName: node - linkType: hard - -"@calcom/platform-libraries@workspace:packages/platform/libraries": +"@calcom/platform-libraries-1.2.3@npm:@calcom/platform-libraries@1.2.3, @calcom/platform-libraries@workspace:packages/platform/libraries": version: 0.0.0-use.local resolution: "@calcom/platform-libraries@workspace:packages/platform/libraries" dependencies: @@ -5133,6 +5123,17 @@ __metadata: languageName: unknown linkType: soft +"@calcom/platform-libraries@npm:@calcom/platform-libraries@0.0.27": + version: 0.0.27 + resolution: "@calcom/platform-libraries@npm:0.0.27" + dependencies: + "@calcom/core": "*" + "@calcom/features": "*" + "@calcom/lib": "*" + checksum: 516c347b3f71e45879925d31f067035a7c6857dc0f474a726dcb58a7949e1d6906e8e21b18a3a480052088ef742048029509aa74b6bde31b51a13fc6ea00a9f4 + languageName: node + linkType: hard + "@calcom/platform-types@*, @calcom/platform-types@workspace:packages/platform/types": version: 0.0.0-use.local resolution: "@calcom/platform-types@workspace:packages/platform/types" From 729bde4f3133db315b8bd9ff7909d990cc6f0910 Mon Sep 17 00:00:00 2001 From: supalarry Date: Thu, 22 Aug 2024 12:15:19 +0200 Subject: [PATCH 17/96] feat: filter by attendee name --- .../ee/bookings/2024-08-13/services/input.service.ts | 1 + packages/lib/bookings/getAllUserBookings.ts | 1 + .../bookings/2024-08-13/inputs/get-bookings.input.ts | 4 ++++ .../server/routers/viewer/bookings/get.handler.ts | 12 +++++++++++- .../server/routers/viewer/bookings/get.schema.ts | 1 + 5 files changed, 18 insertions(+), 1 deletion(-) diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts index e31e26ca7f02d8..5dbd445887119c 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts @@ -338,6 +338,7 @@ export class InputBookingsService_2024_08_13 { transformGetBookingsFilters(queryParams: GetBookingsInput_2024_08_13) { return { attendeeEmail: queryParams.attendeeEmail, + attendeeName: queryParams.attendeeName, teamIds: queryParams.teamsIds || (queryParams.teamId ? [queryParams.teamId] : undefined), eventTypeIds: queryParams.eventTypeIds || (queryParams.eventTypeId ? [queryParams.eventTypeId] : undefined), diff --git a/packages/lib/bookings/getAllUserBookings.ts b/packages/lib/bookings/getAllUserBookings.ts index e7b8ac68439eaa..3c9751a527cb39 100644 --- a/packages/lib/bookings/getAllUserBookings.ts +++ b/packages/lib/bookings/getAllUserBookings.ts @@ -23,6 +23,7 @@ type GetOptions = { userIds?: number[] | undefined; eventTypeIds?: number[] | undefined; attendeeEmail?: string; + attendeeName?: string; }; sort?: SortOptions; }; diff --git a/packages/platform/types/bookings/2024-08-13/inputs/get-bookings.input.ts b/packages/platform/types/bookings/2024-08-13/inputs/get-bookings.input.ts index 1eb5776e8c6b91..576d4cff01f4ac 100644 --- a/packages/platform/types/bookings/2024-08-13/inputs/get-bookings.input.ts +++ b/packages/platform/types/bookings/2024-08-13/inputs/get-bookings.input.ts @@ -49,6 +49,10 @@ export class GetBookingsInput_2024_08_13 { @IsOptional() attendeeEmail?: string; + @IsString() + @IsOptional() + attendeeName?: string; + @IsArray() @Type(() => Number) @IsOptional() diff --git a/packages/trpc/server/routers/viewer/bookings/get.handler.ts b/packages/trpc/server/routers/viewer/bookings/get.handler.ts index e86b4f833db425..fb44d4712b4d69 100644 --- a/packages/trpc/server/routers/viewer/bookings/get.handler.ts +++ b/packages/trpc/server/routers/viewer/bookings/get.handler.ts @@ -169,7 +169,17 @@ export async function getBookings({ bookingWhereInputFilters.attendeeEmail = { attendees: { some: { - email: filters.attendeeEmail, + email: filters.attendeeEmail.trim(), + }, + }, + }; + } + + if (filters?.attendeeName) { + bookingWhereInputFilters.attendeeName = { + attendees: { + some: { + name: filters.attendeeName.trim(), }, }, }; diff --git a/packages/trpc/server/routers/viewer/bookings/get.schema.ts b/packages/trpc/server/routers/viewer/bookings/get.schema.ts index a7d20ce7ec23f0..17c175e5bca335 100644 --- a/packages/trpc/server/routers/viewer/bookings/get.schema.ts +++ b/packages/trpc/server/routers/viewer/bookings/get.schema.ts @@ -7,6 +7,7 @@ export const ZGetInputSchema = z.object({ status: z.enum(["upcoming", "recurring", "past", "cancelled", "unconfirmed"]).optional(), eventTypeIds: z.number().array().optional(), attendeeEmail: z.string().optional(), + attendeeName: z.string().optional(), }), limit: z.number().min(1).max(100).nullish(), cursor: z.number().nullish(), // <-- "cursor" needs to exist when using useInfiniteQuery, but can be any type From 6af7b96d554a3495e32f7c002e7c973abb616c41 Mon Sep 17 00:00:00 2001 From: supalarry Date: Thu, 22 Aug 2024 12:59:40 +0200 Subject: [PATCH 18/96] feat: date range filter --- .../2024-08-13/services/input.service.ts | 2 ++ .../2024-08-13/inputs/get-bookings.input.ts | 16 ++-------------- .../routers/viewer/bookings/get.handler.ts | 16 ++++++++++++++++ .../server/routers/viewer/bookings/get.schema.ts | 2 ++ 4 files changed, 22 insertions(+), 14 deletions(-) diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts index 5dbd445887119c..f2787be75f5312 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts @@ -339,6 +339,8 @@ export class InputBookingsService_2024_08_13 { return { attendeeEmail: queryParams.attendeeEmail, attendeeName: queryParams.attendeeName, + afterStartDate: queryParams.afterStart, + beforeEndDate: queryParams.beforeEnd, teamIds: queryParams.teamsIds || (queryParams.teamId ? [queryParams.teamId] : undefined), eventTypeIds: queryParams.eventTypeIds || (queryParams.eventTypeId ? [queryParams.eventTypeId] : undefined), diff --git a/packages/platform/types/bookings/2024-08-13/inputs/get-bookings.input.ts b/packages/platform/types/bookings/2024-08-13/inputs/get-bookings.input.ts index 576d4cff01f4ac..080e4be91b1339 100644 --- a/packages/platform/types/bookings/2024-08-13/inputs/get-bookings.input.ts +++ b/packages/platform/types/bookings/2024-08-13/inputs/get-bookings.input.ts @@ -1,6 +1,5 @@ import { Transform, Type } from "class-transformer"; import { - ArrayMaxSize, ArrayMinSize, ArrayNotEmpty, IsArray, @@ -75,24 +74,13 @@ export class GetBookingsInput_2024_08_13 { @Type(() => Number) teamId?: number; - @IsOptional() - @IsArray() - @ArrayMinSize(2, { message: "dateRange must contain 2 dates - from and to." }) - @ArrayMaxSize(2, { message: "dateRange must contain 2 dates - from and to." }) - @ArrayNotEmpty({ message: "dateRange cannot be empty." }) - @IsISO8601( - { strict: true }, - { each: true, message: "Each date in dateRange must be a valid ISO 8601 date." } - ) - dateRange?: [string, string]; - @IsOptional() @IsISO8601({ strict: true }, { message: "fromDate must be a valid ISO 8601 date." }) - fromDate?: string; + afterStart?: string; @IsOptional() @IsISO8601({ strict: true }, { message: "toDate must be a valid ISO 8601 date." }) - toDate?: string; + beforeEnd?: string; // note(Lauris): sort @IsOptional() diff --git a/packages/trpc/server/routers/viewer/bookings/get.handler.ts b/packages/trpc/server/routers/viewer/bookings/get.handler.ts index fb44d4712b4d69..d7b62270569592 100644 --- a/packages/trpc/server/routers/viewer/bookings/get.handler.ts +++ b/packages/trpc/server/routers/viewer/bookings/get.handler.ts @@ -185,6 +185,22 @@ export async function getBookings({ }; } + if (filters?.afterStartDate) { + bookingWhereInputFilters.afterStartDate = { + startTime: { + gte: new Date(filters.afterStartDate), + }, + }; + } + + if (filters?.beforeEndDate) { + bookingWhereInputFilters.beforeEndDate = { + endTime: { + lte: new Date(filters.beforeEndDate), + }, + }; + } + const filtersCombined: Prisma.BookingWhereInput[] = !filters ? [] : Object.keys(filters) diff --git a/packages/trpc/server/routers/viewer/bookings/get.schema.ts b/packages/trpc/server/routers/viewer/bookings/get.schema.ts index 17c175e5bca335..28e3472e83a6bf 100644 --- a/packages/trpc/server/routers/viewer/bookings/get.schema.ts +++ b/packages/trpc/server/routers/viewer/bookings/get.schema.ts @@ -8,6 +8,8 @@ export const ZGetInputSchema = z.object({ eventTypeIds: z.number().array().optional(), attendeeEmail: z.string().optional(), attendeeName: z.string().optional(), + afterStartDate: z.string().optional(), + beforeEndDate: z.string().optional(), }), limit: z.number().min(1).max(100).nullish(), cursor: z.number().nullish(), // <-- "cursor" needs to exist when using useInfiniteQuery, but can be any type From 329b6a5b46eb2f589f8d4a8485d890c18742a20b Mon Sep 17 00:00:00 2001 From: supalarry Date: Thu, 22 Aug 2024 13:57:25 +0200 Subject: [PATCH 19/96] chore: format get bookings output --- .../2024-08-13/services/bookings.service.ts | 23 +++++++++++++++++-- .../routers/viewer/bookings/get.handler.ts | 1 + 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts index bd27b2c6f2dfb1..ffeade9a55655d 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts @@ -18,6 +18,12 @@ import { GetBookingsInput_2024_08_13, } from "@calcom/platform-types"; import { PrismaClient } from "@calcom/prisma"; +import { Booking } from "@calcom/prisma/client"; + +type BookingWithAttendeesAndEventType = Booking & { + attendees: { name: string; email: string; timeZone: string; locale: string | null }[]; + eventType: { id: number }; +}; @Injectable() export class BookingsService_2024_08_13 { @@ -103,7 +109,7 @@ export class BookingsService_2024_08_13 { } async getBookings(queryParams: GetBookingsInput_2024_08_13, user: { email: string; id: number }) { - const bookings = await getAllUserBookings({ + const fetchedBookings: { bookings: BookingWithAttendeesAndEventType[] } = await getAllUserBookings({ bookingListingByStatus: queryParams.status || [], skip: queryParams.cursor ?? 0, take: queryParams.limit ?? 10, @@ -116,6 +122,19 @@ export class BookingsService_2024_08_13 { sort: this.inputService.transformGetBookingsSort(queryParams), }); - return bookings; + return fetchedBookings.bookings.map((booking) => { + const formatted = { + ...booking, + eventTypeId: booking.eventType.id, + startTime: new Date(booking.startTime), + endTime: new Date(booking.endTime), + }; + + const isRecurring = !!formatted.recurringEventId; + if (isRecurring) { + return this.outputService.getOutputRecurringBooking(formatted); + } + return this.outputService.getOutputBooking(formatted); + }); } } diff --git a/packages/trpc/server/routers/viewer/bookings/get.handler.ts b/packages/trpc/server/routers/viewer/bookings/get.handler.ts index d7b62270569592..8acfc6240db348 100644 --- a/packages/trpc/server/routers/viewer/bookings/get.handler.ts +++ b/packages/trpc/server/routers/viewer/bookings/get.handler.ts @@ -212,6 +212,7 @@ export async function getBookings({ uid: true, recurringEventId: true, location: true, + responses: true, eventType: { select: { slug: true, From 7a92f9af00553ffee22dad8dd71b4ae5742898a4 Mon Sep 17 00:00:00 2001 From: supalarry Date: Thu, 22 Aug 2024 15:00:40 +0200 Subject: [PATCH 20/96] chore: finish main merge --- .../ee/bookings/2024-08-13/bookings.module.ts | 3 +- apps/api/v2/swagger/documentation.json | 207 ++++++++++++++++++ 2 files changed, 209 insertions(+), 1 deletion(-) diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/bookings.module.ts b/apps/api/v2/src/ee/bookings/2024-08-13/bookings.module.ts index 1b5465646dccf8..47e8afc2c5ab94 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/bookings.module.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/bookings.module.ts @@ -11,10 +11,11 @@ import { PrismaModule } from "@/modules/prisma/prisma.module"; import { RedisModule } from "@/modules/redis/redis.module"; import { TokensModule } from "@/modules/tokens/tokens.module"; import { TokensRepository } from "@/modules/tokens/tokens.repository"; +import { UsersModule } from "@/modules/users/users.module"; import { Module } from "@nestjs/common"; @Module({ - imports: [PrismaModule, RedisModule, TokensModule, BillingModule], + imports: [PrismaModule, RedisModule, TokensModule, BillingModule, UsersModule], providers: [ TokensRepository, OAuthFlowService, diff --git a/apps/api/v2/swagger/documentation.json b/apps/api/v2/swagger/documentation.json index a7c0f711a75321..e4923d829bff8e 100644 --- a/apps/api/v2/swagger/documentation.json +++ b/apps/api/v2/swagger/documentation.json @@ -1219,6 +1219,16 @@ } } ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateScheduleInput_2024_06_11" + } + } + } + }, "responses": { "201": { "description": "", @@ -1321,6 +1331,16 @@ } } ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateScheduleInput_2024_06_11" + } + } + } + }, "responses": { "200": { "description": "", @@ -2360,6 +2380,16 @@ } } ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateScheduleInput_2024_04_15" + } + } + } + }, "responses": { "200": { "description": "", @@ -6028,6 +6058,57 @@ "data" ] }, + "CreateScheduleInput_2024_06_11": { + "type": "object", + "properties": { + "name": { + "type": "string", + "example": "One-on-one coaching" + }, + "timeZone": { + "type": "string", + "example": "Europe/Rome" + }, + "availability": { + "example": [ + { + "days": [ + "Monday", + "Tuesday" + ], + "startTime": "09:00", + "endTime": "10:00" + } + ], + "type": "array", + "items": { + "$ref": "#/components/schemas/ScheduleAvailabilityInput_2024_06_11" + } + }, + "isDefault": { + "type": "boolean", + "example": true + }, + "overrides": { + "example": [ + { + "date": "2024-05-20", + "startTime": "12:00", + "endTime": "14:00" + } + ], + "type": "array", + "items": { + "$ref": "#/components/schemas/ScheduleOverrideInput_2024_06_11" + } + } + }, + "required": [ + "name", + "timeZone", + "isDefault" + ] + }, "CreateScheduleOutput_2024_06_11": { "type": "object", "properties": { @@ -6076,6 +6157,52 @@ "data" ] }, + "UpdateScheduleInput_2024_06_11": { + "type": "object", + "properties": { + "name": { + "type": "string", + "example": "One-on-one coaching" + }, + "timeZone": { + "type": "string", + "example": "Europe/Rome" + }, + "availability": { + "example": [ + { + "days": [ + "Monday", + "Tuesday" + ], + "startTime": "09:00", + "endTime": "10:00" + } + ], + "type": "array", + "items": { + "$ref": "#/components/schemas/ScheduleAvailabilityInput_2024_06_11" + } + }, + "isDefault": { + "type": "boolean", + "example": true + }, + "overrides": { + "example": [ + { + "date": "2024-05-20", + "startTime": "12:00", + "endTime": "14:00" + } + ], + "type": "array", + "items": { + "$ref": "#/components/schemas/ScheduleOverrideInput_2024_06_11" + } + } + } + }, "UpdateScheduleOutput_2024_06_11": { "type": "object", "properties": { @@ -7210,6 +7337,26 @@ "userId" ] }, + "GetDefaultScheduleOutput_2024_06_11": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/ScheduleOutput_2024_06_11" + } + }, + "required": [ + "status", + "data" + ] + }, "CreateAvailabilityInput_2024_04_15": { "type": "object", "properties": { @@ -7507,6 +7654,66 @@ "data" ] }, + "UpdateScheduleInput_2024_04_15": { + "type": "object", + "properties": { + "timeZone": { + "type": "string" + }, + "name": { + "type": "string" + }, + "isDefault": { + "type": "boolean" + }, + "schedule": { + "example": [ + [], + [ + { + "start": "2022-01-01T00:00:00.000Z", + "end": "2022-01-02T00:00:00.000Z" + } + ], + [], + [], + [], + [], + [] + ], + "items": { + "type": "array" + }, + "type": "array" + }, + "dateOverrides": { + "example": [ + [], + [ + { + "start": "2022-01-01T00:00:00.000Z", + "end": "2022-01-02T00:00:00.000Z" + } + ], + [], + [], + [], + [], + [] + ], + "items": { + "type": "array" + }, + "type": "array" + } + }, + "required": [ + "timeZone", + "name", + "isDefault", + "schedule" + ] + }, "EventTypeModel_2024_04_15": { "type": "object", "properties": { From 145e07cf8f7c6646a1c3d7e1acc1af8a7eb5ff10 Mon Sep 17 00:00:00 2001 From: supalarry Date: Fri, 23 Aug 2024 10:31:20 +0200 Subject: [PATCH 21/96] feat: handle instant bookings --- .../controllers/bookings.controller.ts | 12 ++- .../2024-08-13/services/bookings.service.ts | 82 +++++++++++++------ .../2024-08-13/services/input.service.ts | 25 ++---- .../inputs/create-booking-input.pipe.ts | 31 +++++++ .../2024-08-13/inputs/create-booking.input.ts | 6 ++ 5 files changed, 106 insertions(+), 50 deletions(-) diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.ts b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.ts index 8fc07d94aad70c..8126151574f324 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.ts @@ -19,8 +19,15 @@ import { CreateRecurringBookingInput_2024_08_13, GetBookingsInput_2024_08_13, RescheduleBookingInput_2024_08_13, + CreateInstantBookingInput_2024_08_13, } from "@calcom/platform-types"; +export type CreateBookingInput = + | CreateBookingInput_2024_08_13 + | RescheduleBookingInput_2024_08_13 + | CreateRecurringBookingInput_2024_08_13 + | CreateInstantBookingInput_2024_08_13; + @Controller({ path: "/v2/bookings", version: VERSION_2024_08_13_VALUE, @@ -35,10 +42,7 @@ export class BookingsController_2024_08_13 { @Post("/") async createBooking( @Body(new CreateBookingInputPipe()) - body: - | CreateBookingInput_2024_08_13 - | RescheduleBookingInput_2024_08_13 - | CreateRecurringBookingInput_2024_08_13, + body: CreateBookingInput, @Req() request: Request ): Promise { const booking = await this.bookingsService.createBooking(request, body); diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts index ffeade9a55655d..3f3b176b76f41b 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts @@ -1,4 +1,5 @@ import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.repository"; +import { CreateBookingInput } from "@/ee/bookings/2024-08-13/controllers/bookings.controller"; import { InputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/input.service"; import { OutputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/output.service"; import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository"; @@ -10,12 +11,14 @@ import { handleNewBooking, handleNewRecurringBooking, getAllUserBookings, + handleInstantMeeting, } from "@calcom/platform-libraries-1.2.3"; import { CreateBookingInput_2024_08_13, RescheduleBookingInput_2024_08_13, CreateRecurringBookingInput_2024_08_13, GetBookingsInput_2024_08_13, + CreateInstantBookingInput_2024_08_13, } from "@calcom/platform-types"; import { PrismaClient } from "@calcom/prisma"; import { Booking } from "@calcom/prisma/client"; @@ -35,20 +38,50 @@ export class BookingsService_2024_08_13 { private readonly prismaReadService: PrismaReadService ) {} - async createBooking( - request: Request, - body: - | CreateBookingInput_2024_08_13 - | RescheduleBookingInput_2024_08_13 - | CreateRecurringBookingInput_2024_08_13 - ) { - const isRecurring = await this.isRecurring(body); + async createBooking(request: Request, body: CreateBookingInput) { + const bookingType = await this.getBookingType(body); + + switch (bookingType) { + case "regular": + return this.createRegularBookingOrReschedule(request, body); + case "reschedule": + return this.createRegularBookingOrReschedule(request, body); + case "recurring": + return this.createRecurringBooking(request, body as CreateRecurringBookingInput_2024_08_13); + case "instant": + return this.createInstantBooking(request, body as CreateInstantBookingInput_2024_08_13); + } + } + + async getBookingType( + body: CreateBookingInput + ): Promise<"regular" | "reschedule" | "recurring" | "instant"> { + if ("rescheduleBookingUid" in body) { + return "reschedule"; + } + if ("instant" in body && body.instant) { + return "instant"; + } + if (await this.isRecurring(body)) { + return "recurring"; + } + return "regular"; + } - if (!("rescheduleBookingUid" in body) && isRecurring) { - return this.createRecurringBooking(request, body); + async isRecurring(body: CreateBookingInput) { + if ("rescheduleBookingUid" in body) { + return false; } - const bookingRequest = await this.inputService.createBookingRequest(request, body); + const eventType = await this.eventTypesRepository.getEventTypeById(body.eventTypeId); + return !!eventType?.recurringEvent; + } + + async createRegularBookingOrReschedule( + request: Request, + body: CreateBookingInput_2024_08_13 | RescheduleBookingInput_2024_08_13 + ) { + const bookingRequest = await this.inputService.createNonRecurringBookingRequest(request, body); const booking = await handleNewBooking(bookingRequest); if (!booking.id) { throw new Error("Booking was not created"); @@ -62,21 +95,6 @@ export class BookingsService_2024_08_13 { return this.outputService.getOutputBooking(databaseBooking); } - async isRecurring( - body: - | CreateBookingInput_2024_08_13 - | RescheduleBookingInput_2024_08_13 - | CreateRecurringBookingInput_2024_08_13 - ) { - if ("rescheduleBookingUid" in body) { - return false; - } - - const eventType = await this.eventTypesRepository.getEventTypeById(body.eventTypeId); - - return !!eventType?.recurringEvent; - } - async createRecurringBooking(request: Request, body: CreateRecurringBookingInput_2024_08_13) { const bookingRequest = await this.inputService.createRecurringBookingRequest(request, body); const bookings = await handleNewRecurringBooking(bookingRequest); @@ -89,6 +107,18 @@ export class BookingsService_2024_08_13 { return this.outputService.getOutputRecurringBookings(recurringBooking); } + async createInstantBooking(request: Request, body: CreateInstantBookingInput_2024_08_13) { + const bookingRequest = await this.inputService.createNonRecurringBookingRequest(request, body); + const booking = await handleInstantMeeting(bookingRequest); + + const databaseBooking = await this.bookingsRepository.getByIdWithAttendees(booking.bookingId); + if (!databaseBooking) { + throw new Error(`Booking with id=${booking.bookingId} was not found in the database`); + } + + return this.outputService.getOutputBooking(databaseBooking); + } + async getBooking(uid: string) { const booking = await this.bookingsRepository.getByUidWithAttendees(uid); diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts index f2787be75f5312..4c12466be14f87 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts @@ -14,6 +14,7 @@ import { z } from "zod"; import { X_CAL_CLIENT_ID } from "@calcom/platform-constants"; import { CreateBookingInput_2024_08_13, + CreateInstantBookingInput_2024_08_13, CreateRecurringBookingInput_2024_08_13, GetBookingsInput_2024_08_13, RescheduleBookingInput_2024_08_13, @@ -68,14 +69,13 @@ export class InputBookingsService_2024_08_13 { private readonly bookingsRepository: BookingsRepository_2024_08_13 ) {} - async createBookingRequest( + async createNonRecurringBookingRequest( request: Request, body: | CreateBookingInput_2024_08_13 | RescheduleBookingInput_2024_08_13 - | CreateRecurringBookingInput_2024_08_13 + | CreateInstantBookingInput_2024_08_13 ): Promise { - // note(Lauris): update to this.transformInputCreate when rescheduling is implemented const bodyTransformed = await this.transformInputCreate(body); const oAuthClientId = request.get(X_CAL_CLIENT_ID); @@ -88,13 +88,7 @@ export class InputBookingsService_2024_08_13 { const location = await this.getLocation(request, body); Object.assign(newRequest, { userId, ...oAuthParams, platformBookingLocation: location }); - const notRecurring = !("recurringEventTypeId" in body); - newRequest.body = notRecurring - ? { ...bodyTransformed, noEmail: !oAuthParams.arePlatformEmailsEnabled } - : (bodyTransformed as any[]).map((event) => ({ - ...event, - noEmail: !oAuthParams.arePlatformEmailsEnabled, - })); + newRequest.body = { ...bodyTransformed, noEmail: !oAuthParams.arePlatformEmailsEnabled }; return newRequest as unknown as BookingRequest; } @@ -153,20 +147,11 @@ export class InputBookingsService_2024_08_13 { } } - transformInputCreate( - inputBooking: - | CreateBookingInput_2024_08_13 - | RescheduleBookingInput_2024_08_13 - | CreateRecurringBookingInput_2024_08_13 - ) { + transformInputCreate(inputBooking: CreateBookingInput_2024_08_13 | RescheduleBookingInput_2024_08_13) { if ("rescheduleBookingUid" in inputBooking) { return this.transformInputRescheduleBooking(inputBooking); } - if ("recurringEventTypeId" in inputBooking) { - return this.transformInputCreateRecurringBooking(inputBooking); - } - return this.transformInputCreateBooking(inputBooking); } diff --git a/packages/platform/types/bookings/2024-08-13/inputs/create-booking-input.pipe.ts b/packages/platform/types/bookings/2024-08-13/inputs/create-booking-input.pipe.ts index d6d5091992284b..e2e180836eb5c9 100644 --- a/packages/platform/types/bookings/2024-08-13/inputs/create-booking-input.pipe.ts +++ b/packages/platform/types/bookings/2024-08-13/inputs/create-booking-input.pipe.ts @@ -7,6 +7,7 @@ import { validateSync } from "class-validator"; import { CreateRecurringBookingInput_2024_08_13 } from "./create-booking.input"; import { RescheduleBookingInput_2024_08_13 } from "./create-booking.input"; import { CreateBookingInput_2024_08_13 } from "./create-booking.input"; +import { CreateInstantBookingInput_2024_08_13 } from "./create-booking.input"; @Injectable() export class CreateBookingInputPipe implements PipeTransform { @@ -21,6 +22,7 @@ export class CreateBookingInputPipe implements PipeTransform { | CreateBookingInput_2024_08_13 | RescheduleBookingInput_2024_08_13 | CreateRecurringBookingInput_2024_08_13 + | CreateInstantBookingInput_2024_08_13 ) { if (!value) { throw new BadRequestException("Body is required"); @@ -37,6 +39,10 @@ export class CreateBookingInputPipe implements PipeTransform { return this.validateRecurringBooking(value); } + if (this.isInstantBookingInput(value)) { + return this.validateInstantBooking(value); + } + return this.validateBooking(value); } @@ -88,6 +94,22 @@ export class CreateBookingInputPipe implements PipeTransform { return object; } + validateInstantBooking(value: CreateInstantBookingInput_2024_08_13) { + const object = plainToClass(CreateInstantBookingInput_2024_08_13, value); + + const errors = validateSync(object, { + whitelist: true, + forbidNonWhitelisted: true, + skipMissingProperties: false, + }); + + if (errors.length > 0) { + throw new BadRequestException(this.formatErrors(errors)); + } + + return object; + } + private formatErrors(errors: ValidationError[]): string { return errors .map((err) => { @@ -116,4 +138,13 @@ export class CreateBookingInputPipe implements PipeTransform { ): value is CreateRecurringBookingInput_2024_08_13 { return value.hasOwnProperty("recurringEventTypeId"); } + + private isInstantBookingInput( + value: + | CreateBookingInput_2024_08_13 + | RescheduleBookingInput_2024_08_13 + | CreateRecurringBookingInput_2024_08_13 + ): value is CreateRecurringBookingInput_2024_08_13 { + return value.hasOwnProperty("instant") && "instant" in value && value.instant === true; + } } diff --git a/packages/platform/types/bookings/2024-08-13/inputs/create-booking.input.ts b/packages/platform/types/bookings/2024-08-13/inputs/create-booking.input.ts index a98a0e2ad21e28..438efc83392d2a 100644 --- a/packages/platform/types/bookings/2024-08-13/inputs/create-booking.input.ts +++ b/packages/platform/types/bookings/2024-08-13/inputs/create-booking.input.ts @@ -11,6 +11,7 @@ import { IsOptional, IsUrl, IsObject, + IsBoolean, } from "class-validator"; import type { BookingLanguageType } from "./language"; @@ -60,6 +61,11 @@ export class CreateBookingInput_2024_08_13 { bookingFieldsResponses!: Record; } +export class CreateInstantBookingInput_2024_08_13 extends CreateBookingInput_2024_08_13 { + @IsBoolean() + instant!: boolean; +} + export class CreateRecurringBookingInput_2024_08_13 { @IsDateString() start!: string; From d0718295639856898c203680b607cf96ecc0d402 Mon Sep 17 00:00:00 2001 From: supalarry Date: Fri, 23 Aug 2024 11:42:14 +0200 Subject: [PATCH 22/96] refactor: separate reschedule endpoint --- .../controllers/bookings.controller.ts | 54 ++++- .../2024-08-13/services/bookings.service.ts | 86 ++++---- .../2024-08-13/services/input.service.ts | 195 +++++++++--------- apps/api/v2/swagger/documentation.json | 42 ++++ .../inputs/create-booking-input.pipe.ts | 55 +---- .../2024-08-13/inputs/create-booking.input.ts | 8 - .../types/bookings/2024-08-13/inputs/index.ts | 1 + .../inputs/reschedule-booking.input.ts | 6 + 8 files changed, 236 insertions(+), 211 deletions(-) create mode 100644 packages/platform/types/bookings/2024-08-13/inputs/reschedule-booking.input.ts diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.ts b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.ts index 8126151574f324..086bc240ae4c51 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.ts @@ -7,27 +7,30 @@ import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator"; import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator"; import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard"; import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard"; -import { Controller, Post, Logger, Body, UseGuards, Req, Get, Param, Query } from "@nestjs/common"; +import { + Controller, + Post, + Logger, + Body, + UseGuards, + Req, + Get, + Param, + Query, + BadRequestException, +} from "@nestjs/common"; import { ApiTags as DocsTags } from "@nestjs/swagger"; import { User } from "@prisma/client"; import { Request } from "express"; import { BOOKING_READ, SUCCESS_STATUS } from "@calcom/platform-constants"; import { - CreateBookingInput_2024_08_13, CreateBookingInputPipe, - CreateRecurringBookingInput_2024_08_13, + CreateBookingInput, GetBookingsInput_2024_08_13, RescheduleBookingInput_2024_08_13, - CreateInstantBookingInput_2024_08_13, } from "@calcom/platform-types"; -export type CreateBookingInput = - | CreateBookingInput_2024_08_13 - | RescheduleBookingInput_2024_08_13 - | CreateRecurringBookingInput_2024_08_13 - | CreateInstantBookingInput_2024_08_13; - @Controller({ path: "/v2/bookings", version: VERSION_2024_08_13_VALUE, @@ -55,6 +58,10 @@ export class BookingsController_2024_08_13 { @Get("/:bookingUid") async getBooking(@Param("bookingUid") bookingUid: string): Promise { + if (!bookingUid) { + throw new BadRequestException("Booking UID is required in request path /:bookingUid"); + } + const booking = await this.bookingsService.getBooking(bookingUid); return { @@ -77,4 +84,31 @@ export class BookingsController_2024_08_13 { data: bookings, }; } + + @Post("/:bookingUid/reschedule") + async rescheduleBooking( + @Param("bookingUid") bookingUid: string, + @Body() body: RescheduleBookingInput_2024_08_13, + @Req() request: Request + ): Promise { + if (!bookingUid) { + throw new BadRequestException("Booking UID is required in request path /:bookingUid/reschedule"); + } + + const newBooking = await this.bookingsService.rescheduleBooking(request, bookingUid, body); + + return { + status: SUCCESS_STATUS, + data: newBooking, + }; + } + + // @Post("/:bookingId/cancel") + // async cancelBooking( + // @Req() request: Request, + // @Param("bookingId") bookingId: string, + // @Body() body: CancelBookingInput_2024_04_15, + // ): Promise<> { + + // } } diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts index 3f3b176b76f41b..4c8679b160822a 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts @@ -1,5 +1,4 @@ import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.repository"; -import { CreateBookingInput } from "@/ee/bookings/2024-08-13/controllers/bookings.controller"; import { InputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/input.service"; import { OutputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/output.service"; import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository"; @@ -16,6 +15,7 @@ import { import { CreateBookingInput_2024_08_13, RescheduleBookingInput_2024_08_13, + CreateBookingInput, CreateRecurringBookingInput_2024_08_13, GetBookingsInput_2024_08_13, CreateInstantBookingInput_2024_08_13, @@ -39,62 +39,34 @@ export class BookingsService_2024_08_13 { ) {} async createBooking(request: Request, body: CreateBookingInput) { - const bookingType = await this.getBookingType(body); - - switch (bookingType) { - case "regular": - return this.createRegularBookingOrReschedule(request, body); - case "reschedule": - return this.createRegularBookingOrReschedule(request, body); - case "recurring": - return this.createRecurringBooking(request, body as CreateRecurringBookingInput_2024_08_13); - case "instant": - return this.createInstantBooking(request, body as CreateInstantBookingInput_2024_08_13); - } - } - - async getBookingType( - body: CreateBookingInput - ): Promise<"regular" | "reschedule" | "recurring" | "instant"> { - if ("rescheduleBookingUid" in body) { - return "reschedule"; - } if ("instant" in body && body.instant) { - return "instant"; + return this.createInstantBooking(request, body); } - if (await this.isRecurring(body)) { - return "recurring"; - } - return "regular"; - } - async isRecurring(body: CreateBookingInput) { - if ("rescheduleBookingUid" in body) { - return false; + if (await this.isRecurring(body)) { + return this.createRecurringBooking(request, body); } - const eventType = await this.eventTypesRepository.getEventTypeById(body.eventTypeId); - return !!eventType?.recurringEvent; + return this.createRegularBooking(request, body); } - async createRegularBookingOrReschedule( - request: Request, - body: CreateBookingInput_2024_08_13 | RescheduleBookingInput_2024_08_13 - ) { - const bookingRequest = await this.inputService.createNonRecurringBookingRequest(request, body); - const booking = await handleNewBooking(bookingRequest); - if (!booking.id) { - throw new Error("Booking was not created"); - } + async createInstantBooking(request: Request, body: CreateInstantBookingInput_2024_08_13) { + const bookingRequest = await this.inputService.createBookingRequest(request, body); + const booking = await handleInstantMeeting(bookingRequest); - const databaseBooking = await this.bookingsRepository.getByIdWithAttendees(booking.id); + const databaseBooking = await this.bookingsRepository.getByIdWithAttendees(booking.bookingId); if (!databaseBooking) { - throw new Error(`Booking with id=${booking.id} was not found in the database`); + throw new Error(`Booking with id=${booking.bookingId} was not found in the database`); } return this.outputService.getOutputBooking(databaseBooking); } + async isRecurring(body: CreateBookingInput) { + const eventType = await this.eventTypesRepository.getEventTypeById(body.eventTypeId); + return !!eventType?.recurringEvent; + } + async createRecurringBooking(request: Request, body: CreateRecurringBookingInput_2024_08_13) { const bookingRequest = await this.inputService.createRecurringBookingRequest(request, body); const bookings = await handleNewRecurringBooking(bookingRequest); @@ -107,13 +79,16 @@ export class BookingsService_2024_08_13 { return this.outputService.getOutputRecurringBookings(recurringBooking); } - async createInstantBooking(request: Request, body: CreateInstantBookingInput_2024_08_13) { - const bookingRequest = await this.inputService.createNonRecurringBookingRequest(request, body); - const booking = await handleInstantMeeting(bookingRequest); + async createRegularBooking(request: Request, body: CreateBookingInput_2024_08_13) { + const bookingRequest = await this.inputService.createBookingRequest(request, body); + const booking = await handleNewBooking(bookingRequest); + if (!booking.id) { + throw new Error("Booking was not created"); + } - const databaseBooking = await this.bookingsRepository.getByIdWithAttendees(booking.bookingId); + const databaseBooking = await this.bookingsRepository.getByIdWithAttendees(booking.id); if (!databaseBooking) { - throw new Error(`Booking with id=${booking.bookingId} was not found in the database`); + throw new Error(`Booking with id=${booking.id} was not found in the database`); } return this.outputService.getOutputBooking(databaseBooking); @@ -167,4 +142,19 @@ export class BookingsService_2024_08_13 { return this.outputService.getOutputBooking(formatted); }); } + + async rescheduleBooking(request: Request, bookingUid: string, body: RescheduleBookingInput_2024_08_13) { + const bookingRequest = await this.inputService.createRescheduleBookingRequest(request, bookingUid, body); + const booking = await handleNewBooking(bookingRequest); + if (!booking.id) { + throw new Error("Booking was not created"); + } + + const databaseBooking = await this.bookingsRepository.getByIdWithAttendees(booking.id); + if (!databaseBooking) { + throw new Error(`Booking with id=${booking.id} was not found in the database`); + } + + return this.outputService.getOutputBooking(databaseBooking); + } } diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts index 4c12466be14f87..059991de090d7f 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts @@ -69,14 +69,11 @@ export class InputBookingsService_2024_08_13 { private readonly bookingsRepository: BookingsRepository_2024_08_13 ) {} - async createNonRecurringBookingRequest( + async createBookingRequest( request: Request, - body: - | CreateBookingInput_2024_08_13 - | RescheduleBookingInput_2024_08_13 - | CreateInstantBookingInput_2024_08_13 + body: CreateBookingInput_2024_08_13 | CreateInstantBookingInput_2024_08_13 ): Promise { - const bodyTransformed = await this.transformInputCreate(body); + const bodyTransformed = await this.transformInputCreateBooking(body); const oAuthClientId = request.get(X_CAL_CLIENT_ID); const newRequest = { ...request }; @@ -85,7 +82,7 @@ export class InputBookingsService_2024_08_13 { ? await this.createBookingRequestOAuthClientParams(oAuthClientId) : DEFAULT_PLATFORM_PARAMS; - const location = await this.getLocation(request, body); + const location = request.body.meetingUrl; Object.assign(newRequest, { userId, ...oAuthParams, platformBookingLocation: location }); newRequest.body = { ...bodyTransformed, noEmail: !oAuthParams.arePlatformEmailsEnabled }; @@ -93,6 +90,40 @@ export class InputBookingsService_2024_08_13 { return newRequest as unknown as BookingRequest; } + async transformInputCreateBooking(inputBooking: CreateBookingInput_2024_08_13) { + const eventType = await this.eventTypesRepository.getEventTypeByIdWithOwnerAndTeam( + inputBooking.eventTypeId + ); + if (!eventType) { + throw new NotFoundException(`Event type with id=${inputBooking.eventTypeId} not found`); + } + + const startTime = DateTime.fromISO(inputBooking.start, { zone: "utc" }).setZone( + inputBooking.attendee.timeZone + ); + const endTime = startTime.plus({ minutes: eventType.length }); + + return { + start: startTime.toISO(), + end: endTime.toISO(), + eventTypeId: inputBooking.eventTypeId, + eventTypeSlug: eventType.slug, + timeZone: inputBooking.attendee.timeZone, + language: inputBooking.attendee.language || "en", + metadata: inputBooking.metadata || {}, + hasHashedBookingLink: false, + guests: inputBooking.guests, + responses: inputBooking.bookingFieldsResponses + ? { + ...inputBooking.bookingFieldsResponses, + name: inputBooking.attendee.name, + email: inputBooking.attendee.email, + } + : { name: inputBooking.attendee.name, email: inputBooking.attendee.email }, + user: eventType.owner ? eventType.owner.username : eventType.team?.slug, + }; + } + async createRecurringBookingRequest( request: Request, body: CreateRecurringBookingInput_2024_08_13 @@ -107,7 +138,7 @@ export class InputBookingsService_2024_08_13 { ? await this.createBookingRequestOAuthClientParams(oAuthClientId) : DEFAULT_PLATFORM_PARAMS; - const location = await this.getLocation(request, body); + const location = request.body.meetingUrl; Object.assign(newRequest, { userId, ...oAuthParams, platformBookingLocation: location }); newRequest.body = (bodyTransformed as any[]).map((event) => ({ @@ -118,43 +149,6 @@ export class InputBookingsService_2024_08_13 { return newRequest as unknown as BookingRequest; } - private async createBookingRequestOwnerId(req: Request): Promise { - try { - const accessToken = req.get("Authorization")?.replace("Bearer ", ""); - if (accessToken) { - return this.oAuthFlowService.getOwnerId(accessToken); - } - } catch (err) { - this.logger.error(err); - } - } - - private async createBookingRequestOAuthClientParams(clientId: string) { - const params = DEFAULT_PLATFORM_PARAMS; - try { - const client = await this.oAuthClientRepository.getOAuthClient(clientId); - if (client) { - params.platformClientId = clientId; - params.platformCancelUrl = client.bookingCancelRedirectUri ?? ""; - params.platformRescheduleUrl = client.bookingRescheduleRedirectUri ?? ""; - params.platformBookingUrl = client.bookingRedirectUri ?? ""; - params.arePlatformEmailsEnabled = client.areEmailsEnabled ?? false; - } - return params; - } catch (err) { - this.logger.error(err); - return params; - } - } - - transformInputCreate(inputBooking: CreateBookingInput_2024_08_13 | RescheduleBookingInput_2024_08_13) { - if ("rescheduleBookingUid" in inputBooking) { - return this.transformInputRescheduleBooking(inputBooking); - } - - return this.transformInputCreateBooking(inputBooking); - } - async transformInputCreateRecurringBooking(inputBooking: CreateRecurringBookingInput_2024_08_13) { const eventType = await this.eventTypesRepository.getEventTypeByIdWithOwnerAndTeam( inputBooking.eventTypeId @@ -226,67 +220,35 @@ export class InputBookingsService_2024_08_13 { return events; } - async transformInputCreateBooking(inputBooking: CreateBookingInput_2024_08_13) { - const eventType = await this.eventTypesRepository.getEventTypeByIdWithOwnerAndTeam( - inputBooking.eventTypeId - ); - if (!eventType) { - throw new NotFoundException(`Event type with id=${inputBooking.eventTypeId} not found`); - } + async createRescheduleBookingRequest( + request: Request, + bookingUid: string, + body: RescheduleBookingInput_2024_08_13 + ): Promise { + const bodyTransformed = await this.transformInputRescheduleBooking(bookingUid, body); + const oAuthClientId = request.get(X_CAL_CLIENT_ID); - const startTime = DateTime.fromISO(inputBooking.start, { zone: "utc" }).setZone( - inputBooking.attendee.timeZone - ); - const endTime = startTime.plus({ minutes: eventType.length }); + const newRequest = { ...request }; + const userId = (await this.createBookingRequestOwnerId(request)) ?? undefined; + const oAuthParams = oAuthClientId + ? await this.createBookingRequestOAuthClientParams(oAuthClientId) + : DEFAULT_PLATFORM_PARAMS; - return { - start: startTime.toISO(), - end: endTime.toISO(), - eventTypeId: inputBooking.eventTypeId, - eventTypeSlug: eventType.slug, - timeZone: inputBooking.attendee.timeZone, - language: inputBooking.attendee.language || "en", - metadata: inputBooking.metadata || {}, - hasHashedBookingLink: false, - guests: inputBooking.guests, - responses: inputBooking.bookingFieldsResponses - ? { - ...inputBooking.bookingFieldsResponses, - name: inputBooking.attendee.name, - email: inputBooking.attendee.email, - } - : { name: inputBooking.attendee.name, email: inputBooking.attendee.email }, - user: eventType.owner ? eventType.owner.username : eventType.team?.slug, - }; - } + const location = await this.getRescheduleBookingLocation(bookingUid); + Object.assign(newRequest, { userId, ...oAuthParams, platformBookingLocation: location }); - async getLocation( - request: Request, - body: - | CreateBookingInput_2024_08_13 - | RescheduleBookingInput_2024_08_13 - | CreateRecurringBookingInput_2024_08_13 - ) { - if ("rescheduleBookingUid" in body) { - const booking = await this.bookingsRepository.getByUid(body.rescheduleBookingUid); - if (!booking) { - throw new NotFoundException(`Booking with uid=${body.rescheduleBookingUid} not found`); - } - return booking.location; - } + newRequest.body = { ...bodyTransformed, noEmail: !oAuthParams.arePlatformEmailsEnabled }; - return request.body.meetingUrl; + return newRequest as unknown as BookingRequest; } - async transformInputRescheduleBooking(inputBooking: RescheduleBookingInput_2024_08_13) { - const booking = await this.bookingsRepository.getByUidWithAttendees(inputBooking.rescheduleBookingUid); + async transformInputRescheduleBooking(bookingUid: string, inputBooking: RescheduleBookingInput_2024_08_13) { + const booking = await this.bookingsRepository.getByUidWithAttendees(bookingUid); if (!booking) { - throw new NotFoundException(`Booking with uid=${inputBooking.rescheduleBookingUid} not found`); + throw new NotFoundException(`Booking with uid=${bookingUid} not found`); } if (!booking.eventTypeId) { - throw new NotFoundException( - `Booking with uid=${inputBooking.rescheduleBookingUid} is missing event type` - ); + throw new NotFoundException(`Booking with uid=${bookingUid} is missing event type`); } const eventType = await this.eventTypesRepository.getEventTypeByIdWithOwnerAndTeam(booking.eventTypeId); if (!eventType) { @@ -298,7 +260,7 @@ export class InputBookingsService_2024_08_13 { if (!attendee) { throw new NotFoundException( - `Attendee with e-mail ${bookingResponses.email} for booking with uid=${inputBooking.rescheduleBookingUid} not found` + `Attendee with e-mail ${bookingResponses.email} for booking with uid=${bookingUid} not found` ); } @@ -320,6 +282,43 @@ export class InputBookingsService_2024_08_13 { }; } + async getRescheduleBookingLocation(rescheduleBookingUid: string) { + const booking = await this.bookingsRepository.getByUid(rescheduleBookingUid); + if (!booking) { + throw new NotFoundException(`Booking with uid=${rescheduleBookingUid} not found`); + } + return booking.location; + } + + private async createBookingRequestOwnerId(req: Request): Promise { + try { + const accessToken = req.get("Authorization")?.replace("Bearer ", ""); + if (accessToken) { + return this.oAuthFlowService.getOwnerId(accessToken); + } + } catch (err) { + this.logger.error(err); + } + } + + private async createBookingRequestOAuthClientParams(clientId: string) { + const params = DEFAULT_PLATFORM_PARAMS; + try { + const client = await this.oAuthClientRepository.getOAuthClient(clientId); + if (client) { + params.platformClientId = clientId; + params.platformCancelUrl = client.bookingCancelRedirectUri ?? ""; + params.platformRescheduleUrl = client.bookingRescheduleRedirectUri ?? ""; + params.platformBookingUrl = client.bookingRedirectUri ?? ""; + params.arePlatformEmailsEnabled = client.areEmailsEnabled ?? false; + } + return params; + } catch (err) { + this.logger.error(err); + return params; + } + } + transformGetBookingsFilters(queryParams: GetBookingsInput_2024_08_13) { return { attendeeEmail: queryParams.attendeeEmail, diff --git a/apps/api/v2/swagger/documentation.json b/apps/api/v2/swagger/documentation.json index e4923d829bff8e..7c3eb590c0bb07 100644 --- a/apps/api/v2/swagger/documentation.json +++ b/apps/api/v2/swagger/documentation.json @@ -2944,6 +2944,44 @@ "tags": [ "Bookings" ] + }, + "post": { + "operationId": "BookingsController_2024_08_13_rescheduleBooking", + "parameters": [ + { + "name": "bookingUid", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RescheduleBookingInput_2024_08_13" + } + } + } + }, + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateBookingOutput_2024_08_13" + } + } + } + } + }, + "tags": [ + "Bookings" + ] } }, "/v2/bookings/{bookingId}/cancel": { @@ -9094,6 +9132,10 @@ "data" ] }, + "RescheduleBookingInput_2024_08_13": { + "type": "object", + "properties": {} + }, "ReserveSlotInput": { "type": "object", "properties": {} diff --git a/packages/platform/types/bookings/2024-08-13/inputs/create-booking-input.pipe.ts b/packages/platform/types/bookings/2024-08-13/inputs/create-booking-input.pipe.ts index e2e180836eb5c9..891e53b1bb4903 100644 --- a/packages/platform/types/bookings/2024-08-13/inputs/create-booking-input.pipe.ts +++ b/packages/platform/types/bookings/2024-08-13/inputs/create-booking-input.pipe.ts @@ -5,10 +5,14 @@ import type { ValidationError } from "class-validator"; import { validateSync } from "class-validator"; import { CreateRecurringBookingInput_2024_08_13 } from "./create-booking.input"; -import { RescheduleBookingInput_2024_08_13 } from "./create-booking.input"; import { CreateBookingInput_2024_08_13 } from "./create-booking.input"; import { CreateInstantBookingInput_2024_08_13 } from "./create-booking.input"; +export type CreateBookingInput = + | CreateBookingInput_2024_08_13 + | CreateRecurringBookingInput_2024_08_13 + | CreateInstantBookingInput_2024_08_13; + @Injectable() export class CreateBookingInputPipe implements PipeTransform { // note(Lauris): we need empty constructor otherwise v2 can't be started due to error: @@ -17,13 +21,7 @@ export class CreateBookingInputPipe implements PipeTransform { // eslint-disable-next-line @typescript-eslint/no-empty-function constructor() {} - transform( - value: - | CreateBookingInput_2024_08_13 - | RescheduleBookingInput_2024_08_13 - | CreateRecurringBookingInput_2024_08_13 - | CreateInstantBookingInput_2024_08_13 - ) { + transform(value: CreateBookingInput): CreateBookingInput { if (!value) { throw new BadRequestException("Body is required"); } @@ -31,10 +29,6 @@ export class CreateBookingInputPipe implements PipeTransform { throw new BadRequestException("Body should be an object"); } - if (this.isRescheduleBookingInput(value)) { - return this.validateRescheduleBooking(value); - } - if (this.isRecurringBookingInput(value)) { return this.validateRecurringBooking(value); } @@ -62,22 +56,6 @@ export class CreateBookingInputPipe implements PipeTransform { return object; } - validateRescheduleBooking(value: RescheduleBookingInput_2024_08_13) { - const object = plainToClass(RescheduleBookingInput_2024_08_13, value); - - const errors = validateSync(object, { - whitelist: true, - forbidNonWhitelisted: true, - skipMissingProperties: false, - }); - - if (errors.length > 0) { - throw new BadRequestException(this.formatErrors(errors)); - } - - return object; - } - validateRecurringBooking(value: CreateRecurringBookingInput_2024_08_13) { const object = plainToClass(CreateRecurringBookingInput_2024_08_13, value); @@ -121,30 +99,13 @@ export class CreateBookingInputPipe implements PipeTransform { .join(", "); } - private isRescheduleBookingInput( - value: - | CreateBookingInput_2024_08_13 - | RescheduleBookingInput_2024_08_13 - | CreateRecurringBookingInput_2024_08_13 - ): value is RescheduleBookingInput_2024_08_13 { - return value.hasOwnProperty("rescheduleBookingUid"); - } - private isRecurringBookingInput( - value: - | CreateBookingInput_2024_08_13 - | RescheduleBookingInput_2024_08_13 - | CreateRecurringBookingInput_2024_08_13 + value: CreateBookingInput ): value is CreateRecurringBookingInput_2024_08_13 { return value.hasOwnProperty("recurringEventTypeId"); } - private isInstantBookingInput( - value: - | CreateBookingInput_2024_08_13 - | RescheduleBookingInput_2024_08_13 - | CreateRecurringBookingInput_2024_08_13 - ): value is CreateRecurringBookingInput_2024_08_13 { + private isInstantBookingInput(value: CreateBookingInput): value is CreateRecurringBookingInput_2024_08_13 { return value.hasOwnProperty("instant") && "instant" in value && value.instant === true; } } diff --git a/packages/platform/types/bookings/2024-08-13/inputs/create-booking.input.ts b/packages/platform/types/bookings/2024-08-13/inputs/create-booking.input.ts index 438efc83392d2a..e67f4914ede47e 100644 --- a/packages/platform/types/bookings/2024-08-13/inputs/create-booking.input.ts +++ b/packages/platform/types/bookings/2024-08-13/inputs/create-booking.input.ts @@ -94,11 +94,3 @@ export class CreateRecurringBookingInput_2024_08_13 { @IsOptional() bookingFieldsResponses!: Record; } - -export class RescheduleBookingInput_2024_08_13 { - @IsDateString() - start!: string; - - @IsString() - rescheduleBookingUid!: string; -} diff --git a/packages/platform/types/bookings/2024-08-13/inputs/index.ts b/packages/platform/types/bookings/2024-08-13/inputs/index.ts index 1dc86279cf339b..f67fe2858cc5c3 100644 --- a/packages/platform/types/bookings/2024-08-13/inputs/index.ts +++ b/packages/platform/types/bookings/2024-08-13/inputs/index.ts @@ -1,3 +1,4 @@ export * from "./create-booking.input"; export * from "./create-booking-input.pipe"; export * from "./get-bookings.input"; +export * from "./reschedule-booking.input"; diff --git a/packages/platform/types/bookings/2024-08-13/inputs/reschedule-booking.input.ts b/packages/platform/types/bookings/2024-08-13/inputs/reschedule-booking.input.ts new file mode 100644 index 00000000000000..b3f05cce706a70 --- /dev/null +++ b/packages/platform/types/bookings/2024-08-13/inputs/reschedule-booking.input.ts @@ -0,0 +1,6 @@ +import { IsDateString } from "class-validator"; + +export class RescheduleBookingInput_2024_08_13 { + @IsDateString() + start!: string; +} From 481b8d06c59726b560e3baef90935a6b5d02529a Mon Sep 17 00:00:00 2001 From: supalarry Date: Fri, 23 Aug 2024 14:45:47 +0200 Subject: [PATCH 23/96] feat: cancel endpoint --- .../controllers/bookings.controller.ts | 30 ++++++-- .../outputs/cancel-booking.output.ts | 14 ++++ .../outputs/create-booking.output.ts | 1 - .../2024-08-13/services/bookings.service.ts | 8 +++ .../2024-08-13/services/input.service.ts | 41 +++++++++++ apps/api/v2/swagger/documentation.json | 71 +++++++++++++++++++ .../2024-08-13/inputs/cancel-booking.input.ts | 9 +++ .../types/bookings/2024-08-13/inputs/index.ts | 1 + 8 files changed, 167 insertions(+), 8 deletions(-) create mode 100644 apps/api/v2/src/ee/bookings/2024-08-13/outputs/cancel-booking.output.ts create mode 100644 packages/platform/types/bookings/2024-08-13/inputs/cancel-booking.input.ts diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.ts b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.ts index 086bc240ae4c51..bbdd321f91e3ca 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.ts @@ -1,3 +1,4 @@ +import { CancelBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/cancel-booking.output"; import { CreateBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/create-booking.output"; import { GetBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/get-booking.output"; import { GetBookingsOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/get-bookings.output"; @@ -18,6 +19,8 @@ import { Param, Query, BadRequestException, + HttpCode, + HttpStatus, } from "@nestjs/common"; import { ApiTags as DocsTags } from "@nestjs/swagger"; import { User } from "@prisma/client"; @@ -29,6 +32,7 @@ import { CreateBookingInput, GetBookingsInput_2024_08_13, RescheduleBookingInput_2024_08_13, + CancelBookingInput_2024_08_13, } from "@calcom/platform-types"; @Controller({ @@ -103,12 +107,24 @@ export class BookingsController_2024_08_13 { }; } - // @Post("/:bookingId/cancel") - // async cancelBooking( - // @Req() request: Request, - // @Param("bookingId") bookingId: string, - // @Body() body: CancelBookingInput_2024_04_15, - // ): Promise<> { + @Post("/:bookingUid/cancel") + @HttpCode(HttpStatus.OK) + async cancelBooking( + @Req() request: Request, + @Param("bookingUid") bookingUid: string, + @Body() body: CancelBookingInput_2024_08_13 + ): Promise { + if (!bookingUid) { + throw new BadRequestException("Booking UID is required in request path /:bookingUid/cancel"); + } + + console.log("asap here"); + + const cancelledBooking = await this.bookingsService.cancelBooking(request, bookingUid, body); - // } + return { + status: SUCCESS_STATUS, + data: cancelledBooking, + }; + } } diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/outputs/cancel-booking.output.ts b/apps/api/v2/src/ee/bookings/2024-08-13/outputs/cancel-booking.output.ts new file mode 100644 index 00000000000000..19905c4dd30089 --- /dev/null +++ b/apps/api/v2/src/ee/bookings/2024-08-13/outputs/cancel-booking.output.ts @@ -0,0 +1,14 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { IsEnum, ValidateNested } from "class-validator"; + +import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants"; +import { BookingOutput_2024_08_13, RecurringBookingOutput_2024_08_13 } from "@calcom/platform-types"; + +export class CancelBookingOutput_2024_08_13 { + @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) + @IsEnum([SUCCESS_STATUS, ERROR_STATUS]) + status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS; + + @ValidateNested() + data!: BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13[]; +} diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/outputs/create-booking.output.ts b/apps/api/v2/src/ee/bookings/2024-08-13/outputs/create-booking.output.ts index e61ac1276aca21..2edaed89cc39a5 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/outputs/create-booking.output.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/outputs/create-booking.output.ts @@ -1,5 +1,4 @@ import { ApiProperty } from "@nestjs/swagger"; -import { Type } from "class-transformer"; import { IsEnum, ValidateNested } from "class-validator"; import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants"; diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts index 4c8679b160822a..f770e1ea5e4df6 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts @@ -11,6 +11,7 @@ import { handleNewRecurringBooking, getAllUserBookings, handleInstantMeeting, + handleCancelBooking, } from "@calcom/platform-libraries-1.2.3"; import { CreateBookingInput_2024_08_13, @@ -19,6 +20,7 @@ import { CreateRecurringBookingInput_2024_08_13, GetBookingsInput_2024_08_13, CreateInstantBookingInput_2024_08_13, + CancelBookingInput_2024_08_13, } from "@calcom/platform-types"; import { PrismaClient } from "@calcom/prisma"; import { Booking } from "@calcom/prisma/client"; @@ -157,4 +159,10 @@ export class BookingsService_2024_08_13 { return this.outputService.getOutputBooking(databaseBooking); } + + async cancelBooking(request: Request, bookingUid: string, body: CancelBookingInput_2024_08_13) { + const bookingRequest = await this.inputService.createCancelBookingRequest(request, bookingUid, body); + await handleCancelBooking(bookingRequest); + return this.getBooking(bookingUid); + } } diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts index 059991de090d7f..9772adcd77979a 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts @@ -13,6 +13,7 @@ import { z } from "zod"; import { X_CAL_CLIENT_ID } from "@calcom/platform-constants"; import { + CancelBookingInput_2024_08_13, CreateBookingInput_2024_08_13, CreateInstantBookingInput_2024_08_13, CreateRecurringBookingInput_2024_08_13, @@ -342,4 +343,44 @@ export class InputBookingsService_2024_08_13 { sortCreated: queryParams.sortCreated, }; } + + async createCancelBookingRequest( + request: Request, + bookingUid: string, + body: CancelBookingInput_2024_08_13 + ): Promise { + const bodyTransformed = await this.transformInputCancelBooking(bookingUid, body); + const oAuthClientId = request.get(X_CAL_CLIENT_ID); + + const newRequest = { ...request }; + const userId = (await this.createBookingRequestOwnerId(request)) ?? undefined; + const oAuthParams = oAuthClientId + ? await this.createBookingRequestOAuthClientParams(oAuthClientId) + : DEFAULT_PLATFORM_PARAMS; + + Object.assign(newRequest, { userId, ...oAuthParams }); + + newRequest.body = { ...bodyTransformed, noEmail: !oAuthParams.arePlatformEmailsEnabled }; + + return newRequest as unknown as BookingRequest; + } + + async transformInputCancelBooking(bookingUid: string, inputBooking: CancelBookingInput_2024_08_13) { + let allRemainingBookings = false; + let uid = bookingUid; + const recurringBooking = await this.bookingsRepository.getRecurringByUidWithAttendees(bookingUid); + + if (recurringBooking.length) { + // note(Lauirs): this means that bookingUid is equal to recurringEventId on individual bookings of recurring one aka main recurring event + allRemainingBookings = true; + // note(Lauirs): we need to set uid as one of the individual recurring ids, not the main recurring event id + uid = recurringBooking[0].uid; + } + + return { + uid, + cancellationReason: inputBooking.cancellationReason, + allRemainingBookings, + }; + } } diff --git a/apps/api/v2/swagger/documentation.json b/apps/api/v2/swagger/documentation.json index 7c3eb590c0bb07..abf760e5c6fb46 100644 --- a/apps/api/v2/swagger/documentation.json +++ b/apps/api/v2/swagger/documentation.json @@ -3155,6 +3155,46 @@ ] } }, + "/v2/bookings/{bookingUid}/cancel": { + "post": { + "operationId": "BookingsController_2024_08_13_cancelBooking", + "parameters": [ + { + "name": "bookingUid", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CancelBookingInput_2024_08_13" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CancelBookingOutput_2024_08_13" + } + } + } + } + }, + "tags": [ + "Bookings" + ] + } + }, "/v2/slots/reserve": { "post": { "operationId": "SlotsController_reserveSlot", @@ -9136,6 +9176,37 @@ "type": "object", "properties": {} }, + "CancelBookingInput_2024_08_13": { + "type": "object", + "properties": { + "cancellationReason": { + "type": "string" + } + }, + "required": [ + "cancellationReason" + ] + }, + "CancelBookingOutput_2024_08_13": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "type": "object" + } + }, + "required": [ + "status", + "data" + ] + }, "ReserveSlotInput": { "type": "object", "properties": {} diff --git a/packages/platform/types/bookings/2024-08-13/inputs/cancel-booking.input.ts b/packages/platform/types/bookings/2024-08-13/inputs/cancel-booking.input.ts new file mode 100644 index 00000000000000..2ab9c2680806c5 --- /dev/null +++ b/packages/platform/types/bookings/2024-08-13/inputs/cancel-booking.input.ts @@ -0,0 +1,9 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { IsOptional, IsString } from "class-validator"; + +export class CancelBookingInput_2024_08_13 { + @IsString() + @IsOptional() + @ApiProperty() + cancellationReason?: string; +} diff --git a/packages/platform/types/bookings/2024-08-13/inputs/index.ts b/packages/platform/types/bookings/2024-08-13/inputs/index.ts index f67fe2858cc5c3..c652950dc49cc1 100644 --- a/packages/platform/types/bookings/2024-08-13/inputs/index.ts +++ b/packages/platform/types/bookings/2024-08-13/inputs/index.ts @@ -2,3 +2,4 @@ export * from "./create-booking.input"; export * from "./create-booking-input.pipe"; export * from "./get-bookings.input"; export * from "./reschedule-booking.input"; +export * from "./cancel-booking.input"; From 8baaa459ec60b6899d14a5d77c4c0fd7c76ef4e4 Mon Sep 17 00:00:00 2001 From: supalarry Date: Fri, 23 Aug 2024 16:03:12 +0200 Subject: [PATCH 24/96] feat: mark absent host or attendees --- .../controllers/bookings.controller.ts | 29 +++++++- ...utput.ts => cancel-booking.output copy.ts} | 2 +- .../2024-08-13/outputs/mark-absent.output.ts | 14 ++++ .../2024-08-13/services/bookings.service.ts | 34 ++++++++- .../2024-08-13/services/input.service.ts | 8 ++ .../2024-08-13/services/output.service.ts | 28 ++++++- apps/api/v2/swagger/documentation.json | 74 +++++++++++++++++++ .../types/bookings/2024-08-13/inputs/index.ts | 1 + .../2024-08-13/inputs/mark-absent.input.ts | 16 ++++ .../2024-08-13/outputs/booking.output.ts | 12 +++ 10 files changed, 209 insertions(+), 9 deletions(-) rename apps/api/v2/src/ee/bookings/2024-08-13/outputs/{cancel-booking.output.ts => cancel-booking.output copy.ts} (92%) create mode 100644 apps/api/v2/src/ee/bookings/2024-08-13/outputs/mark-absent.output.ts create mode 100644 packages/platform/types/bookings/2024-08-13/inputs/mark-absent.input.ts diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.ts b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.ts index bbdd321f91e3ca..734b384304cf34 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.ts @@ -1,7 +1,8 @@ -import { CancelBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/cancel-booking.output"; +import { CancelBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/cancel-booking.output copy"; import { CreateBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/create-booking.output"; import { GetBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/get-booking.output"; import { GetBookingsOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/get-bookings.output"; +import { MarkAbsentBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/mark-absent.output"; import { BookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/bookings.service"; import { VERSION_2024_08_13_VALUE } from "@/lib/api-versions"; import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator"; @@ -26,13 +27,14 @@ import { ApiTags as DocsTags } from "@nestjs/swagger"; import { User } from "@prisma/client"; import { Request } from "express"; -import { BOOKING_READ, SUCCESS_STATUS } from "@calcom/platform-constants"; +import { BOOKING_READ, BOOKING_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants"; import { CreateBookingInputPipe, CreateBookingInput, GetBookingsInput_2024_08_13, RescheduleBookingInput_2024_08_13, CancelBookingInput_2024_08_13, + MarkAbsentBookingInput_2024_08_13, } from "@calcom/platform-types"; @Controller({ @@ -118,8 +120,6 @@ export class BookingsController_2024_08_13 { throw new BadRequestException("Booking UID is required in request path /:bookingUid/cancel"); } - console.log("asap here"); - const cancelledBooking = await this.bookingsService.cancelBooking(request, bookingUid, body); return { @@ -127,4 +127,25 @@ export class BookingsController_2024_08_13 { data: cancelledBooking, }; } + + @Post("/:bookingUid/mark-absent") + @HttpCode(HttpStatus.OK) + @Permissions([BOOKING_WRITE]) + @UseGuards(ApiAuthGuard) + async markNoShow( + @Param("bookingUid") bookingUid: string, + @Body() body: MarkAbsentBookingInput_2024_08_13, + @GetUser("id") ownerId: number + ): Promise { + if (!bookingUid) { + throw new BadRequestException("Booking UID is required in request path /:bookingUid/cancel"); + } + + const booking = await this.bookingsService.markAbsent(bookingUid, ownerId, body); + + return { + status: SUCCESS_STATUS, + data: booking, + }; + } } diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/outputs/cancel-booking.output.ts b/apps/api/v2/src/ee/bookings/2024-08-13/outputs/cancel-booking.output copy.ts similarity index 92% rename from apps/api/v2/src/ee/bookings/2024-08-13/outputs/cancel-booking.output.ts rename to apps/api/v2/src/ee/bookings/2024-08-13/outputs/cancel-booking.output copy.ts index 19905c4dd30089..7ff7bab4332315 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/outputs/cancel-booking.output.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/outputs/cancel-booking.output copy.ts @@ -10,5 +10,5 @@ export class CancelBookingOutput_2024_08_13 { status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS; @ValidateNested() - data!: BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13[]; + data!: BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13[]; } diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/outputs/mark-absent.output.ts b/apps/api/v2/src/ee/bookings/2024-08-13/outputs/mark-absent.output.ts new file mode 100644 index 00000000000000..dad0ed80d8ec1a --- /dev/null +++ b/apps/api/v2/src/ee/bookings/2024-08-13/outputs/mark-absent.output.ts @@ -0,0 +1,14 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { IsEnum, ValidateNested } from "class-validator"; + +import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants"; +import { BookingOutput_2024_08_13, RecurringBookingOutput_2024_08_13 } from "@calcom/platform-types"; + +export class MarkAbsentBookingOutput_2024_08_13 { + @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) + @IsEnum([SUCCESS_STATUS, ERROR_STATUS]) + status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS; + + @ValidateNested() + data!: BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13; +} diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts index f770e1ea5e4df6..0fec7e1f4b6323 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts @@ -12,6 +12,7 @@ import { getAllUserBookings, handleInstantMeeting, handleCancelBooking, + handleMarkNoShow, } from "@calcom/platform-libraries-1.2.3"; import { CreateBookingInput_2024_08_13, @@ -21,12 +22,19 @@ import { GetBookingsInput_2024_08_13, CreateInstantBookingInput_2024_08_13, CancelBookingInput_2024_08_13, + MarkAbsentBookingInput_2024_08_13, } from "@calcom/platform-types"; import { PrismaClient } from "@calcom/prisma"; import { Booking } from "@calcom/prisma/client"; type BookingWithAttendeesAndEventType = Booking & { - attendees: { name: string; email: string; timeZone: string; locale: string | null }[]; + attendees: { + name: string; + email: string; + timeZone: string; + locale: string | null; + noShow: boolean | null; + }[]; eventType: { id: number }; }; @@ -135,6 +143,7 @@ export class BookingsService_2024_08_13 { eventTypeId: booking.eventType.id, startTime: new Date(booking.startTime), endTime: new Date(booking.endTime), + absentHost: !!booking.noShowHost, }; const isRecurring = !!formatted.recurringEventId; @@ -165,4 +174,27 @@ export class BookingsService_2024_08_13 { await handleCancelBooking(bookingRequest); return this.getBooking(bookingUid); } + + async markAbsent(bookingUid: string, bookingOwnerId: number, body: MarkAbsentBookingInput_2024_08_13) { + const bodyTransformed = this.inputService.transformInputMarkAbsentBooking(body); + + await handleMarkNoShow({ + bookingUid, + attendees: bodyTransformed.attendees, + noShowHost: bodyTransformed.noShowHost, + userId: bookingOwnerId, + }); + + const booking = await this.bookingsRepository.getByUidWithAttendees(bookingUid); + + if (!booking) { + throw new Error(`Booking with uid=${bookingUid} was not found in the database`); + } + + const isRecurring = !!booking.recurringEventId; + if (isRecurring) { + return this.outputService.getOutputRecurringBooking(booking); + } + return this.outputService.getOutputBooking(booking); + } } diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts index 9772adcd77979a..19ff716e28517d 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts @@ -18,6 +18,7 @@ import { CreateInstantBookingInput_2024_08_13, CreateRecurringBookingInput_2024_08_13, GetBookingsInput_2024_08_13, + MarkAbsentBookingInput_2024_08_13, RescheduleBookingInput_2024_08_13, } from "@calcom/platform-types"; @@ -383,4 +384,11 @@ export class InputBookingsService_2024_08_13 { allRemainingBookings, }; } + + transformInputMarkAbsentBooking(inputBooking: MarkAbsentBookingInput_2024_08_13) { + return { + noShowHost: inputBooking.host, + attendees: inputBooking.attendees?.map((email) => ({ email, noShow: true })), + }; + } } diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/output.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/output.service.ts index 16e5a6b78bd391..08f91be7b328ac 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/services/output.service.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/services/output.service.ts @@ -19,7 +19,13 @@ export class OutputBookingsService_2024_08_13 { getOutputBooking( databaseBooking: Booking & { - attendees: { name: string; email: string; timeZone: string; locale: string | null }[]; + attendees: { + name: string; + email: string; + timeZone: string; + locale: string | null; + noShow: boolean | null; + }[]; } ) { const dateStart = DateTime.fromISO(databaseBooking.startTime.toISOString()); @@ -46,9 +52,11 @@ export class OutputBookingsService_2024_08_13 { email: attendee.email, timeZone: attendee.timeZone, language: attendee.locale, + absent: !!attendee.noShow, }, guests: bookingResponses.guests, meetingUrl: databaseBooking.location, + absentHost: !!databaseBooking.noShowHost, }; return plainToClass(BookingOutput_2024_08_13, booking, { strategy: "excludeAll" }); @@ -56,7 +64,13 @@ export class OutputBookingsService_2024_08_13 { async getOutputRecurringBookings( databaseBookings: (Booking & { - attendees: { name: string; email: string; timeZone: string; locale: string | null }[]; + attendees: { + name: string; + email: string; + timeZone: string; + locale: string | null; + noShow: boolean | null; + }[]; })[] ) { const transformed = []; @@ -79,7 +93,13 @@ export class OutputBookingsService_2024_08_13 { getOutputRecurringBooking( databaseBooking: Booking & { - attendees: { name: string; email: string; timeZone: string; locale: string | null }[]; + attendees: { + name: string; + email: string; + timeZone: string; + locale: string | null; + noShow: boolean | null; + }[]; } ) { const dateStart = DateTime.fromISO(databaseBooking.startTime.toISOString()); @@ -106,10 +126,12 @@ export class OutputBookingsService_2024_08_13 { email: attendee.email, timeZone: attendee.timeZone, language: attendee.locale, + absent: !!attendee.noShow, }, guests: bookingResponses.guests, meetingUrl: databaseBooking.location, recurringBookingUid: databaseBooking.recurringEventId, + absentHost: !!databaseBooking.noShowHost, }; return plainToClass(RecurringBookingOutput_2024_08_13, booking, { strategy: "excludeAll" }); diff --git a/apps/api/v2/swagger/documentation.json b/apps/api/v2/swagger/documentation.json index abf760e5c6fb46..727f8888f79656 100644 --- a/apps/api/v2/swagger/documentation.json +++ b/apps/api/v2/swagger/documentation.json @@ -3195,6 +3195,46 @@ ] } }, + "/v2/bookings/{bookingUid}/mark-absent": { + "post": { + "operationId": "BookingsController_2024_08_13_markNoShow", + "parameters": [ + { + "name": "bookingUid", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MarkAbsentBookingInput_2024_08_13" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MarkAbsentBookingOutput_2024_08_13" + } + } + } + } + }, + "tags": [ + "Bookings" + ] + } + }, "/v2/slots/reserve": { "post": { "operationId": "SlotsController_reserveSlot", @@ -9188,6 +9228,40 @@ ] }, "CancelBookingOutput_2024_08_13": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + } + }, + "required": [ + "status" + ] + }, + "MarkAbsentBookingInput_2024_08_13": { + "type": "object", + "properties": { + "host": { + "type": "boolean" + }, + "attendees": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "host", + "attendees" + ] + }, + "MarkAbsentBookingOutput_2024_08_13": { "type": "object", "properties": { "status": { diff --git a/packages/platform/types/bookings/2024-08-13/inputs/index.ts b/packages/platform/types/bookings/2024-08-13/inputs/index.ts index c652950dc49cc1..58b3a49b0fc1d6 100644 --- a/packages/platform/types/bookings/2024-08-13/inputs/index.ts +++ b/packages/platform/types/bookings/2024-08-13/inputs/index.ts @@ -3,3 +3,4 @@ export * from "./create-booking-input.pipe"; export * from "./get-bookings.input"; export * from "./reschedule-booking.input"; export * from "./cancel-booking.input"; +export * from "./mark-absent.input"; diff --git a/packages/platform/types/bookings/2024-08-13/inputs/mark-absent.input.ts b/packages/platform/types/bookings/2024-08-13/inputs/mark-absent.input.ts new file mode 100644 index 00000000000000..c5bbcc11f154f5 --- /dev/null +++ b/packages/platform/types/bookings/2024-08-13/inputs/mark-absent.input.ts @@ -0,0 +1,16 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { IsOptional, IsBoolean, IsEmail, IsArray, ArrayMinSize } from "class-validator"; + +export class MarkAbsentBookingInput_2024_08_13 { + @IsBoolean() + @IsOptional() + @ApiProperty() + host?: boolean; + + @IsArray() + @ArrayMinSize(1) + @IsEmail({}, { each: true }) + @IsOptional() + @ApiProperty({ type: [String] }) + attendees?: string[]; +} diff --git a/packages/platform/types/bookings/2024-08-13/outputs/booking.output.ts b/packages/platform/types/bookings/2024-08-13/outputs/booking.output.ts index 7cda0548483850..021cd2bfdca7af 100644 --- a/packages/platform/types/bookings/2024-08-13/outputs/booking.output.ts +++ b/packages/platform/types/bookings/2024-08-13/outputs/booking.output.ts @@ -1,6 +1,7 @@ import { Expose, Type } from "class-transformer"; import { IsArray, + IsBoolean, IsDateString, IsEmail, IsEnum, @@ -33,6 +34,10 @@ class Attendee { @Expose() @IsOptional() language?: BookingLanguageType; + + @IsBoolean() + @Expose() + absent!: boolean; } export class BookingOutput_2024_08_13 { @IsInt() @@ -78,6 +83,10 @@ export class BookingOutput_2024_08_13 { @IsOptional() @Expose() meetingUrl?: string; + + @IsBoolean() + @Expose() + absentHost!: boolean; } export class RecurringBookingOutput_2024_08_13 { @@ -128,4 +137,7 @@ export class RecurringBookingOutput_2024_08_13 { @IsOptional() @Expose() meetingUrl?: string; + + @IsBoolean() + absentHost!: boolean; } From ec29995da056dce7936891eb1df6096edacca54e Mon Sep 17 00:00:00 2001 From: supalarry Date: Mon, 26 Aug 2024 09:43:13 +0200 Subject: [PATCH 25/96] chore: dont expose metadata for now --- .../bookings/2024-08-13/services/input.service.ts | 9 ++++++--- .../2024-08-13/inputs/create-booking.input.ts | 14 ++++++++------ 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts index 19ff716e28517d..8f974627d6a047 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts @@ -112,7 +112,8 @@ export class InputBookingsService_2024_08_13 { eventTypeSlug: eventType.slug, timeZone: inputBooking.attendee.timeZone, language: inputBooking.attendee.language || "en", - metadata: inputBooking.metadata || {}, + // todo(Lauris): expose after refactoring metadata https://app.campsite.co/cal/posts/zysq8w9rwm9c + // metadata: inputBooking.metadata || {}, hasHashedBookingLink: false, guests: inputBooking.guests, responses: inputBooking.bookingFieldsResponses @@ -190,7 +191,8 @@ export class InputBookingsService_2024_08_13 { recurringEventId, timeZone: inputBooking.attendee.timeZone, language: inputBooking.attendee.language || "en", - metadata: inputBooking.metadata || {}, + // todo(Lauris): expose after refactoring metadata https://app.campsite.co/cal/posts/zysq8w9rwm9c + // metadata: inputBooking.metadata || {}, hasHashedBookingLink: false, guests: inputBooking.guests, responses: inputBooking.bookingFieldsResponses @@ -276,7 +278,8 @@ export class InputBookingsService_2024_08_13 { eventTypeSlug: eventType.slug, timeZone: attendee.timeZone, language: attendee.locale, - metadata: booking.metadata || {}, + // todo(Lauris): expose after refactoring metadata https://app.campsite.co/cal/posts/zysq8w9rwm9c + // metadata: booking.metadata || {}, hasHashedBookingLink: false, guests: bookingResponses.guests, responses: bookingResponses, diff --git a/packages/platform/types/bookings/2024-08-13/inputs/create-booking.input.ts b/packages/platform/types/bookings/2024-08-13/inputs/create-booking.input.ts index e67f4914ede47e..5e221f474714ad 100644 --- a/packages/platform/types/bookings/2024-08-13/inputs/create-booking.input.ts +++ b/packages/platform/types/bookings/2024-08-13/inputs/create-booking.input.ts @@ -52,9 +52,10 @@ export class CreateBookingInput_2024_08_13 { @IsOptional() meetingUrl?: string; - @IsObject() - @IsOptional() - metadata!: Record; + // todo(Lauris): expose after refactoring metadata https://app.campsite.co/cal/posts/zysq8w9rwm9c + // @IsObject() + // @IsOptional() + // metadata!: Record; @IsObject() @IsOptional() @@ -86,9 +87,10 @@ export class CreateRecurringBookingInput_2024_08_13 { @IsOptional() meetingUrl?: string; - @IsObject() - @IsOptional() - metadata!: Record; + // todo(Lauris): expose after refactoring metadata https://app.campsite.co/cal/posts/zysq8w9rwm9c + // @IsObject() + // @IsOptional() + // metadata!: Record; @IsObject() @IsOptional() From 6e5e4d0ab51e8744133407f3320697e8986abda7 Mon Sep 17 00:00:00 2001 From: supalarry Date: Mon, 26 Aug 2024 10:25:59 +0200 Subject: [PATCH 26/96] chore: add hostId to response --- .../src/ee/bookings/2024-08-13/services/output.service.ts | 2 ++ .../types/bookings/2024-08-13/outputs/booking.output.ts | 8 ++++++++ .../trpc/server/routers/viewer/bookings/get.handler.ts | 1 + 3 files changed, 11 insertions(+) diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/output.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/output.service.ts index 08f91be7b328ac..599463e9ded2c1 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/services/output.service.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/services/output.service.ts @@ -42,6 +42,7 @@ export class OutputBookingsService_2024_08_13 { const booking = { id: databaseBooking.id, uid: databaseBooking.uid, + hostId: databaseBooking.userId, status: databaseBooking.status.toLowerCase(), start: databaseBooking.startTime, end: databaseBooking.endTime, @@ -116,6 +117,7 @@ export class OutputBookingsService_2024_08_13 { const booking = { id: databaseBooking.id, uid: databaseBooking.uid, + hostId: databaseBooking.userId, status: databaseBooking.status.toLowerCase(), start: databaseBooking.startTime, end: databaseBooking.endTime, diff --git a/packages/platform/types/bookings/2024-08-13/outputs/booking.output.ts b/packages/platform/types/bookings/2024-08-13/outputs/booking.output.ts index 021cd2bfdca7af..8b55ba7b9c8a1b 100644 --- a/packages/platform/types/bookings/2024-08-13/outputs/booking.output.ts +++ b/packages/platform/types/bookings/2024-08-13/outputs/booking.output.ts @@ -48,6 +48,10 @@ export class BookingOutput_2024_08_13 { @Expose() uid!: string; + @IsInt() + @Expose() + hostId!: number; + @IsEnum(["cancelled", "accepted", "rejected", "pending", "awaiting_host"]) @Expose() status!: "cancelled" | "accepted" | "rejected" | "pending" | "awaiting_host"; @@ -98,6 +102,10 @@ export class RecurringBookingOutput_2024_08_13 { @Expose() uid!: string; + @IsInt() + @Expose() + hostId!: number; + @IsEnum(["cancelled", "accepted", "rejected", "pending", "awaiting_host"]) @Expose() status!: "cancelled" | "accepted" | "rejected" | "pending" | "awaiting_host"; diff --git a/packages/trpc/server/routers/viewer/bookings/get.handler.ts b/packages/trpc/server/routers/viewer/bookings/get.handler.ts index d377492e675aa4..59a1571aa6f9b6 100644 --- a/packages/trpc/server/routers/viewer/bookings/get.handler.ts +++ b/packages/trpc/server/routers/viewer/bookings/get.handler.ts @@ -213,6 +213,7 @@ export async function getBookings({ recurringEventId: true, location: true, responses: true, + userId: true, eventType: { select: { slug: true, From 4ab73d073c76e22c82191a0cbfcc73f586708d61 Mon Sep 17 00:00:00 2001 From: supalarry Date: Mon, 26 Aug 2024 10:52:29 +0200 Subject: [PATCH 27/96] fix: metadata --- .../v2/src/ee/bookings/2024-08-13/services/input.service.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts index 8f974627d6a047..5bc0a56e1e7a4e 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts @@ -114,6 +114,7 @@ export class InputBookingsService_2024_08_13 { language: inputBooking.attendee.language || "en", // todo(Lauris): expose after refactoring metadata https://app.campsite.co/cal/posts/zysq8w9rwm9c // metadata: inputBooking.metadata || {}, + metadata: {}, hasHashedBookingLink: false, guests: inputBooking.guests, responses: inputBooking.bookingFieldsResponses @@ -193,6 +194,7 @@ export class InputBookingsService_2024_08_13 { language: inputBooking.attendee.language || "en", // todo(Lauris): expose after refactoring metadata https://app.campsite.co/cal/posts/zysq8w9rwm9c // metadata: inputBooking.metadata || {}, + metadata: {}, hasHashedBookingLink: false, guests: inputBooking.guests, responses: inputBooking.bookingFieldsResponses @@ -280,6 +282,7 @@ export class InputBookingsService_2024_08_13 { language: attendee.locale, // todo(Lauris): expose after refactoring metadata https://app.campsite.co/cal/posts/zysq8w9rwm9c // metadata: booking.metadata || {}, + metadata: {}, hasHashedBookingLink: false, guests: bookingResponses.guests, responses: bookingResponses, From 86c1563181f359723f848066e710da6bb96962f1 Mon Sep 17 00:00:00 2001 From: supalarry Date: Mon, 26 Aug 2024 11:14:07 +0200 Subject: [PATCH 28/96] feat: bill bookings --- .../controllers/bookings.controller.ts | 7 +++++ .../2024-08-13/services/bookings.service.ts | 31 ++++++++++++++++++- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.ts b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.ts index 734b384304cf34..cf1fc77008c939 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.ts @@ -56,6 +56,12 @@ export class BookingsController_2024_08_13 { ): Promise { const booking = await this.bookingsService.createBooking(request, body); + if (Array.isArray(booking)) { + await this.bookingsService.billBookings(booking); + } else { + await this.bookingsService.billBooking(booking); + } + return { status: SUCCESS_STATUS, data: booking, @@ -102,6 +108,7 @@ export class BookingsController_2024_08_13 { } const newBooking = await this.bookingsService.rescheduleBooking(request, bookingUid, body); + await this.bookingsService.billRescheduledBooking(newBooking, bookingUid); return { status: SUCCESS_STATUS, diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts index 0fec7e1f4b6323..356cf8c6962c85 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts @@ -2,6 +2,7 @@ import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings import { InputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/input.service"; import { OutputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/output.service"; import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository"; +import { BillingService } from "@/modules/billing/services/billing.service"; import { PrismaReadService } from "@/modules/prisma/prisma-read.service"; import { Injectable } from "@nestjs/common"; import { Request } from "express"; @@ -38,6 +39,12 @@ type BookingWithAttendeesAndEventType = Booking & { eventType: { id: number }; }; +type CreatedBooking = { + hostId: number; + uid: string; + start: string; +}; + @Injectable() export class BookingsService_2024_08_13 { constructor( @@ -45,7 +52,8 @@ export class BookingsService_2024_08_13 { private readonly outputService: OutputBookingsService_2024_08_13, private readonly bookingsRepository: BookingsRepository_2024_08_13, private readonly eventTypesRepository: EventTypesRepository_2024_06_14, - private readonly prismaReadService: PrismaReadService + private readonly prismaReadService: PrismaReadService, + private readonly billingService: BillingService ) {} async createBooking(request: Request, body: CreateBookingInput) { @@ -197,4 +205,25 @@ export class BookingsService_2024_08_13 { } return this.outputService.getOutputBooking(booking); } + + async billBookings(bookings: CreatedBooking[]) { + for (const booking of bookings) { + await this.billBooking(booking); + } + } + + async billBooking(booking: CreatedBooking) { + await this.billingService.increaseUsageByUserId(booking.hostId, { + uid: booking.uid, + startTime: new Date(booking.start), + }); + } + + async billRescheduledBooking(newBooking: CreatedBooking, oldBookingUid: string) { + await this.billingService.increaseUsageByUserId(newBooking.hostId, { + uid: newBooking.uid, + startTime: new Date(newBooking.start), + fromReschedule: oldBookingUid, + }); + } } From 7704a286b16a5e08e228281896717d41eda272c4 Mon Sep 17 00:00:00 2001 From: supalarry Date: Mon, 26 Aug 2024 11:32:40 +0200 Subject: [PATCH 29/96] feat: cancellationReason --- .../2024-08-13/services/output.service.ts | 2 ++ .../inputs/reschedule-booking.input.ts | 6 +++++- .../2024-08-13/outputs/booking.output.ts | 20 +++++++++++++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/output.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/output.service.ts index 599463e9ded2c1..8ea2975013eb17 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/services/output.service.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/services/output.service.ts @@ -44,6 +44,7 @@ export class OutputBookingsService_2024_08_13 { uid: databaseBooking.uid, hostId: databaseBooking.userId, status: databaseBooking.status.toLowerCase(), + cancellationReason: databaseBooking.cancellationReason, start: databaseBooking.startTime, end: databaseBooking.endTime, duration, @@ -119,6 +120,7 @@ export class OutputBookingsService_2024_08_13 { uid: databaseBooking.uid, hostId: databaseBooking.userId, status: databaseBooking.status.toLowerCase(), + cancellationReason: databaseBooking.cancellationReason, start: databaseBooking.startTime, end: databaseBooking.endTime, duration, diff --git a/packages/platform/types/bookings/2024-08-13/inputs/reschedule-booking.input.ts b/packages/platform/types/bookings/2024-08-13/inputs/reschedule-booking.input.ts index b3f05cce706a70..a077c367edaaa6 100644 --- a/packages/platform/types/bookings/2024-08-13/inputs/reschedule-booking.input.ts +++ b/packages/platform/types/bookings/2024-08-13/inputs/reschedule-booking.input.ts @@ -1,6 +1,10 @@ -import { IsDateString } from "class-validator"; +import { IsDateString, IsOptional, IsString } from "class-validator"; export class RescheduleBookingInput_2024_08_13 { @IsDateString() start!: string; + + @IsString() + @IsOptional() + reschedulingReason?: string; } diff --git a/packages/platform/types/bookings/2024-08-13/outputs/booking.output.ts b/packages/platform/types/bookings/2024-08-13/outputs/booking.output.ts index 8b55ba7b9c8a1b..33c9caa383a8e6 100644 --- a/packages/platform/types/bookings/2024-08-13/outputs/booking.output.ts +++ b/packages/platform/types/bookings/2024-08-13/outputs/booking.output.ts @@ -56,6 +56,16 @@ export class BookingOutput_2024_08_13 { @Expose() status!: "cancelled" | "accepted" | "rejected" | "pending" | "awaiting_host"; + @IsString() + @IsOptional() + @Expose() + cancellationReason?: string; + + @IsString() + @IsOptional() + @Expose() + reschedulingReason?: string; + @IsDateString() @Expose() start!: string; @@ -110,6 +120,16 @@ export class RecurringBookingOutput_2024_08_13 { @Expose() status!: "cancelled" | "accepted" | "rejected" | "pending" | "awaiting_host"; + @IsString() + @IsOptional() + @Expose() + cancellationReason?: string; + + @IsString() + @IsOptional() + @Expose() + reschedulingReason?: string; + @IsDateString() @Expose() start!: string; From 62b91da9a65ef82f2dfed5b07300d634fdb85d09 Mon Sep 17 00:00:00 2001 From: supalarry Date: Mon, 26 Aug 2024 17:53:47 +0200 Subject: [PATCH 30/96] feat: rescheduling reason --- .../2024-08-13/bookings.repository.ts | 11 +++ .../2024-08-13/services/bookings.service.ts | 8 ++ .../2024-08-13/services/input.service.ts | 3 +- .../2024-08-13/services/output.service.ts | 77 ++++++++++++++++++- .../2024-08-13/outputs/booking.output.ts | 23 +++--- 5 files changed, 109 insertions(+), 13 deletions(-) diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/bookings.repository.ts b/apps/api/v2/src/ee/bookings/2024-08-13/bookings.repository.ts index 9157723592a96b..09505908255054 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/bookings.repository.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/bookings.repository.ts @@ -54,4 +54,15 @@ export class BookingsRepository_2024_08_13 { }, }); } + + async getByFromReschedule(fromReschedule: string) { + return this.dbRead.prisma.booking.findFirst({ + where: { + fromReschedule, + }, + include: { + attendees: true, + }, + }); + } } diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts index 356cf8c6962c85..7c28d25ba8ee94 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts @@ -117,9 +117,17 @@ export class BookingsService_2024_08_13 { if (booking) { const isRecurring = !!booking.recurringEventId; + const isRescheduled = booking.rescheduled; if (isRecurring) { return this.outputService.getOutputRecurringBooking(booking); } + if (isRescheduled) { + const toReschedule = await this.bookingsRepository.getByFromReschedule(uid); + if (!toReschedule) { + throw new Error(`Booking with fromReschedule=${uid} was not found in the database`); + } + return this.outputService.getOutputRescheduledBooking(booking, toReschedule); + } return this.outputService.getOutputBooking(booking); } diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts index 5bc0a56e1e7a4e..1b8bb6e74d2745 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts @@ -285,8 +285,9 @@ export class InputBookingsService_2024_08_13 { metadata: {}, hasHashedBookingLink: false, guests: bookingResponses.guests, - responses: bookingResponses, + responses: { ...bookingResponses, rescheduledReason: inputBooking.reschedulingReason }, user: eventType.owner ? eventType.owner.username : eventType.team?.slug, + rescheduleUid: bookingUid, }; } diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/output.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/output.service.ts index 8ea2975013eb17..3e5969d95893c5 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/services/output.service.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/services/output.service.ts @@ -11,6 +11,7 @@ export const bookingResponsesSchema = z.object({ email: z.string(), name: z.string(), guests: z.array(z.string()).optional(), + rescheduledReason: z.string().optional(), }); @Injectable() @@ -43,8 +44,13 @@ export class OutputBookingsService_2024_08_13 { id: databaseBooking.id, uid: databaseBooking.uid, hostId: databaseBooking.userId, - status: databaseBooking.status.toLowerCase(), - cancellationReason: databaseBooking.cancellationReason, + status: + databaseBooking.rescheduled && !databaseBooking.cancellationReason + ? "rescheduled" + : databaseBooking.status.toLowerCase(), + cancellationReason: databaseBooking.cancellationReason || undefined, + reschedulingReason: bookingResponses?.rescheduledReason, + rescheduledFromUid: databaseBooking.fromReschedule || undefined, start: databaseBooking.startTime, end: databaseBooking.endTime, duration, @@ -64,6 +70,71 @@ export class OutputBookingsService_2024_08_13 { return plainToClass(BookingOutput_2024_08_13, booking, { strategy: "excludeAll" }); } + getOutputRescheduledBooking( + oldDatabaseBooking: Booking & { + attendees: { + name: string; + email: string; + timeZone: string; + locale: string | null; + noShow: boolean | null; + }[]; + }, + newDatabaseBooking: Booking & { + attendees: { + name: string; + email: string; + timeZone: string; + locale: string | null; + noShow: boolean | null; + }[]; + } + ) { + const dateStart = DateTime.fromISO(oldDatabaseBooking.startTime.toISOString()); + const dateEnd = DateTime.fromISO(oldDatabaseBooking.endTime.toISOString()); + const duration = dateEnd.diff(dateStart, "minutes").minutes; + + const bookingResponses = bookingResponsesSchema.parse(oldDatabaseBooking.responses); + const bookingResponsesNew = bookingResponsesSchema.parse(newDatabaseBooking.responses); + const attendee = oldDatabaseBooking.attendees.find( + (attendee) => attendee.email === bookingResponses.email + ); + + if (!attendee) { + throw new Error("Attendee not found"); + } + + const booking = { + id: oldDatabaseBooking.id, + uid: oldDatabaseBooking.uid, + hostId: oldDatabaseBooking.userId, + status: + oldDatabaseBooking.rescheduled && !oldDatabaseBooking.cancellationReason + ? "rescheduled" + : oldDatabaseBooking.status.toLowerCase(), + cancellationReason: oldDatabaseBooking.cancellationReason || undefined, + reschedulingReason: bookingResponsesNew?.rescheduledReason, + rescheduledFromUid: oldDatabaseBooking.fromReschedule || undefined, + rescheduledToUid: newDatabaseBooking.uid, + start: oldDatabaseBooking.startTime, + end: oldDatabaseBooking.endTime, + duration, + eventTypeId: oldDatabaseBooking.eventTypeId, + attendee: { + name: attendee.name, + email: attendee.email, + timeZone: attendee.timeZone, + language: attendee.locale, + absent: !!attendee.noShow, + }, + guests: bookingResponses.guests, + meetingUrl: oldDatabaseBooking.location, + absentHost: !!oldDatabaseBooking.noShowHost, + }; + + return plainToClass(BookingOutput_2024_08_13, booking, { strategy: "excludeAll" }); + } + async getOutputRecurringBookings( databaseBookings: (Booking & { attendees: { @@ -120,7 +191,7 @@ export class OutputBookingsService_2024_08_13 { uid: databaseBooking.uid, hostId: databaseBooking.userId, status: databaseBooking.status.toLowerCase(), - cancellationReason: databaseBooking.cancellationReason, + cancellationReason: databaseBooking.cancellationReason || undefined, start: databaseBooking.startTime, end: databaseBooking.endTime, duration, diff --git a/packages/platform/types/bookings/2024-08-13/outputs/booking.output.ts b/packages/platform/types/bookings/2024-08-13/outputs/booking.output.ts index 33c9caa383a8e6..eb256d2ade2e06 100644 --- a/packages/platform/types/bookings/2024-08-13/outputs/booking.output.ts +++ b/packages/platform/types/bookings/2024-08-13/outputs/booking.output.ts @@ -52,9 +52,9 @@ export class BookingOutput_2024_08_13 { @Expose() hostId!: number; - @IsEnum(["cancelled", "accepted", "rejected", "pending", "awaiting_host"]) + @IsEnum(["cancelled", "accepted", "rejected", "pending", "rescheduled"]) @Expose() - status!: "cancelled" | "accepted" | "rejected" | "pending" | "awaiting_host"; + status!: "cancelled" | "accepted" | "rejected" | "pending" | "rescheduled"; @IsString() @IsOptional() @@ -66,6 +66,16 @@ export class BookingOutput_2024_08_13 { @Expose() reschedulingReason?: string; + @IsString() + @IsOptional() + @Expose() + rescheduledFromUid?: string; + + @IsString() + @IsOptional() + @Expose() + rescheduledToUid?: string; + @IsDateString() @Expose() start!: string; @@ -116,20 +126,15 @@ export class RecurringBookingOutput_2024_08_13 { @Expose() hostId!: number; - @IsEnum(["cancelled", "accepted", "rejected", "pending", "awaiting_host"]) + @IsEnum(["cancelled", "accepted", "rejected", "pending"]) @Expose() - status!: "cancelled" | "accepted" | "rejected" | "pending" | "awaiting_host"; + status!: "cancelled" | "accepted" | "rejected" | "pending"; @IsString() @IsOptional() @Expose() cancellationReason?: string; - @IsString() - @IsOptional() - @Expose() - reschedulingReason?: string; - @IsDateString() @Expose() start!: string; From 593f5740e0a995be47ca84334ddc1277df38ddc8 Mon Sep 17 00:00:00 2001 From: supalarry Date: Mon, 26 Aug 2024 18:07:27 +0200 Subject: [PATCH 31/96] handle already busy booking error --- .../2024-08-13/services/bookings.service.ts | 24 +++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts index 7c28d25ba8ee94..6e9395654daa17 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts @@ -5,6 +5,7 @@ import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_20 import { BillingService } from "@/modules/billing/services/billing.service"; import { PrismaReadService } from "@/modules/prisma/prisma-read.service"; import { Injectable } from "@nestjs/common"; +import { BadRequestException } from "@nestjs/common"; import { Request } from "express"; import { @@ -57,15 +58,24 @@ export class BookingsService_2024_08_13 { ) {} async createBooking(request: Request, body: CreateBookingInput) { - if ("instant" in body && body.instant) { - return this.createInstantBooking(request, body); - } + try { + if ("instant" in body && body.instant) { + return await this.createInstantBooking(request, body); + } - if (await this.isRecurring(body)) { - return this.createRecurringBooking(request, body); - } + if (await this.isRecurring(body)) { + return await this.createRecurringBooking(request, body); + } - return this.createRegularBooking(request, body); + return await this.createRegularBooking(request, body); + } catch (error) { + if (error instanceof Error) { + if (error.message === "no_available_users_found_error") { + throw new BadRequestException("User either already has booking at this time or is not available"); + } + } + throw error; + } } async createInstantBooking(request: Request, body: CreateInstantBookingInput_2024_08_13) { From 85ee892160b88c97dc969c82bb0060ddbd9075bd Mon Sep 17 00:00:00 2001 From: supalarry Date: Tue, 27 Aug 2024 10:14:03 +0200 Subject: [PATCH 32/96] test: create new booking --- .../bookings.controller.e2e-spec.ts | 37 +++++++++++++++++-- .../2024-08-13/services/input.service.ts | 9 ++--- .../2024-08-13/inputs/create-booking.input.ts | 4 +- 3 files changed, 39 insertions(+), 11 deletions(-) diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.e2e-spec.ts b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.e2e-spec.ts index b8eee3d439e923..684f50c2d5ce94 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.e2e-spec.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.e2e-spec.ts @@ -83,7 +83,15 @@ describe("Bookings Endpoints 2024-08-13", () => { it("should create a booking", async () => { const body: CreateBookingInput_2024_08_13 = { - start: new Date().toISOString(), + start: new Date(2030, 0, 8, 13, 0, 0).toISOString(), + eventTypeId, + attendee: { + name: "Mr Proper", + email: "mr_proper@gmail.com", + timeZone: "Europe/Rome", + language: "it", + }, + meetingUrl: "https://meet.google.com/abc-def-ghi", }; return request(app.getHttpServer()) @@ -95,11 +103,34 @@ describe("Bookings Endpoints 2024-08-13", () => { const responseBody: CreateBookingOutput_2024_08_13 = response.body; expect(responseBody.status).toEqual(SUCCESS_STATUS); expect(responseBody.data).toBeDefined(); - - createdBooking = responseBody.data; + expect(responseDataIsBooking(responseBody.data)).toBe(true); + + if (responseDataIsBooking(responseBody.data)) { + const data: BookingOutput_2024_08_13 = responseBody.data; + expect(data.id).toBeDefined(); + expect(data.uid).toBeDefined(); + expect(data.hostId).toEqual(user.id); + expect(data.status).toEqual("accepted"); + expect(data.start).toEqual(body.start); + expect(data.end).toEqual(new Date(2030, 0, 8, 14, 0, 0).toISOString()); + expect(data.duration).toEqual(60); + expect(data.eventTypeId).toEqual(eventTypeId); + expect(data.attendee).toEqual({ ...body.attendee, absent: false }); + expect(data.meetingUrl).toEqual(body.meetingUrl); + expect(data.absentHost).toEqual(false); + createdBooking = data; + } else { + throw new Error( + "Invalid response data - expected booking but received array of possibily recurring bookings" + ); + } }); }); + function responseDataIsBooking(data: any): data is BookingOutput_2024_08_13 { + return !Array.isArray(data) && typeof data === "object" && data && "id" in data; + } + afterAll(async () => { await userRepositoryFixture.deleteByEmail(user.email); await bookingsRepositoryFixture.deleteAllBookings(user.id, user.email); diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts index 1b8bb6e74d2745..cd9776379a0478 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts @@ -96,6 +96,7 @@ export class InputBookingsService_2024_08_13 { const eventType = await this.eventTypesRepository.getEventTypeByIdWithOwnerAndTeam( inputBooking.eventTypeId ); + if (!eventType) { throw new NotFoundException(`Event type with id=${inputBooking.eventTypeId} not found`); } @@ -109,7 +110,6 @@ export class InputBookingsService_2024_08_13 { start: startTime.toISO(), end: endTime.toISO(), eventTypeId: inputBooking.eventTypeId, - eventTypeSlug: eventType.slug, timeZone: inputBooking.attendee.timeZone, language: inputBooking.attendee.language || "en", // todo(Lauris): expose after refactoring metadata https://app.campsite.co/cal/posts/zysq8w9rwm9c @@ -117,6 +117,7 @@ export class InputBookingsService_2024_08_13 { metadata: {}, hasHashedBookingLink: false, guests: inputBooking.guests, + // note(Lauris): responses with name and email are required by the handleNewBooking responses: inputBooking.bookingFieldsResponses ? { ...inputBooking.bookingFieldsResponses, @@ -124,7 +125,6 @@ export class InputBookingsService_2024_08_13 { email: inputBooking.attendee.email, } : { name: inputBooking.attendee.name, email: inputBooking.attendee.email }, - user: eventType.owner ? eventType.owner.username : eventType.team?.slug, }; } @@ -188,7 +188,6 @@ export class InputBookingsService_2024_08_13 { start: startTime.toISO(), end: endTime.toISO(), eventTypeId: inputBooking.eventTypeId, - eventTypeSlug: eventType.slug, recurringEventId, timeZone: inputBooking.attendee.timeZone, language: inputBooking.attendee.language || "en", @@ -197,6 +196,7 @@ export class InputBookingsService_2024_08_13 { metadata: {}, hasHashedBookingLink: false, guests: inputBooking.guests, + // note(Lauris): responses with name and email are required by the handleNewBooking responses: inputBooking.bookingFieldsResponses ? { ...inputBooking.bookingFieldsResponses, @@ -204,7 +204,6 @@ export class InputBookingsService_2024_08_13 { email: inputBooking.attendee.email, } : { name: inputBooking.attendee.name, email: inputBooking.attendee.email }, - user: eventType.owner ? eventType.owner.username : eventType.team?.slug, schedulingType: eventType.schedulingType, }); @@ -277,7 +276,6 @@ export class InputBookingsService_2024_08_13 { start: startTime.toISO(), end: endTime.toISO(), eventTypeId: eventType.id, - eventTypeSlug: eventType.slug, timeZone: attendee.timeZone, language: attendee.locale, // todo(Lauris): expose after refactoring metadata https://app.campsite.co/cal/posts/zysq8w9rwm9c @@ -286,7 +284,6 @@ export class InputBookingsService_2024_08_13 { hasHashedBookingLink: false, guests: bookingResponses.guests, responses: { ...bookingResponses, rescheduledReason: inputBooking.reschedulingReason }, - user: eventType.owner ? eventType.owner.username : eventType.team?.slug, rescheduleUid: bookingUid, }; } diff --git a/packages/platform/types/bookings/2024-08-13/inputs/create-booking.input.ts b/packages/platform/types/bookings/2024-08-13/inputs/create-booking.input.ts index 5e221f474714ad..2b1ed7038dcd3a 100644 --- a/packages/platform/types/bookings/2024-08-13/inputs/create-booking.input.ts +++ b/packages/platform/types/bookings/2024-08-13/inputs/create-booking.input.ts @@ -59,7 +59,7 @@ export class CreateBookingInput_2024_08_13 { @IsObject() @IsOptional() - bookingFieldsResponses!: Record; + bookingFieldsResponses?: Record; } export class CreateInstantBookingInput_2024_08_13 extends CreateBookingInput_2024_08_13 { @@ -94,5 +94,5 @@ export class CreateRecurringBookingInput_2024_08_13 { @IsObject() @IsOptional() - bookingFieldsResponses!: Record; + bookingFieldsResponses?: Record; } From e76e08296b5c7f1161f5b0dcb1a37e3c7b68c1a2 Mon Sep 17 00:00:00 2001 From: supalarry Date: Wed, 28 Aug 2024 09:40:44 +0200 Subject: [PATCH 33/96] fix: handleNewRecurringBooking ignoring noEmail --- .../ee/bookings/2024-08-13/services/input.service.ts | 3 ++- .../bookings/lib/handleNewRecurringBooking.ts | 12 ++++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts index cd9776379a0478..1bc52b3fe7c11a 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts @@ -147,9 +147,10 @@ export class InputBookingsService_2024_08_13 { newRequest.body = (bodyTransformed as any[]).map((event) => ({ ...event, - noEmail: !oAuthParams.arePlatformEmailsEnabled, })); + newRequest.body.noEmail = !oAuthParams.arePlatformEmailsEnabled; + return newRequest as unknown as BookingRequest; } diff --git a/packages/features/bookings/lib/handleNewRecurringBooking.ts b/packages/features/bookings/lib/handleNewRecurringBooking.ts index 9319abbd803650..c2703bc71836d9 100644 --- a/packages/features/bookings/lib/handleNewRecurringBooking.ts +++ b/packages/features/bookings/lib/handleNewRecurringBooking.ts @@ -6,7 +6,15 @@ import { SchedulingType } from "@calcom/prisma/client"; import type { AppsStatus } from "@calcom/types/Calendar"; export const handleNewRecurringBooking = async ( - req: NextApiRequest & { userId?: number } + req: NextApiRequest & { + userId?: number | undefined; + platformClientId?: string; + platformRescheduleUrl?: string; + platformCancelUrl?: string; + platformBookingUrl?: string; + platformBookingLocation?: string; + noEmail?: boolean; + } ): Promise => { const data: RecurringBookingCreateBody[] = req.body; const createdBookings: BookingResponse[] = []; @@ -73,7 +81,7 @@ export const handleNewRecurringBooking = async ( thirdPartyRecurringEventId, numSlotsToCheckForAvailability, currentRecurringIndex: key, - noEmail: key !== 0, + noEmail: req.body.noEmail !== undefined ? req.body.noEmail : key !== 0, luckyUsers, }; From 0b541e1cfccb617bba2921377304ca149e4dbe6c Mon Sep 17 00:00:00 2001 From: supalarry Date: Wed, 28 Aug 2024 09:41:32 +0200 Subject: [PATCH 34/96] test: recurring bookings --- .../bookings.controller.e2e-spec.ts | 105 +++++++++++++++++- .../2024-08-13/outputs/booking.output.ts | 1 + 2 files changed, 103 insertions(+), 3 deletions(-) diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.e2e-spec.ts b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.e2e-spec.ts index 684f50c2d5ce94..b79e0dccfeeae0 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.e2e-spec.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.e2e-spec.ts @@ -18,7 +18,12 @@ import { UserRepositoryFixture } from "test/fixtures/repository/users.repository import { withApiAuth } from "test/utils/withApiAuth"; import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants"; -import { CreateBookingInput_2024_08_13, BookingOutput_2024_08_13 } from "@calcom/platform-types"; +import { + CreateBookingInput_2024_08_13, + BookingOutput_2024_08_13, + CreateRecurringBookingInput_2024_08_13, + RecurringBookingOutput_2024_08_13, +} from "@calcom/platform-types"; describe("Bookings Endpoints 2024-08-13", () => { describe("User Authenticated", () => { @@ -33,8 +38,10 @@ describe("Bookings Endpoints 2024-08-13", () => { let user: User; let eventTypeId: number; + let recurringEventTypeId: number; let createdBooking: BookingOutput_2024_08_13; + let createdRecurringBooking: RecurringBookingOutput_2024_08_13[]; beforeAll(async () => { const moduleRef = await withApiAuth( @@ -70,6 +77,18 @@ describe("Bookings Endpoints 2024-08-13", () => { ); eventTypeId = event.id; + const recurringEvent = await eventTypesRepositoryFixture.create( + // note(Lauris): freq 2 means weekly, interval 1 means every week and count 3 means 3 weeks in a row + { + title: "peer coding recurring", + slug: "peer-coding-recurring", + length: 60, + recurringEvent: { freq: 2, count: 3, interval: 1 }, + }, + user.id + ); + recurringEventTypeId = recurringEvent.id; + app = moduleRef.createNestApplication(); bootstrap(app as NestExpressApplication); @@ -83,7 +102,7 @@ describe("Bookings Endpoints 2024-08-13", () => { it("should create a booking", async () => { const body: CreateBookingInput_2024_08_13 = { - start: new Date(2030, 0, 8, 13, 0, 0).toISOString(), + start: new Date(Date.UTC(2030, 0, 8, 13, 0, 0)).toISOString(), eventTypeId, attendee: { name: "Mr Proper", @@ -112,7 +131,7 @@ describe("Bookings Endpoints 2024-08-13", () => { expect(data.hostId).toEqual(user.id); expect(data.status).toEqual("accepted"); expect(data.start).toEqual(body.start); - expect(data.end).toEqual(new Date(2030, 0, 8, 14, 0, 0).toISOString()); + expect(data.end).toEqual(new Date(Date.UTC(2030, 0, 8, 14, 0, 0)).toISOString()); expect(data.duration).toEqual(60); expect(data.eventTypeId).toEqual(eventTypeId); expect(data.attendee).toEqual({ ...body.attendee, absent: false }); @@ -127,10 +146,90 @@ describe("Bookings Endpoints 2024-08-13", () => { }); }); + it("should create a recurring booking", async () => { + const body: CreateRecurringBookingInput_2024_08_13 = { + start: new Date(Date.UTC(2030, 1, 4, 13, 0, 0)).toISOString(), + eventTypeId: recurringEventTypeId, + attendee: { + name: "Mr Proper", + email: "mr_proper@gmail.com", + timeZone: "Europe/Rome", + language: "it", + }, + meetingUrl: "https://meet.google.com/abc-def-ghi", + }; + + return request(app.getHttpServer()) + .post("/v2/bookings") + .send(body) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .expect(201) + .then(async (response) => { + const responseBody: CreateBookingOutput_2024_08_13 = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + expect(responseDataIsRecurringBooking(responseBody.data)).toBe(true); + + if (responseDataIsRecurringBooking(responseBody.data)) { + const data: RecurringBookingOutput_2024_08_13[] = responseBody.data; + expect(data.length).toEqual(3); + + const firstBooking = data[0]; + expect(firstBooking.id).toBeDefined(); + expect(firstBooking.uid).toBeDefined(); + expect(firstBooking.hostId).toEqual(user.id); + expect(firstBooking.status).toEqual("accepted"); + expect(firstBooking.start).toEqual(new Date(Date.UTC(2030, 1, 4, 13, 0, 0)).toISOString()); + expect(firstBooking.end).toEqual(new Date(Date.UTC(2030, 1, 4, 14, 0, 0)).toISOString()); + expect(firstBooking.duration).toEqual(60); + expect(firstBooking.eventTypeId).toEqual(recurringEventTypeId); + expect(firstBooking.attendee).toEqual({ ...body.attendee, absent: false }); + expect(firstBooking.meetingUrl).toEqual(body.meetingUrl); + expect(firstBooking.absentHost).toEqual(false); + + const secondBooking = data[1]; + expect(secondBooking.id).toBeDefined(); + expect(secondBooking.uid).toBeDefined(); + expect(secondBooking.hostId).toEqual(user.id); + expect(secondBooking.status).toEqual("accepted"); + expect(secondBooking.start).toEqual(new Date(Date.UTC(2030, 1, 11, 13, 0, 0)).toISOString()); + expect(secondBooking.end).toEqual(new Date(Date.UTC(2030, 1, 11, 14, 0, 0)).toISOString()); + expect(secondBooking.duration).toEqual(60); + expect(secondBooking.eventTypeId).toEqual(recurringEventTypeId); + expect(secondBooking.attendee).toEqual({ ...body.attendee, absent: false }); + expect(secondBooking.meetingUrl).toEqual(body.meetingUrl); + expect(secondBooking.absentHost).toEqual(false); + + const thirdBooking = data[2]; + expect(thirdBooking.id).toBeDefined(); + expect(thirdBooking.uid).toBeDefined(); + expect(thirdBooking.hostId).toEqual(user.id); + expect(thirdBooking.status).toEqual("accepted"); + expect(thirdBooking.start).toEqual(new Date(Date.UTC(2030, 1, 18, 13, 0, 0)).toISOString()); + expect(thirdBooking.end).toEqual(new Date(Date.UTC(2030, 1, 18, 14, 0, 0)).toISOString()); + expect(thirdBooking.duration).toEqual(60); + expect(thirdBooking.eventTypeId).toEqual(recurringEventTypeId); + expect(thirdBooking.attendee).toEqual({ ...body.attendee, absent: false }); + expect(thirdBooking.meetingUrl).toEqual(body.meetingUrl); + expect(thirdBooking.absentHost).toEqual(false); + + createdRecurringBooking = data; + } else { + throw new Error( + "Invalid response data - expected recurring booking but received non array response" + ); + } + }); + }); + function responseDataIsBooking(data: any): data is BookingOutput_2024_08_13 { return !Array.isArray(data) && typeof data === "object" && data && "id" in data; } + function responseDataIsRecurringBooking(data: any): data is RecurringBookingOutput_2024_08_13[] { + return Array.isArray(data); + } + afterAll(async () => { await userRepositoryFixture.deleteByEmail(user.email); await bookingsRepositoryFixture.deleteAllBookings(user.id, user.email); diff --git a/packages/platform/types/bookings/2024-08-13/outputs/booking.output.ts b/packages/platform/types/bookings/2024-08-13/outputs/booking.output.ts index eb256d2ade2e06..29f8d383a92dfc 100644 --- a/packages/platform/types/bookings/2024-08-13/outputs/booking.output.ts +++ b/packages/platform/types/bookings/2024-08-13/outputs/booking.output.ts @@ -172,5 +172,6 @@ export class RecurringBookingOutput_2024_08_13 { meetingUrl?: string; @IsBoolean() + @Expose() absentHost!: boolean; } From 985f70307d17de045e89e442d2db5a0b38ac2d8f Mon Sep 17 00:00:00 2001 From: supalarry Date: Wed, 28 Aug 2024 10:07:15 +0200 Subject: [PATCH 35/96] test: get individual bookings --- .../bookings.controller.e2e-spec.ts | 148 ++++++++++++++++++ 1 file changed, 148 insertions(+) diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.e2e-spec.ts b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.e2e-spec.ts index b79e0dccfeeae0..86750dced74653 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.e2e-spec.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.e2e-spec.ts @@ -1,6 +1,7 @@ import { bootstrap } from "@/app"; import { AppModule } from "@/app.module"; import { CreateBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/create-booking.output"; +import { GetBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/get-booking.output"; import { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input"; import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module"; import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service"; @@ -185,6 +186,7 @@ describe("Bookings Endpoints 2024-08-13", () => { expect(firstBooking.eventTypeId).toEqual(recurringEventTypeId); expect(firstBooking.attendee).toEqual({ ...body.attendee, absent: false }); expect(firstBooking.meetingUrl).toEqual(body.meetingUrl); + expect(firstBooking.recurringBookingUid).toBeDefined(); expect(firstBooking.absentHost).toEqual(false); const secondBooking = data[1]; @@ -196,6 +198,7 @@ describe("Bookings Endpoints 2024-08-13", () => { expect(secondBooking.end).toEqual(new Date(Date.UTC(2030, 1, 11, 14, 0, 0)).toISOString()); expect(secondBooking.duration).toEqual(60); expect(secondBooking.eventTypeId).toEqual(recurringEventTypeId); + expect(secondBooking.recurringBookingUid).toBeDefined(); expect(secondBooking.attendee).toEqual({ ...body.attendee, absent: false }); expect(secondBooking.meetingUrl).toEqual(body.meetingUrl); expect(secondBooking.absentHost).toEqual(false); @@ -209,6 +212,7 @@ describe("Bookings Endpoints 2024-08-13", () => { expect(thirdBooking.end).toEqual(new Date(Date.UTC(2030, 1, 18, 14, 0, 0)).toISOString()); expect(thirdBooking.duration).toEqual(60); expect(thirdBooking.eventTypeId).toEqual(recurringEventTypeId); + expect(thirdBooking.recurringBookingUid).toBeDefined(); expect(thirdBooking.attendee).toEqual({ ...body.attendee, absent: false }); expect(thirdBooking.meetingUrl).toEqual(body.meetingUrl); expect(thirdBooking.absentHost).toEqual(false); @@ -222,10 +226,154 @@ describe("Bookings Endpoints 2024-08-13", () => { }); }); + it("should should get a booking", async () => { + return request(app.getHttpServer()) + .get(`/v2/bookings/${createdBooking.uid}`) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .expect(200) + .then(async (response) => { + const responseBody: GetBookingOutput_2024_08_13 = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + expect(responseDataIsBooking(responseBody.data)).toBe(true); + + if (responseDataIsBooking(responseBody.data)) { + const data: BookingOutput_2024_08_13 = responseBody.data; + expect(data.id).toEqual(createdBooking.id); + expect(data.uid).toEqual(createdBooking.uid); + expect(data.hostId).toEqual(user.id); + expect(data.status).toEqual(createdBooking.status); + expect(data.start).toEqual(createdBooking.start); + expect(data.end).toEqual(createdBooking.end); + expect(data.duration).toEqual(createdBooking.duration); + expect(data.eventTypeId).toEqual(createdBooking.eventTypeId); + expect(data.attendee).toEqual(createdBooking.attendee); + expect(data.meetingUrl).toEqual(createdBooking.meetingUrl); + expect(data.absentHost).toEqual(createdBooking.absentHost); + } else { + throw new Error( + "Invalid response data - expected booking but received array of possibily recurring bookings" + ); + } + }); + }); + + it("should should get 1 recurrence of a recurring booking", async () => { + const recurrenceUid = createdRecurringBooking[0].uid; + return request(app.getHttpServer()) + .get(`/v2/bookings/${recurrenceUid}`) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .expect(200) + .then(async (response) => { + const responseBody: GetBookingOutput_2024_08_13 = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + expect(responseDataIsRecurranceBooking(responseBody.data)).toBe(true); + + if (responseDataIsRecurranceBooking(responseBody.data)) { + const data: RecurringBookingOutput_2024_08_13 = responseBody.data; + expect(data.id).toEqual(createdRecurringBooking[0].id); + expect(data.uid).toEqual(createdRecurringBooking[0].uid); + expect(data.hostId).toEqual(user.id); + expect(data.status).toEqual(createdRecurringBooking[0].status); + expect(data.start).toEqual(createdRecurringBooking[0].start); + expect(data.end).toEqual(createdRecurringBooking[0].end); + expect(data.duration).toEqual(createdRecurringBooking[0].duration); + expect(data.eventTypeId).toEqual(createdRecurringBooking[0].eventTypeId); + expect(data.recurringBookingUid).toEqual(createdRecurringBooking[0].recurringBookingUid); + expect(data.attendee).toEqual(createdRecurringBooking[0].attendee); + expect(data.meetingUrl).toEqual(createdRecurringBooking[0].meetingUrl); + expect(data.absentHost).toEqual(createdRecurringBooking[0].absentHost); + createdBooking = data; + } else { + throw new Error( + "Invalid response data - expected booking but received array of possibily recurring bookings" + ); + } + }); + }); + + it("should should all recurrences of the recurring bookings", async () => { + const recurringBookingUid = createdRecurringBooking[0].recurringBookingUid; + return request(app.getHttpServer()) + .get(`/v2/bookings/${recurringBookingUid}`) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .expect(200) + .then(async (response) => { + const responseBody: CreateBookingOutput_2024_08_13 = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + expect(responseDataIsRecurringBooking(responseBody.data)).toBe(true); + + if (responseDataIsRecurringBooking(responseBody.data)) { + const data: RecurringBookingOutput_2024_08_13[] = responseBody.data; + expect(data.length).toEqual(3); + + const firstBooking = data[0]; + expect(firstBooking.id).toEqual(createdRecurringBooking[0].id); + expect(firstBooking.uid).toEqual(createdRecurringBooking[0].uid); + expect(firstBooking.hostId).toEqual(user.id); + expect(firstBooking.status).toEqual(createdRecurringBooking[0].status); + expect(firstBooking.start).toEqual(createdRecurringBooking[0].start); + expect(firstBooking.end).toEqual(createdRecurringBooking[0].end); + expect(firstBooking.duration).toEqual(createdRecurringBooking[0].duration); + expect(firstBooking.eventTypeId).toEqual(createdRecurringBooking[0].eventTypeId); + expect(firstBooking.recurringBookingUid).toEqual(recurringBookingUid); + expect(firstBooking.attendee).toEqual(createdRecurringBooking[0].attendee); + expect(firstBooking.meetingUrl).toEqual(createdRecurringBooking[0].meetingUrl); + expect(firstBooking.absentHost).toEqual(createdRecurringBooking[0].absentHost); + + const secondBooking = data[1]; + expect(secondBooking.id).toEqual(createdRecurringBooking[1].id); + expect(secondBooking.uid).toEqual(createdRecurringBooking[1].uid); + expect(secondBooking.hostId).toEqual(user.id); + expect(secondBooking.status).toEqual(createdRecurringBooking[1].status); + expect(secondBooking.start).toEqual(createdRecurringBooking[1].start); + expect(secondBooking.end).toEqual(createdRecurringBooking[1].end); + expect(secondBooking.duration).toEqual(createdRecurringBooking[1].duration); + expect(secondBooking.eventTypeId).toEqual(createdRecurringBooking[1].eventTypeId); + expect(secondBooking.recurringBookingUid).toEqual(recurringBookingUid); + expect(secondBooking.attendee).toEqual(createdRecurringBooking[1].attendee); + expect(secondBooking.meetingUrl).toEqual(createdRecurringBooking[1].meetingUrl); + expect(secondBooking.absentHost).toEqual(createdRecurringBooking[1].absentHost); + + const thirdBooking = data[2]; + expect(thirdBooking.id).toEqual(createdRecurringBooking[2].id); + expect(thirdBooking.uid).toEqual(createdRecurringBooking[2].uid); + expect(thirdBooking.hostId).toEqual(user.id); + expect(thirdBooking.status).toEqual(createdRecurringBooking[2].status); + expect(thirdBooking.start).toEqual(createdRecurringBooking[2].start); + expect(thirdBooking.end).toEqual(createdRecurringBooking[2].end); + expect(thirdBooking.duration).toEqual(createdRecurringBooking[2].duration); + expect(thirdBooking.eventTypeId).toEqual(createdRecurringBooking[2].eventTypeId); + expect(thirdBooking.recurringBookingUid).toEqual(recurringBookingUid); + expect(thirdBooking.attendee).toEqual(createdRecurringBooking[2].attendee); + expect(thirdBooking.meetingUrl).toEqual(createdRecurringBooking[2].meetingUrl); + expect(thirdBooking.absentHost).toEqual(createdRecurringBooking[2].absentHost); + + createdRecurringBooking = data; + } else { + throw new Error( + "Invalid response data - expected recurring booking but received non array response" + ); + } + }); + }); + function responseDataIsBooking(data: any): data is BookingOutput_2024_08_13 { return !Array.isArray(data) && typeof data === "object" && data && "id" in data; } + function responseDataIsRecurranceBooking(data: any): data is RecurringBookingOutput_2024_08_13 { + return ( + !Array.isArray(data) && + typeof data === "object" && + data && + "id" in data && + "recurringBookingUid" in data + ); + } + function responseDataIsRecurringBooking(data: any): data is RecurringBookingOutput_2024_08_13[] { return Array.isArray(data); } From 65f159a05db9d42a682d880fffe66903e32bb93b Mon Sep 17 00:00:00 2001 From: supalarry Date: Wed, 28 Aug 2024 14:23:43 +0200 Subject: [PATCH 36/96] fix: cancel email sent if arePlatformEmailsEnabled=false but platformClientId is undefined --- packages/features/bookings/lib/handleCancelBooking.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/features/bookings/lib/handleCancelBooking.ts b/packages/features/bookings/lib/handleCancelBooking.ts index ba4b60b22a37f3..3d981e66bc814a 100644 --- a/packages/features/bookings/lib/handleCancelBooking.ts +++ b/packages/features/bookings/lib/handleCancelBooking.ts @@ -144,6 +144,7 @@ export type CustomRequest = NextApiRequest & { platformCancelUrl?: string; platformBookingUrl?: string; arePlatformEmailsEnabled?: boolean; + noEmail?: boolean; }; export type HandleCancelBookingResponse = { @@ -517,7 +518,7 @@ async function handler(req: CustomRequest) { try { // TODO: if emails fail try to requeue them - if (!platformClientId || (platformClientId && arePlatformEmailsEnabled)) + if ((!platformClientId && req.body.noEmail !== true) || (platformClientId && arePlatformEmailsEnabled)) await sendCancelledEmails( evt, { eventName: bookingToDelete?.eventType?.eventName }, From 6a0b82c7103cabcf6d40520db5ae0e14faecf019 Mon Sep 17 00:00:00 2001 From: supalarry Date: Wed, 28 Aug 2024 14:26:54 +0200 Subject: [PATCH 37/96] tests: cancel, reschedule, mark absent --- .../bookings.controller.e2e-spec.ts | 1014 +++++++++++++---- .../controllers/bookings.controller.ts | 3 +- .../outputs/reschedule-booking.output.ts | 14 + .../2024-08-13/services/bookings.service.ts | 46 +- .../2024-08-13/services/output.service.ts | 3 +- apps/api/v2/swagger/documentation.json | 45 +- .../repository/bookings.repository.fixture.ts | 10 + .../2024-08-13/inputs/get-bookings.input.ts | 10 +- 8 files changed, 893 insertions(+), 252 deletions(-) create mode 100644 apps/api/v2/src/ee/bookings/2024-08-13/outputs/reschedule-booking.output.ts diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.e2e-spec.ts b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.e2e-spec.ts index 86750dced74653..eba1a6c11ce889 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.e2e-spec.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.e2e-spec.ts @@ -2,6 +2,9 @@ import { bootstrap } from "@/app"; import { AppModule } from "@/app.module"; import { CreateBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/create-booking.output"; import { GetBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/get-booking.output"; +import { GetBookingsOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/get-bookings.output"; +import { MarkAbsentBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/mark-absent.output"; +import { RescheduleBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/reschedule-booking.output"; import { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input"; import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module"; import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service"; @@ -24,10 +27,14 @@ import { BookingOutput_2024_08_13, CreateRecurringBookingInput_2024_08_13, RecurringBookingOutput_2024_08_13, + RescheduleBookingInput_2024_08_13, + MarkAbsentBookingInput_2024_08_13, } from "@calcom/platform-types"; +import { CancelBookingInput_2024_08_13 } from "@calcom/platform-types"; +import { Booking } from "@calcom/prisma/client"; describe("Bookings Endpoints 2024-08-13", () => { - describe("User Authenticated", () => { + describe("User bookings", () => { let app: INestApplication; let userRepositoryFixture: UserRepositoryFixture; @@ -42,8 +49,11 @@ describe("Bookings Endpoints 2024-08-13", () => { let recurringEventTypeId: number; let createdBooking: BookingOutput_2024_08_13; + let rescheduledBooking: BookingOutput_2024_08_13; let createdRecurringBooking: RecurringBookingOutput_2024_08_13[]; + let bookingInThePast: Booking; + beforeAll(async () => { const moduleRef = await withApiAuth( userEmail, @@ -90,6 +100,38 @@ describe("Bookings Endpoints 2024-08-13", () => { ); recurringEventTypeId = recurringEvent.id; + bookingInThePast = await bookingsRepositoryFixture.create({ + user: { + connect: { + id: user.id, + }, + }, + startTime: new Date(Date.UTC(2020, 0, 8, 13, 0, 0)), + endTime: new Date(Date.UTC(2020, 0, 8, 14, 0, 0)), + title: "peer coding lets goo", + uid: "booking-in-the-past", + eventType: { + connect: { + id: eventTypeId, + }, + }, + location: "integrations:daily", + customInputs: {}, + metadata: {}, + responses: { + name: "Oldie", + email: "oldie@gmail.com", + }, + attendees: { + create: { + email: "oldie@gmail.com", + name: "Oldie", + locale: "lv", + timeZone: "Europe/Rome", + }, + }, + }); + app = moduleRef.createNestApplication(); bootstrap(app as NestExpressApplication); @@ -101,263 +143,775 @@ describe("Bookings Endpoints 2024-08-13", () => { expect(user).toBeDefined(); }); - it("should create a booking", async () => { - const body: CreateBookingInput_2024_08_13 = { - start: new Date(Date.UTC(2030, 0, 8, 13, 0, 0)).toISOString(), - eventTypeId, - attendee: { - name: "Mr Proper", - email: "mr_proper@gmail.com", - timeZone: "Europe/Rome", - language: "it", - }, - meetingUrl: "https://meet.google.com/abc-def-ghi", - }; + describe("create bookings", () => { + it("should create a booking", async () => { + const body: CreateBookingInput_2024_08_13 = { + start: new Date(Date.UTC(2030, 0, 8, 13, 0, 0)).toISOString(), + eventTypeId, + attendee: { + name: "Mr Proper", + email: "mr_proper@gmail.com", + timeZone: "Europe/Rome", + language: "it", + }, + meetingUrl: "https://meet.google.com/abc-def-ghi", + }; + + return request(app.getHttpServer()) + .post("/v2/bookings") + .send(body) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .expect(201) + .then(async (response) => { + const responseBody: CreateBookingOutput_2024_08_13 = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + expect(responseDataIsBooking(responseBody.data)).toBe(true); + + if (responseDataIsBooking(responseBody.data)) { + const data: BookingOutput_2024_08_13 = responseBody.data; + expect(data.id).toBeDefined(); + expect(data.uid).toBeDefined(); + expect(data.hostId).toEqual(user.id); + expect(data.status).toEqual("accepted"); + expect(data.start).toEqual(body.start); + expect(data.end).toEqual(new Date(Date.UTC(2030, 0, 8, 14, 0, 0)).toISOString()); + expect(data.duration).toEqual(60); + expect(data.eventTypeId).toEqual(eventTypeId); + expect(data.attendee).toEqual({ ...body.attendee, absent: false }); + expect(data.meetingUrl).toEqual(body.meetingUrl); + expect(data.absentHost).toEqual(false); + createdBooking = data; + } else { + throw new Error( + "Invalid response data - expected booking but received array of possibily recurring bookings" + ); + } + }); + }); - return request(app.getHttpServer()) - .post("/v2/bookings") - .send(body) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .expect(201) - .then(async (response) => { - const responseBody: CreateBookingOutput_2024_08_13 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - expect(responseDataIsBooking(responseBody.data)).toBe(true); - - if (responseDataIsBooking(responseBody.data)) { - const data: BookingOutput_2024_08_13 = responseBody.data; - expect(data.id).toBeDefined(); - expect(data.uid).toBeDefined(); - expect(data.hostId).toEqual(user.id); - expect(data.status).toEqual("accepted"); - expect(data.start).toEqual(body.start); - expect(data.end).toEqual(new Date(Date.UTC(2030, 0, 8, 14, 0, 0)).toISOString()); - expect(data.duration).toEqual(60); - expect(data.eventTypeId).toEqual(eventTypeId); - expect(data.attendee).toEqual({ ...body.attendee, absent: false }); - expect(data.meetingUrl).toEqual(body.meetingUrl); - expect(data.absentHost).toEqual(false); - createdBooking = data; - } else { - throw new Error( - "Invalid response data - expected booking but received array of possibily recurring bookings" - ); - } - }); + it("should create a recurring booking", async () => { + const body: CreateRecurringBookingInput_2024_08_13 = { + start: new Date(Date.UTC(2030, 1, 4, 13, 0, 0)).toISOString(), + eventTypeId: recurringEventTypeId, + attendee: { + name: "Mr Proper Recurring", + email: "mr_proper_recurring@gmail.com", + timeZone: "Europe/Rome", + language: "it", + }, + meetingUrl: "https://meet.google.com/abc-def-ghi", + }; + + return request(app.getHttpServer()) + .post("/v2/bookings") + .send(body) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .expect(201) + .then(async (response) => { + const responseBody: CreateBookingOutput_2024_08_13 = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + expect(responseDataIsRecurringBooking(responseBody.data)).toBe(true); + + if (responseDataIsRecurringBooking(responseBody.data)) { + const data: RecurringBookingOutput_2024_08_13[] = responseBody.data; + expect(data.length).toEqual(3); + + const firstBooking = data[0]; + expect(firstBooking.id).toBeDefined(); + expect(firstBooking.uid).toBeDefined(); + expect(firstBooking.hostId).toEqual(user.id); + expect(firstBooking.status).toEqual("accepted"); + expect(firstBooking.start).toEqual(new Date(Date.UTC(2030, 1, 4, 13, 0, 0)).toISOString()); + expect(firstBooking.end).toEqual(new Date(Date.UTC(2030, 1, 4, 14, 0, 0)).toISOString()); + expect(firstBooking.duration).toEqual(60); + expect(firstBooking.eventTypeId).toEqual(recurringEventTypeId); + expect(firstBooking.attendee).toEqual({ ...body.attendee, absent: false }); + expect(firstBooking.meetingUrl).toEqual(body.meetingUrl); + expect(firstBooking.recurringBookingUid).toBeDefined(); + expect(firstBooking.absentHost).toEqual(false); + + const secondBooking = data[1]; + expect(secondBooking.id).toBeDefined(); + expect(secondBooking.uid).toBeDefined(); + expect(secondBooking.hostId).toEqual(user.id); + expect(secondBooking.status).toEqual("accepted"); + expect(secondBooking.start).toEqual(new Date(Date.UTC(2030, 1, 11, 13, 0, 0)).toISOString()); + expect(secondBooking.end).toEqual(new Date(Date.UTC(2030, 1, 11, 14, 0, 0)).toISOString()); + expect(secondBooking.duration).toEqual(60); + expect(secondBooking.eventTypeId).toEqual(recurringEventTypeId); + expect(secondBooking.recurringBookingUid).toBeDefined(); + expect(secondBooking.attendee).toEqual({ ...body.attendee, absent: false }); + expect(secondBooking.meetingUrl).toEqual(body.meetingUrl); + expect(secondBooking.absentHost).toEqual(false); + + const thirdBooking = data[2]; + expect(thirdBooking.id).toBeDefined(); + expect(thirdBooking.uid).toBeDefined(); + expect(thirdBooking.hostId).toEqual(user.id); + expect(thirdBooking.status).toEqual("accepted"); + expect(thirdBooking.start).toEqual(new Date(Date.UTC(2030, 1, 18, 13, 0, 0)).toISOString()); + expect(thirdBooking.end).toEqual(new Date(Date.UTC(2030, 1, 18, 14, 0, 0)).toISOString()); + expect(thirdBooking.duration).toEqual(60); + expect(thirdBooking.eventTypeId).toEqual(recurringEventTypeId); + expect(thirdBooking.recurringBookingUid).toBeDefined(); + expect(thirdBooking.attendee).toEqual({ ...body.attendee, absent: false }); + expect(thirdBooking.meetingUrl).toEqual(body.meetingUrl); + expect(thirdBooking.absentHost).toEqual(false); + + createdRecurringBooking = data; + } else { + throw new Error( + "Invalid response data - expected recurring booking but received non array response" + ); + } + }); + }); }); - it("should create a recurring booking", async () => { - const body: CreateRecurringBookingInput_2024_08_13 = { - start: new Date(Date.UTC(2030, 1, 4, 13, 0, 0)).toISOString(), - eventTypeId: recurringEventTypeId, - attendee: { - name: "Mr Proper", - email: "mr_proper@gmail.com", - timeZone: "Europe/Rome", - language: "it", - }, - meetingUrl: "https://meet.google.com/abc-def-ghi", - }; + describe("get individual booking", () => { + it("should should get a booking", async () => { + return request(app.getHttpServer()) + .get(`/v2/bookings/${createdBooking.uid}`) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .expect(200) + .then(async (response) => { + const responseBody: GetBookingOutput_2024_08_13 = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + expect(responseDataIsBooking(responseBody.data)).toBe(true); + + if (responseDataIsBooking(responseBody.data)) { + const data: BookingOutput_2024_08_13 = responseBody.data; + expect(data.id).toEqual(createdBooking.id); + expect(data.uid).toEqual(createdBooking.uid); + expect(data.hostId).toEqual(user.id); + expect(data.status).toEqual(createdBooking.status); + expect(data.start).toEqual(createdBooking.start); + expect(data.end).toEqual(createdBooking.end); + expect(data.duration).toEqual(createdBooking.duration); + expect(data.eventTypeId).toEqual(createdBooking.eventTypeId); + expect(data.attendee).toEqual(createdBooking.attendee); + expect(data.meetingUrl).toEqual(createdBooking.meetingUrl); + expect(data.absentHost).toEqual(createdBooking.absentHost); + } else { + throw new Error( + "Invalid response data - expected booking but received array of possibily recurring bookings" + ); + } + }); + }); - return request(app.getHttpServer()) - .post("/v2/bookings") - .send(body) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .expect(201) - .then(async (response) => { - const responseBody: CreateBookingOutput_2024_08_13 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - expect(responseDataIsRecurringBooking(responseBody.data)).toBe(true); - - if (responseDataIsRecurringBooking(responseBody.data)) { - const data: RecurringBookingOutput_2024_08_13[] = responseBody.data; + it("should should get 1 recurrence of a recurring booking", async () => { + const recurrenceUid = createdRecurringBooking[0].uid; + return request(app.getHttpServer()) + .get(`/v2/bookings/${recurrenceUid}`) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .expect(200) + .then(async (response) => { + const responseBody: GetBookingOutput_2024_08_13 = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + expect(responseDataIsRecurranceBooking(responseBody.data)).toBe(true); + + if (responseDataIsRecurranceBooking(responseBody.data)) { + const data: RecurringBookingOutput_2024_08_13 = responseBody.data; + expect(data.id).toEqual(createdRecurringBooking[0].id); + expect(data.uid).toEqual(createdRecurringBooking[0].uid); + expect(data.hostId).toEqual(user.id); + expect(data.status).toEqual(createdRecurringBooking[0].status); + expect(data.start).toEqual(createdRecurringBooking[0].start); + expect(data.end).toEqual(createdRecurringBooking[0].end); + expect(data.duration).toEqual(createdRecurringBooking[0].duration); + expect(data.eventTypeId).toEqual(createdRecurringBooking[0].eventTypeId); + expect(data.recurringBookingUid).toEqual(createdRecurringBooking[0].recurringBookingUid); + expect(data.attendee).toEqual(createdRecurringBooking[0].attendee); + expect(data.meetingUrl).toEqual(createdRecurringBooking[0].meetingUrl); + expect(data.absentHost).toEqual(createdRecurringBooking[0].absentHost); + } else { + throw new Error( + "Invalid response data - expected booking but received array of possibily recurring bookings" + ); + } + }); + }); + + it("should should get all recurrences of the recurring bookings", async () => { + const recurringBookingUid = createdRecurringBooking[0].recurringBookingUid; + return request(app.getHttpServer()) + .get(`/v2/bookings/${recurringBookingUid}`) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .expect(200) + .then(async (response) => { + const responseBody: CreateBookingOutput_2024_08_13 = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + expect(responseDataIsRecurringBooking(responseBody.data)).toBe(true); + + if (responseDataIsRecurringBooking(responseBody.data)) { + const data: RecurringBookingOutput_2024_08_13[] = responseBody.data; + expect(data.length).toEqual(3); + + const firstBooking = data[0]; + expect(firstBooking.id).toEqual(createdRecurringBooking[0].id); + expect(firstBooking.uid).toEqual(createdRecurringBooking[0].uid); + expect(firstBooking.hostId).toEqual(user.id); + expect(firstBooking.status).toEqual(createdRecurringBooking[0].status); + expect(firstBooking.start).toEqual(createdRecurringBooking[0].start); + expect(firstBooking.end).toEqual(createdRecurringBooking[0].end); + expect(firstBooking.duration).toEqual(createdRecurringBooking[0].duration); + expect(firstBooking.eventTypeId).toEqual(createdRecurringBooking[0].eventTypeId); + expect(firstBooking.recurringBookingUid).toEqual(recurringBookingUid); + expect(firstBooking.attendee).toEqual(createdRecurringBooking[0].attendee); + expect(firstBooking.meetingUrl).toEqual(createdRecurringBooking[0].meetingUrl); + expect(firstBooking.absentHost).toEqual(createdRecurringBooking[0].absentHost); + + const secondBooking = data[1]; + expect(secondBooking.id).toEqual(createdRecurringBooking[1].id); + expect(secondBooking.uid).toEqual(createdRecurringBooking[1].uid); + expect(secondBooking.hostId).toEqual(user.id); + expect(secondBooking.status).toEqual(createdRecurringBooking[1].status); + expect(secondBooking.start).toEqual(createdRecurringBooking[1].start); + expect(secondBooking.end).toEqual(createdRecurringBooking[1].end); + expect(secondBooking.duration).toEqual(createdRecurringBooking[1].duration); + expect(secondBooking.eventTypeId).toEqual(createdRecurringBooking[1].eventTypeId); + expect(secondBooking.recurringBookingUid).toEqual(recurringBookingUid); + expect(secondBooking.attendee).toEqual(createdRecurringBooking[1].attendee); + expect(secondBooking.meetingUrl).toEqual(createdRecurringBooking[1].meetingUrl); + expect(secondBooking.absentHost).toEqual(createdRecurringBooking[1].absentHost); + + const thirdBooking = data[2]; + expect(thirdBooking.id).toEqual(createdRecurringBooking[2].id); + expect(thirdBooking.uid).toEqual(createdRecurringBooking[2].uid); + expect(thirdBooking.hostId).toEqual(user.id); + expect(thirdBooking.status).toEqual(createdRecurringBooking[2].status); + expect(thirdBooking.start).toEqual(createdRecurringBooking[2].start); + expect(thirdBooking.end).toEqual(createdRecurringBooking[2].end); + expect(thirdBooking.duration).toEqual(createdRecurringBooking[2].duration); + expect(thirdBooking.eventTypeId).toEqual(createdRecurringBooking[2].eventTypeId); + expect(thirdBooking.recurringBookingUid).toEqual(recurringBookingUid); + expect(thirdBooking.attendee).toEqual(createdRecurringBooking[2].attendee); + expect(thirdBooking.meetingUrl).toEqual(createdRecurringBooking[2].meetingUrl); + expect(thirdBooking.absentHost).toEqual(createdRecurringBooking[2].absentHost); + + createdRecurringBooking = data; + } else { + throw new Error( + "Invalid response data - expected recurring booking but received non array response" + ); + } + }); + }); + }); + + describe("get bookings", () => { + it("should should get all bookings", async () => { + return request(app.getHttpServer()) + .get(`/v2/bookings`) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .expect(200) + .then(async (response) => { + const responseBody: GetBookingsOutput_2024_08_13 = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + const data: (BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13)[] = responseBody.data; + expect(data.length).toEqual(5); + }); + }); + + it("should should take bookings", async () => { + return request(app.getHttpServer()) + .get(`/v2/bookings?take=3`) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .expect(200) + .then(async (response) => { + const responseBody: GetBookingsOutput_2024_08_13 = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + const data: (BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13)[] = responseBody.data; + expect(data.length).toEqual(3); + }); + }); + + it("should should skip bookings", async () => { + return request(app.getHttpServer()) + .get(`/v2/bookings?skip=2`) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .expect(200) + .then(async (response) => { + const responseBody: GetBookingsOutput_2024_08_13 = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + const data: (BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13)[] = responseBody.data; + expect(data.length).toEqual(3); + }); + }); + + it("should should get upcoming bookings", async () => { + return request(app.getHttpServer()) + .get(`/v2/bookings?status=upcoming`) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .expect(200) + .then(async (response) => { + const responseBody: GetBookingsOutput_2024_08_13 = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + const data: (BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13)[] = responseBody.data; + expect(data.length).toEqual(4); + }); + }); + + it("should should get past bookings", async () => { + return request(app.getHttpServer()) + .get(`/v2/bookings?status=past`) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .expect(200) + .then(async (response) => { + const responseBody: GetBookingsOutput_2024_08_13 = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + const data: (BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13)[] = responseBody.data; + expect(data.length).toEqual(1); + }); + }); + + it("should should get upcoming and past bookings", async () => { + return request(app.getHttpServer()) + .get(`/v2/bookings?status=upcoming,past`) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .expect(200) + .then(async (response) => { + const responseBody: GetBookingsOutput_2024_08_13 = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + const data: (BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13)[] = responseBody.data; + expect(data.length).toEqual(5); + }); + }); + + it("should should get recurring booking recurrences", async () => { + return request(app.getHttpServer()) + .get(`/v2/bookings?status=recurring`) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .expect(200) + .then(async (response) => { + const responseBody: GetBookingsOutput_2024_08_13 = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + const data: (BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13)[] = responseBody.data; expect(data.length).toEqual(3); + }); + }); + + it("should should get bookings by attendee email", async () => { + return request(app.getHttpServer()) + .get(`/v2/bookings?attendeeEmail=mr_proper@gmail.com`) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .expect(200) + .then(async (response) => { + const responseBody: GetBookingsOutput_2024_08_13 = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + const data: (BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13)[] = responseBody.data; + expect(data.length).toEqual(1); + }); + }); + + it("should should get bookings by attendee name", async () => { + return request(app.getHttpServer()) + .get(`/v2/bookings?attendeeName=Mr Proper Recurring`) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .expect(200) + .then(async (response) => { + const responseBody: GetBookingsOutput_2024_08_13 = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + const data: (BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13)[] = responseBody.data; + expect(data.length).toEqual(3); + }); + }); + + it("should should get bookings by eventTypeId", async () => { + return request(app.getHttpServer()) + .get(`/v2/bookings?eventTypeId=${eventTypeId}`) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .expect(200) + .then(async (response) => { + const responseBody: GetBookingsOutput_2024_08_13 = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + const data: (BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13)[] = responseBody.data; + expect(data.length).toEqual(2); + }); + }); + + it("should should get bookings by eventTypeIds", async () => { + return request(app.getHttpServer()) + .get(`/v2/bookings?eventTypeIds[]=${eventTypeId}&eventTypeIds[]=${recurringEventTypeId}`) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .expect(200) + .then(async (response) => { + const responseBody: GetBookingsOutput_2024_08_13 = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + const data: (BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13)[] = responseBody.data; + expect(data.length).toEqual(5); + }); + }); + + it("should should get bookings by after specified start time", async () => { + return request(app.getHttpServer()) + .get(`/v2/bookings?afterStart=${createdRecurringBooking[1].start}`) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .expect(200) + .then(async (response) => { + const responseBody: GetBookingsOutput_2024_08_13 = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + const data: (BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13)[] = responseBody.data; + expect(data.length).toEqual(2); + }); + }); - const firstBooking = data[0]; - expect(firstBooking.id).toBeDefined(); - expect(firstBooking.uid).toBeDefined(); - expect(firstBooking.hostId).toEqual(user.id); - expect(firstBooking.status).toEqual("accepted"); - expect(firstBooking.start).toEqual(new Date(Date.UTC(2030, 1, 4, 13, 0, 0)).toISOString()); - expect(firstBooking.end).toEqual(new Date(Date.UTC(2030, 1, 4, 14, 0, 0)).toISOString()); - expect(firstBooking.duration).toEqual(60); - expect(firstBooking.eventTypeId).toEqual(recurringEventTypeId); - expect(firstBooking.attendee).toEqual({ ...body.attendee, absent: false }); - expect(firstBooking.meetingUrl).toEqual(body.meetingUrl); - expect(firstBooking.recurringBookingUid).toBeDefined(); - expect(firstBooking.absentHost).toEqual(false); - - const secondBooking = data[1]; - expect(secondBooking.id).toBeDefined(); - expect(secondBooking.uid).toBeDefined(); - expect(secondBooking.hostId).toEqual(user.id); - expect(secondBooking.status).toEqual("accepted"); - expect(secondBooking.start).toEqual(new Date(Date.UTC(2030, 1, 11, 13, 0, 0)).toISOString()); - expect(secondBooking.end).toEqual(new Date(Date.UTC(2030, 1, 11, 14, 0, 0)).toISOString()); - expect(secondBooking.duration).toEqual(60); - expect(secondBooking.eventTypeId).toEqual(recurringEventTypeId); - expect(secondBooking.recurringBookingUid).toBeDefined(); - expect(secondBooking.attendee).toEqual({ ...body.attendee, absent: false }); - expect(secondBooking.meetingUrl).toEqual(body.meetingUrl); - expect(secondBooking.absentHost).toEqual(false); - - const thirdBooking = data[2]; - expect(thirdBooking.id).toBeDefined(); - expect(thirdBooking.uid).toBeDefined(); - expect(thirdBooking.hostId).toEqual(user.id); - expect(thirdBooking.status).toEqual("accepted"); - expect(thirdBooking.start).toEqual(new Date(Date.UTC(2030, 1, 18, 13, 0, 0)).toISOString()); - expect(thirdBooking.end).toEqual(new Date(Date.UTC(2030, 1, 18, 14, 0, 0)).toISOString()); - expect(thirdBooking.duration).toEqual(60); - expect(thirdBooking.eventTypeId).toEqual(recurringEventTypeId); - expect(thirdBooking.recurringBookingUid).toBeDefined(); - expect(thirdBooking.attendee).toEqual({ ...body.attendee, absent: false }); - expect(thirdBooking.meetingUrl).toEqual(body.meetingUrl); - expect(thirdBooking.absentHost).toEqual(false); - - createdRecurringBooking = data; - } else { - throw new Error( - "Invalid response data - expected recurring booking but received non array response" - ); - } - }); + it("should should get bookings by before specified end time", async () => { + return request(app.getHttpServer()) + .get(`/v2/bookings?afterStart=${createdRecurringBooking[0].start}`) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .expect(200) + .then(async (response) => { + const responseBody: GetBookingsOutput_2024_08_13 = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + const data: (BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13)[] = responseBody.data; + expect(data.length).toEqual(3); + }); + }); + + it("should should sort bookings by start in descending order", async () => { + return request(app.getHttpServer()) + .get(`/v2/bookings?eventTypeId=${eventTypeId}&sortStart=desc`) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .expect(200) + .then(async (response) => { + const responseBody: GetBookingsOutput_2024_08_13 = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + const data: (BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13)[] = responseBody.data; + expect(data.length).toEqual(2); + expect(data[0].start).toEqual(createdBooking.start); + expect(data[1].start).toEqual(bookingInThePast.startTime.toISOString()); + }); + }); + + it("should should sort bookings by start in ascending order", async () => { + return request(app.getHttpServer()) + .get(`/v2/bookings?eventTypeId=${eventTypeId}&sortStart=asc`) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .expect(200) + .then(async (response) => { + const responseBody: GetBookingsOutput_2024_08_13 = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + const data: (BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13)[] = responseBody.data; + expect(data.length).toEqual(2); + expect(data[0].start).toEqual(bookingInThePast.startTime.toISOString()); + expect(data[1].start).toEqual(createdBooking.start); + }); + }); + + it("should should sort bookings by end in descending order", async () => { + return request(app.getHttpServer()) + .get(`/v2/bookings?eventTypeId=${eventTypeId}&sortEnd=desc`) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .expect(200) + .then(async (response) => { + const responseBody: GetBookingsOutput_2024_08_13 = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + const data: (BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13)[] = responseBody.data; + expect(data.length).toEqual(2); + expect(data[0].start).toEqual(createdBooking.start); + expect(data[1].start).toEqual(bookingInThePast.startTime.toISOString()); + }); + }); + + it("should should sort bookings by end in ascending order", async () => { + return request(app.getHttpServer()) + .get(`/v2/bookings?eventTypeId=${eventTypeId}&sortEnd=asc`) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .expect(200) + .then(async (response) => { + const responseBody: GetBookingsOutput_2024_08_13 = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + const data: (BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13)[] = responseBody.data; + expect(data.length).toEqual(2); + expect(data[0].start).toEqual(bookingInThePast.startTime.toISOString()); + expect(data[1].start).toEqual(createdBooking.start); + }); + }); + + it("should should sort bookings by created in descending order", async () => { + return request(app.getHttpServer()) + .get(`/v2/bookings?eventTypeId=${eventTypeId}&sortCreated=desc`) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .expect(200) + .then(async (response) => { + const responseBody: GetBookingsOutput_2024_08_13 = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + const data: (BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13)[] = responseBody.data; + expect(data.length).toEqual(2); + expect(data[0].start).toEqual(createdBooking.start); + expect(data[1].start).toEqual(bookingInThePast.startTime.toISOString()); + }); + }); + + it("should should sort bookings by created in ascending order", async () => { + return request(app.getHttpServer()) + .get(`/v2/bookings?eventTypeId=${eventTypeId}&sortCreated=asc`) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .expect(200) + .then(async (response) => { + const responseBody: GetBookingsOutput_2024_08_13 = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + const data: (BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13)[] = responseBody.data; + expect(data.length).toEqual(2); + expect(data[0].start).toEqual(bookingInThePast.startTime.toISOString()); + expect(data[1].start).toEqual(createdBooking.start); + }); + }); }); - it("should should get a booking", async () => { - return request(app.getHttpServer()) - .get(`/v2/bookings/${createdBooking.uid}`) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .expect(200) - .then(async (response) => { - const responseBody: GetBookingOutput_2024_08_13 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - expect(responseDataIsBooking(responseBody.data)).toBe(true); - - if (responseDataIsBooking(responseBody.data)) { + describe("reschedule bookings", () => { + it("should should reschedule normal booking", async () => { + const body: RescheduleBookingInput_2024_08_13 = { + start: new Date(Date.UTC(2035, 0, 8, 14, 0, 0)).toISOString(), + reschedulingReason: "Flying to mars that day", + }; + + return request(app.getHttpServer()) + .post(`/v2/bookings/${createdBooking.uid}/reschedule`) + .send(body) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .expect(201) + .then(async (response) => { + const responseBody: RescheduleBookingOutput_2024_08_13 = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); const data: BookingOutput_2024_08_13 = responseBody.data; - expect(data.id).toEqual(createdBooking.id); - expect(data.uid).toEqual(createdBooking.uid); + expect(data.reschedulingReason).toEqual(body.reschedulingReason); + expect(data.start).toEqual(body.start); + expect(data.end).toEqual(new Date(Date.UTC(2035, 0, 8, 15, 0, 0)).toISOString()); + expect(data.rescheduledFromUid).toEqual(createdBooking.uid); + expect(data.id).toBeDefined(); + expect(data.uid).toBeDefined(); expect(data.hostId).toEqual(user.id); expect(data.status).toEqual(createdBooking.status); - expect(data.start).toEqual(createdBooking.start); - expect(data.end).toEqual(createdBooking.end); expect(data.duration).toEqual(createdBooking.duration); expect(data.eventTypeId).toEqual(createdBooking.eventTypeId); expect(data.attendee).toEqual(createdBooking.attendee); expect(data.meetingUrl).toEqual(createdBooking.meetingUrl); expect(data.absentHost).toEqual(createdBooking.absentHost); - } else { - throw new Error( - "Invalid response data - expected booking but received array of possibily recurring bookings" - ); - } - }); - }); - it("should should get 1 recurrence of a recurring booking", async () => { - const recurrenceUid = createdRecurringBooking[0].uid; - return request(app.getHttpServer()) - .get(`/v2/bookings/${recurrenceUid}`) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .expect(200) - .then(async (response) => { - const responseBody: GetBookingOutput_2024_08_13 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - expect(responseDataIsRecurranceBooking(responseBody.data)).toBe(true); - - if (responseDataIsRecurranceBooking(responseBody.data)) { + rescheduledBooking = data; + }); + }); + + it("should point rescheduled booking to the new one", async () => { + return request(app.getHttpServer()) + .get(`/v2/bookings/${createdBooking.uid}`) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .expect(200) + .then(async (response) => { + const responseBody: RescheduleBookingOutput_2024_08_13 = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + const data: BookingOutput_2024_08_13 = responseBody.data; + expect(data.rescheduledToUid).toEqual(rescheduledBooking.uid); + expect(data.status).toEqual("rescheduled"); + + createdBooking = data; + }); + }); + + it("should reschedule recurrence of a recurring booking", async () => { + const body: RescheduleBookingInput_2024_08_13 = { + start: new Date(Date.UTC(2035, 0, 9, 14, 0, 0)).toISOString(), + reschedulingReason: "Flying to mars again", + }; + + return request(app.getHttpServer()) + .post(`/v2/bookings/${createdRecurringBooking[0].uid}/reschedule`) + .send(body) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .expect(201) + .then(async (response) => { + const responseBody: RescheduleBookingOutput_2024_08_13 = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore const data: RecurringBookingOutput_2024_08_13 = responseBody.data; - expect(data.id).toEqual(createdRecurringBooking[0].id); - expect(data.uid).toEqual(createdRecurringBooking[0].uid); + expect(data.id).toBeDefined(); + expect(data.uid).toBeDefined(); expect(data.hostId).toEqual(user.id); expect(data.status).toEqual(createdRecurringBooking[0].status); - expect(data.start).toEqual(createdRecurringBooking[0].start); - expect(data.end).toEqual(createdRecurringBooking[0].end); + expect(data.start).toEqual(body.start); + expect(data.end).toEqual(new Date(Date.UTC(2035, 0, 9, 15, 0, 0)).toISOString()); expect(data.duration).toEqual(createdRecurringBooking[0].duration); - expect(data.eventTypeId).toEqual(createdRecurringBooking[0].eventTypeId); expect(data.recurringBookingUid).toEqual(createdRecurringBooking[0].recurringBookingUid); + expect(data.eventTypeId).toEqual(createdRecurringBooking[0].eventTypeId); expect(data.attendee).toEqual(createdRecurringBooking[0].attendee); expect(data.meetingUrl).toEqual(createdRecurringBooking[0].meetingUrl); expect(data.absentHost).toEqual(createdRecurringBooking[0].absentHost); - createdBooking = data; - } else { - throw new Error( - "Invalid response data - expected booking but received array of possibily recurring bookings" - ); - } - }); + + const oldBooking = await bookingsRepositoryFixture.getByUid(createdRecurringBooking[0].uid); + expect(oldBooking).toBeDefined(); + expect(oldBooking?.status).toEqual("CANCELLED"); + }); + }); + + it("should get recurring booking recurrences after rescheduling one", async () => { + const recurringBookingUid = createdRecurringBooking[0].recurringBookingUid; + return request(app.getHttpServer()) + .get(`/v2/bookings/${recurringBookingUid}`) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .expect(200) + .then(async (response) => { + const responseBody: CreateBookingOutput_2024_08_13 = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + expect(responseDataIsRecurringBooking(responseBody.data)).toBe(true); + + if (responseDataIsRecurringBooking(responseBody.data)) { + const data: RecurringBookingOutput_2024_08_13[] = responseBody.data; + expect(data.length).toEqual(4); + const cancelled = data.find((booking) => booking.status === "cancelled"); + expect(cancelled).toBeDefined(); + const rescheduledNew = data.find( + (booking) => booking.start === new Date(Date.UTC(2035, 0, 9, 14, 0, 0)).toISOString() + ); + expect(rescheduledNew).toBeDefined(); + createdRecurringBooking = data; + } else { + throw new Error( + "Invalid response data - expected recurring booking but received non array response" + ); + } + }); + }); }); - it("should should all recurrences of the recurring bookings", async () => { - const recurringBookingUid = createdRecurringBooking[0].recurringBookingUid; - return request(app.getHttpServer()) - .get(`/v2/bookings/${recurringBookingUid}`) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .expect(200) - .then(async (response) => { - const responseBody: CreateBookingOutput_2024_08_13 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - expect(responseDataIsRecurringBooking(responseBody.data)).toBe(true); - - if (responseDataIsRecurringBooking(responseBody.data)) { - const data: RecurringBookingOutput_2024_08_13[] = responseBody.data; - expect(data.length).toEqual(3); + describe("mark absent", () => { + it("should mark host absent", async () => { + const body: MarkAbsentBookingInput_2024_08_13 = { + host: true, + }; + + return request(app.getHttpServer()) + .post(`/v2/bookings/${createdRecurringBooking[1].uid}/mark-absent`) + .send(body) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .expect(200) + .then(async (response) => { + const responseBody: MarkAbsentBookingOutput_2024_08_13 = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const data: RecurringBookingOutput_2024_08_13 = responseBody.data; + const booking = createdRecurringBooking[1]; + expect(data.absentHost).toEqual(true); - const firstBooking = data[0]; - expect(firstBooking.id).toEqual(createdRecurringBooking[0].id); - expect(firstBooking.uid).toEqual(createdRecurringBooking[0].uid); - expect(firstBooking.hostId).toEqual(user.id); - expect(firstBooking.status).toEqual(createdRecurringBooking[0].status); - expect(firstBooking.start).toEqual(createdRecurringBooking[0].start); - expect(firstBooking.end).toEqual(createdRecurringBooking[0].end); - expect(firstBooking.duration).toEqual(createdRecurringBooking[0].duration); - expect(firstBooking.eventTypeId).toEqual(createdRecurringBooking[0].eventTypeId); - expect(firstBooking.recurringBookingUid).toEqual(recurringBookingUid); - expect(firstBooking.attendee).toEqual(createdRecurringBooking[0].attendee); - expect(firstBooking.meetingUrl).toEqual(createdRecurringBooking[0].meetingUrl); - expect(firstBooking.absentHost).toEqual(createdRecurringBooking[0].absentHost); - - const secondBooking = data[1]; - expect(secondBooking.id).toEqual(createdRecurringBooking[1].id); - expect(secondBooking.uid).toEqual(createdRecurringBooking[1].uid); - expect(secondBooking.hostId).toEqual(user.id); - expect(secondBooking.status).toEqual(createdRecurringBooking[1].status); - expect(secondBooking.start).toEqual(createdRecurringBooking[1].start); - expect(secondBooking.end).toEqual(createdRecurringBooking[1].end); - expect(secondBooking.duration).toEqual(createdRecurringBooking[1].duration); - expect(secondBooking.eventTypeId).toEqual(createdRecurringBooking[1].eventTypeId); - expect(secondBooking.recurringBookingUid).toEqual(recurringBookingUid); - expect(secondBooking.attendee).toEqual(createdRecurringBooking[1].attendee); - expect(secondBooking.meetingUrl).toEqual(createdRecurringBooking[1].meetingUrl); - expect(secondBooking.absentHost).toEqual(createdRecurringBooking[1].absentHost); - - const thirdBooking = data[2]; - expect(thirdBooking.id).toEqual(createdRecurringBooking[2].id); - expect(thirdBooking.uid).toEqual(createdRecurringBooking[2].uid); - expect(thirdBooking.hostId).toEqual(user.id); - expect(thirdBooking.status).toEqual(createdRecurringBooking[2].status); - expect(thirdBooking.start).toEqual(createdRecurringBooking[2].start); - expect(thirdBooking.end).toEqual(createdRecurringBooking[2].end); - expect(thirdBooking.duration).toEqual(createdRecurringBooking[2].duration); - expect(thirdBooking.eventTypeId).toEqual(createdRecurringBooking[2].eventTypeId); - expect(thirdBooking.recurringBookingUid).toEqual(recurringBookingUid); - expect(thirdBooking.attendee).toEqual(createdRecurringBooking[2].attendee); - expect(thirdBooking.meetingUrl).toEqual(createdRecurringBooking[2].meetingUrl); - expect(thirdBooking.absentHost).toEqual(createdRecurringBooking[2].absentHost); - - createdRecurringBooking = data; - } else { - throw new Error( - "Invalid response data - expected recurring booking but received non array response" - ); - } - }); + expect(data.id).toEqual(booking.id); + expect(data.uid).toEqual(booking.uid); + expect(data.hostId).toEqual(user.id); + expect(data.status).toEqual(booking.status); + expect(data.start).toEqual(booking.start); + expect(data.end).toEqual(booking.end); + expect(data.duration).toEqual(booking.duration); + expect(data.eventTypeId).toEqual(booking.eventTypeId); + expect(data.attendee).toEqual(booking.attendee); + expect(data.meetingUrl).toEqual(booking.meetingUrl); + }); + }); + + it("should mark attendee absent", async () => { + const body: MarkAbsentBookingInput_2024_08_13 = { + attendees: ["mr_proper_recurring@gmail.com"], + }; + + return request(app.getHttpServer()) + .post(`/v2/bookings/${createdRecurringBooking[2].uid}/mark-absent`) + .send(body) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .expect(200) + .then(async (response) => { + const responseBody: MarkAbsentBookingOutput_2024_08_13 = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const data: RecurringBookingOutput_2024_08_13 = responseBody.data; + const booking = createdRecurringBooking[2]; + + expect(data.id).toEqual(booking.id); + expect(data.uid).toEqual(booking.uid); + expect(data.hostId).toEqual(user.id); + expect(data.status).toEqual(booking.status); + expect(data.start).toEqual(booking.start); + expect(data.end).toEqual(booking.end); + expect(data.duration).toEqual(booking.duration); + expect(data.eventTypeId).toEqual(booking.eventTypeId); + expect(data.attendee.absent).toEqual(true); + expect(data.absentHost).toEqual(booking.absentHost); + expect(data.meetingUrl).toEqual(booking.meetingUrl); + }); + }); + }); + describe("cancel bookings", () => { + it("should cancel booking", async () => { + const body: CancelBookingInput_2024_08_13 = { + cancellationReason: "Going on a vacation", + }; + + const booking = await bookingsRepositoryFixture.getByUid(rescheduledBooking.uid); + expect(booking).toBeDefined(); + expect(booking?.status).toEqual("ACCEPTED"); + + return request(app.getHttpServer()) + .post(`/v2/bookings/${rescheduledBooking.uid}/cancel`) + .send(body) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .expect(200) + .then(async (response) => { + const responseBody: RescheduleBookingOutput_2024_08_13 = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const data: RecurringBookingOutput_2024_08_13 = responseBody.data; + expect(data.id).toBeDefined(); + expect(data.uid).toBeDefined(); + expect(data.hostId).toEqual(user.id); + expect(data.status).toEqual("cancelled"); + expect(data.cancellationReason).toEqual(body.cancellationReason); + expect(data.start).toEqual(rescheduledBooking.start); + expect(data.end).toEqual(rescheduledBooking.end); + expect(data.duration).toEqual(rescheduledBooking.duration); + expect(data.eventTypeId).toEqual(rescheduledBooking.eventTypeId); + expect(data.attendee).toEqual(rescheduledBooking.attendee); + expect(data.meetingUrl).toEqual(rescheduledBooking.meetingUrl); + expect(data.absentHost).toEqual(rescheduledBooking.absentHost); + + const cancelledBooking = await bookingsRepositoryFixture.getByUid(rescheduledBooking.uid); + expect(cancelledBooking).toBeDefined(); + expect(cancelledBooking?.status).toEqual("CANCELLED"); + }); + }); }); function responseDataIsBooking(data: any): data is BookingOutput_2024_08_13 { diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.ts b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.ts index cf1fc77008c939..fecebc7fe23084 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.ts @@ -3,6 +3,7 @@ import { CreateBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs import { GetBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/get-booking.output"; import { GetBookingsOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/get-bookings.output"; import { MarkAbsentBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/mark-absent.output"; +import { RescheduleBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/reschedule-booking.output"; import { BookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/bookings.service"; import { VERSION_2024_08_13_VALUE } from "@/lib/api-versions"; import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator"; @@ -102,7 +103,7 @@ export class BookingsController_2024_08_13 { @Param("bookingUid") bookingUid: string, @Body() body: RescheduleBookingInput_2024_08_13, @Req() request: Request - ): Promise { + ): Promise { if (!bookingUid) { throw new BadRequestException("Booking UID is required in request path /:bookingUid/reschedule"); } diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/outputs/reschedule-booking.output.ts b/apps/api/v2/src/ee/bookings/2024-08-13/outputs/reschedule-booking.output.ts new file mode 100644 index 00000000000000..7585792469f356 --- /dev/null +++ b/apps/api/v2/src/ee/bookings/2024-08-13/outputs/reschedule-booking.output.ts @@ -0,0 +1,14 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { IsEnum, ValidateNested } from "class-validator"; + +import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants"; +import { BookingOutput_2024_08_13, RecurringBookingOutput_2024_08_13 } from "@calcom/platform-types"; + +export class RescheduleBookingOutput_2024_08_13 { + @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) + @IsEnum([SUCCESS_STATUS, ERROR_STATUS]) + status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS; + + @ValidateNested() + data!: BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13; +} diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts index 6e9395654daa17..c1c21ec655617d 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts @@ -128,9 +128,6 @@ export class BookingsService_2024_08_13 { if (booking) { const isRecurring = !!booking.recurringEventId; const isRescheduled = booking.rescheduled; - if (isRecurring) { - return this.outputService.getOutputRecurringBooking(booking); - } if (isRescheduled) { const toReschedule = await this.bookingsRepository.getByFromReschedule(uid); if (!toReschedule) { @@ -138,6 +135,9 @@ export class BookingsService_2024_08_13 { } return this.outputService.getOutputRescheduledBooking(booking, toReschedule); } + if (isRecurring) { + return this.outputService.getOutputRecurringBooking(booking); + } return this.outputService.getOutputBooking(booking); } @@ -152,8 +152,8 @@ export class BookingsService_2024_08_13 { async getBookings(queryParams: GetBookingsInput_2024_08_13, user: { email: string; id: number }) { const fetchedBookings: { bookings: BookingWithAttendeesAndEventType[] } = await getAllUserBookings({ bookingListingByStatus: queryParams.status || [], - skip: queryParams.cursor ?? 0, - take: queryParams.limit ?? 10, + skip: queryParams.skip ?? 0, + take: queryParams.take ?? 100, // todo: add filters here like by eventtype id etc filters: this.inputService.transformGetBookingsFilters(queryParams), ctx: { @@ -181,18 +181,34 @@ export class BookingsService_2024_08_13 { } async rescheduleBooking(request: Request, bookingUid: string, body: RescheduleBookingInput_2024_08_13) { - const bookingRequest = await this.inputService.createRescheduleBookingRequest(request, bookingUid, body); - const booking = await handleNewBooking(bookingRequest); - if (!booking.id) { - throw new Error("Booking was not created"); - } + try { + const bookingRequest = await this.inputService.createRescheduleBookingRequest( + request, + bookingUid, + body + ); + const booking = await handleNewBooking(bookingRequest); + if (!booking.id) { + throw new Error("Booking was not created"); + } - const databaseBooking = await this.bookingsRepository.getByIdWithAttendees(booking.id); - if (!databaseBooking) { - throw new Error(`Booking with id=${booking.id} was not found in the database`); - } + const databaseBooking = await this.bookingsRepository.getByIdWithAttendees(booking.id); + if (!databaseBooking) { + throw new Error(`Booking with id=${booking.id} was not found in the database`); + } - return this.outputService.getOutputBooking(databaseBooking); + if (databaseBooking.recurringEventId) { + return this.outputService.getOutputRecurringBooking(databaseBooking); + } + return this.outputService.getOutputBooking(databaseBooking); + } catch (error) { + if (error instanceof Error) { + if (error.message === "no_available_users_found_error") { + throw new BadRequestException("User either already has booking at this time or is not available"); + } + } + throw error; + } } async cancelBooking(request: Request, bookingUid: string, body: CancelBookingInput_2024_08_13) { diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/output.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/output.service.ts index 3e5969d95893c5..e417bb208f4893 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/services/output.service.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/services/output.service.ts @@ -116,6 +116,7 @@ export class OutputBookingsService_2024_08_13 { reschedulingReason: bookingResponsesNew?.rescheduledReason, rescheduledFromUid: oldDatabaseBooking.fromReschedule || undefined, rescheduledToUid: newDatabaseBooking.uid, + recurringBookingUid: newDatabaseBooking.recurringEventId || undefined, start: oldDatabaseBooking.startTime, end: oldDatabaseBooking.endTime, duration, @@ -161,7 +162,7 @@ export class OutputBookingsService_2024_08_13 { transformed.push(this.getOutputRecurringBooking(databaseBooking)); } - return transformed; + return transformed.sort((a, b) => new Date(a.start).getTime() - new Date(b.start).getTime()); } getOutputRecurringBooking( diff --git a/apps/api/v2/swagger/documentation.json b/apps/api/v2/swagger/documentation.json index fe48e90d737af1..ce52a7703e2e46 100644 --- a/apps/api/v2/swagger/documentation.json +++ b/apps/api/v2/swagger/documentation.json @@ -2849,7 +2849,28 @@ "/v2/bookings": { "get": { "operationId": "BookingsController_2024_08_13_getBookings", - "parameters": [], + "parameters": [ + { + "name": "take", + "required": false, + "in": "query", + "description": "The number of items to return", + "example": 10, + "schema": { + "type": "number" + } + }, + { + "name": "skip", + "required": false, + "in": "query", + "description": "The number of items to skip", + "example": 0, + "schema": { + "type": "number" + } + } + ], "responses": { "200": { "description": "", @@ -2973,7 +2994,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CreateBookingOutput_2024_08_13" + "$ref": "#/components/schemas/RescheduleBookingOutput_2024_08_13" } } } @@ -9205,6 +9226,26 @@ "type": "object", "properties": {} }, + "RescheduleBookingOutput_2024_08_13": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "type": "object" + } + }, + "required": [ + "status", + "data" + ] + }, "CancelBookingInput_2024_08_13": { "type": "object", "properties": { diff --git a/apps/api/v2/test/fixtures/repository/bookings.repository.fixture.ts b/apps/api/v2/test/fixtures/repository/bookings.repository.fixture.ts index 83b7e5f21a6664..5bc6ccacb554da 100644 --- a/apps/api/v2/test/fixtures/repository/bookings.repository.fixture.ts +++ b/apps/api/v2/test/fixtures/repository/bookings.repository.fixture.ts @@ -3,6 +3,8 @@ import { PrismaWriteService } from "@/modules/prisma/prisma-write.service"; import { TestingModule } from "@nestjs/testing"; import { Booking, User } from "@prisma/client"; +import { Prisma } from "@calcom/prisma/client"; + export class BookingsRepositoryFixture { private prismaReadClient: PrismaReadService["prisma"]; private prismaWriteClient: PrismaWriteService["prisma"]; @@ -16,6 +18,14 @@ export class BookingsRepositoryFixture { return this.prismaReadClient.booking.findFirst({ where: { id: bookingId } }); } + async getByUid(bookingUid: Booking["uid"]) { + return this.prismaReadClient.booking.findUnique({ where: { uid: bookingUid } }); + } + + async create(booking: Prisma.BookingCreateInput) { + return this.prismaWriteClient.booking.create({ data: booking }); + } + async deleteById(bookingId: Booking["id"]) { return this.prismaWriteClient.booking.delete({ where: { id: bookingId } }); } diff --git a/packages/platform/types/bookings/2024-08-13/inputs/get-bookings.input.ts b/packages/platform/types/bookings/2024-08-13/inputs/get-bookings.input.ts index 080e4be91b1339..8835464db75ae7 100644 --- a/packages/platform/types/bookings/2024-08-13/inputs/get-bookings.input.ts +++ b/packages/platform/types/bookings/2024-08-13/inputs/get-bookings.input.ts @@ -1,3 +1,4 @@ +import { ApiProperty } from "@nestjs/swagger"; import { Transform, Type } from "class-transformer"; import { ArrayMinSize, @@ -102,15 +103,18 @@ export class GetBookingsInput_2024_08_13 { sortCreated?: SortOrderType; // note(Lauris): pagination + @ApiProperty({ required: false, description: "The number of items to return", example: 10 }) @Transform(({ value }: { value: string }) => value && parseInt(value)) @IsNumber() @Min(1) - @Max(100) + @Max(250) @IsOptional() - limit?: number; + take?: number; + @ApiProperty({ required: false, description: "The number of items to skip", example: 0 }) @Transform(({ value }: { value: string }) => value && parseInt(value)) @IsNumber() + @Min(0) @IsOptional() - cursor?: number; + skip?: number; } From b02bf75433f169f87e42d45240c78dbf0a7465d4 Mon Sep 17 00:00:00 2001 From: supalarry Date: Wed, 28 Aug 2024 14:35:33 +0200 Subject: [PATCH 38/96] fix: generateIcsFile null pointer exception --- packages/emails/lib/generateIcsFile.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/emails/lib/generateIcsFile.ts b/packages/emails/lib/generateIcsFile.ts index cb63bbadfa9be6..2c2745393653da 100644 --- a/packages/emails/lib/generateIcsFile.ts +++ b/packages/emails/lib/generateIcsFile.ts @@ -31,6 +31,7 @@ export default function generateIcsFile({ if ( role !== GenerateIcsRole.ATTENDEE && calEvent.destinationCalendar && + calEvent.destinationCalendar.length && calEvent.destinationCalendar[0].integration === "office365_calendar" ) return null; From 80684a77924af010280d38d0095badc9fd77de9e Mon Sep 17 00:00:00 2001 From: supalarry Date: Wed, 28 Aug 2024 14:48:47 +0200 Subject: [PATCH 39/96] cancel test --- .../bookings.controller.e2e-spec.ts | 36 +++++++++++++++++-- .../bookings/lib/handleCancelBooking.ts | 3 +- 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.e2e-spec.ts b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.e2e-spec.ts index eba1a6c11ce889..eb5231feb3bc34 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.e2e-spec.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.e2e-spec.ts @@ -18,10 +18,17 @@ import { User } from "@prisma/client"; import * as request from "supertest"; import { BookingsRepositoryFixture } from "test/fixtures/repository/bookings.repository.fixture"; import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture"; +import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture"; +import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture"; import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture"; import { withApiAuth } from "test/utils/withApiAuth"; -import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants"; +import { + CAL_API_VERSION_HEADER, + SUCCESS_STATUS, + VERSION_2024_08_13, + X_CAL_CLIENT_ID, +} from "@calcom/platform-constants"; import { CreateBookingInput_2024_08_13, BookingOutput_2024_08_13, @@ -31,16 +38,20 @@ import { MarkAbsentBookingInput_2024_08_13, } from "@calcom/platform-types"; import { CancelBookingInput_2024_08_13 } from "@calcom/platform-types"; -import { Booking } from "@calcom/prisma/client"; +import { Booking, PlatformOAuthClient, Team } from "@calcom/prisma/client"; describe("Bookings Endpoints 2024-08-13", () => { describe("User bookings", () => { let app: INestApplication; + let organization: Team; let userRepositoryFixture: UserRepositoryFixture; let bookingsRepositoryFixture: BookingsRepositoryFixture; let schedulesService: SchedulesService_2024_04_15; let eventTypesRepositoryFixture: EventTypesRepositoryFixture; + let oauthClientRepositoryFixture: OAuthClientRepositoryFixture; + let oAuthClient: PlatformOAuthClient; + let teamRepositoryFixture: TeamRepositoryFixture; const userEmail = "bookings-controller-e2e@api.com"; let user: User; @@ -70,8 +81,13 @@ describe("Bookings Endpoints 2024-08-13", () => { userRepositoryFixture = new UserRepositoryFixture(moduleRef); bookingsRepositoryFixture = new BookingsRepositoryFixture(moduleRef); eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef); + oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef); + teamRepositoryFixture = new TeamRepositoryFixture(moduleRef); schedulesService = moduleRef.get(SchedulesService_2024_04_15); + organization = await teamRepositoryFixture.create({ name: "organization bookings" }); + oAuthClient = await createOAuthClient(organization.id); + user = await userRepositoryFixture.create({ email: userEmail, }); @@ -138,6 +154,19 @@ describe("Bookings Endpoints 2024-08-13", () => { await app.init(); }); + async function createOAuthClient(organizationId: number) { + const data = { + logo: "logo-url", + name: "name", + redirectUris: ["http://localhost:5555"], + permissions: 32, + }; + const secret = "secret"; + + const client = await oauthClientRepositoryFixture.create(organizationId, data, secret); + return client; + } + it("should be defined", () => { expect(userRepositoryFixture).toBeDefined(); expect(user).toBeDefined(); @@ -886,6 +915,7 @@ describe("Bookings Endpoints 2024-08-13", () => { .post(`/v2/bookings/${rescheduledBooking.uid}/cancel`) .send(body) .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .set(X_CAL_CLIENT_ID, oAuthClient.id) .expect(200) .then(async (response) => { const responseBody: RescheduleBookingOutput_2024_08_13 = response.body; @@ -933,6 +963,8 @@ describe("Bookings Endpoints 2024-08-13", () => { } afterAll(async () => { + await oauthClientRepositoryFixture.delete(oAuthClient.id); + await teamRepositoryFixture.delete(organization.id); await userRepositoryFixture.deleteByEmail(user.email); await bookingsRepositoryFixture.deleteAllBookings(user.id, user.email); await app.close(); diff --git a/packages/features/bookings/lib/handleCancelBooking.ts b/packages/features/bookings/lib/handleCancelBooking.ts index 3d981e66bc814a..ba4b60b22a37f3 100644 --- a/packages/features/bookings/lib/handleCancelBooking.ts +++ b/packages/features/bookings/lib/handleCancelBooking.ts @@ -144,7 +144,6 @@ export type CustomRequest = NextApiRequest & { platformCancelUrl?: string; platformBookingUrl?: string; arePlatformEmailsEnabled?: boolean; - noEmail?: boolean; }; export type HandleCancelBookingResponse = { @@ -518,7 +517,7 @@ async function handler(req: CustomRequest) { try { // TODO: if emails fail try to requeue them - if ((!platformClientId && req.body.noEmail !== true) || (platformClientId && arePlatformEmailsEnabled)) + if (!platformClientId || (platformClientId && arePlatformEmailsEnabled)) await sendCancelledEmails( evt, { eventName: bookingToDelete?.eventType?.eventName }, From 940ba3c8e9e663f2837b387a255961554a76993a Mon Sep 17 00:00:00 2001 From: supalarry Date: Wed, 28 Aug 2024 15:15:47 +0200 Subject: [PATCH 40/96] error msg improve --- .../src/ee/bookings/2024-08-13/services/bookings.service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts index c1c21ec655617d..5c5cf34cb605a9 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts @@ -4,7 +4,7 @@ import { OutputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/servi import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository"; import { BillingService } from "@/modules/billing/services/billing.service"; import { PrismaReadService } from "@/modules/prisma/prisma-read.service"; -import { Injectable } from "@nestjs/common"; +import { Injectable, NotFoundException } from "@nestjs/common"; import { BadRequestException } from "@nestjs/common"; import { Request } from "express"; @@ -143,7 +143,7 @@ export class BookingsService_2024_08_13 { const recurringBooking = await this.bookingsRepository.getRecurringByUidWithAttendees(uid); if (!recurringBooking.length) { - throw new Error(`Booking with uid=${uid} was not found in the database`); + throw new NotFoundException(`Booking with uid=${uid} was not found in the database`); } return this.outputService.getOutputRecurringBookings(recurringBooking); From ae73003ecee7944a945a861cd309c7fac85fb2c6 Mon Sep 17 00:00:00 2001 From: supalarry Date: Thu, 29 Aug 2024 11:44:26 +0200 Subject: [PATCH 41/96] tests: team event type creation and teamId, teamIds filters --- .../bookings.controller.e2e-spec.ts | 337 ++++++++++++++++++ .../repository/hosts.repository.fixture.ts | 25 ++ 2 files changed, 362 insertions(+) create mode 100644 apps/api/v2/test/fixtures/repository/hosts.repository.fixture.ts diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.e2e-spec.ts b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.e2e-spec.ts index eb5231feb3bc34..b449a514d77879 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.e2e-spec.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.e2e-spec.ts @@ -18,7 +18,11 @@ import { User } from "@prisma/client"; import * as request from "supertest"; import { BookingsRepositoryFixture } from "test/fixtures/repository/bookings.repository.fixture"; import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture"; +import { HostsRepositoryFixture } from "test/fixtures/repository/hosts.repository.fixture"; +import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture"; import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture"; +import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.repository.fixture"; +import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture"; import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture"; import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture"; import { withApiAuth } from "test/utils/withApiAuth"; @@ -970,4 +974,337 @@ describe("Bookings Endpoints 2024-08-13", () => { await app.close(); }); }); + + describe("Team bookings", () => { + let app: INestApplication; + let organization: Team; + let team1: Team; + let team2: Team; + + let userRepositoryFixture: UserRepositoryFixture; + let bookingsRepositoryFixture: BookingsRepositoryFixture; + let schedulesService: SchedulesService_2024_04_15; + let eventTypesRepositoryFixture: EventTypesRepositoryFixture; + let oauthClientRepositoryFixture: OAuthClientRepositoryFixture; + let oAuthClient: PlatformOAuthClient; + let teamRepositoryFixture: TeamRepositoryFixture; + let membershipsRepositoryFixture: MembershipRepositoryFixture; + let hostsRepositoryFixture: HostsRepositoryFixture; + let organizationsRepositoryFixture: OrganizationRepositoryFixture; + let profileRepositoryFixture: ProfileRepositoryFixture; + + const teamUserEmail = "orgUser1team1@api.com"; + let teamUser: User; + + let team1EventTypeId: number; + let team2EventTypeId: number; + + beforeAll(async () => { + const moduleRef = await withApiAuth( + teamUserEmail, + Test.createTestingModule({ + imports: [AppModule, PrismaModule, UsersModule, SchedulesModule_2024_04_15], + }) + ) + .overrideGuard(PermissionsGuard) + .useValue({ + canActivate: () => true, + }) + .compile(); + + userRepositoryFixture = new UserRepositoryFixture(moduleRef); + bookingsRepositoryFixture = new BookingsRepositoryFixture(moduleRef); + eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef); + oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef); + teamRepositoryFixture = new TeamRepositoryFixture(moduleRef); + organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef); + profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef); + membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef); + hostsRepositoryFixture = new HostsRepositoryFixture(moduleRef); + schedulesService = moduleRef.get(SchedulesService_2024_04_15); + + organization = await organizationsRepositoryFixture.create({ name: "organization team bookings" }); + team1 = await teamRepositoryFixture.create({ + name: "team 1", + isOrganization: false, + parent: { connect: { id: organization.id } }, + }); + + team2 = await teamRepositoryFixture.create({ + name: "team 2", + isOrganization: false, + parent: { connect: { id: organization.id } }, + }); + + oAuthClient = await createOAuthClient(organization.id); + + teamUser = await userRepositoryFixture.create({ + email: teamUserEmail, + }); + + const userSchedule: CreateScheduleInput_2024_04_15 = { + name: "working time", + timeZone: "Europe/Rome", + isDefault: true, + }; + await schedulesService.createUserSchedule(teamUser.id, userSchedule); + + await profileRepositoryFixture.create({ + uid: `usr-${teamUser.id}`, + username: teamUserEmail, + organization: { + connect: { + id: organization.id, + }, + }, + user: { + connect: { + id: teamUser.id, + }, + }, + }); + + await membershipsRepositoryFixture.create({ + role: "MEMBER", + user: { connect: { id: teamUser.id } }, + team: { connect: { id: team1.id } }, + accepted: true, + }); + + await membershipsRepositoryFixture.create({ + role: "MEMBER", + user: { connect: { id: teamUser.id } }, + team: { connect: { id: team2.id } }, + accepted: true, + }); + + const team1EventType = await eventTypesRepositoryFixture.createTeamEventType({ + schedulingType: "COLLECTIVE", + team: { + connect: { id: team1.id }, + }, + title: "Collective Event Type", + slug: "collective-event-type", + length: 60, + assignAllTeamMembers: true, + bookingFields: [], + locations: [], + }); + + team1EventTypeId = team1EventType.id; + + const team2EventType = await eventTypesRepositoryFixture.createTeamEventType({ + schedulingType: "COLLECTIVE", + team: { + connect: { id: team2.id }, + }, + title: "Collective Event Type 2", + slug: "collective-event-type-2", + length: 60, + assignAllTeamMembers: true, + bookingFields: [], + locations: [], + }); + + team2EventTypeId = team2EventType.id; + + await hostsRepositoryFixture.create({ + isFixed: true, + user: { + connect: { + id: teamUser.id, + }, + }, + eventType: { + connect: { + id: team1EventType.id, + }, + }, + }); + + await hostsRepositoryFixture.create({ + isFixed: true, + user: { + connect: { + id: teamUser.id, + }, + }, + eventType: { + connect: { + id: team2EventType.id, + }, + }, + }); + + app = moduleRef.createNestApplication(); + bootstrap(app as NestExpressApplication); + + await app.init(); + }); + + describe("create team bookings", () => { + it("should create a team 1 booking", async () => { + const body: CreateBookingInput_2024_08_13 = { + start: new Date(Date.UTC(2030, 0, 8, 13, 0, 0)).toISOString(), + eventTypeId: team1EventTypeId, + attendee: { + name: "alice", + email: "alice@gmail.com", + timeZone: "Europe/Madrid", + language: "es", + }, + meetingUrl: "https://meet.google.com/abc-def-ghi", + }; + + return request(app.getHttpServer()) + .post("/v2/bookings") + .send(body) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .expect(201) + .then(async (response) => { + const responseBody: CreateBookingOutput_2024_08_13 = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + expect(responseDataIsBooking(responseBody.data)).toBe(true); + + if (responseDataIsBooking(responseBody.data)) { + const data: BookingOutput_2024_08_13 = responseBody.data; + expect(data.id).toBeDefined(); + expect(data.uid).toBeDefined(); + expect(data.hostId).toBeDefined(); + expect(data.status).toEqual("accepted"); + expect(data.start).toEqual(body.start); + expect(data.end).toEqual(new Date(Date.UTC(2030, 0, 8, 14, 0, 0)).toISOString()); + expect(data.duration).toEqual(60); + expect(data.eventTypeId).toEqual(team1EventTypeId); + expect(data.attendee).toEqual({ ...body.attendee, absent: false }); + expect(data.meetingUrl).toEqual(body.meetingUrl); + expect(data.absentHost).toEqual(false); + } else { + throw new Error( + "Invalid response data - expected booking but received array of possibily recurring bookings" + ); + } + }); + }); + + it("should create a team 2 booking", async () => { + const body: CreateBookingInput_2024_08_13 = { + start: new Date(Date.UTC(2030, 0, 8, 10, 0, 0)).toISOString(), + eventTypeId: team2EventTypeId, + attendee: { + name: "bob", + email: "bob@gmail.com", + timeZone: "Europe/Rome", + language: "it", + }, + meetingUrl: "https://meet.google.com/abc-def-ghi", + }; + + return request(app.getHttpServer()) + .post("/v2/bookings") + .send(body) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .expect(201) + .then(async (response) => { + const responseBody: CreateBookingOutput_2024_08_13 = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + expect(responseDataIsBooking(responseBody.data)).toBe(true); + + if (responseDataIsBooking(responseBody.data)) { + const data: BookingOutput_2024_08_13 = responseBody.data; + expect(data.id).toBeDefined(); + expect(data.uid).toBeDefined(); + expect(data.hostId).toBeDefined(); + expect(data.status).toEqual("accepted"); + expect(data.start).toEqual(body.start); + expect(data.end).toEqual(new Date(Date.UTC(2030, 0, 8, 11, 0, 0)).toISOString()); + expect(data.duration).toEqual(60); + expect(data.eventTypeId).toEqual(team2EventTypeId); + expect(data.attendee).toEqual({ ...body.attendee, absent: false }); + expect(data.meetingUrl).toEqual(body.meetingUrl); + expect(data.absentHost).toEqual(false); + } else { + throw new Error( + "Invalid response data - expected booking but received array of possibily recurring bookings" + ); + } + }); + }); + }); + + describe("get team bookings", () => { + it("should should get bookings by teamId", async () => { + return request(app.getHttpServer()) + .get(`/v2/bookings?teamId=${team1.id}`) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .expect(200) + .then(async (response) => { + const responseBody: GetBookingsOutput_2024_08_13 = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + const data: (BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13)[] = responseBody.data; + expect(data.length).toEqual(1); + expect(data[0].eventTypeId).toEqual(team1EventTypeId); + }); + }); + + it("should should get bookings by teamId", async () => { + return request(app.getHttpServer()) + .get(`/v2/bookings?teamId=${team2.id}`) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .expect(200) + .then(async (response) => { + const responseBody: GetBookingsOutput_2024_08_13 = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + const data: (BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13)[] = responseBody.data; + expect(data.length).toEqual(1); + expect(data[0].eventTypeId).toEqual(team2EventTypeId); + }); + }); + + it("should should get bookings by teamIds", async () => { + return request(app.getHttpServer()) + .get(`/v2/bookings?teamIds[]=${team1.id}&teamIds[]=${team2.id}`) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .expect(200) + .then(async (response) => { + const responseBody: GetBookingsOutput_2024_08_13 = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + const data: (BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13)[] = responseBody.data; + expect(data.length).toEqual(2); + expect(data.find((booking) => booking.eventTypeId === team1EventTypeId)).toBeDefined(); + expect(data.find((booking) => booking.eventTypeId === team2EventTypeId)).toBeDefined(); + }); + }); + }); + + async function createOAuthClient(organizationId: number) { + const data = { + logo: "logo-url", + name: "name", + redirectUris: ["http://localhost:5555"], + permissions: 32, + }; + const secret = "secret"; + + const client = await oauthClientRepositoryFixture.create(organizationId, data, secret); + return client; + } + + function responseDataIsBooking(data: any): data is BookingOutput_2024_08_13 { + return !Array.isArray(data) && typeof data === "object" && data && "id" in data; + } + + afterAll(async () => { + await oauthClientRepositoryFixture.delete(oAuthClient.id); + await teamRepositoryFixture.delete(organization.id); + await userRepositoryFixture.deleteByEmail(teamUser.email); + await bookingsRepositoryFixture.deleteAllBookings(teamUser.id, teamUser.email); + await app.close(); + }); + }); }); diff --git a/apps/api/v2/test/fixtures/repository/hosts.repository.fixture.ts b/apps/api/v2/test/fixtures/repository/hosts.repository.fixture.ts new file mode 100644 index 00000000000000..304fdf1415a690 --- /dev/null +++ b/apps/api/v2/test/fixtures/repository/hosts.repository.fixture.ts @@ -0,0 +1,25 @@ +import { CreateEventTypeInput_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/inputs/create-event-type.input"; +import { PrismaReadService } from "@/modules/prisma/prisma-read.service"; +import { PrismaWriteService } from "@/modules/prisma/prisma-write.service"; +import { TestingModule } from "@nestjs/testing"; +import { EventType } from "@prisma/client"; + +import { Prisma } from "@calcom/prisma/client"; + +export class HostsRepositoryFixture { + private prismaReadClient: PrismaReadService["prisma"]; + private prismaWriteClient: PrismaWriteService["prisma"]; + + constructor(private readonly module: TestingModule) { + this.prismaReadClient = module.get(PrismaReadService).prisma; + this.prismaWriteClient = module.get(PrismaWriteService).prisma; + } + + async create(data: Prisma.HostCreateInput) { + return this.prismaWriteClient.host.create({ + data: { + ...data, + }, + }); + } +} From e8ee6495a501a0f3f55b0671303476e0c925c8b2 Mon Sep 17 00:00:00 2001 From: supalarry Date: Thu, 29 Aug 2024 14:38:46 +0200 Subject: [PATCH 42/96] test: cancel recurring booking --- .../bookings.controller.e2e-spec.ts | 45 ++++++++++++++++++- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.e2e-spec.ts b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.e2e-spec.ts index b449a514d77879..920540444c34d9 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.e2e-spec.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.e2e-spec.ts @@ -1,5 +1,6 @@ import { bootstrap } from "@/app"; import { AppModule } from "@/app.module"; +import { CancelBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/cancel-booking.output copy"; import { CreateBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/create-booking.output"; import { GetBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/get-booking.output"; import { GetBookingsOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/get-bookings.output"; @@ -922,12 +923,12 @@ describe("Bookings Endpoints 2024-08-13", () => { .set(X_CAL_CLIENT_ID, oAuthClient.id) .expect(200) .then(async (response) => { - const responseBody: RescheduleBookingOutput_2024_08_13 = response.body; + const responseBody: CancelBookingOutput_2024_08_13 = response.body; expect(responseBody.status).toEqual(SUCCESS_STATUS); expect(responseBody.data).toBeDefined(); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - const data: RecurringBookingOutput_2024_08_13 = responseBody.data; + const data: BookingOutput_2024_08_13 = responseBody.data; expect(data.id).toBeDefined(); expect(data.uid).toBeDefined(); expect(data.hostId).toEqual(user.id); @@ -946,6 +947,46 @@ describe("Bookings Endpoints 2024-08-13", () => { expect(cancelledBooking?.status).toEqual("CANCELLED"); }); }); + + it("should cancel recurring booking", async () => { + const body: CancelBookingInput_2024_08_13 = { + cancellationReason: "Going on a vacation", + }; + + return request(app.getHttpServer()) + .post(`/v2/bookings/${createdRecurringBooking[1].recurringBookingUid}/cancel`) + .send(body) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .set(X_CAL_CLIENT_ID, oAuthClient.id) + .expect(200) + .then(async (response) => { + const responseBody: CancelBookingOutput_2024_08_13 = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + expect(responseDataIsRecurringBooking(responseBody.data)).toBe(true); + + if (responseDataIsRecurringBooking(responseBody.data)) { + const data: RecurringBookingOutput_2024_08_13[] = responseBody.data; + expect(data.length).toEqual(4); + + const firstBooking = data[0]; + expect(firstBooking.status).toEqual("cancelled"); + + const secondBooking = data[1]; + expect(secondBooking.status).toEqual("cancelled"); + + const thirdBooking = data[2]; + expect(thirdBooking.status).toEqual("cancelled"); + + const fourthBooking = data[3]; + expect(fourthBooking.status).toEqual("cancelled"); + } else { + throw new Error( + "Invalid response data - expected recurring booking but received non array response" + ); + } + }); + }); }); function responseDataIsBooking(data: any): data is BookingOutput_2024_08_13 { From d3b9981d380a0c5025c5344ede22e19e0f5cbc78 Mon Sep 17 00:00:00 2001 From: supalarry Date: Mon, 2 Sep 2024 11:13:42 +0200 Subject: [PATCH 43/96] refactor: make hosts and attendees an array --- .../2024-08-13/bookings.repository.ts | 10 +- .../bookings.controller.e2e-spec.ts | 151 ++++++++++++++---- .../2024-08-13/services/bookings.service.ts | 40 +++-- .../2024-08-13/services/input.service.ts | 4 +- .../2024-08-13/services/output.service.ts | 117 +++++--------- .../2024-08-13/outputs/booking.output.ts | 39 +++-- .../routers/viewer/bookings/get.handler.ts | 1 - 7 files changed, 220 insertions(+), 142 deletions(-) diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/bookings.repository.ts b/apps/api/v2/src/ee/bookings/2024-08-13/bookings.repository.ts index 09505908255054..57d36bbe747fc3 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/bookings.repository.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/bookings.repository.ts @@ -22,35 +22,38 @@ export class BookingsRepository_2024_08_13 { }); } - async getByIdWithAttendees(id: number) { + async getByIdWithAttendeesAndUser(id: number) { return this.dbRead.prisma.booking.findUnique({ where: { id, }, include: { attendees: true, + user: true, }, }); } - async getByUidWithAttendees(uid: string) { + async getByUidWithAttendeesAndUser(uid: string) { return this.dbRead.prisma.booking.findUnique({ where: { uid, }, include: { attendees: true, + user: true, }, }); } - async getRecurringByUidWithAttendees(uid: string) { + async getRecurringByUidWithAttendeesAndUser(uid: string) { return this.dbRead.prisma.booking.findMany({ where: { recurringEventId: uid, }, include: { attendees: true, + user: true, }, }); } @@ -62,6 +65,7 @@ export class BookingsRepository_2024_08_13 { }, include: { attendees: true, + user: true, }, }); } diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.e2e-spec.ts b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.e2e-spec.ts index 920540444c34d9..12e71dabab2c96 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.e2e-spec.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.e2e-spec.ts @@ -206,13 +206,18 @@ describe("Bookings Endpoints 2024-08-13", () => { const data: BookingOutput_2024_08_13 = responseBody.data; expect(data.id).toBeDefined(); expect(data.uid).toBeDefined(); - expect(data.hostId).toEqual(user.id); + expect(data.hosts[0].id).toEqual(user.id); expect(data.status).toEqual("accepted"); expect(data.start).toEqual(body.start); expect(data.end).toEqual(new Date(Date.UTC(2030, 0, 8, 14, 0, 0)).toISOString()); expect(data.duration).toEqual(60); expect(data.eventTypeId).toEqual(eventTypeId); - expect(data.attendee).toEqual({ ...body.attendee, absent: false }); + expect(data.attendees[0]).toEqual({ + name: body.attendee.name, + timeZone: body.attendee.timeZone, + language: body.attendee.language, + absent: false, + }); expect(data.meetingUrl).toEqual(body.meetingUrl); expect(data.absentHost).toEqual(false); createdBooking = data; @@ -255,13 +260,18 @@ describe("Bookings Endpoints 2024-08-13", () => { const firstBooking = data[0]; expect(firstBooking.id).toBeDefined(); expect(firstBooking.uid).toBeDefined(); - expect(firstBooking.hostId).toEqual(user.id); + expect(firstBooking.hosts[0].id).toEqual(user.id); expect(firstBooking.status).toEqual("accepted"); expect(firstBooking.start).toEqual(new Date(Date.UTC(2030, 1, 4, 13, 0, 0)).toISOString()); expect(firstBooking.end).toEqual(new Date(Date.UTC(2030, 1, 4, 14, 0, 0)).toISOString()); expect(firstBooking.duration).toEqual(60); expect(firstBooking.eventTypeId).toEqual(recurringEventTypeId); - expect(firstBooking.attendee).toEqual({ ...body.attendee, absent: false }); + expect(firstBooking.attendees[0]).toEqual({ + name: body.attendee.name, + timeZone: body.attendee.timeZone, + language: body.attendee.language, + absent: false, + }); expect(firstBooking.meetingUrl).toEqual(body.meetingUrl); expect(firstBooking.recurringBookingUid).toBeDefined(); expect(firstBooking.absentHost).toEqual(false); @@ -269,28 +279,38 @@ describe("Bookings Endpoints 2024-08-13", () => { const secondBooking = data[1]; expect(secondBooking.id).toBeDefined(); expect(secondBooking.uid).toBeDefined(); - expect(secondBooking.hostId).toEqual(user.id); + expect(secondBooking.hosts[0].id).toEqual(user.id); expect(secondBooking.status).toEqual("accepted"); expect(secondBooking.start).toEqual(new Date(Date.UTC(2030, 1, 11, 13, 0, 0)).toISOString()); expect(secondBooking.end).toEqual(new Date(Date.UTC(2030, 1, 11, 14, 0, 0)).toISOString()); expect(secondBooking.duration).toEqual(60); expect(secondBooking.eventTypeId).toEqual(recurringEventTypeId); expect(secondBooking.recurringBookingUid).toBeDefined(); - expect(secondBooking.attendee).toEqual({ ...body.attendee, absent: false }); + expect(secondBooking.attendees[0]).toEqual({ + name: body.attendee.name, + timeZone: body.attendee.timeZone, + language: body.attendee.language, + absent: false, + }); expect(secondBooking.meetingUrl).toEqual(body.meetingUrl); expect(secondBooking.absentHost).toEqual(false); const thirdBooking = data[2]; expect(thirdBooking.id).toBeDefined(); expect(thirdBooking.uid).toBeDefined(); - expect(thirdBooking.hostId).toEqual(user.id); + expect(thirdBooking.hosts[0].id).toEqual(user.id); expect(thirdBooking.status).toEqual("accepted"); expect(thirdBooking.start).toEqual(new Date(Date.UTC(2030, 1, 18, 13, 0, 0)).toISOString()); expect(thirdBooking.end).toEqual(new Date(Date.UTC(2030, 1, 18, 14, 0, 0)).toISOString()); expect(thirdBooking.duration).toEqual(60); expect(thirdBooking.eventTypeId).toEqual(recurringEventTypeId); expect(thirdBooking.recurringBookingUid).toBeDefined(); - expect(thirdBooking.attendee).toEqual({ ...body.attendee, absent: false }); + expect(thirdBooking.attendees[0]).toEqual({ + name: body.attendee.name, + timeZone: body.attendee.timeZone, + language: body.attendee.language, + absent: false, + }); expect(thirdBooking.meetingUrl).toEqual(body.meetingUrl); expect(thirdBooking.absentHost).toEqual(false); @@ -320,13 +340,13 @@ describe("Bookings Endpoints 2024-08-13", () => { const data: BookingOutput_2024_08_13 = responseBody.data; expect(data.id).toEqual(createdBooking.id); expect(data.uid).toEqual(createdBooking.uid); - expect(data.hostId).toEqual(user.id); + expect(data.hosts[0].id).toEqual(user.id); expect(data.status).toEqual(createdBooking.status); expect(data.start).toEqual(createdBooking.start); expect(data.end).toEqual(createdBooking.end); expect(data.duration).toEqual(createdBooking.duration); expect(data.eventTypeId).toEqual(createdBooking.eventTypeId); - expect(data.attendee).toEqual(createdBooking.attendee); + expect(data.attendees[0]).toEqual(createdBooking.attendees[0]); expect(data.meetingUrl).toEqual(createdBooking.meetingUrl); expect(data.absentHost).toEqual(createdBooking.absentHost); } else { @@ -353,14 +373,14 @@ describe("Bookings Endpoints 2024-08-13", () => { const data: RecurringBookingOutput_2024_08_13 = responseBody.data; expect(data.id).toEqual(createdRecurringBooking[0].id); expect(data.uid).toEqual(createdRecurringBooking[0].uid); - expect(data.hostId).toEqual(user.id); + expect(data.hosts[0].id).toEqual(user.id); expect(data.status).toEqual(createdRecurringBooking[0].status); expect(data.start).toEqual(createdRecurringBooking[0].start); expect(data.end).toEqual(createdRecurringBooking[0].end); expect(data.duration).toEqual(createdRecurringBooking[0].duration); expect(data.eventTypeId).toEqual(createdRecurringBooking[0].eventTypeId); expect(data.recurringBookingUid).toEqual(createdRecurringBooking[0].recurringBookingUid); - expect(data.attendee).toEqual(createdRecurringBooking[0].attendee); + expect(data.attendees[0]).toEqual(createdRecurringBooking[0].attendees[0]); expect(data.meetingUrl).toEqual(createdRecurringBooking[0].meetingUrl); expect(data.absentHost).toEqual(createdRecurringBooking[0].absentHost); } else { @@ -390,42 +410,42 @@ describe("Bookings Endpoints 2024-08-13", () => { const firstBooking = data[0]; expect(firstBooking.id).toEqual(createdRecurringBooking[0].id); expect(firstBooking.uid).toEqual(createdRecurringBooking[0].uid); - expect(firstBooking.hostId).toEqual(user.id); + expect(firstBooking.hosts[0].id).toEqual(user.id); expect(firstBooking.status).toEqual(createdRecurringBooking[0].status); expect(firstBooking.start).toEqual(createdRecurringBooking[0].start); expect(firstBooking.end).toEqual(createdRecurringBooking[0].end); expect(firstBooking.duration).toEqual(createdRecurringBooking[0].duration); expect(firstBooking.eventTypeId).toEqual(createdRecurringBooking[0].eventTypeId); expect(firstBooking.recurringBookingUid).toEqual(recurringBookingUid); - expect(firstBooking.attendee).toEqual(createdRecurringBooking[0].attendee); + expect(firstBooking.attendees[0]).toEqual(createdRecurringBooking[0].attendees[0]); expect(firstBooking.meetingUrl).toEqual(createdRecurringBooking[0].meetingUrl); expect(firstBooking.absentHost).toEqual(createdRecurringBooking[0].absentHost); const secondBooking = data[1]; expect(secondBooking.id).toEqual(createdRecurringBooking[1].id); expect(secondBooking.uid).toEqual(createdRecurringBooking[1].uid); - expect(secondBooking.hostId).toEqual(user.id); + expect(secondBooking.hosts[0].id).toEqual(user.id); expect(secondBooking.status).toEqual(createdRecurringBooking[1].status); expect(secondBooking.start).toEqual(createdRecurringBooking[1].start); expect(secondBooking.end).toEqual(createdRecurringBooking[1].end); expect(secondBooking.duration).toEqual(createdRecurringBooking[1].duration); expect(secondBooking.eventTypeId).toEqual(createdRecurringBooking[1].eventTypeId); expect(secondBooking.recurringBookingUid).toEqual(recurringBookingUid); - expect(secondBooking.attendee).toEqual(createdRecurringBooking[1].attendee); + expect(secondBooking.attendees[0]).toEqual(createdRecurringBooking[1].attendees[0]); expect(secondBooking.meetingUrl).toEqual(createdRecurringBooking[1].meetingUrl); expect(secondBooking.absentHost).toEqual(createdRecurringBooking[1].absentHost); const thirdBooking = data[2]; expect(thirdBooking.id).toEqual(createdRecurringBooking[2].id); expect(thirdBooking.uid).toEqual(createdRecurringBooking[2].uid); - expect(thirdBooking.hostId).toEqual(user.id); + expect(thirdBooking.hosts[0].id).toEqual(user.id); expect(thirdBooking.status).toEqual(createdRecurringBooking[2].status); expect(thirdBooking.start).toEqual(createdRecurringBooking[2].start); expect(thirdBooking.end).toEqual(createdRecurringBooking[2].end); expect(thirdBooking.duration).toEqual(createdRecurringBooking[2].duration); expect(thirdBooking.eventTypeId).toEqual(createdRecurringBooking[2].eventTypeId); expect(thirdBooking.recurringBookingUid).toEqual(recurringBookingUid); - expect(thirdBooking.attendee).toEqual(createdRecurringBooking[2].attendee); + expect(thirdBooking.attendees[0]).toEqual(createdRecurringBooking[2].attendees[0]); expect(thirdBooking.meetingUrl).toEqual(createdRecurringBooking[2].meetingUrl); expect(thirdBooking.absentHost).toEqual(createdRecurringBooking[2].absentHost); @@ -742,11 +762,11 @@ describe("Bookings Endpoints 2024-08-13", () => { expect(data.rescheduledFromUid).toEqual(createdBooking.uid); expect(data.id).toBeDefined(); expect(data.uid).toBeDefined(); - expect(data.hostId).toEqual(user.id); + expect(data.hosts[0].id).toEqual(user.id); expect(data.status).toEqual(createdBooking.status); expect(data.duration).toEqual(createdBooking.duration); expect(data.eventTypeId).toEqual(createdBooking.eventTypeId); - expect(data.attendee).toEqual(createdBooking.attendee); + expect(data.attendees[0]).toEqual(createdBooking.attendees[0]); expect(data.meetingUrl).toEqual(createdBooking.meetingUrl); expect(data.absentHost).toEqual(createdBooking.absentHost); @@ -791,14 +811,14 @@ describe("Bookings Endpoints 2024-08-13", () => { const data: RecurringBookingOutput_2024_08_13 = responseBody.data; expect(data.id).toBeDefined(); expect(data.uid).toBeDefined(); - expect(data.hostId).toEqual(user.id); + expect(data.hosts[0].id).toEqual(user.id); expect(data.status).toEqual(createdRecurringBooking[0].status); expect(data.start).toEqual(body.start); expect(data.end).toEqual(new Date(Date.UTC(2035, 0, 9, 15, 0, 0)).toISOString()); expect(data.duration).toEqual(createdRecurringBooking[0].duration); expect(data.recurringBookingUid).toEqual(createdRecurringBooking[0].recurringBookingUid); expect(data.eventTypeId).toEqual(createdRecurringBooking[0].eventTypeId); - expect(data.attendee).toEqual(createdRecurringBooking[0].attendee); + expect(data.attendees[0]).toEqual(createdRecurringBooking[0].attendees[0]); expect(data.meetingUrl).toEqual(createdRecurringBooking[0].meetingUrl); expect(data.absentHost).toEqual(createdRecurringBooking[0].absentHost); @@ -862,13 +882,13 @@ describe("Bookings Endpoints 2024-08-13", () => { expect(data.id).toEqual(booking.id); expect(data.uid).toEqual(booking.uid); - expect(data.hostId).toEqual(user.id); + expect(data.hosts[0].id).toEqual(user.id); expect(data.status).toEqual(booking.status); expect(data.start).toEqual(booking.start); expect(data.end).toEqual(booking.end); expect(data.duration).toEqual(booking.duration); expect(data.eventTypeId).toEqual(booking.eventTypeId); - expect(data.attendee).toEqual(booking.attendee); + expect(data.attendees[0]).toEqual(booking.attendees[0]); expect(data.meetingUrl).toEqual(booking.meetingUrl); }); }); @@ -894,13 +914,13 @@ describe("Bookings Endpoints 2024-08-13", () => { expect(data.id).toEqual(booking.id); expect(data.uid).toEqual(booking.uid); - expect(data.hostId).toEqual(user.id); + expect(data.hosts[0].id).toEqual(user.id); expect(data.status).toEqual(booking.status); expect(data.start).toEqual(booking.start); expect(data.end).toEqual(booking.end); expect(data.duration).toEqual(booking.duration); expect(data.eventTypeId).toEqual(booking.eventTypeId); - expect(data.attendee.absent).toEqual(true); + expect(data.attendees[0].absent).toEqual(true); expect(data.absentHost).toEqual(booking.absentHost); expect(data.meetingUrl).toEqual(booking.meetingUrl); }); @@ -931,14 +951,14 @@ describe("Bookings Endpoints 2024-08-13", () => { const data: BookingOutput_2024_08_13 = responseBody.data; expect(data.id).toBeDefined(); expect(data.uid).toBeDefined(); - expect(data.hostId).toEqual(user.id); + expect(data.hosts[0].id).toEqual(user.id); expect(data.status).toEqual("cancelled"); expect(data.cancellationReason).toEqual(body.cancellationReason); expect(data.start).toEqual(rescheduledBooking.start); expect(data.end).toEqual(rescheduledBooking.end); expect(data.duration).toEqual(rescheduledBooking.duration); expect(data.eventTypeId).toEqual(rescheduledBooking.eventTypeId); - expect(data.attendee).toEqual(rescheduledBooking.attendee); + expect(data.attendees[0]).toEqual(rescheduledBooking.attendees[0]); expect(data.meetingUrl).toEqual(rescheduledBooking.meetingUrl); expect(data.absentHost).toEqual(rescheduledBooking.absentHost); @@ -1035,7 +1055,9 @@ describe("Bookings Endpoints 2024-08-13", () => { let profileRepositoryFixture: ProfileRepositoryFixture; const teamUserEmail = "orgUser1team1@api.com"; + const teamUserEmail2 = "orgUser2team1@api.com"; let teamUser: User; + let teamUser2: User; let team1EventTypeId: number; let team2EventTypeId: number; @@ -1081,6 +1103,14 @@ describe("Bookings Endpoints 2024-08-13", () => { teamUser = await userRepositoryFixture.create({ email: teamUserEmail, + locale: "it", + name: "orgUser1team1", + }); + + teamUser2 = await userRepositoryFixture.create({ + email: teamUserEmail2, + locale: "es", + name: "orgUser2team1", }); const userSchedule: CreateScheduleInput_2024_04_15 = { @@ -1089,6 +1119,7 @@ describe("Bookings Endpoints 2024-08-13", () => { isDefault: true, }; await schedulesService.createUserSchedule(teamUser.id, userSchedule); + await schedulesService.createUserSchedule(teamUser2.id, userSchedule); await profileRepositoryFixture.create({ uid: `usr-${teamUser.id}`, @@ -1105,6 +1136,21 @@ describe("Bookings Endpoints 2024-08-13", () => { }, }); + await profileRepositoryFixture.create({ + uid: `usr-${teamUser2.id}`, + username: teamUserEmail2, + organization: { + connect: { + id: organization.id, + }, + }, + user: { + connect: { + id: teamUser2.id, + }, + }, + }); + await membershipsRepositoryFixture.create({ role: "MEMBER", user: { connect: { id: teamUser.id } }, @@ -1119,6 +1165,13 @@ describe("Bookings Endpoints 2024-08-13", () => { accepted: true, }); + await membershipsRepositoryFixture.create({ + role: "MEMBER", + user: { connect: { id: teamUser2.id } }, + team: { connect: { id: team2.id } }, + accepted: true, + }); + const team1EventType = await eventTypesRepositoryFixture.createTeamEventType({ schedulingType: "COLLECTIVE", team: { @@ -1177,6 +1230,20 @@ describe("Bookings Endpoints 2024-08-13", () => { }, }); + await hostsRepositoryFixture.create({ + isFixed: true, + user: { + connect: { + id: teamUser2.id, + }, + }, + eventType: { + connect: { + id: team2EventType.id, + }, + }, + }); + app = moduleRef.createNestApplication(); bootstrap(app as NestExpressApplication); @@ -1212,13 +1279,20 @@ describe("Bookings Endpoints 2024-08-13", () => { const data: BookingOutput_2024_08_13 = responseBody.data; expect(data.id).toBeDefined(); expect(data.uid).toBeDefined(); - expect(data.hostId).toBeDefined(); + expect(data.hosts.length).toEqual(1); + expect(data.hosts[0].id).toEqual(teamUser.id); expect(data.status).toEqual("accepted"); expect(data.start).toEqual(body.start); expect(data.end).toEqual(new Date(Date.UTC(2030, 0, 8, 14, 0, 0)).toISOString()); expect(data.duration).toEqual(60); expect(data.eventTypeId).toEqual(team1EventTypeId); - expect(data.attendee).toEqual({ ...body.attendee, absent: false }); + expect(data.attendees.length).toEqual(1); + expect(data.attendees[0]).toEqual({ + name: body.attendee.name, + timeZone: body.attendee.timeZone, + language: body.attendee.language, + absent: false, + }); expect(data.meetingUrl).toEqual(body.meetingUrl); expect(data.absentHost).toEqual(false); } else { @@ -1257,13 +1331,26 @@ describe("Bookings Endpoints 2024-08-13", () => { const data: BookingOutput_2024_08_13 = responseBody.data; expect(data.id).toBeDefined(); expect(data.uid).toBeDefined(); - expect(data.hostId).toBeDefined(); + expect(data.hosts.length).toEqual(1); + expect(data.hosts[0].id).toEqual(teamUser.id); expect(data.status).toEqual("accepted"); expect(data.start).toEqual(body.start); expect(data.end).toEqual(new Date(Date.UTC(2030, 0, 8, 11, 0, 0)).toISOString()); expect(data.duration).toEqual(60); expect(data.eventTypeId).toEqual(team2EventTypeId); - expect(data.attendee).toEqual({ ...body.attendee, absent: false }); + expect(data.attendees.length).toEqual(2); + expect(data.attendees[0]).toEqual({ + name: body.attendee.name, + timeZone: body.attendee.timeZone, + language: body.attendee.language, + absent: false, + }); + expect(data.attendees[1]).toEqual({ + name: teamUser2.name, + timeZone: teamUser2.timeZone, + language: teamUser2.locale, + absent: false, + }); expect(data.meetingUrl).toEqual(body.meetingUrl); expect(data.absentHost).toEqual(false); } else { diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts index 5c5cf34cb605a9..61fb25d4ee1e97 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts @@ -4,7 +4,7 @@ import { OutputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/servi import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository"; import { BillingService } from "@/modules/billing/services/billing.service"; import { PrismaReadService } from "@/modules/prisma/prisma-read.service"; -import { Injectable, NotFoundException } from "@nestjs/common"; +import { Injectable, Logger, NotFoundException } from "@nestjs/common"; import { BadRequestException } from "@nestjs/common"; import { Request } from "express"; @@ -38,16 +38,18 @@ type BookingWithAttendeesAndEventType = Booking & { noShow: boolean | null; }[]; eventType: { id: number }; + user: { id: number; name: string | null; email: string } | null; }; type CreatedBooking = { - hostId: number; + hosts: { id: number }[]; uid: string; start: string; }; @Injectable() export class BookingsService_2024_08_13 { + private readonly logger = new Logger("BookingsService"); constructor( private readonly inputService: InputBookingsService_2024_08_13, private readonly outputService: OutputBookingsService_2024_08_13, @@ -82,7 +84,7 @@ export class BookingsService_2024_08_13 { const bookingRequest = await this.inputService.createBookingRequest(request, body); const booking = await handleInstantMeeting(bookingRequest); - const databaseBooking = await this.bookingsRepository.getByIdWithAttendees(booking.bookingId); + const databaseBooking = await this.bookingsRepository.getByIdWithAttendeesAndUser(booking.bookingId); if (!databaseBooking) { throw new Error(`Booking with id=${booking.bookingId} was not found in the database`); } @@ -103,7 +105,7 @@ export class BookingsService_2024_08_13 { throw new Error("Recurring booking was not created"); } - const recurringBooking = await this.bookingsRepository.getRecurringByUidWithAttendees(uid); + const recurringBooking = await this.bookingsRepository.getRecurringByUidWithAttendeesAndUser(uid); return this.outputService.getOutputRecurringBookings(recurringBooking); } @@ -114,7 +116,7 @@ export class BookingsService_2024_08_13 { throw new Error("Booking was not created"); } - const databaseBooking = await this.bookingsRepository.getByIdWithAttendees(booking.id); + const databaseBooking = await this.bookingsRepository.getByIdWithAttendeesAndUser(booking.id); if (!databaseBooking) { throw new Error(`Booking with id=${booking.id} was not found in the database`); } @@ -123,7 +125,7 @@ export class BookingsService_2024_08_13 { } async getBooking(uid: string) { - const booking = await this.bookingsRepository.getByUidWithAttendees(uid); + const booking = await this.bookingsRepository.getByUidWithAttendeesAndUser(uid); if (booking) { const isRecurring = !!booking.recurringEventId; @@ -141,7 +143,7 @@ export class BookingsService_2024_08_13 { return this.outputService.getOutputBooking(booking); } - const recurringBooking = await this.bookingsRepository.getRecurringByUidWithAttendees(uid); + const recurringBooking = await this.bookingsRepository.getRecurringByUidWithAttendeesAndUser(uid); if (!recurringBooking.length) { throw new NotFoundException(`Booking with uid=${uid} was not found in the database`); } @@ -153,8 +155,8 @@ export class BookingsService_2024_08_13 { const fetchedBookings: { bookings: BookingWithAttendeesAndEventType[] } = await getAllUserBookings({ bookingListingByStatus: queryParams.status || [], skip: queryParams.skip ?? 0, - take: queryParams.take ?? 100, - // todo: add filters here like by eventtype id etc + // note(Lauris): we substract -1 because getAllUSerBookings child function adds +1 for some reason + take: queryParams.take ? queryParams.take - 1 : 100, filters: this.inputService.transformGetBookingsFilters(queryParams), ctx: { user, @@ -192,7 +194,7 @@ export class BookingsService_2024_08_13 { throw new Error("Booking was not created"); } - const databaseBooking = await this.bookingsRepository.getByIdWithAttendees(booking.id); + const databaseBooking = await this.bookingsRepository.getByIdWithAttendeesAndUser(booking.id); if (!databaseBooking) { throw new Error(`Booking with id=${booking.id} was not found in the database`); } @@ -227,7 +229,7 @@ export class BookingsService_2024_08_13 { userId: bookingOwnerId, }); - const booking = await this.bookingsRepository.getByUidWithAttendees(bookingUid); + const booking = await this.bookingsRepository.getByUidWithAttendeesAndUser(bookingUid); if (!booking) { throw new Error(`Booking with uid=${bookingUid} was not found in the database`); @@ -247,14 +249,26 @@ export class BookingsService_2024_08_13 { } async billBooking(booking: CreatedBooking) { - await this.billingService.increaseUsageByUserId(booking.hostId, { + const hostId = booking.hosts[0].id; + if (!hostId) { + this.logger.error(`Booking with uid=${booking.uid} has no host`); + return; + } + + await this.billingService.increaseUsageByUserId(hostId, { uid: booking.uid, startTime: new Date(booking.start), }); } async billRescheduledBooking(newBooking: CreatedBooking, oldBookingUid: string) { - await this.billingService.increaseUsageByUserId(newBooking.hostId, { + const hostId = newBooking.hosts[0].id; + if (!hostId) { + this.logger.error(`Booking with uid=${newBooking.uid} has no host`); + return; + } + + await this.billingService.increaseUsageByUserId(hostId, { uid: newBooking.uid, startTime: new Date(newBooking.start), fromReschedule: oldBookingUid, diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts index 1bc52b3fe7c11a..4abf11add66e73 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts @@ -249,7 +249,7 @@ export class InputBookingsService_2024_08_13 { } async transformInputRescheduleBooking(bookingUid: string, inputBooking: RescheduleBookingInput_2024_08_13) { - const booking = await this.bookingsRepository.getByUidWithAttendees(bookingUid); + const booking = await this.bookingsRepository.getByUidWithAttendeesAndUser(bookingUid); if (!booking) { throw new NotFoundException(`Booking with uid=${bookingUid} not found`); } @@ -374,7 +374,7 @@ export class InputBookingsService_2024_08_13 { async transformInputCancelBooking(bookingUid: string, inputBooking: CancelBookingInput_2024_08_13) { let allRemainingBookings = false; let uid = bookingUid; - const recurringBooking = await this.bookingsRepository.getRecurringByUidWithAttendees(bookingUid); + const recurringBooking = await this.bookingsRepository.getRecurringByUidWithAttendeesAndUser(bookingUid); if (recurringBooking.length) { // note(Lauirs): this means that bookingUid is equal to recurringEventId on individual bookings of recurring one aka main recurring event diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/output.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/output.service.ts index e417bb208f4893..b8bb795256dc2e 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/services/output.service.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/services/output.service.ts @@ -14,21 +14,21 @@ export const bookingResponsesSchema = z.object({ rescheduledReason: z.string().optional(), }); +type DatabaseBooking = Booking & { + attendees: { + name: string; + email: string; + timeZone: string; + locale: string | null; + noShow: boolean | null; + }[]; +} & { user: { id: number; name: string | null; email: string } | null }; + @Injectable() export class OutputBookingsService_2024_08_13 { constructor(private readonly bookingsRepository: BookingsRepository_2024_08_13) {} - getOutputBooking( - databaseBooking: Booking & { - attendees: { - name: string; - email: string; - timeZone: string; - locale: string | null; - noShow: boolean | null; - }[]; - } - ) { + getOutputBooking(databaseBooking: DatabaseBooking) { const dateStart = DateTime.fromISO(databaseBooking.startTime.toISOString()); const dateEnd = DateTime.fromISO(databaseBooking.endTime.toISOString()); const duration = dateEnd.diff(dateStart, "minutes").minutes; @@ -43,7 +43,7 @@ export class OutputBookingsService_2024_08_13 { const booking = { id: databaseBooking.id, uid: databaseBooking.uid, - hostId: databaseBooking.userId, + hosts: [databaseBooking.user], status: databaseBooking.rescheduled && !databaseBooking.cancellationReason ? "rescheduled" @@ -55,13 +55,13 @@ export class OutputBookingsService_2024_08_13 { end: databaseBooking.endTime, duration, eventTypeId: databaseBooking.eventTypeId, - attendee: { + attendees: databaseBooking.attendees.map((attendee) => ({ name: attendee.name, email: attendee.email, timeZone: attendee.timeZone, language: attendee.locale, absent: !!attendee.noShow, - }, + })), guests: bookingResponses.guests, meetingUrl: databaseBooking.location, absentHost: !!databaseBooking.noShowHost, @@ -70,33 +70,14 @@ export class OutputBookingsService_2024_08_13 { return plainToClass(BookingOutput_2024_08_13, booking, { strategy: "excludeAll" }); } - getOutputRescheduledBooking( - oldDatabaseBooking: Booking & { - attendees: { - name: string; - email: string; - timeZone: string; - locale: string | null; - noShow: boolean | null; - }[]; - }, - newDatabaseBooking: Booking & { - attendees: { - name: string; - email: string; - timeZone: string; - locale: string | null; - noShow: boolean | null; - }[]; - } - ) { - const dateStart = DateTime.fromISO(oldDatabaseBooking.startTime.toISOString()); - const dateEnd = DateTime.fromISO(oldDatabaseBooking.endTime.toISOString()); + getOutputRescheduledBooking(rescheduledBooking: DatabaseBooking, newDatabaseBooking: DatabaseBooking) { + const dateStart = DateTime.fromISO(rescheduledBooking.startTime.toISOString()); + const dateEnd = DateTime.fromISO(rescheduledBooking.endTime.toISOString()); const duration = dateEnd.diff(dateStart, "minutes").minutes; - const bookingResponses = bookingResponsesSchema.parse(oldDatabaseBooking.responses); + const bookingResponses = bookingResponsesSchema.parse(rescheduledBooking.responses); const bookingResponsesNew = bookingResponsesSchema.parse(newDatabaseBooking.responses); - const attendee = oldDatabaseBooking.attendees.find( + const attendee = rescheduledBooking.attendees.find( (attendee) => attendee.email === bookingResponses.email ); @@ -105,48 +86,38 @@ export class OutputBookingsService_2024_08_13 { } const booking = { - id: oldDatabaseBooking.id, - uid: oldDatabaseBooking.uid, - hostId: oldDatabaseBooking.userId, + id: rescheduledBooking.id, + uid: rescheduledBooking.uid, + hosts: [rescheduledBooking.user], status: - oldDatabaseBooking.rescheduled && !oldDatabaseBooking.cancellationReason + rescheduledBooking.rescheduled && !rescheduledBooking.cancellationReason ? "rescheduled" - : oldDatabaseBooking.status.toLowerCase(), - cancellationReason: oldDatabaseBooking.cancellationReason || undefined, + : rescheduledBooking.status.toLowerCase(), + cancellationReason: rescheduledBooking.cancellationReason || undefined, reschedulingReason: bookingResponsesNew?.rescheduledReason, - rescheduledFromUid: oldDatabaseBooking.fromReschedule || undefined, + rescheduledFromUid: rescheduledBooking.fromReschedule || undefined, rescheduledToUid: newDatabaseBooking.uid, recurringBookingUid: newDatabaseBooking.recurringEventId || undefined, - start: oldDatabaseBooking.startTime, - end: oldDatabaseBooking.endTime, + start: rescheduledBooking.startTime, + end: rescheduledBooking.endTime, duration, - eventTypeId: oldDatabaseBooking.eventTypeId, - attendee: { + eventTypeId: rescheduledBooking.eventTypeId, + attendees: rescheduledBooking.attendees.map((attendee) => ({ name: attendee.name, email: attendee.email, timeZone: attendee.timeZone, language: attendee.locale, absent: !!attendee.noShow, - }, + })), guests: bookingResponses.guests, - meetingUrl: oldDatabaseBooking.location, - absentHost: !!oldDatabaseBooking.noShowHost, + meetingUrl: rescheduledBooking.location, + absentHost: !!rescheduledBooking.noShowHost, }; return plainToClass(BookingOutput_2024_08_13, booking, { strategy: "excludeAll" }); } - async getOutputRecurringBookings( - databaseBookings: (Booking & { - attendees: { - name: string; - email: string; - timeZone: string; - locale: string | null; - noShow: boolean | null; - }[]; - })[] - ) { + async getOutputRecurringBookings(databaseBookings: DatabaseBooking[]) { const transformed = []; for (const booking of databaseBookings) { @@ -154,7 +125,7 @@ export class OutputBookingsService_2024_08_13 { throw new Error("Booking was not created"); } - const databaseBooking = await this.bookingsRepository.getByIdWithAttendees(booking.id); + const databaseBooking = await this.bookingsRepository.getByIdWithAttendeesAndUser(booking.id); if (!databaseBooking) { throw new Error(`Booking with id=${booking.id} was not found in the database`); } @@ -165,17 +136,7 @@ export class OutputBookingsService_2024_08_13 { return transformed.sort((a, b) => new Date(a.start).getTime() - new Date(b.start).getTime()); } - getOutputRecurringBooking( - databaseBooking: Booking & { - attendees: { - name: string; - email: string; - timeZone: string; - locale: string | null; - noShow: boolean | null; - }[]; - } - ) { + getOutputRecurringBooking(databaseBooking: DatabaseBooking) { const dateStart = DateTime.fromISO(databaseBooking.startTime.toISOString()); const dateEnd = DateTime.fromISO(databaseBooking.endTime.toISOString()); const duration = dateEnd.diff(dateStart, "minutes").minutes; @@ -190,20 +151,20 @@ export class OutputBookingsService_2024_08_13 { const booking = { id: databaseBooking.id, uid: databaseBooking.uid, - hostId: databaseBooking.userId, + hosts: [databaseBooking.user], status: databaseBooking.status.toLowerCase(), cancellationReason: databaseBooking.cancellationReason || undefined, start: databaseBooking.startTime, end: databaseBooking.endTime, duration, eventTypeId: databaseBooking.eventTypeId, - attendee: { + attendees: databaseBooking.attendees.map((attendee) => ({ name: attendee.name, email: attendee.email, timeZone: attendee.timeZone, language: attendee.locale, absent: !!attendee.noShow, - }, + })), guests: bookingResponses.guests, meetingUrl: databaseBooking.location, recurringBookingUid: databaseBooking.recurringEventId, diff --git a/packages/platform/types/bookings/2024-08-13/outputs/booking.output.ts b/packages/platform/types/bookings/2024-08-13/outputs/booking.output.ts index 29f8d383a92dfc..0f7278683ca105 100644 --- a/packages/platform/types/bookings/2024-08-13/outputs/booking.output.ts +++ b/packages/platform/types/bookings/2024-08-13/outputs/booking.output.ts @@ -3,7 +3,6 @@ import { IsArray, IsBoolean, IsDateString, - IsEmail, IsEnum, IsInt, IsOptional, @@ -21,10 +20,6 @@ class Attendee { @Expose() name!: string; - @IsEmail() - @Expose() - email!: string; - @IsTimeZone() @Expose() // note(Lauris): setup CapitalizeTimezone @@ -39,6 +34,22 @@ class Attendee { @Expose() absent!: boolean; } + +class Host { + @IsInt() + @Expose() + id!: number; + + @IsString() + @Expose() + name!: string; + + @IsTimeZone() + @Expose() + // note(Lauris): setup CapitalizeTimezone + timeZone!: string; +} + export class BookingOutput_2024_08_13 { @IsInt() @Expose() @@ -48,9 +59,10 @@ export class BookingOutput_2024_08_13 { @Expose() uid!: string; - @IsInt() + @ValidateNested({ each: true }) + @Type(() => Host) @Expose() - hostId!: number; + hosts!: Host[]; @IsEnum(["cancelled", "accepted", "rejected", "pending", "rescheduled"]) @Expose() @@ -92,10 +104,10 @@ export class BookingOutput_2024_08_13 { @Expose() eventTypeId!: number; - @ValidateNested() + @ValidateNested({ each: true }) @Type(() => Attendee) @Expose() - attendee!: Attendee; + attendees!: Attendee[]; @IsArray() @IsString({ each: true }) @@ -122,9 +134,10 @@ export class RecurringBookingOutput_2024_08_13 { @Expose() uid!: string; - @IsInt() + @ValidateNested({ each: true }) + @Type(() => Host) @Expose() - hostId!: number; + hosts!: Host[]; @IsEnum(["cancelled", "accepted", "rejected", "pending"]) @Expose() @@ -155,10 +168,10 @@ export class RecurringBookingOutput_2024_08_13 { @Expose() recurringBookingUid!: string; - @ValidateNested() + @ValidateNested({ each: true }) @Type(() => Attendee) @Expose() - attendee!: Attendee; + attendees!: Attendee[]; @IsArray() @IsString({ each: true }) diff --git a/packages/trpc/server/routers/viewer/bookings/get.handler.ts b/packages/trpc/server/routers/viewer/bookings/get.handler.ts index 59a1571aa6f9b6..d377492e675aa4 100644 --- a/packages/trpc/server/routers/viewer/bookings/get.handler.ts +++ b/packages/trpc/server/routers/viewer/bookings/get.handler.ts @@ -213,7 +213,6 @@ export async function getBookings({ recurringEventId: true, location: true, responses: true, - userId: true, eventType: { select: { slug: true, From 2fd41a20dd61933c6e23030fcaf34504a8597dd3 Mon Sep 17 00:00:00 2001 From: supalarry Date: Mon, 2 Sep 2024 12:07:47 +0200 Subject: [PATCH 44/96] sort by asc start --- packages/lib/bookings/getAllUserBookings.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/lib/bookings/getAllUserBookings.ts b/packages/lib/bookings/getAllUserBookings.ts index 3c9751a527cb39..82ca861b77957d 100644 --- a/packages/lib/bookings/getAllUserBookings.ts +++ b/packages/lib/bookings/getAllUserBookings.ts @@ -128,7 +128,7 @@ function getOrderBy( return { createdAt: sort.sortCreated }; } - return { startTime: "desc" }; + return { startTime: "asc" }; } export default getAllUserBookings; From f3a9762e7e06763bdd8467695765410054e4032b Mon Sep 17 00:00:00 2001 From: supalarry Date: Mon, 2 Sep 2024 13:23:19 +0200 Subject: [PATCH 45/96] simplify --- .../bookings.controller.e2e-spec.ts | 5 +- .../2024-08-13/services/bookings.service.ts | 8 - .../2024-08-13/services/output.service.ts | 54 +-- apps/api/v2/swagger/documentation.json | 424 ++++++++---------- .../2024-08-13/outputs/booking.output.ts | 15 +- 5 files changed, 207 insertions(+), 299 deletions(-) diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.e2e-spec.ts b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.e2e-spec.ts index 12e71dabab2c96..ba7b2e1209ef4d 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.e2e-spec.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.e2e-spec.ts @@ -774,7 +774,7 @@ describe("Bookings Endpoints 2024-08-13", () => { }); }); - it("should point rescheduled booking to the new one", async () => { + it("should set rescheduled booking status to cancelled", async () => { return request(app.getHttpServer()) .get(`/v2/bookings/${createdBooking.uid}`) .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) @@ -784,8 +784,7 @@ describe("Bookings Endpoints 2024-08-13", () => { expect(responseBody.status).toEqual(SUCCESS_STATUS); expect(responseBody.data).toBeDefined(); const data: BookingOutput_2024_08_13 = responseBody.data; - expect(data.rescheduledToUid).toEqual(rescheduledBooking.uid); - expect(data.status).toEqual("rescheduled"); + expect(data.status).toEqual("cancelled"); createdBooking = data; }); diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts index 61fb25d4ee1e97..cf6418055d52f7 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts @@ -129,14 +129,6 @@ export class BookingsService_2024_08_13 { if (booking) { const isRecurring = !!booking.recurringEventId; - const isRescheduled = booking.rescheduled; - if (isRescheduled) { - const toReschedule = await this.bookingsRepository.getByFromReschedule(uid); - if (!toReschedule) { - throw new Error(`Booking with fromReschedule=${uid} was not found in the database`); - } - return this.outputService.getOutputRescheduledBooking(booking, toReschedule); - } if (isRecurring) { return this.outputService.getOutputRecurringBooking(booking); } diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/output.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/output.service.ts index b8bb795256dc2e..742d805ae2237a 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/services/output.service.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/services/output.service.ts @@ -44,10 +44,7 @@ export class OutputBookingsService_2024_08_13 { id: databaseBooking.id, uid: databaseBooking.uid, hosts: [databaseBooking.user], - status: - databaseBooking.rescheduled && !databaseBooking.cancellationReason - ? "rescheduled" - : databaseBooking.status.toLowerCase(), + status: databaseBooking.status.toLowerCase(), cancellationReason: databaseBooking.cancellationReason || undefined, reschedulingReason: bookingResponses?.rescheduledReason, rescheduledFromUid: databaseBooking.fromReschedule || undefined, @@ -70,53 +67,6 @@ export class OutputBookingsService_2024_08_13 { return plainToClass(BookingOutput_2024_08_13, booking, { strategy: "excludeAll" }); } - getOutputRescheduledBooking(rescheduledBooking: DatabaseBooking, newDatabaseBooking: DatabaseBooking) { - const dateStart = DateTime.fromISO(rescheduledBooking.startTime.toISOString()); - const dateEnd = DateTime.fromISO(rescheduledBooking.endTime.toISOString()); - const duration = dateEnd.diff(dateStart, "minutes").minutes; - - const bookingResponses = bookingResponsesSchema.parse(rescheduledBooking.responses); - const bookingResponsesNew = bookingResponsesSchema.parse(newDatabaseBooking.responses); - const attendee = rescheduledBooking.attendees.find( - (attendee) => attendee.email === bookingResponses.email - ); - - if (!attendee) { - throw new Error("Attendee not found"); - } - - const booking = { - id: rescheduledBooking.id, - uid: rescheduledBooking.uid, - hosts: [rescheduledBooking.user], - status: - rescheduledBooking.rescheduled && !rescheduledBooking.cancellationReason - ? "rescheduled" - : rescheduledBooking.status.toLowerCase(), - cancellationReason: rescheduledBooking.cancellationReason || undefined, - reschedulingReason: bookingResponsesNew?.rescheduledReason, - rescheduledFromUid: rescheduledBooking.fromReschedule || undefined, - rescheduledToUid: newDatabaseBooking.uid, - recurringBookingUid: newDatabaseBooking.recurringEventId || undefined, - start: rescheduledBooking.startTime, - end: rescheduledBooking.endTime, - duration, - eventTypeId: rescheduledBooking.eventTypeId, - attendees: rescheduledBooking.attendees.map((attendee) => ({ - name: attendee.name, - email: attendee.email, - timeZone: attendee.timeZone, - language: attendee.locale, - absent: !!attendee.noShow, - })), - guests: bookingResponses.guests, - meetingUrl: rescheduledBooking.location, - absentHost: !!rescheduledBooking.noShowHost, - }; - - return plainToClass(BookingOutput_2024_08_13, booking, { strategy: "excludeAll" }); - } - async getOutputRecurringBookings(databaseBookings: DatabaseBooking[]) { const transformed = []; @@ -154,6 +104,8 @@ export class OutputBookingsService_2024_08_13 { hosts: [databaseBooking.user], status: databaseBooking.status.toLowerCase(), cancellationReason: databaseBooking.cancellationReason || undefined, + reschedulingReason: bookingResponses?.rescheduledReason, + rescheduledFromUid: databaseBooking.fromReschedule || undefined, start: databaseBooking.startTime, end: databaseBooking.endTime, duration, diff --git a/apps/api/v2/swagger/documentation.json b/apps/api/v2/swagger/documentation.json index e8ab679a3fa146..6c086e891f3b28 100644 --- a/apps/api/v2/swagger/documentation.json +++ b/apps/api/v2/swagger/documentation.json @@ -2287,7 +2287,7 @@ }, "/v2/organizations/{orgId}/webhooks": { "get": { - "operationId": "OrganizationsWebhooksController_getAllWebhooks", + "operationId": "OrganizationsWebhooksController_getAllOrganizationWebhooks", "parameters": [ { "name": "orgId", @@ -2324,7 +2324,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/OrgWebhooksResponseDto" + "$ref": "#/components/schemas/TeamWebhooksOutputResponseDto" } } } @@ -2335,7 +2335,7 @@ ] }, "post": { - "operationId": "OrganizationsWebhooksController_createWebhook", + "operationId": "OrganizationsWebhooksController_createOrganizationWebhook", "parameters": [ { "name": "orgId", @@ -2362,7 +2362,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/OrgWebhooksResponseDto" + "$ref": "#/components/schemas/TeamWebhookOutputResponseDto" } } } @@ -2375,16 +2375,8 @@ }, "/v2/organizations/{orgId}/webhooks/{webhookId}": { "get": { - "operationId": "OrganizationsWebhooksController_getOrgWebhook", + "operationId": "OrganizationsWebhooksController_getOrganizationWebhook", "parameters": [ - { - "name": "orgId", - "required": true, - "in": "path", - "schema": { - "type": "number" - } - }, { "name": "webhookId", "required": true, @@ -2400,7 +2392,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/OrgWebhooksResponseDto" + "$ref": "#/components/schemas/TeamWebhookOutputResponseDto" } } } @@ -2413,14 +2405,6 @@ "delete": { "operationId": "OrganizationsWebhooksController_deleteWebhook", "parameters": [ - { - "name": "orgId", - "required": true, - "in": "path", - "schema": { - "type": "number" - } - }, { "name": "webhookId", "required": true, @@ -2436,7 +2420,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/OrgWebhooksResponseDto" + "$ref": "#/components/schemas/TeamWebhookOutputResponseDto" } } } @@ -2447,16 +2431,8 @@ ] }, "patch": { - "operationId": "OrganizationsWebhooksController_updateWebhook", + "operationId": "OrganizationsWebhooksController_updateOrgWebhook", "parameters": [ - { - "name": "orgId", - "required": true, - "in": "path", - "schema": { - "type": "number" - } - }, { "name": "webhookId", "required": true, @@ -2482,7 +2458,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/OrgWebhooksResponseDto" + "$ref": "#/components/schemas/TeamWebhookOutputResponseDto" } } } @@ -7690,6 +7666,189 @@ "userId" ] }, + "TeamWebhookOutputDto": { + "type": "object", + "properties": { + "payloadTemplate": { + "type": "string", + "description": "The template of the payload that will be sent to the subscriberUrl, check cal.com/docs/core-features/webhooks for more information", + "example": "{\"content\":\"A new event has been scheduled\",\"type\":\"{{type}}\",\"name\":\"{{title}}\",\"organizer\":\"{{organizer.name}}\",\"booker\":\"{{attendees.0.name}}\"}" + }, + "teamId": { + "type": "number" + }, + "id": { + "type": "number" + }, + "triggers": { + "type": "array", + "items": { + "type": "object" + } + }, + "subscriberUrl": { + "type": "string" + }, + "active": { + "type": "boolean" + }, + "secret": { + "type": "string" + } + }, + "required": [ + "payloadTemplate", + "teamId", + "id", + "triggers", + "subscriberUrl", + "active" + ] + }, + "TeamWebhooksOutputResponseDto": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TeamWebhookOutputDto" + } + } + }, + "required": [ + "status", + "data" + ] + }, + "CreateWebhookInputDto": { + "type": "object", + "properties": { + "payloadTemplate": { + "type": "string", + "description": "The template of the payload that will be sent to the subscriberUrl, check cal.com/docs/core-features/webhooks for more information", + "example": "{\"content\":\"A new event has been scheduled\",\"type\":\"{{type}}\",\"name\":\"{{title}}\",\"organizer\":\"{{organizer.name}}\",\"booker\":\"{{attendees.0.name}}\"}" + }, + "triggers": { + "type": "string", + "example": [ + "BOOKING_CREATED", + "BOOKING_RESCHEDULED", + "BOOKING_CANCELLED", + "BOOKING_CONFIRMED", + "BOOKING_REJECTED", + "BOOKING_COMPLETED", + "BOOKING_NO_SHOW", + "BOOKING_REOPENED" + ], + "enum": [ + "BOOKING_CREATED", + "BOOKING_PAYMENT_INITIATED", + "BOOKING_PAID", + "BOOKING_RESCHEDULED", + "BOOKING_REQUESTED", + "BOOKING_CANCELLED", + "BOOKING_REJECTED", + "BOOKING_NO_SHOW_UPDATED", + "FORM_SUBMITTED", + "MEETING_ENDED", + "MEETING_STARTED", + "RECORDING_READY", + "INSTANT_MEETING", + "RECORDING_TRANSCRIPTION_GENERATED" + ] + }, + "active": { + "type": "boolean" + }, + "subscriberUrl": { + "type": "string" + }, + "secret": { + "type": "string" + } + }, + "required": [ + "triggers", + "active", + "subscriberUrl" + ] + }, + "TeamWebhookOutputResponseDto": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/TeamWebhookOutputDto" + } + }, + "required": [ + "status", + "data" + ] + }, + "UpdateWebhookInputDto": { + "type": "object", + "properties": { + "payloadTemplate": { + "type": "string", + "description": "The template of the payload that will be sent to the subscriberUrl, check cal.com/docs/core-features/webhooks for more information", + "example": "{\"content\":\"A new event has been scheduled\",\"type\":\"{{type}}\",\"name\":\"{{title}}\",\"organizer\":\"{{organizer.name}}\",\"booker\":\"{{attendees.0.name}}\"}" + }, + "triggers": { + "type": "string", + "example": [ + "BOOKING_CREATED", + "BOOKING_RESCHEDULED", + "BOOKING_CANCELLED", + "BOOKING_CONFIRMED", + "BOOKING_REJECTED", + "BOOKING_COMPLETED", + "BOOKING_NO_SHOW", + "BOOKING_REOPENED" + ], + "enum": [ + "BOOKING_CREATED", + "BOOKING_PAYMENT_INITIATED", + "BOOKING_PAID", + "BOOKING_RESCHEDULED", + "BOOKING_REQUESTED", + "BOOKING_CANCELLED", + "BOOKING_REJECTED", + "BOOKING_NO_SHOW_UPDATED", + "FORM_SUBMITTED", + "MEETING_ENDED", + "MEETING_STARTED", + "RECORDING_READY", + "INSTANT_MEETING", + "RECORDING_TRANSCRIPTION_GENERATED" + ] + }, + "active": { + "type": "boolean" + }, + "subscriberUrl": { + "type": "string" + }, + "secret": { + "type": "string" + } + } + }, "GetDefaultScheduleOutput_2024_06_11": { "type": "object", "properties": { @@ -9540,59 +9699,6 @@ "type": "object", "properties": {} }, - "CreateWebhookInputDto": { - "type": "object", - "properties": { - "payloadTemplate": { - "type": "string", - "description": "The template of the payload that will be sent to the subscriberUrl, check cal.com/docs/core-features/webhooks for more information", - "example": "{\"content\":\"A new event has been scheduled\",\"type\":\"{{type}}\",\"name\":\"{{title}}\",\"organizer\":\"{{organizer.name}}\",\"booker\":\"{{attendees.0.name}}\"}" - }, - "triggers": { - "type": "string", - "example": [ - "BOOKING_CREATED", - "BOOKING_RESCHEDULED", - "BOOKING_CANCELLED", - "BOOKING_CONFIRMED", - "BOOKING_REJECTED", - "BOOKING_COMPLETED", - "BOOKING_NO_SHOW", - "BOOKING_REOPENED" - ], - "enum": [ - "BOOKING_CREATED", - "BOOKING_PAYMENT_INITIATED", - "BOOKING_PAID", - "BOOKING_RESCHEDULED", - "BOOKING_REQUESTED", - "BOOKING_CANCELLED", - "BOOKING_REJECTED", - "BOOKING_NO_SHOW_UPDATED", - "FORM_SUBMITTED", - "MEETING_ENDED", - "MEETING_STARTED", - "RECORDING_READY", - "INSTANT_MEETING", - "RECORDING_TRANSCRIPTION_GENERATED" - ] - }, - "active": { - "type": "boolean" - }, - "subscriberUrl": { - "type": "string" - }, - "secret": { - "type": "string" - } - }, - "required": [ - "triggers", - "active", - "subscriberUrl" - ] - }, "UserWebhookOutputDto": { "type": "object", "properties": { @@ -9632,84 +9738,6 @@ "active" ] }, - "OrgWebhookOutputDto": { - "type": "object", - "properties": { - "payloadTemplate": { - "type": "string", - "description": "The template of the payload that will be sent to the subscriberUrl, check cal.com/docs/core-features/webhooks for more information", - "example": "{\"content\":\"A new event has been scheduled\",\"type\":\"{{type}}\",\"name\":\"{{title}}\",\"organizer\":\"{{organizer.name}}\",\"booker\":\"{{attendees.0.name}}\"}" - }, - "teamId": { - "type": "number" - }, - "id": { - "type": "number" - }, - "triggers": { - "type": "array", - "items": { - "type": "object" - } - }, - "subscriberUrl": { - "type": "string" - }, - "active": { - "type": "boolean" - }, - "secret": { - "type": "string" - } - }, - "required": [ - "payloadTemplate", - "userId", - "id", - "triggers", - "subscriberUrl", - "active" - ] - }, - "GetAllOrgWebhooksDto": { - "type": "object", - "properties": { - "payloadTemplate": { - "type": "string", - "description": "The template of the payload that will be sent to the subscriberUrl, check cal.com/docs/core-features/webhooks for more information", - "example": "{\"content\":\"A new event has been scheduled\",\"type\":\"{{type}}\",\"name\":\"{{title}}\",\"organizer\":\"{{organizer.name}}\",\"booker\":\"{{attendees.0.name}}\"}" - }, - "teamId": { - "type": "number" - }, - "id": { - "type": "number" - }, - "triggers": { - "type": "array", - "items": { - "type": "object" - } - }, - "subscriberUrl": { - "type": "string" - }, - "active": { - "type": "boolean" - }, - "secret": { - "type": "string" - } - }, - "required": [ - "payloadTemplate", - "userId", - "id", - "triggers", - "subscriberUrl", - "active" - ] - }, "UserWebhookOutputResponseDto": { "type": "object", "properties": { @@ -9730,74 +9758,6 @@ "data" ] }, - "OrgWebhooksResponseDto": { - "type": "object", - "properties": { - "status": { - "type": "string", - "example": "success", - "enum": [ - "success", - "error" - ] - }, - "data": { - "$ref": "#/components/schemas/GetAllOrgWebhooksDto" - } - }, - "required": [ - "status", - "data" - ] - }, - "UpdateWebhookInputDto": { - "type": "object", - "properties": { - "payloadTemplate": { - "type": "string", - "description": "The template of the payload that will be sent to the subscriberUrl, check cal.com/docs/core-features/webhooks for more information", - "example": "{\"content\":\"A new event has been scheduled\",\"type\":\"{{type}}\",\"name\":\"{{title}}\",\"organizer\":\"{{organizer.name}}\",\"booker\":\"{{attendees.0.name}}\"}" - }, - "triggers": { - "type": "string", - "example": [ - "BOOKING_CREATED", - "BOOKING_RESCHEDULED", - "BOOKING_CANCELLED", - "BOOKING_CONFIRMED", - "BOOKING_REJECTED", - "BOOKING_COMPLETED", - "BOOKING_NO_SHOW", - "BOOKING_REOPENED" - ], - "enum": [ - "BOOKING_CREATED", - "BOOKING_PAYMENT_INITIATED", - "BOOKING_PAID", - "BOOKING_RESCHEDULED", - "BOOKING_REQUESTED", - "BOOKING_CANCELLED", - "BOOKING_REJECTED", - "BOOKING_NO_SHOW_UPDATED", - "FORM_SUBMITTED", - "MEETING_ENDED", - "MEETING_STARTED", - "RECORDING_READY", - "INSTANT_MEETING", - "RECORDING_TRANSCRIPTION_GENERATED" - ] - }, - "active": { - "type": "boolean" - }, - "subscriberUrl": { - "type": "string" - }, - "secret": { - "type": "string" - } - } - }, "UserWebhooksOutputResponseDto": { "type": "object", "properties": { diff --git a/packages/platform/types/bookings/2024-08-13/outputs/booking.output.ts b/packages/platform/types/bookings/2024-08-13/outputs/booking.output.ts index 0f7278683ca105..db8eff9a64e61c 100644 --- a/packages/platform/types/bookings/2024-08-13/outputs/booking.output.ts +++ b/packages/platform/types/bookings/2024-08-13/outputs/booking.output.ts @@ -83,11 +83,6 @@ export class BookingOutput_2024_08_13 { @Expose() rescheduledFromUid?: string; - @IsString() - @IsOptional() - @Expose() - rescheduledToUid?: string; - @IsDateString() @Expose() start!: string; @@ -148,6 +143,16 @@ export class RecurringBookingOutput_2024_08_13 { @Expose() cancellationReason?: string; + @IsString() + @IsOptional() + @Expose() + reschedulingReason?: string; + + @IsString() + @IsOptional() + @Expose() + rescheduledFromUid?: string; + @IsDateString() @Expose() start!: string; From 7dbd4b3035ab45cc19a33ac33000a024e39446ef Mon Sep 17 00:00:00 2001 From: supalarry Date: Mon, 2 Sep 2024 13:29:47 +0200 Subject: [PATCH 46/96] refactor: absent --- .../bookings.controller.e2e-spec.ts | 2 +- .../2024-08-13/services/input.service.ts | 5 ++++- .../2024-08-13/inputs/mark-absent.input.ts | 20 ++++++++++++++----- 3 files changed, 20 insertions(+), 7 deletions(-) diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.e2e-spec.ts b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.e2e-spec.ts index ba7b2e1209ef4d..15027dff8f506a 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.e2e-spec.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.e2e-spec.ts @@ -894,7 +894,7 @@ describe("Bookings Endpoints 2024-08-13", () => { it("should mark attendee absent", async () => { const body: MarkAbsentBookingInput_2024_08_13 = { - attendees: ["mr_proper_recurring@gmail.com"], + attendees: [{ email: "mr_proper_recurring@gmail.com", absent: true }], }; return request(app.getHttpServer()) diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts index 4abf11add66e73..878a0526385414 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts @@ -393,7 +393,10 @@ export class InputBookingsService_2024_08_13 { transformInputMarkAbsentBooking(inputBooking: MarkAbsentBookingInput_2024_08_13) { return { noShowHost: inputBooking.host, - attendees: inputBooking.attendees?.map((email) => ({ email, noShow: true })), + attendees: inputBooking.attendees?.map((attendee) => ({ + email: attendee.email, + noShow: attendee.absent, + })), }; } } diff --git a/packages/platform/types/bookings/2024-08-13/inputs/mark-absent.input.ts b/packages/platform/types/bookings/2024-08-13/inputs/mark-absent.input.ts index c5bbcc11f154f5..ac2dd897d4f880 100644 --- a/packages/platform/types/bookings/2024-08-13/inputs/mark-absent.input.ts +++ b/packages/platform/types/bookings/2024-08-13/inputs/mark-absent.input.ts @@ -1,5 +1,14 @@ import { ApiProperty } from "@nestjs/swagger"; -import { IsOptional, IsBoolean, IsEmail, IsArray, ArrayMinSize } from "class-validator"; +import { Type } from "class-transformer"; +import { IsOptional, IsBoolean, IsEmail, IsArray, ArrayMinSize, ValidateNested } from "class-validator"; + +class Attendee { + @IsEmail() + email!: string; + + @IsBoolean() + absent!: boolean; +} export class MarkAbsentBookingInput_2024_08_13 { @IsBoolean() @@ -7,10 +16,11 @@ export class MarkAbsentBookingInput_2024_08_13 { @ApiProperty() host?: boolean; - @IsArray() @ArrayMinSize(1) - @IsEmail({}, { each: true }) - @IsOptional() @ApiProperty({ type: [String] }) - attendees?: string[]; + @ValidateNested() + @Type(() => Attendee) + @IsArray() + @IsOptional() + attendees?: Attendee[]; } From 1584e137b9cae322d12f69a600911ed1303b18ca Mon Sep 17 00:00:00 2001 From: supalarry Date: Mon, 2 Sep 2024 14:31:38 +0200 Subject: [PATCH 47/96] fix: make work with api key --- .../ee/bookings/2024-08-13/bookings.module.ts | 2 + .../bookings.controller.e2e-spec.ts | 141 ++++++++++++++++++ .../2024-08-13/services/input.service.ts | 21 ++- 3 files changed, 160 insertions(+), 4 deletions(-) diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/bookings.module.ts b/apps/api/v2/src/ee/bookings/2024-08-13/bookings.module.ts index 47e8afc2c5ab94..21737c4717fe9f 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/bookings.module.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/bookings.module.ts @@ -4,6 +4,7 @@ import { BookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/bo import { InputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/input.service"; import { OutputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/output.service"; import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository"; +import { ApiKeyRepository } from "@/modules/api-key/api-key-repository"; import { BillingModule } from "@/modules/billing/billing.module"; import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository"; import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service"; @@ -25,6 +26,7 @@ import { Module } from "@nestjs/common"; OutputBookingsService_2024_08_13, BookingsRepository_2024_08_13, EventTypesRepository_2024_06_14, + ApiKeyRepository, ], controllers: [BookingsController_2024_08_13], }) diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.e2e-spec.ts b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.e2e-spec.ts index 15027dff8f506a..565eb47f6bda4b 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.e2e-spec.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.e2e-spec.ts @@ -17,6 +17,7 @@ import { NestExpressApplication } from "@nestjs/platform-express"; import { Test } from "@nestjs/testing"; import { User } from "@prisma/client"; import * as request from "supertest"; +import { ApiKeysRepositoryFixture } from "test/fixtures/repository/api-keys.repository.fixture"; import { BookingsRepositoryFixture } from "test/fixtures/repository/bookings.repository.fixture"; import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture"; import { HostsRepositoryFixture } from "test/fixtures/repository/hosts.repository.fixture"; @@ -57,6 +58,8 @@ describe("Bookings Endpoints 2024-08-13", () => { let oauthClientRepositoryFixture: OAuthClientRepositoryFixture; let oAuthClient: PlatformOAuthClient; let teamRepositoryFixture: TeamRepositoryFixture; + let apiKeysRepositoryFixture: ApiKeysRepositoryFixture; + let apiKeyString: string; const userEmail = "bookings-controller-e2e@api.com"; let user: User; @@ -89,6 +92,7 @@ describe("Bookings Endpoints 2024-08-13", () => { oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef); teamRepositoryFixture = new TeamRepositoryFixture(moduleRef); schedulesService = moduleRef.get(SchedulesService_2024_04_15); + apiKeysRepositoryFixture = new ApiKeysRepositoryFixture(moduleRef); organization = await teamRepositoryFixture.create({ name: "organization bookings" }); oAuthClient = await createOAuthClient(organization.id); @@ -97,6 +101,9 @@ describe("Bookings Endpoints 2024-08-13", () => { email: userEmail, }); + const { keyString } = await apiKeysRepositoryFixture.createApiKey(user.id, null); + apiKeyString = keyString; + const userSchedule: CreateScheduleInput_2024_04_15 = { name: "working time", timeZone: "Europe/Rome", @@ -1434,4 +1441,138 @@ describe("Bookings Endpoints 2024-08-13", () => { await app.close(); }); }); + + describe("With api key", () => { + let app: INestApplication; + let organization: Team; + + let userRepositoryFixture: UserRepositoryFixture; + let bookingsRepositoryFixture: BookingsRepositoryFixture; + let schedulesService: SchedulesService_2024_04_15; + let eventTypesRepositoryFixture: EventTypesRepositoryFixture; + let teamRepositoryFixture: TeamRepositoryFixture; + let apiKeysRepositoryFixture: ApiKeysRepositoryFixture; + let apiKeyString: string; + + const userEmail = "bookings-controller-e2e@api.com"; + let user: User; + + let eventTypeId: number; + + beforeAll(async () => { + const moduleRef = await withApiAuth( + userEmail, + Test.createTestingModule({ + imports: [AppModule, PrismaModule, UsersModule, SchedulesModule_2024_04_15], + }) + ) + .overrideGuard(PermissionsGuard) + .useValue({ + canActivate: () => true, + }) + .compile(); + + userRepositoryFixture = new UserRepositoryFixture(moduleRef); + bookingsRepositoryFixture = new BookingsRepositoryFixture(moduleRef); + eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef); + teamRepositoryFixture = new TeamRepositoryFixture(moduleRef); + schedulesService = moduleRef.get(SchedulesService_2024_04_15); + apiKeysRepositoryFixture = new ApiKeysRepositoryFixture(moduleRef); + + organization = await teamRepositoryFixture.create({ name: "organization bookings" }); + + user = await userRepositoryFixture.create({ + email: userEmail, + }); + + const { keyString } = await apiKeysRepositoryFixture.createApiKey(user.id, null); + apiKeyString = keyString; + + const userSchedule: CreateScheduleInput_2024_04_15 = { + name: "working time", + timeZone: "Europe/Rome", + isDefault: true, + }; + await schedulesService.createUserSchedule(user.id, userSchedule); + const event = await eventTypesRepositoryFixture.create( + { title: "peer coding", slug: "peer-coding", length: 60 }, + user.id + ); + eventTypeId = event.id; + + app = moduleRef.createNestApplication(); + bootstrap(app as NestExpressApplication); + + await app.init(); + }); + + it("should be defined", () => { + expect(userRepositoryFixture).toBeDefined(); + expect(user).toBeDefined(); + }); + + describe("create bookings", () => { + it("should create a booking with api key", async () => { + const body: CreateBookingInput_2024_08_13 = { + start: new Date(Date.UTC(2030, 0, 8, 13, 0, 0)).toISOString(), + eventTypeId, + attendee: { + name: "Mr Key", + email: "mr_key@gmail.com", + timeZone: "Europe/Rome", + language: "it", + }, + meetingUrl: "https://meet.google.com/abc-def-ghi", + }; + + return request(app.getHttpServer()) + .post("/v2/bookings") + .send(body) + .set({ Authorization: `Bearer cal_test_${apiKeyString}` }) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .expect(201) + .then(async (response) => { + const responseBody: CreateBookingOutput_2024_08_13 = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + expect(responseDataIsBooking(responseBody.data)).toBe(true); + + if (responseDataIsBooking(responseBody.data)) { + const data: BookingOutput_2024_08_13 = responseBody.data; + expect(data.id).toBeDefined(); + expect(data.uid).toBeDefined(); + expect(data.hosts[0].id).toEqual(user.id); + expect(data.status).toEqual("accepted"); + expect(data.start).toEqual(body.start); + expect(data.end).toEqual(new Date(Date.UTC(2030, 0, 8, 14, 0, 0)).toISOString()); + expect(data.duration).toEqual(60); + expect(data.eventTypeId).toEqual(eventTypeId); + expect(data.attendees[0]).toEqual({ + name: body.attendee.name, + timeZone: body.attendee.timeZone, + language: body.attendee.language, + absent: false, + }); + expect(data.meetingUrl).toEqual(body.meetingUrl); + expect(data.absentHost).toEqual(false); + } else { + throw new Error( + "Invalid response data - expected booking but received array of possibily recurring bookings" + ); + } + }); + }); + }); + + function responseDataIsBooking(data: any): data is BookingOutput_2024_08_13 { + return !Array.isArray(data) && typeof data === "object" && data && "id" in data; + } + + afterAll(async () => { + await teamRepositoryFixture.delete(organization.id); + await userRepositoryFixture.deleteByEmail(user.email); + await bookingsRepositoryFixture.deleteAllBookings(user.id, user.email); + await app.close(); + }); + }); }); diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts index 878a0526385414..6d05e29e63bdfe 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts @@ -1,10 +1,13 @@ import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.repository"; import { bookingResponsesSchema } from "@/ee/bookings/2024-08-13/services/output.service"; import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository"; +import { hashAPIKey, isApiKey, stripApiKey } from "@/lib/api-key"; +import { ApiKeyRepository } from "@/modules/api-key/api-key-repository"; import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository"; import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service"; import { Injectable, NotFoundException } from "@nestjs/common"; import { Logger } from "@nestjs/common"; +import { ConfigService } from "@nestjs/config"; import { Request } from "express"; import { DateTime } from "luxon"; import { NextApiRequest } from "next/types"; @@ -68,7 +71,9 @@ export class InputBookingsService_2024_08_13 { private readonly oAuthFlowService: OAuthFlowService, private readonly oAuthClientRepository: OAuthClientRepository, private readonly eventTypesRepository: EventTypesRepository_2024_06_14, - private readonly bookingsRepository: BookingsRepository_2024_08_13 + private readonly bookingsRepository: BookingsRepository_2024_08_13, + private readonly config: ConfigService, + private readonly apiKeyRepository: ApiKeyRepository ) {} async createBookingRequest( @@ -299,9 +304,17 @@ export class InputBookingsService_2024_08_13 { private async createBookingRequestOwnerId(req: Request): Promise { try { - const accessToken = req.get("Authorization")?.replace("Bearer ", ""); - if (accessToken) { - return this.oAuthFlowService.getOwnerId(accessToken); + const bearerToken = req.get("Authorization")?.replace("Bearer ", ""); + if (bearerToken) { + if (isApiKey(bearerToken, this.config.get("api.apiKeyPrefix") ?? "cal_")) { + const strippedApiKey = stripApiKey(bearerToken, this.config.get("api.keyPrefix")); + const apiKeyHash = hashAPIKey(strippedApiKey); + const keyData = await this.apiKeyRepository.getApiKeyFromHash(apiKeyHash); + return keyData?.userId; + } else { + // Access Token + return this.oAuthFlowService.getOwnerId(bearerToken); + } } } catch (err) { this.logger.error(err); From 9359f01181f5c99f99070db8e57c1404f8e8060d Mon Sep 17 00:00:00 2001 From: supalarry Date: Mon, 2 Sep 2024 14:51:58 +0200 Subject: [PATCH 48/96] test --- apps/api/v2/package.json | 2 +- .../bookings.controller.e2e-spec.ts | 6 ------ yarn.lock | 20 +++++++++---------- 3 files changed, 11 insertions(+), 17 deletions(-) diff --git a/apps/api/v2/package.json b/apps/api/v2/package.json index fbecf9f3773182..9979906bf70a0d 100644 --- a/apps/api/v2/package.json +++ b/apps/api/v2/package.json @@ -64,7 +64,7 @@ "reflect-metadata": "^0.1.13", "rxjs": "^7.8.1", "stripe": "^15.3.0", - "uuid": "^10.0.0", + "uuid": "^8.3.2", "winston": "^3.11.0", "zod": "^3.22.4" }, diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.e2e-spec.ts b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.e2e-spec.ts index 565eb47f6bda4b..b56ef89f6b2b6e 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.e2e-spec.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.e2e-spec.ts @@ -58,8 +58,6 @@ describe("Bookings Endpoints 2024-08-13", () => { let oauthClientRepositoryFixture: OAuthClientRepositoryFixture; let oAuthClient: PlatformOAuthClient; let teamRepositoryFixture: TeamRepositoryFixture; - let apiKeysRepositoryFixture: ApiKeysRepositoryFixture; - let apiKeyString: string; const userEmail = "bookings-controller-e2e@api.com"; let user: User; @@ -92,7 +90,6 @@ describe("Bookings Endpoints 2024-08-13", () => { oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef); teamRepositoryFixture = new TeamRepositoryFixture(moduleRef); schedulesService = moduleRef.get(SchedulesService_2024_04_15); - apiKeysRepositoryFixture = new ApiKeysRepositoryFixture(moduleRef); organization = await teamRepositoryFixture.create({ name: "organization bookings" }); oAuthClient = await createOAuthClient(organization.id); @@ -101,9 +98,6 @@ describe("Bookings Endpoints 2024-08-13", () => { email: userEmail, }); - const { keyString } = await apiKeysRepositoryFixture.createApiKey(user.id, null); - apiKeyString = keyString; - const userSchedule: CreateScheduleInput_2024_04_15 = { name: "working time", timeZone: "Europe/Rome", diff --git a/yarn.lock b/yarn.lock index 7bad996cad9958..7a358c9ec8e9a6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4192,7 +4192,7 @@ __metadata: ts-node: ^10.9.1 tsconfig-paths: ^4.1.0 typescript: ^4.9.4 - uuid: ^10.0.0 + uuid: ^8.3.2 winston: ^3.11.0 zod: ^3.22.4 languageName: unknown @@ -5440,6 +5440,15 @@ __metadata: languageName: unknown linkType: soft +"@calcom/synthflow@workspace:packages/app-store/synthflow": + version: 0.0.0-use.local + resolution: "@calcom/synthflow@workspace:packages/app-store/synthflow" + dependencies: + "@calcom/lib": "*" + "@calcom/types": "*" + languageName: unknown + linkType: soft + "@calcom/tandemvideo@workspace:packages/app-store/tandemvideo": version: 0.0.0-use.local resolution: "@calcom/tandemvideo@workspace:packages/app-store/tandemvideo" @@ -47478,15 +47487,6 @@ __metadata: languageName: node linkType: hard -"uuid@npm:^10.0.0": - version: 10.0.0 - resolution: "uuid@npm:10.0.0" - bin: - uuid: dist/bin/uuid - checksum: 4b81611ade2885d2313ddd8dc865d93d8dccc13ddf901745edca8f86d99bc46d7a330d678e7532e7ebf93ce616679fb19b2e3568873ac0c14c999032acb25869 - languageName: node - linkType: hard - "uuid@npm:^3.3.2": version: 3.4.0 resolution: "uuid@npm:3.4.0" From 927b66998edceb2e7155201efb4ba16103df8390 Mon Sep 17 00:00:00 2001 From: supalarry Date: Tue, 3 Sep 2024 09:19:23 +0200 Subject: [PATCH 49/96] ts remove any --- .../api/v2/src/ee/bookings/2024-08-13/services/input.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts index 6d05e29e63bdfe..f3d755fd6508c4 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts @@ -150,7 +150,7 @@ export class InputBookingsService_2024_08_13 { const location = request.body.meetingUrl; Object.assign(newRequest, { userId, ...oAuthParams, platformBookingLocation: location }); - newRequest.body = (bodyTransformed as any[]).map((event) => ({ + newRequest.body = bodyTransformed.map((event) => ({ ...event, })); From 45b593bb27210e5f32367d3077112498c9340690 Mon Sep 17 00:00:00 2001 From: supalarry Date: Mon, 9 Sep 2024 08:40:27 +0200 Subject: [PATCH 50/96] feat: BookingUidGuard --- .../controllers/bookings.controller.ts | 23 ++++--------------- .../2024-08-13/guards/booking-uid.guard.ts | 16 +++++++++++++ 2 files changed, 21 insertions(+), 18 deletions(-) create mode 100644 apps/api/v2/src/ee/bookings/2024-08-13/guards/booking-uid.guard.ts diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.ts b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.ts index fecebc7fe23084..3d58d9393787fa 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.ts @@ -1,3 +1,4 @@ +import { BookingUidGuard } from "@/ee/bookings/2024-08-13/guards/booking-uid.guard"; import { CancelBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/cancel-booking.output copy"; import { CreateBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/create-booking.output"; import { GetBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/get-booking.output"; @@ -20,7 +21,6 @@ import { Get, Param, Query, - BadRequestException, HttpCode, HttpStatus, } from "@nestjs/common"; @@ -70,11 +70,8 @@ export class BookingsController_2024_08_13 { } @Get("/:bookingUid") + @UseGuards(BookingUidGuard) async getBooking(@Param("bookingUid") bookingUid: string): Promise { - if (!bookingUid) { - throw new BadRequestException("Booking UID is required in request path /:bookingUid"); - } - const booking = await this.bookingsService.getBooking(bookingUid); return { @@ -99,15 +96,12 @@ export class BookingsController_2024_08_13 { } @Post("/:bookingUid/reschedule") + @UseGuards(BookingUidGuard) async rescheduleBooking( @Param("bookingUid") bookingUid: string, @Body() body: RescheduleBookingInput_2024_08_13, @Req() request: Request ): Promise { - if (!bookingUid) { - throw new BadRequestException("Booking UID is required in request path /:bookingUid/reschedule"); - } - const newBooking = await this.bookingsService.rescheduleBooking(request, bookingUid, body); await this.bookingsService.billRescheduledBooking(newBooking, bookingUid); @@ -118,16 +112,13 @@ export class BookingsController_2024_08_13 { } @Post("/:bookingUid/cancel") + @UseGuards(BookingUidGuard) @HttpCode(HttpStatus.OK) async cancelBooking( @Req() request: Request, @Param("bookingUid") bookingUid: string, @Body() body: CancelBookingInput_2024_08_13 ): Promise { - if (!bookingUid) { - throw new BadRequestException("Booking UID is required in request path /:bookingUid/cancel"); - } - const cancelledBooking = await this.bookingsService.cancelBooking(request, bookingUid, body); return { @@ -139,16 +130,12 @@ export class BookingsController_2024_08_13 { @Post("/:bookingUid/mark-absent") @HttpCode(HttpStatus.OK) @Permissions([BOOKING_WRITE]) - @UseGuards(ApiAuthGuard) + @UseGuards(ApiAuthGuard, BookingUidGuard) async markNoShow( @Param("bookingUid") bookingUid: string, @Body() body: MarkAbsentBookingInput_2024_08_13, @GetUser("id") ownerId: number ): Promise { - if (!bookingUid) { - throw new BadRequestException("Booking UID is required in request path /:bookingUid/cancel"); - } - const booking = await this.bookingsService.markAbsent(bookingUid, ownerId, body); return { diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/guards/booking-uid.guard.ts b/apps/api/v2/src/ee/bookings/2024-08-13/guards/booking-uid.guard.ts new file mode 100644 index 00000000000000..05d508a4fb38e9 --- /dev/null +++ b/apps/api/v2/src/ee/bookings/2024-08-13/guards/booking-uid.guard.ts @@ -0,0 +1,16 @@ +import { Injectable, CanActivate, ExecutionContext, BadRequestException } from "@nestjs/common"; + +@Injectable() +export class BookingUidGuard implements CanActivate { + canActivate(context: ExecutionContext): boolean { + const request = context.switchToHttp().getRequest(); + + const bookingUid = request.params.bookingUid; + + if (!bookingUid) { + throw new BadRequestException("Booking UID missing in the request path"); + } + + return true; + } +} From 7f8ba4987d5fc0329fe9610a10f880c8b9156f11 Mon Sep 17 00:00:00 2001 From: supalarry Date: Mon, 9 Sep 2024 09:04:50 +0200 Subject: [PATCH 51/96] fix: recurring booking no email --- .../src/ee/bookings/2024-08-13/services/input.service.ts | 9 ++++++--- .../features/bookings/lib/handleNewRecurringBooking.ts | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts index f3d755fd6508c4..46b587c2e1e28b 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts @@ -148,14 +148,17 @@ export class InputBookingsService_2024_08_13 { : DEFAULT_PLATFORM_PARAMS; const location = request.body.meetingUrl; - Object.assign(newRequest, { userId, ...oAuthParams, platformBookingLocation: location }); + Object.assign(newRequest, { + userId, + ...oAuthParams, + platformBookingLocation: location, + noEmail: !oAuthParams.arePlatformEmailsEnabled, + }); newRequest.body = bodyTransformed.map((event) => ({ ...event, })); - newRequest.body.noEmail = !oAuthParams.arePlatformEmailsEnabled; - return newRequest as unknown as BookingRequest; } diff --git a/packages/features/bookings/lib/handleNewRecurringBooking.ts b/packages/features/bookings/lib/handleNewRecurringBooking.ts index c2703bc71836d9..3185046f38f77a 100644 --- a/packages/features/bookings/lib/handleNewRecurringBooking.ts +++ b/packages/features/bookings/lib/handleNewRecurringBooking.ts @@ -81,7 +81,7 @@ export const handleNewRecurringBooking = async ( thirdPartyRecurringEventId, numSlotsToCheckForAvailability, currentRecurringIndex: key, - noEmail: req.body.noEmail !== undefined ? req.body.noEmail : key !== 0, + noEmail: req.noEmail !== undefined ? req.noEmail : key !== 0, luckyUsers, }; From 958fd5cae63c29bef8db2a6e881119f6b95c4d3e Mon Sep 17 00:00:00 2001 From: supalarry Date: Mon, 9 Sep 2024 09:23:45 +0200 Subject: [PATCH 52/96] fix: legacy bookings recurring noEmail --- .../controllers/bookings.controller.e2e-spec.ts | 2 +- .../2024-04-15/controllers/bookings.controller.ts | 11 ++++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/apps/api/v2/src/ee/bookings/2024-04-15/controllers/bookings.controller.e2e-spec.ts b/apps/api/v2/src/ee/bookings/2024-04-15/controllers/bookings.controller.e2e-spec.ts index b614e14f3ed594..0e249c11fdb636 100644 --- a/apps/api/v2/src/ee/bookings/2024-04-15/controllers/bookings.controller.e2e-spec.ts +++ b/apps/api/v2/src/ee/bookings/2024-04-15/controllers/bookings.controller.e2e-spec.ts @@ -166,7 +166,7 @@ describe("Bookings Endpoints 2024-04-15", () => { guests: [], }; - const body: CreateBookingInput = { + const body: CreateBookingInput_2024_04_15 = { start: bookingStart, end: bookingEnd, eventTypeId: bookingEventTypeId, diff --git a/apps/api/v2/src/ee/bookings/2024-04-15/controllers/bookings.controller.ts b/apps/api/v2/src/ee/bookings/2024-04-15/controllers/bookings.controller.ts index f4415621df95da..a7c8ea2f2440a8 100644 --- a/apps/api/v2/src/ee/bookings/2024-04-15/controllers/bookings.controller.ts +++ b/apps/api/v2/src/ee/bookings/2024-04-15/controllers/bookings.controller.ts @@ -43,7 +43,6 @@ import { handleNewBooking, BookingResponse, HttpError, - handleNewRecurringBooking, handleInstantMeeting, handleMarkNoShow, getAllUserBookings, @@ -51,7 +50,8 @@ import { handleCancelBooking, getBookingForReschedule, ErrorCode, -} from "@calcom/platform-libraries-1.2.3"; +} from "@calcom/platform-libraries"; +import { handleNewRecurringBooking } from "@calcom/platform-libraries-1.2.3"; import { GetBookingsInput_2024_04_15, CancelBookingInput_2024_04_15, @@ -373,7 +373,12 @@ export class BookingsController_2024_04_15 { const oAuthParams = oAuthClientId ? await this.getOAuthClientsParams(oAuthClientId) : DEFAULT_PLATFORM_PARAMS; - Object.assign(req, { userId, ...oAuthParams, platformBookingLocation }); + Object.assign(req, { + userId, + ...oAuthParams, + platformBookingLocation, + noEmail: !oAuthParams.arePlatformEmailsEnabled, + }); return req as unknown as NextApiRequest & { userId?: number } & OAuthRequestParams; } From 53a73d0d4499e55a081ca7cb4d3261c1399fab0f Mon Sep 17 00:00:00 2001 From: supalarry Date: Mon, 9 Sep 2024 10:00:54 +0200 Subject: [PATCH 53/96] add swagger --- apps/api/v2/swagger/documentation.json | 11022 +++++++++++++++++++++++ 1 file changed, 11022 insertions(+) create mode 100644 apps/api/v2/swagger/documentation.json diff --git a/apps/api/v2/swagger/documentation.json b/apps/api/v2/swagger/documentation.json new file mode 100644 index 00000000000000..28d9bd26efff69 --- /dev/null +++ b/apps/api/v2/swagger/documentation.json @@ -0,0 +1,11022 @@ +{ + "openapi": "3.0.0", + "paths": { + "/health": { + "get": { + "operationId": "AppController_getHealth", + "parameters": [], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + }, + "tags": [ + "Health - development only" + ] + } + }, + "/v2/oauth-clients/{clientId}/users": { + "get": { + "operationId": "OAuthClientUsersController_getManagedUsers", + "parameters": [ + { + "name": "clientId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + }, + { + "name": "limit", + "required": false, + "in": "query", + "description": "The number of items to return", + "example": 10, + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetManagedUsersOutput" + } + } + } + } + }, + "tags": [ + "Managed users" + ] + }, + "post": { + "operationId": "OAuthClientUsersController_createUser", + "parameters": [ + { + "name": "clientId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateManagedUserInput" + } + } + } + }, + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateManagedUserOutput" + } + } + } + } + }, + "tags": [ + "Managed users" + ] + } + }, + "/v2/oauth-clients/{clientId}/users/{userId}": { + "get": { + "operationId": "OAuthClientUsersController_getUserById", + "parameters": [ + { + "name": "clientId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + }, + { + "name": "userId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetManagedUserOutput" + } + } + } + } + }, + "tags": [ + "Managed users" + ] + }, + "patch": { + "operationId": "OAuthClientUsersController_updateUser", + "parameters": [ + { + "name": "clientId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + }, + { + "name": "userId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateManagedUserInput" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetManagedUserOutput" + } + } + } + } + }, + "tags": [ + "Managed users" + ] + }, + "delete": { + "operationId": "OAuthClientUsersController_deleteUser", + "parameters": [ + { + "name": "clientId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + }, + { + "name": "userId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetManagedUserOutput" + } + } + } + } + }, + "tags": [ + "Managed users" + ] + } + }, + "/v2/oauth-clients/{clientId}/users/{userId}/force-refresh": { + "post": { + "operationId": "OAuthClientUsersController_forceRefresh", + "parameters": [ + { + "name": "userId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "clientId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/KeysResponseDto" + } + } + } + } + }, + "tags": [ + "Managed users" + ] + } + }, + "/v2/oauth-clients": { + "post": { + "operationId": "OAuthClientsController_createOAuthClient", + "summary": "", + "description": "⚠️ First, this endpoint requires `Cookie: next-auth.session-token=eyJhbGciOiJ` header. Log into Cal web app using owner of organization that was created after visiting `/settings/organizations/new`, refresh swagger docs, and the cookie will be added to requests automatically to pass the NextAuthGuard.\nSecond, make sure that the logged in user has organizationId set to pass the OrganizationRolesGuard guard.", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateOAuthClientInput" + } + } + } + }, + "responses": { + "201": { + "description": "Create an OAuth client", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateOAuthClientResponseDto" + } + } + } + } + }, + "tags": [ + "OAuth - development only" + ] + }, + "get": { + "operationId": "OAuthClientsController_getOAuthClients", + "summary": "", + "description": "⚠️ First, this endpoint requires `Cookie: next-auth.session-token=eyJhbGciOiJ` header. Log into Cal web app using owner of organization that was created after visiting `/settings/organizations/new`, refresh swagger docs, and the cookie will be added to requests automatically to pass the NextAuthGuard.\nSecond, make sure that the logged in user has organizationId set to pass the OrganizationRolesGuard guard.", + "parameters": [], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetOAuthClientsResponseDto" + } + } + } + } + }, + "tags": [ + "OAuth - development only" + ] + } + }, + "/v2/oauth-clients/{clientId}": { + "get": { + "operationId": "OAuthClientsController_getOAuthClientById", + "summary": "", + "description": "⚠️ First, this endpoint requires `Cookie: next-auth.session-token=eyJhbGciOiJ` header. Log into Cal web app using owner of organization that was created after visiting `/settings/organizations/new`, refresh swagger docs, and the cookie will be added to requests automatically to pass the NextAuthGuard.\nSecond, make sure that the logged in user has organizationId set to pass the OrganizationRolesGuard guard.", + "parameters": [ + { + "name": "clientId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetOAuthClientResponseDto" + } + } + } + } + }, + "tags": [ + "OAuth - development only" + ] + }, + "patch": { + "operationId": "OAuthClientsController_updateOAuthClient", + "summary": "", + "description": "⚠️ First, this endpoint requires `Cookie: next-auth.session-token=eyJhbGciOiJ` header. Log into Cal web app using owner of organization that was created after visiting `/settings/organizations/new`, refresh swagger docs, and the cookie will be added to requests automatically to pass the NextAuthGuard.\nSecond, make sure that the logged in user has organizationId set to pass the OrganizationRolesGuard guard.", + "parameters": [ + { + "name": "clientId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateOAuthClientInput" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetOAuthClientResponseDto" + } + } + } + } + }, + "tags": [ + "OAuth - development only" + ] + }, + "delete": { + "operationId": "OAuthClientsController_deleteOAuthClient", + "summary": "", + "description": "⚠️ First, this endpoint requires `Cookie: next-auth.session-token=eyJhbGciOiJ` header. Log into Cal web app using owner of organization that was created after visiting `/settings/organizations/new`, refresh swagger docs, and the cookie will be added to requests automatically to pass the NextAuthGuard.\nSecond, make sure that the logged in user has organizationId set to pass the OrganizationRolesGuard guard.", + "parameters": [ + { + "name": "clientId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetOAuthClientResponseDto" + } + } + } + } + }, + "tags": [ + "OAuth - development only" + ] + } + }, + "/v2/oauth-clients/{clientId}/managed-users": { + "get": { + "operationId": "OAuthClientsController_getOAuthClientManagedUsersById", + "summary": "", + "description": "⚠️ First, this endpoint requires `Cookie: next-auth.session-token=eyJhbGciOiJ` header. Log into Cal web app using owner of organization that was created after visiting `/settings/organizations/new`, refresh swagger docs, and the cookie will be added to requests automatically to pass the NextAuthGuard.\nSecond, make sure that the logged in user has organizationId set to pass the OrganizationRolesGuard guard.", + "parameters": [ + { + "name": "clientId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + }, + { + "name": "limit", + "required": false, + "in": "query", + "description": "The number of items to return", + "example": 10, + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetManagedUsersOutput" + } + } + } + } + }, + "tags": [ + "OAuth - development only" + ] + } + }, + "/v2/oauth/{clientId}/authorize": { + "post": { + "operationId": "OAuthFlowController_authorize", + "summary": "Authorize an OAuth client", + "description": "Redirects the user to the specified 'redirect_uri' with an authorization code in query parameter if the client is authorized successfully. The code is then exchanged for access and refresh tokens via the `/exchange` endpoint.", + "parameters": [ + { + "name": "clientId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OAuthAuthorizeInput" + } + } + } + }, + "responses": { + "200": { + "description": "The user is redirected to the 'redirect_uri' with an authorization code in query parameter e.g. `redirectUri?code=secretcode.`" + }, + "400": { + "description": "Bad request if the OAuth client is not found, if the redirect URI is invalid, or if the user has already authorized the client." + } + }, + "tags": [ + "OAuth - development only" + ] + } + }, + "/v2/oauth/{clientId}/exchange": { + "post": { + "operationId": "OAuthFlowController_exchange", + "summary": "Exchange authorization code for access tokens", + "description": "Exchanges the authorization code received from the `/authorize` endpoint for access and refresh tokens. The authorization code should be provided in the 'Authorization' header prefixed with 'Bearer '.", + "parameters": [ + { + "name": "Authorization", + "required": true, + "in": "header", + "schema": { + "type": "string" + } + }, + { + "name": "clientId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ExchangeAuthorizationCodeInput" + } + } + } + }, + "responses": { + "200": { + "description": "Successfully exchanged authorization code for access and refresh tokens.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/KeysResponseDto" + } + } + } + }, + "400": { + "description": "Bad request if the authorization code is missing, invalid, or if the client ID and secret do not match." + } + }, + "tags": [ + "OAuth - development only" + ] + } + }, + "/v2/oauth/{clientId}/refresh": { + "post": { + "operationId": "OAuthFlowController_refreshAccessToken", + "parameters": [ + { + "name": "clientId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + }, + { + "name": "x-cal-secret-key", + "required": true, + "in": "header", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RefreshTokenInput" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/KeysResponseDto" + } + } + } + } + }, + "tags": [ + "OAuth - development only" + ] + } + }, + "/v2/event-types": { + "post": { + "operationId": "EventTypesController_2024_04_15_createEventType", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateEventTypeInput_2024_04_15" + } + } + } + }, + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateEventTypeOutput" + } + } + } + } + }, + "tags": [ + "Event types" + ] + }, + "get": { + "operationId": "EventTypesController_2024_04_15_getEventTypes", + "parameters": [], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetEventTypesOutput" + } + } + } + } + }, + "tags": [ + "Event types" + ] + } + }, + "/v2/event-types/{eventTypeId}": { + "get": { + "operationId": "EventTypesController_2024_04_15_getEventType", + "parameters": [ + { + "name": "eventTypeId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetEventTypeOutput" + } + } + } + } + }, + "tags": [ + "Event types" + ] + }, + "patch": { + "operationId": "EventTypesController_2024_04_15_updateEventType", + "parameters": [ + { + "name": "eventTypeId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateEventTypeInput_2024_04_15" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateEventTypeOutput" + } + } + } + } + }, + "tags": [ + "Event types" + ] + }, + "delete": { + "operationId": "EventTypesController_2024_04_15_deleteEventType", + "parameters": [ + { + "name": "eventTypeId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteEventTypeOutput" + } + } + } + } + }, + "tags": [ + "Event types" + ] + } + }, + "/v2/selected-calendars": { + "post": { + "operationId": "SelectedCalendarsController_addSelectedCalendar", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SelectedCalendarsInputDto" + } + } + } + }, + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SelectedCalendarOutputResponseDto" + } + } + } + } + }, + "tags": [ + "Selected-Calendars" + ] + }, + "delete": { + "operationId": "SelectedCalendarsController_removeSelectedCalendar", + "parameters": [ + { + "name": "integration", + "required": true, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "externalId", + "required": true, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "credentialId", + "required": true, + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SelectedCalendarOutputResponseDto" + } + } + } + } + }, + "tags": [ + "Selected-Calendars" + ] + } + }, + "/v2/event-types/{username}/{eventSlug}/public": { + "get": { + "operationId": "EventTypesController_2024_04_15_getPublicEventType", + "parameters": [ + { + "name": "username", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + }, + { + "name": "eventSlug", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + }, + { + "name": "isTeamEvent", + "required": false, + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "org", + "required": false, + "in": "query", + "schema": { + "nullable": true, + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetEventTypePublicOutput" + } + } + } + } + }, + "tags": [ + "Event types" + ] + } + }, + "/v2/event-types/{username}/public": { + "get": { + "operationId": "EventTypesController_2024_04_15_getPublicEventTypes", + "parameters": [ + { + "name": "username", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetEventTypesPublicOutput" + } + } + } + } + }, + "tags": [ + "Event types" + ] + } + }, + "/v2/organizations/{orgId}/teams": { + "get": { + "operationId": "OrganizationsTeamsController_getAllTeams", + "summary": "Get all the teams of an organization.", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "take", + "required": false, + "in": "query", + "description": "The number of items to return", + "example": 10, + "schema": { + "type": "number" + } + }, + { + "name": "skip", + "required": false, + "in": "query", + "description": "The number of items to skip", + "example": 0, + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrgTeamsOutputResponseDto" + } + } + } + } + }, + "tags": [ + "Organizations Teams" + ] + }, + "post": { + "operationId": "OrganizationsTeamsController_createTeam", + "summary": "Create a team for an organization.", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "x-cal-client-id", + "required": true, + "in": "header", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateOrgTeamDto" + } + } + } + }, + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrgTeamOutputResponseDto" + } + } + } + } + }, + "tags": [ + "Organizations Teams" + ] + } + }, + "/v2/organizations/{orgId}/teams/me": { + "get": { + "operationId": "OrganizationsTeamsController_getMyTeams", + "summary": "Get the organization's teams user is a member of", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "take", + "required": false, + "in": "query", + "description": "The number of items to return", + "example": 10, + "schema": { + "type": "number" + } + }, + { + "name": "skip", + "required": false, + "in": "query", + "description": "The number of items to skip", + "example": 0, + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrgMeTeamsOutputResponseDto" + } + } + } + } + }, + "tags": [ + "Organizations Teams" + ] + } + }, + "/v2/organizations/{orgId}/teams/{teamId}": { + "get": { + "operationId": "OrganizationsTeamsController_getTeam", + "summary": "Get a team of the organization by ID.", + "parameters": [], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrgTeamOutputResponseDto" + } + } + } + } + }, + "tags": [ + "Organizations Teams" + ] + }, + "delete": { + "operationId": "OrganizationsTeamsController_deleteTeam", + "summary": "Delete a team of the organization by ID.", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "teamId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrgTeamOutputResponseDto" + } + } + } + } + }, + "tags": [ + "Organizations Teams" + ] + }, + "patch": { + "operationId": "OrganizationsTeamsController_updateTeam", + "summary": "Update a team of the organization by ID.", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "teamId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateOrgTeamDto" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrgTeamOutputResponseDto" + } + } + } + } + }, + "tags": [ + "Organizations Teams" + ] + } + }, + "/v2/organizations/{orgId}/schedules": { + "get": { + "operationId": "OrganizationsSchedulesController_getOrganizationSchedules", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "take", + "required": false, + "in": "query", + "description": "The number of items to return", + "example": 10, + "schema": { + "type": "number" + } + }, + { + "name": "skip", + "required": false, + "in": "query", + "description": "The number of items to skip", + "example": 0, + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetSchedulesOutput_2024_06_11" + } + } + } + } + }, + "tags": [ + "Organizations Schedules" + ] + } + }, + "/v2/organizations/{orgId}/users/{userId}/schedules": { + "post": { + "operationId": "OrganizationsSchedulesController_createUserSchedule", + "parameters": [ + { + "name": "userId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateScheduleInput_2024_06_11" + } + } + } + }, + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateScheduleOutput_2024_06_11" + } + } + } + } + }, + "tags": [ + "Organizations Schedules" + ] + }, + "get": { + "operationId": "OrganizationsSchedulesController_getUserSchedules", + "parameters": [ + { + "name": "userId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetSchedulesOutput_2024_06_11" + } + } + } + } + }, + "tags": [ + "Organizations Schedules" + ] + } + }, + "/v2/organizations/{orgId}/users/{userId}/schedules/{scheduleId}": { + "get": { + "operationId": "OrganizationsSchedulesController_getUserSchedule", + "parameters": [ + { + "name": "userId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "scheduleId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetScheduleOutput_2024_06_11" + } + } + } + } + }, + "tags": [ + "Organizations Schedules" + ] + }, + "patch": { + "operationId": "OrganizationsSchedulesController_updateUserSchedule", + "parameters": [ + { + "name": "userId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "scheduleId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateScheduleInput_2024_06_11" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateScheduleOutput_2024_06_11" + } + } + } + } + }, + "tags": [ + "Organizations Schedules" + ] + }, + "delete": { + "operationId": "OrganizationsSchedulesController_deleteUserSchedule", + "parameters": [ + { + "name": "userId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "scheduleId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteScheduleOutput_2024_06_11" + } + } + } + } + }, + "tags": [ + "Organizations Schedules" + ] + } + }, + "/v2/organizations/{orgId}/users": { + "get": { + "operationId": "OrganizationsUsersController_getOrganizationsUsers", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "take", + "required": false, + "in": "query", + "description": "The number of items to return", + "example": 10, + "schema": { + "type": "number" + } + }, + { + "name": "skip", + "required": false, + "in": "query", + "description": "The number of items to skip", + "example": 0, + "schema": { + "type": "number" + } + }, + { + "name": "emails", + "required": false, + "in": "query", + "description": "The email address or an array of email addresses to filter by", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetOrganizationUsersOutput" + } + } + } + } + }, + "tags": [ + "Organizations Users" + ] + }, + "post": { + "operationId": "OrganizationsUsersController_createOrganizationUser", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateOrganizationUserInput" + } + } + } + }, + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetOrganizationUserOutput" + } + } + } + } + }, + "tags": [ + "Organizations Users" + ] + } + }, + "/v2/organizations/{orgId}/users/{userId}": { + "patch": { + "operationId": "OrganizationsUsersController_updateOrganizationUser", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "userId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateOrganizationUserInput" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetOrganizationUserOutput" + } + } + } + } + }, + "tags": [ + "Organizations Users" + ] + }, + "delete": { + "operationId": "OrganizationsUsersController_deleteOrganizationUser", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "userId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetOrganizationUserOutput" + } + } + } + } + }, + "tags": [ + "Organizations Users" + ] + } + }, + "/v2/organizations/{orgId}/memberships": { + "get": { + "operationId": "OrganizationsMembershipsController_getAllMemberships", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "take", + "required": false, + "in": "query", + "description": "The number of items to return", + "example": 10, + "schema": { + "type": "number" + } + }, + { + "name": "skip", + "required": false, + "in": "query", + "description": "The number of items to skip", + "example": 0, + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetAllOrgMemberships" + } + } + } + } + }, + "tags": [ + "Organizations Memberships" + ] + }, + "post": { + "operationId": "OrganizationsMembershipsController_createMembership", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateOrgMembershipDto" + } + } + } + }, + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateOrgMembershipOutput" + } + } + } + } + }, + "tags": [ + "Organizations Memberships" + ] + } + }, + "/v2/organizations/{orgId}/memberships/{membershipId}": { + "get": { + "operationId": "OrganizationsMembershipsController_getOrgMembership", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "membershipId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetOrgMembership" + } + } + } + } + }, + "tags": [ + "Organizations Memberships" + ] + }, + "delete": { + "operationId": "OrganizationsMembershipsController_deleteMembership", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "membershipId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteOrgMembership" + } + } + } + } + }, + "tags": [ + "Organizations Memberships" + ] + }, + "patch": { + "operationId": "OrganizationsMembershipsController_updateMembership", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "membershipId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateOrgMembershipDto" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateOrgMembership" + } + } + } + } + }, + "tags": [ + "Organizations Memberships" + ] + } + }, + "/v2/organizations/{orgId}/teams/{teamId}/event-types": { + "post": { + "operationId": "OrganizationsEventTypesController_createTeamEventType", + "parameters": [ + { + "name": "teamId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateTeamEventTypeInput_2024_06_14" + } + } + } + }, + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateTeamEventTypeOutput" + } + } + } + } + }, + "tags": [ + "Organizations Event Types" + ] + }, + "get": { + "operationId": "OrganizationsEventTypesController_getTeamEventTypes", + "parameters": [ + { + "name": "teamId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetTeamEventTypesOutput" + } + } + } + } + }, + "tags": [ + "Organizations Event Types" + ] + } + }, + "/v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId}": { + "get": { + "operationId": "OrganizationsEventTypesController_getTeamEventType", + "parameters": [ + { + "name": "teamId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "eventTypeId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetTeamEventTypeOutput" + } + } + } + } + }, + "tags": [ + "Organizations Event Types" + ] + }, + "patch": { + "operationId": "OrganizationsEventTypesController_updateTeamEventType", + "parameters": [ + { + "name": "teamId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "eventTypeId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateTeamEventTypeInput_2024_06_14" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateTeamEventTypeOutput" + } + } + } + } + }, + "tags": [ + "Organizations Event Types" + ] + }, + "delete": { + "operationId": "OrganizationsEventTypesController_deleteTeamEventType", + "parameters": [ + { + "name": "teamId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "eventTypeId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteTeamEventTypeOutput" + } + } + } + } + }, + "tags": [ + "Organizations Event Types" + ] + } + }, + "/v2/organizations/{orgId}/teams/event-types": { + "get": { + "operationId": "OrganizationsEventTypesController_getTeamsEventTypes", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "take", + "required": false, + "in": "query", + "description": "The number of items to return", + "example": 10, + "schema": { + "type": "number" + } + }, + { + "name": "skip", + "required": false, + "in": "query", + "description": "The number of items to skip", + "example": 0, + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetTeamEventTypesOutput" + } + } + } + } + }, + "tags": [ + "Organizations Event Types" + ] + } + }, + "/v2/organizations/{orgId}/teams/{teamId}/memberships": { + "get": { + "operationId": "OrganizationsTeamsMembershipsController_getAllOrgTeamMemberships", + "summary": "Get all the memberships of a team of an organization.", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "teamId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "take", + "required": false, + "in": "query", + "description": "The number of items to return", + "example": 10, + "schema": { + "type": "number" + } + }, + { + "name": "skip", + "required": false, + "in": "query", + "description": "The number of items to skip", + "example": 0, + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrgTeamMembershipsOutputResponseDto" + } + } + } + } + }, + "tags": [ + "Organizations Teams" + ] + }, + "post": { + "operationId": "OrganizationsTeamsMembershipsController_createOrgTeamMembership", + "summary": "Create a membership of an organization's team", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "teamId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateOrgTeamMembershipDto" + } + } + } + }, + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrgTeamMembershipOutputResponseDto" + } + } + } + } + }, + "tags": [ + "Organizations Teams" + ] + } + }, + "/v2/organizations/{orgId}/teams/{teamId}/memberships/{membershipId}": { + "get": { + "operationId": "OrganizationsTeamsMembershipsController_getOrgTeamMembership", + "summary": "Get the membership of an organization's team by ID", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "teamId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "membershipId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrgTeamMembershipOutputResponseDto" + } + } + } + } + }, + "tags": [ + "Organizations Teams" + ] + }, + "delete": { + "operationId": "OrganizationsTeamsMembershipsController_deleteOrgTeamMembership", + "summary": "Delete the membership of an organization's team by ID", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "teamId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "membershipId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrgTeamMembershipOutputResponseDto" + } + } + } + } + }, + "tags": [ + "Organizations Teams" + ] + }, + "patch": { + "operationId": "OrganizationsTeamsMembershipsController_updateOrgTeamMembership", + "summary": "Update the membership of an organization's team by ID", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "teamId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "membershipId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateOrgTeamMembershipDto" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrgTeamMembershipOutputResponseDto" + } + } + } + } + }, + "tags": [ + "Organizations Teams" + ] + } + }, + "/v2/organizations/{orgId}/attributes": { + "get": { + "operationId": "OrganizationsAttributesController_getOrganizationAttributes", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "take", + "required": false, + "in": "query", + "description": "The number of items to return", + "example": 10, + "schema": { + "type": "number" + } + }, + { + "name": "skip", + "required": false, + "in": "query", + "description": "The number of items to skip", + "example": 0, + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetOrganizationAttributesOutput" + } + } + } + } + } + }, + "post": { + "operationId": "OrganizationsAttributesController_createOrganizationAttribute", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateOrganizationAttributeInput" + } + } + } + }, + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateOrganizationAttributesOutput" + } + } + } + } + } + } + }, + "/v2/organizations/{orgId}/attributes/{attributeId}": { + "get": { + "operationId": "OrganizationsAttributesController_getOrganizationAttribute", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "attributeId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetSingleAttributeOutput" + } + } + } + } + } + }, + "patch": { + "operationId": "OrganizationsAttributesController_updateOrganizationAttribute", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "attributeId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateOrganizationAttributeInput" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateOrganizationAttributesOutput" + } + } + } + } + } + }, + "delete": { + "operationId": "OrganizationsAttributesController_deleteOrganizationAttribute", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "attributeId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteOrganizationAttributesOutput" + } + } + } + } + } + } + }, + "/v2/organizations/{orgId}/attributes/{attributeId}/options": { + "post": { + "operationId": "OrganizationsOptionsAttributesController_createOrganizationAttributeOption", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "attributeId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateOrganizationAttributeOptionInput" + } + } + } + }, + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateAttributeOptionOutput" + } + } + } + } + } + }, + "get": { + "operationId": "OrganizationsOptionsAttributesController_getOrganizationAttributeOptions", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "attributeId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetAllAttributeOptionOutput" + } + } + } + } + } + } + }, + "/v2/organizations/{orgId}/attributes/{attributeId}/options/{optionId}": { + "delete": { + "operationId": "OrganizationsOptionsAttributesController_deleteOrganizationAttributeOption", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "attributeId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + }, + { + "name": "optionId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteAttributeOptionOutput" + } + } + } + } + } + }, + "patch": { + "operationId": "OrganizationsOptionsAttributesController_updateOrganizationAttributeOption", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "attributeId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + }, + { + "name": "optionId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateOrganizationAttributeOptionInput" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateAttributeOptionOutput" + } + } + } + } + } + } + }, + "/v2/organizations/{orgId}/attributes/options/{userId}": { + "post": { + "operationId": "OrganizationsOptionsAttributesController_assignOrganizationAttributeOptionToUser", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "userId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AssignOrganizationAttributeOptionToUserInput" + } + } + } + }, + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AssignOptionUserOutput" + } + } + } + } + } + }, + "get": { + "operationId": "OrganizationsOptionsAttributesController_getOrganizationAttributeOptionsForUser", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "userId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetOptionUserOutput" + } + } + } + } + } + } + }, + "/v2/organizations/{orgId}/attributes/options/{userId}/{attributeOptionId}": { + "delete": { + "operationId": "OrganizationsOptionsAttributesController_unassignOrganizationAttributeOptionFromUser", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "userId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "attributeOptionId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnassignOptionUserOutput" + } + } + } + } + } + } + }, + "/v2/organizations/{orgId}/webhooks": { + "get": { + "operationId": "OrganizationsWebhooksController_getAllOrganizationWebhooks", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "take", + "required": false, + "in": "query", + "description": "The number of items to return", + "example": 10, + "schema": { + "type": "number" + } + }, + { + "name": "skip", + "required": false, + "in": "query", + "description": "The number of items to skip", + "example": 0, + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TeamWebhooksOutputResponseDto" + } + } + } + } + }, + "tags": [ + "Organizations Webhooks" + ] + }, + "post": { + "operationId": "OrganizationsWebhooksController_createOrganizationWebhook", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateWebhookInputDto" + } + } + } + }, + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TeamWebhookOutputResponseDto" + } + } + } + } + }, + "tags": [ + "Organizations Webhooks" + ] + } + }, + "/v2/organizations/{orgId}/webhooks/{webhookId}": { + "get": { + "operationId": "OrganizationsWebhooksController_getOrganizationWebhook", + "parameters": [ + { + "name": "webhookId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TeamWebhookOutputResponseDto" + } + } + } + } + }, + "tags": [ + "Organizations Webhooks" + ] + }, + "delete": { + "operationId": "OrganizationsWebhooksController_deleteWebhook", + "parameters": [ + { + "name": "webhookId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TeamWebhookOutputResponseDto" + } + } + } + } + }, + "tags": [ + "Organizations Webhooks" + ] + }, + "patch": { + "operationId": "OrganizationsWebhooksController_updateOrgWebhook", + "parameters": [ + { + "name": "webhookId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateWebhookInputDto" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TeamWebhookOutputResponseDto" + } + } + } + } + }, + "tags": [ + "Organizations Webhooks" + ] + } + }, + "/v2/schedules": { + "post": { + "operationId": "SchedulesController_2024_04_15_createSchedule", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateScheduleInput_2024_04_15" + } + } + } + }, + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateScheduleOutput_2024_04_15" + } + } + } + } + }, + "tags": [ + "Schedules" + ] + }, + "get": { + "operationId": "SchedulesController_2024_04_15_getSchedules", + "parameters": [], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetSchedulesOutput_2024_04_15" + } + } + } + } + }, + "tags": [ + "Schedules" + ] + } + }, + "/v2/schedules/default": { + "get": { + "operationId": "SchedulesController_2024_04_15_getDefaultSchedule", + "parameters": [], + "responses": { + "200": { + "description": "Returns the default schedule", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetDefaultScheduleOutput_2024_04_15" + } + } + } + } + }, + "tags": [ + "Schedules" + ] + } + }, + "/v2/schedules/{scheduleId}": { + "get": { + "operationId": "SchedulesController_2024_04_15_getSchedule", + "parameters": [ + { + "name": "scheduleId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetScheduleOutput_2024_04_15" + } + } + } + } + }, + "tags": [ + "Schedules" + ] + }, + "patch": { + "operationId": "SchedulesController_2024_04_15_updateSchedule", + "parameters": [ + { + "name": "scheduleId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateScheduleInput_2024_04_15" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateScheduleOutput_2024_04_15" + } + } + } + } + }, + "tags": [ + "Schedules" + ] + }, + "delete": { + "operationId": "SchedulesController_2024_04_15_deleteSchedule", + "parameters": [ + { + "name": "scheduleId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteScheduleOutput_2024_04_15" + } + } + } + } + }, + "tags": [ + "Schedules" + ] + } + }, + "/v2/gcal/oauth/auth-url": { + "get": { + "operationId": "GcalController_redirect", + "parameters": [ + { + "name": "Authorization", + "required": true, + "in": "header", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GcalAuthUrlOutput" + } + } + } + } + }, + "tags": [ + "Google Calendar" + ] + } + }, + "/v2/gcal/oauth/save": { + "get": { + "operationId": "GcalController_save", + "parameters": [ + { + "name": "state", + "required": true, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "code", + "required": true, + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GcalSaveRedirectOutput" + } + } + } + } + }, + "tags": [ + "Google Calendar" + ] + } + }, + "/v2/gcal/check": { + "get": { + "operationId": "GcalController_check", + "parameters": [], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GcalCheckOutput" + } + } + } + } + }, + "tags": [ + "Google Calendar" + ] + } + }, + "/v2/provider/{clientId}": { + "get": { + "operationId": "CalProviderController_verifyClientId", + "parameters": [ + { + "name": "clientId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProviderVerifyClientOutput" + } + } + } + } + }, + "tags": [ + "Cal provider" + ] + } + }, + "/v2/provider/{clientId}/access-token": { + "get": { + "operationId": "CalProviderController_verifyAccessToken", + "parameters": [ + { + "name": "clientId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProviderVerifyAccessTokenOutput" + } + } + } + } + }, + "tags": [ + "Cal provider" + ] + } + }, + "/v2/me": { + "get": { + "operationId": "MeController_getMe", + "parameters": [], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetMeOutput" + } + } + } + } + }, + "tags": [ + "Me" + ] + }, + "patch": { + "operationId": "MeController_updateMe", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateManagedUserInput" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateMeOutput" + } + } + } + } + }, + "tags": [ + "Me" + ] + } + }, + "/v2/calendars/busy-times": { + "get": { + "operationId": "CalendarsController_getBusyTimes", + "parameters": [], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetBusyTimesOutput" + } + } + } + } + }, + "tags": [ + "Calendars" + ] + } + }, + "/v2/calendars": { + "get": { + "operationId": "CalendarsController_getCalendars", + "parameters": [], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConnectedCalendarsOutput" + } + } + } + } + }, + "tags": [ + "Calendars" + ] + } + }, + "/v2/calendars/{calendar}/connect": { + "get": { + "operationId": "CalendarsController_redirect", + "parameters": [ + { + "name": "Authorization", + "required": true, + "in": "header", + "schema": { + "type": "string" + } + }, + { + "name": "calendar", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + } + }, + "tags": [ + "Calendars" + ] + } + }, + "/v2/calendars/{calendar}/save": { + "get": { + "operationId": "CalendarsController_save", + "parameters": [ + { + "name": "state", + "required": true, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "code", + "required": true, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "calendar", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "" + } + }, + "tags": [ + "Calendars" + ] + } + }, + "/v2/calendars/{calendar}/credentials": { + "post": { + "operationId": "CalendarsController_syncCredentials", + "parameters": [ + { + "name": "calendar", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "201": { + "description": "" + } + }, + "tags": [ + "Calendars" + ] + } + }, + "/v2/calendars/{calendar}/check": { + "get": { + "operationId": "CalendarsController_check", + "parameters": [ + { + "name": "calendar", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + } + }, + "tags": [ + "Calendars" + ] + } + }, + "/v2/calendars/{calendar}/disconnect": { + "post": { + "operationId": "CalendarsController_deleteCalendarCredentials", + "parameters": [ + { + "name": "calendar", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteCalendarCredentialsInputBodyDto" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeletedCalendarCredentialsOutputResponseDto" + } + } + } + } + }, + "tags": [ + "Calendars" + ] + } + }, + "/v2/bookings": { + "get": { + "operationId": "BookingsController_2024_08_13_getBookings", + "parameters": [ + { + "name": "take", + "required": false, + "in": "query", + "description": "The number of items to return", + "example": 10, + "schema": { + "type": "number" + } + }, + { + "name": "skip", + "required": false, + "in": "query", + "description": "The number of items to skip", + "example": 0, + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetBookingsOutput_2024_08_13" + } + } + } + } + }, + "tags": [ + "Bookings" + ] + }, + "post": { + "operationId": "BookingsController_2024_08_13_createBooking", + "parameters": [], + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateBookingOutput_2024_08_13" + } + } + } + } + }, + "tags": [ + "Bookings" + ] + } + }, + "/v2/bookings/{bookingUid}": { + "get": { + "operationId": "BookingsController_2024_08_13_getBooking", + "parameters": [ + { + "name": "bookingUid", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetBookingOutput_2024_08_13" + } + } + } + } + }, + "tags": [ + "Bookings" + ] + } + }, + "/v2/bookings/{bookingUid}/reschedule": { + "get": { + "operationId": "BookingsController_2024_04_15_getBookingForReschedule", + "parameters": [ + { + "name": "bookingUid", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + } + }, + "tags": [ + "Bookings" + ] + }, + "post": { + "operationId": "BookingsController_2024_08_13_rescheduleBooking", + "parameters": [ + { + "name": "bookingUid", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RescheduleBookingInput_2024_08_13" + } + } + } + }, + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RescheduleBookingOutput_2024_08_13" + } + } + } + } + }, + "tags": [ + "Bookings" + ] + } + }, + "/v2/bookings/{bookingId}/cancel": { + "post": { + "operationId": "BookingsController_2024_04_15_cancelBooking", + "parameters": [ + { + "name": "bookingId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + }, + { + "name": "x-cal-client-id", + "required": true, + "in": "header", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CancelBookingInput_2024_04_15" + } + } + } + }, + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + } + }, + "tags": [ + "Bookings" + ] + } + }, + "/v2/bookings/{bookingUid}/mark-no-show": { + "post": { + "operationId": "BookingsController_2024_04_15_markNoShow", + "parameters": [ + { + "name": "bookingUid", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MarkNoShowInput_2024_04_15" + } + } + } + }, + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MarkNoShowOutput_2024_04_15" + } + } + } + } + }, + "tags": [ + "Bookings" + ] + } + }, + "/v2/bookings/recurring": { + "post": { + "operationId": "BookingsController_2024_04_15_createRecurringBooking", + "parameters": [ + { + "name": "x-cal-client-id", + "required": true, + "in": "header", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + }, + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + } + }, + "tags": [ + "Bookings" + ] + } + }, + "/v2/bookings/instant": { + "post": { + "operationId": "BookingsController_2024_04_15_createInstantBooking", + "parameters": [ + { + "name": "x-cal-client-id", + "required": true, + "in": "header", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateBookingInput_2024_04_15" + } + } + } + }, + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + } + }, + "tags": [ + "Bookings" + ] + } + }, + "/v2/bookings/{bookingUid}/cancel": { + "post": { + "operationId": "BookingsController_2024_08_13_cancelBooking", + "parameters": [ + { + "name": "bookingUid", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CancelBookingInput_2024_08_13" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CancelBookingOutput_2024_08_13" + } + } + } + } + }, + "tags": [ + "Bookings" + ] + } + }, + "/v2/bookings/{bookingUid}/mark-absent": { + "post": { + "operationId": "BookingsController_2024_08_13_markNoShow", + "parameters": [ + { + "name": "bookingUid", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MarkAbsentBookingInput_2024_08_13" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MarkAbsentBookingOutput_2024_08_13" + } + } + } + } + }, + "tags": [ + "Bookings" + ] + } + }, + "/v2/slots/reserve": { + "post": { + "operationId": "SlotsController_reserveSlot", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ReserveSlotInput" + } + } + } + }, + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + } + }, + "tags": [ + "Slots" + ] + } + }, + "/v2/slots/selected-slot": { + "delete": { + "operationId": "SlotsController_deleteSelectedSlot", + "parameters": [], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + } + }, + "tags": [ + "Slots" + ] + } + }, + "/v2/slots/available": { + "get": { + "operationId": "SlotsController_getAvailableSlots", + "parameters": [], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + } + }, + "tags": [ + "Slots" + ] + } + }, + "/v2/timezones": { + "get": { + "operationId": "TimezonesController_getTimeZones", + "parameters": [], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + } + }, + "tags": [ + "Timezones" + ] + } + }, + "/v2/webhooks": { + "post": { + "operationId": "WebhooksController_createWebhook", + "summary": "Create a webhook", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateWebhookInputDto" + } + } + } + }, + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserWebhookOutputResponseDto" + } + } + } + } + }, + "tags": [ + "Users' Webhooks" + ] + }, + "get": { + "operationId": "WebhooksController_getWebhooks", + "summary": "Get all user webhooks paginated", + "parameters": [ + { + "name": "take", + "required": false, + "in": "query", + "description": "The number of items to return", + "example": 10, + "schema": { + "type": "number" + } + }, + { + "name": "skip", + "required": false, + "in": "query", + "description": "The number of items to skip", + "example": 0, + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserWebhooksOutputResponseDto" + } + } + } + } + }, + "tags": [ + "Users' Webhooks" + ] + } + }, + "/v2/webhooks/{webhookId}": { + "patch": { + "operationId": "WebhooksController_updateWebhook", + "summary": "Update a webhook", + "parameters": [ + { + "name": "webhookId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateWebhookInputDto" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserWebhookOutputResponseDto" + } + } + } + } + }, + "tags": [ + "Users' Webhooks" + ] + }, + "get": { + "operationId": "WebhooksController_getWebhook", + "summary": "Get a webhook", + "parameters": [], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserWebhookOutputResponseDto" + } + } + } + } + }, + "tags": [ + "Users' Webhooks" + ] + }, + "delete": { + "operationId": "WebhooksController_deleteWebhook", + "summary": "Delete a webhook", + "parameters": [ + { + "name": "webhookId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserWebhookOutputResponseDto" + } + } + } + } + }, + "tags": [ + "Users' Webhooks" + ] + } + }, + "/v2/event-types/{eventTypeId}/webhooks": { + "post": { + "operationId": "EventTypeWebhooksController_createEventTypeWebhook", + "summary": "Create a webhook for an event-type", + "parameters": [ + { + "name": "eventTypeId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateWebhookInputDto" + } + } + } + }, + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EventTypeWebhookOutputResponseDto" + } + } + } + } + }, + "tags": [ + "Users' EventTypes Webhooks" + ] + }, + "get": { + "operationId": "EventTypeWebhooksController_getEventTypeWebhooks", + "summary": "Get all webhooks of an event-type", + "parameters": [ + { + "name": "eventTypeId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "take", + "required": false, + "in": "query", + "description": "The number of items to return", + "example": 10, + "schema": { + "type": "number" + } + }, + { + "name": "skip", + "required": false, + "in": "query", + "description": "The number of items to skip", + "example": 0, + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EventTypeWebhooksOutputResponseDto" + } + } + } + } + }, + "tags": [ + "Users' EventTypes Webhooks" + ] + }, + "delete": { + "operationId": "EventTypeWebhooksController_deleteAllEventTypeWebhooks", + "summary": "Delete all webhooks of an event-type", + "parameters": [ + { + "name": "eventTypeId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteManyWebhooksOutputResponseDto" + } + } + } + } + }, + "tags": [ + "Users' EventTypes Webhooks" + ] + } + }, + "/v2/event-types/{eventTypeId}/webhooks/{webhookId}": { + "patch": { + "operationId": "EventTypeWebhooksController_updateEventTypeWebhook", + "summary": "Update a webhook of an event-type", + "parameters": [ + { + "name": "webhookId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateWebhookInputDto" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EventTypeWebhookOutputResponseDto" + } + } + } + } + }, + "tags": [ + "Users' EventTypes Webhooks" + ] + }, + "get": { + "operationId": "EventTypeWebhooksController_getEventTypeWebhook", + "summary": "Get a webhook of an event-type", + "parameters": [], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EventTypeWebhookOutputResponseDto" + } + } + } + } + }, + "tags": [ + "Users' EventTypes Webhooks" + ] + }, + "delete": { + "operationId": "EventTypeWebhooksController_deleteEventTypeWebhook", + "summary": "Delete a webhook of an event-type", + "parameters": [], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EventTypeWebhookOutputResponseDto" + } + } + } + } + }, + "tags": [ + "Users' EventTypes Webhooks" + ] + } + }, + "/v2/oauth-clients/{clientId}/webhooks": { + "post": { + "operationId": "OAuthClientWebhooksController_createOAuthClientWebhook", + "summary": "Create a webhook for an oAuthClient", + "parameters": [ + { + "name": "clientId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateWebhookInputDto" + } + } + } + }, + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OAuthClientWebhookOutputResponseDto" + } + } + } + } + }, + "tags": [ + "OAuthClients Webhooks" + ] + }, + "get": { + "operationId": "OAuthClientWebhooksController_getOAuthClientWebhooks", + "summary": "Get all webhooks of an oAuthClient", + "parameters": [ + { + "name": "clientId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + }, + { + "name": "take", + "required": false, + "in": "query", + "description": "The number of items to return", + "example": 10, + "schema": { + "type": "number" + } + }, + { + "name": "skip", + "required": false, + "in": "query", + "description": "The number of items to skip", + "example": 0, + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OAuthClientWebhooksOutputResponseDto" + } + } + } + } + }, + "tags": [ + "OAuthClients Webhooks" + ] + }, + "delete": { + "operationId": "OAuthClientWebhooksController_deleteAllOAuthClientWebhooks", + "summary": "Delete all webhooks of an oAuthClient", + "parameters": [ + { + "name": "clientId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteManyWebhooksOutputResponseDto" + } + } + } + } + }, + "tags": [ + "OAuthClients Webhooks" + ] + } + }, + "/v2/oauth-clients/{clientId}/webhooks/{webhookId}": { + "patch": { + "operationId": "OAuthClientWebhooksController_updateOAuthClientWebhook", + "summary": "Update a webhook of an oAuthClient", + "parameters": [ + { + "name": "webhookId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateWebhookInputDto" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OAuthClientWebhookOutputResponseDto" + } + } + } + } + }, + "tags": [ + "OAuthClients Webhooks" + ] + }, + "get": { + "operationId": "OAuthClientWebhooksController_getOAuthClientWebhook", + "summary": "Get a webhook of an oAuthClient", + "parameters": [], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OAuthClientWebhookOutputResponseDto" + } + } + } + } + }, + "tags": [ + "OAuthClients Webhooks" + ] + }, + "delete": { + "operationId": "OAuthClientWebhooksController_deleteOAuthClientWebhook", + "summary": "Delete a webhook of an oAuthClient", + "parameters": [], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OAuthClientWebhookOutputResponseDto" + } + } + } + } + }, + "tags": [ + "OAuthClients Webhooks" + ] + } + }, + "/v2/destination-calendars": { + "put": { + "operationId": "DestinationCalendarsController_updateDestinationCalendars", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DestinationCalendarsInputBodyDto" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DestinationCalendarsOutputResponseDto" + } + } + } + } + }, + "tags": [ + "Destination-Calendars", + "Select a third party destination calendar where events will be created" + ] + } + } + }, + "info": { + "title": "Cal.com v2 API", + "description": "", + "version": "1.0.0", + "contact": {} + }, + "tags": [], + "servers": [], + "components": { + "schemas": { + "ManagedUserOutput": { + "type": "object", + "properties": { + "id": { + "type": "number", + "example": 1 + }, + "email": { + "type": "string", + "example": "alice+cluo37fwd0001khkzqqynkpj3@example.com" + }, + "username": { + "type": "string", + "nullable": true, + "example": "alice" + }, + "timeZone": { + "type": "string", + "example": "America/New_York" + }, + "weekStart": { + "type": "string", + "example": "Sunday" + }, + "createdDate": { + "type": "string", + "example": "2024-04-01T00:00:00.000Z" + }, + "timeFormat": { + "type": "number", + "nullable": true, + "example": 12 + }, + "defaultScheduleId": { + "type": "number", + "nullable": true, + "example": null + }, + "locale": { + "enum": [ + "ar", + "ca", + "de", + "es", + "eu", + "he", + "id", + "ja", + "lv", + "pl", + "ro", + "sr", + "th", + "vi", + "az", + "cs", + "el", + "es-419", + "fi", + "hr", + "it", + "km", + "nl", + "pt", + "ru", + "sv", + "tr", + "zh-CN", + "bg", + "da", + "en", + "et", + "fr", + "hu", + "iw", + "ko", + "no", + "pt-BR", + "sk", + "ta", + "uk", + "zh-TW" + ], + "type": "string", + "example": "en" + } + }, + "required": [ + "id", + "email", + "username", + "timeZone", + "weekStart", + "createdDate", + "timeFormat", + "defaultScheduleId" + ] + }, + "GetManagedUsersOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ManagedUserOutput" + } + } + }, + "required": [ + "status", + "data" + ] + }, + "CreateManagedUserInput": { + "type": "object", + "properties": { + "email": { + "type": "string", + "example": "alice@example.com" + }, + "timeFormat": { + "type": "number", + "example": 12, + "enum": [ + 12, + 24 + ], + "description": "Must be 12 or 24" + }, + "weekStart": { + "type": "string", + "example": "Monday", + "enum": [ + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday", + "Sunday" + ] + }, + "timeZone": { + "type": "string", + "example": "America/New_York" + }, + "locale": { + "enum": [ + "ar", + "ca", + "de", + "es", + "eu", + "he", + "id", + "ja", + "lv", + "pl", + "ro", + "sr", + "th", + "vi", + "az", + "cs", + "el", + "es-419", + "fi", + "hr", + "it", + "km", + "nl", + "pt", + "ru", + "sv", + "tr", + "zh-CN", + "bg", + "da", + "en", + "et", + "fr", + "hu", + "iw", + "ko", + "no", + "pt-BR", + "sk", + "ta", + "uk", + "zh-TW" + ], + "type": "string", + "example": "en" + }, + "name": { + "type": "string" + } + }, + "required": [ + "email" + ] + }, + "CreateManagedUserData": { + "type": "object", + "properties": { + "user": { + "$ref": "#/components/schemas/ManagedUserOutput" + }, + "accessToken": { + "type": "string" + }, + "refreshToken": { + "type": "string" + }, + "accessTokenExpiresAt": { + "type": "number" + } + }, + "required": [ + "user", + "accessToken", + "refreshToken", + "accessTokenExpiresAt" + ] + }, + "CreateManagedUserOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/CreateManagedUserData" + } + }, + "required": [ + "status", + "data" + ] + }, + "GetManagedUserOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/ManagedUserOutput" + } + }, + "required": [ + "status", + "data" + ] + }, + "UpdateManagedUserInput": { + "type": "object", + "properties": { + "timeFormat": { + "type": "number", + "enum": [ + 12, + 24 + ], + "example": 12, + "description": "Must be 12 or 24" + }, + "weekStart": { + "type": "string", + "enum": [ + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday", + "Sunday" + ], + "example": "Monday" + }, + "locale": { + "enum": [ + "ar", + "ca", + "de", + "es", + "eu", + "he", + "id", + "ja", + "lv", + "pl", + "ro", + "sr", + "th", + "vi", + "az", + "cs", + "el", + "es-419", + "fi", + "hr", + "it", + "km", + "nl", + "pt", + "ru", + "sv", + "tr", + "zh-CN", + "bg", + "da", + "en", + "et", + "fr", + "hu", + "iw", + "ko", + "no", + "pt-BR", + "sk", + "ta", + "uk", + "zh-TW" + ], + "type": "string", + "example": "en" + }, + "email": { + "type": "string" + }, + "name": { + "type": "string" + }, + "defaultScheduleId": { + "type": "number" + }, + "timeZone": { + "type": "string" + } + } + }, + "KeysDto": { + "type": "object", + "properties": { + "accessToken": { + "type": "string", + "example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" + }, + "refreshToken": { + "type": "string", + "example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" + }, + "accessTokenExpiresAt": { + "type": "number" + } + }, + "required": [ + "accessToken", + "refreshToken", + "accessTokenExpiresAt" + ] + }, + "KeysResponseDto": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/KeysDto" + } + }, + "required": [ + "status", + "data" + ] + }, + "CreateOAuthClientInput": { + "type": "object", + "properties": {} + }, + "DataDto": { + "type": "object", + "properties": { + "clientId": { + "type": "string", + "example": "clsx38nbl0001vkhlwin9fmt0" + }, + "clientSecret": { + "type": "string", + "example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoib2F1dGgtY2xpZW50Iiwi" + } + }, + "required": [ + "clientId", + "clientSecret" + ] + }, + "CreateOAuthClientResponseDto": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "success", + "error" + ], + "example": "success" + }, + "data": { + "example": { + "clientId": "clsx38nbl0001vkhlwin9fmt0", + "clientSecret": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoib2F1dGgtY2xpZW50Iiwi" + }, + "allOf": [ + { + "$ref": "#/components/schemas/DataDto" + } + ] + } + }, + "required": [ + "status", + "data" + ] + }, + "PlatformOAuthClientDto": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "clsx38nbl0001vkhlwin9fmt0" + }, + "name": { + "type": "string", + "example": "MyClient" + }, + "secret": { + "type": "string", + "example": "secretValue" + }, + "permissions": { + "type": "number", + "example": 3 + }, + "logo": { + "type": "string", + "nullable": true, + "example": "https://example.com/logo.png" + }, + "redirectUris": { + "example": [ + "https://example.com/callback" + ], + "type": "array", + "items": { + "type": "string" + } + }, + "organizationId": { + "type": "number", + "example": 1 + }, + "createdAt": { + "format": "date-time", + "type": "string", + "example": "2024-03-23T08:33:21.851Z" + } + }, + "required": [ + "id", + "name", + "secret", + "permissions", + "redirectUris", + "organizationId", + "createdAt" + ] + }, + "GetOAuthClientsResponseDto": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PlatformOAuthClientDto" + } + } + }, + "required": [ + "status", + "data" + ] + }, + "GetOAuthClientResponseDto": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/PlatformOAuthClientDto" + } + }, + "required": [ + "status", + "data" + ] + }, + "UpdateOAuthClientInput": { + "type": "object", + "properties": { + "logo": { + "type": "string" + }, + "name": { + "type": "string" + }, + "redirectUris": { + "default": [], + "type": "array", + "items": { + "type": "string" + } + }, + "bookingRedirectUri": { + "type": "string" + }, + "bookingCancelRedirectUri": { + "type": "string" + }, + "bookingRescheduleRedirectUri": { + "type": "string" + }, + "areEmailsEnabled": { + "type": "boolean" + } + } + }, + "OAuthAuthorizeInput": { + "type": "object", + "properties": { + "redirectUri": { + "type": "string" + } + }, + "required": [ + "redirectUri" + ] + }, + "ExchangeAuthorizationCodeInput": { + "type": "object", + "properties": { + "clientSecret": { + "type": "string" + } + }, + "required": [ + "clientSecret" + ] + }, + "RefreshTokenInput": { + "type": "object", + "properties": { + "refreshToken": { + "type": "string" + } + }, + "required": [ + "refreshToken" + ] + }, + "CreateEventTypeInput_2024_06_14": { + "type": "object", + "properties": { + "lengthInMinutes": { + "type": "number", + "example": 60 + }, + "title": { + "type": "string", + "example": "Learn the secrets of masterchief!" + }, + "description": { + "type": "string", + "example": "Discover the culinary wonders of the Argentina by making the best flan ever!" + } + }, + "required": [ + "lengthInMinutes", + "title", + "description" + ] + }, + "EventTypeOutput_2024_06_14": { + "type": "object", + "properties": { + "id": { + "type": "number", + "example": 1 + } + }, + "required": [ + "id" + ] + }, + "CreateEventTypeOutput_2024_06_14": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "success", + "error" + ], + "example": "success" + }, + "data": { + "$ref": "#/components/schemas/EventTypeOutput_2024_06_14" + } + }, + "required": [ + "status", + "data" + ] + }, + "GetEventTypeOutput_2024_06_14": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "success", + "error" + ], + "example": "success" + }, + "data": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/EventTypeOutput_2024_06_14" + } + ] + } + }, + "required": [ + "status", + "data" + ] + }, + "GetEventTypesOutput_2024_06_14": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "success", + "error" + ], + "example": "success" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/EventTypeOutput_2024_06_14" + } + } + }, + "required": [ + "status", + "data" + ] + }, + "UpdateEventTypeInput_2024_06_14": { + "type": "object", + "properties": {} + }, + "UpdateEventTypeOutput_2024_06_14": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "success", + "error" + ], + "example": "success" + }, + "data": { + "$ref": "#/components/schemas/EventTypeOutput_2024_06_14" + } + }, + "required": [ + "status", + "data" + ] + }, + "DeleteData_2024_06_14": { + "type": "object", + "properties": { + "id": { + "type": "number", + "example": 1 + }, + "lengthInMinutes": { + "type": "number", + "example": 60 + }, + "title": { + "type": "string", + "example": "Learn the secrets of masterchief!" + }, + "slug": { + "type": "string" + } + }, + "required": [ + "id", + "lengthInMinutes", + "title", + "slug" + ] + }, + "DeleteEventTypeOutput_2024_06_14": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "success", + "error" + ], + "example": "success" + }, + "data": { + "$ref": "#/components/schemas/DeleteData_2024_06_14" + } + }, + "required": [ + "status", + "data" + ] + }, + "SelectedCalendarsInputDto": { + "type": "object", + "properties": { + "integration": { + "type": "string" + }, + "externalId": { + "type": "string" + }, + "credentialId": { + "type": "number" + } + }, + "required": [ + "integration", + "externalId", + "credentialId" + ] + }, + "SelectedCalendarOutputDto": { + "type": "object", + "properties": { + "userId": { + "type": "number" + }, + "integration": { + "type": "string" + }, + "externalId": { + "type": "string" + }, + "credentialId": { + "type": "number", + "nullable": true + } + }, + "required": [ + "userId", + "integration", + "externalId", + "credentialId" + ] + }, + "SelectedCalendarOutputResponseDto": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/SelectedCalendarOutputDto" + } + }, + "required": [ + "status", + "data" + ] + }, + "EventTypeLocation_2024_04_15": { + "type": "object", + "properties": { + "type": { + "type": "string", + "example": "link" + }, + "link": { + "type": "string", + "example": "https://masterchief.com/argentina/flan/video/9129412" + } + }, + "required": [ + "type" + ] + }, + "CreateEventTypeInput_2024_04_15": { + "type": "object", + "properties": { + "length": { + "type": "number", + "minimum": 1, + "example": 60 + }, + "slug": { + "type": "string", + "example": "cooking-class" + }, + "title": { + "type": "string", + "example": "Learn the secrets of masterchief!" + }, + "description": { + "type": "string", + "example": "Discover the culinary wonders of the Argentina by making the best flan ever!" + }, + "locations": { + "type": "array", + "items": { + "$ref": "#/components/schemas/EventTypeLocation_2024_04_15" + } + }, + "disableGuests": { + "type": "boolean" + }, + "slotInterval": { + "type": "number", + "minimum": 0 + }, + "minimumBookingNotice": { + "type": "number", + "minimum": 0 + }, + "beforeEventBuffer": { + "type": "number", + "minimum": 0 + }, + "afterEventBuffer": { + "type": "number", + "minimum": 0 + } + }, + "required": [ + "length", + "slug", + "title" + ] + }, + "EventTypeOutput": { + "type": "object", + "properties": { + "id": { + "type": "number", + "example": 1 + }, + "length": { + "type": "number", + "example": 60 + }, + "slug": { + "type": "string", + "example": "cooking-class" + }, + "title": { + "type": "string", + "example": "Learn the secrets of masterchief!" + }, + "description": { + "type": "string", + "nullable": true, + "example": "Discover the culinary wonders of the Argentina by making the best flan ever!" + }, + "locations": { + "nullable": true, + "type": "array", + "items": { + "$ref": "#/components/schemas/EventTypeLocation_2024_04_15" + } + } + }, + "required": [ + "id", + "length", + "slug", + "title", + "description", + "locations" + ] + }, + "CreateEventTypeOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/EventTypeOutput" + } + }, + "required": [ + "status", + "data" + ] + }, + "Data": { + "type": "object", + "properties": { + "eventType": { + "$ref": "#/components/schemas/EventTypeOutput" + } + }, + "required": [ + "eventType" + ] + }, + "GetEventTypeOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/Data" + } + }, + "required": [ + "status", + "data" + ] + }, + "EventTypeGroup": { + "type": "object", + "properties": { + "eventTypes": { + "type": "array", + "items": { + "$ref": "#/components/schemas/EventTypeOutput" + } + } + }, + "required": [ + "eventTypes" + ] + }, + "GetEventTypesData": { + "type": "object", + "properties": { + "eventTypeGroups": { + "type": "array", + "items": { + "$ref": "#/components/schemas/EventTypeGroup" + } + } + }, + "required": [ + "eventTypeGroups" + ] + }, + "GetEventTypesOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/GetEventTypesData" + } + }, + "required": [ + "status", + "data" + ] + }, + "Location": { + "type": "object", + "properties": { + "type": { + "type": "string" + } + }, + "required": [ + "type" + ] + }, + "Source": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string" + }, + "label": { + "type": "string" + } + }, + "required": [ + "id", + "type", + "label" + ] + }, + "BookingField": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "type": { + "type": "string" + }, + "defaultLabel": { + "type": "string" + }, + "label": { + "type": "string" + }, + "placeholder": { + "type": "string" + }, + "required": { + "type": "boolean" + }, + "getOptionsAt": { + "type": "string" + }, + "hideWhenJustOneOption": { + "type": "boolean" + }, + "editable": { + "type": "string" + }, + "sources": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Source" + } + }, + "disableOnPrefill": { + "type": "boolean" + } + }, + "required": [ + "name", + "type" + ] + }, + "Organization": { + "type": "object", + "properties": { + "id": { + "type": "number" + }, + "slug": { + "type": "string", + "nullable": true + }, + "name": { + "type": "string" + }, + "metadata": { + "type": "object" + } + }, + "required": [ + "id", + "name", + "metadata" + ] + }, + "Profile": { + "type": "object", + "properties": { + "username": { + "type": "string", + "nullable": true + }, + "id": { + "type": "number", + "nullable": true + }, + "userId": { + "type": "number" + }, + "uid": { + "type": "string" + }, + "name": { + "type": "string" + }, + "organizationId": { + "type": "number", + "nullable": true + }, + "organization": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/Organization" + } + ] + }, + "upId": { + "type": "string" + }, + "image": { + "type": "string" + }, + "brandColor": { + "type": "string" + }, + "darkBrandColor": { + "type": "string" + }, + "theme": { + "type": "string" + }, + "bookerLayouts": { + "type": "object" + } + }, + "required": [ + "username", + "id", + "organizationId", + "upId" + ] + }, + "Owner": { + "type": "object", + "properties": { + "id": { + "type": "number" + }, + "avatarUrl": { + "type": "string", + "nullable": true + }, + "username": { + "type": "string", + "nullable": true + }, + "name": { + "type": "string", + "nullable": true + }, + "weekStart": { + "type": "string" + }, + "brandColor": { + "type": "string", + "nullable": true + }, + "darkBrandColor": { + "type": "string", + "nullable": true + }, + "theme": { + "type": "string", + "nullable": true + }, + "metadata": { + "type": "object" + }, + "defaultScheduleId": { + "type": "number", + "nullable": true + }, + "nonProfileUsername": { + "type": "string", + "nullable": true + }, + "profile": { + "$ref": "#/components/schemas/Profile" + } + }, + "required": [ + "id", + "username", + "name", + "weekStart", + "metadata", + "nonProfileUsername", + "profile" + ] + }, + "Schedule": { + "type": "object", + "properties": { + "id": { + "type": "number" + }, + "timeZone": { + "type": "string", + "nullable": true + } + }, + "required": [ + "id", + "timeZone" + ] + }, + "User": { + "type": "object", + "properties": { + "username": { + "type": "string", + "nullable": true + }, + "name": { + "type": "string", + "nullable": true + }, + "weekStart": { + "type": "string" + }, + "organizationId": { + "type": "number" + }, + "avatarUrl": { + "type": "string", + "nullable": true + }, + "profile": { + "$ref": "#/components/schemas/Profile" + }, + "bookerUrl": { + "type": "string" + } + }, + "required": [ + "username", + "name", + "weekStart", + "profile", + "bookerUrl" + ] + }, + "PublicEventTypeOutput": { + "type": "object", + "properties": { + "id": { + "type": "number" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "eventName": { + "type": "string", + "nullable": true + }, + "slug": { + "type": "string" + }, + "isInstantEvent": { + "type": "boolean" + }, + "aiPhoneCallConfig": { + "type": "object" + }, + "schedulingType": { + "type": "object" + }, + "length": { + "type": "number" + }, + "locations": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Location" + } + }, + "customInputs": { + "type": "array", + "items": { + "type": "object" + } + }, + "disableGuests": { + "type": "boolean" + }, + "metadata": { + "type": "object", + "nullable": true + }, + "lockTimeZoneToggleOnBookingPage": { + "type": "boolean" + }, + "requiresConfirmation": { + "type": "boolean" + }, + "requiresBookerEmailVerification": { + "type": "boolean" + }, + "recurringEvent": { + "type": "object" + }, + "price": { + "type": "number" + }, + "currency": { + "type": "string" + }, + "seatsPerTimeSlot": { + "type": "number", + "nullable": true + }, + "seatsShowAvailabilityCount": { + "type": "boolean", + "nullable": true + }, + "bookingFields": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BookingField" + } + }, + "team": { + "type": "object" + }, + "successRedirectUrl": { + "type": "string", + "nullable": true + }, + "workflows": { + "type": "array", + "items": { + "type": "object" + } + }, + "hosts": { + "type": "array", + "items": { + "type": "object" + } + }, + "owner": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/Owner" + } + ] + }, + "schedule": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/Schedule" + } + ] + }, + "hidden": { + "type": "boolean" + }, + "assignAllTeamMembers": { + "type": "boolean" + }, + "bookerLayouts": { + "type": "object" + }, + "users": { + "type": "array", + "items": { + "$ref": "#/components/schemas/User" + } + }, + "entity": { + "type": "object" + }, + "isDynamic": { + "type": "boolean" + } + }, + "required": [ + "id", + "title", + "description", + "slug", + "isInstantEvent", + "length", + "locations", + "customInputs", + "disableGuests", + "metadata", + "lockTimeZoneToggleOnBookingPage", + "requiresConfirmation", + "requiresBookerEmailVerification", + "price", + "currency", + "seatsShowAvailabilityCount", + "bookingFields", + "workflows", + "hosts", + "owner", + "schedule", + "hidden", + "assignAllTeamMembers", + "users", + "entity", + "isDynamic" + ] + }, + "GetEventTypePublicOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/PublicEventTypeOutput" + } + ] + } + }, + "required": [ + "status", + "data" + ] + }, + "PublicEventType": { + "type": "object", + "properties": { + "id": { + "type": "number", + "example": 1 + }, + "length": { + "type": "number", + "example": 60 + }, + "slug": { + "type": "string", + "example": "cooking-class" + }, + "title": { + "type": "string", + "example": "Learn the secrets of masterchief!" + }, + "description": { + "type": "string", + "nullable": true + } + }, + "required": [ + "id", + "length", + "slug", + "title" + ] + }, + "GetEventTypesPublicOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PublicEventType" + } + } + }, + "required": [ + "status", + "data" + ] + }, + "Option": { + "type": "object", + "properties": { + "value": { + "type": "string" + }, + "label": { + "type": "string" + } + }, + "required": [ + "value", + "label" + ] + }, + "VariantsConfig": { + "type": "object", + "properties": { + "variants": { + "type": "object" + } + }, + "required": [ + "variants" + ] + }, + "View": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "label": { + "type": "string" + }, + "description": { + "type": "string" + } + }, + "required": [ + "id", + "label" + ] + }, + "BookingField_2024_04_15": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "number", + "boolean", + "address", + "name", + "text", + "textarea", + "email", + "phone", + "multiemail", + "select", + "multiselect", + "checkbox", + "radio", + "radioInput" + ] + }, + "name": { + "type": "string" + }, + "options": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Option" + } + }, + "label": { + "type": "string" + }, + "labelAsSafeHtml": { + "type": "string" + }, + "defaultLabel": { + "type": "string" + }, + "placeholder": { + "type": "string" + }, + "required": { + "type": "boolean" + }, + "getOptionsAt": { + "type": "string" + }, + "optionsInputs": { + "type": "object" + }, + "variant": { + "type": "string" + }, + "variantsConfig": { + "$ref": "#/components/schemas/VariantsConfig" + }, + "views": { + "type": "array", + "items": { + "$ref": "#/components/schemas/View" + } + }, + "hideWhenJustOneOption": { + "type": "boolean" + }, + "hidden": { + "type": "boolean" + }, + "editable": { + "type": "string", + "enum": [ + "system", + "system-but-optional", + "system-but-hidden", + "user", + "user-readonly" + ] + }, + "sources": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Source" + } + }, + "disableOnPrefill": { + "type": "boolean" + } + }, + "required": [ + "type", + "name" + ] + }, + "UpdateEventTypeInput_2024_04_15": { + "type": "object", + "properties": { + "length": { + "type": "number", + "minimum": 1 + }, + "slug": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "hidden": { + "type": "boolean" + }, + "locations": { + "type": "array", + "items": { + "$ref": "#/components/schemas/EventTypeLocation_2024_04_15" + } + }, + "bookingFields": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BookingField_2024_04_15" + } + }, + "disableGuests": { + "type": "boolean" + }, + "minimumBookingNotice": { + "type": "number", + "minimum": 0 + }, + "beforeEventBuffer": { + "type": "number", + "minimum": 0 + }, + "afterEventBuffer": { + "type": "number", + "minimum": 0 + }, + "slotInterval": { + "type": "number", + "minimum": 0 + } + } + }, + "UpdateEventTypeOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/EventTypeOutput" + } + }, + "required": [ + "status", + "data" + ] + }, + "DeleteData": { + "type": "object", + "properties": { + "id": { + "type": "number", + "example": 1 + }, + "length": { + "type": "number", + "example": 60 + }, + "slug": { + "type": "string", + "example": "cooking-class" + }, + "title": { + "type": "string", + "example": "Learn the secrets of masterchief!" + } + }, + "required": [ + "id", + "length", + "slug", + "title" + ] + }, + "DeleteEventTypeOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/DeleteData" + } + }, + "required": [ + "status", + "data" + ] + }, + "OrgTeamOutputDto": { + "type": "object", + "properties": { + "id": { + "type": "number" + }, + "parentId": { + "type": "number" + }, + "name": { + "type": "string", + "minLength": 1 + }, + "slug": { + "type": "string" + }, + "logoUrl": { + "type": "string" + }, + "calVideoLogo": { + "type": "string" + }, + "appLogo": { + "type": "string" + }, + "appIconLogo": { + "type": "string" + }, + "bio": { + "type": "string" + }, + "hideBranding": { + "type": "boolean" + }, + "isOrganization": { + "type": "boolean" + }, + "isPrivate": { + "type": "boolean" + }, + "hideBookATeamMember": { + "type": "boolean", + "default": false + }, + "metadata": { + "type": "string" + }, + "theme": { + "type": "string" + }, + "brandColor": { + "type": "string" + }, + "darkBrandColor": { + "type": "string" + }, + "bannerUrl": { + "type": "string" + }, + "timeFormat": { + "type": "number" + }, + "timeZone": { + "type": "string", + "default": "Europe/London" + }, + "weekStart": { + "type": "string", + "default": "Sunday" + } + }, + "required": [ + "id", + "name" + ] + }, + "OrgTeamsOutputResponseDto": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/OrgTeamOutputDto" + } + } + }, + "required": [ + "status", + "data" + ] + }, + "OrgMeTeamsOutputResponseDto": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/OrgTeamOutputDto" + } + } + }, + "required": [ + "status", + "data" + ] + }, + "OrgTeamOutputResponseDto": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/OrgTeamOutputDto" + } + }, + "required": [ + "status", + "data" + ] + }, + "UpdateOrgTeamDto": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1 + }, + "slug": { + "type": "string" + }, + "logoUrl": { + "type": "string" + }, + "calVideoLogo": { + "type": "string" + }, + "appLogo": { + "type": "string" + }, + "appIconLogo": { + "type": "string" + }, + "bio": { + "type": "string" + }, + "hideBranding": { + "type": "boolean", + "default": false + }, + "isPrivate": { + "type": "boolean" + }, + "hideBookATeamMember": { + "type": "boolean" + }, + "metadata": { + "type": "string" + }, + "theme": { + "type": "string" + }, + "brandColor": { + "type": "string" + }, + "darkBrandColor": { + "type": "string" + }, + "bannerUrl": { + "type": "string" + }, + "timeFormat": { + "type": "number" + }, + "timeZone": { + "type": "string", + "default": "Europe/London" + }, + "weekStart": { + "type": "string", + "default": "Sunday" + } + } + }, + "CreateOrgTeamDto": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1 + }, + "slug": { + "type": "string" + }, + "logoUrl": { + "type": "string" + }, + "calVideoLogo": { + "type": "string" + }, + "appLogo": { + "type": "string" + }, + "appIconLogo": { + "type": "string" + }, + "bio": { + "type": "string" + }, + "hideBranding": { + "type": "boolean", + "default": false + }, + "isPrivate": { + "type": "boolean" + }, + "hideBookATeamMember": { + "type": "boolean" + }, + "metadata": { + "type": "string" + }, + "theme": { + "type": "string" + }, + "brandColor": { + "type": "string" + }, + "darkBrandColor": { + "type": "string" + }, + "bannerUrl": { + "type": "string" + }, + "timeFormat": { + "type": "number" + }, + "timeZone": { + "type": "string", + "default": "Europe/London" + }, + "weekStart": { + "type": "string", + "default": "Sunday" + }, + "autoAcceptCreator": { + "type": "boolean", + "default": true + } + }, + "required": [ + "name" + ] + }, + "ScheduleAvailabilityInput_2024_06_11": { + "type": "object", + "properties": { + "days": { + "example": [ + "Monday", + "Tuesday" + ], + "type": "array", + "items": { + "type": "object" + } + }, + "startTime": { + "type": "string", + "pattern": "TIME_FORMAT_HH_MM", + "example": "09:00" + }, + "endTime": { + "type": "string", + "pattern": "TIME_FORMAT_HH_MM", + "example": "10:00" + } + }, + "required": [ + "days", + "startTime", + "endTime" + ] + }, + "ScheduleOverrideInput_2024_06_11": { + "type": "object", + "properties": { + "date": { + "type": "string", + "example": "2024-05-20" + }, + "startTime": { + "type": "string", + "pattern": "TIME_FORMAT_HH_MM", + "example": "12:00" + }, + "endTime": { + "type": "string", + "pattern": "TIME_FORMAT_HH_MM", + "example": "13:00" + } + }, + "required": [ + "date", + "startTime", + "endTime" + ] + }, + "ScheduleOutput_2024_06_11": { + "type": "object", + "properties": { + "id": { + "type": "number", + "example": 254 + }, + "ownerId": { + "type": "number", + "example": 478 + }, + "name": { + "type": "string", + "example": "One-on-one coaching" + }, + "timeZone": { + "type": "string", + "example": "Europe/Rome" + }, + "availability": { + "example": [ + { + "days": [ + "Monday", + "Tuesday" + ], + "startTime": "09:00", + "endTime": "10:00" + } + ], + "type": "array", + "items": { + "$ref": "#/components/schemas/ScheduleAvailabilityInput_2024_06_11" + } + }, + "isDefault": { + "type": "boolean", + "example": true + }, + "overrides": { + "example": [ + { + "date": "2024-05-20", + "startTime": "12:00", + "endTime": "13:00" + } + ], + "type": "array", + "items": { + "$ref": "#/components/schemas/ScheduleOverrideInput_2024_06_11" + } + } + }, + "required": [ + "id", + "ownerId", + "name", + "timeZone", + "availability", + "isDefault", + "overrides" + ] + }, + "GetSchedulesOutput_2024_06_11": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ScheduleOutput_2024_06_11" + } + }, + "error": { + "type": "object" + } + }, + "required": [ + "status", + "data" + ] + }, + "CreateScheduleInput_2024_06_11": { + "type": "object", + "properties": { + "name": { + "type": "string", + "example": "One-on-one coaching" + }, + "timeZone": { + "type": "string", + "example": "Europe/Rome" + }, + "availability": { + "example": [ + { + "days": [ + "Monday", + "Tuesday" + ], + "startTime": "09:00", + "endTime": "10:00" + } + ], + "type": "array", + "items": { + "$ref": "#/components/schemas/ScheduleAvailabilityInput_2024_06_11" + } + }, + "isDefault": { + "type": "boolean", + "example": true + }, + "overrides": { + "example": [ + { + "date": "2024-05-20", + "startTime": "12:00", + "endTime": "14:00" + } + ], + "type": "array", + "items": { + "$ref": "#/components/schemas/ScheduleOverrideInput_2024_06_11" + } + } + }, + "required": [ + "name", + "timeZone", + "isDefault" + ] + }, + "CreateScheduleOutput_2024_06_11": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/ScheduleOutput_2024_06_11" + } + }, + "required": [ + "status", + "data" + ] + }, + "GetScheduleOutput_2024_06_11": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/ScheduleOutput_2024_06_11" + } + ] + }, + "error": { + "type": "object" + } + }, + "required": [ + "status", + "data" + ] + }, + "UpdateScheduleInput_2024_06_11": { + "type": "object", + "properties": { + "name": { + "type": "string", + "example": "One-on-one coaching" + }, + "timeZone": { + "type": "string", + "example": "Europe/Rome" + }, + "availability": { + "example": [ + { + "days": [ + "Monday", + "Tuesday" + ], + "startTime": "09:00", + "endTime": "10:00" + } + ], + "type": "array", + "items": { + "$ref": "#/components/schemas/ScheduleAvailabilityInput_2024_06_11" + } + }, + "isDefault": { + "type": "boolean", + "example": true + }, + "overrides": { + "example": [ + { + "date": "2024-05-20", + "startTime": "12:00", + "endTime": "14:00" + } + ], + "type": "array", + "items": { + "$ref": "#/components/schemas/ScheduleOverrideInput_2024_06_11" + } + } + } + }, + "UpdateScheduleOutput_2024_06_11": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/ScheduleOutput_2024_06_11" + }, + "error": { + "type": "object" + } + }, + "required": [ + "status", + "data" + ] + }, + "DeleteScheduleOutput_2024_06_11": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + } + }, + "required": [ + "status" + ] + }, + "GetUserOutput": { + "type": "object", + "properties": { + "id": { + "type": "number", + "description": "The ID of the user", + "example": 1 + }, + "username": { + "type": "string", + "nullable": true, + "description": "The username of the user", + "example": "john_doe" + }, + "name": { + "type": "string", + "nullable": true, + "description": "The name of the user", + "example": "John Doe" + }, + "email": { + "type": "string", + "description": "The email of the user", + "example": "john@example.com" + }, + "emailVerified": { + "format": "date-time", + "type": "string", + "nullable": true, + "description": "The date when the email was verified", + "example": "2022-01-01T00:00:00Z" + }, + "bio": { + "type": "string", + "nullable": true, + "description": "The bio of the user", + "example": "I am a software developer" + }, + "avatarUrl": { + "type": "string", + "nullable": true, + "description": "The URL of the user's avatar", + "example": "https://example.com/avatar.jpg" + }, + "timeZone": { + "type": "string", + "description": "The time zone of the user", + "example": "America/New_York" + }, + "weekStart": { + "type": "string", + "description": "The week start day of the user", + "example": "Monday" + }, + "appTheme": { + "type": "string", + "nullable": true, + "description": "The app theme of the user", + "example": "light" + }, + "theme": { + "type": "string", + "nullable": true, + "description": "The theme of the user", + "example": "default" + }, + "defaultScheduleId": { + "type": "number", + "nullable": true, + "description": "The ID of the default schedule for the user", + "example": 1 + }, + "locale": { + "type": "string", + "nullable": true, + "description": "The locale of the user", + "example": "en-US" + }, + "timeFormat": { + "type": "number", + "nullable": true, + "description": "The time format of the user", + "example": 12 + }, + "hideBranding": { + "type": "boolean", + "description": "Whether to hide branding for the user", + "example": false + }, + "brandColor": { + "type": "string", + "nullable": true, + "description": "The brand color of the user", + "example": "#ffffff" + }, + "darkBrandColor": { + "type": "string", + "nullable": true, + "description": "The dark brand color of the user", + "example": "#000000" + }, + "allowDynamicBooking": { + "type": "boolean", + "nullable": true, + "description": "Whether dynamic booking is allowed for the user", + "example": true + }, + "createdDate": { + "format": "date-time", + "type": "string", + "description": "The date when the user was created", + "example": "2022-01-01T00:00:00Z" + }, + "verified": { + "type": "boolean", + "nullable": true, + "description": "Whether the user is verified", + "example": true + }, + "invitedTo": { + "type": "number", + "nullable": true, + "description": "The ID of the user who invited this user", + "example": 1 + } + }, + "required": [ + "id", + "email", + "timeZone", + "weekStart", + "hideBranding", + "createdDate" + ] + }, + "GetOrganizationUsersOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/GetUserOutput" + } + } + }, + "required": [ + "status", + "data" + ] + }, + "CreateOrganizationUserInput": { + "type": "object", + "properties": { + "email": { + "type": "string", + "description": "User email address", + "example": "user@example.com" + }, + "username": { + "type": "string", + "description": "Username", + "example": "user123" + }, + "weekday": { + "type": "string", + "description": "Preferred weekday", + "example": "Monday" + }, + "brandColor": { + "type": "string", + "description": "Brand color in HEX format", + "example": "#FFFFFF" + }, + "darkBrandColor": { + "type": "string", + "description": "Dark brand color in HEX format", + "example": "#000000" + }, + "hideBranding": { + "type": "boolean", + "description": "Hide branding", + "example": false + }, + "timeZone": { + "type": "string", + "description": "Time zone", + "example": "America/New_York" + }, + "theme": { + "type": "string", + "nullable": true, + "description": "Theme", + "example": "dark" + }, + "appTheme": { + "type": "string", + "nullable": true, + "description": "Application theme", + "example": "light" + }, + "timeFormat": { + "type": "number", + "description": "Time format", + "example": 24 + }, + "defaultScheduleId": { + "type": "number", + "minimum": 0, + "description": "Default schedule ID", + "example": 1 + }, + "locale": { + "type": "string", + "nullable": true, + "default": "en", + "description": "Locale", + "example": "en" + }, + "avatarUrl": { + "type": "string", + "description": "Avatar URL", + "example": "https://example.com/avatar.jpg" + }, + "organizationRole": { + "type": "object", + "default": "MEMBER" + }, + "autoAccept": { + "type": "object", + "default": true + } + }, + "required": [ + "email", + "organizationRole", + "autoAccept" + ] + }, + "GetOrganizationUserOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/GetUserOutput" + } + }, + "required": [ + "status", + "data" + ] + }, + "UpdateOrganizationUserInput": { + "type": "object", + "properties": {} + }, + "OrgMembershipOutputDto": { + "type": "object", + "properties": { + "role": { + "type": "string", + "enum": [ + "MEMBER", + "OWNER", + "ADMIN" + ] + }, + "id": { + "type": "number" + }, + "userId": { + "type": "number" + }, + "teamId": { + "type": "number" + }, + "accepted": { + "type": "boolean" + }, + "disableImpersonation": { + "type": "boolean" + } + }, + "required": [ + "role", + "id", + "userId", + "teamId", + "accepted" + ] + }, + "GetAllOrgMemberships": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/OrgMembershipOutputDto" + } + }, + "required": [ + "status", + "data" + ] + }, + "CreateOrgMembershipDto": { + "type": "object", + "properties": { + "role": { + "type": "string", + "default": "MEMBER", + "enum": [ + "MEMBER", + "OWNER", + "ADMIN" + ] + }, + "userId": { + "type": "number" + }, + "accepted": { + "type": "boolean", + "default": false + }, + "disableImpersonation": { + "type": "boolean", + "default": false + } + }, + "required": [ + "role", + "userId" + ] + }, + "CreateOrgMembershipOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/OrgMembershipOutputDto" + } + }, + "required": [ + "status", + "data" + ] + }, + "GetOrgMembership": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/OrgMembershipOutputDto" + } + }, + "required": [ + "status", + "data" + ] + }, + "DeleteOrgMembership": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/OrgMembershipOutputDto" + } + }, + "required": [ + "status", + "data" + ] + }, + "UpdateOrgMembershipDto": { + "type": "object", + "properties": { + "role": { + "type": "string", + "default": "MEMBER", + "enum": [ + "MEMBER", + "OWNER", + "ADMIN" + ] + }, + "accepted": { + "type": "boolean", + "default": false + }, + "disableImpersonation": { + "type": "boolean", + "default": false + } + } + }, + "UpdateOrgMembership": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/OrgMembershipOutputDto" + } + }, + "required": [ + "status", + "data" + ] + }, + "CreateTeamEventTypeInput_2024_06_14": { + "type": "object", + "properties": { + "lengthInMinutes": { + "type": "number", + "example": 60 + }, + "title": { + "type": "string", + "example": "Learn the secrets of masterchief!" + }, + "description": { + "type": "string", + "example": "Discover the culinary wonders of the Argentina by making the best flan ever!" + }, + "schedulingType": { + "type": "object" + }, + "hosts": { + "type": "array", + "items": { + "type": "string" + } + }, + "assignAllTeamMembers": { + "type": "boolean" + } + }, + "required": [ + "lengthInMinutes", + "title", + "description", + "schedulingType", + "hosts", + "assignAllTeamMembers" + ] + }, + "CreateTeamEventTypeOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "oneOf": [ + { + "$ref": "#/components/schemas/TeamEventTypeOutput_2024_06_14" + }, + { + "type": "array", + "items": { + "$ref": "#/components/schemas/TeamEventTypeOutput_2024_06_14" + } + } + ] + } + }, + "required": [ + "status", + "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" + } + }, + "required": [ + "interval", + "occurrences", + "frequency" + ] + }, + "TeamEventTypeResponseHost": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "userId": { + "type": "number" + }, + "mandatory": { + "type": "boolean", + "default": false + }, + "priority": { + "type": "object", + "default": "medium" + } + }, + "required": [ + "name", + "userId" + ] + }, + "BookingLimitsCount_2024_06_14": { + "type": "object", + "properties": { + "day": { + "type": "number", + "minimum": 1, + "description": "The number of bookings per day", + "example": 1 + }, + "week": { + "type": "number", + "minimum": 1, + "description": "The number of bookings per week", + "example": 2 + }, + "month": { + "type": "number", + "minimum": 1, + "description": "The number of bookings per month", + "example": 3 + }, + "year": { + "type": "number", + "minimum": 1, + "description": "The number of bookings per year", + "example": 4 + } + } + }, + "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 + } + } + }, + "TeamEventTypeOutput_2024_06_14": { + "type": "object", + "properties": { + "id": { + "type": "number", + "example": 1 + }, + "lengthInMinutes": { + "type": "number", + "minimum": 1 + }, + "title": { + "type": "string" + }, + "slug": { + "type": "string" + }, + "description": { + "type": "string" + }, + "locations": { + "type": "array", + "items": { + "type": "object" + } + }, + "bookingFields": { + "type": "array", + "items": { + "type": "object" + } + }, + "disableGuests": { + "type": "boolean" + }, + "slotInterval": { + "type": "number", + "nullable": true + }, + "minimumBookingNotice": { + "type": "number", + "minimum": 0 + }, + "beforeEventBuffer": { + "type": "number" + }, + "afterEventBuffer": { + "type": "number" + }, + "schedulingType": { + "type": "object", + "nullable": true + }, + "recurrence": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/Recurrence_2024_06_14" + } + ] + }, + "metadata": { + "type": "object" + }, + "requiresConfirmation": { + "type": "boolean" + }, + "price": { + "type": "number" + }, + "currency": { + "type": "string" + }, + "lockTimeZoneToggleOnBookingPage": { + "type": "boolean" + }, + "seatsPerTimeSlot": { + "type": "number", + "nullable": true + }, + "forwardParamsSuccessRedirect": { + "type": "boolean", + "nullable": true + }, + "successRedirectUrl": { + "type": "string", + "nullable": true + }, + "seatsShowAvailabilityCount": { + "type": "boolean", + "nullable": true + }, + "isInstantEvent": { + "type": "boolean" + }, + "scheduleId": { + "type": "number", + "nullable": true + }, + "teamId": { + "type": "number", + "nullable": true + }, + "ownerId": { + "type": "number", + "nullable": true + }, + "parentEventTypeId": { + "type": "number", + "nullable": true + }, + "hosts": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TeamEventTypeResponseHost" + } + }, + "assignAllTeamMembers": { + "type": "boolean" + }, + "bookingLimitsCount": { + "$ref": "#/components/schemas/BookingLimitsCount_2024_06_14" + }, + "onlyShowFirstAvailableSlot": { + "type": "boolean" + }, + "bookingLimitsDuration": { + "$ref": "#/components/schemas/BookingLimitsDuration_2024_06_14" + }, + "bookingWindow": { + "type": "object" + }, + "offsetStart": { + "type": "number", + "minimum": 1 + } + }, + "required": [ + "id", + "lengthInMinutes", + "title", + "slug", + "description", + "locations", + "bookingFields", + "disableGuests", + "schedulingType", + "recurrence", + "metadata", + "requiresConfirmation", + "price", + "currency", + "lockTimeZoneToggleOnBookingPage", + "seatsPerTimeSlot", + "forwardParamsSuccessRedirect", + "successRedirectUrl", + "seatsShowAvailabilityCount", + "isInstantEvent", + "scheduleId", + "hosts" + ] + }, + "GetTeamEventTypeOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/TeamEventTypeOutput_2024_06_14" + } + }, + "required": [ + "status", + "data" + ] + }, + "GetTeamEventTypesOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TeamEventTypeOutput_2024_06_14" + } + } + }, + "required": [ + "status", + "data" + ] + }, + "UpdateTeamEventTypeInput_2024_06_14": { + "type": "object", + "properties": { + "lengthInMinutes": { + "type": "number" + }, + "title": { + "type": "string" + }, + "slug": { + "type": "string" + }, + "description": { + "type": "string" + }, + "locations": { + "type": "array", + "items": { + "type": "string" + } + }, + "bookingFields": { + "type": "array", + "items": { + "type": "string" + } + }, + "disableGuests": { + "type": "boolean" + }, + "slotInterval": { + "type": "number" + }, + "minimumBookingNotice": { + "type": "number" + }, + "beforeEventBuffer": { + "type": "number" + }, + "afterEventBuffer": { + "type": "number" + }, + "hosts": { + "type": "array", + "items": { + "type": "string" + } + }, + "assignAllTeamMembers": { + "type": "boolean" + } + }, + "required": [ + "lengthInMinutes", + "title", + "slug", + "description", + "locations", + "bookingFields", + "disableGuests", + "slotInterval", + "minimumBookingNotice", + "beforeEventBuffer", + "afterEventBuffer", + "hosts", + "assignAllTeamMembers" + ] + }, + "UpdateTeamEventTypeOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "oneOf": [ + { + "$ref": "#/components/schemas/TeamEventTypeOutput_2024_06_14" + }, + { + "type": "array", + "items": { + "$ref": "#/components/schemas/TeamEventTypeOutput_2024_06_14" + } + } + ] + } + }, + "required": [ + "status", + "data" + ] + }, + "DeleteTeamEventTypeOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "type": "object" + } + }, + "required": [ + "status", + "data" + ] + }, + "OrgTeamMembershipOutputDto": { + "type": "object", + "properties": { + "role": { + "type": "string", + "enum": [ + "MEMBER", + "OWNER", + "ADMIN" + ] + }, + "id": { + "type": "number" + }, + "userId": { + "type": "number" + }, + "teamId": { + "type": "number" + }, + "accepted": { + "type": "boolean" + }, + "disableImpersonation": { + "type": "boolean" + } + }, + "required": [ + "role", + "id", + "userId", + "teamId", + "accepted" + ] + }, + "OrgTeamMembershipsOutputResponseDto": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/OrgTeamMembershipOutputDto" + } + } + }, + "required": [ + "status", + "data" + ] + }, + "OrgTeamMembershipOutputResponseDto": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/OrgTeamMembershipOutputDto" + } + }, + "required": [ + "status", + "data" + ] + }, + "UpdateOrgTeamMembershipDto": { + "type": "object", + "properties": { + "role": { + "type": "string", + "default": "MEMBER", + "enum": [ + "MEMBER", + "OWNER", + "ADMIN" + ] + }, + "accepted": { + "type": "boolean", + "default": false + }, + "disableImpersonation": { + "type": "boolean", + "default": false + } + } + }, + "CreateOrgTeamMembershipDto": { + "type": "object", + "properties": { + "role": { + "type": "string", + "default": "MEMBER", + "enum": [ + "MEMBER", + "OWNER", + "ADMIN" + ] + }, + "userId": { + "type": "number" + }, + "accepted": { + "type": "boolean", + "default": false + }, + "disableImpersonation": { + "type": "boolean", + "default": false + } + }, + "required": [ + "role", + "userId" + ] + }, + "Attribute": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "The ID of the attribute", + "example": "attr_123" + }, + "teamId": { + "type": "number", + "description": "The team ID associated with the attribute", + "example": 1 + }, + "type": { + "type": "string", + "description": "The type of the attribute", + "enum": [ + "TEXT", + "NUMBER", + "SINGLE_SELECT", + "MULTI_SELECT" + ] + }, + "name": { + "type": "string", + "description": "The name of the attribute", + "example": "Attribute Name" + }, + "slug": { + "type": "string", + "description": "The slug of the attribute", + "example": "attribute-name" + }, + "enabled": { + "type": "boolean", + "description": "Whether the attribute is enabled and displayed on their profile", + "example": true + }, + "usersCanEditRelation": { + "type": "boolean", + "description": "Whether users can edit the relation", + "example": true + } + }, + "required": [ + "id", + "teamId", + "type", + "name", + "slug", + "enabled" + ] + }, + "GetOrganizationAttributesOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Attribute" + } + } + }, + "required": [ + "status", + "data" + ] + }, + "GetSingleAttributeOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/Attribute" + } + ] + } + }, + "required": [ + "status", + "data" + ] + }, + "CreateOrganizationAttributeOptionInput": { + "type": "object", + "properties": { + "value": { + "type": "string" + }, + "slug": { + "type": "string" + } + }, + "required": [ + "value", + "slug" + ] + }, + "CreateOrganizationAttributeInput": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "slug": { + "type": "string" + }, + "type": { + "type": "object" + }, + "options": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CreateOrganizationAttributeOptionInput" + } + }, + "enabled": { + "type": "boolean" + } + }, + "required": [ + "name", + "slug", + "type", + "options" + ] + }, + "CreateOrganizationAttributesOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/Attribute" + } + }, + "required": [ + "status", + "data" + ] + }, + "UpdateOrganizationAttributeInput": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "slug": { + "type": "string" + }, + "type": { + "type": "object" + }, + "enabled": { + "type": "boolean" + } + } + }, + "UpdateOrganizationAttributesOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/Attribute" + } + }, + "required": [ + "status", + "data" + ] + }, + "DeleteOrganizationAttributesOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/Attribute" + } + }, + "required": [ + "status", + "data" + ] + }, + "OptionOutput": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "The ID of the option", + "example": "attr_option_id" + }, + "attributeId": { + "type": "string", + "description": "The ID of the attribute", + "example": "attr_id" + }, + "value": { + "type": "string", + "description": "The value of the option", + "example": "option_value" + }, + "slug": { + "type": "string", + "description": "The slug of the option", + "example": "option-slug" + } + }, + "required": [ + "id", + "attributeId", + "value", + "slug" + ] + }, + "CreateAttributeOptionOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/OptionOutput" + } + }, + "required": [ + "status", + "data" + ] + }, + "DeleteAttributeOptionOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/OptionOutput" + } + }, + "required": [ + "status", + "data" + ] + }, + "UpdateOrganizationAttributeOptionInput": { + "type": "object", + "properties": { + "value": { + "type": "string" + }, + "slug": { + "type": "string" + } + } + }, + "UpdateAttributeOptionOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/OptionOutput" + } + }, + "required": [ + "status", + "data" + ] + }, + "GetAllAttributeOptionOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/OptionOutput" + } + } + }, + "required": [ + "status", + "data" + ] + }, + "AssignOrganizationAttributeOptionToUserInput": { + "type": "object", + "properties": { + "value": { + "type": "string" + }, + "attributeOptionId": { + "type": "string" + }, + "attributeId": { + "type": "string" + } + }, + "required": [ + "attributeId" + ] + }, + "AssignOptionUserOutputData": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "The ID of the option assigned to the user" + }, + "memberId": { + "type": "number", + "description": "The ID form the org membership for the user" + }, + "attributeOptionId": { + "type": "string", + "description": "The value of the option" + } + }, + "required": [ + "id", + "memberId", + "attributeOptionId" + ] + }, + "AssignOptionUserOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/AssignOptionUserOutputData" + } + }, + "required": [ + "status", + "data" + ] + }, + "UnassignOptionUserOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/AssignOptionUserOutputData" + } + }, + "required": [ + "status", + "data" + ] + }, + "GetOptionUserOutputData": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "The ID of the option assigned to the user" + }, + "attributeId": { + "type": "string", + "description": "The ID of the attribute" + }, + "value": { + "type": "string", + "description": "The value of the option" + }, + "slug": { + "type": "string", + "description": "The slug of the option" + } + }, + "required": [ + "id", + "attributeId", + "value", + "slug" + ] + }, + "GetOptionUserOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/GetOptionUserOutputData" + } + } + }, + "required": [ + "status", + "data" + ] + }, + "TeamWebhookOutputDto": { + "type": "object", + "properties": { + "payloadTemplate": { + "type": "string", + "description": "The template of the payload that will be sent to the subscriberUrl, check cal.com/docs/core-features/webhooks for more information", + "example": "{\"content\":\"A new event has been scheduled\",\"type\":\"{{type}}\",\"name\":\"{{title}}\",\"organizer\":\"{{organizer.name}}\",\"booker\":\"{{attendees.0.name}}\"}" + }, + "teamId": { + "type": "number" + }, + "id": { + "type": "number" + }, + "triggers": { + "type": "array", + "items": { + "type": "object" + } + }, + "subscriberUrl": { + "type": "string" + }, + "active": { + "type": "boolean" + }, + "secret": { + "type": "string" + } + }, + "required": [ + "payloadTemplate", + "teamId", + "id", + "triggers", + "subscriberUrl", + "active" + ] + }, + "TeamWebhooksOutputResponseDto": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TeamWebhookOutputDto" + } + } + }, + "required": [ + "status", + "data" + ] + }, + "CreateWebhookInputDto": { + "type": "object", + "properties": { + "payloadTemplate": { + "type": "string", + "description": "The template of the payload that will be sent to the subscriberUrl, check cal.com/docs/core-features/webhooks for more information", + "example": "{\"content\":\"A new event has been scheduled\",\"type\":\"{{type}}\",\"name\":\"{{title}}\",\"organizer\":\"{{organizer.name}}\",\"booker\":\"{{attendees.0.name}}\"}" + }, + "triggers": { + "type": "string", + "example": [ + "BOOKING_CREATED", + "BOOKING_RESCHEDULED", + "BOOKING_CANCELLED", + "BOOKING_CONFIRMED", + "BOOKING_REJECTED", + "BOOKING_COMPLETED", + "BOOKING_NO_SHOW", + "BOOKING_REOPENED" + ], + "enum": [ + "BOOKING_CREATED", + "BOOKING_PAYMENT_INITIATED", + "BOOKING_PAID", + "BOOKING_RESCHEDULED", + "BOOKING_REQUESTED", + "BOOKING_CANCELLED", + "BOOKING_REJECTED", + "BOOKING_NO_SHOW_UPDATED", + "FORM_SUBMITTED", + "MEETING_ENDED", + "MEETING_STARTED", + "RECORDING_READY", + "INSTANT_MEETING", + "RECORDING_TRANSCRIPTION_GENERATED", + "OOO_CREATED" + ] + }, + "active": { + "type": "boolean" + }, + "subscriberUrl": { + "type": "string" + }, + "secret": { + "type": "string" + } + }, + "required": [ + "triggers", + "active", + "subscriberUrl" + ] + }, + "TeamWebhookOutputResponseDto": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/TeamWebhookOutputDto" + } + }, + "required": [ + "status", + "data" + ] + }, + "UpdateWebhookInputDto": { + "type": "object", + "properties": { + "payloadTemplate": { + "type": "string", + "description": "The template of the payload that will be sent to the subscriberUrl, check cal.com/docs/core-features/webhooks for more information", + "example": "{\"content\":\"A new event has been scheduled\",\"type\":\"{{type}}\",\"name\":\"{{title}}\",\"organizer\":\"{{organizer.name}}\",\"booker\":\"{{attendees.0.name}}\"}" + }, + "triggers": { + "type": "string", + "example": [ + "BOOKING_CREATED", + "BOOKING_RESCHEDULED", + "BOOKING_CANCELLED", + "BOOKING_CONFIRMED", + "BOOKING_REJECTED", + "BOOKING_COMPLETED", + "BOOKING_NO_SHOW", + "BOOKING_REOPENED" + ], + "enum": [ + "BOOKING_CREATED", + "BOOKING_PAYMENT_INITIATED", + "BOOKING_PAID", + "BOOKING_RESCHEDULED", + "BOOKING_REQUESTED", + "BOOKING_CANCELLED", + "BOOKING_REJECTED", + "BOOKING_NO_SHOW_UPDATED", + "FORM_SUBMITTED", + "MEETING_ENDED", + "MEETING_STARTED", + "RECORDING_READY", + "INSTANT_MEETING", + "RECORDING_TRANSCRIPTION_GENERATED", + "OOO_CREATED" + ] + }, + "active": { + "type": "boolean" + }, + "subscriberUrl": { + "type": "string" + }, + "secret": { + "type": "string" + } + } + }, + "GetDefaultScheduleOutput_2024_06_11": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/ScheduleOutput_2024_06_11" + } + }, + "required": [ + "status", + "data" + ] + }, + "CreateAvailabilityInput_2024_04_15": { + "type": "object", + "properties": { + "days": { + "example": [ + 1, + 2 + ], + "type": "array", + "items": { + "type": "number" + } + }, + "startTime": { + "format": "date-time", + "type": "string" + }, + "endTime": { + "format": "date-time", + "type": "string" + } + }, + "required": [ + "days", + "startTime", + "endTime" + ] + }, + "CreateScheduleInput_2024_04_15": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "timeZone": { + "type": "string" + }, + "availabilities": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CreateAvailabilityInput_2024_04_15" + } + }, + "isDefault": { + "type": "boolean" + } + }, + "required": [ + "name", + "timeZone", + "isDefault" + ] + }, + "WorkingHours": { + "type": "object", + "properties": { + "days": { + "type": "array", + "items": { + "type": "number" + } + }, + "startTime": { + "type": "number" + }, + "endTime": { + "type": "number" + }, + "userId": { + "type": "number", + "nullable": true + } + }, + "required": [ + "days", + "startTime", + "endTime" + ] + }, + "AvailabilityModel": { + "type": "object", + "properties": { + "id": { + "type": "number" + }, + "userId": { + "type": "number", + "nullable": true + }, + "eventTypeId": { + "type": "number", + "nullable": true + }, + "days": { + "type": "array", + "items": { + "type": "number" + } + }, + "startTime": { + "format": "date-time", + "type": "string" + }, + "endTime": { + "format": "date-time", + "type": "string" + }, + "date": { + "format": "date-time", + "type": "string", + "nullable": true + }, + "scheduleId": { + "type": "number", + "nullable": true + } + }, + "required": [ + "id", + "days", + "startTime", + "endTime" + ] + }, + "TimeRange": { + "type": "object", + "properties": { + "userId": { + "type": "number", + "nullable": true + }, + "start": { + "format": "date-time", + "type": "string" + }, + "end": { + "format": "date-time", + "type": "string" + } + }, + "required": [ + "start", + "end" + ] + }, + "ScheduleOutput": { + "type": "object", + "properties": { + "id": { + "type": "number" + }, + "name": { + "type": "string" + }, + "isManaged": { + "type": "boolean" + }, + "workingHours": { + "type": "array", + "items": { + "$ref": "#/components/schemas/WorkingHours" + } + }, + "schedule": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AvailabilityModel" + } + }, + "availability": { + "type": "array", + "items": { + "required": true, + "type": "array", + "items": { + "$ref": "#/components/schemas/TimeRange" + } + } + }, + "timeZone": { + "type": "string" + }, + "dateOverrides": { + "type": "array", + "items": { + "type": "object" + } + }, + "isDefault": { + "type": "boolean" + }, + "isLastSchedule": { + "type": "boolean" + }, + "readOnly": { + "type": "boolean" + } + }, + "required": [ + "id", + "name", + "isManaged", + "workingHours", + "schedule", + "availability", + "timeZone", + "dateOverrides", + "isDefault", + "isLastSchedule", + "readOnly" + ] + }, + "CreateScheduleOutput_2024_04_15": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/ScheduleOutput" + } + }, + "required": [ + "status", + "data" + ] + }, + "GetDefaultScheduleOutput_2024_04_15": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/ScheduleOutput" + } + ] + } + }, + "required": [ + "status", + "data" + ] + }, + "GetScheduleOutput_2024_04_15": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/ScheduleOutput" + } + }, + "required": [ + "status", + "data" + ] + }, + "GetSchedulesOutput_2024_04_15": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/ScheduleOutput" + } + }, + "required": [ + "status", + "data" + ] + }, + "UpdateScheduleInput_2024_04_15": { + "type": "object", + "properties": { + "timeZone": { + "type": "string" + }, + "name": { + "type": "string" + }, + "isDefault": { + "type": "boolean" + }, + "schedule": { + "example": [ + [], + [ + { + "start": "2022-01-01T00:00:00.000Z", + "end": "2022-01-02T00:00:00.000Z" + } + ], + [], + [], + [], + [], + [] + ], + "items": { + "type": "array" + }, + "type": "array" + }, + "dateOverrides": { + "example": [ + [], + [ + { + "start": "2022-01-01T00:00:00.000Z", + "end": "2022-01-02T00:00:00.000Z" + } + ], + [], + [], + [], + [], + [] + ], + "items": { + "type": "array" + }, + "type": "array" + } + }, + "required": [ + "timeZone", + "name", + "isDefault", + "schedule" + ] + }, + "EventTypeModel_2024_04_15": { + "type": "object", + "properties": { + "id": { + "type": "number" + }, + "eventName": { + "type": "string", + "nullable": true + } + }, + "required": [ + "id" + ] + }, + "AvailabilityModel_2024_04_15": { + "type": "object", + "properties": { + "id": { + "type": "number" + }, + "userId": { + "type": "number", + "nullable": true + }, + "scheduleId": { + "type": "number", + "nullable": true + }, + "eventTypeId": { + "type": "number", + "nullable": true + }, + "days": { + "type": "array", + "items": { + "type": "number" + } + }, + "startTime": { + "format": "date-time", + "type": "string" + }, + "endTime": { + "format": "date-time", + "type": "string" + }, + "date": { + "format": "date-time", + "type": "string", + "nullable": true + } + }, + "required": [ + "id", + "days" + ] + }, + "ScheduleModel_2024_04_15": { + "type": "object", + "properties": { + "id": { + "type": "number" + }, + "userId": { + "type": "number" + }, + "name": { + "type": "string" + }, + "timeZone": { + "type": "string", + "nullable": true + }, + "eventType": { + "type": "array", + "items": { + "$ref": "#/components/schemas/EventTypeModel_2024_04_15" + } + }, + "availability": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AvailabilityModel_2024_04_15" + } + } + }, + "required": [ + "id", + "userId", + "name" + ] + }, + "UpdatedScheduleOutput_2024_04_15": { + "type": "object", + "properties": { + "schedule": { + "$ref": "#/components/schemas/ScheduleModel_2024_04_15" + }, + "isDefault": { + "type": "boolean" + }, + "timeZone": { + "type": "string" + }, + "prevDefaultId": { + "type": "number", + "nullable": true + }, + "currentDefaultId": { + "type": "number", + "nullable": true + } + }, + "required": [ + "schedule", + "isDefault" + ] + }, + "UpdateScheduleOutput_2024_04_15": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/UpdatedScheduleOutput_2024_04_15" + } + }, + "required": [ + "status", + "data" + ] + }, + "DeleteScheduleOutput_2024_04_15": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + } + }, + "required": [ + "status" + ] + }, + "AuthUrlData": { + "type": "object", + "properties": { + "authUrl": { + "type": "string" + } + }, + "required": [ + "authUrl" + ] + }, + "GcalAuthUrlOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/AuthUrlData" + } + }, + "required": [ + "status", + "data" + ] + }, + "GcalSaveRedirectOutput": { + "type": "object", + "properties": { + "url": { + "type": "string" + } + }, + "required": [ + "url" + ] + }, + "GcalCheckOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + } + }, + "required": [ + "status" + ] + }, + "ProviderVerifyClientData": { + "type": "object", + "properties": { + "clientId": { + "type": "string" + }, + "organizationId": { + "type": "number" + }, + "name": { + "type": "string" + } + }, + "required": [ + "clientId", + "organizationId", + "name" + ] + }, + "ProviderVerifyClientOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/ProviderVerifyClientData" + } + }, + "required": [ + "status", + "data" + ] + }, + "ProviderVerifyAccessTokenOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + } + }, + "required": [ + "status" + ] + }, + "MeOrgOutput": { + "type": "object", + "properties": { + "isPlatform": { + "type": "boolean" + }, + "id": { + "type": "number" + } + }, + "required": [ + "isPlatform", + "id" + ] + }, + "MeOutput": { + "type": "object", + "properties": { + "id": { + "type": "number" + }, + "username": { + "type": "string" + }, + "email": { + "type": "string" + }, + "timeFormat": { + "type": "number" + }, + "defaultScheduleId": { + "type": "number", + "nullable": true + }, + "weekStart": { + "type": "string" + }, + "timeZone": { + "type": "string" + }, + "organizationId": { + "type": "number", + "nullable": true + }, + "organization": { + "$ref": "#/components/schemas/MeOrgOutput" + } + }, + "required": [ + "id", + "username", + "email", + "timeFormat", + "defaultScheduleId", + "weekStart", + "timeZone", + "organizationId" + ] + }, + "GetMeOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/MeOutput" + } + }, + "required": [ + "status", + "data" + ] + }, + "UpdateMeOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/MeOutput" + } + }, + "required": [ + "status", + "data" + ] + }, + "BusyTimesOutput": { + "type": "object", + "properties": { + "start": { + "format": "date-time", + "type": "string" + }, + "end": { + "format": "date-time", + "type": "string" + }, + "source": { + "type": "string", + "nullable": true + } + }, + "required": [ + "start", + "end" + ] + }, + "GetBusyTimesOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BusyTimesOutput" + } + } + }, + "required": [ + "status", + "data" + ] + }, + "Integration": { + "type": "object", + "properties": { + "appData": { + "type": "object", + "nullable": true + }, + "dirName": { + "type": "string" + }, + "__template": { + "type": "string" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "installed": { + "type": "boolean" + }, + "type": { + "type": "string" + }, + "title": { + "type": "string" + }, + "variant": { + "type": "string" + }, + "category": { + "type": "string" + }, + "categories": { + "type": "array", + "items": { + "type": "string" + } + }, + "logo": { + "type": "string" + }, + "publisher": { + "type": "string" + }, + "slug": { + "type": "string" + }, + "url": { + "type": "string" + }, + "email": { + "type": "string" + }, + "locationOption": { + "type": "object", + "nullable": true + } + }, + "required": [ + "name", + "description", + "type", + "variant", + "categories", + "logo", + "publisher", + "slug", + "url", + "email", + "locationOption" + ] + }, + "Primary": { + "type": "object", + "properties": { + "externalId": { + "type": "string" + }, + "integration": { + "type": "string" + }, + "name": { + "type": "string" + }, + "primary": { + "type": "boolean", + "nullable": true + }, + "readOnly": { + "type": "boolean" + }, + "email": { + "type": "string" + }, + "isSelected": { + "type": "boolean" + }, + "credentialId": { + "type": "number" + } + }, + "required": [ + "externalId", + "primary", + "readOnly", + "isSelected", + "credentialId" + ] + }, + "Calendar": { + "type": "object", + "properties": { + "externalId": { + "type": "string" + }, + "integration": { + "type": "string" + }, + "name": { + "type": "string" + }, + "primary": { + "type": "boolean", + "nullable": true + }, + "readOnly": { + "type": "boolean" + }, + "email": { + "type": "string" + }, + "isSelected": { + "type": "boolean" + }, + "credentialId": { + "type": "number" + } + }, + "required": [ + "externalId", + "readOnly", + "isSelected", + "credentialId" + ] + }, + "ConnectedCalendar": { + "type": "object", + "properties": { + "integration": { + "$ref": "#/components/schemas/Integration" + }, + "credentialId": { + "type": "number" + }, + "primary": { + "$ref": "#/components/schemas/Primary" + }, + "calendars": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Calendar" + } + } + }, + "required": [ + "integration", + "credentialId" + ] + }, + "DestinationCalendar": { + "type": "object", + "properties": { + "id": { + "type": "number" + }, + "integration": { + "type": "string" + }, + "externalId": { + "type": "string" + }, + "primaryEmail": { + "type": "string", + "nullable": true + }, + "userId": { + "type": "number", + "nullable": true + }, + "eventTypeId": { + "type": "number", + "nullable": true + }, + "credentialId": { + "type": "number", + "nullable": true + }, + "name": { + "type": "string", + "nullable": true + }, + "primary": { + "type": "boolean" + }, + "readOnly": { + "type": "boolean" + }, + "email": { + "type": "string" + }, + "integrationTitle": { + "type": "string" + } + }, + "required": [ + "id", + "integration", + "externalId", + "primaryEmail", + "userId", + "eventTypeId", + "credentialId" + ] + }, + "ConnectedCalendarsData": { + "type": "object", + "properties": { + "connectedCalendars": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ConnectedCalendar" + } + }, + "destinationCalendar": { + "$ref": "#/components/schemas/DestinationCalendar" + } + }, + "required": [ + "connectedCalendars", + "destinationCalendar" + ] + }, + "ConnectedCalendarsOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/ConnectedCalendarsData" + } + }, + "required": [ + "status", + "data" + ] + }, + "DeleteCalendarCredentialsInputBodyDto": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "example": 10, + "description": "Credential ID of the calendar to delete, as returned by the /calendars endpoint" + } + }, + "required": [ + "id" + ] + }, + "DeletedCalendarCredentialsOutputDto": { + "type": "object", + "properties": { + "id": { + "type": "number" + }, + "type": { + "type": "string" + }, + "userId": { + "type": "number", + "nullable": true + }, + "teamId": { + "type": "number", + "nullable": true + }, + "appId": { + "type": "string", + "nullable": true + }, + "invalid": { + "type": "boolean", + "nullable": true + } + }, + "required": [ + "id", + "type", + "userId", + "teamId", + "appId", + "invalid" + ] + }, + "DeletedCalendarCredentialsOutputResponseDto": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/DeletedCalendarCredentialsOutputDto" + } + }, + "required": [ + "status", + "data" + ] + }, + "Attendee": { + "type": "object", + "properties": { + "id": { + "type": "number" + }, + "email": { + "type": "string" + }, + "name": { + "type": "string" + }, + "timeZone": { + "type": "string" + }, + "locale": { + "type": "string", + "nullable": true + }, + "bookingId": { + "type": "number", + "nullable": true + } + }, + "required": [ + "id", + "email", + "name", + "timeZone", + "locale", + "bookingId" + ] + }, + "EventType": { + "type": "object", + "properties": { + "slug": { + "type": "string" + }, + "id": { + "type": "number" + }, + "eventName": { + "type": "string", + "nullable": true + }, + "price": { + "type": "number" + }, + "recurringEvent": { + "type": "object" + }, + "currency": { + "type": "string" + }, + "metadata": { + "type": "object" + }, + "seatsShowAttendees": { + "type": "object" + }, + "seatsShowAvailabilityCount": { + "type": "object" + }, + "team": { + "type": "object", + "nullable": true + } + }, + "required": [ + "price", + "currency", + "metadata" + ] + }, + "Reference": { + "type": "object", + "properties": { + "id": { + "type": "number" + }, + "type": { + "type": "string" + }, + "uid": { + "type": "string" + }, + "meetingId": { + "type": "string", + "nullable": true + }, + "thirdPartyRecurringEventId": { + "type": "string", + "nullable": true + }, + "meetingPassword": { + "type": "string", + "nullable": true + }, + "meetingUrl": { + "type": "string", + "nullable": true + }, + "bookingId": { + "type": "number", + "nullable": true + }, + "externalCalendarId": { + "type": "string", + "nullable": true + }, + "deleted": { + "type": "object" + }, + "credentialId": { + "type": "number", + "nullable": true + } + }, + "required": [ + "id", + "type", + "uid", + "meetingPassword", + "bookingId", + "externalCalendarId", + "credentialId" + ] + }, + "GetBookingsDataEntry": { + "type": "object", + "properties": { + "id": { + "type": "number" + }, + "title": { + "type": "string" + }, + "userPrimaryEmail": { + "type": "string", + "nullable": true + }, + "description": { + "type": "string", + "nullable": true + }, + "customInputs": { + "type": "object" + }, + "startTime": { + "type": "string" + }, + "endTime": { + "type": "string" + }, + "attendees": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Attendee" + } + }, + "metadata": { + "type": "object" + }, + "uid": { + "type": "string" + }, + "recurringEventId": { + "type": "string", + "nullable": true + }, + "location": { + "type": "string", + "nullable": true + }, + "eventType": { + "$ref": "#/components/schemas/EventType" + }, + "status": { + "type": "object" + }, + "paid": { + "type": "boolean" + }, + "payment": { + "type": "array", + "items": { + "type": "object" + } + }, + "references": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Reference" + } + }, + "isRecorded": { + "type": "boolean" + }, + "seatsReferences": { + "type": "array", + "items": { + "type": "object" + } + }, + "user": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/User" + } + ] + }, + "rescheduled": { + "type": "object" + } + }, + "required": [ + "id", + "title", + "description", + "customInputs", + "startTime", + "endTime", + "attendees", + "metadata", + "uid", + "recurringEventId", + "location", + "eventType", + "status", + "paid", + "payment", + "references", + "isRecorded", + "seatsReferences", + "user" + ] + }, + "GetBookingsData_2024_04_15": { + "type": "object", + "properties": { + "bookings": { + "type": "array", + "items": { + "$ref": "#/components/schemas/GetBookingsDataEntry" + } + }, + "recurringInfo": { + "type": "array", + "items": { + "type": "object" + } + }, + "nextCursor": { + "type": "number", + "nullable": true + } + }, + "required": [ + "bookings", + "recurringInfo", + "nextCursor" + ] + }, + "GetBookingsOutput_2024_04_15": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/GetBookingsData_2024_04_15" + } + }, + "required": [ + "status", + "data" + ] + }, + "GetBookingData_2024_04_15": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "id": { + "type": "number" + }, + "uid": { + "type": "string" + }, + "description": { + "type": "string", + "nullable": true + }, + "customInputs": { + "type": "object" + }, + "smsReminderNumber": { + "type": "string", + "nullable": true + }, + "recurringEventId": { + "type": "string", + "nullable": true + }, + "startTime": { + "format": "date-time", + "type": "string" + }, + "endTime": { + "format": "date-time", + "type": "string" + }, + "location": { + "type": "string", + "nullable": true + }, + "status": { + "type": "string" + }, + "metadata": { + "type": "object" + }, + "cancellationReason": { + "type": "string", + "nullable": true + }, + "responses": { + "type": "object" + }, + "rejectionReason": { + "type": "string", + "nullable": true + }, + "userPrimaryEmail": { + "type": "string", + "nullable": true + }, + "user": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/User" + } + ] + }, + "attendees": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Attendee" + } + }, + "eventTypeId": { + "type": "number", + "nullable": true + }, + "eventType": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/EventType" + } + ] + } + }, + "required": [ + "title", + "id", + "uid", + "description", + "customInputs", + "smsReminderNumber", + "recurringEventId", + "startTime", + "endTime", + "location", + "status", + "metadata", + "cancellationReason", + "responses", + "rejectionReason", + "userPrimaryEmail", + "user", + "attendees", + "eventTypeId", + "eventType" + ] + }, + "GetBookingOutput_2024_04_15": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/GetBookingData_2024_04_15" + } + }, + "required": [ + "status", + "data" + ] + }, + "Response": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "email": { + "type": "string" + }, + "guests": { + "type": "array", + "items": { + "type": "string" + } + }, + "location": { + "$ref": "#/components/schemas/Location" + }, + "notes": { + "type": "string" + } + }, + "required": [ + "name", + "email", + "guests" + ] + }, + "CreateBookingInput_2024_04_15": { + "type": "object", + "properties": { + "end": { + "type": "string" + }, + "start": { + "type": "string" + }, + "eventTypeId": { + "type": "number" + }, + "eventTypeSlug": { + "type": "string" + }, + "rescheduleUid": { + "type": "string" + }, + "timeZone": { + "type": "string" + }, + "user": { + "type": "array", + "items": { + "type": "string" + } + }, + "language": { + "type": "string" + }, + "bookingUid": { + "type": "string" + }, + "metadata": { + "type": "object" + }, + "hasHashedBookingLink": { + "type": "boolean" + }, + "hashedLink": { + "type": "string", + "nullable": true + }, + "seatReferenceUid": { + "type": "string" + }, + "responses": { + "$ref": "#/components/schemas/Response" + }, + "orgSlug": { + "type": "string" + }, + "locationUrl": { + "type": "string" + } + }, + "required": [ + "start", + "eventTypeId", + "timeZone", + "language", + "metadata", + "hashedLink", + "responses" + ] + }, + "CancelBookingInput_2024_04_15": { + "type": "object", + "properties": { + "id": { + "type": "number" + }, + "uid": { + "type": "string" + }, + "allRemainingBookings": { + "type": "boolean" + }, + "cancellationReason": { + "type": "string" + }, + "seatReferenceUid": { + "type": "string" + } + }, + "required": [ + "id", + "uid", + "allRemainingBookings", + "cancellationReason", + "seatReferenceUid" + ] + }, + "MarkNoShowInput_2024_04_15": { + "type": "object", + "properties": { + "noShowHost": { + "type": "boolean" + }, + "attendees": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Attendee" + } + } + } + }, + "HandleMarkNoShowData_2024_04_15": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "noShowHost": { + "type": "boolean" + }, + "attendees": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Attendee" + } + } + }, + "required": [ + "message" + ] + }, + "MarkNoShowOutput_2024_04_15": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/HandleMarkNoShowData_2024_04_15" + } + }, + "required": [ + "status", + "data" + ] + }, + "CreateBookingOutput_2024_08_13": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "type": "object" + } + }, + "required": [ + "status", + "data" + ] + }, + "BookingOutput_2024_08_13": { + "type": "object", + "properties": {} + }, + "GetBookingOutput_2024_08_13": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/BookingOutput_2024_08_13" + } + }, + "required": [ + "status", + "data" + ] + }, + "GetBookingsOutput_2024_08_13": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "type": "array", + "items": { + "type": "object" + } + } + }, + "required": [ + "status", + "data" + ] + }, + "RescheduleBookingInput_2024_08_13": { + "type": "object", + "properties": {} + }, + "RescheduleBookingOutput_2024_08_13": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "type": "object" + } + }, + "required": [ + "status", + "data" + ] + }, + "CancelBookingInput_2024_08_13": { + "type": "object", + "properties": { + "cancellationReason": { + "type": "string" + } + }, + "required": [ + "cancellationReason" + ] + }, + "CancelBookingOutput_2024_08_13": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + } + }, + "required": [ + "status" + ] + }, + "MarkAbsentBookingInput_2024_08_13": { + "type": "object", + "properties": { + "host": { + "type": "boolean" + }, + "attendees": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "host", + "attendees" + ] + }, + "MarkAbsentBookingOutput_2024_08_13": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "type": "object" + } + }, + "required": [ + "status", + "data" + ] + }, + "ReserveSlotInput": { + "type": "object", + "properties": {} + }, + "UserWebhookOutputDto": { + "type": "object", + "properties": { + "payloadTemplate": { + "type": "string", + "description": "The template of the payload that will be sent to the subscriberUrl, check cal.com/docs/core-features/webhooks for more information", + "example": "{\"content\":\"A new event has been scheduled\",\"type\":\"{{type}}\",\"name\":\"{{title}}\",\"organizer\":\"{{organizer.name}}\",\"booker\":\"{{attendees.0.name}}\"}" + }, + "userId": { + "type": "number" + }, + "id": { + "type": "number" + }, + "triggers": { + "type": "array", + "items": { + "type": "object" + } + }, + "subscriberUrl": { + "type": "string" + }, + "active": { + "type": "boolean" + }, + "secret": { + "type": "string" + } + }, + "required": [ + "payloadTemplate", + "userId", + "id", + "triggers", + "subscriberUrl", + "active" + ] + }, + "UserWebhookOutputResponseDto": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/UserWebhookOutputDto" + } + }, + "required": [ + "status", + "data" + ] + }, + "UserWebhooksOutputResponseDto": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UserWebhookOutputDto" + } + } + }, + "required": [ + "status", + "data" + ] + }, + "EventTypeWebhookOutputDto": { + "type": "object", + "properties": { + "payloadTemplate": { + "type": "string", + "description": "The template of the payload that will be sent to the subscriberUrl, check cal.com/docs/core-features/webhooks for more information", + "example": "{\"content\":\"A new event has been scheduled\",\"type\":\"{{type}}\",\"name\":\"{{title}}\",\"organizer\":\"{{organizer.name}}\",\"booker\":\"{{attendees.0.name}}\"}" + }, + "eventTypeId": { + "type": "number" + }, + "id": { + "type": "number" + }, + "triggers": { + "type": "array", + "items": { + "type": "object" + } + }, + "subscriberUrl": { + "type": "string" + }, + "active": { + "type": "boolean" + }, + "secret": { + "type": "string" + } + }, + "required": [ + "payloadTemplate", + "eventTypeId", + "id", + "triggers", + "subscriberUrl", + "active" + ] + }, + "EventTypeWebhookOutputResponseDto": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/EventTypeWebhookOutputDto" + } + }, + "required": [ + "status", + "data" + ] + }, + "EventTypeWebhooksOutputResponseDto": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/EventTypeWebhookOutputDto" + } + } + }, + "required": [ + "status", + "data" + ] + }, + "DeleteManyWebhooksOutputResponseDto": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "type": "string" + } + }, + "required": [ + "status", + "data" + ] + }, + "OAuthClientWebhookOutputDto": { + "type": "object", + "properties": { + "payloadTemplate": { + "type": "string", + "description": "The template of the payload that will be sent to the subscriberUrl, check cal.com/docs/core-features/webhooks for more information", + "example": "{\"content\":\"A new event has been scheduled\",\"type\":\"{{type}}\",\"name\":\"{{title}}\",\"organizer\":\"{{organizer.name}}\",\"booker\":\"{{attendees.0.name}}\"}" + }, + "oAuthClientId": { + "type": "string" + }, + "id": { + "type": "number" + }, + "triggers": { + "type": "array", + "items": { + "type": "object" + } + }, + "subscriberUrl": { + "type": "string" + }, + "active": { + "type": "boolean" + }, + "secret": { + "type": "string" + } + }, + "required": [ + "payloadTemplate", + "oAuthClientId", + "id", + "triggers", + "subscriberUrl", + "active" + ] + }, + "OAuthClientWebhookOutputResponseDto": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/OAuthClientWebhookOutputDto" + } + }, + "required": [ + "status", + "data" + ] + }, + "OAuthClientWebhooksOutputResponseDto": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/OAuthClientWebhookOutputDto" + } + } + }, + "required": [ + "status", + "data" + ] + }, + "DestinationCalendarsInputBodyDto": { + "type": "object", + "properties": { + "integration": { + "type": "string", + "example": "apple_calendar", + "description": "The calendar service you want to integrate, as returned by the /calendars endpoint", + "enum": [ + "apple_calendar", + "google_calendar", + "office365_calendar" + ] + }, + "externalId": { + "type": "string", + "example": "https://caldav.icloud.com/26962146906/calendars/1644422A-1945-4438-BBC0-4F0Q23A57R7S/", + "description": "Unique identifier used to represent the specfic calendar, as returned by the /calendars endpoint" + } + }, + "required": [ + "integration", + "externalId" + ] + }, + "DestinationCalendarsOutputDto": { + "type": "object", + "properties": { + "userId": { + "type": "number" + }, + "integration": { + "type": "string" + }, + "externalId": { + "type": "string" + }, + "credentialId": { + "type": "number", + "nullable": true + } + }, + "required": [ + "userId", + "integration", + "externalId", + "credentialId" + ] + }, + "DestinationCalendarsOutputResponseDto": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/DestinationCalendarsOutputDto" + } + }, + "required": [ + "status", + "data" + ] + } + } + } +} \ No newline at end of file From f4712bc5940b5afb26d7485b3d33600ff18f4510 Mon Sep 17 00:00:00 2001 From: supalarry Date: Mon, 9 Sep 2024 15:00:06 +0200 Subject: [PATCH 54/96] retrigger build --- .../v2/src/ee/bookings/2024-08-13/services/bookings.service.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts index cf6418055d52f7..cd10fb9f794f0b 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts @@ -112,6 +112,7 @@ export class BookingsService_2024_08_13 { async createRegularBooking(request: Request, body: CreateBookingInput_2024_08_13) { const bookingRequest = await this.inputService.createBookingRequest(request, body); const booking = await handleNewBooking(bookingRequest); + if (!booking.id) { throw new Error("Booking was not created"); } From 27a50f77441d6c3a6bb3e97dfb457a1c79620e82 Mon Sep 17 00:00:00 2001 From: supalarry Date: Mon, 9 Sep 2024 15:41:36 +0200 Subject: [PATCH 55/96] fix: atom booker work with v2 --- .../ee/bookings/2024-04-15/controllers/bookings.controller.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/api/v2/src/ee/bookings/2024-04-15/controllers/bookings.controller.ts b/apps/api/v2/src/ee/bookings/2024-04-15/controllers/bookings.controller.ts index a7c8ea2f2440a8..01116308eb246b 100644 --- a/apps/api/v2/src/ee/bookings/2024-04-15/controllers/bookings.controller.ts +++ b/apps/api/v2/src/ee/bookings/2024-04-15/controllers/bookings.controller.ts @@ -5,7 +5,7 @@ import { GetBookingOutput_2024_04_15 } from "@/ee/bookings/2024-04-15/outputs/ge import { GetBookingsOutput_2024_04_15 } from "@/ee/bookings/2024-04-15/outputs/get-bookings.output"; import { MarkNoShowOutput_2024_04_15 } from "@/ee/bookings/2024-04-15/outputs/mark-no-show.output"; import { hashAPIKey, isApiKey, stripApiKey } from "@/lib/api-key"; -import { VERSION_2024_04_15_VALUE } from "@/lib/api-versions"; +import { VERSION_2024_04_15, VERSION_2024_06_11, VERSION_2024_06_14 } from "@/lib/api-versions"; import { ApiKeyRepository } from "@/modules/api-key/api-key-repository"; import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator"; import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator"; @@ -84,7 +84,7 @@ const DEFAULT_PLATFORM_PARAMS = { @Controller({ path: "/v2/bookings", - version: VERSION_2024_04_15_VALUE, + version: [VERSION_2024_04_15, VERSION_2024_06_11, VERSION_2024_06_14], }) @UseGuards(PermissionsGuard) @DocsTags("Bookings") From 8d25a22bc9a0766a94153c2879071611296b9b7a Mon Sep 17 00:00:00 2001 From: supalarry Date: Tue, 10 Sep 2024 14:59:28 +0200 Subject: [PATCH 56/96] docs: exclude old controller from docs --- .../2024-04-15/controllers/bookings.controller.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/apps/api/v2/src/ee/bookings/2024-04-15/controllers/bookings.controller.ts b/apps/api/v2/src/ee/bookings/2024-04-15/controllers/bookings.controller.ts index 01116308eb246b..76ef491b3516e9 100644 --- a/apps/api/v2/src/ee/bookings/2024-04-15/controllers/bookings.controller.ts +++ b/apps/api/v2/src/ee/bookings/2024-04-15/controllers/bookings.controller.ts @@ -31,7 +31,11 @@ import { UseGuards, } from "@nestjs/common"; import { ConfigService } from "@nestjs/config"; -import { ApiQuery, ApiTags as DocsTags } from "@nestjs/swagger"; +import { + ApiQuery, + ApiTags as DocsTags, + ApiExcludeController as DocsExcludeController, +} from "@nestjs/swagger"; import { User } from "@prisma/client"; import { Request } from "express"; import { NextApiRequest } from "next/types"; @@ -87,7 +91,7 @@ const DEFAULT_PLATFORM_PARAMS = { version: [VERSION_2024_04_15, VERSION_2024_06_11, VERSION_2024_06_14], }) @UseGuards(PermissionsGuard) -@DocsTags("Bookings") +@DocsExcludeController(true) export class BookingsController_2024_04_15 { private readonly logger = new Logger("BookingsController"); From 549b6b02d782e1f5f000b35b0b3d2231496b7051 Mon Sep 17 00:00:00 2001 From: supalarry Date: Tue, 10 Sep 2024 15:40:50 +0200 Subject: [PATCH 57/96] refactor: make eventTypeIds and teamIds getBookings query params comma separated string --- .../bookings.controller.e2e-spec.ts | 4 +- .../2024-08-13/inputs/get-bookings.input.ts | 38 +++++++++++++++++-- 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.e2e-spec.ts b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.e2e-spec.ts index b56ef89f6b2b6e..ab60e5e080053f 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.e2e-spec.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.e2e-spec.ts @@ -603,7 +603,7 @@ describe("Bookings Endpoints 2024-08-13", () => { it("should should get bookings by eventTypeIds", async () => { return request(app.getHttpServer()) - .get(`/v2/bookings?eventTypeIds[]=${eventTypeId}&eventTypeIds[]=${recurringEventTypeId}`) + .get(`/v2/bookings?eventTypeIds=${eventTypeId},${recurringEventTypeId}`) .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) .expect(200) .then(async (response) => { @@ -1395,7 +1395,7 @@ describe("Bookings Endpoints 2024-08-13", () => { it("should should get bookings by teamIds", async () => { return request(app.getHttpServer()) - .get(`/v2/bookings?teamIds[]=${team1.id}&teamIds[]=${team2.id}`) + .get(`/v2/bookings?teamIds=${team1.id},${team2.id}`) .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) .expect(200) .then(async (response) => { diff --git a/packages/platform/types/bookings/2024-08-13/inputs/get-bookings.input.ts b/packages/platform/types/bookings/2024-08-13/inputs/get-bookings.input.ts index 8835464db75ae7..e9091bc325b4c6 100644 --- a/packages/platform/types/bookings/2024-08-13/inputs/get-bookings.input.ts +++ b/packages/platform/types/bookings/2024-08-13/inputs/get-bookings.input.ts @@ -43,19 +43,43 @@ export class GetBookingsInput_2024_08_13 { each: true, message: "Invalid status. Allowed are upcoming, recurring, past, cancelled, unconfirmed", }) + @ApiProperty({ + required: false, + description: + "Filter bookings by status. If you want to filter by multiple statuses, separate them with a comma.", + example: "?status=upcoming,past", + enum: Status, + isArray: true, + }) status?: StatusType[]; @IsString() @IsOptional() + @ApiProperty({ + required: false, + description: "Filter bookings by the attendee's email address.", + example: "example@domain.com", + }) attendeeEmail?: string; @IsString() @IsOptional() + @ApiProperty({ + required: false, + description: "Filter bookings by the attendee's name.", + example: "John Doe", + }) attendeeName?: string; - @IsArray() - @Type(() => Number) @IsOptional() + @Transform(({ value }) => { + if (typeof value === "string") { + return value.split(",").map((eventTypeId: string) => parseInt(eventTypeId)); + } + return value; + }) + @IsArray() + @IsNumber({}, { each: true }) @ArrayMinSize(1, { message: "eventTypeIds must contain at least 1 event type id" }) eventTypeIds?: number[]; @@ -64,9 +88,15 @@ export class GetBookingsInput_2024_08_13 { @Type(() => Number) eventTypeId?: number; - @IsArray() - @Type(() => Number) @IsOptional() + @Transform(({ value }) => { + if (typeof value === "string") { + return value.split(",").map((teamId: string) => parseInt(teamId)); + } + return value; + }) + @IsArray() + @IsNumber({}, { each: true }) @ArrayMinSize(1, { message: "teamIds must contain at least 1 team id" }) teamsIds?: number[]; From 36f2920801e070a3cc72e172707f91e8478e2e59 Mon Sep 17 00:00:00 2001 From: supalarry Date: Tue, 10 Sep 2024 16:04:26 +0200 Subject: [PATCH 58/96] docs: swagger for get bookings query --- .../2024-08-13/inputs/get-bookings.input.ts | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/packages/platform/types/bookings/2024-08-13/inputs/get-bookings.input.ts b/packages/platform/types/bookings/2024-08-13/inputs/get-bookings.input.ts index e9091bc325b4c6..1a7307cf89f454 100644 --- a/packages/platform/types/bookings/2024-08-13/inputs/get-bookings.input.ts +++ b/packages/platform/types/bookings/2024-08-13/inputs/get-bookings.input.ts @@ -56,6 +56,7 @@ export class GetBookingsInput_2024_08_13 { @IsString() @IsOptional() @ApiProperty({ + type: String, required: false, description: "Filter bookings by the attendee's email address.", example: "example@domain.com", @@ -65,6 +66,7 @@ export class GetBookingsInput_2024_08_13 { @IsString() @IsOptional() @ApiProperty({ + type: String, required: false, description: "Filter bookings by the attendee's name.", example: "John Doe", @@ -81,11 +83,24 @@ export class GetBookingsInput_2024_08_13 { @IsArray() @IsNumber({}, { each: true }) @ArrayMinSize(1, { message: "eventTypeIds must contain at least 1 event type id" }) + @ApiProperty({ + type: String, + required: false, + description: + "Filter bookings by event type ids belonging to the user. Event type ids must be separated by a comma.", + example: "?eventTypeIds=100,200", + }) eventTypeIds?: number[]; @IsInt() @IsOptional() @Type(() => Number) + @ApiProperty({ + type: String, + required: false, + description: "Filter bookings by event type id belonging to the user.", + example: "?eventTypeId=100", + }) eventTypeId?: number; @IsOptional() @@ -98,19 +113,43 @@ export class GetBookingsInput_2024_08_13 { @IsArray() @IsNumber({}, { each: true }) @ArrayMinSize(1, { message: "teamIds must contain at least 1 team id" }) + @ApiProperty({ + type: String, + required: false, + description: "Filter bookings by team ids that user is part of. Team ids must be separated by a comma.", + example: "?teamIds=50,60", + }) teamsIds?: number[]; @IsInt() @IsOptional() @Type(() => Number) + @ApiProperty({ + type: String, + required: false, + description: "Filter bookings by team id that user is part of", + example: "?teamId=50", + }) teamId?: number; @IsOptional() @IsISO8601({ strict: true }, { message: "fromDate must be a valid ISO 8601 date." }) + @ApiProperty({ + type: String, + required: false, + description: "Filter bookings with start after this date string.", + example: "?afterStart=2025-03-07T10:00:00.000Z", + }) afterStart?: string; @IsOptional() @IsISO8601({ strict: true }, { message: "toDate must be a valid ISO 8601 date." }) + @ApiProperty({ + type: String, + required: false, + description: "Filter bookings with end before this date string.", + example: "?beforeEnd=2025-03-07T11:00:00.000Z", + }) beforeEnd?: string; // note(Lauris): sort @@ -118,18 +157,37 @@ export class GetBookingsInput_2024_08_13 { @IsEnum(SortOrder, { message: 'SortStart must be either "asc" or "desc".', }) + @ApiProperty({ + required: false, + description: "Sort results by their start time in ascending or descending order.", + example: "?sortStart=asc OR ?sortStart=desc", + enum: SortOrder, + }) sortStart?: SortOrderType; @IsOptional() @IsEnum(SortOrder, { message: 'SortEnd must be either "asc" or "desc".', }) + @ApiProperty({ + required: false, + description: "Sort results by their end time in ascending or descending order.", + example: "?sortEnd=asc OR ?sortEnd=desc", + enum: SortOrder, + }) sortEnd?: SortOrderType; @IsOptional() @IsEnum(SortOrder, { message: 'SortCreated must be either "asc" or "desc".', }) + @ApiProperty({ + required: false, + description: + "Sort results by their creation time (when booking was made) in ascending or descending order.", + example: "?sortEnd=asc OR ?sortEnd=desc", + enum: SortOrder, + }) sortCreated?: SortOrderType; // note(Lauris): pagination From 49729e51e35b0ef89c90f0dbe6af7353986babe0 Mon Sep 17 00:00:00 2001 From: supalarry Date: Tue, 10 Sep 2024 17:10:29 +0200 Subject: [PATCH 59/96] swagger docs --- .../bookings.controller.e2e-spec.ts | 2 +- .../controllers/bookings.controller.ts | 9 +- .../outputs/cancel-booking.output copy.ts | 14 - .../outputs/cancel-booking.output.ts | 26 + .../outputs/create-booking.output.ts | 13 +- .../2024-08-13/outputs/get-booking.output.ts | 15 +- .../2024-08-13/outputs/get-bookings.output.ts | 14 +- .../2024-08-13/outputs/mark-absent.output.ts | 13 +- .../outputs/reschedule-booking.output.ts | 13 +- apps/api/v2/swagger/documentation.json | 1186 +++++++---------- .../2024-08-13/outputs/booking.output.ts | 41 +- 11 files changed, 589 insertions(+), 757 deletions(-) delete mode 100644 apps/api/v2/src/ee/bookings/2024-08-13/outputs/cancel-booking.output copy.ts create mode 100644 apps/api/v2/src/ee/bookings/2024-08-13/outputs/cancel-booking.output.ts diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.e2e-spec.ts b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.e2e-spec.ts index ab60e5e080053f..67e5bbdcb583f4 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.e2e-spec.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.e2e-spec.ts @@ -1,6 +1,6 @@ import { bootstrap } from "@/app"; import { AppModule } from "@/app.module"; -import { CancelBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/cancel-booking.output copy"; +import { CancelBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/cancel-booking.output"; import { CreateBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/create-booking.output"; import { GetBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/get-booking.output"; import { GetBookingsOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/get-bookings.output"; diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.ts b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.ts index 3d58d9393787fa..53fd012715a1b8 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.ts @@ -1,5 +1,5 @@ import { BookingUidGuard } from "@/ee/bookings/2024-08-13/guards/booking-uid.guard"; -import { CancelBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/cancel-booking.output copy"; +import { CancelBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/cancel-booking.output"; import { CreateBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/create-booking.output"; import { GetBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/get-booking.output"; import { GetBookingsOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/get-bookings.output"; @@ -24,7 +24,7 @@ import { HttpCode, HttpStatus, } from "@nestjs/common"; -import { ApiTags as DocsTags } from "@nestjs/swagger"; +import { ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger"; import { User } from "@prisma/client"; import { Request } from "express"; @@ -71,6 +71,11 @@ export class BookingsController_2024_08_13 { @Get("/:bookingUid") @UseGuards(BookingUidGuard) + @ApiOperation({ + summary: "Get booking", + description: + ":bookingUid can be uid of a normal booking, one of the recurring booking recurrences, or uid of recurring booking which will return an array of all recurring booking recurrences.", + }) async getBooking(@Param("bookingUid") bookingUid: string): Promise { const booking = await this.bookingsService.getBooking(bookingUid); diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/outputs/cancel-booking.output copy.ts b/apps/api/v2/src/ee/bookings/2024-08-13/outputs/cancel-booking.output copy.ts deleted file mode 100644 index 7ff7bab4332315..00000000000000 --- a/apps/api/v2/src/ee/bookings/2024-08-13/outputs/cancel-booking.output copy.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { ApiProperty } from "@nestjs/swagger"; -import { IsEnum, ValidateNested } from "class-validator"; - -import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants"; -import { BookingOutput_2024_08_13, RecurringBookingOutput_2024_08_13 } from "@calcom/platform-types"; - -export class CancelBookingOutput_2024_08_13 { - @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) - @IsEnum([SUCCESS_STATUS, ERROR_STATUS]) - status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS; - - @ValidateNested() - data!: BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13[]; -} diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/outputs/cancel-booking.output.ts b/apps/api/v2/src/ee/bookings/2024-08-13/outputs/cancel-booking.output.ts new file mode 100644 index 00000000000000..f463f698611f18 --- /dev/null +++ b/apps/api/v2/src/ee/bookings/2024-08-13/outputs/cancel-booking.output.ts @@ -0,0 +1,26 @@ +import { ApiProperty, ApiExtraModels, getSchemaPath } from "@nestjs/swagger"; +import { Type } from "class-transformer"; +import { IsEnum, ValidateNested } from "class-validator"; + +import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants"; +import { BookingOutput_2024_08_13, RecurringBookingOutput_2024_08_13 } from "@calcom/platform-types"; + +@ApiExtraModels(BookingOutput_2024_08_13, RecurringBookingOutput_2024_08_13) +export class CancelBookingOutput_2024_08_13 { + @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) + @IsEnum([SUCCESS_STATUS, ERROR_STATUS]) + status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS; + + @ValidateNested() + @ApiProperty({ + oneOf: [ + { $ref: getSchemaPath(BookingOutput_2024_08_13) }, + { $ref: getSchemaPath(RecurringBookingOutput_2024_08_13) }, + { type: "array", items: { $ref: getSchemaPath(RecurringBookingOutput_2024_08_13) } }, + ], + description: + "Booking data, which can be either a BookingOutput object, a RecurringBookingOutput object, or an array of RecurringBookingOutput objects", + }) + @Type(() => Object) + data!: BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13[]; +} diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/outputs/create-booking.output.ts b/apps/api/v2/src/ee/bookings/2024-08-13/outputs/create-booking.output.ts index 2edaed89cc39a5..deab5e452ac518 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/outputs/create-booking.output.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/outputs/create-booking.output.ts @@ -1,14 +1,25 @@ -import { ApiProperty } from "@nestjs/swagger"; +import { ApiProperty, ApiExtraModels, getSchemaPath } from "@nestjs/swagger"; +import { Type } from "class-transformer"; import { IsEnum, ValidateNested } from "class-validator"; import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants"; import { BookingOutput_2024_08_13, RecurringBookingOutput_2024_08_13 } from "@calcom/platform-types"; +@ApiExtraModels(BookingOutput_2024_08_13, RecurringBookingOutput_2024_08_13) export class CreateBookingOutput_2024_08_13 { @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) @IsEnum([SUCCESS_STATUS, ERROR_STATUS]) status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS; @ValidateNested() + @ApiProperty({ + oneOf: [ + { $ref: getSchemaPath(BookingOutput_2024_08_13) }, + { type: "array", items: { $ref: getSchemaPath(RecurringBookingOutput_2024_08_13) } }, + ], + description: + "Booking data, which can be either a BookingOutput object or an array of RecurringBookingOutput objects", + }) + @Type(() => Object) data!: BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13[]; } diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/outputs/get-booking.output.ts b/apps/api/v2/src/ee/bookings/2024-08-13/outputs/get-booking.output.ts index 223fb726adf25c..aa4745785e7c2d 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/outputs/get-booking.output.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/outputs/get-booking.output.ts @@ -1,19 +1,26 @@ -import { ApiProperty } from "@nestjs/swagger"; +import { ApiProperty, ApiExtraModels, getSchemaPath } from "@nestjs/swagger"; import { Type } from "class-transformer"; import { IsEnum, ValidateNested } from "class-validator"; import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants"; import { BookingOutput_2024_08_13, RecurringBookingOutput_2024_08_13 } from "@calcom/platform-types"; +@ApiExtraModels(BookingOutput_2024_08_13, RecurringBookingOutput_2024_08_13) export class GetBookingOutput_2024_08_13 { @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) @IsEnum([SUCCESS_STATUS, ERROR_STATUS]) status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS; + @ValidateNested() @ApiProperty({ - type: BookingOutput_2024_08_13, + oneOf: [ + { $ref: getSchemaPath(BookingOutput_2024_08_13) }, + { $ref: getSchemaPath(RecurringBookingOutput_2024_08_13) }, + { type: "array", items: { $ref: getSchemaPath(RecurringBookingOutput_2024_08_13) } }, + ], + description: + "Booking data, which can be either a BookingOutput object, a RecurringBookingOutput object, or an array of RecurringBookingOutput objects", }) - @ValidateNested() - @Type(() => BookingOutput_2024_08_13) + @Type(() => Object) data!: BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13[]; } diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/outputs/get-bookings.output.ts b/apps/api/v2/src/ee/bookings/2024-08-13/outputs/get-bookings.output.ts index 7d5553e85a1cb5..3f3c0623a2e392 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/outputs/get-bookings.output.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/outputs/get-bookings.output.ts @@ -1,14 +1,26 @@ -import { ApiProperty } from "@nestjs/swagger"; +import { ApiProperty, ApiExtraModels, getSchemaPath } from "@nestjs/swagger"; import { IsEnum, ValidateNested } from "class-validator"; import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants"; import { BookingOutput_2024_08_13, RecurringBookingOutput_2024_08_13 } from "@calcom/platform-types"; +@ApiExtraModels(BookingOutput_2024_08_13, RecurringBookingOutput_2024_08_13) export class GetBookingsOutput_2024_08_13 { @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) @IsEnum([SUCCESS_STATUS, ERROR_STATUS]) status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS; + @ApiProperty({ + type: "array", + items: { + oneOf: [ + { $ref: getSchemaPath(BookingOutput_2024_08_13) }, + { $ref: getSchemaPath(RecurringBookingOutput_2024_08_13) }, + ], + }, + description: + "Array of booking data, which can contain either BookingOutput objects or RecurringBookingOutput objects", + }) @ValidateNested({ each: true }) data!: (BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13)[]; } diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/outputs/mark-absent.output.ts b/apps/api/v2/src/ee/bookings/2024-08-13/outputs/mark-absent.output.ts index dad0ed80d8ec1a..16dcaae6a1a7e4 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/outputs/mark-absent.output.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/outputs/mark-absent.output.ts @@ -1,14 +1,25 @@ -import { ApiProperty } from "@nestjs/swagger"; +import { ApiProperty, ApiExtraModels, getSchemaPath } from "@nestjs/swagger"; +import { Type } from "class-transformer"; import { IsEnum, ValidateNested } from "class-validator"; import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants"; import { BookingOutput_2024_08_13, RecurringBookingOutput_2024_08_13 } from "@calcom/platform-types"; +@ApiExtraModels(BookingOutput_2024_08_13, RecurringBookingOutput_2024_08_13) export class MarkAbsentBookingOutput_2024_08_13 { @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) @IsEnum([SUCCESS_STATUS, ERROR_STATUS]) status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS; + @ApiProperty({ + oneOf: [ + { $ref: getSchemaPath(BookingOutput_2024_08_13) }, + { $ref: getSchemaPath(RecurringBookingOutput_2024_08_13) }, + ], + description: + "Booking data, which can be either a BookingOutput object or a RecurringBookingOutput object", + }) @ValidateNested() + @Type(() => Object) data!: BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13; } diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/outputs/reschedule-booking.output.ts b/apps/api/v2/src/ee/bookings/2024-08-13/outputs/reschedule-booking.output.ts index 7585792469f356..2c45b9dd3ca5e7 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/outputs/reschedule-booking.output.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/outputs/reschedule-booking.output.ts @@ -1,14 +1,25 @@ -import { ApiProperty } from "@nestjs/swagger"; +import { ApiProperty, ApiExtraModels, getSchemaPath } from "@nestjs/swagger"; +import { Type } from "class-transformer"; import { IsEnum, ValidateNested } from "class-validator"; import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants"; import { BookingOutput_2024_08_13, RecurringBookingOutput_2024_08_13 } from "@calcom/platform-types"; +@ApiExtraModels(BookingOutput_2024_08_13, RecurringBookingOutput_2024_08_13) export class RescheduleBookingOutput_2024_08_13 { @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) @IsEnum([SUCCESS_STATUS, ERROR_STATUS]) status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS; + @ApiProperty({ + oneOf: [ + { $ref: getSchemaPath(BookingOutput_2024_08_13) }, + { $ref: getSchemaPath(RecurringBookingOutput_2024_08_13) }, + ], + description: + "Booking data, which can be either a BookingOutput object or a RecurringBookingOutput object", + }) @ValidateNested() + @Type(() => Object) data!: BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13; } diff --git a/apps/api/v2/swagger/documentation.json b/apps/api/v2/swagger/documentation.json index 28d9bd26efff69..da177a0613c4fb 100644 --- a/apps/api/v2/swagger/documentation.json +++ b/apps/api/v2/swagger/documentation.json @@ -3324,7 +3324,31 @@ "/v2/calendars/busy-times": { "get": { "operationId": "CalendarsController_getBusyTimes", - "parameters": [], + "parameters": [ + { + "name": "loggedInUsersTz", + "required": true, + "in": "query", + "description": "The timezone of the logged in user represented as a string", + "example": "America/New_York", + "schema": { + "type": "string" + } + }, + { + "name": "calendarsToLoad", + "required": true, + "in": "query", + "description": "An array of Calendar objects representing the calendars to be loaded", + "example": "[{ credentialId: \"1\", externalId: \"AQgtJE7RnHEeyisVq2ENs2gAAAgEGAAAACgtJE7RnHEeyisVq2ENs2gAAAhSDAAAA\" }, { credentialId: \"2\", externalId: \"AQM7RnHEeyisVq2ENs2gAAAhFDBBBBB\" }]", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + ], "responses": { "200": { "description": "", @@ -3534,9 +3558,170 @@ } }, "/v2/bookings": { + "post": { + "operationId": "BookingsController_2024_08_13_createBooking", + "parameters": [], + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateBookingOutput_2024_08_13" + } + } + } + } + }, + "tags": [ + "Bookings" + ] + }, "get": { "operationId": "BookingsController_2024_08_13_getBookings", "parameters": [ + { + "name": "status", + "required": false, + "in": "query", + "description": "Filter bookings by status. If you want to filter by multiple statuses, separate them with a comma.", + "example": "?status=upcoming,past", + "schema": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "upcoming", + "recurring", + "past", + "cancelled", + "unconfirmed" + ] + } + } + }, + { + "name": "attendeeEmail", + "required": false, + "in": "query", + "description": "Filter bookings by the attendee's email address.", + "example": "example@domain.com", + "schema": { + "type": "string" + } + }, + { + "name": "attendeeName", + "required": false, + "in": "query", + "description": "Filter bookings by the attendee's name.", + "example": "John Doe", + "schema": { + "type": "string" + } + }, + { + "name": "eventTypeIds", + "required": false, + "in": "query", + "description": "Filter bookings by event type ids belonging to the user. Event type ids must be separated by a comma.", + "example": "?eventTypeIds=100,200", + "schema": { + "type": "string" + } + }, + { + "name": "eventTypeId", + "required": false, + "in": "query", + "description": "Filter bookings by event type id belonging to the user.", + "example": "?eventTypeId=100", + "schema": { + "type": "string" + } + }, + { + "name": "teamsIds", + "required": false, + "in": "query", + "description": "Filter bookings by team ids that user is part of. Team ids must be separated by a comma.", + "example": "?teamIds=50,60", + "schema": { + "type": "string" + } + }, + { + "name": "teamId", + "required": false, + "in": "query", + "description": "Filter bookings by team id that user is part of", + "example": "?teamId=50", + "schema": { + "type": "string" + } + }, + { + "name": "afterStart", + "required": false, + "in": "query", + "description": "Filter bookings with start after this date string.", + "example": "?afterStart=2025-03-07T10:00:00.000Z", + "schema": { + "type": "string" + } + }, + { + "name": "beforeEnd", + "required": false, + "in": "query", + "description": "Filter bookings with end before this date string.", + "example": "?beforeEnd=2025-03-07T11:00:00.000Z", + "schema": { + "type": "string" + } + }, + { + "name": "sortStart", + "required": false, + "in": "query", + "description": "Sort results by their start time in ascending or descending order.", + "example": "?sortStart=asc OR ?sortStart=desc", + "schema": { + "enum": [ + "asc", + "desc" + ], + "type": "string" + } + }, + { + "name": "sortEnd", + "required": false, + "in": "query", + "description": "Sort results by their end time in ascending or descending order.", + "example": "?sortEnd=asc OR ?sortEnd=desc", + "schema": { + "enum": [ + "asc", + "desc" + ], + "type": "string" + } + }, + { + "name": "sortCreated", + "required": false, + "in": "query", + "description": "Sort results by their creation time (when booking was made) in ascending or descending order.", + "example": "?sortEnd=asc OR ?sortEnd=desc", + "schema": { + "enum": [ + "asc", + "desc" + ], + "type": "string" + } + }, { "name": "take", "required": false, @@ -3573,30 +3758,13 @@ "tags": [ "Bookings" ] - }, - "post": { - "operationId": "BookingsController_2024_08_13_createBooking", - "parameters": [], - "responses": { - "201": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateBookingOutput_2024_08_13" - } - } - } - } - }, - "tags": [ - "Bookings" - ] } }, "/v2/bookings/{bookingUid}": { "get": { "operationId": "BookingsController_2024_08_13_getBooking", + "summary": "Get booking", + "description": ":bookingUid can be uid of a normal booking, one of the recurring booking recurrences, or uid of recurring booking which will return an array of all recurring booking recurrences.", "parameters": [ { "name": "bookingUid", @@ -3625,34 +3793,6 @@ } }, "/v2/bookings/{bookingUid}/reschedule": { - "get": { - "operationId": "BookingsController_2024_04_15_getBookingForReschedule", - "parameters": [ - { - "name": "bookingUid", - "required": true, - "in": "path", - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "type": "object" - } - } - } - } - }, - "tags": [ - "Bookings" - ] - }, "post": { "operationId": "BookingsController_2024_08_13_rescheduleBooking", "parameters": [ @@ -3692,25 +3832,17 @@ ] } }, - "/v2/bookings/{bookingId}/cancel": { + "/v2/bookings/{bookingUid}/cancel": { "post": { - "operationId": "BookingsController_2024_04_15_cancelBooking", + "operationId": "BookingsController_2024_08_13_cancelBooking", "parameters": [ { - "name": "bookingId", + "name": "bookingUid", "required": true, "in": "path", "schema": { "type": "string" } - }, - { - "name": "x-cal-client-id", - "required": true, - "in": "header", - "schema": { - "type": "string" - } } ], "requestBody": { @@ -3718,18 +3850,18 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CancelBookingInput_2024_04_15" + "$ref": "#/components/schemas/CancelBookingInput_2024_08_13" } } } }, "responses": { - "201": { + "200": { "description": "", "content": { "application/json": { "schema": { - "type": "object" + "$ref": "#/components/schemas/CancelBookingOutput_2024_08_13" } } } @@ -3740,9 +3872,9 @@ ] } }, - "/v2/bookings/{bookingUid}/mark-no-show": { + "/v2/bookings/{bookingUid}/mark-absent": { "post": { - "operationId": "BookingsController_2024_04_15_markNoShow", + "operationId": "BookingsController_2024_08_13_markNoShow", "parameters": [ { "name": "bookingUid", @@ -3758,18 +3890,18 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/MarkNoShowInput_2024_04_15" + "$ref": "#/components/schemas/MarkAbsentBookingInput_2024_08_13" } } } }, "responses": { - "201": { + "200": { "description": "", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/MarkNoShowOutput_2024_04_15" + "$ref": "#/components/schemas/MarkAbsentBookingOutput_2024_08_13" } } } @@ -3780,28 +3912,16 @@ ] } }, - "/v2/bookings/recurring": { + "/v2/slots/reserve": { "post": { - "operationId": "BookingsController_2024_04_15_createRecurringBooking", - "parameters": [ - { - "name": "x-cal-client-id", - "required": true, - "in": "header", - "schema": { - "type": "string" - } - } - ], + "operationId": "SlotsController_reserveSlot", + "parameters": [], "requestBody": { "required": true, "content": { "application/json": { "schema": { - "type": "array", - "items": { - "type": "string" - } + "$ref": "#/components/schemas/ReserveSlotInput" } } } @@ -3819,35 +3939,16 @@ } }, "tags": [ - "Bookings" + "Slots" ] } }, - "/v2/bookings/instant": { - "post": { - "operationId": "BookingsController_2024_04_15_createInstantBooking", - "parameters": [ - { - "name": "x-cal-client-id", - "required": true, - "in": "header", - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateBookingInput_2024_04_15" - } - } - } - }, + "/v2/slots/selected-slot": { + "delete": { + "operationId": "SlotsController_deleteSelectedSlot", + "parameters": [], "responses": { - "201": { + "200": { "description": "", "content": { "application/json": { @@ -3859,139 +3960,7 @@ } }, "tags": [ - "Bookings" - ] - } - }, - "/v2/bookings/{bookingUid}/cancel": { - "post": { - "operationId": "BookingsController_2024_08_13_cancelBooking", - "parameters": [ - { - "name": "bookingUid", - "required": true, - "in": "path", - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CancelBookingInput_2024_08_13" - } - } - } - }, - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CancelBookingOutput_2024_08_13" - } - } - } - } - }, - "tags": [ - "Bookings" - ] - } - }, - "/v2/bookings/{bookingUid}/mark-absent": { - "post": { - "operationId": "BookingsController_2024_08_13_markNoShow", - "parameters": [ - { - "name": "bookingUid", - "required": true, - "in": "path", - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MarkAbsentBookingInput_2024_08_13" - } - } - } - }, - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MarkAbsentBookingOutput_2024_08_13" - } - } - } - } - }, - "tags": [ - "Bookings" - ] - } - }, - "/v2/slots/reserve": { - "post": { - "operationId": "SlotsController_reserveSlot", - "parameters": [], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ReserveSlotInput" - } - } - } - }, - "responses": { - "201": { - "description": "", - "content": { - "application/json": { - "schema": { - "type": "object" - } - } - } - } - }, - "tags": [ - "Slots" - ] - } - }, - "/v2/slots/selected-slot": { - "delete": { - "operationId": "SlotsController_deleteSelectedSlot", - "parameters": [], - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "type": "object" - } - } - } - } - }, - "tags": [ - "Slots" + "Slots" ] } }, @@ -9933,160 +9902,153 @@ "data" ] }, - "Attendee": { + "Host": { "type": "object", "properties": { "id": { - "type": "number" - }, - "email": { - "type": "string" + "type": "number", + "example": 1 }, "name": { - "type": "string" + "type": "string", + "example": "Jane Doe" }, "timeZone": { - "type": "string" - }, - "locale": { "type": "string", - "nullable": true - }, - "bookingId": { - "type": "number", - "nullable": true + "example": "America/Los_Angeles" } }, "required": [ "id", - "email", "name", - "timeZone", - "locale", - "bookingId" + "timeZone" ] }, - "EventType": { + "Attendee": { "type": "object", "properties": { - "slug": { - "type": "string" - }, - "id": { - "type": "number" - }, - "eventName": { + "name": { "type": "string", - "nullable": true - }, - "price": { - "type": "number" - }, - "recurringEvent": { - "type": "object" - }, - "currency": { - "type": "string" - }, - "metadata": { - "type": "object" + "example": "John Doe" }, - "seatsShowAttendees": { - "type": "object" + "timeZone": { + "type": "string", + "example": "America/New_York" }, - "seatsShowAvailabilityCount": { - "type": "object" + "language": { + "type": "string", + "enum": [ + "ar", + "ca", + "de", + "es", + "eu", + "he", + "id", + "ja", + "lv", + "pl", + "ro", + "sr", + "th", + "vi", + "az", + "cs", + "el", + "es-419", + "fi", + "hr", + "it", + "km", + "nl", + "pt", + "ru", + "sv", + "tr", + "zh-CN", + "bg", + "da", + "en", + "et", + "fr", + "hu", + "iw", + "ko", + "no", + "pt-BR", + "sk", + "ta", + "uk", + "zh-TW" + ], + "example": "en" }, - "team": { - "type": "object", - "nullable": true + "absent": { + "type": "boolean", + "example": false } }, "required": [ - "price", - "currency", - "metadata" + "name", + "timeZone", + "absent" ] }, - "Reference": { + "BookingOutput_2024_08_13": { "type": "object", "properties": { "id": { - "type": "number" - }, - "type": { - "type": "string" + "type": "number", + "example": 123 }, "uid": { - "type": "string" + "type": "string", + "example": "booking_uid_123" + }, + "hosts": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Host" + } }, - "meetingId": { + "status": { "type": "string", - "nullable": true + "enum": [ + "cancelled", + "accepted", + "rejected", + "pending", + "rescheduled" + ], + "example": "accepted" }, - "thirdPartyRecurringEventId": { + "cancellationReason": { "type": "string", - "nullable": true + "example": "User requested cancellation" }, - "meetingPassword": { + "reschedulingReason": { "type": "string", - "nullable": true + "example": "User rescheduled the event" }, - "meetingUrl": { + "rescheduledFromUid": { "type": "string", - "nullable": true + "example": "previous_uid_123" }, - "bookingId": { - "type": "number", - "nullable": true + "start": { + "type": "string", + "example": "2024-08-13T15:30:00Z" }, - "externalCalendarId": { + "end": { "type": "string", - "nullable": true + "example": "2024-08-13T16:30:00Z" }, - "deleted": { - "type": "object" - }, - "credentialId": { + "duration": { "type": "number", - "nullable": true - } - }, - "required": [ - "id", - "type", - "uid", - "meetingPassword", - "bookingId", - "externalCalendarId", - "credentialId" - ] - }, - "GetBookingsDataEntry": { - "type": "object", - "properties": { - "id": { - "type": "number" - }, - "title": { - "type": "string" - }, - "userPrimaryEmail": { - "type": "string", - "nullable": true - }, - "description": { - "type": "string", - "nullable": true - }, - "customInputs": { - "type": "object" - }, - "startTime": { - "type": "string" + "example": 60 }, - "endTime": { - "type": "string" + "eventTypeId": { + "type": "number", + "example": 45 }, "attendees": { "type": "array", @@ -10094,197 +10056,96 @@ "$ref": "#/components/schemas/Attendee" } }, - "metadata": { - "type": "object" - }, - "uid": { - "type": "string" - }, - "recurringEventId": { - "type": "string", - "nullable": true - }, - "location": { - "type": "string", - "nullable": true - }, - "eventType": { - "$ref": "#/components/schemas/EventType" - }, - "status": { - "type": "object" - }, - "paid": { - "type": "boolean" - }, - "payment": { - "type": "array", - "items": { - "type": "object" - } - }, - "references": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Reference" - } - }, - "isRecorded": { - "type": "boolean" - }, - "seatsReferences": { + "guests": { + "example": [ + "guest1@example.com", + "guest2@example.com" + ], "type": "array", "items": { - "type": "object" + "type": "string" } }, - "user": { - "nullable": true, - "allOf": [ - { - "$ref": "#/components/schemas/User" - } - ] + "meetingUrl": { + "type": "string", + "example": "https://example.com/meeting" }, - "rescheduled": { - "type": "object" + "absentHost": { + "type": "boolean", + "example": true } }, "required": [ "id", - "title", - "description", - "customInputs", - "startTime", - "endTime", - "attendees", - "metadata", "uid", - "recurringEventId", - "location", - "eventType", + "hosts", "status", - "paid", - "payment", - "references", - "isRecorded", - "seatsReferences", - "user" + "start", + "end", + "duration", + "eventTypeId", + "attendees", + "absentHost" ] }, - "GetBookingsData_2024_04_15": { + "RecurringBookingOutput_2024_08_13": { "type": "object", "properties": { - "bookings": { - "type": "array", - "items": { - "$ref": "#/components/schemas/GetBookingsDataEntry" - } + "id": { + "type": "number", + "example": 456 }, - "recurringInfo": { + "uid": { + "type": "string", + "example": "recurring_uid_123" + }, + "hosts": { "type": "array", "items": { - "type": "object" + "$ref": "#/components/schemas/Host" } }, - "nextCursor": { - "type": "number", - "nullable": true - } - }, - "required": [ - "bookings", - "recurringInfo", - "nextCursor" - ] - }, - "GetBookingsOutput_2024_04_15": { - "type": "object", - "properties": { "status": { "type": "string", - "example": "success", "enum": [ - "success", - "error" - ] - }, - "data": { - "$ref": "#/components/schemas/GetBookingsData_2024_04_15" - } - }, - "required": [ - "status", - "data" - ] - }, - "GetBookingData_2024_04_15": { - "type": "object", - "properties": { - "title": { - "type": "string" - }, - "id": { - "type": "number" - }, - "uid": { - "type": "string" + "cancelled", + "accepted", + "rejected", + "pending" + ], + "example": "pending" }, - "description": { + "cancellationReason": { "type": "string", - "nullable": true - }, - "customInputs": { - "type": "object" + "example": "Event was cancelled" }, - "smsReminderNumber": { + "reschedulingReason": { "type": "string", - "nullable": true + "example": "Event was rescheduled" }, - "recurringEventId": { + "rescheduledFromUid": { "type": "string", - "nullable": true + "example": "previous_recurring_uid_123" }, - "startTime": { - "format": "date-time", - "type": "string" - }, - "endTime": { - "format": "date-time", - "type": "string" - }, - "location": { + "start": { "type": "string", - "nullable": true - }, - "status": { - "type": "string" + "example": "2024-08-13T15:30:00Z" }, - "metadata": { - "type": "object" - }, - "cancellationReason": { + "end": { "type": "string", - "nullable": true + "example": "2024-08-13T16:30:00Z" }, - "responses": { - "type": "object" + "duration": { + "type": "number", + "example": 30 }, - "rejectionReason": { - "type": "string", - "nullable": true + "eventTypeId": { + "type": "number", + "example": 50 }, - "userPrimaryEmail": { + "recurringBookingUid": { "type": "string", - "nullable": true - }, - "user": { - "nullable": true, - "allOf": [ - { - "$ref": "#/components/schemas/User" - } - ] + "example": "recurring_uid_987" }, "attendees": { "type": "array", @@ -10292,235 +10153,37 @@ "$ref": "#/components/schemas/Attendee" } }, - "eventTypeId": { - "type": "number", - "nullable": true - }, - "eventType": { - "nullable": true, - "allOf": [ - { - "$ref": "#/components/schemas/EventType" - } - ] - } - }, - "required": [ - "title", - "id", - "uid", - "description", - "customInputs", - "smsReminderNumber", - "recurringEventId", - "startTime", - "endTime", - "location", - "status", - "metadata", - "cancellationReason", - "responses", - "rejectionReason", - "userPrimaryEmail", - "user", - "attendees", - "eventTypeId", - "eventType" - ] - }, - "GetBookingOutput_2024_04_15": { - "type": "object", - "properties": { - "status": { - "type": "string", - "example": "success", - "enum": [ - "success", - "error" - ] - }, - "data": { - "$ref": "#/components/schemas/GetBookingData_2024_04_15" - } - }, - "required": [ - "status", - "data" - ] - }, - "Response": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "email": { - "type": "string" - }, "guests": { + "example": [ + "guest3@example.com", + "guest4@example.com" + ], "type": "array", "items": { "type": "string" } }, - "location": { - "$ref": "#/components/schemas/Location" - }, - "notes": { - "type": "string" - } - }, - "required": [ - "name", - "email", - "guests" - ] - }, - "CreateBookingInput_2024_04_15": { - "type": "object", - "properties": { - "end": { - "type": "string" - }, - "start": { - "type": "string" - }, - "eventTypeId": { - "type": "number" - }, - "eventTypeSlug": { - "type": "string" - }, - "rescheduleUid": { - "type": "string" - }, - "timeZone": { - "type": "string" - }, - "user": { - "type": "array", - "items": { - "type": "string" - } - }, - "language": { - "type": "string" - }, - "bookingUid": { - "type": "string" - }, - "metadata": { - "type": "object" - }, - "hasHashedBookingLink": { - "type": "boolean" - }, - "hashedLink": { + "meetingUrl": { "type": "string", - "nullable": true - }, - "seatReferenceUid": { - "type": "string" - }, - "responses": { - "$ref": "#/components/schemas/Response" - }, - "orgSlug": { - "type": "string" - }, - "locationUrl": { - "type": "string" - } - }, - "required": [ - "start", - "eventTypeId", - "timeZone", - "language", - "metadata", - "hashedLink", - "responses" - ] - }, - "CancelBookingInput_2024_04_15": { - "type": "object", - "properties": { - "id": { - "type": "number" - }, - "uid": { - "type": "string" - }, - "allRemainingBookings": { - "type": "boolean" - }, - "cancellationReason": { - "type": "string" + "example": "https://example.com/recurring-meeting" }, - "seatReferenceUid": { - "type": "string" + "absentHost": { + "type": "boolean", + "example": false } }, "required": [ "id", "uid", - "allRemainingBookings", - "cancellationReason", - "seatReferenceUid" - ] - }, - "MarkNoShowInput_2024_04_15": { - "type": "object", - "properties": { - "noShowHost": { - "type": "boolean" - }, - "attendees": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Attendee" - } - } - } - }, - "HandleMarkNoShowData_2024_04_15": { - "type": "object", - "properties": { - "message": { - "type": "string" - }, - "noShowHost": { - "type": "boolean" - }, - "attendees": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Attendee" - } - } - }, - "required": [ - "message" - ] - }, - "MarkNoShowOutput_2024_04_15": { - "type": "object", - "properties": { - "status": { - "type": "string", - "example": "success", - "enum": [ - "success", - "error" - ] - }, - "data": { - "$ref": "#/components/schemas/HandleMarkNoShowData_2024_04_15" - } - }, - "required": [ + "hosts", "status", - "data" + "start", + "end", + "duration", + "eventTypeId", + "recurringBookingUid", + "attendees", + "absentHost" ] }, "CreateBookingOutput_2024_08_13": { @@ -10535,7 +10198,18 @@ ] }, "data": { - "type": "object" + "oneOf": [ + { + "$ref": "#/components/schemas/BookingOutput_2024_08_13" + }, + { + "type": "array", + "items": { + "$ref": "#/components/schemas/RecurringBookingOutput_2024_08_13" + } + } + ], + "description": "Booking data, which can be either a BookingOutput object or an array of RecurringBookingOutput objects" } }, "required": [ @@ -10543,10 +10217,6 @@ "data" ] }, - "BookingOutput_2024_08_13": { - "type": "object", - "properties": {} - }, "GetBookingOutput_2024_08_13": { "type": "object", "properties": { @@ -10559,7 +10229,21 @@ ] }, "data": { - "$ref": "#/components/schemas/BookingOutput_2024_08_13" + "oneOf": [ + { + "$ref": "#/components/schemas/BookingOutput_2024_08_13" + }, + { + "$ref": "#/components/schemas/RecurringBookingOutput_2024_08_13" + }, + { + "type": "array", + "items": { + "$ref": "#/components/schemas/RecurringBookingOutput_2024_08_13" + } + } + ], + "description": "Booking data, which can be either a BookingOutput object, a RecurringBookingOutput object, or an array of RecurringBookingOutput objects" } }, "required": [ @@ -10581,8 +10265,16 @@ "data": { "type": "array", "items": { - "type": "object" - } + "oneOf": [ + { + "$ref": "#/components/schemas/BookingOutput_2024_08_13" + }, + { + "$ref": "#/components/schemas/RecurringBookingOutput_2024_08_13" + } + ] + }, + "description": "Array of booking data, which can contain either BookingOutput objects or RecurringBookingOutput objects" } }, "required": [ @@ -10606,7 +10298,15 @@ ] }, "data": { - "type": "object" + "oneOf": [ + { + "$ref": "#/components/schemas/BookingOutput_2024_08_13" + }, + { + "$ref": "#/components/schemas/RecurringBookingOutput_2024_08_13" + } + ], + "description": "Booking data, which can be either a BookingOutput object or a RecurringBookingOutput object" } }, "required": [ @@ -10635,10 +10335,28 @@ "success", "error" ] + }, + "data": { + "oneOf": [ + { + "$ref": "#/components/schemas/BookingOutput_2024_08_13" + }, + { + "$ref": "#/components/schemas/RecurringBookingOutput_2024_08_13" + }, + { + "type": "array", + "items": { + "$ref": "#/components/schemas/RecurringBookingOutput_2024_08_13" + } + } + ], + "description": "Booking data, which can be either a BookingOutput object, a RecurringBookingOutput object, or an array of RecurringBookingOutput objects" } }, "required": [ - "status" + "status", + "data" ] }, "MarkAbsentBookingInput_2024_08_13": { @@ -10671,7 +10389,15 @@ ] }, "data": { - "type": "object" + "oneOf": [ + { + "$ref": "#/components/schemas/BookingOutput_2024_08_13" + }, + { + "$ref": "#/components/schemas/RecurringBookingOutput_2024_08_13" + } + ], + "description": "Booking data, which can be either a BookingOutput object or a RecurringBookingOutput object" } }, "required": [ diff --git a/packages/platform/types/bookings/2024-08-13/outputs/booking.output.ts b/packages/platform/types/bookings/2024-08-13/outputs/booking.output.ts index db8eff9a64e61c..46f2e1a8c01604 100644 --- a/packages/platform/types/bookings/2024-08-13/outputs/booking.output.ts +++ b/packages/platform/types/bookings/2024-08-13/outputs/booking.output.ts @@ -1,3 +1,4 @@ +import { ApiProperty } from "@nestjs/swagger"; import { Expose, Type } from "class-transformer"; import { IsArray, @@ -16,179 +17,215 @@ import type { BookingLanguageType } from "../inputs/language"; import { BookingLanguage } from "../inputs/language"; class Attendee { + @ApiProperty({ type: String, example: "John Doe" }) @IsString() @Expose() name!: string; + @ApiProperty({ type: String, example: "America/New_York" }) @IsTimeZone() @Expose() - // note(Lauris): setup CapitalizeTimezone timeZone!: string; + @ApiProperty({ enum: BookingLanguage, required: false, example: "en" }) @IsEnum(BookingLanguage) @Expose() @IsOptional() language?: BookingLanguageType; + @ApiProperty({ type: Boolean, example: false }) @IsBoolean() @Expose() absent!: boolean; } class Host { + @ApiProperty({ type: Number, example: 1 }) @IsInt() @Expose() id!: number; + @ApiProperty({ type: String, example: "Jane Doe" }) @IsString() @Expose() name!: string; + @ApiProperty({ type: String, example: "America/Los_Angeles" }) @IsTimeZone() @Expose() - // note(Lauris): setup CapitalizeTimezone timeZone!: string; } export class BookingOutput_2024_08_13 { + @ApiProperty({ type: Number, example: 123 }) @IsInt() @Expose() id!: number; + @ApiProperty({ type: String, example: "booking_uid_123" }) @IsString() @Expose() uid!: string; + @ApiProperty({ type: [Host] }) @ValidateNested({ each: true }) @Type(() => Host) @Expose() hosts!: Host[]; + @ApiProperty({ enum: ["cancelled", "accepted", "rejected", "pending", "rescheduled"], example: "accepted" }) @IsEnum(["cancelled", "accepted", "rejected", "pending", "rescheduled"]) @Expose() status!: "cancelled" | "accepted" | "rejected" | "pending" | "rescheduled"; + @ApiProperty({ type: String, required: false, example: "User requested cancellation" }) @IsString() @IsOptional() @Expose() cancellationReason?: string; + @ApiProperty({ type: String, required: false, example: "User rescheduled the event" }) @IsString() @IsOptional() @Expose() reschedulingReason?: string; + @ApiProperty({ type: String, required: false, example: "previous_uid_123" }) @IsString() @IsOptional() @Expose() rescheduledFromUid?: string; + @ApiProperty({ type: String, example: "2024-08-13T15:30:00Z" }) @IsDateString() @Expose() start!: string; + @ApiProperty({ type: String, example: "2024-08-13T16:30:00Z" }) @IsDateString() @Expose() end!: string; + @ApiProperty({ type: Number, example: 60 }) @IsInt() @Expose() duration!: number; + @ApiProperty({ type: Number, example: 45 }) @IsInt() @Expose() eventTypeId!: number; + @ApiProperty({ type: [Attendee] }) @ValidateNested({ each: true }) @Type(() => Attendee) @Expose() attendees!: Attendee[]; + @ApiProperty({ type: [String], required: false, example: ["guest1@example.com", "guest2@example.com"] }) @IsArray() @IsString({ each: true }) @IsOptional() @Expose() guests?: string[]; + @ApiProperty({ type: String, required: false, example: "https://example.com/meeting" }) @IsUrl() @IsOptional() @Expose() meetingUrl?: string; + @ApiProperty({ type: Boolean, example: true }) @IsBoolean() @Expose() absentHost!: boolean; } export class RecurringBookingOutput_2024_08_13 { + @ApiProperty({ type: Number, example: 456 }) @IsInt() @Expose() id!: number; + @ApiProperty({ type: String, example: "recurring_uid_123" }) @IsString() @Expose() uid!: string; + @ApiProperty({ type: [Host] }) @ValidateNested({ each: true }) @Type(() => Host) @Expose() hosts!: Host[]; + @ApiProperty({ enum: ["cancelled", "accepted", "rejected", "pending"], example: "pending" }) @IsEnum(["cancelled", "accepted", "rejected", "pending"]) @Expose() status!: "cancelled" | "accepted" | "rejected" | "pending"; + @ApiProperty({ type: String, required: false, example: "Event was cancelled" }) @IsString() @IsOptional() @Expose() cancellationReason?: string; + @ApiProperty({ type: String, required: false, example: "Event was rescheduled" }) @IsString() @IsOptional() @Expose() reschedulingReason?: string; + @ApiProperty({ type: String, required: false, example: "previous_recurring_uid_123" }) @IsString() @IsOptional() @Expose() rescheduledFromUid?: string; + @ApiProperty({ type: String, example: "2024-08-13T15:30:00Z" }) @IsDateString() @Expose() start!: string; + @ApiProperty({ type: String, example: "2024-08-13T16:30:00Z" }) @IsDateString() @Expose() end!: string; + @ApiProperty({ type: Number, example: 30 }) @IsInt() @Expose() duration!: number; + @ApiProperty({ type: Number, example: 50 }) @IsInt() @Expose() eventTypeId!: number; + @ApiProperty({ type: String, example: "recurring_uid_987" }) @IsString() @Expose() recurringBookingUid!: string; + @ApiProperty({ type: [Attendee] }) @ValidateNested({ each: true }) @Type(() => Attendee) @Expose() attendees!: Attendee[]; + @ApiProperty({ type: [String], required: false, example: ["guest3@example.com", "guest4@example.com"] }) @IsArray() @IsString({ each: true }) @IsOptional() @Expose() guests?: string[]; + @ApiProperty({ type: String, required: false, example: "https://example.com/recurring-meeting" }) @IsUrl() @IsOptional() @Expose() meetingUrl?: string; + @ApiProperty({ type: Boolean, example: false }) @IsBoolean() @Expose() absentHost!: boolean; From 31956a4b3c54222166924a90d2381f4ab96f6c81 Mon Sep 17 00:00:00 2001 From: supalarry Date: Wed, 11 Sep 2024 09:15:24 +0200 Subject: [PATCH 60/96] swagger docs --- .../controllers/bookings.controller.ts | 62 +++- apps/api/v2/swagger/documentation.json | 333 ++++++++++++++++-- .../2024-08-13/inputs/cancel-booking.input.ts | 2 +- .../2024-08-13/inputs/create-booking.input.ts | 105 +++++- .../2024-08-13/inputs/mark-absent.input.ts | 10 +- .../inputs/reschedule-booking.input.ts | 10 + 6 files changed, 482 insertions(+), 40 deletions(-) diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.ts b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.ts index 53fd012715a1b8..e03844f4ae697a 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.ts @@ -24,7 +24,14 @@ import { HttpCode, HttpStatus, } from "@nestjs/common"; -import { ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger"; +import { + ApiOperation, + ApiTags as DocsTags, + ApiHeader, + getSchemaPath, + ApiBody, + ApiExtraModels, +} from "@nestjs/swagger"; import { User } from "@prisma/client"; import { Request } from "express"; @@ -36,6 +43,9 @@ import { RescheduleBookingInput_2024_08_13, CancelBookingInput_2024_08_13, MarkAbsentBookingInput_2024_08_13, + CreateBookingInput_2024_08_13, + CreateInstantBookingInput_2024_08_13, + CreateRecurringBookingInput_2024_08_13, } from "@calcom/platform-types"; @Controller({ @@ -44,12 +54,48 @@ import { }) @UseGuards(PermissionsGuard) @DocsTags("Bookings") +@ApiHeader({ + name: "cal-api-version", + description: `Must be set to \`2024-08-13\``, + required: true, +}) export class BookingsController_2024_08_13 { private readonly logger = new Logger("BookingsController"); constructor(private readonly bookingsService: BookingsService_2024_08_13) {} @Post("/") + @ApiOperation({ + summary: "Create booking", + description: ` + POST /v2/bookings is used to create regular bookings, recurring bookings and instant bookings. The request bodies for all 3 are almost the same except: + If eventTypeId in the request body is id of a regular event, then regular booking is created. + + If it is an id of a recurring event type, then recurring booking is created. + + Meaning that the request bodies are equal but the outcome depends on what kind of event type it is with the goal of making it as seamless for developers as possible. + + For team event types it is possible to create instant meeting. To do that just pass \`"instant": true\` to the request body. + + The start needs to be in UTC aka if the timezone is GMT+2 in Rome and meeting should start at 11, then UTC time should have hours 09:00 aka without time zone. + `, + }) + @ApiBody({ + schema: { + oneOf: [ + { $ref: getSchemaPath(CreateBookingInput_2024_08_13) }, + { $ref: getSchemaPath(CreateInstantBookingInput_2024_08_13) }, + { $ref: getSchemaPath(CreateRecurringBookingInput_2024_08_13) }, + ], + }, + description: + "Accepts different types of booking input: CreateBookingInput_2024_08_13, CreateInstantBookingInput_2024_08_13, or CreateRecurringBookingInput_2024_08_13", + }) + @ApiExtraModels( + CreateBookingInput_2024_08_13, + CreateInstantBookingInput_2024_08_13, + CreateRecurringBookingInput_2024_08_13 + ) async createBooking( @Body(new CreateBookingInputPipe()) body: CreateBookingInput, @@ -73,8 +119,13 @@ export class BookingsController_2024_08_13 { @UseGuards(BookingUidGuard) @ApiOperation({ summary: "Get booking", - description: - ":bookingUid can be uid of a normal booking, one of the recurring booking recurrences, or uid of recurring booking which will return an array of all recurring booking recurrences.", + description: `\`:bookingUid\` can be + + 1. uid of a normal booking + + 2. uid of one of the recurring booking recurrences + + 3. uid of recurring booking which will return an array of all recurring booking recurrences (stored as recurringBookingUid on one of the individual recurrences).`, }) async getBooking(@Param("bookingUid") bookingUid: string): Promise { const booking = await this.bookingsService.getBooking(bookingUid); @@ -102,6 +153,11 @@ export class BookingsController_2024_08_13 { @Post("/:bookingUid/reschedule") @UseGuards(BookingUidGuard) + @ApiOperation({ + summary: "Reschedule booking", + description: + "Reschedule a booking by passing `:bookingUid` of the booking that should be rescheduled and pass request body with a new start time to create a new booking.", + }) async rescheduleBooking( @Param("bookingUid") bookingUid: string, @Body() body: RescheduleBookingInput_2024_08_13, diff --git a/apps/api/v2/swagger/documentation.json b/apps/api/v2/swagger/documentation.json index da177a0613c4fb..7a2309f2a13f4b 100644 --- a/apps/api/v2/swagger/documentation.json +++ b/apps/api/v2/swagger/documentation.json @@ -3560,7 +3560,40 @@ "/v2/bookings": { "post": { "operationId": "BookingsController_2024_08_13_createBooking", - "parameters": [], + "summary": "Create booking", + "description": "\n POST /v2/bookings is used to create regular bookings, recurring bookings and instant bookings. The request bodies for all 3 are almost the same except:\n If eventTypeId in the request body is id of a regular event, then regular booking is created.\n\n If it is an id of a recurring event type, then recurring booking is created.\n\n Meaning that the request bodies are equal but the outcome depends on what kind of event type it is with the goal of making it as seamless for developers as possible.\n\n For team event types it is possible to create instant meeting. To do that just pass `\"instant\": true` to the request body.\n \n The start needs to be in UTC aka if the timezone is GMT+2 in Rome and meeting should start at 11, then UTC time should have hours 09:00 aka without time zone.\n ", + "parameters": [ + { + "name": "cal-api-version", + "in": "header", + "description": "Must be set to `2024-08-13`", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "description": "Accepts different types of booking input: CreateBookingInput_2024_08_13, CreateInstantBookingInput_2024_08_13, or CreateRecurringBookingInput_2024_08_13", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/CreateBookingInput_2024_08_13" + }, + { + "$ref": "#/components/schemas/CreateInstantBookingInput_2024_08_13" + }, + { + "$ref": "#/components/schemas/CreateRecurringBookingInput_2024_08_13" + } + ] + } + } + } + }, "responses": { "201": { "description": "", @@ -3580,6 +3613,15 @@ "get": { "operationId": "BookingsController_2024_08_13_getBookings", "parameters": [ + { + "name": "cal-api-version", + "in": "header", + "description": "Must be set to `2024-08-13`", + "required": true, + "schema": { + "type": "string" + } + }, { "name": "status", "required": false, @@ -3764,8 +3806,17 @@ "get": { "operationId": "BookingsController_2024_08_13_getBooking", "summary": "Get booking", - "description": ":bookingUid can be uid of a normal booking, one of the recurring booking recurrences, or uid of recurring booking which will return an array of all recurring booking recurrences.", + "description": "`:bookingUid` can be\n \n 1. uid of a normal booking\n \n 2. uid of one of the recurring booking recurrences\n \n 3. uid of recurring booking which will return an array of all recurring booking recurrences (stored as recurringBookingUid on one of the individual recurrences).", "parameters": [ + { + "name": "cal-api-version", + "in": "header", + "description": "Must be set to `2024-08-13`", + "required": true, + "schema": { + "type": "string" + } + }, { "name": "bookingUid", "required": true, @@ -3795,7 +3846,18 @@ "/v2/bookings/{bookingUid}/reschedule": { "post": { "operationId": "BookingsController_2024_08_13_rescheduleBooking", + "summary": "Reschedule booking", + "description": "Reschedule a booking by passing `:bookingUid` of the booking that should be rescheduled and pass request body with a new start time to create a new booking.", "parameters": [ + { + "name": "cal-api-version", + "in": "header", + "description": "Must be set to `2024-08-13`", + "required": true, + "schema": { + "type": "string" + } + }, { "name": "bookingUid", "required": true, @@ -3836,6 +3898,15 @@ "post": { "operationId": "BookingsController_2024_08_13_cancelBooking", "parameters": [ + { + "name": "cal-api-version", + "in": "header", + "description": "Must be set to `2024-08-13`", + "required": true, + "schema": { + "type": "string" + } + }, { "name": "bookingUid", "required": true, @@ -3876,6 +3947,15 @@ "post": { "operationId": "BookingsController_2024_08_13_markNoShow", "parameters": [ + { + "name": "cal-api-version", + "in": "header", + "description": "Must be set to `2024-08-13`", + "required": true, + "schema": { + "type": "string" + } + }, { "name": "bookingUid", "required": true, @@ -9902,37 +9982,22 @@ "data" ] }, - "Host": { - "type": "object", - "properties": { - "id": { - "type": "number", - "example": 1 - }, - "name": { - "type": "string", - "example": "Jane Doe" - }, - "timeZone": { - "type": "string", - "example": "America/Los_Angeles" - } - }, - "required": [ - "id", - "name", - "timeZone" - ] - }, "Attendee": { "type": "object", "properties": { "name": { "type": "string", + "description": "The name of the attendee.", "example": "John Doe" }, + "email": { + "type": "string", + "description": "The email of the attendee.", + "example": "john.doe@example.com" + }, "timeZone": { "type": "string", + "description": "The time zone of the attendee.", "example": "America/New_York" }, "language": { @@ -9981,17 +10046,196 @@ "uk", "zh-TW" ], - "example": "en" + "description": "The preferred language of the attendee. Used for booking confirmation.", + "example": "it", + "default": "en" + } + }, + "required": [ + "name", + "email", + "timeZone" + ] + }, + "CreateBookingInput_2024_08_13": { + "type": "object", + "properties": { + "start": { + "type": "string", + "description": "The start time of the booking in ISO 8601 format in UTC timezone.", + "example": "2024-08-13T09:00:00Z" + }, + "eventTypeId": { + "type": "number", + "description": "The ID of the event type that is booked.", + "example": 123 + }, + "attendee": { + "description": "The attendee's details.", + "allOf": [ + { + "$ref": "#/components/schemas/Attendee" + } + ] + }, + "guests": { + "description": "An optional list of guest emails attending the event.", + "example": [ + "guest1@example.com", + "guest2@example.com" + ], + "type": "array", + "items": { + "type": "string" + } + }, + "meetingUrl": { + "type": "string", + "description": "Meeting URL just for this booking. Displayed in email and calendar event. If not provided then cal video link will be generated.", + "example": "https://example.com/meeting" + }, + "bookingFieldsResponses": { + "type": "object", + "description": "Booking field responses.", + "example": { + "customField": "customValue" + } + } + }, + "required": [ + "start", + "eventTypeId", + "attendee" + ] + }, + "CreateInstantBookingInput_2024_08_13": { + "type": "object", + "properties": { + "start": { + "type": "string", + "description": "The start time of the booking in ISO 8601 format in UTC timezone.", + "example": "2024-08-13T09:00:00Z" + }, + "eventTypeId": { + "type": "number", + "description": "The ID of the event type that is booked.", + "example": 123 }, - "absent": { + "attendee": { + "description": "The attendee's details.", + "allOf": [ + { + "$ref": "#/components/schemas/Attendee" + } + ] + }, + "guests": { + "description": "An optional list of guest emails attending the event.", + "example": [ + "guest1@example.com", + "guest2@example.com" + ], + "type": "array", + "items": { + "type": "string" + } + }, + "meetingUrl": { + "type": "string", + "description": "Meeting URL just for this booking. Displayed in email and calendar event. If not provided then cal video link will be generated.", + "example": "https://example.com/meeting" + }, + "bookingFieldsResponses": { + "type": "object", + "description": "Booking field responses.", + "example": { + "customField": "customValue" + } + }, + "instant": { "type": "boolean", - "example": false + "description": "Flag indicating if the booking is an instant booking. Only available for team events.", + "example": true } }, "required": [ + "start", + "eventTypeId", + "attendee", + "instant" + ] + }, + "CreateRecurringBookingInput_2024_08_13": { + "type": "object", + "properties": { + "start": { + "type": "string", + "description": "The start time of the booking in ISO 8601 format in UTC timezone.", + "example": "2024-08-13T09:00:00Z" + }, + "eventTypeId": { + "type": "number", + "description": "The ID of the event type that is booked.", + "example": 123 + }, + "attendee": { + "description": "The attendee's details.", + "allOf": [ + { + "$ref": "#/components/schemas/Attendee" + } + ] + }, + "guests": { + "description": "An optional list of guest emails attending the event.", + "example": [ + "guest1@example.com", + "guest2@example.com" + ], + "type": "array", + "items": { + "type": "string" + } + }, + "meetingUrl": { + "type": "string", + "description": "Meeting URL just for this booking. Displayed in email and calendar event. If not provided then cal video link will be generated.", + "example": "https://example.com/meeting" + }, + "bookingFieldsResponses": { + "type": "object", + "description": "Booking field responses.", + "example": { + "customField": "customValue" + } + } + }, + "required": [ + "start", + "eventTypeId", + "attendee" + ] + }, + "Host": { + "type": "object", + "properties": { + "id": { + "type": "number", + "example": 1 + }, + "name": { + "type": "string", + "example": "Jane Doe" + }, + "timeZone": { + "type": "string", + "example": "America/Los_Angeles" + } + }, + "required": [ + "id", "name", - "timeZone", - "absent" + "timeZone" ] }, "BookingOutput_2024_08_13": { @@ -10284,7 +10528,21 @@ }, "RescheduleBookingInput_2024_08_13": { "type": "object", - "properties": {} + "properties": { + "start": { + "type": "string", + "description": "Start time in ISO 8601 format for the new booking", + "example": "2024-08-13T10:00:00Z" + }, + "reschedulingReason": { + "type": "string", + "example": "User requested reschedule", + "description": "Reason for rescheduling the booking" + } + }, + "required": [ + "start" + ] }, "RescheduleBookingOutput_2024_08_13": { "type": "object", @@ -10318,7 +10576,8 @@ "type": "object", "properties": { "cancellationReason": { - "type": "string" + "type": "string", + "example": "User requested cancellation" } }, "required": [ @@ -10363,9 +10622,18 @@ "type": "object", "properties": { "host": { - "type": "boolean" + "type": "boolean", + "example": false, + "description": "Whether the host was absent" }, "attendees": { + "description": "Toggle whether an attendee was absent or not.", + "example": [ + { + "absent": true, + "email": "someone@gmail.com" + } + ], "type": "array", "items": { "type": "string" @@ -10373,7 +10641,6 @@ } }, "required": [ - "host", "attendees" ] }, diff --git a/packages/platform/types/bookings/2024-08-13/inputs/cancel-booking.input.ts b/packages/platform/types/bookings/2024-08-13/inputs/cancel-booking.input.ts index 2ab9c2680806c5..1fa5b5f51eb21f 100644 --- a/packages/platform/types/bookings/2024-08-13/inputs/cancel-booking.input.ts +++ b/packages/platform/types/bookings/2024-08-13/inputs/cancel-booking.input.ts @@ -4,6 +4,6 @@ import { IsOptional, IsString } from "class-validator"; export class CancelBookingInput_2024_08_13 { @IsString() @IsOptional() - @ApiProperty() + @ApiProperty({ example: "User requested cancellation" }) cancellationReason?: string; } diff --git a/packages/platform/types/bookings/2024-08-13/inputs/create-booking.input.ts b/packages/platform/types/bookings/2024-08-13/inputs/create-booking.input.ts index 2b1ed7038dcd3a..6982c1f474b492 100644 --- a/packages/platform/types/bookings/2024-08-13/inputs/create-booking.input.ts +++ b/packages/platform/types/bookings/2024-08-13/inputs/create-booking.input.ts @@ -1,3 +1,4 @@ +import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; import { Type } from "class-transformer"; import { IsInt, @@ -18,80 +19,182 @@ import type { BookingLanguageType } from "./language"; import { BookingLanguage } from "./language"; class Attendee { + @ApiProperty({ + type: String, + description: "The name of the attendee.", + example: "John Doe", + }) @IsString() name!: string; + @ApiProperty({ + type: String, + description: "The email of the attendee.", + example: "john.doe@example.com", + }) @IsEmail() email!: string; + @ApiProperty({ + type: String, + description: "The time zone of the attendee.", + example: "America/New_York", + }) @IsTimeZone() - // note(Lauris): setup CapitalizeTimezone timeZone!: string; + @ApiPropertyOptional({ + enum: BookingLanguage, + description: "The preferred language of the attendee. Used for booking confirmation.", + example: BookingLanguage.it, + default: BookingLanguage.en, + }) @IsEnum(BookingLanguage) @IsOptional() language?: BookingLanguageType; } + export class CreateBookingInput_2024_08_13 { + @ApiProperty({ + type: String, + description: "The start time of the booking in ISO 8601 format in UTC timezone.", + example: "2024-08-13T09:00:00Z", + }) @IsDateString() start!: string; + @ApiProperty({ + type: Number, + description: "The ID of the event type that is booked.", + example: 123, + }) @IsInt() eventTypeId!: number; + @ApiProperty({ + type: Attendee, + description: "The attendee's details.", + }) @ValidateNested() @Type(() => Attendee) attendee!: Attendee; + @ApiPropertyOptional({ + type: [String], + description: "An optional list of guest emails attending the event.", + example: ["guest1@example.com", "guest2@example.com"], + }) @IsArray() @IsString({ each: true }) @IsOptional() guests?: string[]; + @ApiPropertyOptional({ + type: String, + description: + "Meeting URL just for this booking. Displayed in email and calendar event. If not provided then cal video link will be generated.", + example: "https://example.com/meeting", + }) @IsUrl() @IsOptional() meetingUrl?: string; // todo(Lauris): expose after refactoring metadata https://app.campsite.co/cal/posts/zysq8w9rwm9c + // @ApiProperty({ + // type: Object, + // description: "Optional metadata for the booking.", + // example: { key: "value" }, + // required: false, + // }) // @IsObject() // @IsOptional() // metadata!: Record; + @ApiPropertyOptional({ + type: Object, + description: "Booking field responses.", + example: { customField: "customValue" }, + required: false, + }) @IsObject() @IsOptional() bookingFieldsResponses?: Record; } export class CreateInstantBookingInput_2024_08_13 extends CreateBookingInput_2024_08_13 { + @ApiProperty({ + type: Boolean, + description: "Flag indicating if the booking is an instant booking. Only available for team events.", + example: true, + }) @IsBoolean() instant!: boolean; } export class CreateRecurringBookingInput_2024_08_13 { + @ApiProperty({ + type: String, + description: "The start time of the booking in ISO 8601 format in UTC timezone.", + example: "2024-08-13T09:00:00Z", + }) @IsDateString() start!: string; + @ApiProperty({ + type: Number, + description: "The ID of the event type that is booked.", + example: 123, + }) @IsInt() eventTypeId!: number; + @ApiProperty({ + type: Attendee, + description: "The attendee's details.", + }) @ValidateNested() @Type(() => Attendee) attendee!: Attendee; + @ApiProperty({ + type: [String], + description: "An optional list of guest emails attending the event.", + example: ["guest1@example.com", "guest2@example.com"], + required: false, + }) @IsArray() @IsString({ each: true }) @IsOptional() guests?: string[]; + @ApiProperty({ + type: String, + description: + "Meeting URL just for this booking. Displayed in email and calendar event. If not provided then cal video link will be generated.", + example: "https://example.com/meeting", + required: false, + }) @IsUrl() @IsOptional() meetingUrl?: string; // todo(Lauris): expose after refactoring metadata https://app.campsite.co/cal/posts/zysq8w9rwm9c + // @ApiProperty({ + // type: Object, + // description: "Optional metadata for the booking.", + // example: { key: "value" }, + // required: false, + // }) // @IsObject() // @IsOptional() // metadata!: Record; + @ApiProperty({ + type: Object, + description: "Booking field responses.", + example: { customField: "customValue" }, + required: false, + }) @IsObject() @IsOptional() bookingFieldsResponses?: Record; diff --git a/packages/platform/types/bookings/2024-08-13/inputs/mark-absent.input.ts b/packages/platform/types/bookings/2024-08-13/inputs/mark-absent.input.ts index ac2dd897d4f880..4dc28920e4b382 100644 --- a/packages/platform/types/bookings/2024-08-13/inputs/mark-absent.input.ts +++ b/packages/platform/types/bookings/2024-08-13/inputs/mark-absent.input.ts @@ -4,20 +4,26 @@ import { IsOptional, IsBoolean, IsEmail, IsArray, ArrayMinSize, ValidateNested } class Attendee { @IsEmail() + @ApiProperty() email!: string; @IsBoolean() + @ApiProperty() absent!: boolean; } export class MarkAbsentBookingInput_2024_08_13 { @IsBoolean() @IsOptional() - @ApiProperty() + @ApiProperty({ example: false, required: false, description: "Whether the host was absent" }) host?: boolean; @ArrayMinSize(1) - @ApiProperty({ type: [String] }) + @ApiProperty({ + type: [String], + description: "Toggle whether an attendee was absent or not.", + example: [{ absent: true, email: "someone@gmail.com" }], + }) @ValidateNested() @Type(() => Attendee) @IsArray() diff --git a/packages/platform/types/bookings/2024-08-13/inputs/reschedule-booking.input.ts b/packages/platform/types/bookings/2024-08-13/inputs/reschedule-booking.input.ts index a077c367edaaa6..998d7b3950fb78 100644 --- a/packages/platform/types/bookings/2024-08-13/inputs/reschedule-booking.input.ts +++ b/packages/platform/types/bookings/2024-08-13/inputs/reschedule-booking.input.ts @@ -1,10 +1,20 @@ +import { ApiProperty } from "@nestjs/swagger"; import { IsDateString, IsOptional, IsString } from "class-validator"; export class RescheduleBookingInput_2024_08_13 { @IsDateString() + @ApiProperty({ + description: "Start time in ISO 8601 format for the new booking", + example: "2024-08-13T10:00:00Z", + }) start!: string; @IsString() @IsOptional() + @ApiProperty({ + example: "User requested reschedule", + description: "Reason for rescheduling the booking", + required: false, + }) reschedulingReason?: string; } From db299a43fe918e6cec8b144b8d39e25b63ad98fe Mon Sep 17 00:00:00 2001 From: supalarry Date: Thu, 12 Sep 2024 12:03:35 +0200 Subject: [PATCH 61/96] docs: document authorization header --- .../controllers/bookings.controller.ts | 12 ++++++++++++ apps/api/v2/swagger/documentation.json | 18 ++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.ts b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.ts index e03844f4ae697a..d7f84c4a010154 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.ts @@ -138,6 +138,12 @@ export class BookingsController_2024_08_13 { @Get("/") @UseGuards(ApiAuthGuard) + @ApiHeader({ + name: "Authorization", + description: + "value must be `Bearer ` where `` either managed user access token or api key prefixed with cal_", + required: true, + }) @Permissions([BOOKING_READ]) async getBookings( @Query() queryParams: GetBookingsInput_2024_08_13, @@ -192,6 +198,12 @@ export class BookingsController_2024_08_13 { @HttpCode(HttpStatus.OK) @Permissions([BOOKING_WRITE]) @UseGuards(ApiAuthGuard, BookingUidGuard) + @ApiHeader({ + name: "Authorization", + description: + "value must be `Bearer ` where `` either managed user access token or api key prefixed with cal_", + required: true, + }) async markNoShow( @Param("bookingUid") bookingUid: string, @Body() body: MarkAbsentBookingInput_2024_08_13, diff --git a/apps/api/v2/swagger/documentation.json b/apps/api/v2/swagger/documentation.json index 7a2309f2a13f4b..67a50ee46f0bfb 100644 --- a/apps/api/v2/swagger/documentation.json +++ b/apps/api/v2/swagger/documentation.json @@ -3783,6 +3783,15 @@ "schema": { "type": "number" } + }, + { + "name": "Authorization", + "in": "header", + "description": "value must be `Bearer ` where `` either managed user access token or api key prefixed with cal_", + "required": true, + "schema": { + "type": "string" + } } ], "responses": { @@ -3963,6 +3972,15 @@ "schema": { "type": "string" } + }, + { + "name": "Authorization", + "in": "header", + "description": "value must be `Bearer ` where `` either managed user access token or api key prefixed with cal_", + "required": true, + "schema": { + "type": "string" + } } ], "requestBody": { From 7c99e0f4686ff63691a29275ea26223329f3fec2 Mon Sep 17 00:00:00 2001 From: supalarry Date: Thu, 12 Sep 2024 12:05:05 +0200 Subject: [PATCH 62/96] refactor: remove unused attendee variable --- .../v2/src/ee/bookings/2024-08-13/services/output.service.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/output.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/output.service.ts index 742d805ae2237a..eaab568ac07db7 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/services/output.service.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/services/output.service.ts @@ -34,11 +34,6 @@ export class OutputBookingsService_2024_08_13 { const duration = dateEnd.diff(dateStart, "minutes").minutes; const bookingResponses = bookingResponsesSchema.parse(databaseBooking.responses); - const attendee = databaseBooking.attendees.find((attendee) => attendee.email === bookingResponses.email); - - if (!attendee) { - throw new Error("Attendee not found"); - } const booking = { id: databaseBooking.id, From f9944a06f1024d45c091b78b7c90a89a628f05e0 Mon Sep 17 00:00:00 2001 From: supalarry Date: Thu, 12 Sep 2024 12:07:44 +0200 Subject: [PATCH 63/96] refactor: remove unused check --- .../v2/src/ee/bookings/2024-08-13/services/output.service.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/output.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/output.service.ts index eaab568ac07db7..75a3ec77c9dd2b 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/services/output.service.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/services/output.service.ts @@ -66,10 +66,6 @@ export class OutputBookingsService_2024_08_13 { const transformed = []; for (const booking of databaseBookings) { - if (!booking.id) { - throw new Error("Booking was not created"); - } - const databaseBooking = await this.bookingsRepository.getByIdWithAttendeesAndUser(booking.id); if (!databaseBooking) { throw new Error(`Booking with id=${booking.id} was not found in the database`); From 441ac833ae9720a31e3af5a09d34fe6c97c0f042 Mon Sep 17 00:00:00 2001 From: supalarry Date: Thu, 12 Sep 2024 12:09:12 +0200 Subject: [PATCH 64/96] refactor: remove unused attendee variable --- .../v2/src/ee/bookings/2024-08-13/services/output.service.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/output.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/output.service.ts index 75a3ec77c9dd2b..27a2318fd365e8 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/services/output.service.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/services/output.service.ts @@ -83,11 +83,6 @@ export class OutputBookingsService_2024_08_13 { const duration = dateEnd.diff(dateStart, "minutes").minutes; const bookingResponses = bookingResponsesSchema.parse(databaseBooking.responses); - const attendee = databaseBooking.attendees.find((attendee) => attendee.email === bookingResponses.email); - - if (!attendee) { - throw new Error("Attendee not found"); - } const booking = { id: databaseBooking.id, From f20d834b80ab7cd23f25e42d0dfb1ca23d70f9d6 Mon Sep 17 00:00:00 2001 From: supalarry Date: Thu, 12 Sep 2024 12:11:03 +0200 Subject: [PATCH 65/96] refactor: spelling --- .../src/ee/bookings/2024-08-13/services/input.service.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts index 46b587c2e1e28b..a74123242bec98 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts @@ -173,11 +173,11 @@ export class InputBookingsService_2024_08_13 { throw new NotFoundException(`Event type with id=${inputBooking.eventTypeId} is not a recurring event`); } - const occurrance = recurringEventSchema.parse(eventType.recurringEvent); - const repeatsEvery = occurrance.interval; - const repeatsTimes = occurrance.count; + const occurrence = recurringEventSchema.parse(eventType.recurringEvent); + const repeatsEvery = occurrence.interval; + const repeatsTimes = occurrence.count; // note(Lauris): timeBetween 0=yearly, 1=monthly and 2=weekly - const timeBetween = occurrance.freq; + const timeBetween = occurrence.freq; if (!repeatsTimes) { throw new Error("Repeats times is required"); From 3046d1a1911414b5c847cd1ce085255cc6f72b73 Mon Sep 17 00:00:00 2001 From: supalarry Date: Thu, 12 Sep 2024 13:51:32 +0200 Subject: [PATCH 66/96] use published platform libraries --- apps/api/v2/package.json | 3 +- .../controllers/bookings.controller.ts | 8 +- .../2024-08-13/bookings.repository.ts | 14 + .../bookings.controller.e2e-spec.ts | 1 + .../2024-08-13/services/bookings.service.ts | 44 +- apps/api/v2/swagger/documentation.json | 11165 ++++++++++++++++ packages/platform/libraries/CHANGELOG.md | 3 + packages/platform/libraries/package.json | 2 +- .../routers/viewer/bookings/get.handler.ts | 2 - yarn.lock | 16 +- 10 files changed, 11213 insertions(+), 45 deletions(-) diff --git a/apps/api/v2/package.json b/apps/api/v2/package.json index fc06fd4b54d422..77553eaf031cff 100644 --- a/apps/api/v2/package.json +++ b/apps/api/v2/package.json @@ -28,9 +28,8 @@ "dependencies": { "@calcom/platform-constants": "*", "@calcom/platform-enums": "*", - "@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.34", + "@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.35", "@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2", - "@calcom/platform-libraries-1.2.3": "npm:@calcom/platform-libraries@1.2.3", "@calcom/platform-types": "*", "@calcom/platform-utils": "*", "@calcom/prisma": "*", diff --git a/apps/api/v2/src/ee/bookings/2024-04-15/controllers/bookings.controller.ts b/apps/api/v2/src/ee/bookings/2024-04-15/controllers/bookings.controller.ts index 76ef491b3516e9..069b1a52bfe52e 100644 --- a/apps/api/v2/src/ee/bookings/2024-04-15/controllers/bookings.controller.ts +++ b/apps/api/v2/src/ee/bookings/2024-04-15/controllers/bookings.controller.ts @@ -31,11 +31,7 @@ import { UseGuards, } from "@nestjs/common"; import { ConfigService } from "@nestjs/config"; -import { - ApiQuery, - ApiTags as DocsTags, - ApiExcludeController as DocsExcludeController, -} from "@nestjs/swagger"; +import { ApiQuery, ApiExcludeController as DocsExcludeController } from "@nestjs/swagger"; import { User } from "@prisma/client"; import { Request } from "express"; import { NextApiRequest } from "next/types"; @@ -44,6 +40,7 @@ import { v4 as uuidv4 } from "uuid"; import { X_CAL_CLIENT_ID } from "@calcom/platform-constants"; import { BOOKING_READ, SUCCESS_STATUS, BOOKING_WRITE } from "@calcom/platform-constants"; import { + handleNewRecurringBooking, handleNewBooking, BookingResponse, HttpError, @@ -55,7 +52,6 @@ import { getBookingForReschedule, ErrorCode, } from "@calcom/platform-libraries"; -import { handleNewRecurringBooking } from "@calcom/platform-libraries-1.2.3"; import { GetBookingsInput_2024_04_15, CancelBookingInput_2024_04_15, diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/bookings.repository.ts b/apps/api/v2/src/ee/bookings/2024-08-13/bookings.repository.ts index 57d36bbe747fc3..fea95d8b845628 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/bookings.repository.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/bookings.repository.ts @@ -14,6 +14,20 @@ export class BookingsRepository_2024_08_13 { }); } + async getByIdsWithAttendeesAndUser(ids: number[]) { + return this.dbRead.prisma.booking.findMany({ + where: { + id: { + in: ids, + }, + }, + include: { + attendees: true, + user: true, + }, + }); + } + async getByUid(bookingUid: string) { return this.dbRead.prisma.booking.findUnique({ where: { diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.e2e-spec.ts b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.e2e-spec.ts index 67e5bbdcb583f4..0b9e1ac9936424 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.e2e-spec.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.e2e-spec.ts @@ -654,6 +654,7 @@ describe("Bookings Endpoints 2024-08-13", () => { expect(responseBody.data).toBeDefined(); const data: (BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13)[] = responseBody.data; expect(data.length).toEqual(2); + console.log("asap data", JSON.stringify(data, null, 2)); expect(data[0].start).toEqual(createdBooking.start); expect(data[1].start).toEqual(bookingInThePast.startTime.toISOString()); }); diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts index cd10fb9f794f0b..e9956fcbe419f2 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts @@ -15,7 +15,7 @@ import { handleInstantMeeting, handleCancelBooking, handleMarkNoShow, -} from "@calcom/platform-libraries-1.2.3"; +} from "@calcom/platform-libraries"; import { CreateBookingInput_2024_08_13, RescheduleBookingInput_2024_08_13, @@ -25,21 +25,10 @@ import { CreateInstantBookingInput_2024_08_13, CancelBookingInput_2024_08_13, MarkAbsentBookingInput_2024_08_13, + BookingOutput_2024_08_13, + RecurringBookingOutput_2024_08_13, } from "@calcom/platform-types"; import { PrismaClient } from "@calcom/prisma"; -import { Booking } from "@calcom/prisma/client"; - -type BookingWithAttendeesAndEventType = Booking & { - attendees: { - name: string; - email: string; - timeZone: string; - locale: string | null; - noShow: boolean | null; - }[]; - eventType: { id: number }; - user: { id: number; name: string | null; email: string } | null; -}; type CreatedBooking = { hosts: { id: number }[]; @@ -145,7 +134,7 @@ export class BookingsService_2024_08_13 { } async getBookings(queryParams: GetBookingsInput_2024_08_13, user: { email: string; id: number }) { - const fetchedBookings: { bookings: BookingWithAttendeesAndEventType[] } = await getAllUserBookings({ + const fetchedBookings: { bookings: { id: number }[] } = await getAllUserBookings({ bookingListingByStatus: queryParams.status || [], skip: queryParams.skip ?? 0, // note(Lauris): we substract -1 because getAllUSerBookings child function adds +1 for some reason @@ -157,11 +146,23 @@ export class BookingsService_2024_08_13 { }, sort: this.inputService.transformGetBookingsSort(queryParams), }); + // note(Lauris): fetchedBookings don't have attendees information and responses and i don't want to add them to the handler query, + // because its used elsewhere in code that does not need that information, so i get ids, fetch bookings and then return them formatted in same order as ids. + const ids = fetchedBookings.bookings.map((booking) => booking.id); + const bookings = await this.bookingsRepository.getByIdsWithAttendeesAndUser(ids); + + const bookingMap = new Map(bookings.map((booking) => [booking.id, booking])); + const orderedBookings = ids.map((id) => bookingMap.get(id)); + + const formattedBookings: (BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13)[] = []; + for (const booking of orderedBookings) { + if (!booking) { + continue; + } - return fetchedBookings.bookings.map((booking) => { const formatted = { ...booking, - eventTypeId: booking.eventType.id, + eventTypeId: booking.eventTypeId, startTime: new Date(booking.startTime), endTime: new Date(booking.endTime), absentHost: !!booking.noShowHost, @@ -169,10 +170,13 @@ export class BookingsService_2024_08_13 { const isRecurring = !!formatted.recurringEventId; if (isRecurring) { - return this.outputService.getOutputRecurringBooking(formatted); + formattedBookings.push(this.outputService.getOutputRecurringBooking(formatted)); + } else { + formattedBookings.push(this.outputService.getOutputBooking(formatted)); } - return this.outputService.getOutputBooking(formatted); - }); + } + + return formattedBookings; } async rescheduleBooking(request: Request, bookingUid: string, body: RescheduleBookingInput_2024_08_13) { diff --git a/apps/api/v2/swagger/documentation.json b/apps/api/v2/swagger/documentation.json index e69de29bb2d1d6..3160c84ae58ac9 100644 --- a/apps/api/v2/swagger/documentation.json +++ b/apps/api/v2/swagger/documentation.json @@ -0,0 +1,11165 @@ +{ + "openapi": "3.0.0", + "paths": { + "/health": { + "get": { + "operationId": "AppController_getHealth", + "parameters": [], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + }, + "tags": [ + "Health - development only" + ] + } + }, + "/v2/oauth-clients/{clientId}/users": { + "get": { + "operationId": "OAuthClientUsersController_getManagedUsers", + "parameters": [ + { + "name": "clientId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + }, + { + "name": "limit", + "required": false, + "in": "query", + "description": "The number of items to return", + "example": 10, + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetManagedUsersOutput" + } + } + } + } + }, + "tags": [ + "Managed users" + ] + }, + "post": { + "operationId": "OAuthClientUsersController_createUser", + "parameters": [ + { + "name": "clientId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateManagedUserInput" + } + } + } + }, + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateManagedUserOutput" + } + } + } + } + }, + "tags": [ + "Managed users" + ] + } + }, + "/v2/oauth-clients/{clientId}/users/{userId}": { + "get": { + "operationId": "OAuthClientUsersController_getUserById", + "parameters": [ + { + "name": "clientId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + }, + { + "name": "userId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetManagedUserOutput" + } + } + } + } + }, + "tags": [ + "Managed users" + ] + }, + "patch": { + "operationId": "OAuthClientUsersController_updateUser", + "parameters": [ + { + "name": "clientId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + }, + { + "name": "userId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateManagedUserInput" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetManagedUserOutput" + } + } + } + } + }, + "tags": [ + "Managed users" + ] + }, + "delete": { + "operationId": "OAuthClientUsersController_deleteUser", + "parameters": [ + { + "name": "clientId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + }, + { + "name": "userId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetManagedUserOutput" + } + } + } + } + }, + "tags": [ + "Managed users" + ] + } + }, + "/v2/oauth-clients/{clientId}/users/{userId}/force-refresh": { + "post": { + "operationId": "OAuthClientUsersController_forceRefresh", + "parameters": [ + { + "name": "userId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "clientId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/KeysResponseDto" + } + } + } + } + }, + "tags": [ + "Managed users" + ] + } + }, + "/v2/oauth-clients": { + "post": { + "operationId": "OAuthClientsController_createOAuthClient", + "summary": "", + "description": "⚠️ First, this endpoint requires `Cookie: next-auth.session-token=eyJhbGciOiJ` header. Log into Cal web app using owner of organization that was created after visiting `/settings/organizations/new`, refresh swagger docs, and the cookie will be added to requests automatically to pass the NextAuthGuard.\nSecond, make sure that the logged in user has organizationId set to pass the OrganizationRolesGuard guard.", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateOAuthClientInput" + } + } + } + }, + "responses": { + "201": { + "description": "Create an OAuth client", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateOAuthClientResponseDto" + } + } + } + } + }, + "tags": [ + "OAuth - development only" + ] + }, + "get": { + "operationId": "OAuthClientsController_getOAuthClients", + "summary": "", + "description": "⚠️ First, this endpoint requires `Cookie: next-auth.session-token=eyJhbGciOiJ` header. Log into Cal web app using owner of organization that was created after visiting `/settings/organizations/new`, refresh swagger docs, and the cookie will be added to requests automatically to pass the NextAuthGuard.\nSecond, make sure that the logged in user has organizationId set to pass the OrganizationRolesGuard guard.", + "parameters": [], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetOAuthClientsResponseDto" + } + } + } + } + }, + "tags": [ + "OAuth - development only" + ] + } + }, + "/v2/oauth-clients/{clientId}": { + "get": { + "operationId": "OAuthClientsController_getOAuthClientById", + "summary": "", + "description": "⚠️ First, this endpoint requires `Cookie: next-auth.session-token=eyJhbGciOiJ` header. Log into Cal web app using owner of organization that was created after visiting `/settings/organizations/new`, refresh swagger docs, and the cookie will be added to requests automatically to pass the NextAuthGuard.\nSecond, make sure that the logged in user has organizationId set to pass the OrganizationRolesGuard guard.", + "parameters": [ + { + "name": "clientId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetOAuthClientResponseDto" + } + } + } + } + }, + "tags": [ + "OAuth - development only" + ] + }, + "patch": { + "operationId": "OAuthClientsController_updateOAuthClient", + "summary": "", + "description": "⚠️ First, this endpoint requires `Cookie: next-auth.session-token=eyJhbGciOiJ` header. Log into Cal web app using owner of organization that was created after visiting `/settings/organizations/new`, refresh swagger docs, and the cookie will be added to requests automatically to pass the NextAuthGuard.\nSecond, make sure that the logged in user has organizationId set to pass the OrganizationRolesGuard guard.", + "parameters": [ + { + "name": "clientId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateOAuthClientInput" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetOAuthClientResponseDto" + } + } + } + } + }, + "tags": [ + "OAuth - development only" + ] + }, + "delete": { + "operationId": "OAuthClientsController_deleteOAuthClient", + "summary": "", + "description": "⚠️ First, this endpoint requires `Cookie: next-auth.session-token=eyJhbGciOiJ` header. Log into Cal web app using owner of organization that was created after visiting `/settings/organizations/new`, refresh swagger docs, and the cookie will be added to requests automatically to pass the NextAuthGuard.\nSecond, make sure that the logged in user has organizationId set to pass the OrganizationRolesGuard guard.", + "parameters": [ + { + "name": "clientId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetOAuthClientResponseDto" + } + } + } + } + }, + "tags": [ + "OAuth - development only" + ] + } + }, + "/v2/oauth-clients/{clientId}/managed-users": { + "get": { + "operationId": "OAuthClientsController_getOAuthClientManagedUsersById", + "summary": "", + "description": "⚠️ First, this endpoint requires `Cookie: next-auth.session-token=eyJhbGciOiJ` header. Log into Cal web app using owner of organization that was created after visiting `/settings/organizations/new`, refresh swagger docs, and the cookie will be added to requests automatically to pass the NextAuthGuard.\nSecond, make sure that the logged in user has organizationId set to pass the OrganizationRolesGuard guard.", + "parameters": [ + { + "name": "clientId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + }, + { + "name": "limit", + "required": false, + "in": "query", + "description": "The number of items to return", + "example": 10, + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetManagedUsersOutput" + } + } + } + } + }, + "tags": [ + "OAuth - development only" + ] + } + }, + "/v2/oauth/{clientId}/authorize": { + "post": { + "operationId": "OAuthFlowController_authorize", + "summary": "Authorize an OAuth client", + "description": "Redirects the user to the specified 'redirect_uri' with an authorization code in query parameter if the client is authorized successfully. The code is then exchanged for access and refresh tokens via the `/exchange` endpoint.", + "parameters": [ + { + "name": "clientId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OAuthAuthorizeInput" + } + } + } + }, + "responses": { + "200": { + "description": "The user is redirected to the 'redirect_uri' with an authorization code in query parameter e.g. `redirectUri?code=secretcode.`" + }, + "400": { + "description": "Bad request if the OAuth client is not found, if the redirect URI is invalid, or if the user has already authorized the client." + } + }, + "tags": [ + "OAuth - development only" + ] + } + }, + "/v2/oauth/{clientId}/exchange": { + "post": { + "operationId": "OAuthFlowController_exchange", + "summary": "Exchange authorization code for access tokens", + "description": "Exchanges the authorization code received from the `/authorize` endpoint for access and refresh tokens. The authorization code should be provided in the 'Authorization' header prefixed with 'Bearer '.", + "parameters": [ + { + "name": "Authorization", + "required": true, + "in": "header", + "schema": { + "type": "string" + } + }, + { + "name": "clientId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ExchangeAuthorizationCodeInput" + } + } + } + }, + "responses": { + "200": { + "description": "Successfully exchanged authorization code for access and refresh tokens.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/KeysResponseDto" + } + } + } + }, + "400": { + "description": "Bad request if the authorization code is missing, invalid, or if the client ID and secret do not match." + } + }, + "tags": [ + "OAuth - development only" + ] + } + }, + "/v2/oauth/{clientId}/refresh": { + "post": { + "operationId": "OAuthFlowController_refreshAccessToken", + "parameters": [ + { + "name": "clientId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + }, + { + "name": "x-cal-secret-key", + "required": true, + "in": "header", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RefreshTokenInput" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/KeysResponseDto" + } + } + } + } + }, + "tags": [ + "OAuth - development only" + ] + } + }, + "/v2/event-types": { + "post": { + "operationId": "EventTypesController_2024_04_15_createEventType", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateEventTypeInput_2024_04_15" + } + } + } + }, + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateEventTypeOutput" + } + } + } + } + }, + "tags": [ + "Event types" + ] + }, + "get": { + "operationId": "EventTypesController_2024_04_15_getEventTypes", + "parameters": [], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetEventTypesOutput" + } + } + } + } + }, + "tags": [ + "Event types" + ] + } + }, + "/v2/event-types/{eventTypeId}": { + "get": { + "operationId": "EventTypesController_2024_04_15_getEventType", + "parameters": [ + { + "name": "eventTypeId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetEventTypeOutput" + } + } + } + } + }, + "tags": [ + "Event types" + ] + }, + "patch": { + "operationId": "EventTypesController_2024_04_15_updateEventType", + "parameters": [ + { + "name": "eventTypeId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateEventTypeInput_2024_04_15" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateEventTypeOutput" + } + } + } + } + }, + "tags": [ + "Event types" + ] + }, + "delete": { + "operationId": "EventTypesController_2024_04_15_deleteEventType", + "parameters": [ + { + "name": "eventTypeId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteEventTypeOutput" + } + } + } + } + }, + "tags": [ + "Event types" + ] + } + }, + "/v2/selected-calendars": { + "post": { + "operationId": "SelectedCalendarsController_addSelectedCalendar", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SelectedCalendarsInputDto" + } + } + } + }, + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SelectedCalendarOutputResponseDto" + } + } + } + } + }, + "tags": [ + "Selected-Calendars" + ] + }, + "delete": { + "operationId": "SelectedCalendarsController_removeSelectedCalendar", + "parameters": [ + { + "name": "integration", + "required": true, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "externalId", + "required": true, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "credentialId", + "required": true, + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SelectedCalendarOutputResponseDto" + } + } + } + } + }, + "tags": [ + "Selected-Calendars" + ] + } + }, + "/v2/event-types/{username}/{eventSlug}/public": { + "get": { + "operationId": "EventTypesController_2024_04_15_getPublicEventType", + "parameters": [ + { + "name": "username", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + }, + { + "name": "eventSlug", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + }, + { + "name": "isTeamEvent", + "required": false, + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "org", + "required": false, + "in": "query", + "schema": { + "nullable": true, + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetEventTypePublicOutput" + } + } + } + } + }, + "tags": [ + "Event types" + ] + } + }, + "/v2/event-types/{username}/public": { + "get": { + "operationId": "EventTypesController_2024_04_15_getPublicEventTypes", + "parameters": [ + { + "name": "username", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetEventTypesPublicOutput" + } + } + } + } + }, + "tags": [ + "Event types" + ] + } + }, + "/v2/organizations/{orgId}/teams": { + "get": { + "operationId": "OrganizationsTeamsController_getAllTeams", + "summary": "Get all the teams of an organization.", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "take", + "required": false, + "in": "query", + "description": "The number of items to return", + "example": 10, + "schema": { + "type": "number" + } + }, + { + "name": "skip", + "required": false, + "in": "query", + "description": "The number of items to skip", + "example": 0, + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrgTeamsOutputResponseDto" + } + } + } + } + }, + "tags": [ + "Organizations Teams" + ] + }, + "post": { + "operationId": "OrganizationsTeamsController_createTeam", + "summary": "Create a team for an organization.", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "x-cal-client-id", + "required": true, + "in": "header", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateOrgTeamDto" + } + } + } + }, + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrgTeamOutputResponseDto" + } + } + } + } + }, + "tags": [ + "Organizations Teams" + ] + } + }, + "/v2/organizations/{orgId}/teams/me": { + "get": { + "operationId": "OrganizationsTeamsController_getMyTeams", + "summary": "Get the organization's teams user is a member of", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "take", + "required": false, + "in": "query", + "description": "The number of items to return", + "example": 10, + "schema": { + "type": "number" + } + }, + { + "name": "skip", + "required": false, + "in": "query", + "description": "The number of items to skip", + "example": 0, + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrgMeTeamsOutputResponseDto" + } + } + } + } + }, + "tags": [ + "Organizations Teams" + ] + } + }, + "/v2/organizations/{orgId}/teams/{teamId}": { + "get": { + "operationId": "OrganizationsTeamsController_getTeam", + "summary": "Get a team of the organization by ID.", + "parameters": [], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrgTeamOutputResponseDto" + } + } + } + } + }, + "tags": [ + "Organizations Teams" + ] + }, + "delete": { + "operationId": "OrganizationsTeamsController_deleteTeam", + "summary": "Delete a team of the organization by ID.", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "teamId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrgTeamOutputResponseDto" + } + } + } + } + }, + "tags": [ + "Organizations Teams" + ] + }, + "patch": { + "operationId": "OrganizationsTeamsController_updateTeam", + "summary": "Update a team of the organization by ID.", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "teamId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateOrgTeamDto" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrgTeamOutputResponseDto" + } + } + } + } + }, + "tags": [ + "Organizations Teams" + ] + } + }, + "/v2/organizations/{orgId}/schedules": { + "get": { + "operationId": "OrganizationsSchedulesController_getOrganizationSchedules", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "take", + "required": false, + "in": "query", + "description": "The number of items to return", + "example": 10, + "schema": { + "type": "number" + } + }, + { + "name": "skip", + "required": false, + "in": "query", + "description": "The number of items to skip", + "example": 0, + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetSchedulesOutput_2024_06_11" + } + } + } + } + }, + "tags": [ + "Organizations Schedules" + ] + } + }, + "/v2/organizations/{orgId}/users/{userId}/schedules": { + "post": { + "operationId": "OrganizationsSchedulesController_createUserSchedule", + "parameters": [ + { + "name": "userId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateScheduleInput_2024_06_11" + } + } + } + }, + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateScheduleOutput_2024_06_11" + } + } + } + } + }, + "tags": [ + "Organizations Schedules" + ] + }, + "get": { + "operationId": "OrganizationsSchedulesController_getUserSchedules", + "parameters": [ + { + "name": "userId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetSchedulesOutput_2024_06_11" + } + } + } + } + }, + "tags": [ + "Organizations Schedules" + ] + } + }, + "/v2/organizations/{orgId}/users/{userId}/schedules/{scheduleId}": { + "get": { + "operationId": "OrganizationsSchedulesController_getUserSchedule", + "parameters": [ + { + "name": "userId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "scheduleId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetScheduleOutput_2024_06_11" + } + } + } + } + }, + "tags": [ + "Organizations Schedules" + ] + }, + "patch": { + "operationId": "OrganizationsSchedulesController_updateUserSchedule", + "parameters": [ + { + "name": "userId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "scheduleId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateScheduleInput_2024_06_11" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateScheduleOutput_2024_06_11" + } + } + } + } + }, + "tags": [ + "Organizations Schedules" + ] + }, + "delete": { + "operationId": "OrganizationsSchedulesController_deleteUserSchedule", + "parameters": [ + { + "name": "userId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "scheduleId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteScheduleOutput_2024_06_11" + } + } + } + } + }, + "tags": [ + "Organizations Schedules" + ] + } + }, + "/v2/organizations/{orgId}/users": { + "get": { + "operationId": "OrganizationsUsersController_getOrganizationsUsers", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "take", + "required": false, + "in": "query", + "description": "The number of items to return", + "example": 10, + "schema": { + "type": "number" + } + }, + { + "name": "skip", + "required": false, + "in": "query", + "description": "The number of items to skip", + "example": 0, + "schema": { + "type": "number" + } + }, + { + "name": "emails", + "required": false, + "in": "query", + "description": "The email address or an array of email addresses to filter by", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetOrganizationUsersOutput" + } + } + } + } + }, + "tags": [ + "Organizations Users" + ] + }, + "post": { + "operationId": "OrganizationsUsersController_createOrganizationUser", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateOrganizationUserInput" + } + } + } + }, + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetOrganizationUserOutput" + } + } + } + } + }, + "tags": [ + "Organizations Users" + ] + } + }, + "/v2/organizations/{orgId}/users/{userId}": { + "patch": { + "operationId": "OrganizationsUsersController_updateOrganizationUser", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "userId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateOrganizationUserInput" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetOrganizationUserOutput" + } + } + } + } + }, + "tags": [ + "Organizations Users" + ] + }, + "delete": { + "operationId": "OrganizationsUsersController_deleteOrganizationUser", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "userId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetOrganizationUserOutput" + } + } + } + } + }, + "tags": [ + "Organizations Users" + ] + } + }, + "/v2/organizations/{orgId}/memberships": { + "get": { + "operationId": "OrganizationsMembershipsController_getAllMemberships", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "take", + "required": false, + "in": "query", + "description": "The number of items to return", + "example": 10, + "schema": { + "type": "number" + } + }, + { + "name": "skip", + "required": false, + "in": "query", + "description": "The number of items to skip", + "example": 0, + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetAllOrgMemberships" + } + } + } + } + }, + "tags": [ + "Organizations Memberships" + ] + }, + "post": { + "operationId": "OrganizationsMembershipsController_createMembership", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateOrgMembershipDto" + } + } + } + }, + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateOrgMembershipOutput" + } + } + } + } + }, + "tags": [ + "Organizations Memberships" + ] + } + }, + "/v2/organizations/{orgId}/memberships/{membershipId}": { + "get": { + "operationId": "OrganizationsMembershipsController_getOrgMembership", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "membershipId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetOrgMembership" + } + } + } + } + }, + "tags": [ + "Organizations Memberships" + ] + }, + "delete": { + "operationId": "OrganizationsMembershipsController_deleteMembership", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "membershipId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteOrgMembership" + } + } + } + } + }, + "tags": [ + "Organizations Memberships" + ] + }, + "patch": { + "operationId": "OrganizationsMembershipsController_updateMembership", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "membershipId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateOrgMembershipDto" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateOrgMembership" + } + } + } + } + }, + "tags": [ + "Organizations Memberships" + ] + } + }, + "/v2/organizations/{orgId}/teams/{teamId}/event-types": { + "post": { + "operationId": "OrganizationsEventTypesController_createTeamEventType", + "parameters": [ + { + "name": "teamId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateTeamEventTypeInput_2024_06_14" + } + } + } + }, + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateTeamEventTypeOutput" + } + } + } + } + }, + "tags": [ + "Organizations Event Types" + ] + }, + "get": { + "operationId": "OrganizationsEventTypesController_getTeamEventTypes", + "parameters": [ + { + "name": "teamId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetTeamEventTypesOutput" + } + } + } + } + }, + "tags": [ + "Organizations Event Types" + ] + } + }, + "/v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId}": { + "get": { + "operationId": "OrganizationsEventTypesController_getTeamEventType", + "parameters": [ + { + "name": "teamId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "eventTypeId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetTeamEventTypeOutput" + } + } + } + } + }, + "tags": [ + "Organizations Event Types" + ] + }, + "patch": { + "operationId": "OrganizationsEventTypesController_updateTeamEventType", + "parameters": [ + { + "name": "teamId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "eventTypeId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateTeamEventTypeInput_2024_06_14" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateTeamEventTypeOutput" + } + } + } + } + }, + "tags": [ + "Organizations Event Types" + ] + }, + "delete": { + "operationId": "OrganizationsEventTypesController_deleteTeamEventType", + "parameters": [ + { + "name": "teamId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "eventTypeId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteTeamEventTypeOutput" + } + } + } + } + }, + "tags": [ + "Organizations Event Types" + ] + } + }, + "/v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId}/create-phone-call": { + "post": { + "operationId": "OrganizationsEventTypesController_createPhoneCall", + "parameters": [ + { + "name": "eventTypeId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreatePhoneCallInput" + } + } + } + }, + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreatePhoneCallOutput" + } + } + } + } + }, + "tags": [ + "Organizations Event Types" + ] + } + }, + "/v2/organizations/{orgId}/teams/event-types": { + "get": { + "operationId": "OrganizationsEventTypesController_getTeamsEventTypes", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "take", + "required": false, + "in": "query", + "description": "The number of items to return", + "example": 10, + "schema": { + "type": "number" + } + }, + { + "name": "skip", + "required": false, + "in": "query", + "description": "The number of items to skip", + "example": 0, + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetTeamEventTypesOutput" + } + } + } + } + }, + "tags": [ + "Organizations Event Types" + ] + } + }, + "/v2/organizations/{orgId}/teams/{teamId}/memberships": { + "get": { + "operationId": "OrganizationsTeamsMembershipsController_getAllOrgTeamMemberships", + "summary": "Get all the memberships of a team of an organization.", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "teamId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "take", + "required": false, + "in": "query", + "description": "The number of items to return", + "example": 10, + "schema": { + "type": "number" + } + }, + { + "name": "skip", + "required": false, + "in": "query", + "description": "The number of items to skip", + "example": 0, + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrgTeamMembershipsOutputResponseDto" + } + } + } + } + }, + "tags": [ + "Organizations Teams" + ] + }, + "post": { + "operationId": "OrganizationsTeamsMembershipsController_createOrgTeamMembership", + "summary": "Create a membership of an organization's team", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "teamId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateOrgTeamMembershipDto" + } + } + } + }, + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrgTeamMembershipOutputResponseDto" + } + } + } + } + }, + "tags": [ + "Organizations Teams" + ] + } + }, + "/v2/organizations/{orgId}/teams/{teamId}/memberships/{membershipId}": { + "get": { + "operationId": "OrganizationsTeamsMembershipsController_getOrgTeamMembership", + "summary": "Get the membership of an organization's team by ID", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "teamId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "membershipId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrgTeamMembershipOutputResponseDto" + } + } + } + } + }, + "tags": [ + "Organizations Teams" + ] + }, + "delete": { + "operationId": "OrganizationsTeamsMembershipsController_deleteOrgTeamMembership", + "summary": "Delete the membership of an organization's team by ID", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "teamId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "membershipId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrgTeamMembershipOutputResponseDto" + } + } + } + } + }, + "tags": [ + "Organizations Teams" + ] + }, + "patch": { + "operationId": "OrganizationsTeamsMembershipsController_updateOrgTeamMembership", + "summary": "Update the membership of an organization's team by ID", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "teamId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "membershipId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateOrgTeamMembershipDto" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrgTeamMembershipOutputResponseDto" + } + } + } + } + }, + "tags": [ + "Organizations Teams" + ] + } + }, + "/v2/organizations/{orgId}/attributes": { + "get": { + "operationId": "OrganizationsAttributesController_getOrganizationAttributes", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "take", + "required": false, + "in": "query", + "description": "The number of items to return", + "example": 10, + "schema": { + "type": "number" + } + }, + { + "name": "skip", + "required": false, + "in": "query", + "description": "The number of items to skip", + "example": 0, + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetOrganizationAttributesOutput" + } + } + } + } + } + }, + "post": { + "operationId": "OrganizationsAttributesController_createOrganizationAttribute", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateOrganizationAttributeInput" + } + } + } + }, + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateOrganizationAttributesOutput" + } + } + } + } + } + } + }, + "/v2/organizations/{orgId}/attributes/{attributeId}": { + "get": { + "operationId": "OrganizationsAttributesController_getOrganizationAttribute", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "attributeId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetSingleAttributeOutput" + } + } + } + } + } + }, + "patch": { + "operationId": "OrganizationsAttributesController_updateOrganizationAttribute", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "attributeId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateOrganizationAttributeInput" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateOrganizationAttributesOutput" + } + } + } + } + } + }, + "delete": { + "operationId": "OrganizationsAttributesController_deleteOrganizationAttribute", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "attributeId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteOrganizationAttributesOutput" + } + } + } + } + } + } + }, + "/v2/organizations/{orgId}/attributes/{attributeId}/options": { + "post": { + "operationId": "OrganizationsOptionsAttributesController_createOrganizationAttributeOption", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "attributeId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateOrganizationAttributeOptionInput" + } + } + } + }, + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateAttributeOptionOutput" + } + } + } + } + } + }, + "get": { + "operationId": "OrganizationsOptionsAttributesController_getOrganizationAttributeOptions", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "attributeId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetAllAttributeOptionOutput" + } + } + } + } + } + } + }, + "/v2/organizations/{orgId}/attributes/{attributeId}/options/{optionId}": { + "delete": { + "operationId": "OrganizationsOptionsAttributesController_deleteOrganizationAttributeOption", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "attributeId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + }, + { + "name": "optionId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteAttributeOptionOutput" + } + } + } + } + } + }, + "patch": { + "operationId": "OrganizationsOptionsAttributesController_updateOrganizationAttributeOption", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "attributeId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + }, + { + "name": "optionId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateOrganizationAttributeOptionInput" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateAttributeOptionOutput" + } + } + } + } + } + } + }, + "/v2/organizations/{orgId}/attributes/options/{userId}": { + "post": { + "operationId": "OrganizationsOptionsAttributesController_assignOrganizationAttributeOptionToUser", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "userId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AssignOrganizationAttributeOptionToUserInput" + } + } + } + }, + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AssignOptionUserOutput" + } + } + } + } + } + }, + "get": { + "operationId": "OrganizationsOptionsAttributesController_getOrganizationAttributeOptionsForUser", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "userId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetOptionUserOutput" + } + } + } + } + } + } + }, + "/v2/organizations/{orgId}/attributes/options/{userId}/{attributeOptionId}": { + "delete": { + "operationId": "OrganizationsOptionsAttributesController_unassignOrganizationAttributeOptionFromUser", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "userId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "attributeOptionId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnassignOptionUserOutput" + } + } + } + } + } + } + }, + "/v2/organizations/{orgId}/webhooks": { + "get": { + "operationId": "OrganizationsWebhooksController_getAllOrganizationWebhooks", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "take", + "required": false, + "in": "query", + "description": "The number of items to return", + "example": 10, + "schema": { + "type": "number" + } + }, + { + "name": "skip", + "required": false, + "in": "query", + "description": "The number of items to skip", + "example": 0, + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TeamWebhooksOutputResponseDto" + } + } + } + } + }, + "tags": [ + "Organizations Webhooks" + ] + }, + "post": { + "operationId": "OrganizationsWebhooksController_createOrganizationWebhook", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateWebhookInputDto" + } + } + } + }, + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TeamWebhookOutputResponseDto" + } + } + } + } + }, + "tags": [ + "Organizations Webhooks" + ] + } + }, + "/v2/organizations/{orgId}/webhooks/{webhookId}": { + "get": { + "operationId": "OrganizationsWebhooksController_getOrganizationWebhook", + "parameters": [ + { + "name": "webhookId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TeamWebhookOutputResponseDto" + } + } + } + } + }, + "tags": [ + "Organizations Webhooks" + ] + }, + "delete": { + "operationId": "OrganizationsWebhooksController_deleteWebhook", + "parameters": [ + { + "name": "webhookId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TeamWebhookOutputResponseDto" + } + } + } + } + }, + "tags": [ + "Organizations Webhooks" + ] + }, + "patch": { + "operationId": "OrganizationsWebhooksController_updateOrgWebhook", + "parameters": [ + { + "name": "webhookId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateWebhookInputDto" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TeamWebhookOutputResponseDto" + } + } + } + } + }, + "tags": [ + "Organizations Webhooks" + ] + } + }, + "/v2/schedules": { + "post": { + "operationId": "SchedulesController_2024_04_15_createSchedule", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateScheduleInput_2024_04_15" + } + } + } + }, + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateScheduleOutput_2024_04_15" + } + } + } + } + }, + "tags": [ + "Schedules" + ] + }, + "get": { + "operationId": "SchedulesController_2024_04_15_getSchedules", + "parameters": [], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetSchedulesOutput_2024_04_15" + } + } + } + } + }, + "tags": [ + "Schedules" + ] + } + }, + "/v2/schedules/default": { + "get": { + "operationId": "SchedulesController_2024_04_15_getDefaultSchedule", + "parameters": [], + "responses": { + "200": { + "description": "Returns the default schedule", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetDefaultScheduleOutput_2024_04_15" + } + } + } + } + }, + "tags": [ + "Schedules" + ] + } + }, + "/v2/schedules/{scheduleId}": { + "get": { + "operationId": "SchedulesController_2024_04_15_getSchedule", + "parameters": [ + { + "name": "scheduleId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetScheduleOutput_2024_04_15" + } + } + } + } + }, + "tags": [ + "Schedules" + ] + }, + "patch": { + "operationId": "SchedulesController_2024_04_15_updateSchedule", + "parameters": [ + { + "name": "scheduleId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateScheduleInput_2024_04_15" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateScheduleOutput_2024_04_15" + } + } + } + } + }, + "tags": [ + "Schedules" + ] + }, + "delete": { + "operationId": "SchedulesController_2024_04_15_deleteSchedule", + "parameters": [ + { + "name": "scheduleId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteScheduleOutput_2024_04_15" + } + } + } + } + }, + "tags": [ + "Schedules" + ] + } + }, + "/v2/gcal/oauth/auth-url": { + "get": { + "operationId": "GcalController_redirect", + "parameters": [ + { + "name": "Authorization", + "required": true, + "in": "header", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GcalAuthUrlOutput" + } + } + } + } + }, + "tags": [ + "Google Calendar" + ] + } + }, + "/v2/gcal/oauth/save": { + "get": { + "operationId": "GcalController_save", + "parameters": [ + { + "name": "state", + "required": true, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "code", + "required": true, + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GcalSaveRedirectOutput" + } + } + } + } + }, + "tags": [ + "Google Calendar" + ] + } + }, + "/v2/gcal/check": { + "get": { + "operationId": "GcalController_check", + "parameters": [], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GcalCheckOutput" + } + } + } + } + }, + "tags": [ + "Google Calendar" + ] + } + }, + "/v2/provider/{clientId}": { + "get": { + "operationId": "CalProviderController_verifyClientId", + "parameters": [ + { + "name": "clientId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProviderVerifyClientOutput" + } + } + } + } + }, + "tags": [ + "Cal provider" + ] + } + }, + "/v2/provider/{clientId}/access-token": { + "get": { + "operationId": "CalProviderController_verifyAccessToken", + "parameters": [ + { + "name": "clientId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProviderVerifyAccessTokenOutput" + } + } + } + } + }, + "tags": [ + "Cal provider" + ] + } + }, + "/v2/me": { + "get": { + "operationId": "MeController_getMe", + "parameters": [], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetMeOutput" + } + } + } + } + }, + "tags": [ + "Me" + ] + }, + "patch": { + "operationId": "MeController_updateMe", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateManagedUserInput" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateMeOutput" + } + } + } + } + }, + "tags": [ + "Me" + ] + } + }, + "/v2/calendars/busy-times": { + "get": { + "operationId": "CalendarsController_getBusyTimes", + "parameters": [ + { + "name": "loggedInUsersTz", + "required": true, + "in": "query", + "description": "The timezone of the logged in user represented as a string", + "example": "America/New_York", + "schema": { + "type": "string" + } + }, + { + "name": "calendarsToLoad", + "required": true, + "in": "query", + "description": "An array of Calendar objects representing the calendars to be loaded", + "example": "[{ credentialId: \"1\", externalId: \"AQgtJE7RnHEeyisVq2ENs2gAAAgEGAAAACgtJE7RnHEeyisVq2ENs2gAAAhSDAAAA\" }, { credentialId: \"2\", externalId: \"AQM7RnHEeyisVq2ENs2gAAAhFDBBBBB\" }]", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetBusyTimesOutput" + } + } + } + } + }, + "tags": [ + "Calendars" + ] + } + }, + "/v2/calendars": { + "get": { + "operationId": "CalendarsController_getCalendars", + "parameters": [], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConnectedCalendarsOutput" + } + } + } + } + }, + "tags": [ + "Calendars" + ] + } + }, + "/v2/calendars/{calendar}/connect": { + "get": { + "operationId": "CalendarsController_redirect", + "parameters": [ + { + "name": "Authorization", + "required": true, + "in": "header", + "schema": { + "type": "string" + } + }, + { + "name": "calendar", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + } + }, + "tags": [ + "Calendars" + ] + } + }, + "/v2/calendars/{calendar}/save": { + "get": { + "operationId": "CalendarsController_save", + "parameters": [ + { + "name": "state", + "required": true, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "code", + "required": true, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "calendar", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "" + } + }, + "tags": [ + "Calendars" + ] + } + }, + "/v2/calendars/{calendar}/credentials": { + "post": { + "operationId": "CalendarsController_syncCredentials", + "parameters": [ + { + "name": "calendar", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "201": { + "description": "" + } + }, + "tags": [ + "Calendars" + ] + } + }, + "/v2/calendars/{calendar}/check": { + "get": { + "operationId": "CalendarsController_check", + "parameters": [ + { + "name": "calendar", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + } + }, + "tags": [ + "Calendars" + ] + } + }, + "/v2/calendars/{calendar}/disconnect": { + "post": { + "operationId": "CalendarsController_deleteCalendarCredentials", + "parameters": [ + { + "name": "calendar", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteCalendarCredentialsInputBodyDto" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeletedCalendarCredentialsOutputResponseDto" + } + } + } + } + }, + "tags": [ + "Calendars" + ] + } + }, + "/v2/bookings": { + "post": { + "operationId": "BookingsController_2024_08_13_createBooking", + "summary": "Create booking", + "description": "\n POST /v2/bookings is used to create regular bookings, recurring bookings and instant bookings. The request bodies for all 3 are almost the same except:\n If eventTypeId in the request body is id of a regular event, then regular booking is created.\n\n If it is an id of a recurring event type, then recurring booking is created.\n\n Meaning that the request bodies are equal but the outcome depends on what kind of event type it is with the goal of making it as seamless for developers as possible.\n\n For team event types it is possible to create instant meeting. To do that just pass `\"instant\": true` to the request body.\n \n The start needs to be in UTC aka if the timezone is GMT+2 in Rome and meeting should start at 11, then UTC time should have hours 09:00 aka without time zone.\n ", + "parameters": [ + { + "name": "cal-api-version", + "in": "header", + "description": "Must be set to `2024-08-13`", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "description": "Accepts different types of booking input: CreateBookingInput_2024_08_13, CreateInstantBookingInput_2024_08_13, or CreateRecurringBookingInput_2024_08_13", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/CreateBookingInput_2024_08_13" + }, + { + "$ref": "#/components/schemas/CreateInstantBookingInput_2024_08_13" + }, + { + "$ref": "#/components/schemas/CreateRecurringBookingInput_2024_08_13" + } + ] + } + } + } + }, + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateBookingOutput_2024_08_13" + } + } + } + } + }, + "tags": [ + "Bookings" + ] + }, + "get": { + "operationId": "BookingsController_2024_08_13_getBookings", + "parameters": [ + { + "name": "cal-api-version", + "in": "header", + "description": "Must be set to `2024-08-13`", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "status", + "required": false, + "in": "query", + "description": "Filter bookings by status. If you want to filter by multiple statuses, separate them with a comma.", + "example": "?status=upcoming,past", + "schema": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "upcoming", + "recurring", + "past", + "cancelled", + "unconfirmed" + ] + } + } + }, + { + "name": "attendeeEmail", + "required": false, + "in": "query", + "description": "Filter bookings by the attendee's email address.", + "example": "example@domain.com", + "schema": { + "type": "string" + } + }, + { + "name": "attendeeName", + "required": false, + "in": "query", + "description": "Filter bookings by the attendee's name.", + "example": "John Doe", + "schema": { + "type": "string" + } + }, + { + "name": "eventTypeIds", + "required": false, + "in": "query", + "description": "Filter bookings by event type ids belonging to the user. Event type ids must be separated by a comma.", + "example": "?eventTypeIds=100,200", + "schema": { + "type": "string" + } + }, + { + "name": "eventTypeId", + "required": false, + "in": "query", + "description": "Filter bookings by event type id belonging to the user.", + "example": "?eventTypeId=100", + "schema": { + "type": "string" + } + }, + { + "name": "teamsIds", + "required": false, + "in": "query", + "description": "Filter bookings by team ids that user is part of. Team ids must be separated by a comma.", + "example": "?teamIds=50,60", + "schema": { + "type": "string" + } + }, + { + "name": "teamId", + "required": false, + "in": "query", + "description": "Filter bookings by team id that user is part of", + "example": "?teamId=50", + "schema": { + "type": "string" + } + }, + { + "name": "afterStart", + "required": false, + "in": "query", + "description": "Filter bookings with start after this date string.", + "example": "?afterStart=2025-03-07T10:00:00.000Z", + "schema": { + "type": "string" + } + }, + { + "name": "beforeEnd", + "required": false, + "in": "query", + "description": "Filter bookings with end before this date string.", + "example": "?beforeEnd=2025-03-07T11:00:00.000Z", + "schema": { + "type": "string" + } + }, + { + "name": "sortStart", + "required": false, + "in": "query", + "description": "Sort results by their start time in ascending or descending order.", + "example": "?sortStart=asc OR ?sortStart=desc", + "schema": { + "enum": [ + "asc", + "desc" + ], + "type": "string" + } + }, + { + "name": "sortEnd", + "required": false, + "in": "query", + "description": "Sort results by their end time in ascending or descending order.", + "example": "?sortEnd=asc OR ?sortEnd=desc", + "schema": { + "enum": [ + "asc", + "desc" + ], + "type": "string" + } + }, + { + "name": "sortCreated", + "required": false, + "in": "query", + "description": "Sort results by their creation time (when booking was made) in ascending or descending order.", + "example": "?sortEnd=asc OR ?sortEnd=desc", + "schema": { + "enum": [ + "asc", + "desc" + ], + "type": "string" + } + }, + { + "name": "take", + "required": false, + "in": "query", + "description": "The number of items to return", + "example": 10, + "schema": { + "type": "number" + } + }, + { + "name": "skip", + "required": false, + "in": "query", + "description": "The number of items to skip", + "example": 0, + "schema": { + "type": "number" + } + }, + { + "name": "Authorization", + "in": "header", + "description": "value must be `Bearer ` where `` either managed user access token or api key prefixed with cal_", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetBookingsOutput_2024_08_13" + } + } + } + } + }, + "tags": [ + "Bookings" + ] + } + }, + "/v2/bookings/{bookingUid}": { + "get": { + "operationId": "BookingsController_2024_08_13_getBooking", + "summary": "Get booking", + "description": "`:bookingUid` can be\n \n 1. uid of a normal booking\n \n 2. uid of one of the recurring booking recurrences\n \n 3. uid of recurring booking which will return an array of all recurring booking recurrences (stored as recurringBookingUid on one of the individual recurrences).", + "parameters": [ + { + "name": "cal-api-version", + "in": "header", + "description": "Must be set to `2024-08-13`", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "bookingUid", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetBookingOutput_2024_08_13" + } + } + } + } + }, + "tags": [ + "Bookings" + ] + } + }, + "/v2/bookings/{bookingUid}/reschedule": { + "post": { + "operationId": "BookingsController_2024_08_13_rescheduleBooking", + "summary": "Reschedule booking", + "description": "Reschedule a booking by passing `:bookingUid` of the booking that should be rescheduled and pass request body with a new start time to create a new booking.", + "parameters": [ + { + "name": "cal-api-version", + "in": "header", + "description": "Must be set to `2024-08-13`", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "bookingUid", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RescheduleBookingInput_2024_08_13" + } + } + } + }, + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RescheduleBookingOutput_2024_08_13" + } + } + } + } + }, + "tags": [ + "Bookings" + ] + } + }, + "/v2/bookings/{bookingUid}/cancel": { + "post": { + "operationId": "BookingsController_2024_08_13_cancelBooking", + "parameters": [ + { + "name": "cal-api-version", + "in": "header", + "description": "Must be set to `2024-08-13`", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "bookingUid", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CancelBookingInput_2024_08_13" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CancelBookingOutput_2024_08_13" + } + } + } + } + }, + "tags": [ + "Bookings" + ] + } + }, + "/v2/bookings/{bookingUid}/mark-absent": { + "post": { + "operationId": "BookingsController_2024_08_13_markNoShow", + "parameters": [ + { + "name": "cal-api-version", + "in": "header", + "description": "Must be set to `2024-08-13`", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "bookingUid", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + }, + { + "name": "Authorization", + "in": "header", + "description": "value must be `Bearer ` where `` either managed user access token or api key prefixed with cal_", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MarkAbsentBookingInput_2024_08_13" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MarkAbsentBookingOutput_2024_08_13" + } + } + } + } + }, + "tags": [ + "Bookings" + ] + } + }, + "/v2/slots/reserve": { + "post": { + "operationId": "SlotsController_reserveSlot", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ReserveSlotInput" + } + } + } + }, + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + } + }, + "tags": [ + "Slots" + ] + } + }, + "/v2/slots/selected-slot": { + "delete": { + "operationId": "SlotsController_deleteSelectedSlot", + "parameters": [], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + } + }, + "tags": [ + "Slots" + ] + } + }, + "/v2/slots/available": { + "get": { + "operationId": "SlotsController_getAvailableSlots", + "parameters": [], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + } + }, + "tags": [ + "Slots" + ] + } + }, + "/v2/timezones": { + "get": { + "operationId": "TimezonesController_getTimeZones", + "parameters": [], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + } + }, + "tags": [ + "Timezones" + ] + } + }, + "/v2/webhooks": { + "post": { + "operationId": "WebhooksController_createWebhook", + "summary": "Create a webhook", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateWebhookInputDto" + } + } + } + }, + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserWebhookOutputResponseDto" + } + } + } + } + }, + "tags": [ + "Users' Webhooks" + ] + }, + "get": { + "operationId": "WebhooksController_getWebhooks", + "summary": "Get all user webhooks paginated", + "parameters": [ + { + "name": "take", + "required": false, + "in": "query", + "description": "The number of items to return", + "example": 10, + "schema": { + "type": "number" + } + }, + { + "name": "skip", + "required": false, + "in": "query", + "description": "The number of items to skip", + "example": 0, + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserWebhooksOutputResponseDto" + } + } + } + } + }, + "tags": [ + "Users' Webhooks" + ] + } + }, + "/v2/webhooks/{webhookId}": { + "patch": { + "operationId": "WebhooksController_updateWebhook", + "summary": "Update a webhook", + "parameters": [ + { + "name": "webhookId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateWebhookInputDto" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserWebhookOutputResponseDto" + } + } + } + } + }, + "tags": [ + "Users' Webhooks" + ] + }, + "get": { + "operationId": "WebhooksController_getWebhook", + "summary": "Get a webhook", + "parameters": [], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserWebhookOutputResponseDto" + } + } + } + } + }, + "tags": [ + "Users' Webhooks" + ] + }, + "delete": { + "operationId": "WebhooksController_deleteWebhook", + "summary": "Delete a webhook", + "parameters": [ + { + "name": "webhookId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserWebhookOutputResponseDto" + } + } + } + } + }, + "tags": [ + "Users' Webhooks" + ] + } + }, + "/v2/event-types/{eventTypeId}/webhooks": { + "post": { + "operationId": "EventTypeWebhooksController_createEventTypeWebhook", + "summary": "Create a webhook for an event-type", + "parameters": [ + { + "name": "eventTypeId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateWebhookInputDto" + } + } + } + }, + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EventTypeWebhookOutputResponseDto" + } + } + } + } + }, + "tags": [ + "Users' EventTypes Webhooks" + ] + }, + "get": { + "operationId": "EventTypeWebhooksController_getEventTypeWebhooks", + "summary": "Get all webhooks of an event-type", + "parameters": [ + { + "name": "eventTypeId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "take", + "required": false, + "in": "query", + "description": "The number of items to return", + "example": 10, + "schema": { + "type": "number" + } + }, + { + "name": "skip", + "required": false, + "in": "query", + "description": "The number of items to skip", + "example": 0, + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EventTypeWebhooksOutputResponseDto" + } + } + } + } + }, + "tags": [ + "Users' EventTypes Webhooks" + ] + }, + "delete": { + "operationId": "EventTypeWebhooksController_deleteAllEventTypeWebhooks", + "summary": "Delete all webhooks of an event-type", + "parameters": [ + { + "name": "eventTypeId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteManyWebhooksOutputResponseDto" + } + } + } + } + }, + "tags": [ + "Users' EventTypes Webhooks" + ] + } + }, + "/v2/event-types/{eventTypeId}/webhooks/{webhookId}": { + "patch": { + "operationId": "EventTypeWebhooksController_updateEventTypeWebhook", + "summary": "Update a webhook of an event-type", + "parameters": [ + { + "name": "webhookId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateWebhookInputDto" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EventTypeWebhookOutputResponseDto" + } + } + } + } + }, + "tags": [ + "Users' EventTypes Webhooks" + ] + }, + "get": { + "operationId": "EventTypeWebhooksController_getEventTypeWebhook", + "summary": "Get a webhook of an event-type", + "parameters": [], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EventTypeWebhookOutputResponseDto" + } + } + } + } + }, + "tags": [ + "Users' EventTypes Webhooks" + ] + }, + "delete": { + "operationId": "EventTypeWebhooksController_deleteEventTypeWebhook", + "summary": "Delete a webhook of an event-type", + "parameters": [], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EventTypeWebhookOutputResponseDto" + } + } + } + } + }, + "tags": [ + "Users' EventTypes Webhooks" + ] + } + }, + "/v2/oauth-clients/{clientId}/webhooks": { + "post": { + "operationId": "OAuthClientWebhooksController_createOAuthClientWebhook", + "summary": "Create a webhook for an oAuthClient", + "parameters": [ + { + "name": "clientId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateWebhookInputDto" + } + } + } + }, + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OAuthClientWebhookOutputResponseDto" + } + } + } + } + }, + "tags": [ + "OAuthClients Webhooks" + ] + }, + "get": { + "operationId": "OAuthClientWebhooksController_getOAuthClientWebhooks", + "summary": "Get all webhooks of an oAuthClient", + "parameters": [ + { + "name": "clientId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + }, + { + "name": "take", + "required": false, + "in": "query", + "description": "The number of items to return", + "example": 10, + "schema": { + "type": "number" + } + }, + { + "name": "skip", + "required": false, + "in": "query", + "description": "The number of items to skip", + "example": 0, + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OAuthClientWebhooksOutputResponseDto" + } + } + } + } + }, + "tags": [ + "OAuthClients Webhooks" + ] + }, + "delete": { + "operationId": "OAuthClientWebhooksController_deleteAllOAuthClientWebhooks", + "summary": "Delete all webhooks of an oAuthClient", + "parameters": [ + { + "name": "clientId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteManyWebhooksOutputResponseDto" + } + } + } + } + }, + "tags": [ + "OAuthClients Webhooks" + ] + } + }, + "/v2/oauth-clients/{clientId}/webhooks/{webhookId}": { + "patch": { + "operationId": "OAuthClientWebhooksController_updateOAuthClientWebhook", + "summary": "Update a webhook of an oAuthClient", + "parameters": [ + { + "name": "webhookId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateWebhookInputDto" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OAuthClientWebhookOutputResponseDto" + } + } + } + } + }, + "tags": [ + "OAuthClients Webhooks" + ] + }, + "get": { + "operationId": "OAuthClientWebhooksController_getOAuthClientWebhook", + "summary": "Get a webhook of an oAuthClient", + "parameters": [], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OAuthClientWebhookOutputResponseDto" + } + } + } + } + }, + "tags": [ + "OAuthClients Webhooks" + ] + }, + "delete": { + "operationId": "OAuthClientWebhooksController_deleteOAuthClientWebhook", + "summary": "Delete a webhook of an oAuthClient", + "parameters": [], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OAuthClientWebhookOutputResponseDto" + } + } + } + } + }, + "tags": [ + "OAuthClients Webhooks" + ] + } + }, + "/v2/destination-calendars": { + "put": { + "operationId": "DestinationCalendarsController_updateDestinationCalendars", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DestinationCalendarsInputBodyDto" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DestinationCalendarsOutputResponseDto" + } + } + } + } + }, + "tags": [ + "Destination-Calendars", + "Select a third party destination calendar where events will be created" + ] + } + } + }, + "info": { + "title": "Cal.com v2 API", + "description": "", + "version": "1.0.0", + "contact": {} + }, + "tags": [], + "servers": [], + "components": { + "schemas": { + "ManagedUserOutput": { + "type": "object", + "properties": { + "id": { + "type": "number", + "example": 1 + }, + "email": { + "type": "string", + "example": "alice+cluo37fwd0001khkzqqynkpj3@example.com" + }, + "username": { + "type": "string", + "nullable": true, + "example": "alice" + }, + "timeZone": { + "type": "string", + "example": "America/New_York" + }, + "weekStart": { + "type": "string", + "example": "Sunday" + }, + "createdDate": { + "type": "string", + "example": "2024-04-01T00:00:00.000Z" + }, + "timeFormat": { + "type": "number", + "nullable": true, + "example": 12 + }, + "defaultScheduleId": { + "type": "number", + "nullable": true, + "example": null + }, + "locale": { + "enum": [ + "ar", + "ca", + "de", + "es", + "eu", + "he", + "id", + "ja", + "lv", + "pl", + "ro", + "sr", + "th", + "vi", + "az", + "cs", + "el", + "es-419", + "fi", + "hr", + "it", + "km", + "nl", + "pt", + "ru", + "sv", + "tr", + "zh-CN", + "bg", + "da", + "en", + "et", + "fr", + "hu", + "iw", + "ko", + "no", + "pt-BR", + "sk", + "ta", + "uk", + "zh-TW" + ], + "type": "string", + "example": "en" + } + }, + "required": [ + "id", + "email", + "username", + "timeZone", + "weekStart", + "createdDate", + "timeFormat", + "defaultScheduleId" + ] + }, + "GetManagedUsersOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ManagedUserOutput" + } + } + }, + "required": [ + "status", + "data" + ] + }, + "CreateManagedUserInput": { + "type": "object", + "properties": { + "email": { + "type": "string", + "example": "alice@example.com" + }, + "timeFormat": { + "type": "number", + "example": 12, + "enum": [ + 12, + 24 + ], + "description": "Must be 12 or 24" + }, + "weekStart": { + "type": "string", + "example": "Monday", + "enum": [ + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday", + "Sunday" + ] + }, + "timeZone": { + "type": "string", + "example": "America/New_York" + }, + "locale": { + "enum": [ + "ar", + "ca", + "de", + "es", + "eu", + "he", + "id", + "ja", + "lv", + "pl", + "ro", + "sr", + "th", + "vi", + "az", + "cs", + "el", + "es-419", + "fi", + "hr", + "it", + "km", + "nl", + "pt", + "ru", + "sv", + "tr", + "zh-CN", + "bg", + "da", + "en", + "et", + "fr", + "hu", + "iw", + "ko", + "no", + "pt-BR", + "sk", + "ta", + "uk", + "zh-TW" + ], + "type": "string", + "example": "en" + }, + "name": { + "type": "string" + } + }, + "required": [ + "email" + ] + }, + "CreateManagedUserData": { + "type": "object", + "properties": { + "user": { + "$ref": "#/components/schemas/ManagedUserOutput" + }, + "accessToken": { + "type": "string" + }, + "refreshToken": { + "type": "string" + }, + "accessTokenExpiresAt": { + "type": "number" + } + }, + "required": [ + "user", + "accessToken", + "refreshToken", + "accessTokenExpiresAt" + ] + }, + "CreateManagedUserOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/CreateManagedUserData" + } + }, + "required": [ + "status", + "data" + ] + }, + "GetManagedUserOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/ManagedUserOutput" + } + }, + "required": [ + "status", + "data" + ] + }, + "UpdateManagedUserInput": { + "type": "object", + "properties": { + "timeFormat": { + "type": "number", + "enum": [ + 12, + 24 + ], + "example": 12, + "description": "Must be 12 or 24" + }, + "weekStart": { + "type": "string", + "enum": [ + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday", + "Sunday" + ], + "example": "Monday" + }, + "locale": { + "enum": [ + "ar", + "ca", + "de", + "es", + "eu", + "he", + "id", + "ja", + "lv", + "pl", + "ro", + "sr", + "th", + "vi", + "az", + "cs", + "el", + "es-419", + "fi", + "hr", + "it", + "km", + "nl", + "pt", + "ru", + "sv", + "tr", + "zh-CN", + "bg", + "da", + "en", + "et", + "fr", + "hu", + "iw", + "ko", + "no", + "pt-BR", + "sk", + "ta", + "uk", + "zh-TW" + ], + "type": "string", + "example": "en" + }, + "email": { + "type": "string" + }, + "name": { + "type": "string" + }, + "defaultScheduleId": { + "type": "number" + }, + "timeZone": { + "type": "string" + } + } + }, + "KeysDto": { + "type": "object", + "properties": { + "accessToken": { + "type": "string", + "example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" + }, + "refreshToken": { + "type": "string", + "example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" + }, + "accessTokenExpiresAt": { + "type": "number" + } + }, + "required": [ + "accessToken", + "refreshToken", + "accessTokenExpiresAt" + ] + }, + "KeysResponseDto": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/KeysDto" + } + }, + "required": [ + "status", + "data" + ] + }, + "CreateOAuthClientInput": { + "type": "object", + "properties": {} + }, + "DataDto": { + "type": "object", + "properties": { + "clientId": { + "type": "string", + "example": "clsx38nbl0001vkhlwin9fmt0" + }, + "clientSecret": { + "type": "string", + "example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoib2F1dGgtY2xpZW50Iiwi" + } + }, + "required": [ + "clientId", + "clientSecret" + ] + }, + "CreateOAuthClientResponseDto": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "success", + "error" + ], + "example": "success" + }, + "data": { + "example": { + "clientId": "clsx38nbl0001vkhlwin9fmt0", + "clientSecret": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoib2F1dGgtY2xpZW50Iiwi" + }, + "allOf": [ + { + "$ref": "#/components/schemas/DataDto" + } + ] + } + }, + "required": [ + "status", + "data" + ] + }, + "PlatformOAuthClientDto": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "clsx38nbl0001vkhlwin9fmt0" + }, + "name": { + "type": "string", + "example": "MyClient" + }, + "secret": { + "type": "string", + "example": "secretValue" + }, + "permissions": { + "type": "number", + "example": 3 + }, + "logo": { + "type": "string", + "nullable": true, + "example": "https://example.com/logo.png" + }, + "redirectUris": { + "example": [ + "https://example.com/callback" + ], + "type": "array", + "items": { + "type": "string" + } + }, + "organizationId": { + "type": "number", + "example": 1 + }, + "createdAt": { + "format": "date-time", + "type": "string", + "example": "2024-03-23T08:33:21.851Z" + } + }, + "required": [ + "id", + "name", + "secret", + "permissions", + "redirectUris", + "organizationId", + "createdAt" + ] + }, + "GetOAuthClientsResponseDto": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PlatformOAuthClientDto" + } + } + }, + "required": [ + "status", + "data" + ] + }, + "GetOAuthClientResponseDto": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/PlatformOAuthClientDto" + } + }, + "required": [ + "status", + "data" + ] + }, + "UpdateOAuthClientInput": { + "type": "object", + "properties": { + "logo": { + "type": "string" + }, + "name": { + "type": "string" + }, + "redirectUris": { + "default": [], + "type": "array", + "items": { + "type": "string" + } + }, + "bookingRedirectUri": { + "type": "string" + }, + "bookingCancelRedirectUri": { + "type": "string" + }, + "bookingRescheduleRedirectUri": { + "type": "string" + }, + "areEmailsEnabled": { + "type": "boolean" + } + } + }, + "OAuthAuthorizeInput": { + "type": "object", + "properties": { + "redirectUri": { + "type": "string" + } + }, + "required": [ + "redirectUri" + ] + }, + "ExchangeAuthorizationCodeInput": { + "type": "object", + "properties": { + "clientSecret": { + "type": "string" + } + }, + "required": [ + "clientSecret" + ] + }, + "RefreshTokenInput": { + "type": "object", + "properties": { + "refreshToken": { + "type": "string" + } + }, + "required": [ + "refreshToken" + ] + }, + "CreateEventTypeInput_2024_06_14": { + "type": "object", + "properties": { + "lengthInMinutes": { + "type": "number", + "example": 60 + }, + "title": { + "type": "string", + "example": "Learn the secrets of masterchief!" + }, + "description": { + "type": "string", + "example": "Discover the culinary wonders of the Argentina by making the best flan ever!" + } + }, + "required": [ + "lengthInMinutes", + "title", + "description" + ] + }, + "EventTypeOutput_2024_06_14": { + "type": "object", + "properties": { + "id": { + "type": "number", + "example": 1 + } + }, + "required": [ + "id" + ] + }, + "CreateEventTypeOutput_2024_06_14": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "success", + "error" + ], + "example": "success" + }, + "data": { + "$ref": "#/components/schemas/EventTypeOutput_2024_06_14" + } + }, + "required": [ + "status", + "data" + ] + }, + "GetEventTypeOutput_2024_06_14": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "success", + "error" + ], + "example": "success" + }, + "data": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/EventTypeOutput_2024_06_14" + } + ] + } + }, + "required": [ + "status", + "data" + ] + }, + "GetEventTypesOutput_2024_06_14": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "success", + "error" + ], + "example": "success" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/EventTypeOutput_2024_06_14" + } + } + }, + "required": [ + "status", + "data" + ] + }, + "UpdateEventTypeInput_2024_06_14": { + "type": "object", + "properties": {} + }, + "UpdateEventTypeOutput_2024_06_14": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "success", + "error" + ], + "example": "success" + }, + "data": { + "$ref": "#/components/schemas/EventTypeOutput_2024_06_14" + } + }, + "required": [ + "status", + "data" + ] + }, + "DeleteData_2024_06_14": { + "type": "object", + "properties": { + "id": { + "type": "number", + "example": 1 + }, + "lengthInMinutes": { + "type": "number", + "example": 60 + }, + "title": { + "type": "string", + "example": "Learn the secrets of masterchief!" + }, + "slug": { + "type": "string" + } + }, + "required": [ + "id", + "lengthInMinutes", + "title", + "slug" + ] + }, + "DeleteEventTypeOutput_2024_06_14": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "success", + "error" + ], + "example": "success" + }, + "data": { + "$ref": "#/components/schemas/DeleteData_2024_06_14" + } + }, + "required": [ + "status", + "data" + ] + }, + "SelectedCalendarsInputDto": { + "type": "object", + "properties": { + "integration": { + "type": "string" + }, + "externalId": { + "type": "string" + }, + "credentialId": { + "type": "number" + } + }, + "required": [ + "integration", + "externalId", + "credentialId" + ] + }, + "SelectedCalendarOutputDto": { + "type": "object", + "properties": { + "userId": { + "type": "number" + }, + "integration": { + "type": "string" + }, + "externalId": { + "type": "string" + }, + "credentialId": { + "type": "number", + "nullable": true + } + }, + "required": [ + "userId", + "integration", + "externalId", + "credentialId" + ] + }, + "SelectedCalendarOutputResponseDto": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/SelectedCalendarOutputDto" + } + }, + "required": [ + "status", + "data" + ] + }, + "EventTypeLocation_2024_04_15": { + "type": "object", + "properties": { + "type": { + "type": "string", + "example": "link" + }, + "link": { + "type": "string", + "example": "https://masterchief.com/argentina/flan/video/9129412" + } + }, + "required": [ + "type" + ] + }, + "CreateEventTypeInput_2024_04_15": { + "type": "object", + "properties": { + "length": { + "type": "number", + "minimum": 1, + "example": 60 + }, + "slug": { + "type": "string", + "example": "cooking-class" + }, + "title": { + "type": "string", + "example": "Learn the secrets of masterchief!" + }, + "description": { + "type": "string", + "example": "Discover the culinary wonders of the Argentina by making the best flan ever!" + }, + "locations": { + "type": "array", + "items": { + "$ref": "#/components/schemas/EventTypeLocation_2024_04_15" + } + }, + "disableGuests": { + "type": "boolean" + }, + "slotInterval": { + "type": "number", + "minimum": 0 + }, + "minimumBookingNotice": { + "type": "number", + "minimum": 0 + }, + "beforeEventBuffer": { + "type": "number", + "minimum": 0 + }, + "afterEventBuffer": { + "type": "number", + "minimum": 0 + } + }, + "required": [ + "length", + "slug", + "title" + ] + }, + "EventTypeOutput": { + "type": "object", + "properties": { + "id": { + "type": "number", + "example": 1 + }, + "length": { + "type": "number", + "example": 60 + }, + "slug": { + "type": "string", + "example": "cooking-class" + }, + "title": { + "type": "string", + "example": "Learn the secrets of masterchief!" + }, + "description": { + "type": "string", + "nullable": true, + "example": "Discover the culinary wonders of the Argentina by making the best flan ever!" + }, + "locations": { + "nullable": true, + "type": "array", + "items": { + "$ref": "#/components/schemas/EventTypeLocation_2024_04_15" + } + } + }, + "required": [ + "id", + "length", + "slug", + "title", + "description", + "locations" + ] + }, + "CreateEventTypeOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/EventTypeOutput" + } + }, + "required": [ + "status", + "data" + ] + }, + "Data": { + "type": "object", + "properties": { + "eventType": { + "$ref": "#/components/schemas/EventTypeOutput" + } + }, + "required": [ + "eventType" + ] + }, + "GetEventTypeOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/Data" + } + }, + "required": [ + "status", + "data" + ] + }, + "EventTypeGroup": { + "type": "object", + "properties": { + "eventTypes": { + "type": "array", + "items": { + "$ref": "#/components/schemas/EventTypeOutput" + } + } + }, + "required": [ + "eventTypes" + ] + }, + "GetEventTypesData": { + "type": "object", + "properties": { + "eventTypeGroups": { + "type": "array", + "items": { + "$ref": "#/components/schemas/EventTypeGroup" + } + } + }, + "required": [ + "eventTypeGroups" + ] + }, + "GetEventTypesOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/GetEventTypesData" + } + }, + "required": [ + "status", + "data" + ] + }, + "Location": { + "type": "object", + "properties": { + "type": { + "type": "string" + } + }, + "required": [ + "type" + ] + }, + "Source": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string" + }, + "label": { + "type": "string" + } + }, + "required": [ + "id", + "type", + "label" + ] + }, + "BookingField": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "type": { + "type": "string" + }, + "defaultLabel": { + "type": "string" + }, + "label": { + "type": "string" + }, + "placeholder": { + "type": "string" + }, + "required": { + "type": "boolean" + }, + "getOptionsAt": { + "type": "string" + }, + "hideWhenJustOneOption": { + "type": "boolean" + }, + "editable": { + "type": "string" + }, + "sources": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Source" + } + }, + "disableOnPrefill": { + "type": "boolean" + } + }, + "required": [ + "name", + "type" + ] + }, + "Organization": { + "type": "object", + "properties": { + "id": { + "type": "number" + }, + "slug": { + "type": "string", + "nullable": true + }, + "name": { + "type": "string" + }, + "metadata": { + "type": "object" + } + }, + "required": [ + "id", + "name", + "metadata" + ] + }, + "Profile": { + "type": "object", + "properties": { + "username": { + "type": "string", + "nullable": true + }, + "id": { + "type": "number", + "nullable": true + }, + "userId": { + "type": "number" + }, + "uid": { + "type": "string" + }, + "name": { + "type": "string" + }, + "organizationId": { + "type": "number", + "nullable": true + }, + "organization": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/Organization" + } + ] + }, + "upId": { + "type": "string" + }, + "image": { + "type": "string" + }, + "brandColor": { + "type": "string" + }, + "darkBrandColor": { + "type": "string" + }, + "theme": { + "type": "string" + }, + "bookerLayouts": { + "type": "object" + } + }, + "required": [ + "username", + "id", + "organizationId", + "upId" + ] + }, + "Owner": { + "type": "object", + "properties": { + "id": { + "type": "number" + }, + "avatarUrl": { + "type": "string", + "nullable": true + }, + "username": { + "type": "string", + "nullable": true + }, + "name": { + "type": "string", + "nullable": true + }, + "weekStart": { + "type": "string" + }, + "brandColor": { + "type": "string", + "nullable": true + }, + "darkBrandColor": { + "type": "string", + "nullable": true + }, + "theme": { + "type": "string", + "nullable": true + }, + "metadata": { + "type": "object" + }, + "defaultScheduleId": { + "type": "number", + "nullable": true + }, + "nonProfileUsername": { + "type": "string", + "nullable": true + }, + "profile": { + "$ref": "#/components/schemas/Profile" + } + }, + "required": [ + "id", + "username", + "name", + "weekStart", + "metadata", + "nonProfileUsername", + "profile" + ] + }, + "Schedule": { + "type": "object", + "properties": { + "id": { + "type": "number" + }, + "timeZone": { + "type": "string", + "nullable": true + } + }, + "required": [ + "id", + "timeZone" + ] + }, + "User": { + "type": "object", + "properties": { + "username": { + "type": "string", + "nullable": true + }, + "name": { + "type": "string", + "nullable": true + }, + "weekStart": { + "type": "string" + }, + "organizationId": { + "type": "number" + }, + "avatarUrl": { + "type": "string", + "nullable": true + }, + "profile": { + "$ref": "#/components/schemas/Profile" + }, + "bookerUrl": { + "type": "string" + } + }, + "required": [ + "username", + "name", + "weekStart", + "profile", + "bookerUrl" + ] + }, + "PublicEventTypeOutput": { + "type": "object", + "properties": { + "id": { + "type": "number" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "eventName": { + "type": "string", + "nullable": true + }, + "slug": { + "type": "string" + }, + "isInstantEvent": { + "type": "boolean" + }, + "aiPhoneCallConfig": { + "type": "object" + }, + "schedulingType": { + "type": "object" + }, + "length": { + "type": "number" + }, + "locations": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Location" + } + }, + "customInputs": { + "type": "array", + "items": { + "type": "object" + } + }, + "disableGuests": { + "type": "boolean" + }, + "metadata": { + "type": "object", + "nullable": true + }, + "lockTimeZoneToggleOnBookingPage": { + "type": "boolean" + }, + "requiresConfirmation": { + "type": "boolean" + }, + "requiresBookerEmailVerification": { + "type": "boolean" + }, + "recurringEvent": { + "type": "object" + }, + "price": { + "type": "number" + }, + "currency": { + "type": "string" + }, + "seatsPerTimeSlot": { + "type": "number", + "nullable": true + }, + "seatsShowAvailabilityCount": { + "type": "boolean", + "nullable": true + }, + "bookingFields": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BookingField" + } + }, + "team": { + "type": "object" + }, + "successRedirectUrl": { + "type": "string", + "nullable": true + }, + "workflows": { + "type": "array", + "items": { + "type": "object" + } + }, + "hosts": { + "type": "array", + "items": { + "type": "object" + } + }, + "owner": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/Owner" + } + ] + }, + "schedule": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/Schedule" + } + ] + }, + "hidden": { + "type": "boolean" + }, + "assignAllTeamMembers": { + "type": "boolean" + }, + "bookerLayouts": { + "type": "object" + }, + "users": { + "type": "array", + "items": { + "$ref": "#/components/schemas/User" + } + }, + "entity": { + "type": "object" + }, + "isDynamic": { + "type": "boolean" + } + }, + "required": [ + "id", + "title", + "description", + "slug", + "isInstantEvent", + "length", + "locations", + "customInputs", + "disableGuests", + "metadata", + "lockTimeZoneToggleOnBookingPage", + "requiresConfirmation", + "requiresBookerEmailVerification", + "price", + "currency", + "seatsShowAvailabilityCount", + "bookingFields", + "workflows", + "hosts", + "owner", + "schedule", + "hidden", + "assignAllTeamMembers", + "users", + "entity", + "isDynamic" + ] + }, + "GetEventTypePublicOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/PublicEventTypeOutput" + } + ] + } + }, + "required": [ + "status", + "data" + ] + }, + "PublicEventType": { + "type": "object", + "properties": { + "id": { + "type": "number", + "example": 1 + }, + "length": { + "type": "number", + "example": 60 + }, + "slug": { + "type": "string", + "example": "cooking-class" + }, + "title": { + "type": "string", + "example": "Learn the secrets of masterchief!" + }, + "description": { + "type": "string", + "nullable": true + } + }, + "required": [ + "id", + "length", + "slug", + "title" + ] + }, + "GetEventTypesPublicOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PublicEventType" + } + } + }, + "required": [ + "status", + "data" + ] + }, + "Option": { + "type": "object", + "properties": { + "value": { + "type": "string" + }, + "label": { + "type": "string" + } + }, + "required": [ + "value", + "label" + ] + }, + "VariantsConfig": { + "type": "object", + "properties": { + "variants": { + "type": "object" + } + }, + "required": [ + "variants" + ] + }, + "View": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "label": { + "type": "string" + }, + "description": { + "type": "string" + } + }, + "required": [ + "id", + "label" + ] + }, + "BookingField_2024_04_15": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "number", + "boolean", + "address", + "name", + "text", + "textarea", + "email", + "phone", + "multiemail", + "select", + "multiselect", + "checkbox", + "radio", + "radioInput" + ] + }, + "name": { + "type": "string" + }, + "options": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Option" + } + }, + "label": { + "type": "string" + }, + "labelAsSafeHtml": { + "type": "string" + }, + "defaultLabel": { + "type": "string" + }, + "placeholder": { + "type": "string" + }, + "required": { + "type": "boolean" + }, + "getOptionsAt": { + "type": "string" + }, + "optionsInputs": { + "type": "object" + }, + "variant": { + "type": "string" + }, + "variantsConfig": { + "$ref": "#/components/schemas/VariantsConfig" + }, + "views": { + "type": "array", + "items": { + "$ref": "#/components/schemas/View" + } + }, + "hideWhenJustOneOption": { + "type": "boolean" + }, + "hidden": { + "type": "boolean" + }, + "editable": { + "type": "string", + "enum": [ + "system", + "system-but-optional", + "system-but-hidden", + "user", + "user-readonly" + ] + }, + "sources": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Source" + } + }, + "disableOnPrefill": { + "type": "boolean" + } + }, + "required": [ + "type", + "name" + ] + }, + "UpdateEventTypeInput_2024_04_15": { + "type": "object", + "properties": { + "length": { + "type": "number", + "minimum": 1 + }, + "slug": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "hidden": { + "type": "boolean" + }, + "locations": { + "type": "array", + "items": { + "$ref": "#/components/schemas/EventTypeLocation_2024_04_15" + } + }, + "bookingFields": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BookingField_2024_04_15" + } + }, + "disableGuests": { + "type": "boolean" + }, + "minimumBookingNotice": { + "type": "number", + "minimum": 0 + }, + "beforeEventBuffer": { + "type": "number", + "minimum": 0 + }, + "afterEventBuffer": { + "type": "number", + "minimum": 0 + }, + "slotInterval": { + "type": "number", + "minimum": 0 + } + } + }, + "UpdateEventTypeOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/EventTypeOutput" + } + }, + "required": [ + "status", + "data" + ] + }, + "DeleteData": { + "type": "object", + "properties": { + "id": { + "type": "number", + "example": 1 + }, + "length": { + "type": "number", + "example": 60 + }, + "slug": { + "type": "string", + "example": "cooking-class" + }, + "title": { + "type": "string", + "example": "Learn the secrets of masterchief!" + } + }, + "required": [ + "id", + "length", + "slug", + "title" + ] + }, + "DeleteEventTypeOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/DeleteData" + } + }, + "required": [ + "status", + "data" + ] + }, + "OrgTeamOutputDto": { + "type": "object", + "properties": { + "id": { + "type": "number" + }, + "parentId": { + "type": "number" + }, + "name": { + "type": "string", + "minLength": 1 + }, + "slug": { + "type": "string" + }, + "logoUrl": { + "type": "string" + }, + "calVideoLogo": { + "type": "string" + }, + "appLogo": { + "type": "string" + }, + "appIconLogo": { + "type": "string" + }, + "bio": { + "type": "string" + }, + "hideBranding": { + "type": "boolean" + }, + "isOrganization": { + "type": "boolean" + }, + "isPrivate": { + "type": "boolean" + }, + "hideBookATeamMember": { + "type": "boolean", + "default": false + }, + "metadata": { + "type": "string" + }, + "theme": { + "type": "string" + }, + "brandColor": { + "type": "string" + }, + "darkBrandColor": { + "type": "string" + }, + "bannerUrl": { + "type": "string" + }, + "timeFormat": { + "type": "number" + }, + "timeZone": { + "type": "string", + "default": "Europe/London" + }, + "weekStart": { + "type": "string", + "default": "Sunday" + } + }, + "required": [ + "id", + "name" + ] + }, + "OrgTeamsOutputResponseDto": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/OrgTeamOutputDto" + } + } + }, + "required": [ + "status", + "data" + ] + }, + "OrgMeTeamsOutputResponseDto": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/OrgTeamOutputDto" + } + } + }, + "required": [ + "status", + "data" + ] + }, + "OrgTeamOutputResponseDto": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/OrgTeamOutputDto" + } + }, + "required": [ + "status", + "data" + ] + }, + "UpdateOrgTeamDto": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1 + }, + "slug": { + "type": "string" + }, + "logoUrl": { + "type": "string" + }, + "calVideoLogo": { + "type": "string" + }, + "appLogo": { + "type": "string" + }, + "appIconLogo": { + "type": "string" + }, + "bio": { + "type": "string" + }, + "hideBranding": { + "type": "boolean", + "default": false + }, + "isPrivate": { + "type": "boolean" + }, + "hideBookATeamMember": { + "type": "boolean" + }, + "metadata": { + "type": "string" + }, + "theme": { + "type": "string" + }, + "brandColor": { + "type": "string" + }, + "darkBrandColor": { + "type": "string" + }, + "bannerUrl": { + "type": "string" + }, + "timeFormat": { + "type": "number" + }, + "timeZone": { + "type": "string", + "default": "Europe/London" + }, + "weekStart": { + "type": "string", + "default": "Sunday" + } + } + }, + "CreateOrgTeamDto": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1 + }, + "slug": { + "type": "string" + }, + "logoUrl": { + "type": "string" + }, + "calVideoLogo": { + "type": "string" + }, + "appLogo": { + "type": "string" + }, + "appIconLogo": { + "type": "string" + }, + "bio": { + "type": "string" + }, + "hideBranding": { + "type": "boolean", + "default": false + }, + "isPrivate": { + "type": "boolean" + }, + "hideBookATeamMember": { + "type": "boolean" + }, + "metadata": { + "type": "string" + }, + "theme": { + "type": "string" + }, + "brandColor": { + "type": "string" + }, + "darkBrandColor": { + "type": "string" + }, + "bannerUrl": { + "type": "string" + }, + "timeFormat": { + "type": "number" + }, + "timeZone": { + "type": "string", + "default": "Europe/London" + }, + "weekStart": { + "type": "string", + "default": "Sunday" + }, + "autoAcceptCreator": { + "type": "boolean", + "default": true + } + }, + "required": [ + "name" + ] + }, + "ScheduleAvailabilityInput_2024_06_11": { + "type": "object", + "properties": { + "days": { + "example": [ + "Monday", + "Tuesday" + ], + "type": "array", + "items": { + "type": "object" + } + }, + "startTime": { + "type": "string", + "pattern": "TIME_FORMAT_HH_MM", + "example": "09:00" + }, + "endTime": { + "type": "string", + "pattern": "TIME_FORMAT_HH_MM", + "example": "10:00" + } + }, + "required": [ + "days", + "startTime", + "endTime" + ] + }, + "ScheduleOverrideInput_2024_06_11": { + "type": "object", + "properties": { + "date": { + "type": "string", + "example": "2024-05-20" + }, + "startTime": { + "type": "string", + "pattern": "TIME_FORMAT_HH_MM", + "example": "12:00" + }, + "endTime": { + "type": "string", + "pattern": "TIME_FORMAT_HH_MM", + "example": "13:00" + } + }, + "required": [ + "date", + "startTime", + "endTime" + ] + }, + "ScheduleOutput_2024_06_11": { + "type": "object", + "properties": { + "id": { + "type": "number", + "example": 254 + }, + "ownerId": { + "type": "number", + "example": 478 + }, + "name": { + "type": "string", + "example": "One-on-one coaching" + }, + "timeZone": { + "type": "string", + "example": "Europe/Rome" + }, + "availability": { + "example": [ + { + "days": [ + "Monday", + "Tuesday" + ], + "startTime": "09:00", + "endTime": "10:00" + } + ], + "type": "array", + "items": { + "$ref": "#/components/schemas/ScheduleAvailabilityInput_2024_06_11" + } + }, + "isDefault": { + "type": "boolean", + "example": true + }, + "overrides": { + "example": [ + { + "date": "2024-05-20", + "startTime": "12:00", + "endTime": "13:00" + } + ], + "type": "array", + "items": { + "$ref": "#/components/schemas/ScheduleOverrideInput_2024_06_11" + } + } + }, + "required": [ + "id", + "ownerId", + "name", + "timeZone", + "availability", + "isDefault", + "overrides" + ] + }, + "GetSchedulesOutput_2024_06_11": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ScheduleOutput_2024_06_11" + } + }, + "error": { + "type": "object" + } + }, + "required": [ + "status", + "data" + ] + }, + "CreateScheduleInput_2024_06_11": { + "type": "object", + "properties": { + "name": { + "type": "string", + "example": "One-on-one coaching" + }, + "timeZone": { + "type": "string", + "example": "Europe/Rome" + }, + "availability": { + "example": [ + { + "days": [ + "Monday", + "Tuesday" + ], + "startTime": "09:00", + "endTime": "10:00" + } + ], + "type": "array", + "items": { + "$ref": "#/components/schemas/ScheduleAvailabilityInput_2024_06_11" + } + }, + "isDefault": { + "type": "boolean", + "example": true + }, + "overrides": { + "example": [ + { + "date": "2024-05-20", + "startTime": "12:00", + "endTime": "14:00" + } + ], + "type": "array", + "items": { + "$ref": "#/components/schemas/ScheduleOverrideInput_2024_06_11" + } + } + }, + "required": [ + "name", + "timeZone", + "isDefault" + ] + }, + "CreateScheduleOutput_2024_06_11": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/ScheduleOutput_2024_06_11" + } + }, + "required": [ + "status", + "data" + ] + }, + "GetScheduleOutput_2024_06_11": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/ScheduleOutput_2024_06_11" + } + ] + }, + "error": { + "type": "object" + } + }, + "required": [ + "status", + "data" + ] + }, + "UpdateScheduleInput_2024_06_11": { + "type": "object", + "properties": { + "name": { + "type": "string", + "example": "One-on-one coaching" + }, + "timeZone": { + "type": "string", + "example": "Europe/Rome" + }, + "availability": { + "example": [ + { + "days": [ + "Monday", + "Tuesday" + ], + "startTime": "09:00", + "endTime": "10:00" + } + ], + "type": "array", + "items": { + "$ref": "#/components/schemas/ScheduleAvailabilityInput_2024_06_11" + } + }, + "isDefault": { + "type": "boolean", + "example": true + }, + "overrides": { + "example": [ + { + "date": "2024-05-20", + "startTime": "12:00", + "endTime": "14:00" + } + ], + "type": "array", + "items": { + "$ref": "#/components/schemas/ScheduleOverrideInput_2024_06_11" + } + } + } + }, + "UpdateScheduleOutput_2024_06_11": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/ScheduleOutput_2024_06_11" + }, + "error": { + "type": "object" + } + }, + "required": [ + "status", + "data" + ] + }, + "DeleteScheduleOutput_2024_06_11": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + } + }, + "required": [ + "status" + ] + }, + "GetUserOutput": { + "type": "object", + "properties": { + "id": { + "type": "number", + "description": "The ID of the user", + "example": 1 + }, + "username": { + "type": "string", + "nullable": true, + "description": "The username of the user", + "example": "john_doe" + }, + "name": { + "type": "string", + "nullable": true, + "description": "The name of the user", + "example": "John Doe" + }, + "email": { + "type": "string", + "description": "The email of the user", + "example": "john@example.com" + }, + "emailVerified": { + "format": "date-time", + "type": "string", + "nullable": true, + "description": "The date when the email was verified", + "example": "2022-01-01T00:00:00Z" + }, + "bio": { + "type": "string", + "nullable": true, + "description": "The bio of the user", + "example": "I am a software developer" + }, + "avatarUrl": { + "type": "string", + "nullable": true, + "description": "The URL of the user's avatar", + "example": "https://example.com/avatar.jpg" + }, + "timeZone": { + "type": "string", + "description": "The time zone of the user", + "example": "America/New_York" + }, + "weekStart": { + "type": "string", + "description": "The week start day of the user", + "example": "Monday" + }, + "appTheme": { + "type": "string", + "nullable": true, + "description": "The app theme of the user", + "example": "light" + }, + "theme": { + "type": "string", + "nullable": true, + "description": "The theme of the user", + "example": "default" + }, + "defaultScheduleId": { + "type": "number", + "nullable": true, + "description": "The ID of the default schedule for the user", + "example": 1 + }, + "locale": { + "type": "string", + "nullable": true, + "description": "The locale of the user", + "example": "en-US" + }, + "timeFormat": { + "type": "number", + "nullable": true, + "description": "The time format of the user", + "example": 12 + }, + "hideBranding": { + "type": "boolean", + "description": "Whether to hide branding for the user", + "example": false + }, + "brandColor": { + "type": "string", + "nullable": true, + "description": "The brand color of the user", + "example": "#ffffff" + }, + "darkBrandColor": { + "type": "string", + "nullable": true, + "description": "The dark brand color of the user", + "example": "#000000" + }, + "allowDynamicBooking": { + "type": "boolean", + "nullable": true, + "description": "Whether dynamic booking is allowed for the user", + "example": true + }, + "createdDate": { + "format": "date-time", + "type": "string", + "description": "The date when the user was created", + "example": "2022-01-01T00:00:00Z" + }, + "verified": { + "type": "boolean", + "nullable": true, + "description": "Whether the user is verified", + "example": true + }, + "invitedTo": { + "type": "number", + "nullable": true, + "description": "The ID of the user who invited this user", + "example": 1 + } + }, + "required": [ + "id", + "email", + "timeZone", + "weekStart", + "hideBranding", + "createdDate" + ] + }, + "GetOrganizationUsersOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/GetUserOutput" + } + } + }, + "required": [ + "status", + "data" + ] + }, + "CreateOrganizationUserInput": { + "type": "object", + "properties": { + "email": { + "type": "string", + "description": "User email address", + "example": "user@example.com" + }, + "username": { + "type": "string", + "description": "Username", + "example": "user123" + }, + "weekday": { + "type": "string", + "description": "Preferred weekday", + "example": "Monday" + }, + "brandColor": { + "type": "string", + "description": "Brand color in HEX format", + "example": "#FFFFFF" + }, + "darkBrandColor": { + "type": "string", + "description": "Dark brand color in HEX format", + "example": "#000000" + }, + "hideBranding": { + "type": "boolean", + "description": "Hide branding", + "example": false + }, + "timeZone": { + "type": "string", + "description": "Time zone", + "example": "America/New_York" + }, + "theme": { + "type": "string", + "nullable": true, + "description": "Theme", + "example": "dark" + }, + "appTheme": { + "type": "string", + "nullable": true, + "description": "Application theme", + "example": "light" + }, + "timeFormat": { + "type": "number", + "description": "Time format", + "example": 24 + }, + "defaultScheduleId": { + "type": "number", + "minimum": 0, + "description": "Default schedule ID", + "example": 1 + }, + "locale": { + "type": "string", + "nullable": true, + "default": "en", + "description": "Locale", + "example": "en" + }, + "avatarUrl": { + "type": "string", + "description": "Avatar URL", + "example": "https://example.com/avatar.jpg" + }, + "organizationRole": { + "type": "object", + "default": "MEMBER" + }, + "autoAccept": { + "type": "object", + "default": true + } + }, + "required": [ + "email", + "organizationRole", + "autoAccept" + ] + }, + "GetOrganizationUserOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/GetUserOutput" + } + }, + "required": [ + "status", + "data" + ] + }, + "UpdateOrganizationUserInput": { + "type": "object", + "properties": {} + }, + "OrgMembershipOutputDto": { + "type": "object", + "properties": { + "role": { + "type": "string", + "enum": [ + "MEMBER", + "OWNER", + "ADMIN" + ] + }, + "id": { + "type": "number" + }, + "userId": { + "type": "number" + }, + "teamId": { + "type": "number" + }, + "accepted": { + "type": "boolean" + }, + "disableImpersonation": { + "type": "boolean" + } + }, + "required": [ + "role", + "id", + "userId", + "teamId", + "accepted" + ] + }, + "GetAllOrgMemberships": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/OrgMembershipOutputDto" + } + }, + "required": [ + "status", + "data" + ] + }, + "CreateOrgMembershipDto": { + "type": "object", + "properties": { + "role": { + "type": "string", + "default": "MEMBER", + "enum": [ + "MEMBER", + "OWNER", + "ADMIN" + ] + }, + "userId": { + "type": "number" + }, + "accepted": { + "type": "boolean", + "default": false + }, + "disableImpersonation": { + "type": "boolean", + "default": false + } + }, + "required": [ + "role", + "userId" + ] + }, + "CreateOrgMembershipOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/OrgMembershipOutputDto" + } + }, + "required": [ + "status", + "data" + ] + }, + "GetOrgMembership": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/OrgMembershipOutputDto" + } + }, + "required": [ + "status", + "data" + ] + }, + "DeleteOrgMembership": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/OrgMembershipOutputDto" + } + }, + "required": [ + "status", + "data" + ] + }, + "UpdateOrgMembershipDto": { + "type": "object", + "properties": { + "role": { + "type": "string", + "default": "MEMBER", + "enum": [ + "MEMBER", + "OWNER", + "ADMIN" + ] + }, + "accepted": { + "type": "boolean", + "default": false + }, + "disableImpersonation": { + "type": "boolean", + "default": false + } + } + }, + "UpdateOrgMembership": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/OrgMembershipOutputDto" + } + }, + "required": [ + "status", + "data" + ] + }, + "CreateTeamEventTypeInput_2024_06_14": { + "type": "object", + "properties": { + "lengthInMinutes": { + "type": "number", + "example": 60 + }, + "title": { + "type": "string", + "example": "Learn the secrets of masterchief!" + }, + "description": { + "type": "string", + "example": "Discover the culinary wonders of the Argentina by making the best flan ever!" + }, + "schedulingType": { + "type": "object" + }, + "hosts": { + "type": "array", + "items": { + "type": "string" + } + }, + "assignAllTeamMembers": { + "type": "boolean" + } + }, + "required": [ + "lengthInMinutes", + "title", + "description", + "schedulingType", + "hosts", + "assignAllTeamMembers" + ] + }, + "CreateTeamEventTypeOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "oneOf": [ + { + "$ref": "#/components/schemas/TeamEventTypeOutput_2024_06_14" + }, + { + "type": "array", + "items": { + "$ref": "#/components/schemas/TeamEventTypeOutput_2024_06_14" + } + } + ] + } + }, + "required": [ + "status", + "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" + } + }, + "required": [ + "interval", + "occurrences", + "frequency" + ] + }, + "TeamEventTypeResponseHost": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "userId": { + "type": "number" + }, + "mandatory": { + "type": "boolean", + "default": false + }, + "priority": { + "type": "object", + "default": "medium" + } + }, + "required": [ + "name", + "userId" + ] + }, + "BookingLimitsCount_2024_06_14": { + "type": "object", + "properties": { + "day": { + "type": "number", + "minimum": 1, + "description": "The number of bookings per day", + "example": 1 + }, + "week": { + "type": "number", + "minimum": 1, + "description": "The number of bookings per week", + "example": 2 + }, + "month": { + "type": "number", + "minimum": 1, + "description": "The number of bookings per month", + "example": 3 + }, + "year": { + "type": "number", + "minimum": 1, + "description": "The number of bookings per year", + "example": 4 + } + } + }, + "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 + } + } + }, + "TeamEventTypeOutput_2024_06_14": { + "type": "object", + "properties": { + "id": { + "type": "number", + "example": 1 + }, + "lengthInMinutes": { + "type": "number", + "minimum": 1 + }, + "title": { + "type": "string" + }, + "slug": { + "type": "string" + }, + "description": { + "type": "string" + }, + "locations": { + "type": "array", + "items": { + "type": "object" + } + }, + "bookingFields": { + "type": "array", + "items": { + "type": "object" + } + }, + "disableGuests": { + "type": "boolean" + }, + "slotInterval": { + "type": "number", + "nullable": true + }, + "minimumBookingNotice": { + "type": "number", + "minimum": 0 + }, + "beforeEventBuffer": { + "type": "number" + }, + "afterEventBuffer": { + "type": "number" + }, + "schedulingType": { + "type": "object", + "nullable": true + }, + "recurrence": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/Recurrence_2024_06_14" + } + ] + }, + "metadata": { + "type": "object" + }, + "requiresConfirmation": { + "type": "boolean" + }, + "price": { + "type": "number" + }, + "currency": { + "type": "string" + }, + "lockTimeZoneToggleOnBookingPage": { + "type": "boolean" + }, + "seatsPerTimeSlot": { + "type": "number", + "nullable": true + }, + "forwardParamsSuccessRedirect": { + "type": "boolean", + "nullable": true + }, + "successRedirectUrl": { + "type": "string", + "nullable": true + }, + "seatsShowAvailabilityCount": { + "type": "boolean", + "nullable": true + }, + "isInstantEvent": { + "type": "boolean" + }, + "scheduleId": { + "type": "number", + "nullable": true + }, + "teamId": { + "type": "number", + "nullable": true + }, + "ownerId": { + "type": "number", + "nullable": true + }, + "parentEventTypeId": { + "type": "number", + "nullable": true + }, + "hosts": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TeamEventTypeResponseHost" + } + }, + "assignAllTeamMembers": { + "type": "boolean" + }, + "bookingLimitsCount": { + "$ref": "#/components/schemas/BookingLimitsCount_2024_06_14" + }, + "onlyShowFirstAvailableSlot": { + "type": "boolean" + }, + "bookingLimitsDuration": { + "$ref": "#/components/schemas/BookingLimitsDuration_2024_06_14" + }, + "bookingWindow": { + "type": "object" + }, + "offsetStart": { + "type": "number", + "minimum": 1 + } + }, + "required": [ + "id", + "lengthInMinutes", + "title", + "slug", + "description", + "locations", + "bookingFields", + "disableGuests", + "schedulingType", + "recurrence", + "metadata", + "requiresConfirmation", + "price", + "currency", + "lockTimeZoneToggleOnBookingPage", + "seatsPerTimeSlot", + "forwardParamsSuccessRedirect", + "successRedirectUrl", + "seatsShowAvailabilityCount", + "isInstantEvent", + "scheduleId", + "hosts" + ] + }, + "GetTeamEventTypeOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/TeamEventTypeOutput_2024_06_14" + } + }, + "required": [ + "status", + "data" + ] + }, + "CreatePhoneCallInput": { + "type": "object", + "properties": { + "yourPhoneNumber": { + "type": "string", + "pattern": "/^\\+[1-9]\\d{1,14}$/", + "description": "Your phone number" + }, + "numberToCall": { + "type": "string", + "pattern": "/^\\+[1-9]\\d{1,14}$/", + "description": "Number to call" + }, + "calApiKey": { + "type": "string", + "description": "CAL API Key" + }, + "enabled": { + "type": "object", + "default": true, + "description": "Enabled status" + }, + "templateType": { + "default": "CUSTOM_TEMPLATE", + "enum": [ + "CHECK_IN_APPOINTMENT", + "CUSTOM_TEMPLATE" + ], + "type": "string", + "description": "Template type" + }, + "schedulerName": { + "type": "string", + "description": "Scheduler name" + }, + "guestName": { + "type": "string", + "description": "Guest name" + }, + "guestEmail": { + "type": "string", + "description": "Guest email" + }, + "guestCompany": { + "type": "string", + "description": "Guest company" + }, + "beginMessage": { + "type": "string", + "description": "Begin message" + }, + "generalPrompt": { + "type": "string", + "description": "General prompt" + } + }, + "required": [ + "yourPhoneNumber", + "numberToCall", + "calApiKey", + "enabled", + "templateType" + ] + }, + "CreatePhoneCallOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/Data" + } + }, + "required": [ + "status", + "data" + ] + }, + "GetTeamEventTypesOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TeamEventTypeOutput_2024_06_14" + } + } + }, + "required": [ + "status", + "data" + ] + }, + "UpdateTeamEventTypeInput_2024_06_14": { + "type": "object", + "properties": { + "lengthInMinutes": { + "type": "number" + }, + "title": { + "type": "string" + }, + "slug": { + "type": "string" + }, + "description": { + "type": "string" + }, + "locations": { + "type": "array", + "items": { + "type": "string" + } + }, + "bookingFields": { + "type": "array", + "items": { + "type": "string" + } + }, + "disableGuests": { + "type": "boolean" + }, + "slotInterval": { + "type": "number" + }, + "minimumBookingNotice": { + "type": "number" + }, + "beforeEventBuffer": { + "type": "number" + }, + "afterEventBuffer": { + "type": "number" + }, + "hosts": { + "type": "array", + "items": { + "type": "string" + } + }, + "assignAllTeamMembers": { + "type": "boolean" + } + }, + "required": [ + "lengthInMinutes", + "title", + "slug", + "description", + "locations", + "bookingFields", + "disableGuests", + "slotInterval", + "minimumBookingNotice", + "beforeEventBuffer", + "afterEventBuffer", + "hosts", + "assignAllTeamMembers" + ] + }, + "UpdateTeamEventTypeOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "oneOf": [ + { + "$ref": "#/components/schemas/TeamEventTypeOutput_2024_06_14" + }, + { + "type": "array", + "items": { + "$ref": "#/components/schemas/TeamEventTypeOutput_2024_06_14" + } + } + ] + } + }, + "required": [ + "status", + "data" + ] + }, + "DeleteTeamEventTypeOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "type": "object" + } + }, + "required": [ + "status", + "data" + ] + }, + "OrgTeamMembershipOutputDto": { + "type": "object", + "properties": { + "role": { + "type": "string", + "enum": [ + "MEMBER", + "OWNER", + "ADMIN" + ] + }, + "id": { + "type": "number" + }, + "userId": { + "type": "number" + }, + "teamId": { + "type": "number" + }, + "accepted": { + "type": "boolean" + }, + "disableImpersonation": { + "type": "boolean" + } + }, + "required": [ + "role", + "id", + "userId", + "teamId", + "accepted" + ] + }, + "OrgTeamMembershipsOutputResponseDto": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/OrgTeamMembershipOutputDto" + } + } + }, + "required": [ + "status", + "data" + ] + }, + "OrgTeamMembershipOutputResponseDto": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/OrgTeamMembershipOutputDto" + } + }, + "required": [ + "status", + "data" + ] + }, + "UpdateOrgTeamMembershipDto": { + "type": "object", + "properties": { + "role": { + "type": "string", + "default": "MEMBER", + "enum": [ + "MEMBER", + "OWNER", + "ADMIN" + ] + }, + "accepted": { + "type": "boolean", + "default": false + }, + "disableImpersonation": { + "type": "boolean", + "default": false + } + } + }, + "CreateOrgTeamMembershipDto": { + "type": "object", + "properties": { + "role": { + "type": "string", + "default": "MEMBER", + "enum": [ + "MEMBER", + "OWNER", + "ADMIN" + ] + }, + "userId": { + "type": "number" + }, + "accepted": { + "type": "boolean", + "default": false + }, + "disableImpersonation": { + "type": "boolean", + "default": false + } + }, + "required": [ + "role", + "userId" + ] + }, + "Attribute": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "The ID of the attribute", + "example": "attr_123" + }, + "teamId": { + "type": "number", + "description": "The team ID associated with the attribute", + "example": 1 + }, + "type": { + "type": "string", + "description": "The type of the attribute", + "enum": [ + "TEXT", + "NUMBER", + "SINGLE_SELECT", + "MULTI_SELECT" + ] + }, + "name": { + "type": "string", + "description": "The name of the attribute", + "example": "Attribute Name" + }, + "slug": { + "type": "string", + "description": "The slug of the attribute", + "example": "attribute-name" + }, + "enabled": { + "type": "boolean", + "description": "Whether the attribute is enabled and displayed on their profile", + "example": true + }, + "usersCanEditRelation": { + "type": "boolean", + "description": "Whether users can edit the relation", + "example": true + } + }, + "required": [ + "id", + "teamId", + "type", + "name", + "slug", + "enabled" + ] + }, + "GetOrganizationAttributesOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Attribute" + } + } + }, + "required": [ + "status", + "data" + ] + }, + "GetSingleAttributeOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/Attribute" + } + ] + } + }, + "required": [ + "status", + "data" + ] + }, + "CreateOrganizationAttributeOptionInput": { + "type": "object", + "properties": { + "value": { + "type": "string" + }, + "slug": { + "type": "string" + } + }, + "required": [ + "value", + "slug" + ] + }, + "CreateOrganizationAttributeInput": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "slug": { + "type": "string" + }, + "type": { + "type": "object" + }, + "options": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CreateOrganizationAttributeOptionInput" + } + }, + "enabled": { + "type": "boolean" + } + }, + "required": [ + "name", + "slug", + "type", + "options" + ] + }, + "CreateOrganizationAttributesOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/Attribute" + } + }, + "required": [ + "status", + "data" + ] + }, + "UpdateOrganizationAttributeInput": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "slug": { + "type": "string" + }, + "type": { + "type": "object" + }, + "enabled": { + "type": "boolean" + } + } + }, + "UpdateOrganizationAttributesOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/Attribute" + } + }, + "required": [ + "status", + "data" + ] + }, + "DeleteOrganizationAttributesOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/Attribute" + } + }, + "required": [ + "status", + "data" + ] + }, + "OptionOutput": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "The ID of the option", + "example": "attr_option_id" + }, + "attributeId": { + "type": "string", + "description": "The ID of the attribute", + "example": "attr_id" + }, + "value": { + "type": "string", + "description": "The value of the option", + "example": "option_value" + }, + "slug": { + "type": "string", + "description": "The slug of the option", + "example": "option-slug" + } + }, + "required": [ + "id", + "attributeId", + "value", + "slug" + ] + }, + "CreateAttributeOptionOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/OptionOutput" + } + }, + "required": [ + "status", + "data" + ] + }, + "DeleteAttributeOptionOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/OptionOutput" + } + }, + "required": [ + "status", + "data" + ] + }, + "UpdateOrganizationAttributeOptionInput": { + "type": "object", + "properties": { + "value": { + "type": "string" + }, + "slug": { + "type": "string" + } + } + }, + "UpdateAttributeOptionOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/OptionOutput" + } + }, + "required": [ + "status", + "data" + ] + }, + "GetAllAttributeOptionOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/OptionOutput" + } + } + }, + "required": [ + "status", + "data" + ] + }, + "AssignOrganizationAttributeOptionToUserInput": { + "type": "object", + "properties": { + "value": { + "type": "string" + }, + "attributeOptionId": { + "type": "string" + }, + "attributeId": { + "type": "string" + } + }, + "required": [ + "attributeId" + ] + }, + "AssignOptionUserOutputData": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "The ID of the option assigned to the user" + }, + "memberId": { + "type": "number", + "description": "The ID form the org membership for the user" + }, + "attributeOptionId": { + "type": "string", + "description": "The value of the option" + } + }, + "required": [ + "id", + "memberId", + "attributeOptionId" + ] + }, + "AssignOptionUserOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/AssignOptionUserOutputData" + } + }, + "required": [ + "status", + "data" + ] + }, + "UnassignOptionUserOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/AssignOptionUserOutputData" + } + }, + "required": [ + "status", + "data" + ] + }, + "GetOptionUserOutputData": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "The ID of the option assigned to the user" + }, + "attributeId": { + "type": "string", + "description": "The ID of the attribute" + }, + "value": { + "type": "string", + "description": "The value of the option" + }, + "slug": { + "type": "string", + "description": "The slug of the option" + } + }, + "required": [ + "id", + "attributeId", + "value", + "slug" + ] + }, + "GetOptionUserOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/GetOptionUserOutputData" + } + } + }, + "required": [ + "status", + "data" + ] + }, + "TeamWebhookOutputDto": { + "type": "object", + "properties": { + "payloadTemplate": { + "type": "string", + "description": "The template of the payload that will be sent to the subscriberUrl, check cal.com/docs/core-features/webhooks for more information", + "example": "{\"content\":\"A new event has been scheduled\",\"type\":\"{{type}}\",\"name\":\"{{title}}\",\"organizer\":\"{{organizer.name}}\",\"booker\":\"{{attendees.0.name}}\"}" + }, + "teamId": { + "type": "number" + }, + "id": { + "type": "number" + }, + "triggers": { + "type": "array", + "items": { + "type": "object" + } + }, + "subscriberUrl": { + "type": "string" + }, + "active": { + "type": "boolean" + }, + "secret": { + "type": "string" + } + }, + "required": [ + "payloadTemplate", + "teamId", + "id", + "triggers", + "subscriberUrl", + "active" + ] + }, + "TeamWebhooksOutputResponseDto": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TeamWebhookOutputDto" + } + } + }, + "required": [ + "status", + "data" + ] + }, + "CreateWebhookInputDto": { + "type": "object", + "properties": { + "payloadTemplate": { + "type": "string", + "description": "The template of the payload that will be sent to the subscriberUrl, check cal.com/docs/core-features/webhooks for more information", + "example": "{\"content\":\"A new event has been scheduled\",\"type\":\"{{type}}\",\"name\":\"{{title}}\",\"organizer\":\"{{organizer.name}}\",\"booker\":\"{{attendees.0.name}}\"}" + }, + "triggers": { + "type": "string", + "example": [ + "BOOKING_CREATED", + "BOOKING_RESCHEDULED", + "BOOKING_CANCELLED", + "BOOKING_CONFIRMED", + "BOOKING_REJECTED", + "BOOKING_COMPLETED", + "BOOKING_NO_SHOW", + "BOOKING_REOPENED" + ], + "enum": [ + "BOOKING_CREATED", + "BOOKING_PAYMENT_INITIATED", + "BOOKING_PAID", + "BOOKING_RESCHEDULED", + "BOOKING_REQUESTED", + "BOOKING_CANCELLED", + "BOOKING_REJECTED", + "BOOKING_NO_SHOW_UPDATED", + "FORM_SUBMITTED", + "MEETING_ENDED", + "MEETING_STARTED", + "RECORDING_READY", + "INSTANT_MEETING", + "RECORDING_TRANSCRIPTION_GENERATED", + "OOO_CREATED" + ] + }, + "active": { + "type": "boolean" + }, + "subscriberUrl": { + "type": "string" + }, + "secret": { + "type": "string" + } + }, + "required": [ + "triggers", + "active", + "subscriberUrl" + ] + }, + "TeamWebhookOutputResponseDto": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/TeamWebhookOutputDto" + } + }, + "required": [ + "status", + "data" + ] + }, + "UpdateWebhookInputDto": { + "type": "object", + "properties": { + "payloadTemplate": { + "type": "string", + "description": "The template of the payload that will be sent to the subscriberUrl, check cal.com/docs/core-features/webhooks for more information", + "example": "{\"content\":\"A new event has been scheduled\",\"type\":\"{{type}}\",\"name\":\"{{title}}\",\"organizer\":\"{{organizer.name}}\",\"booker\":\"{{attendees.0.name}}\"}" + }, + "triggers": { + "type": "string", + "example": [ + "BOOKING_CREATED", + "BOOKING_RESCHEDULED", + "BOOKING_CANCELLED", + "BOOKING_CONFIRMED", + "BOOKING_REJECTED", + "BOOKING_COMPLETED", + "BOOKING_NO_SHOW", + "BOOKING_REOPENED" + ], + "enum": [ + "BOOKING_CREATED", + "BOOKING_PAYMENT_INITIATED", + "BOOKING_PAID", + "BOOKING_RESCHEDULED", + "BOOKING_REQUESTED", + "BOOKING_CANCELLED", + "BOOKING_REJECTED", + "BOOKING_NO_SHOW_UPDATED", + "FORM_SUBMITTED", + "MEETING_ENDED", + "MEETING_STARTED", + "RECORDING_READY", + "INSTANT_MEETING", + "RECORDING_TRANSCRIPTION_GENERATED", + "OOO_CREATED" + ] + }, + "active": { + "type": "boolean" + }, + "subscriberUrl": { + "type": "string" + }, + "secret": { + "type": "string" + } + } + }, + "GetDefaultScheduleOutput_2024_06_11": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/ScheduleOutput_2024_06_11" + } + }, + "required": [ + "status", + "data" + ] + }, + "CreateAvailabilityInput_2024_04_15": { + "type": "object", + "properties": { + "days": { + "example": [ + 1, + 2 + ], + "type": "array", + "items": { + "type": "number" + } + }, + "startTime": { + "format": "date-time", + "type": "string" + }, + "endTime": { + "format": "date-time", + "type": "string" + } + }, + "required": [ + "days", + "startTime", + "endTime" + ] + }, + "CreateScheduleInput_2024_04_15": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "timeZone": { + "type": "string" + }, + "availabilities": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CreateAvailabilityInput_2024_04_15" + } + }, + "isDefault": { + "type": "boolean" + } + }, + "required": [ + "name", + "timeZone", + "isDefault" + ] + }, + "WorkingHours": { + "type": "object", + "properties": { + "days": { + "type": "array", + "items": { + "type": "number" + } + }, + "startTime": { + "type": "number" + }, + "endTime": { + "type": "number" + }, + "userId": { + "type": "number", + "nullable": true + } + }, + "required": [ + "days", + "startTime", + "endTime" + ] + }, + "AvailabilityModel": { + "type": "object", + "properties": { + "id": { + "type": "number" + }, + "userId": { + "type": "number", + "nullable": true + }, + "eventTypeId": { + "type": "number", + "nullable": true + }, + "days": { + "type": "array", + "items": { + "type": "number" + } + }, + "startTime": { + "format": "date-time", + "type": "string" + }, + "endTime": { + "format": "date-time", + "type": "string" + }, + "date": { + "format": "date-time", + "type": "string", + "nullable": true + }, + "scheduleId": { + "type": "number", + "nullable": true + } + }, + "required": [ + "id", + "days", + "startTime", + "endTime" + ] + }, + "TimeRange": { + "type": "object", + "properties": { + "userId": { + "type": "number", + "nullable": true + }, + "start": { + "format": "date-time", + "type": "string" + }, + "end": { + "format": "date-time", + "type": "string" + } + }, + "required": [ + "start", + "end" + ] + }, + "ScheduleOutput": { + "type": "object", + "properties": { + "id": { + "type": "number" + }, + "name": { + "type": "string" + }, + "isManaged": { + "type": "boolean" + }, + "workingHours": { + "type": "array", + "items": { + "$ref": "#/components/schemas/WorkingHours" + } + }, + "schedule": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AvailabilityModel" + } + }, + "availability": { + "type": "array", + "items": { + "required": true, + "type": "array", + "items": { + "$ref": "#/components/schemas/TimeRange" + } + } + }, + "timeZone": { + "type": "string" + }, + "dateOverrides": { + "type": "array", + "items": { + "type": "object" + } + }, + "isDefault": { + "type": "boolean" + }, + "isLastSchedule": { + "type": "boolean" + }, + "readOnly": { + "type": "boolean" + } + }, + "required": [ + "id", + "name", + "isManaged", + "workingHours", + "schedule", + "availability", + "timeZone", + "dateOverrides", + "isDefault", + "isLastSchedule", + "readOnly" + ] + }, + "CreateScheduleOutput_2024_04_15": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/ScheduleOutput" + } + }, + "required": [ + "status", + "data" + ] + }, + "GetDefaultScheduleOutput_2024_04_15": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/ScheduleOutput" + } + ] + } + }, + "required": [ + "status", + "data" + ] + }, + "GetScheduleOutput_2024_04_15": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/ScheduleOutput" + } + }, + "required": [ + "status", + "data" + ] + }, + "GetSchedulesOutput_2024_04_15": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/ScheduleOutput" + } + }, + "required": [ + "status", + "data" + ] + }, + "UpdateScheduleInput_2024_04_15": { + "type": "object", + "properties": { + "timeZone": { + "type": "string" + }, + "name": { + "type": "string" + }, + "isDefault": { + "type": "boolean" + }, + "schedule": { + "example": [ + [], + [ + { + "start": "2022-01-01T00:00:00.000Z", + "end": "2022-01-02T00:00:00.000Z" + } + ], + [], + [], + [], + [], + [] + ], + "items": { + "type": "array" + }, + "type": "array" + }, + "dateOverrides": { + "example": [ + [], + [ + { + "start": "2022-01-01T00:00:00.000Z", + "end": "2022-01-02T00:00:00.000Z" + } + ], + [], + [], + [], + [], + [] + ], + "items": { + "type": "array" + }, + "type": "array" + } + }, + "required": [ + "timeZone", + "name", + "isDefault", + "schedule" + ] + }, + "EventTypeModel_2024_04_15": { + "type": "object", + "properties": { + "id": { + "type": "number" + }, + "eventName": { + "type": "string", + "nullable": true + } + }, + "required": [ + "id" + ] + }, + "AvailabilityModel_2024_04_15": { + "type": "object", + "properties": { + "id": { + "type": "number" + }, + "userId": { + "type": "number", + "nullable": true + }, + "scheduleId": { + "type": "number", + "nullable": true + }, + "eventTypeId": { + "type": "number", + "nullable": true + }, + "days": { + "type": "array", + "items": { + "type": "number" + } + }, + "startTime": { + "format": "date-time", + "type": "string" + }, + "endTime": { + "format": "date-time", + "type": "string" + }, + "date": { + "format": "date-time", + "type": "string", + "nullable": true + } + }, + "required": [ + "id", + "days" + ] + }, + "ScheduleModel_2024_04_15": { + "type": "object", + "properties": { + "id": { + "type": "number" + }, + "userId": { + "type": "number" + }, + "name": { + "type": "string" + }, + "timeZone": { + "type": "string", + "nullable": true + }, + "eventType": { + "type": "array", + "items": { + "$ref": "#/components/schemas/EventTypeModel_2024_04_15" + } + }, + "availability": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AvailabilityModel_2024_04_15" + } + } + }, + "required": [ + "id", + "userId", + "name" + ] + }, + "UpdatedScheduleOutput_2024_04_15": { + "type": "object", + "properties": { + "schedule": { + "$ref": "#/components/schemas/ScheduleModel_2024_04_15" + }, + "isDefault": { + "type": "boolean" + }, + "timeZone": { + "type": "string" + }, + "prevDefaultId": { + "type": "number", + "nullable": true + }, + "currentDefaultId": { + "type": "number", + "nullable": true + } + }, + "required": [ + "schedule", + "isDefault" + ] + }, + "UpdateScheduleOutput_2024_04_15": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/UpdatedScheduleOutput_2024_04_15" + } + }, + "required": [ + "status", + "data" + ] + }, + "DeleteScheduleOutput_2024_04_15": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + } + }, + "required": [ + "status" + ] + }, + "AuthUrlData": { + "type": "object", + "properties": { + "authUrl": { + "type": "string" + } + }, + "required": [ + "authUrl" + ] + }, + "GcalAuthUrlOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/AuthUrlData" + } + }, + "required": [ + "status", + "data" + ] + }, + "GcalSaveRedirectOutput": { + "type": "object", + "properties": { + "url": { + "type": "string" + } + }, + "required": [ + "url" + ] + }, + "GcalCheckOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + } + }, + "required": [ + "status" + ] + }, + "ProviderVerifyClientData": { + "type": "object", + "properties": { + "clientId": { + "type": "string" + }, + "organizationId": { + "type": "number" + }, + "name": { + "type": "string" + } + }, + "required": [ + "clientId", + "organizationId", + "name" + ] + }, + "ProviderVerifyClientOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/ProviderVerifyClientData" + } + }, + "required": [ + "status", + "data" + ] + }, + "ProviderVerifyAccessTokenOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + } + }, + "required": [ + "status" + ] + }, + "MeOrgOutput": { + "type": "object", + "properties": { + "isPlatform": { + "type": "boolean" + }, + "id": { + "type": "number" + } + }, + "required": [ + "isPlatform", + "id" + ] + }, + "MeOutput": { + "type": "object", + "properties": { + "id": { + "type": "number" + }, + "username": { + "type": "string" + }, + "email": { + "type": "string" + }, + "timeFormat": { + "type": "number" + }, + "defaultScheduleId": { + "type": "number", + "nullable": true + }, + "weekStart": { + "type": "string" + }, + "timeZone": { + "type": "string" + }, + "organizationId": { + "type": "number", + "nullable": true + }, + "organization": { + "$ref": "#/components/schemas/MeOrgOutput" + } + }, + "required": [ + "id", + "username", + "email", + "timeFormat", + "defaultScheduleId", + "weekStart", + "timeZone", + "organizationId" + ] + }, + "GetMeOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/MeOutput" + } + }, + "required": [ + "status", + "data" + ] + }, + "UpdateMeOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/MeOutput" + } + }, + "required": [ + "status", + "data" + ] + }, + "BusyTimesOutput": { + "type": "object", + "properties": { + "start": { + "format": "date-time", + "type": "string" + }, + "end": { + "format": "date-time", + "type": "string" + }, + "source": { + "type": "string", + "nullable": true + } + }, + "required": [ + "start", + "end" + ] + }, + "GetBusyTimesOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BusyTimesOutput" + } + } + }, + "required": [ + "status", + "data" + ] + }, + "Integration": { + "type": "object", + "properties": { + "appData": { + "type": "object", + "nullable": true + }, + "dirName": { + "type": "string" + }, + "__template": { + "type": "string" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "installed": { + "type": "boolean" + }, + "type": { + "type": "string" + }, + "title": { + "type": "string" + }, + "variant": { + "type": "string" + }, + "category": { + "type": "string" + }, + "categories": { + "type": "array", + "items": { + "type": "string" + } + }, + "logo": { + "type": "string" + }, + "publisher": { + "type": "string" + }, + "slug": { + "type": "string" + }, + "url": { + "type": "string" + }, + "email": { + "type": "string" + }, + "locationOption": { + "type": "object", + "nullable": true + } + }, + "required": [ + "name", + "description", + "type", + "variant", + "categories", + "logo", + "publisher", + "slug", + "url", + "email", + "locationOption" + ] + }, + "Primary": { + "type": "object", + "properties": { + "externalId": { + "type": "string" + }, + "integration": { + "type": "string" + }, + "name": { + "type": "string" + }, + "primary": { + "type": "boolean", + "nullable": true + }, + "readOnly": { + "type": "boolean" + }, + "email": { + "type": "string" + }, + "isSelected": { + "type": "boolean" + }, + "credentialId": { + "type": "number" + } + }, + "required": [ + "externalId", + "primary", + "readOnly", + "isSelected", + "credentialId" + ] + }, + "Calendar": { + "type": "object", + "properties": { + "externalId": { + "type": "string" + }, + "integration": { + "type": "string" + }, + "name": { + "type": "string" + }, + "primary": { + "type": "boolean", + "nullable": true + }, + "readOnly": { + "type": "boolean" + }, + "email": { + "type": "string" + }, + "isSelected": { + "type": "boolean" + }, + "credentialId": { + "type": "number" + } + }, + "required": [ + "externalId", + "readOnly", + "isSelected", + "credentialId" + ] + }, + "ConnectedCalendar": { + "type": "object", + "properties": { + "integration": { + "$ref": "#/components/schemas/Integration" + }, + "credentialId": { + "type": "number" + }, + "primary": { + "$ref": "#/components/schemas/Primary" + }, + "calendars": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Calendar" + } + } + }, + "required": [ + "integration", + "credentialId" + ] + }, + "DestinationCalendar": { + "type": "object", + "properties": { + "id": { + "type": "number" + }, + "integration": { + "type": "string" + }, + "externalId": { + "type": "string" + }, + "primaryEmail": { + "type": "string", + "nullable": true + }, + "userId": { + "type": "number", + "nullable": true + }, + "eventTypeId": { + "type": "number", + "nullable": true + }, + "credentialId": { + "type": "number", + "nullable": true + }, + "name": { + "type": "string", + "nullable": true + }, + "primary": { + "type": "boolean" + }, + "readOnly": { + "type": "boolean" + }, + "email": { + "type": "string" + }, + "integrationTitle": { + "type": "string" + } + }, + "required": [ + "id", + "integration", + "externalId", + "primaryEmail", + "userId", + "eventTypeId", + "credentialId" + ] + }, + "ConnectedCalendarsData": { + "type": "object", + "properties": { + "connectedCalendars": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ConnectedCalendar" + } + }, + "destinationCalendar": { + "$ref": "#/components/schemas/DestinationCalendar" + } + }, + "required": [ + "connectedCalendars", + "destinationCalendar" + ] + }, + "ConnectedCalendarsOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/ConnectedCalendarsData" + } + }, + "required": [ + "status", + "data" + ] + }, + "DeleteCalendarCredentialsInputBodyDto": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "example": 10, + "description": "Credential ID of the calendar to delete, as returned by the /calendars endpoint" + } + }, + "required": [ + "id" + ] + }, + "DeletedCalendarCredentialsOutputDto": { + "type": "object", + "properties": { + "id": { + "type": "number" + }, + "type": { + "type": "string" + }, + "userId": { + "type": "number", + "nullable": true + }, + "teamId": { + "type": "number", + "nullable": true + }, + "appId": { + "type": "string", + "nullable": true + }, + "invalid": { + "type": "boolean", + "nullable": true + } + }, + "required": [ + "id", + "type", + "userId", + "teamId", + "appId", + "invalid" + ] + }, + "DeletedCalendarCredentialsOutputResponseDto": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/DeletedCalendarCredentialsOutputDto" + } + }, + "required": [ + "status", + "data" + ] + }, + "Attendee": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "The name of the attendee.", + "example": "John Doe" + }, + "email": { + "type": "string", + "description": "The email of the attendee.", + "example": "john.doe@example.com" + }, + "timeZone": { + "type": "string", + "description": "The time zone of the attendee.", + "example": "America/New_York" + }, + "language": { + "type": "string", + "enum": [ + "ar", + "ca", + "de", + "es", + "eu", + "he", + "id", + "ja", + "lv", + "pl", + "ro", + "sr", + "th", + "vi", + "az", + "cs", + "el", + "es-419", + "fi", + "hr", + "it", + "km", + "nl", + "pt", + "ru", + "sv", + "tr", + "zh-CN", + "bg", + "da", + "en", + "et", + "fr", + "hu", + "iw", + "ko", + "no", + "pt-BR", + "sk", + "ta", + "uk", + "zh-TW" + ], + "description": "The preferred language of the attendee. Used for booking confirmation.", + "example": "it", + "default": "en" + } + }, + "required": [ + "name", + "email", + "timeZone" + ] + }, + "CreateBookingInput_2024_08_13": { + "type": "object", + "properties": { + "start": { + "type": "string", + "description": "The start time of the booking in ISO 8601 format in UTC timezone.", + "example": "2024-08-13T09:00:00Z" + }, + "eventTypeId": { + "type": "number", + "description": "The ID of the event type that is booked.", + "example": 123 + }, + "attendee": { + "description": "The attendee's details.", + "allOf": [ + { + "$ref": "#/components/schemas/Attendee" + } + ] + }, + "guests": { + "description": "An optional list of guest emails attending the event.", + "example": [ + "guest1@example.com", + "guest2@example.com" + ], + "type": "array", + "items": { + "type": "string" + } + }, + "meetingUrl": { + "type": "string", + "description": "Meeting URL just for this booking. Displayed in email and calendar event. If not provided then cal video link will be generated.", + "example": "https://example.com/meeting" + }, + "bookingFieldsResponses": { + "type": "object", + "description": "Booking field responses.", + "example": { + "customField": "customValue" + } + } + }, + "required": [ + "start", + "eventTypeId", + "attendee" + ] + }, + "CreateInstantBookingInput_2024_08_13": { + "type": "object", + "properties": { + "start": { + "type": "string", + "description": "The start time of the booking in ISO 8601 format in UTC timezone.", + "example": "2024-08-13T09:00:00Z" + }, + "eventTypeId": { + "type": "number", + "description": "The ID of the event type that is booked.", + "example": 123 + }, + "attendee": { + "description": "The attendee's details.", + "allOf": [ + { + "$ref": "#/components/schemas/Attendee" + } + ] + }, + "guests": { + "description": "An optional list of guest emails attending the event.", + "example": [ + "guest1@example.com", + "guest2@example.com" + ], + "type": "array", + "items": { + "type": "string" + } + }, + "meetingUrl": { + "type": "string", + "description": "Meeting URL just for this booking. Displayed in email and calendar event. If not provided then cal video link will be generated.", + "example": "https://example.com/meeting" + }, + "bookingFieldsResponses": { + "type": "object", + "description": "Booking field responses.", + "example": { + "customField": "customValue" + } + }, + "instant": { + "type": "boolean", + "description": "Flag indicating if the booking is an instant booking. Only available for team events.", + "example": true + } + }, + "required": [ + "start", + "eventTypeId", + "attendee", + "instant" + ] + }, + "CreateRecurringBookingInput_2024_08_13": { + "type": "object", + "properties": { + "start": { + "type": "string", + "description": "The start time of the booking in ISO 8601 format in UTC timezone.", + "example": "2024-08-13T09:00:00Z" + }, + "eventTypeId": { + "type": "number", + "description": "The ID of the event type that is booked.", + "example": 123 + }, + "attendee": { + "description": "The attendee's details.", + "allOf": [ + { + "$ref": "#/components/schemas/Attendee" + } + ] + }, + "guests": { + "description": "An optional list of guest emails attending the event.", + "example": [ + "guest1@example.com", + "guest2@example.com" + ], + "type": "array", + "items": { + "type": "string" + } + }, + "meetingUrl": { + "type": "string", + "description": "Meeting URL just for this booking. Displayed in email and calendar event. If not provided then cal video link will be generated.", + "example": "https://example.com/meeting" + }, + "bookingFieldsResponses": { + "type": "object", + "description": "Booking field responses.", + "example": { + "customField": "customValue" + } + } + }, + "required": [ + "start", + "eventTypeId", + "attendee" + ] + }, + "Host": { + "type": "object", + "properties": { + "id": { + "type": "number", + "example": 1 + }, + "name": { + "type": "string", + "example": "Jane Doe" + }, + "timeZone": { + "type": "string", + "example": "America/Los_Angeles" + } + }, + "required": [ + "id", + "name", + "timeZone" + ] + }, + "BookingOutput_2024_08_13": { + "type": "object", + "properties": { + "id": { + "type": "number", + "example": 123 + }, + "uid": { + "type": "string", + "example": "booking_uid_123" + }, + "hosts": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Host" + } + }, + "status": { + "type": "string", + "enum": [ + "cancelled", + "accepted", + "rejected", + "pending", + "rescheduled" + ], + "example": "accepted" + }, + "cancellationReason": { + "type": "string", + "example": "User requested cancellation" + }, + "reschedulingReason": { + "type": "string", + "example": "User rescheduled the event" + }, + "rescheduledFromUid": { + "type": "string", + "example": "previous_uid_123" + }, + "start": { + "type": "string", + "example": "2024-08-13T15:30:00Z" + }, + "end": { + "type": "string", + "example": "2024-08-13T16:30:00Z" + }, + "duration": { + "type": "number", + "example": 60 + }, + "eventTypeId": { + "type": "number", + "example": 45 + }, + "attendees": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Attendee" + } + }, + "guests": { + "example": [ + "guest1@example.com", + "guest2@example.com" + ], + "type": "array", + "items": { + "type": "string" + } + }, + "meetingUrl": { + "type": "string", + "example": "https://example.com/meeting" + }, + "absentHost": { + "type": "boolean", + "example": true + } + }, + "required": [ + "id", + "uid", + "hosts", + "status", + "start", + "end", + "duration", + "eventTypeId", + "attendees", + "absentHost" + ] + }, + "RecurringBookingOutput_2024_08_13": { + "type": "object", + "properties": { + "id": { + "type": "number", + "example": 456 + }, + "uid": { + "type": "string", + "example": "recurring_uid_123" + }, + "hosts": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Host" + } + }, + "status": { + "type": "string", + "enum": [ + "cancelled", + "accepted", + "rejected", + "pending" + ], + "example": "pending" + }, + "cancellationReason": { + "type": "string", + "example": "Event was cancelled" + }, + "reschedulingReason": { + "type": "string", + "example": "Event was rescheduled" + }, + "rescheduledFromUid": { + "type": "string", + "example": "previous_recurring_uid_123" + }, + "start": { + "type": "string", + "example": "2024-08-13T15:30:00Z" + }, + "end": { + "type": "string", + "example": "2024-08-13T16:30:00Z" + }, + "duration": { + "type": "number", + "example": 30 + }, + "eventTypeId": { + "type": "number", + "example": 50 + }, + "recurringBookingUid": { + "type": "string", + "example": "recurring_uid_987" + }, + "attendees": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Attendee" + } + }, + "guests": { + "example": [ + "guest3@example.com", + "guest4@example.com" + ], + "type": "array", + "items": { + "type": "string" + } + }, + "meetingUrl": { + "type": "string", + "example": "https://example.com/recurring-meeting" + }, + "absentHost": { + "type": "boolean", + "example": false + } + }, + "required": [ + "id", + "uid", + "hosts", + "status", + "start", + "end", + "duration", + "eventTypeId", + "recurringBookingUid", + "attendees", + "absentHost" + ] + }, + "CreateBookingOutput_2024_08_13": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "oneOf": [ + { + "$ref": "#/components/schemas/BookingOutput_2024_08_13" + }, + { + "type": "array", + "items": { + "$ref": "#/components/schemas/RecurringBookingOutput_2024_08_13" + } + } + ], + "description": "Booking data, which can be either a BookingOutput object or an array of RecurringBookingOutput objects" + } + }, + "required": [ + "status", + "data" + ] + }, + "GetBookingOutput_2024_08_13": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "oneOf": [ + { + "$ref": "#/components/schemas/BookingOutput_2024_08_13" + }, + { + "$ref": "#/components/schemas/RecurringBookingOutput_2024_08_13" + }, + { + "type": "array", + "items": { + "$ref": "#/components/schemas/RecurringBookingOutput_2024_08_13" + } + } + ], + "description": "Booking data, which can be either a BookingOutput object, a RecurringBookingOutput object, or an array of RecurringBookingOutput objects" + } + }, + "required": [ + "status", + "data" + ] + }, + "GetBookingsOutput_2024_08_13": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/BookingOutput_2024_08_13" + }, + { + "$ref": "#/components/schemas/RecurringBookingOutput_2024_08_13" + } + ] + }, + "description": "Array of booking data, which can contain either BookingOutput objects or RecurringBookingOutput objects" + } + }, + "required": [ + "status", + "data" + ] + }, + "RescheduleBookingInput_2024_08_13": { + "type": "object", + "properties": { + "start": { + "type": "string", + "description": "Start time in ISO 8601 format for the new booking", + "example": "2024-08-13T10:00:00Z" + }, + "reschedulingReason": { + "type": "string", + "example": "User requested reschedule", + "description": "Reason for rescheduling the booking" + } + }, + "required": [ + "start" + ] + }, + "RescheduleBookingOutput_2024_08_13": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "oneOf": [ + { + "$ref": "#/components/schemas/BookingOutput_2024_08_13" + }, + { + "$ref": "#/components/schemas/RecurringBookingOutput_2024_08_13" + } + ], + "description": "Booking data, which can be either a BookingOutput object or a RecurringBookingOutput object" + } + }, + "required": [ + "status", + "data" + ] + }, + "CancelBookingInput_2024_08_13": { + "type": "object", + "properties": { + "cancellationReason": { + "type": "string", + "example": "User requested cancellation" + } + }, + "required": [ + "cancellationReason" + ] + }, + "CancelBookingOutput_2024_08_13": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "oneOf": [ + { + "$ref": "#/components/schemas/BookingOutput_2024_08_13" + }, + { + "$ref": "#/components/schemas/RecurringBookingOutput_2024_08_13" + }, + { + "type": "array", + "items": { + "$ref": "#/components/schemas/RecurringBookingOutput_2024_08_13" + } + } + ], + "description": "Booking data, which can be either a BookingOutput object, a RecurringBookingOutput object, or an array of RecurringBookingOutput objects" + } + }, + "required": [ + "status", + "data" + ] + }, + "MarkAbsentBookingInput_2024_08_13": { + "type": "object", + "properties": { + "host": { + "type": "boolean", + "example": false, + "description": "Whether the host was absent" + }, + "attendees": { + "description": "Toggle whether an attendee was absent or not.", + "example": [ + { + "absent": true, + "email": "someone@gmail.com" + } + ], + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "attendees" + ] + }, + "MarkAbsentBookingOutput_2024_08_13": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "oneOf": [ + { + "$ref": "#/components/schemas/BookingOutput_2024_08_13" + }, + { + "$ref": "#/components/schemas/RecurringBookingOutput_2024_08_13" + } + ], + "description": "Booking data, which can be either a BookingOutput object or a RecurringBookingOutput object" + } + }, + "required": [ + "status", + "data" + ] + }, + "ReserveSlotInput": { + "type": "object", + "properties": {} + }, + "UserWebhookOutputDto": { + "type": "object", + "properties": { + "payloadTemplate": { + "type": "string", + "description": "The template of the payload that will be sent to the subscriberUrl, check cal.com/docs/core-features/webhooks for more information", + "example": "{\"content\":\"A new event has been scheduled\",\"type\":\"{{type}}\",\"name\":\"{{title}}\",\"organizer\":\"{{organizer.name}}\",\"booker\":\"{{attendees.0.name}}\"}" + }, + "userId": { + "type": "number" + }, + "id": { + "type": "number" + }, + "triggers": { + "type": "array", + "items": { + "type": "object" + } + }, + "subscriberUrl": { + "type": "string" + }, + "active": { + "type": "boolean" + }, + "secret": { + "type": "string" + } + }, + "required": [ + "payloadTemplate", + "userId", + "id", + "triggers", + "subscriberUrl", + "active" + ] + }, + "UserWebhookOutputResponseDto": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/UserWebhookOutputDto" + } + }, + "required": [ + "status", + "data" + ] + }, + "UserWebhooksOutputResponseDto": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UserWebhookOutputDto" + } + } + }, + "required": [ + "status", + "data" + ] + }, + "EventTypeWebhookOutputDto": { + "type": "object", + "properties": { + "payloadTemplate": { + "type": "string", + "description": "The template of the payload that will be sent to the subscriberUrl, check cal.com/docs/core-features/webhooks for more information", + "example": "{\"content\":\"A new event has been scheduled\",\"type\":\"{{type}}\",\"name\":\"{{title}}\",\"organizer\":\"{{organizer.name}}\",\"booker\":\"{{attendees.0.name}}\"}" + }, + "eventTypeId": { + "type": "number" + }, + "id": { + "type": "number" + }, + "triggers": { + "type": "array", + "items": { + "type": "object" + } + }, + "subscriberUrl": { + "type": "string" + }, + "active": { + "type": "boolean" + }, + "secret": { + "type": "string" + } + }, + "required": [ + "payloadTemplate", + "eventTypeId", + "id", + "triggers", + "subscriberUrl", + "active" + ] + }, + "EventTypeWebhookOutputResponseDto": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/EventTypeWebhookOutputDto" + } + }, + "required": [ + "status", + "data" + ] + }, + "EventTypeWebhooksOutputResponseDto": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/EventTypeWebhookOutputDto" + } + } + }, + "required": [ + "status", + "data" + ] + }, + "DeleteManyWebhooksOutputResponseDto": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "type": "string" + } + }, + "required": [ + "status", + "data" + ] + }, + "OAuthClientWebhookOutputDto": { + "type": "object", + "properties": { + "payloadTemplate": { + "type": "string", + "description": "The template of the payload that will be sent to the subscriberUrl, check cal.com/docs/core-features/webhooks for more information", + "example": "{\"content\":\"A new event has been scheduled\",\"type\":\"{{type}}\",\"name\":\"{{title}}\",\"organizer\":\"{{organizer.name}}\",\"booker\":\"{{attendees.0.name}}\"}" + }, + "oAuthClientId": { + "type": "string" + }, + "id": { + "type": "number" + }, + "triggers": { + "type": "array", + "items": { + "type": "object" + } + }, + "subscriberUrl": { + "type": "string" + }, + "active": { + "type": "boolean" + }, + "secret": { + "type": "string" + } + }, + "required": [ + "payloadTemplate", + "oAuthClientId", + "id", + "triggers", + "subscriberUrl", + "active" + ] + }, + "OAuthClientWebhookOutputResponseDto": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/OAuthClientWebhookOutputDto" + } + }, + "required": [ + "status", + "data" + ] + }, + "OAuthClientWebhooksOutputResponseDto": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/OAuthClientWebhookOutputDto" + } + } + }, + "required": [ + "status", + "data" + ] + }, + "DestinationCalendarsInputBodyDto": { + "type": "object", + "properties": { + "integration": { + "type": "string", + "example": "apple_calendar", + "description": "The calendar service you want to integrate, as returned by the /calendars endpoint", + "enum": [ + "apple_calendar", + "google_calendar", + "office365_calendar" + ] + }, + "externalId": { + "type": "string", + "example": "https://caldav.icloud.com/26962146906/calendars/1644422A-1945-4438-BBC0-4F0Q23A57R7S/", + "description": "Unique identifier used to represent the specfic calendar, as returned by the /calendars endpoint" + } + }, + "required": [ + "integration", + "externalId" + ] + }, + "DestinationCalendarsOutputDto": { + "type": "object", + "properties": { + "userId": { + "type": "number" + }, + "integration": { + "type": "string" + }, + "externalId": { + "type": "string" + }, + "credentialId": { + "type": "number", + "nullable": true + } + }, + "required": [ + "userId", + "integration", + "externalId", + "credentialId" + ] + }, + "DestinationCalendarsOutputResponseDto": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/DestinationCalendarsOutputDto" + } + }, + "required": [ + "status", + "data" + ] + } + } + } +} \ No newline at end of file diff --git a/packages/platform/libraries/CHANGELOG.md b/packages/platform/libraries/CHANGELOG.md index a0762701137871..827d539c3b8305 100644 --- a/packages/platform/libraries/CHANGELOG.md +++ b/packages/platform/libraries/CHANGELOG.md @@ -1,3 +1,6 @@ +## 0.0.35 +Released to support v2 bookings refactor https://github.com/calcom/cal.com/pull/16200/ (released from this branch) + ## 0.0.31 PR https://github.com/calcom/cal.com/pull/16414 fixed issue of deleting and rescheduling recurring events. diff --git a/packages/platform/libraries/package.json b/packages/platform/libraries/package.json index f640ed918904b9..dcc479a64f7c0d 100644 --- a/packages/platform/libraries/package.json +++ b/packages/platform/libraries/package.json @@ -1,6 +1,6 @@ { "name": "@calcom/platform-libraries", - "version": "1.2.3", + "version": "0.0.35", "main": "./dist/index.cjs", "module": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/packages/trpc/server/routers/viewer/bookings/get.handler.ts b/packages/trpc/server/routers/viewer/bookings/get.handler.ts index d377492e675aa4..a0cae887fc3521 100644 --- a/packages/trpc/server/routers/viewer/bookings/get.handler.ts +++ b/packages/trpc/server/routers/viewer/bookings/get.handler.ts @@ -212,7 +212,6 @@ export async function getBookings({ uid: true, recurringEventId: true, location: true, - responses: true, eventType: { select: { slug: true, @@ -237,7 +236,6 @@ export async function getBookings({ }, status: true, paid: true, - payment: { select: { paymentOption: true, diff --git a/yarn.lock b/yarn.lock index 41c16b6b101678..ee62ae221caab3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4134,9 +4134,8 @@ __metadata: dependencies: "@calcom/platform-constants": "*" "@calcom/platform-enums": "*" - "@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.33" + "@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.35" "@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2" - "@calcom/platform-libraries-1.2.3": "npm:@calcom/platform-libraries@1.2.3" "@calcom/platform-types": "*" "@calcom/platform-utils": "*" "@calcom/prisma": "*" @@ -5120,7 +5119,7 @@ __metadata: languageName: node linkType: hard -"@calcom/platform-libraries-1.2.3@npm:@calcom/platform-libraries@1.2.3, @calcom/platform-libraries@workspace:packages/platform/libraries": +"@calcom/platform-libraries@npm:@calcom/platform-libraries@0.0.35, @calcom/platform-libraries@workspace:packages/platform/libraries": version: 0.0.0-use.local resolution: "@calcom/platform-libraries@workspace:packages/platform/libraries" dependencies: @@ -5136,17 +5135,6 @@ __metadata: languageName: unknown linkType: soft -"@calcom/platform-libraries@npm:@calcom/platform-libraries@0.0.33": - version: 0.0.33 - resolution: "@calcom/platform-libraries@npm:0.0.33" - dependencies: - "@calcom/core": "*" - "@calcom/features": "*" - "@calcom/lib": "*" - checksum: 75d78e298b380fbf72568ecd7fe70b8a1decc4c5a644d7e5032a5c493645967baf6645a27149754e0b173a3e36f4f83d9600129d858b4160fba77004daf59997 - languageName: node - linkType: hard - "@calcom/platform-types@*, @calcom/platform-types@workspace:packages/platform/types": version: 0.0.0-use.local resolution: "@calcom/platform-types@workspace:packages/platform/types" From a580d521b3c7275d626e63419e9642b48950a2bc Mon Sep 17 00:00:00 2001 From: supalarry Date: Thu, 12 Sep 2024 14:32:39 +0200 Subject: [PATCH 67/96] fix: ci --- .../services/event-types.service.ts | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/event-types.service.ts b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/event-types.service.ts index ea800cd87d4773..bbded74b95e547 100644 --- a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/event-types.service.ts +++ b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/event-types.service.ts @@ -38,17 +38,8 @@ export class EventTypesService_2024_06_14 { async createUserEventType(user: UserWithProfile, body: CreateEventTypeInput_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 { bookingLimits, durationLimits, ...bodyTransformed } = + this.inputEventTypesService.transformInputCreateEventType(body); const { eventType: eventTypeCreated } = await createEventType({ input: bodyTransformed, ctx: { @@ -64,12 +55,6 @@ export class EventTypesService_2024_06_14 { id: eventTypeCreated.id, bookingLimits, durationLimits, - periodType, - periodDays, - periodCountCalendarDays, - periodStartDate, - periodEndDate, - recurrence, ...bodyTransformed, }, ctx: { From 31c84582fd182446bdc83e8dd2274def453db5fd Mon Sep 17 00:00:00 2001 From: supalarry Date: Thu, 12 Sep 2024 14:35:14 +0200 Subject: [PATCH 68/96] fix: ci --- .../services/event-types/organizations-event-types.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/api/v2/src/modules/organizations/services/event-types/organizations-event-types.service.ts b/apps/api/v2/src/modules/organizations/services/event-types/organizations-event-types.service.ts index e23bbe0e29587a..2eef2c4ccea2ee 100644 --- a/apps/api/v2/src/modules/organizations/services/event-types/organizations-event-types.service.ts +++ b/apps/api/v2/src/modules/organizations/services/event-types/organizations-event-types.service.ts @@ -105,7 +105,7 @@ export class OrganizationsEventTypesService { role: user.role, organizationId: user.organizationId, organization: { isOrgAdmin }, - profile: { id: profileId }, + profile: { id: profileId || null }, metadata: user.metadata, }; } From 7dd755ea0f0fb2f7a6b389eda9785c7507aa6e65 Mon Sep 17 00:00:00 2001 From: supalarry Date: Thu, 12 Sep 2024 14:45:47 +0200 Subject: [PATCH 69/96] fix: ci --- .../services/output-event-types.service.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/output-event-types.service.ts b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/output-event-types.service.ts index 205793df44fe8e..a8ac4032bb2b8b 100644 --- a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/output-event-types.service.ts +++ b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/output-event-types.service.ts @@ -156,6 +156,7 @@ export class OutputEventTypesService_2024_06_14 { transformRecurringEvent(recurringEvent: any) { if (!recurringEvent) return null; const recurringEventParsed = parseRecurringEvent(recurringEvent); + if (!recurringEventParsed) return null; return getResponseEventTypeRecurrence(recurringEventParsed); } From 360b4f29994d3da7a24306ec7cae9c54f9395283 Mon Sep 17 00:00:00 2001 From: supalarry Date: Thu, 12 Sep 2024 16:06:38 +0200 Subject: [PATCH 70/96] fix: ci --- .../controllers/destination-calendars.controller.e2e-spec.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/api/v2/src/modules/destination-calendars/controllers/destination-calendars.controller.e2e-spec.ts b/apps/api/v2/src/modules/destination-calendars/controllers/destination-calendars.controller.e2e-spec.ts index 80e4b7037dc6ef..aea7ef04d6110e 100644 --- a/apps/api/v2/src/modules/destination-calendars/controllers/destination-calendars.controller.e2e-spec.ts +++ b/apps/api/v2/src/modules/destination-calendars/controllers/destination-calendars.controller.e2e-spec.ts @@ -98,6 +98,7 @@ describe("Platform Destination Calendar Endpoints", () => { error: { message: "" }, }, ], + destinationCalendar: null, }) ); app = moduleRef.createNestApplication(); From 186008684788094a739120df8d93db0df7d07eb7 Mon Sep 17 00:00:00 2001 From: supalarry Date: Thu, 12 Sep 2024 16:38:09 +0200 Subject: [PATCH 71/96] cleanup script platform types --- packages/platform/types/package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/platform/types/package.json b/packages/platform/types/package.json index 80110c80ad4a2d..466c1501fe4d6f 100644 --- a/packages/platform/types/package.json +++ b/packages/platform/types/package.json @@ -4,7 +4,8 @@ "main": "./dist/index.js", "types": "./dist/index.js", "scripts": { - "build": "tsc --build --force tsconfig.json", + "build": "yarn clean && tsc --build --force tsconfig.json", + "clean": "rm -rf ./dist", "build:watch": "tsc --build --force ./tsconfig.json --watch", "post-install": "yarn build" }, From cdb7cc91c9c72f22f2b40fb25f09cbc1c510eaf8 Mon Sep 17 00:00:00 2001 From: supalarry Date: Fri, 13 Sep 2024 08:40:29 +0200 Subject: [PATCH 72/96] fix: use libraries from npm --- packages/platform/libraries/package.json | 2 +- yarn.lock | 13 ++++++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/packages/platform/libraries/package.json b/packages/platform/libraries/package.json index dcc479a64f7c0d..2df5d3d0370e2a 100644 --- a/packages/platform/libraries/package.json +++ b/packages/platform/libraries/package.json @@ -1,6 +1,6 @@ { "name": "@calcom/platform-libraries", - "version": "0.0.35", + "version": "0.0.0", "main": "./dist/index.cjs", "module": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/yarn.lock b/yarn.lock index ee62ae221caab3..9c5fb6b5eca861 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5119,7 +5119,18 @@ __metadata: languageName: node linkType: hard -"@calcom/platform-libraries@npm:@calcom/platform-libraries@0.0.35, @calcom/platform-libraries@workspace:packages/platform/libraries": +"@calcom/platform-libraries@npm:@calcom/platform-libraries@0.0.35": + version: 0.0.35 + resolution: "@calcom/platform-libraries@npm:0.0.35" + dependencies: + "@calcom/core": "*" + "@calcom/features": "*" + "@calcom/lib": "*" + checksum: e4ed48c6e518b21dc0c13e7237dc667c38544480d022bf47b4aae3dbcf1a2da8e2cc4aeaf652e4ce90c04a0190c015199908b3dce51333c6b3920933fde660d9 + languageName: node + linkType: hard + +"@calcom/platform-libraries@workspace:packages/platform/libraries": version: 0.0.0-use.local resolution: "@calcom/platform-libraries@workspace:packages/platform/libraries" dependencies: From 49efa8e948bfb362d2195a2c3a153d828cbcbe84 Mon Sep 17 00:00:00 2001 From: Morgan Vernay Date: Fri, 13 Sep 2024 11:12:10 +0300 Subject: [PATCH 73/96] chore: set test env vapid keys --- apps/api/v2/test/setEnvVars.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/api/v2/test/setEnvVars.ts b/apps/api/v2/test/setEnvVars.ts index e7a2393878329e..102a16fc138dd6 100644 --- a/apps/api/v2/test/setEnvVars.ts +++ b/apps/api/v2/test/setEnvVars.ts @@ -22,4 +22,7 @@ const env: Partial> = { process.env = { ...env, ...process.env, + NEXT_PUBLIC_VAPID_PUBLIC_KEY: + "BIds0AQJ96xGBjTSMHTOqLBLutQE7Lu32KKdgSdy7A2cS4mKI2cgb3iGkhDJa5Siy-stezyuPm8qpbhmNxdNHMw", + VAPID_PRIVATE_KEY: "6cJtkASCar5sZWguIAW7OjvyixpBw9p8zL8WDDwk9Jk", }; From fab1cb0f5eeb65e4f542bfbeb83849ceed7ba428 Mon Sep 17 00:00:00 2001 From: supalarry Date: Fri, 13 Sep 2024 12:45:09 +0200 Subject: [PATCH 74/96] fix: event type tests --- .../services/input-event-types.service.ts | 64 ++++++++++++++++++- .../inputs/booking-fields.input.ts | 49 -------------- 2 files changed, 63 insertions(+), 50 deletions(-) diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/input-event-types.service.ts b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/input-event-types.service.ts index 1901acd77a657e..20df5debc78acb 100644 --- a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/input-event-types.service.ts +++ b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/input-event-types.service.ts @@ -6,6 +6,7 @@ import { transformApiEventTypeIntervalLimits, transformApiEventTypeFutureBookingLimits, transformApiEventTypeRecurrence, + SystemField, } from "@calcom/platform-libraries"; import { CreateEventTypeInput_2024_06_14, UpdateEventTypeInput_2024_06_14 } from "@calcom/platform-types"; @@ -81,7 +82,68 @@ export class InputEventTypesService_2024_06_14 { } transformInputBookingFields(inputBookingFields: CreateEventTypeInput_2024_06_14["bookingFields"]) { - return transformApiEventTypeBookingFields(inputBookingFields); + const transformed = transformApiEventTypeBookingFields(inputBookingFields); + + const systemBeforeFields: SystemField[] = [ + { + type: "name", + name: "name", + editable: "system", + defaultLabel: "your_name", + required: true, + sources: [ + { + label: "Default", + id: "default", + type: "default", + }, + ], + }, + { + defaultLabel: "email_address", + type: "email", + name: "email", + required: true, + editable: "system", + sources: [ + { + label: "Default", + id: "default", + type: "default", + }, + ], + }, + { + defaultLabel: "location", + type: "radioInput", + name: "location", + editable: "system", + hideWhenJustOneOption: true, + required: false, + getOptionsAt: "locations", + optionsInputs: { + attendeeInPerson: { + type: "address", + required: true, + placeholder: "", + }, + phone: { + type: "phone", + required: true, + placeholder: "", + }, + }, + sources: [ + { + label: "Default", + id: "default", + type: "default", + }, + ], + }, + ]; + + return [...systemBeforeFields, ...transformed]; } transformInputIntervalLimits(inputBookingFields: CreateEventTypeInput_2024_06_14["bookingLimitsCount"]) { diff --git a/packages/platform/types/event-types/event-types_2024_06_14/inputs/booking-fields.input.ts b/packages/platform/types/event-types/event-types_2024_06_14/inputs/booking-fields.input.ts index 2dbd1098feb143..b544f304a9c66e 100644 --- a/packages/platform/types/event-types/event-types_2024_06_14/inputs/booking-fields.input.ts +++ b/packages/platform/types/event-types/event-types_2024_06_14/inputs/booking-fields.input.ts @@ -21,51 +21,6 @@ const bookingFields = [ "boolean", ] as const; -export class NameField_2024_06_14 { - @IsIn(bookingFields) - @DocsProperty() - type!: "name"; - - @IsString() - @DocsProperty() - slug!: string; - - @IsString() - @DocsProperty() - label!: string; - - @IsBoolean() - @DocsProperty() - required!: boolean; - - @IsString() - @IsOptional() - @DocsProperty() - placeholder?: string; -} -export class EmailField_2024_06_14 { - @IsIn(bookingFields) - @DocsProperty() - type!: "email"; - - @IsString() - @DocsProperty() - slug!: string; - - @IsString() - @DocsProperty() - label!: string; - - @IsBoolean() - @DocsProperty() - required!: boolean; - - @IsString() - @IsOptional() - @DocsProperty() - placeholder?: string; -} - export class PhoneField_2024_06_14 { @IsIn(bookingFields) @DocsProperty() @@ -322,8 +277,6 @@ export class BooleanField_2024_06_14 { } export type BookingField_2024_06_14 = - | NameField_2024_06_14 - | EmailField_2024_06_14 | PhoneField_2024_06_14 | AddressField_2024_06_14 | TextField_2024_06_14 @@ -339,8 +292,6 @@ export type BookingField_2024_06_14 = @ValidatorConstraint({ async: true }) class BookingFieldValidator_2024_06_14 implements ValidatorConstraintInterface { private classTypeMap: { [key: string]: new () => BookingField_2024_06_14 } = { - name: NameField_2024_06_14, - email: EmailField_2024_06_14, phone: PhoneField_2024_06_14, address: AddressField_2024_06_14, text: TextField_2024_06_14, From bee9a15cb27cd34705f34c427b6b50d51e3b7ee7 Mon Sep 17 00:00:00 2001 From: supalarry Date: Fri, 13 Sep 2024 12:57:31 +0200 Subject: [PATCH 75/96] fix: remove location from system fields --- .../services/input-event-types.service.ts | 28 ------------------- 1 file changed, 28 deletions(-) diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/input-event-types.service.ts b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/input-event-types.service.ts index 20df5debc78acb..776ca2badabe1a 100644 --- a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/input-event-types.service.ts +++ b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/input-event-types.service.ts @@ -113,34 +113,6 @@ export class InputEventTypesService_2024_06_14 { }, ], }, - { - defaultLabel: "location", - type: "radioInput", - name: "location", - editable: "system", - hideWhenJustOneOption: true, - required: false, - getOptionsAt: "locations", - optionsInputs: { - attendeeInPerson: { - type: "address", - required: true, - placeholder: "", - }, - phone: { - type: "phone", - required: true, - placeholder: "", - }, - }, - sources: [ - { - label: "Default", - id: "default", - type: "default", - }, - ], - }, ]; return [...systemBeforeFields, ...transformed]; From e64b473b73f7ef0fe88942cd87277d29a512b946 Mon Sep 17 00:00:00 2001 From: supalarry Date: Fri, 13 Sep 2024 13:44:35 +0200 Subject: [PATCH 76/96] fix legacy event types --- .../services/event-types.service.ts | 12 ++++++- .../services/input-event-types.service.ts | 33 ++--------------- .../v2/src/lib/get-default-booking-fields.ts | 36 +++++++++++++++++++ 3 files changed, 49 insertions(+), 32 deletions(-) create mode 100644 apps/api/v2/src/lib/get-default-booking-fields.ts diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_04_15/services/event-types.service.ts b/apps/api/v2/src/ee/event-types/event-types_2024_04_15/services/event-types.service.ts index 549851f6998334..76f47d48bfd93c 100644 --- a/apps/api/v2/src/ee/event-types/event-types_2024_04_15/services/event-types.service.ts +++ b/apps/api/v2/src/ee/event-types/event-types_2024_04_15/services/event-types.service.ts @@ -3,6 +3,7 @@ import { EventTypesRepository_2024_04_15 } from "@/ee/event-types/event-types_20 import { CreateEventTypeInput_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/inputs/create-event-type.input"; import { UpdateEventTypeInput_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/inputs/update-event-type.input"; import { EventTypeOutput } from "@/ee/event-types/event-types_2024_04_15/outputs/event-type.output"; +import { getDefaultBookingFields } from "@/lib/get-default-booking-fields"; import { MembershipsRepository } from "@/modules/memberships/memberships.repository"; import { PrismaWriteService } from "@/modules/prisma/prisma-write.service"; import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository"; @@ -36,7 +37,7 @@ export class EventTypesService_2024_04_15 { await this.checkCanCreateEventType(user.id, body); const eventTypeUser = await this.getUserToCreateEvent(user); const { eventType } = await createEventType({ - input: body, + input: this.transformInputCreateEventType(body), ctx: { user: eventTypeUser, // eslint-disable-next-line @typescript-eslint/ban-ts-comment @@ -47,6 +48,15 @@ export class EventTypesService_2024_04_15 { return eventType as EventTypeOutput; } + transformInputCreateEventType(inputEventType: CreateEventTypeInput_2024_04_15) { + const systemBeforeFields = getDefaultBookingFields(); + + return { + ...inputEventType, + bookingFields: systemBeforeFields, + }; + } + async checkCanCreateEventType(userId: number, body: CreateEventTypeInput_2024_04_15) { const existsWithSlug = await this.eventTypesRepository.getUserEventTypeBySlug(userId, body.slug); if (existsWithSlug) { diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/input-event-types.service.ts b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/input-event-types.service.ts index 776ca2badabe1a..4670dec13b3a9a 100644 --- a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/input-event-types.service.ts +++ b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/input-event-types.service.ts @@ -1,3 +1,4 @@ +import { getDefaultBookingFields } from "@/lib/get-default-booking-fields"; import { Injectable } from "@nestjs/common"; import { @@ -6,7 +7,6 @@ import { transformApiEventTypeIntervalLimits, transformApiEventTypeFutureBookingLimits, transformApiEventTypeRecurrence, - SystemField, } from "@calcom/platform-libraries"; import { CreateEventTypeInput_2024_06_14, UpdateEventTypeInput_2024_06_14 } from "@calcom/platform-types"; @@ -84,36 +84,7 @@ export class InputEventTypesService_2024_06_14 { transformInputBookingFields(inputBookingFields: CreateEventTypeInput_2024_06_14["bookingFields"]) { const transformed = transformApiEventTypeBookingFields(inputBookingFields); - const systemBeforeFields: SystemField[] = [ - { - type: "name", - name: "name", - editable: "system", - defaultLabel: "your_name", - required: true, - sources: [ - { - label: "Default", - id: "default", - type: "default", - }, - ], - }, - { - defaultLabel: "email_address", - type: "email", - name: "email", - required: true, - editable: "system", - sources: [ - { - label: "Default", - id: "default", - type: "default", - }, - ], - }, - ]; + const systemBeforeFields = getDefaultBookingFields(); return [...systemBeforeFields, ...transformed]; } diff --git a/apps/api/v2/src/lib/get-default-booking-fields.ts b/apps/api/v2/src/lib/get-default-booking-fields.ts new file mode 100644 index 00000000000000..811a526a401adb --- /dev/null +++ b/apps/api/v2/src/lib/get-default-booking-fields.ts @@ -0,0 +1,36 @@ +import { SystemField } from "@calcom/platform-libraries"; + +export function getDefaultBookingFields() { + const systemBeforeFields: SystemField[] = [ + { + type: "name", + name: "name", + editable: "system", + defaultLabel: "your_name", + required: true, + sources: [ + { + label: "Default", + id: "default", + type: "default", + }, + ], + }, + { + defaultLabel: "email_address", + type: "email", + name: "email", + required: true, + editable: "system", + sources: [ + { + label: "Default", + id: "default", + type: "default", + }, + ], + }, + ]; + + return systemBeforeFields; +} From 7e285af9aa2163c548e960f3f9cbbe6d7f9b965d Mon Sep 17 00:00:00 2001 From: supalarry Date: Fri, 13 Sep 2024 15:09:46 +0200 Subject: [PATCH 77/96] Revert "fix legacy event types" This reverts commit e64b473b73f7ef0fe88942cd87277d29a512b946. --- .../services/event-types.service.ts | 12 +------ .../services/input-event-types.service.ts | 33 +++++++++++++++-- .../v2/src/lib/get-default-booking-fields.ts | 36 ------------------- 3 files changed, 32 insertions(+), 49 deletions(-) delete mode 100644 apps/api/v2/src/lib/get-default-booking-fields.ts diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_04_15/services/event-types.service.ts b/apps/api/v2/src/ee/event-types/event-types_2024_04_15/services/event-types.service.ts index 76f47d48bfd93c..549851f6998334 100644 --- a/apps/api/v2/src/ee/event-types/event-types_2024_04_15/services/event-types.service.ts +++ b/apps/api/v2/src/ee/event-types/event-types_2024_04_15/services/event-types.service.ts @@ -3,7 +3,6 @@ import { EventTypesRepository_2024_04_15 } from "@/ee/event-types/event-types_20 import { CreateEventTypeInput_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/inputs/create-event-type.input"; import { UpdateEventTypeInput_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/inputs/update-event-type.input"; import { EventTypeOutput } from "@/ee/event-types/event-types_2024_04_15/outputs/event-type.output"; -import { getDefaultBookingFields } from "@/lib/get-default-booking-fields"; import { MembershipsRepository } from "@/modules/memberships/memberships.repository"; import { PrismaWriteService } from "@/modules/prisma/prisma-write.service"; import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository"; @@ -37,7 +36,7 @@ export class EventTypesService_2024_04_15 { await this.checkCanCreateEventType(user.id, body); const eventTypeUser = await this.getUserToCreateEvent(user); const { eventType } = await createEventType({ - input: this.transformInputCreateEventType(body), + input: body, ctx: { user: eventTypeUser, // eslint-disable-next-line @typescript-eslint/ban-ts-comment @@ -48,15 +47,6 @@ export class EventTypesService_2024_04_15 { return eventType as EventTypeOutput; } - transformInputCreateEventType(inputEventType: CreateEventTypeInput_2024_04_15) { - const systemBeforeFields = getDefaultBookingFields(); - - return { - ...inputEventType, - bookingFields: systemBeforeFields, - }; - } - async checkCanCreateEventType(userId: number, body: CreateEventTypeInput_2024_04_15) { const existsWithSlug = await this.eventTypesRepository.getUserEventTypeBySlug(userId, body.slug); if (existsWithSlug) { diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/input-event-types.service.ts b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/input-event-types.service.ts index 4670dec13b3a9a..776ca2badabe1a 100644 --- a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/input-event-types.service.ts +++ b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/input-event-types.service.ts @@ -1,4 +1,3 @@ -import { getDefaultBookingFields } from "@/lib/get-default-booking-fields"; import { Injectable } from "@nestjs/common"; import { @@ -7,6 +6,7 @@ import { transformApiEventTypeIntervalLimits, transformApiEventTypeFutureBookingLimits, transformApiEventTypeRecurrence, + SystemField, } from "@calcom/platform-libraries"; import { CreateEventTypeInput_2024_06_14, UpdateEventTypeInput_2024_06_14 } from "@calcom/platform-types"; @@ -84,7 +84,36 @@ export class InputEventTypesService_2024_06_14 { transformInputBookingFields(inputBookingFields: CreateEventTypeInput_2024_06_14["bookingFields"]) { const transformed = transformApiEventTypeBookingFields(inputBookingFields); - const systemBeforeFields = getDefaultBookingFields(); + const systemBeforeFields: SystemField[] = [ + { + type: "name", + name: "name", + editable: "system", + defaultLabel: "your_name", + required: true, + sources: [ + { + label: "Default", + id: "default", + type: "default", + }, + ], + }, + { + defaultLabel: "email_address", + type: "email", + name: "email", + required: true, + editable: "system", + sources: [ + { + label: "Default", + id: "default", + type: "default", + }, + ], + }, + ]; return [...systemBeforeFields, ...transformed]; } diff --git a/apps/api/v2/src/lib/get-default-booking-fields.ts b/apps/api/v2/src/lib/get-default-booking-fields.ts deleted file mode 100644 index 811a526a401adb..00000000000000 --- a/apps/api/v2/src/lib/get-default-booking-fields.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { SystemField } from "@calcom/platform-libraries"; - -export function getDefaultBookingFields() { - const systemBeforeFields: SystemField[] = [ - { - type: "name", - name: "name", - editable: "system", - defaultLabel: "your_name", - required: true, - sources: [ - { - label: "Default", - id: "default", - type: "default", - }, - ], - }, - { - defaultLabel: "email_address", - type: "email", - name: "email", - required: true, - editable: "system", - sources: [ - { - label: "Default", - id: "default", - type: "default", - }, - ], - }, - ]; - - return systemBeforeFields; -} From 98925de1ff5e66ac8189257606608a7187afd1f3 Mon Sep 17 00:00:00 2001 From: supalarry Date: Fri, 13 Sep 2024 15:09:56 +0200 Subject: [PATCH 78/96] Revert "fix: remove location from system fields" This reverts commit bee9a15cb27cd34705f34c427b6b50d51e3b7ee7. --- .../services/input-event-types.service.ts | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/input-event-types.service.ts b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/input-event-types.service.ts index 776ca2badabe1a..20df5debc78acb 100644 --- a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/input-event-types.service.ts +++ b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/input-event-types.service.ts @@ -113,6 +113,34 @@ export class InputEventTypesService_2024_06_14 { }, ], }, + { + defaultLabel: "location", + type: "radioInput", + name: "location", + editable: "system", + hideWhenJustOneOption: true, + required: false, + getOptionsAt: "locations", + optionsInputs: { + attendeeInPerson: { + type: "address", + required: true, + placeholder: "", + }, + phone: { + type: "phone", + required: true, + placeholder: "", + }, + }, + sources: [ + { + label: "Default", + id: "default", + type: "default", + }, + ], + }, ]; return [...systemBeforeFields, ...transformed]; From 76008be11517a78478b5224604250dc239f0fc48 Mon Sep 17 00:00:00 2001 From: supalarry Date: Fri, 13 Sep 2024 15:10:08 +0200 Subject: [PATCH 79/96] Revert "fix: event type tests" This reverts commit fab1cb0f5eeb65e4f542bfbeb83849ceed7ba428. --- .../services/input-event-types.service.ts | 64 +------------------ .../inputs/booking-fields.input.ts | 49 ++++++++++++++ 2 files changed, 50 insertions(+), 63 deletions(-) diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/input-event-types.service.ts b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/input-event-types.service.ts index 20df5debc78acb..1901acd77a657e 100644 --- a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/input-event-types.service.ts +++ b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/input-event-types.service.ts @@ -6,7 +6,6 @@ import { transformApiEventTypeIntervalLimits, transformApiEventTypeFutureBookingLimits, transformApiEventTypeRecurrence, - SystemField, } from "@calcom/platform-libraries"; import { CreateEventTypeInput_2024_06_14, UpdateEventTypeInput_2024_06_14 } from "@calcom/platform-types"; @@ -82,68 +81,7 @@ export class InputEventTypesService_2024_06_14 { } transformInputBookingFields(inputBookingFields: CreateEventTypeInput_2024_06_14["bookingFields"]) { - const transformed = transformApiEventTypeBookingFields(inputBookingFields); - - const systemBeforeFields: SystemField[] = [ - { - type: "name", - name: "name", - editable: "system", - defaultLabel: "your_name", - required: true, - sources: [ - { - label: "Default", - id: "default", - type: "default", - }, - ], - }, - { - defaultLabel: "email_address", - type: "email", - name: "email", - required: true, - editable: "system", - sources: [ - { - label: "Default", - id: "default", - type: "default", - }, - ], - }, - { - defaultLabel: "location", - type: "radioInput", - name: "location", - editable: "system", - hideWhenJustOneOption: true, - required: false, - getOptionsAt: "locations", - optionsInputs: { - attendeeInPerson: { - type: "address", - required: true, - placeholder: "", - }, - phone: { - type: "phone", - required: true, - placeholder: "", - }, - }, - sources: [ - { - label: "Default", - id: "default", - type: "default", - }, - ], - }, - ]; - - return [...systemBeforeFields, ...transformed]; + return transformApiEventTypeBookingFields(inputBookingFields); } transformInputIntervalLimits(inputBookingFields: CreateEventTypeInput_2024_06_14["bookingLimitsCount"]) { diff --git a/packages/platform/types/event-types/event-types_2024_06_14/inputs/booking-fields.input.ts b/packages/platform/types/event-types/event-types_2024_06_14/inputs/booking-fields.input.ts index b544f304a9c66e..2dbd1098feb143 100644 --- a/packages/platform/types/event-types/event-types_2024_06_14/inputs/booking-fields.input.ts +++ b/packages/platform/types/event-types/event-types_2024_06_14/inputs/booking-fields.input.ts @@ -21,6 +21,51 @@ const bookingFields = [ "boolean", ] as const; +export class NameField_2024_06_14 { + @IsIn(bookingFields) + @DocsProperty() + type!: "name"; + + @IsString() + @DocsProperty() + slug!: string; + + @IsString() + @DocsProperty() + label!: string; + + @IsBoolean() + @DocsProperty() + required!: boolean; + + @IsString() + @IsOptional() + @DocsProperty() + placeholder?: string; +} +export class EmailField_2024_06_14 { + @IsIn(bookingFields) + @DocsProperty() + type!: "email"; + + @IsString() + @DocsProperty() + slug!: string; + + @IsString() + @DocsProperty() + label!: string; + + @IsBoolean() + @DocsProperty() + required!: boolean; + + @IsString() + @IsOptional() + @DocsProperty() + placeholder?: string; +} + export class PhoneField_2024_06_14 { @IsIn(bookingFields) @DocsProperty() @@ -277,6 +322,8 @@ export class BooleanField_2024_06_14 { } export type BookingField_2024_06_14 = + | NameField_2024_06_14 + | EmailField_2024_06_14 | PhoneField_2024_06_14 | AddressField_2024_06_14 | TextField_2024_06_14 @@ -292,6 +339,8 @@ export type BookingField_2024_06_14 = @ValidatorConstraint({ async: true }) class BookingFieldValidator_2024_06_14 implements ValidatorConstraintInterface { private classTypeMap: { [key: string]: new () => BookingField_2024_06_14 } = { + name: NameField_2024_06_14, + email: EmailField_2024_06_14, phone: PhoneField_2024_06_14, address: AddressField_2024_06_14, text: TextField_2024_06_14, From 073e326b9e2d274d38f2182a9836a4f42eb8b6da Mon Sep 17 00:00:00 2001 From: supalarry Date: Wed, 18 Sep 2024 17:09:46 +0200 Subject: [PATCH 80/96] update libraries --- apps/api/v2/package.json | 2 +- packages/platform/libraries/CHANGELOG.md | 3 +++ yarn.lock | 10 +++++----- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/apps/api/v2/package.json b/apps/api/v2/package.json index 1e3b4f1c8af040..df9376a9b34af9 100644 --- a/apps/api/v2/package.json +++ b/apps/api/v2/package.json @@ -28,7 +28,7 @@ "dependencies": { "@calcom/platform-constants": "*", "@calcom/platform-enums": "*", - "@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.36", + "@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.37", "@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2", "@calcom/platform-types": "*", "@calcom/platform-utils": "*", diff --git a/packages/platform/libraries/CHANGELOG.md b/packages/platform/libraries/CHANGELOG.md index 0e770e96e923e4..a3d31a9fe46110 100644 --- a/packages/platform/libraries/CHANGELOG.md +++ b/packages/platform/libraries/CHANGELOG.md @@ -1,3 +1,6 @@ +## 0.0.37 +Released to support PR https://github.com/calcom/cal.com/pull/16200 + ## 0.0.36 Released to support PR https://github.com/calcom/cal.com/pull/16685 diff --git a/yarn.lock b/yarn.lock index 3b257cce7eb08a..4065521a7ef428 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4134,7 +4134,7 @@ __metadata: dependencies: "@calcom/platform-constants": "*" "@calcom/platform-enums": "*" - "@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.36" + "@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.37" "@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2" "@calcom/platform-types": "*" "@calcom/platform-utils": "*" @@ -5121,14 +5121,14 @@ __metadata: languageName: node linkType: hard -"@calcom/platform-libraries@npm:@calcom/platform-libraries@0.0.36": - version: 0.0.36 - resolution: "@calcom/platform-libraries@npm:0.0.36" +"@calcom/platform-libraries@npm:@calcom/platform-libraries@0.0.37": + version: 0.0.37 + resolution: "@calcom/platform-libraries@npm:0.0.37" dependencies: "@calcom/core": "*" "@calcom/features": "*" "@calcom/lib": "*" - checksum: 12147c9e10dda5c879d56d88ebc5a50a17e85b7ce683654af56c0eaafedb1167373bba3764c29c30e466ea7776bf96b7b191327703c125d22381b40802d09a02 + checksum: 02bd6c330d6eb80bc33977e9f6bd3b4f583bc014d26519272d7e7f76ad307a4810067933d0e98c8632bd689b2dd25c5b7a02b8e0509976534d772f2ae169674d languageName: node linkType: hard From 01b6e9afa43dcda4bb1e46cc277a54dd9927030f Mon Sep 17 00:00:00 2001 From: supalarry Date: Wed, 18 Sep 2024 18:23:04 +0200 Subject: [PATCH 81/96] fix: increase node space for ci runner --- .github/workflows/e2e-api-v2.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/e2e-api-v2.yml b/.github/workflows/e2e-api-v2.yml index 91682e2fd6bbc3..958c19125fe7d9 100644 --- a/.github/workflows/e2e-api-v2.yml +++ b/.github/workflows/e2e-api-v2.yml @@ -15,7 +15,7 @@ env: IS_E2E: true NEXTAUTH_SECRET: ${{ secrets.CI_NEXTAUTH_SECRET }} NEXTAUTH_URL: ${{ secrets.CI_NEXTAUTH_URL }} - NODE_OPTIONS: --max-old-space-size=4096 + NODE_OPTIONS: --max-old-space-size=8192 REDIS_URL: "redis://localhost:6379" STRIPE_PRIVATE_KEY: ${{ secrets.CI_STRIPE_PRIVATE_KEY }} STRIPE_API_KEY: ${{ secrets.CI_STRIPE_PRIVATE_KEY }} From 4b103f3de7e2bd6f57149c93d385ee254236eabc Mon Sep 17 00:00:00 2001 From: supalarry Date: Thu, 19 Sep 2024 09:01:49 +0200 Subject: [PATCH 82/96] fix: increase node space for ci runner --- .github/workflows/e2e-api-v2.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/e2e-api-v2.yml b/.github/workflows/e2e-api-v2.yml index 958c19125fe7d9..7d18d291c90736 100644 --- a/.github/workflows/e2e-api-v2.yml +++ b/.github/workflows/e2e-api-v2.yml @@ -15,7 +15,7 @@ env: IS_E2E: true NEXTAUTH_SECRET: ${{ secrets.CI_NEXTAUTH_SECRET }} NEXTAUTH_URL: ${{ secrets.CI_NEXTAUTH_URL }} - NODE_OPTIONS: --max-old-space-size=8192 + NODE_OPTIONS: --max-old-space-size=16384 REDIS_URL: "redis://localhost:6379" STRIPE_PRIVATE_KEY: ${{ secrets.CI_STRIPE_PRIVATE_KEY }} STRIPE_API_KEY: ${{ secrets.CI_STRIPE_PRIVATE_KEY }} From 445cfdaa44aa48a0c322f40ce6b83643728b97ab Mon Sep 17 00:00:00 2001 From: supalarry Date: Thu, 19 Sep 2024 09:50:16 +0200 Subject: [PATCH 83/96] fix: increase node space for ci runner --- .github/workflows/e2e-api-v2.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/e2e-api-v2.yml b/.github/workflows/e2e-api-v2.yml index 7d18d291c90736..7b678fdb52094f 100644 --- a/.github/workflows/e2e-api-v2.yml +++ b/.github/workflows/e2e-api-v2.yml @@ -15,7 +15,7 @@ env: IS_E2E: true NEXTAUTH_SECRET: ${{ secrets.CI_NEXTAUTH_SECRET }} NEXTAUTH_URL: ${{ secrets.CI_NEXTAUTH_URL }} - NODE_OPTIONS: --max-old-space-size=16384 + NODE_OPTIONS: --max-old-space-size=8192 REDIS_URL: "redis://localhost:6379" STRIPE_PRIVATE_KEY: ${{ secrets.CI_STRIPE_PRIVATE_KEY }} STRIPE_API_KEY: ${{ secrets.CI_STRIPE_PRIVATE_KEY }} @@ -70,6 +70,7 @@ jobs: - name: Run Tests working-directory: apps/api/v2 run: | + export NODE_OPTIONS=--max-old-space-size=8192 yarn test:e2e EXIT_CODE=$? echo "yarn test:e2e command exit code: $EXIT_CODE" From e93d1189445d723f6f66f4206c970479a193eb16 Mon Sep 17 00:00:00 2001 From: supalarry Date: Thu, 19 Sep 2024 13:59:50 +0200 Subject: [PATCH 84/96] readd swagger --- apps/api/v2/swagger/documentation.json | 12140 +++++++++++++++++++++++ 1 file changed, 12140 insertions(+) create mode 100644 apps/api/v2/swagger/documentation.json diff --git a/apps/api/v2/swagger/documentation.json b/apps/api/v2/swagger/documentation.json new file mode 100644 index 00000000000000..1f465b0ea5b6f7 --- /dev/null +++ b/apps/api/v2/swagger/documentation.json @@ -0,0 +1,12140 @@ +{ + "openapi": "3.0.0", + "paths": { + "/health": { + "get": { + "operationId": "AppController_getHealth", + "parameters": [], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + }, + "tags": [ + "Health - development only" + ] + } + }, + "/v2/oauth-clients/{clientId}/users": { + "get": { + "operationId": "OAuthClientUsersController_getManagedUsers", + "parameters": [ + { + "name": "clientId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + }, + { + "name": "limit", + "required": false, + "in": "query", + "description": "The number of items to return", + "example": 10, + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetManagedUsersOutput" + } + } + } + } + }, + "tags": [ + "Managed users" + ] + }, + "post": { + "operationId": "OAuthClientUsersController_createUser", + "parameters": [ + { + "name": "clientId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateManagedUserInput" + } + } + } + }, + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateManagedUserOutput" + } + } + } + } + }, + "tags": [ + "Managed users" + ] + } + }, + "/v2/oauth-clients/{clientId}/users/{userId}": { + "get": { + "operationId": "OAuthClientUsersController_getUserById", + "parameters": [ + { + "name": "clientId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + }, + { + "name": "userId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetManagedUserOutput" + } + } + } + } + }, + "tags": [ + "Managed users" + ] + }, + "patch": { + "operationId": "OAuthClientUsersController_updateUser", + "parameters": [ + { + "name": "clientId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + }, + { + "name": "userId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateManagedUserInput" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetManagedUserOutput" + } + } + } + } + }, + "tags": [ + "Managed users" + ] + }, + "delete": { + "operationId": "OAuthClientUsersController_deleteUser", + "parameters": [ + { + "name": "clientId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + }, + { + "name": "userId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetManagedUserOutput" + } + } + } + } + }, + "tags": [ + "Managed users" + ] + } + }, + "/v2/oauth-clients/{clientId}/users/{userId}/force-refresh": { + "post": { + "operationId": "OAuthClientUsersController_forceRefresh", + "parameters": [ + { + "name": "userId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "clientId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/KeysResponseDto" + } + } + } + } + }, + "tags": [ + "Managed users" + ] + } + }, + "/v2/oauth-clients": { + "post": { + "operationId": "OAuthClientsController_createOAuthClient", + "summary": "", + "description": "⚠️ First, this endpoint requires `Cookie: next-auth.session-token=eyJhbGciOiJ` header. Log into Cal web app using owner of organization that was created after visiting `/settings/organizations/new`, refresh swagger docs, and the cookie will be added to requests automatically to pass the NextAuthGuard.\nSecond, make sure that the logged in user has organizationId set to pass the OrganizationRolesGuard guard.", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateOAuthClientInput" + } + } + } + }, + "responses": { + "201": { + "description": "Create an OAuth client", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateOAuthClientResponseDto" + } + } + } + } + }, + "tags": [ + "OAuth - development only" + ] + }, + "get": { + "operationId": "OAuthClientsController_getOAuthClients", + "summary": "", + "description": "⚠️ First, this endpoint requires `Cookie: next-auth.session-token=eyJhbGciOiJ` header. Log into Cal web app using owner of organization that was created after visiting `/settings/organizations/new`, refresh swagger docs, and the cookie will be added to requests automatically to pass the NextAuthGuard.\nSecond, make sure that the logged in user has organizationId set to pass the OrganizationRolesGuard guard.", + "parameters": [], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetOAuthClientsResponseDto" + } + } + } + } + }, + "tags": [ + "OAuth - development only" + ] + } + }, + "/v2/oauth-clients/{clientId}": { + "get": { + "operationId": "OAuthClientsController_getOAuthClientById", + "summary": "", + "description": "⚠️ First, this endpoint requires `Cookie: next-auth.session-token=eyJhbGciOiJ` header. Log into Cal web app using owner of organization that was created after visiting `/settings/organizations/new`, refresh swagger docs, and the cookie will be added to requests automatically to pass the NextAuthGuard.\nSecond, make sure that the logged in user has organizationId set to pass the OrganizationRolesGuard guard.", + "parameters": [ + { + "name": "clientId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetOAuthClientResponseDto" + } + } + } + } + }, + "tags": [ + "OAuth - development only" + ] + }, + "patch": { + "operationId": "OAuthClientsController_updateOAuthClient", + "summary": "", + "description": "⚠️ First, this endpoint requires `Cookie: next-auth.session-token=eyJhbGciOiJ` header. Log into Cal web app using owner of organization that was created after visiting `/settings/organizations/new`, refresh swagger docs, and the cookie will be added to requests automatically to pass the NextAuthGuard.\nSecond, make sure that the logged in user has organizationId set to pass the OrganizationRolesGuard guard.", + "parameters": [ + { + "name": "clientId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateOAuthClientInput" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetOAuthClientResponseDto" + } + } + } + } + }, + "tags": [ + "OAuth - development only" + ] + }, + "delete": { + "operationId": "OAuthClientsController_deleteOAuthClient", + "summary": "", + "description": "⚠️ First, this endpoint requires `Cookie: next-auth.session-token=eyJhbGciOiJ` header. Log into Cal web app using owner of organization that was created after visiting `/settings/organizations/new`, refresh swagger docs, and the cookie will be added to requests automatically to pass the NextAuthGuard.\nSecond, make sure that the logged in user has organizationId set to pass the OrganizationRolesGuard guard.", + "parameters": [ + { + "name": "clientId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetOAuthClientResponseDto" + } + } + } + } + }, + "tags": [ + "OAuth - development only" + ] + } + }, + "/v2/oauth-clients/{clientId}/managed-users": { + "get": { + "operationId": "OAuthClientsController_getOAuthClientManagedUsersById", + "summary": "", + "description": "⚠️ First, this endpoint requires `Cookie: next-auth.session-token=eyJhbGciOiJ` header. Log into Cal web app using owner of organization that was created after visiting `/settings/organizations/new`, refresh swagger docs, and the cookie will be added to requests automatically to pass the NextAuthGuard.\nSecond, make sure that the logged in user has organizationId set to pass the OrganizationRolesGuard guard.", + "parameters": [ + { + "name": "clientId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + }, + { + "name": "limit", + "required": false, + "in": "query", + "description": "The number of items to return", + "example": 10, + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetManagedUsersOutput" + } + } + } + } + }, + "tags": [ + "OAuth - development only" + ] + } + }, + "/v2/oauth/{clientId}/authorize": { + "post": { + "operationId": "OAuthFlowController_authorize", + "summary": "Authorize an OAuth client", + "description": "Redirects the user to the specified 'redirect_uri' with an authorization code in query parameter if the client is authorized successfully. The code is then exchanged for access and refresh tokens via the `/exchange` endpoint.", + "parameters": [ + { + "name": "clientId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OAuthAuthorizeInput" + } + } + } + }, + "responses": { + "200": { + "description": "The user is redirected to the 'redirect_uri' with an authorization code in query parameter e.g. `redirectUri?code=secretcode.`" + }, + "400": { + "description": "Bad request if the OAuth client is not found, if the redirect URI is invalid, or if the user has already authorized the client." + } + }, + "tags": [ + "OAuth - development only" + ] + } + }, + "/v2/oauth/{clientId}/exchange": { + "post": { + "operationId": "OAuthFlowController_exchange", + "summary": "Exchange authorization code for access tokens", + "description": "Exchanges the authorization code received from the `/authorize` endpoint for access and refresh tokens. The authorization code should be provided in the 'Authorization' header prefixed with 'Bearer '.", + "parameters": [ + { + "name": "Authorization", + "required": true, + "in": "header", + "schema": { + "type": "string" + } + }, + { + "name": "clientId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ExchangeAuthorizationCodeInput" + } + } + } + }, + "responses": { + "200": { + "description": "Successfully exchanged authorization code for access and refresh tokens.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/KeysResponseDto" + } + } + } + }, + "400": { + "description": "Bad request if the authorization code is missing, invalid, or if the client ID and secret do not match." + } + }, + "tags": [ + "OAuth - development only" + ] + } + }, + "/v2/oauth/{clientId}/refresh": { + "post": { + "operationId": "OAuthFlowController_refreshAccessToken", + "parameters": [ + { + "name": "clientId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + }, + { + "name": "x-cal-secret-key", + "required": true, + "in": "header", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RefreshTokenInput" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/KeysResponseDto" + } + } + } + } + }, + "tags": [ + "OAuth - development only" + ] + } + }, + "/v2/event-types": { + "post": { + "operationId": "EventTypesController_2024_06_14_createEventType", + "parameters": [ + { + "name": "cal-api-version", + "in": "header", + "description": "Must be set to `2024-06-14`", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "Authorization", + "in": "header", + "description": "value must be `Bearer ` where `` either managed user access token or api key prefixed with cal_", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateEventTypeInput_2024_06_14" + } + } + } + }, + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateEventTypeOutput_2024_06_14" + } + } + } + } + }, + "tags": [ + "Event types" + ] + }, + "get": { + "operationId": "EventTypesController_2024_06_14_getEventTypes", + "parameters": [ + { + "name": "cal-api-version", + "in": "header", + "description": "Must be set to `2024-06-14`", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "username", + "required": true, + "in": "query", + "description": "The username of the user to get event types for. If only username provided will get all event types.", + "schema": { + "type": "string" + } + }, + { + "name": "eventSlug", + "required": true, + "in": "query", + "description": "Slug of event type to return. Notably, if eventSlug is provided then username must be provided too, because multiple users can have event with same slug.", + "schema": { + "type": "string" + } + }, + { + "name": "usernames", + "required": true, + "in": "query", + "description": "Get dynamic event type for multiple usernames separated by comma. e.g `usernames=alice,bob`", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetEventTypesOutput_2024_06_14" + } + } + } + } + }, + "tags": [ + "Event types" + ] + } + }, + "/v2/event-types/{eventTypeId}": { + "get": { + "operationId": "EventTypesController_2024_06_14_getEventTypeById", + "parameters": [ + { + "name": "cal-api-version", + "in": "header", + "description": "Must be set to `2024-06-14`", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "eventTypeId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + }, + { + "name": "Authorization", + "in": "header", + "description": "value must be `Bearer ` where `` either managed user access token or api key prefixed with cal_", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetEventTypeOutput_2024_06_14" + } + } + } + } + }, + "tags": [ + "Event types" + ] + }, + "patch": { + "operationId": "EventTypesController_2024_06_14_updateEventType", + "parameters": [ + { + "name": "cal-api-version", + "in": "header", + "description": "Must be set to `2024-06-14`", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "eventTypeId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "Authorization", + "in": "header", + "description": "value must be `Bearer ` where `` either managed user access token or api key prefixed with cal_", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateEventTypeInput_2024_06_14" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateEventTypeOutput_2024_06_14" + } + } + } + } + }, + "tags": [ + "Event types" + ] + }, + "delete": { + "operationId": "EventTypesController_2024_06_14_deleteEventType", + "parameters": [ + { + "name": "cal-api-version", + "in": "header", + "description": "Must be set to `2024-06-14`", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "eventTypeId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "Authorization", + "in": "header", + "description": "value must be `Bearer ` where `` either managed user access token or api key prefixed with cal_", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteEventTypeOutput_2024_06_14" + } + } + } + } + }, + "tags": [ + "Event types" + ] + } + }, + "/v2/selected-calendars": { + "post": { + "operationId": "SelectedCalendarsController_addSelectedCalendar", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SelectedCalendarsInputDto" + } + } + } + }, + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SelectedCalendarOutputResponseDto" + } + } + } + } + }, + "tags": [ + "Selected-Calendars" + ] + }, + "delete": { + "operationId": "SelectedCalendarsController_removeSelectedCalendar", + "parameters": [ + { + "name": "integration", + "required": true, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "externalId", + "required": true, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "credentialId", + "required": true, + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SelectedCalendarOutputResponseDto" + } + } + } + } + }, + "tags": [ + "Selected-Calendars" + ] + } + }, + "/v2/organizations/{orgId}/teams": { + "get": { + "operationId": "OrganizationsTeamsController_getAllTeams", + "summary": "Get all the teams of an organization.", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "take", + "required": false, + "in": "query", + "description": "The number of items to return", + "example": 10, + "schema": { + "type": "number" + } + }, + { + "name": "skip", + "required": false, + "in": "query", + "description": "The number of items to skip", + "example": 0, + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrgTeamsOutputResponseDto" + } + } + } + } + }, + "tags": [ + "Organizations Teams" + ] + }, + "post": { + "operationId": "OrganizationsTeamsController_createTeam", + "summary": "Create a team for an organization.", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "x-cal-client-id", + "required": true, + "in": "header", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateOrgTeamDto" + } + } + } + }, + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrgTeamOutputResponseDto" + } + } + } + } + }, + "tags": [ + "Organizations Teams" + ] + } + }, + "/v2/organizations/{orgId}/teams/me": { + "get": { + "operationId": "OrganizationsTeamsController_getMyTeams", + "summary": "Get the organization's teams user is a member of", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "take", + "required": false, + "in": "query", + "description": "The number of items to return", + "example": 10, + "schema": { + "type": "number" + } + }, + { + "name": "skip", + "required": false, + "in": "query", + "description": "The number of items to skip", + "example": 0, + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrgMeTeamsOutputResponseDto" + } + } + } + } + }, + "tags": [ + "Organizations Teams" + ] + } + }, + "/v2/organizations/{orgId}/teams/{teamId}": { + "get": { + "operationId": "OrganizationsTeamsController_getTeam", + "summary": "Get a team of the organization by ID.", + "parameters": [], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrgTeamOutputResponseDto" + } + } + } + } + }, + "tags": [ + "Organizations Teams" + ] + }, + "delete": { + "operationId": "OrganizationsTeamsController_deleteTeam", + "summary": "Delete a team of the organization by ID.", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "teamId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrgTeamOutputResponseDto" + } + } + } + } + }, + "tags": [ + "Organizations Teams" + ] + }, + "patch": { + "operationId": "OrganizationsTeamsController_updateTeam", + "summary": "Update a team of the organization by ID.", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "teamId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateOrgTeamDto" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrgTeamOutputResponseDto" + } + } + } + } + }, + "tags": [ + "Organizations Teams" + ] + } + }, + "/v2/organizations/{orgId}/schedules": { + "get": { + "operationId": "OrganizationsSchedulesController_getOrganizationSchedules", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "take", + "required": false, + "in": "query", + "description": "The number of items to return", + "example": 10, + "schema": { + "type": "number" + } + }, + { + "name": "skip", + "required": false, + "in": "query", + "description": "The number of items to skip", + "example": 0, + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetSchedulesOutput_2024_06_11" + } + } + } + } + }, + "tags": [ + "Organizations Schedules" + ] + } + }, + "/v2/organizations/{orgId}/users/{userId}/schedules": { + "post": { + "operationId": "OrganizationsSchedulesController_createUserSchedule", + "parameters": [ + { + "name": "userId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateScheduleInput_2024_06_11" + } + } + } + }, + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateScheduleOutput_2024_06_11" + } + } + } + } + }, + "tags": [ + "Organizations Schedules" + ] + }, + "get": { + "operationId": "OrganizationsSchedulesController_getUserSchedules", + "parameters": [ + { + "name": "userId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetSchedulesOutput_2024_06_11" + } + } + } + } + }, + "tags": [ + "Organizations Schedules" + ] + } + }, + "/v2/organizations/{orgId}/users/{userId}/schedules/{scheduleId}": { + "get": { + "operationId": "OrganizationsSchedulesController_getUserSchedule", + "parameters": [ + { + "name": "userId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "scheduleId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetScheduleOutput_2024_06_11" + } + } + } + } + }, + "tags": [ + "Organizations Schedules" + ] + }, + "patch": { + "operationId": "OrganizationsSchedulesController_updateUserSchedule", + "parameters": [ + { + "name": "userId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "scheduleId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateScheduleInput_2024_06_11" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateScheduleOutput_2024_06_11" + } + } + } + } + }, + "tags": [ + "Organizations Schedules" + ] + }, + "delete": { + "operationId": "OrganizationsSchedulesController_deleteUserSchedule", + "parameters": [ + { + "name": "userId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "scheduleId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteScheduleOutput_2024_06_11" + } + } + } + } + }, + "tags": [ + "Organizations Schedules" + ] + } + }, + "/v2/organizations/{orgId}/users": { + "get": { + "operationId": "OrganizationsUsersController_getOrganizationsUsers", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "take", + "required": false, + "in": "query", + "description": "The number of items to return", + "example": 10, + "schema": { + "type": "number" + } + }, + { + "name": "skip", + "required": false, + "in": "query", + "description": "The number of items to skip", + "example": 0, + "schema": { + "type": "number" + } + }, + { + "name": "emails", + "required": false, + "in": "query", + "description": "The email address or an array of email addresses to filter by", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetOrganizationUsersOutput" + } + } + } + } + }, + "tags": [ + "Organizations Users" + ] + }, + "post": { + "operationId": "OrganizationsUsersController_createOrganizationUser", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateOrganizationUserInput" + } + } + } + }, + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetOrganizationUserOutput" + } + } + } + } + }, + "tags": [ + "Organizations Users" + ] + } + }, + "/v2/organizations/{orgId}/users/{userId}": { + "patch": { + "operationId": "OrganizationsUsersController_updateOrganizationUser", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "userId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateOrganizationUserInput" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetOrganizationUserOutput" + } + } + } + } + }, + "tags": [ + "Organizations Users" + ] + }, + "delete": { + "operationId": "OrganizationsUsersController_deleteOrganizationUser", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "userId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetOrganizationUserOutput" + } + } + } + } + }, + "tags": [ + "Organizations Users" + ] + } + }, + "/v2/organizations/{orgId}/memberships": { + "get": { + "operationId": "OrganizationsMembershipsController_getAllMemberships", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "take", + "required": false, + "in": "query", + "description": "The number of items to return", + "example": 10, + "schema": { + "type": "number" + } + }, + { + "name": "skip", + "required": false, + "in": "query", + "description": "The number of items to skip", + "example": 0, + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetAllOrgMemberships" + } + } + } + } + }, + "tags": [ + "Organizations Memberships" + ] + }, + "post": { + "operationId": "OrganizationsMembershipsController_createMembership", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateOrgMembershipDto" + } + } + } + }, + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateOrgMembershipOutput" + } + } + } + } + }, + "tags": [ + "Organizations Memberships" + ] + } + }, + "/v2/organizations/{orgId}/memberships/{membershipId}": { + "get": { + "operationId": "OrganizationsMembershipsController_getOrgMembership", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "membershipId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetOrgMembership" + } + } + } + } + }, + "tags": [ + "Organizations Memberships" + ] + }, + "delete": { + "operationId": "OrganizationsMembershipsController_deleteMembership", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "membershipId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteOrgMembership" + } + } + } + } + }, + "tags": [ + "Organizations Memberships" + ] + }, + "patch": { + "operationId": "OrganizationsMembershipsController_updateMembership", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "membershipId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateOrgMembershipDto" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateOrgMembership" + } + } + } + } + }, + "tags": [ + "Organizations Memberships" + ] + } + }, + "/v2/organizations/{orgId}/teams/{teamId}/event-types": { + "post": { + "operationId": "OrganizationsEventTypesController_createTeamEventType", + "parameters": [ + { + "name": "teamId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateTeamEventTypeInput_2024_06_14" + } + } + } + }, + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateTeamEventTypeOutput" + } + } + } + } + }, + "tags": [ + "Organizations Event Types" + ] + }, + "get": { + "operationId": "OrganizationsEventTypesController_getTeamEventTypes", + "parameters": [ + { + "name": "teamId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "eventSlug", + "required": true, + "in": "query", + "description": "Slug of team event type to return.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetTeamEventTypesOutput" + } + } + } + } + }, + "tags": [ + "Organizations Event Types" + ] + } + }, + "/v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId}": { + "get": { + "operationId": "OrganizationsEventTypesController_getTeamEventType", + "parameters": [ + { + "name": "teamId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "eventTypeId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetTeamEventTypeOutput" + } + } + } + } + }, + "tags": [ + "Organizations Event Types" + ] + }, + "patch": { + "operationId": "OrganizationsEventTypesController_updateTeamEventType", + "parameters": [ + { + "name": "teamId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "eventTypeId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateTeamEventTypeInput_2024_06_14" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateTeamEventTypeOutput" + } + } + } + } + }, + "tags": [ + "Organizations Event Types" + ] + }, + "delete": { + "operationId": "OrganizationsEventTypesController_deleteTeamEventType", + "parameters": [ + { + "name": "teamId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "eventTypeId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteTeamEventTypeOutput" + } + } + } + } + }, + "tags": [ + "Organizations Event Types" + ] + } + }, + "/v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId}/create-phone-call": { + "post": { + "operationId": "OrganizationsEventTypesController_createPhoneCall", + "parameters": [ + { + "name": "eventTypeId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreatePhoneCallInput" + } + } + } + }, + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreatePhoneCallOutput" + } + } + } + } + }, + "tags": [ + "Organizations Event Types" + ] + } + }, + "/v2/organizations/{orgId}/teams/event-types": { + "get": { + "operationId": "OrganizationsEventTypesController_getTeamsEventTypes", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "take", + "required": false, + "in": "query", + "description": "The number of items to return", + "example": 10, + "schema": { + "type": "number" + } + }, + { + "name": "skip", + "required": false, + "in": "query", + "description": "The number of items to skip", + "example": 0, + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetTeamEventTypesOutput" + } + } + } + } + }, + "tags": [ + "Organizations Event Types" + ] + } + }, + "/v2/organizations/{orgId}/teams/{teamId}/memberships": { + "get": { + "operationId": "OrganizationsTeamsMembershipsController_getAllOrgTeamMemberships", + "summary": "Get all the memberships of a team of an organization.", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "teamId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "take", + "required": false, + "in": "query", + "description": "The number of items to return", + "example": 10, + "schema": { + "type": "number" + } + }, + { + "name": "skip", + "required": false, + "in": "query", + "description": "The number of items to skip", + "example": 0, + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrgTeamMembershipsOutputResponseDto" + } + } + } + } + }, + "tags": [ + "Organizations Teams" + ] + }, + "post": { + "operationId": "OrganizationsTeamsMembershipsController_createOrgTeamMembership", + "summary": "Create a membership of an organization's team", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "teamId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateOrgTeamMembershipDto" + } + } + } + }, + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrgTeamMembershipOutputResponseDto" + } + } + } + } + }, + "tags": [ + "Organizations Teams" + ] + } + }, + "/v2/organizations/{orgId}/teams/{teamId}/memberships/{membershipId}": { + "get": { + "operationId": "OrganizationsTeamsMembershipsController_getOrgTeamMembership", + "summary": "Get the membership of an organization's team by ID", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "teamId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "membershipId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrgTeamMembershipOutputResponseDto" + } + } + } + } + }, + "tags": [ + "Organizations Teams" + ] + }, + "delete": { + "operationId": "OrganizationsTeamsMembershipsController_deleteOrgTeamMembership", + "summary": "Delete the membership of an organization's team by ID", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "teamId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "membershipId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrgTeamMembershipOutputResponseDto" + } + } + } + } + }, + "tags": [ + "Organizations Teams" + ] + }, + "patch": { + "operationId": "OrganizationsTeamsMembershipsController_updateOrgTeamMembership", + "summary": "Update the membership of an organization's team by ID", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "teamId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "membershipId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateOrgTeamMembershipDto" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OrgTeamMembershipOutputResponseDto" + } + } + } + } + }, + "tags": [ + "Organizations Teams" + ] + } + }, + "/v2/organizations/{orgId}/attributes": { + "get": { + "operationId": "OrganizationsAttributesController_getOrganizationAttributes", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "take", + "required": false, + "in": "query", + "description": "The number of items to return", + "example": 10, + "schema": { + "type": "number" + } + }, + { + "name": "skip", + "required": false, + "in": "query", + "description": "The number of items to skip", + "example": 0, + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetOrganizationAttributesOutput" + } + } + } + } + } + }, + "post": { + "operationId": "OrganizationsAttributesController_createOrganizationAttribute", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateOrganizationAttributeInput" + } + } + } + }, + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateOrganizationAttributesOutput" + } + } + } + } + } + } + }, + "/v2/organizations/{orgId}/attributes/{attributeId}": { + "get": { + "operationId": "OrganizationsAttributesController_getOrganizationAttribute", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "attributeId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetSingleAttributeOutput" + } + } + } + } + } + }, + "patch": { + "operationId": "OrganizationsAttributesController_updateOrganizationAttribute", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "attributeId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateOrganizationAttributeInput" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateOrganizationAttributesOutput" + } + } + } + } + } + }, + "delete": { + "operationId": "OrganizationsAttributesController_deleteOrganizationAttribute", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "attributeId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteOrganizationAttributesOutput" + } + } + } + } + } + } + }, + "/v2/organizations/{orgId}/attributes/{attributeId}/options": { + "post": { + "operationId": "OrganizationsOptionsAttributesController_createOrganizationAttributeOption", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "attributeId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateOrganizationAttributeOptionInput" + } + } + } + }, + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateAttributeOptionOutput" + } + } + } + } + } + }, + "get": { + "operationId": "OrganizationsOptionsAttributesController_getOrganizationAttributeOptions", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "attributeId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetAllAttributeOptionOutput" + } + } + } + } + } + } + }, + "/v2/organizations/{orgId}/attributes/{attributeId}/options/{optionId}": { + "delete": { + "operationId": "OrganizationsOptionsAttributesController_deleteOrganizationAttributeOption", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "attributeId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + }, + { + "name": "optionId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteAttributeOptionOutput" + } + } + } + } + } + }, + "patch": { + "operationId": "OrganizationsOptionsAttributesController_updateOrganizationAttributeOption", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "attributeId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + }, + { + "name": "optionId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateOrganizationAttributeOptionInput" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateAttributeOptionOutput" + } + } + } + } + } + } + }, + "/v2/organizations/{orgId}/attributes/options/{userId}": { + "post": { + "operationId": "OrganizationsOptionsAttributesController_assignOrganizationAttributeOptionToUser", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "userId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AssignOrganizationAttributeOptionToUserInput" + } + } + } + }, + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AssignOptionUserOutput" + } + } + } + } + } + }, + "get": { + "operationId": "OrganizationsOptionsAttributesController_getOrganizationAttributeOptionsForUser", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "userId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetOptionUserOutput" + } + } + } + } + } + } + }, + "/v2/organizations/{orgId}/attributes/options/{userId}/{attributeOptionId}": { + "delete": { + "operationId": "OrganizationsOptionsAttributesController_unassignOrganizationAttributeOptionFromUser", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "userId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "attributeOptionId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnassignOptionUserOutput" + } + } + } + } + } + } + }, + "/v2/organizations/{orgId}/webhooks": { + "get": { + "operationId": "OrganizationsWebhooksController_getAllOrganizationWebhooks", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "take", + "required": false, + "in": "query", + "description": "The number of items to return", + "example": 10, + "schema": { + "type": "number" + } + }, + { + "name": "skip", + "required": false, + "in": "query", + "description": "The number of items to skip", + "example": 0, + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TeamWebhooksOutputResponseDto" + } + } + } + } + }, + "tags": [ + "Organizations Webhooks" + ] + }, + "post": { + "operationId": "OrganizationsWebhooksController_createOrganizationWebhook", + "parameters": [ + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateWebhookInputDto" + } + } + } + }, + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TeamWebhookOutputResponseDto" + } + } + } + } + }, + "tags": [ + "Organizations Webhooks" + ] + } + }, + "/v2/organizations/{orgId}/webhooks/{webhookId}": { + "get": { + "operationId": "OrganizationsWebhooksController_getOrganizationWebhook", + "parameters": [ + { + "name": "webhookId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TeamWebhookOutputResponseDto" + } + } + } + } + }, + "tags": [ + "Organizations Webhooks" + ] + }, + "delete": { + "operationId": "OrganizationsWebhooksController_deleteWebhook", + "parameters": [ + { + "name": "webhookId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TeamWebhookOutputResponseDto" + } + } + } + } + }, + "tags": [ + "Organizations Webhooks" + ] + }, + "patch": { + "operationId": "OrganizationsWebhooksController_updateOrgWebhook", + "parameters": [ + { + "name": "webhookId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateWebhookInputDto" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TeamWebhookOutputResponseDto" + } + } + } + } + }, + "tags": [ + "Organizations Webhooks" + ] + } + }, + "/v2/schedules": { + "post": { + "operationId": "SchedulesController_2024_06_11_createSchedule", + "summary": "Create a schedule", + "description": "\n The point of creating schedules is for event types to be available at specific times.\n\n First goal of schedules is to have a default schedule. If you are platform customer and created managed users, then it is important to note that each managed user should have a default schedule.\n 1. If you passed `timeZone` when creating managed user, then the default schedule from Monday to Friday from 9AM to 5PM will be created with that timezone. Managed user can then change the default schedule via `AvailabilitySettings` atom.\n 2. If you did not, then we assume you want that user has specific schedule right away. You should create default schedule by specifying\n `\"isDefault\": true` in the request body. Until the user has a default schedule that user can't be booked or manage his / her schedule via the AvailabilitySettings atom.\n\n Second goal is to create other schedules that event types can point to, so that when that event is booked availability is not checked against the default schedule but against that specific schedule.\n After creating a non default schedule you can update event type to point to that schedule via the PATCH `event-types/{eventTypeId}` endpoint.\n\n When specifying start time and end time for each day use 24 hour format e.g. 08:00, 15:00 etc.\n ", + "parameters": [ + { + "name": "Authorization", + "in": "header", + "description": "value must be `Bearer ` where `` either managed user access token or api key prefixed with cal_", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "cal-api-version", + "in": "header", + "description": "Must be set to `2024-06-11`", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateScheduleInput_2024_06_11" + } + } + } + }, + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateScheduleOutput_2024_06_11" + } + } + } + } + }, + "tags": [ + "Schedules" + ] + }, + "get": { + "operationId": "SchedulesController_2024_06_11_getSchedules", + "summary": "", + "description": "Returns all schedules of the authenticated user", + "parameters": [ + { + "name": "Authorization", + "in": "header", + "description": "value must be `Bearer ` where `` either managed user access token or api key prefixed with cal_", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "cal-api-version", + "in": "header", + "description": "Must be set to `2024-06-11`", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetSchedulesOutput_2024_06_11" + } + } + } + } + }, + "tags": [ + "Schedules" + ] + } + }, + "/v2/schedules/default": { + "get": { + "operationId": "SchedulesController_2024_06_11_getDefaultSchedule", + "parameters": [ + { + "name": "Authorization", + "in": "header", + "description": "value must be `Bearer ` where `` either managed user access token or api key prefixed with cal_", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "cal-api-version", + "in": "header", + "description": "Must be set to `2024-06-11`", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Returns the default schedule of the authenticated user", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetDefaultScheduleOutput_2024_06_11" + } + } + } + } + }, + "tags": [ + "Schedules" + ] + } + }, + "/v2/schedules/{scheduleId}": { + "get": { + "operationId": "SchedulesController_2024_06_11_getSchedule", + "parameters": [ + { + "name": "Authorization", + "in": "header", + "description": "value must be `Bearer ` where `` either managed user access token or api key prefixed with cal_", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "cal-api-version", + "in": "header", + "description": "Must be set to `2024-06-11`", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "scheduleId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetScheduleOutput_2024_06_11" + } + } + } + } + }, + "tags": [ + "Schedules" + ] + }, + "patch": { + "operationId": "SchedulesController_2024_06_11_updateSchedule", + "parameters": [ + { + "name": "Authorization", + "in": "header", + "description": "value must be `Bearer ` where `` either managed user access token or api key prefixed with cal_", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "cal-api-version", + "in": "header", + "description": "Must be set to `2024-06-11`", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "scheduleId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateScheduleInput_2024_06_11" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateScheduleOutput_2024_06_11" + } + } + } + } + }, + "tags": [ + "Schedules" + ] + }, + "delete": { + "operationId": "SchedulesController_2024_06_11_deleteSchedule", + "parameters": [ + { + "name": "Authorization", + "in": "header", + "description": "value must be `Bearer ` where `` either managed user access token or api key prefixed with cal_", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "cal-api-version", + "in": "header", + "description": "Must be set to `2024-06-11`", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "scheduleId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteScheduleOutput_2024_06_11" + } + } + } + } + }, + "tags": [ + "Schedules" + ] + } + }, + "/v2/gcal/oauth/auth-url": { + "get": { + "operationId": "GcalController_redirect", + "parameters": [ + { + "name": "Authorization", + "required": true, + "in": "header", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GcalAuthUrlOutput" + } + } + } + } + }, + "tags": [ + "Google Calendar" + ] + } + }, + "/v2/gcal/oauth/save": { + "get": { + "operationId": "GcalController_save", + "parameters": [ + { + "name": "state", + "required": true, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "code", + "required": true, + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GcalSaveRedirectOutput" + } + } + } + } + }, + "tags": [ + "Google Calendar" + ] + } + }, + "/v2/gcal/check": { + "get": { + "operationId": "GcalController_check", + "parameters": [], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GcalCheckOutput" + } + } + } + } + }, + "tags": [ + "Google Calendar" + ] + } + }, + "/v2/provider/{clientId}": { + "get": { + "operationId": "CalProviderController_verifyClientId", + "parameters": [ + { + "name": "clientId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProviderVerifyClientOutput" + } + } + } + } + }, + "tags": [ + "Cal provider" + ] + } + }, + "/v2/provider/{clientId}/access-token": { + "get": { + "operationId": "CalProviderController_verifyAccessToken", + "parameters": [ + { + "name": "clientId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProviderVerifyAccessTokenOutput" + } + } + } + } + }, + "tags": [ + "Cal provider" + ] + } + }, + "/v2/me": { + "get": { + "operationId": "MeController_getMe", + "parameters": [], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetMeOutput" + } + } + } + } + }, + "tags": [ + "Me" + ] + }, + "patch": { + "operationId": "MeController_updateMe", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateManagedUserInput" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateMeOutput" + } + } + } + } + }, + "tags": [ + "Me" + ] + } + }, + "/v2/calendars/busy-times": { + "get": { + "operationId": "CalendarsController_getBusyTimes", + "parameters": [ + { + "name": "loggedInUsersTz", + "required": true, + "in": "query", + "description": "The timezone of the logged in user represented as a string", + "example": "America/New_York", + "schema": { + "type": "string" + } + }, + { + "name": "calendarsToLoad", + "required": true, + "in": "query", + "description": "An array of Calendar objects representing the calendars to be loaded", + "example": "[{ credentialId: \"1\", externalId: \"AQgtJE7RnHEeyisVq2ENs2gAAAgEGAAAACgtJE7RnHEeyisVq2ENs2gAAAhSDAAAA\" }, { credentialId: \"2\", externalId: \"AQM7RnHEeyisVq2ENs2gAAAhFDBBBBB\" }]", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetBusyTimesOutput" + } + } + } + } + }, + "tags": [ + "Calendars" + ] + } + }, + "/v2/calendars": { + "get": { + "operationId": "CalendarsController_getCalendars", + "parameters": [], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConnectedCalendarsOutput" + } + } + } + } + }, + "tags": [ + "Calendars" + ] + } + }, + "/v2/calendars/{calendar}/connect": { + "get": { + "operationId": "CalendarsController_redirect", + "parameters": [ + { + "name": "Authorization", + "required": true, + "in": "header", + "schema": { + "type": "string" + } + }, + { + "name": "calendar", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + } + }, + "tags": [ + "Calendars" + ] + } + }, + "/v2/calendars/{calendar}/save": { + "get": { + "operationId": "CalendarsController_save", + "parameters": [ + { + "name": "state", + "required": true, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "code", + "required": true, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "calendar", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "" + } + }, + "tags": [ + "Calendars" + ] + } + }, + "/v2/calendars/{calendar}/credentials": { + "post": { + "operationId": "CalendarsController_syncCredentials", + "parameters": [ + { + "name": "calendar", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "201": { + "description": "" + } + }, + "tags": [ + "Calendars" + ] + } + }, + "/v2/calendars/{calendar}/check": { + "get": { + "operationId": "CalendarsController_check", + "parameters": [ + { + "name": "calendar", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + } + }, + "tags": [ + "Calendars" + ] + } + }, + "/v2/calendars/{calendar}/disconnect": { + "post": { + "operationId": "CalendarsController_deleteCalendarCredentials", + "parameters": [ + { + "name": "calendar", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteCalendarCredentialsInputBodyDto" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeletedCalendarCredentialsOutputResponseDto" + } + } + } + } + }, + "tags": [ + "Calendars" + ] + } + }, + "/v2/bookings": { + "post": { + "operationId": "BookingsController_2024_08_13_createBooking", + "summary": "Create booking", + "description": "\n POST /v2/bookings is used to create regular bookings, recurring bookings and instant bookings. The request bodies for all 3 are almost the same except:\n If eventTypeId in the request body is id of a regular event, then regular booking is created.\n\n If it is an id of a recurring event type, then recurring booking is created.\n\n Meaning that the request bodies are equal but the outcome depends on what kind of event type it is with the goal of making it as seamless for developers as possible.\n\n For team event types it is possible to create instant meeting. To do that just pass `\"instant\": true` to the request body.\n \n The start needs to be in UTC aka if the timezone is GMT+2 in Rome and meeting should start at 11, then UTC time should have hours 09:00 aka without time zone.\n ", + "parameters": [ + { + "name": "cal-api-version", + "in": "header", + "description": "Must be set to `2024-08-13`", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "description": "Accepts different types of booking input: CreateBookingInput_2024_08_13, CreateInstantBookingInput_2024_08_13, or CreateRecurringBookingInput_2024_08_13", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/CreateBookingInput_2024_08_13" + }, + { + "$ref": "#/components/schemas/CreateInstantBookingInput_2024_08_13" + }, + { + "$ref": "#/components/schemas/CreateRecurringBookingInput_2024_08_13" + } + ] + } + } + } + }, + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateBookingOutput_2024_08_13" + } + } + } + } + }, + "tags": [ + "Bookings" + ] + }, + "get": { + "operationId": "BookingsController_2024_08_13_getBookings", + "parameters": [ + { + "name": "cal-api-version", + "in": "header", + "description": "Must be set to `2024-08-13`", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "status", + "required": false, + "in": "query", + "description": "Filter bookings by status. If you want to filter by multiple statuses, separate them with a comma.", + "example": "?status=upcoming,past", + "schema": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "upcoming", + "recurring", + "past", + "cancelled", + "unconfirmed" + ] + } + } + }, + { + "name": "attendeeEmail", + "required": false, + "in": "query", + "description": "Filter bookings by the attendee's email address.", + "example": "example@domain.com", + "schema": { + "type": "string" + } + }, + { + "name": "attendeeName", + "required": false, + "in": "query", + "description": "Filter bookings by the attendee's name.", + "example": "John Doe", + "schema": { + "type": "string" + } + }, + { + "name": "eventTypeIds", + "required": false, + "in": "query", + "description": "Filter bookings by event type ids belonging to the user. Event type ids must be separated by a comma.", + "example": "?eventTypeIds=100,200", + "schema": { + "type": "string" + } + }, + { + "name": "eventTypeId", + "required": false, + "in": "query", + "description": "Filter bookings by event type id belonging to the user.", + "example": "?eventTypeId=100", + "schema": { + "type": "string" + } + }, + { + "name": "teamsIds", + "required": false, + "in": "query", + "description": "Filter bookings by team ids that user is part of. Team ids must be separated by a comma.", + "example": "?teamIds=50,60", + "schema": { + "type": "string" + } + }, + { + "name": "teamId", + "required": false, + "in": "query", + "description": "Filter bookings by team id that user is part of", + "example": "?teamId=50", + "schema": { + "type": "string" + } + }, + { + "name": "afterStart", + "required": false, + "in": "query", + "description": "Filter bookings with start after this date string.", + "example": "?afterStart=2025-03-07T10:00:00.000Z", + "schema": { + "type": "string" + } + }, + { + "name": "beforeEnd", + "required": false, + "in": "query", + "description": "Filter bookings with end before this date string.", + "example": "?beforeEnd=2025-03-07T11:00:00.000Z", + "schema": { + "type": "string" + } + }, + { + "name": "sortStart", + "required": false, + "in": "query", + "description": "Sort results by their start time in ascending or descending order.", + "example": "?sortStart=asc OR ?sortStart=desc", + "schema": { + "enum": [ + "asc", + "desc" + ], + "type": "string" + } + }, + { + "name": "sortEnd", + "required": false, + "in": "query", + "description": "Sort results by their end time in ascending or descending order.", + "example": "?sortEnd=asc OR ?sortEnd=desc", + "schema": { + "enum": [ + "asc", + "desc" + ], + "type": "string" + } + }, + { + "name": "sortCreated", + "required": false, + "in": "query", + "description": "Sort results by their creation time (when booking was made) in ascending or descending order.", + "example": "?sortEnd=asc OR ?sortEnd=desc", + "schema": { + "enum": [ + "asc", + "desc" + ], + "type": "string" + } + }, + { + "name": "take", + "required": false, + "in": "query", + "description": "The number of items to return", + "example": 10, + "schema": { + "type": "number" + } + }, + { + "name": "skip", + "required": false, + "in": "query", + "description": "The number of items to skip", + "example": 0, + "schema": { + "type": "number" + } + }, + { + "name": "Authorization", + "in": "header", + "description": "value must be `Bearer ` where `` either managed user access token or api key prefixed with cal_", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetBookingsOutput_2024_08_13" + } + } + } + } + }, + "tags": [ + "Bookings" + ] + } + }, + "/v2/bookings/{bookingUid}": { + "get": { + "operationId": "BookingsController_2024_08_13_getBooking", + "summary": "Get booking", + "description": "`:bookingUid` can be\n \n 1. uid of a normal booking\n \n 2. uid of one of the recurring booking recurrences\n \n 3. uid of recurring booking which will return an array of all recurring booking recurrences (stored as recurringBookingUid on one of the individual recurrences).", + "parameters": [ + { + "name": "cal-api-version", + "in": "header", + "description": "Must be set to `2024-08-13`", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "bookingUid", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetBookingOutput_2024_08_13" + } + } + } + } + }, + "tags": [ + "Bookings" + ] + } + }, + "/v2/bookings/{bookingUid}/reschedule": { + "post": { + "operationId": "BookingsController_2024_08_13_rescheduleBooking", + "summary": "Reschedule booking", + "description": "Reschedule a booking by passing `:bookingUid` of the booking that should be rescheduled and pass request body with a new start time to create a new booking.", + "parameters": [ + { + "name": "cal-api-version", + "in": "header", + "description": "Must be set to `2024-08-13`", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "bookingUid", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RescheduleBookingInput_2024_08_13" + } + } + } + }, + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RescheduleBookingOutput_2024_08_13" + } + } + } + } + }, + "tags": [ + "Bookings" + ] + } + }, + "/v2/bookings/{bookingUid}/cancel": { + "post": { + "operationId": "BookingsController_2024_08_13_cancelBooking", + "parameters": [ + { + "name": "cal-api-version", + "in": "header", + "description": "Must be set to `2024-08-13`", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "bookingUid", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CancelBookingInput_2024_08_13" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CancelBookingOutput_2024_08_13" + } + } + } + } + }, + "tags": [ + "Bookings" + ] + } + }, + "/v2/bookings/{bookingUid}/mark-absent": { + "post": { + "operationId": "BookingsController_2024_08_13_markNoShow", + "parameters": [ + { + "name": "cal-api-version", + "in": "header", + "description": "Must be set to `2024-08-13`", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "bookingUid", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + }, + { + "name": "Authorization", + "in": "header", + "description": "value must be `Bearer ` where `` either managed user access token or api key prefixed with cal_", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MarkAbsentBookingInput_2024_08_13" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MarkAbsentBookingOutput_2024_08_13" + } + } + } + } + }, + "tags": [ + "Bookings" + ] + } + }, + "/v2/slots/reserve": { + "post": { + "operationId": "SlotsController_reserveSlot", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ReserveSlotInput" + } + } + } + }, + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + } + }, + "tags": [ + "Slots" + ] + } + }, + "/v2/slots/selected-slot": { + "delete": { + "operationId": "SlotsController_deleteSelectedSlot", + "parameters": [], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + } + }, + "tags": [ + "Slots" + ] + } + }, + "/v2/slots/available": { + "get": { + "operationId": "SlotsController_getAvailableSlots", + "parameters": [], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + } + }, + "tags": [ + "Slots" + ] + } + }, + "/v2/timezones": { + "get": { + "operationId": "TimezonesController_getTimeZones", + "parameters": [], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + } + }, + "tags": [ + "Timezones" + ] + } + }, + "/v2/webhooks": { + "post": { + "operationId": "WebhooksController_createWebhook", + "summary": "Create a webhook", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateWebhookInputDto" + } + } + } + }, + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserWebhookOutputResponseDto" + } + } + } + } + }, + "tags": [ + "Users' Webhooks" + ] + }, + "get": { + "operationId": "WebhooksController_getWebhooks", + "summary": "Get all user webhooks paginated", + "parameters": [ + { + "name": "take", + "required": false, + "in": "query", + "description": "The number of items to return", + "example": 10, + "schema": { + "type": "number" + } + }, + { + "name": "skip", + "required": false, + "in": "query", + "description": "The number of items to skip", + "example": 0, + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserWebhooksOutputResponseDto" + } + } + } + } + }, + "tags": [ + "Users' Webhooks" + ] + } + }, + "/v2/webhooks/{webhookId}": { + "patch": { + "operationId": "WebhooksController_updateWebhook", + "summary": "Update a webhook", + "parameters": [ + { + "name": "webhookId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateWebhookInputDto" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserWebhookOutputResponseDto" + } + } + } + } + }, + "tags": [ + "Users' Webhooks" + ] + }, + "get": { + "operationId": "WebhooksController_getWebhook", + "summary": "Get a webhook", + "parameters": [], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserWebhookOutputResponseDto" + } + } + } + } + }, + "tags": [ + "Users' Webhooks" + ] + }, + "delete": { + "operationId": "WebhooksController_deleteWebhook", + "summary": "Delete a webhook", + "parameters": [ + { + "name": "webhookId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserWebhookOutputResponseDto" + } + } + } + } + }, + "tags": [ + "Users' Webhooks" + ] + } + }, + "/v2/event-types/{eventTypeId}/webhooks": { + "post": { + "operationId": "EventTypeWebhooksController_createEventTypeWebhook", + "summary": "Create a webhook for an event-type", + "parameters": [ + { + "name": "eventTypeId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateWebhookInputDto" + } + } + } + }, + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EventTypeWebhookOutputResponseDto" + } + } + } + } + }, + "tags": [ + "Users' EventTypes Webhooks" + ] + }, + "get": { + "operationId": "EventTypeWebhooksController_getEventTypeWebhooks", + "summary": "Get all webhooks of an event-type", + "parameters": [ + { + "name": "eventTypeId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "take", + "required": false, + "in": "query", + "description": "The number of items to return", + "example": 10, + "schema": { + "type": "number" + } + }, + { + "name": "skip", + "required": false, + "in": "query", + "description": "The number of items to skip", + "example": 0, + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EventTypeWebhooksOutputResponseDto" + } + } + } + } + }, + "tags": [ + "Users' EventTypes Webhooks" + ] + }, + "delete": { + "operationId": "EventTypeWebhooksController_deleteAllEventTypeWebhooks", + "summary": "Delete all webhooks of an event-type", + "parameters": [ + { + "name": "eventTypeId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteManyWebhooksOutputResponseDto" + } + } + } + } + }, + "tags": [ + "Users' EventTypes Webhooks" + ] + } + }, + "/v2/event-types/{eventTypeId}/webhooks/{webhookId}": { + "patch": { + "operationId": "EventTypeWebhooksController_updateEventTypeWebhook", + "summary": "Update a webhook of an event-type", + "parameters": [ + { + "name": "webhookId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateWebhookInputDto" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EventTypeWebhookOutputResponseDto" + } + } + } + } + }, + "tags": [ + "Users' EventTypes Webhooks" + ] + }, + "get": { + "operationId": "EventTypeWebhooksController_getEventTypeWebhook", + "summary": "Get a webhook of an event-type", + "parameters": [], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EventTypeWebhookOutputResponseDto" + } + } + } + } + }, + "tags": [ + "Users' EventTypes Webhooks" + ] + }, + "delete": { + "operationId": "EventTypeWebhooksController_deleteEventTypeWebhook", + "summary": "Delete a webhook of an event-type", + "parameters": [], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EventTypeWebhookOutputResponseDto" + } + } + } + } + }, + "tags": [ + "Users' EventTypes Webhooks" + ] + } + }, + "/v2/oauth-clients/{clientId}/webhooks": { + "post": { + "operationId": "OAuthClientWebhooksController_createOAuthClientWebhook", + "summary": "Create a webhook for an oAuthClient", + "parameters": [ + { + "name": "clientId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateWebhookInputDto" + } + } + } + }, + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OAuthClientWebhookOutputResponseDto" + } + } + } + } + }, + "tags": [ + "OAuthClients Webhooks" + ] + }, + "get": { + "operationId": "OAuthClientWebhooksController_getOAuthClientWebhooks", + "summary": "Get all webhooks of an oAuthClient", + "parameters": [ + { + "name": "clientId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + }, + { + "name": "take", + "required": false, + "in": "query", + "description": "The number of items to return", + "example": 10, + "schema": { + "type": "number" + } + }, + { + "name": "skip", + "required": false, + "in": "query", + "description": "The number of items to skip", + "example": 0, + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OAuthClientWebhooksOutputResponseDto" + } + } + } + } + }, + "tags": [ + "OAuthClients Webhooks" + ] + }, + "delete": { + "operationId": "OAuthClientWebhooksController_deleteAllOAuthClientWebhooks", + "summary": "Delete all webhooks of an oAuthClient", + "parameters": [ + { + "name": "clientId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteManyWebhooksOutputResponseDto" + } + } + } + } + }, + "tags": [ + "OAuthClients Webhooks" + ] + } + }, + "/v2/oauth-clients/{clientId}/webhooks/{webhookId}": { + "patch": { + "operationId": "OAuthClientWebhooksController_updateOAuthClientWebhook", + "summary": "Update a webhook of an oAuthClient", + "parameters": [ + { + "name": "webhookId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateWebhookInputDto" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OAuthClientWebhookOutputResponseDto" + } + } + } + } + }, + "tags": [ + "OAuthClients Webhooks" + ] + }, + "get": { + "operationId": "OAuthClientWebhooksController_getOAuthClientWebhook", + "summary": "Get a webhook of an oAuthClient", + "parameters": [], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OAuthClientWebhookOutputResponseDto" + } + } + } + } + }, + "tags": [ + "OAuthClients Webhooks" + ] + }, + "delete": { + "operationId": "OAuthClientWebhooksController_deleteOAuthClientWebhook", + "summary": "Delete a webhook of an oAuthClient", + "parameters": [], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OAuthClientWebhookOutputResponseDto" + } + } + } + } + }, + "tags": [ + "OAuthClients Webhooks" + ] + } + }, + "/v2/destination-calendars": { + "put": { + "operationId": "DestinationCalendarsController_updateDestinationCalendars", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DestinationCalendarsInputBodyDto" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DestinationCalendarsOutputResponseDto" + } + } + } + } + }, + "tags": [ + "Destination-Calendars", + "Select a third party destination calendar where events will be created" + ] + } + } + }, + "info": { + "title": "Cal.com v2 API", + "description": "", + "version": "1.0.0", + "contact": {} + }, + "tags": [], + "servers": [], + "components": { + "schemas": { + "ManagedUserOutput": { + "type": "object", + "properties": { + "id": { + "type": "number", + "example": 1 + }, + "email": { + "type": "string", + "example": "alice+cluo37fwd0001khkzqqynkpj3@example.com" + }, + "username": { + "type": "string", + "nullable": true, + "example": "alice" + }, + "timeZone": { + "type": "string", + "example": "America/New_York" + }, + "weekStart": { + "type": "string", + "example": "Sunday" + }, + "createdDate": { + "type": "string", + "example": "2024-04-01T00:00:00.000Z" + }, + "timeFormat": { + "type": "number", + "nullable": true, + "example": 12 + }, + "defaultScheduleId": { + "type": "number", + "nullable": true, + "example": null + }, + "locale": { + "enum": [ + "ar", + "ca", + "de", + "es", + "eu", + "he", + "id", + "ja", + "lv", + "pl", + "ro", + "sr", + "th", + "vi", + "az", + "cs", + "el", + "es-419", + "fi", + "hr", + "it", + "km", + "nl", + "pt", + "ru", + "sv", + "tr", + "zh-CN", + "bg", + "da", + "en", + "et", + "fr", + "hu", + "iw", + "ko", + "no", + "pt-BR", + "sk", + "ta", + "uk", + "zh-TW" + ], + "type": "string", + "example": "en" + } + }, + "required": [ + "id", + "email", + "username", + "timeZone", + "weekStart", + "createdDate", + "timeFormat", + "defaultScheduleId" + ] + }, + "GetManagedUsersOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ManagedUserOutput" + } + } + }, + "required": [ + "status", + "data" + ] + }, + "CreateManagedUserInput": { + "type": "object", + "properties": { + "email": { + "type": "string", + "example": "alice@example.com" + }, + "timeFormat": { + "type": "number", + "example": 12, + "enum": [ + 12, + 24 + ], + "description": "Must be 12 or 24" + }, + "weekStart": { + "type": "string", + "example": "Monday", + "enum": [ + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday", + "Sunday" + ] + }, + "timeZone": { + "type": "string", + "example": "America/New_York", + "description": "Timezone is used to create user's default schedule from Monday to Friday from 9AM to 5PM. If it is not passed then user does not have\n a default schedule and it must be created manually via the /schedules endpoint. Until the schedule is created, the user can't access availability atom to set his / her availability nor booked." + }, + "locale": { + "enum": [ + "ar", + "ca", + "de", + "es", + "eu", + "he", + "id", + "ja", + "lv", + "pl", + "ro", + "sr", + "th", + "vi", + "az", + "cs", + "el", + "es-419", + "fi", + "hr", + "it", + "km", + "nl", + "pt", + "ru", + "sv", + "tr", + "zh-CN", + "bg", + "da", + "en", + "et", + "fr", + "hu", + "iw", + "ko", + "no", + "pt-BR", + "sk", + "ta", + "uk", + "zh-TW" + ], + "type": "string", + "example": "en" + }, + "name": { + "type": "string" + } + }, + "required": [ + "email" + ] + }, + "CreateManagedUserData": { + "type": "object", + "properties": { + "user": { + "$ref": "#/components/schemas/ManagedUserOutput" + }, + "accessToken": { + "type": "string" + }, + "refreshToken": { + "type": "string" + }, + "accessTokenExpiresAt": { + "type": "number" + } + }, + "required": [ + "user", + "accessToken", + "refreshToken", + "accessTokenExpiresAt" + ] + }, + "CreateManagedUserOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/CreateManagedUserData" + } + }, + "required": [ + "status", + "data" + ] + }, + "GetManagedUserOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/ManagedUserOutput" + } + }, + "required": [ + "status", + "data" + ] + }, + "UpdateManagedUserInput": { + "type": "object", + "properties": { + "timeFormat": { + "type": "number", + "enum": [ + 12, + 24 + ], + "example": 12, + "description": "Must be 12 or 24" + }, + "weekStart": { + "type": "string", + "enum": [ + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday", + "Sunday" + ], + "example": "Monday" + }, + "locale": { + "enum": [ + "ar", + "ca", + "de", + "es", + "eu", + "he", + "id", + "ja", + "lv", + "pl", + "ro", + "sr", + "th", + "vi", + "az", + "cs", + "el", + "es-419", + "fi", + "hr", + "it", + "km", + "nl", + "pt", + "ru", + "sv", + "tr", + "zh-CN", + "bg", + "da", + "en", + "et", + "fr", + "hu", + "iw", + "ko", + "no", + "pt-BR", + "sk", + "ta", + "uk", + "zh-TW" + ], + "type": "string", + "example": "en" + }, + "email": { + "type": "string" + }, + "name": { + "type": "string" + }, + "defaultScheduleId": { + "type": "number" + }, + "timeZone": { + "type": "string" + } + } + }, + "KeysDto": { + "type": "object", + "properties": { + "accessToken": { + "type": "string", + "example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" + }, + "refreshToken": { + "type": "string", + "example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" + }, + "accessTokenExpiresAt": { + "type": "number" + } + }, + "required": [ + "accessToken", + "refreshToken", + "accessTokenExpiresAt" + ] + }, + "KeysResponseDto": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/KeysDto" + } + }, + "required": [ + "status", + "data" + ] + }, + "CreateOAuthClientInput": { + "type": "object", + "properties": {} + }, + "DataDto": { + "type": "object", + "properties": { + "clientId": { + "type": "string", + "example": "clsx38nbl0001vkhlwin9fmt0" + }, + "clientSecret": { + "type": "string", + "example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoib2F1dGgtY2xpZW50Iiwi" + } + }, + "required": [ + "clientId", + "clientSecret" + ] + }, + "CreateOAuthClientResponseDto": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "success", + "error" + ], + "example": "success" + }, + "data": { + "example": { + "clientId": "clsx38nbl0001vkhlwin9fmt0", + "clientSecret": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoib2F1dGgtY2xpZW50Iiwi" + }, + "allOf": [ + { + "$ref": "#/components/schemas/DataDto" + } + ] + } + }, + "required": [ + "status", + "data" + ] + }, + "PlatformOAuthClientDto": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "clsx38nbl0001vkhlwin9fmt0" + }, + "name": { + "type": "string", + "example": "MyClient" + }, + "secret": { + "type": "string", + "example": "secretValue" + }, + "permissions": { + "type": "number", + "example": 3 + }, + "logo": { + "type": "string", + "nullable": true, + "example": "https://example.com/logo.png" + }, + "redirectUris": { + "example": [ + "https://example.com/callback" + ], + "type": "array", + "items": { + "type": "string" + } + }, + "organizationId": { + "type": "number", + "example": 1 + }, + "createdAt": { + "format": "date-time", + "type": "string", + "example": "2024-03-23T08:33:21.851Z" + } + }, + "required": [ + "id", + "name", + "secret", + "permissions", + "redirectUris", + "organizationId", + "createdAt" + ] + }, + "GetOAuthClientsResponseDto": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PlatformOAuthClientDto" + } + } + }, + "required": [ + "status", + "data" + ] + }, + "GetOAuthClientResponseDto": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/PlatformOAuthClientDto" + } + }, + "required": [ + "status", + "data" + ] + }, + "UpdateOAuthClientInput": { + "type": "object", + "properties": { + "logo": { + "type": "string" + }, + "name": { + "type": "string" + }, + "redirectUris": { + "default": [], + "type": "array", + "items": { + "type": "string" + } + }, + "bookingRedirectUri": { + "type": "string" + }, + "bookingCancelRedirectUri": { + "type": "string" + }, + "bookingRescheduleRedirectUri": { + "type": "string" + }, + "areEmailsEnabled": { + "type": "boolean" + } + } + }, + "OAuthAuthorizeInput": { + "type": "object", + "properties": { + "redirectUri": { + "type": "string" + } + }, + "required": [ + "redirectUri" + ] + }, + "ExchangeAuthorizationCodeInput": { + "type": "object", + "properties": { + "clientSecret": { + "type": "string" + } + }, + "required": [ + "clientSecret" + ] + }, + "RefreshTokenInput": { + "type": "object", + "properties": { + "refreshToken": { + "type": "string" + } + }, + "required": [ + "refreshToken" + ] + }, + "AddressLocation_2024_06_14": { + "type": "object", + "properties": { + "type": { + "type": "string", + "example": "address", + "description": "only allowed value for type is `address`" + }, + "address": { + "type": "string", + "example": "123 Example St, City, Country" + }, + "public": { + "type": "boolean" + } + }, + "required": [ + "type", + "address", + "public" + ] + }, + "LinkLocation_2024_06_14": { + "type": "object", + "properties": { + "type": { + "type": "string", + "example": "link", + "description": "only allowed value for type is `link`" + }, + "link": { + "type": "string", + "example": "https://customvideo.com/join/123456" + }, + "public": { + "type": "boolean" + } + }, + "required": [ + "type", + "link", + "public" + ] + }, + "IntegrationLocation_2024_06_14": { + "type": "object", + "properties": { + "type": { + "type": "string", + "example": "integration", + "description": "only allowed value for type is `integration`" + }, + "integration": { + "type": "string", + "example": "cal-video", + "enum": [ + "cal-video" + ] + } + }, + "required": [ + "type", + "integration" + ] + }, + "PhoneLocation_2024_06_14": { + "type": "object", + "properties": { + "type": { + "type": "string", + "example": "phone", + "description": "only allowed value for type is `phone`" + }, + "phone": { + "type": "string", + "example": "+37120993151" + }, + "public": { + "type": "boolean" + } + }, + "required": [ + "type", + "phone", + "public" + ] + }, + "PhoneFieldInput_2024_06_14": { + "type": "object", + "properties": { + "type": { + "type": "string", + "example": "phone", + "description": "only allowed value for type is `phone`" + }, + "slug": { + "type": "string", + "description": "Unique identifier for the field in format `some-slug`. It is used to access response to this booking field during the booking", + "example": "some-slug" + }, + "label": { + "type": "string" + }, + "required": { + "type": "boolean" + }, + "placeholder": { + "type": "string" + } + }, + "required": [ + "type", + "slug", + "label", + "required", + "placeholder" + ] + }, + "AddressFieldInput_2024_06_14": { + "type": "object", + "properties": { + "type": { + "type": "string", + "example": "address", + "description": "only allowed value for type is `address`" + }, + "slug": { + "type": "string", + "description": "Unique identifier for the field in format `some-slug`. It is used to access response to this booking field during the booking", + "example": "some-slug" + }, + "label": { + "type": "string", + "example": "Please enter your address" + }, + "required": { + "type": "boolean" + }, + "placeholder": { + "type": "string", + "example": "e.g., 1234 Main St" + } + }, + "required": [ + "type", + "slug", + "label", + "required", + "placeholder" + ] + }, + "TextFieldInput_2024_06_14": { + "type": "object", + "properties": { + "type": { + "type": "string", + "example": "text", + "description": "only allowed value for type is `text`" + }, + "slug": { + "type": "string", + "description": "Unique identifier for the field in format `some-slug`. It is used to access response to this booking field during the booking", + "example": "some-slug" + }, + "label": { + "type": "string", + "example": "Please enter your text" + }, + "required": { + "type": "boolean" + }, + "placeholder": { + "type": "string", + "example": "e.g., Enter text here" + } + }, + "required": [ + "type", + "slug", + "label", + "required", + "placeholder" + ] + }, + "NumberFieldInput_2024_06_14": { + "type": "object", + "properties": { + "type": { + "type": "string", + "example": "number", + "description": "only allowed value for type is `number`" + }, + "slug": { + "type": "string", + "description": "Unique identifier for the field in format `some-slug`. It is used to access response to this booking field during the booking", + "example": "some-slug" + }, + "label": { + "type": "string", + "example": "Please enter a number" + }, + "required": { + "type": "boolean" + }, + "placeholder": { + "type": "string", + "example": "e.g., 100" + } + }, + "required": [ + "type", + "slug", + "label", + "required", + "placeholder" + ] + }, + "TextAreaFieldInput_2024_06_14": { + "type": "object", + "properties": { + "type": { + "type": "string", + "example": "textarea", + "description": "only allowed value for type is `textarea`" + }, + "slug": { + "type": "string", + "description": "Unique identifier for the field in format `some-slug`. It is used to access response to this booking field during the booking", + "example": "some-slug" + }, + "label": { + "type": "string", + "example": "Please enter detailed information" + }, + "required": { + "type": "boolean" + }, + "placeholder": { + "type": "string", + "example": "e.g., Detailed description here..." + } + }, + "required": [ + "type", + "slug", + "label", + "required", + "placeholder" + ] + }, + "SelectFieldInput_2024_06_14": { + "type": "object", + "properties": { + "type": { + "type": "string", + "example": "select", + "description": "only allowed value for type is `select`" + }, + "slug": { + "type": "string", + "description": "Unique identifier for the field in format `some-slug`. It is used to access response to this booking field during the booking", + "example": "some-slug" + }, + "label": { + "type": "string", + "example": "Please select an option" + }, + "required": { + "type": "boolean" + }, + "placeholder": { + "type": "string", + "example": "Select..." + }, + "options": { + "example": [ + "Option 1", + "Option 2" + ], + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "type", + "slug", + "label", + "required", + "placeholder", + "options" + ] + }, + "MultiSelectFieldInput_2024_06_14": { + "type": "object", + "properties": { + "type": { + "type": "string", + "example": "multiselect", + "description": "only allowed value for type is `multiselect`" + }, + "slug": { + "type": "string", + "description": "Unique identifier for the field in format `some-slug`. It is used to access response to this booking field during the booking", + "example": "some-slug" + }, + "label": { + "type": "string", + "example": "Please select multiple options" + }, + "required": { + "type": "boolean" + }, + "options": { + "example": [ + "Option 1", + "Option 2" + ], + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "type", + "slug", + "label", + "required", + "options" + ] + }, + "MultiEmailFieldInput_2024_06_14": { + "type": "object", + "properties": { + "type": { + "type": "string", + "example": "multiemail", + "description": "only allowed value for type is `multiemail`" + }, + "slug": { + "type": "string", + "description": "Unique identifier for the field in format `some-slug`. It is used to access response to this booking field during the booking", + "example": "some-slug" + }, + "label": { + "type": "string", + "example": "Please enter multiple emails" + }, + "required": { + "type": "boolean" + }, + "placeholder": { + "type": "string", + "example": "e.g., example@example.com" + } + }, + "required": [ + "type", + "slug", + "label", + "required", + "placeholder" + ] + }, + "CheckboxGroupFieldInput_2024_06_14": { + "type": "object", + "properties": { + "type": { + "type": "string", + "example": "checkbox", + "description": "only allowed value for type is `checkbox`" + }, + "slug": { + "type": "string", + "description": "Unique identifier for the field in format `some-slug`. It is used to access response to this booking field during the booking", + "example": "some-slug" + }, + "label": { + "type": "string", + "example": "Select all that apply" + }, + "required": { + "type": "boolean" + }, + "options": { + "example": [ + "Checkbox 1", + "Checkbox 2" + ], + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "type", + "slug", + "label", + "required", + "options" + ] + }, + "RadioGroupFieldInput_2024_06_14": { + "type": "object", + "properties": { + "type": { + "type": "string", + "example": "radio", + "description": "only allowed value for type is `radio`" + }, + "slug": { + "type": "string", + "description": "Unique identifier for the field in format `some-slug`. It is used to access response to this booking field during the booking", + "example": "some-slug" + }, + "label": { + "type": "string", + "example": "Select one option" + }, + "required": { + "type": "boolean" + }, + "options": { + "example": [ + "Radio 1", + "Radio 2" + ], + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "type", + "slug", + "label", + "required", + "options" + ] + }, + "BooleanFieldInput_2024_06_14": { + "type": "object", + "properties": { + "type": { + "type": "string", + "example": "boolean", + "description": "only allowed value for type is `boolean`" + }, + "slug": { + "type": "string", + "description": "Unique identifier for the field in format `some-slug`. It is used to access response to this booking field during the booking", + "example": "some-slug" + }, + "label": { + "type": "string", + "example": "Agree to terms?" + }, + "required": { + "type": "boolean" + } + }, + "required": [ + "type", + "slug", + "label", + "required" + ] + }, + "BusinessDaysWindow_2024_06_14": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "businessDays", + "calendarDays", + "range" + ], + "description": "Whether the window should be business days, calendar days or a range of dates" + }, + "value": { + "type": "number", + "example": 5, + "description": "How many business day into the future can this event be booked" + }, + "rolling": { + "type": "boolean", + "example": true, + "description": "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." + } + }, + "required": [ + "type", + "value", + "rolling" + ] + }, + "CalendarDaysWindow_2024_06_14": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "businessDays", + "calendarDays", + "range" + ], + "description": "Whether the window should be business days, calendar days or a range of dates" + }, + "value": { + "type": "number", + "example": 5, + "description": "How many calendar days into the future can this event be booked" + }, + "rolling": { + "type": "boolean", + "example": true, + "description": "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." + } + }, + "required": [ + "type", + "value", + "rolling" + ] + }, + "RangeWindow_2024_06_14": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "businessDays", + "calendarDays", + "range" + ], + "description": "Whether the window should be business days, calendar days or a range of dates" + }, + "value": { + "example": [ + "2030-09-05", + "2030-09-09" + ], + "description": "Date range for when this event can be booked.", + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "type", + "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": { + "lengthInMinutes": { + "type": "number", + "example": 60 + }, + "title": { + "type": "string", + "example": "Learn the secrets of masterchief!" + }, + "slug": { + "type": "string", + "example": "learn-the-secrets-of-masterchief" + }, + "description": { + "type": "string", + "example": "Discover the culinary wonders of the Argentina by making the best flan ever!" + }, + "locations": { + "type": "array", + "description": "Locations where the event will take place. If not provided, cal video link will be used as the location.", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/AddressLocation_2024_06_14" + }, + { + "$ref": "#/components/schemas/LinkLocation_2024_06_14" + }, + { + "$ref": "#/components/schemas/IntegrationLocation_2024_06_14" + }, + { + "$ref": "#/components/schemas/PhoneLocation_2024_06_14" + } + ] + } + }, + "bookingFields": { + "type": "array", + "description": "Custom fields that can be added to the booking form when the event is booked by someone. By default booking form has name and email field.", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/PhoneFieldInput_2024_06_14" + }, + { + "$ref": "#/components/schemas/AddressFieldInput_2024_06_14" + }, + { + "$ref": "#/components/schemas/TextFieldInput_2024_06_14" + }, + { + "$ref": "#/components/schemas/NumberFieldInput_2024_06_14" + }, + { + "$ref": "#/components/schemas/TextAreaFieldInput_2024_06_14" + }, + { + "$ref": "#/components/schemas/SelectFieldInput_2024_06_14" + }, + { + "$ref": "#/components/schemas/MultiSelectFieldInput_2024_06_14" + }, + { + "$ref": "#/components/schemas/MultiEmailFieldInput_2024_06_14" + }, + { + "$ref": "#/components/schemas/CheckboxGroupFieldInput_2024_06_14" + }, + { + "$ref": "#/components/schemas/RadioGroupFieldInput_2024_06_14" + }, + { + "$ref": "#/components/schemas/BooleanFieldInput_2024_06_14" + } + ] + } + }, + "disableGuests": { + "type": "boolean", + "description": "If true, person booking this event't cant add guests via their emails." + }, + "slotInterval": { + "type": "number", + "description": "Number representing length of each slot when event is booked. By default it equal length of the event type.\n If event length is 60 minutes then we would have slots 9AM, 10AM, 11AM etc. but if it was changed to 30 minutes then\n we would have slots 9AM, 9:30AM, 10AM, 10:30AM etc. as the available times to book the 60 minute event." + }, + "minimumBookingNotice": { + "type": "number", + "description": "Minimum number of minutes before the event that a booking can be made." + }, + "beforeEventBuffer": { + "type": "number", + "description": "Time spaces that can be pre-pended before an event to give more time before it." + }, + "afterEventBuffer": { + "type": "number", + "description": "Time spaces that can be appended after an event to give more time after it." + }, + "scheduleId": { + "type": "number", + "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" + } + ] + }, + "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" + } + ] + }, + "bookingWindow": { + "type": "array", + "description": "Limit how far in the future this event can be booked", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/BusinessDaysWindow_2024_06_14" + }, + { + "$ref": "#/components/schemas/CalendarDaysWindow_2024_06_14" + }, + { + "$ref": "#/components/schemas/RangeWindow_2024_06_14" + } + ] + } + }, + "offsetStart": { + "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" + } + ] + } + }, + "required": [ + "lengthInMinutes", + "title", + "slug", + "description", + "locations", + "bookingFields", + "disableGuests", + "slotInterval", + "minimumBookingNotice", + "beforeEventBuffer", + "afterEventBuffer", + "scheduleId", + "bookingLimitsCount", + "onlyShowFirstAvailableSlot", + "bookingLimitsDuration", + "bookingWindow", + "offsetStart", + "recurrence" + ] + }, + "EmailDefaultFieldOutput_2024_06_14": { + "type": "object", + "properties": { + "isDefault": { + "type": "object", + "default": true, + "description": "This property is always true because it's a default field", + "example": true + }, + "slug": { + "type": "string", + "default": "email" + }, + "type": { + "type": "string", + "default": "email" + }, + "required": { + "type": "boolean" + } + }, + "required": [ + "isDefault", + "slug", + "type", + "required" + ] + }, + "NameDefaultFieldOutput_2024_06_14": { + "type": "object", + "properties": { + "isDefault": { + "type": "object", + "default": true, + "description": "This property is always true because it's a default field", + "example": true + }, + "slug": { + "type": "string", + "default": "name" + }, + "type": { + "type": "string", + "default": "name" + }, + "required": { + "type": "boolean" + } + }, + "required": [ + "isDefault", + "slug", + "type", + "required" + ] + }, + "LocationDefaultFieldOutput_2024_06_14": { + "type": "object", + "properties": { + "isDefault": { + "type": "object", + "default": true, + "description": "This property is always true because it's a default field", + "example": true + }, + "slug": { + "type": "string", + "default": "location" + }, + "type": { + "type": "string", + "default": "radioInput" + }, + "required": { + "type": "boolean" + } + }, + "required": [ + "isDefault", + "slug", + "type", + "required" + ] + }, + "RescheduleReasonDefaultFieldOutput_2024_06_14": { + "type": "object", + "properties": { + "isDefault": { + "type": "object", + "default": true, + "description": "This property is always true because it's a default field", + "example": true + }, + "slug": { + "type": "string", + "default": "rescheduleReason" + }, + "type": { + "type": "string", + "default": "textarea" + }, + "required": { + "type": "boolean" + } + }, + "required": [ + "isDefault", + "slug", + "type", + "required" + ] + }, + "TitleDefaultFieldOutput_2024_06_14": { + "type": "object", + "properties": { + "isDefault": { + "type": "object", + "default": true, + "description": "This property is always true because it's a default field", + "example": true + }, + "slug": { + "type": "string", + "default": "title" + }, + "type": { + "type": "string", + "default": "text" + }, + "required": { + "type": "boolean" + } + }, + "required": [ + "isDefault", + "slug", + "type", + "required" + ] + }, + "NotesDefaultFieldOutput_2024_06_14": { + "type": "object", + "properties": { + "isDefault": { + "type": "object", + "default": true, + "description": "This property is always true because it's a default field", + "example": true + }, + "slug": { + "type": "string", + "default": "notes" + }, + "type": { + "type": "string", + "default": "textarea" + }, + "required": { + "type": "boolean" + } + }, + "required": [ + "isDefault", + "slug", + "type", + "required" + ] + }, + "GuestsDefaultFieldOutput_2024_06_14": { + "type": "object", + "properties": { + "isDefault": { + "type": "object", + "default": true, + "description": "This property is always true because it's a default field", + "example": true + }, + "slug": { + "type": "string", + "default": "guests" + }, + "type": { + "type": "string", + "default": "multiemail" + }, + "required": { + "type": "boolean" + } + }, + "required": [ + "isDefault", + "slug", + "type", + "required" + ] + }, + "AddressFieldOutput_2024_06_14": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "phone", + "address", + "text", + "number", + "textarea", + "select", + "multiselect", + "multiemail", + "checkbox", + "radio", + "boolean" + ], + "example": "address", + "description": "only allowed value for type is `address`" + }, + "slug": { + "type": "string", + "description": "Unique identifier for the field in format `some-slug`. It is used to access response to this booking field during the booking", + "example": "some-slug" + }, + "label": { + "type": "string", + "example": "Please enter your address" + }, + "required": { + "type": "boolean" + }, + "placeholder": { + "type": "string", + "example": "e.g., 1234 Main St" + }, + "isDefault": { + "type": "object", + "default": false, + "description": "This property is always false because it's not default field but custom field", + "example": false + } + }, + "required": [ + "type", + "slug", + "label", + "required", + "isDefault" + ] + }, + "BooleanFieldOutput_2024_06_14": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "phone", + "address", + "text", + "number", + "textarea", + "select", + "multiselect", + "multiemail", + "checkbox", + "radio", + "boolean" + ], + "example": "boolean", + "description": "only allowed value for type is `boolean`" + }, + "slug": { + "type": "string", + "description": "Unique identifier for the field in format `some-slug`. It is used to access response to this booking field during the booking", + "example": "some-slug" + }, + "label": { + "type": "string", + "example": "Agree to terms?" + }, + "required": { + "type": "boolean" + }, + "isDefault": { + "type": "object", + "default": false, + "description": "This property is always false because it's not default field but custom field", + "example": false + } + }, + "required": [ + "type", + "slug", + "label", + "required", + "isDefault" + ] + }, + "CheckboxGroupFieldOutput_2024_06_14": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "phone", + "address", + "text", + "number", + "textarea", + "select", + "multiselect", + "multiemail", + "checkbox", + "radio", + "boolean" + ], + "example": "checkbox", + "description": "only allowed value for type is `checkbox`" + }, + "slug": { + "type": "string", + "description": "Unique identifier for the field in format `some-slug`. It is used to access response to this booking field during the booking", + "example": "some-slug" + }, + "label": { + "type": "string", + "example": "Select all that apply" + }, + "required": { + "type": "boolean" + }, + "options": { + "example": [ + "Checkbox 1", + "Checkbox 2" + ], + "type": "array", + "items": { + "type": "string" + } + }, + "isDefault": { + "type": "object", + "default": false, + "description": "This property is always false because it's not default field but custom field", + "example": false + } + }, + "required": [ + "type", + "slug", + "label", + "required", + "options", + "isDefault" + ] + }, + "MultiEmailFieldOutput_2024_06_14": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "phone", + "address", + "text", + "number", + "textarea", + "select", + "multiselect", + "multiemail", + "checkbox", + "radio", + "boolean" + ], + "example": "multiemail", + "description": "only allowed value for type is `multiemail`" + }, + "slug": { + "type": "string", + "description": "Unique identifier for the field in format `some-slug`. It is used to access response to this booking field during the booking", + "example": "some-slug" + }, + "label": { + "type": "string", + "example": "Please enter multiple emails" + }, + "required": { + "type": "boolean" + }, + "placeholder": { + "type": "string", + "example": "e.g., example@example.com" + }, + "isDefault": { + "type": "object", + "default": false, + "description": "This property is always false because it's not default field but custom field", + "example": false + } + }, + "required": [ + "type", + "slug", + "label", + "required", + "isDefault" + ] + }, + "MultiSelectFieldOutput_2024_06_14": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "phone", + "address", + "text", + "number", + "textarea", + "select", + "multiselect", + "multiemail", + "checkbox", + "radio", + "boolean" + ], + "example": "multiselect", + "description": "only allowed value for type is `multiselect`" + }, + "slug": { + "type": "string", + "description": "Unique identifier for the field in format `some-slug`. It is used to access response to this booking field during the booking", + "example": "some-slug" + }, + "label": { + "type": "string", + "example": "Please select multiple options" + }, + "required": { + "type": "boolean" + }, + "options": { + "example": [ + "Option 1", + "Option 2" + ], + "type": "array", + "items": { + "type": "string" + } + }, + "isDefault": { + "type": "object", + "default": false, + "description": "This property is always false because it's not default field but custom field", + "example": false + } + }, + "required": [ + "type", + "slug", + "label", + "required", + "options", + "isDefault" + ] + }, + "NumberFieldOutput_2024_06_14": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "phone", + "address", + "text", + "number", + "textarea", + "select", + "multiselect", + "multiemail", + "checkbox", + "radio", + "boolean" + ], + "example": "number", + "description": "only allowed value for type is `number`" + }, + "slug": { + "type": "string", + "description": "Unique identifier for the field in format `some-slug`. It is used to access response to this booking field during the booking", + "example": "some-slug" + }, + "label": { + "type": "string", + "example": "Please enter a number" + }, + "required": { + "type": "boolean" + }, + "placeholder": { + "type": "string", + "example": "e.g., 100" + }, + "isDefault": { + "type": "object", + "default": false, + "description": "This property is always false because it's not default field but custom field", + "example": false + } + }, + "required": [ + "type", + "slug", + "label", + "required", + "isDefault" + ] + }, + "PhoneFieldOutput_2024_06_14": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "phone", + "address", + "text", + "number", + "textarea", + "select", + "multiselect", + "multiemail", + "checkbox", + "radio", + "boolean" + ], + "example": "phone", + "description": "only allowed value for type is `phone`" + }, + "slug": { + "type": "string", + "description": "Unique identifier for the field in format `some-slug`. It is used to access response to this booking field during the booking", + "example": "some-slug" + }, + "label": { + "type": "string" + }, + "required": { + "type": "boolean" + }, + "placeholder": { + "type": "string" + }, + "isDefault": { + "type": "object", + "default": false, + "description": "This property is always false because it's not default field but custom field", + "example": false + } + }, + "required": [ + "type", + "slug", + "label", + "required", + "isDefault" + ] + }, + "RadioGroupFieldOutput_2024_06_14": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "phone", + "address", + "text", + "number", + "textarea", + "select", + "multiselect", + "multiemail", + "checkbox", + "radio", + "boolean" + ], + "example": "radio", + "description": "only allowed value for type is `radio`" + }, + "slug": { + "type": "string", + "description": "Unique identifier for the field in format `some-slug`. It is used to access response to this booking field during the booking", + "example": "some-slug" + }, + "label": { + "type": "string", + "example": "Select one option" + }, + "required": { + "type": "boolean" + }, + "options": { + "example": [ + "Radio 1", + "Radio 2" + ], + "type": "array", + "items": { + "type": "string" + } + }, + "isDefault": { + "type": "object", + "default": false, + "description": "This property is always false because it's not default field but custom field", + "example": false + } + }, + "required": [ + "type", + "slug", + "label", + "required", + "options", + "isDefault" + ] + }, + "SelectFieldOutput_2024_06_14": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "phone", + "address", + "text", + "number", + "textarea", + "select", + "multiselect", + "multiemail", + "checkbox", + "radio", + "boolean" + ], + "example": "select", + "description": "only allowed value for type is `select`" + }, + "slug": { + "type": "string", + "description": "Unique identifier for the field in format `some-slug`. It is used to access response to this booking field during the booking", + "example": "some-slug" + }, + "label": { + "type": "string", + "example": "Please select an option" + }, + "required": { + "type": "boolean" + }, + "placeholder": { + "type": "string", + "example": "Select..." + }, + "options": { + "example": [ + "Option 1", + "Option 2" + ], + "type": "array", + "items": { + "type": "string" + } + }, + "isDefault": { + "type": "object", + "default": false, + "description": "This property is always false because it's not default field but custom field", + "example": false + } + }, + "required": [ + "type", + "slug", + "label", + "required", + "options", + "isDefault" + ] + }, + "TextAreaFieldOutput_2024_06_14": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "phone", + "address", + "text", + "number", + "textarea", + "select", + "multiselect", + "multiemail", + "checkbox", + "radio", + "boolean" + ], + "example": "textarea", + "description": "only allowed value for type is `textarea`" + }, + "slug": { + "type": "string", + "description": "Unique identifier for the field in format `some-slug`. It is used to access response to this booking field during the booking", + "example": "some-slug" + }, + "label": { + "type": "string", + "example": "Please enter detailed information" + }, + "required": { + "type": "boolean" + }, + "placeholder": { + "type": "string", + "example": "e.g., Detailed description here..." + }, + "isDefault": { + "type": "object", + "default": false, + "description": "This property is always false because it's not default field but custom field", + "example": false + } + }, + "required": [ + "type", + "slug", + "label", + "required", + "isDefault" + ] + }, + "TextFieldOutput_2024_06_14": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "phone", + "address", + "text", + "number", + "textarea", + "select", + "multiselect", + "multiemail", + "checkbox", + "radio", + "boolean" + ], + "example": "text", + "description": "only allowed value for type is `text`" + }, + "slug": { + "type": "string", + "description": "Unique identifier for the field in format `some-slug`. It is used to access response to this booking field during the booking", + "example": "some-slug" + }, + "label": { + "type": "string", + "example": "Please enter your text" + }, + "required": { + "type": "boolean" + }, + "placeholder": { + "type": "string", + "example": "e.g., Enter text here" + }, + "isDefault": { + "type": "object", + "default": false, + "description": "This property is always false because it's not default field but custom field", + "example": false + } + }, + "required": [ + "type", + "slug", + "label", + "required", + "isDefault" + ] + }, + "EventTypeOutput_2024_06_14": { + "type": "object", + "properties": { + "id": { + "type": "number", + "example": 1 + }, + "lengthInMinutes": { + "type": "number", + "example": 60 + }, + "title": { + "type": "string", + "example": "Learn the secrets of masterchief!" + }, + "slug": { + "type": "string", + "example": "learn-the-secrets-of-masterchief" + }, + "description": { + "type": "string", + "example": "Discover the culinary wonders of the Argentina by making the best flan ever!" + }, + "locations": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/AddressLocation_2024_06_14" + }, + { + "$ref": "#/components/schemas/LinkLocation_2024_06_14" + }, + { + "$ref": "#/components/schemas/IntegrationLocation_2024_06_14" + }, + { + "$ref": "#/components/schemas/PhoneLocation_2024_06_14" + } + ] + } + }, + "bookingFields": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/NameDefaultFieldOutput_2024_06_14" + }, + { + "$ref": "#/components/schemas/EmailDefaultFieldOutput_2024_06_14" + }, + { + "$ref": "#/components/schemas/LocationDefaultFieldOutput_2024_06_14" + }, + { + "$ref": "#/components/schemas/RescheduleReasonDefaultFieldOutput_2024_06_14" + }, + { + "$ref": "#/components/schemas/TitleDefaultFieldOutput_2024_06_14" + }, + { + "$ref": "#/components/schemas/NotesDefaultFieldOutput_2024_06_14" + }, + { + "$ref": "#/components/schemas/GuestsDefaultFieldOutput_2024_06_14" + }, + { + "$ref": "#/components/schemas/PhoneFieldOutput_2024_06_14" + }, + { + "$ref": "#/components/schemas/AddressFieldOutput_2024_06_14" + }, + { + "$ref": "#/components/schemas/TextFieldOutput_2024_06_14" + }, + { + "$ref": "#/components/schemas/NumberFieldOutput_2024_06_14" + }, + { + "$ref": "#/components/schemas/TextAreaFieldOutput_2024_06_14" + }, + { + "$ref": "#/components/schemas/SelectFieldOutput_2024_06_14" + }, + { + "$ref": "#/components/schemas/MultiSelectFieldOutput_2024_06_14" + }, + { + "$ref": "#/components/schemas/MultiEmailFieldOutput_2024_06_14" + }, + { + "$ref": "#/components/schemas/CheckboxGroupFieldOutput_2024_06_14" + }, + { + "$ref": "#/components/schemas/RadioGroupFieldOutput_2024_06_14" + }, + { + "$ref": "#/components/schemas/BooleanFieldOutput_2024_06_14" + } + ] + } + }, + "disableGuests": { + "type": "boolean" + }, + "slotInterval": { + "type": "number", + "example": 60 + }, + "minimumBookingNotice": { + "type": "number", + "example": 0 + }, + "beforeEventBuffer": { + "type": "number", + "example": 0 + }, + "afterEventBuffer": { + "type": "number", + "example": 0 + }, + "recurrence": { + "type": "object" + }, + "metadata": { + "type": "object" + }, + "requiresConfirmation": { + "type": "boolean" + }, + "price": { + "type": "number" + }, + "currency": { + "type": "string" + }, + "lockTimeZoneToggleOnBookingPage": { + "type": "boolean" + }, + "seatsPerTimeSlot": { + "type": "object" + }, + "forwardParamsSuccessRedirect": { + "type": "object" + }, + "successRedirectUrl": { + "type": "object" + }, + "seatsShowAvailabilityCount": { + "type": "object" + }, + "scheduleId": { + "type": "object" + }, + "bookingLimitsCount": { + "$ref": "#/components/schemas/BookingLimitsCount_2024_06_14" + }, + "onlyShowFirstAvailableSlot": { + "type": "boolean" + }, + "bookingLimitsDuration": { + "$ref": "#/components/schemas/BookingLimitsDuration_2024_06_14" + }, + "bookingWindow": { + "type": "array", + "description": "Limit how far in the future this event can be booked", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/BusinessDaysWindow_2024_06_14" + }, + { + "$ref": "#/components/schemas/CalendarDaysWindow_2024_06_14" + }, + { + "$ref": "#/components/schemas/RangeWindow_2024_06_14" + } + ] + } + }, + "offsetStart": { + "type": "number" + }, + "ownerId": { + "type": "number", + "example": 10 + }, + "users": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "id", + "lengthInMinutes", + "title", + "slug", + "description", + "locations", + "bookingFields", + "disableGuests", + "slotInterval", + "minimumBookingNotice", + "beforeEventBuffer", + "afterEventBuffer", + "recurrence", + "metadata", + "requiresConfirmation", + "price", + "currency", + "lockTimeZoneToggleOnBookingPage", + "seatsPerTimeSlot", + "forwardParamsSuccessRedirect", + "successRedirectUrl", + "seatsShowAvailabilityCount", + "scheduleId", + "bookingLimitsCount", + "onlyShowFirstAvailableSlot", + "bookingLimitsDuration", + "bookingWindow", + "offsetStart", + "ownerId", + "users" + ] + }, + "CreateEventTypeOutput_2024_06_14": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "success", + "error" + ], + "example": "success" + }, + "data": { + "$ref": "#/components/schemas/EventTypeOutput_2024_06_14" + } + }, + "required": [ + "status", + "data" + ] + }, + "GetEventTypeOutput_2024_06_14": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "success", + "error" + ], + "example": "success" + }, + "data": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/EventTypeOutput_2024_06_14" + } + ] + } + }, + "required": [ + "status", + "data" + ] + }, + "GetEventTypesOutput_2024_06_14": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "success", + "error" + ], + "example": "success" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/EventTypeOutput_2024_06_14" + } + } + }, + "required": [ + "status", + "data" + ] + }, + "UpdateEventTypeInput_2024_06_14": { + "type": "object", + "properties": { + "lengthInMinutes": { + "type": "number", + "example": 60 + }, + "title": { + "type": "string", + "example": "Learn the secrets of masterchief!" + }, + "slug": { + "type": "string", + "example": "learn-the-secrets-of-masterchief" + }, + "description": { + "type": "string", + "example": "Discover the culinary wonders of the Argentina by making the best flan ever!" + }, + "locations": { + "type": "array", + "description": "Locations where the event will take place. If not provided, cal video link will be used as the location.", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/AddressLocation_2024_06_14" + }, + { + "$ref": "#/components/schemas/LinkLocation_2024_06_14" + }, + { + "$ref": "#/components/schemas/IntegrationLocation_2024_06_14" + }, + { + "$ref": "#/components/schemas/PhoneLocation_2024_06_14" + } + ] + } + }, + "bookingFields": { + "type": "array", + "description": "Custom fields that can be added to the booking form when the event is booked by someone. By default booking form has name and email field.", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/PhoneFieldInput_2024_06_14" + }, + { + "$ref": "#/components/schemas/AddressFieldInput_2024_06_14" + }, + { + "$ref": "#/components/schemas/TextFieldInput_2024_06_14" + }, + { + "$ref": "#/components/schemas/NumberFieldInput_2024_06_14" + }, + { + "$ref": "#/components/schemas/TextAreaFieldInput_2024_06_14" + }, + { + "$ref": "#/components/schemas/SelectFieldInput_2024_06_14" + }, + { + "$ref": "#/components/schemas/MultiSelectFieldInput_2024_06_14" + }, + { + "$ref": "#/components/schemas/MultiEmailFieldInput_2024_06_14" + }, + { + "$ref": "#/components/schemas/CheckboxGroupFieldInput_2024_06_14" + }, + { + "$ref": "#/components/schemas/RadioGroupFieldInput_2024_06_14" + }, + { + "$ref": "#/components/schemas/BooleanFieldInput_2024_06_14" + } + ] + } + }, + "disableGuests": { + "type": "boolean", + "description": "If true, person booking this event't cant add guests via their emails." + }, + "slotInterval": { + "type": "number", + "description": "Number representing length of each slot when event is booked. By default it equal length of the event type.\n If event length is 60 minutes then we would have slots 9AM, 10AM, 11AM etc. but if it was changed to 30 minutes then\n we would have slots 9AM, 9:30AM, 10AM, 10:30AM etc. as the available times to book the 60 minute event." + }, + "minimumBookingNotice": { + "type": "number", + "description": "Minimum number of minutes before the event that a booking can be made." + }, + "beforeEventBuffer": { + "type": "number", + "description": "Time spaces that can be pre-pended before an event to give more time before it." + }, + "afterEventBuffer": { + "type": "number", + "description": "Time spaces that can be appended after an event to give more time after it." + }, + "scheduleId": { + "type": "number", + "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" + } + ] + }, + "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" + } + ] + }, + "bookingWindow": { + "type": "array", + "description": "Limit how far in the future this event can be booked", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/BusinessDaysWindow_2024_06_14" + }, + { + "$ref": "#/components/schemas/CalendarDaysWindow_2024_06_14" + }, + { + "$ref": "#/components/schemas/RangeWindow_2024_06_14" + } + ] + } + }, + "offsetStart": { + "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" + } + ] + } + }, + "required": [ + "lengthInMinutes", + "title", + "slug", + "description", + "locations", + "bookingFields", + "disableGuests", + "slotInterval", + "minimumBookingNotice", + "beforeEventBuffer", + "afterEventBuffer", + "scheduleId", + "bookingLimitsCount", + "onlyShowFirstAvailableSlot", + "bookingLimitsDuration", + "bookingWindow", + "offsetStart", + "recurrence" + ] + }, + "UpdateEventTypeOutput_2024_06_14": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "success", + "error" + ], + "example": "success" + }, + "data": { + "$ref": "#/components/schemas/EventTypeOutput_2024_06_14" + } + }, + "required": [ + "status", + "data" + ] + }, + "DeleteData_2024_06_14": { + "type": "object", + "properties": { + "id": { + "type": "number", + "example": 1 + }, + "lengthInMinutes": { + "type": "number", + "example": 60 + }, + "title": { + "type": "string", + "example": "Learn the secrets of masterchief!" + }, + "slug": { + "type": "string" + } + }, + "required": [ + "id", + "lengthInMinutes", + "title", + "slug" + ] + }, + "DeleteEventTypeOutput_2024_06_14": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "success", + "error" + ], + "example": "success" + }, + "data": { + "$ref": "#/components/schemas/DeleteData_2024_06_14" + } + }, + "required": [ + "status", + "data" + ] + }, + "SelectedCalendarsInputDto": { + "type": "object", + "properties": { + "integration": { + "type": "string" + }, + "externalId": { + "type": "string" + }, + "credentialId": { + "type": "number" + } + }, + "required": [ + "integration", + "externalId", + "credentialId" + ] + }, + "SelectedCalendarOutputDto": { + "type": "object", + "properties": { + "userId": { + "type": "number" + }, + "integration": { + "type": "string" + }, + "externalId": { + "type": "string" + }, + "credentialId": { + "type": "number", + "nullable": true + } + }, + "required": [ + "userId", + "integration", + "externalId", + "credentialId" + ] + }, + "SelectedCalendarOutputResponseDto": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/SelectedCalendarOutputDto" + } + }, + "required": [ + "status", + "data" + ] + }, + "OrgTeamOutputDto": { + "type": "object", + "properties": { + "id": { + "type": "number" + }, + "parentId": { + "type": "number" + }, + "name": { + "type": "string", + "minLength": 1 + }, + "slug": { + "type": "string" + }, + "logoUrl": { + "type": "string" + }, + "calVideoLogo": { + "type": "string" + }, + "appLogo": { + "type": "string" + }, + "appIconLogo": { + "type": "string" + }, + "bio": { + "type": "string" + }, + "hideBranding": { + "type": "boolean" + }, + "isOrganization": { + "type": "boolean" + }, + "isPrivate": { + "type": "boolean" + }, + "hideBookATeamMember": { + "type": "boolean", + "default": false + }, + "metadata": { + "type": "string" + }, + "theme": { + "type": "string" + }, + "brandColor": { + "type": "string" + }, + "darkBrandColor": { + "type": "string" + }, + "bannerUrl": { + "type": "string" + }, + "timeFormat": { + "type": "number" + }, + "timeZone": { + "type": "string", + "default": "Europe/London" + }, + "weekStart": { + "type": "string", + "default": "Sunday" + } + }, + "required": [ + "id", + "name" + ] + }, + "OrgTeamsOutputResponseDto": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/OrgTeamOutputDto" + } + } + }, + "required": [ + "status", + "data" + ] + }, + "OrgMeTeamsOutputResponseDto": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/OrgTeamOutputDto" + } + } + }, + "required": [ + "status", + "data" + ] + }, + "OrgTeamOutputResponseDto": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/OrgTeamOutputDto" + } + }, + "required": [ + "status", + "data" + ] + }, + "UpdateOrgTeamDto": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1 + }, + "slug": { + "type": "string" + }, + "logoUrl": { + "type": "string" + }, + "calVideoLogo": { + "type": "string" + }, + "appLogo": { + "type": "string" + }, + "appIconLogo": { + "type": "string" + }, + "bio": { + "type": "string" + }, + "hideBranding": { + "type": "boolean", + "default": false + }, + "isPrivate": { + "type": "boolean" + }, + "hideBookATeamMember": { + "type": "boolean" + }, + "metadata": { + "type": "string" + }, + "theme": { + "type": "string" + }, + "brandColor": { + "type": "string" + }, + "darkBrandColor": { + "type": "string" + }, + "bannerUrl": { + "type": "string" + }, + "timeFormat": { + "type": "number" + }, + "timeZone": { + "type": "string", + "default": "Europe/London" + }, + "weekStart": { + "type": "string", + "default": "Sunday" + } + } + }, + "CreateOrgTeamDto": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1 + }, + "slug": { + "type": "string" + }, + "logoUrl": { + "type": "string" + }, + "calVideoLogo": { + "type": "string" + }, + "appLogo": { + "type": "string" + }, + "appIconLogo": { + "type": "string" + }, + "bio": { + "type": "string" + }, + "hideBranding": { + "type": "boolean", + "default": false + }, + "isPrivate": { + "type": "boolean" + }, + "hideBookATeamMember": { + "type": "boolean" + }, + "metadata": { + "type": "string" + }, + "theme": { + "type": "string" + }, + "brandColor": { + "type": "string" + }, + "darkBrandColor": { + "type": "string" + }, + "bannerUrl": { + "type": "string" + }, + "timeFormat": { + "type": "number" + }, + "timeZone": { + "type": "string", + "default": "Europe/London" + }, + "weekStart": { + "type": "string", + "default": "Sunday" + }, + "autoAcceptCreator": { + "type": "boolean", + "default": true + } + }, + "required": [ + "name" + ] + }, + "ScheduleAvailabilityInput_2024_06_11": { + "type": "object", + "properties": { + "days": { + "type": "string", + "enum": [ + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday", + "Sunday" + ], + "example": [ + "Monday", + "Tuesday" + ], + "description": "Array of days when schedule is active." + }, + "startTime": { + "type": "string", + "pattern": "TIME_FORMAT_HH_MM", + "example": "08:00", + "description": "startTime must be a valid time in format HH:MM e.g. 08:00" + }, + "endTime": { + "type": "string", + "pattern": "TIME_FORMAT_HH_MM", + "example": "15:00", + "description": "endTime must be a valid time in format HH:MM e.g. 15:00" + } + }, + "required": [ + "days", + "startTime", + "endTime" + ] + }, + "ScheduleOverrideInput_2024_06_11": { + "type": "object", + "properties": { + "date": { + "type": "string", + "example": "2024-05-20" + }, + "startTime": { + "type": "string", + "pattern": "TIME_FORMAT_HH_MM", + "example": "12:00", + "description": "startTime must be a valid time in format HH:MM e.g. 12:00" + }, + "endTime": { + "type": "string", + "pattern": "TIME_FORMAT_HH_MM", + "example": "13:00", + "description": "endTime must be a valid time in format HH:MM e.g. 13:00" + } + }, + "required": [ + "date", + "startTime", + "endTime" + ] + }, + "ScheduleOutput_2024_06_11": { + "type": "object", + "properties": { + "id": { + "type": "number", + "example": 254 + }, + "ownerId": { + "type": "number", + "example": 478 + }, + "name": { + "type": "string", + "example": "Catch up hours" + }, + "timeZone": { + "type": "string", + "example": "Europe/Rome" + }, + "availability": { + "example": [ + { + "days": [ + "Monday", + "Tuesday" + ], + "startTime": "17:00", + "endTime": "19:00" + }, + { + "days": [ + "Wednesday", + "Thursday" + ], + "startTime": "16:00", + "endTime": "20:00" + } + ], + "type": "array", + "items": { + "$ref": "#/components/schemas/ScheduleAvailabilityInput_2024_06_11" + } + }, + "isDefault": { + "type": "boolean", + "example": true + }, + "overrides": { + "example": [ + { + "date": "2024-05-20", + "startTime": "18:00", + "endTime": "21:00" + } + ], + "type": "array", + "items": { + "$ref": "#/components/schemas/ScheduleOverrideInput_2024_06_11" + } + } + }, + "required": [ + "id", + "ownerId", + "name", + "timeZone", + "availability", + "isDefault", + "overrides" + ] + }, + "GetSchedulesOutput_2024_06_11": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ScheduleOutput_2024_06_11" + } + }, + "error": { + "type": "object" + } + }, + "required": [ + "status", + "data" + ] + }, + "CreateScheduleInput_2024_06_11": { + "type": "object", + "properties": { + "name": { + "type": "string", + "example": "Catch up hours" + }, + "timeZone": { + "type": "string", + "example": "Europe/Rome", + "description": "Timezone is used to calculate available times when an event using the schedule is booked." + }, + "availability": { + "description": "Each object contains days and times when the user is available. If not passed, the default availability is Monday to Friday from 09:00 to 17:00.", + "example": [ + { + "days": [ + "Monday", + "Tuesday" + ], + "startTime": "17:00", + "endTime": "19:00" + }, + { + "days": [ + "Wednesday", + "Thursday" + ], + "startTime": "16:00", + "endTime": "20:00" + } + ], + "type": "array", + "items": { + "$ref": "#/components/schemas/ScheduleAvailabilityInput_2024_06_11" + } + }, + "isDefault": { + "type": "boolean", + "example": true, + "description": "Each user should have 1 default schedule. If you specified `timeZone` when creating managed user, then the default schedule will be created with that timezone.\n Default schedule means that if an event type is not tied to a specific schedule then the default schedule is used." + }, + "overrides": { + "description": "Need to change availability for a specific date? Add an override.", + "example": [ + { + "date": "2024-05-20", + "startTime": "18:00", + "endTime": "21:00" + } + ], + "type": "array", + "items": { + "$ref": "#/components/schemas/ScheduleOverrideInput_2024_06_11" + } + } + }, + "required": [ + "name", + "timeZone", + "isDefault" + ] + }, + "CreateScheduleOutput_2024_06_11": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/ScheduleOutput_2024_06_11" + } + }, + "required": [ + "status", + "data" + ] + }, + "GetScheduleOutput_2024_06_11": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/ScheduleOutput_2024_06_11" + } + ] + }, + "error": { + "type": "object" + } + }, + "required": [ + "status", + "data" + ] + }, + "UpdateScheduleInput_2024_06_11": { + "type": "object", + "properties": { + "name": { + "type": "string", + "example": "One-on-one coaching" + }, + "timeZone": { + "type": "string", + "example": "Europe/Rome" + }, + "availability": { + "example": [ + { + "days": [ + "Monday", + "Tuesday" + ], + "startTime": "09:00", + "endTime": "10:00" + } + ], + "type": "array", + "items": { + "$ref": "#/components/schemas/ScheduleAvailabilityInput_2024_06_11" + } + }, + "isDefault": { + "type": "boolean", + "example": true + }, + "overrides": { + "example": [ + { + "date": "2024-05-20", + "startTime": "12:00", + "endTime": "14:00" + } + ], + "type": "array", + "items": { + "$ref": "#/components/schemas/ScheduleOverrideInput_2024_06_11" + } + } + } + }, + "UpdateScheduleOutput_2024_06_11": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/ScheduleOutput_2024_06_11" + }, + "error": { + "type": "object" + } + }, + "required": [ + "status", + "data" + ] + }, + "DeleteScheduleOutput_2024_06_11": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + } + }, + "required": [ + "status" + ] + }, + "GetUserOutput": { + "type": "object", + "properties": { + "id": { + "type": "number", + "description": "The ID of the user", + "example": 1 + }, + "username": { + "type": "string", + "nullable": true, + "description": "The username of the user", + "example": "john_doe" + }, + "name": { + "type": "string", + "nullable": true, + "description": "The name of the user", + "example": "John Doe" + }, + "email": { + "type": "string", + "description": "The email of the user", + "example": "john@example.com" + }, + "emailVerified": { + "format": "date-time", + "type": "string", + "nullable": true, + "description": "The date when the email was verified", + "example": "2022-01-01T00:00:00Z" + }, + "bio": { + "type": "string", + "nullable": true, + "description": "The bio of the user", + "example": "I am a software developer" + }, + "avatarUrl": { + "type": "string", + "nullable": true, + "description": "The URL of the user's avatar", + "example": "https://example.com/avatar.jpg" + }, + "timeZone": { + "type": "string", + "description": "The time zone of the user", + "example": "America/New_York" + }, + "weekStart": { + "type": "string", + "description": "The week start day of the user", + "example": "Monday" + }, + "appTheme": { + "type": "string", + "nullable": true, + "description": "The app theme of the user", + "example": "light" + }, + "theme": { + "type": "string", + "nullable": true, + "description": "The theme of the user", + "example": "default" + }, + "defaultScheduleId": { + "type": "number", + "nullable": true, + "description": "The ID of the default schedule for the user", + "example": 1 + }, + "locale": { + "type": "string", + "nullable": true, + "description": "The locale of the user", + "example": "en-US" + }, + "timeFormat": { + "type": "number", + "nullable": true, + "description": "The time format of the user", + "example": 12 + }, + "hideBranding": { + "type": "boolean", + "description": "Whether to hide branding for the user", + "example": false + }, + "brandColor": { + "type": "string", + "nullable": true, + "description": "The brand color of the user", + "example": "#ffffff" + }, + "darkBrandColor": { + "type": "string", + "nullable": true, + "description": "The dark brand color of the user", + "example": "#000000" + }, + "allowDynamicBooking": { + "type": "boolean", + "nullable": true, + "description": "Whether dynamic booking is allowed for the user", + "example": true + }, + "createdDate": { + "format": "date-time", + "type": "string", + "description": "The date when the user was created", + "example": "2022-01-01T00:00:00Z" + }, + "verified": { + "type": "boolean", + "nullable": true, + "description": "Whether the user is verified", + "example": true + }, + "invitedTo": { + "type": "number", + "nullable": true, + "description": "The ID of the user who invited this user", + "example": 1 + } + }, + "required": [ + "id", + "email", + "timeZone", + "weekStart", + "hideBranding", + "createdDate" + ] + }, + "GetOrganizationUsersOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/GetUserOutput" + } + } + }, + "required": [ + "status", + "data" + ] + }, + "CreateOrganizationUserInput": { + "type": "object", + "properties": { + "email": { + "type": "string", + "description": "User email address", + "example": "user@example.com" + }, + "username": { + "type": "string", + "description": "Username", + "example": "user123" + }, + "weekday": { + "type": "string", + "description": "Preferred weekday", + "example": "Monday" + }, + "brandColor": { + "type": "string", + "description": "Brand color in HEX format", + "example": "#FFFFFF" + }, + "darkBrandColor": { + "type": "string", + "description": "Dark brand color in HEX format", + "example": "#000000" + }, + "hideBranding": { + "type": "boolean", + "description": "Hide branding", + "example": false + }, + "timeZone": { + "type": "string", + "description": "Time zone", + "example": "America/New_York" + }, + "theme": { + "type": "string", + "nullable": true, + "description": "Theme", + "example": "dark" + }, + "appTheme": { + "type": "string", + "nullable": true, + "description": "Application theme", + "example": "light" + }, + "timeFormat": { + "type": "number", + "description": "Time format", + "example": 24 + }, + "defaultScheduleId": { + "type": "number", + "minimum": 0, + "description": "Default schedule ID", + "example": 1 + }, + "locale": { + "type": "string", + "nullable": true, + "default": "en", + "description": "Locale", + "example": "en" + }, + "avatarUrl": { + "type": "string", + "description": "Avatar URL", + "example": "https://example.com/avatar.jpg" + }, + "organizationRole": { + "type": "object", + "default": "MEMBER" + }, + "autoAccept": { + "type": "object", + "default": true + } + }, + "required": [ + "email", + "organizationRole", + "autoAccept" + ] + }, + "GetOrganizationUserOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/GetUserOutput" + } + }, + "required": [ + "status", + "data" + ] + }, + "UpdateOrganizationUserInput": { + "type": "object", + "properties": {} + }, + "OrgMembershipOutputDto": { + "type": "object", + "properties": { + "role": { + "type": "string", + "enum": [ + "MEMBER", + "OWNER", + "ADMIN" + ] + }, + "id": { + "type": "number" + }, + "userId": { + "type": "number" + }, + "teamId": { + "type": "number" + }, + "accepted": { + "type": "boolean" + }, + "disableImpersonation": { + "type": "boolean" + } + }, + "required": [ + "role", + "id", + "userId", + "teamId", + "accepted" + ] + }, + "GetAllOrgMemberships": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/OrgMembershipOutputDto" + } + }, + "required": [ + "status", + "data" + ] + }, + "CreateOrgMembershipDto": { + "type": "object", + "properties": { + "role": { + "type": "string", + "default": "MEMBER", + "enum": [ + "MEMBER", + "OWNER", + "ADMIN" + ] + }, + "userId": { + "type": "number" + }, + "accepted": { + "type": "boolean", + "default": false + }, + "disableImpersonation": { + "type": "boolean", + "default": false + } + }, + "required": [ + "role", + "userId" + ] + }, + "CreateOrgMembershipOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/OrgMembershipOutputDto" + } + }, + "required": [ + "status", + "data" + ] + }, + "GetOrgMembership": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/OrgMembershipOutputDto" + } + }, + "required": [ + "status", + "data" + ] + }, + "DeleteOrgMembership": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/OrgMembershipOutputDto" + } + }, + "required": [ + "status", + "data" + ] + }, + "UpdateOrgMembershipDto": { + "type": "object", + "properties": { + "role": { + "type": "string", + "default": "MEMBER", + "enum": [ + "MEMBER", + "OWNER", + "ADMIN" + ] + }, + "accepted": { + "type": "boolean", + "default": false + }, + "disableImpersonation": { + "type": "boolean", + "default": false + } + } + }, + "UpdateOrgMembership": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/OrgMembershipOutputDto" + } + }, + "required": [ + "status", + "data" + ] + }, + "CreateTeamEventTypeInput_2024_06_14": { + "type": "object", + "properties": { + "lengthInMinutes": { + "type": "number", + "example": 60 + }, + "title": { + "type": "string", + "example": "Learn the secrets of masterchief!" + }, + "slug": { + "type": "string", + "example": "learn-the-secrets-of-masterchief" + }, + "description": { + "type": "string", + "example": "Discover the culinary wonders of the Argentina by making the best flan ever!" + }, + "locations": { + "type": "array", + "description": "Locations where the event will take place. If not provided, cal video link will be used as the location.", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/AddressLocation_2024_06_14" + }, + { + "$ref": "#/components/schemas/LinkLocation_2024_06_14" + }, + { + "$ref": "#/components/schemas/IntegrationLocation_2024_06_14" + }, + { + "$ref": "#/components/schemas/PhoneLocation_2024_06_14" + } + ] + } + }, + "bookingFields": { + "type": "array", + "description": "Custom fields that can be added to the booking form when the event is booked by someone. By default booking form has name and email field.", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/PhoneFieldInput_2024_06_14" + }, + { + "$ref": "#/components/schemas/AddressFieldInput_2024_06_14" + }, + { + "$ref": "#/components/schemas/TextFieldInput_2024_06_14" + }, + { + "$ref": "#/components/schemas/NumberFieldInput_2024_06_14" + }, + { + "$ref": "#/components/schemas/TextAreaFieldInput_2024_06_14" + }, + { + "$ref": "#/components/schemas/SelectFieldInput_2024_06_14" + }, + { + "$ref": "#/components/schemas/MultiSelectFieldInput_2024_06_14" + }, + { + "$ref": "#/components/schemas/MultiEmailFieldInput_2024_06_14" + }, + { + "$ref": "#/components/schemas/CheckboxGroupFieldInput_2024_06_14" + }, + { + "$ref": "#/components/schemas/RadioGroupFieldInput_2024_06_14" + }, + { + "$ref": "#/components/schemas/BooleanFieldInput_2024_06_14" + } + ] + } + }, + "disableGuests": { + "type": "boolean", + "description": "If true, person booking this event't cant add guests via their emails." + }, + "slotInterval": { + "type": "number", + "description": "Number representing length of each slot when event is booked. By default it equal length of the event type.\n If event length is 60 minutes then we would have slots 9AM, 10AM, 11AM etc. but if it was changed to 30 minutes then\n we would have slots 9AM, 9:30AM, 10AM, 10:30AM etc. as the available times to book the 60 minute event." + }, + "minimumBookingNotice": { + "type": "number", + "description": "Minimum number of minutes before the event that a booking can be made." + }, + "beforeEventBuffer": { + "type": "number", + "description": "Time spaces that can be pre-pended before an event to give more time before it." + }, + "afterEventBuffer": { + "type": "number", + "description": "Time spaces that can be appended after an event to give more time after it." + }, + "scheduleId": { + "type": "number", + "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" + } + ] + }, + "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" + } + ] + }, + "bookingWindow": { + "type": "array", + "description": "Limit how far in the future this event can be booked", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/BusinessDaysWindow_2024_06_14" + }, + { + "$ref": "#/components/schemas/CalendarDaysWindow_2024_06_14" + }, + { + "$ref": "#/components/schemas/RangeWindow_2024_06_14" + } + ] + } + }, + "offsetStart": { + "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" + } + ] + }, + "schedulingType": { + "type": "object" + }, + "hosts": { + "type": "array", + "items": { + "type": "string" + } + }, + "assignAllTeamMembers": { + "type": "boolean", + "description": "If true, all current and future team members will be assigned to this event type" + } + }, + "required": [ + "lengthInMinutes", + "title", + "slug", + "description", + "locations", + "bookingFields", + "disableGuests", + "slotInterval", + "minimumBookingNotice", + "beforeEventBuffer", + "afterEventBuffer", + "scheduleId", + "bookingLimitsCount", + "onlyShowFirstAvailableSlot", + "bookingLimitsDuration", + "bookingWindow", + "offsetStart", + "recurrence", + "schedulingType", + "hosts", + "assignAllTeamMembers" + ] + }, + "CreateTeamEventTypeOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "oneOf": [ + { + "$ref": "#/components/schemas/TeamEventTypeOutput_2024_06_14" + }, + { + "type": "array", + "items": { + "$ref": "#/components/schemas/TeamEventTypeOutput_2024_06_14" + } + } + ] + } + }, + "required": [ + "status", + "data" + ] + }, + "TeamEventTypeResponseHost": { + "type": "object", + "properties": { + "userId": { + "type": "number", + "description": "Which user is the host of this event" + }, + "mandatory": { + "type": "boolean", + "default": false, + "description": "Only relevant for round robin event types. If true then the user must attend round robin event always." + }, + "priority": { + "type": "string", + "default": "medium", + "enum": [ + "lowest", + "low", + "medium", + "high", + "highest" + ] + }, + "name": { + "type": "string", + "example": "John Doe" + } + }, + "required": [ + "userId", + "name" + ] + }, + "TeamEventTypeOutput_2024_06_14": { + "type": "object", + "properties": { + "id": { + "type": "number", + "example": 1 + }, + "lengthInMinutes": { + "type": "number", + "minimum": 1, + "example": 60 + }, + "title": { + "type": "string", + "example": "Learn the secrets of masterchief!" + }, + "slug": { + "type": "string", + "example": "learn-the-secrets-of-masterchief" + }, + "description": { + "type": "string", + "example": "Discover the culinary wonders of the Argentina by making the best flan ever!" + }, + "locations": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/AddressLocation_2024_06_14" + }, + { + "$ref": "#/components/schemas/LinkLocation_2024_06_14" + }, + { + "$ref": "#/components/schemas/IntegrationLocation_2024_06_14" + }, + { + "$ref": "#/components/schemas/PhoneLocation_2024_06_14" + } + ] + } + }, + "bookingFields": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/NameDefaultFieldOutput_2024_06_14" + }, + { + "$ref": "#/components/schemas/EmailDefaultFieldOutput_2024_06_14" + }, + { + "$ref": "#/components/schemas/LocationDefaultFieldOutput_2024_06_14" + }, + { + "$ref": "#/components/schemas/RescheduleReasonDefaultFieldOutput_2024_06_14" + }, + { + "$ref": "#/components/schemas/TitleDefaultFieldOutput_2024_06_14" + }, + { + "$ref": "#/components/schemas/NotesDefaultFieldOutput_2024_06_14" + }, + { + "$ref": "#/components/schemas/GuestsDefaultFieldOutput_2024_06_14" + }, + { + "$ref": "#/components/schemas/PhoneFieldOutput_2024_06_14" + }, + { + "$ref": "#/components/schemas/AddressFieldOutput_2024_06_14" + }, + { + "$ref": "#/components/schemas/TextFieldOutput_2024_06_14" + }, + { + "$ref": "#/components/schemas/NumberFieldOutput_2024_06_14" + }, + { + "$ref": "#/components/schemas/TextAreaFieldOutput_2024_06_14" + }, + { + "$ref": "#/components/schemas/SelectFieldOutput_2024_06_14" + }, + { + "$ref": "#/components/schemas/MultiSelectFieldOutput_2024_06_14" + }, + { + "$ref": "#/components/schemas/MultiEmailFieldOutput_2024_06_14" + }, + { + "$ref": "#/components/schemas/CheckboxGroupFieldOutput_2024_06_14" + }, + { + "$ref": "#/components/schemas/RadioGroupFieldOutput_2024_06_14" + }, + { + "$ref": "#/components/schemas/BooleanFieldOutput_2024_06_14" + } + ] + } + }, + "disableGuests": { + "type": "boolean" + }, + "slotInterval": { + "type": "number", + "nullable": true, + "example": 60 + }, + "minimumBookingNotice": { + "type": "number", + "minimum": 0, + "example": 0 + }, + "beforeEventBuffer": { + "type": "number", + "example": 0 + }, + "afterEventBuffer": { + "type": "number", + "example": 0 + }, + "recurrence": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/Recurrence_2024_06_14" + } + ] + }, + "metadata": { + "type": "object" + }, + "requiresConfirmation": { + "type": "boolean" + }, + "price": { + "type": "number" + }, + "currency": { + "type": "string" + }, + "lockTimeZoneToggleOnBookingPage": { + "type": "boolean" + }, + "seatsPerTimeSlot": { + "type": "number", + "nullable": true + }, + "forwardParamsSuccessRedirect": { + "type": "boolean", + "nullable": true + }, + "successRedirectUrl": { + "type": "string", + "nullable": true + }, + "seatsShowAvailabilityCount": { + "type": "boolean", + "nullable": true + }, + "scheduleId": { + "type": "number", + "nullable": true + }, + "bookingLimitsCount": { + "$ref": "#/components/schemas/BookingLimitsCount_2024_06_14" + }, + "onlyShowFirstAvailableSlot": { + "type": "boolean" + }, + "bookingLimitsDuration": { + "$ref": "#/components/schemas/BookingLimitsDuration_2024_06_14" + }, + "bookingWindow": { + "type": "array", + "description": "Limit how far in the future this event can be booked", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/BusinessDaysWindow_2024_06_14" + }, + { + "$ref": "#/components/schemas/CalendarDaysWindow_2024_06_14" + }, + { + "$ref": "#/components/schemas/RangeWindow_2024_06_14" + } + ] + } + }, + "offsetStart": { + "type": "number", + "minimum": 1 + }, + "schedulingType": { + "type": "string", + "nullable": true, + "enum": [ + "ROUND_ROBIN", + "COLLECTIVE", + "MANAGED" + ] + }, + "teamId": { + "type": "number", + "nullable": true + }, + "ownerId": { + "type": "number", + "nullable": true + }, + "parentEventTypeId": { + "type": "number", + "nullable": true, + "description": "For managed event types parent event type is the event type that this event type is based on" + }, + "hosts": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TeamEventTypeResponseHost" + } + }, + "assignAllTeamMembers": { + "type": "boolean" + } + }, + "required": [ + "id", + "lengthInMinutes", + "title", + "slug", + "description", + "locations", + "bookingFields", + "disableGuests", + "recurrence", + "metadata", + "requiresConfirmation", + "price", + "currency", + "lockTimeZoneToggleOnBookingPage", + "seatsPerTimeSlot", + "forwardParamsSuccessRedirect", + "successRedirectUrl", + "seatsShowAvailabilityCount", + "scheduleId", + "schedulingType", + "hosts" + ] + }, + "GetTeamEventTypeOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/TeamEventTypeOutput_2024_06_14" + } + }, + "required": [ + "status", + "data" + ] + }, + "CreatePhoneCallInput": { + "type": "object", + "properties": { + "yourPhoneNumber": { + "type": "string", + "pattern": "/^\\+[1-9]\\d{1,14}$/", + "description": "Your phone number" + }, + "numberToCall": { + "type": "string", + "pattern": "/^\\+[1-9]\\d{1,14}$/", + "description": "Number to call" + }, + "calApiKey": { + "type": "string", + "description": "CAL API Key" + }, + "enabled": { + "type": "object", + "default": true, + "description": "Enabled status" + }, + "templateType": { + "default": "CUSTOM_TEMPLATE", + "enum": [ + "CHECK_IN_APPOINTMENT", + "CUSTOM_TEMPLATE" + ], + "type": "string", + "description": "Template type" + }, + "schedulerName": { + "type": "string", + "description": "Scheduler name" + }, + "guestName": { + "type": "string", + "description": "Guest name" + }, + "guestEmail": { + "type": "string", + "description": "Guest email" + }, + "guestCompany": { + "type": "string", + "description": "Guest company" + }, + "beginMessage": { + "type": "string", + "description": "Begin message" + }, + "generalPrompt": { + "type": "string", + "description": "General prompt" + } + }, + "required": [ + "yourPhoneNumber", + "numberToCall", + "calApiKey", + "enabled", + "templateType" + ] + }, + "Data": { + "type": "object", + "properties": { + "callId": { + "type": "string" + }, + "agentId": { + "type": "string" + } + }, + "required": [ + "callId", + "agentId" + ] + }, + "CreatePhoneCallOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/Data" + } + }, + "required": [ + "status", + "data" + ] + }, + "GetTeamEventTypesOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TeamEventTypeOutput_2024_06_14" + } + } + }, + "required": [ + "status", + "data" + ] + }, + "UpdateTeamEventTypeInput_2024_06_14": { + "type": "object", + "properties": { + "lengthInMinutes": { + "type": "number", + "example": 60 + }, + "title": { + "type": "string", + "example": "Learn the secrets of masterchief!" + }, + "slug": { + "type": "string", + "example": "learn-the-secrets-of-masterchief" + }, + "description": { + "type": "string", + "example": "Discover the culinary wonders of the Argentina by making the best flan ever!" + }, + "locations": { + "type": "array", + "description": "Locations where the event will take place. If not provided, cal video link will be used as the location.", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/AddressLocation_2024_06_14" + }, + { + "$ref": "#/components/schemas/LinkLocation_2024_06_14" + }, + { + "$ref": "#/components/schemas/IntegrationLocation_2024_06_14" + }, + { + "$ref": "#/components/schemas/PhoneLocation_2024_06_14" + } + ] + } + }, + "bookingFields": { + "type": "array", + "description": "Custom fields that can be added to the booking form when the event is booked by someone. By default booking form has name and email field.", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/PhoneFieldInput_2024_06_14" + }, + { + "$ref": "#/components/schemas/AddressFieldInput_2024_06_14" + }, + { + "$ref": "#/components/schemas/TextFieldInput_2024_06_14" + }, + { + "$ref": "#/components/schemas/NumberFieldInput_2024_06_14" + }, + { + "$ref": "#/components/schemas/TextAreaFieldInput_2024_06_14" + }, + { + "$ref": "#/components/schemas/SelectFieldInput_2024_06_14" + }, + { + "$ref": "#/components/schemas/MultiSelectFieldInput_2024_06_14" + }, + { + "$ref": "#/components/schemas/MultiEmailFieldInput_2024_06_14" + }, + { + "$ref": "#/components/schemas/CheckboxGroupFieldInput_2024_06_14" + }, + { + "$ref": "#/components/schemas/RadioGroupFieldInput_2024_06_14" + }, + { + "$ref": "#/components/schemas/BooleanFieldInput_2024_06_14" + } + ] + } + }, + "disableGuests": { + "type": "boolean", + "description": "If true, person booking this event't cant add guests via their emails." + }, + "slotInterval": { + "type": "number", + "description": "Number representing length of each slot when event is booked. By default it equal length of the event type.\n If event length is 60 minutes then we would have slots 9AM, 10AM, 11AM etc. but if it was changed to 30 minutes then\n we would have slots 9AM, 9:30AM, 10AM, 10:30AM etc. as the available times to book the 60 minute event." + }, + "minimumBookingNotice": { + "type": "number", + "description": "Minimum number of minutes before the event that a booking can be made." + }, + "beforeEventBuffer": { + "type": "number", + "description": "Time spaces that can be pre-pended before an event to give more time before it." + }, + "afterEventBuffer": { + "type": "number", + "description": "Time spaces that can be appended after an event to give more time after it." + }, + "scheduleId": { + "type": "number", + "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" + } + ] + }, + "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" + } + ] + }, + "bookingWindow": { + "type": "array", + "description": "Limit how far in the future this event can be booked", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/BusinessDaysWindow_2024_06_14" + }, + { + "$ref": "#/components/schemas/CalendarDaysWindow_2024_06_14" + }, + { + "$ref": "#/components/schemas/RangeWindow_2024_06_14" + } + ] + } + }, + "offsetStart": { + "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" + } + ] + }, + "hosts": { + "type": "array", + "items": { + "type": "string" + } + }, + "assignAllTeamMembers": { + "type": "boolean", + "description": "If true, all current and future team members will be assigned to this event type" + } + }, + "required": [ + "lengthInMinutes", + "title", + "slug", + "description", + "locations", + "bookingFields", + "disableGuests", + "slotInterval", + "minimumBookingNotice", + "beforeEventBuffer", + "afterEventBuffer", + "scheduleId", + "bookingLimitsCount", + "onlyShowFirstAvailableSlot", + "bookingLimitsDuration", + "bookingWindow", + "offsetStart", + "recurrence", + "hosts", + "assignAllTeamMembers" + ] + }, + "UpdateTeamEventTypeOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "oneOf": [ + { + "$ref": "#/components/schemas/TeamEventTypeOutput_2024_06_14" + }, + { + "type": "array", + "items": { + "$ref": "#/components/schemas/TeamEventTypeOutput_2024_06_14" + } + } + ] + } + }, + "required": [ + "status", + "data" + ] + }, + "DeleteTeamEventTypeOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "type": "object" + } + }, + "required": [ + "status", + "data" + ] + }, + "OrgTeamMembershipOutputDto": { + "type": "object", + "properties": { + "role": { + "type": "string", + "enum": [ + "MEMBER", + "OWNER", + "ADMIN" + ] + }, + "id": { + "type": "number" + }, + "userId": { + "type": "number" + }, + "teamId": { + "type": "number" + }, + "accepted": { + "type": "boolean" + }, + "disableImpersonation": { + "type": "boolean" + } + }, + "required": [ + "role", + "id", + "userId", + "teamId", + "accepted" + ] + }, + "OrgTeamMembershipsOutputResponseDto": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/OrgTeamMembershipOutputDto" + } + } + }, + "required": [ + "status", + "data" + ] + }, + "OrgTeamMembershipOutputResponseDto": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/OrgTeamMembershipOutputDto" + } + }, + "required": [ + "status", + "data" + ] + }, + "UpdateOrgTeamMembershipDto": { + "type": "object", + "properties": { + "role": { + "type": "string", + "default": "MEMBER", + "enum": [ + "MEMBER", + "OWNER", + "ADMIN" + ] + }, + "accepted": { + "type": "boolean", + "default": false + }, + "disableImpersonation": { + "type": "boolean", + "default": false + } + } + }, + "CreateOrgTeamMembershipDto": { + "type": "object", + "properties": { + "role": { + "type": "string", + "default": "MEMBER", + "enum": [ + "MEMBER", + "OWNER", + "ADMIN" + ] + }, + "userId": { + "type": "number" + }, + "accepted": { + "type": "boolean", + "default": false + }, + "disableImpersonation": { + "type": "boolean", + "default": false + } + }, + "required": [ + "role", + "userId" + ] + }, + "Attribute": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "The ID of the attribute", + "example": "attr_123" + }, + "teamId": { + "type": "number", + "description": "The team ID associated with the attribute", + "example": 1 + }, + "type": { + "type": "string", + "description": "The type of the attribute", + "enum": [ + "TEXT", + "NUMBER", + "SINGLE_SELECT", + "MULTI_SELECT" + ] + }, + "name": { + "type": "string", + "description": "The name of the attribute", + "example": "Attribute Name" + }, + "slug": { + "type": "string", + "description": "The slug of the attribute", + "example": "attribute-name" + }, + "enabled": { + "type": "boolean", + "description": "Whether the attribute is enabled and displayed on their profile", + "example": true + }, + "usersCanEditRelation": { + "type": "boolean", + "description": "Whether users can edit the relation", + "example": true + } + }, + "required": [ + "id", + "teamId", + "type", + "name", + "slug", + "enabled" + ] + }, + "GetOrganizationAttributesOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Attribute" + } + } + }, + "required": [ + "status", + "data" + ] + }, + "GetSingleAttributeOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/Attribute" + } + ] + } + }, + "required": [ + "status", + "data" + ] + }, + "CreateOrganizationAttributeOptionInput": { + "type": "object", + "properties": { + "value": { + "type": "string" + }, + "slug": { + "type": "string" + } + }, + "required": [ + "value", + "slug" + ] + }, + "CreateOrganizationAttributeInput": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "slug": { + "type": "string" + }, + "type": { + "type": "object" + }, + "options": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CreateOrganizationAttributeOptionInput" + } + }, + "enabled": { + "type": "boolean" + } + }, + "required": [ + "name", + "slug", + "type", + "options" + ] + }, + "CreateOrganizationAttributesOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/Attribute" + } + }, + "required": [ + "status", + "data" + ] + }, + "UpdateOrganizationAttributeInput": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "slug": { + "type": "string" + }, + "type": { + "type": "object" + }, + "enabled": { + "type": "boolean" + } + } + }, + "UpdateOrganizationAttributesOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/Attribute" + } + }, + "required": [ + "status", + "data" + ] + }, + "DeleteOrganizationAttributesOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/Attribute" + } + }, + "required": [ + "status", + "data" + ] + }, + "OptionOutput": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "The ID of the option", + "example": "attr_option_id" + }, + "attributeId": { + "type": "string", + "description": "The ID of the attribute", + "example": "attr_id" + }, + "value": { + "type": "string", + "description": "The value of the option", + "example": "option_value" + }, + "slug": { + "type": "string", + "description": "The slug of the option", + "example": "option-slug" + } + }, + "required": [ + "id", + "attributeId", + "value", + "slug" + ] + }, + "CreateAttributeOptionOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/OptionOutput" + } + }, + "required": [ + "status", + "data" + ] + }, + "DeleteAttributeOptionOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/OptionOutput" + } + }, + "required": [ + "status", + "data" + ] + }, + "UpdateOrganizationAttributeOptionInput": { + "type": "object", + "properties": { + "value": { + "type": "string" + }, + "slug": { + "type": "string" + } + } + }, + "UpdateAttributeOptionOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/OptionOutput" + } + }, + "required": [ + "status", + "data" + ] + }, + "GetAllAttributeOptionOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/OptionOutput" + } + } + }, + "required": [ + "status", + "data" + ] + }, + "AssignOrganizationAttributeOptionToUserInput": { + "type": "object", + "properties": { + "value": { + "type": "string" + }, + "attributeOptionId": { + "type": "string" + }, + "attributeId": { + "type": "string" + } + }, + "required": [ + "attributeId" + ] + }, + "AssignOptionUserOutputData": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "The ID of the option assigned to the user" + }, + "memberId": { + "type": "number", + "description": "The ID form the org membership for the user" + }, + "attributeOptionId": { + "type": "string", + "description": "The value of the option" + } + }, + "required": [ + "id", + "memberId", + "attributeOptionId" + ] + }, + "AssignOptionUserOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/AssignOptionUserOutputData" + } + }, + "required": [ + "status", + "data" + ] + }, + "UnassignOptionUserOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/AssignOptionUserOutputData" + } + }, + "required": [ + "status", + "data" + ] + }, + "GetOptionUserOutputData": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "The ID of the option assigned to the user" + }, + "attributeId": { + "type": "string", + "description": "The ID of the attribute" + }, + "value": { + "type": "string", + "description": "The value of the option" + }, + "slug": { + "type": "string", + "description": "The slug of the option" + } + }, + "required": [ + "id", + "attributeId", + "value", + "slug" + ] + }, + "GetOptionUserOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/GetOptionUserOutputData" + } + } + }, + "required": [ + "status", + "data" + ] + }, + "TeamWebhookOutputDto": { + "type": "object", + "properties": { + "payloadTemplate": { + "type": "string", + "description": "The template of the payload that will be sent to the subscriberUrl, check cal.com/docs/core-features/webhooks for more information", + "example": "{\"content\":\"A new event has been scheduled\",\"type\":\"{{type}}\",\"name\":\"{{title}}\",\"organizer\":\"{{organizer.name}}\",\"booker\":\"{{attendees.0.name}}\"}" + }, + "teamId": { + "type": "number" + }, + "id": { + "type": "number" + }, + "triggers": { + "type": "array", + "items": { + "type": "object" + } + }, + "subscriberUrl": { + "type": "string" + }, + "active": { + "type": "boolean" + }, + "secret": { + "type": "string" + } + }, + "required": [ + "payloadTemplate", + "teamId", + "id", + "triggers", + "subscriberUrl", + "active" + ] + }, + "TeamWebhooksOutputResponseDto": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TeamWebhookOutputDto" + } + } + }, + "required": [ + "status", + "data" + ] + }, + "CreateWebhookInputDto": { + "type": "object", + "properties": { + "payloadTemplate": { + "type": "string", + "description": "The template of the payload that will be sent to the subscriberUrl, check cal.com/docs/core-features/webhooks for more information", + "example": "{\"content\":\"A new event has been scheduled\",\"type\":\"{{type}}\",\"name\":\"{{title}}\",\"organizer\":\"{{organizer.name}}\",\"booker\":\"{{attendees.0.name}}\"}" + }, + "triggers": { + "type": "string", + "example": [ + "BOOKING_CREATED", + "BOOKING_RESCHEDULED", + "BOOKING_CANCELLED", + "BOOKING_CONFIRMED", + "BOOKING_REJECTED", + "BOOKING_COMPLETED", + "BOOKING_NO_SHOW", + "BOOKING_REOPENED" + ], + "enum": [ + "BOOKING_CREATED", + "BOOKING_PAYMENT_INITIATED", + "BOOKING_PAID", + "BOOKING_RESCHEDULED", + "BOOKING_REQUESTED", + "BOOKING_CANCELLED", + "BOOKING_REJECTED", + "BOOKING_NO_SHOW_UPDATED", + "FORM_SUBMITTED", + "MEETING_ENDED", + "MEETING_STARTED", + "RECORDING_READY", + "INSTANT_MEETING", + "RECORDING_TRANSCRIPTION_GENERATED", + "OOO_CREATED" + ] + }, + "active": { + "type": "boolean" + }, + "subscriberUrl": { + "type": "string" + }, + "secret": { + "type": "string" + } + }, + "required": [ + "triggers", + "active", + "subscriberUrl" + ] + }, + "TeamWebhookOutputResponseDto": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/TeamWebhookOutputDto" + } + }, + "required": [ + "status", + "data" + ] + }, + "UpdateWebhookInputDto": { + "type": "object", + "properties": { + "payloadTemplate": { + "type": "string", + "description": "The template of the payload that will be sent to the subscriberUrl, check cal.com/docs/core-features/webhooks for more information", + "example": "{\"content\":\"A new event has been scheduled\",\"type\":\"{{type}}\",\"name\":\"{{title}}\",\"organizer\":\"{{organizer.name}}\",\"booker\":\"{{attendees.0.name}}\"}" + }, + "triggers": { + "type": "string", + "example": [ + "BOOKING_CREATED", + "BOOKING_RESCHEDULED", + "BOOKING_CANCELLED", + "BOOKING_CONFIRMED", + "BOOKING_REJECTED", + "BOOKING_COMPLETED", + "BOOKING_NO_SHOW", + "BOOKING_REOPENED" + ], + "enum": [ + "BOOKING_CREATED", + "BOOKING_PAYMENT_INITIATED", + "BOOKING_PAID", + "BOOKING_RESCHEDULED", + "BOOKING_REQUESTED", + "BOOKING_CANCELLED", + "BOOKING_REJECTED", + "BOOKING_NO_SHOW_UPDATED", + "FORM_SUBMITTED", + "MEETING_ENDED", + "MEETING_STARTED", + "RECORDING_READY", + "INSTANT_MEETING", + "RECORDING_TRANSCRIPTION_GENERATED", + "OOO_CREATED" + ] + }, + "active": { + "type": "boolean" + }, + "subscriberUrl": { + "type": "string" + }, + "secret": { + "type": "string" + } + } + }, + "GetDefaultScheduleOutput_2024_06_11": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/ScheduleOutput_2024_06_11" + } + }, + "required": [ + "status", + "data" + ] + }, + "AuthUrlData": { + "type": "object", + "properties": { + "authUrl": { + "type": "string" + } + }, + "required": [ + "authUrl" + ] + }, + "GcalAuthUrlOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/AuthUrlData" + } + }, + "required": [ + "status", + "data" + ] + }, + "GcalSaveRedirectOutput": { + "type": "object", + "properties": { + "url": { + "type": "string" + } + }, + "required": [ + "url" + ] + }, + "GcalCheckOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + } + }, + "required": [ + "status" + ] + }, + "ProviderVerifyClientData": { + "type": "object", + "properties": { + "clientId": { + "type": "string" + }, + "organizationId": { + "type": "number" + }, + "name": { + "type": "string" + } + }, + "required": [ + "clientId", + "organizationId", + "name" + ] + }, + "ProviderVerifyClientOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/ProviderVerifyClientData" + } + }, + "required": [ + "status", + "data" + ] + }, + "ProviderVerifyAccessTokenOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + } + }, + "required": [ + "status" + ] + }, + "MeOrgOutput": { + "type": "object", + "properties": { + "isPlatform": { + "type": "boolean" + }, + "id": { + "type": "number" + } + }, + "required": [ + "isPlatform", + "id" + ] + }, + "MeOutput": { + "type": "object", + "properties": { + "id": { + "type": "number" + }, + "username": { + "type": "string" + }, + "email": { + "type": "string" + }, + "timeFormat": { + "type": "number" + }, + "defaultScheduleId": { + "type": "number", + "nullable": true + }, + "weekStart": { + "type": "string" + }, + "timeZone": { + "type": "string" + }, + "organizationId": { + "type": "number", + "nullable": true + }, + "organization": { + "$ref": "#/components/schemas/MeOrgOutput" + } + }, + "required": [ + "id", + "username", + "email", + "timeFormat", + "defaultScheduleId", + "weekStart", + "timeZone", + "organizationId" + ] + }, + "GetMeOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/MeOutput" + } + }, + "required": [ + "status", + "data" + ] + }, + "UpdateMeOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/MeOutput" + } + }, + "required": [ + "status", + "data" + ] + }, + "BusyTimesOutput": { + "type": "object", + "properties": { + "start": { + "format": "date-time", + "type": "string" + }, + "end": { + "format": "date-time", + "type": "string" + }, + "source": { + "type": "string", + "nullable": true + } + }, + "required": [ + "start", + "end" + ] + }, + "GetBusyTimesOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BusyTimesOutput" + } + } + }, + "required": [ + "status", + "data" + ] + }, + "Integration": { + "type": "object", + "properties": { + "appData": { + "type": "object", + "nullable": true + }, + "dirName": { + "type": "string" + }, + "__template": { + "type": "string" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "installed": { + "type": "boolean" + }, + "type": { + "type": "string" + }, + "title": { + "type": "string" + }, + "variant": { + "type": "string" + }, + "category": { + "type": "string" + }, + "categories": { + "type": "array", + "items": { + "type": "string" + } + }, + "logo": { + "type": "string" + }, + "publisher": { + "type": "string" + }, + "slug": { + "type": "string" + }, + "url": { + "type": "string" + }, + "email": { + "type": "string" + }, + "locationOption": { + "type": "object", + "nullable": true + } + }, + "required": [ + "name", + "description", + "type", + "variant", + "categories", + "logo", + "publisher", + "slug", + "url", + "email", + "locationOption" + ] + }, + "Primary": { + "type": "object", + "properties": { + "externalId": { + "type": "string" + }, + "integration": { + "type": "string" + }, + "name": { + "type": "string" + }, + "primary": { + "type": "boolean", + "nullable": true + }, + "readOnly": { + "type": "boolean" + }, + "email": { + "type": "string" + }, + "isSelected": { + "type": "boolean" + }, + "credentialId": { + "type": "number" + } + }, + "required": [ + "externalId", + "primary", + "readOnly", + "isSelected", + "credentialId" + ] + }, + "Calendar": { + "type": "object", + "properties": { + "externalId": { + "type": "string" + }, + "integration": { + "type": "string" + }, + "name": { + "type": "string" + }, + "primary": { + "type": "boolean", + "nullable": true + }, + "readOnly": { + "type": "boolean" + }, + "email": { + "type": "string" + }, + "isSelected": { + "type": "boolean" + }, + "credentialId": { + "type": "number" + } + }, + "required": [ + "externalId", + "readOnly", + "isSelected", + "credentialId" + ] + }, + "ConnectedCalendar": { + "type": "object", + "properties": { + "integration": { + "$ref": "#/components/schemas/Integration" + }, + "credentialId": { + "type": "number" + }, + "primary": { + "$ref": "#/components/schemas/Primary" + }, + "calendars": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Calendar" + } + } + }, + "required": [ + "integration", + "credentialId" + ] + }, + "DestinationCalendar": { + "type": "object", + "properties": { + "id": { + "type": "number" + }, + "integration": { + "type": "string" + }, + "externalId": { + "type": "string" + }, + "primaryEmail": { + "type": "string", + "nullable": true + }, + "userId": { + "type": "number", + "nullable": true + }, + "eventTypeId": { + "type": "number", + "nullable": true + }, + "credentialId": { + "type": "number", + "nullable": true + }, + "name": { + "type": "string", + "nullable": true + }, + "primary": { + "type": "boolean" + }, + "readOnly": { + "type": "boolean" + }, + "email": { + "type": "string" + }, + "integrationTitle": { + "type": "string" + } + }, + "required": [ + "id", + "integration", + "externalId", + "primaryEmail", + "userId", + "eventTypeId", + "credentialId" + ] + }, + "ConnectedCalendarsData": { + "type": "object", + "properties": { + "connectedCalendars": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ConnectedCalendar" + } + }, + "destinationCalendar": { + "$ref": "#/components/schemas/DestinationCalendar" + } + }, + "required": [ + "connectedCalendars", + "destinationCalendar" + ] + }, + "ConnectedCalendarsOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/ConnectedCalendarsData" + } + }, + "required": [ + "status", + "data" + ] + }, + "DeleteCalendarCredentialsInputBodyDto": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "example": 10, + "description": "Credential ID of the calendar to delete, as returned by the /calendars endpoint" + } + }, + "required": [ + "id" + ] + }, + "DeletedCalendarCredentialsOutputDto": { + "type": "object", + "properties": { + "id": { + "type": "number" + }, + "type": { + "type": "string" + }, + "userId": { + "type": "number", + "nullable": true + }, + "teamId": { + "type": "number", + "nullable": true + }, + "appId": { + "type": "string", + "nullable": true + }, + "invalid": { + "type": "boolean", + "nullable": true + } + }, + "required": [ + "id", + "type", + "userId", + "teamId", + "appId", + "invalid" + ] + }, + "DeletedCalendarCredentialsOutputResponseDto": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/DeletedCalendarCredentialsOutputDto" + } + }, + "required": [ + "status", + "data" + ] + }, + "Attendee": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "The name of the attendee.", + "example": "John Doe" + }, + "email": { + "type": "string", + "description": "The email of the attendee.", + "example": "john.doe@example.com" + }, + "timeZone": { + "type": "string", + "description": "The time zone of the attendee.", + "example": "America/New_York" + }, + "language": { + "type": "string", + "enum": [ + "ar", + "ca", + "de", + "es", + "eu", + "he", + "id", + "ja", + "lv", + "pl", + "ro", + "sr", + "th", + "vi", + "az", + "cs", + "el", + "es-419", + "fi", + "hr", + "it", + "km", + "nl", + "pt", + "ru", + "sv", + "tr", + "zh-CN", + "bg", + "da", + "en", + "et", + "fr", + "hu", + "iw", + "ko", + "no", + "pt-BR", + "sk", + "ta", + "uk", + "zh-TW" + ], + "description": "The preferred language of the attendee. Used for booking confirmation.", + "example": "it", + "default": "en" + } + }, + "required": [ + "name", + "email", + "timeZone" + ] + }, + "CreateBookingInput_2024_08_13": { + "type": "object", + "properties": { + "start": { + "type": "string", + "description": "The start time of the booking in ISO 8601 format in UTC timezone.", + "example": "2024-08-13T09:00:00Z" + }, + "eventTypeId": { + "type": "number", + "description": "The ID of the event type that is booked.", + "example": 123 + }, + "attendee": { + "description": "The attendee's details.", + "allOf": [ + { + "$ref": "#/components/schemas/Attendee" + } + ] + }, + "guests": { + "description": "An optional list of guest emails attending the event.", + "example": [ + "guest1@example.com", + "guest2@example.com" + ], + "type": "array", + "items": { + "type": "string" + } + }, + "meetingUrl": { + "type": "string", + "description": "Meeting URL just for this booking. Displayed in email and calendar event. If not provided then cal video link will be generated.", + "example": "https://example.com/meeting" + }, + "bookingFieldsResponses": { + "type": "object", + "description": "Booking field responses.", + "example": { + "customField": "customValue" + } + } + }, + "required": [ + "start", + "eventTypeId", + "attendee" + ] + }, + "CreateInstantBookingInput_2024_08_13": { + "type": "object", + "properties": { + "start": { + "type": "string", + "description": "The start time of the booking in ISO 8601 format in UTC timezone.", + "example": "2024-08-13T09:00:00Z" + }, + "eventTypeId": { + "type": "number", + "description": "The ID of the event type that is booked.", + "example": 123 + }, + "attendee": { + "description": "The attendee's details.", + "allOf": [ + { + "$ref": "#/components/schemas/Attendee" + } + ] + }, + "guests": { + "description": "An optional list of guest emails attending the event.", + "example": [ + "guest1@example.com", + "guest2@example.com" + ], + "type": "array", + "items": { + "type": "string" + } + }, + "meetingUrl": { + "type": "string", + "description": "Meeting URL just for this booking. Displayed in email and calendar event. If not provided then cal video link will be generated.", + "example": "https://example.com/meeting" + }, + "bookingFieldsResponses": { + "type": "object", + "description": "Booking field responses.", + "example": { + "customField": "customValue" + } + }, + "instant": { + "type": "boolean", + "description": "Flag indicating if the booking is an instant booking. Only available for team events.", + "example": true + } + }, + "required": [ + "start", + "eventTypeId", + "attendee", + "instant" + ] + }, + "CreateRecurringBookingInput_2024_08_13": { + "type": "object", + "properties": { + "start": { + "type": "string", + "description": "The start time of the booking in ISO 8601 format in UTC timezone.", + "example": "2024-08-13T09:00:00Z" + }, + "eventTypeId": { + "type": "number", + "description": "The ID of the event type that is booked.", + "example": 123 + }, + "attendee": { + "description": "The attendee's details.", + "allOf": [ + { + "$ref": "#/components/schemas/Attendee" + } + ] + }, + "guests": { + "description": "An optional list of guest emails attending the event.", + "example": [ + "guest1@example.com", + "guest2@example.com" + ], + "type": "array", + "items": { + "type": "string" + } + }, + "meetingUrl": { + "type": "string", + "description": "Meeting URL just for this booking. Displayed in email and calendar event. If not provided then cal video link will be generated.", + "example": "https://example.com/meeting" + }, + "bookingFieldsResponses": { + "type": "object", + "description": "Booking field responses.", + "example": { + "customField": "customValue" + } + } + }, + "required": [ + "start", + "eventTypeId", + "attendee" + ] + }, + "Host": { + "type": "object", + "properties": { + "id": { + "type": "number", + "example": 1 + }, + "name": { + "type": "string", + "example": "Jane Doe" + }, + "timeZone": { + "type": "string", + "example": "America/Los_Angeles" + } + }, + "required": [ + "id", + "name", + "timeZone" + ] + }, + "BookingOutput_2024_08_13": { + "type": "object", + "properties": { + "id": { + "type": "number", + "example": 123 + }, + "uid": { + "type": "string", + "example": "booking_uid_123" + }, + "hosts": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Host" + } + }, + "status": { + "type": "string", + "enum": [ + "cancelled", + "accepted", + "rejected", + "pending", + "rescheduled" + ], + "example": "accepted" + }, + "cancellationReason": { + "type": "string", + "example": "User requested cancellation" + }, + "reschedulingReason": { + "type": "string", + "example": "User rescheduled the event" + }, + "rescheduledFromUid": { + "type": "string", + "example": "previous_uid_123" + }, + "start": { + "type": "string", + "example": "2024-08-13T15:30:00Z" + }, + "end": { + "type": "string", + "example": "2024-08-13T16:30:00Z" + }, + "duration": { + "type": "number", + "example": 60 + }, + "eventTypeId": { + "type": "number", + "example": 45 + }, + "attendees": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Attendee" + } + }, + "guests": { + "example": [ + "guest1@example.com", + "guest2@example.com" + ], + "type": "array", + "items": { + "type": "string" + } + }, + "meetingUrl": { + "type": "string", + "example": "https://example.com/meeting" + }, + "absentHost": { + "type": "boolean", + "example": true + } + }, + "required": [ + "id", + "uid", + "hosts", + "status", + "start", + "end", + "duration", + "eventTypeId", + "attendees", + "absentHost" + ] + }, + "RecurringBookingOutput_2024_08_13": { + "type": "object", + "properties": { + "id": { + "type": "number", + "example": 456 + }, + "uid": { + "type": "string", + "example": "recurring_uid_123" + }, + "hosts": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Host" + } + }, + "status": { + "type": "string", + "enum": [ + "cancelled", + "accepted", + "rejected", + "pending" + ], + "example": "pending" + }, + "cancellationReason": { + "type": "string", + "example": "Event was cancelled" + }, + "reschedulingReason": { + "type": "string", + "example": "Event was rescheduled" + }, + "rescheduledFromUid": { + "type": "string", + "example": "previous_recurring_uid_123" + }, + "start": { + "type": "string", + "example": "2024-08-13T15:30:00Z" + }, + "end": { + "type": "string", + "example": "2024-08-13T16:30:00Z" + }, + "duration": { + "type": "number", + "example": 30 + }, + "eventTypeId": { + "type": "number", + "example": 50 + }, + "recurringBookingUid": { + "type": "string", + "example": "recurring_uid_987" + }, + "attendees": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Attendee" + } + }, + "guests": { + "example": [ + "guest3@example.com", + "guest4@example.com" + ], + "type": "array", + "items": { + "type": "string" + } + }, + "meetingUrl": { + "type": "string", + "example": "https://example.com/recurring-meeting" + }, + "absentHost": { + "type": "boolean", + "example": false + } + }, + "required": [ + "id", + "uid", + "hosts", + "status", + "start", + "end", + "duration", + "eventTypeId", + "recurringBookingUid", + "attendees", + "absentHost" + ] + }, + "CreateBookingOutput_2024_08_13": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "oneOf": [ + { + "$ref": "#/components/schemas/BookingOutput_2024_08_13" + }, + { + "type": "array", + "items": { + "$ref": "#/components/schemas/RecurringBookingOutput_2024_08_13" + } + } + ], + "description": "Booking data, which can be either a BookingOutput object or an array of RecurringBookingOutput objects" + } + }, + "required": [ + "status", + "data" + ] + }, + "GetBookingOutput_2024_08_13": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "oneOf": [ + { + "$ref": "#/components/schemas/BookingOutput_2024_08_13" + }, + { + "$ref": "#/components/schemas/RecurringBookingOutput_2024_08_13" + }, + { + "type": "array", + "items": { + "$ref": "#/components/schemas/RecurringBookingOutput_2024_08_13" + } + } + ], + "description": "Booking data, which can be either a BookingOutput object, a RecurringBookingOutput object, or an array of RecurringBookingOutput objects" + } + }, + "required": [ + "status", + "data" + ] + }, + "GetBookingsOutput_2024_08_13": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/BookingOutput_2024_08_13" + }, + { + "$ref": "#/components/schemas/RecurringBookingOutput_2024_08_13" + } + ] + }, + "description": "Array of booking data, which can contain either BookingOutput objects or RecurringBookingOutput objects" + } + }, + "required": [ + "status", + "data" + ] + }, + "RescheduleBookingInput_2024_08_13": { + "type": "object", + "properties": { + "start": { + "type": "string", + "description": "Start time in ISO 8601 format for the new booking", + "example": "2024-08-13T10:00:00Z" + }, + "reschedulingReason": { + "type": "string", + "example": "User requested reschedule", + "description": "Reason for rescheduling the booking" + } + }, + "required": [ + "start" + ] + }, + "RescheduleBookingOutput_2024_08_13": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "oneOf": [ + { + "$ref": "#/components/schemas/BookingOutput_2024_08_13" + }, + { + "$ref": "#/components/schemas/RecurringBookingOutput_2024_08_13" + } + ], + "description": "Booking data, which can be either a BookingOutput object or a RecurringBookingOutput object" + } + }, + "required": [ + "status", + "data" + ] + }, + "CancelBookingInput_2024_08_13": { + "type": "object", + "properties": { + "cancellationReason": { + "type": "string", + "example": "User requested cancellation" + } + }, + "required": [ + "cancellationReason" + ] + }, + "CancelBookingOutput_2024_08_13": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "oneOf": [ + { + "$ref": "#/components/schemas/BookingOutput_2024_08_13" + }, + { + "$ref": "#/components/schemas/RecurringBookingOutput_2024_08_13" + }, + { + "type": "array", + "items": { + "$ref": "#/components/schemas/RecurringBookingOutput_2024_08_13" + } + } + ], + "description": "Booking data, which can be either a BookingOutput object, a RecurringBookingOutput object, or an array of RecurringBookingOutput objects" + } + }, + "required": [ + "status", + "data" + ] + }, + "MarkAbsentBookingInput_2024_08_13": { + "type": "object", + "properties": { + "host": { + "type": "boolean", + "example": false, + "description": "Whether the host was absent" + }, + "attendees": { + "description": "Toggle whether an attendee was absent or not.", + "example": [ + { + "absent": true, + "email": "someone@gmail.com" + } + ], + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "attendees" + ] + }, + "MarkAbsentBookingOutput_2024_08_13": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "oneOf": [ + { + "$ref": "#/components/schemas/BookingOutput_2024_08_13" + }, + { + "$ref": "#/components/schemas/RecurringBookingOutput_2024_08_13" + } + ], + "description": "Booking data, which can be either a BookingOutput object or a RecurringBookingOutput object" + } + }, + "required": [ + "status", + "data" + ] + }, + "ReserveSlotInput": { + "type": "object", + "properties": {} + }, + "UserWebhookOutputDto": { + "type": "object", + "properties": { + "payloadTemplate": { + "type": "string", + "description": "The template of the payload that will be sent to the subscriberUrl, check cal.com/docs/core-features/webhooks for more information", + "example": "{\"content\":\"A new event has been scheduled\",\"type\":\"{{type}}\",\"name\":\"{{title}}\",\"organizer\":\"{{organizer.name}}\",\"booker\":\"{{attendees.0.name}}\"}" + }, + "userId": { + "type": "number" + }, + "id": { + "type": "number" + }, + "triggers": { + "type": "array", + "items": { + "type": "object" + } + }, + "subscriberUrl": { + "type": "string" + }, + "active": { + "type": "boolean" + }, + "secret": { + "type": "string" + } + }, + "required": [ + "payloadTemplate", + "userId", + "id", + "triggers", + "subscriberUrl", + "active" + ] + }, + "UserWebhookOutputResponseDto": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/UserWebhookOutputDto" + } + }, + "required": [ + "status", + "data" + ] + }, + "UserWebhooksOutputResponseDto": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UserWebhookOutputDto" + } + } + }, + "required": [ + "status", + "data" + ] + }, + "EventTypeWebhookOutputDto": { + "type": "object", + "properties": { + "payloadTemplate": { + "type": "string", + "description": "The template of the payload that will be sent to the subscriberUrl, check cal.com/docs/core-features/webhooks for more information", + "example": "{\"content\":\"A new event has been scheduled\",\"type\":\"{{type}}\",\"name\":\"{{title}}\",\"organizer\":\"{{organizer.name}}\",\"booker\":\"{{attendees.0.name}}\"}" + }, + "eventTypeId": { + "type": "number" + }, + "id": { + "type": "number" + }, + "triggers": { + "type": "array", + "items": { + "type": "object" + } + }, + "subscriberUrl": { + "type": "string" + }, + "active": { + "type": "boolean" + }, + "secret": { + "type": "string" + } + }, + "required": [ + "payloadTemplate", + "eventTypeId", + "id", + "triggers", + "subscriberUrl", + "active" + ] + }, + "EventTypeWebhookOutputResponseDto": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/EventTypeWebhookOutputDto" + } + }, + "required": [ + "status", + "data" + ] + }, + "EventTypeWebhooksOutputResponseDto": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/EventTypeWebhookOutputDto" + } + } + }, + "required": [ + "status", + "data" + ] + }, + "DeleteManyWebhooksOutputResponseDto": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "type": "string" + } + }, + "required": [ + "status", + "data" + ] + }, + "OAuthClientWebhookOutputDto": { + "type": "object", + "properties": { + "payloadTemplate": { + "type": "string", + "description": "The template of the payload that will be sent to the subscriberUrl, check cal.com/docs/core-features/webhooks for more information", + "example": "{\"content\":\"A new event has been scheduled\",\"type\":\"{{type}}\",\"name\":\"{{title}}\",\"organizer\":\"{{organizer.name}}\",\"booker\":\"{{attendees.0.name}}\"}" + }, + "oAuthClientId": { + "type": "string" + }, + "id": { + "type": "number" + }, + "triggers": { + "type": "array", + "items": { + "type": "object" + } + }, + "subscriberUrl": { + "type": "string" + }, + "active": { + "type": "boolean" + }, + "secret": { + "type": "string" + } + }, + "required": [ + "payloadTemplate", + "oAuthClientId", + "id", + "triggers", + "subscriberUrl", + "active" + ] + }, + "OAuthClientWebhookOutputResponseDto": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/OAuthClientWebhookOutputDto" + } + }, + "required": [ + "status", + "data" + ] + }, + "OAuthClientWebhooksOutputResponseDto": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/OAuthClientWebhookOutputDto" + } + } + }, + "required": [ + "status", + "data" + ] + }, + "DestinationCalendarsInputBodyDto": { + "type": "object", + "properties": { + "integration": { + "type": "string", + "example": "apple_calendar", + "description": "The calendar service you want to integrate, as returned by the /calendars endpoint", + "enum": [ + "apple_calendar", + "google_calendar", + "office365_calendar" + ] + }, + "externalId": { + "type": "string", + "example": "https://caldav.icloud.com/26962146906/calendars/1644422A-1945-4438-BBC0-4F0Q23A57R7S/", + "description": "Unique identifier used to represent the specfic calendar, as returned by the /calendars endpoint" + } + }, + "required": [ + "integration", + "externalId" + ] + }, + "DestinationCalendarsOutputDto": { + "type": "object", + "properties": { + "userId": { + "type": "number" + }, + "integration": { + "type": "string" + }, + "externalId": { + "type": "string" + }, + "credentialId": { + "type": "number", + "nullable": true + } + }, + "required": [ + "userId", + "integration", + "externalId", + "credentialId" + ] + }, + "DestinationCalendarsOutputResponseDto": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/DestinationCalendarsOutputDto" + } + }, + "required": [ + "status", + "data" + ] + } + } + } +} \ No newline at end of file From 68d4dbf2c29ca62c9179cf3e5e94fb940e2d50d0 Mon Sep 17 00:00:00 2001 From: supalarry Date: Thu, 19 Sep 2024 17:23:37 +0200 Subject: [PATCH 85/96] ci --- .github/workflows/e2e-api-v2.yml | 3 +-- apps/api/v2/package.json | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/e2e-api-v2.yml b/.github/workflows/e2e-api-v2.yml index 7b678fdb52094f..7d18d291c90736 100644 --- a/.github/workflows/e2e-api-v2.yml +++ b/.github/workflows/e2e-api-v2.yml @@ -15,7 +15,7 @@ env: IS_E2E: true NEXTAUTH_SECRET: ${{ secrets.CI_NEXTAUTH_SECRET }} NEXTAUTH_URL: ${{ secrets.CI_NEXTAUTH_URL }} - NODE_OPTIONS: --max-old-space-size=8192 + NODE_OPTIONS: --max-old-space-size=16384 REDIS_URL: "redis://localhost:6379" STRIPE_PRIVATE_KEY: ${{ secrets.CI_STRIPE_PRIVATE_KEY }} STRIPE_API_KEY: ${{ secrets.CI_STRIPE_PRIVATE_KEY }} @@ -70,7 +70,6 @@ jobs: - name: Run Tests working-directory: apps/api/v2 run: | - export NODE_OPTIONS=--max-old-space-size=8192 yarn test:e2e EXIT_CODE=$? echo "yarn test:e2e command exit code: $EXIT_CODE" diff --git a/apps/api/v2/package.json b/apps/api/v2/package.json index df9376a9b34af9..6fabecce20cadd 100644 --- a/apps/api/v2/package.json +++ b/apps/api/v2/package.json @@ -19,7 +19,7 @@ "test:watch": "yarn dev:build && jest --watch", "test:cov": "yarn dev:build && jest --coverage", "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", - "test:e2e": "yarn dev:build && jest --runInBand --detectOpenHandles --forceExit --config ./jest-e2e.json", + "test:e2e": "yarn dev:build && jest --detectOpenHandles --forceExit --config ./jest-e2e.json", "test:e2e:watch": "yarn dev:build && jest --runInBand --detectOpenHandles --forceExit --config ./jest-e2e.json --watch", "prisma": "yarn workspace @calcom/prisma prisma", "generate-schemas": "yarn prisma generate && yarn prisma format", From f0603ed770b9167348fd4b920a6da84b3ccd199a Mon Sep 17 00:00:00 2001 From: supalarry Date: Fri, 20 Sep 2024 09:14:16 +0200 Subject: [PATCH 86/96] ci --- .github/workflows/e2e-api-v2.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/e2e-api-v2.yml b/.github/workflows/e2e-api-v2.yml index 7d18d291c90736..dcd7c483228498 100644 --- a/.github/workflows/e2e-api-v2.yml +++ b/.github/workflows/e2e-api-v2.yml @@ -15,7 +15,7 @@ env: IS_E2E: true NEXTAUTH_SECRET: ${{ secrets.CI_NEXTAUTH_SECRET }} NEXTAUTH_URL: ${{ secrets.CI_NEXTAUTH_URL }} - NODE_OPTIONS: --max-old-space-size=16384 + NODE_OPTIONS: --max-old-space-size=29000 REDIS_URL: "redis://localhost:6379" STRIPE_PRIVATE_KEY: ${{ secrets.CI_STRIPE_PRIVATE_KEY }} STRIPE_API_KEY: ${{ secrets.CI_STRIPE_PRIVATE_KEY }} From fb224f0be4690bb7345e5d61cdbba19a8f768053 Mon Sep 17 00:00:00 2001 From: supalarry Date: Fri, 20 Sep 2024 11:43:00 +0200 Subject: [PATCH 87/96] refactor: increase idle worker memory jest e2e --- apps/api/v2/jest-e2e.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/api/v2/jest-e2e.json b/apps/api/v2/jest-e2e.json index a0c04f1046cf49..3e868125c9ecfb 100644 --- a/apps/api/v2/jest-e2e.json +++ b/apps/api/v2/jest-e2e.json @@ -11,5 +11,6 @@ "^.+\\.(t|j)s$": "ts-jest" }, "setupFiles": ["/test/setEnvVars.ts"], - "reporters": ["default", "jest-summarizing-reporter"] + "reporters": ["default", "jest-summarizing-reporter"], + "workerIdleMemoryLimit": "512MB" } From f53b1b31ceda728fa8e8fe776daccd3b19343eee Mon Sep 17 00:00:00 2001 From: Morgan Vernay Date: Fri, 20 Sep 2024 13:25:29 +0300 Subject: [PATCH 88/96] fixup! refactor: increase idle worker memory jest e2e --- apps/api/v2/jest-e2e.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/api/v2/jest-e2e.json b/apps/api/v2/jest-e2e.json index 3e868125c9ecfb..6ca2727a5babe2 100644 --- a/apps/api/v2/jest-e2e.json +++ b/apps/api/v2/jest-e2e.json @@ -12,5 +12,7 @@ }, "setupFiles": ["/test/setEnvVars.ts"], "reporters": ["default", "jest-summarizing-reporter"], - "workerIdleMemoryLimit": "512MB" + "workerIdleMemoryLimit": "512MB", + "isolateModules": true, + "maxWorkers": 1 } From 5f4cb45d32c1aee17dfc02a134b5b2c5f165aa67 Mon Sep 17 00:00:00 2001 From: Morgan Vernay Date: Fri, 20 Sep 2024 13:41:50 +0300 Subject: [PATCH 89/96] fixup! fixup! refactor: increase idle worker memory jest e2e --- apps/api/v2/jest-e2e.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/api/v2/jest-e2e.json b/apps/api/v2/jest-e2e.json index 6ca2727a5babe2..072cd92d6015ce 100644 --- a/apps/api/v2/jest-e2e.json +++ b/apps/api/v2/jest-e2e.json @@ -13,6 +13,6 @@ "setupFiles": ["/test/setEnvVars.ts"], "reporters": ["default", "jest-summarizing-reporter"], "workerIdleMemoryLimit": "512MB", - "isolateModules": true, + "resetModules": true, "maxWorkers": 1 } From d28b0ce0a6da49a7af827065a0cb646668404e64 Mon Sep 17 00:00:00 2001 From: supalarry Date: Fri, 20 Sep 2024 13:15:43 +0200 Subject: [PATCH 90/96] refactor: split bookings e2e into smaller e2e files --- .../api-key-bookings.controller.e2e-spec.ts | 160 ++++++ .../team-bookings.controller.e2e-spec.ts | 435 ++++++++++++++ ...s => user-bookings.controller.e2e-spec.ts} | 539 ------------------ 3 files changed, 595 insertions(+), 539 deletions(-) create mode 100644 apps/api/v2/src/ee/bookings/2024-08-13/controllers/api-key-bookings.controller.e2e-spec.ts create mode 100644 apps/api/v2/src/ee/bookings/2024-08-13/controllers/team-bookings.controller.e2e-spec.ts rename apps/api/v2/src/ee/bookings/2024-08-13/controllers/{bookings.controller.e2e-spec.ts => user-bookings.controller.e2e-spec.ts} (71%) diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/api-key-bookings.controller.e2e-spec.ts b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/api-key-bookings.controller.e2e-spec.ts new file mode 100644 index 00000000000000..f28af04e4e63eb --- /dev/null +++ b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/api-key-bookings.controller.e2e-spec.ts @@ -0,0 +1,160 @@ +import { bootstrap } from "@/app"; +import { AppModule } from "@/app.module"; +import { CreateBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/create-booking.output"; +import { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input"; +import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module"; +import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service"; +import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard"; +import { PrismaModule } from "@/modules/prisma/prisma.module"; +import { UsersModule } from "@/modules/users/users.module"; +import { INestApplication } from "@nestjs/common"; +import { NestExpressApplication } from "@nestjs/platform-express"; +import { Test } from "@nestjs/testing"; +import { User } from "@prisma/client"; +import * as request from "supertest"; +import { ApiKeysRepositoryFixture } from "test/fixtures/repository/api-keys.repository.fixture"; +import { BookingsRepositoryFixture } from "test/fixtures/repository/bookings.repository.fixture"; +import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture"; +import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture"; +import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture"; +import { withApiAuth } from "test/utils/withApiAuth"; + +import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants"; +import { CreateBookingInput_2024_08_13, BookingOutput_2024_08_13 } from "@calcom/platform-types"; +import { Team } from "@calcom/prisma/client"; + +describe("Bookings Endpoints 2024-08-13", () => { + describe("With api key", () => { + let app: INestApplication; + let organization: Team; + + let userRepositoryFixture: UserRepositoryFixture; + let bookingsRepositoryFixture: BookingsRepositoryFixture; + let schedulesService: SchedulesService_2024_04_15; + let eventTypesRepositoryFixture: EventTypesRepositoryFixture; + let teamRepositoryFixture: TeamRepositoryFixture; + let apiKeysRepositoryFixture: ApiKeysRepositoryFixture; + let apiKeyString: string; + + const userEmail = "bookings-controller-e2e@api.com"; + let user: User; + + let eventTypeId: number; + + beforeAll(async () => { + const moduleRef = await withApiAuth( + userEmail, + Test.createTestingModule({ + imports: [AppModule, PrismaModule, UsersModule, SchedulesModule_2024_04_15], + }) + ) + .overrideGuard(PermissionsGuard) + .useValue({ + canActivate: () => true, + }) + .compile(); + + userRepositoryFixture = new UserRepositoryFixture(moduleRef); + bookingsRepositoryFixture = new BookingsRepositoryFixture(moduleRef); + eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef); + teamRepositoryFixture = new TeamRepositoryFixture(moduleRef); + schedulesService = moduleRef.get(SchedulesService_2024_04_15); + apiKeysRepositoryFixture = new ApiKeysRepositoryFixture(moduleRef); + + organization = await teamRepositoryFixture.create({ name: "organization bookings" }); + + user = await userRepositoryFixture.create({ + email: userEmail, + }); + + const { keyString } = await apiKeysRepositoryFixture.createApiKey(user.id, null); + apiKeyString = keyString; + + const userSchedule: CreateScheduleInput_2024_04_15 = { + name: "working time", + timeZone: "Europe/Rome", + isDefault: true, + }; + await schedulesService.createUserSchedule(user.id, userSchedule); + const event = await eventTypesRepositoryFixture.create( + { title: "peer coding", slug: "peer-coding", length: 60 }, + user.id + ); + eventTypeId = event.id; + + app = moduleRef.createNestApplication(); + bootstrap(app as NestExpressApplication); + + await app.init(); + }); + + it("should be defined", () => { + expect(userRepositoryFixture).toBeDefined(); + expect(user).toBeDefined(); + }); + + describe("create bookings", () => { + it("should create a booking with api key", async () => { + const body: CreateBookingInput_2024_08_13 = { + start: new Date(Date.UTC(2030, 0, 8, 13, 0, 0)).toISOString(), + eventTypeId, + attendee: { + name: "Mr Key", + email: "mr_key@gmail.com", + timeZone: "Europe/Rome", + language: "it", + }, + meetingUrl: "https://meet.google.com/abc-def-ghi", + }; + + return request(app.getHttpServer()) + .post("/v2/bookings") + .send(body) + .set({ Authorization: `Bearer cal_test_${apiKeyString}` }) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .expect(201) + .then(async (response) => { + const responseBody: CreateBookingOutput_2024_08_13 = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + expect(responseDataIsBooking(responseBody.data)).toBe(true); + + if (responseDataIsBooking(responseBody.data)) { + const data: BookingOutput_2024_08_13 = responseBody.data; + expect(data.id).toBeDefined(); + expect(data.uid).toBeDefined(); + expect(data.hosts[0].id).toEqual(user.id); + expect(data.status).toEqual("accepted"); + expect(data.start).toEqual(body.start); + expect(data.end).toEqual(new Date(Date.UTC(2030, 0, 8, 14, 0, 0)).toISOString()); + expect(data.duration).toEqual(60); + expect(data.eventTypeId).toEqual(eventTypeId); + expect(data.attendees[0]).toEqual({ + name: body.attendee.name, + timeZone: body.attendee.timeZone, + language: body.attendee.language, + absent: false, + }); + expect(data.meetingUrl).toEqual(body.meetingUrl); + expect(data.absentHost).toEqual(false); + } else { + throw new Error( + "Invalid response data - expected booking but received array of possibily recurring bookings" + ); + } + }); + }); + }); + + function responseDataIsBooking(data: any): data is BookingOutput_2024_08_13 { + return !Array.isArray(data) && typeof data === "object" && data && "id" in data; + } + + afterAll(async () => { + await teamRepositoryFixture.delete(organization.id); + await userRepositoryFixture.deleteByEmail(user.email); + await bookingsRepositoryFixture.deleteAllBookings(user.id, user.email); + await app.close(); + }); + }); +}); diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/team-bookings.controller.e2e-spec.ts b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/team-bookings.controller.e2e-spec.ts new file mode 100644 index 00000000000000..1f3139e60c4a50 --- /dev/null +++ b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/team-bookings.controller.e2e-spec.ts @@ -0,0 +1,435 @@ +import { bootstrap } from "@/app"; +import { AppModule } from "@/app.module"; +import { CreateBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/create-booking.output"; +import { GetBookingsOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/get-bookings.output"; +import { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input"; +import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module"; +import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service"; +import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard"; +import { PrismaModule } from "@/modules/prisma/prisma.module"; +import { UsersModule } from "@/modules/users/users.module"; +import { INestApplication } from "@nestjs/common"; +import { NestExpressApplication } from "@nestjs/platform-express"; +import { Test } from "@nestjs/testing"; +import { User } from "@prisma/client"; +import * as request from "supertest"; +import { BookingsRepositoryFixture } from "test/fixtures/repository/bookings.repository.fixture"; +import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture"; +import { HostsRepositoryFixture } from "test/fixtures/repository/hosts.repository.fixture"; +import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture"; +import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture"; +import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.repository.fixture"; +import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture"; +import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture"; +import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture"; +import { withApiAuth } from "test/utils/withApiAuth"; + +import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants"; +import { + CreateBookingInput_2024_08_13, + BookingOutput_2024_08_13, + RecurringBookingOutput_2024_08_13, +} from "@calcom/platform-types"; +import { PlatformOAuthClient, Team } from "@calcom/prisma/client"; + +describe("Bookings Endpoints 2024-08-13", () => { + describe("Team bookings", () => { + let app: INestApplication; + let organization: Team; + let team1: Team; + let team2: Team; + + let userRepositoryFixture: UserRepositoryFixture; + let bookingsRepositoryFixture: BookingsRepositoryFixture; + let schedulesService: SchedulesService_2024_04_15; + let eventTypesRepositoryFixture: EventTypesRepositoryFixture; + let oauthClientRepositoryFixture: OAuthClientRepositoryFixture; + let oAuthClient: PlatformOAuthClient; + let teamRepositoryFixture: TeamRepositoryFixture; + let membershipsRepositoryFixture: MembershipRepositoryFixture; + let hostsRepositoryFixture: HostsRepositoryFixture; + let organizationsRepositoryFixture: OrganizationRepositoryFixture; + let profileRepositoryFixture: ProfileRepositoryFixture; + + const teamUserEmail = "orgUser1team1@api.com"; + const teamUserEmail2 = "orgUser2team1@api.com"; + let teamUser: User; + let teamUser2: User; + + let team1EventTypeId: number; + let team2EventTypeId: number; + + beforeAll(async () => { + const moduleRef = await withApiAuth( + teamUserEmail, + Test.createTestingModule({ + imports: [AppModule, PrismaModule, UsersModule, SchedulesModule_2024_04_15], + }) + ) + .overrideGuard(PermissionsGuard) + .useValue({ + canActivate: () => true, + }) + .compile(); + + userRepositoryFixture = new UserRepositoryFixture(moduleRef); + bookingsRepositoryFixture = new BookingsRepositoryFixture(moduleRef); + eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef); + oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef); + teamRepositoryFixture = new TeamRepositoryFixture(moduleRef); + organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef); + profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef); + membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef); + hostsRepositoryFixture = new HostsRepositoryFixture(moduleRef); + schedulesService = moduleRef.get(SchedulesService_2024_04_15); + + organization = await organizationsRepositoryFixture.create({ name: "organization team bookings" }); + team1 = await teamRepositoryFixture.create({ + name: "team 1", + isOrganization: false, + parent: { connect: { id: organization.id } }, + }); + + team2 = await teamRepositoryFixture.create({ + name: "team 2", + isOrganization: false, + parent: { connect: { id: organization.id } }, + }); + + oAuthClient = await createOAuthClient(organization.id); + + teamUser = await userRepositoryFixture.create({ + email: teamUserEmail, + locale: "it", + name: "orgUser1team1", + }); + + teamUser2 = await userRepositoryFixture.create({ + email: teamUserEmail2, + locale: "es", + name: "orgUser2team1", + }); + + const userSchedule: CreateScheduleInput_2024_04_15 = { + name: "working time", + timeZone: "Europe/Rome", + isDefault: true, + }; + await schedulesService.createUserSchedule(teamUser.id, userSchedule); + await schedulesService.createUserSchedule(teamUser2.id, userSchedule); + + await profileRepositoryFixture.create({ + uid: `usr-${teamUser.id}`, + username: teamUserEmail, + organization: { + connect: { + id: organization.id, + }, + }, + user: { + connect: { + id: teamUser.id, + }, + }, + }); + + await profileRepositoryFixture.create({ + uid: `usr-${teamUser2.id}`, + username: teamUserEmail2, + organization: { + connect: { + id: organization.id, + }, + }, + user: { + connect: { + id: teamUser2.id, + }, + }, + }); + + await membershipsRepositoryFixture.create({ + role: "MEMBER", + user: { connect: { id: teamUser.id } }, + team: { connect: { id: team1.id } }, + accepted: true, + }); + + await membershipsRepositoryFixture.create({ + role: "MEMBER", + user: { connect: { id: teamUser.id } }, + team: { connect: { id: team2.id } }, + accepted: true, + }); + + await membershipsRepositoryFixture.create({ + role: "MEMBER", + user: { connect: { id: teamUser2.id } }, + team: { connect: { id: team2.id } }, + accepted: true, + }); + + const team1EventType = await eventTypesRepositoryFixture.createTeamEventType({ + schedulingType: "COLLECTIVE", + team: { + connect: { id: team1.id }, + }, + title: "Collective Event Type", + slug: "collective-event-type", + length: 60, + assignAllTeamMembers: true, + bookingFields: [], + locations: [], + }); + + team1EventTypeId = team1EventType.id; + + const team2EventType = await eventTypesRepositoryFixture.createTeamEventType({ + schedulingType: "COLLECTIVE", + team: { + connect: { id: team2.id }, + }, + title: "Collective Event Type 2", + slug: "collective-event-type-2", + length: 60, + assignAllTeamMembers: true, + bookingFields: [], + locations: [], + }); + + team2EventTypeId = team2EventType.id; + + await hostsRepositoryFixture.create({ + isFixed: true, + user: { + connect: { + id: teamUser.id, + }, + }, + eventType: { + connect: { + id: team1EventType.id, + }, + }, + }); + + await hostsRepositoryFixture.create({ + isFixed: true, + user: { + connect: { + id: teamUser.id, + }, + }, + eventType: { + connect: { + id: team2EventType.id, + }, + }, + }); + + await hostsRepositoryFixture.create({ + isFixed: true, + user: { + connect: { + id: teamUser2.id, + }, + }, + eventType: { + connect: { + id: team2EventType.id, + }, + }, + }); + + app = moduleRef.createNestApplication(); + bootstrap(app as NestExpressApplication); + + await app.init(); + }); + + describe("create team bookings", () => { + it("should create a team 1 booking", async () => { + const body: CreateBookingInput_2024_08_13 = { + start: new Date(Date.UTC(2030, 0, 8, 13, 0, 0)).toISOString(), + eventTypeId: team1EventTypeId, + attendee: { + name: "alice", + email: "alice@gmail.com", + timeZone: "Europe/Madrid", + language: "es", + }, + meetingUrl: "https://meet.google.com/abc-def-ghi", + }; + + return request(app.getHttpServer()) + .post("/v2/bookings") + .send(body) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .expect(201) + .then(async (response) => { + const responseBody: CreateBookingOutput_2024_08_13 = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + expect(responseDataIsBooking(responseBody.data)).toBe(true); + + if (responseDataIsBooking(responseBody.data)) { + const data: BookingOutput_2024_08_13 = responseBody.data; + expect(data.id).toBeDefined(); + expect(data.uid).toBeDefined(); + expect(data.hosts.length).toEqual(1); + expect(data.hosts[0].id).toEqual(teamUser.id); + expect(data.status).toEqual("accepted"); + expect(data.start).toEqual(body.start); + expect(data.end).toEqual(new Date(Date.UTC(2030, 0, 8, 14, 0, 0)).toISOString()); + expect(data.duration).toEqual(60); + expect(data.eventTypeId).toEqual(team1EventTypeId); + expect(data.attendees.length).toEqual(1); + expect(data.attendees[0]).toEqual({ + name: body.attendee.name, + timeZone: body.attendee.timeZone, + language: body.attendee.language, + absent: false, + }); + expect(data.meetingUrl).toEqual(body.meetingUrl); + expect(data.absentHost).toEqual(false); + } else { + throw new Error( + "Invalid response data - expected booking but received array of possibily recurring bookings" + ); + } + }); + }); + + it("should create a team 2 booking", async () => { + const body: CreateBookingInput_2024_08_13 = { + start: new Date(Date.UTC(2030, 0, 8, 10, 0, 0)).toISOString(), + eventTypeId: team2EventTypeId, + attendee: { + name: "bob", + email: "bob@gmail.com", + timeZone: "Europe/Rome", + language: "it", + }, + meetingUrl: "https://meet.google.com/abc-def-ghi", + }; + + return request(app.getHttpServer()) + .post("/v2/bookings") + .send(body) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .expect(201) + .then(async (response) => { + const responseBody: CreateBookingOutput_2024_08_13 = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + expect(responseDataIsBooking(responseBody.data)).toBe(true); + + if (responseDataIsBooking(responseBody.data)) { + const data: BookingOutput_2024_08_13 = responseBody.data; + expect(data.id).toBeDefined(); + expect(data.uid).toBeDefined(); + expect(data.hosts.length).toEqual(1); + expect(data.hosts[0].id).toEqual(teamUser.id); + expect(data.status).toEqual("accepted"); + expect(data.start).toEqual(body.start); + expect(data.end).toEqual(new Date(Date.UTC(2030, 0, 8, 11, 0, 0)).toISOString()); + expect(data.duration).toEqual(60); + expect(data.eventTypeId).toEqual(team2EventTypeId); + expect(data.attendees.length).toEqual(2); + expect(data.attendees[0]).toEqual({ + name: body.attendee.name, + timeZone: body.attendee.timeZone, + language: body.attendee.language, + absent: false, + }); + expect(data.attendees[1]).toEqual({ + name: teamUser2.name, + timeZone: teamUser2.timeZone, + language: teamUser2.locale, + absent: false, + }); + expect(data.meetingUrl).toEqual(body.meetingUrl); + expect(data.absentHost).toEqual(false); + } else { + throw new Error( + "Invalid response data - expected booking but received array of possibily recurring bookings" + ); + } + }); + }); + }); + + describe("get team bookings", () => { + it("should should get bookings by teamId", async () => { + return request(app.getHttpServer()) + .get(`/v2/bookings?teamId=${team1.id}`) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .expect(200) + .then(async (response) => { + const responseBody: GetBookingsOutput_2024_08_13 = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + const data: (BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13)[] = responseBody.data; + expect(data.length).toEqual(1); + expect(data[0].eventTypeId).toEqual(team1EventTypeId); + }); + }); + + it("should should get bookings by teamId", async () => { + return request(app.getHttpServer()) + .get(`/v2/bookings?teamId=${team2.id}`) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .expect(200) + .then(async (response) => { + const responseBody: GetBookingsOutput_2024_08_13 = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + const data: (BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13)[] = responseBody.data; + expect(data.length).toEqual(1); + expect(data[0].eventTypeId).toEqual(team2EventTypeId); + }); + }); + + it("should should get bookings by teamIds", async () => { + return request(app.getHttpServer()) + .get(`/v2/bookings?teamIds=${team1.id},${team2.id}`) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .expect(200) + .then(async (response) => { + const responseBody: GetBookingsOutput_2024_08_13 = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + const data: (BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13)[] = responseBody.data; + expect(data.length).toEqual(2); + expect(data.find((booking) => booking.eventTypeId === team1EventTypeId)).toBeDefined(); + expect(data.find((booking) => booking.eventTypeId === team2EventTypeId)).toBeDefined(); + }); + }); + }); + + async function createOAuthClient(organizationId: number) { + const data = { + logo: "logo-url", + name: "name", + redirectUris: ["http://localhost:5555"], + permissions: 32, + }; + const secret = "secret"; + + const client = await oauthClientRepositoryFixture.create(organizationId, data, secret); + return client; + } + + function responseDataIsBooking(data: any): data is BookingOutput_2024_08_13 { + return !Array.isArray(data) && typeof data === "object" && data && "id" in data; + } + + afterAll(async () => { + await oauthClientRepositoryFixture.delete(oAuthClient.id); + await teamRepositoryFixture.delete(organization.id); + await userRepositoryFixture.deleteByEmail(teamUser.email); + await bookingsRepositoryFixture.deleteAllBookings(teamUser.id, teamUser.email); + await app.close(); + }); + }); +}); diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.e2e-spec.ts b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/user-bookings.controller.e2e-spec.ts similarity index 71% rename from apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.e2e-spec.ts rename to apps/api/v2/src/ee/bookings/2024-08-13/controllers/user-bookings.controller.e2e-spec.ts index 0b9e1ac9936424..1203c8bf6a0a5e 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.e2e-spec.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/user-bookings.controller.e2e-spec.ts @@ -17,14 +17,9 @@ import { NestExpressApplication } from "@nestjs/platform-express"; import { Test } from "@nestjs/testing"; import { User } from "@prisma/client"; import * as request from "supertest"; -import { ApiKeysRepositoryFixture } from "test/fixtures/repository/api-keys.repository.fixture"; import { BookingsRepositoryFixture } from "test/fixtures/repository/bookings.repository.fixture"; import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture"; -import { HostsRepositoryFixture } from "test/fixtures/repository/hosts.repository.fixture"; -import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture"; import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture"; -import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.repository.fixture"; -import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture"; import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture"; import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture"; import { withApiAuth } from "test/utils/withApiAuth"; @@ -1036,538 +1031,4 @@ describe("Bookings Endpoints 2024-08-13", () => { await app.close(); }); }); - - describe("Team bookings", () => { - let app: INestApplication; - let organization: Team; - let team1: Team; - let team2: Team; - - let userRepositoryFixture: UserRepositoryFixture; - let bookingsRepositoryFixture: BookingsRepositoryFixture; - let schedulesService: SchedulesService_2024_04_15; - let eventTypesRepositoryFixture: EventTypesRepositoryFixture; - let oauthClientRepositoryFixture: OAuthClientRepositoryFixture; - let oAuthClient: PlatformOAuthClient; - let teamRepositoryFixture: TeamRepositoryFixture; - let membershipsRepositoryFixture: MembershipRepositoryFixture; - let hostsRepositoryFixture: HostsRepositoryFixture; - let organizationsRepositoryFixture: OrganizationRepositoryFixture; - let profileRepositoryFixture: ProfileRepositoryFixture; - - const teamUserEmail = "orgUser1team1@api.com"; - const teamUserEmail2 = "orgUser2team1@api.com"; - let teamUser: User; - let teamUser2: User; - - let team1EventTypeId: number; - let team2EventTypeId: number; - - beforeAll(async () => { - const moduleRef = await withApiAuth( - teamUserEmail, - Test.createTestingModule({ - imports: [AppModule, PrismaModule, UsersModule, SchedulesModule_2024_04_15], - }) - ) - .overrideGuard(PermissionsGuard) - .useValue({ - canActivate: () => true, - }) - .compile(); - - userRepositoryFixture = new UserRepositoryFixture(moduleRef); - bookingsRepositoryFixture = new BookingsRepositoryFixture(moduleRef); - eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef); - oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef); - teamRepositoryFixture = new TeamRepositoryFixture(moduleRef); - organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef); - profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef); - membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef); - hostsRepositoryFixture = new HostsRepositoryFixture(moduleRef); - schedulesService = moduleRef.get(SchedulesService_2024_04_15); - - organization = await organizationsRepositoryFixture.create({ name: "organization team bookings" }); - team1 = await teamRepositoryFixture.create({ - name: "team 1", - isOrganization: false, - parent: { connect: { id: organization.id } }, - }); - - team2 = await teamRepositoryFixture.create({ - name: "team 2", - isOrganization: false, - parent: { connect: { id: organization.id } }, - }); - - oAuthClient = await createOAuthClient(organization.id); - - teamUser = await userRepositoryFixture.create({ - email: teamUserEmail, - locale: "it", - name: "orgUser1team1", - }); - - teamUser2 = await userRepositoryFixture.create({ - email: teamUserEmail2, - locale: "es", - name: "orgUser2team1", - }); - - const userSchedule: CreateScheduleInput_2024_04_15 = { - name: "working time", - timeZone: "Europe/Rome", - isDefault: true, - }; - await schedulesService.createUserSchedule(teamUser.id, userSchedule); - await schedulesService.createUserSchedule(teamUser2.id, userSchedule); - - await profileRepositoryFixture.create({ - uid: `usr-${teamUser.id}`, - username: teamUserEmail, - organization: { - connect: { - id: organization.id, - }, - }, - user: { - connect: { - id: teamUser.id, - }, - }, - }); - - await profileRepositoryFixture.create({ - uid: `usr-${teamUser2.id}`, - username: teamUserEmail2, - organization: { - connect: { - id: organization.id, - }, - }, - user: { - connect: { - id: teamUser2.id, - }, - }, - }); - - await membershipsRepositoryFixture.create({ - role: "MEMBER", - user: { connect: { id: teamUser.id } }, - team: { connect: { id: team1.id } }, - accepted: true, - }); - - await membershipsRepositoryFixture.create({ - role: "MEMBER", - user: { connect: { id: teamUser.id } }, - team: { connect: { id: team2.id } }, - accepted: true, - }); - - await membershipsRepositoryFixture.create({ - role: "MEMBER", - user: { connect: { id: teamUser2.id } }, - team: { connect: { id: team2.id } }, - accepted: true, - }); - - const team1EventType = await eventTypesRepositoryFixture.createTeamEventType({ - schedulingType: "COLLECTIVE", - team: { - connect: { id: team1.id }, - }, - title: "Collective Event Type", - slug: "collective-event-type", - length: 60, - assignAllTeamMembers: true, - bookingFields: [], - locations: [], - }); - - team1EventTypeId = team1EventType.id; - - const team2EventType = await eventTypesRepositoryFixture.createTeamEventType({ - schedulingType: "COLLECTIVE", - team: { - connect: { id: team2.id }, - }, - title: "Collective Event Type 2", - slug: "collective-event-type-2", - length: 60, - assignAllTeamMembers: true, - bookingFields: [], - locations: [], - }); - - team2EventTypeId = team2EventType.id; - - await hostsRepositoryFixture.create({ - isFixed: true, - user: { - connect: { - id: teamUser.id, - }, - }, - eventType: { - connect: { - id: team1EventType.id, - }, - }, - }); - - await hostsRepositoryFixture.create({ - isFixed: true, - user: { - connect: { - id: teamUser.id, - }, - }, - eventType: { - connect: { - id: team2EventType.id, - }, - }, - }); - - await hostsRepositoryFixture.create({ - isFixed: true, - user: { - connect: { - id: teamUser2.id, - }, - }, - eventType: { - connect: { - id: team2EventType.id, - }, - }, - }); - - app = moduleRef.createNestApplication(); - bootstrap(app as NestExpressApplication); - - await app.init(); - }); - - describe("create team bookings", () => { - it("should create a team 1 booking", async () => { - const body: CreateBookingInput_2024_08_13 = { - start: new Date(Date.UTC(2030, 0, 8, 13, 0, 0)).toISOString(), - eventTypeId: team1EventTypeId, - attendee: { - name: "alice", - email: "alice@gmail.com", - timeZone: "Europe/Madrid", - language: "es", - }, - meetingUrl: "https://meet.google.com/abc-def-ghi", - }; - - return request(app.getHttpServer()) - .post("/v2/bookings") - .send(body) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .expect(201) - .then(async (response) => { - const responseBody: CreateBookingOutput_2024_08_13 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - expect(responseDataIsBooking(responseBody.data)).toBe(true); - - if (responseDataIsBooking(responseBody.data)) { - const data: BookingOutput_2024_08_13 = responseBody.data; - expect(data.id).toBeDefined(); - expect(data.uid).toBeDefined(); - expect(data.hosts.length).toEqual(1); - expect(data.hosts[0].id).toEqual(teamUser.id); - expect(data.status).toEqual("accepted"); - expect(data.start).toEqual(body.start); - expect(data.end).toEqual(new Date(Date.UTC(2030, 0, 8, 14, 0, 0)).toISOString()); - expect(data.duration).toEqual(60); - expect(data.eventTypeId).toEqual(team1EventTypeId); - expect(data.attendees.length).toEqual(1); - expect(data.attendees[0]).toEqual({ - name: body.attendee.name, - timeZone: body.attendee.timeZone, - language: body.attendee.language, - absent: false, - }); - expect(data.meetingUrl).toEqual(body.meetingUrl); - expect(data.absentHost).toEqual(false); - } else { - throw new Error( - "Invalid response data - expected booking but received array of possibily recurring bookings" - ); - } - }); - }); - - it("should create a team 2 booking", async () => { - const body: CreateBookingInput_2024_08_13 = { - start: new Date(Date.UTC(2030, 0, 8, 10, 0, 0)).toISOString(), - eventTypeId: team2EventTypeId, - attendee: { - name: "bob", - email: "bob@gmail.com", - timeZone: "Europe/Rome", - language: "it", - }, - meetingUrl: "https://meet.google.com/abc-def-ghi", - }; - - return request(app.getHttpServer()) - .post("/v2/bookings") - .send(body) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .expect(201) - .then(async (response) => { - const responseBody: CreateBookingOutput_2024_08_13 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - expect(responseDataIsBooking(responseBody.data)).toBe(true); - - if (responseDataIsBooking(responseBody.data)) { - const data: BookingOutput_2024_08_13 = responseBody.data; - expect(data.id).toBeDefined(); - expect(data.uid).toBeDefined(); - expect(data.hosts.length).toEqual(1); - expect(data.hosts[0].id).toEqual(teamUser.id); - expect(data.status).toEqual("accepted"); - expect(data.start).toEqual(body.start); - expect(data.end).toEqual(new Date(Date.UTC(2030, 0, 8, 11, 0, 0)).toISOString()); - expect(data.duration).toEqual(60); - expect(data.eventTypeId).toEqual(team2EventTypeId); - expect(data.attendees.length).toEqual(2); - expect(data.attendees[0]).toEqual({ - name: body.attendee.name, - timeZone: body.attendee.timeZone, - language: body.attendee.language, - absent: false, - }); - expect(data.attendees[1]).toEqual({ - name: teamUser2.name, - timeZone: teamUser2.timeZone, - language: teamUser2.locale, - absent: false, - }); - expect(data.meetingUrl).toEqual(body.meetingUrl); - expect(data.absentHost).toEqual(false); - } else { - throw new Error( - "Invalid response data - expected booking but received array of possibily recurring bookings" - ); - } - }); - }); - }); - - describe("get team bookings", () => { - it("should should get bookings by teamId", async () => { - return request(app.getHttpServer()) - .get(`/v2/bookings?teamId=${team1.id}`) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .expect(200) - .then(async (response) => { - const responseBody: GetBookingsOutput_2024_08_13 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - const data: (BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13)[] = responseBody.data; - expect(data.length).toEqual(1); - expect(data[0].eventTypeId).toEqual(team1EventTypeId); - }); - }); - - it("should should get bookings by teamId", async () => { - return request(app.getHttpServer()) - .get(`/v2/bookings?teamId=${team2.id}`) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .expect(200) - .then(async (response) => { - const responseBody: GetBookingsOutput_2024_08_13 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - const data: (BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13)[] = responseBody.data; - expect(data.length).toEqual(1); - expect(data[0].eventTypeId).toEqual(team2EventTypeId); - }); - }); - - it("should should get bookings by teamIds", async () => { - return request(app.getHttpServer()) - .get(`/v2/bookings?teamIds=${team1.id},${team2.id}`) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .expect(200) - .then(async (response) => { - const responseBody: GetBookingsOutput_2024_08_13 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - const data: (BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13)[] = responseBody.data; - expect(data.length).toEqual(2); - expect(data.find((booking) => booking.eventTypeId === team1EventTypeId)).toBeDefined(); - expect(data.find((booking) => booking.eventTypeId === team2EventTypeId)).toBeDefined(); - }); - }); - }); - - async function createOAuthClient(organizationId: number) { - const data = { - logo: "logo-url", - name: "name", - redirectUris: ["http://localhost:5555"], - permissions: 32, - }; - const secret = "secret"; - - const client = await oauthClientRepositoryFixture.create(organizationId, data, secret); - return client; - } - - function responseDataIsBooking(data: any): data is BookingOutput_2024_08_13 { - return !Array.isArray(data) && typeof data === "object" && data && "id" in data; - } - - afterAll(async () => { - await oauthClientRepositoryFixture.delete(oAuthClient.id); - await teamRepositoryFixture.delete(organization.id); - await userRepositoryFixture.deleteByEmail(teamUser.email); - await bookingsRepositoryFixture.deleteAllBookings(teamUser.id, teamUser.email); - await app.close(); - }); - }); - - describe("With api key", () => { - let app: INestApplication; - let organization: Team; - - let userRepositoryFixture: UserRepositoryFixture; - let bookingsRepositoryFixture: BookingsRepositoryFixture; - let schedulesService: SchedulesService_2024_04_15; - let eventTypesRepositoryFixture: EventTypesRepositoryFixture; - let teamRepositoryFixture: TeamRepositoryFixture; - let apiKeysRepositoryFixture: ApiKeysRepositoryFixture; - let apiKeyString: string; - - const userEmail = "bookings-controller-e2e@api.com"; - let user: User; - - let eventTypeId: number; - - beforeAll(async () => { - const moduleRef = await withApiAuth( - userEmail, - Test.createTestingModule({ - imports: [AppModule, PrismaModule, UsersModule, SchedulesModule_2024_04_15], - }) - ) - .overrideGuard(PermissionsGuard) - .useValue({ - canActivate: () => true, - }) - .compile(); - - userRepositoryFixture = new UserRepositoryFixture(moduleRef); - bookingsRepositoryFixture = new BookingsRepositoryFixture(moduleRef); - eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef); - teamRepositoryFixture = new TeamRepositoryFixture(moduleRef); - schedulesService = moduleRef.get(SchedulesService_2024_04_15); - apiKeysRepositoryFixture = new ApiKeysRepositoryFixture(moduleRef); - - organization = await teamRepositoryFixture.create({ name: "organization bookings" }); - - user = await userRepositoryFixture.create({ - email: userEmail, - }); - - const { keyString } = await apiKeysRepositoryFixture.createApiKey(user.id, null); - apiKeyString = keyString; - - const userSchedule: CreateScheduleInput_2024_04_15 = { - name: "working time", - timeZone: "Europe/Rome", - isDefault: true, - }; - await schedulesService.createUserSchedule(user.id, userSchedule); - const event = await eventTypesRepositoryFixture.create( - { title: "peer coding", slug: "peer-coding", length: 60 }, - user.id - ); - eventTypeId = event.id; - - app = moduleRef.createNestApplication(); - bootstrap(app as NestExpressApplication); - - await app.init(); - }); - - it("should be defined", () => { - expect(userRepositoryFixture).toBeDefined(); - expect(user).toBeDefined(); - }); - - describe("create bookings", () => { - it("should create a booking with api key", async () => { - const body: CreateBookingInput_2024_08_13 = { - start: new Date(Date.UTC(2030, 0, 8, 13, 0, 0)).toISOString(), - eventTypeId, - attendee: { - name: "Mr Key", - email: "mr_key@gmail.com", - timeZone: "Europe/Rome", - language: "it", - }, - meetingUrl: "https://meet.google.com/abc-def-ghi", - }; - - return request(app.getHttpServer()) - .post("/v2/bookings") - .send(body) - .set({ Authorization: `Bearer cal_test_${apiKeyString}` }) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .expect(201) - .then(async (response) => { - const responseBody: CreateBookingOutput_2024_08_13 = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - expect(responseDataIsBooking(responseBody.data)).toBe(true); - - if (responseDataIsBooking(responseBody.data)) { - const data: BookingOutput_2024_08_13 = responseBody.data; - expect(data.id).toBeDefined(); - expect(data.uid).toBeDefined(); - expect(data.hosts[0].id).toEqual(user.id); - expect(data.status).toEqual("accepted"); - expect(data.start).toEqual(body.start); - expect(data.end).toEqual(new Date(Date.UTC(2030, 0, 8, 14, 0, 0)).toISOString()); - expect(data.duration).toEqual(60); - expect(data.eventTypeId).toEqual(eventTypeId); - expect(data.attendees[0]).toEqual({ - name: body.attendee.name, - timeZone: body.attendee.timeZone, - language: body.attendee.language, - absent: false, - }); - expect(data.meetingUrl).toEqual(body.meetingUrl); - expect(data.absentHost).toEqual(false); - } else { - throw new Error( - "Invalid response data - expected booking but received array of possibily recurring bookings" - ); - } - }); - }); - }); - - function responseDataIsBooking(data: any): data is BookingOutput_2024_08_13 { - return !Array.isArray(data) && typeof data === "object" && data && "id" in data; - } - - afterAll(async () => { - await teamRepositoryFixture.delete(organization.id); - await userRepositoryFixture.deleteByEmail(user.email); - await bookingsRepositoryFixture.deleteAllBookings(user.id, user.email); - await app.close(); - }); - }); }); From 87fb4f01ba5f290d825f4f10d2b7e0808590aaf0 Mon Sep 17 00:00:00 2001 From: Morgan Vernay Date: Fri, 20 Sep 2024 14:45:12 +0300 Subject: [PATCH 91/96] fixup! refactor: split bookings e2e into smaller e2e files --- apps/api/v2/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/api/v2/package.json b/apps/api/v2/package.json index 6fabecce20cadd..2e4985a89259f3 100644 --- a/apps/api/v2/package.json +++ b/apps/api/v2/package.json @@ -19,7 +19,7 @@ "test:watch": "yarn dev:build && jest --watch", "test:cov": "yarn dev:build && jest --coverage", "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", - "test:e2e": "yarn dev:build && jest --detectOpenHandles --forceExit --config ./jest-e2e.json", + "test:e2e": "NODE_OPTIONS='--max-old-space-size=4096' yarn dev:build && jest --ci --detectOpenHandles --forceExit --config ./jest-e2e.json", "test:e2e:watch": "yarn dev:build && jest --runInBand --detectOpenHandles --forceExit --config ./jest-e2e.json --watch", "prisma": "yarn workspace @calcom/prisma prisma", "generate-schemas": "yarn prisma generate && yarn prisma format", @@ -33,7 +33,7 @@ "@calcom/platform-types": "*", "@calcom/platform-utils": "*", "@calcom/prisma": "*", - "@golevelup/ts-jest": "^0.4.0", + "@golevelup/ts- ": "^0.4.0", "@microsoft/microsoft-graph-types-beta": "^0.42.0-preview", "@nestjs/bull": "^10.1.1", "@nestjs/common": "^10.0.0", From 58b9a85a955962e3ae7b63833056cda07687ca63 Mon Sep 17 00:00:00 2001 From: Morgan Vernay Date: Fri, 20 Sep 2024 15:04:48 +0300 Subject: [PATCH 92/96] fixup! fixup! refactor: split bookings e2e into smaller e2e files --- apps/api/v2/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/api/v2/package.json b/apps/api/v2/package.json index 2e4985a89259f3..5990bdeae62349 100644 --- a/apps/api/v2/package.json +++ b/apps/api/v2/package.json @@ -33,7 +33,7 @@ "@calcom/platform-types": "*", "@calcom/platform-utils": "*", "@calcom/prisma": "*", - "@golevelup/ts- ": "^0.4.0", + "@golevelup/ts-jest": "^0.4.0", "@microsoft/microsoft-graph-types-beta": "^0.42.0-preview", "@nestjs/bull": "^10.1.1", "@nestjs/common": "^10.0.0", From 17e149d001ce0c03608231d803292157ecda7b91 Mon Sep 17 00:00:00 2001 From: Morgan Vernay Date: Fri, 20 Sep 2024 15:35:58 +0300 Subject: [PATCH 93/96] fixup! fixup! fixup! refactor: split bookings e2e into smaller e2e files --- apps/api/v2/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/api/v2/package.json b/apps/api/v2/package.json index 5990bdeae62349..6300577cb33ac0 100644 --- a/apps/api/v2/package.json +++ b/apps/api/v2/package.json @@ -19,7 +19,7 @@ "test:watch": "yarn dev:build && jest --watch", "test:cov": "yarn dev:build && jest --coverage", "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", - "test:e2e": "NODE_OPTIONS='--max-old-space-size=4096' yarn dev:build && jest --ci --detectOpenHandles --forceExit --config ./jest-e2e.json", + "test:e2e": "yarn dev:build && jest --ci --detectOpenHandles --forceExit --config ./jest-e2e.json", "test:e2e:watch": "yarn dev:build && jest --runInBand --detectOpenHandles --forceExit --config ./jest-e2e.json --watch", "prisma": "yarn workspace @calcom/prisma prisma", "generate-schemas": "yarn prisma generate && yarn prisma format", From 53bb49d96bdd4c44baca57ed1a29813c9a36275a Mon Sep 17 00:00:00 2001 From: Morgan Vernay Date: Mon, 23 Sep 2024 10:14:25 +0300 Subject: [PATCH 94/96] fixup! Merge branch 'main' into v2-refactor-bookings --- apps/api/v2/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/api/v2/package.json b/apps/api/v2/package.json index 6300577cb33ac0..c2cad589ac0f96 100644 --- a/apps/api/v2/package.json +++ b/apps/api/v2/package.json @@ -19,7 +19,7 @@ "test:watch": "yarn dev:build && jest --watch", "test:cov": "yarn dev:build && jest --coverage", "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", - "test:e2e": "yarn dev:build && jest --ci --detectOpenHandles --forceExit --config ./jest-e2e.json", + "test:e2e": "yarn dev:build && NODE_OPTIONS='--max_old_space_size=8192' jest --ci --forceExit --config ./jest-e2e.json", "test:e2e:watch": "yarn dev:build && jest --runInBand --detectOpenHandles --forceExit --config ./jest-e2e.json --watch", "prisma": "yarn workspace @calcom/prisma prisma", "generate-schemas": "yarn prisma generate && yarn prisma format", From 69ba3cdc58fe624e79db8451952f2f8cebaf15c8 Mon Sep 17 00:00:00 2001 From: supalarry Date: Mon, 23 Sep 2024 10:05:27 +0200 Subject: [PATCH 95/96] revert event types service --- .../services/event-types.service.ts | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/event-types.service.ts b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/event-types.service.ts index bbded74b95e547..ea800cd87d4773 100644 --- a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/event-types.service.ts +++ b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/event-types.service.ts @@ -38,8 +38,17 @@ export class EventTypesService_2024_06_14 { async createUserEventType(user: UserWithProfile, body: CreateEventTypeInput_2024_06_14) { await this.checkCanCreateEventType(user.id, body); const eventTypeUser = await this.getUserToCreateEvent(user); - const { bookingLimits, durationLimits, ...bodyTransformed } = - this.inputEventTypesService.transformInputCreateEventType(body); + const { + bookingLimits, + durationLimits, + periodType = undefined, + periodDays = undefined, + periodCountCalendarDays = undefined, + periodStartDate = undefined, + periodEndDate = undefined, + recurrence = undefined, + ...bodyTransformed + } = this.inputEventTypesService.transformInputCreateEventType(body); const { eventType: eventTypeCreated } = await createEventType({ input: bodyTransformed, ctx: { @@ -55,6 +64,12 @@ export class EventTypesService_2024_06_14 { id: eventTypeCreated.id, bookingLimits, durationLimits, + periodType, + periodDays, + periodCountCalendarDays, + periodStartDate, + periodEndDate, + recurrence, ...bodyTransformed, }, ctx: { From 5ddcf312f2650cb1254bbc53905f6b9d9d54fc5b Mon Sep 17 00:00:00 2001 From: Morgan Vernay Date: Mon, 23 Sep 2024 14:39:03 +0300 Subject: [PATCH 96/96] fix: remove resetModule, maxWorker 2 jest e2e config --- apps/api/v2/jest-e2e.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/api/v2/jest-e2e.json b/apps/api/v2/jest-e2e.json index 072cd92d6015ce..fe8f6ba838c8db 100644 --- a/apps/api/v2/jest-e2e.json +++ b/apps/api/v2/jest-e2e.json @@ -13,6 +13,5 @@ "setupFiles": ["/test/setEnvVars.ts"], "reporters": ["default", "jest-summarizing-reporter"], "workerIdleMemoryLimit": "512MB", - "resetModules": true, - "maxWorkers": 1 + "maxWorkers": 2 }