From fff521e171dc2cc8866ad376aa9bf3cf11aa2469 Mon Sep 17 00:00:00 2001 From: Lauris Skraucis Date: Tue, 4 Mar 2025 18:08:21 +0100 Subject: [PATCH] fix: v2 managed user attendee bookings (#19716) * Revert "Revert "fix: v2 managed user attendee bookings (#19612)" (#19635)" This reverts commit 2600a6c3d539304af80dd562cc4af9312786b92d. * fix: make guests optional --- .../ee/bookings/2024-04-15/bookings.module.ts | 26 +- .../bookings.controller.e2e-spec.ts | 6 +- .../controllers/bookings.controller.ts | 51 +- .../managed-user-bookings.e2e-spec.ts | 461 +++++++++++++++++ .../2024-04-15/inputs/create-booking.input.ts | 15 +- .../2024-04-15/outputs/get-bookings.output.ts | 2 +- .../ee/bookings/2024-08-13/bookings.module.ts | 19 +- .../e2e/managed-user-bookings.e2e-spec.ts | 487 ++++++++++++++++++ .../2024-08-13/services/bookings.service.ts | 14 +- .../2024-08-13/services/input.service.ts | 114 ++-- .../shared/platform-bookings.service.ts | 59 +++ .../outputs/create-managed-user.output.ts | 2 +- .../services/oauth-clients-users.service.ts | 1 - 13 files changed, 1172 insertions(+), 85 deletions(-) create mode 100644 apps/api/v2/src/ee/bookings/2024-04-15/controllers/managed-user-bookings.e2e-spec.ts create mode 100644 apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/managed-user-bookings.e2e-spec.ts create mode 100644 apps/api/v2/src/ee/bookings/shared/platform-bookings.service.ts diff --git a/apps/api/v2/src/ee/bookings/2024-04-15/bookings.module.ts b/apps/api/v2/src/ee/bookings/2024-04-15/bookings.module.ts index d1ba1149e7..41b03b8592 100644 --- a/apps/api/v2/src/ee/bookings/2024-04-15/bookings.module.ts +++ b/apps/api/v2/src/ee/bookings/2024-04-15/bookings.module.ts @@ -1,17 +1,39 @@ import { BookingsController_2024_04_15 } from "@/ee/bookings/2024-04-15/controllers/bookings.controller"; +import { PlatformBookingsService } from "@/ee/bookings/shared/platform-bookings.service"; +import { EventTypesModule_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/event-types.module"; +import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module"; +import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module"; import { ApiKeysRepository } from "@/modules/api-keys/api-keys-repository"; import { BillingModule } from "@/modules/billing/billing.module"; import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository"; +import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-clients-users.service"; import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service"; import { PrismaModule } from "@/modules/prisma/prisma.module"; import { RedisModule } from "@/modules/redis/redis.module"; import { TokensModule } from "@/modules/tokens/tokens.module"; import { TokensRepository } from "@/modules/tokens/tokens.repository"; +import { UsersModule } from "@/modules/users/users.module"; import { Module } from "@nestjs/common"; @Module({ - imports: [PrismaModule, RedisModule, TokensModule, BillingModule], - providers: [TokensRepository, OAuthFlowService, OAuthClientRepository, ApiKeysRepository], + imports: [ + PrismaModule, + RedisModule, + TokensModule, + BillingModule, + UsersModule, + EventTypesModule_2024_04_15, + SchedulesModule_2024_04_15, + EventTypesModule_2024_06_14, + ], + providers: [ + TokensRepository, + OAuthFlowService, + OAuthClientRepository, + ApiKeysRepository, + OAuthClientUsersService, + PlatformBookingsService, + ], controllers: [BookingsController_2024_04_15], }) export class BookingsModule_2024_04_15 {} diff --git a/apps/api/v2/src/ee/bookings/2024-04-15/controllers/bookings.controller.e2e-spec.ts b/apps/api/v2/src/ee/bookings/2024-04-15/controllers/bookings.controller.e2e-spec.ts index cc31378ffd..099d5bb6c8 100644 --- a/apps/api/v2/src/ee/bookings/2024-04-15/controllers/bookings.controller.e2e-spec.ts +++ b/apps/api/v2/src/ee/bookings/2024-04-15/controllers/bookings.controller.e2e-spec.ts @@ -114,7 +114,6 @@ describe("Bookings Endpoints 2024-04-15", () => { optionValue: "", }, notes: "test", - guests: [], }; const body: CreateBookingInput_2024_04_15 = { @@ -145,6 +144,7 @@ describe("Bookings Endpoints 2024-04-15", () => { expect(responseBody.data.eventTypeId).toEqual(bookingEventTypeId); expect(responseBody.data.user.timeZone).toEqual(bookingTimeZone); expect(responseBody.data.metadata).toEqual(bookingMetadata); + expect(responseBody.data.responses).toEqual(bookingResponses); createdBooking = responseBody.data; }); @@ -244,6 +244,7 @@ describe("Bookings Endpoints 2024-04-15", () => { expect(responseBody.data.eventTypeId).toEqual(bookingEventTypeId); expect(responseBody.data.user.timeZone).toEqual(bookingTimeZone); expect(responseBody.data.metadata).toEqual(bookingMetadata); + expect(responseBody.data.responses).toEqual(bookingResponses); createdBooking = responseBody.data; }); @@ -269,7 +270,7 @@ describe("Bookings Endpoints 2024-04-15", () => { optionValue: "", }, notes: "test", - guests: [], + guests: ["someone@example.com"], }; const body: CreateBookingInput_2024_04_15 = { @@ -301,6 +302,7 @@ describe("Bookings Endpoints 2024-04-15", () => { expect(responseBody.data.eventTypeId).toEqual(bookingEventTypeId); expect(responseBody.data.user.timeZone).toEqual(bookingTimeZone); expect(responseBody.data.metadata).toEqual(newBookingMetadata); + expect(responseBody.data.responses).toEqual(bookingResponses); createdBooking = responseBody.data; }); diff --git a/apps/api/v2/src/ee/bookings/2024-04-15/controllers/bookings.controller.ts b/apps/api/v2/src/ee/bookings/2024-04-15/controllers/bookings.controller.ts index cbae0a5b7c..b1930a5ddc 100644 --- a/apps/api/v2/src/ee/bookings/2024-04-15/controllers/bookings.controller.ts +++ b/apps/api/v2/src/ee/bookings/2024-04-15/controllers/bookings.controller.ts @@ -4,6 +4,7 @@ import { MarkNoShowInput_2024_04_15 } from "@/ee/bookings/2024-04-15/inputs/mark import { GetBookingOutput_2024_04_15 } from "@/ee/bookings/2024-04-15/outputs/get-booking.output"; import { GetBookingsOutput_2024_04_15 } from "@/ee/bookings/2024-04-15/outputs/get-bookings.output"; import { MarkNoShowOutput_2024_04_15 } from "@/ee/bookings/2024-04-15/outputs/mark-no-show.output"; +import { PlatformBookingsService } from "@/ee/bookings/shared/platform-bookings.service"; import { hashAPIKey, isApiKey, stripApiKey } from "@/lib/api-key"; import { VERSION_2024_04_15, VERSION_2024_06_11, VERSION_2024_06_14 } from "@/lib/api-versions"; import { ApiKeysRepository } from "@/modules/api-keys/api-keys-repository"; @@ -99,7 +100,8 @@ export class BookingsController_2024_04_15 { private readonly oAuthClientRepository: OAuthClientRepository, private readonly billingService: BillingService, private readonly config: ConfigService, - private readonly apiKeyRepository: ApiKeysRepository + private readonly apiKeyRepository: ApiKeysRepository, + private readonly platformBookingsService: PlatformBookingsService ) {} @Get("/") @@ -166,7 +168,8 @@ export class BookingsController_2024_04_15 { @Headers(X_CAL_CLIENT_ID) clientId?: string, @Headers(X_CAL_PLATFORM_EMBED) isEmbed?: string ): Promise>> { - const oAuthClientId = clientId?.toString(); + const oAuthClientId = + clientId?.toString() || (await this.getOAuthClientIdFromEventType(body.eventTypeId)); const { orgSlug, locationUrl } = body; req.headers["x-cal-force-slug"] = orgSlug; try { @@ -194,7 +197,7 @@ export class BookingsController_2024_04_15 { async cancelBooking( @Req() req: BookingRequest, @Param("bookingUid") bookingUid: string, - @Body() _: CancelBookingInput_2024_04_15, + @Body() body: CancelBookingInput_2024_04_15, @Headers(X_CAL_CLIENT_ID) clientId?: string, @Headers(X_CAL_PLATFORM_EMBED) isEmbed?: string ): Promise> { @@ -257,11 +260,12 @@ export class BookingsController_2024_04_15 { @Post("/recurring") async createRecurringBooking( @Req() req: BookingRequest, - @Body() _: CreateRecurringBookingInput_2024_04_15[], + @Body() body: CreateRecurringBookingInput_2024_04_15[], @Headers(X_CAL_CLIENT_ID) clientId?: string, @Headers(X_CAL_PLATFORM_EMBED) isEmbed?: string ): Promise> { - const oAuthClientId = clientId?.toString(); + const oAuthClientId = + clientId?.toString() || (await this.getOAuthClientIdFromEventType(body[0]?.eventTypeId)); try { const recurringEventId = uuidv4(); for (const recurringEvent of req.body) { @@ -296,11 +300,12 @@ export class BookingsController_2024_04_15 { @Post("/instant") async createInstantBooking( @Req() req: BookingRequest, - @Body() _: CreateBookingInput_2024_04_15, + @Body() body: CreateBookingInput_2024_04_15, @Headers(X_CAL_CLIENT_ID) clientId?: string, @Headers(X_CAL_PLATFORM_EMBED) isEmbed?: string ): Promise>>> { - const oAuthClientId = clientId?.toString(); + const oAuthClientId = + clientId?.toString() || (await this.getOAuthClientIdFromEventType(body.eventTypeId)); req.userId = (await this.getOwnerId(req)) ?? -1; try { const instantMeeting = await handleInstantMeeting( @@ -346,6 +351,17 @@ export class BookingsController_2024_04_15 { } } + private async getOAuthClientIdFromEventType(eventTypeId: number): Promise { + if (!eventTypeId) { + return undefined; + } + const oAuthClientParams = await this.platformBookingsService.getOAuthClientParams(eventTypeId); + if (!oAuthClientParams) { + return undefined; + } + return oAuthClientParams.platformClientId; + } + private async getOAuthClientsParams(clientId: string, isEmbed = false): Promise { const res = { ...DEFAULT_PLATFORM_PARAMS }; @@ -396,9 +412,27 @@ export class BookingsController_2024_04_15 { noEmail: !oAuthParams.arePlatformEmailsEnabled, creationSource: CreationSource.API_V2, }; + if (oAuthClientId) { + await this.setPlatformAttendeesEmails(clone.body, oAuthClientId); + } return clone as unknown as NextApiRequest & { userId?: number } & OAuthRequestParams; } + async setPlatformAttendeesEmails(requestBody: any, oAuthClientId: string): Promise { + if (requestBody?.responses?.email) { + requestBody.responses.email = await this.platformBookingsService.getPlatformAttendeeEmail( + requestBody.responses.email, + oAuthClientId + ); + } + if (requestBody?.responses?.guests && requestBody?.responses?.guests.length) { + requestBody.responses.guests = await this.platformBookingsService.getPlatformAttendeesEmails( + requestBody.responses.guests, + oAuthClientId + ); + } + } + private async createNextApiRecurringBookingRequest( req: BookingRequest, oAuthClientId?: string, @@ -425,6 +459,9 @@ export class BookingsController_2024_04_15 { noEmail: !oAuthParams.arePlatformEmailsEnabled, creationSource: CreationSource.API_V2, }); + if (oAuthClientId) { + await this.setPlatformAttendeesEmails(clone.body, oAuthClientId); + } return clone as unknown as NextApiRequest & { userId?: number } & OAuthRequestParams; } diff --git a/apps/api/v2/src/ee/bookings/2024-04-15/controllers/managed-user-bookings.e2e-spec.ts b/apps/api/v2/src/ee/bookings/2024-04-15/controllers/managed-user-bookings.e2e-spec.ts new file mode 100644 index 0000000000..100ad8129b --- /dev/null +++ b/apps/api/v2/src/ee/bookings/2024-04-15/controllers/managed-user-bookings.e2e-spec.ts @@ -0,0 +1,461 @@ +import { bootstrap } from "@/app"; +import { AppModule } from "@/app.module"; +import { CreateBookingInput_2024_04_15 } from "@/ee/bookings/2024-04-15/inputs/create-booking.input"; +import { + GetBookingsDataEntry, + GetBookingsOutput_2024_04_15, +} from "@/ee/bookings/2024-04-15/outputs/get-bookings.output"; +import { HttpExceptionFilter } from "@/filters/http-exception.filter"; +import { PrismaExceptionFilter } from "@/filters/prisma-exception.filter"; +import { Locales } from "@/lib/enums/locales"; +import { + CreateManagedUserData, + CreateManagedUserOutput, +} from "@/modules/oauth-clients/controllers/oauth-client-users/outputs/create-managed-user.output"; +import { CreateManagedUserInput } from "@/modules/users/inputs/create-managed-user.input"; +import { UsersModule } from "@/modules/users/users.module"; +import { INestApplication } from "@nestjs/common"; +import { NestExpressApplication } from "@nestjs/platform-express"; +import { Test } from "@nestjs/testing"; +import { PlatformOAuthClient, Team, User } from "@prisma/client"; +import * as request from "supertest"; +import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture"; +import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture"; +import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture"; +import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture"; +import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture"; +import { randomString } from "test/utils/randomString"; + +import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_06_14 } from "@calcom/platform-constants"; +import { + ApiSuccessResponse, + CreateEventTypeInput_2024_06_14, + EventTypeOutput_2024_06_14, +} from "@calcom/platform-types"; + +const CLIENT_REDIRECT_URI = "http://localhost:4321"; + +describe("Managed user bookings 2024-04-15", () => { + let app: INestApplication; + + let oAuthClient: PlatformOAuthClient; + let organization: Team; + let userRepositoryFixture: UserRepositoryFixture; + let oauthClientRepositoryFixture: OAuthClientRepositoryFixture; + let teamRepositoryFixture: TeamRepositoryFixture; + let profilesRepositoryFixture: ProfileRepositoryFixture; + let membershipsRepositoryFixture: MembershipRepositoryFixture; + + const platformAdminEmail = `managed-users-bookings-admin-${randomString()}@api.com`; + let platformAdmin: User; + + const managedUsersTimeZone = "Europe/Rome"; + const firstManagedUserEmail = `managed-user-bookings-2024-04-15-first-user@api.com`; + const secondManagedUserEmail = `managed-user-bookings-2024-04-15-second-user@api.com`; + const thirdManagedUserEmail = `managed-user-bookings-2024-04-15-third-user@api.com`; + + let firstManagedUser: CreateManagedUserData; + let secondManagedUser: CreateManagedUserData; + let thirdManagedUser: CreateManagedUserData; + + let firstManagedUserEventTypeId: number; + + let firstManagedUserBookingsCount = 0; + let secondManagedUserBookingsCount = 0; + let thirdManagedUserBookingsCount = 0; + + beforeAll(async () => { + const moduleRef = await Test.createTestingModule({ + providers: [PrismaExceptionFilter, HttpExceptionFilter], + imports: [AppModule, UsersModule], + }).compile(); + + app = moduleRef.createNestApplication(); + bootstrap(app as NestExpressApplication); + + oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef); + userRepositoryFixture = new UserRepositoryFixture(moduleRef); + teamRepositoryFixture = new TeamRepositoryFixture(moduleRef); + profilesRepositoryFixture = new ProfileRepositoryFixture(moduleRef); + membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef); + + platformAdmin = await userRepositoryFixture.create({ email: platformAdminEmail }); + + organization = await teamRepositoryFixture.create({ + name: `oauth-client-users-organization-${randomString()}`, + isPlatform: true, + isOrganization: true, + }); + oAuthClient = await createOAuthClient(organization.id); + + await profilesRepositoryFixture.create({ + uid: "asd1qwwqeqw-asddsadasd", + username: platformAdminEmail, + organization: { connect: { id: organization.id } }, + user: { + connect: { id: platformAdmin.id }, + }, + }); + + await membershipsRepositoryFixture.create({ + role: "OWNER", + user: { connect: { id: platformAdmin.id } }, + team: { connect: { id: organization.id } }, + accepted: true, + }); + + await app.init(); + }); + + async function createOAuthClient(organizationId: number) { + const data = { + logo: "logo-url", + name: "name", + redirectUris: [CLIENT_REDIRECT_URI], + permissions: 1023, + }; + const secret = "secret"; + + const client = await oauthClientRepositoryFixture.create(organizationId, data, secret); + return client; + } + + it(`should create first managed user`, async () => { + const requestBody: CreateManagedUserInput = { + email: firstManagedUserEmail, + timeZone: managedUsersTimeZone, + weekStart: "Monday", + timeFormat: 24, + locale: Locales.FR, + name: "Alice Smith", + avatarUrl: "https://cal.com/api/avatar/2b735186-b01b-46d3-87da-019b8f61776b.png", + }; + + const response = await request(app.getHttpServer()) + .post(`/api/v2/oauth-clients/${oAuthClient.id}/users`) + .set("x-cal-secret-key", oAuthClient.secret) + .send(requestBody) + .expect(201); + + const responseBody: CreateManagedUserOutput = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + expect(responseBody.data.user.email).toEqual(getOAuthUserEmail(oAuthClient.id, requestBody.email)); + expect(responseBody.data.accessToken).toBeDefined(); + expect(responseBody.data.refreshToken).toBeDefined(); + + firstManagedUser = responseBody.data; + }); + + it("should create an event type for managed user", async () => { + const body: CreateEventTypeInput_2024_06_14 = { + title: "Managed user bookings first managed user event type", + slug: "managed-user-bookings-first-managed-user-event-type", + description: "Managed user bookings first managed user event type description", + lengthInMinutes: 30, + }; + + return request(app.getHttpServer()) + .post("/api/v2/event-types") + .set(CAL_API_VERSION_HEADER, VERSION_2024_06_14) + .set("Authorization", `Bearer ${firstManagedUser.accessToken}`) + .send(body) + .expect(201) + .then(async (response) => { + const responseBody: ApiSuccessResponse = response.body; + const createdEventType = responseBody.data; + expect(createdEventType).toHaveProperty("id"); + expect(createdEventType.title).toEqual(body.title); + expect(createdEventType.description).toEqual(body.description); + expect(createdEventType.lengthInMinutes).toEqual(body.lengthInMinutes); + expect(createdEventType.ownerId).toEqual(firstManagedUser.user.id); + firstManagedUserEventTypeId = createdEventType.id; + }); + }); + + it(`should create second managed user`, async () => { + const requestBody: CreateManagedUserInput = { + email: secondManagedUserEmail, + timeZone: managedUsersTimeZone, + weekStart: "Monday", + timeFormat: 24, + locale: Locales.FR, + name: "Bob Smith", + avatarUrl: "https://cal.com/api/avatar/2b735186-b01b-46d3-87da-019b8f61776b.png", + }; + + const response = await request(app.getHttpServer()) + .post(`/api/v2/oauth-clients/${oAuthClient.id}/users`) + .set("x-cal-secret-key", oAuthClient.secret) + .send(requestBody) + .expect(201); + + const responseBody: CreateManagedUserOutput = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + expect(responseBody.data.user.email).toEqual(getOAuthUserEmail(oAuthClient.id, requestBody.email)); + expect(responseBody.data.accessToken).toBeDefined(); + expect(responseBody.data.refreshToken).toBeDefined(); + + secondManagedUser = responseBody.data; + }); + + it(`should create third managed user`, async () => { + const requestBody: CreateManagedUserInput = { + email: thirdManagedUserEmail, + timeZone: managedUsersTimeZone, + weekStart: "Monday", + timeFormat: 24, + locale: Locales.FR, + name: "Charlie Smith", + avatarUrl: "https://cal.com/api/avatar/2b735186-b01b-46d3-87da-019b8f61776b.png", + }; + + const response = await request(app.getHttpServer()) + .post(`/api/v2/oauth-clients/${oAuthClient.id}/users`) + .set("x-cal-secret-key", oAuthClient.secret) + .send(requestBody) + .expect(201); + + const responseBody: CreateManagedUserOutput = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + expect(responseBody.data.user.email).toEqual(getOAuthUserEmail(oAuthClient.id, requestBody.email)); + expect(responseBody.data.accessToken).toBeDefined(); + expect(responseBody.data.refreshToken).toBeDefined(); + + thirdManagedUser = responseBody.data; + }); + + describe("bookings using original emails", () => { + it("managed user should be booked by managed user attendee and booking shows up in both users' bookings", async () => { + const body: CreateBookingInput_2024_04_15 = { + start: "2040-05-21T09:30:00.000Z", + end: "2040-05-21T10:00:00.000Z", + eventTypeId: firstManagedUserEventTypeId, + timeZone: "Europe/London", + language: "en", + metadata: {}, + hashedLink: "", + responses: { + name: secondManagedUser.user.name || "unknown-name", + email: secondManagedUserEmail, + location: { + value: "attendeeInPerson", + optionValue: "rome", + }, + notes: "test", + guests: [], + }, + }; + + await request(app.getHttpServer()).post("/v2/bookings").send(body).expect(201); + + firstManagedUserBookingsCount += 1; + secondManagedUserBookingsCount += 1; + + const getFirstManagedUserBookings = await request(app.getHttpServer()) + .get("/v2/bookings") + .set("Authorization", `Bearer ${firstManagedUser.accessToken}`) + .expect(200); + + const firstManagedUserBookingsResponseBody: GetBookingsOutput_2024_04_15 = + getFirstManagedUserBookings.body; + const firstManagedUserBookings: GetBookingsDataEntry[] = + firstManagedUserBookingsResponseBody.data.bookings; + expect(firstManagedUserBookings.length).toEqual(firstManagedUserBookingsCount); + + const getSecondManagedUserBookings = await request(app.getHttpServer()) + .get("/v2/bookings") + .set("Authorization", `Bearer ${secondManagedUser.accessToken}`) + .expect(200); + const secondManagedUserBookingsResponseBody: GetBookingsOutput_2024_04_15 = + getSecondManagedUserBookings.body; + const secondManagedUserBookings: GetBookingsDataEntry[] = + secondManagedUserBookingsResponseBody.data.bookings; + expect(secondManagedUserBookings.length).toEqual(secondManagedUserBookingsCount); + }); + + it("managed user should be booked by managed user attendee and managed user as a guest and booking shows up in all three users' bookings", async () => { + const body: CreateBookingInput_2024_04_15 = { + start: "2040-05-21T10:30:00.000Z", + end: "2040-05-21T11:00:00.000Z", + eventTypeId: firstManagedUserEventTypeId, + timeZone: "Europe/London", + language: "en", + metadata: {}, + hashedLink: "", + responses: { + name: thirdManagedUser.user.name || "unknown-name", + email: thirdManagedUserEmail, + location: { + value: "attendeeInPerson", + optionValue: "rome", + }, + notes: "test", + guests: [secondManagedUserEmail], + }, + }; + + await request(app.getHttpServer()).post("/v2/bookings").send(body).expect(201); + + firstManagedUserBookingsCount += 1; + secondManagedUserBookingsCount += 1; + thirdManagedUserBookingsCount += 1; + + const getFirstManagedUserBookings = await request(app.getHttpServer()) + .get("/v2/bookings") + .set("Authorization", `Bearer ${firstManagedUser.accessToken}`) + .expect(200); + const firstManagedUserBookingsResponseBody: GetBookingsOutput_2024_04_15 = + getFirstManagedUserBookings.body; + const firstManagedUserBookings: GetBookingsDataEntry[] = + firstManagedUserBookingsResponseBody.data.bookings; + expect(firstManagedUserBookings.length).toEqual(firstManagedUserBookingsCount); + + const getSecondManagedUserBookings = await request(app.getHttpServer()) + .get("/v2/bookings") + .set("Authorization", `Bearer ${secondManagedUser.accessToken}`) + .expect(200); + const secondManagedUserBookingsResponseBody: GetBookingsOutput_2024_04_15 = + getSecondManagedUserBookings.body; + const secondManagedUserBookings: GetBookingsDataEntry[] = + secondManagedUserBookingsResponseBody.data.bookings; + expect(secondManagedUserBookings.length).toEqual(secondManagedUserBookingsCount); + + const getThirdManagedUserBookings = await request(app.getHttpServer()) + .get("/v2/bookings") + .set("Authorization", `Bearer ${thirdManagedUser.accessToken}`) + .expect(200); + const thirdManagedUserBookingsResponseBody: GetBookingsOutput_2024_04_15 = + getThirdManagedUserBookings.body; + const thirdManagedUserBookings: GetBookingsDataEntry[] = + thirdManagedUserBookingsResponseBody.data.bookings; + expect(thirdManagedUserBookings.length).toEqual(thirdManagedUserBookingsCount); + }); + }); + + describe("bookings using OAuth client emails", () => { + it("managed user should be booked by managed user attendee and booking shows up in both users' bookings", async () => { + const body: CreateBookingInput_2024_04_15 = { + start: "2040-05-21T12:00:00.000Z", + end: "2040-05-21T12:30:00.000Z", + eventTypeId: firstManagedUserEventTypeId, + timeZone: "Europe/London", + language: "en", + metadata: {}, + hashedLink: "", + responses: { + name: secondManagedUser.user.name || "unknown-name", + email: secondManagedUser.user.email, + location: { + value: "attendeeInPerson", + optionValue: "rome", + }, + notes: "test", + guests: [], + }, + }; + + await request(app.getHttpServer()).post("/v2/bookings").send(body).expect(201); + + firstManagedUserBookingsCount += 1; + secondManagedUserBookingsCount += 1; + + const getFirstManagedUserBookings = await request(app.getHttpServer()) + .get("/v2/bookings") + .set("Authorization", `Bearer ${firstManagedUser.accessToken}`) + .expect(200); + + const firstManagedUserBookingsResponseBody: GetBookingsOutput_2024_04_15 = + getFirstManagedUserBookings.body; + const firstManagedUserBookings: GetBookingsDataEntry[] = + firstManagedUserBookingsResponseBody.data.bookings; + expect(firstManagedUserBookings.length).toEqual(firstManagedUserBookingsCount); + + const getSecondManagedUserBookings = await request(app.getHttpServer()) + .get("/v2/bookings") + .set("Authorization", `Bearer ${secondManagedUser.accessToken}`) + .expect(200); + + const secondManagedUserBookingsResponseBody: GetBookingsOutput_2024_04_15 = + getSecondManagedUserBookings.body; + const secondManagedUserBookings: GetBookingsDataEntry[] = + secondManagedUserBookingsResponseBody.data.bookings; + expect(secondManagedUserBookings.length).toEqual(secondManagedUserBookingsCount); + }); + + it("managed user should be booked by managed user attendee and managed user as a guest and booking shows up in all three users' bookings", async () => { + const body: CreateBookingInput_2024_04_15 = { + start: "2040-05-21T13:00:00.000Z", + end: "2040-05-21T13:30:00.000Z", + eventTypeId: firstManagedUserEventTypeId, + timeZone: "Europe/London", + language: "en", + metadata: {}, + hashedLink: "", + responses: { + name: thirdManagedUser.user.name || "unknown-name", + email: thirdManagedUser.user.email, + location: { + value: "attendeeInPerson", + optionValue: "rome", + }, + notes: "test", + guests: [secondManagedUser.user.email], + }, + }; + + await request(app.getHttpServer()).post("/v2/bookings").send(body).expect(201); + + firstManagedUserBookingsCount += 1; + secondManagedUserBookingsCount += 1; + thirdManagedUserBookingsCount += 1; + + const getFirstManagedUserBookings = await request(app.getHttpServer()) + .get("/v2/bookings") + .set("Authorization", `Bearer ${firstManagedUser.accessToken}`) + .expect(200); + const firstManagedUserBookingsResponseBody: GetBookingsOutput_2024_04_15 = + getFirstManagedUserBookings.body; + const firstManagedUserBookings: GetBookingsDataEntry[] = + firstManagedUserBookingsResponseBody.data.bookings; + expect(firstManagedUserBookings.length).toEqual(firstManagedUserBookingsCount); + + const getSecondManagedUserBookings = await request(app.getHttpServer()) + .get("/v2/bookings") + .set("Authorization", `Bearer ${secondManagedUser.accessToken}`) + .expect(200); + const secondManagedUserBookingsResponseBody: GetBookingsOutput_2024_04_15 = + getSecondManagedUserBookings.body; + const secondManagedUserBookings: GetBookingsDataEntry[] = + secondManagedUserBookingsResponseBody.data.bookings; + expect(secondManagedUserBookings.length).toEqual(secondManagedUserBookingsCount); + + const getThirdManagedUserBookings = await request(app.getHttpServer()) + .get("/v2/bookings") + .set("Authorization", `Bearer ${thirdManagedUser.accessToken}`) + .expect(200); + const thirdManagedUserBookingsResponseBody: GetBookingsOutput_2024_04_15 = + getThirdManagedUserBookings.body; + const thirdManagedUserBookings: GetBookingsDataEntry[] = + thirdManagedUserBookingsResponseBody.data.bookings; + expect(thirdManagedUserBookings.length).toEqual(thirdManagedUserBookingsCount); + }); + }); + + function getOAuthUserEmail(oAuthClientId: string, userEmail: string) { + const [username, emailDomain] = userEmail.split("@"); + return `${username}+${oAuthClientId}@${emailDomain}`; + } + + afterAll(async () => { + await userRepositoryFixture.delete(firstManagedUser.user.id); + await userRepositoryFixture.delete(secondManagedUser.user.id); + await userRepositoryFixture.delete(thirdManagedUser.user.id); + await userRepositoryFixture.delete(platformAdmin.id); + await oauthClientRepositoryFixture.delete(oAuthClient.id); + await teamRepositoryFixture.delete(organization.id); + await app.close(); + }); +}); diff --git a/apps/api/v2/src/ee/bookings/2024-04-15/inputs/create-booking.input.ts b/apps/api/v2/src/ee/bookings/2024-04-15/inputs/create-booking.input.ts index 30512f2ab8..09006269d1 100644 --- a/apps/api/v2/src/ee/bookings/2024-04-15/inputs/create-booking.input.ts +++ b/apps/api/v2/src/ee/bookings/2024-04-15/inputs/create-booking.input.ts @@ -8,8 +8,9 @@ import { IsOptional, IsArray, IsObject, - IsEmail, ValidateNested, + isEmail, + Validate, } from "class-validator"; class Location { @@ -27,14 +28,17 @@ class Response { @ApiProperty() name!: string; - @IsEmail() + @Validate((value: string) => !value || isEmail(value), { + message: "Invalid response email", + }) @ApiProperty() email!: string; + @IsOptional() @IsArray() @IsString({ each: true }) - @ApiProperty({ type: [String] }) - guests!: string[]; + @ApiPropertyOptional({ type: [String] }) + guests?: string[]; @IsOptional() @ValidateNested() @@ -113,8 +117,9 @@ export class CreateBookingInput_2024_04_15 { @ApiPropertyOptional() seatReferenceUid?: string; - @Type(() => Response) @ApiProperty({ type: Response }) + @ValidateNested() + @Type(() => Response) responses!: Response; @IsString() diff --git a/apps/api/v2/src/ee/bookings/2024-04-15/outputs/get-bookings.output.ts b/apps/api/v2/src/ee/bookings/2024-04-15/outputs/get-bookings.output.ts index 91270c24fb..743863ecc4 100644 --- a/apps/api/v2/src/ee/bookings/2024-04-15/outputs/get-bookings.output.ts +++ b/apps/api/v2/src/ee/bookings/2024-04-15/outputs/get-bookings.output.ts @@ -160,7 +160,7 @@ class User { email!: string; } -class GetBookingsDataEntry { +export class GetBookingsDataEntry { @IsInt() @ApiProperty() id!: number; diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/bookings.module.ts b/apps/api/v2/src/ee/bookings/2024-08-13/bookings.module.ts index 42b732f33c..6585b7055b 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/bookings.module.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/bookings.module.ts @@ -3,12 +3,17 @@ import { BookingsController_2024_08_13 } from "@/ee/bookings/2024-08-13/controll import { BookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/bookings.service"; import { InputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/input.service"; import { OutputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/output.service"; +import { PlatformBookingsService } from "@/ee/bookings/shared/platform-bookings.service"; +import { EventTypesModule_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/event-types.module"; +import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module"; import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository"; +import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module"; import { ApiKeysRepository } from "@/modules/api-keys/api-keys-repository"; import { BillingModule } from "@/modules/billing/billing.module"; import { BookingSeatModule } from "@/modules/booking-seat/booking-seat.module"; import { BookingSeatRepository } from "@/modules/booking-seat/booking-seat.repository"; import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository"; +import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-clients-users.service"; import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service"; import { PrismaModule } from "@/modules/prisma/prisma.module"; import { RedisModule } from "@/modules/redis/redis.module"; @@ -18,11 +23,22 @@ import { UsersModule } from "@/modules/users/users.module"; import { Module } from "@nestjs/common"; @Module({ - imports: [PrismaModule, RedisModule, TokensModule, BillingModule, UsersModule, BookingSeatModule], + imports: [ + PrismaModule, + RedisModule, + TokensModule, + BillingModule, + UsersModule, + BookingSeatModule, + SchedulesModule_2024_04_15, + EventTypesModule_2024_04_15, + EventTypesModule_2024_06_14, + ], providers: [ TokensRepository, OAuthFlowService, OAuthClientRepository, + OAuthClientUsersService, BookingsService_2024_08_13, InputBookingsService_2024_08_13, OutputBookingsService_2024_08_13, @@ -30,6 +46,7 @@ import { Module } from "@nestjs/common"; EventTypesRepository_2024_06_14, BookingSeatRepository, ApiKeysRepository, + PlatformBookingsService, ], controllers: [BookingsController_2024_08_13], exports: [InputBookingsService_2024_08_13, OutputBookingsService_2024_08_13, BookingsService_2024_08_13], diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/managed-user-bookings.e2e-spec.ts b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/managed-user-bookings.e2e-spec.ts new file mode 100644 index 0000000000..3fcd33e7c7 --- /dev/null +++ b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/managed-user-bookings.e2e-spec.ts @@ -0,0 +1,487 @@ +import { bootstrap } from "@/app"; +import { AppModule } from "@/app.module"; +import { HttpExceptionFilter } from "@/filters/http-exception.filter"; +import { PrismaExceptionFilter } from "@/filters/prisma-exception.filter"; +import { Locales } from "@/lib/enums/locales"; +import { + CreateManagedUserData, + CreateManagedUserOutput, +} from "@/modules/oauth-clients/controllers/oauth-client-users/outputs/create-managed-user.output"; +import { CreateManagedUserInput } from "@/modules/users/inputs/create-managed-user.input"; +import { UsersModule } from "@/modules/users/users.module"; +import { INestApplication } from "@nestjs/common"; +import { NestExpressApplication } from "@nestjs/platform-express"; +import { Test } from "@nestjs/testing"; +import { PlatformOAuthClient, Team, User } from "@prisma/client"; +import * as request from "supertest"; +import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture"; +import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture"; +import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture"; +import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture"; +import { SchedulesRepositoryFixture } from "test/fixtures/repository/schedules.repository.fixture"; +import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture"; +import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture"; +import { randomString } from "test/utils/randomString"; + +import { + CAL_API_VERSION_HEADER, + SUCCESS_STATUS, + VERSION_2024_06_14, + VERSION_2024_08_13, +} from "@calcom/platform-constants"; +import { + ApiSuccessResponse, + BookingOutput_2024_08_13, + CreateBookingInput_2024_08_13, + CreateEventTypeInput_2024_06_14, + EventTypeOutput_2024_06_14, + GetBookingsOutput_2024_08_13, +} from "@calcom/platform-types"; + +const CLIENT_REDIRECT_URI = "http://localhost:4321"; + +describe("Managed user bookings 2024-08-13", () => { + let app: INestApplication; + + let oAuthClient: PlatformOAuthClient; + let organization: Team; + let userRepositoryFixture: UserRepositoryFixture; + let oauthClientRepositoryFixture: OAuthClientRepositoryFixture; + let teamRepositoryFixture: TeamRepositoryFixture; + let eventTypesRepositoryFixture: EventTypesRepositoryFixture; + let schedulesRepositoryFixture: SchedulesRepositoryFixture; + let profilesRepositoryFixture: ProfileRepositoryFixture; + let membershipsRepositoryFixture: MembershipRepositoryFixture; + + const platformAdminEmail = `managed-users-bookings-2024-08-13-admin-${randomString()}@api.com`; + let platformAdmin: User; + + const managedUsersTimeZone = "Europe/Rome"; + const firstManagedUserEmail = `managed-user-bookings-2024-08-13-first-user-${randomString()}@api.com`; + const secondManagedUserEmail = `managed-user-bookings-2024-08-13-second-user-${randomString()}@api.com`; + const thirdManagedUserEmail = `managed-user-bookings-2024-08-13-third-user-${randomString()}@api.com`; + + let firstManagedUser: CreateManagedUserData; + let secondManagedUser: CreateManagedUserData; + let thirdManagedUser: CreateManagedUserData; + + let firstManagedUserEventTypeId: number; + + let firstManagedUserBookingsCount = 0; + let secondManagedUserBookingsCount = 0; + let thirdManagedUserBookingsCount = 0; + + beforeAll(async () => { + const moduleRef = await Test.createTestingModule({ + providers: [PrismaExceptionFilter, HttpExceptionFilter], + imports: [AppModule, UsersModule], + }).compile(); + + app = moduleRef.createNestApplication(); + bootstrap(app as NestExpressApplication); + + oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef); + userRepositoryFixture = new UserRepositoryFixture(moduleRef); + teamRepositoryFixture = new TeamRepositoryFixture(moduleRef); + eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef); + schedulesRepositoryFixture = new SchedulesRepositoryFixture(moduleRef); + profilesRepositoryFixture = new ProfileRepositoryFixture(moduleRef); + membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef); + + platformAdmin = await userRepositoryFixture.create({ email: platformAdminEmail }); + + organization = await teamRepositoryFixture.create({ + name: `oauth-client-users-organization-${randomString()}`, + isPlatform: true, + isOrganization: true, + }); + oAuthClient = await createOAuthClient(organization.id); + + await profilesRepositoryFixture.create({ + uid: "asd1qwwqeqw-asddsadasd", + username: platformAdminEmail, + organization: { connect: { id: organization.id } }, + user: { + connect: { id: platformAdmin.id }, + }, + }); + + await membershipsRepositoryFixture.create({ + role: "OWNER", + user: { connect: { id: platformAdmin.id } }, + team: { connect: { id: organization.id } }, + accepted: true, + }); + + await app.init(); + }); + + async function createOAuthClient(organizationId: number) { + const data = { + logo: "logo-url", + name: "name", + redirectUris: [CLIENT_REDIRECT_URI], + permissions: 1023, + }; + const secret = "secret"; + + const client = await oauthClientRepositoryFixture.create(organizationId, data, secret); + return client; + } + + it(`should create first managed user`, async () => { + const requestBody: CreateManagedUserInput = { + email: firstManagedUserEmail, + timeZone: managedUsersTimeZone, + weekStart: "Monday", + timeFormat: 24, + locale: Locales.FR, + name: "Alice Smith", + avatarUrl: "https://cal.com/api/avatar/2b735186-b01b-46d3-87da-019b8f61776b.png", + }; + + const response = await request(app.getHttpServer()) + .post(`/api/v2/oauth-clients/${oAuthClient.id}/users`) + .set("x-cal-secret-key", oAuthClient.secret) + .send(requestBody) + .expect(201); + + const responseBody: CreateManagedUserOutput = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + expect(responseBody.data.user.email).toEqual(getOAuthUserEmail(oAuthClient.id, requestBody.email)); + expect(responseBody.data.accessToken).toBeDefined(); + expect(responseBody.data.refreshToken).toBeDefined(); + + firstManagedUser = responseBody.data; + }); + + it("should create an event type for managed user", async () => { + const body: CreateEventTypeInput_2024_06_14 = { + title: "Managed user bookings first managed user event type", + slug: "managed-user-bookings-first-managed-user-event-type", + description: "Managed user bookings first managed user event type description", + lengthInMinutes: 30, + }; + + return request(app.getHttpServer()) + .post("/api/v2/event-types") + .set(CAL_API_VERSION_HEADER, VERSION_2024_06_14) + .set("Authorization", `Bearer ${firstManagedUser.accessToken}`) + .send(body) + .expect(201) + .then(async (response) => { + const responseBody: ApiSuccessResponse = response.body; + const createdEventType = responseBody.data; + expect(createdEventType).toHaveProperty("id"); + expect(createdEventType.title).toEqual(body.title); + expect(createdEventType.description).toEqual(body.description); + expect(createdEventType.lengthInMinutes).toEqual(body.lengthInMinutes); + expect(createdEventType.ownerId).toEqual(firstManagedUser.user.id); + firstManagedUserEventTypeId = createdEventType.id; + }); + }); + + it(`should create second managed user`, async () => { + const requestBody: CreateManagedUserInput = { + email: secondManagedUserEmail, + timeZone: managedUsersTimeZone, + weekStart: "Monday", + timeFormat: 24, + locale: Locales.FR, + name: "Bob Smith", + avatarUrl: "https://cal.com/api/avatar/2b735186-b01b-46d3-87da-019b8f61776b.png", + }; + + const response = await request(app.getHttpServer()) + .post(`/api/v2/oauth-clients/${oAuthClient.id}/users`) + .set("x-cal-secret-key", oAuthClient.secret) + .send(requestBody) + .expect(201); + + const responseBody: CreateManagedUserOutput = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + expect(responseBody.data.user.email).toEqual(getOAuthUserEmail(oAuthClient.id, requestBody.email)); + expect(responseBody.data.accessToken).toBeDefined(); + expect(responseBody.data.refreshToken).toBeDefined(); + + secondManagedUser = responseBody.data; + }); + + it(`should create third managed user`, async () => { + const requestBody: CreateManagedUserInput = { + email: thirdManagedUserEmail, + timeZone: managedUsersTimeZone, + weekStart: "Monday", + timeFormat: 24, + locale: Locales.FR, + name: "Charlie Smith", + avatarUrl: "https://cal.com/api/avatar/2b735186-b01b-46d3-87da-019b8f61776b.png", + }; + + const response = await request(app.getHttpServer()) + .post(`/api/v2/oauth-clients/${oAuthClient.id}/users`) + .set("x-cal-secret-key", oAuthClient.secret) + .send(requestBody) + .expect(201); + + const responseBody: CreateManagedUserOutput = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + expect(responseBody.data.user.email).toEqual(getOAuthUserEmail(oAuthClient.id, requestBody.email)); + expect(responseBody.data.accessToken).toBeDefined(); + expect(responseBody.data.refreshToken).toBeDefined(); + + thirdManagedUser = responseBody.data; + }); + + describe("bookings using original emails", () => { + it("managed user should be booked by managed user attendee and booking shows up in both users' bookings", async () => { + const body: CreateBookingInput_2024_08_13 = { + start: new Date(Date.UTC(2030, 0, 8, 13, 0, 0)).toISOString(), + eventTypeId: firstManagedUserEventTypeId, + attendee: { + name: secondManagedUser.user.name!, + email: secondManagedUserEmail, + timeZone: secondManagedUser.user.timeZone, + language: secondManagedUser.user.locale, + }, + location: "https://meet.google.com/abc-def-ghi", + }; + + await request(app.getHttpServer()) + .post("/v2/bookings") + .send(body) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .expect(201); + + firstManagedUserBookingsCount += 1; + secondManagedUserBookingsCount += 1; + + const getFirstManagedUserBookings = await request(app.getHttpServer()) + .get("/v2/bookings") + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .set("Authorization", `Bearer ${firstManagedUser.accessToken}`) + .expect(200); + + const firstManagedUserBookingsResponseBody: GetBookingsOutput_2024_08_13 = + getFirstManagedUserBookings.body; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const firstManagedUserBookings: BookingOutput_2024_08_13[] = firstManagedUserBookingsResponseBody.data; + expect(firstManagedUserBookings.length).toEqual(firstManagedUserBookingsCount); + + const getSecondManagedUserBookings = await request(app.getHttpServer()) + .get("/v2/bookings") + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .set("Authorization", `Bearer ${secondManagedUser.accessToken}`) + .expect(200); + + const secondManagedUserBookingsResponseBody: GetBookingsOutput_2024_08_13 = + getSecondManagedUserBookings.body; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const secondManagedUserBookings: BookingOutput_2024_08_13[] = + secondManagedUserBookingsResponseBody.data; + expect(secondManagedUserBookings.length).toEqual(secondManagedUserBookingsCount); + }); + + it("managed user should be booked by managed user attendee and managed user as a guest and booking shows up in all three users' bookings", async () => { + const body: CreateBookingInput_2024_08_13 = { + start: new Date(Date.UTC(2030, 0, 8, 14, 0, 0)).toISOString(), + eventTypeId: firstManagedUserEventTypeId, + attendee: { + name: thirdManagedUser.user.name!, + email: thirdManagedUserEmail, + timeZone: secondManagedUser.user.timeZone, + language: secondManagedUser.user.locale, + }, + guests: [secondManagedUserEmail], + location: "https://meet.google.com/abc-def-ghi", + }; + + await request(app.getHttpServer()) + .post("/v2/bookings") + .send(body) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .expect(201); + + firstManagedUserBookingsCount += 1; + secondManagedUserBookingsCount += 1; + thirdManagedUserBookingsCount += 1; + + const getFirstManagedUserBookings = await request(app.getHttpServer()) + .get("/v2/bookings") + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .set("Authorization", `Bearer ${firstManagedUser.accessToken}`) + .expect(200); + + const firstManagedUserBookingsResponseBody: GetBookingsOutput_2024_08_13 = + getFirstManagedUserBookings.body; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const firstManagedUserBookings: BookingOutput_2024_08_13[] = firstManagedUserBookingsResponseBody.data; + expect(firstManagedUserBookings.length).toEqual(firstManagedUserBookingsCount); + + const getSecondManagedUserBookings = await request(app.getHttpServer()) + .get("/v2/bookings") + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .set("Authorization", `Bearer ${secondManagedUser.accessToken}`) + .expect(200); + + const secondManagedUserBookingsResponseBody: GetBookingsOutput_2024_08_13 = + getSecondManagedUserBookings.body; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const secondManagedUserBookings: BookingOutput_2024_08_13[] = + secondManagedUserBookingsResponseBody.data; + expect(secondManagedUserBookings.length).toEqual(secondManagedUserBookingsCount); + + const getThirdManagedUserBookings = await request(app.getHttpServer()) + .get("/v2/bookings") + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .set("Authorization", `Bearer ${thirdManagedUser.accessToken}`) + .expect(200); + + const thirdManagedUserBookingsResponseBody: GetBookingsOutput_2024_08_13 = + getThirdManagedUserBookings.body; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const thirdManagedUserBookings: BookingOutput_2024_08_13[] = thirdManagedUserBookingsResponseBody.data; + expect(thirdManagedUserBookings.length).toEqual(thirdManagedUserBookingsCount); + }); + }); + + describe("bookings using OAuth client emails", () => { + it("managed user should be booked by managed user attendee and booking shows up in both users' bookings", async () => { + const body: CreateBookingInput_2024_08_13 = { + start: new Date(Date.UTC(2030, 0, 8, 15, 0, 0)).toISOString(), + eventTypeId: firstManagedUserEventTypeId, + attendee: { + name: secondManagedUser.user.name!, + email: secondManagedUser.user.email, + timeZone: secondManagedUser.user.timeZone, + language: secondManagedUser.user.locale, + }, + location: "https://meet.google.com/abc-def-ghi", + }; + + await request(app.getHttpServer()) + .post("/v2/bookings") + .send(body) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .expect(201); + + firstManagedUserBookingsCount += 1; + secondManagedUserBookingsCount += 1; + + const getFirstManagedUserBookings = await request(app.getHttpServer()) + .get("/v2/bookings") + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .set("Authorization", `Bearer ${firstManagedUser.accessToken}`) + .expect(200); + + const firstManagedUserBookingsResponseBody: GetBookingsOutput_2024_08_13 = + getFirstManagedUserBookings.body; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const firstManagedUserBookings: BookingOutput_2024_08_13[] = firstManagedUserBookingsResponseBody.data; + expect(firstManagedUserBookings.length).toEqual(firstManagedUserBookingsCount); + + const getSecondManagedUserBookings = await request(app.getHttpServer()) + .get("/v2/bookings") + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .set("Authorization", `Bearer ${secondManagedUser.accessToken}`) + .expect(200); + + const secondManagedUserBookingsResponseBody: GetBookingsOutput_2024_08_13 = + getSecondManagedUserBookings.body; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const secondManagedUserBookings: BookingOutput_2024_08_13[] = + secondManagedUserBookingsResponseBody.data; + expect(secondManagedUserBookings.length).toEqual(secondManagedUserBookingsCount); + }); + + it("managed user should be booked by managed user attendee and managed user as a guest and booking shows up in all three users' bookings", async () => { + const body: CreateBookingInput_2024_08_13 = { + start: new Date(Date.UTC(2030, 0, 8, 15, 30, 0)).toISOString(), + eventTypeId: firstManagedUserEventTypeId, + attendee: { + name: thirdManagedUser.user.name!, + email: thirdManagedUser.user.email, + timeZone: secondManagedUser.user.timeZone, + language: secondManagedUser.user.locale, + }, + guests: [secondManagedUser.user.email], + location: "https://meet.google.com/abc-def-ghi", + }; + + await request(app.getHttpServer()) + .post("/v2/bookings") + .send(body) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .expect(201); + + firstManagedUserBookingsCount += 1; + secondManagedUserBookingsCount += 1; + thirdManagedUserBookingsCount += 1; + + const getFirstManagedUserBookings = await request(app.getHttpServer()) + .get("/v2/bookings") + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .set("Authorization", `Bearer ${firstManagedUser.accessToken}`) + .expect(200); + + const firstManagedUserBookingsResponseBody: GetBookingsOutput_2024_08_13 = + getFirstManagedUserBookings.body; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const firstManagedUserBookings: BookingOutput_2024_08_13[] = firstManagedUserBookingsResponseBody.data; + expect(firstManagedUserBookings.length).toEqual(firstManagedUserBookingsCount); + + const getSecondManagedUserBookings = await request(app.getHttpServer()) + .get("/v2/bookings") + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .set("Authorization", `Bearer ${secondManagedUser.accessToken}`) + .expect(200); + + const secondManagedUserBookingsResponseBody: GetBookingsOutput_2024_08_13 = + getSecondManagedUserBookings.body; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const secondManagedUserBookings: BookingOutput_2024_08_13[] = + secondManagedUserBookingsResponseBody.data; + expect(secondManagedUserBookings.length).toEqual(secondManagedUserBookingsCount); + + const getThirdManagedUserBookings = await request(app.getHttpServer()) + .get("/v2/bookings") + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .set("Authorization", `Bearer ${thirdManagedUser.accessToken}`) + .expect(200); + + const thirdManagedUserBookingsResponseBody: GetBookingsOutput_2024_08_13 = + getThirdManagedUserBookings.body; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const thirdManagedUserBookings: BookingOutput_2024_08_13[] = thirdManagedUserBookingsResponseBody.data; + expect(thirdManagedUserBookings.length).toEqual(thirdManagedUserBookingsCount); + }); + }); + + function getOAuthUserEmail(oAuthClientId: string, userEmail: string) { + const [username, emailDomain] = userEmail.split("@"); + return `${username}+${oAuthClientId}@${emailDomain}`; + } + + afterAll(async () => { + await userRepositoryFixture.delete(firstManagedUser.user.id); + await userRepositoryFixture.delete(secondManagedUser.user.id); + await userRepositoryFixture.delete(thirdManagedUser.user.id); + await userRepositoryFixture.delete(platformAdmin.id); + await oauthClientRepositoryFixture.delete(oAuthClient.id); + await teamRepositoryFixture.delete(organization.id); + await app.close(); + }); +}); diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts index 1fba30a154..b5ff991699 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts @@ -1,6 +1,7 @@ import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.repository"; import { InputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/input.service"; import { OutputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/output.service"; +import { PlatformBookingsService } from "@/ee/bookings/shared/platform-bookings.service"; import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository"; import { BillingService } from "@/modules/billing/services/billing.service"; import { BookingSeatRepository } from "@/modules/booking-seat/booking-seat.repository"; @@ -67,7 +68,8 @@ export class BookingsService_2024_08_13 { private readonly prismaReadService: PrismaReadService, private readonly billingService: BillingService, private readonly usersService: UsersService, - private readonly usersRepository: UsersRepository + private readonly usersRepository: UsersRepository, + private readonly platformBookingsService: PlatformBookingsService ) {} async createBooking(request: Request, body: CreateBookingInput) { @@ -368,7 +370,7 @@ export class BookingsService_2024_08_13 { const bodyTransformed = this.inputService.transformInputMarkAbsentBooking(body); const bookingBefore = await this.bookingsRepository.getByUid(bookingUid); const platformClientParams = bookingBefore?.eventTypeId - ? await this.inputService.getOAuthClientParams(bookingBefore.eventTypeId) + ? await this.platformBookingsService.getOAuthClientParams(bookingBefore.eventTypeId) : undefined; await handleMarkNoShow({ @@ -432,7 +434,7 @@ export class BookingsService_2024_08_13 { } const platformClientParams = booking.eventTypeId - ? await this.inputService.getOAuthClientParams(booking.eventTypeId) + ? await this.platformBookingsService.getOAuthClientParams(booking.eventTypeId) : undefined; const emailsEnabled = platformClientParams ? platformClientParams.arePlatformEmailsEnabled : true; @@ -471,7 +473,7 @@ export class BookingsService_2024_08_13 { } const platformClientParams = booking.eventTypeId - ? await this.inputService.getOAuthClientParams(booking.eventTypeId) + ? await this.platformBookingsService.getOAuthClientParams(booking.eventTypeId) : undefined; const emailsEnabled = platformClientParams ? platformClientParams.arePlatformEmailsEnabled : true; @@ -498,7 +500,7 @@ export class BookingsService_2024_08_13 { } const platformClientParams = booking.eventTypeId - ? await this.inputService.getOAuthClientParams(booking.eventTypeId) + ? await this.platformBookingsService.getOAuthClientParams(booking.eventTypeId) : undefined; const emailsEnabled = platformClientParams ? platformClientParams.arePlatformEmailsEnabled : true; @@ -526,7 +528,7 @@ export class BookingsService_2024_08_13 { } const platformClientParams = booking.eventTypeId - ? await this.inputService.getOAuthClientParams(booking.eventTypeId) + ? await this.platformBookingsService.getOAuthClientParams(booking.eventTypeId) : undefined; const emailsEnabled = platformClientParams ? platformClientParams.arePlatformEmailsEnabled : true; diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts index 72e4645b96..49fb65ea66 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts @@ -3,11 +3,11 @@ import { bookingResponsesSchema, seatedBookingDataSchema, } from "@/ee/bookings/2024-08-13/services/output.service"; +import { PlatformBookingsService } from "@/ee/bookings/shared/platform-bookings.service"; import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository"; import { hashAPIKey, isApiKey, stripApiKey } from "@/lib/api-key"; import { ApiKeysRepository } from "@/modules/api-keys/api-keys-repository"; import { BookingSeatRepository } from "@/modules/booking-seat/booking-seat.repository"; -import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository"; import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service"; import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common"; import { Logger } from "@nestjs/common"; @@ -33,7 +33,7 @@ import { RescheduleBookingInput_2024_08_13, RescheduleSeatedBookingInput_2024_08_13, } from "@calcom/platform-types"; -import { EventType, PlatformOAuthClient } from "@calcom/prisma/client"; +import { EventType } from "@calcom/prisma/client"; type BookingRequest = NextApiRequest & { userId: number | undefined } & OAuthRequestParams; @@ -70,20 +70,20 @@ export class InputBookingsService_2024_08_13 { constructor( private readonly oAuthFlowService: OAuthFlowService, - private readonly oAuthClientRepository: OAuthClientRepository, private readonly eventTypesRepository: EventTypesRepository_2024_06_14, private readonly bookingsRepository: BookingsRepository_2024_08_13, private readonly config: ConfigService, private readonly apiKeyRepository: ApiKeysRepository, - private readonly bookingSeatRepository: BookingSeatRepository + private readonly bookingSeatRepository: BookingSeatRepository, + private readonly platformBookingsService: PlatformBookingsService ) {} async createBookingRequest( request: Request, body: CreateBookingInput_2024_08_13 | CreateInstantBookingInput_2024_08_13 ): Promise { - const bodyTransformed = await this.transformInputCreateBooking(body); - const oAuthClientParams = await this.getOAuthClientParams(body.eventTypeId); + const oAuthClientParams = await this.platformBookingsService.getOAuthClientParams(body.eventTypeId); + const bodyTransformed = await this.transformInputCreateBooking(body, oAuthClientParams?.platformClientId); const newRequest = { ...request }; const userId = (await this.createBookingRequestOwnerId(request)) ?? undefined; @@ -111,30 +111,7 @@ export class InputBookingsService_2024_08_13 { return newRequest as unknown as BookingRequest; } - async getOAuthClientParams(eventTypeId: number) { - const eventType = await this.eventTypesRepository.getEventTypeById(eventTypeId); - - let oAuthClient: PlatformOAuthClient | null = null; - if (eventType?.userId) { - oAuthClient = await this.oAuthClientRepository.getByUserId(eventType.userId); - } else if (eventType?.teamId) { - oAuthClient = await this.oAuthClientRepository.getByTeamId(eventType.teamId); - } - - if (oAuthClient) { - return { - platformClientId: oAuthClient.id, - platformCancelUrl: oAuthClient.bookingCancelRedirectUri, - platformRescheduleUrl: oAuthClient.bookingRescheduleRedirectUri, - platformBookingUrl: oAuthClient.bookingRedirectUri, - arePlatformEmailsEnabled: oAuthClient.areEmailsEnabled, - }; - } - - return undefined; - } - - async transformInputCreateBooking(inputBooking: CreateBookingInput_2024_08_13) { + async transformInputCreateBooking(inputBooking: CreateBookingInput_2024_08_13, platformClientId?: string) { const eventType = await this.eventTypesRepository.getEventTypeByIdWithOwnerAndTeam( inputBooking.eventTypeId ); @@ -151,7 +128,17 @@ export class InputBookingsService_2024_08_13 { ); const endTime = startTime.plus({ minutes: lengthInMinutes }); - const guests = inputBooking.guests; + const guests = + inputBooking.guests && platformClientId + ? await this.platformBookingsService.getPlatformAttendeesEmails(inputBooking.guests, platformClientId) + : inputBooking.guests; + const attendeeEmail = + inputBooking.attendee.email && platformClientId + ? await this.platformBookingsService.getPlatformAttendeeEmail( + inputBooking.attendee.email, + platformClientId + ) + : inputBooking.attendee.email; return { start: startTime.toISO(), @@ -163,20 +150,13 @@ export class InputBookingsService_2024_08_13 { hasHashedBookingLink: false, guests, // note(Lauris): responses with name and email are required by the handleNewBooking - responses: inputBooking.bookingFieldsResponses - ? { - ...inputBooking.bookingFieldsResponses, - name: inputBooking.attendee.name, - email: inputBooking.attendee.email ?? "", - attendeePhoneNumber: inputBooking.attendee.phoneNumber, - guests, - } - : { - name: inputBooking.attendee.name, - email: inputBooking.attendee.email ?? "", - attendeePhoneNumber: inputBooking.attendee.phoneNumber, - guests, - }, + responses: { + ...(inputBooking.bookingFieldsResponses || {}), + name: inputBooking.attendee.name, + email: attendeeEmail ?? "", + attendeePhoneNumber: inputBooking.attendee.phoneNumber, + guests, + }, }; } @@ -203,9 +183,12 @@ export class InputBookingsService_2024_08_13 { request: Request, body: CreateRecurringBookingInput_2024_08_13 ): Promise { + const oAuthClientParams = await this.platformBookingsService.getOAuthClientParams(body.eventTypeId); // note(Lauris): update to this.transformInputCreate when rescheduling is implemented - const bodyTransformed = await this.transformInputCreateRecurringBooking(body); - const oAuthClientParams = await this.getOAuthClientParams(body.eventTypeId); + const bodyTransformed = await this.transformInputCreateRecurringBooking( + body, + oAuthClientParams?.platformClientId + ); const newRequest = { ...request }; const userId = (await this.createBookingRequestOwnerId(request)) ?? undefined; @@ -231,7 +214,10 @@ export class InputBookingsService_2024_08_13 { return newRequest as unknown as BookingRequest; } - async transformInputCreateRecurringBooking(inputBooking: CreateRecurringBookingInput_2024_08_13) { + async transformInputCreateRecurringBooking( + inputBooking: CreateRecurringBookingInput_2024_08_13, + platformClientId?: string + ) { const eventType = await this.eventTypesRepository.getEventTypeByIdWithOwnerAndTeam( inputBooking.eventTypeId ); @@ -261,7 +247,17 @@ export class InputBookingsService_2024_08_13 { inputBooking.attendee.timeZone ); - const guests = inputBooking.guests; + const guests = + inputBooking.guests && platformClientId + ? await this.platformBookingsService.getPlatformAttendeesEmails(inputBooking.guests, platformClientId) + : inputBooking.guests; + const attendeeEmail = + inputBooking.attendee.email && platformClientId + ? await this.platformBookingsService.getPlatformAttendeeEmail( + inputBooking.attendee.email, + platformClientId + ) + : inputBooking.attendee.email; for (let i = 0; i < repeatsTimes; i++) { const endTime = startTime.plus({ minutes: eventType.length }); @@ -277,14 +273,12 @@ export class InputBookingsService_2024_08_13 { hasHashedBookingLink: false, guests, // note(Lauris): responses with name and email are required by the handleNewBooking - responses: inputBooking.bookingFieldsResponses - ? { - ...inputBooking.bookingFieldsResponses, - name: inputBooking.attendee.name, - email: inputBooking.attendee.email, - guests, - } - : { name: inputBooking.attendee.name, email: inputBooking.attendee.email, guests }, + responses: { + ...(inputBooking.bookingFieldsResponses || {}), + name: inputBooking.attendee.name, + email: attendeeEmail, + guests, + }, schedulingType: eventType.schedulingType, }); @@ -315,7 +309,9 @@ export class InputBookingsService_2024_08_13 { ? await this.transformInputRescheduleSeatedBooking(bookingUid, body) : await this.transformInputRescheduleBooking(bookingUid, body); - const oAuthClientParams = await this.getOAuthClientParams(bodyTransformed.eventTypeId); + const oAuthClientParams = await this.platformBookingsService.getOAuthClientParams( + bodyTransformed.eventTypeId + ); const newRequest = { ...request }; const userId = (await this.createBookingRequestOwnerId(request)) ?? undefined; @@ -501,7 +497,7 @@ export class InputBookingsService_2024_08_13 { } const oAuthClientParams = booking.eventTypeId - ? await this.getOAuthClientParams(booking.eventTypeId) + ? await this.platformBookingsService.getOAuthClientParams(booking.eventTypeId) : undefined; const newRequest = { ...request }; diff --git a/apps/api/v2/src/ee/bookings/shared/platform-bookings.service.ts b/apps/api/v2/src/ee/bookings/shared/platform-bookings.service.ts new file mode 100644 index 0000000000..5d1145ce0d --- /dev/null +++ b/apps/api/v2/src/ee/bookings/shared/platform-bookings.service.ts @@ -0,0 +1,59 @@ +import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository"; +import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository"; +import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-clients-users.service"; +import { UsersRepository } from "@/modules/users/users.repository"; +import { Injectable } from "@nestjs/common"; + +import { PlatformOAuthClient } from "@calcom/prisma/client"; + +@Injectable() +export class PlatformBookingsService { + constructor( + private readonly oAuthClientsUsersService: OAuthClientUsersService, + private readonly usersRepository: UsersRepository, + private readonly eventTypesRepository: EventTypesRepository_2024_06_14, + private readonly oAuthClientRepository: OAuthClientRepository + ) {} + + async getPlatformAttendeesEmails(guestsEmails: string[], platformClientId: string) { + return Promise.all( + guestsEmails.map((guestEmail) => this.getPlatformAttendeeEmail(guestEmail, platformClientId)) + ); + } + + async getPlatformAttendeeEmail(attendeeEmail: string, platformClientId: string) { + if (!attendeeEmail.includes(platformClientId)) { + // note(Lauris): we need to do this when managed user is booking another managed user and the managed user doing the booking entered their email without oAuth client id + // or if one of the guests added are managed users without oAuth client id in their email. + const oAuthUserEmail = this.oAuthClientsUsersService.getOAuthUserEmail(platformClientId, attendeeEmail); + const oAuthUser = await this.usersRepository.findByEmail(oAuthUserEmail); + if (oAuthUser) { + return oAuthUserEmail; + } + } + return attendeeEmail; + } + + async getOAuthClientParams(eventTypeId: number) { + const eventType = await this.eventTypesRepository.getEventTypeById(eventTypeId); + + let oAuthClient: PlatformOAuthClient | null = null; + if (eventType?.userId) { + oAuthClient = await this.oAuthClientRepository.getByUserId(eventType.userId); + } else if (eventType?.teamId) { + oAuthClient = await this.oAuthClientRepository.getByTeamId(eventType.teamId); + } + + if (oAuthClient) { + return { + platformClientId: oAuthClient.id, + platformCancelUrl: oAuthClient.bookingCancelRedirectUri, + platformRescheduleUrl: oAuthClient.bookingRescheduleRedirectUri, + platformBookingUrl: oAuthClient.bookingRedirectUri, + arePlatformEmailsEnabled: oAuthClient.areEmailsEnabled, + }; + } + + return undefined; + } +} diff --git a/apps/api/v2/src/modules/oauth-clients/controllers/oauth-client-users/outputs/create-managed-user.output.ts b/apps/api/v2/src/modules/oauth-clients/controllers/oauth-client-users/outputs/create-managed-user.output.ts index 54c2e40e0f..0fcc1ffe32 100644 --- a/apps/api/v2/src/modules/oauth-clients/controllers/oauth-client-users/outputs/create-managed-user.output.ts +++ b/apps/api/v2/src/modules/oauth-clients/controllers/oauth-client-users/outputs/create-managed-user.output.ts @@ -5,7 +5,7 @@ import { IsEnum, IsNumber, IsString, ValidateNested } from "class-validator"; import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants"; -class CreateManagedUserData { +export class CreateManagedUserData { @ApiProperty({ type: ManagedUserOutput, }) diff --git a/apps/api/v2/src/modules/oauth-clients/services/oauth-clients-users.service.ts b/apps/api/v2/src/modules/oauth-clients/services/oauth-clients-users.service.ts index 809eb37bbb..907b3a98fe 100644 --- a/apps/api/v2/src/modules/oauth-clients/services/oauth-clients-users.service.ts +++ b/apps/api/v2/src/modules/oauth-clients/services/oauth-clients-users.service.ts @@ -1,6 +1,5 @@ import { EventTypesService_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/services/event-types.service"; import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service"; -import { OrganizationsTeamsService } from "@/modules/organizations/teams/index/services/organizations-teams.service"; import { TokensRepository } from "@/modules/tokens/tokens.repository"; import { CreateManagedUserInput } from "@/modules/users/inputs/create-managed-user.input"; import { UpdateManagedUserInput } from "@/modules/users/inputs/update-managed-user.input";