diff --git a/apps/api/v2/src/modules/organizations/event-types/services/input.service.ts b/apps/api/v2/src/modules/organizations/event-types/services/input.service.ts index 0d6699a6fd..a757119b11 100644 --- a/apps/api/v2/src/modules/organizations/event-types/services/input.service.ts +++ b/apps/api/v2/src/modules/organizations/event-types/services/input.service.ts @@ -15,6 +15,10 @@ import { HostPriority, EmailSettings_2024_06_14, } from "@calcom/platform-types"; +import type { EventType } from "@calcom/prisma/client"; + +export const HOSTS_REQUIRED_WHEN_SWITCHING_SCHEDULING_TYPE_ERROR = + "Hosts required when switching schedulingType. Please provide 'hosts' or set 'assignAllTeamMembers: true' to specify how hosts should be configured for the new scheduling type."; export type TransformedCreateTeamEventTypeInput = Awaited< ReturnType["transformInputCreateTeamEventType"]> @@ -116,10 +120,12 @@ export class InputOrganizationsEventTypesService { teamId: number, inputEventType: CreateTeamEventTypeInput_2024_06_14 ) { - const { hosts, assignAllTeamMembers, locations, emailSettings, ...rest } = inputEventType; + const { assignAllTeamMembers, locations, emailSettings, ...rest } = inputEventType; const eventType = this.inputEventTypesService.transformInputCreateEventType(rest); + const isManagedEventType = rest.schedulingType === "MANAGED"; + const defaultLocations: CreateTeamEventTypeInput_2024_06_14["locations"] = [ { type: "integration", @@ -127,12 +133,13 @@ export class InputOrganizationsEventTypesService { }, ]; - const children = await this.getChildEventTypesForManagedEventTypeCreate(inputEventType, teamId); + const children = isManagedEventType + ? await this.getChildEventTypesForManagedEventTypeCreate(inputEventType, teamId) + : undefined; - let metadata = - rest.schedulingType === "MANAGED" - ? { managedEventConfig: {}, ...eventType.metadata } - : eventType.metadata; + let metadata = isManagedEventType + ? { managedEventConfig: {}, ...eventType.metadata } + : eventType.metadata; if (emailSettings) { metadata = this.addEmailSettingsToMetadata(emailSettings, metadata); @@ -140,12 +147,7 @@ export class InputOrganizationsEventTypesService { const teamEventType = { ...eventType, - // note(Lauris): we don't populate hosts for managed event-types because they are handled by the children - hosts: !(rest.schedulingType === "MANAGED") - ? assignAllTeamMembers - ? await this.getAllTeamMembers(teamId, inputEventType.schedulingType) - : this.transformInputHosts(hosts, inputEventType.schedulingType) - : undefined, + hosts: await this.transformInputCreateTeamEventTypeHosts(teamId, inputEventType), assignAllTeamMembers, locations: this.transformInputTeamLocations(locations || defaultLocations), metadata, @@ -155,6 +157,24 @@ export class InputOrganizationsEventTypesService { return teamEventType; } + private async transformInputCreateTeamEventTypeHosts( + teamId: number, + inputEventType: CreateTeamEventTypeInput_2024_06_14 + ) { + const { hosts, assignAllTeamMembers, schedulingType } = inputEventType; + + // note(Lauris): we don't populate hosts for managed event-types because they are handled by the children - each child managed event type is associated with + // a specific user and hosts property is only for team event types e.g round robin and collective. + if (schedulingType === "MANAGED") { + return undefined; + } + + if (assignAllTeamMembers) { + return await this.getAllTeamMembers(teamId, inputEventType.schedulingType); + } + return this.transformInputHosts(hosts, inputEventType.schedulingType); + } + private addEmailSettingsToMetadata( emailSettings: EmailSettings_2024_06_14, metadata: NonNullable @@ -190,7 +210,7 @@ export class InputOrganizationsEventTypesService { teamId: number, inputEventType: UpdateTeamEventTypeInput_2024_06_14 ) { - const { hosts, assignAllTeamMembers, locations, emailSettings, ...rest } = inputEventType; + const { assignAllTeamMembers, locations, emailSettings, ...rest } = inputEventType; const eventType = await this.inputEventTypesService.transformInputUpdateEventType(rest, eventTypeId); const dbEventType = await this.teamsEventTypesRepository.getTeamEventType(teamId, eventTypeId); @@ -213,11 +233,11 @@ export class InputOrganizationsEventTypesService { const teamEventType = { ...eventType, // note(Lauris): we don't populate hosts for managed event-types because they are handled by the children - hosts: !children - ? assignAllTeamMembers - ? await this.getAllTeamMembers(teamId, dbEventType.schedulingType) - : this.transformInputHosts(hosts, dbEventType.schedulingType) - : undefined, + hosts: await this.transformInputUpdateTeamEventTypeHosts( + teamId, + dbEventType.schedulingType, + inputEventType + ), assignAllTeamMembers, children, locations: locations ? this.transformInputTeamLocations(locations) : undefined, @@ -227,6 +247,33 @@ export class InputOrganizationsEventTypesService { return teamEventType; } + private async transformInputUpdateTeamEventTypeHosts( + teamId: number, + dbEventTypeSchedulingType: EventType["schedulingType"], + inputEventType: UpdateTeamEventTypeInput_2024_06_14 + ) { + const { hosts, assignAllTeamMembers } = inputEventType; + + if (dbEventTypeSchedulingType === "MANAGED") { + // note(Lauris): we don't populate hosts for managed event-types because they are handled by the event type children + return undefined; + } + + const isSchedulingTypeChanging = + inputEventType.schedulingType && inputEventType.schedulingType !== dbEventTypeSchedulingType; + + if (isSchedulingTypeChanging && !assignAllTeamMembers && !hosts) { + throw new BadRequestException(HOSTS_REQUIRED_WHEN_SWITCHING_SCHEDULING_TYPE_ERROR); + } + + const nextSchedulingType = inputEventType.schedulingType || dbEventTypeSchedulingType; + if (assignAllTeamMembers) { + return await this.getAllTeamMembers(teamId, nextSchedulingType); + } + + return this.transformInputHosts(hosts, nextSchedulingType); + } + async getChildEventTypesForManagedEventTypeUpdate( eventTypeId: number, inputEventType: UpdateTeamEventTypeInput_2024_06_14, @@ -273,7 +320,7 @@ export class InputOrganizationsEventTypesService { } async getChildEventTypesForManagedEventTypeCreate( - inputEventType: UpdateTeamEventTypeInput_2024_06_14, + inputEventType: Pick, teamId: number ) { const ownersIds = await this.getOwnersIdsForManagedEventTypeCreate(teamId, inputEventType); @@ -289,7 +336,7 @@ export class InputOrganizationsEventTypesService { async getOwnersIdsForManagedEventTypeCreate( teamId: number, - inputEventType: UpdateTeamEventTypeInput_2024_06_14 + inputEventType: Pick ) { if (inputEventType.assignAllTeamMembers) { return await this.getTeamUsersIds(teamId); 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 index ba2ffb0fa7..73d104615a 100644 --- 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 @@ -1,5 +1,6 @@ import { bootstrap } from "@/app"; import { AppModule } from "@/app.module"; +import { HOSTS_REQUIRED_WHEN_SWITCHING_SCHEDULING_TYPE_ERROR } from "@/modules/organizations/event-types/services/input.service"; import { PrismaModule } from "@/modules/prisma/prisma.module"; import { TokensModule } from "@/modules/tokens/tokens.module"; import { UsersModule } from "@/modules/users/users.module"; @@ -758,6 +759,586 @@ describe("Organizations Event Types Endpoints", () => { expect(expected.priority).toEqual(received?.priority); } + describe("updating scheduling type", () => { + it("should return 400 error if schedulingType: managed is passed", async () => { + const createBody: CreateTeamEventTypeInput_2024_06_14 = { + title: `teams-event-types-scheduling-collective-${randomString()}`, + slug: `teams-event-types-scheduling-collective-${randomString()}`, + description: "Test collective event type", + lengthInMinutes: 60, + locations: [ + { + type: "integration", + integration: "cal-video", + }, + ], + schedulingType: "COLLECTIVE", + hosts: [ + { + userId: teamMember1.id, + }, + ], + }; + + const createResponse = await request(app.getHttpServer()) + .post(`/v2/teams/${team.id}/event-types`) + .send(createBody) + .expect(201); + + const createdEventType: ApiSuccessResponse = createResponse.body; + + const updateBody = { + schedulingType: "managed", + }; + + await request(app.getHttpServer()) + .patch(`/v2/teams/${team.id}/event-types/${createdEventType.data.id}`) + .send(updateBody) + .expect(400); + + await eventTypesRepositoryFixture.delete(createdEventType.data.id); + }); + + it("should require hosts when changing round robin event type to collective without providing hosts", async () => { + const createBody: CreateTeamEventTypeInput_2024_06_14 = { + title: `teams-event-types-scheduling-roundrobin-${randomString()}`, + slug: `teams-event-types-scheduling-roundrobin-${randomString()}`, + description: "Test round robin event type", + lengthInMinutes: 60, + locations: [ + { + type: "integration", + integration: "cal-video", + }, + ], + schedulingType: "ROUND_ROBIN", + hosts: [ + { + userId: teamMember1.id, + priority: "high", + }, + { + userId: teamMember2.id, + priority: "low", + }, + ], + }; + + const createResponse = await request(app.getHttpServer()) + .post(`/v2/teams/${team.id}/event-types`) + .send(createBody) + .expect(201); + + const createdEventType: ApiSuccessResponse = createResponse.body; + + const updateBody = { + schedulingType: "collective", + }; + + const response = await request(app.getHttpServer()) + .patch(`/v2/teams/${team.id}/event-types/${createdEventType.data.id}`) + .send(updateBody); + + expect(response.status).toBe(400); + expect(response.body.error.message).toBe(HOSTS_REQUIRED_WHEN_SWITCHING_SCHEDULING_TYPE_ERROR); + + await eventTypesRepositoryFixture.delete(createdEventType.data.id); + }); + + it("should require hosts when changing collective event type to roundRobin without providing hosts", async () => { + const createBody: CreateTeamEventTypeInput_2024_06_14 = { + title: `teams-event-types-scheduling-collective-${randomString()}`, + slug: `teams-event-types-scheduling-collective-${randomString()}`, + description: "Test collective event type", + lengthInMinutes: 60, + locations: [ + { + type: "integration", + integration: "cal-video", + }, + ], + schedulingType: "COLLECTIVE", + hosts: [ + { + userId: teamMember1.id, + }, + { + userId: teamMember2.id, + }, + ], + }; + + const createResponse = await request(app.getHttpServer()) + .post(`/v2/teams/${team.id}/event-types`) + .send(createBody) + .expect(201); + + const createdEventType: ApiSuccessResponse = createResponse.body; + + const updateBody = { + schedulingType: "roundRobin", + }; + + const response = await request(app.getHttpServer()) + .patch(`/v2/teams/${team.id}/event-types/${createdEventType.data.id}`) + .send(updateBody); + + expect(response.status).toBe(400); + expect(response.body.error.message).toBe(HOSTS_REQUIRED_WHEN_SWITCHING_SCHEDULING_TYPE_ERROR); + + await eventTypesRepositoryFixture.delete(createdEventType.data.id); + }); + + it("should change round robin event type to collective and pass new hosts", async () => { + const createBody: CreateTeamEventTypeInput_2024_06_14 = { + title: `teams-event-types-scheduling-roundrobin-${randomString()}`, + slug: `teams-event-types-scheduling-roundrobin-${randomString()}`, + description: "Test round robin event type", + lengthInMinutes: 60, + locations: [ + { + type: "integration", + integration: "cal-video", + }, + ], + schedulingType: "ROUND_ROBIN", + hosts: [ + { + userId: teamMember1.id, + priority: "high", + }, + ], + }; + + const createResponse = await request(app.getHttpServer()) + .post(`/v2/teams/${team.id}/event-types`) + .send(createBody) + .expect(201); + + const createdEventType: ApiSuccessResponse = createResponse.body; + + const updateBody = { + schedulingType: "collective", + hosts: [ + { + userId: teamMember2.id, + }, + ], + }; + + const updateResponse = await request(app.getHttpServer()) + .patch(`/v2/teams/${team.id}/event-types/${createdEventType.data.id}`) + .send(updateBody) + .expect(200); + + const responseBody: ApiSuccessResponse = updateResponse.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data.schedulingType).toEqual("collective"); + expect(responseBody.data.hosts).toHaveLength(1); + expect(responseBody.data.hosts[0].userId).toEqual(teamMember2.id); + + await eventTypesRepositoryFixture.delete(createdEventType.data.id); + }); + + it("should change collective event type to roundRobin and pass new hosts", async () => { + const createBody = { + title: `teams-event-types-scheduling-collective-${randomString()}`, + slug: `teams-event-types-scheduling-collective-${randomString()}`, + description: "Test collective event type", + lengthInMinutes: 60, + locations: [ + { + type: "integration", + integration: "cal-video", + }, + ], + schedulingType: "collective", + hosts: [ + { + userId: teamMember1.id, + }, + ], + }; + + const createResponse = await request(app.getHttpServer()) + .post(`/v2/teams/${team.id}/event-types`) + .send(createBody) + .expect(201); + + const createdEventType: ApiSuccessResponse = createResponse.body; + + const updateBody = { + schedulingType: "roundRobin", + hosts: [ + { + userId: teamMember2.id, + priority: "medium", + }, + ], + }; + + const updateResponse = await request(app.getHttpServer()) + .patch(`/v2/teams/${team.id}/event-types/${createdEventType.data.id}`) + .send(updateBody) + .expect(200); + + const responseBody: ApiSuccessResponse = updateResponse.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data.schedulingType).toEqual("roundRobin"); + expect(responseBody.data.hosts).toHaveLength(1); + expect(responseBody.data.hosts[0].userId).toEqual(teamMember2.id); + + await eventTypesRepositoryFixture.delete(createdEventType.data.id); + }); + + it("should change collective event type to roundRobin with assignAllTeamMembers: true", async () => { + const createBody: CreateTeamEventTypeInput_2024_06_14 = { + title: `teams-event-types-scheduling-collective-${randomString()}`, + slug: `teams-event-types-scheduling-collective-${randomString()}`, + description: "Test collective event type", + lengthInMinutes: 60, + locations: [ + { + type: "integration", + integration: "cal-video", + }, + ], + schedulingType: "COLLECTIVE", + hosts: [ + { + userId: teamMember1.id, + }, + ], + }; + + const createResponse = await request(app.getHttpServer()) + .post(`/v2/teams/${team.id}/event-types`) + .send(createBody) + .expect(201); + + const createdEventType: ApiSuccessResponse = createResponse.body; + + const updateBody = { + schedulingType: "roundRobin", + assignAllTeamMembers: true, + }; + + const updateResponse = await request(app.getHttpServer()) + .patch(`/v2/teams/${team.id}/event-types/${createdEventType.data.id}`) + .send(updateBody) + .expect(200); + + const responseBody: ApiSuccessResponse = updateResponse.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data.schedulingType).toEqual("roundRobin"); + expect(responseBody.data.hosts).toHaveLength(3); + const hostUserIds = responseBody.data.hosts.map((host) => host.userId); + expect(hostUserIds).toContain(teamMember1.id); + expect(hostUserIds).toContain(teamMember2.id); + expect(hostUserIds).toContain(userAdmin.id); + + await eventTypesRepositoryFixture.delete(createdEventType.data.id); + }); + + it("should change round robin event type to collective with assignAllTeamMembers: true", async () => { + const createBody: CreateTeamEventTypeInput_2024_06_14 = { + title: `teams-event-types-scheduling-roundrobin-${randomString()}`, + slug: `teams-event-types-scheduling-roundrobin-${randomString()}`, + description: "Test round robin event type", + lengthInMinutes: 60, + locations: [ + { + type: "integration", + integration: "cal-video", + }, + ], + schedulingType: "ROUND_ROBIN", + hosts: [ + { + userId: teamMember1.id, + priority: "high", + }, + ], + }; + + const createResponse = await request(app.getHttpServer()) + .post(`/v2/teams/${team.id}/event-types`) + .send(createBody) + .expect(201); + + const createdEventType: ApiSuccessResponse = createResponse.body; + + const updateBody = { + schedulingType: "collective", + assignAllTeamMembers: true, + }; + + const updateResponse = await request(app.getHttpServer()) + .patch(`/v2/teams/${team.id}/event-types/${createdEventType.data.id}`) + .send(updateBody) + .expect(200); + + const responseBody: ApiSuccessResponse = updateResponse.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data.schedulingType).toEqual("collective"); + expect(responseBody.data.hosts).toHaveLength(3); + const hostUserIds = responseBody.data.hosts.map((host) => host.userId); + expect(hostUserIds).toContain(teamMember1.id); + expect(hostUserIds).toContain(teamMember2.id); + expect(hostUserIds).toContain(userAdmin.id); + + await eventTypesRepositoryFixture.delete(createdEventType.data.id); + }); + + it("should preserve existing hosts when updating without changing scheduling type", async () => { + const createBody = { + title: `teams-event-types-scheduling-preserve-${randomString()}`, + slug: `teams-event-types-scheduling-preserve-${randomString()}`, + description: "Test preserve hosts", + lengthInMinutes: 60, + locations: [ + { + type: "integration", + integration: "cal-video", + }, + ], + schedulingType: "collective", + hosts: [ + { + userId: teamMember1.id, + }, + { + userId: teamMember2.id, + }, + ], + }; + + const createResponse = await request(app.getHttpServer()) + .post(`/v2/teams/${team.id}/event-types`) + .send(createBody) + .expect(201); + + const createdEventType: ApiSuccessResponse = createResponse.body; + + const updateBody = { + title: "Updated title", + }; + + const updateResponse = await request(app.getHttpServer()) + .patch(`/v2/teams/${team.id}/event-types/${createdEventType.data.id}`) + .send(updateBody) + .expect(200); + + const responseBody: ApiSuccessResponse = updateResponse.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data.title).toEqual("Updated title"); + expect(responseBody.data.schedulingType).toEqual("collective"); + expect(responseBody.data.hosts).toHaveLength(2); + expect(responseBody.data.hosts.map((h) => h.userId)).toContain(teamMember1.id); + expect(responseBody.data.hosts.map((h) => h.userId)).toContain(teamMember2.id); + + await eventTypesRepositoryFixture.delete(createdEventType.data.id); + }); + }); + + describe("should update event type title", () => { + it("should preserve hosts when updating collective event type title", async () => { + const collectiveEventType = await eventTypesRepositoryFixture.create( + { + title: `collective-preserve-hosts-${randomString()}`, + slug: `collective-preserve-hosts-${randomString()}`, + length: 60, + team: { + connect: { + id: team.id, + }, + }, + schedulingType: "COLLECTIVE", + hosts: { + create: [ + { + userId: teamMember1.id, + isFixed: true, + }, + { + userId: teamMember2.id, + isFixed: true, + }, + ], + }, + }, + userAdmin.id + ); + + const newTitle = `updated-collective-title-${randomString()}`; + const updateBody = { + title: newTitle, + }; + + const response = await request(app.getHttpServer()) + .patch(`/v2/teams/${team.id}/event-types/${collectiveEventType.id}`) + .send(updateBody) + .expect(200); + + const responseBody: ApiSuccessResponse = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data.title).toEqual(newTitle); + expect(responseBody.data.schedulingType).toEqual("collective"); + expect(responseBody.data.hosts).toHaveLength(2); + expect(responseBody.data.hosts.map((h) => h.userId)).toContain(teamMember1.id); + expect(responseBody.data.hosts.map((h) => h.userId)).toContain(teamMember2.id); + + await eventTypesRepositoryFixture.delete(collectiveEventType.id); + }); + + it("should preserve hosts when updating round robin event type title", async () => { + const roundRobinEventType = await eventTypesRepositoryFixture.create( + { + title: `roundrobin-preserve-hosts-${randomString()}`, + slug: `roundrobin-preserve-hosts-${randomString()}`, + length: 60, + team: { + connect: { + id: team.id, + }, + }, + schedulingType: "ROUND_ROBIN", + hosts: { + create: [ + { + userId: teamMember1.id, + isFixed: false, + priority: 2, + }, + { + userId: teamMember2.id, + isFixed: true, + priority: 1, + }, + ], + }, + }, + userAdmin.id + ); + + const newTitle = `updated-roundrobin-title-${randomString()}`; + const updateBody = { + title: newTitle, + }; + + const response = await request(app.getHttpServer()) + .patch(`/v2/teams/${team.id}/event-types/${roundRobinEventType.id}`) + .send(updateBody) + .expect(200); + + const responseBody: ApiSuccessResponse = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data.title).toEqual(newTitle); + expect(responseBody.data.schedulingType).toEqual("roundRobin"); + expect(responseBody.data.hosts).toHaveLength(2); + expect(responseBody.data.hosts.map((h) => h.userId)).toContain(teamMember1.id); + expect(responseBody.data.hosts.map((h) => h.userId)).toContain(teamMember2.id); + + await eventTypesRepositoryFixture.delete(roundRobinEventType.id); + }); + }); + + describe("should update event type hosts", () => { + it("should update collective event type hosts", async () => { + const collectiveEventType = await eventTypesRepositoryFixture.create( + { + title: `collective-update-hosts-${randomString()}`, + slug: `collective-update-hosts-${randomString()}`, + length: 60, + team: { + connect: { + id: team.id, + }, + }, + schedulingType: "COLLECTIVE", + hosts: { + create: [ + { + userId: teamMember1.id, + isFixed: true, + }, + ], + }, + }, + userAdmin.id + ); + + const updateBody = { + hosts: [ + { + userId: teamMember2.id, + }, + ], + }; + + const response = await request(app.getHttpServer()) + .patch(`/v2/teams/${team.id}/event-types/${collectiveEventType.id}`) + .send(updateBody) + .expect(200); + + const responseBody: ApiSuccessResponse = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data.schedulingType).toEqual("collective"); + expect(responseBody.data.hosts).toHaveLength(1); + expect(responseBody.data.hosts[0].userId).toEqual(teamMember2.id); + + await eventTypesRepositoryFixture.delete(collectiveEventType.id); + }); + + it("should update round robin event type hosts", async () => { + const roundRobinEventType = await eventTypesRepositoryFixture.create( + { + title: `roundrobin-update-hosts-${randomString()}`, + slug: `roundrobin-update-hosts-${randomString()}`, + length: 60, + team: { + connect: { + id: team.id, + }, + }, + schedulingType: "ROUND_ROBIN", + hosts: { + create: [ + { + userId: teamMember1.id, + isFixed: true, + priority: 1, + }, + ], + }, + }, + userAdmin.id + ); + + const updateBody = { + hosts: [ + { + userId: teamMember2.id, + priority: "high", + }, + ], + }; + + const response = await request(app.getHttpServer()) + .patch(`/v2/teams/${team.id}/event-types/${roundRobinEventType.id}`) + .send(updateBody) + .expect(200); + + const responseBody: ApiSuccessResponse = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data.schedulingType).toEqual("roundRobin"); + expect(responseBody.data.hosts).toHaveLength(1); + expect(responseBody.data.hosts[0].userId).toEqual(teamMember2.id); + + await eventTypesRepositoryFixture.delete(roundRobinEventType.id); + }); + }); + afterAll(async () => { await userRepositoryFixture.deleteByEmail(userAdmin.email); await userRepositoryFixture.deleteByEmail(teamMember1.email); diff --git a/docs/api-reference/v2/openapi.json b/docs/api-reference/v2/openapi.json index ecef1f6c9d..1b3df6da5f 100644 --- a/docs/api-reference/v2/openapi.json +++ b/docs/api-reference/v2/openapi.json @@ -3864,6 +3864,16 @@ "type": "string" } }, + { + "name": "bookingUid", + "required": false, + "in": "query", + "description": "Filter bookings by the booking Uid.", + "example": "2NtaeaVcKfpmSZ4CthFdfk", + "schema": { + "type": "string" + } + }, { "name": "eventTypeIds", "required": false, @@ -21928,6 +21938,12 @@ "default": false, "description": "Boolean to require authentication for booking this event type via api. If true, only authenticated users who are the event-type owner or org/team admin/owner can book this event type." }, + "schedulingType": { + "type": "string", + "enum": ["collective", "roundRobin"], + "example": "collective", + "description": "The scheduling type for the team event - collective or roundRobin. ❗If you change scheduling type you must also provide `hosts` or `assignAllTeamMembers` in the request body, otherwise the event type will have no hosts - this is required because\n in case of collective event type all hosts are mandatory but in case of round robin some or non can be mandatory so we can't predict how you want the hosts to be setup which is why you must provide that information. If you want to convert round robin or collective into managed or managed into round robin or collective then you will have to create a new team event type and delete old one." + }, "hosts": { "description": "Hosts contain specific team members you want to assign to this event type, but if you want to assign all team members, use `assignAllTeamMembers: true` instead and omit this field. For platform customers the hosts can include userIds only of managed users. Provide either hosts or assignAllTeamMembers but not both", "type": "array", diff --git a/packages/platform/types/event-types/event-types_2024_06_14/inputs/update-event-type.input.ts b/packages/platform/types/event-types/event-types_2024_06_14/inputs/update-event-type.input.ts index 7ac1697478..550c1c3e0b 100644 --- a/packages/platform/types/event-types/event-types_2024_06_14/inputs/update-event-type.input.ts +++ b/packages/platform/types/event-types/event-types_2024_06_14/inputs/update-event-type.input.ts @@ -11,8 +11,11 @@ import { ArrayNotEmpty, ArrayUnique, IsUrl, + IsIn, } from "class-validator"; +import { SchedulingType } from "@calcom/platform-enums"; + import { RequiresAtLeastOnePropertyWhenNotDisabled } from "../../../utils/RequiresOneOfPropertiesWhenNotDisabled"; import { BookerActiveBookingsLimit_2024_06_14 } from "./booker-active-booking-limit.input"; import { BookerLayouts_2024_06_14 } from "./booker-layouts.input"; @@ -476,6 +479,25 @@ export class UpdateEventTypeInput_2024_06_14 extends BaseUpdateEventTypeInput { } export class UpdateTeamEventTypeInput_2024_06_14 extends BaseUpdateEventTypeInput { + @Transform(({ value }) => { + if (value === "collective") { + return SchedulingType.COLLECTIVE; + } + if (value === "roundRobin") { + return SchedulingType.ROUND_ROBIN; + } + return value; + }) + @IsIn([SchedulingType.COLLECTIVE, SchedulingType.ROUND_ROBIN]) + @IsOptional() + @DocsPropertyOptional({ + enum: ["collective", "roundRobin"], + example: "collective", + description: `The scheduling type for the team event - collective or roundRobin. ❗If you change scheduling type you must also provide \`hosts\` or \`assignAllTeamMembers\` in the request body, otherwise the event type will have no hosts - this is required because + in case of collective event type all hosts are mandatory but in case of round robin some or non can be mandatory so we can't predict how you want the hosts to be setup which is why you must provide that information. If you want to convert round robin or collective into managed or managed into round robin or collective then you will have to create a new team event type and delete old one.`, + }) + schedulingType?: "COLLECTIVE" | "ROUND_ROBIN"; + @ValidateNested({ each: true }) @Type(() => Host) @IsArray()