feat: api v2 GET booking attendees endpoint (#27664)
* feat: add booking attendees endpoint to API v2 Co-Authored-By: rajiv@cal.com <sahalrajiv6900@gmail.com> * feat: add rate limiting to booking attendees endpoint Co-Authored-By: rajiv@cal.com <sahalrajiv6900@gmail.com> * refactor: simplify attendees output to id, bookingId, name, email, timeZone Co-Authored-By: rajiv@cal.com <sahalrajiv6900@gmail.com> * test: add E2E tests for booking attendees endpoint Co-Authored-By: rajiv@cal.com <sahalrajiv6900@gmail.com> * chore: update bookings repository * fixup: add pbac guards and update service logic * chore: update openapi spec * test: add rate limiting E2E test for booking attendees endpoint Co-Authored-By: rajiv@cal.com <sahalrajiv6900@gmail.com> * fix: tests * fix: return 404 instead of 403 for non-existent booking in BookingPbacGuard The BookingPbacGuard was returning 403 (Forbidden) for non-existent bookings because doesUserIdHaveAccessToBooking returns false when a booking doesn't exist, which the guard treated as an access denial. Added an explicit booking existence check in the guard before the access check, so non-existent bookings now correctly return 404 (Not Found) as documented in the PR description. Updated the E2E test to expect 404 for non-existent booking UIDs. Issue identified by cubic. Co-Authored-By: unknown <> * fixup * fix: return 404 instead of 403 for non-existent booking in attendees endpoint BookingPbacGuard now checks booking existence before the access check, returning 404 (Not Found) instead of 403 (Forbidden) for non-existent booking UIDs. Updated the E2E test assertion and description to match. Issue identified by cubic (confidence 9/10). Co-Authored-By: unknown <> * chore: implement PR feedback * chore: update tests * fixup * chore: update endpoint decsription * feat: endpoint to retrieve specific attendee * chore: update e2e tests * chore: implement cubic feedback * fix: update test to expect 403 for non-existent booking UID (BookingPbacGuard behavior) Co-Authored-By: rajiv@cal.com <sahalrajiv6900@gmail.com> * fix: merge conflicts * feat: endpoint to get attendees * chore: update findByUidIncludeEventTypeAttendeesAndUser method * chore: implement PR feedback * fix: e2e tests * chore: update e2e tests * fixup fixup * fix: remove phoneNumber assertion since it's optional and not provided in test * chore: implement PR feedback * fix: keep the same output shape for get attendees and get attendee endpoint * chore: update openapi spec --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: bot_apk <apk@cognition.ai>
This commit is contained in:
11 files changed
+906
-29
No files matched your search
@@ -1,12 +1,33 @@
|
||||
import { BOOKING_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import {
|
||||
BOOKING_WRITE,
|
||||
BOOKING_READ,
|
||||
SUCCESS_STATUS,
|
||||
} from "@calcom/platform-constants";
|
||||
import { AddAttendeeInput_2024_08_13 } from "@calcom/platform-types";
|
||||
import { Body, Controller, HttpCode, HttpStatus, Param, Post, UseGuards } from "@nestjs/common";
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
Param,
|
||||
Post,
|
||||
UseGuards,
|
||||
Get,
|
||||
ParseIntPipe,
|
||||
} from "@nestjs/common";
|
||||
import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
|
||||
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 { AddAttendeeOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/add-attendee.output";
|
||||
import {
|
||||
GetBookingAttendeesOutput_2024_08_13,
|
||||
GetBookingAttendeeOutput_2024_08_13,
|
||||
} from "@/ee/bookings/2024-08-13/outputs/get-booking-attendees.output";
|
||||
import { BookingAttendeesService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/booking-attendees.service";
|
||||
import { VERSION_2024_08_13, VERSION_2024_08_13_VALUE } 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 } from "@/lib/docs/headers";
|
||||
import { Throttle } from "@/lib/endpoint-throttler-decorator";
|
||||
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||
@@ -28,7 +49,60 @@ import { ApiAuthGuardUser } from "@/modules/auth/strategies/api-auth/api-auth.st
|
||||
required: true,
|
||||
})
|
||||
export class BookingAttendeesController_2024_08_13 {
|
||||
constructor(private readonly bookingAttendeesService: BookingAttendeesService_2024_08_13) {}
|
||||
constructor(
|
||||
private readonly bookingAttendeesService: BookingAttendeesService_2024_08_13
|
||||
) {}
|
||||
|
||||
@Get("/")
|
||||
@Permissions([BOOKING_READ])
|
||||
@UseGuards(ApiAuthGuard, BookingUidGuard, BookingPbacGuard)
|
||||
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||
@ApiOperation({
|
||||
summary: "Get all attendees for a booking",
|
||||
description: `Retrieve all attendees for a specific booking by its UID.
|
||||
|
||||
<Note>The cal-api-version header is required for this endpoint. Without it, the request will fail with a 404 error.</Note>
|
||||
`,
|
||||
})
|
||||
async getBookingAttendees(
|
||||
@Param("bookingUid") bookingUid: string,
|
||||
@GetUser() user: ApiAuthGuardUser
|
||||
): Promise<GetBookingAttendeesOutput_2024_08_13> {
|
||||
const attendees = await this.bookingAttendeesService.getBookingAttendees(
|
||||
bookingUid
|
||||
);
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: attendees,
|
||||
};
|
||||
}
|
||||
|
||||
@Get("/:attendeeId")
|
||||
@Permissions([BOOKING_READ])
|
||||
@UseGuards(ApiAuthGuard, BookingUidGuard, BookingPbacGuard)
|
||||
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||
@ApiOperation({
|
||||
summary: "Get a specific attendee for a booking",
|
||||
description: `Retrieve a specific attendee by their ID for a booking identified by its UID.
|
||||
|
||||
<Note>The cal-api-version header is required for this endpoint. Without it, the request will fail with a 404 error.</Note>
|
||||
`,
|
||||
})
|
||||
async getBookingAttendee(
|
||||
@Param("bookingUid") bookingUid: string,
|
||||
@Param("attendeeId", ParseIntPipe) attendeeId: number
|
||||
): Promise<GetBookingAttendeeOutput_2024_08_13> {
|
||||
const attendee = await this.bookingAttendeesService.getBookingAttendee(
|
||||
bookingUid,
|
||||
attendeeId
|
||||
);
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: attendee,
|
||||
};
|
||||
}
|
||||
|
||||
@Post("/")
|
||||
@HttpCode(HttpStatus.CREATED)
|
||||
@@ -61,7 +135,11 @@ export class BookingAttendeesController_2024_08_13 {
|
||||
@Body() body: AddAttendeeInput_2024_08_13,
|
||||
@GetUser() user: ApiAuthGuardUser
|
||||
): Promise<AddAttendeeOutput_2024_08_13> {
|
||||
const attendee = await this.bookingAttendeesService.addAttendee(bookingUid, body, user);
|
||||
const attendee = await this.bookingAttendeesService.addAttendee(
|
||||
bookingUid,
|
||||
body,
|
||||
user
|
||||
);
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
|
||||
@@ -0,0 +1,386 @@
|
||||
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants";
|
||||
import type { BookingOutput_2024_08_13, CreateBookingInput_2024_08_13 } from "@calcom/platform-types";
|
||||
import type { Team, User } from "@calcom/prisma/client";
|
||||
import { INestApplication } from "@nestjs/common";
|
||||
import { NestExpressApplication } from "@nestjs/platform-express";
|
||||
import { Test } from "@nestjs/testing";
|
||||
import request from "supertest";
|
||||
import { BookingsRepositoryFixture } from "test/fixtures/repository/bookings.repository.fixture";
|
||||
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
|
||||
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
|
||||
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
|
||||
import { TokensRepositoryFixture } from "test/fixtures/repository/tokens.repository.fixture";
|
||||
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||
import { randomString } from "test/utils/randomString";
|
||||
import { mockThrottlerGuard } from "test/utils/withNoThrottler";
|
||||
import { AppModule } from "@/app.module";
|
||||
import { bootstrap } from "@/bootstrap";
|
||||
import { CreateBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/create-booking.output";
|
||||
import {
|
||||
GetBookingAttendeeOutput_2024_08_13,
|
||||
GetBookingAttendeesOutput_2024_08_13,
|
||||
} from "@/ee/bookings/2024-08-13/outputs/get-booking-attendees.output";
|
||||
import { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input";
|
||||
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
|
||||
import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service";
|
||||
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { UsersModule } from "@/modules/users/users.module";
|
||||
|
||||
type TestUser = {
|
||||
user: User;
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
};
|
||||
|
||||
type TestSetup = {
|
||||
organizer: TestUser;
|
||||
unrelatedUser: TestUser;
|
||||
eventTypeId: number;
|
||||
bookingUid: string;
|
||||
attendeeId: number;
|
||||
};
|
||||
|
||||
describe("Bookings Endpoints 2024-08-13 get attendees", () => {
|
||||
let app: INestApplication;
|
||||
let organization: Team;
|
||||
|
||||
let userRepositoryFixture: UserRepositoryFixture;
|
||||
let bookingsRepositoryFixture: BookingsRepositoryFixture;
|
||||
let schedulesService: SchedulesService_2024_04_15;
|
||||
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
|
||||
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
|
||||
let teamRepositoryFixture: TeamRepositoryFixture;
|
||||
let tokensRepositoryFixture: TokensRepositoryFixture;
|
||||
|
||||
let testSetup: TestSetup;
|
||||
|
||||
beforeAll(async () => {
|
||||
mockThrottlerGuard();
|
||||
|
||||
const moduleRef = await Test.createTestingModule({
|
||||
imports: [AppModule, PrismaModule, UsersModule, SchedulesModule_2024_04_15],
|
||||
})
|
||||
.overrideGuard(PermissionsGuard)
|
||||
.useValue({
|
||||
canActivate: () => true,
|
||||
})
|
||||
.compile();
|
||||
|
||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||
bookingsRepositoryFixture = new BookingsRepositoryFixture(moduleRef);
|
||||
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
|
||||
oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef);
|
||||
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
|
||||
tokensRepositoryFixture = new TokensRepositoryFixture(moduleRef);
|
||||
|
||||
organization = await teamRepositoryFixture.create({
|
||||
name: `get-attendees-2024-08-13-organization-${randomString()}`,
|
||||
});
|
||||
|
||||
await setupTestData();
|
||||
|
||||
app = moduleRef.createNestApplication();
|
||||
bootstrap(app as NestExpressApplication);
|
||||
|
||||
await app.init();
|
||||
});
|
||||
|
||||
async function setupTestData() {
|
||||
const oAuthClient = await createOAuthClient(organization.id);
|
||||
|
||||
const organizerUser = await userRepositoryFixture.create({
|
||||
email: `get-attendees-2024-08-13-organizer-${randomString()}@api.com`,
|
||||
platformOAuthClients: {
|
||||
connect: { id: oAuthClient.id },
|
||||
},
|
||||
});
|
||||
|
||||
const unrelatedUserData = await userRepositoryFixture.create({
|
||||
email: `get-attendees-2024-08-13-unrelated-${randomString()}@api.com`,
|
||||
platformOAuthClients: {
|
||||
connect: { id: oAuthClient.id },
|
||||
},
|
||||
});
|
||||
|
||||
const organizerTokens = await tokensRepositoryFixture.createTokens(organizerUser.id, oAuthClient.id);
|
||||
const unrelatedUserTokens = await tokensRepositoryFixture.createTokens(
|
||||
unrelatedUserData.id,
|
||||
oAuthClient.id
|
||||
);
|
||||
|
||||
const schedule: CreateScheduleInput_2024_04_15 = {
|
||||
name: `get-attendees-2024-08-13-schedule-${randomString()}`,
|
||||
timeZone: "Europe/Rome",
|
||||
isDefault: true,
|
||||
};
|
||||
await schedulesService.createUserSchedule(organizerUser.id, schedule);
|
||||
|
||||
const eventType = await eventTypesRepositoryFixture.create(
|
||||
{
|
||||
title: `get-attendees-2024-08-13-event-type-${randomString()}`,
|
||||
slug: `get-attendees-2024-08-13-event-type-${randomString()}`,
|
||||
length: 60,
|
||||
},
|
||||
organizerUser.id
|
||||
);
|
||||
|
||||
testSetup = {
|
||||
organizer: {
|
||||
user: organizerUser,
|
||||
accessToken: organizerTokens.accessToken,
|
||||
refreshToken: organizerTokens.refreshToken,
|
||||
},
|
||||
unrelatedUser: {
|
||||
user: unrelatedUserData,
|
||||
accessToken: unrelatedUserTokens.accessToken,
|
||||
refreshToken: unrelatedUserTokens.refreshToken,
|
||||
},
|
||||
eventTypeId: eventType.id,
|
||||
bookingUid: "",
|
||||
attendeeId: 0,
|
||||
};
|
||||
}
|
||||
|
||||
async function createOAuthClient(organizationId: number) {
|
||||
const data = {
|
||||
logo: "logo-url",
|
||||
name: "name",
|
||||
redirectUris: ["http://localhost:5555"],
|
||||
permissions: 32,
|
||||
areEmailsEnabled: true,
|
||||
};
|
||||
const secret = "secret";
|
||||
|
||||
const client = await oauthClientRepositoryFixture.create(organizationId, data, secret);
|
||||
return client;
|
||||
}
|
||||
|
||||
describe("GET /v2/bookings/:bookingUid/attendees", () => {
|
||||
const attendeeEmail = "attendee@example.com";
|
||||
const attendeeName = "Test Attendee";
|
||||
const attendeeTimeZone = "Europe/Rome";
|
||||
|
||||
beforeAll(async () => {
|
||||
const createBookingBody: CreateBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2030, 0, 8, 13, 0, 0)).toISOString(),
|
||||
eventTypeId: testSetup.eventTypeId,
|
||||
attendee: {
|
||||
name: attendeeName,
|
||||
email: attendeeEmail,
|
||||
timeZone: attendeeTimeZone,
|
||||
language: "it",
|
||||
},
|
||||
location: "https://meet.google.com/abc-def-ghi",
|
||||
bookingFieldsResponses: {
|
||||
customField: "customValue",
|
||||
},
|
||||
metadata: {
|
||||
userId: "100",
|
||||
},
|
||||
};
|
||||
|
||||
const createBookingResponse = await request(app.getHttpServer())
|
||||
.post("/v2/bookings")
|
||||
.send(createBookingBody)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(201);
|
||||
|
||||
const createBookingResponseBody: CreateBookingOutput_2024_08_13 = createBookingResponse.body;
|
||||
expect(createBookingResponseBody.status).toEqual(SUCCESS_STATUS);
|
||||
|
||||
if (!responseDataIsBooking(createBookingResponseBody.data)) {
|
||||
throw new Error(
|
||||
"Invalid response data - expected booking but received array of possibly recurring bookings"
|
||||
);
|
||||
}
|
||||
|
||||
testSetup.bookingUid = createBookingResponseBody.data.uid;
|
||||
});
|
||||
|
||||
describe("Authentication", () => {
|
||||
it("should return 401 when getting attendees without authentication", async () => {
|
||||
const getAttendeesResponse = await request(app.getHttpServer())
|
||||
.get(`/v2/bookings/${testSetup.bookingUid}/attendees`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13);
|
||||
|
||||
expect(getAttendeesResponse.status).toBe(401);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Authorization - Organizer", () => {
|
||||
it("should allow booking organizer to get attendees", async () => {
|
||||
const getAttendeesResponse = await request(app.getHttpServer())
|
||||
.get(`/v2/bookings/${testSetup.bookingUid}/attendees`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.set("Authorization", `Bearer ${testSetup.organizer.accessToken}`)
|
||||
.expect(200);
|
||||
|
||||
const getAttendeesResponseBody: GetBookingAttendeesOutput_2024_08_13 = getAttendeesResponse.body;
|
||||
|
||||
expect(getAttendeesResponseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(getAttendeesResponseBody.data).toBeDefined();
|
||||
expect(Array.isArray(getAttendeesResponseBody.data)).toBe(true);
|
||||
expect(getAttendeesResponseBody.data.length).toBe(1);
|
||||
|
||||
const attendee = getAttendeesResponseBody.data[0];
|
||||
expect(attendee.name).toEqual(attendeeName);
|
||||
expect(attendee.email).toEqual(attendeeEmail);
|
||||
expect(attendee.timeZone).toEqual(attendeeTimeZone);
|
||||
});
|
||||
|
||||
it("should return 403 when unrelated user tries to get attendees", async () => {
|
||||
await request(app.getHttpServer())
|
||||
.get(`/v2/bookings/${testSetup.bookingUid}/attendees`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.set("Authorization", `Bearer ${testSetup.unrelatedUser.accessToken}`)
|
||||
.expect(403);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Response format", () => {
|
||||
it("should return attendees with correct structure", async () => {
|
||||
const getAttendeesResponse = await request(app.getHttpServer())
|
||||
.get(`/v2/bookings/${testSetup.bookingUid}/attendees`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.set("Authorization", `Bearer ${testSetup.organizer.accessToken}`)
|
||||
.expect(200);
|
||||
|
||||
const getAttendeesResponseBody: GetBookingAttendeesOutput_2024_08_13 = getAttendeesResponse.body;
|
||||
|
||||
expect(getAttendeesResponseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(getAttendeesResponseBody.data).toBeDefined();
|
||||
|
||||
const attendee = getAttendeesResponseBody.data[0];
|
||||
|
||||
expect(typeof attendee.id).toBe("number");
|
||||
expect(typeof attendee.name).toBe("string");
|
||||
expect(typeof attendee.email).toBe("string");
|
||||
expect(typeof attendee.timeZone).toBe("string");
|
||||
expect(attendee).toHaveProperty("displayEmail");
|
||||
expect(attendee).toHaveProperty("language");
|
||||
expect(attendee).toHaveProperty("absent");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("GET /v2/bookings/:bookingUid/attendees/:attendeeId", () => {
|
||||
const attendeeEmail = "attendee@example.com";
|
||||
const attendeeName = "Test Attendee";
|
||||
const attendeeTimeZone = "Europe/Rome";
|
||||
|
||||
beforeAll(async () => {
|
||||
const getAttendeesResponse = await request(app.getHttpServer())
|
||||
.get(`/v2/bookings/${testSetup.bookingUid}/attendees`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.set("Authorization", `Bearer ${testSetup.organizer.accessToken}`)
|
||||
.expect(200);
|
||||
|
||||
const getAttendeesResponseBody: GetBookingAttendeesOutput_2024_08_13 = getAttendeesResponse.body;
|
||||
if (getAttendeesResponseBody.data?.length) {
|
||||
testSetup.attendeeId = getAttendeesResponseBody.data[0].id;
|
||||
}
|
||||
});
|
||||
|
||||
describe("Authentication", () => {
|
||||
it("should return 401 when getting single attendee without authentication", async () => {
|
||||
const getAttendeeResponse = await request(app.getHttpServer())
|
||||
.get(`/v2/bookings/${testSetup.bookingUid}/attendees/${testSetup.attendeeId}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13);
|
||||
|
||||
expect(getAttendeeResponse.status).toBe(401);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Authorization", () => {
|
||||
it("should allow booking organizer to get single attendee", async () => {
|
||||
const getAttendeeResponse = await request(app.getHttpServer())
|
||||
.get(`/v2/bookings/${testSetup.bookingUid}/attendees/${testSetup.attendeeId}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.set("Authorization", `Bearer ${testSetup.organizer.accessToken}`)
|
||||
.expect(200);
|
||||
|
||||
const getAttendeeResponseBody: GetBookingAttendeeOutput_2024_08_13 = getAttendeeResponse.body;
|
||||
|
||||
expect(getAttendeeResponseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(getAttendeeResponseBody.data).toBeDefined();
|
||||
expect(getAttendeeResponseBody.data.name).toEqual(attendeeName);
|
||||
expect(getAttendeeResponseBody.data.email).toEqual(attendeeEmail);
|
||||
expect(getAttendeeResponseBody.data.timeZone).toEqual(attendeeTimeZone);
|
||||
});
|
||||
|
||||
it("should return 403 when unrelated user tries to get single attendee", async () => {
|
||||
await request(app.getHttpServer())
|
||||
.get(`/v2/bookings/${testSetup.bookingUid}/attendees/${testSetup.attendeeId}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.set("Authorization", `Bearer ${testSetup.unrelatedUser.accessToken}`)
|
||||
.expect(403);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Not found cases", () => {
|
||||
it("should return 404 for non-existent attendee ID", async () => {
|
||||
const nonExistentAttendeeId = 999999999;
|
||||
await request(app.getHttpServer())
|
||||
.get(`/v2/bookings/${testSetup.bookingUid}/attendees/${nonExistentAttendeeId}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.set("Authorization", `Bearer ${testSetup.organizer.accessToken}`)
|
||||
.expect(404);
|
||||
});
|
||||
|
||||
it("should return 403 for non-existent booking UID because BookingPbacGuard treats missing bookings as unauthorized", async () => {
|
||||
await request(app.getHttpServer())
|
||||
.get(`/v2/bookings/non-existent-booking-uid/attendees/${testSetup.attendeeId}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.set("Authorization", `Bearer ${testSetup.organizer.accessToken}`)
|
||||
.expect(403);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Response format", () => {
|
||||
it("should return single attendee with correct structure", async () => {
|
||||
const getAttendeeResponse = await request(app.getHttpServer())
|
||||
.get(`/v2/bookings/${testSetup.bookingUid}/attendees/${testSetup.attendeeId}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.set("Authorization", `Bearer ${testSetup.organizer.accessToken}`)
|
||||
.expect(200);
|
||||
|
||||
const getAttendeeResponseBody: GetBookingAttendeeOutput_2024_08_13 = getAttendeeResponse.body;
|
||||
|
||||
expect(getAttendeeResponseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(getAttendeeResponseBody.data).toBeDefined();
|
||||
|
||||
expect(Array.isArray(getAttendeeResponseBody.data)).toBe(false);
|
||||
|
||||
const attendee = getAttendeeResponseBody.data;
|
||||
|
||||
expect(typeof attendee.id).toBe("number");
|
||||
expect(typeof attendee.name).toBe("string");
|
||||
expect(typeof attendee.email).toBe("string");
|
||||
expect(typeof attendee.timeZone).toBe("string");
|
||||
expect(attendee).toHaveProperty("displayEmail");
|
||||
expect(attendee).toHaveProperty("language");
|
||||
expect(attendee).toHaveProperty("absent");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await teamRepositoryFixture.delete(organization.id);
|
||||
|
||||
await userRepositoryFixture.deleteByEmail(testSetup.organizer.user.email);
|
||||
await userRepositoryFixture.deleteByEmail(testSetup.unrelatedUser.user.email);
|
||||
|
||||
await bookingsRepositoryFixture.deleteAllBookings(
|
||||
testSetup.organizer.user.id,
|
||||
testSetup.organizer.user.email
|
||||
);
|
||||
|
||||
await app.close();
|
||||
});
|
||||
|
||||
function responseDataIsBooking(data: unknown): data is BookingOutput_2024_08_13 {
|
||||
return !Array.isArray(data) && typeof data === "object" && data !== null && "id" in data;
|
||||
}
|
||||
});
|
||||
@@ -17,13 +17,17 @@ export class BookingPbacGuard implements CanActivate {
|
||||
private bookingAccessService: BookingAccessService;
|
||||
|
||||
constructor(private readonly prismaReadService: PrismaReadService) {
|
||||
this.bookingAccessService = new BookingAccessService(this.prismaReadService.prisma);
|
||||
this.bookingAccessService = new BookingAccessService(
|
||||
this.prismaReadService.prisma
|
||||
);
|
||||
}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const request = context
|
||||
.switchToHttp()
|
||||
.getRequest<Request & { user?: ApiAuthGuardUser; pbacAuthorizedRequest?: boolean }>();
|
||||
.getRequest<
|
||||
Request & { user?: ApiAuthGuardUser; pbacAuthorizedRequest?: boolean }
|
||||
>();
|
||||
const user = request.user;
|
||||
const bookingUid = request.params.bookingUid;
|
||||
|
||||
@@ -32,13 +36,16 @@ export class BookingPbacGuard implements CanActivate {
|
||||
}
|
||||
|
||||
if (!bookingUid) {
|
||||
throw new BadRequestException("BookingPbacGuard - bookingUid is required");
|
||||
throw new BadRequestException(
|
||||
"BookingPbacGuard - bookingUid is required"
|
||||
);
|
||||
}
|
||||
|
||||
const hasAccess = await this.bookingAccessService.doesUserIdHaveAccessToBooking({
|
||||
userId: user.id,
|
||||
bookingUid,
|
||||
});
|
||||
const hasAccess =
|
||||
await this.bookingAccessService.doesUserIdHaveAccessToBooking({
|
||||
userId: user.id,
|
||||
bookingUid,
|
||||
});
|
||||
|
||||
if (!hasAccess) {
|
||||
throw new ForbiddenException(
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { ERROR_STATUS, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { BookingAttendee } from "@calcom/platform-types";
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { Expose, Type } from "class-transformer";
|
||||
import { IsArray, IsEnum, IsNumber, ValidateNested } from "class-validator";
|
||||
|
||||
export class BookingAttendeeWithId_2024_08_13 extends BookingAttendee {
|
||||
@ApiProperty({ type: Number, example: 251 })
|
||||
@IsNumber()
|
||||
@Expose()
|
||||
id!: number;
|
||||
}
|
||||
|
||||
export class GetBookingAttendeesOutput_2024_08_13 {
|
||||
@ApiProperty({
|
||||
example: SUCCESS_STATUS,
|
||||
enum: [SUCCESS_STATUS, ERROR_STATUS],
|
||||
})
|
||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||
|
||||
@ApiProperty({ type: [BookingAttendeeWithId_2024_08_13] })
|
||||
@ValidateNested({ each: true })
|
||||
@IsArray()
|
||||
@Type(() => BookingAttendeeWithId_2024_08_13)
|
||||
data!: BookingAttendeeWithId_2024_08_13[];
|
||||
}
|
||||
|
||||
export class GetBookingAttendeeOutput_2024_08_13 {
|
||||
@ApiProperty({
|
||||
example: SUCCESS_STATUS,
|
||||
enum: [SUCCESS_STATUS, ERROR_STATUS],
|
||||
})
|
||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||
|
||||
@ApiProperty({ type: BookingAttendeeWithId_2024_08_13 })
|
||||
@ValidateNested()
|
||||
@Type(() => BookingAttendeeWithId_2024_08_13)
|
||||
data!: BookingAttendeeWithId_2024_08_13;
|
||||
}
|
||||
@@ -6,7 +6,10 @@ import type { Prisma } from "@calcom/prisma/client";
|
||||
|
||||
@Injectable()
|
||||
export class BookingsRepository_2024_08_13 {
|
||||
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
|
||||
constructor(
|
||||
private readonly dbRead: PrismaReadService,
|
||||
private readonly dbWrite: PrismaWriteService
|
||||
) {}
|
||||
|
||||
async getById(id: number) {
|
||||
return this.dbRead.prisma.booking.findUnique({
|
||||
@@ -72,6 +75,25 @@ export class BookingsRepository_2024_08_13 {
|
||||
});
|
||||
}
|
||||
|
||||
async getByUidWithAttendees(uid: string) {
|
||||
return this.dbRead.prisma.booking.findUnique({
|
||||
where: {
|
||||
uid,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
attendees: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
timeZone: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getByUidWithEventType(bookingUid: string) {
|
||||
return this.dbRead.prisma.booking.findUnique({
|
||||
where: {
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import { BookingAttendeesService } from "@calcom/platform-libraries/bookings";
|
||||
import { ErrorCode, ErrorWithCode } from "@calcom/platform-libraries/errors";
|
||||
import type { AddAttendeeInput_2024_08_13 } from "@calcom/platform-types";
|
||||
import { HttpException, Injectable, NotFoundException } from "@nestjs/common";
|
||||
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||
import { plainToClass } from "class-transformer";
|
||||
import { BookingAttendeeOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/add-attendee.output";
|
||||
import {
|
||||
BookingAttendeeWithId_2024_08_13,
|
||||
} from "@/ee/bookings/2024-08-13/outputs/get-booking-attendees.output";
|
||||
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/repositories/bookings.repository";
|
||||
import { PlatformBookingsService } from "@/ee/bookings/shared/platform-bookings.service";
|
||||
import type { ApiAuthGuardUser } from "@/modules/auth/strategies/api-auth/api-auth.strategy";
|
||||
@@ -18,21 +22,84 @@ export class BookingAttendeesService_2024_08_13 {
|
||||
this.bookingAttendeesService = new BookingAttendeesService();
|
||||
}
|
||||
|
||||
async getBookingAttendees(
|
||||
bookingUid: string
|
||||
): Promise<BookingAttendeeWithId_2024_08_13[]> {
|
||||
const attendees = await this.bookingAttendeesService.getBookingAttendees(
|
||||
bookingUid
|
||||
);
|
||||
|
||||
return attendees.map((attendee) =>
|
||||
plainToClass(
|
||||
BookingAttendeeWithId_2024_08_13,
|
||||
{
|
||||
id: attendee.id,
|
||||
name: attendee.name,
|
||||
email: attendee.email,
|
||||
displayEmail: this.getDisplayEmail(attendee.email),
|
||||
timeZone: attendee.timeZone,
|
||||
language: attendee.locale ?? undefined,
|
||||
absent: attendee.noShow ?? false,
|
||||
phoneNumber: attendee.phoneNumber ?? undefined,
|
||||
},
|
||||
{ strategy: "excludeAll" }
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
async getBookingAttendee(
|
||||
bookingUid: string,
|
||||
attendeeId: number
|
||||
): Promise<BookingAttendeeWithId_2024_08_13> {
|
||||
try {
|
||||
const attendee = await this.bookingAttendeesService.getBookingAttendee(
|
||||
bookingUid,
|
||||
attendeeId
|
||||
);
|
||||
|
||||
return plainToClass(
|
||||
BookingAttendeeWithId_2024_08_13,
|
||||
{
|
||||
id: attendee.id,
|
||||
name: attendee.name,
|
||||
email: attendee.email,
|
||||
displayEmail: this.getDisplayEmail(attendee.email),
|
||||
timeZone: attendee.timeZone,
|
||||
language: attendee.locale ?? undefined,
|
||||
absent: attendee.noShow ?? false,
|
||||
phoneNumber: attendee.phoneNumber ?? undefined,
|
||||
},
|
||||
{ strategy: "excludeAll" }
|
||||
);
|
||||
} catch (e) {
|
||||
if (e instanceof ErrorWithCode && e.code === ErrorCode.NotFound) {
|
||||
throw new NotFoundException(e.message);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
async addAttendee(
|
||||
bookingUid: string,
|
||||
input: AddAttendeeInput_2024_08_13,
|
||||
user: ApiAuthGuardUser
|
||||
): Promise<BookingAttendeeOutput_2024_08_13> {
|
||||
const booking = await this.bookingsRepository.getByUidWithEventType(bookingUid);
|
||||
const booking = await this.bookingsRepository.getByUidWithEventType(
|
||||
bookingUid
|
||||
);
|
||||
if (!booking) {
|
||||
throw new NotFoundException(`Booking with uid ${bookingUid} not found`);
|
||||
}
|
||||
|
||||
const platformClientParams = booking.eventTypeId
|
||||
? await this.platformBookingsService.getOAuthClientParams(booking.eventTypeId)
|
||||
? await this.platformBookingsService.getOAuthClientParams(
|
||||
booking.eventTypeId
|
||||
)
|
||||
: undefined;
|
||||
|
||||
const emailsEnabled = platformClientParams ? platformClientParams.arePlatformEmailsEnabled : true;
|
||||
const emailsEnabled = platformClientParams
|
||||
? platformClientParams.arePlatformEmailsEnabled
|
||||
: true;
|
||||
|
||||
const createdAttendee = await this.bookingAttendeesService.addAttendee({
|
||||
bookingId: booking.id,
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import type { Booking, Prisma, User } from "@calcom/prisma/client";
|
||||
import { TestingModule } from "@nestjs/testing";
|
||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { TestingModule } from "@nestjs/testing";
|
||||
|
||||
import type { Booking, User, Prisma } from "@calcom/prisma/client";
|
||||
|
||||
export class BookingsRepositoryFixture {
|
||||
private prismaReadClient: PrismaReadService["prisma"];
|
||||
@@ -21,6 +20,13 @@ export class BookingsRepositoryFixture {
|
||||
return this.prismaReadClient.booking.findUnique({ where: { uid: bookingUid } });
|
||||
}
|
||||
|
||||
async getByUidWithAttendees(bookingUid: Booking["uid"]) {
|
||||
return this.prismaReadClient.booking.findUnique({
|
||||
where: { uid: bookingUid },
|
||||
include: { attendees: true },
|
||||
});
|
||||
}
|
||||
|
||||
async getByRecurringBookingUid(recurringBookingUid: string) {
|
||||
return this.prismaReadClient.booking.findMany({
|
||||
where: {
|
||||
|
||||
@@ -10235,6 +10235,52 @@
|
||||
}
|
||||
},
|
||||
"/v2/bookings/{bookingUid}/attendees": {
|
||||
"get": {
|
||||
"operationId": "BookingAttendeesController_2024_08_13_getBookingAttendees",
|
||||
"summary": "Get all attendees for a booking",
|
||||
"description": "Retrieve all attendees for a specific booking by its UID.\n \n <Note>The cal-api-version header is required for this endpoint. Without it, the request will fail with a 404 error.</Note>\n ",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "cal-api-version",
|
||||
"in": "header",
|
||||
"description": "Must be set to 2024-08-13. This header is required as this endpoint does not exist in older API versions.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "bookingUid",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Authorization",
|
||||
"in": "header",
|
||||
"description": "value must be `Bearer <token>` where `<token>` is api key prefixed with cal_ or managed user access token",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/GetBookingAttendeesOutput_2024_08_13"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": ["Bookings / Attendees"]
|
||||
},
|
||||
"post": {
|
||||
"operationId": "BookingAttendeesController_2024_08_13_addAttendee",
|
||||
"summary": "Add an attendee to a booking",
|
||||
@@ -10292,6 +10338,62 @@
|
||||
"tags": ["Bookings / Attendees"]
|
||||
}
|
||||
},
|
||||
"/v2/bookings/{bookingUid}/attendees/{attendeeId}": {
|
||||
"get": {
|
||||
"operationId": "BookingAttendeesController_2024_08_13_getBookingAttendee",
|
||||
"summary": "Get a specific attendee for a booking",
|
||||
"description": "Retrieve a specific attendee by their ID for a booking identified by its UID.\n \n <Note>The cal-api-version header is required for this endpoint. Without it, the request will fail with a 404 error.</Note>\n ",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "cal-api-version",
|
||||
"in": "header",
|
||||
"description": "Must be set to 2024-08-13. This header is required as this endpoint does not exist in older API versions.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "bookingUid",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "attendeeId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Authorization",
|
||||
"in": "header",
|
||||
"description": "value must be `Bearer <token>` where `<token>` is api key prefixed with cal_ or managed user access token",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/GetBookingAttendeeOutput_2024_08_13"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": ["Bookings / Attendees"]
|
||||
}
|
||||
},
|
||||
"/v2/bookings/{bookingUid}/guests": {
|
||||
"post": {
|
||||
"operationId": "BookingGuestsController_2024_08_13_addGuests",
|
||||
@@ -31580,6 +31682,121 @@
|
||||
},
|
||||
"required": ["status", "data"]
|
||||
},
|
||||
"BookingAttendeeWithId_2024_08_13": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"example": "John Doe"
|
||||
},
|
||||
"email": {
|
||||
"type": "string",
|
||||
"example": "john@example.com"
|
||||
},
|
||||
"displayEmail": {
|
||||
"type": "string",
|
||||
"example": "john@example.com",
|
||||
"description": "Clean email for display purposes"
|
||||
},
|
||||
"timeZone": {
|
||||
"type": "string",
|
||||
"example": "America/New_York"
|
||||
},
|
||||
"language": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"ar",
|
||||
"ca",
|
||||
"de",
|
||||
"es",
|
||||
"eu",
|
||||
"he",
|
||||
"id",
|
||||
"ja",
|
||||
"lv",
|
||||
"pl",
|
||||
"ro",
|
||||
"sr",
|
||||
"th",
|
||||
"vi",
|
||||
"az",
|
||||
"cs",
|
||||
"el",
|
||||
"es-419",
|
||||
"fi",
|
||||
"hr",
|
||||
"it",
|
||||
"km",
|
||||
"nl",
|
||||
"pt",
|
||||
"ru",
|
||||
"sv",
|
||||
"tr",
|
||||
"zh-CN",
|
||||
"bg",
|
||||
"da",
|
||||
"en",
|
||||
"et",
|
||||
"fr",
|
||||
"hu",
|
||||
"iw",
|
||||
"ko",
|
||||
"no",
|
||||
"pt-BR",
|
||||
"sk",
|
||||
"ta",
|
||||
"uk",
|
||||
"zh-TW",
|
||||
"bn"
|
||||
],
|
||||
"example": "en"
|
||||
},
|
||||
"absent": {
|
||||
"type": "boolean",
|
||||
"example": false
|
||||
},
|
||||
"phoneNumber": {
|
||||
"type": "string",
|
||||
"example": "+1234567890"
|
||||
},
|
||||
"id": {
|
||||
"type": "number",
|
||||
"example": 251
|
||||
}
|
||||
},
|
||||
"required": ["name", "email", "displayEmail", "timeZone", "absent", "id"]
|
||||
},
|
||||
"GetBookingAttendeesOutput_2024_08_13": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "string",
|
||||
"example": "success",
|
||||
"enum": ["success", "error"]
|
||||
},
|
||||
"data": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/BookingAttendeeWithId_2024_08_13"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["status", "data"]
|
||||
},
|
||||
"GetBookingAttendeeOutput_2024_08_13": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "string",
|
||||
"example": "success",
|
||||
"enum": ["success", "error"]
|
||||
},
|
||||
"data": {
|
||||
"$ref": "#/components/schemas/BookingAttendeeWithId_2024_08_13"
|
||||
}
|
||||
},
|
||||
"required": ["status", "data"]
|
||||
},
|
||||
"AddAttendeeInput_2024_08_13": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -493,11 +493,13 @@ export class BookingRepository implements IBookingRepository {
|
||||
},
|
||||
attendees: {
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
timeZone: true,
|
||||
locale: true,
|
||||
phoneNumber: true,
|
||||
noShow: true,
|
||||
},
|
||||
},
|
||||
user: {
|
||||
|
||||
@@ -3,9 +3,16 @@ import { makeUserActor } from "@calcom/features/booking-audit/lib/makeActor";
|
||||
import type { ActionSource } from "@calcom/features/booking-audit/lib/types/actionSource";
|
||||
import { getBookingEventHandlerService } from "@calcom/features/bookings/di/BookingEventHandlerService.container";
|
||||
import { BookingEmailSmsHandler } from "@calcom/features/bookings/lib/BookingEmailSmsHandler";
|
||||
import { BookingRepository } from "@calcom/features/bookings/repositories/BookingRepository";
|
||||
import { getFeaturesRepository } from "@calcom/features/di/containers/FeaturesRepository";
|
||||
import { ErrorCode } from "@calcom/lib/errorCodes";
|
||||
import { ErrorWithCode } from "@calcom/lib/errors";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import type { Booking, TUser } from "@calcom/trpc/server/routers/viewer/bookings/addGuests.handler";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import type {
|
||||
Booking,
|
||||
TUser,
|
||||
} from "@calcom/trpc/server/routers/viewer/bookings/addGuests.handler";
|
||||
import {
|
||||
buildCalendarEvent,
|
||||
getBooking,
|
||||
@@ -41,6 +48,42 @@ export type CreatedAttendee = {
|
||||
};
|
||||
|
||||
export class BookingAttendeesService {
|
||||
async getBookingAttendees(bookingUid: string) {
|
||||
const bookingRepository = new BookingRepository(prisma);
|
||||
const booking =
|
||||
await bookingRepository.findByUidIncludeEventTypeAttendeesAndUser({
|
||||
bookingUid,
|
||||
});
|
||||
|
||||
if (!booking) {
|
||||
throw new Error(`Booking with uid ${bookingUid} not found`);
|
||||
}
|
||||
|
||||
return booking.attendees;
|
||||
}
|
||||
|
||||
async getBookingAttendee(bookingUid: string, attendeeId: number) {
|
||||
const bookingRepository = new BookingRepository(prisma);
|
||||
const booking =
|
||||
await bookingRepository.findByUidIncludeEventTypeAttendeesAndUser({
|
||||
bookingUid,
|
||||
});
|
||||
|
||||
if (!booking) {
|
||||
throw new Error(`Booking with uid ${bookingUid} not found`);
|
||||
}
|
||||
|
||||
const attendee = booking.attendees.find((a) => a.id === attendeeId);
|
||||
if (!attendee) {
|
||||
throw new ErrorWithCode(
|
||||
ErrorCode.NotFound,
|
||||
`Attendee with id ${attendeeId} not found in booking ${bookingUid}`
|
||||
);
|
||||
}
|
||||
|
||||
return attendee;
|
||||
}
|
||||
|
||||
async addAttendee({
|
||||
bookingId,
|
||||
attendee,
|
||||
@@ -56,7 +99,10 @@ export class BookingAttendeesService {
|
||||
|
||||
const organizer = await getOrganizerData(booking.userId);
|
||||
|
||||
const validatedAttendees = await sanitizeAndFilterGuests([attendee], booking);
|
||||
const validatedAttendees = await sanitizeAndFilterGuests(
|
||||
[attendee],
|
||||
booking
|
||||
);
|
||||
|
||||
const newAttendeeDetails = validatedAttendees.map((a) => ({
|
||||
name: a.name || "",
|
||||
@@ -89,7 +135,10 @@ export class BookingAttendeesService {
|
||||
const featuresRepository = getFeaturesRepository();
|
||||
const organizationId = user.organizationId ?? null;
|
||||
const isBookingAuditEnabled = organizationId
|
||||
? await featuresRepository.checkIfTeamHasFeature(organizationId, "booking-audit")
|
||||
? await featuresRepository.checkIfTeamHasFeature(
|
||||
organizationId,
|
||||
"booking-audit"
|
||||
)
|
||||
: false;
|
||||
|
||||
await bookingEventHandlerService.onAttendeeAdded({
|
||||
@@ -134,7 +183,9 @@ export class BookingAttendeesService {
|
||||
await emailsAndSmsHandler.handleAddAttendee({
|
||||
evt,
|
||||
eventType: {
|
||||
metadata: eventTypeMetaDataSchemaWithTypedApps.parse(booking?.eventType?.metadata),
|
||||
metadata: eventTypeMetaDataSchemaWithTypedApps.parse(
|
||||
booking?.eventType?.metadata
|
||||
),
|
||||
schedulingType: booking.eventType?.schedulingType || null,
|
||||
},
|
||||
newGuests: [attendeeEmail],
|
||||
|
||||
@@ -291,11 +291,11 @@ class BaseBookingOutput_2024_08_13 {
|
||||
}
|
||||
|
||||
export class BookingOutput_2024_08_13 extends BaseBookingOutput_2024_08_13 {
|
||||
@ApiProperty({ type: [BookingAttendee] })
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => BookingAttendee)
|
||||
@Expose()
|
||||
attendees!: BookingAttendee[];
|
||||
@ApiProperty({ type: [BookingAttendee] })
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => BookingAttendee)
|
||||
@Expose()
|
||||
attendees!: BookingAttendee[];
|
||||
|
||||
@ApiPropertyOptional({
|
||||
type: [String],
|
||||
|
||||
Reference in new issue
Block a user