Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
e8d3e34
refactor: move org admin related logic to org server
Ryukemeister Oct 22, 2025
c6858f3
fix: update cancellation logic to make sure org admin can cancel seat…
Ryukemeister Oct 22, 2025
28dfa8b
Merge branch 'main' into allow-org-admin-to-cancel-seated-bookings
Ryukemeister Oct 22, 2025
f4f2de4
fix: import path
Ryukemeister Oct 22, 2025
a1d3571
Merge branch 'main' into allow-org-admin-to-cancel-seated-bookings
Ryukemeister Oct 23, 2025
5d67b09
update bookings repository
Ryukemeister Oct 23, 2025
9b46778
fix: update reschedule endpoint logic to let org admin reschedule boo…
Ryukemeister Oct 23, 2025
426e8ad
refactor: make logic more simple
Ryukemeister Oct 24, 2025
9e87e85
Merge branch 'main' into allow-org-admin-to-cancel-seated-bookings
Ryukemeister Oct 24, 2025
c1b34ea
chore: update platform libraries
Ryukemeister Oct 24, 2025
fc4849b
more refactors
Ryukemeister Oct 24, 2025
bc977d4
fix: add check to make sure org admin can reschedule booking
Ryukemeister Oct 24, 2025
771afe9
chore: remove unused comments
Ryukemeister Oct 24, 2025
5fa1053
test: add e2e tests for org admin reschedule and cancel seated bookings
devin-ai-integration[bot] Oct 24, 2025
6748d52
Merge branch 'main' into allow-org-admin-to-cancel-seated-bookings
Ryukemeister Oct 26, 2025
fb536c3
chore: add cubic feedback
Ryukemeister Oct 26, 2025
766c36d
fix: tests for seated booking management by org admin
Ryukemeister Oct 26, 2025
2dac4fe
Merge branch 'main' into allow-org-admin-to-cancel-seated-bookings
Ryukemeister Oct 26, 2025
7c4189b
Merge branch 'main' into allow-org-admin-to-cancel-seated-bookings
Ryukemeister Oct 28, 2025
ae8ffa3
chore: implement PR feedback
Ryukemeister Oct 28, 2025
375b1d0
fix: resolve merge conflicts
Ryukemeister Oct 28, 2025
2597170
fixup
Ryukemeister Oct 28, 2025
46857d4
chore: update docs
Ryukemeister Oct 28, 2025
8c50f68
Merge branch 'main' into allow-org-admin-to-cancel-seated-bookings
Ryukemeister Oct 28, 2025
0e70429
fixup: get optional user from request and then pass it down to getBoo…
Ryukemeister Oct 28, 2025
cf52157
Merge branch 'main' into allow-org-admin-to-cancel-seated-bookings
Ryukemeister Oct 28, 2025
9533cf3
fix: validate seatUid before checking booking cancellation status
devin-ai-integration[bot] Oct 29, 2025
415a952
Merge branch 'main' into allow-org-admin-to-cancel-seated-bookings
Ryukemeister Oct 29, 2025
8638c10
Merge branch 'main' into allow-org-admin-to-cancel-seated-bookings
Ryukemeister Oct 29, 2025
a1f0ad3
Merge branch 'main' into allow-org-admin-to-cancel-seated-bookings
Ryukemeister Oct 29, 2025
696754d
Merge branch 'main' into allow-org-admin-to-cancel-seated-bookings
Ryukemeister Oct 30, 2025
202d468
Merge branch 'main' into allow-org-admin-to-cancel-seated-bookings
Ryukemeister Oct 30, 2025
6f57ff8
Apply changes for benchmark PR
tomerqodo Nov 6, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,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";
Expand Down Expand Up @@ -172,8 +177,12 @@ export class BookingsController_2024_04_15 {
}

