Revert "fix: v2 managed user attendee bookings (#19612)" (#19635)

This reverts commit c0591f3cdb.

Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
This commit is contained in:
Omar López
2025-03-01 03:01:45 +00:00
committed by GitHub
co-authored by Morgan
parent 5adbe4b570
commit 2600a6c3d5
12 changed files with 81 additions and 1165 deletions
@@ -1,39 +1,17 @@
import { BookingsController_2024_04_15 } from "@/ee/bookings/2024-04-15/controllers/bookings.controller";
import { PlatformBookingsService } from "@/ee/bookings/shared/platform-bookings.service";
import { EventTypesModule_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/event-types.module";
import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module";
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
import { ApiKeysRepository } from "@/modules/api-keys/api-keys-repository";
import { BillingModule } from "@/modules/billing/billing.module";
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-clients-users.service";
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { RedisModule } from "@/modules/redis/redis.module";
import { TokensModule } from "@/modules/tokens/tokens.module";
import { TokensRepository } from "@/modules/tokens/tokens.repository";
import { UsersModule } from "@/modules/users/users.module";
import { Module } from "@nestjs/common";
@Module({
imports: [
PrismaModule,
RedisModule,
TokensModule,
BillingModule,
UsersModule,
EventTypesModule_2024_04_15,
SchedulesModule_2024_04_15,
EventTypesModule_2024_06_14,
],
providers: [
TokensRepository,
OAuthFlowService,
OAuthClientRepository,
ApiKeysRepository,
OAuthClientUsersService,
PlatformBookingsService,
],
imports: [PrismaModule, RedisModule, TokensModule, BillingModule],
providers: [TokensRepository, OAuthFlowService, OAuthClientRepository, ApiKeysRepository],
controllers: [BookingsController_2024_04_15],
})
export class BookingsModule_2024_04_15 {}
@@ -4,7 +4,6 @@ import { MarkNoShowInput_2024_04_15 } from "@/ee/bookings/2024-04-15/inputs/mark
import { GetBookingOutput_2024_04_15 } from "@/ee/bookings/2024-04-15/outputs/get-booking.output";
import { GetBookingsOutput_2024_04_15 } from "@/ee/bookings/2024-04-15/outputs/get-bookings.output";
import { MarkNoShowOutput_2024_04_15 } from "@/ee/bookings/2024-04-15/outputs/mark-no-show.output";
import { PlatformBookingsService } from "@/ee/bookings/shared/platform-bookings.service";
import { hashAPIKey, isApiKey, stripApiKey } from "@/lib/api-key";
import { VERSION_2024_04_15, VERSION_2024_06_11, VERSION_2024_06_14 } from "@/lib/api-versions";
import { ApiKeysRepository } from "@/modules/api-keys/api-keys-repository";
@@ -100,8 +99,7 @@ export class BookingsController_2024_04_15 {
private readonly oAuthClientRepository: OAuthClientRepository,
private readonly billingService: BillingService,
private readonly config: ConfigService,
private readonly apiKeyRepository: ApiKeysRepository,
private readonly platformBookingsService: PlatformBookingsService
private readonly apiKeyRepository: ApiKeysRepository
) {}
@Get("/")
@@ -168,8 +166,7 @@ export class BookingsController_2024_04_15 {
@Headers(X_CAL_CLIENT_ID) clientId?: string,
@Headers(X_CAL_PLATFORM_EMBED) isEmbed?: string
): Promise<ApiResponse<Partial<BookingResponse>>> {
const oAuthClientId =
clientId?.toString() || (await this.getOAuthClientIdFromEventType(body.eventTypeId));
const oAuthClientId = clientId?.toString();
const { orgSlug, locationUrl } = body;
req.headers["x-cal-force-slug"] = orgSlug;
try {
@@ -197,7 +194,7 @@ export class BookingsController_2024_04_15 {
async cancelBooking(
@Req() req: BookingRequest,
@Param("bookingUid") bookingUid: string,
@Body() body: CancelBookingInput_2024_04_15,
@Body() _: CancelBookingInput_2024_04_15,
@Headers(X_CAL_CLIENT_ID) clientId?: string,
@Headers(X_CAL_PLATFORM_EMBED) isEmbed?: string
): Promise<ApiResponse<{ bookingId: number; bookingUid: string; onlyRemovedAttendee: boolean }>> {
@@ -260,12 +257,11 @@ export class BookingsController_2024_04_15 {
@Post("/recurring")
async createRecurringBooking(
@Req() req: BookingRequest,
@Body() body: CreateRecurringBookingInput_2024_04_15[],
@Body() _: CreateRecurringBookingInput_2024_04_15[],
@Headers(X_CAL_CLIENT_ID) clientId?: string,
@Headers(X_CAL_PLATFORM_EMBED) isEmbed?: string
): Promise<ApiResponse<BookingResponse[]>> {
const oAuthClientId =
clientId?.toString() || (await this.getOAuthClientIdFromEventType(body[0]?.eventTypeId));
const oAuthClientId = clientId?.toString();
try {
const recurringEventId = uuidv4();
for (const recurringEvent of req.body) {
@@ -300,12 +296,11 @@ export class BookingsController_2024_04_15 {
@Post("/instant")
async createInstantBooking(
@Req() req: BookingRequest,
@Body() body: CreateBookingInput_2024_04_15,
@Body() _: CreateBookingInput_2024_04_15,
@Headers(X_CAL_CLIENT_ID) clientId?: string,
@Headers(X_CAL_PLATFORM_EMBED) isEmbed?: string
): Promise<ApiResponse<Awaited<ReturnType<typeof handleInstantMeeting>>>> {
const oAuthClientId =
clientId?.toString() || (await this.getOAuthClientIdFromEventType(body.eventTypeId));
const oAuthClientId = clientId?.toString();
req.userId = (await this.getOwnerId(req)) ?? -1;
try {
const instantMeeting = await handleInstantMeeting(
@@ -351,17 +346,6 @@ export class BookingsController_2024_04_15 {
}
}
private async getOAuthClientIdFromEventType(eventTypeId: number): Promise<string | undefined> {
if (!eventTypeId) {
return undefined;
}
const oAuthClientParams = await this.platformBookingsService.getOAuthClientParams(eventTypeId);
if (!oAuthClientParams) {
return undefined;
}
return oAuthClientParams.platformClientId;
}
private async getOAuthClientsParams(clientId: string, isEmbed = false): Promise<OAuthRequestParams> {
const res = { ...DEFAULT_PLATFORM_PARAMS };
@@ -412,27 +396,9 @@ export class BookingsController_2024_04_15 {
noEmail: !oAuthParams.arePlatformEmailsEnabled,
creationSource: CreationSource.API_V2,
};
if (oAuthClientId) {
await this.setPlatformAttendeesEmails(clone.body, oAuthClientId);
}
return clone as unknown as NextApiRequest & { userId?: number } & OAuthRequestParams;
}
async setPlatformAttendeesEmails(requestBody: any, oAuthClientId: string): Promise<void> {
if (requestBody?.responses?.email) {
requestBody.responses.email = await this.platformBookingsService.getPlatformAttendeeEmail(
requestBody.responses.email,
oAuthClientId
);
}
if (requestBody?.responses?.guests) {
requestBody.responses.guests = await this.platformBookingsService.getPlatformAttendeesEmails(
requestBody.responses.guests,
oAuthClientId
);
}
}
private async createNextApiRecurringBookingRequest(
req: BookingRequest,
oAuthClientId?: string,
@@ -459,9 +425,6 @@ export class BookingsController_2024_04_15 {
noEmail: !oAuthParams.arePlatformEmailsEnabled,
creationSource: CreationSource.API_V2,
});
if (oAuthClientId) {
await this.setPlatformAttendeesEmails(clone.body, oAuthClientId);
}
return clone as unknown as NextApiRequest & { userId?: number } & OAuthRequestParams;
}
@@ -1,461 +0,0 @@
import { bootstrap } from "@/app";
import { AppModule } from "@/app.module";
import { CreateBookingInput_2024_04_15 } from "@/ee/bookings/2024-04-15/inputs/create-booking.input";
import {
GetBookingsDataEntry,
GetBookingsOutput_2024_04_15,
} from "@/ee/bookings/2024-04-15/outputs/get-bookings.output";
import { HttpExceptionFilter } from "@/filters/http-exception.filter";
import { PrismaExceptionFilter } from "@/filters/prisma-exception.filter";
import { Locales } from "@/lib/enums/locales";
import {
CreateManagedUserData,
CreateManagedUserOutput,
} from "@/modules/oauth-clients/controllers/oauth-client-users/outputs/create-managed-user.output";
import { CreateManagedUserInput } from "@/modules/users/inputs/create-managed-user.input";
import { UsersModule } from "@/modules/users/users.module";
import { INestApplication } from "@nestjs/common";
import { NestExpressApplication } from "@nestjs/platform-express";
import { Test } from "@nestjs/testing";
import { PlatformOAuthClient, Team, User } from "@prisma/client";
import * as request from "supertest";
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { randomString } from "test/utils/randomString";
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_06_14 } from "@calcom/platform-constants";
import {
ApiSuccessResponse,
CreateEventTypeInput_2024_06_14,
EventTypeOutput_2024_06_14,
} from "@calcom/platform-types";
const CLIENT_REDIRECT_URI = "http://localhost:4321";
describe("Managed user bookings 2024-04-15", () => {
let app: INestApplication;
let oAuthClient: PlatformOAuthClient;
let organization: Team;
let userRepositoryFixture: UserRepositoryFixture;
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
let teamRepositoryFixture: TeamRepositoryFixture;
let profilesRepositoryFixture: ProfileRepositoryFixture;
let membershipsRepositoryFixture: MembershipRepositoryFixture;
const platformAdminEmail = `managed-users-bookings-admin-${randomString()}@api.com`;
let platformAdmin: User;
const managedUsersTimeZone = "Europe/Rome";
const firstManagedUserEmail = `managed-user-bookings-2024-04-15-first-user@api.com`;
const secondManagedUserEmail = `managed-user-bookings-2024-04-15-second-user@api.com`;
const thirdManagedUserEmail = `managed-user-bookings-2024-04-15-third-user@api.com`;
let firstManagedUser: CreateManagedUserData;
let secondManagedUser: CreateManagedUserData;
let thirdManagedUser: CreateManagedUserData;
let firstManagedUserEventTypeId: number;
let firstManagedUserBookingsCount = 0;
let secondManagedUserBookingsCount = 0;
let thirdManagedUserBookingsCount = 0;
beforeAll(async () => {
const moduleRef = await Test.createTestingModule({
providers: [PrismaExceptionFilter, HttpExceptionFilter],
imports: [AppModule, UsersModule],
}).compile();
app = moduleRef.createNestApplication();
bootstrap(app as NestExpressApplication);
oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef);
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
profilesRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
platformAdmin = await userRepositoryFixture.create({ email: platformAdminEmail });
organization = await teamRepositoryFixture.create({
name: `oauth-client-users-organization-${randomString()}`,
isPlatform: true,
isOrganization: true,
});
oAuthClient = await createOAuthClient(organization.id);
await profilesRepositoryFixture.create({
uid: "asd1qwwqeqw-asddsadasd",
username: platformAdminEmail,
organization: { connect: { id: organization.id } },
user: {
connect: { id: platformAdmin.id },
},
});
await membershipsRepositoryFixture.create({
role: "OWNER",
user: { connect: { id: platformAdmin.id } },
team: { connect: { id: organization.id } },
accepted: true,
});
await app.init();
});
async function createOAuthClient(organizationId: number) {
const data = {
logo: "logo-url",
name: "name",
redirectUris: [CLIENT_REDIRECT_URI],
permissions: 1023,
};
const secret = "secret";
const client = await oauthClientRepositoryFixture.create(organizationId, data, secret);
return client;
}
it(`should create first managed user`, async () => {
const requestBody: CreateManagedUserInput = {
email: firstManagedUserEmail,
timeZone: managedUsersTimeZone,
weekStart: "Monday",
timeFormat: 24,
locale: Locales.FR,
name: "Alice Smith",
avatarUrl: "https://cal.com/api/avatar/2b735186-b01b-46d3-87da-019b8f61776b.png",
};
const response = await request(app.getHttpServer())
.post(`/api/v2/oauth-clients/${oAuthClient.id}/users`)
.set("x-cal-secret-key", oAuthClient.secret)
.send(requestBody)
.expect(201);
const responseBody: CreateManagedUserOutput = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseBody.data.user.email).toEqual(getOAuthUserEmail(oAuthClient.id, requestBody.email));
expect(responseBody.data.accessToken).toBeDefined();
expect(responseBody.data.refreshToken).toBeDefined();
firstManagedUser = responseBody.data;
});
it("should create an event type for managed user", async () => {
const body: CreateEventTypeInput_2024_06_14 = {
title: "Managed user bookings first managed user event type",
slug: "managed-user-bookings-first-managed-user-event-type",
description: "Managed user bookings first managed user event type description",
lengthInMinutes: 30,
};
return request(app.getHttpServer())
.post("/api/v2/event-types")
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.set("Authorization", `Bearer ${firstManagedUser.accessToken}`)
.send(body)
.expect(201)
.then(async (response) => {
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
const createdEventType = responseBody.data;
expect(createdEventType).toHaveProperty("id");
expect(createdEventType.title).toEqual(body.title);
expect(createdEventType.description).toEqual(body.description);
expect(createdEventType.lengthInMinutes).toEqual(body.lengthInMinutes);
expect(createdEventType.ownerId).toEqual(firstManagedUser.user.id);
firstManagedUserEventTypeId = createdEventType.id;
});
});
it(`should create second managed user`, async () => {
const requestBody: CreateManagedUserInput = {
email: secondManagedUserEmail,
timeZone: managedUsersTimeZone,
weekStart: "Monday",
timeFormat: 24,
locale: Locales.FR,
name: "Bob Smith",
avatarUrl: "https://cal.com/api/avatar/2b735186-b01b-46d3-87da-019b8f61776b.png",
};
const response = await request(app.getHttpServer())
.post(`/api/v2/oauth-clients/${oAuthClient.id}/users`)
.set("x-cal-secret-key", oAuthClient.secret)
.send(requestBody)
.expect(201);
const responseBody: CreateManagedUserOutput = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseBody.data.user.email).toEqual(getOAuthUserEmail(oAuthClient.id, requestBody.email));
expect(responseBody.data.accessToken).toBeDefined();
expect(responseBody.data.refreshToken).toBeDefined();
secondManagedUser = responseBody.data;
});
it(`should create third managed user`, async () => {
const requestBody: CreateManagedUserInput = {
email: thirdManagedUserEmail,
timeZone: managedUsersTimeZone,
weekStart: "Monday",
timeFormat: 24,
locale: Locales.FR,
name: "Charlie Smith",
avatarUrl: "https://cal.com/api/avatar/2b735186-b01b-46d3-87da-019b8f61776b.png",
};
const response = await request(app.getHttpServer())
.post(`/api/v2/oauth-clients/${oAuthClient.id}/users`)
.set("x-cal-secret-key", oAuthClient.secret)
.send(requestBody)
.expect(201);
const responseBody: CreateManagedUserOutput = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseBody.data.user.email).toEqual(getOAuthUserEmail(oAuthClient.id, requestBody.email));
expect(responseBody.data.accessToken).toBeDefined();
expect(responseBody.data.refreshToken).toBeDefined();
thirdManagedUser = responseBody.data;
});
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_04_15 = {
start: "2040-05-21T09:30:00.000Z",
end: "2040-05-21T10:00:00.000Z",
eventTypeId: firstManagedUserEventTypeId,
timeZone: "Europe/London",
language: "en",
metadata: {},
hashedLink: "",
responses: {
name: secondManagedUser.user.name || "unknown-name",
email: secondManagedUserEmail,
location: {
value: "attendeeInPerson",
optionValue: "rome",
},
notes: "test",
guests: [],
},
};
await request(app.getHttpServer()).post("/v2/bookings").send(body).expect(201);
firstManagedUserBookingsCount += 1;
secondManagedUserBookingsCount += 1;
const getFirstManagedUserBookings = await request(app.getHttpServer())
.get("/v2/bookings")
.set("Authorization", `Bearer ${firstManagedUser.accessToken}`)
.expect(200);
const firstManagedUserBookingsResponseBody: GetBookingsOutput_2024_04_15 =
getFirstManagedUserBookings.body;
const firstManagedUserBookings: GetBookingsDataEntry[] =
firstManagedUserBookingsResponseBody.data.bookings;
expect(firstManagedUserBookings.length).toEqual(firstManagedUserBookingsCount);
const getSecondManagedUserBookings = await request(app.getHttpServer())
.get("/v2/bookings")
.set("Authorization", `Bearer ${secondManagedUser.accessToken}`)
.expect(200);
const secondManagedUserBookingsResponseBody: GetBookingsOutput_2024_04_15 =
getSecondManagedUserBookings.body;
const secondManagedUserBookings: GetBookingsDataEntry[] =
secondManagedUserBookingsResponseBody.data.bookings;
expect(secondManagedUserBookings.length).toEqual(secondManagedUserBookingsCount);
});
it("managed user should be booked by managed user attendee and managed user as a guest and booking shows up in all three users' bookings", async () => {
const body: CreateBookingInput_2024_04_15 = {
start: "2040-05-21T10:30:00.000Z",
end: "2040-05-21T11:00:00.000Z",
eventTypeId: firstManagedUserEventTypeId,
timeZone: "Europe/London",
language: "en",
metadata: {},
hashedLink: "",
responses: {
name: thirdManagedUser.user.name || "unknown-name",
email: thirdManagedUserEmail,
location: {
value: "attendeeInPerson",
optionValue: "rome",
},
notes: "test",
guests: [secondManagedUserEmail],
},
};
await request(app.getHttpServer()).post("/v2/bookings").send(body).expect(201);
firstManagedUserBookingsCount += 1;
secondManagedUserBookingsCount += 1;
thirdManagedUserBookingsCount += 1;
const getFirstManagedUserBookings = await request(app.getHttpServer())
.get("/v2/bookings")
.set("Authorization", `Bearer ${firstManagedUser.accessToken}`)
.expect(200);
const firstManagedUserBookingsResponseBody: GetBookingsOutput_2024_04_15 =
getFirstManagedUserBookings.body;
const firstManagedUserBookings: GetBookingsDataEntry[] =
firstManagedUserBookingsResponseBody.data.bookings;
expect(firstManagedUserBookings.length).toEqual(firstManagedUserBookingsCount);
const getSecondManagedUserBookings = await request(app.getHttpServer())
.get("/v2/bookings")
.set("Authorization", `Bearer ${secondManagedUser.accessToken}`)
.expect(200);
const secondManagedUserBookingsResponseBody: GetBookingsOutput_2024_04_15 =
getSecondManagedUserBookings.body;
const secondManagedUserBookings: GetBookingsDataEntry[] =
secondManagedUserBookingsResponseBody.data.bookings;
expect(secondManagedUserBookings.length).toEqual(secondManagedUserBookingsCount);
const getThirdManagedUserBookings = await request(app.getHttpServer())
.get("/v2/bookings")
.set("Authorization", `Bearer ${thirdManagedUser.accessToken}`)
.expect(200);
const thirdManagedUserBookingsResponseBody: GetBookingsOutput_2024_04_15 =
getThirdManagedUserBookings.body;
const thirdManagedUserBookings: GetBookingsDataEntry[] =
thirdManagedUserBookingsResponseBody.data.bookings;
expect(thirdManagedUserBookings.length).toEqual(thirdManagedUserBookingsCount);
});
});
describe("bookings using OAuth client emails", () => {
it("managed user should be booked by managed user attendee and booking shows up in both users' bookings", async () => {
const body: CreateBookingInput_2024_04_15 = {
start: "2040-05-21T12:00:00.000Z",
end: "2040-05-21T12:30:00.000Z",
eventTypeId: firstManagedUserEventTypeId,
timeZone: "Europe/London",
language: "en",
metadata: {},
hashedLink: "",
responses: {
name: secondManagedUser.user.name || "unknown-name",
email: secondManagedUser.user.email,
location: {
value: "attendeeInPerson",
optionValue: "rome",
},
notes: "test",
guests: [],
},
};
await request(app.getHttpServer()).post("/v2/bookings").send(body).expect(201);
firstManagedUserBookingsCount += 1;
secondManagedUserBookingsCount += 1;
const getFirstManagedUserBookings = await request(app.getHttpServer())
.get("/v2/bookings")
.set("Authorization", `Bearer ${firstManagedUser.accessToken}`)
.expect(200);
const firstManagedUserBookingsResponseBody: GetBookingsOutput_2024_04_15 =
getFirstManagedUserBookings.body;
const firstManagedUserBookings: GetBookingsDataEntry[] =
firstManagedUserBookingsResponseBody.data.bookings;
expect(firstManagedUserBookings.length).toEqual(firstManagedUserBookingsCount);
const getSecondManagedUserBookings = await request(app.getHttpServer())
.get("/v2/bookings")
.set("Authorization", `Bearer ${secondManagedUser.accessToken}`)
.expect(200);
const secondManagedUserBookingsResponseBody: GetBookingsOutput_2024_04_15 =
getSecondManagedUserBookings.body;
const secondManagedUserBookings: GetBookingsDataEntry[] =
secondManagedUserBookingsResponseBody.data.bookings;
expect(secondManagedUserBookings.length).toEqual(secondManagedUserBookingsCount);
});
it("managed user should be booked by managed user attendee and managed user as a guest and booking shows up in all three users' bookings", async () => {
const body: CreateBookingInput_2024_04_15 = {
start: "2040-05-21T13:00:00.000Z",
end: "2040-05-21T13:30:00.000Z",
eventTypeId: firstManagedUserEventTypeId,
timeZone: "Europe/London",
language: "en",
metadata: {},
hashedLink: "",
responses: {
name: thirdManagedUser.user.name || "unknown-name",
email: thirdManagedUser.user.email,
location: {
value: "attendeeInPerson",
optionValue: "rome",
},
notes: "test",
guests: [secondManagedUser.user.email],
},
};
await request(app.getHttpServer()).post("/v2/bookings").send(body).expect(201);
firstManagedUserBookingsCount += 1;
secondManagedUserBookingsCount += 1;
thirdManagedUserBookingsCount += 1;
const getFirstManagedUserBookings = await request(app.getHttpServer())
.get("/v2/bookings")
.set("Authorization", `Bearer ${firstManagedUser.accessToken}`)
.expect(200);
const firstManagedUserBookingsResponseBody: GetBookingsOutput_2024_04_15 =
getFirstManagedUserBookings.body;
const firstManagedUserBookings: GetBookingsDataEntry[] =
firstManagedUserBookingsResponseBody.data.bookings;
expect(firstManagedUserBookings.length).toEqual(firstManagedUserBookingsCount);
const getSecondManagedUserBookings = await request(app.getHttpServer())
.get("/v2/bookings")
.set("Authorization", `Bearer ${secondManagedUser.accessToken}`)
.expect(200);
const secondManagedUserBookingsResponseBody: GetBookingsOutput_2024_04_15 =
getSecondManagedUserBookings.body;
const secondManagedUserBookings: GetBookingsDataEntry[] =
secondManagedUserBookingsResponseBody.data.bookings;
expect(secondManagedUserBookings.length).toEqual(secondManagedUserBookingsCount);
const getThirdManagedUserBookings = await request(app.getHttpServer())
.get("/v2/bookings")
.set("Authorization", `Bearer ${thirdManagedUser.accessToken}`)
.expect(200);
const thirdManagedUserBookingsResponseBody: GetBookingsOutput_2024_04_15 =
getThirdManagedUserBookings.body;
const thirdManagedUserBookings: GetBookingsDataEntry[] =
thirdManagedUserBookingsResponseBody.data.bookings;
expect(thirdManagedUserBookings.length).toEqual(thirdManagedUserBookingsCount);
});
});
function getOAuthUserEmail(oAuthClientId: string, userEmail: string) {
const [username, emailDomain] = userEmail.split("@");
return `${username}+${oAuthClientId}@${emailDomain}`;
}
afterAll(async () => {
await userRepositoryFixture.delete(firstManagedUser.user.id);
await userRepositoryFixture.delete(secondManagedUser.user.id);
await userRepositoryFixture.delete(thirdManagedUser.user.id);
await userRepositoryFixture.delete(platformAdmin.id);
await oauthClientRepositoryFixture.delete(oAuthClient.id);
await teamRepositoryFixture.delete(organization.id);
await app.close();
});
});
@@ -8,9 +8,8 @@ import {
IsOptional,
IsArray,
IsObject,
IsEmail,
ValidateNested,
isEmail,
Validate,
} from "class-validator";
class Location {
@@ -28,9 +27,7 @@ class Response {
@ApiProperty()
name!: string;
@Validate((value: string) => !value || isEmail(value), {
message: "Invalid response email",
})
@IsEmail()
@ApiProperty()
email!: string;
@@ -116,9 +113,8 @@ export class CreateBookingInput_2024_04_15 {
@ApiPropertyOptional()
seatReferenceUid?: string;
@ApiProperty({ type: Response })
@ValidateNested()
@Type(() => Response)
@ApiProperty({ type: Response })
responses!: Response;
@IsString()
@@ -160,7 +160,7 @@ class User {
email!: string;
}
export class GetBookingsDataEntry {
class GetBookingsDataEntry {
@IsInt()
@ApiProperty()
id!: number;
@@ -3,17 +3,12 @@ import { BookingsController_2024_08_13 } from "@/ee/bookings/2024-08-13/controll
import { BookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/bookings.service";
import { InputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/input.service";
import { OutputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/output.service";
import { PlatformBookingsService } from "@/ee/bookings/shared/platform-bookings.service";
import { EventTypesModule_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/event-types.module";
import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module";
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
import { ApiKeysRepository } from "@/modules/api-keys/api-keys-repository";
import { BillingModule } from "@/modules/billing/billing.module";
import { BookingSeatModule } from "@/modules/booking-seat/booking-seat.module";
import { BookingSeatRepository } from "@/modules/booking-seat/booking-seat.repository";
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-clients-users.service";
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { RedisModule } from "@/modules/redis/redis.module";
@@ -23,22 +18,11 @@ import { UsersModule } from "@/modules/users/users.module";
import { Module } from "@nestjs/common";
@Module({
imports: [
PrismaModule,
RedisModule,
TokensModule,
BillingModule,
UsersModule,
BookingSeatModule,
SchedulesModule_2024_04_15,
EventTypesModule_2024_04_15,
EventTypesModule_2024_06_14,
],
imports: [PrismaModule, RedisModule, TokensModule, BillingModule, UsersModule, BookingSeatModule],
providers: [
TokensRepository,
OAuthFlowService,
OAuthClientRepository,
OAuthClientUsersService,
BookingsService_2024_08_13,
InputBookingsService_2024_08_13,
OutputBookingsService_2024_08_13,
@@ -46,7 +30,6 @@ import { Module } from "@nestjs/common";
EventTypesRepository_2024_06_14,
BookingSeatRepository,
ApiKeysRepository,
PlatformBookingsService,
],
controllers: [BookingsController_2024_08_13],
exports: [InputBookingsService_2024_08_13, OutputBookingsService_2024_08_13, BookingsService_2024_08_13],
@@ -1,487 +0,0 @@
import { bootstrap } from "@/app";
import { AppModule } from "@/app.module";
import { HttpExceptionFilter } from "@/filters/http-exception.filter";
import { PrismaExceptionFilter } from "@/filters/prisma-exception.filter";
import { Locales } from "@/lib/enums/locales";
import {
CreateManagedUserData,
CreateManagedUserOutput,
} from "@/modules/oauth-clients/controllers/oauth-client-users/outputs/create-managed-user.output";
import { CreateManagedUserInput } from "@/modules/users/inputs/create-managed-user.input";
import { UsersModule } from "@/modules/users/users.module";
import { INestApplication } from "@nestjs/common";
import { NestExpressApplication } from "@nestjs/platform-express";
import { Test } from "@nestjs/testing";
import { PlatformOAuthClient, Team, User } from "@prisma/client";
import * as request from "supertest";
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
import { SchedulesRepositoryFixture } from "test/fixtures/repository/schedules.repository.fixture";
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { randomString } from "test/utils/randomString";
import {
CAL_API_VERSION_HEADER,
SUCCESS_STATUS,
VERSION_2024_06_14,
VERSION_2024_08_13,
} from "@calcom/platform-constants";
import {
ApiSuccessResponse,
BookingOutput_2024_08_13,
CreateBookingInput_2024_08_13,
CreateEventTypeInput_2024_06_14,
EventTypeOutput_2024_06_14,
GetBookingsOutput_2024_08_13,
} from "@calcom/platform-types";
const CLIENT_REDIRECT_URI = "http://localhost:4321";
describe("Managed user bookings 2024-08-13", () => {
let app: INestApplication;
let oAuthClient: PlatformOAuthClient;
let organization: Team;
let userRepositoryFixture: UserRepositoryFixture;
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
let teamRepositoryFixture: TeamRepositoryFixture;
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
let schedulesRepositoryFixture: SchedulesRepositoryFixture;
let profilesRepositoryFixture: ProfileRepositoryFixture;
let membershipsRepositoryFixture: MembershipRepositoryFixture;
const platformAdminEmail = `managed-users-bookings-2024-08-13-admin-${randomString()}@api.com`;
let platformAdmin: User;
const managedUsersTimeZone = "Europe/Rome";
const firstManagedUserEmail = `managed-user-bookings-2024-08-13-first-user-${randomString()}@api.com`;
const secondManagedUserEmail = `managed-user-bookings-2024-08-13-second-user-${randomString()}@api.com`;
const thirdManagedUserEmail = `managed-user-bookings-2024-08-13-third-user-${randomString()}@api.com`;
let firstManagedUser: CreateManagedUserData;
let secondManagedUser: CreateManagedUserData;
let thirdManagedUser: CreateManagedUserData;
let firstManagedUserEventTypeId: number;
let firstManagedUserBookingsCount = 0;
let secondManagedUserBookingsCount = 0;
let thirdManagedUserBookingsCount = 0;
beforeAll(async () => {
const moduleRef = await Test.createTestingModule({
providers: [PrismaExceptionFilter, HttpExceptionFilter],
imports: [AppModule, UsersModule],
}).compile();
app = moduleRef.createNestApplication();
bootstrap(app as NestExpressApplication);
oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef);
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
schedulesRepositoryFixture = new SchedulesRepositoryFixture(moduleRef);
profilesRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
platformAdmin = await userRepositoryFixture.create({ email: platformAdminEmail });
organization = await teamRepositoryFixture.create({
name: `oauth-client-users-organization-${randomString()}`,
isPlatform: true,
isOrganization: true,
});
oAuthClient = await createOAuthClient(organization.id);
await profilesRepositoryFixture.create({
uid: "asd1qwwqeqw-asddsadasd",
username: platformAdminEmail,
organization: { connect: { id: organization.id } },
user: {
connect: { id: platformAdmin.id },
},
});
await membershipsRepositoryFixture.create({
role: "OWNER",
user: { connect: { id: platformAdmin.id } },
team: { connect: { id: organization.id } },
accepted: true,
});
await app.init();
});
async function createOAuthClient(organizationId: number) {
const data = {
logo: "logo-url",
name: "name",
redirectUris: [CLIENT_REDIRECT_URI],
permissions: 1023,
};
const secret = "secret";
const client = await oauthClientRepositoryFixture.create(organizationId, data, secret);
return client;
}
it(`should create first managed user`, async () => {
const requestBody: CreateManagedUserInput = {
email: firstManagedUserEmail,
timeZone: managedUsersTimeZone,
weekStart: "Monday",
timeFormat: 24,
locale: Locales.FR,
name: "Alice Smith",
avatarUrl: "https://cal.com/api/avatar/2b735186-b01b-46d3-87da-019b8f61776b.png",
};
const response = await request(app.getHttpServer())
.post(`/api/v2/oauth-clients/${oAuthClient.id}/users`)
.set("x-cal-secret-key", oAuthClient.secret)
.send(requestBody)
.expect(201);
const responseBody: CreateManagedUserOutput = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseBody.data.user.email).toEqual(getOAuthUserEmail(oAuthClient.id, requestBody.email));
expect(responseBody.data.accessToken).toBeDefined();
expect(responseBody.data.refreshToken).toBeDefined();
firstManagedUser = responseBody.data;
});
it("should create an event type for managed user", async () => {
const body: CreateEventTypeInput_2024_06_14 = {
title: "Managed user bookings first managed user event type",
slug: "managed-user-bookings-first-managed-user-event-type",
description: "Managed user bookings first managed user event type description",
lengthInMinutes: 30,
};
return request(app.getHttpServer())
.post("/api/v2/event-types")
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.set("Authorization", `Bearer ${firstManagedUser.accessToken}`)
.send(body)
.expect(201)
.then(async (response) => {
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
const createdEventType = responseBody.data;
expect(createdEventType).toHaveProperty("id");
expect(createdEventType.title).toEqual(body.title);
expect(createdEventType.description).toEqual(body.description);
expect(createdEventType.lengthInMinutes).toEqual(body.lengthInMinutes);
expect(createdEventType.ownerId).toEqual(firstManagedUser.user.id);
firstManagedUserEventTypeId = createdEventType.id;
});
});
it(`should create second managed user`, async () => {
const requestBody: CreateManagedUserInput = {
email: secondManagedUserEmail,
timeZone: managedUsersTimeZone,
weekStart: "Monday",
timeFormat: 24,
locale: Locales.FR,
name: "Bob Smith",
avatarUrl: "https://cal.com/api/avatar/2b735186-b01b-46d3-87da-019b8f61776b.png",
};
const response = await request(app.getHttpServer())
.post(`/api/v2/oauth-clients/${oAuthClient.id}/users`)
.set("x-cal-secret-key", oAuthClient.secret)
.send(requestBody)
.expect(201);
const responseBody: CreateManagedUserOutput = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseBody.data.user.email).toEqual(getOAuthUserEmail(oAuthClient.id, requestBody.email));
expect(responseBody.data.accessToken).toBeDefined();
expect(responseBody.data.refreshToken).toBeDefined();
secondManagedUser = responseBody.data;
});
it(`should create third managed user`, async () => {
const requestBody: CreateManagedUserInput = {
email: thirdManagedUserEmail,
timeZone: managedUsersTimeZone,
weekStart: "Monday",
timeFormat: 24,
locale: Locales.FR,
name: "Charlie Smith",
avatarUrl: "https://cal.com/api/avatar/2b735186-b01b-46d3-87da-019b8f61776b.png",
};
const response = await request(app.getHttpServer())
.post(`/api/v2/oauth-clients/${oAuthClient.id}/users`)
.set("x-cal-secret-key", oAuthClient.secret)
.send(requestBody)
.expect(201);
const responseBody: CreateManagedUserOutput = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseBody.data.user.email).toEqual(getOAuthUserEmail(oAuthClient.id, requestBody.email));
expect(responseBody.data.accessToken).toBeDefined();
expect(responseBody.data.refreshToken).toBeDefined();
thirdManagedUser = responseBody.data;
});
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 = {
start: new Date(Date.UTC(2030, 0, 8, 13, 0, 0)).toISOString(),
eventTypeId: firstManagedUserEventTypeId,
attendee: {
name: secondManagedUser.user.name!,
email: secondManagedUserEmail,
timeZone: secondManagedUser.user.timeZone,
language: secondManagedUser.user.locale,
},
location: "https://meet.google.com/abc-def-ghi",
};
await request(app.getHttpServer())
.post("/v2/bookings")
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201);
firstManagedUserBookingsCount += 1;
secondManagedUserBookingsCount += 1;
const getFirstManagedUserBookings = await request(app.getHttpServer())
.get("/v2/bookings")
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.set("Authorization", `Bearer ${firstManagedUser.accessToken}`)
.expect(200);
const firstManagedUserBookingsResponseBody: GetBookingsOutput_2024_08_13 =
getFirstManagedUserBookings.body;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const firstManagedUserBookings: BookingOutput_2024_08_13[] = firstManagedUserBookingsResponseBody.data;
expect(firstManagedUserBookings.length).toEqual(firstManagedUserBookingsCount);
const getSecondManagedUserBookings = await request(app.getHttpServer())
.get("/v2/bookings")
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.set("Authorization", `Bearer ${secondManagedUser.accessToken}`)
.expect(200);
const secondManagedUserBookingsResponseBody: GetBookingsOutput_2024_08_13 =
getSecondManagedUserBookings.body;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const secondManagedUserBookings: BookingOutput_2024_08_13[] =
secondManagedUserBookingsResponseBody.data;
expect(secondManagedUserBookings.length).toEqual(secondManagedUserBookingsCount);
});
it("managed user should be booked by managed user attendee and managed user as a guest and booking shows up in all three users' bookings", async () => {
const body: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2030, 0, 8, 14, 0, 0)).toISOString(),
eventTypeId: firstManagedUserEventTypeId,
attendee: {
name: thirdManagedUser.user.name!,
email: thirdManagedUserEmail,
timeZone: secondManagedUser.user.timeZone,
language: secondManagedUser.user.locale,
},
guests: [secondManagedUserEmail],
location: "https://meet.google.com/abc-def-ghi",
};
await request(app.getHttpServer())
.post("/v2/bookings")
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201);
firstManagedUserBookingsCount += 1;
secondManagedUserBookingsCount += 1;
thirdManagedUserBookingsCount += 1;
const getFirstManagedUserBookings = await request(app.getHttpServer())
.get("/v2/bookings")
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.set("Authorization", `Bearer ${firstManagedUser.accessToken}`)
.expect(200);
const firstManagedUserBookingsResponseBody: GetBookingsOutput_2024_08_13 =
getFirstManagedUserBookings.body;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const firstManagedUserBookings: BookingOutput_2024_08_13[] = firstManagedUserBookingsResponseBody.data;
expect(firstManagedUserBookings.length).toEqual(firstManagedUserBookingsCount);
const getSecondManagedUserBookings = await request(app.getHttpServer())
.get("/v2/bookings")
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.set("Authorization", `Bearer ${secondManagedUser.accessToken}`)
.expect(200);
const secondManagedUserBookingsResponseBody: GetBookingsOutput_2024_08_13 =
getSecondManagedUserBookings.body;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const secondManagedUserBookings: BookingOutput_2024_08_13[] =
secondManagedUserBookingsResponseBody.data;
expect(secondManagedUserBookings.length).toEqual(secondManagedUserBookingsCount);
const getThirdManagedUserBookings = await request(app.getHttpServer())
.get("/v2/bookings")
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.set("Authorization", `Bearer ${thirdManagedUser.accessToken}`)
.expect(200);
const thirdManagedUserBookingsResponseBody: GetBookingsOutput_2024_08_13 =
getThirdManagedUserBookings.body;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const thirdManagedUserBookings: BookingOutput_2024_08_13[] = thirdManagedUserBookingsResponseBody.data;
expect(thirdManagedUserBookings.length).toEqual(thirdManagedUserBookingsCount);
});
});
describe("bookings using OAuth client 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 = {
start: new Date(Date.UTC(2030, 0, 8, 15, 0, 0)).toISOString(),
eventTypeId: firstManagedUserEventTypeId,
attendee: {
name: secondManagedUser.user.name!,
email: secondManagedUser.user.email,
timeZone: secondManagedUser.user.timeZone,
language: secondManagedUser.user.locale,
},
location: "https://meet.google.com/abc-def-ghi",
};
await request(app.getHttpServer())
.post("/v2/bookings")
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201);
firstManagedUserBookingsCount += 1;
secondManagedUserBookingsCount += 1;
const getFirstManagedUserBookings = await request(app.getHttpServer())
.get("/v2/bookings")
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.set("Authorization", `Bearer ${firstManagedUser.accessToken}`)
.expect(200);
const firstManagedUserBookingsResponseBody: GetBookingsOutput_2024_08_13 =
getFirstManagedUserBookings.body;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const firstManagedUserBookings: BookingOutput_2024_08_13[] = firstManagedUserBookingsResponseBody.data;
expect(firstManagedUserBookings.length).toEqual(firstManagedUserBookingsCount);
const getSecondManagedUserBookings = await request(app.getHttpServer())
.get("/v2/bookings")
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.set("Authorization", `Bearer ${secondManagedUser.accessToken}`)
.expect(200);
const secondManagedUserBookingsResponseBody: GetBookingsOutput_2024_08_13 =
getSecondManagedUserBookings.body;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const secondManagedUserBookings: BookingOutput_2024_08_13[] =
secondManagedUserBookingsResponseBody.data;
expect(secondManagedUserBookings.length).toEqual(secondManagedUserBookingsCount);
});
it("managed user should be booked by managed user attendee and managed user as a guest and booking shows up in all three users' bookings", async () => {
const body: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2030, 0, 8, 15, 30, 0)).toISOString(),
eventTypeId: firstManagedUserEventTypeId,
attendee: {
name: thirdManagedUser.user.name!,
email: thirdManagedUser.user.email,
timeZone: secondManagedUser.user.timeZone,
language: secondManagedUser.user.locale,
},
guests: [secondManagedUser.user.email],
location: "https://meet.google.com/abc-def-ghi",
};
await request(app.getHttpServer())
.post("/v2/bookings")
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201);
firstManagedUserBookingsCount += 1;
secondManagedUserBookingsCount += 1;
thirdManagedUserBookingsCount += 1;
const getFirstManagedUserBookings = await request(app.getHttpServer())
.get("/v2/bookings")
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.set("Authorization", `Bearer ${firstManagedUser.accessToken}`)
.expect(200);
const firstManagedUserBookingsResponseBody: GetBookingsOutput_2024_08_13 =
getFirstManagedUserBookings.body;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const firstManagedUserBookings: BookingOutput_2024_08_13[] = firstManagedUserBookingsResponseBody.data;
expect(firstManagedUserBookings.length).toEqual(firstManagedUserBookingsCount);
const getSecondManagedUserBookings = await request(app.getHttpServer())
.get("/v2/bookings")
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.set("Authorization", `Bearer ${secondManagedUser.accessToken}`)
.expect(200);
const secondManagedUserBookingsResponseBody: GetBookingsOutput_2024_08_13 =
getSecondManagedUserBookings.body;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const secondManagedUserBookings: BookingOutput_2024_08_13[] =
secondManagedUserBookingsResponseBody.data;
expect(secondManagedUserBookings.length).toEqual(secondManagedUserBookingsCount);
const getThirdManagedUserBookings = await request(app.getHttpServer())
.get("/v2/bookings")
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.set("Authorization", `Bearer ${thirdManagedUser.accessToken}`)
.expect(200);
const thirdManagedUserBookingsResponseBody: GetBookingsOutput_2024_08_13 =
getThirdManagedUserBookings.body;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const thirdManagedUserBookings: BookingOutput_2024_08_13[] = thirdManagedUserBookingsResponseBody.data;
expect(thirdManagedUserBookings.length).toEqual(thirdManagedUserBookingsCount);
});
});
function getOAuthUserEmail(oAuthClientId: string, userEmail: string) {
const [username, emailDomain] = userEmail.split("@");
return `${username}+${oAuthClientId}@${emailDomain}`;
}
afterAll(async () => {
await userRepositoryFixture.delete(firstManagedUser.user.id);
await userRepositoryFixture.delete(secondManagedUser.user.id);
await userRepositoryFixture.delete(thirdManagedUser.user.id);
await userRepositoryFixture.delete(platformAdmin.id);
await oauthClientRepositoryFixture.delete(oAuthClient.id);
await teamRepositoryFixture.delete(organization.id);
await app.close();
});
});
@@ -1,7 +1,6 @@
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.repository";
import { InputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/input.service";
import { OutputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/output.service";
import { PlatformBookingsService } from "@/ee/bookings/shared/platform-bookings.service";
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
import { BillingService } from "@/modules/billing/services/billing.service";
import { BookingSeatRepository } from "@/modules/booking-seat/booking-seat.repository";
@@ -68,8 +67,7 @@ export class BookingsService_2024_08_13 {
private readonly prismaReadService: PrismaReadService,
private readonly billingService: BillingService,
private readonly usersService: UsersService,
private readonly usersRepository: UsersRepository,
private readonly platformBookingsService: PlatformBookingsService
private readonly usersRepository: UsersRepository
) {}
async createBooking(request: Request, body: CreateBookingInput) {
@@ -370,7 +368,7 @@ export class BookingsService_2024_08_13 {
const bodyTransformed = this.inputService.transformInputMarkAbsentBooking(body);
const bookingBefore = await this.bookingsRepository.getByUid(bookingUid);
const platformClientParams = bookingBefore?.eventTypeId
? await this.platformBookingsService.getOAuthClientParams(bookingBefore.eventTypeId)
? await this.inputService.getOAuthClientParams(bookingBefore.eventTypeId)
: undefined;
await handleMarkNoShow({
@@ -434,7 +432,7 @@ export class BookingsService_2024_08_13 {
}
const platformClientParams = booking.eventTypeId
? await this.platformBookingsService.getOAuthClientParams(booking.eventTypeId)
? await this.inputService.getOAuthClientParams(booking.eventTypeId)
: undefined;
const emailsEnabled = platformClientParams ? platformClientParams.arePlatformEmailsEnabled : true;
@@ -473,7 +471,7 @@ export class BookingsService_2024_08_13 {
}
const platformClientParams = booking.eventTypeId
? await this.platformBookingsService.getOAuthClientParams(booking.eventTypeId)
? await this.inputService.getOAuthClientParams(booking.eventTypeId)
: undefined;
const emailsEnabled = platformClientParams ? platformClientParams.arePlatformEmailsEnabled : true;
@@ -500,7 +498,7 @@ export class BookingsService_2024_08_13 {
}
const platformClientParams = booking.eventTypeId
? await this.platformBookingsService.getOAuthClientParams(booking.eventTypeId)
? await this.inputService.getOAuthClientParams(booking.eventTypeId)
: undefined;
const emailsEnabled = platformClientParams ? platformClientParams.arePlatformEmailsEnabled : true;
@@ -528,7 +526,7 @@ export class BookingsService_2024_08_13 {
}
const platformClientParams = booking.eventTypeId
? await this.platformBookingsService.getOAuthClientParams(booking.eventTypeId)
? await this.inputService.getOAuthClientParams(booking.eventTypeId)
: undefined;
const emailsEnabled = platformClientParams ? platformClientParams.arePlatformEmailsEnabled : true;
@@ -3,11 +3,11 @@ import {
bookingResponsesSchema,
seatedBookingDataSchema,
} from "@/ee/bookings/2024-08-13/services/output.service";
import { PlatformBookingsService } from "@/ee/bookings/shared/platform-bookings.service";
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
import { hashAPIKey, isApiKey, stripApiKey } from "@/lib/api-key";
import { ApiKeysRepository } from "@/modules/api-keys/api-keys-repository";
import { BookingSeatRepository } from "@/modules/booking-seat/booking-seat.repository";
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common";
import { Logger } from "@nestjs/common";
@@ -33,7 +33,7 @@ import {
RescheduleBookingInput_2024_08_13,
RescheduleSeatedBookingInput_2024_08_13,
} from "@calcom/platform-types";
import { EventType } from "@calcom/prisma/client";
import { EventType, PlatformOAuthClient } from "@calcom/prisma/client";
type BookingRequest = NextApiRequest & { userId: number | undefined } & OAuthRequestParams;
@@ -70,20 +70,20 @@ export class InputBookingsService_2024_08_13 {
constructor(
private readonly oAuthFlowService: OAuthFlowService,
private readonly oAuthClientRepository: OAuthClientRepository,
private readonly eventTypesRepository: EventTypesRepository_2024_06_14,
private readonly bookingsRepository: BookingsRepository_2024_08_13,
private readonly config: ConfigService,
private readonly apiKeyRepository: ApiKeysRepository,
private readonly bookingSeatRepository: BookingSeatRepository,
private readonly platformBookingsService: PlatformBookingsService
private readonly bookingSeatRepository: BookingSeatRepository
) {}
async createBookingRequest(
request: Request,
body: CreateBookingInput_2024_08_13 | CreateInstantBookingInput_2024_08_13
): Promise<BookingRequest> {
const oAuthClientParams = await this.platformBookingsService.getOAuthClientParams(body.eventTypeId);
const bodyTransformed = await this.transformInputCreateBooking(body, oAuthClientParams?.platformClientId);
const bodyTransformed = await this.transformInputCreateBooking(body);
const oAuthClientParams = await this.getOAuthClientParams(body.eventTypeId);
const newRequest = { ...request };
const userId = (await this.createBookingRequestOwnerId(request)) ?? undefined;
@@ -111,7 +111,30 @@ export class InputBookingsService_2024_08_13 {
return newRequest as unknown as BookingRequest;
}
async transformInputCreateBooking(inputBooking: CreateBookingInput_2024_08_13, platformClientId?: string) {
async getOAuthClientParams(eventTypeId: number) {
const eventType = await this.eventTypesRepository.getEventTypeById(eventTypeId);
let oAuthClient: PlatformOAuthClient | null = null;
if (eventType?.userId) {
oAuthClient = await this.oAuthClientRepository.getByUserId(eventType.userId);
} else if (eventType?.teamId) {
oAuthClient = await this.oAuthClientRepository.getByTeamId(eventType.teamId);
}
if (oAuthClient) {
return {
platformClientId: oAuthClient.id,
platformCancelUrl: oAuthClient.bookingCancelRedirectUri,
platformRescheduleUrl: oAuthClient.bookingRescheduleRedirectUri,
platformBookingUrl: oAuthClient.bookingRedirectUri,
arePlatformEmailsEnabled: oAuthClient.areEmailsEnabled,
};
}
return undefined;
}
async transformInputCreateBooking(inputBooking: CreateBookingInput_2024_08_13) {
const eventType = await this.eventTypesRepository.getEventTypeByIdWithOwnerAndTeam(
inputBooking.eventTypeId
);
@@ -128,17 +151,7 @@ export class InputBookingsService_2024_08_13 {
);
const endTime = startTime.plus({ minutes: lengthInMinutes });
const guests =
inputBooking.guests && platformClientId
? await this.platformBookingsService.getPlatformAttendeesEmails(inputBooking.guests, platformClientId)
: inputBooking.guests;
const attendeeEmail =
inputBooking.attendee.email && platformClientId
? await this.platformBookingsService.getPlatformAttendeeEmail(
inputBooking.attendee.email,
platformClientId
)
: inputBooking.attendee.email;
const guests = inputBooking.guests;
return {
start: startTime.toISO(),
@@ -150,13 +163,20 @@ export class InputBookingsService_2024_08_13 {
hasHashedBookingLink: false,
guests,
// note(Lauris): responses with name and email are required by the handleNewBooking
responses: {
...(inputBooking.bookingFieldsResponses || {}),
name: inputBooking.attendee.name,
email: attendeeEmail ?? "",
attendeePhoneNumber: inputBooking.attendee.phoneNumber,
guests,
},
responses: inputBooking.bookingFieldsResponses
? {
...inputBooking.bookingFieldsResponses,
name: inputBooking.attendee.name,
email: inputBooking.attendee.email ?? "",
attendeePhoneNumber: inputBooking.attendee.phoneNumber,
guests,
}
: {
name: inputBooking.attendee.name,
email: inputBooking.attendee.email ?? "",
attendeePhoneNumber: inputBooking.attendee.phoneNumber,
guests,
},
};
}
@@ -183,12 +203,9 @@ export class InputBookingsService_2024_08_13 {
request: Request,
body: CreateRecurringBookingInput_2024_08_13
): Promise<BookingRequest> {
const oAuthClientParams = await this.platformBookingsService.getOAuthClientParams(body.eventTypeId);
// note(Lauris): update to this.transformInputCreate when rescheduling is implemented
const bodyTransformed = await this.transformInputCreateRecurringBooking(
body,
oAuthClientParams?.platformClientId
);
const bodyTransformed = await this.transformInputCreateRecurringBooking(body);
const oAuthClientParams = await this.getOAuthClientParams(body.eventTypeId);
const newRequest = { ...request };
const userId = (await this.createBookingRequestOwnerId(request)) ?? undefined;
@@ -214,10 +231,7 @@ export class InputBookingsService_2024_08_13 {
return newRequest as unknown as BookingRequest;
}
async transformInputCreateRecurringBooking(
inputBooking: CreateRecurringBookingInput_2024_08_13,
platformClientId?: string
) {
async transformInputCreateRecurringBooking(inputBooking: CreateRecurringBookingInput_2024_08_13) {
const eventType = await this.eventTypesRepository.getEventTypeByIdWithOwnerAndTeam(
inputBooking.eventTypeId
);
@@ -247,17 +261,7 @@ export class InputBookingsService_2024_08_13 {
inputBooking.attendee.timeZone
);
const guests =
inputBooking.guests && platformClientId
? await this.platformBookingsService.getPlatformAttendeesEmails(inputBooking.guests, platformClientId)
: inputBooking.guests;
const attendeeEmail =
inputBooking.attendee.email && platformClientId
? await this.platformBookingsService.getPlatformAttendeeEmail(
inputBooking.attendee.email,
platformClientId
)
: inputBooking.attendee.email;
const guests = inputBooking.guests;
for (let i = 0; i < repeatsTimes; i++) {
const endTime = startTime.plus({ minutes: eventType.length });
@@ -273,12 +277,14 @@ export class InputBookingsService_2024_08_13 {
hasHashedBookingLink: false,
guests,
// note(Lauris): responses with name and email are required by the handleNewBooking
responses: {
...(inputBooking.bookingFieldsResponses || {}),
name: inputBooking.attendee.name,
email: attendeeEmail,
guests,
},
responses: inputBooking.bookingFieldsResponses
? {
...inputBooking.bookingFieldsResponses,
name: inputBooking.attendee.name,
email: inputBooking.attendee.email,
guests,
}
: { name: inputBooking.attendee.name, email: inputBooking.attendee.email, guests },
schedulingType: eventType.schedulingType,
});
@@ -309,9 +315,7 @@ export class InputBookingsService_2024_08_13 {
? await this.transformInputRescheduleSeatedBooking(bookingUid, body)
: await this.transformInputRescheduleBooking(bookingUid, body);
const oAuthClientParams = await this.platformBookingsService.getOAuthClientParams(
bodyTransformed.eventTypeId
);
const oAuthClientParams = await this.getOAuthClientParams(bodyTransformed.eventTypeId);
const newRequest = { ...request };
const userId = (await this.createBookingRequestOwnerId(request)) ?? undefined;
@@ -497,7 +501,7 @@ export class InputBookingsService_2024_08_13 {
}
const oAuthClientParams = booking.eventTypeId
? await this.platformBookingsService.getOAuthClientParams(booking.eventTypeId)
? await this.getOAuthClientParams(booking.eventTypeId)
: undefined;
const newRequest = { ...request };
@@ -1,59 +0,0 @@
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-clients-users.service";
import { UsersRepository } from "@/modules/users/users.repository";
import { Injectable } from "@nestjs/common";
import { PlatformOAuthClient } from "@calcom/prisma/client";
@Injectable()
export class PlatformBookingsService {
constructor(
private readonly oAuthClientsUsersService: OAuthClientUsersService,
private readonly usersRepository: UsersRepository,
private readonly eventTypesRepository: EventTypesRepository_2024_06_14,
private readonly oAuthClientRepository: OAuthClientRepository
) {}
async getPlatformAttendeesEmails(guestsEmails: string[], platformClientId: string) {
return Promise.all(
guestsEmails.map((guestEmail) => this.getPlatformAttendeeEmail(guestEmail, platformClientId))
);
}
async getPlatformAttendeeEmail(attendeeEmail: string, platformClientId: string) {
if (!attendeeEmail.includes(platformClientId)) {
// note(Lauris): we need to do this when managed user is booking another managed user and the managed user doing the booking entered their email without oAuth client id
// or if one of the guests added are managed users without oAuth client id in their email.
const oAuthUserEmail = this.oAuthClientsUsersService.getOAuthUserEmail(platformClientId, attendeeEmail);
const oAuthUser = await this.usersRepository.findByEmail(oAuthUserEmail);
if (oAuthUser) {
return oAuthUserEmail;
}
}
return attendeeEmail;
}
async getOAuthClientParams(eventTypeId: number) {
const eventType = await this.eventTypesRepository.getEventTypeById(eventTypeId);
let oAuthClient: PlatformOAuthClient | null = null;
if (eventType?.userId) {
oAuthClient = await this.oAuthClientRepository.getByUserId(eventType.userId);
} else if (eventType?.teamId) {
oAuthClient = await this.oAuthClientRepository.getByTeamId(eventType.teamId);
}
if (oAuthClient) {
return {
platformClientId: oAuthClient.id,
platformCancelUrl: oAuthClient.bookingCancelRedirectUri,
platformRescheduleUrl: oAuthClient.bookingRescheduleRedirectUri,
platformBookingUrl: oAuthClient.bookingRedirectUri,
arePlatformEmailsEnabled: oAuthClient.areEmailsEnabled,
};
}
return undefined;
}
}
@@ -5,7 +5,7 @@ import { IsEnum, IsNumber, IsString, ValidateNested } from "class-validator";
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
export class CreateManagedUserData {
class CreateManagedUserData {
@ApiProperty({
type: ManagedUserOutput,
})
@@ -1,5 +1,6 @@
import { EventTypesService_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/services/event-types.service";
import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service";
import { OrganizationsTeamsService } from "@/modules/organizations/teams/index/services/organizations-teams.service";
import { TokensRepository } from "@/modules/tokens/tokens.repository";
import { CreateManagedUserInput } from "@/modules/users/inputs/create-managed-user.input";
import { UpdateManagedUserInput } from "@/modules/users/inputs/update-managed-user.input";