diff --git a/apps/api/v2/src/ee/platform-endpoints-module.ts b/apps/api/v2/src/ee/platform-endpoints-module.ts index a82c797165..c198fd643c 100644 --- a/apps/api/v2/src/ee/platform-endpoints-module.ts +++ b/apps/api/v2/src/ee/platform-endpoints-module.ts @@ -9,6 +9,7 @@ import { ProviderModule } from "@/ee/provider/provider.module"; import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module"; import { SchedulesModule_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/schedules.module"; import { SlotsModule } from "@/modules/slots/slots.module"; +import { TeamsEventTypesModule } from "@/modules/teams/event-types/teams-event-types.module"; import { TeamsModule } from "@/modules/teams/teams/teams.module"; import { TeamsMembershipsModule } from "@/modules/teams/memberships/teams-memberships.module"; import type { MiddlewareConsumer, NestModule } from "@nestjs/common"; @@ -20,6 +21,7 @@ import { Module } from "@nestjs/common"; ProviderModule, SchedulesModule_2024_04_15, SchedulesModule_2024_06_11, + TeamsEventTypesModule, MeModule, EventTypesModule_2024_04_15, EventTypesModule_2024_06_14, diff --git a/apps/api/v2/src/modules/atoms/atoms.module.ts b/apps/api/v2/src/modules/atoms/atoms.module.ts index 47c4065c20..7b55b27910 100644 --- a/apps/api/v2/src/modules/atoms/atoms.module.ts +++ b/apps/api/v2/src/modules/atoms/atoms.module.ts @@ -8,12 +8,13 @@ import { CredentialsRepository } from "@/modules/credentials/credentials.reposit import { MembershipsRepository } from "@/modules/memberships/memberships.repository"; import { OrganizationsModule } from "@/modules/organizations/organizations.module"; import { PrismaModule } from "@/modules/prisma/prisma.module"; +import { TeamsEventTypesModule } from "@/modules/teams/event-types/teams-event-types.module"; import { UsersService } from "@/modules/users/services/users.service"; import { UsersRepository } from "@/modules/users/users.repository"; import { Module } from "@nestjs/common"; @Module({ - imports: [PrismaModule, EventTypesModule_2024_06_14, OrganizationsModule], + imports: [PrismaModule, EventTypesModule_2024_06_14, OrganizationsModule, TeamsEventTypesModule], providers: [ EventTypesAtomService, ConferencingAtomsService, diff --git a/apps/api/v2/src/modules/atoms/services/event-types-atom.service.ts b/apps/api/v2/src/modules/atoms/services/event-types-atom.service.ts index a7bb974cdc..6a22323c7e 100644 --- a/apps/api/v2/src/modules/atoms/services/event-types-atom.service.ts +++ b/apps/api/v2/src/modules/atoms/services/event-types-atom.service.ts @@ -2,10 +2,9 @@ import { EventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_ import { AtomsRepository } from "@/modules/atoms/atoms.repository"; import { CredentialsRepository } from "@/modules/credentials/credentials.repository"; import { MembershipsRepository } from "@/modules/memberships/memberships.repository"; -import { OrganizationsEventTypesService } from "@/modules/organizations/services/event-types/organizations-event-types.service"; +import { TeamsEventTypesService } from "@/modules/teams/event-types/services/teams-event-types.service"; import { PrismaReadService } from "@/modules/prisma/prisma-read.service"; import { PrismaWriteService } from "@/modules/prisma/prisma-write.service"; -import { TeamsRepository } from "@/modules/teams/teams/teams.repository"; import { UsersService } from "@/modules/users/services/users.service"; import { UserWithProfile } from "@/modules/users/users.repository"; import { Injectable, NotFoundException, ForbiddenException } from "@nestjs/common"; @@ -50,7 +49,7 @@ export class EventTypesAtomService { private readonly dbWrite: PrismaWriteService, private readonly dbRead: PrismaReadService, private readonly eventTypeService: EventTypesService_2024_06_14, - private readonly teamEventTypeService: OrganizationsEventTypesService + private readonly teamEventTypeService: TeamsEventTypesService ) {} async getUserEventType(user: UserWithProfile, eventTypeId: number) { diff --git a/apps/api/v2/src/modules/organizations/controllers/event-types/organizations-event-types.controller.ts b/apps/api/v2/src/modules/organizations/controllers/event-types/organizations-event-types.controller.ts index 72e68ea5c5..57170c7a64 100644 --- a/apps/api/v2/src/modules/organizations/controllers/event-types/organizations-event-types.controller.ts +++ b/apps/api/v2/src/modules/organizations/controllers/event-types/organizations-event-types.controller.ts @@ -1,6 +1,5 @@ import { CreatePhoneCallInput } from "@/ee/event-types/event-types_2024_06_14/inputs/create-phone-call.input"; import { CreatePhoneCallOutput } from "@/ee/event-types/event-types_2024_06_14/outputs/create-phone-call.output"; -import { InputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/input-event-types.service"; import { API_VERSIONS_VALUES } from "@/lib/api-versions"; import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator"; import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator"; @@ -11,15 +10,15 @@ import { IsAdminAPIEnabledGuard } from "@/modules/auth/guards/organizations/is-a 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 { CreateTeamEventTypeOutput } from "@/modules/organizations/controllers/event-types/outputs/teams/create-team-event-type.output"; -import { DeleteTeamEventTypeOutput } from "@/modules/organizations/controllers/event-types/outputs/teams/delete-team-event-type.output"; -import { GetTeamEventTypeOutput } from "@/modules/organizations/controllers/event-types/outputs/teams/get-team-event-type.output"; -import { GetTeamEventTypesOutput } from "@/modules/organizations/controllers/event-types/outputs/teams/get-team-event-types.output"; -import { UpdateTeamEventTypeOutput } from "@/modules/organizations/controllers/event-types/outputs/teams/update-team-event-type.output"; import { OutputTeamEventTypesResponsePipe } from "@/modules/organizations/controllers/pipes/event-types/team-event-types-response.transformer"; import { InputOrganizationsEventTypesService } from "@/modules/organizations/services/event-types/input.service"; import { OrganizationsEventTypesService } from "@/modules/organizations/services/event-types/organizations-event-types.service"; import { DatabaseTeamEventType } from "@/modules/organizations/services/event-types/output.service"; +import { CreateTeamEventTypeOutput } from "@/modules/teams/event-types/outputs/create-team-event-type.output"; +import { DeleteTeamEventTypeOutput } from "@/modules/teams/event-types/outputs/delete-team-event-type.output"; +import { GetTeamEventTypeOutput } from "@/modules/teams/event-types/outputs/get-team-event-type.output"; +import { GetTeamEventTypesOutput } from "@/modules/teams/event-types/outputs/get-team-event-types.output"; +import { UpdateTeamEventTypeOutput } from "@/modules/teams/event-types/outputs/update-team-event-type.output"; import { UserWithProfile } from "@/modules/users/users.repository"; import { Controller, @@ -62,7 +61,6 @@ export class OrganizationsEventTypesController { constructor( private readonly organizationsEventTypesService: OrganizationsEventTypesService, private readonly inputService: InputOrganizationsEventTypesService, - private readonly inputUserEventTypesService: InputEventTypesService_2024_06_14, private readonly outputTeamEventTypesResponsePipe: OutputTeamEventTypesResponsePipe ) {} @@ -180,7 +178,11 @@ export class OrganizationsEventTypesController { @Query() queryParams: SkipTakePagination ): Promise { const { skip, take } = queryParams; - const eventTypes = await this.organizationsEventTypesService.getTeamsEventTypes(orgId, skip, take); + const eventTypes = await this.organizationsEventTypesService.getOrganizationsTeamsEventTypes( + orgId, + skip, + take + ); return { status: SUCCESS_STATUS, 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 17a0a8f7e8..523a3b89ed 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,8 +15,8 @@ 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 { TeamsEventTypesService } from "@/modules/teams/event-types/services/teams-event-types.service"; import { Controller, UseGuards, @@ -51,7 +51,7 @@ export class OrganizationsTeamsMembershipsController { constructor( private organizationsTeamsMembershipsService: OrganizationsTeamsMembershipsService, - private oganizationsEventTypesService: OrganizationsEventTypesService, + private teamsEventTypesService: TeamsEventTypesService, private readonly organizationsRepository: OrganizationsRepository ) {} @@ -119,7 +119,7 @@ export class OrganizationsTeamsMembershipsController { membershipId ); - await this.oganizationsEventTypesService.deleteUserTeamEventTypesAndHosts(membership.userId, teamId); + await this.teamsEventTypesService.deleteUserTeamEventTypesAndHosts(membership.userId, teamId); return { status: SUCCESS_STATUS, diff --git a/apps/api/v2/src/modules/organizations/organizations.module.ts b/apps/api/v2/src/modules/organizations/organizations.module.ts index f82de9b164..3a708a0d71 100644 --- a/apps/api/v2/src/modules/organizations/organizations.module.ts +++ b/apps/api/v2/src/modules/organizations/organizations.module.ts @@ -39,6 +39,7 @@ import { OrganizationsService } from "@/modules/organizations/services/organizat import { PrismaModule } from "@/modules/prisma/prisma.module"; import { RedisModule } from "@/modules/redis/redis.module"; import { StripeModule } from "@/modules/stripe/stripe.module"; +import { TeamsEventTypesModule } from "@/modules/teams/event-types/teams-event-types.module"; import { TeamsModule } from "@/modules/teams/teams/teams.module"; import { UsersModule } from "@/modules/users/users.module"; import { WebhooksService } from "@/modules/webhooks/services/webhooks.service"; @@ -54,6 +55,7 @@ import { Module } from "@nestjs/common"; RedisModule, EmailModule, EventTypesModule_2024_06_14, + TeamsEventTypesModule, TeamsModule, ], providers: [ 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 4db11de61c..5d337bc577 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 @@ -1,91 +1,10 @@ import { PrismaReadService } from "@/modules/prisma/prisma-read.service"; -import { PrismaWriteService } from "@/modules/prisma/prisma-write.service"; import { Injectable } from "@nestjs/common"; @Injectable() export class OrganizationsEventTypesRepository { - constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {} - - async getTeamEventType(teamId: number, eventTypeId: number) { - return this.dbRead.prisma.eventType.findUnique({ - where: { - id: eventTypeId, - teamId, - }, - include: { users: true, schedule: true, hosts: true, destinationCalendar: true }, - }); - } - - async getTeamEventTypeBySlug(teamId: number, eventTypeSlug: string) { - return this.dbRead.prisma.eventType.findUnique({ - where: { - teamId_slug: { - teamId, - slug: eventTypeSlug, - }, - }, - include: { - users: true, - schedule: true, - hosts: true, - destinationCalendar: true, - team: { - select: { - bannerUrl: true, - name: true, - logoUrl: true, - slug: true, - weekStart: true, - brandColor: true, - darkBrandColor: true, - theme: true, - }, - }, - }, - }); - } - - async getTeamEventTypes(teamId: number) { - return this.dbRead.prisma.eventType.findMany({ - where: { - teamId, - }, - include: { - users: true, - schedule: true, - hosts: true, - destinationCalendar: true, - team: { - select: { - bannerUrl: true, - name: true, - logoUrl: true, - slug: true, - weekStart: true, - brandColor: true, - darkBrandColor: true, - theme: true, - }, - }, - }, - }); - } - - async getEventTypeById(eventTypeId: number) { - return this.dbRead.prisma.eventType.findUnique({ - where: { id: eventTypeId }, - include: { users: true, schedule: true, hosts: true, destinationCalendar: true }, - }); - } - - async getEventTypeChildren(eventTypeId: number) { - return this.dbRead.prisma.eventType.findMany({ - where: { parentId: eventTypeId }, - include: { users: true, schedule: true, hosts: true, destinationCalendar: true }, - }); - } - - async getTeamsEventTypes(orgId: number, skip: number, take: number) { + constructor(private readonly dbRead: PrismaReadService) {} + async getOrganizationTeamsEventTypes(orgId: number, skip: number, take: number) { return this.dbRead.prisma.eventType.findMany({ where: { team: { @@ -97,33 +16,4 @@ export class OrganizationsEventTypesRepository { include: { users: true, schedule: true, hosts: true, destinationCalendar: true }, }); } - - async getEventTypeByIdWithChildren(eventTypeId: number) { - return this.dbRead.prisma.eventType.findUnique({ - where: { id: eventTypeId }, - 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/input.service.ts b/apps/api/v2/src/modules/organizations/services/event-types/input.service.ts index f95c8ae402..430fbb85f1 100644 --- a/apps/api/v2/src/modules/organizations/services/event-types/input.service.ts +++ b/apps/api/v2/src/modules/organizations/services/event-types/input.service.ts @@ -1,5 +1,5 @@ import { InputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/input-event-types.service"; -import { OrganizationsEventTypesRepository } from "@/modules/organizations/repositories/organizations-event-types.repository"; +import { TeamsEventTypesRepository } from "@/modules/teams/event-types/teams-event-types.repository"; import { TeamsRepository } from "@/modules/teams/teams/teams.repository"; import { UsersRepository } from "@/modules/users/users.repository"; import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common"; @@ -17,7 +17,7 @@ export class InputOrganizationsEventTypesService { private readonly inputEventTypesService: InputEventTypesService_2024_06_14, private readonly teamsRepository: TeamsRepository, private readonly usersRepository: UsersRepository, - private readonly orgEventTypesRepository: OrganizationsEventTypesRepository + private readonly teamsEventTypesRepository: TeamsEventTypesRepository ) {} async transformAndValidateCreateTeamEventTypeInput( userId: number, @@ -115,7 +115,7 @@ export class InputOrganizationsEventTypesService { const { hosts, assignAllTeamMembers, ...rest } = inputEventType; const eventType = await this.inputEventTypesService.transformInputUpdateEventType(rest, eventTypeId); - const dbEventType = await this.orgEventTypesRepository.getTeamEventType(teamId, eventTypeId); + const dbEventType = await this.teamsEventTypesRepository.getTeamEventType(teamId, eventTypeId); if (!dbEventType) { throw new BadRequestException("Event type to update not found"); @@ -144,7 +144,7 @@ export class InputOrganizationsEventTypesService { ) { let eventType = null; if (eventTypeId) { - eventType = await this.orgEventTypesRepository.getEventTypeByIdWithChildren(eventTypeId); + eventType = await this.teamsEventTypesRepository.getEventTypeByIdWithChildren(eventTypeId); if (!eventType || eventType.schedulingType !== "MANAGED") { return undefined; } 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 e99baaa6c9..7a62972ff9 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 @@ -1,14 +1,13 @@ -import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository"; -import { EventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/event-types.service"; import { MembershipsRepository } from "@/modules/memberships/memberships.repository"; import { OrganizationsEventTypesRepository } from "@/modules/organizations/repositories/organizations-event-types.repository"; import { DatabaseTeamEventType } from "@/modules/organizations/services/event-types/output.service"; import { PrismaWriteService } from "@/modules/prisma/prisma-write.service"; +import { TeamsEventTypesService } from "@/modules/teams/event-types/services/teams-event-types.service"; import { UsersService } from "@/modules/users/services/users.service"; import { UserWithProfile } from "@/modules/users/users.repository"; -import { Injectable, NotFoundException, Logger } from "@nestjs/common"; +import { Injectable, Logger } from "@nestjs/common"; -import { createEventType, updateEventType } from "@calcom/platform-libraries"; +import { createEventType } from "@calcom/platform-libraries"; import { InputTeamEventTransformed_2024_06_14 } from "@calcom/platform-types"; @Injectable() @@ -16,10 +15,9 @@ export class OrganizationsEventTypesService { private readonly logger = new Logger("OrganizationsEventTypesService"); constructor( - private readonly eventTypesService: EventTypesService_2024_06_14, private readonly dbWrite: PrismaWriteService, private readonly organizationEventTypesRepository: OrganizationsEventTypesRepository, - private readonly eventTypesRepository: EventTypesRepository_2024_06_14, + private readonly teamsEventTypesService: TeamsEventTypesService, private readonly membershipsRepository: MembershipsRepository, private readonly usersService: UsersService ) {} @@ -44,15 +42,7 @@ export class OrganizationsEventTypesService { }, }); - return this.updateTeamEventType(eventTypeCreated.id, teamId, body, user); - } - - async validateEventTypeExists(teamId: number, eventTypeId: number) { - const eventType = await this.organizationEventTypesRepository.getTeamEventType(teamId, eventTypeId); - - if (!eventType) { - throw new NotFoundException(`Event type with id ${eventTypeId} not found`); - } + return this.teamsEventTypesService.updateTeamEventType(eventTypeCreated.id, teamId, body, user); } async getUserToCreateTeamEvent(user: UserWithProfile, organizationId: number) { @@ -71,34 +61,23 @@ export class OrganizationsEventTypesService { } async getTeamEventType(teamId: number, eventTypeId: number): Promise { - const eventType = await this.organizationEventTypesRepository.getTeamEventType(teamId, eventTypeId); - - if (!eventType) { - return null; - } - - return eventType; + return this.teamsEventTypesService.getTeamEventType(teamId, eventTypeId); } async getTeamEventTypeBySlug(teamId: number, eventTypeSlug: string): Promise { - const eventType = await this.organizationEventTypesRepository.getTeamEventTypeBySlug( - teamId, - eventTypeSlug - ); - - if (!eventType) { - return null; - } - - return eventType; + return this.teamsEventTypesService.getTeamEventTypeBySlug(teamId, eventTypeSlug); } async getTeamEventTypes(teamId: number): Promise { - return await this.organizationEventTypesRepository.getTeamEventTypes(teamId); + return await this.teamsEventTypesService.getTeamEventTypes(teamId); } - async getTeamsEventTypes(orgId: number, skip = 0, take = 250): Promise { - return await this.organizationEventTypesRepository.getTeamsEventTypes(orgId, skip, take); + async getOrganizationsTeamsEventTypes( + orgId: number, + skip = 0, + take = 250 + ): Promise { + return await this.organizationEventTypesRepository.getOrganizationTeamsEventTypes(orgId, skip, take); } async updateTeamEventType( @@ -107,57 +86,10 @@ export class OrganizationsEventTypesService { body: InputTeamEventTransformed_2024_06_14, user: UserWithProfile ): Promise { - await this.validateEventTypeExists(teamId, eventTypeId); - const eventTypeUser = await this.eventTypesService.getUserToUpdateEvent(user); - - await updateEventType({ - input: { id: eventTypeId, ...body }, - ctx: { - user: eventTypeUser, - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - prisma: this.dbWrite.prisma, - }, - }); - - const eventType = await this.organizationEventTypesRepository.getEventTypeById(eventTypeId); - - if (!eventType) { - throw new NotFoundException(`Event type with id ${eventTypeId} not found`); - } - - if (eventType.schedulingType !== "MANAGED") { - return eventType; - } - - const childrenEventTypes = await this.organizationEventTypesRepository.getEventTypeChildren(eventType.id); - - return [eventType, ...childrenEventTypes]; + return this.teamsEventTypesService.updateTeamEventType(eventTypeId, teamId, body, user); } async deleteTeamEventType(teamId: number, eventTypeId: number) { - const existingEventType = await this.organizationEventTypesRepository.getTeamEventType( - teamId, - eventTypeId - ); - - if (!existingEventType) { - throw new NotFoundException(`Event type with ID=${eventTypeId} does not exist.`); - } - - 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, - }); - } + return this.teamsEventTypesService.deleteTeamEventType(teamId, eventTypeId); } } diff --git a/apps/api/v2/src/modules/organizations/services/event-types/output.service.ts b/apps/api/v2/src/modules/organizations/services/event-types/output.service.ts index 213a21c433..d4ae9a5f93 100644 --- a/apps/api/v2/src/modules/organizations/services/event-types/output.service.ts +++ b/apps/api/v2/src/modules/organizations/services/event-types/output.service.ts @@ -1,5 +1,5 @@ import { OutputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/output-event-types.service"; -import { OrganizationsEventTypesRepository } from "@/modules/organizations/repositories/organizations-event-types.repository"; +import { TeamsEventTypesRepository } from "@/modules/teams/event-types/teams-event-types.repository"; import { UsersRepository } from "@/modules/users/users.repository"; import { Injectable } from "@nestjs/common"; import type { EventType, User, Schedule, Host, DestinationCalendar } from "@prisma/client"; @@ -77,7 +77,7 @@ type Input = Pick< export class OutputOrganizationsEventTypesService { constructor( private readonly outputEventTypesService: OutputEventTypesService_2024_06_14, - private readonly organizationEventTypesRepository: OrganizationsEventTypesRepository, + private readonly teamsEventTypesRepository: TeamsEventTypesRepository, private readonly usersRepository: UsersRepository ) {} @@ -117,7 +117,7 @@ export class OutputOrganizationsEventTypesService { } async getManagedEventTypeHosts(eventTypeId: number) { - const children = await this.organizationEventTypesRepository.getEventTypeChildren(eventTypeId); + const children = await this.teamsEventTypesRepository.getEventTypeChildren(eventTypeId); const transformedHosts: TeamEventTypeResponseHost[] = []; for (const child of children) { if (child.userId) { diff --git a/apps/api/v2/src/modules/teams/event-types/controllers/teams-event-types.controller.e2e-spec.ts b/apps/api/v2/src/modules/teams/event-types/controllers/teams-event-types.controller.e2e-spec.ts new file mode 100644 index 0000000000..5112dba72d --- /dev/null +++ b/apps/api/v2/src/modules/teams/event-types/controllers/teams-event-types.controller.e2e-spec.ts @@ -0,0 +1,570 @@ +import { bootstrap } from "@/app"; +import { AppModule } from "@/app.module"; +import { PrismaModule } from "@/modules/prisma/prisma.module"; +import { TokensModule } from "@/modules/tokens/tokens.module"; +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 * 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"; +import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture"; +import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture"; +import { randomNumber } from "test/utils/randomNumber"; +import { withApiAuth } from "test/utils/withApiAuth"; + +import { SUCCESS_STATUS } from "@calcom/platform-constants"; +import { + BookingWindowPeriodInputTypeEnum_2024_06_14, + BookerLayoutsInputEnum_2024_06_14, + ConfirmationPolicyEnum, + NoticeThresholdUnitEnum, +} from "@calcom/platform-enums"; +import { + ApiSuccessResponse, + CreateTeamEventTypeInput_2024_06_14, + Host, + TeamEventTypeOutput_2024_06_14, + UpdateTeamEventTypeInput_2024_06_14, +} from "@calcom/platform-types"; +import { Team } from "@calcom/prisma/client"; + +describe("Organizations Event Types Endpoints", () => { + describe("User Authentication - User is Org Admin", () => { + let app: INestApplication; + + let userRepositoryFixture: UserRepositoryFixture; + let organizationsRepositoryFixture: OrganizationRepositoryFixture; + let teamsRepositoryFixture: TeamRepositoryFixture; + let membershipsRepositoryFixture: MembershipRepositoryFixture; + let profileRepositoryFixture: ProfileRepositoryFixture; + let eventTypesRepositoryFixture: EventTypesRepositoryFixture; + + let team: Team; + let falseTestTeam: Team; + + const userEmail = `auth-${randomNumber()}@api.com`; + let userAdmin: User; + + const teammate1Email = "teammate111@team.com"; + const teammate2Email = "teammate221@team.com"; + const falseTestUserEmail = "false-user@false-team.com"; + let teamMember1: User; + let teamMember2: User; + let falseTestUser: User; + + let collectiveEventType: TeamEventTypeOutput_2024_06_14; + let managedEventType: TeamEventTypeOutput_2024_06_14; + + beforeAll(async () => { + const moduleRef = await withApiAuth( + userEmail, + Test.createTestingModule({ + imports: [AppModule, PrismaModule, UsersModule, TokensModule], + }) + ).compile(); + + userRepositoryFixture = new UserRepositoryFixture(moduleRef); + organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef); + teamsRepositoryFixture = new TeamRepositoryFixture(moduleRef); + membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef); + profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef); + eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef); + + userAdmin = await userRepositoryFixture.create({ + email: userEmail, + username: userEmail, + role: "ADMIN", + }); + + teamMember1 = await userRepositoryFixture.create({ + email: teammate1Email, + username: teammate1Email, + }); + + teamMember2 = await userRepositoryFixture.create({ + email: teammate2Email, + username: teammate2Email, + }); + + falseTestUser = await userRepositoryFixture.create({ + email: falseTestUserEmail, + username: falseTestUserEmail, + }); + + team = await teamsRepositoryFixture.create({ + name: "Test org team", + isOrganization: false, + }); + + falseTestTeam = await teamsRepositoryFixture.create({ + name: "Outside org team", + isOrganization: false, + }); + + await membershipsRepositoryFixture.create({ + role: "ADMIN", + user: { connect: { id: userAdmin.id } }, + team: { connect: { id: team.id } }, + accepted: true, + }); + + await membershipsRepositoryFixture.create({ + role: "MEMBER", + user: { connect: { id: teamMember1.id } }, + team: { connect: { id: team.id } }, + accepted: true, + }); + + await membershipsRepositoryFixture.create({ + role: "MEMBER", + user: { connect: { id: teamMember2.id } }, + team: { connect: { id: team.id } }, + accepted: true, + }); + + await membershipsRepositoryFixture.create({ + role: "MEMBER", + user: { connect: { id: falseTestUser.id } }, + team: { connect: { id: falseTestTeam.id } }, + accepted: true, + }); + + app = moduleRef.createNestApplication(); + bootstrap(app as NestExpressApplication); + + await app.init(); + }); + + it("should not be able to create event-type for user outside team", async () => { + const userId = falseTestUser.id; + + const body: CreateTeamEventTypeInput_2024_06_14 = { + title: "Coding consultation", + slug: "coding-consultation", + description: "Our team will review your codebase.", + lengthInMinutes: 60, + locations: [ + { + type: "integration", + integration: "cal-video", + }, + ], + schedulingType: "COLLECTIVE", + hosts: [ + { + userId, + mandatory: true, + priority: "high", + }, + ], + }; + + return request(app.getHttpServer()).post(`/v2/teams/${team.id}/event-types`).send(body).expect(404); + }); + + it("should create a collective team event-type", async () => { + const body: CreateTeamEventTypeInput_2024_06_14 = { + title: "Coding consultation collective", + slug: "coding-consultation collective", + description: "Our team will review your codebase.", + lengthInMinutes: 60, + locations: [ + { + type: "integration", + integration: "cal-video", + }, + ], + bookingFields: [ + { + type: "select", + label: "select which language is your codebase in", + slug: "select-language", + required: true, + placeholder: "select language", + options: ["javascript", "python", "cobol"], + }, + ], + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + schedulingType: "collective", + hosts: [ + { + userId: teamMember1.id, + }, + { + userId: teamMember2.id, + }, + ], + bookingLimitsCount: { + day: 2, + week: 5, + }, + onlyShowFirstAvailableSlot: true, + bookingLimitsDuration: { + day: 60, + week: 100, + }, + offsetStart: 30, + bookingWindow: { + type: BookingWindowPeriodInputTypeEnum_2024_06_14.calendarDays, + value: 30, + rolling: true, + }, + bookerLayouts: { + enabledLayouts: [ + BookerLayoutsInputEnum_2024_06_14.column, + BookerLayoutsInputEnum_2024_06_14.month, + BookerLayoutsInputEnum_2024_06_14.week, + ], + defaultLayout: BookerLayoutsInputEnum_2024_06_14.month, + }, + + confirmationPolicy: { + type: ConfirmationPolicyEnum.TIME, + noticeThreshold: { + count: 60, + unit: NoticeThresholdUnitEnum.MINUTES, + }, + blockUnconfirmedBookingsInBooker: true, + }, + requiresBookerEmailVerification: true, + hideCalendarNotes: true, + hideCalendarEventDetails: true, + lockTimeZoneToggleOnBookingPage: true, + color: { + darkThemeHex: "#292929", + lightThemeHex: "#fafafa", + }, + }; + + return request(app.getHttpServer()) + .post(`/v2/teams/${team.id}/event-types`) + .send(body) + .expect(201) + .then((response) => { + const responseBody: ApiSuccessResponse = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + + 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]); + expect(data.bookingLimitsCount).toEqual(body.bookingLimitsCount); + expect(data.onlyShowFirstAvailableSlot).toEqual(body.onlyShowFirstAvailableSlot); + expect(data.bookingLimitsDuration).toEqual(body.bookingLimitsDuration); + expect(data.offsetStart).toEqual(body.offsetStart); + expect(data.bookingWindow).toEqual(body.bookingWindow); + expect(data.bookerLayouts).toEqual(body.bookerLayouts); + expect(data.confirmationPolicy).toEqual(body.confirmationPolicy); + expect(data.requiresBookerEmailVerification).toEqual(body.requiresBookerEmailVerification); + expect(data.hideCalendarNotes).toEqual(body.hideCalendarNotes); + expect(data.hideCalendarEventDetails).toEqual(body.hideCalendarEventDetails); + expect(data.lockTimeZoneToggleOnBookingPage).toEqual(body.lockTimeZoneToggleOnBookingPage); + expect(data.color).toEqual(body.color); + + collectiveEventType = responseBody.data; + }); + }); + + it("should create a managed team event-type", async () => { + const body: CreateTeamEventTypeInput_2024_06_14 = { + title: "Coding consultation managed", + slug: "coding-consultation-managed", + description: "Our team will review your codebase.", + lengthInMinutes: 60, + locations: [ + { + type: "integration", + integration: "cal-video", + }, + ], + schedulingType: "MANAGED", + hosts: [ + { + userId: teamMember1.id, + mandatory: true, + priority: "high", + }, + { + userId: teamMember2.id, + mandatory: false, + priority: "low", + }, + ], + }; + + return request(app.getHttpServer()) + .post(`/v2/teams/${team.id}/event-types`) + .send(body) + .expect(201) + .then(async (response) => { + const responseBody: ApiSuccessResponse = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + + const data = responseBody.data; + expect(data.length).toEqual(3); + + const teammate1EventTypes = await eventTypesRepositoryFixture.getAllUserEventTypes(teamMember1.id); + const teammate2EventTypes = await eventTypesRepositoryFixture.getAllUserEventTypes(teamMember2.id); + const teamEventTypes = await eventTypesRepositoryFixture.getAllTeamEventTypes(team.id); + + expect(teammate1EventTypes.length).toEqual(1); + expect(teammate2EventTypes.length).toEqual(1); + expect(teamEventTypes.filter((eventType) => eventType.schedulingType === "MANAGED").length).toEqual( + 1 + ); + + const responseTeamEvent = responseBody.data.find((event) => event.teamId === team.id); + expect(responseTeamEvent).toBeDefined(); + if (!responseTeamEvent) { + throw new Error("Team event not found"); + } + + const responseTeammate1Event = responseBody.data.find((event) => event.ownerId === teamMember1.id); + expect(responseTeammate1Event).toBeDefined(); + expect(responseTeammate1Event?.parentEventTypeId).toEqual(responseTeamEvent?.id); + + const responseTeammate2Event = responseBody.data.find((event) => event.ownerId === teamMember2.id); + expect(responseTeammate2Event).toBeDefined(); + expect(responseTeammate2Event?.parentEventTypeId).toEqual(responseTeamEvent?.id); + + managedEventType = responseTeamEvent; + }); + }); + + it("should not get a non existing event-type", async () => { + return request(app.getHttpServer()).get(`/v2/teams/${team.id}/event-types/999999`).expect(404); + }); + + it("should get a team event-type", async () => { + return request(app.getHttpServer()) + .get(`/v2/teams/${team.id}/event-types/${collectiveEventType.id}`) + .expect(200) + .then((response) => { + const responseBody: ApiSuccessResponse = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + + const data = responseBody.data; + expect(data.title).toEqual(collectiveEventType.title); + expect(data.hosts.length).toEqual(2); + evaluateHost(collectiveEventType.hosts[0], data.hosts[0]); + evaluateHost(collectiveEventType.hosts[1], data.hosts[1]); + + collectiveEventType = responseBody.data; + }); + }); + + it("should get team event-types", async () => { + return request(app.getHttpServer()) + .get(`/v2/teams/${team.id}/event-types`) + .expect(200) + .then((response) => { + const responseBody: ApiSuccessResponse = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + + const data = responseBody.data; + expect(data.length).toEqual(2); + + const eventTypeCollective = data.find((eventType) => eventType.schedulingType === "COLLECTIVE"); + const eventTypeManaged = data.find((eventType) => eventType.schedulingType === "MANAGED"); + + expect(eventTypeCollective?.title).toEqual(collectiveEventType.title); + expect(eventTypeCollective?.hosts.length).toEqual(2); + + expect(eventTypeManaged?.title).toEqual(managedEventType.title); + expect(eventTypeManaged?.hosts.length).toEqual(2); + evaluateHost(collectiveEventType.hosts[0], eventTypeCollective?.hosts[0]); + evaluateHost(collectiveEventType.hosts[1], eventTypeCollective?.hosts[1]); + }); + }); + + it("should not be able to update non existing event-type", async () => { + const body: UpdateTeamEventTypeInput_2024_06_14 = { + title: "Clean code consultation", + }; + + return request(app.getHttpServer()) + .patch(`/v2/teams/${team.id}/event-types/999999`) + .send(body) + .expect(400); + }); + + it("should update collective event-type", async () => { + const newHosts: UpdateTeamEventTypeInput_2024_06_14["hosts"] = [ + { + userId: teamMember1.id, + }, + ]; + + const body: UpdateTeamEventTypeInput_2024_06_14 = { + hosts: newHosts, + }; + + return request(app.getHttpServer()) + .patch(`/v2/teams/${team.id}/event-types/${collectiveEventType.id}`) + .send(body) + .expect(200) + .then((response) => { + const responseBody: ApiSuccessResponse = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + + const eventType = responseBody.data; + expect(eventType.title).toEqual(collectiveEventType.title); + expect(eventType.hosts.length).toEqual(1); + evaluateHost(eventType.hosts[0], newHosts[0]); + }); + }); + + it("should update managed event-type", async () => { + const newTitle = "Coding consultation managed updated"; + const newHosts: UpdateTeamEventTypeInput_2024_06_14["hosts"] = [ + { + userId: teamMember1.id, + mandatory: true, + priority: "medium", + }, + ]; + + const body: UpdateTeamEventTypeInput_2024_06_14 = { + title: newTitle, + hosts: newHosts, + }; + + return request(app.getHttpServer()) + .patch(`/v2/teams/${team.id}/event-types/${managedEventType.id}`) + .send(body) + .expect(200) + .then(async (response) => { + const responseBody: ApiSuccessResponse = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + + const data = responseBody.data; + expect(data.length).toEqual(2); + + const teammate1EventTypes = await eventTypesRepositoryFixture.getAllUserEventTypes(teamMember1.id); + const teammate2EventTypes = await eventTypesRepositoryFixture.getAllUserEventTypes(teamMember2.id); + const teamEventTypes = await eventTypesRepositoryFixture.getAllTeamEventTypes(team.id); + const managedTeamEventTypes = teamEventTypes.filter( + (eventType) => eventType.schedulingType === "MANAGED" + ); + + expect(teammate1EventTypes.length).toEqual(1); + expect(teammate1EventTypes[0].title).toEqual(newTitle); + expect(teammate2EventTypes.length).toEqual(0); + expect(managedTeamEventTypes.length).toEqual(1); + expect(managedTeamEventTypes[0].assignAllTeamMembers).toEqual(false); + expect( + teamEventTypes.filter((eventType) => eventType.schedulingType === "MANAGED")?.[0]?.title + ).toEqual(newTitle); + + const responseTeamEvent = responseBody.data.find( + (eventType) => eventType.schedulingType === "MANAGED" + ); + expect(responseTeamEvent).toBeDefined(); + expect(responseTeamEvent?.title).toEqual(newTitle); + expect(responseTeamEvent?.assignAllTeamMembers).toEqual(false); + + const responseTeammate1Event = responseBody.data.find( + (eventType) => eventType.ownerId === teamMember1.id + ); + expect(responseTeammate1Event).toBeDefined(); + expect(responseTeammate1Event?.title).toEqual(newTitle); + + managedEventType = responseBody.data[0]; + }); + }); + + it("should assign all members to managed event-type", async () => { + const body: UpdateTeamEventTypeInput_2024_06_14 = { + assignAllTeamMembers: true, + }; + + return request(app.getHttpServer()) + .patch(`/v2/teams/${team.id}/event-types/${managedEventType.id}`) + .send(body) + .expect(200) + .then(async (response) => { + const responseBody: ApiSuccessResponse = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + + const data = responseBody.data; + // note(Lauris): we expect 4 because we have 2 team members, 1 team admin and 4th is the event object itself. + expect(data.length).toEqual(4); + + const teammate1EventTypes = await eventTypesRepositoryFixture.getAllUserEventTypes(teamMember1.id); + const teammate2EventTypes = await eventTypesRepositoryFixture.getAllUserEventTypes(teamMember2.id); + const teamEventTypes = await eventTypesRepositoryFixture.getAllTeamEventTypes(team.id); + const managedTeamEventTypes = teamEventTypes.filter( + (eventType) => eventType.schedulingType === "MANAGED" + ); + + expect(teammate1EventTypes.length).toEqual(1); + expect(teammate2EventTypes.length).toEqual(1); + expect(managedTeamEventTypes.length).toEqual(1); + expect(managedTeamEventTypes[0].assignAllTeamMembers).toEqual(true); + + const responseTeamEvent = responseBody.data.find( + (eventType) => eventType.schedulingType === "MANAGED" + ); + expect(responseTeamEvent).toBeDefined(); + expect(responseTeamEvent?.teamId).toEqual(team.id); + expect(responseTeamEvent?.assignAllTeamMembers).toEqual(true); + + const responseTeammate1Event = responseBody.data.find( + (eventType) => eventType.ownerId === teamMember1.id + ); + expect(responseTeammate1Event).toBeDefined(); + expect(responseTeammate1Event?.parentEventTypeId).toEqual(responseTeamEvent?.id); + + const responseTeammate2Event = responseBody.data.find( + (eventType) => eventType.ownerId === teamMember2.id + ); + expect(responseTeammate1Event).toBeDefined(); + expect(responseTeammate2Event?.parentEventTypeId).toEqual(responseTeamEvent?.id); + + if (responseTeamEvent) { + managedEventType = responseTeamEvent; + } + }); + }); + + it("should delete event-type not part of the team", async () => { + return request(app.getHttpServer()).delete(`/v2/teams/${team.id}/event-types/99999`).expect(404); + }); + + it("should delete collective event-type", async () => { + return request(app.getHttpServer()) + .delete(`/v2/teams/${team.id}/event-types/${collectiveEventType.id}`) + .expect(200); + }); + + it("should delete managed event-type", async () => { + return request(app.getHttpServer()) + .delete(`/v2/teams/${team.id}/event-types/${managedEventType.id}`) + .expect(200); + }); + + function evaluateHost(expected: Host, received: Host | undefined) { + expect(expected.userId).toEqual(received?.userId); + expect(expected.mandatory).toEqual(received?.mandatory); + expect(expected.priority).toEqual(received?.priority); + } + + afterAll(async () => { + await userRepositoryFixture.deleteByEmail(userAdmin.email); + await userRepositoryFixture.deleteByEmail(teamMember1.email); + await userRepositoryFixture.deleteByEmail(teamMember2.email); + await userRepositoryFixture.deleteByEmail(falseTestUser.email); + await teamsRepositoryFixture.delete(team.id); + await teamsRepositoryFixture.delete(falseTestTeam.id); + await app.close(); + }); + }); +}); diff --git a/apps/api/v2/src/modules/teams/event-types/controllers/teams-event-types.controller.ts b/apps/api/v2/src/modules/teams/event-types/controllers/teams-event-types.controller.ts new file mode 100644 index 0000000000..c441c96f48 --- /dev/null +++ b/apps/api/v2/src/modules/teams/event-types/controllers/teams-event-types.controller.ts @@ -0,0 +1,208 @@ +import { CreatePhoneCallInput } from "@/ee/event-types/event-types_2024_06_14/inputs/create-phone-call.input"; +import { CreatePhoneCallOutput } from "@/ee/event-types/event-types_2024_06_14/outputs/create-phone-call.output"; +import { API_VERSIONS_VALUES } from "@/lib/api-versions"; +import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator"; +import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator"; +import { Roles } from "@/modules/auth/decorators/roles/roles.decorator"; +import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard"; +import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard"; +import { OutputTeamEventTypesResponsePipe } from "@/modules/organizations/controllers/pipes/event-types/team-event-types-response.transformer"; +import { InputOrganizationsEventTypesService } from "@/modules/organizations/services/event-types/input.service"; +import { DatabaseTeamEventType } from "@/modules/organizations/services/event-types/output.service"; +import { CreateTeamEventTypeOutput } from "@/modules/teams/event-types/outputs/create-team-event-type.output"; +import { DeleteTeamEventTypeOutput } from "@/modules/teams/event-types/outputs/delete-team-event-type.output"; +import { GetTeamEventTypeOutput } from "@/modules/teams/event-types/outputs/get-team-event-type.output"; +import { GetTeamEventTypesOutput } from "@/modules/teams/event-types/outputs/get-team-event-types.output"; +import { UpdateTeamEventTypeOutput } from "@/modules/teams/event-types/outputs/update-team-event-type.output"; +import { TeamsEventTypesService } from "@/modules/teams/event-types/services/teams-event-types.service"; +import { UserWithProfile } from "@/modules/users/users.repository"; +import { + Controller, + UseGuards, + Get, + Post, + Param, + ParseIntPipe, + Body, + Patch, + Delete, + HttpCode, + HttpStatus, + NotFoundException, + Query, +} from "@nestjs/common"; +import { ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger"; + +import { ERROR_STATUS, SUCCESS_STATUS } from "@calcom/platform-constants"; +import { handleCreatePhoneCall } from "@calcom/platform-libraries"; +import { + CreateTeamEventTypeInput_2024_06_14, + GetTeamEventTypesQuery_2024_06_14, + SkipTakePagination, + TeamEventTypeOutput_2024_06_14, + UpdateTeamEventTypeInput_2024_06_14, +} from "@calcom/platform-types"; + +export type EventTypeHandlerResponse = { + data: DatabaseTeamEventType[] | DatabaseTeamEventType; + status: typeof SUCCESS_STATUS | typeof ERROR_STATUS; +}; + +@Controller({ + path: "/v2/teams/:teamId/event-types", + version: API_VERSIONS_VALUES, +}) +@DocsTags("Teams / Event Types") +export class TeamsEventTypesController { + constructor( + private readonly teamsEventTypesService: TeamsEventTypesService, + private readonly inputService: InputOrganizationsEventTypesService, + private readonly outputTeamEventTypesResponsePipe: OutputTeamEventTypesResponsePipe + ) {} + + @Roles("TEAM_ADMIN") + @PlatformPlan("ESSENTIALS") + @UseGuards(ApiAuthGuard, RolesGuard) + @Post("/") + @ApiOperation({ summary: "Create an event type" }) + async createTeamEventType( + @GetUser() user: UserWithProfile, + @Param("teamId", ParseIntPipe) teamId: number, + @Body() bodyEventType: CreateTeamEventTypeInput_2024_06_14 + ): Promise { + const transformedBody = await this.inputService.transformAndValidateCreateTeamEventTypeInput( + user.id, + teamId, + bodyEventType + ); + + const eventType = await this.teamsEventTypesService.createTeamEventType(user, teamId, transformedBody); + + return { + status: SUCCESS_STATUS, + data: await this.outputTeamEventTypesResponsePipe.transform(eventType), + }; + } + + @Roles("TEAM_ADMIN") + @UseGuards(ApiAuthGuard, RolesGuard) + @Get("/:eventTypeId") + @ApiOperation({ summary: "Get an event type" }) + async getTeamEventType( + @Param("teamId", ParseIntPipe) teamId: number, + @Param("eventTypeId") eventTypeId: number + ): Promise { + const eventType = await this.teamsEventTypesService.getTeamEventType(teamId, eventTypeId); + + if (!eventType) { + throw new NotFoundException(`Event type with id ${eventTypeId} not found`); + } + + return { + status: SUCCESS_STATUS, + data: (await this.outputTeamEventTypesResponsePipe.transform( + eventType + )) as TeamEventTypeOutput_2024_06_14, + }; + } + + @Roles("TEAM_ADMIN") + @Post("/:eventTypeId/create-phone-call") + @UseGuards(ApiAuthGuard, RolesGuard) + @ApiOperation({ summary: "Create a phone call" }) + async createPhoneCall( + @Param("eventTypeId") eventTypeId: number, + @Param("orgId", ParseIntPipe) orgId: number, + @Body() body: CreatePhoneCallInput, + @GetUser() user: UserWithProfile + ): Promise { + const data = await handleCreatePhoneCall({ + user: { + id: user.id, + timeZone: user.timeZone, + profile: { organization: { id: orgId } }, + }, + input: { ...body, eventTypeId }, + }); + + return { + status: SUCCESS_STATUS, + data, + }; + } + + @Get("/") + @ApiOperation({ summary: "Get a team event type" }) + async getTeamEventTypes( + @Param("teamId", ParseIntPipe) teamId: number, + @Query() queryParams: GetTeamEventTypesQuery_2024_06_14 + ): Promise { + const { eventSlug } = queryParams; + + if (eventSlug) { + const eventType = await this.teamsEventTypesService.getTeamEventTypeBySlug(teamId, eventSlug); + + return { + status: SUCCESS_STATUS, + data: await this.outputTeamEventTypesResponsePipe.transform(eventType ? [eventType] : []), + }; + } + + const eventTypes = await this.teamsEventTypesService.getTeamEventTypes(teamId); + + return { + status: SUCCESS_STATUS, + data: await this.outputTeamEventTypesResponsePipe.transform(eventTypes), + }; + } + + @Roles("TEAM_ADMIN") + @UseGuards(ApiAuthGuard, RolesGuard) + @Patch("/:eventTypeId") + @ApiOperation({ summary: "Update a team event type" }) + async updateTeamEventType( + @Param("teamId", ParseIntPipe) teamId: number, + @Param("eventTypeId", ParseIntPipe) eventTypeId: number, + @GetUser() user: UserWithProfile, + @Body() bodyEventType: UpdateTeamEventTypeInput_2024_06_14 + ): Promise { + const transformedBody = await this.inputService.transformAndValidateUpdateTeamEventTypeInput( + user.id, + eventTypeId, + teamId, + bodyEventType + ); + + const eventType = await this.teamsEventTypesService.updateTeamEventType( + eventTypeId, + teamId, + transformedBody, + user + ); + + return { + status: SUCCESS_STATUS, + data: await this.outputTeamEventTypesResponsePipe.transform(eventType), + }; + } + + @Roles("TEAM_ADMIN") + @UseGuards(ApiAuthGuard, RolesGuard) + @Delete("/:eventTypeId") + @HttpCode(HttpStatus.OK) + @ApiOperation({ summary: "Delete a team event type" }) + async deleteTeamEventType( + @Param("teamId", ParseIntPipe) teamId: number, + @Param("eventTypeId", ParseIntPipe) eventTypeId: number + ): Promise { + const eventType = await this.teamsEventTypesService.deleteTeamEventType(teamId, eventTypeId); + + return { + status: SUCCESS_STATUS, + data: { + id: eventTypeId, + title: eventType.title, + }, + }; + } +} diff --git a/apps/api/v2/src/modules/organizations/controllers/event-types/outputs/teams/create-team-event-type.output.ts b/apps/api/v2/src/modules/teams/event-types/outputs/create-team-event-type.output.ts similarity index 100% rename from apps/api/v2/src/modules/organizations/controllers/event-types/outputs/teams/create-team-event-type.output.ts rename to apps/api/v2/src/modules/teams/event-types/outputs/create-team-event-type.output.ts diff --git a/apps/api/v2/src/modules/organizations/controllers/event-types/outputs/teams/delete-team-event-type.output.ts b/apps/api/v2/src/modules/teams/event-types/outputs/delete-team-event-type.output.ts similarity index 100% rename from apps/api/v2/src/modules/organizations/controllers/event-types/outputs/teams/delete-team-event-type.output.ts rename to apps/api/v2/src/modules/teams/event-types/outputs/delete-team-event-type.output.ts diff --git a/apps/api/v2/src/modules/organizations/controllers/event-types/outputs/teams/get-team-event-type.output.ts b/apps/api/v2/src/modules/teams/event-types/outputs/get-team-event-type.output.ts similarity index 100% rename from apps/api/v2/src/modules/organizations/controllers/event-types/outputs/teams/get-team-event-type.output.ts rename to apps/api/v2/src/modules/teams/event-types/outputs/get-team-event-type.output.ts diff --git a/apps/api/v2/src/modules/organizations/controllers/event-types/outputs/teams/get-team-event-types.output.ts b/apps/api/v2/src/modules/teams/event-types/outputs/get-team-event-types.output.ts similarity index 100% rename from apps/api/v2/src/modules/organizations/controllers/event-types/outputs/teams/get-team-event-types.output.ts rename to apps/api/v2/src/modules/teams/event-types/outputs/get-team-event-types.output.ts diff --git a/apps/api/v2/src/modules/organizations/controllers/event-types/outputs/teams/update-team-event-type.output.ts b/apps/api/v2/src/modules/teams/event-types/outputs/update-team-event-type.output.ts similarity index 100% rename from apps/api/v2/src/modules/organizations/controllers/event-types/outputs/teams/update-team-event-type.output.ts rename to apps/api/v2/src/modules/teams/event-types/outputs/update-team-event-type.output.ts diff --git a/apps/api/v2/src/modules/teams/event-types/services/teams-event-types.service.ts b/apps/api/v2/src/modules/teams/event-types/services/teams-event-types.service.ts new file mode 100644 index 0000000000..5694599d7e --- /dev/null +++ b/apps/api/v2/src/modules/teams/event-types/services/teams-event-types.service.ts @@ -0,0 +1,148 @@ +import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository"; +import { EventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/event-types.service"; +import { DatabaseTeamEventType } from "@/modules/organizations/services/event-types/output.service"; +import { PrismaWriteService } from "@/modules/prisma/prisma-write.service"; +import { TeamsEventTypesRepository } from "@/modules/teams/event-types/teams-event-types.repository"; +import { UsersService } from "@/modules/users/services/users.service"; +import { UserWithProfile } from "@/modules/users/users.repository"; +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 TeamsEventTypesService { + private readonly logger = new Logger("TeamsEventTypesService"); + + constructor( + private readonly eventTypesService: EventTypesService_2024_06_14, + private readonly dbWrite: PrismaWriteService, + private readonly teamsEventTypesRepository: TeamsEventTypesRepository, + private readonly eventTypesRepository: EventTypesRepository_2024_06_14, + private readonly usersService: UsersService + ) {} + + async createTeamEventType( + user: UserWithProfile, + teamId: number, + body: InputTeamEventTransformed_2024_06_14 + ): Promise { + const eventTypeUser = await this.getUserToCreateTeamEvent(user); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { hosts, children, destinationCalendar, ...rest } = body; + + const { eventType: eventTypeCreated } = await createEventType({ + input: { teamId: teamId, ...rest }, + ctx: { + user: eventTypeUser, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + prisma: this.dbWrite.prisma, + }, + }); + + return this.updateTeamEventType(eventTypeCreated.id, teamId, body, user); + } + + async validateEventTypeExists(teamId: number, eventTypeId: number) { + const eventType = await this.teamsEventTypesRepository.getTeamEventType(teamId, eventTypeId); + + if (!eventType) { + throw new NotFoundException(`Event type with id ${eventTypeId} not found`); + } + } + + async getUserToCreateTeamEvent(user: UserWithProfile) { + const profileId = this.usersService.getUserMainProfile(user)?.id; + + return { + id: user.id, + role: user.role, + organizationId: null, + organization: { id: null, isOrgAdmin: false, metadata: {}, requestedSlug: null }, + profile: { id: profileId || null }, + metadata: user.metadata, + }; + } + + async getTeamEventType(teamId: number, eventTypeId: number): Promise { + const eventType = await this.teamsEventTypesRepository.getTeamEventType(teamId, eventTypeId); + + if (!eventType) { + return null; + } + + return eventType; + } + + async getTeamEventTypeBySlug(teamId: number, eventTypeSlug: string): Promise { + const eventType = await this.teamsEventTypesRepository.getTeamEventTypeBySlug(teamId, eventTypeSlug); + + if (!eventType) { + return null; + } + + return eventType; + } + + async getTeamEventTypes(teamId: number): Promise { + return await this.teamsEventTypesRepository.getTeamEventTypes(teamId); + } + + async updateTeamEventType( + eventTypeId: number, + teamId: number, + body: InputTeamEventTransformed_2024_06_14, + user: UserWithProfile + ): Promise { + await this.validateEventTypeExists(teamId, eventTypeId); + const eventTypeUser = await this.eventTypesService.getUserToUpdateEvent(user); + + await updateEventType({ + input: { id: eventTypeId, ...body }, + ctx: { + user: eventTypeUser, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + prisma: this.dbWrite.prisma, + }, + }); + + const eventType = await this.teamsEventTypesRepository.getEventTypeById(eventTypeId); + + if (!eventType) { + throw new NotFoundException(`Event type with id ${eventTypeId} not found`); + } + + if (eventType.schedulingType !== "MANAGED") { + return eventType; + } + + const childrenEventTypes = await this.teamsEventTypesRepository.getEventTypeChildren(eventType.id); + + return [eventType, ...childrenEventTypes]; + } + + async deleteTeamEventType(teamId: number, eventTypeId: number) { + const existingEventType = await this.teamsEventTypesRepository.getTeamEventType(teamId, eventTypeId); + + if (!existingEventType) { + throw new NotFoundException(`Event type with ID=${eventTypeId} does not exist.`); + } + + return this.eventTypesRepository.deleteEventType(eventTypeId); + } + + async deleteUserTeamEventTypesAndHosts(userId: number, teamId: number) { + try { + await this.teamsEventTypesRepository.deleteUserManagedTeamEventTypes(userId, teamId); + await this.teamsEventTypesRepository.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/teams/event-types/teams-event-types.module.ts b/apps/api/v2/src/modules/teams/event-types/teams-event-types.module.ts new file mode 100644 index 0000000000..10e5556a7c --- /dev/null +++ b/apps/api/v2/src/modules/teams/event-types/teams-event-types.module.ts @@ -0,0 +1,36 @@ +import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module"; +import { MembershipsModule } from "@/modules/memberships/memberships.module"; +import { OutputTeamEventTypesResponsePipe } from "@/modules/organizations/controllers/pipes/event-types/team-event-types-response.transformer"; +import { OrganizationsTeamsRepository } from "@/modules/organizations/repositories/organizations-teams.repository"; +import { InputOrganizationsEventTypesService } from "@/modules/organizations/services/event-types/input.service"; +import { OutputOrganizationsEventTypesService } from "@/modules/organizations/services/event-types/output.service"; +import { PrismaModule } from "@/modules/prisma/prisma.module"; +import { RedisModule } from "@/modules/redis/redis.module"; +import { TeamsEventTypesController } from "@/modules/teams/event-types/controllers/teams-event-types.controller"; +import { TeamsEventTypesService } from "@/modules/teams/event-types/services/teams-event-types.service"; +import { TeamsEventTypesRepository } from "@/modules/teams/event-types/teams-event-types.repository"; +import { TeamsModule } from "@/modules/teams/teams/teams.module"; +import { UsersModule } from "@/modules/users/users.module"; +import { Module } from "@nestjs/common"; + +@Module({ + imports: [ + PrismaModule, + RedisModule, + MembershipsModule, + EventTypesModule_2024_06_14, + UsersModule, + TeamsModule, + ], + providers: [ + TeamsEventTypesRepository, + TeamsEventTypesService, + InputOrganizationsEventTypesService, + OrganizationsTeamsRepository, + OutputTeamEventTypesResponsePipe, + OutputOrganizationsEventTypesService, + ], + exports: [TeamsEventTypesRepository, TeamsEventTypesService], + controllers: [TeamsEventTypesController], +}) +export class TeamsEventTypesModule {} diff --git a/apps/api/v2/src/modules/teams/event-types/teams-event-types.repository.ts b/apps/api/v2/src/modules/teams/event-types/teams-event-types.repository.ts new file mode 100644 index 0000000000..3b438df5d2 --- /dev/null +++ b/apps/api/v2/src/modules/teams/event-types/teams-event-types.repository.ts @@ -0,0 +1,116 @@ +import { PrismaReadService } from "@/modules/prisma/prisma-read.service"; +import { PrismaWriteService } from "@/modules/prisma/prisma-write.service"; +import { Injectable } from "@nestjs/common"; + +@Injectable() +export class TeamsEventTypesRepository { + constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {} + + async getTeamEventType(teamId: number, eventTypeId: number) { + return this.dbRead.prisma.eventType.findUnique({ + where: { + id: eventTypeId, + teamId, + }, + include: { users: true, schedule: true, hosts: true, destinationCalendar: true }, + }); + } + + async getTeamEventTypeBySlug(teamId: number, eventTypeSlug: string) { + return this.dbRead.prisma.eventType.findUnique({ + where: { + teamId_slug: { + teamId, + slug: eventTypeSlug, + }, + }, + include: { + users: true, + schedule: true, + hosts: true, + destinationCalendar: true, + team: { + select: { + bannerUrl: true, + name: true, + logoUrl: true, + slug: true, + weekStart: true, + brandColor: true, + darkBrandColor: true, + theme: true, + }, + }, + }, + }); + } + + async getTeamEventTypes(teamId: number) { + return this.dbRead.prisma.eventType.findMany({ + where: { + teamId, + }, + include: { + users: true, + schedule: true, + hosts: true, + destinationCalendar: true, + team: { + select: { + bannerUrl: true, + name: true, + logoUrl: true, + slug: true, + weekStart: true, + brandColor: true, + darkBrandColor: true, + theme: true, + }, + }, + }, + }); + } + + async getEventTypeById(eventTypeId: number) { + return this.dbRead.prisma.eventType.findUnique({ + where: { id: eventTypeId }, + include: { users: true, schedule: true, hosts: true, destinationCalendar: true }, + }); + } + + async getEventTypeChildren(eventTypeId: number) { + return this.dbRead.prisma.eventType.findMany({ + where: { parentId: eventTypeId }, + include: { users: true, schedule: true, hosts: true, destinationCalendar: true }, + }); + } + + async getEventTypeByIdWithChildren(eventTypeId: number) { + return this.dbRead.prisma.eventType.findUnique({ + where: { id: eventTypeId }, + 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/teams/memberships/controllers/teams-memberships.controller.ts b/apps/api/v2/src/modules/teams/memberships/controllers/teams-memberships.controller.ts index 5a2fdbbe30..2d1ed36504 100644 --- a/apps/api/v2/src/modules/teams/memberships/controllers/teams-memberships.controller.ts +++ b/apps/api/v2/src/modules/teams/memberships/controllers/teams-memberships.controller.ts @@ -2,7 +2,7 @@ import { API_VERSIONS_VALUES } from "@/lib/api-versions"; import { Roles } from "@/modules/auth/decorators/roles/roles.decorator"; import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard"; import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard"; -import { OrganizationsEventTypesService } from "@/modules/organizations/services/event-types/organizations-event-types.service"; +import { TeamsEventTypesService } from "@/modules/teams/event-types/services/teams-event-types.service"; import { CreateTeamMembershipInput } from "@/modules/teams/memberships/inputs/create-team-membership.input"; import { UpdateTeamMembershipInput } from "@/modules/teams/memberships/inputs/update-team-membership.input"; import { CreateTeamMembershipOutput } from "@/modules/teams/memberships/outputs/create-team-membership.output"; @@ -45,7 +45,7 @@ export class TeamsMembershipsController { constructor( private teamsMembershipsService: TeamsMembershipsService, - private oganizationsEventTypesService: OrganizationsEventTypesService + private teamsEventTypesService: TeamsEventTypesService ) {} @Roles("TEAM_ADMIN") @@ -149,7 +149,7 @@ export class TeamsMembershipsController { ): Promise { const membership = await this.teamsMembershipsService.deleteTeamMembership(teamId, membershipId); - await this.oganizationsEventTypesService.deleteUserTeamEventTypesAndHosts(membership.userId, teamId); + await this.teamsEventTypesService.deleteUserTeamEventTypesAndHosts(membership.userId, teamId); return { status: SUCCESS_STATUS, diff --git a/apps/api/v2/src/modules/teams/memberships/teams-memberships.module.ts b/apps/api/v2/src/modules/teams/memberships/teams-memberships.module.ts index b0cbd078d0..40e53915a3 100644 --- a/apps/api/v2/src/modules/teams/memberships/teams-memberships.module.ts +++ b/apps/api/v2/src/modules/teams/memberships/teams-memberships.module.ts @@ -2,13 +2,14 @@ import { MembershipsModule } from "@/modules/memberships/memberships.module"; import { OrganizationsModule } from "@/modules/organizations/organizations.module"; import { PrismaModule } from "@/modules/prisma/prisma.module"; import { RedisModule } from "@/modules/redis/redis.module"; +import { TeamsEventTypesModule } from "@/modules/teams/event-types/teams-event-types.module"; import { TeamsMembershipsController } from "@/modules/teams/memberships/controllers/teams-memberships.controller"; import { TeamsMembershipsService } from "@/modules/teams/memberships/services/teams-memberships.service"; import { TeamsMembershipsRepository } from "@/modules/teams/memberships/teams-memberships.repository"; import { Module } from "@nestjs/common"; @Module({ - imports: [PrismaModule, RedisModule, OrganizationsModule, MembershipsModule], + imports: [PrismaModule, RedisModule, OrganizationsModule, MembershipsModule, TeamsEventTypesModule], providers: [TeamsMembershipsRepository, TeamsMembershipsService], controllers: [TeamsMembershipsController], }) diff --git a/apps/api/v2/swagger/documentation.json b/apps/api/v2/swagger/documentation.json index 2334ac5489..47d7fe5bc1 100644 --- a/apps/api/v2/swagger/documentation.json +++ b/apps/api/v2/swagger/documentation.json @@ -5554,6 +5554,257 @@ ] } }, + "/v2/teams/{teamId}/event-types": { + "post": { + "operationId": "TeamsEventTypesController_createTeamEventType", + "summary": "Create an event type", + "parameters": [ + { + "name": "teamId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateTeamEventTypeInput_2024_06_14" + } + } + } + }, + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateTeamEventTypeOutput" + } + } + } + } + }, + "tags": [ + "Teams / Event Types" + ] + }, + "get": { + "operationId": "TeamsEventTypesController_getTeamEventTypes", + "summary": "Get a team event type", + "parameters": [ + { + "name": "teamId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "eventSlug", + "required": false, + "in": "query", + "description": "Slug of team event type to return.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetTeamEventTypesOutput" + } + } + } + } + }, + "tags": [ + "Teams / Event Types" + ] + } + }, + "/v2/teams/{teamId}/event-types/{eventTypeId}": { + "get": { + "operationId": "TeamsEventTypesController_getTeamEventType", + "summary": "Get an event type", + "parameters": [ + { + "name": "teamId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "eventTypeId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetTeamEventTypeOutput" + } + } + } + } + }, + "tags": [ + "Teams / Event Types" + ] + }, + "patch": { + "operationId": "TeamsEventTypesController_updateTeamEventType", + "summary": "Update a team event type", + "parameters": [ + { + "name": "teamId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "eventTypeId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateTeamEventTypeInput_2024_06_14" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateTeamEventTypeOutput" + } + } + } + } + }, + "tags": [ + "Teams / Event Types" + ] + }, + "delete": { + "operationId": "TeamsEventTypesController_deleteTeamEventType", + "summary": "Delete a team event type", + "parameters": [ + { + "name": "teamId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "eventTypeId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteTeamEventTypeOutput" + } + } + } + } + }, + "tags": [ + "Teams / Event Types" + ] + } + }, + "/v2/teams/{teamId}/event-types/{eventTypeId}/create-phone-call": { + "post": { + "operationId": "TeamsEventTypesController_createPhoneCall", + "summary": "Create a phone call", + "parameters": [ + { + "name": "eventTypeId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreatePhoneCallInput" + } + } + } + }, + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreatePhoneCallOutput" + } + } + } + } + }, + "tags": [ + "Teams / Event Types" + ] + } + }, "/v2/teams/{teamId}/memberships": { "post": { "operationId": "TeamsMembershipsController_createTeamMembership", diff --git a/docs/api-reference/v2/openapi.json b/docs/api-reference/v2/openapi.json index 34f3803c86..cf007dd321 100644 --- a/docs/api-reference/v2/openapi.json +++ b/docs/api-reference/v2/openapi.json @@ -5271,6 +5271,245 @@ "tags": ["Teams"] } }, + "/v2/teams/{teamId}/event-types": { + "post": { + "operationId": "TeamsEventTypesController_createTeamEventType", + "summary": "Create an event type", + "parameters": [ + { + "name": "teamId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateTeamEventTypeInput_2024_06_14" + } + } + } + }, + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateTeamEventTypeOutput" + } + } + } + } + }, + "tags": ["Teams / Event Types"] + }, + "get": { + "operationId": "TeamsEventTypesController_getTeamEventTypes", + "summary": "Get a team event type", + "parameters": [ + { + "name": "teamId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "eventSlug", + "required": false, + "in": "query", + "description": "Slug of team event type to return.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetTeamEventTypesOutput" + } + } + } + } + }, + "tags": ["Teams / Event Types"] + } + }, + "/v2/teams/{teamId}/event-types/{eventTypeId}": { + "get": { + "operationId": "TeamsEventTypesController_getTeamEventType", + "summary": "Get an event type", + "parameters": [ + { + "name": "teamId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "eventTypeId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetTeamEventTypeOutput" + } + } + } + } + }, + "tags": ["Teams / Event Types"] + }, + "patch": { + "operationId": "TeamsEventTypesController_updateTeamEventType", + "summary": "Update a team event type", + "parameters": [ + { + "name": "teamId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "eventTypeId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateTeamEventTypeInput_2024_06_14" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateTeamEventTypeOutput" + } + } + } + } + }, + "tags": ["Teams / Event Types"] + }, + "delete": { + "operationId": "TeamsEventTypesController_deleteTeamEventType", + "summary": "Delete a team event type", + "parameters": [ + { + "name": "teamId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "eventTypeId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteTeamEventTypeOutput" + } + } + } + } + }, + "tags": ["Teams / Event Types"] + } + }, + "/v2/teams/{teamId}/event-types/{eventTypeId}/create-phone-call": { + "post": { + "operationId": "TeamsEventTypesController_createPhoneCall", + "summary": "Create a phone call", + "parameters": [ + { + "name": "eventTypeId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "orgId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreatePhoneCallInput" + } + } + } + }, + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreatePhoneCallOutput" + } + } + } + } + }, + "tags": ["Teams / Event Types"] + } + }, "/v2/teams/{teamId}/memberships": { "post": { "operationId": "TeamsMembershipsController_createTeamMembership",