From 2e2a6c73b158bfa8a71138797d06cf8334b2bec3 Mon Sep 17 00:00:00 2001 From: Somay Chauhan Date: Tue, 24 Sep 2024 16:06:05 +0530 Subject: [PATCH] feat: add advanced tab attributes to api/v2/event-types (#16314) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * added bookerLayouts * added requiresConfirmation * added tests * undo extra changes * undo extra change * fixed imports * added requiresBookerEmailVerification * added hideCalendarNotes * added lockTimeZoneToggleOnBookingPage * added eventTypeColor * undo extra changes * added seats * added requiresConfirmationWillBlockSlot to requiresConfirmation * refactor: Implement BaseEventType class to avoid duplication across EventType input/output classes ## D-R-Y ## * temp-fix: will revert later * Correct placement of users array from EventTypeOutput to TeamEventTypeOutput * Refactor: Shifted transformation logic to controllers using interceptors and pipes for a cleaner service layer. * small fix * Refactor: Moved transformation logic from service to controller using interceptors and pipes ** organisations/event-types * feat: adde tests for `validateEventTypeInputs` * fix: failing test * fix: layout validator * renamed blockCalendarForUnconfirmedBookings to blockUnconfirmedBookingsInBooker * testing changes * added disabled state * fix merege conflict * renamed requiresConfirmation to confirmationPolicy * renamed OutputEventTypesResponseInterceptor to OutputTeamEventTypesResponseInterceptor * renamed `darkThemeColor` -> `darkThemeHex` and `lightThemeColor` -> `lightThemeHex` * renamed `eventTypeColor` -> `color` * changed `requiresConfirmation` -> `confirmationPolicy` * added tests for disabled state * added class-validators to `disabled?: false` * Revert: Split create and update input/output classes * feat: Refactor responses to use DTOs with plainToClass for explicit property control • Updated all endpoints to return response DTOs (e.g., CreateTeamEventTypeOutput, GetTeamEventTypeOutput) to ensure consistent Swagger documentation generation. • Introduced plainToClass for transforming responses, enabling explicit control over which properties are returned in the response, following Morgan’s recommendation. • Replaced generic HandlerResponse type with class-validator based DTOs for each endpoint, ensuring TypeScript enforces correct response structure. • Applied strategy: "excludeAll" to limit response properties to only those explicitly defined in the DTO, preventing unintended fields from being included in the API responses. * added pipes to transform output data * add clean script to platform-types package.json * added customName * added destination calendar * fixed type errors * added useDestinationCalendarEmail * refactor: api-reqest and api-response * Update event-type.output.ts * fixed errors * Update yarn.lock * added some missed properties to output * Update constants.ts * removed return type * fixed some type errors * reuse types * Improve readability of validation functions * Update CHANGELOG.md * chore: post publish platform libraries * Update documentation.json * fix: reset platform libraries to 0.0.0 * fixup! fix: reset platform libraries to 0.0.0 * Update event-type.tranformed.ts --------- Co-authored-by: supalarry Co-authored-by: Morgan Vernay --- apps/api/v2/package.json | 2 +- .../outputs/connected-calendars.output.ts | 2 +- .../calendars/services/calendars.service.ts | 4 - .../src/ee/calendars/services/gcal.service.ts | 2 +- .../constants/constants.ts | 56 +- .../event-types.controller.e2e-spec.ts | 448 +- .../controllers/event-types.controller.ts | 36 +- .../event-types.module.ts | 10 + .../event-types.repository.ts | 57 +- .../pipes/event-type-response.transformer.ts | 39 + .../services/event-types.service.ts | 83 +- .../services/input-event-types.service.ts | 265 +- .../services/output-event-types.service.ts | 110 +- .../organizations-event-types.controller.ts | 53 +- .../organizations-event-types.e2e-spec.ts | 39 +- .../team-event-types-response.transformer.ts | 38 + .../organizations/organizations.module.ts | 2 + .../organizations-event-types.repository.ts | 12 +- .../services/event-types/input.service.ts | 120 +- .../organizations-event-types.service.ts | 112 +- .../services/event-types/output.service.ts | 24 +- apps/api/v2/swagger/documentation.json | 458 +- .../web/playwright/managed-event-types.e2e.ts | 1 - apps/web/playwright/reschedule.e2e.ts | 1 - .../zoomvideo/lib/VideoApiAdapter.ts | 2 +- packages/features/auth/SAMLLogin.tsx | 4 +- .../api-to-internal/api-to-internal.spec.ts | 172 +- .../api-to-internal/booker-layouts.ts | 22 + .../api-to-internal/confirmation-policy.ts | 36 + .../api-to-internal/event-colors.ts | 15 + .../api-to-internal/future-booking-limits.ts | 8 + .../transformers/api-to-internal/index.ts | 4 + .../api-to-internal/interval-limits.ts | 3 + .../transformers/api-to-internal/locations.ts | 2 +- .../api-to-internal/recurrence.ts | 3 +- .../transformers/api-to-internal/seats.ts | 22 + .../internal-to-api/booker-layouts.ts | 20 + .../internal-to-api/event-type-colors.ts | 10 + .../internal-to-api/future-booking-limits.ts | 6 + .../transformers/internal-to-api/index.ts | 4 + .../internal-to-api/internal-to-api.spec.ts | 170 + .../internal-to-api/requires-confirmation.ts | 29 + .../transformers/internal-to-api/seats.ts | 20 + .../enums/event-types/booker-layouts.enum.ts | 11 + .../enums/event-types/booking-window.enum.ts | 1 + .../event-types/confirmation-policy.enum.ts | 9 + packages/platform/enums/index.ts | 2 + packages/platform/enums/monorepo.ts | 2 + packages/platform/libraries/CHANGELOG.md | 18 + packages/platform/libraries/index.ts | 11 +- .../event-types_2024_06_14/index.ts | 1 + .../inputs/booker-layouts.input.ts | 52 + .../inputs/booking-limits-count.input.ts | 19 +- .../inputs/booking-limits-duration.input.ts | 19 +- .../inputs/booking-window.input.ts | 62 +- .../inputs/confirmation-policy.input.ts | 108 + .../inputs/create-event-type.input.ts | 94 +- .../inputs/destination-calendar.input.ts | 18 + .../inputs/disabled.input.ts | 13 + .../inputs/event-type-color.input.ts | 26 + .../event-types_2024_06_14/inputs/index.ts | 6 + .../inputs/recurrence.input.ts | 6 +- .../inputs/seats.input.ts | 41 + .../inputs/update-event-type.input.ts | 96 +- .../outputs/event-type.output.ts | 95 +- .../transformed/event-type.tranformed.ts | 59 + .../transformed/index.ts | 1 + yarn.lock | 5868 +---------------- 68 files changed, 2859 insertions(+), 6305 deletions(-) create mode 100644 apps/api/v2/src/ee/event-types/event-types_2024_06_14/pipes/event-type-response.transformer.ts create mode 100644 apps/api/v2/src/modules/organizations/controllers/pipes/event-types/team-event-types-response.transformer.ts create mode 100644 packages/lib/event-types/transformers/api-to-internal/booker-layouts.ts create mode 100644 packages/lib/event-types/transformers/api-to-internal/confirmation-policy.ts create mode 100644 packages/lib/event-types/transformers/api-to-internal/event-colors.ts create mode 100644 packages/lib/event-types/transformers/api-to-internal/seats.ts create mode 100644 packages/lib/event-types/transformers/internal-to-api/booker-layouts.ts create mode 100644 packages/lib/event-types/transformers/internal-to-api/event-type-colors.ts create mode 100644 packages/lib/event-types/transformers/internal-to-api/requires-confirmation.ts create mode 100644 packages/lib/event-types/transformers/internal-to-api/seats.ts create mode 100644 packages/platform/enums/event-types/booker-layouts.enum.ts create mode 100644 packages/platform/enums/event-types/confirmation-policy.enum.ts create mode 100644 packages/platform/types/event-types/event-types_2024_06_14/inputs/booker-layouts.input.ts create mode 100644 packages/platform/types/event-types/event-types_2024_06_14/inputs/confirmation-policy.input.ts create mode 100644 packages/platform/types/event-types/event-types_2024_06_14/inputs/destination-calendar.input.ts create mode 100644 packages/platform/types/event-types/event-types_2024_06_14/inputs/disabled.input.ts create mode 100644 packages/platform/types/event-types/event-types_2024_06_14/inputs/event-type-color.input.ts create mode 100644 packages/platform/types/event-types/event-types_2024_06_14/inputs/seats.input.ts create mode 100644 packages/platform/types/event-types/event-types_2024_06_14/transformed/event-type.tranformed.ts create mode 100644 packages/platform/types/event-types/event-types_2024_06_14/transformed/index.ts diff --git a/apps/api/v2/package.json b/apps/api/v2/package.json index c2cad589ac..94c876b7b9 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.37", + "@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.39", "@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2", "@calcom/platform-types": "*", "@calcom/platform-utils": "*", diff --git a/apps/api/v2/src/ee/calendars/outputs/connected-calendars.output.ts b/apps/api/v2/src/ee/calendars/outputs/connected-calendars.output.ts index f084caad4a..64d9bced1c 100644 --- a/apps/api/v2/src/ee/calendars/outputs/connected-calendars.output.ts +++ b/apps/api/v2/src/ee/calendars/outputs/connected-calendars.output.ts @@ -198,7 +198,7 @@ class DestinationCalendar { integrationTitle?: string; } -class ConnectedCalendarsData { +export class ConnectedCalendarsData { @ValidateNested({ each: true }) @IsArray() connectedCalendars!: ConnectedCalendar[]; diff --git a/apps/api/v2/src/ee/calendars/services/calendars.service.ts b/apps/api/v2/src/ee/calendars/services/calendars.service.ts index ba2ce7b2a8..d19cd7a13b 100644 --- a/apps/api/v2/src/ee/calendars/services/calendars.service.ts +++ b/apps/api/v2/src/ee/calendars/services/calendars.service.ts @@ -4,7 +4,6 @@ import { CredentialsRepository, CredentialsWithUserEmail, } from "@/modules/credentials/credentials.repository"; -import { PrismaReadService } from "@/modules/prisma/prisma-read.service"; import { PrismaWriteService } from "@/modules/prisma/prisma-write.service"; import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository"; import { UsersRepository } from "@/modules/users/users.repository"; @@ -14,7 +13,6 @@ import { UnauthorizedException, NotFoundException, } from "@nestjs/common"; -import { ConfigService } from "@nestjs/config"; import { User } from "@prisma/client"; import { Prisma } from "@prisma/client"; import { DateTime } from "luxon"; @@ -34,9 +32,7 @@ export class CalendarsService { private readonly credentialsRepository: CredentialsRepository, private readonly appsRepository: AppsRepository, private readonly calendarsRepository: CalendarsRepository, - private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService, - private readonly config: ConfigService, private readonly selectedCalendarsRepository: SelectedCalendarsRepository ) {} diff --git a/apps/api/v2/src/ee/calendars/services/gcal.service.ts b/apps/api/v2/src/ee/calendars/services/gcal.service.ts index ea1d220d41..c7c282f028 100644 --- a/apps/api/v2/src/ee/calendars/services/gcal.service.ts +++ b/apps/api/v2/src/ee/calendars/services/gcal.service.ts @@ -42,7 +42,7 @@ export class GoogleCalendarService implements OAuthCalendarApp { ): Promise<{ status: typeof SUCCESS_STATUS; data: { authUrl: string } }> { const accessToken = authorization.replace("Bearer ", ""); const origin = req.get("origin") ?? req.get("host"); - const redirectUrl = await await this.getCalendarRedirectUrl(accessToken, origin ?? "", redir); + const redirectUrl = await this.getCalendarRedirectUrl(accessToken, origin ?? "", redir); return { status: SUCCESS_STATUS, data: { authUrl: redirectUrl } }; } diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/constants/constants.ts b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/constants/constants.ts index 690c1b4dec..fdd4df2624 100644 --- a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/constants/constants.ts +++ b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/constants/constants.ts @@ -1,16 +1,44 @@ +import { IntegrationLocation } from "@calcom/lib"; + +type BaseEventType = { + length: number; + slug: string; + title: string; +}; + +type EventTypeWithLocation = BaseEventType & { + locations: IntegrationLocation[]; +}; + +const thirtyMinutes: BaseEventType = { + length: 30, + slug: "thirty-minutes", + title: "30 Minutes", +}; + +const thirtyMinutesVideo: EventTypeWithLocation = { + length: 30, + slug: "thirty-minutes-video", + title: "30 Minutes", + locations: [{ type: "integrations:daily" }], +}; + +const sixtyMinutes: BaseEventType = { + length: 60, + slug: "sixty-minutes", + title: "60 Minutes", +}; + +const sixtyMinutesVideo: EventTypeWithLocation = { + length: 60, + slug: "sixty-minutes-video", + title: "60 Minutes", + locations: [{ type: "integrations:daily" }], +}; + export const DEFAULT_EVENT_TYPES = { - thirtyMinutes: { length: 30, slug: "thirty-minutes", title: "30 Minutes" }, - thirtyMinutesVideo: { - length: 30, - slug: "thirty-minutes-video", - title: "30 Minutes", - locations: [{ type: "integrations:daily" }], - }, - sixtyMinutes: { length: 60, slug: "sixty-minutes", title: "60 Minutes" }, - sixtyMinutesVideo: { - length: 60, - slug: "sixty-minutes-video", - title: "60 Minutes", - locations: [{ type: "integrations:daily" }], - }, + thirtyMinutes, + thirtyMinutesVideo, + sixtyMinutes, + sixtyMinutesVideo, }; diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/controllers/event-types.controller.e2e-spec.ts b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/controllers/event-types.controller.e2e-spec.ts index 3b8990c8b1..f2c18566ff 100644 --- a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/controllers/event-types.controller.e2e-spec.ts +++ b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/controllers/event-types.controller.e2e-spec.ts @@ -20,7 +20,13 @@ import { UserRepositoryFixture } from "test/fixtures/repository/users.repository import { withApiAuth } from "test/utils/withApiAuth"; import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_06_14 } from "@calcom/platform-constants"; -import { BookingWindowPeriodInputTypeEnum_2024_06_14, FrequencyInput } from "@calcom/platform-enums"; +import { + BookingWindowPeriodInputTypeEnum_2024_06_14, + BookerLayoutsInputEnum_2024_06_14, + ConfirmationPolicyEnum, + NoticeThresholdUnitEnum, + FrequencyInput, +} from "@calcom/platform-enums"; import { ApiSuccessResponse, CreateEventTypeInput_2024_06_14, @@ -230,11 +236,36 @@ describe("Event types Endpoints", () => { value: 30, rolling: true, }, + bookerLayouts: { + enabledLayouts: [ + BookerLayoutsInputEnum_2024_06_14.column, + BookerLayoutsInputEnum_2024_06_14.month, + BookerLayoutsInputEnum_2024_06_14.week, + ], + defaultLayout: BookerLayoutsInputEnum_2024_06_14.month, + }, + confirmationPolicy: { + type: ConfirmationPolicyEnum.TIME, + noticeThreshold: { + count: 60, + unit: NoticeThresholdUnitEnum.MINUTES, + }, + blockUnconfirmedBookingsInBooker: true, + }, recurrence: { frequency: FrequencyInput.weekly, interval: 2, occurrences: 10, + disabled: false, }, + requiresBookerEmailVerification: false, + hideCalendarNotes: false, + lockTimeZoneToggleOnBookingPage: true, + color: { + darkThemeHex: "#292929", + lightThemeHex: "#fafafa", + }, + customName: `{Event type title} between {Organiser} and {Scheduler}`, }; return request(app.getHttpServer()) @@ -257,7 +288,19 @@ describe("Event types Endpoints", () => { expect(createdEventType.bookingLimitsDuration).toEqual(body.bookingLimitsDuration); expect(createdEventType.offsetStart).toEqual(body.offsetStart); expect(createdEventType.bookingWindow).toEqual(body.bookingWindow); + expect(createdEventType.bookerLayouts).toEqual(body.bookerLayouts); + expect(createdEventType.confirmationPolicy).toEqual(body.confirmationPolicy); expect(createdEventType.recurrence).toEqual(body.recurrence); + expect(createdEventType.customName).toEqual(body.customName); + expect(createdEventType.requiresBookerEmailVerification).toEqual( + body.requiresBookerEmailVerification + ); + + expect(createdEventType.hideCalendarNotes).toEqual(body.hideCalendarNotes); + expect(createdEventType.lockTimeZoneToggleOnBookingPage).toEqual( + body.lockTimeZoneToggleOnBookingPage + ); + expect(createdEventType.color).toEqual(body.color); const responseBookingFields = body.bookingFields || []; const expectedBookingFields = [ @@ -272,6 +315,317 @@ describe("Event types Endpoints", () => { }); }); + it(`/GET/even-types by username`, async () => { + const response = await request(app.getHttpServer()) + .get(`/api/v2/event-types?username=${username}`) + .set(CAL_API_VERSION_HEADER, VERSION_2024_06_14) + // note: bearer token value mocked using "withAccessTokenAuth" for user which id is used when creating event type above + .set("Authorization", `Bearer whatever`) + .expect(200); + + const responseBody: ApiSuccessResponse = response.body; + + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + expect(responseBody.data?.length).toEqual(1); + + const fetchedEventType = responseBody.data?.[0]; + + expect(fetchedEventType?.id).toEqual(eventType.id); + expect(fetchedEventType?.title).toEqual(eventType.title); + expect(fetchedEventType?.description).toEqual(eventType.description); + expect(fetchedEventType?.lengthInMinutes).toEqual(eventType.lengthInMinutes); + expect(fetchedEventType?.locations).toEqual(eventType.locations); + expect(fetchedEventType?.bookingFields).toEqual(eventType.bookingFields); + expect(fetchedEventType?.ownerId).toEqual(user.id); + expect(fetchedEventType.bookingLimitsCount).toEqual(eventType.bookingLimitsCount); + expect(fetchedEventType.onlyShowFirstAvailableSlot).toEqual(eventType.onlyShowFirstAvailableSlot); + expect(fetchedEventType.bookingLimitsDuration).toEqual(eventType.bookingLimitsDuration); + expect(fetchedEventType.offsetStart).toEqual(eventType.offsetStart); + expect(fetchedEventType.bookingWindow).toEqual(eventType.bookingWindow); + expect(fetchedEventType.bookerLayouts).toEqual(eventType.bookerLayouts); + expect(fetchedEventType.confirmationPolicy).toEqual(eventType.confirmationPolicy); + expect(fetchedEventType.recurrence).toEqual(eventType.recurrence); + expect(fetchedEventType.customName).toEqual(eventType.customName); + expect(fetchedEventType.requiresBookerEmailVerification).toEqual( + eventType.requiresBookerEmailVerification + ); + expect(fetchedEventType.hideCalendarNotes).toEqual(eventType.hideCalendarNotes); + expect(fetchedEventType.lockTimeZoneToggleOnBookingPage).toEqual( + eventType.lockTimeZoneToggleOnBookingPage + ); + expect(fetchedEventType.color).toEqual(eventType.color); + }); + + it("should return an error when creating an event type with seats enabled and multiple locations", async () => { + const body: CreateEventTypeInput_2024_06_14 = { + title: "Coding class 2", + slug: "coding-class-2", + description: "Let's learn how to code like a pro.", + lengthInMinutes: 60, + locations: [ + { + type: "integration", + integration: "cal-video", + }, + { + type: "phone", + phone: "+37120993151", + public: true, + }, + ], + scheduleId: firstSchedule.id, + seats: { + seatsPerTimeSlot: 4, + showAttendeeInfo: true, + showAvailabilityCount: true, + }, + }; + + await request(app.getHttpServer()) + .post("/api/v2/event-types") + .set(CAL_API_VERSION_HEADER, VERSION_2024_06_14) + .send(body) + .expect(400); + }); + + it("should return an error when trying to enable seats for an event type with multiple locations", async () => { + const body: CreateEventTypeInput_2024_06_14 = { + title: "Coding class 3", + slug: "coding-class-3", + description: "Let's learn how to code like a pro.", + lengthInMinutes: 60, + locations: [ + { + type: "integration", + integration: "cal-video", + }, + { + type: "phone", + phone: "+37120993151", + public: true, + }, + ], + scheduleId: firstSchedule.id, + }; + + const createResponse = await request(app.getHttpServer()) + .post("/api/v2/event-types") + .set(CAL_API_VERSION_HEADER, VERSION_2024_06_14) + .send(body) + .expect(201); + + const createdEventType = createResponse.body.data; + + expect(createdEventType).toMatchObject({ + id: expect.any(Number), + title: body.title, + description: body.description, + lengthInMinutes: body.lengthInMinutes, + locations: body.locations, + scheduleId: firstSchedule.id, + }); + + return request(app.getHttpServer()) + .patch(`/api/v2/event-types/${createdEventType.id}`) + .set(CAL_API_VERSION_HEADER, VERSION_2024_06_14) + .send({ + seats: { + seatsPerTimeSlot: 4, + showAttendeeInfo: true, + showAvailabilityCount: true, + }, + }) + .expect(400); + }); + + it("should return an error when creating an event type with seats enabled and confirmationPolicy enabled", async () => { + const body: CreateEventTypeInput_2024_06_14 = { + title: "Coding class 4", + slug: "coding-class-4", + description: "Let's learn how to code like a pro.", + lengthInMinutes: 60, + scheduleId: firstSchedule.id, + confirmationPolicy: { + type: ConfirmationPolicyEnum.ALWAYS, + blockUnconfirmedBookingsInBooker: false, + }, + seats: { + seatsPerTimeSlot: 4, + showAttendeeInfo: true, + showAvailabilityCount: true, + }, + }; + + await request(app.getHttpServer()) + .post("/api/v2/event-types") + .set(CAL_API_VERSION_HEADER, VERSION_2024_06_14) + .send(body) + .expect(400); + }); + + it("should return an error when trying to enable seats for an event type with confirmationPolicy enabled", async () => { + const body: CreateEventTypeInput_2024_06_14 = { + title: "Coding class 5", + slug: "coding-class-5", + description: "Let's learn how to code like a pro.", + lengthInMinutes: 60, + confirmationPolicy: { + type: ConfirmationPolicyEnum.ALWAYS, + blockUnconfirmedBookingsInBooker: false, + }, + scheduleId: firstSchedule.id, + }; + + const createResponse = await request(app.getHttpServer()) + .post("/api/v2/event-types") + .set(CAL_API_VERSION_HEADER, VERSION_2024_06_14) + .send(body) + .expect(201); + + const createdEventType = createResponse.body.data; + console.log("createdEventType: ", createdEventType); + + expect(createdEventType).toMatchObject({ + id: expect.any(Number), + title: body.title, + description: body.description, + lengthInMinutes: body.lengthInMinutes, + confirmationPolicy: body.confirmationPolicy, + scheduleId: firstSchedule.id, + }); + + return request(app.getHttpServer()) + .patch(`/api/v2/event-types/${createdEventType.id}`) + .set(CAL_API_VERSION_HEADER, VERSION_2024_06_14) + .send({ + seats: { + seatsPerTimeSlot: 4, + showAttendeeInfo: true, + showAvailabilityCount: true, + }, + }) + .expect(400); + }); + + it("should return an error when trying to set multiple locations for an event type with seats enabled", async () => { + const body: CreateEventTypeInput_2024_06_14 = { + title: "Coding class 6", + slug: "coding-class-6", + description: "Let's learn how to code like a pro.", + lengthInMinutes: 60, + scheduleId: firstSchedule.id, + seats: { + seatsPerTimeSlot: 4, + showAttendeeInfo: true, + showAvailabilityCount: true, + }, + }; + + const createResponse = await request(app.getHttpServer()) + .post("/api/v2/event-types") + .set(CAL_API_VERSION_HEADER, VERSION_2024_06_14) + .send(body) + .expect(201); + + const createdEventType = createResponse.body.data; + + expect(createdEventType).toMatchObject({ + id: expect.any(Number), + title: body.title, + description: body.description, + lengthInMinutes: body.lengthInMinutes, + seats: body.seats, + scheduleId: firstSchedule.id, + }); + + return request(app.getHttpServer()) + .patch(`/api/v2/event-types/${createdEventType.id}`) + .set(CAL_API_VERSION_HEADER, VERSION_2024_06_14) + .send({ + locations: [ + { + type: "integration", + integration: "cal-video", + }, + { + type: "phone", + phone: "+37120993151", + public: true, + }, + ], + }) + .expect(400); + }); + + it("should return an error when creating an event type with confirmationPolicy enabled and seats enabled", async () => { + const body: CreateEventTypeInput_2024_06_14 = { + title: "Coding class 7", + slug: "coding-class-7", + description: "Let's learn how to code like a pro.", + lengthInMinutes: 60, + confirmationPolicy: { + type: ConfirmationPolicyEnum.ALWAYS, + blockUnconfirmedBookingsInBooker: false, + }, + scheduleId: firstSchedule.id, + seats: { + seatsPerTimeSlot: 4, + showAttendeeInfo: true, + showAvailabilityCount: true, + }, + }; + + await request(app.getHttpServer()) + .post("/api/v2/event-types") + .set(CAL_API_VERSION_HEADER, VERSION_2024_06_14) + .send(body) + .expect(400); + }); + + it("should return an error when trying to enable confirmationPolicy for an event type with seats enabled", async () => { + const body: CreateEventTypeInput_2024_06_14 = { + title: "Coding class 8", + slug: "coding-class-8", + description: "Let's learn how to code like a pro.", + lengthInMinutes: 60, + seats: { + seatsPerTimeSlot: 4, + showAttendeeInfo: true, + showAvailabilityCount: true, + }, + scheduleId: firstSchedule.id, + }; + + const createResponse = await request(app.getHttpServer()) + .post("/api/v2/event-types") + .set(CAL_API_VERSION_HEADER, VERSION_2024_06_14) + .send(body) + .expect(201); + + const createdEventType = createResponse.body.data; + + expect(createdEventType).toMatchObject({ + id: expect.any(Number), + title: body.title, + description: body.description, + lengthInMinutes: body.lengthInMinutes, + seats: body.seats, + scheduleId: firstSchedule.id, + }); + + return request(app.getHttpServer()) + .patch(`/api/v2/event-types/${createdEventType.id}`) + .set(CAL_API_VERSION_HEADER, VERSION_2024_06_14) + .send({ + confirmationPolicy: { + type: ConfirmationPolicyEnum.ALWAYS, + blockUnconfirmedBookingsInBooker: false, + }, + }) + .expect(400); + }); + it("should update event type", async () => { const newTitle = "Coding class in Italian!"; @@ -293,11 +647,32 @@ describe("Event types Endpoints", () => { value: 40, rolling: false, }, + bookerLayouts: { + enabledLayouts: [ + BookerLayoutsInputEnum_2024_06_14.column, + BookerLayoutsInputEnum_2024_06_14.month, + BookerLayoutsInputEnum_2024_06_14.week, + ], + defaultLayout: BookerLayoutsInputEnum_2024_06_14.month, + }, + confirmationPolicy: { + type: ConfirmationPolicyEnum.ALWAYS, + blockUnconfirmedBookingsInBooker: false, + }, recurrence: { frequency: FrequencyInput.monthly, interval: 4, occurrences: 10, + disabled: false, }, + requiresBookerEmailVerification: true, + hideCalendarNotes: true, + lockTimeZoneToggleOnBookingPage: true, + color: { + darkThemeHex: "#292929", + lightThemeHex: "#fafafa", + }, + customName: `{Event type title} betweennnnnnnnnnn {Organiser} and {Scheduler}`, }; return request(app.getHttpServer()) @@ -323,7 +698,18 @@ describe("Event types Endpoints", () => { expect(updatedEventType.bookingLimitsDuration).toEqual(body.bookingLimitsDuration); expect(updatedEventType.offsetStart).toEqual(body.offsetStart); expect(updatedEventType.bookingWindow).toEqual(body.bookingWindow); + expect(updatedEventType.bookerLayouts).toEqual(body.bookerLayouts); + expect(updatedEventType.confirmationPolicy).toEqual(body.confirmationPolicy); expect(updatedEventType.recurrence).toEqual(body.recurrence); + expect(updatedEventType.customName).toEqual(body.customName); + expect(updatedEventType.requiresBookerEmailVerification).toEqual( + body.requiresBookerEmailVerification + ); + expect(updatedEventType.hideCalendarNotes).toEqual(body.hideCalendarNotes); + expect(updatedEventType.lockTimeZoneToggleOnBookingPage).toEqual( + body.lockTimeZoneToggleOnBookingPage + ); + expect(updatedEventType.color).toEqual(body.color); eventType.title = newTitle; eventType.scheduleId = secondSchedule.id; @@ -332,7 +718,14 @@ describe("Event types Endpoints", () => { eventType.bookingLimitsDuration = updatedEventType.bookingLimitsDuration; eventType.offsetStart = updatedEventType.offsetStart; eventType.bookingWindow = updatedEventType.bookingWindow; + eventType.bookerLayouts = updatedEventType.bookerLayouts; + eventType.confirmationPolicy = updatedEventType.confirmationPolicy; eventType.recurrence = updatedEventType.recurrence; + eventType.customName = updatedEventType.customName; + eventType.requiresBookerEmailVerification = updatedEventType.requiresBookerEmailVerification; + eventType.hideCalendarNotes = updatedEventType.hideCalendarNotes; + eventType.lockTimeZoneToggleOnBookingPage = updatedEventType.lockTimeZoneToggleOnBookingPage; + eventType.color = updatedEventType.color; }); }); @@ -373,38 +766,18 @@ describe("Event types Endpoints", () => { expect(fetchedEventType.bookingLimitsDuration).toEqual(eventType.bookingLimitsDuration); expect(fetchedEventType.offsetStart).toEqual(eventType.offsetStart); expect(fetchedEventType.bookingWindow).toEqual(eventType.bookingWindow); + expect(fetchedEventType.bookerLayouts).toEqual(eventType.bookerLayouts); + expect(fetchedEventType.confirmationPolicy).toEqual(eventType.confirmationPolicy); expect(fetchedEventType.recurrence).toEqual(eventType.recurrence); - }); - - it(`/GET/even-types by username`, async () => { - const response = await request(app.getHttpServer()) - .get(`/api/v2/event-types?username=${username}`) - .set(CAL_API_VERSION_HEADER, VERSION_2024_06_14) - // note: bearer token value mocked using "withAccessTokenAuth" for user which id is used when creating event type above - .set("Authorization", `Bearer whatever`) - .expect(200); - - const responseBody: ApiSuccessResponse = response.body; - - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); - expect(responseBody.data?.length).toEqual(1); - - const fetchedEventType = responseBody.data?.[0]; - - expect(fetchedEventType?.id).toEqual(eventType.id); - expect(fetchedEventType?.title).toEqual(eventType.title); - expect(fetchedEventType?.description).toEqual(eventType.description); - expect(fetchedEventType?.lengthInMinutes).toEqual(eventType.lengthInMinutes); - expect(fetchedEventType?.locations).toEqual(eventType.locations); - expect(fetchedEventType?.bookingFields).toEqual(eventType.bookingFields); - expect(fetchedEventType?.ownerId).toEqual(user.id); - expect(fetchedEventType.bookingLimitsCount).toEqual(eventType.bookingLimitsCount); - expect(fetchedEventType.onlyShowFirstAvailableSlot).toEqual(eventType.onlyShowFirstAvailableSlot); - expect(fetchedEventType.bookingLimitsDuration).toEqual(eventType.bookingLimitsDuration); - expect(fetchedEventType.offsetStart).toEqual(eventType.offsetStart); - expect(fetchedEventType.bookingWindow).toEqual(eventType.bookingWindow); - expect(fetchedEventType.recurrence).toEqual(eventType.recurrence); + expect(fetchedEventType.customName).toEqual(eventType.customName); + expect(fetchedEventType.requiresBookerEmailVerification).toEqual( + eventType.requiresBookerEmailVerification + ); + expect(fetchedEventType.hideCalendarNotes).toEqual(eventType.hideCalendarNotes); + expect(fetchedEventType.lockTimeZoneToggleOnBookingPage).toEqual( + eventType.lockTimeZoneToggleOnBookingPage + ); + expect(fetchedEventType.color).toEqual(eventType.color); }); it(`/GET/event-types by username and eventSlug`, async () => { @@ -430,7 +803,18 @@ describe("Event types Endpoints", () => { expect(fetchedEventType.bookingLimitsDuration).toEqual(eventType.bookingLimitsDuration); expect(fetchedEventType.offsetStart).toEqual(eventType.offsetStart); expect(fetchedEventType.bookingWindow).toEqual(eventType.bookingWindow); + expect(fetchedEventType.bookerLayouts).toEqual(eventType.bookerLayouts); + expect(fetchedEventType.confirmationPolicy).toEqual(eventType.confirmationPolicy); expect(fetchedEventType.recurrence).toEqual(eventType.recurrence); + expect(fetchedEventType.customName).toEqual(eventType.customName); + expect(fetchedEventType.requiresBookerEmailVerification).toEqual( + eventType.requiresBookerEmailVerification + ); + expect(fetchedEventType.hideCalendarNotes).toEqual(eventType.hideCalendarNotes); + expect(fetchedEventType.lockTimeZoneToggleOnBookingPage).toEqual( + eventType.lockTimeZoneToggleOnBookingPage + ); + expect(fetchedEventType.color).toEqual(eventType.color); }); it(`/GET/:id not existing`, async () => { diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/controllers/event-types.controller.ts b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/controllers/event-types.controller.ts index f6f69ff52a..01a366d0af 100644 --- a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/controllers/event-types.controller.ts +++ b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/controllers/event-types.controller.ts @@ -3,7 +3,9 @@ import { DeleteEventTypeOutput_2024_06_14 } from "@/ee/event-types/event-types_2 import { GetEventTypeOutput_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/outputs/get-event-type.output"; import { GetEventTypesOutput_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/outputs/get-event-types.output"; import { UpdateEventTypeOutput_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/outputs/update-event-type.output"; +import { EventTypeResponseTransformPipe } from "@/ee/event-types/event-types_2024_06_14/pipes/event-type-response.transformer"; import { EventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/event-types.service"; +import { InputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/input-event-types.service"; import { VERSION_2024_06_14_VALUE } from "@/lib/api-versions"; import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator"; import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator"; @@ -23,14 +25,15 @@ import { HttpStatus, Delete, Query, + ParseIntPipe, } from "@nestjs/common"; import { ApiHeader, ApiTags as DocsTags } from "@nestjs/swagger"; import { EVENT_TYPE_READ, EVENT_TYPE_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants"; import { - CreateEventTypeInput_2024_06_14, UpdateEventTypeInput_2024_06_14, GetEventTypesQuery_2024_06_14, + CreateEventTypeInput_2024_06_14, } from "@calcom/platform-types"; @Controller({ @@ -45,7 +48,11 @@ import { required: true, }) export class EventTypesController_2024_06_14 { - constructor(private readonly eventTypesService: EventTypesService_2024_06_14) {} + constructor( + private readonly eventTypesService: EventTypesService_2024_06_14, + private readonly inputEventTypesService: InputEventTypesService_2024_06_14, + private readonly eventTypeResponseTransformPipe: EventTypeResponseTransformPipe + ) {} @Post("/") @Permissions([EVENT_TYPE_WRITE]) @@ -60,11 +67,16 @@ export class EventTypesController_2024_06_14 { @Body() body: CreateEventTypeInput_2024_06_14, @GetUser() user: UserWithProfile ): Promise { - const eventType = await this.eventTypesService.createUserEventType(user, body); + const transformedBody = await this.inputEventTypesService.transformAndValidateCreateEventTypeInput( + user.id, + body + ); + + const eventType = await this.eventTypesService.createUserEventType(user, transformedBody); return { status: SUCCESS_STATUS, - data: eventType, + data: this.eventTypeResponseTransformPipe.transform(eventType), }; } @@ -89,7 +101,7 @@ export class EventTypesController_2024_06_14 { return { status: SUCCESS_STATUS, - data: eventType, + data: this.eventTypeResponseTransformPipe.transform(eventType), }; } @@ -101,7 +113,7 @@ export class EventTypesController_2024_06_14 { return { status: SUCCESS_STATUS, - data: eventTypes, + data: this.eventTypeResponseTransformPipe.transform(eventTypes), }; } @@ -116,15 +128,21 @@ export class EventTypesController_2024_06_14 { }) @HttpCode(HttpStatus.OK) async updateEventType( - @Param("eventTypeId") eventTypeId: number, + @Param("eventTypeId", ParseIntPipe) eventTypeId: number, @Body() body: UpdateEventTypeInput_2024_06_14, @GetUser() user: UserWithProfile ): Promise { - const eventType = await this.eventTypesService.updateEventType(eventTypeId, body, user); + const transformedBody = await this.inputEventTypesService.transformAndValidateUpdateEventTypeInput( + body, + user.id, + eventTypeId + ); + + const eventType = await this.eventTypesService.updateEventType(eventTypeId, transformedBody, user); return { status: SUCCESS_STATUS, - data: eventType, + data: this.eventTypeResponseTransformPipe.transform(eventType), }; } diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/event-types.module.ts b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/event-types.module.ts index 64dae68d31..23b8cda8e3 100644 --- a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/event-types.module.ts +++ b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/event-types.module.ts @@ -1,9 +1,14 @@ +import { CalendarsRepository } from "@/ee/calendars/calendars.repository"; +import { CalendarsService } from "@/ee/calendars/services/calendars.service"; import { EventTypesController_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/controllers/event-types.controller"; import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository"; +import { EventTypeResponseTransformPipe } from "@/ee/event-types/event-types_2024_06_14/pipes/event-type-response.transformer"; import { EventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/event-types.service"; import { InputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/input-event-types.service"; import { OutputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/output-event-types.service"; import { SchedulesRepository_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/schedules.repository"; +import { AppsRepository } from "@/modules/apps/apps.repository"; +import { CredentialsRepository } from "@/modules/credentials/credentials.repository"; import { MembershipsModule } from "@/modules/memberships/memberships.module"; import { PrismaModule } from "@/modules/prisma/prisma.module"; import { SelectedCalendarsModule } from "@/modules/selected-calendars/selected-calendars.module"; @@ -22,6 +27,11 @@ import { Module } from "@nestjs/common"; UsersRepository, UsersService, SchedulesRepository_2024_06_11, + EventTypeResponseTransformPipe, + CalendarsService, + CredentialsRepository, + AppsRepository, + CalendarsRepository, ], controllers: [EventTypesController_2024_06_14], exports: [ 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 1c0ce0f0c5..f789049ff4 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 @@ -4,36 +4,10 @@ import { UsersService } from "@/modules/users/services/users.service"; import { UserWithProfile } from "@/modules/users/users.repository"; import { Injectable } from "@nestjs/common"; -import { - getEventTypeById, - transformBookingFieldsApiToInternal, - transformLocationsApiToInternal, -} from "@calcom/platform-libraries"; -import { CreateEventTypeInput_2024_06_14 } from "@calcom/platform-types"; +import { getEventTypeById } from "@calcom/platform-libraries"; +import { InputEventTransformed_2024_06_14 } from "@calcom/platform-types"; import type { PrismaClient } from "@calcom/prisma"; -type InputEventTransformed = Omit< - CreateEventTypeInput_2024_06_14, - | "lengthInMinutes" - | "locations" - | "bookingFields" - | "bookingLimitsCount" - | "onlyShowFirstAvailableSlot" - | "bookingLimitsDuration" - | "offsetStart" - | "periodType" - | "periodDays" - | "periodCountCalendarDays" - | "periodStartDate" - | "periodEndDate" - | "recurrence" -> & { - length: number; - slug: string; - locations?: ReturnType; - bookingFields?: ReturnType; -}; - @Injectable() export class EventTypesRepository_2024_06_14 { constructor( @@ -42,7 +16,10 @@ export class EventTypesRepository_2024_06_14 { private usersService: UsersService ) {} - async createUserEventType(userId: number, body: InputEventTransformed) { + async createUserEventType( + userId: number, + body: Omit + ) { return this.dbWrite.prisma.eventType.create({ data: { ...body, @@ -57,7 +34,19 @@ export class EventTypesRepository_2024_06_14 { async getEventTypeWithSeats(eventTypeId: number) { return this.dbRead.prisma.eventType.findUnique({ where: { id: eventTypeId }, - select: { users: { select: { id: true } }, seatsPerTimeSlot: true }, + select: { + users: { select: { id: true } }, + seatsPerTimeSlot: true, + locations: true, + requiresConfirmation: true, + }, + }); + } + + async getEventTypeWithMetaData(eventTypeId: number) { + return this.dbRead.prisma.eventType.findUnique({ + where: { id: eventTypeId }, + select: { metadata: true }, }); } @@ -67,7 +56,7 @@ export class EventTypesRepository_2024_06_14 { id: eventTypeId, userId, }, - include: { users: true, schedule: true }, + include: { users: true, schedule: true, destinationCalendar: true }, }); } @@ -76,7 +65,7 @@ export class EventTypesRepository_2024_06_14 { where: { userId, }, - include: { users: true, schedule: true }, + include: { users: true, schedule: true, destinationCalendar: true }, }); } @@ -98,7 +87,7 @@ export class EventTypesRepository_2024_06_14 { async getEventTypeById(eventTypeId: number) { return this.dbRead.prisma.eventType.findUnique({ where: { id: eventTypeId }, - include: { users: true, schedule: true }, + include: { users: true, schedule: true, destinationCalendar: true }, }); } @@ -117,7 +106,7 @@ export class EventTypesRepository_2024_06_14 { slug: slug, }, }, - include: { users: true, schedule: true }, + include: { users: true, schedule: true, destinationCalendar: true }, }); } diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/pipes/event-type-response.transformer.ts b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/pipes/event-type-response.transformer.ts new file mode 100644 index 0000000000..971acae8f0 --- /dev/null +++ b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/pipes/event-type-response.transformer.ts @@ -0,0 +1,39 @@ +import { Injectable, PipeTransform } from "@nestjs/common"; +import { plainToClass } from "class-transformer"; + +import { EventTypeOutput_2024_06_14 } from "@calcom/platform-types"; + +import { + DatabaseEventType, + OutputEventTypesService_2024_06_14, +} from "../services/output-event-types.service"; + +type EventTypeResponse = DatabaseEventType & { ownerId: number }; + +@Injectable() +export class EventTypeResponseTransformPipe implements PipeTransform { + constructor(private readonly outputEventTypesService: OutputEventTypesService_2024_06_14) {} + + private transformEventType(eventType: EventTypeResponse): EventTypeOutput_2024_06_14 { + return plainToClass( + EventTypeOutput_2024_06_14, + this.outputEventTypesService.getResponseEventType(eventType.ownerId, eventType), + { strategy: "exposeAll" } + ); + } + + // Implementing function overloading to ensure correct return types based on input type: + transform(value: EventTypeResponse[]): EventTypeOutput_2024_06_14[]; + + transform(value: EventTypeResponse): EventTypeOutput_2024_06_14; + + transform( + value: EventTypeResponse | EventTypeResponse[] + ): EventTypeOutput_2024_06_14 | EventTypeOutput_2024_06_14[] { + if (Array.isArray(value)) { + return value.map((item) => this.transformEventType(item)); + } else { + return this.transformEventType(value); + } + } +} 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 ea800cd87d..7ffb3cf8b5 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 @@ -1,7 +1,5 @@ import { DEFAULT_EVENT_TYPES } from "@/ee/event-types/event-types_2024_06_14/constants/constants"; import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository"; -import { InputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/input-event-types.service"; -import { OutputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/output-event-types.service"; import { SchedulesRepository_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/schedules.repository"; import { MembershipsRepository } from "@/modules/memberships/memberships.repository"; import { PrismaWriteService } from "@/modules/prisma/prisma-write.service"; @@ -13,20 +11,13 @@ import { BadRequestException, ForbiddenException, Injectable, NotFoundException import { createEventType, updateEventType } from "@calcom/platform-libraries"; import { getEventTypesPublic, EventTypesPublic } from "@calcom/platform-libraries"; import { dynamicEvent } from "@calcom/platform-libraries"; -import { - CreateEventTypeInput_2024_06_14, - UpdateEventTypeInput_2024_06_14, - GetEventTypesQuery_2024_06_14, - EventTypeOutput_2024_06_14, -} from "@calcom/platform-types"; +import { GetEventTypesQuery_2024_06_14, InputEventTransformed_2024_06_14 } from "@calcom/platform-types"; import { EventType } from "@calcom/prisma/client"; @Injectable() export class EventTypesService_2024_06_14 { constructor( private readonly eventTypesRepository: EventTypesRepository_2024_06_14, - private readonly inputEventTypesService: InputEventTypesService_2024_06_14, - private readonly outputEventTypesService: OutputEventTypesService_2024_06_14, private readonly membershipsRepository: MembershipsRepository, private readonly usersRepository: UsersRepository, private readonly usersService: UsersService, @@ -35,22 +26,13 @@ export class EventTypesService_2024_06_14 { private readonly schedulesRepository: SchedulesRepository_2024_06_11 ) {} - async createUserEventType(user: UserWithProfile, body: CreateEventTypeInput_2024_06_14) { + async createUserEventType(user: UserWithProfile, body: InputEventTransformed_2024_06_14) { await this.checkCanCreateEventType(user.id, body); const eventTypeUser = await this.getUserToCreateEvent(user); - const { - bookingLimits, - durationLimits, - periodType = undefined, - periodDays = undefined, - periodCountCalendarDays = undefined, - periodStartDate = undefined, - periodEndDate = undefined, - recurrence = undefined, - ...bodyTransformed - } = this.inputEventTypesService.transformInputCreateEventType(body); + const { destinationCalendar: _destinationCalendar, ...rest } = body; + const { eventType: eventTypeCreated } = await createEventType({ - input: bodyTransformed, + input: rest, ctx: { user: eventTypeUser, // eslint-disable-next-line @typescript-eslint/ban-ts-comment @@ -62,15 +44,7 @@ export class EventTypesService_2024_06_14 { await updateEventType({ input: { id: eventTypeCreated.id, - bookingLimits, - durationLimits, - periodType, - periodDays, - periodCountCalendarDays, - periodStartDate, - periodEndDate, - recurrence, - ...bodyTransformed, + ...body, }, ctx: { user: eventTypeUser, @@ -86,10 +60,13 @@ export class EventTypesService_2024_06_14 { throw new NotFoundException(`Event type with id ${eventTypeCreated.id} not found`); } - return this.outputEventTypesService.getResponseEventType(user.id, eventType); + return { + ownerId: user.id, + ...eventType, + }; } - async checkCanCreateEventType(userId: number, body: CreateEventTypeInput_2024_06_14) { + async checkCanCreateEventType(userId: number, body: InputEventTransformed_2024_06_14) { const existsWithSlug = await this.eventTypesRepository.getUserEventTypeBySlug(userId, body.slug); if (existsWithSlug) { throw new BadRequestException("User already has an event type with this slug."); @@ -109,7 +86,10 @@ export class EventTypesService_2024_06_14 { return null; } - return this.outputEventTypesService.getResponseEventType(user.id, eventType); + return { + ownerId: user.id, + ...eventType, + }; } async getEventTypesByUsername(username: string) { @@ -117,7 +97,7 @@ export class EventTypesService_2024_06_14 { if (!user) { return []; } - return this.getUserEventTypes(user.id); + return await this.getUserEventTypes(user.id); } async getUserToCreateEvent(user: UserWithProfile) { @@ -147,17 +127,19 @@ export class EventTypesService_2024_06_14 { } this.checkUserOwnsEventType(userId, eventType); - return this.outputEventTypesService.getResponseEventType(userId, eventType); + + return { + ownerId: userId, + ...eventType, + }; } async getUserEventTypes(userId: number) { const eventTypes = await this.eventTypesRepository.getUserEventTypes(userId); - const eventTypePromises = eventTypes.map(async (eventType) => { - return await this.outputEventTypesService.getResponseEventType(userId, eventType); + return eventTypes.map((eventType) => { + return { ownerId: userId, ...eventType }; }); - - return await Promise.all(eventTypePromises); } async getUserEventTypeForAtom(user: UserWithProfile, eventTypeId: number) { @@ -190,7 +172,7 @@ export class EventTypesService_2024_06_14 { return await getEventTypesPublic(user.id); } - async getEventTypes(queryParams: GetEventTypesQuery_2024_06_14): Promise { + async getEventTypes(queryParams: GetEventTypesQuery_2024_06_14) { const { username, eventSlug, usernames } = queryParams; if (username && eventSlug) { @@ -218,12 +200,12 @@ export class EventTypesService_2024_06_14 { usersFiltered.push(user); } } - - return this.outputEventTypesService.getResponseEventType(0, { + return { + ownerId: 0, ...dynamicEvent, users: usersFiltered, isInstantEvent: false, - }); + }; } async createUserDefaultEventTypes(userId: number) { @@ -239,12 +221,12 @@ export class EventTypesService_2024_06_14 { return defaultEventTypes; } - async updateEventType(eventTypeId: number, body: UpdateEventTypeInput_2024_06_14, user: UserWithProfile) { + async updateEventType(eventTypeId: number, body: InputEventTransformed_2024_06_14, user: UserWithProfile) { await this.checkCanUpdateEventType(user.id, eventTypeId, body.scheduleId); const eventTypeUser = await this.getUserToUpdateEvent(user); - const bodyTransformed = this.inputEventTypesService.transformInputUpdateEventType(body); + await updateEventType({ - input: { id: eventTypeId, ...bodyTransformed }, + input: { id: eventTypeId, ...body }, ctx: { user: eventTypeUser, // eslint-disable-next-line @typescript-eslint/ban-ts-comment @@ -259,7 +241,10 @@ export class EventTypesService_2024_06_14 { throw new NotFoundException(`Event type with id ${eventTypeId} not found`); } - return this.outputEventTypesService.getResponseEventType(user.id, eventType); + return { + ownerId: user.id, + ...eventType, + }; } async checkCanUpdateEventType(userId: number, eventTypeId: number, scheduleId: number | undefined) { 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 b604b3632a..7b2fcb968c 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,8 @@ -import { Injectable } from "@nestjs/common"; +import { ConnectedCalendarsData } from "@/ee/calendars/outputs/connected-calendars.output"; +import { CalendarsService } from "@/ee/calendars/services/calendars.service"; +import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository"; +import { UserWithProfile } from "@/modules/users/users.repository"; +import { Injectable, BadRequestException } from "@nestjs/common"; import { transformBookingFieldsApiToInternal, @@ -9,12 +13,85 @@ import { systemBeforeFieldName, systemBeforeFieldEmail, systemAfterFieldRescheduleReason, + EventTypeMetaDataSchema, + systemBeforeFieldLocation, + transformBookerLayoutsApiToInternal, + transformConfirmationPolicyApiToInternal, + transformEventColorsApiToInternal, + validateCustomEventName, + transformSeatsApiToInternal, } from "@calcom/platform-libraries"; -import { systemBeforeFieldLocation } from "@calcom/platform-libraries"; -import { CreateEventTypeInput_2024_06_14, UpdateEventTypeInput_2024_06_14 } from "@calcom/platform-types"; +import { + CreateEventTypeInput_2024_06_14, + DestinationCalendar_2024_06_14, + InputEventTransformed_2024_06_14, + UpdateEventTypeInput_2024_06_14, +} from "@calcom/platform-types"; + +import { OutputEventTypesService_2024_06_14 } from "./output-event-types.service"; + +interface ValidationContext { + eventTypeId?: number; + seatsPerTimeSlot?: number | null; + locations?: InputEventTransformed_2024_06_14["locations"]; + requiresConfirmation?: boolean; + eventName?: string; +} @Injectable() export class InputEventTypesService_2024_06_14 { + constructor( + private readonly eventTypesRepository: EventTypesRepository_2024_06_14, + private readonly outputEventTypesService: OutputEventTypesService_2024_06_14, + private readonly calendarsService: CalendarsService + ) {} + + async transformAndValidateCreateEventTypeInput( + userId: UserWithProfile["id"], + inputEventType: CreateEventTypeInput_2024_06_14 + ) { + const transformedBody = this.transformInputCreateEventType(inputEventType); + + await this.validateEventTypeInputs({ + seatsPerTimeSlot: transformedBody.seatsPerTimeSlot, + locations: transformedBody.locations, + requiresConfirmation: transformedBody.requiresConfirmation, + eventName: transformedBody.eventName, + }); + + transformedBody.destinationCalendar && + (await this.validateInputDestinationCalendar(userId, transformedBody.destinationCalendar)); + + transformedBody.useEventTypeDestinationCalendarEmail && + (await this.validateInputUseDestinationCalendarEmail(userId)); + + return transformedBody; + } + + async transformAndValidateUpdateEventTypeInput( + inputEventType: UpdateEventTypeInput_2024_06_14, + userId: UserWithProfile["id"], + eventTypeId: number + ) { + const transformedBody = await this.transformInputUpdateEventType(inputEventType, eventTypeId); + + await this.validateEventTypeInputs({ + eventTypeId: eventTypeId, + seatsPerTimeSlot: transformedBody.seatsPerTimeSlot, + locations: transformedBody.locations, + requiresConfirmation: transformedBody.requiresConfirmation, + eventName: transformedBody.eventName, + }); + + transformedBody.destinationCalendar && + (await this.validateInputDestinationCalendar(userId, transformedBody.destinationCalendar)); + + transformedBody.useEventTypeDestinationCalendarEmail && + (await this.validateInputUseDestinationCalendarEmail(userId)); + + return transformedBody; + } + transformInputCreateEventType(inputEventType: CreateEventTypeInput_2024_06_14) { const defaultLocations: CreateEventTypeInput_2024_06_14["locations"] = [ { @@ -30,9 +107,16 @@ export class InputEventTypesService_2024_06_14 { bookingLimitsCount, bookingLimitsDuration, bookingWindow, + bookerLayouts, + confirmationPolicy, + color, recurrence, + seats, + customName, + useDestinationCalendarEmail, ...rest } = inputEventType; + const confirmationPolicyTransformed = this.transformInputConfirmationPolicy(confirmationPolicy); const hasMultipleLocations = (locations || defaultLocations).length > 1; const eventType = { @@ -45,25 +129,47 @@ export class InputEventTypesService_2024_06_14 { ? this.transformInputIntervalLimits(bookingLimitsDuration) : undefined, ...this.transformInputBookingWindow(bookingWindow), + metadata: { + bookerLayouts: this.transformInputBookerLayouts(bookerLayouts), + requiresConfirmationThreshold: + confirmationPolicyTransformed?.requiresConfirmationThreshold ?? undefined, + }, + requiresConfirmation: confirmationPolicyTransformed?.requiresConfirmation ?? undefined, + requiresConfirmationWillBlockSlot: + confirmationPolicyTransformed?.requiresConfirmationWillBlockSlot ?? undefined, + eventTypeColor: this.transformInputEventTypeColor(color), recurringEvent: recurrence ? this.transformInputRecurrignEvent(recurrence) : undefined, + ...this.transformInputSeatOptions(seats), + eventName: customName, + useEventTypeDestinationCalendarEmail: useDestinationCalendarEmail, }; return eventType; } - transformInputUpdateEventType(inputEventType: UpdateEventTypeInput_2024_06_14) { + async transformInputUpdateEventType(inputEventType: UpdateEventTypeInput_2024_06_14, eventTypeId: number) { const { lengthInMinutes, locations, bookingFields, - scheduleId, bookingLimitsCount, bookingLimitsDuration, bookingWindow, + bookerLayouts, + confirmationPolicy, + color, recurrence, + seats, + customName, + useDestinationCalendarEmail, ...rest } = inputEventType; + const eventTypeDb = await this.eventTypesRepository.getEventTypeWithMetaData(eventTypeId); + const metadataTransformed = !!eventTypeDb?.metadata + ? EventTypeMetaDataSchema.parse(eventTypeDb.metadata) + : {}; + const confirmationPolicyTransformed = this.transformInputConfirmationPolicy(confirmationPolicy); const hasMultipleLocations = !!(locations && locations?.length > 1); const eventType = { @@ -73,13 +179,25 @@ export class InputEventTypesService_2024_06_14 { bookingFields: bookingFields ? this.transformInputBookingFields(bookingFields, hasMultipleLocations) : undefined, - schedule: scheduleId, bookingLimits: bookingLimitsCount ? this.transformInputIntervalLimits(bookingLimitsCount) : undefined, durationLimits: bookingLimitsDuration ? this.transformInputIntervalLimits(bookingLimitsDuration) : undefined, ...this.transformInputBookingWindow(bookingWindow), + metadata: { + ...metadataTransformed, + bookerLayouts: this.transformInputBookerLayouts(bookerLayouts), + requiresConfirmationThreshold: + confirmationPolicyTransformed?.requiresConfirmationThreshold ?? undefined, + }, recurringEvent: recurrence ? this.transformInputRecurrignEvent(recurrence) : undefined, + requiresConfirmation: confirmationPolicyTransformed?.requiresConfirmation ?? undefined, + requiresConfirmationWillBlockSlot: + confirmationPolicyTransformed?.requiresConfirmationWillBlockSlot ?? undefined, + eventTypeColor: this.transformInputEventTypeColor(color), + ...this.transformInputSeatOptions(seats), + eventName: customName, + useEventTypeDestinationCalendarEmail: useDestinationCalendarEmail, }; return eventType; @@ -114,7 +232,142 @@ export class InputEventTypesService_2024_06_14 { return !!res ? res : {}; } + transformInputBookerLayouts(inputBookerLayouts: CreateEventTypeInput_2024_06_14["bookerLayouts"]) { + return transformBookerLayoutsApiToInternal(inputBookerLayouts); + } + + transformInputConfirmationPolicy( + requiresConfirmation: CreateEventTypeInput_2024_06_14["confirmationPolicy"] + ) { + return transformConfirmationPolicyApiToInternal(requiresConfirmation); + } transformInputRecurrignEvent(recurrence: CreateEventTypeInput_2024_06_14["recurrence"]) { return transformRecurrenceApiToInternal(recurrence); } + + transformInputEventTypeColor(color: CreateEventTypeInput_2024_06_14["color"]) { + return transformEventColorsApiToInternal(color); + } + + transformInputSeatOptions(seats: CreateEventTypeInput_2024_06_14["seats"]) { + return transformSeatsApiToInternal(seats); + } + async validateEventTypeInputs({ + eventTypeId, + seatsPerTimeSlot, + locations, + requiresConfirmation, + eventName, + }: ValidationContext) { + let seatsPerTimeSlotDb: number | null = null; + let locationsDb: ReturnType = []; + let requiresConfirmationDb = false; + + if (eventTypeId != null) { + const eventTypeDb = await this.eventTypesRepository.getEventTypeWithSeats(eventTypeId); + seatsPerTimeSlotDb = eventTypeDb?.seatsPerTimeSlot ?? null; + locationsDb = this.outputEventTypesService.transformLocations(eventTypeDb?.locations) ?? []; + requiresConfirmationDb = eventTypeDb?.requiresConfirmation ?? false; + } + + const seatsPerTimeSlotFinal = seatsPerTimeSlot !== undefined ? seatsPerTimeSlot : seatsPerTimeSlotDb; + const seatsEnabledFinal = seatsPerTimeSlotFinal != null && seatsPerTimeSlotFinal > 0; + + const locationsFinal = locations !== undefined ? locations : locationsDb; + const requiresConfirmationFinal = + requiresConfirmation !== undefined ? requiresConfirmation : requiresConfirmationDb; + + this.validateSeatsSingleLocationRule(seatsEnabledFinal, locationsFinal); + this.validateSeatsRequiresConfirmationFalseRule(seatsEnabledFinal, requiresConfirmationFinal); + this.validateMultipleLocationsSeatsDisabledRule(locationsFinal, seatsEnabledFinal); + this.validateRequiresConfirmationSeatsDisabledRule(requiresConfirmationFinal, seatsEnabledFinal); + + if (eventName) { + await this.validateCustomEventNameInput(eventName); + } + } + validateSeatsSingleLocationRule( + seatsEnabled: boolean, + locations: ReturnType + ) { + if (seatsEnabled && locations.length > 1) { + throw new BadRequestException( + "Seats Validation failed: Seats are enabled but more than one location provided." + ); + } + } + + validateSeatsRequiresConfirmationFalseRule(seatsEnabled: boolean, requiresConfirmation: boolean) { + if (seatsEnabled && requiresConfirmation) { + throw new BadRequestException( + "Seats Validation failed: Seats are enabled but requiresConfirmation is true." + ); + } + } + + validateMultipleLocationsSeatsDisabledRule( + locations: ReturnType, + seatsEnabled: boolean + ) { + if (locations.length > 1 && seatsEnabled) { + throw new BadRequestException("Locations Validation failed: Multiple locations but seats are enabled."); + } + } + + validateRequiresConfirmationSeatsDisabledRule(requiresConfirmation: boolean, seatsEnabled: boolean) { + if (requiresConfirmation && seatsEnabled) { + throw new BadRequestException( + "RequiresConfirmation Validation failed: Seats are enabled but requiresConfirmation is true." + ); + } + } + + async validateCustomEventNameInput(value: string) { + const validationResult = validateCustomEventName(value); + if (validationResult !== true) { + throw new BadRequestException(`Invalid event name variables: ${validationResult}`); + } + return; + } + + async validateInputDestinationCalendar( + userId: number, + destinationCalendar: DestinationCalendar_2024_06_14 + ) { + const calendars: ConnectedCalendarsData = await this.calendarsService.getCalendars(userId); + + const allCals = calendars.connectedCalendars.map((cal) => cal.calendars ?? []).flat(); + + const matchedCalendar = allCals.find( + (cal) => + cal.externalId === destinationCalendar.externalId && + cal.integration === destinationCalendar.integration + ); + + if (!matchedCalendar) { + throw new BadRequestException("Invalid destinationCalendarId: Calendar does not exist"); + } + + if (matchedCalendar.readOnly) { + throw new BadRequestException("Invalid destinationCalendarId: Calendar does not have write permission"); + } + + return; + } + + async validateInputUseDestinationCalendarEmail(userId: number) { + const calendars: ConnectedCalendarsData = await this.calendarsService.getCalendars(userId); + + const allCals = calendars.connectedCalendars.map((cal) => cal.calendars ?? []).flat(); + + const primaryCalendar = allCals.find((cal) => cal.primary); + + if (!primaryCalendar) { + throw new BadRequestException( + "Validation failed: A primary connected calendar is required to set useDestinationCalendarEmail" + ); + } + + return; + } } 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 82c7490a06..9ab61ccd67 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 @@ -1,5 +1,5 @@ import { Injectable } from "@nestjs/common"; -import type { EventType, User, Schedule } from "@prisma/client"; +import type { EventType, User, Schedule, DestinationCalendar } from "@prisma/client"; import { EventTypeMetaDataSchema, @@ -15,11 +15,25 @@ import { transformIntervalLimitsInternalToApi, transformFutureBookingLimitsInternalToApi, transformRecurrenceInternalToApi, + transformBookerLayoutsInternalToApi, + transformRequiresConfirmationInternalToApi, + transformEventTypeColorsInternalToApi, + parseEventTypeColor, + transformSeatsInternalToApi, } from "@calcom/platform-libraries"; -import { TransformFutureBookingsLimitSchema_2024_06_14 } from "@calcom/platform-types"; +import { + TransformFutureBookingsLimitSchema_2024_06_14, + BookerLayoutsTransformedSchema, + NoticeThresholdTransformedSchema, + EventTypeOutput_2024_06_14, +} from "@calcom/platform-types"; -type EventTypeRelations = { users: User[]; schedule: Schedule | null }; -type DatabaseEventType = EventType & EventTypeRelations; +type EventTypeRelations = { + users: User[]; + schedule: Schedule | null; + destinationCalendar?: DestinationCalendar | null; +}; +export type DatabaseEventType = EventType & EventTypeRelations; type Input = Pick< DatabaseEventType, @@ -58,11 +72,19 @@ type Input = Pick< | "periodCountCalendarDays" | "periodStartDate" | "periodEndDate" + | "requiresBookerEmailVerification" + | "hideCalendarNotes" + | "eventTypeColor" + | "seatsShowAttendees" + | "requiresConfirmationWillBlockSlot" + | "eventName" + | "destinationCalendar" + | "useEventTypeDestinationCalendarEmail" >; @Injectable() export class OutputEventTypesService_2024_06_14 { - async getResponseEventType(ownerId: number, databaseEventType: Input) { + getResponseEventType(ownerId: number, databaseEventType: Input): EventTypeOutput_2024_06_14 { const { id, length, @@ -74,8 +96,6 @@ export class OutputEventTypesService_2024_06_14 { beforeEventBuffer, afterEventBuffer, slug, - schedulingType, - requiresConfirmation, price, currency, lockTimeZoneToggleOnBookingPage, @@ -87,17 +107,34 @@ export class OutputEventTypesService_2024_06_14 { scheduleId, onlyShowFirstAvailableSlot, offsetStart, + requiresBookerEmailVerification, + hideCalendarNotes, + seatsShowAttendees, + useEventTypeDestinationCalendarEmail, } = databaseEventType; const locations = this.transformLocations(databaseEventType.locations); + const customName = databaseEventType?.eventName ?? undefined; const bookingFields = databaseEventType.bookingFields ? this.transformBookingFields(BookingFieldsSchema.parse(databaseEventType.bookingFields)) : []; const recurrence = this.transformRecurringEvent(databaseEventType.recurringEvent); const metadata = this.transformMetadata(databaseEventType.metadata) || {}; - const users = this.transformUsers(databaseEventType.users); + const users = this.transformUsers(databaseEventType.users || []); const bookingLimitsCount = this.transformIntervalLimits(databaseEventType.bookingLimits); const bookingLimitsDuration = this.transformIntervalLimits(databaseEventType.durationLimits); + const color = this.transformEventTypeColor(databaseEventType.eventTypeColor); + const bookerLayouts = this.transformBookerLayouts( + metadata.bookerLayouts as unknown as BookerLayoutsTransformedSchema + ); + const confirmationPolicy = this.transformRequiresConfirmation( + !!databaseEventType.requiresConfirmation, + !!databaseEventType.requiresConfirmationWillBlockSlot, + metadata.requiresConfirmationThreshold as NoticeThresholdTransformedSchema + ); + delete metadata["bookerLayouts"]; + delete metadata["requiresConfirmationThreshold"]; + const seats = this.transformSeats(seatsPerTimeSlot, seatsShowAttendees, seatsShowAvailabilityCount); const bookingWindow = this.transformBookingWindow({ periodType: databaseEventType.periodType, periodDays: databaseEventType.periodDays, @@ -105,6 +142,7 @@ export class OutputEventTypesService_2024_06_14 { periodStartDate: databaseEventType.periodStartDate, periodEndDate: databaseEventType.periodEndDate, } as TransformFutureBookingsLimitSchema_2024_06_14); + const destinationCalendar = this.transformDestinationCalendar(databaseEventType.destinationCalendar); return { id, @@ -121,16 +159,12 @@ export class OutputEventTypesService_2024_06_14 { minimumBookingNotice, beforeEventBuffer, afterEventBuffer, - schedulingType, metadata, - requiresConfirmation, price, currency, lockTimeZoneToggleOnBookingPage, - seatsPerTimeSlot, forwardParamsSuccessRedirect, successRedirectUrl, - seatsShowAvailabilityCount, isInstantEvent, users, scheduleId, @@ -139,6 +173,15 @@ export class OutputEventTypesService_2024_06_14 { onlyShowFirstAvailableSlot, offsetStart, bookingWindow, + bookerLayouts, + confirmationPolicy, + requiresBookerEmailVerification, + hideCalendarNotes, + color, + seats, + customName, + destinationCalendar, + useDestinationCalendarEmail: useEventTypeDestinationCalendarEmail, }; } @@ -147,6 +190,14 @@ export class OutputEventTypesService_2024_06_14 { return transformLocationsInternalToApi(TransformedLocationsSchema.parse(locations)); } + transformDestinationCalendar(destinationCalendar?: DestinationCalendar | null) { + if (!destinationCalendar) return undefined; + return { + integration: destinationCalendar.integration, + externalId: destinationCalendar.externalId, + }; + } + transformBookingFields(bookingFields: (SystemField | CustomField)[] | null) { if (!bookingFields) return []; @@ -189,4 +240,39 @@ export class OutputEventTypesService_2024_06_14 { transformBookingWindow(bookingLimits: TransformFutureBookingsLimitSchema_2024_06_14) { return transformFutureBookingLimitsInternalToApi(bookingLimits); } + + transformBookerLayouts(bookerLayouts: BookerLayoutsTransformedSchema) { + if (!bookerLayouts) return undefined; + return transformBookerLayoutsInternalToApi(bookerLayouts); + } + + transformRequiresConfirmation( + requiresConfirmation: boolean, + requiresConfirmationWillBlockSlot: boolean, + requiresConfirmationThreshold?: NoticeThresholdTransformedSchema + ) { + return transformRequiresConfirmationInternalToApi( + requiresConfirmation, + requiresConfirmationWillBlockSlot, + requiresConfirmationThreshold + ); + } + + transformEventTypeColor(eventTypeColor: any) { + if (!eventTypeColor) return undefined; + const parsedeventTypeColor = parseEventTypeColor(eventTypeColor); + return transformEventTypeColorsInternalToApi(parsedeventTypeColor); + } + + transformSeats( + seatsPerTimeSlot: number | null, + seatsShowAttendees: boolean | null, + seatsShowAvailabilityCount: boolean | null + ) { + return transformSeatsInternalToApi({ + seatsPerTimeSlot, + seatsShowAttendees: !!seatsShowAttendees, + seatsShowAvailabilityCount: !!seatsShowAvailabilityCount, + }); + } } diff --git a/apps/api/v2/src/modules/organizations/controllers/event-types/organizations-event-types.controller.ts b/apps/api/v2/src/modules/organizations/controllers/event-types/organizations-event-types.controller.ts index 6ba6caf3d2..3a328692d4 100644 --- a/apps/api/v2/src/modules/organizations/controllers/event-types/organizations-event-types.controller.ts +++ b/apps/api/v2/src/modules/organizations/controllers/event-types/organizations-event-types.controller.ts @@ -1,5 +1,6 @@ import { CreatePhoneCallInput } from "@/ee/event-types/event-types_2024_06_14/inputs/create-phone-call.input"; import { CreatePhoneCallOutput } from "@/ee/event-types/event-types_2024_06_14/outputs/create-phone-call.output"; +import { InputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/input-event-types.service"; import { API_VERSIONS_VALUES } from "@/lib/api-versions"; import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator"; import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator"; @@ -15,7 +16,10 @@ import { DeleteTeamEventTypeOutput } from "@/modules/organizations/controllers/e import { GetTeamEventTypeOutput } from "@/modules/organizations/controllers/event-types/outputs/teams/get-team-event-type.output"; import { GetTeamEventTypesOutput } from "@/modules/organizations/controllers/event-types/outputs/teams/get-team-event-types.output"; import { UpdateTeamEventTypeOutput } from "@/modules/organizations/controllers/event-types/outputs/teams/update-team-event-type.output"; +import { OutputTeamEventTypesResponsePipe } from "@/modules/organizations/controllers/pipes/event-types/team-event-types-response.transformer"; +import { InputOrganizationsEventTypesService } from "@/modules/organizations/services/event-types/input.service"; import { OrganizationsEventTypesService } from "@/modules/organizations/services/event-types/organizations-event-types.service"; +import { DatabaseTeamEventType } from "@/modules/organizations/services/event-types/output.service"; import { UserWithProfile } from "@/modules/users/users.repository"; import { Controller, @@ -34,22 +38,33 @@ import { } from "@nestjs/common"; import { ApiTags as DocsTags } from "@nestjs/swagger"; -import { SUCCESS_STATUS } from "@calcom/platform-constants"; +import { ERROR_STATUS, SUCCESS_STATUS } from "@calcom/platform-constants"; import { handleCreatePhoneCall } from "@calcom/platform-libraries"; import { CreateTeamEventTypeInput_2024_06_14, GetTeamEventTypesQuery_2024_06_14, SkipTakePagination, + TeamEventTypeOutput_2024_06_14, UpdateTeamEventTypeInput_2024_06_14, } from "@calcom/platform-types"; +export type EventTypeHandlerResponse = { + data: DatabaseTeamEventType[] | DatabaseTeamEventType; + status: typeof SUCCESS_STATUS | typeof ERROR_STATUS; +}; + @Controller({ path: "/v2/organizations/:orgId", version: API_VERSIONS_VALUES, }) @DocsTags("Organizations Event Types") export class OrganizationsEventTypesController { - constructor(private readonly organizationsEventTypesService: OrganizationsEventTypesService) {} + constructor( + private readonly organizationsEventTypesService: OrganizationsEventTypesService, + private readonly inputService: InputOrganizationsEventTypesService, + private readonly inputUserEventTypesService: InputEventTypesService_2024_06_14, + private readonly outputTeamEventTypesResponsePipe: OutputTeamEventTypesResponsePipe + ) {} @Roles("TEAM_ADMIN") @PlatformPlan("ESSENTIALS") @@ -61,16 +76,22 @@ export class OrganizationsEventTypesController { @Param("orgId", ParseIntPipe) orgId: number, @Body() bodyEventType: CreateTeamEventTypeInput_2024_06_14 ): Promise { + const transformedBody = await this.inputService.transformAndValidateCreateTeamEventTypeInput( + user.id, + teamId, + bodyEventType + ); + const eventType = await this.organizationsEventTypesService.createTeamEventType( user, teamId, orgId, - bodyEventType + transformedBody ); return { status: SUCCESS_STATUS, - data: eventType, + data: await this.outputTeamEventTypesResponsePipe.transform(eventType), }; } @@ -90,7 +111,9 @@ export class OrganizationsEventTypesController { return { status: SUCCESS_STATUS, - data: eventType, + data: (await this.outputTeamEventTypesResponsePipe.transform( + eventType + )) as TeamEventTypeOutput_2024_06_14, }; } @@ -125,12 +148,13 @@ export class OrganizationsEventTypesController { @Query() queryParams: GetTeamEventTypesQuery_2024_06_14 ): Promise { const { eventSlug } = queryParams; + if (eventSlug) { const eventType = await this.organizationsEventTypesService.getTeamEventTypeBySlug(teamId, eventSlug); return { status: SUCCESS_STATUS, - data: eventType ? [eventType] : [], + data: await this.outputTeamEventTypesResponsePipe.transform(eventType ? [eventType] : []), }; } @@ -138,7 +162,7 @@ export class OrganizationsEventTypesController { return { status: SUCCESS_STATUS, - data: eventTypes, + data: await this.outputTeamEventTypesResponsePipe.transform(eventTypes), }; } @@ -155,7 +179,7 @@ export class OrganizationsEventTypesController { return { status: SUCCESS_STATUS, - data: eventTypes, + data: await this.outputTeamEventTypesResponsePipe.transform(eventTypes), }; } @@ -169,16 +193,23 @@ export class OrganizationsEventTypesController { @GetUser() user: UserWithProfile, @Body() bodyEventType: UpdateTeamEventTypeInput_2024_06_14 ): Promise { + const transformedBody = await this.inputService.transformAndValidateUpdateTeamEventTypeInput( + user.id, + eventTypeId, + teamId, + bodyEventType + ); + const eventType = await this.organizationsEventTypesService.updateTeamEventType( eventTypeId, teamId, - bodyEventType, + transformedBody, user ); return { status: SUCCESS_STATUS, - data: eventType, + data: await this.outputTeamEventTypesResponsePipe.transform(eventType), }; } @@ -189,7 +220,7 @@ export class OrganizationsEventTypesController { @HttpCode(HttpStatus.OK) async deleteTeamEventType( @Param("teamId", ParseIntPipe) teamId: number, - @Param("eventTypeId") eventTypeId: number + @Param("eventTypeId", ParseIntPipe) eventTypeId: number ): Promise { const eventType = await this.organizationsEventTypesService.deleteTeamEventType(teamId, eventTypeId); diff --git a/apps/api/v2/src/modules/organizations/controllers/event-types/organizations-event-types.e2e-spec.ts b/apps/api/v2/src/modules/organizations/controllers/event-types/organizations-event-types.e2e-spec.ts index a95e32cc20..0075dab763 100644 --- a/apps/api/v2/src/modules/organizations/controllers/event-types/organizations-event-types.e2e-spec.ts +++ b/apps/api/v2/src/modules/organizations/controllers/event-types/organizations-event-types.e2e-spec.ts @@ -17,7 +17,12 @@ import { UserRepositoryFixture } from "test/fixtures/repository/users.repository import { withApiAuth } from "test/utils/withApiAuth"; import { SUCCESS_STATUS } from "@calcom/platform-constants"; -import { BookingWindowPeriodInputTypeEnum_2024_06_14 } from "@calcom/platform-enums"; +import { + BookingWindowPeriodInputTypeEnum_2024_06_14, + BookerLayoutsInputEnum_2024_06_14, + ConfirmationPolicyEnum, + NoticeThresholdUnitEnum, +} from "@calcom/platform-enums"; import { ApiSuccessResponse, CreateTeamEventTypeInput_2024_06_14, @@ -276,6 +281,30 @@ describe("Organizations Event Types Endpoints", () => { value: 30, rolling: true, }, + bookerLayouts: { + enabledLayouts: [ + BookerLayoutsInputEnum_2024_06_14.column, + BookerLayoutsInputEnum_2024_06_14.month, + BookerLayoutsInputEnum_2024_06_14.week, + ], + defaultLayout: BookerLayoutsInputEnum_2024_06_14.month, + }, + + confirmationPolicy: { + type: ConfirmationPolicyEnum.TIME, + noticeThreshold: { + count: 60, + unit: NoticeThresholdUnitEnum.MINUTES, + }, + blockUnconfirmedBookingsInBooker: true, + }, + requiresBookerEmailVerification: true, + hideCalendarNotes: true, + lockTimeZoneToggleOnBookingPage: true, + color: { + darkThemeHex: "#292929", + lightThemeHex: "#fafafa", + }, }; return request(app.getHttpServer()) @@ -297,6 +326,12 @@ describe("Organizations Event Types Endpoints", () => { expect(data.bookingLimitsDuration).toEqual(body.bookingLimitsDuration); expect(data.offsetStart).toEqual(body.offsetStart); expect(data.bookingWindow).toEqual(body.bookingWindow); + expect(data.bookerLayouts).toEqual(body.bookerLayouts); + expect(data.confirmationPolicy).toEqual(body.confirmationPolicy); + expect(data.requiresBookerEmailVerification).toEqual(body.requiresBookerEmailVerification); + expect(data.hideCalendarNotes).toEqual(body.hideCalendarNotes); + expect(data.lockTimeZoneToggleOnBookingPage).toEqual(body.lockTimeZoneToggleOnBookingPage); + expect(data.color).toEqual(body.color); collectiveEventType = responseBody.data; }); @@ -444,7 +479,7 @@ describe("Organizations Event Types Endpoints", () => { return request(app.getHttpServer()) .patch(`/v2/organizations/${org.id}/teams/${team.id}/event-types/999999`) .send(body) - .expect(404); + .expect(400); }); it("should update collective event-type", async () => { diff --git a/apps/api/v2/src/modules/organizations/controllers/pipes/event-types/team-event-types-response.transformer.ts b/apps/api/v2/src/modules/organizations/controllers/pipes/event-types/team-event-types-response.transformer.ts new file mode 100644 index 0000000000..2a4e6fb26b --- /dev/null +++ b/apps/api/v2/src/modules/organizations/controllers/pipes/event-types/team-event-types-response.transformer.ts @@ -0,0 +1,38 @@ +import { OutputOrganizationsEventTypesService } from "@/modules/organizations/services/event-types/output.service"; +import { DatabaseTeamEventType } from "@/modules/organizations/services/event-types/output.service"; +import { Injectable, PipeTransform } from "@nestjs/common"; +import { plainToClass } from "class-transformer"; + +import { TeamEventTypeOutput_2024_06_14 } from "@calcom/platform-types"; + +@Injectable() +export class OutputTeamEventTypesResponsePipe implements PipeTransform { + constructor(private readonly outputOrganizationsEventTypesService: OutputOrganizationsEventTypesService) {} + + private async transformEventType(item: DatabaseTeamEventType): Promise { + return plainToClass( + TeamEventTypeOutput_2024_06_14, + await this.outputOrganizationsEventTypesService.getResponseTeamEventType(item), + { strategy: "exposeAll" } + ); + } + + // Implementing function overloading to ensure correct return types based on input type: + async transform(value: DatabaseTeamEventType[]): Promise; + + async transform(value: DatabaseTeamEventType): Promise; + + async transform( + value: DatabaseTeamEventType | DatabaseTeamEventType[] + ): Promise; + + async transform( + value: DatabaseTeamEventType | DatabaseTeamEventType[] + ): Promise { + if (Array.isArray(value)) { + return await Promise.all(value.map((item) => this.transformEventType(item))); + } else { + return await this.transformEventType(value); + } + } +} diff --git a/apps/api/v2/src/modules/organizations/organizations.module.ts b/apps/api/v2/src/modules/organizations/organizations.module.ts index 82e381396e..410db2c8d5 100644 --- a/apps/api/v2/src/modules/organizations/organizations.module.ts +++ b/apps/api/v2/src/modules/organizations/organizations.module.ts @@ -7,6 +7,7 @@ import { OrganizationsOptionsAttributesController } from "@/modules/organization import { OrganizationsAttributesController } from "@/modules/organizations/controllers/attributes/organizations-attributes.controller"; import { OrganizationsEventTypesController } from "@/modules/organizations/controllers/event-types/organizations-event-types.controller"; import { OrganizationsMembershipsController } from "@/modules/organizations/controllers/memberships/organizations-membership.controller"; +import { OutputTeamEventTypesResponsePipe } from "@/modules/organizations/controllers/pipes/event-types/team-event-types-response.transformer"; import { OrganizationsSchedulesController } from "@/modules/organizations/controllers/schedules/organizations-schedules.controller"; import { OrganizationsTeamsMembershipsController } from "@/modules/organizations/controllers/teams/memberships/organizations-teams-memberships.controller"; import { OrganizationsTeamsController } from "@/modules/organizations/controllers/teams/organizations-teams.controller"; @@ -79,6 +80,7 @@ import { Module } from "@nestjs/common"; OrganizationsWebhooksService, WebhooksRepository, WebhooksService, + OutputTeamEventTypesResponsePipe, ], exports: [ OrganizationsService, diff --git a/apps/api/v2/src/modules/organizations/repositories/organizations-event-types.repository.ts b/apps/api/v2/src/modules/organizations/repositories/organizations-event-types.repository.ts index 252877e2c9..0ea6edbe6a 100644 --- a/apps/api/v2/src/modules/organizations/repositories/organizations-event-types.repository.ts +++ b/apps/api/v2/src/modules/organizations/repositories/organizations-event-types.repository.ts @@ -12,7 +12,7 @@ export class OrganizationsEventTypesRepository { id: eventTypeId, teamId, }, - include: { users: true, schedule: true, hosts: true }, + include: { users: true, schedule: true, hosts: true, destinationCalendar: true }, }); } @@ -24,7 +24,7 @@ export class OrganizationsEventTypesRepository { slug: eventTypeSlug, }, }, - include: { users: true, schedule: true, hosts: true }, + include: { users: true, schedule: true, hosts: true, destinationCalendar: true }, }); } @@ -33,21 +33,21 @@ export class OrganizationsEventTypesRepository { where: { teamId, }, - include: { users: true, schedule: true, hosts: true }, + include: { users: true, schedule: true, hosts: true, destinationCalendar: true }, }); } async getEventTypeById(eventTypeId: number) { return this.dbRead.prisma.eventType.findUnique({ where: { id: eventTypeId }, - include: { users: true, schedule: true, hosts: true }, + include: { users: true, schedule: true, hosts: true, destinationCalendar: true }, }); } async getEventTypeChildren(eventTypeId: number) { return this.dbRead.prisma.eventType.findMany({ where: { parentId: eventTypeId }, - include: { users: true, schedule: true, hosts: true }, + include: { users: true, schedule: true, hosts: true, destinationCalendar: true }, }); } @@ -60,7 +60,7 @@ export class OrganizationsEventTypesRepository { }, skip, take, - include: { users: true, schedule: true, hosts: true }, + include: { users: true, schedule: true, hosts: true, destinationCalendar: true }, }); } diff --git a/apps/api/v2/src/modules/organizations/services/event-types/input.service.ts b/apps/api/v2/src/modules/organizations/services/event-types/input.service.ts index 5016ded854..cf6c722a9c 100644 --- a/apps/api/v2/src/modules/organizations/services/event-types/input.service.ts +++ b/apps/api/v2/src/modules/organizations/services/event-types/input.service.ts @@ -2,7 +2,7 @@ import { InputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types_ import { OrganizationsEventTypesRepository } from "@/modules/organizations/repositories/organizations-event-types.repository"; import { OrganizationsTeamsRepository } from "@/modules/organizations/repositories/organizations-teams.repository"; import { UsersRepository } from "@/modules/users/users.repository"; -import { BadRequestException, Injectable } from "@nestjs/common"; +import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common"; import { CreateTeamEventTypeInput_2024_06_14, @@ -19,37 +19,89 @@ export class InputOrganizationsEventTypesService { private readonly usersRepository: UsersRepository, private readonly orgEventTypesRepository: OrganizationsEventTypesRepository ) {} + async transformAndValidateCreateTeamEventTypeInput( + userId: number, + teamId: number, + inputEventType: CreateTeamEventTypeInput_2024_06_14 + ) { + await this.validateHosts(teamId, inputEventType.hosts); + + const transformedBody = await this.transformInputCreateTeamEventType(teamId, inputEventType); + + await this.inputEventTypesService.validateEventTypeInputs({ + seatsPerTimeSlot: transformedBody.seatsPerTimeSlot, + locations: transformedBody.locations, + requiresConfirmation: transformedBody.requiresConfirmation, + eventName: transformedBody.eventName, + }); + + transformedBody.destinationCalendar && + (await this.inputEventTypesService.validateInputDestinationCalendar( + userId, + transformedBody.destinationCalendar + )); + + transformedBody.useEventTypeDestinationCalendarEmail && + (await this.inputEventTypesService.validateInputUseDestinationCalendarEmail(userId)); + + return transformedBody; + } + + async transformAndValidateUpdateTeamEventTypeInput( + userId: number, + eventTypeId: number, + teamId: number, + inputEventType: UpdateTeamEventTypeInput_2024_06_14 + ) { + await this.validateHosts(teamId, inputEventType.hosts); + + const transformedBody = await this.transformInputUpdateTeamEventType(eventTypeId, teamId, inputEventType); + + await this.inputEventTypesService.validateEventTypeInputs({ + eventTypeId: eventTypeId, + seatsPerTimeSlot: transformedBody.seatsPerTimeSlot, + locations: transformedBody.locations, + requiresConfirmation: transformedBody.requiresConfirmation, + eventName: transformedBody.eventName, + }); + + transformedBody.destinationCalendar && + (await this.inputEventTypesService.validateInputDestinationCalendar( + userId, + transformedBody.destinationCalendar + )); + + transformedBody.useEventTypeDestinationCalendarEmail && + (await this.inputEventTypesService.validateInputUseDestinationCalendarEmail(userId)); + + return transformedBody; + } + async transformInputCreateTeamEventType( teamId: number, inputEventType: CreateTeamEventTypeInput_2024_06_14 ) { - const { - hosts, - assignAllTeamMembers, - bookingLimitsCount, - bookingLimitsDuration, - bookingWindow, - bookingFields, - recurrence, - ...rest - } = inputEventType; + const { hosts, assignAllTeamMembers, ...rest } = inputEventType; const eventType = this.inputEventTypesService.transformInputCreateEventType(rest); + const children = await this.getChildEventTypesForManagedEventType(null, inputEventType, teamId); - const metadata = rest.schedulingType === "MANAGED" ? { managedEventConfig: {} } : undefined; + const metadata = + rest.schedulingType === "MANAGED" + ? { managedEventConfig: {}, ...eventType.metadata } + : eventType.metadata; const teamEventType = { ...eventType, - hosts: assignAllTeamMembers - ? await this.getAllTeamMembers(teamId, inputEventType.schedulingType) - : this.transformInputHosts(hosts, inputEventType.schedulingType), + // note(Lauris): we don't populate hosts for managed event-types because they are handled by the children + hosts: !(rest.schedulingType === "MANAGED") + ? assignAllTeamMembers + ? await this.getAllTeamMembers(teamId, inputEventType.schedulingType) + : this.transformInputHosts(hosts, inputEventType.schedulingType) + : undefined, assignAllTeamMembers, - bookingLimitsCount, - bookingLimitsDuration, - bookingWindow, - bookingFields, - recurrence, metadata, + children, }; return teamEventType; @@ -62,7 +114,7 @@ export class InputOrganizationsEventTypesService { ) { const { hosts, assignAllTeamMembers, ...rest } = inputEventType; - const eventType = this.inputEventTypesService.transformInputUpdateEventType(rest); + const eventType = await this.inputEventTypesService.transformInputUpdateEventType(rest, eventTypeId); const dbEventType = await this.orgEventTypesRepository.getTeamEventType(teamId, eventTypeId); if (!dbEventType) { @@ -86,14 +138,16 @@ export class InputOrganizationsEventTypesService { } async getChildEventTypesForManagedEventType( - eventTypeId: number, + eventTypeId: number | null, inputEventType: UpdateTeamEventTypeInput_2024_06_14, teamId: number ) { - const eventType = await this.orgEventTypesRepository.getEventTypeByIdWithChildren(eventTypeId); - - if (!eventType || eventType.schedulingType !== "MANAGED") { - return undefined; + let eventType = null; + if (eventTypeId) { + eventType = await this.orgEventTypesRepository.getEventTypeByIdWithChildren(eventTypeId); + if (!eventType || eventType.schedulingType !== "MANAGED") { + return undefined; + } } const ownersIds = await this.getOwnersIdsForManagedEventType(teamId, inputEventType, eventType); @@ -110,7 +164,7 @@ export class InputOrganizationsEventTypesService { async getOwnersIdsForManagedEventType( teamId: number, inputEventType: UpdateTeamEventTypeInput_2024_06_14, - eventType: { children: { userId: number | null }[] } + eventType: { children: { userId: number | null }[] } | null ) { if (inputEventType.assignAllTeamMembers) { return await this.organizationsTeamsRepository.getTeamMembersIds(teamId); @@ -122,7 +176,7 @@ export class InputOrganizationsEventTypesService { } // note(Lauris): when API user DOES NOT update managed event type users, but we still need existing managed event type users to know which event-types to update - return eventType.children.map((child) => child.userId).filter((id) => !!id) as number[]; + return eventType?.children.map((child) => child.userId).filter((id) => !!id) as number[]; } async getOwnersForManagedEventType(userIds: number[]) { @@ -170,6 +224,16 @@ export class InputOrganizationsEventTypesService { ), })); } + + async validateHosts(teamId: number, hosts: CreateTeamEventTypeInput_2024_06_14["hosts"] | undefined) { + if (hosts && hosts.length) { + const membersIds = await this.organizationsTeamsRepository.getTeamMembersIds(teamId); + const invalidHosts = hosts.filter((host) => !membersIds.includes(host.userId)); + if (invalidHosts.length) { + throw new NotFoundException(`Invalid hosts: ${invalidHosts.join(", ")}`); + } + } + } } function getPriorityValue(priority: keyof typeof HostPriority): number { 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 2eef2c4cce..7a6b380fec 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 @@ -2,31 +2,23 @@ import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_20 import { EventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/event-types.service"; import { MembershipsRepository } from "@/modules/memberships/memberships.repository"; import { OrganizationsEventTypesRepository } from "@/modules/organizations/repositories/organizations-event-types.repository"; -import { OrganizationsTeamsRepository } from "@/modules/organizations/repositories/organizations-teams.repository"; -import { InputOrganizationsEventTypesService } from "@/modules/organizations/services/event-types/input.service"; -import { OutputOrganizationsEventTypesService } from "@/modules/organizations/services/event-types/output.service"; +import { DatabaseTeamEventType } from "@/modules/organizations/services/event-types/output.service"; import { PrismaWriteService } from "@/modules/prisma/prisma-write.service"; import { UsersService } from "@/modules/users/services/users.service"; import { UserWithProfile } from "@/modules/users/users.repository"; import { Injectable, NotFoundException } from "@nestjs/common"; import { createEventType, updateEventType } from "@calcom/platform-libraries"; -import { - CreateTeamEventTypeInput_2024_06_14, - UpdateTeamEventTypeInput_2024_06_14, -} from "@calcom/platform-types"; +import { InputTeamEventTransformed_2024_06_14 } from "@calcom/platform-types"; @Injectable() export class OrganizationsEventTypesService { constructor( - private readonly inputService: InputOrganizationsEventTypesService, private readonly eventTypesService: EventTypesService_2024_06_14, private readonly dbWrite: PrismaWriteService, private readonly organizationEventTypesRepository: OrganizationsEventTypesRepository, private readonly eventTypesRepository: EventTypesRepository_2024_06_14, - private readonly outputService: OutputOrganizationsEventTypesService, private readonly membershipsRepository: MembershipsRepository, - private readonly organizationsTeamsRepository: OrganizationsTeamsRepository, private readonly usersService: UsersService ) {} @@ -34,24 +26,14 @@ export class OrganizationsEventTypesService { user: UserWithProfile, teamId: number, orgId: number, - body: CreateTeamEventTypeInput_2024_06_14 - ) { - await this.validateHosts(teamId, body.hosts); + body: InputTeamEventTransformed_2024_06_14 + ): Promise { const eventTypeUser = await this.getUserToCreateTeamEvent(user, orgId); - const { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - hosts, - assignAllTeamMembers, - locations, - bookingLimitsCount, - bookingLimitsDuration, - bookingWindow, - bookingFields, - recurrence, - ...rest - } = await this.inputService.transformInputCreateTeamEventType(teamId, body); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { hosts, children, destinationCalendar, ...rest } = body; + const { eventType: eventTypeCreated } = await createEventType({ - input: { teamId: teamId, locations, ...rest }, + input: { teamId: teamId, ...rest }, ctx: { user: eventTypeUser, // eslint-disable-next-line @typescript-eslint/ban-ts-comment @@ -60,31 +42,7 @@ export class OrganizationsEventTypesService { }, }); - return await this.updateTeamEventType( - eventTypeCreated.id, - teamId, - { - hosts: body.hosts, - assignAllTeamMembers, - bookingLimitsCount, - bookingLimitsDuration, - bookingWindow, - bookingFields, - recurrence, - ...rest, - }, - user - ); - } - - async validateHosts(teamId: number, hosts: CreateTeamEventTypeInput_2024_06_14["hosts"] | undefined) { - if (hosts && hosts.length) { - const membersIds = await this.organizationsTeamsRepository.getTeamMembersIds(teamId); - const invalidHosts = hosts.filter((host) => !membersIds.includes(host.userId)); - if (invalidHosts.length) { - throw new NotFoundException(`Invalid hosts: ${invalidHosts.join(", ")}`); - } - } + return this.updateTeamEventType(eventTypeCreated.id, teamId, body, user); } async validateEventTypeExists(teamId: number, eventTypeId: number) { @@ -110,17 +68,17 @@ export class OrganizationsEventTypesService { }; } - async getTeamEventType(teamId: number, eventTypeId: number) { + async getTeamEventType(teamId: number, eventTypeId: number): Promise { const eventType = await this.organizationEventTypesRepository.getTeamEventType(teamId, eventTypeId); if (!eventType) { return null; } - return this.outputService.getResponseTeamEventType(eventType); + return eventType; } - async getTeamEventTypeBySlug(teamId: number, eventTypeSlug: string) { + async getTeamEventTypeBySlug(teamId: number, eventTypeSlug: string): Promise { const eventType = await this.organizationEventTypesRepository.getTeamEventTypeBySlug( teamId, eventTypeSlug @@ -130,46 +88,28 @@ export class OrganizationsEventTypesService { return null; } - return this.outputService.getResponseTeamEventType(eventType); + return eventType; } - async getTeamEventTypes(teamId: number) { - const eventTypes = await this.organizationEventTypesRepository.getTeamEventTypes(teamId); - - const eventTypePromises = eventTypes.map(async (eventType) => { - return await this.outputService.getResponseTeamEventType(eventType); - }); - - return await Promise.all(eventTypePromises); + async getTeamEventTypes(teamId: number): Promise { + return await this.organizationEventTypesRepository.getTeamEventTypes(teamId); } - async getTeamsEventTypes(orgId: number, skip = 0, take = 250) { - const eventTypes = await this.organizationEventTypesRepository.getTeamsEventTypes(orgId, skip, take); - - const eventTypePromises = eventTypes.map(async (eventType) => { - return await this.outputService.getResponseTeamEventType(eventType); - }); - - return await Promise.all(eventTypePromises); + async getTeamsEventTypes(orgId: number, skip = 0, take = 250): Promise { + return await this.organizationEventTypesRepository.getTeamsEventTypes(orgId, skip, take); } async updateTeamEventType( eventTypeId: number, teamId: number, - body: UpdateTeamEventTypeInput_2024_06_14, + body: InputTeamEventTransformed_2024_06_14, user: UserWithProfile - ) { + ): Promise { await this.validateEventTypeExists(teamId, eventTypeId); - await this.validateHosts(teamId, body.hosts); const eventTypeUser = await this.eventTypesService.getUserToUpdateEvent(user); - const bodyTransformed = await this.inputService.transformInputUpdateTeamEventType( - eventTypeId, - teamId, - body - ); await updateEventType({ - input: { id: eventTypeId, ...bodyTransformed }, + input: { id: eventTypeId, ...body }, ctx: { user: eventTypeUser, // eslint-disable-next-line @typescript-eslint/ban-ts-comment @@ -185,18 +125,12 @@ export class OrganizationsEventTypesService { } if (eventType.schedulingType !== "MANAGED") { - return this.outputService.getResponseTeamEventType(eventType); + return eventType; } - const children = await this.organizationEventTypesRepository.getEventTypeChildren(eventType.id); + const childrenEventTypes = await this.organizationEventTypesRepository.getEventTypeChildren(eventType.id); - const eventTypes = [eventType, ...children]; - - const eventTypePromises = eventTypes.map(async (e) => { - return await this.outputService.getResponseTeamEventType(e); - }); - - return await Promise.all(eventTypePromises); + return [eventType, ...childrenEventTypes]; } async deleteTeamEventType(teamId: number, eventTypeId: number) { diff --git a/apps/api/v2/src/modules/organizations/services/event-types/output.service.ts b/apps/api/v2/src/modules/organizations/services/event-types/output.service.ts index 81c24df95b..bcbdea637f 100644 --- a/apps/api/v2/src/modules/organizations/services/event-types/output.service.ts +++ b/apps/api/v2/src/modules/organizations/services/event-types/output.service.ts @@ -2,16 +2,21 @@ import { OutputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types import { OrganizationsEventTypesRepository } from "@/modules/organizations/repositories/organizations-event-types.repository"; import { UsersRepository } from "@/modules/users/users.repository"; import { Injectable } from "@nestjs/common"; -import type { EventType, User, Schedule, Host } from "@prisma/client"; +import type { EventType, User, Schedule, Host, DestinationCalendar } from "@prisma/client"; import { SchedulingType } from "@prisma/client"; import { HostPriority, TeamEventTypeResponseHost } from "@calcom/platform-types"; -type EventTypeRelations = { users: User[]; schedule: Schedule | null; hosts: Host[] }; -type DatabaseEventType = EventType & EventTypeRelations; +type EventTypeRelations = { + users: User[]; + schedule: Schedule | null; + hosts: Host[]; + destinationCalendar?: DestinationCalendar | null; +}; +export type DatabaseTeamEventType = EventType & EventTypeRelations; type Input = Pick< - DatabaseEventType, + DatabaseTeamEventType, | "id" | "length" | "title" @@ -52,6 +57,14 @@ type Input = Pick< | "periodCountCalendarDays" | "periodStartDate" | "periodEndDate" + | "requiresBookerEmailVerification" + | "hideCalendarNotes" + | "lockTimeZoneToggleOnBookingPage" + | "eventTypeColor" + | "seatsShowAttendees" + | "requiresConfirmationWillBlockSlot" + | "eventName" + | "useEventTypeDestinationCalendarEmail" >; @Injectable() @@ -65,7 +78,7 @@ export class OutputOrganizationsEventTypesService { async getResponseTeamEventType(databaseEventType: Input) { const { teamId, userId, parentId, assignAllTeamMembers } = databaseEventType; // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { ownerId, users, ...rest } = await this.outputEventTypesService.getResponseEventType( + const { ownerId, users, ...rest } = this.outputEventTypesService.getResponseEventType( 0, databaseEventType ); @@ -80,6 +93,7 @@ export class OutputOrganizationsEventTypesService { teamId, ownerId: userId, parentEventTypeId: parentId, + schedulingType: databaseEventType.schedulingType, assignAllTeamMembers: teamId ? assignAllTeamMembers : undefined, }; } diff --git a/apps/api/v2/swagger/documentation.json b/apps/api/v2/swagger/documentation.json index eb71168010..03ea86a45a 100644 --- a/apps/api/v2/swagger/documentation.json +++ b/apps/api/v2/swagger/documentation.json @@ -4923,6 +4923,11 @@ "nullable": true, "example": "alice" }, + "name": { + "type": "string", + "nullable": true, + "example": "alice" + }, "timeZone": { "type": "string", "example": "America/New_York" @@ -4998,6 +5003,7 @@ "id", "email", "username", + "name", "timeZone", "weekStart", "createdDate", @@ -6079,94 +6085,6 @@ "value" ] }, - "BookingLimitsCount_2024_06_14": { - "type": "object", - "properties": { - "day": { - "type": "number", - "description": "The number of bookings per day", - "example": 1 - }, - "week": { - "type": "number", - "description": "The number of bookings per week", - "example": 2 - }, - "month": { - "type": "number", - "description": "The number of bookings per month", - "example": 3 - }, - "year": { - "type": "number", - "description": "The number of bookings per year", - "example": 4 - } - }, - "required": [ - "day", - "week", - "month", - "year" - ] - }, - "BookingLimitsDuration_2024_06_14": { - "type": "object", - "properties": { - "day": { - "type": "number", - "minimum": 15, - "description": "The duration of bookings per day (must be a multiple of 15)", - "example": 60 - }, - "week": { - "type": "number", - "minimum": 15, - "description": "The duration of bookings per week (must be a multiple of 15)", - "example": 120 - }, - "month": { - "type": "number", - "minimum": 15, - "description": "The duration of bookings per month (must be a multiple of 15)", - "example": 180 - }, - "year": { - "type": "number", - "minimum": 15, - "description": "The duration of bookings per year (must be a multiple of 15)", - "example": 240 - } - } - }, - "Recurrence_2024_06_14": { - "type": "object", - "properties": { - "interval": { - "type": "number", - "example": 10, - "description": "Repeats every {count} week | month | year" - }, - "occurrences": { - "type": "number", - "example": 10, - "description": "Repeats for a maximum of {count} events" - }, - "frequency": { - "type": "string", - "enum": [ - "yearly", - "monthly", - "weekly" - ] - } - }, - "required": [ - "interval", - "occurrences", - "frequency" - ] - }, "CreateEventTypeInput_2024_06_14": { "type": "object", "properties": { @@ -6272,24 +6190,16 @@ "description": "If you want that this event has different schedule than user's default one you can specify it here." }, "bookingLimitsCount": { - "description": "Limit how many times this event can be booked", - "allOf": [ - { - "$ref": "#/components/schemas/BookingLimitsCount_2024_06_14" - } - ] + "type": "object", + "description": "Limit how many times this event can be booked" }, "onlyShowFirstAvailableSlot": { "type": "boolean", "description": "This will limit your availability for this event type to one slot per day, scheduled at the earliest available time." }, "bookingLimitsDuration": { - "description": "Limit total amount of time that this event can be booked", - "allOf": [ - { - "$ref": "#/components/schemas/BookingLimitsDuration_2024_06_14" - } - ] + "type": "object", + "description": "Limit total amount of time that this event can be booked" }, "bookingWindow": { "type": "array", @@ -6312,13 +6222,10 @@ "type": "number", "description": "Offset timeslots shown to bookers by a specified number of minutes" }, - "recurrence": { - "description": "Create a recurring event that can be booked once but will occur multiple times", - "allOf": [ - { - "$ref": "#/components/schemas/Recurrence_2024_06_14" - } - ] + "customName": { + "type": "string", + "description": "Customizable event name with valid variables: \n {Event type title}, {Organiser}, {Scheduler}, {Location}, {Organiser first name}, \n {Scheduler first name}, {Scheduler last name}, {Event duration}, {LOCATION}, \n {HOST/ATTENDEE}, {HOST}, {ATTENDEE}, {USER}", + "example": "{Event type title} between {Organiser} and {Scheduler}" } }, "required": [ @@ -6339,7 +6246,7 @@ "bookingLimitsDuration", "bookingWindow", "offsetStart", - "recurrence" + "customName" ] }, "EmailDefaultFieldOutput_2024_06_14": { @@ -7136,6 +7043,104 @@ "isDefault" ] }, + "BaseBookingLimitsDuration_2024_06_14": { + "type": "object", + "properties": { + "day": { + "type": "number", + "minimum": 15, + "description": "The duration of bookings per day (must be a multiple of 15)", + "example": 60 + }, + "week": { + "type": "number", + "minimum": 15, + "description": "The duration of bookings per week (must be a multiple of 15)", + "example": 120 + }, + "month": { + "type": "number", + "minimum": 15, + "description": "The duration of bookings per month (must be a multiple of 15)", + "example": 180 + }, + "year": { + "type": "number", + "minimum": 15, + "description": "The duration of bookings per year (must be a multiple of 15)", + "example": 240 + }, + "disabled": { + "type": "boolean", + "default": false + } + } + }, + "BookerLayouts_2024_06_14": { + "type": "object", + "properties": {} + }, + "EventTypeColor_2024_06_14": { + "type": "object", + "properties": { + "lightThemeHex": { + "type": "string", + "description": "Color used for event types in light theme", + "example": "#292929" + }, + "darkThemeHex": { + "type": "string", + "description": "Color used for event types in dark theme", + "example": "#fafafa" + } + }, + "required": [ + "lightThemeHex", + "darkThemeHex" + ] + }, + "Seats_2024_06_14": { + "type": "object", + "properties": { + "seatsPerTimeSlot": { + "type": "number", + "description": "Number of seats available per time slot", + "example": 4 + }, + "showAttendeeInfo": { + "type": "boolean", + "description": "Show attendee information to other guests", + "example": true + }, + "showAvailabilityCount": { + "type": "boolean", + "description": "Display the count of available seats", + "example": true + } + }, + "required": [ + "seatsPerTimeSlot", + "showAttendeeInfo", + "showAvailabilityCount" + ] + }, + "DestinationCalendar_2024_06_14": { + "type": "object", + "properties": { + "integration": { + "type": "string", + "description": "The integration type of the destination calendar. Refer to the /api/v2/calendars endpoint to retrieve the integration type of your connected calendars." + }, + "externalId": { + "type": "string", + "description": "The external ID of the destination calendar. Refer to the /api/v2/calendars endpoint to retrieve the external IDs of your connected calendars." + } + }, + "required": [ + "integration", + "externalId" + ] + }, "EventTypeOutput_2024_06_14": { "type": "object", "properties": { @@ -7157,7 +7162,7 @@ }, "description": { "type": "string", - "example": "Discover the culinary wonders of the Argentina by making the best flan ever!" + "example": "Discover the culinary wonders of Argentina by making the best flan ever!" }, "locations": { "type": "array", @@ -7264,9 +7269,6 @@ "metadata": { "type": "object" }, - "requiresConfirmation": { - "type": "boolean" - }, "price": { "type": "number" }, @@ -7285,6 +7287,9 @@ "successRedirectUrl": { "type": "object" }, + "isInstantEvent": { + "type": "boolean" + }, "seatsShowAvailabilityCount": { "type": "object" }, @@ -7292,13 +7297,13 @@ "type": "object" }, "bookingLimitsCount": { - "$ref": "#/components/schemas/BookingLimitsCount_2024_06_14" + "type": "object" }, "onlyShowFirstAvailableSlot": { "type": "boolean" }, "bookingLimitsDuration": { - "$ref": "#/components/schemas/BookingLimitsDuration_2024_06_14" + "type": "object" }, "bookingWindow": { "type": "array", @@ -7317,9 +7322,36 @@ ] } }, + "bookerLayouts": { + "$ref": "#/components/schemas/BookerLayouts_2024_06_14" + }, + "confirmationPolicy": { + "type": "object" + }, + "requiresBookerEmailVerification": { + "type": "boolean" + }, + "hideCalendarNotes": { + "type": "boolean" + }, + "color": { + "$ref": "#/components/schemas/EventTypeColor_2024_06_14" + }, + "seats": { + "$ref": "#/components/schemas/Seats_2024_06_14" + }, "offsetStart": { "type": "number" }, + "customName": { + "type": "string" + }, + "destinationCalendar": { + "$ref": "#/components/schemas/DestinationCalendar_2024_06_14" + }, + "useDestinationCalendarEmail": { + "type": "boolean" + }, "ownerId": { "type": "number", "example": 10 @@ -7346,20 +7378,29 @@ "afterEventBuffer", "recurrence", "metadata", - "requiresConfirmation", "price", "currency", "lockTimeZoneToggleOnBookingPage", "seatsPerTimeSlot", "forwardParamsSuccessRedirect", "successRedirectUrl", + "isInstantEvent", "seatsShowAvailabilityCount", "scheduleId", "bookingLimitsCount", "onlyShowFirstAvailableSlot", "bookingLimitsDuration", "bookingWindow", + "bookerLayouts", + "confirmationPolicy", + "requiresBookerEmailVerification", + "hideCalendarNotes", + "color", + "seats", "offsetStart", + "customName", + "destinationCalendar", + "useDestinationCalendarEmail", "ownerId", "users" ] @@ -7537,24 +7578,16 @@ "description": "If you want that this event has different schedule than user's default one you can specify it here." }, "bookingLimitsCount": { - "description": "Limit how many times this event can be booked", - "allOf": [ - { - "$ref": "#/components/schemas/BookingLimitsCount_2024_06_14" - } - ] + "type": "object", + "description": "Limit how many times this event can be booked" }, "onlyShowFirstAvailableSlot": { "type": "boolean", "description": "This will limit your availability for this event type to one slot per day, scheduled at the earliest available time." }, "bookingLimitsDuration": { - "description": "Limit total amount of time that this event can be booked", - "allOf": [ - { - "$ref": "#/components/schemas/BookingLimitsDuration_2024_06_14" - } - ] + "type": "object", + "description": "Limit total amount of time that this event can be booked" }, "bookingWindow": { "type": "array", @@ -7577,13 +7610,10 @@ "type": "number", "description": "Offset timeslots shown to bookers by a specified number of minutes" }, - "recurrence": { - "description": "Create a recurring event that can be booked once but will occur multiple times", - "allOf": [ - { - "$ref": "#/components/schemas/Recurrence_2024_06_14" - } - ] + "customName": { + "type": "string", + "description": "Customizable event name with valid variables: \n {Event type title}, {Organiser}, {Scheduler}, {Location}, {Organiser first name}, \n {Scheduler first name}, {Scheduler last name}, {Event duration}, {LOCATION}, \n {HOST/ATTENDEE}, {HOST}, {ATTENDEE}, {USER}", + "example": "{Event type title} between {Organiser} and {Scheduler}" } }, "required": [ @@ -7604,7 +7634,7 @@ "bookingLimitsDuration", "bookingWindow", "offsetStart", - "recurrence" + "customName" ] }, "UpdateEventTypeOutput_2024_06_14": { @@ -7938,6 +7968,9 @@ "weekStart": { "type": "string", "default": "Sunday" + }, + "bookingLimits": { + "type": "string" } } }, @@ -8930,24 +8963,16 @@ "description": "If you want that this event has different schedule than user's default one you can specify it here." }, "bookingLimitsCount": { - "description": "Limit how many times this event can be booked", - "allOf": [ - { - "$ref": "#/components/schemas/BookingLimitsCount_2024_06_14" - } - ] + "type": "object", + "description": "Limit how many times this event can be booked" }, "onlyShowFirstAvailableSlot": { "type": "boolean", "description": "This will limit your availability for this event type to one slot per day, scheduled at the earliest available time." }, "bookingLimitsDuration": { - "description": "Limit total amount of time that this event can be booked", - "allOf": [ - { - "$ref": "#/components/schemas/BookingLimitsDuration_2024_06_14" - } - ] + "type": "object", + "description": "Limit total amount of time that this event can be booked" }, "bookingWindow": { "type": "array", @@ -8970,13 +8995,10 @@ "type": "number", "description": "Offset timeslots shown to bookers by a specified number of minutes" }, - "recurrence": { - "description": "Create a recurring event that can be booked once but will occur multiple times", - "allOf": [ - { - "$ref": "#/components/schemas/Recurrence_2024_06_14" - } - ] + "customName": { + "type": "string", + "description": "Customizable event name with valid variables: \n {Event type title}, {Organiser}, {Scheduler}, {Location}, {Organiser first name}, \n {Scheduler first name}, {Scheduler last name}, {Event duration}, {LOCATION}, \n {HOST/ATTENDEE}, {HOST}, {ATTENDEE}, {USER}", + "example": "{Event type title} between {Organiser} and {Scheduler}" }, "schedulingType": { "type": "object" @@ -9010,7 +9032,7 @@ "bookingLimitsDuration", "bookingWindow", "offsetStart", - "recurrence", + "customName", "schedulingType", "hosts", "assignAllTeamMembers" @@ -9046,6 +9068,38 @@ "data" ] }, + "Recurrence_2024_06_14": { + "type": "object", + "properties": { + "interval": { + "type": "number", + "example": 10, + "description": "Repeats every {count} week | month | year" + }, + "occurrences": { + "type": "number", + "example": 10, + "description": "Repeats for a maximum of {count} events" + }, + "frequency": { + "enum": [ + "yearly", + "monthly", + "weekly" + ], + "type": "string" + }, + "disabled": { + "type": "boolean", + "default": false + } + }, + "required": [ + "interval", + "occurrences", + "frequency" + ] + }, "TeamEventTypeResponseHost": { "type": "object", "properties": { @@ -9101,7 +9155,7 @@ }, "description": { "type": "string", - "example": "Discover the culinary wonders of the Argentina by making the best flan ever!" + "example": "Discover the culinary wonders of Argentina by making the best flan ever!" }, "locations": { "type": "array", @@ -9215,9 +9269,6 @@ "metadata": { "type": "object" }, - "requiresConfirmation": { - "type": "boolean" - }, "price": { "type": "number" }, @@ -9239,6 +9290,9 @@ "type": "string", "nullable": true }, + "isInstantEvent": { + "type": "boolean" + }, "seatsShowAvailabilityCount": { "type": "boolean", "nullable": true @@ -9248,13 +9302,13 @@ "nullable": true }, "bookingLimitsCount": { - "$ref": "#/components/schemas/BookingLimitsCount_2024_06_14" + "type": "object" }, "onlyShowFirstAvailableSlot": { "type": "boolean" }, "bookingLimitsDuration": { - "$ref": "#/components/schemas/BookingLimitsDuration_2024_06_14" + "type": "object" }, "bookingWindow": { "type": "array", @@ -9273,18 +9327,36 @@ ] } }, + "bookerLayouts": { + "$ref": "#/components/schemas/BookerLayouts_2024_06_14" + }, + "confirmationPolicy": { + "type": "object" + }, + "requiresBookerEmailVerification": { + "type": "boolean" + }, + "hideCalendarNotes": { + "type": "boolean" + }, + "color": { + "$ref": "#/components/schemas/EventTypeColor_2024_06_14" + }, + "seats": { + "$ref": "#/components/schemas/Seats_2024_06_14" + }, "offsetStart": { "type": "number", "minimum": 1 }, - "schedulingType": { - "type": "string", - "nullable": true, - "enum": [ - "ROUND_ROBIN", - "COLLECTIVE", - "MANAGED" - ] + "customName": { + "type": "string" + }, + "destinationCalendar": { + "$ref": "#/components/schemas/DestinationCalendar_2024_06_14" + }, + "useDestinationCalendarEmail": { + "type": "boolean" }, "teamId": { "type": "number", @@ -9297,7 +9369,7 @@ "parentEventTypeId": { "type": "number", "nullable": true, - "description": "For managed event types parent event type is the event type that this event type is based on" + "description": "For managed event types, parent event type is the event type that this event type is based on" }, "hosts": { "type": "array", @@ -9307,6 +9379,15 @@ }, "assignAllTeamMembers": { "type": "boolean" + }, + "schedulingType": { + "type": "string", + "nullable": true, + "enum": [ + "ROUND_ROBIN", + "COLLECTIVE", + "MANAGED" + ] } }, "required": [ @@ -9320,17 +9401,15 @@ "disableGuests", "recurrence", "metadata", - "requiresConfirmation", "price", "currency", "lockTimeZoneToggleOnBookingPage", - "seatsPerTimeSlot", "forwardParamsSuccessRedirect", "successRedirectUrl", - "seatsShowAvailabilityCount", + "isInstantEvent", "scheduleId", - "schedulingType", - "hosts" + "hosts", + "schedulingType" ] }, "GetTeamEventTypeOutput": { @@ -9580,24 +9659,16 @@ "description": "If you want that this event has different schedule than user's default one you can specify it here." }, "bookingLimitsCount": { - "description": "Limit how many times this event can be booked", - "allOf": [ - { - "$ref": "#/components/schemas/BookingLimitsCount_2024_06_14" - } - ] + "type": "object", + "description": "Limit how many times this event can be booked" }, "onlyShowFirstAvailableSlot": { "type": "boolean", "description": "This will limit your availability for this event type to one slot per day, scheduled at the earliest available time." }, "bookingLimitsDuration": { - "description": "Limit total amount of time that this event can be booked", - "allOf": [ - { - "$ref": "#/components/schemas/BookingLimitsDuration_2024_06_14" - } - ] + "type": "object", + "description": "Limit total amount of time that this event can be booked" }, "bookingWindow": { "type": "array", @@ -9620,13 +9691,10 @@ "type": "number", "description": "Offset timeslots shown to bookers by a specified number of minutes" }, - "recurrence": { - "description": "Create a recurring event that can be booked once but will occur multiple times", - "allOf": [ - { - "$ref": "#/components/schemas/Recurrence_2024_06_14" - } - ] + "customName": { + "type": "string", + "description": "Customizable event name with valid variables: \n {Event type title}, {Organiser}, {Scheduler}, {Location}, {Organiser first name}, \n {Scheduler first name}, {Scheduler last name}, {Event duration}, {LOCATION}, \n {HOST/ATTENDEE}, {HOST}, {ATTENDEE}, {USER}", + "example": "{Event type title} between {Organiser} and {Scheduler}" }, "hosts": { "type": "array", @@ -9657,7 +9725,7 @@ "bookingLimitsDuration", "bookingWindow", "offsetStart", - "recurrence", + "customName", "hosts", "assignAllTeamMembers" ] diff --git a/apps/web/playwright/managed-event-types.e2e.ts b/apps/web/playwright/managed-event-types.e2e.ts index ca0e85e542..d4f8b66bf4 100644 --- a/apps/web/playwright/managed-event-types.e2e.ts +++ b/apps/web/playwright/managed-event-types.e2e.ts @@ -1,6 +1,5 @@ import type { Locator, Page } from "@playwright/test"; import { expect } from "@playwright/test"; -import { apiLogin } from "playwright/fixtures/users"; import { SchedulingType } from "@calcom/prisma/enums"; diff --git a/apps/web/playwright/reschedule.e2e.ts b/apps/web/playwright/reschedule.e2e.ts index b008dd1c09..580622c66f 100644 --- a/apps/web/playwright/reschedule.e2e.ts +++ b/apps/web/playwright/reschedule.e2e.ts @@ -6,7 +6,6 @@ import { MembershipRole } from "@calcom/prisma/client"; import { BookingStatus } from "@calcom/prisma/enums"; import { bookingMetadataSchema } from "@calcom/prisma/zod-utils"; -import { apiLogin } from "./fixtures/users"; import { test } from "./lib/fixtures"; import { bookTimeSlot, diff --git a/packages/app-store/zoomvideo/lib/VideoApiAdapter.ts b/packages/app-store/zoomvideo/lib/VideoApiAdapter.ts index b5c00af0b8..c08c72fa56 100644 --- a/packages/app-store/zoomvideo/lib/VideoApiAdapter.ts +++ b/packages/app-store/zoomvideo/lib/VideoApiAdapter.ts @@ -332,7 +332,7 @@ const ZoomVideoApiAdapter = (credential: CredentialPayload): VideoApiAdapter => }, body: JSON.stringify(await translateEvent(event)), }); - + const updatedMeeting = await fetchZoomApi(`meetings/${bookingRef.uid}`); const result = zoomEventResultSchema.parse(updatedMeeting); diff --git a/packages/features/auth/SAMLLogin.tsx b/packages/features/auth/SAMLLogin.tsx index 6094b015c7..e8931fcd8f 100644 --- a/packages/features/auth/SAMLLogin.tsx +++ b/packages/features/auth/SAMLLogin.tsx @@ -4,7 +4,7 @@ import { useFormContext } from "react-hook-form"; import z from "zod"; import { HOSTED_CAL_FEATURES } from "@calcom/lib/constants"; -import { useLastUsed, LastUsed } from "@calcom/lib/hooks/useLastUsed"; +import { useLastUsed } from "@calcom/lib/hooks/useLastUsed"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { trpc } from "@calcom/trpc/react"; import { Button } from "@calcom/ui"; @@ -38,7 +38,7 @@ export function SAMLLogin({ samlTenantID, samlProductID, setErrorMessage }: Prop