@Get("/:bookingUid/reschedule")
async getBookingForReschedule(@Param("bookingUid") bookingUid: string): Promise<ApiResponse<unknown>> {
const booking = await getBookingForReschedule(bookingUid);
@UseGuards(OptionalApiAuthGuard)
async getBookingForReschedule(
@Param("bookingUid") bookingUid: string,
@GetOptionalUser() user: AuthOptionalUser
): Promise<ApiResponse<unknown>> {
const booking = await getBookingForReschedule(bookingUid, user?.id);

Comment on lines 179 to 186

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

5. Org admin reschedule get denied 🐞 Bug ≡ Correctness

GET /v2/bookings/:bookingUid/reschedule now passes an optional authenticated user ID, but
getBookingForReschedule still only authorizes seated booking access for booking owner/host, so org
admins cannot fetch booking data needed to reschedule all seats.
Agent Prompt
### Issue description
The controller now supports optional authentication and forwards `user?.id` to `getBookingForReschedule`, but the underlying seated-booking authorization gate does not recognize org admins. Org admins will still receive `null` (and thus 404) when requesting bookingUid-based seated reschedule details.

### Issue Context
This endpoint appears to be the API path for retrieving reschedule data. For seated bookings without seatUid, `getBookingForReschedule` requires the requester to be owner/host.

### Fix Focus Areas
- apps/api/v2/src/ee/bookings/2024-04-15/controllers/bookings.controller.ts[179-195]
- packages/features/bookings/lib/get-booking.ts[183-196]

### Recommended fix
Extend the seated-booking authorization gate in `getBookingForReschedule` to also allow org admins (using the same org-admin check helper used elsewhere) when a `userId` is provided.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

if (!booking) {
throw new NotFoundException(`Booking with UID=${bookingUid} does not exist.`);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,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.`,
Expand Down Expand Up @@ -328,7 +328,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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -773,6 +788,107 @@ describe("Managed user bookings 2024-08-13", () => {
});
});

describe("seated booking management by org admin", () => {
let seatedBookingUid: string;

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: {
name: secondManagedUser.user.name!,
email: secondManagedUser.user.email,
timeZone: secondManagedUser.user.timeZone,
language: secondManagedUser.user.locale,
},
};

const responseOne = await request(app.getHttpServer())
.post("/v2/bookings")
.send(bodyOne)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201);

const responseBodyForFirstAttendee: ApiSuccessResponse<BookingOutput_2024_08_13> = responseOne.body;
expect(responseBodyForFirstAttendee.status).toEqual(SUCCESS_STATUS);
expect(responseBodyForFirstAttendee.data).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<BookingOutput_2024_08_13> = 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 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())
.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<BookingOutput_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.start).toEqual(newStartTime);

seatedBookingUid = bookingData.uid;
});

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)
.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");
});
});

describe("event type booking requires authentication", () => {
let eventTypeRequiringAuthenticationId: number;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,20 @@ export class BookingsRepository_2024_08_13 {
});
}

async getByUidWithUserIdAndSeatsReferencesCount(bookingUid: string) {
return this.dbRead.prisma.booking.findUnique({
where: {
uid: bookingUid,
},
select: {
userId: true,
seatsReferences: {
take: 1,
},
},
});
}

async getByUid(bookingUid: string) {
return this.dbRead.prisma.booking.findUnique({
where: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import {
confirmBookingHandler,
getCalendarLinks,
} from "@calcom/platform-libraries";
import { PrismaOrgMembershipRepository } from "@calcom/platform-libraries/bookings";
import {
CreateBookingInput_2024_08_13,
CreateBookingInput,
Expand All @@ -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";

Expand Down Expand Up @@ -762,16 +764,26 @@ export class BookingsService_2024_08_13 {
authUser: AuthOptionalUser
) {
try {
await this.canRescheduleBooking(bookingUid);
const isIndividualSeatRequest = this.isRescheduleSeatedBody(body);
const isIndividualSeatReschedule = await this.shouldRescheduleIndividualSeat(
bookingUid,
isIndividualSeatRequest,
authUser
);

const bookingRequest = await this.inputService.createRescheduleBookingRequest(
request,
bookingUid,
body
body,
isIndividualSeatReschedule
);

await this.canRescheduleBooking(bookingUid);

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,
Expand Down Expand Up @@ -840,6 +852,57 @@ export class BookingsService_2024_08_13 {
return booking;
}

async shouldRescheduleIndividualSeat(
bookingUid: string,
isIndividualSeatReschedule: boolean,
authUser: AuthOptionalUser
) {
const booking = await this.bookingsRepository.getByUidWithUserIdAndSeatsReferencesCount(bookingUid);

if (!booking) {
throw new NotFoundException(`Booking with uid=${bookingUid} was not found in the database`);
}

const hasSeatsPresent = booking.seatsReferences.length > 0;

if (!hasSeatsPresent) return false;

return this.isIndividualSeatOrOrgAdminReschedule(
isIndividualSeatReschedule,
booking.userId,
authUser?.id
);
}

async isIndividualSeatOrOrgAdminReschedule(
isIndividualSeatReschedule: boolean,
bookingUserId: number | null,
authUserId?: number | null
) {
if (isIndividualSeatReschedule) {
return true;
}

if (!authUserId) {
throw new Error(`No auth user found`);
}

if (!bookingUserId) {
throw new Error(`No user found for booking`);
}

const isOrgAdmin = await PrismaOrgMembershipRepository.isLoggedInUserOrgAdminOfBookingHost(
bookingUserId,
authUserId
);
Comment on lines +894 to +897

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[breaking-issue] [Critical Bug: Swapped Parameters in Org Admin Authorization Check]
In bookings.service.ts line 894-896, the parameters are swapped when calling isLoggedInUserOrgAdminOfBookingHost. The method expects (loggedInUserId, bookingUserId) but receives (bookingUserId, authUserId), causing the authorization check to evaluate if the booking user is an admin of the logged-in user instead of vice versa.

>� Agent Prompt
Copy this prompt and use it to remediate the issue with your preferred AI generation tools
**Issue:** Parameters passed in wrong order to authorization check method

**Context:** The isIndividualSeatOrOrgAdminReschedule method calls isLoggedInUserOrgAdminOfBookingHost to check if the logged-in user is an org admin of the booking user. The parameters are currently swapped, causing the check to evaluate backwards.

**Recommended files to fix:**
- apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts[894-897]

**Fix:** Swap the parameters on lines 895-896. Change from `bookingUserId, authUserId` to `authUserId, bookingUserId`. The method signature is `isLoggedInUserOrgAdminOfBookingHost(loggedInUserId: number, bookingUserId: number)`, so authUserId should be first and bookingUserId should be second.

Comment on lines +894 to +897

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[action-required] [Swapped Parameters in Org Admin Function Call]
The call to isLoggedInUserOrgAdminOfBookingHost at lines 894-896 passes bookingUserId as the first parameter and authUserId as the second, but the function signature expects (loggedInUserId, bookingUserId). This causes the function to check the wrong relationship.

>� Agent Prompt
Copy this prompt and use it to remediate the issue with your preferred AI generation tools
**Issue**: Parameters passed in wrong order to isLoggedInUserOrgAdminOfBookingHost, causing incorrect authorization checks.

**Context**: The function signature is isLoggedInUserOrgAdminOfBookingHost(loggedInUserId: number, bookingUserId: number), but the call passes (bookingUserId, authUserId) which is backwards.

**Recommended files to fix**:
- apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts[894-897]

**Fix**: Swap the parameters at lines 895-896 to pass authUserId as the first parameter and bookingUserId as the second parameter. Change from `isLoggedInUserOrgAdminOfBookingHost(bookingUserId, authUserId)` to `isLoggedInUserOrgAdminOfBookingHost(authUserId, bookingUserId)`.

Comment on lines +894 to +897

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[action-required] [Swapped Parameters in Org Admin Check]
In bookings.service.ts lines 894-896, the parameters to isLoggedInUserOrgAdminOfBookingHost are swapped. It passes bookingUserId first and authUserId second, but the function expects loggedInUserId first. This checks the wrong authorization relationship.

>Agent Prompt
Copy this prompt and use it to remediate the issue with your preferred AI generation tools
## Issue
Lines 894-896 in apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts[894-897] pass parameters in the wrong order to isLoggedInUserOrgAdminOfBookingHost.

## Context
The function signature is isLoggedInUserOrgAdminOfBookingHost(loggedInUserId: number, bookingUserId: number). The first parameter should be the logged-in user (authUserId) and the second should be the booking user (bookingUserId). All other call sites in the codebase pass parameters correctly.

## Fix
Change lines 894-896 from:
```typescript
const isOrgAdmin = await PrismaOrgMembershipRepository.isLoggedInUserOrgAdminOfBookingHost(
  bookingUserId,
  authUserId
);
```
to:
```typescript
const isOrgAdmin = await PrismaOrgMembershipRepository.isLoggedInUserOrgAdminOfBookingHost(
  authUserId,
  bookingUserId
);
```

## Files to Fix
- apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts[894-897]

Comment on lines +894 to +897

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

2. Org admin args swapped 🐞 Bug ⛨ Security

BookingsService_2024_08_13 calls isLoggedInUserOrgAdminOfBookingHost with `(bookingUserId,
authUserId) instead of (authUserId, bookingUserId)`, so org-admin authorization is evaluated
against the wrong identities.
Agent Prompt
### Issue description
`PrismaOrgMembershipRepository.isLoggedInUserOrgAdminOfBookingHost` is defined as `(loggedInUserId, bookingUserId)` but is called with reversed arguments in `BookingsService_2024_08_13`, leading to incorrect authorization decisions.

### Issue Context
This is used to determine whether a seated booking reschedule should be allowed for an org admin.

### Fix Focus Areas
- apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts[877-900]

### Recommended fix
Call:
```ts
await PrismaOrgMembershipRepository.isLoggedInUserOrgAdminOfBookingHost(authUserId, bookingUserId)
```
(not the reversed order).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


return isOrgAdmin;
}

isRescheduleSeatedBody(body: RescheduleBookingInput): body is RescheduleSeatedBookingInput_2024_08_13 {
return "seatUid" in body;
}

async cancelBooking(
request: Request,
bookingUid: string,
Expand Down
19 changes: 12 additions & 7 deletions apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -507,11 +507,13 @@ export class InputBookingsService_2024_08_13 {
async createRescheduleBookingRequest(
request: Request,
bookingUid: string,
body: RescheduleBookingInput
body: RescheduleBookingInput,
isIndividualSeatReschedule: boolean
): Promise<BookingRequest> {
const bodyTransformed = this.isRescheduleSeatedBody(body)
? await this.transformInputRescheduleSeatedBooking(bookingUid, body)
: await this.transformInputRescheduleBooking(bookingUid, body);
const bodyTransformed =
!isIndividualSeatReschedule && "seatUid" in body
? await this.transformInputRescheduleSeatedBooking(bookingUid, body)
: await this.transformInputRescheduleBooking(bookingUid, body, isIndividualSeatReschedule);
Comment on lines +513 to +516

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

3. Seat reschedule routing broken 🐞 Bug ≡ Correctness

For seated reschedules where seatUid is present, createRescheduleBookingRequest routes the
request to transformInputRescheduleBooking (non-seat path) because of a negated condition, causing
individual-seat reschedules to fail with the “seatUid required” error.
Agent Prompt
### Issue description
`createRescheduleBookingRequest` uses:
```ts
!isIndividualSeatReschedule && "seatUid" in body
  ? transformInputRescheduleSeatedBooking(...)
  : transformInputRescheduleBooking(...)
```
This makes the seated transformer *unreachable* precisely when `seatUid` exists (the individual-seat case), which then drives seated requests into `transformInputRescheduleBooking` where they can throw the seated-booking error.

### Issue Context
`BookingsService_2024_08_13` sets the boolean to `true` when the body contains `seatUid`, so the negation breaks the intended branch.

### Fix Focus Areas
- apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts[507-517]
- apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts[611-632]
- apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts[760-779]

### Recommended fix
1. Select the transformer based primarily on the presence of `seatUid`:
   - if `"seatUid" in body` -> `transformInputRescheduleSeatedBooking`
   - else -> `transformInputRescheduleBooking`
2. If you need to support org-admin “all seats” reschedule, use a separate boolean (e.g. `isOrgAdminAllSeatsReschedule`) and apply it only to the seated-booking validation (not to transformer selection).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


const oAuthClientParams = await this.platformBookingsService.getOAuthClientParams(
bodyTransformed.eventTypeId
Expand Down Expand Up @@ -606,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`);
Expand All @@ -618,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`
);
Expand Down Expand Up @@ -652,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(),
Expand Down
16 changes: 14 additions & 2 deletions packages/features/bookings/lib/handleCancelBooking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 { 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";
Expand Down Expand Up @@ -144,8 +145,19 @@ 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 &&
bookingToDelete.userId &&
(await PrismaOrgMembershipRepository.isLoggedInUserOrgAdminOfBookingHost(
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",
});
Comment on lines +156 to +160

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[breaking-issue] [Critical Logic Error: Missing NOT Operator in Cancel Booking Authorization]
Line 156 in handleCancelBooking.ts is missing a NOT operator before userIsOrgAdminOfBookingUser. The current logic throws a 401 error when the user IS an org admin (and not a host/owner), which is backwards. This prevents authorized org admins from canceling seated bookings.

>� Agent Prompt
Copy this prompt and use it to remediate the issue with your preferred AI generation tools
**Issue:** Missing NOT operator in authorization check prevents org admins from canceling bookings

**Context:** The handleCancelBooking function checks if a user is authorized to cancel a seated booking. The condition should throw an error when the user lacks all three authorizations (not host, not owner, not org admin), but currently throws when they ARE an org admin.

**Recommended files to fix:**
- packages/features/bookings/lib/handleCancelBooking.ts[156-161]

**Fix:** Change line 156 from `if (!userIsHost && !userIsOwnerOfEventType && userIsOrgAdminOfBookingUser) {` to `if (!userIsHost && !userIsOwnerOfEventType && !userIsOrgAdminOfBookingUser) {`. Add the NOT operator (!) before userIsOrgAdminOfBookingUser so the error is thrown when the user is NOT authorized, not when they ARE authorized.

Comment on lines +156 to +160

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[action-required] [Inverted Authorization Logic in Cancel Booking]
Line 156 throws a 401 error when userIsOrgAdminOfBookingUser is true, but should throw when it's false. The condition should check if the user is NOT a host AND NOT an owner AND NOT an org admin before denying access.

>� Agent Prompt
Copy this prompt and use it to remediate the issue with your preferred AI generation tools
**Issue**: Inverted authorization logic throws 401 error when user IS an org admin instead of when they are NOT.

**Context**: The condition should deny access only when the user lacks all three authorizations: not a host, not an owner, and not an org admin. Currently it denies access when the user IS an org admin.

**Recommended files to fix**:
- packages/features/bookings/lib/handleCancelBooking.ts[156-161]

**Fix**: Add the NOT operator to userIsOrgAdminOfBookingUser in the condition at line 156. Change from `if (!userIsHost && !userIsOwnerOfEventType && userIsOrgAdminOfBookingUser)` to `if (!userIsHost && !userIsOwnerOfEventType && !userIsOrgAdminOfBookingUser)`. This ensures the error is thrown only when the user lacks all three authorizations.

Comment on lines +156 to +160

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[action-required] [Incorrect Authorization Logic in Cancel Booking]
In handleCancelBooking.ts line 156, the condition throws an error when userIsOrgAdminOfBookingUser is true. This blocks org admins from canceling seated bookings, which contradicts the feature's purpose and will cause the e2e test to fail.

>Agent Prompt
Copy this prompt and use it to remediate the issue with your preferred AI generation tools
## Issue
Line 156 in packages/features/bookings/lib/handleCancelBooking.ts[156-161] throws an error when the user IS an org admin, blocking legitimate cancellations.

## Context
The authorization check should allow org admins to cancel seated bookings for their organization members. The error should only be thrown when the user is NOT a host, NOT an owner, AND NOT an org admin. The similar logic in rescheduleSeatedBooking.ts:111 correctly uses '!isOrgAdmin'.

## Fix
Change line 156 from:
```typescript
if (!userIsHost && !userIsOwnerOfEventType && userIsOrgAdminOfBookingUser) {
```
to:
```typescript
if (!userIsHost && !userIsOwnerOfEventType && !userIsOrgAdminOfBookingUser) {
```

## Files to Fix
- packages/features/bookings/lib/handleCancelBooking.ts[156-156]

Comment on lines +148 to +160

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[action-required] [Incorrect Boolean Logic in Cancel Authorization]
Line 156 in handleCancelBooking.ts checks '!userIsHost && !userIsOwnerOfEventType && userIsOrgAdminOfBookingUser' which requires the user to be an org admin AND not a host AND not an owner to pass. This should use OR logic to allow any of these roles to cancel.

>Agent Prompt
Copy this prompt and use it to remediate the issue with your preferred AI generation tools
## Issue Description
The authorization check in handleCancelBooking.ts has incorrect boolean logic that prevents org admins from canceling bookings.

## Context
The code should allow users who are hosts, owners, OR org admins to cancel bookings. Currently it throws an error when the user IS an org admin (and not a host/owner), which is backwards.

## Recommended Fix
File: packages/features/bookings/lib/handleCancelBooking.ts[156-161]

Change line 156 from:
```typescript
if (!userIsHost && !userIsOwnerOfEventType && userIsOrgAdminOfBookingUser) {
```

To:
```typescript
if (!userIsHost && !userIsOwnerOfEventType && !userIsOrgAdminOfBookingUser) {
```

This ensures the error is only thrown when the user is NONE of the authorized roles (not a host, not an owner, and not an org admin).

Comment on lines +148 to +160

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[action-required] [Incorrect Authorization Logic in Cancel Booking]
In handleCancelBooking.ts, the authorization check on line 155 uses && instead of || in the condition, causing it to throw an error when the user IS an org admin of the booking user, which is backwards. The condition should use || to throw an error when the user is NOT a host AND NOT an owner AND NOT an org admin.

>Agent Prompt
Copy this prompt and use it to remediate the issue with your preferred AI generation tools
## Issue Description
The authorization check in `handleCancelBooking` has incorrect logic that blocks org admins instead of allowing them to cancel bookings.

## Context
The PR adds org admin authorization for canceling bookings. The condition should throw an error when the user is NOT a host AND NOT an owner AND NOT an org admin.

## Recommended Fix
Modify the file: `packages/features/bookings/lib/handleCancelBooking.ts`

**Line 155**: Change from:
```typescript
if (!userIsHost && !userIsOwnerOfEventType && userIsOrgAdminOfBookingUser) {
```

To:
```typescript
if (!userIsHost && !userIsOwnerOfEventType && !userIsOrgAdminOfBookingUser) {
```

Add the negation operator `!` before `userIsOrgAdminOfBookingUser` to correctly allow org admins to cancel bookings.

Comment on lines +156 to +160

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[action-required] [Incorrect Authorization Logic in Cancel Booking]
In handleCancelBooking.ts line 156, the authorization check uses !userIsHost && !userIsOwnerOfEventType && userIsOrgAdminOfBookingUser which throws an error when the user IS an org admin. The logic should allow org admins, not block them.

>Agent Prompt
Copy this prompt and use it to remediate the issue with your preferred AI generation tools
## Issue Description
The authorization check in `handleCancelBooking.ts` incorrectly blocks org admins from canceling bookings instead of allowing them.

## Context
The PR adds functionality for org admins to cancel bookings for managed users. The authorization check should allow users who are hosts, owners, OR org admins. Currently, it throws an error when the user IS an org admin.

## Recommended Fix
File: packages/features/bookings/lib/handleCancelBooking.ts[156-161]

Change line 156 from:
```typescript
if (!userIsHost && !userIsOwnerOfEventType && userIsOrgAdminOfBookingUser) {
```

To:
```typescript
if (!userIsHost && !userIsOwnerOfEventType && !userIsOrgAdminOfBookingUser) {
```

This ensures the error is thrown only when the user is NOT a host AND NOT an owner AND NOT an org admin, allowing org admins to proceed with cancellation.

Comment on lines +148 to +160

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

4. Cancel auth condition inverted 🐞 Bug ≡ Correctness

handleCancelBooking throws 401 when userIsOrgAdminOfBookingUser is true, which blocks org admins
from cancelling seated bookings (all seats) once the org-admin check is corrected.
Agent Prompt
### Issue description
The seated-booking cancellation guard is inverted:
```ts
if (!userIsHost && !userIsOwnerOfEventType && userIsOrgAdminOfBookingUser) throw 401;
```
This throws *because* the user is an org admin, rather than when they are not.

### Issue Context
This block runs when cancelling a seated booking without `seatReferenceUid` (i.e., cancelling all seats).

### Fix Focus Areas
- packages/features/bookings/lib/handleCancelBooking.ts[140-162]

### Recommended fix
Change the condition to:
```ts
if (!userIsHost && !userIsOwnerOfEventType && !userIsOrgAdminOfBookingUser) {
  throw new HttpError({ statusCode: 401, message: ... });
}
```

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

}
}

Expand Down
Loading
Loading