refactor: platform org event types (#15961)

* chore: platform-libraries export updateNewTeamMemberEventTypes

* chore: platform-libraries export updateNewTeamMemberEventTypes

* chore: v2 api bump platform-libraries to 0.0.22

* fix: add created managed user to assignAllMembers: true team event-types

* fix: add created organizations user to assignAllMembers: true team event-types

* refactor: organization teams chose if team creator is auto-accepted

* refactor: managed users have only 2 default event types with video

* refactor: lowercase schedulingType for creating team event-types

* refactor: lowercase schedulingType for creating team event-types

* feat: separate platform teams by OAuth client

* swagger regenerate

* refactor: delete platform team when oauth client deleted

* revert: keep 4 default event-types

* fix: tests && merge of main into this branch

* fix: typecheck

---------

Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
This commit is contained in:
Lauris Skraucis
2024-07-30 22:23:31 +03:00
committed by GitHub
co-authored by Morgan
parent 2f946fc5da
commit 41e565fcf8
45 changed files with 1186 additions and 296 deletions
@@ -1,6 +1,6 @@
import { Injectable } from "@nestjs/common";
import { sendSignupToOrganizationEmail, getTranslation } from "@calcom/platform-libraries-0.0.21";
import { sendSignupToOrganizationEmail, getTranslation } from "@calcom/platform-libraries-0.0.22";
@Injectable()
export class EmailService {
@@ -14,7 +14,7 @@ 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 { PlatformOAuthClient, Team, User, EventType } from "@prisma/client";
import * as request from "supertest";
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
@@ -204,10 +204,16 @@ describe("OAuth Client Users Endpoints", () => {
// note(Lauris): to determine count see default event types created in EventTypesService.createUserDefaultEventTypes
expect(defaultEventTypes?.length).toEqual(4);
expect(
defaultEventTypes?.find((eventType) => eventType.length === DEFAULT_EVENT_TYPES.thirtyMinutes.length)
defaultEventTypes?.find((eventType) => eventType.slug === DEFAULT_EVENT_TYPES.thirtyMinutes.slug)
).toBeTruthy();
expect(
defaultEventTypes?.find((eventType) => eventType.length === DEFAULT_EVENT_TYPES.sixtyMinutes.length)
defaultEventTypes?.find((eventType) => eventType.slug === DEFAULT_EVENT_TYPES.sixtyMinutes.slug)
).toBeTruthy();
expect(
defaultEventTypes?.find((eventType) => eventType.slug === DEFAULT_EVENT_TYPES.thirtyMinutesVideo.slug)
).toBeTruthy();
expect(
defaultEventTypes?.find((eventType) => eventType.slug === DEFAULT_EVENT_TYPES.sixtyMinutesVideo.slug)
).toBeTruthy();
}
@@ -301,4 +307,243 @@ describe("OAuth Client Users Endpoints", () => {
await app.close();
});
});
describe("User team even-types", () => {
let app: INestApplication;
let oAuthClient1: PlatformOAuthClient;
let oAuthClient2: PlatformOAuthClient;
let organization: Team;
let team1: Team;
let team2: Team;
let owner: User;
let managedEventType1: EventType;
let userRepositoryFixture: UserRepositoryFixture;
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
let teamRepositoryFixture: TeamRepositoryFixture;
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
let profileRepositoryFixture: ProfileRepositoryFixture;
let postResponseData: CreateUserResponse;
const userEmail = "oauth-client-users-user@gmail.com";
const userTimeZone = "Europe/Rome";
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);
profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
organization = await teamRepositoryFixture.create({
name: "Testy Organization",
isOrganization: true,
});
owner = await userRepositoryFixture.create({
email: userEmail,
username: userEmail,
organization: { connect: { id: organization.id } },
});
await profileRepositoryFixture.create({
uid: `usr-${owner.id}`,
username: userEmail,
organization: {
connect: {
id: organization.id,
},
},
user: {
connect: {
id: owner.id,
},
},
});
oAuthClient1 = await createOAuthClient(organization.id);
oAuthClient2 = await createOAuthClient(organization.id);
team1 = await teamRepositoryFixture.create({
name: "Testy org team",
isOrganization: false,
parent: { connect: { id: organization.id } },
createdByOAuthClient: { connect: { id: oAuthClient1.id } },
});
team2 = await teamRepositoryFixture.create({
name: "Testy org team 2",
isOrganization: false,
parent: { connect: { id: organization.id } },
createdByOAuthClient: { connect: { id: oAuthClient2.id } },
});
// note(Lauris): team1 team event-types
await eventTypesRepositoryFixture.createTeamEventType({
schedulingType: "COLLECTIVE",
team: {
connect: { id: team1.id },
},
title: "Collective Event Type",
slug: "collective-event-type",
length: 30,
assignAllTeamMembers: true,
bookingFields: [],
locations: [],
});
managedEventType1 = await eventTypesRepositoryFixture.createTeamEventType({
schedulingType: "MANAGED",
team: {
connect: { id: team1.id },
},
title: "Managed Event Type",
slug: "managed-event-type",
length: 60,
assignAllTeamMembers: true,
bookingFields: [],
locations: [],
});
// note(Lauris): team2 team event-types
await eventTypesRepositoryFixture.createTeamEventType({
schedulingType: "COLLECTIVE",
team: {
connect: { id: team2.id },
},
title: "Collective Event Type team 2",
slug: "collective-event-type-team-2",
length: 30,
assignAllTeamMembers: true,
bookingFields: [],
locations: [],
});
await eventTypesRepositoryFixture.createTeamEventType({
schedulingType: "MANAGED",
team: {
connect: { id: team2.id },
},
title: "Managed Event Type team 2",
slug: "managed-event-type-team-2",
length: 60,
assignAllTeamMembers: true,
bookingFields: [],
locations: [],
});
await app.init();
});
async function createOAuthClient(organizationId: number) {
const data = {
logo: "logo-url",
name: "name",
redirectUris: [CLIENT_REDIRECT_URI],
permissions: 32,
};
const secret = "secret";
const client = await oauthClientRepositoryFixture.create(organizationId, data, secret);
return client;
}
it("should be defined", () => {
expect(oauthClientRepositoryFixture).toBeDefined();
expect(userRepositoryFixture).toBeDefined();
expect(oAuthClient1).toBeDefined();
});
it(`should create managed user and update team event-types of OAuthClient marked as assignAllTeamMembers: true`, async () => {
const requestBody: CreateManagedUserInput = {
email: userEmail,
timeZone: userTimeZone,
weekStart: "Monday",
timeFormat: 24,
locale: Locales.FR,
};
const response = await request(app.getHttpServer())
.post(`/api/v2/oauth-clients/${oAuthClient1.id}/users`)
.set("x-cal-secret-key", oAuthClient1.secret)
.send(requestBody)
.expect(201);
const responseBody: ApiSuccessResponse<{
user: Omit<User, "password">;
accessToken: string;
refreshToken: string;
}> = response.body;
postResponseData = responseBody.data;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
await userHasCorrectEventTypes(responseBody.data.user.id);
await teamHasCorrectEventTypes(team1.id);
});
async function userHasCorrectEventTypes(userId: number) {
const eventTypes = await eventTypesRepositoryFixture.getAllUserEventTypes(userId);
expect(eventTypes?.length).toEqual(5);
// note(Lauris): managed event-types with assignAllTeamMembers: true
expect(eventTypes?.find((eventType) => eventType.slug === managedEventType1.slug)).toBeTruthy();
// note(Lauris): default event types
expect(
eventTypes?.find((eventType) => eventType.slug === DEFAULT_EVENT_TYPES.thirtyMinutes.slug)
).toBeTruthy();
expect(
eventTypes?.find((eventType) => eventType.slug === DEFAULT_EVENT_TYPES.sixtyMinutes.slug)
).toBeTruthy();
expect(
eventTypes?.find((eventType) => eventType.slug === DEFAULT_EVENT_TYPES.thirtyMinutesVideo.slug)
).toBeTruthy();
expect(
eventTypes?.find((eventType) => eventType.slug === DEFAULT_EVENT_TYPES.sixtyMinutesVideo.slug)
).toBeTruthy();
}
async function teamHasCorrectEventTypes(teamId: number) {
const eventTypes = await eventTypesRepositoryFixture.getAllTeamEventTypes(teamId);
expect(eventTypes?.length).toEqual(2);
// note(Lauris): managed event-types with assignAllTeamMembers: true
expect(eventTypes?.find((eventType) => eventType.slug === managedEventType1.slug)).toBeTruthy();
// note(Lauris): check if managed user added to collective event-type hosts given that it has assignAllTeamMembers: true
const collective = eventTypes?.find((eventType) => eventType.schedulingType === "COLLECTIVE");
expect(collective).toBeTruthy();
expect(collective?.hosts).toBeDefined();
expect(collective?.hosts?.length).toEqual(1);
expect(collective?.hosts[0].userId).toEqual(postResponseData.user.id);
}
afterAll(async () => {
await oauthClientRepositoryFixture.delete(oAuthClient1.id);
await teamRepositoryFixture.delete(organization.id);
await userRepositoryFixture.delete(owner.id);
try {
await userRepositoryFixture.delete(postResponseData.user.id);
} catch (e) {
// User might have been deleted by the test
}
await app.close();
});
});
});
@@ -78,6 +78,7 @@ export class OAuthClientUsersController {
`Creating user with data: ${JSON.stringify(body, null, 2)} for OAuth Client with ID ${oAuthClientId}`
);
const client = await this.oauthRepository.getOAuthClient(oAuthClientId);
console.log("asap createUser client", JSON.stringify(client, null, 2));
const isPlatformManaged = true;
const { user, tokens } = await this.oAuthClientUsersService.createOauthClientUser(
@@ -10,6 +10,7 @@ import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repo
import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-clients-users.service";
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
import { OrganizationsModule } from "@/modules/organizations/organizations.module";
import { OrganizationsTeamsService } from "@/modules/organizations/services/organizations-teams.service";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { RedisModule } from "@/modules/redis/redis.module";
import { StripeModule } from "@/modules/stripe/stripe.module";
@@ -33,7 +34,13 @@ import { Global, Module } from "@nestjs/common";
BillingModule,
SchedulesModule_2024_04_15,
],
providers: [OAuthClientRepository, TokensRepository, OAuthFlowService, OAuthClientUsersService],
providers: [
OAuthClientRepository,
TokensRepository,
OAuthFlowService,
OAuthClientUsersService,
OrganizationsTeamsService,
],
controllers: [OAuthClientUsersController, OAuthClientsController, OAuthFlowController],
exports: [OAuthClientRepository],
})
@@ -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/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";
@@ -7,7 +8,7 @@ import { UsersRepository } from "@/modules/users/users.repository";
import { BadRequestException, Injectable } from "@nestjs/common";
import { User } from "@prisma/client";
import { createNewUsersConnectToOrgIfExists, slugify } from "@calcom/platform-libraries-0.0.21";
import { createNewUsersConnectToOrgIfExists, slugify } from "@calcom/platform-libraries-0.0.22";
@Injectable()
export class OAuthClientUsersService {
@@ -15,7 +16,8 @@ export class OAuthClientUsersService {
private readonly userRepository: UsersRepository,
private readonly tokensRepository: TokensRepository,
private readonly eventTypesService: EventTypesService_2024_04_15,
private readonly schedulesService: SchedulesService_2024_04_15
private readonly schedulesService: SchedulesService_2024_04_15,
private readonly organizationsTeamsService: OrganizationsTeamsService
) {}
async createOauthClientUser(
@@ -78,6 +80,8 @@ export class OAuthClientUsersService {
user.defaultScheduleId = defaultSchedule.id;
}
await this.organizationsTeamsService.addUserToPlatformTeamEvents(user.id, organizationId, oAuthClientId);
return {
user,
tokens: {
@@ -248,7 +248,9 @@ describe("Organizations Event Types Endpoints", () => {
options: ["javascript", "python", "cobol"],
},
],
schedulingType: "COLLECTIVE",
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
schedulingType: "collective",
hosts: [
{
userId: teammate1.id,
@@ -270,6 +272,7 @@ describe("Organizations Event Types Endpoints", () => {
const data = responseBody.data;
expect(data.title).toEqual(body.title);
expect(data.hosts.length).toEqual(2);
expect(data.schedulingType).toEqual("COLLECTIVE");
evaluateHost(body.hosts[0], data.hosts[0]);
evaluateHost(body.hosts[1], data.hosts[1]);
@@ -385,7 +388,6 @@ describe("Organizations Event Types Endpoints", () => {
expect(responseBody.status).toEqual(SUCCESS_STATUS);
const data = responseBody.data;
console.log("asap responseBody.data", JSON.stringify(responseBody.data, null, 2));
expect(data.length).toEqual(2);
const eventTypeCollective = data.find((eventType) => eventType.schedulingType === "COLLECTIVE");
@@ -11,20 +11,20 @@ import { Test } from "@nestjs/testing";
import { 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 { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { withApiAuth } from "test/utils/withApiAuth";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import { SUCCESS_STATUS, X_CAL_CLIENT_ID, X_CAL_SECRET_KEY } from "@calcom/platform-constants";
import { ApiSuccessResponse } from "@calcom/platform-types";
import { Team } from "@calcom/prisma/client";
import { PlatformOAuthClient, Team } from "@calcom/prisma/client";
describe("Organizations Team Endpoints", () => {
describe("User Authentication - User is Org Admin", () => {
let app: INestApplication;
let userRepositoryFixture: UserRepositoryFixture;
let organizationsRepositoryFixture: TeamRepositoryFixture;
let teamsRepositoryFixture: TeamRepositoryFixture;
let membershipsRepositoryFixture: MembershipRepositoryFixture;
@@ -32,6 +32,7 @@ describe("Organizations Team Endpoints", () => {
let team: Team;
let team2: Team;
let teamCreatedViaApi: Team;
let teamCreatedViaApi2: Team;
const userEmail = "org-admin-teams-controller-e2e@api.com";
let user: User;
@@ -45,7 +46,6 @@ describe("Organizations Team Endpoints", () => {
).compile();
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
organizationsRepositoryFixture = new TeamRepositoryFixture(moduleRef);
teamsRepositoryFixture = new TeamRepositoryFixture(moduleRef);
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
@@ -54,7 +54,7 @@ describe("Organizations Team Endpoints", () => {
username: userEmail,
});
org = await organizationsRepositoryFixture.create({
org = await teamsRepositoryFixture.create({
name: "Test Organization",
isOrganization: true,
});
@@ -85,7 +85,7 @@ describe("Organizations Team Endpoints", () => {
it("should be defined", () => {
expect(userRepositoryFixture).toBeDefined();
expect(organizationsRepositoryFixture).toBeDefined();
expect(teamsRepositoryFixture).toBeDefined();
expect(user).toBeDefined();
expect(org).toBeDefined();
});
@@ -147,6 +147,7 @@ describe("Organizations Team Endpoints", () => {
teamCreatedViaApi.id
);
expect(membership?.role ?? "").toEqual("OWNER");
expect(membership?.accepted).toEqual(true);
});
});
@@ -199,11 +200,35 @@ describe("Organizations Team Endpoints", () => {
return request(app.getHttpServer()).get(`/v2/organizations/${org.id}/teams/123132145`).expect(404);
});
it("should create the team of the org without auto-accepting creator", async () => {
return request(app.getHttpServer())
.post(`/v2/organizations/${org.id}/teams`)
.send({
name: "Team II created via API",
autoAcceptCreator: false,
} satisfies CreateOrgTeamDto)
.expect(201)
.then(async (response) => {
const responseBody: ApiSuccessResponse<Team> = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
teamCreatedViaApi2 = responseBody.data;
expect(teamCreatedViaApi2.name).toEqual("Team II created via API");
expect(teamCreatedViaApi2.parentId).toEqual(org.id);
const membership = await membershipsRepositoryFixture.getUserMembershipByTeamId(
user.id,
teamCreatedViaApi2.id
);
expect(membership?.role ?? "").toEqual("OWNER");
expect(membership?.accepted).toEqual(false);
});
});
afterAll(async () => {
await userRepositoryFixture.deleteByEmail(user.email);
await teamsRepositoryFixture.delete(team.id);
await teamsRepositoryFixture.delete(team2.id);
await organizationsRepositoryFixture.delete(org.id);
await teamsRepositoryFixture.delete(teamCreatedViaApi2.id);
await teamsRepositoryFixture.delete(org.id);
await app.close();
});
});
@@ -214,7 +239,6 @@ describe("Organizations Team Endpoints", () => {
let app: INestApplication;
let userRepositoryFixture: UserRepositoryFixture;
let organizationsRepositoryFixture: TeamRepositoryFixture;
let teamsRepositoryFixture: TeamRepositoryFixture;
let membershipsRepositoryFixture: MembershipRepositoryFixture;
@@ -234,7 +258,6 @@ describe("Organizations Team Endpoints", () => {
).compile();
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
organizationsRepositoryFixture = new TeamRepositoryFixture(moduleRef);
teamsRepositoryFixture = new TeamRepositoryFixture(moduleRef);
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
@@ -243,7 +266,7 @@ describe("Organizations Team Endpoints", () => {
username: userEmail,
});
org = await organizationsRepositoryFixture.create({
org = await teamsRepositoryFixture.create({
name: "Test Organization",
isOrganization: true,
});
@@ -274,7 +297,7 @@ describe("Organizations Team Endpoints", () => {
it("should be defined", () => {
expect(userRepositoryFixture).toBeDefined();
expect(organizationsRepositoryFixture).toBeDefined();
expect(teamsRepositoryFixture).toBeDefined();
expect(user).toBeDefined();
expect(org).toBeDefined();
});
@@ -317,7 +340,7 @@ describe("Organizations Team Endpoints", () => {
await userRepositoryFixture.deleteByEmail(user.email);
await teamsRepositoryFixture.delete(team.id);
await teamsRepositoryFixture.delete(team2.id);
await organizationsRepositoryFixture.delete(org.id);
await teamsRepositoryFixture.delete(org.id);
await app.close();
});
});
@@ -448,3 +471,158 @@ describe("Organizations Team Endpoints", () => {
});
});
});
describe("Organizations Team Endpoints", () => {
describe("Platform teams", () => {
let app: INestApplication;
let userRepositoryFixture: UserRepositoryFixture;
let teamsRepositoryFixture: TeamRepositoryFixture;
let membershipsRepositoryFixture: MembershipRepositoryFixture;
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
let oAuthClient1: PlatformOAuthClient;
let oAuthClient2: PlatformOAuthClient;
let org: Team;
let team1: Team;
let team2: Team;
const userEmail = "platform-org-member-teams-owner-controller-e2e@api.com";
let user: User;
beforeAll(async () => {
const moduleRef = await withApiAuth(
userEmail,
Test.createTestingModule({
imports: [AppModule, PrismaModule, UsersModule, TokensModule],
})
).compile();
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
teamsRepositoryFixture = new TeamRepositoryFixture(moduleRef);
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef);
user = await userRepositoryFixture.create({
email: userEmail,
username: userEmail,
});
org = await teamsRepositoryFixture.create({
name: "Platform Test Organization",
isOrganization: true,
});
await membershipsRepositoryFixture.create({
role: "ADMIN",
user: { connect: { id: user.id } },
team: { connect: { id: org.id } },
});
oAuthClient1 = await createOAuthClient(org.id);
oAuthClient2 = await createOAuthClient(org.id);
app = moduleRef.createNestApplication();
bootstrap(app as NestExpressApplication);
await app.init();
});
async function createOAuthClient(organizationId: number) {
const data = {
logo: "logo-url",
name: "name",
redirectUris: ["redirect-uri"],
permissions: 32,
};
const secret = "secret";
return await oauthClientRepositoryFixture.create(organizationId, data, secret);
}
it("should be defined", () => {
expect(userRepositoryFixture).toBeDefined();
expect(teamsRepositoryFixture).toBeDefined();
expect(user).toBeDefined();
expect(org).toBeDefined();
});
it("should create first oAuth client team", async () => {
const teamName = "Platform team created via API";
return request(app.getHttpServer())
.post(`/v2/organizations/${org.id}/teams`)
.set(X_CAL_CLIENT_ID, oAuthClient1.id)
.send({
name: teamName,
} satisfies CreateOrgTeamDto)
.expect(201)
.then(async (response) => {
const responseBody: ApiSuccessResponse<Team> = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
team1 = responseBody.data;
expect(team1.name).toEqual(teamName);
expect(team1.parentId).toEqual(org.id);
const membership = await membershipsRepositoryFixture.getUserMembershipByTeamId(user.id, team1.id);
expect(membership?.role ?? "").toEqual("OWNER");
expect(membership?.accepted).toEqual(true);
});
});
it("should create second oAuth client team", async () => {
const teamName = "Platform team II created via API";
return request(app.getHttpServer())
.post(`/v2/organizations/${org.id}/teams`)
.set(X_CAL_CLIENT_ID, oAuthClient2.id)
.set(X_CAL_SECRET_KEY, oAuthClient2.secret)
.send({
name: teamName,
} satisfies CreateOrgTeamDto)
.expect(201)
.then(async (response) => {
const responseBody: ApiSuccessResponse<Team> = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
team2 = responseBody.data;
expect(team2.name).toEqual(teamName);
expect(team2.parentId).toEqual(org.id);
const membership = await membershipsRepositoryFixture.getUserMembershipByTeamId(user.id, team2.id);
expect(membership?.role ?? "").toEqual("OWNER");
expect(membership?.accepted).toEqual(true);
});
});
it("should get all the platform teams correctly tied to OAuth clients", async () => {
return request(app.getHttpServer())
.get(`/v2/organizations/${org.id}/teams`)
.expect(200)
.then(async (response) => {
const responseBody: ApiSuccessResponse<Team[]> = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data[0].id).toEqual(team1.id);
expect(responseBody.data[1].id).toEqual(team2.id);
const oAuthClientTeams = await teamsRepositoryFixture.getPlatformOrgTeams(org.id, oAuthClient1.id);
expect(oAuthClientTeams.length).toEqual(1);
const oAuthClientTeam = oAuthClientTeams[0];
expect(oAuthClientTeam.id).toEqual(team1.id);
expect(oAuthClientTeam.name).toEqual(team1.name);
const oAuthClient2Teams = await teamsRepositoryFixture.getPlatformOrgTeams(org.id, oAuthClient2.id);
expect(oAuthClient2Teams.length).toEqual(1);
const oAuthClientTeam2 = oAuthClient2Teams[0];
expect(oAuthClientTeam2.id).toEqual(team2.id);
expect(oAuthClientTeam2.name).toEqual(team2.name);
});
});
afterAll(async () => {
await userRepositoryFixture.deleteByEmail(user.email);
await teamsRepositoryFixture.delete(team1.id);
await teamsRepositoryFixture.delete(org.id);
await app.close();
});
});
});
@@ -9,6 +9,7 @@ import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard";
import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
import { IsTeamInOrg } from "@/modules/auth/guards/teams/is-team-in-org.guard";
import { CreateOrgTeamDto } from "@/modules/organizations/inputs/create-organization-team.input";
import { UpdateOrgTeamDto } from "@/modules/organizations/inputs/update-organization-team.input";
import {
OrgMeTeamOutputDto,
OrgMeTeamsOutputResponseDto,
@@ -28,11 +29,12 @@ import {
Patch,
Post,
Body,
Headers,
} from "@nestjs/common";
import { ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
import { plainToClass } from "class-transformer";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import { SUCCESS_STATUS, X_CAL_CLIENT_ID } from "@calcom/platform-constants";
import { OrgTeamOutputDto } from "@calcom/platform-types";
import { SkipTakePagination } from "@calcom/platform-types";
import { Team } from "@calcom/prisma/client";
@@ -126,7 +128,7 @@ export class OrganizationsTeamsController {
async updateTeam(
@Param("orgId", ParseIntPipe) orgId: number,
@Param("teamId", ParseIntPipe) teamId: number,
@Body() body: CreateOrgTeamDto
@Body() body: UpdateOrgTeamDto
): Promise<OrgTeamOutputResponseDto> {
const team = await this.organizationsTeamsService.updateOrgTeam(orgId, teamId, body);
return {
@@ -142,9 +144,13 @@ export class OrganizationsTeamsController {
async createTeam(
@Param("orgId", ParseIntPipe) orgId: number,
@Body() body: CreateOrgTeamDto,
@GetUser() user: UserWithProfile
@GetUser() user: UserWithProfile,
@Headers(X_CAL_CLIENT_ID) oAuthClientId?: string
): Promise<OrgTeamOutputResponseDto> {
const team = await this.organizationsTeamsService.createOrgTeam(orgId, body, user);
const team = oAuthClientId
? await this.organizationsTeamsService.createPlatformOrgTeam(orgId, oAuthClientId, body, user)
: await this.organizationsTeamsService.createOrgTeam(orgId, body, user);
return {
status: SUCCESS_STATUS,
data: plainToClass(OrgTeamOutputDto, team, { strategy: "excludeAll" }),
@@ -8,6 +8,7 @@ import { INestApplication } from "@nestjs/common";
import { NestExpressApplication } from "@nestjs/platform-express";
import { Test } from "@nestjs/testing";
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 { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
@@ -15,7 +16,7 @@ import { UserRepositoryFixture } from "test/fixtures/repository/users.repository
import { withApiAuth } from "test/utils/withApiAuth";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import { User, Team, Membership } from "@calcom/prisma/client";
import { User, Team, EventType } from "@calcom/prisma/client";
describe("Organizations Users Endpoints", () => {
describe("Member role", () => {
@@ -29,7 +30,6 @@ describe("Organizations Users Endpoints", () => {
const userEmail = "member1@org.com";
let user: User;
let org: Team;
let membership: Membership;
beforeAll(async () => {
const moduleRef = await withApiAuth(
@@ -70,7 +70,7 @@ describe("Organizations Users Endpoints", () => {
},
});
membership = await membershipFixtures.addUserToOrg(user, org, "MEMBER", true);
await membershipFixtures.addUserToOrg(user, org, "MEMBER", true);
app = moduleRef.createNestApplication();
bootstrap(app as NestExpressApplication);
@@ -344,4 +344,165 @@ describe("Organizations Users Endpoints", () => {
await app.close();
});
});
describe("Member event-types", () => {
let app: INestApplication;
let userRepositoryFixture: UserRepositoryFixture;
let organizationsRepositoryFixture: TeamRepositoryFixture;
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
let membershipFixtures: MembershipRepositoryFixture;
let profileRepositoryFixture: ProfileRepositoryFixture;
const authEmail = "auth@org.com";
let user: User;
let org: Team;
let team: Team;
let managedEventType: EventType;
let createdUser: User;
beforeAll(async () => {
const moduleRef = await withApiAuth(
authEmail,
Test.createTestingModule({
imports: [AppModule, PrismaModule, UsersModule, TokensModule],
})
).compile();
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
organizationsRepositoryFixture = new TeamRepositoryFixture(moduleRef);
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
membershipFixtures = new MembershipRepositoryFixture(moduleRef);
profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
org = await organizationsRepositoryFixture.create({
name: "Test org 4",
isOrganization: true,
});
team = await organizationsRepositoryFixture.create({
name: "Test org 4 team",
isOrganization: false,
parent: { connect: { id: org.id } },
});
user = await userRepositoryFixture.create({
email: authEmail,
username: authEmail,
organization: { connect: { id: org.id } },
});
await profileRepositoryFixture.create({
uid: `usr-${user.id}`,
username: authEmail,
organization: {
connect: {
id: org.id,
},
},
user: {
connect: {
id: user.id,
},
},
});
await membershipFixtures.addUserToOrg(user, org, "ADMIN", true);
await eventTypesRepositoryFixture.createTeamEventType({
schedulingType: "COLLECTIVE",
team: {
connect: { id: team.id },
},
title: "Collective Event Type",
slug: "collective-event-type",
length: 30,
assignAllTeamMembers: true,
bookingFields: [],
locations: [],
});
managedEventType = await eventTypesRepositoryFixture.createTeamEventType({
schedulingType: "MANAGED",
team: {
connect: { id: team.id },
},
title: "Managed Event Type",
slug: "managed-event-type",
length: 60,
assignAllTeamMembers: true,
bookingFields: [],
locations: [],
});
app = moduleRef.createNestApplication();
bootstrap(app as NestExpressApplication);
await app.init();
});
it("should be defined", () => {
expect(userRepositoryFixture).toBeDefined();
expect(organizationsRepositoryFixture).toBeDefined();
expect(user).toBeDefined();
expect(org).toBeDefined();
});
it("should create a new org user with team event-types", async () => {
const newOrgUser = {
email: "new-org-member-d@org.com",
organizationRole: "MEMBER",
autoAccept: true,
};
const { body } = await request(app.getHttpServer())
.post(`/v2/organizations/${org.id}/users`)
.send({
email: newOrgUser.email,
})
.set("Content-Type", "application/json")
.set("Accept", "application/json");
const userData = body.data;
expect(body.status).toBe(SUCCESS_STATUS);
userHasCorrectEventTypes(userData.id);
createdUser = userData;
teamHasCorrectEventTypes(team.id);
});
async function userHasCorrectEventTypes(userId: number) {
const eventTypes = await eventTypesRepositoryFixture.getAllUserEventTypes(userId);
expect(eventTypes?.length).toEqual(1);
// note(Lauris): managed event-types with assignAllTeamMembers: true
expect(eventTypes?.find((eventType) => eventType.slug === managedEventType.slug)).toBeTruthy();
}
async function teamHasCorrectEventTypes(teamId: number) {
const eventTypes = await eventTypesRepositoryFixture.getAllTeamEventTypes(teamId);
expect(eventTypes?.length).toEqual(2);
// note(Lauris): managed event-types with assignAllTeamMembers: true
expect(eventTypes?.find((eventType) => eventType.slug === managedEventType.slug)).toBeTruthy();
// note(Lauris): check if managed user added to collective event-type hosts given that it has assignAllTeamMembers: true
const collective = eventTypes?.find((eventType) => eventType.schedulingType === "COLLECTIVE");
expect(collective).toBeTruthy();
expect(collective?.hosts).toBeDefined();
expect(collective?.hosts?.length).toEqual(1);
expect(collective?.hosts[0].userId).toEqual(createdUser.id);
}
afterAll(async () => {
// await membershipFixtures.delete(membership.id);
await userRepositoryFixture.deleteByEmail(user.email);
await userRepositoryFixture.deleteByEmail(createdUser.email);
await organizationsRepositoryFixture.delete(org.id);
await app.close();
await app.close();
});
});
});
@@ -72,4 +72,8 @@ export class CreateOrgTeamDto {
@IsOptional()
@IsString()
readonly weekStart?: string = "Sunday";
@IsOptional()
@IsBoolean()
readonly autoAcceptCreator?: boolean = true;
}
@@ -0,0 +1,75 @@
import { IsBoolean, IsOptional, IsString, IsUrl, Length } from "class-validator";
export class UpdateOrgTeamDto {
@IsString()
@Length(1)
readonly name?: string;
@IsOptional()
@IsString()
readonly slug?: string;
@IsOptional()
@IsUrl()
readonly logoUrl?: string;
@IsOptional()
@IsUrl()
readonly calVideoLogo?: string;
@IsOptional()
@IsUrl()
readonly appLogo?: string;
@IsOptional()
@IsUrl()
readonly appIconLogo?: string;
@IsOptional()
@IsString()
readonly bio?: string;
@IsOptional()
@IsBoolean()
readonly hideBranding?: boolean = false;
@IsOptional()
@IsBoolean()
readonly isPrivate?: boolean;
@IsOptional()
@IsBoolean()
readonly hideBookATeamMember?: boolean;
@IsOptional()
@IsString()
readonly metadata?: string; // Assuming metadata is a JSON string. Adjust accordingly if it's a nested object.
@IsOptional()
@IsString()
readonly theme?: string;
@IsOptional()
@IsString()
readonly brandColor?: string;
@IsOptional()
@IsString()
readonly darkBrandColor?: string;
@IsOptional()
@IsUrl()
readonly bannerUrl?: string;
@IsOptional()
@IsString()
readonly timeFormat?: number;
@IsOptional()
@IsString()
readonly timeZone?: string = "Europe/London";
@IsOptional()
@IsString()
readonly weekStart?: string = "Sunday";
}
@@ -1,4 +1,5 @@
import { CreateOrgTeamDto } from "@/modules/organizations/inputs/create-organization-team.input";
import { UpdateOrgTeamDto } from "@/modules/organizations/inputs/update-organization-team.input";
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
import { Injectable } from "@nestjs/common";
@@ -16,6 +17,14 @@ export class OrganizationsTeamsRepository {
});
}
async findOrgTeams(organizationId: number) {
return this.dbRead.prisma.team.findMany({
where: {
parentId: organizationId,
},
});
}
async deleteOrgTeam(organizationId: number, teamId: number) {
return this.dbRead.prisma.team.delete({
where: {
@@ -32,7 +41,26 @@ export class OrganizationsTeamsRepository {
});
}
async updateOrgTeam(organizationId: number, teamId: number, data: CreateOrgTeamDto) {
async createPlatformOrgTeam(organizationId: number, oAuthClientId: string, data: CreateOrgTeamDto) {
return this.dbRead.prisma.team.create({
data: {
...data,
parentId: organizationId,
createdByOAuthClientId: oAuthClientId,
},
});
}
async getPlatformOrgTeams(organizationId: number, oAuthClientId: string) {
return this.dbRead.prisma.team.findMany({
where: {
parentId: organizationId,
createdByOAuthClientId: oAuthClientId,
},
});
}
async updateOrgTeam(organizationId: number, teamId: number, data: UpdateOrgTeamDto) {
return this.dbRead.prisma.team.update({
data: { ...data },
where: { id: teamId, parentId: organizationId, isOrganization: false },
@@ -9,7 +9,7 @@ import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { UserWithProfile } from "@/modules/users/users.repository";
import { Injectable, NotFoundException } from "@nestjs/common";
import { createEventType, updateEventType } from "@calcom/platform-libraries-0.0.21";
import { createEventType, updateEventType } from "@calcom/platform-libraries-0.0.22";
import {
CreateTeamEventTypeInput_2024_06_14,
UpdateTeamEventTypeInput_2024_06_14,
@@ -1,9 +1,12 @@
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
import { CreateOrgTeamDto } from "@/modules/organizations/inputs/create-organization-team.input";
import { UpdateOrgTeamDto } from "@/modules/organizations/inputs/update-organization-team.input";
import { OrganizationsTeamsRepository } from "@/modules/organizations/repositories/organizations-teams.repository";
import { UserWithProfile } from "@/modules/users/users.repository";
import { Injectable } from "@nestjs/common";
import { updateNewTeamMemberEventTypes } from "@calcom/platform-libraries-0.0.22";
@Injectable()
export class OrganizationsTeamsService {
constructor(
@@ -31,16 +34,58 @@ export class OrganizationsTeamsService {
return team;
}
async updateOrgTeam(organizationId: number, teamId: number, data: CreateOrgTeamDto) {
async updateOrgTeam(organizationId: number, teamId: number, data: UpdateOrgTeamDto) {
const team = await this.organizationsTeamRepository.updateOrgTeam(organizationId, teamId, data);
return team;
}
async createOrgTeam(organizationId: number, data: CreateOrgTeamDto, user: UserWithProfile) {
const team = await this.organizationsTeamRepository.createOrgTeam(organizationId, data);
const { autoAcceptCreator, ...rest } = data;
const team = await this.organizationsTeamRepository.createOrgTeam(organizationId, rest);
if (user.role !== "ADMIN") {
await this.membershipsRepository.createMembership(team.id, user.id, "OWNER", true);
await this.membershipsRepository.createMembership(team.id, user.id, "OWNER", !!autoAcceptCreator);
}
return team;
}
async createPlatformOrgTeam(
organizationId: number,
oAuthClientId: string,
data: CreateOrgTeamDto,
user: UserWithProfile
) {
const { autoAcceptCreator, ...rest } = data;
const team = await this.organizationsTeamRepository.createPlatformOrgTeam(
organizationId,
oAuthClientId,
rest
);
if (user.role !== "ADMIN") {
await this.membershipsRepository.createMembership(team.id, user.id, "OWNER", !!autoAcceptCreator);
}
return team;
}
async addUserToTeamEvents(userId: number, organizationId: number) {
const orgTeams = await this.organizationsTeamRepository.findOrgTeams(organizationId);
for (const team of orgTeams) {
await updateNewTeamMemberEventTypes(userId, team.id);
}
}
async addUserToPlatformTeamEvents(userId: number, organizationId: number, oAuthClientId: string) {
const oAuthClientTeams = await this.organizationsTeamRepository.getPlatformOrgTeams(
organizationId,
oAuthClientId
);
for (const team of oAuthClientTeams) {
await updateNewTeamMemberEventTypes(userId, team.id);
}
}
}
@@ -2,17 +2,19 @@ import { EmailService } from "@/modules/email/email.service";
import { CreateOrganizationUserInput } from "@/modules/organizations/inputs/create-organization-user.input";
import { UpdateOrganizationUserInput } from "@/modules/organizations/inputs/update-organization-user.input";
import { OrganizationsUsersRepository } from "@/modules/organizations/repositories/organizations-users.repository";
import { OrganizationsTeamsService } from "@/modules/organizations/services/organizations-teams.service";
import { CreateUserInput } from "@/modules/users/inputs/create-user.input";
import { Injectable, ConflictException } from "@nestjs/common";
import { plainToInstance } from "class-transformer";
import { createNewUsersConnectToOrgIfExists } from "@calcom/platform-libraries-0.0.21";
import { createNewUsersConnectToOrgIfExists } from "@calcom/platform-libraries-0.0.22";
import { Team } from "@calcom/prisma/client";
@Injectable()
export class OrganizationsUsersService {
constructor(
private readonly organizationsUsersRepository: OrganizationsUsersRepository,
private readonly organizationsTeamsService: OrganizationsTeamsService,
private readonly emailService: EmailService
) {}
@@ -77,6 +79,8 @@ export class OrganizationsUsersService {
updateUserBody
);
await this.organizationsTeamsService.addUserToTeamEvents(user.id, org.id);
// Need to send email to new user to create password
await this.emailService.sendSignupToOrganizationEmail({
usernameOrEmail,
@@ -5,8 +5,8 @@ import { ApiTags as DocsTags } from "@nestjs/swagger";
import { Response as ExpressResponse, Request as ExpressRequest } from "express";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import { getAvailableSlots } from "@calcom/platform-libraries-0.0.21";
import type { AvailableSlotsType } from "@calcom/platform-libraries-0.0.21";
import { getAvailableSlots } from "@calcom/platform-libraries-0.0.22";
import type { AvailableSlotsType } from "@calcom/platform-libraries-0.0.22";
import { RemoveSelectedSlotInput, ReserveSlotInput } from "@calcom/platform-types";
import { ApiResponse, GetAvailableSlotsInput } from "@calcom/platform-types";
@@ -3,7 +3,7 @@ import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { Injectable } from "@nestjs/common";
import { DateTime } from "luxon";
import { MINUTES_TO_BOOK } from "@calcom/platform-libraries-0.0.21";
import { MINUTES_TO_BOOK } from "@calcom/platform-libraries-0.0.22";
import { ReserveSlotInput } from "@calcom/platform-types";
@Injectable()
@@ -4,7 +4,7 @@ import { Controller, Get } from "@nestjs/common";
import { ApiTags as DocsTags } from "@nestjs/swagger";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import type { CityTimezones } from "@calcom/platform-libraries-0.0.21";
import type { CityTimezones } from "@calcom/platform-libraries-0.0.22";
import { ApiResponse } from "@calcom/platform-types";
@Controller({
@@ -1,8 +1,8 @@
import { RedisService } from "@/modules/redis/redis.service";
import { Injectable } from "@nestjs/common";
import { cityTimezonesHandler } from "@calcom/platform-libraries-0.0.21";
import type { CityTimezones } from "@calcom/platform-libraries-0.0.21";
import { cityTimezonesHandler } from "@calcom/platform-libraries-0.0.22";
import type { CityTimezones } from "@calcom/platform-libraries-0.0.22";
@Injectable()
export class TimezonesService {