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 6f14185ca1..0153a514fe 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
@@ -1,3 +1,50 @@
+import { BOOKING_READ, BOOKING_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
+import {
+ CancelBookingInput,
+ CancelBookingInput_2024_08_13,
+ CancelBookingInputPipe,
+ CancelSeatedBookingInput_2024_08_13,
+ CreateBookingInput,
+ CreateBookingInput_2024_08_13,
+ CreateBookingInputPipe,
+ CreateInstantBookingInput_2024_08_13,
+ CreateRecurringBookingInput_2024_08_13,
+ DeclineBookingInput_2024_08_13,
+ GetBookingOutput_2024_08_13,
+ GetBookingRecordingsOutput,
+ GetBookingsInput_2024_08_13,
+ GetBookingsOutput_2024_08_13,
+ GetBookingTranscriptsOutput,
+ GetBookingVideoSessionsOutput,
+ MarkAbsentBookingInput_2024_08_13,
+ ReassignToUserBookingInput_2024_08_13,
+ RescheduleBookingInput,
+ RescheduleBookingInput_2024_08_13,
+ RescheduleBookingInputPipe,
+ RescheduleSeatedBookingInput_2024_08_13,
+} from "@calcom/platform-types";
+import {
+ Body,
+ Controller,
+ Get,
+ HttpCode,
+ HttpStatus,
+ Logger,
+ Param,
+ Post,
+ Query,
+ Req,
+ UseGuards,
+} from "@nestjs/common";
+import {
+ ApiBody,
+ ApiExtraModels,
+ ApiHeader,
+ ApiOperation,
+ ApiTags as DocsTags,
+ getSchemaPath,
+} from "@nestjs/swagger";
+import { Request } from "express";
import { BookingPbacGuard } from "@/ee/bookings/2024-08-13/guards/booking-pbac.guard";
import { BookingUidGuard } from "@/ee/bookings/2024-08-13/guards/booking-uid.guard";
import { BookingReferencesFilterInput_2024_08_13 } from "@/ee/bookings/2024-08-13/inputs/booking-references-filter.input";
@@ -11,7 +58,7 @@ import { RescheduleBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/out
import { BookingReferencesService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/booking-references.service";
import { BookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/bookings.service";
import { CalVideoService } from "@/ee/bookings/2024-08-13/services/cal-video.service";
-import { VERSION_2024_08_13_VALUE, VERSION_2024_08_13 } from "@/lib/api-versions";
+import { VERSION_2024_08_13, VERSION_2024_08_13_VALUE } from "@/lib/api-versions";
import {
API_KEY_OR_ACCESS_TOKEN_HEADER,
OPTIONAL_API_KEY_OR_ACCESS_TOKEN_HEADER,
@@ -31,56 +78,6 @@ import { OptionalApiAuthGuard } from "@/modules/auth/guards/optional-api-auth/op
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
import { ApiAuthGuardUser } from "@/modules/auth/strategies/api-auth/api-auth.strategy";
import { UsersService } from "@/modules/users/services/users.service";
-import {
- Controller,
- Post,
- Logger,
- Body,
- UseGuards,
- Req,
- Get,
- Param,
- Query,
- HttpCode,
- HttpStatus,
-} from "@nestjs/common";
-import {
- ApiOperation,
- ApiTags as DocsTags,
- ApiHeader,
- getSchemaPath,
- ApiBody,
- ApiExtraModels,
-} from "@nestjs/swagger";
-import { Request } from "express";
-
-import { BOOKING_READ, BOOKING_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
-import {
- CancelBookingInput,
- CancelBookingInput_2024_08_13,
- CancelBookingInputPipe,
- CancelSeatedBookingInput_2024_08_13,
- GetBookingOutput_2024_08_13,
- GetBookingsOutput_2024_08_13,
- RescheduleBookingInput,
- RescheduleBookingInput_2024_08_13,
- RescheduleBookingInputPipe,
- RescheduleSeatedBookingInput_2024_08_13,
- GetBookingRecordingsOutput,
- GetBookingTranscriptsOutput,
- GetBookingVideoSessionsOutput,
-} from "@calcom/platform-types";
-import {
- CreateBookingInputPipe,
- CreateBookingInput,
- GetBookingsInput_2024_08_13,
- ReassignToUserBookingInput_2024_08_13,
- MarkAbsentBookingInput_2024_08_13,
- CreateBookingInput_2024_08_13,
- CreateInstantBookingInput_2024_08_13,
- CreateRecurringBookingInput_2024_08_13,
- DeclineBookingInput_2024_08_13,
-} from "@calcom/platform-types";
@Controller({
path: "/v2/bookings",
@@ -178,6 +175,33 @@ export class BookingsController_2024_08_13 {
};
}
+ @Get("/by-seat/:seatUid")
+ @UseGuards(OptionalApiAuthGuard)
+ @ApiHeader(OPTIONAL_X_CAL_CLIENT_ID_HEADER)
+ @ApiHeader(OPTIONAL_X_CAL_SECRET_KEY_HEADER)
+ @ApiHeader(OPTIONAL_API_KEY_OR_ACCESS_TOKEN_HEADER)
+ @ApiOperation({
+ summary: "Get a booking by seat UID",
+ description: `Get a seated booking by its seat reference UID. This is useful when you have a seatUid from a seated booking and want to retrieve the full booking details.
+
+ If you are fetching 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.
+
+ Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.
+ `,
+ })
+ async getBookingBySeatUid(
+ @Param("seatUid") seatUid: string,
+ @GetOptionalUser() user: AuthOptionalUser
+ ): Promise {
+ const booking = await this.bookingsService.getBookingBySeatUid(seatUid, user);
+
+ return {
+ status: SUCCESS_STATUS,
+ data: booking,
+ };
+ }
+
@Get("/:bookingUid")
@UseGuards(BookingUidGuard, OptionalApiAuthGuard)
@ApiHeader(OPTIONAL_X_CAL_CLIENT_ID_HEADER)
diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/seated-bookings.e2e-spec.ts b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/seated-bookings.e2e-spec.ts
index dca23f60eb..6591c027b9 100644
--- a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/seated-bookings.e2e-spec.ts
+++ b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/seated-bookings.e2e-spec.ts
@@ -364,6 +364,185 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
});
+ it("should get a booking by seatUid", async () => {
+ return request(app.getHttpServer())
+ .get(`/v2/bookings/by-seat/${createdSeatedBooking.seatUid}`)
+ .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
+ .expect(200)
+ .then(async (response) => {
+ const responseBody: GetBookingOutput_2024_08_13 = response.body;
+ expect(responseBody.status).toEqual(SUCCESS_STATUS);
+ expect(responseBody.data).toBeDefined();
+ expect(responseDataIsGetSeatedBooking(responseBody.data)).toBe(true);
+
+ if (responseDataIsGetSeatedBooking(responseBody.data)) {
+ const data: GetSeatedBookingOutput_2024_08_13 = responseBody.data;
+ expect(data.id).toEqual(createdSeatedBooking.id);
+ expect(data.uid).toEqual(createdSeatedBooking.uid);
+ expect(data.hosts[0].id).toEqual(user.id);
+ expect(data.status).toEqual("accepted");
+ expect(data.eventTypeId).toEqual(seatedEventTypeId);
+ expect(data.eventType).toEqual({
+ id: seatedEventTypeId,
+ slug: seatedEventSlug,
+ });
+ expect(data.attendees.length).toEqual(2);
+ } else {
+ throw new Error(
+ "Invalid response data - expected seated booking but received non seated booking response"
+ );
+ }
+ });
+ });
+
+ it("should return 404 when getting booking by non-existent seatUid", async () => {
+ return request(app.getHttpServer())
+ .get("/v2/bookings/by-seat/non-existent-seat-uid")
+ .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
+ .expect(404)
+ .then(async (response) => {
+ expect(response.body.error.message).toEqual(
+ "Booking with seatUid=non-existent-seat-uid was not found in the database"
+ );
+ });
+ });
+
+ describe("get booking by seatUid with seatsShowAttendees disabled", () => {
+ let bookingWithAttendeesDisabled: CreateSeatedBookingOutput_2024_08_13;
+ let secondAttendeeSeatUid: string;
+ const emailAttendeeThree = `seated-bookings-attendee3-${randomString()}@api.com`;
+ const nameAttendeeThree = `Attendee Three ${randomString()}`;
+
+ it("should book first seat on event type with seatsShowAttendees disabled", async () => {
+ const body: CreateBookingInput_2024_08_13 = {
+ start: new Date(Date.UTC(2030, 0, 11, 13, 0, 0)).toISOString(),
+ eventTypeId: seatedEventTypeIdAttendeesDisabledId,
+ attendee: {
+ name: nameAttendeeOne,
+ email: emailAttendeeOne,
+ timeZone: "Europe/Rome",
+ language: "it",
+ },
+ bookingFieldsResponses: {
+ codingLanguage: "TypeScript",
+ },
+ metadata: {
+ userId: "100",
+ },
+ };
+
+ return request(app.getHttpServer())
+ .post("/v2/bookings")
+ .send(body)
+ .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
+ .expect(201)
+ .then(async (response) => {
+ const responseBody: CreateBookingOutput_2024_08_13 = response.body;
+ expect(responseBody.status).toEqual(SUCCESS_STATUS);
+ expect(responseBody.data).toBeDefined();
+ expect(responseDataIsCreateSeatedBooking(responseBody.data)).toBe(true);
+
+ if (responseDataIsCreateSeatedBooking(responseBody.data)) {
+ const data: CreateSeatedBookingOutput_2024_08_13 = responseBody.data;
+ expect(data.seatUid).toBeDefined();
+ bookingWithAttendeesDisabled = data;
+ } else {
+ throw new Error("Invalid response data - expected seated booking");
+ }
+ });
+ });
+
+ it("should book second seat on same event type with seatsShowAttendees disabled", async () => {
+ const body: CreateBookingInput_2024_08_13 = {
+ start: new Date(Date.UTC(2030, 0, 11, 13, 0, 0)).toISOString(),
+ eventTypeId: seatedEventTypeIdAttendeesDisabledId,
+ attendee: {
+ name: nameAttendeeThree,
+ email: emailAttendeeThree,
+ timeZone: "Europe/Rome",
+ language: "it",
+ },
+ bookingFieldsResponses: {
+ codingLanguage: "Rust",
+ },
+ metadata: {
+ userId: "300",
+ },
+ };
+
+ return request(app.getHttpServer())
+ .post("/v2/bookings")
+ .send(body)
+ .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
+ .expect(201)
+ .then(async (response) => {
+ const responseBody: CreateBookingOutput_2024_08_13 = response.body;
+ expect(responseBody.status).toEqual(SUCCESS_STATUS);
+ expect(responseBody.data).toBeDefined();
+ expect(responseDataIsCreateSeatedBooking(responseBody.data)).toBe(true);
+
+ if (responseDataIsCreateSeatedBooking(responseBody.data)) {
+ const data: CreateSeatedBookingOutput_2024_08_13 = responseBody.data;
+ expect(data.seatUid).toBeDefined();
+ secondAttendeeSeatUid = data.seatUid;
+ } else {
+ throw new Error("Invalid response data - expected seated booking");
+ }
+ });
+ });
+
+ it("should return only the attendee for the specific seatUid when seatsShowAttendees is false and no auth", async () => {
+ return request(app.getHttpServer())
+ .get(`/v2/bookings/by-seat/${secondAttendeeSeatUid}`)
+ .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
+ .expect(200)
+ .then(async (response) => {
+ const responseBody: GetBookingOutput_2024_08_13 = response.body;
+ expect(responseBody.status).toEqual(SUCCESS_STATUS);
+ expect(responseBody.data).toBeDefined();
+ expect(responseDataIsGetSeatedBooking(responseBody.data)).toBe(true);
+
+ if (responseDataIsGetSeatedBooking(responseBody.data)) {
+ const data: GetSeatedBookingOutput_2024_08_13 = responseBody.data;
+ expect(data.id).toEqual(bookingWithAttendeesDisabled.id);
+ expect(data.uid).toEqual(bookingWithAttendeesDisabled.uid);
+ expect(data.eventTypeId).toEqual(seatedEventTypeIdAttendeesDisabledId);
+ expect(data.attendees.length).toEqual(1);
+ expect(data.attendees[0].email).toEqual(emailAttendeeThree);
+ expect(data.attendees[0].name).toEqual(nameAttendeeThree);
+ expect(data.attendees[0].seatUid).toEqual(secondAttendeeSeatUid);
+ } else {
+ throw new Error("Invalid response data - expected seated booking");
+ }
+ });
+ });
+
+ it("should return all attendees when admin auth is provided", async () => {
+ return request(app.getHttpServer())
+ .get(`/v2/bookings/by-seat/${secondAttendeeSeatUid}`)
+ .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
+ .set("Authorization", `Bearer ${apiKeyString}`)
+ .expect(200)
+ .then(async (response) => {
+ const responseBody: GetBookingOutput_2024_08_13 = response.body;
+ expect(responseBody.status).toEqual(SUCCESS_STATUS);
+ expect(responseBody.data).toBeDefined();
+ expect(responseDataIsGetSeatedBooking(responseBody.data)).toBe(true);
+
+ if (responseDataIsGetSeatedBooking(responseBody.data)) {
+ const data: GetSeatedBookingOutput_2024_08_13 = responseBody.data;
+ expect(data.id).toEqual(bookingWithAttendeesDisabled.id);
+ expect(data.uid).toEqual(bookingWithAttendeesDisabled.uid);
+ expect(data.eventTypeId).toEqual(seatedEventTypeIdAttendeesDisabledId);
+ expect(data.attendees.length).toEqual(2);
+ await bookingsRepositoryFixture.deleteById(data.id);
+ } else {
+ throw new Error("Invalid response data - expected seated booking");
+ }
+ });
+ });
+ });
+
it("should not be able to reschedule seated booking if seatUid is not provided", async () => {
const body = {
start: new Date(Date.UTC(2030, 0, 8, 15, 0, 0)).toISOString(),
@@ -924,11 +1103,13 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
function responseDataIsCreateSeatedBooking(data: any): data is CreateSeatedBookingOutput_2024_08_13 {
- return Object.hasOwn(data, "seatUid");
+ return Object.prototype.hasOwnProperty.call(data, "seatUid");
}
function responseDataIsGetSeatedBooking(data: any): data is GetSeatedBookingOutput_2024_08_13 {
- return data?.attendees?.every((attendee: any) => attendee?.hasOwnProperty("seatUid"));
+ return data?.attendees?.every((attendee: any) =>
+ Object.prototype.hasOwnProperty.call(attendee, "seatUid")
+ );
}
afterAll(async () => {
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 acf2487c30..556eb9ffcd 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
@@ -615,6 +615,50 @@ export class BookingsService_2024_08_13 {
return this.outputService.getOutputRecurringBookings(ids);
}
+ async getBookingBySeatUid(seatUid: string, authUser: AuthOptionalUser) {
+ const bookingSeat =
+ await this.bookingSeatRepository.getByReferenceUidIncludeBookingWithAttendeesAndUserAndEvent(seatUid);
+
+ if (!bookingSeat || !bookingSeat.booking) {
+ throw new NotFoundException(`Booking with seatUid=${seatUid} was not found in the database`);
+ }
+
+ const booking = bookingSeat.booking;
+ const userIsEventTypeAdminOrOwner =
+ authUser && booking.eventType
+ ? await this.eventTypeAccessService.userIsEventTypeAdminOrOwner(
+ authUser,
+ booking.eventType as EventType
+ )
+ : false;
+
+ const isRecurring = !!booking.recurringEventId;
+ const seatsShowAttendees = !!booking.eventType?.seatsShowAttendees;
+ const showAllAttendees = userIsEventTypeAdminOrOwner || seatsShowAttendees;
+
+ // When user is not admin and seatsShowAttendees is false, show only the attendee for this seatUid
+ if (!showAllAttendees) {
+ const seatAttendee = booking.attendees.find(
+ (attendee) => attendee.bookingSeat?.referenceUid === seatUid
+ );
+ const bookingWithFilteredAttendees = {
+ ...booking,
+ attendees: seatAttendee ? [seatAttendee] : [],
+ };
+
+ if (isRecurring) {
+ return this.outputService.getOutputRecurringSeatedBooking(bookingWithFilteredAttendees, true);
+ }
+ return this.outputService.getOutputSeatedBooking(bookingWithFilteredAttendees, true);
+ }
+
+ if (isRecurring) {
+ return this.outputService.getOutputRecurringSeatedBooking(booking, true);
+ }
+
+ return this.outputService.getOutputSeatedBooking(booking, true);
+ }
+
async getBookings(
queryParams: GetBookingsInput_2024_08_13,
user: { email: string; id: number; orgId?: number },
diff --git a/apps/api/v2/src/modules/booking-seat/booking-seat.repository.ts b/apps/api/v2/src/modules/booking-seat/booking-seat.repository.ts
index de8275112d..df850c743e 100644
--- a/apps/api/v2/src/modules/booking-seat/booking-seat.repository.ts
+++ b/apps/api/v2/src/modules/booking-seat/booking-seat.repository.ts
@@ -1,10 +1,13 @@
+import { Injectable } from "@nestjs/common";
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
-import { Injectable } from "@nestjs/common";
@Injectable()
export class BookingSeatRepository {
- constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
+ constructor(
+ private readonly dbRead: PrismaReadService,
+ private readonly dbWrite: PrismaWriteService
+ ) {}
async getByReferenceUid(referenceUid: string) {
return this.dbRead.prisma.bookingSeat.findUnique({
@@ -16,4 +19,101 @@ export class BookingSeatRepository {
},
});
}
+
+ async getByReferenceUidIncludeBookingWithAttendeesAndUserAndEvent(referenceUid: string) {
+ return this.dbRead.prisma.bookingSeat.findUnique({
+ where: {
+ referenceUid,
+ },
+ select: {
+ id: true,
+ referenceUid: true,
+ data: true,
+ metadata: true,
+ bookingId: true,
+ attendeeId: true,
+ booking: {
+ select: {
+ id: true,
+ uid: true,
+ userId: true,
+ idempotencyKey: true,
+ userPrimaryEmail: true,
+ title: true,
+ description: true,
+ customInputs: true,
+ responses: true,
+ startTime: true,
+ endTime: true,
+ status: true,
+ location: true,
+ metadata: true,
+ paid: true,
+ destinationCalendarId: true,
+ cancellationReason: true,
+ rejectionReason: true,
+ reassignReason: true,
+ reassignById: true,
+ dynamicEventSlugRef: true,
+ dynamicGroupSlugRef: true,
+ fromReschedule: true,
+ rescheduled: true,
+ recurringEventId: true,
+ smsReminderNumber: true,
+ scheduledJobs: true,
+ isRecorded: true,
+ iCalUID: true,
+ iCalSequence: true,
+ rating: true,
+ ratingFeedback: true,
+ noShowHost: true,
+ oneTimePassword: true,
+ cancelledBy: true,
+ rescheduledBy: true,
+ createdAt: true,
+ updatedAt: true,
+ eventTypeId: true,
+ creationSource: true,
+ attendees: {
+ select: {
+ name: true,
+ email: true,
+ timeZone: true,
+ locale: true,
+ phoneNumber: true,
+ noShow: true,
+ bookingSeat: {
+ select: {
+ id: true,
+ referenceUid: true,
+ data: true,
+ metadata: true,
+ bookingId: true,
+ attendeeId: true,
+ },
+ },
+ },
+ },
+ user: {
+ select: {
+ id: true,
+ name: true,
+ email: true,
+ username: true,
+ },
+ },
+ eventType: {
+ select: {
+ id: true,
+ slug: true,
+ seatsShowAttendees: true,
+ teamId: true,
+ userId: true,
+ },
+ },
+ },
+ },
+ },
+ });
+ }
}