From 563b31b7d0d04ab2e4b26c7f2303300a6c374e19 Mon Sep 17 00:00:00 2001 From: Lauris Skraucis Date: Wed, 2 Apr 2025 17:21:27 +0200 Subject: [PATCH] fix: v2 bookings attendeeEmail filter (#20506) * fix: GET bookings if attendeeEmail belong to managed user * fix: GET bookings if attendeeEmail belong to managed user but does not include oAuthId --- .../e2e/managed-user-bookings.e2e-spec.ts | 74 ++++++++++++++++++- .../2024-08-13/services/bookings.service.ts | 38 +++++++++- .../2024-08-13/inputs/get-bookings.input.ts | 8 ++ 3 files changed, 115 insertions(+), 5 deletions(-) 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 index ddc0b95cc3..6f35d10c39 100644 --- 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 @@ -301,8 +301,8 @@ describe("Managed user bookings 2024-08-13", () => { attendee: { name: thirdManagedUser.user.name!, email: thirdManagedUserEmail, - timeZone: secondManagedUser.user.timeZone, - language: secondManagedUser.user.locale, + timeZone: thirdManagedUser.user.timeZone, + language: thirdManagedUser.user.locale, }, guests: [secondManagedUserEmail], location: "https://meet.google.com/abc-def-ghi", @@ -418,8 +418,8 @@ describe("Managed user bookings 2024-08-13", () => { attendee: { name: thirdManagedUser.user.name!, email: thirdManagedUser.user.email, - timeZone: secondManagedUser.user.timeZone, - language: secondManagedUser.user.locale, + timeZone: thirdManagedUser.user.timeZone, + language: thirdManagedUser.user.locale, }, guests: [secondManagedUser.user.email], location: "https://meet.google.com/abc-def-ghi", @@ -477,6 +477,72 @@ describe("Managed user bookings 2024-08-13", () => { }); }); + describe("fetch bookings by attendeeEmail", () => { + it("should return bookings for the original email when original email is attendee only", async () => { + const response = await request(app.getHttpServer()) + .get(`/v2/bookings?attendeeEmail=${thirdManagedUserEmail}`) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .set("Authorization", `Bearer ${firstManagedUser.accessToken}`) + .expect(200); + + const thirdManagedUserAttendeeBookingsResponseBody: GetBookingsOutput_2024_08_13 = response.body; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const thirdManagedUserAttendeeBookings: BookingOutput_2024_08_13[] = + thirdManagedUserAttendeeBookingsResponseBody.data; + expect(thirdManagedUserBookingsCount).toEqual(2); + expect(thirdManagedUserAttendeeBookings.length).toEqual(thirdManagedUserBookingsCount); + }); + + it("should return bookings for the oAuth email when original oAuth email is attendee only", async () => { + const response = await request(app.getHttpServer()) + .get(`/v2/bookings?attendeeEmail=${thirdManagedUser.user.email}`) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .set("Authorization", `Bearer ${firstManagedUser.accessToken}`) + .expect(200); + + const thirdManagedUserAttendeeBookingsResponseBody: GetBookingsOutput_2024_08_13 = response.body; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const thirdManagedUserAttendeeBookings: BookingOutput_2024_08_13[] = + thirdManagedUserAttendeeBookingsResponseBody.data; + expect(thirdManagedUserBookingsCount).toEqual(2); + expect(thirdManagedUserAttendeeBookings.length).toEqual(thirdManagedUserBookingsCount); + }); + + it("should return bookings for the original email when original email is attendee or guest", async () => { + const response = await request(app.getHttpServer()) + .get(`/v2/bookings?attendeeEmail=${secondManagedUserEmail}`) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .set("Authorization", `Bearer ${firstManagedUser.accessToken}`) + .expect(200); + + const secondManagedUserAttendeeBookingsResponseBody: GetBookingsOutput_2024_08_13 = response.body; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const secondManagedUserAttendeeBookings: BookingOutput_2024_08_13[] = + secondManagedUserAttendeeBookingsResponseBody.data; + expect(secondManagedUserBookingsCount).toEqual(4); + expect(secondManagedUserAttendeeBookings.length).toEqual(secondManagedUserBookingsCount); + }); + + it("should return bookings for the oAuth email when original oAuth email is attendee or guest", async () => { + const response = await request(app.getHttpServer()) + .get(`/v2/bookings?attendeeEmail=${secondManagedUser.user.email}`) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .set("Authorization", `Bearer ${firstManagedUser.accessToken}`) + .expect(200); + + const secondManagedUserAttendeeBookingsResponseBody: GetBookingsOutput_2024_08_13 = response.body; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const secondManagedUserAttendeeBookings: BookingOutput_2024_08_13[] = + secondManagedUserAttendeeBookingsResponseBody.data; + expect(secondManagedUserBookingsCount).toEqual(4); + expect(secondManagedUserAttendeeBookings.length).toEqual(secondManagedUserBookingsCount); + }); + }); + afterAll(async () => { await userRepositoryFixture.delete(firstManagedUser.user.id); await userRepositoryFixture.delete(secondManagedUser.user.id); 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 132c7afd5b..d8560e103e 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 @@ -6,6 +6,8 @@ import { PlatformBookingsService } from "@/ee/bookings/shared/platform-bookings. 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"; +import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository"; +import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-clients-users.service"; import { PrismaReadService } from "@/modules/prisma/prisma-read.service"; import { UsersService } from "@/modules/users/services/users.service"; import { UsersRepository, UserWithProfile } from "@/modules/users/users.repository"; @@ -72,7 +74,8 @@ export class BookingsService_2024_08_13 { private readonly billingService: BillingService, private readonly usersService: UsersService, private readonly usersRepository: UsersRepository, - private readonly platformBookingsService: PlatformBookingsService + private readonly platformBookingsService: PlatformBookingsService, + private readonly oAuthClientRepository: OAuthClientRepository ) {} async createBooking(request: Request, body: CreateBookingInput) { @@ -226,6 +229,10 @@ export class BookingsService_2024_08_13 { } async getBookings(queryParams: GetBookingsInput_2024_08_13, user: { email: string; id: number }) { + if (queryParams.attendeeEmail) { + queryParams.attendeeEmail = await this.getAttendeeEmail(queryParams.attendeeEmail, user); + } + const fetchedBookings: { bookings: { id: number }[] } = await getAllUserBookings({ bookingListingByStatus: queryParams.status || [], skip: queryParams.skip ?? 0, @@ -281,6 +288,35 @@ export class BookingsService_2024_08_13 { return formattedBookings; } + async getAttendeeEmail(queryParamsAttendeeEmail: string, user: { id: number }) { + // note(Lauris): this is to handle attendees that are managed users - in attendee table their email is one of managed users e.g + // urdasdqinm+clxyyy21o0003sbk7yw5z6tzg@example.com but if attendeeEmail is passed as urdasdqinm@example.com then we check if user whose + // access token is used is a managed user and if attendee with passed email has managed user email composed of passed email without oAuth client id + + // authenticated user oAuth client id. + const oAuthClient = await this.oAuthClientRepository.getByUserId(user.id); + if (!oAuthClient) { + return queryParamsAttendeeEmail; + } + // note(Lauris): query param already contains oAuth client id in the attendeeEmail + if (queryParamsAttendeeEmail.includes(oAuthClient.id)) { + return queryParamsAttendeeEmail; + } + + const managedAttendeeEmail = OAuthClientUsersService.getOAuthUserEmail( + oAuthClient.id, + queryParamsAttendeeEmail + ); + const [attendee, managedAttendee] = await Promise.all([ + this.usersRepository.findByEmail(queryParamsAttendeeEmail), + this.usersRepository.findByEmail(managedAttendeeEmail), + ]); + if (!attendee && managedAttendee) { + return managedAttendeeEmail; + } + + return queryParamsAttendeeEmail; + } + async rescheduleBooking(request: Request, bookingUid: string, body: RescheduleBookingInput) { try { const bookingRequest = await this.inputService.createRescheduleBookingRequest( diff --git a/packages/platform/types/bookings/2024-08-13/inputs/get-bookings.input.ts b/packages/platform/types/bookings/2024-08-13/inputs/get-bookings.input.ts index 72d157f81e..9e601238aa 100644 --- a/packages/platform/types/bookings/2024-08-13/inputs/get-bookings.input.ts +++ b/packages/platform/types/bookings/2024-08-13/inputs/get-bookings.input.ts @@ -61,6 +61,14 @@ export class GetBookingsInput_2024_08_13 { description: "Filter bookings by the attendee's email address.", example: "example@domain.com", }) + @Transform(({ value }) => { + if (typeof value === "string") { + // note(Lauris): we replace inner white spaces with "+" because managed user emails have "+" in them but if they are not URL encoded + // when making request "+" becomes empty space " ". + return value.trim().replace(/\s+/g, "+"); + } + return value; + }) attendeeEmail?: string; @IsString()