diff --git a/apps/api/v2/src/modules/oauth-clients/controllers/oauth-client-users/oauth-client-users.controller.e2e-spec.ts b/apps/api/v2/src/modules/oauth-clients/controllers/oauth-client-users/oauth-client-users.controller.e2e-spec.ts index 880782c3bf..75dbe65785 100644 --- a/apps/api/v2/src/modules/oauth-clients/controllers/oauth-client-users/oauth-client-users.controller.e2e-spec.ts +++ b/apps/api/v2/src/modules/oauth-clients/controllers/oauth-client-users/oauth-client-users.controller.e2e-spec.ts @@ -510,7 +510,7 @@ describe("OAuth Client Users Endpoints", () => { expect(oAuthClient1).toBeDefined(); }); - it(`should create managed user and update team event-types of OAuthClient marked as assignAllTeamMembers: true`, async () => { + it(`should create managed user and update team event-types`, async () => { const requestBody: CreateManagedUserInput = { email: userEmail, timeZone: userTimeZone, @@ -532,48 +532,13 @@ describe("OAuth Client Users Endpoints", () => { expect(responseBody.status).toEqual(SUCCESS_STATUS); expect(responseBody.data).toBeDefined(); - await userHasCorrectEventTypes(responseBody.data.user.id); await teamHasCorrectEventTypes(team1.id); expect(responseBody.data.user.name).toEqual(requestBody.name); }); - 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 () => { diff --git a/apps/api/v2/src/modules/oauth-clients/services/oauth-clients-users.service.ts b/apps/api/v2/src/modules/oauth-clients/services/oauth-clients-users.service.ts index 83cc989817..a8e57d7a5c 100644 --- a/apps/api/v2/src/modules/oauth-clients/services/oauth-clients-users.service.ts +++ b/apps/api/v2/src/modules/oauth-clients/services/oauth-clients-users.service.ts @@ -83,8 +83,6 @@ export class OAuthClientUsersService { user.defaultScheduleId = defaultSchedule.id; } - await this.organizationsTeamsService.addUserToPlatformTeamEvents(user.id, organizationId, oAuthClientId); - return { user, tokens: { diff --git a/apps/api/v2/src/modules/organizations/controllers/teams/memberships/organizations-teams-memberships.controller.e2e-spec.ts b/apps/api/v2/src/modules/organizations/controllers/teams/memberships/organizations-teams-memberships.controller.e2e-spec.ts index f03a0affdb..9c74cde26e 100644 --- a/apps/api/v2/src/modules/organizations/controllers/teams/memberships/organizations-teams-memberships.controller.e2e-spec.ts +++ b/apps/api/v2/src/modules/organizations/controllers/teams/memberships/organizations-teams-memberships.controller.e2e-spec.ts @@ -9,8 +9,9 @@ import { UsersModule } from "@/modules/users/users.module"; import { INestApplication } from "@nestjs/common"; import { NestExpressApplication } from "@nestjs/platform-express"; import { Test } from "@nestjs/testing"; -import { User } from "@prisma/client"; +import { EventType, 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 { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.repository.fixture"; import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture"; @@ -26,6 +27,7 @@ describe("Organizations Teams Memberships Endpoints", () => { describe("User Authentication - User is Org Admin", () => { let app: INestApplication; + let eventTypesRepositoryFixture: EventTypesRepositoryFixture; let userRepositoryFixture: UserRepositoryFixture; let organizationsRepositoryFixture: OrganizationRepositoryFixture; let teamsRepositoryFixture: TeamRepositoryFixture; @@ -36,6 +38,8 @@ describe("Organizations Teams Memberships Endpoints", () => { let org: Team; let orgTeam: Team; let nonOrgTeam: Team; + let teamEventType: EventType; + let managedEventType: EventType; let membership: Membership; let membership2: Membership; let membershipCreatedViaApi: OrgTeamMembershipOutputDto; @@ -66,6 +70,7 @@ describe("Organizations Teams Memberships Endpoints", () => { profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef); membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef); + eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef); user = await userRepositoryFixture.create({ email: userEmail, @@ -97,6 +102,32 @@ describe("Organizations Teams Memberships Endpoints", () => { parent: { connect: { id: org.id } }, }); + teamEventType = await eventTypesRepositoryFixture.createTeamEventType({ + schedulingType: "COLLECTIVE", + team: { + connect: { id: orgTeam.id }, + }, + title: "Collective Event Type", + slug: "collective-event-type", + length: 30, + assignAllTeamMembers: true, + bookingFields: [], + locations: [], + }); + + managedEventType = await eventTypesRepositoryFixture.createTeamEventType({ + schedulingType: "MANAGED", + team: { + connect: { id: orgTeam.id }, + }, + title: "Managed Event Type", + slug: "managed-event-type", + length: 60, + assignAllTeamMembers: true, + bookingFields: [], + locations: [], + }); + nonOrgTeam = await teamsRepositoryFixture.create({ name: "Non Org Team", isOrganization: false, @@ -264,7 +295,7 @@ describe("Organizations Teams Memberships Endpoints", () => { .expect(404); }); - it("should create the membership of the org's team", async () => { + it("should have created the membership of the org's team and assigned team wide events", async () => { return request(app.getHttpServer()) .post(`/v2/organizations/${org.id}/teams/${orgTeam.id}/memberships`) .send({ @@ -281,9 +312,22 @@ describe("Organizations Teams Memberships Endpoints", () => { expect(membershipCreatedViaApi.role).toEqual("MEMBER"); expect(membershipCreatedViaApi.userId).toEqual(userToInviteViaApi.id); expect(membershipCreatedViaApi.user.email).toEqual(userToInviteViaApi.email); + userHasCorrectEventTypes(membershipCreatedViaApi.userId); }); }); + async function userHasCorrectEventTypes(userId: number) { + const managedEventTypes = await eventTypesRepositoryFixture.getAllUserEventTypes(userId); + const teamEventTypes = await eventTypesRepositoryFixture.getAllTeamEventTypes(orgTeam.id); + expect(managedEventTypes?.length).toEqual(1); + expect(teamEventTypes?.length).toEqual(2); + const collectiveEvenType = teamEventTypes?.find((eventType) => eventType.slug === teamEventType.slug); + expect(collectiveEvenType).toBeTruthy(); + const userHost = collectiveEvenType?.hosts.find((host) => host.userId === userId); + expect(userHost).toBeTruthy(); + expect(managedEventTypes?.find((eventType) => eventType.slug === managedEventType.slug)).toBeTruthy(); + } + it("should fail to create the membership of the org's team for a non org user", async () => { return request(app.getHttpServer()) .post(`/v2/organizations/${org.id}/teams/${orgTeam.id}/memberships`) diff --git a/apps/api/v2/src/modules/organizations/controllers/teams/memberships/organizations-teams-memberships.controller.ts b/apps/api/v2/src/modules/organizations/controllers/teams/memberships/organizations-teams-memberships.controller.ts index 7b01590bd0..dc80b0d2c7 100644 --- a/apps/api/v2/src/modules/organizations/controllers/teams/memberships/organizations-teams-memberships.controller.ts +++ b/apps/api/v2/src/modules/organizations/controllers/teams/memberships/organizations-teams-memberships.controller.ts @@ -15,6 +15,7 @@ import { OrgTeamMembershipsOutputResponseDto, OrgTeamMembershipOutputResponseDto, } from "@/modules/organizations/outputs/organization-teams-memberships.output"; +import { OrganizationsEventTypesService } from "@/modules/organizations/services/event-types/organizations-event-types.service"; import { OrganizationsTeamsMembershipsService } from "@/modules/organizations/services/organizations-teams-memberships.service"; import { Controller, @@ -35,6 +36,7 @@ import { ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger"; import { plainToClass } from "class-transformer"; import { SUCCESS_STATUS } from "@calcom/platform-constants"; +import { updateNewTeamMemberEventTypes } from "@calcom/platform-libraries"; import { SkipTakePagination } from "@calcom/platform-types"; @Controller({ @@ -46,6 +48,7 @@ import { SkipTakePagination } from "@calcom/platform-types"; export class OrganizationsTeamsMembershipsController { constructor( private organizationsTeamsMembershipsService: OrganizationsTeamsMembershipsService, + private oganizationsEventTypesService: OrganizationsEventTypesService, private readonly organizationsRepository: OrganizationsRepository ) {} @@ -112,6 +115,9 @@ export class OrganizationsTeamsMembershipsController { teamId, membershipId ); + + await this.oganizationsEventTypesService.deleteUserTeamEventTypesAndHosts(membership.userId, teamId); + return { status: SUCCESS_STATUS, data: plainToClass(OrgTeamMembershipOutputDto, membership, { strategy: "excludeAll" }), @@ -129,15 +135,25 @@ export class OrganizationsTeamsMembershipsController { @Param("membershipId", ParseIntPipe) membershipId: number, @Body() data: UpdateOrgTeamMembershipDto ): Promise { - const membership = await this.organizationsTeamsMembershipsService.updateOrgTeamMembership( + const currentMembership = await this.organizationsTeamsMembershipsService.getOrgTeamMembership( + orgId, + teamId, + membershipId + ); + const updatedMembership = await this.organizationsTeamsMembershipsService.updateOrgTeamMembership( orgId, teamId, membershipId, data ); + + if (!currentMembership.accepted && updatedMembership.accepted) { + await updateNewTeamMemberEventTypes(updatedMembership.userId, teamId); + } + return { status: SUCCESS_STATUS, - data: plainToClass(OrgTeamMembershipOutputDto, membership, { strategy: "excludeAll" }), + data: plainToClass(OrgTeamMembershipOutputDto, updatedMembership, { strategy: "excludeAll" }), }; } @@ -158,6 +174,9 @@ export class OrganizationsTeamsMembershipsController { } const membership = await this.organizationsTeamsMembershipsService.createOrgTeamMembership(teamId, data); + if (membership.accepted) { + await updateNewTeamMemberEventTypes(user.id, teamId); + } return { status: SUCCESS_STATUS, data: plainToClass(OrgTeamMembershipOutputDto, membership, { strategy: "excludeAll" }), diff --git a/apps/api/v2/src/modules/organizations/controllers/users/organizations-users.e2e-spec.ts b/apps/api/v2/src/modules/organizations/controllers/users/organizations-users.e2e-spec.ts index 3f1205570a..d1ca934384 100644 --- a/apps/api/v2/src/modules/organizations/controllers/users/organizations-users.e2e-spec.ts +++ b/apps/api/v2/src/modules/organizations/controllers/users/organizations-users.e2e-spec.ts @@ -469,34 +469,13 @@ describe("Organizations Users Endpoints", () => { 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 () => { diff --git a/apps/api/v2/src/modules/organizations/repositories/organizations-event-types.repository.ts b/apps/api/v2/src/modules/organizations/repositories/organizations-event-types.repository.ts index 0ea6edbe6a..d00b594956 100644 --- a/apps/api/v2/src/modules/organizations/repositories/organizations-event-types.repository.ts +++ b/apps/api/v2/src/modules/organizations/repositories/organizations-event-types.repository.ts @@ -70,4 +70,26 @@ export class OrganizationsEventTypesRepository { include: { children: true }, }); } + + async deleteUserManagedTeamEventTypes(userId: number, teamId: number) { + return this.dbWrite.prisma.eventType.deleteMany({ + where: { + parent: { + teamId, + }, + userId, + }, + }); + } + + async removeUserFromTeamEventTypesHosts(userId: number, teamId: number) { + return this.dbWrite.prisma.host.deleteMany({ + where: { + userId, + eventType: { + teamId, + }, + }, + }); + } } diff --git a/apps/api/v2/src/modules/organizations/services/event-types/organizations-event-types.service.ts b/apps/api/v2/src/modules/organizations/services/event-types/organizations-event-types.service.ts index 7a6b380fec..e99baaa6c9 100644 --- a/apps/api/v2/src/modules/organizations/services/event-types/organizations-event-types.service.ts +++ b/apps/api/v2/src/modules/organizations/services/event-types/organizations-event-types.service.ts @@ -6,13 +6,15 @@ import { DatabaseTeamEventType } from "@/modules/organizations/services/event-ty import { PrismaWriteService } from "@/modules/prisma/prisma-write.service"; import { UsersService } from "@/modules/users/services/users.service"; import { UserWithProfile } from "@/modules/users/users.repository"; -import { Injectable, NotFoundException } from "@nestjs/common"; +import { Injectable, NotFoundException, Logger } from "@nestjs/common"; import { createEventType, updateEventType } from "@calcom/platform-libraries"; import { InputTeamEventTransformed_2024_06_14 } from "@calcom/platform-types"; @Injectable() export class OrganizationsEventTypesService { + private readonly logger = new Logger("OrganizationsEventTypesService"); + constructor( private readonly eventTypesService: EventTypesService_2024_06_14, private readonly dbWrite: PrismaWriteService, @@ -145,4 +147,17 @@ export class OrganizationsEventTypesService { return this.eventTypesRepository.deleteEventType(eventTypeId); } + + async deleteUserTeamEventTypesAndHosts(userId: number, teamId: number) { + try { + await this.organizationEventTypesRepository.deleteUserManagedTeamEventTypes(userId, teamId); + await this.organizationEventTypesRepository.removeUserFromTeamEventTypesHosts(userId, teamId); + } catch (err) { + this.logger.error("Could not remove user from all team event-types.", { + error: err, + userId, + teamId, + }); + } + } } diff --git a/apps/api/v2/src/modules/organizations/services/organizations-teams.service.ts b/apps/api/v2/src/modules/organizations/services/organizations-teams.service.ts index 9ec2f3edc1..391163a1c1 100644 --- a/apps/api/v2/src/modules/organizations/services/organizations-teams.service.ts +++ b/apps/api/v2/src/modules/organizations/services/organizations-teams.service.ts @@ -5,8 +5,6 @@ import { OrganizationsTeamsRepository } from "@/modules/organizations/repositori import { UserWithProfile } from "@/modules/users/users.repository"; import { Injectable } from "@nestjs/common"; -import { updateNewTeamMemberEventTypes } from "@calcom/platform-libraries"; - @Injectable() export class OrganizationsTeamsService { constructor( @@ -69,23 +67,4 @@ export class OrganizationsTeamsService { } 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); - } - } } diff --git a/apps/api/v2/src/modules/organizations/services/organizations-users-service.ts b/apps/api/v2/src/modules/organizations/services/organizations-users-service.ts index 6a8622c681..3a1003ad85 100644 --- a/apps/api/v2/src/modules/organizations/services/organizations-users-service.ts +++ b/apps/api/v2/src/modules/organizations/services/organizations-users-service.ts @@ -79,8 +79,6 @@ 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,