From 25d39b42738d8119a13dd1e95354b23cce656002 Mon Sep 17 00:00:00 2001 From: sean-brydon <55134778+sean-brydon@users.noreply.github.com> Date: Fri, 8 Nov 2024 12:42:59 +0000 Subject: [PATCH] feat: api v2 reassign (#17460) * adding input types and method to controller/service * add output types * add outputs and inputs * feat: reassign automatically * feat: reassign to user * e2e test for reassign * update platform libraries + experimental modules for jest * fix test --------- Co-authored-by: supalarry Co-authored-by: Lauris Skraucis --- apps/api/v2/package.json | 4 +- .../2024-08-13/bookings.repository.ts | 11 + .../controllers/bookings.controller.ts | 58 ++- .../controllers/reassign-bookings.e2e-spec.ts | 337 ++++++++++++++++++ .../outputs/reassign-booking.output.ts | 21 ++ .../2024-08-13/services/bookings.service.ts | 60 +++- .../2024-08-13/services/output.service.ts | 16 + packages/platform/libraries/index.ts | 3 + .../types/bookings/2024-08-13/inputs/index.ts | 1 + .../inputs/reassign-to-user.input.ts | 13 + .../2024-08-13/outputs/booking.output.ts | 31 ++ yarn.lock | 71 +++- 12 files changed, 614 insertions(+), 12 deletions(-) create mode 100644 apps/api/v2/src/ee/bookings/2024-08-13/controllers/reassign-bookings.e2e-spec.ts create mode 100644 apps/api/v2/src/ee/bookings/2024-08-13/outputs/reassign-booking.output.ts create mode 100644 packages/platform/types/bookings/2024-08-13/inputs/reassign-to-user.input.ts diff --git a/apps/api/v2/package.json b/apps/api/v2/package.json index 77cbe4910d..0ba1ec5124 100644 --- a/apps/api/v2/package.json +++ b/apps/api/v2/package.json @@ -19,7 +19,7 @@ "test:watch": "yarn dev:build && jest --watch", "test:cov": "yarn dev:build && jest --coverage", "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", - "test:e2e": "yarn dev:build && NODE_OPTIONS='--max_old_space_size=8192' jest --ci --forceExit --config ./jest-e2e.json", + "test:e2e": "yarn dev:build && NODE_OPTIONS='--max_old_space_size=8192 --experimental-vm-modules' jest --ci --forceExit --config ./jest-e2e.json", "test:e2e:watch": "yarn dev:build && jest --runInBand --detectOpenHandles --forceExit --config ./jest-e2e.json --watch", "prisma": "yarn workspace @calcom/prisma prisma", "generate-schemas": "yarn prisma generate && yarn prisma format", @@ -28,7 +28,7 @@ "dependencies": { "@calcom/platform-constants": "*", "@calcom/platform-enums": "*", - "@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.56", + "@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.57", "@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2", "@calcom/platform-types": "*", "@calcom/platform-utils": "*", diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/bookings.repository.ts b/apps/api/v2/src/ee/bookings/2024-08-13/bookings.repository.ts index 2cc2dfb3bb..03b9d0410f 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/bookings.repository.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/bookings.repository.ts @@ -56,6 +56,17 @@ export class BookingsRepository_2024_08_13 { }); } + async getByUidWithUser(bookingUid: string) { + return this.dbRead.prisma.booking.findUnique({ + where: { + uid: bookingUid, + }, + include: { + user: true, + }, + }); + } + async getByIdWithAttendeesAndUserAndEvent(id: number) { return this.dbRead.prisma.booking.findUnique({ where: { diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.ts b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.ts index 6e23698203..ddddd42d4c 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.ts @@ -2,6 +2,7 @@ import { BookingUidGuard } from "@/ee/bookings/2024-08-13/guards/booking-uid.gua import { CancelBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/cancel-booking.output"; import { CreateBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/create-booking.output"; import { MarkAbsentBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/mark-absent.output"; +import { ReassignBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/reassign-booking.output"; import { RescheduleBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/reschedule-booking.output"; import { BookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/bookings.service"; import { VERSION_2024_08_13_VALUE } from "@/lib/api-versions"; @@ -9,6 +10,7 @@ import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator"; import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator"; import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard"; import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard"; +import { UserWithProfile } from "@/modules/users/users.repository"; import { Controller, Post, @@ -46,8 +48,7 @@ import { CreateBookingInputPipe, CreateBookingInput, GetBookingsInput_2024_08_13, - RescheduleBookingInput_2024_08_13, - CancelBookingInput_2024_08_13, + ReassignToUserBookingInput_2024_08_13, MarkAbsentBookingInput_2024_08_13, CreateBookingInput_2024_08_13, CreateInstantBookingInput_2024_08_13, @@ -227,4 +228,57 @@ export class BookingsController_2024_08_13 { data: booking, }; } + + @Post("/:bookingUid/reassign") + @HttpCode(HttpStatus.OK) + @Permissions([BOOKING_WRITE]) + @UseGuards(ApiAuthGuard, BookingUidGuard) + @ApiHeader({ + name: "Authorization", + description: + "value must be `Bearer ` where `` either managed user access token or api key prefixed with cal_", + required: true, + }) + @ApiOperation({ summary: "Automatically reassign booking to a new host" }) + async reassignBooking( + @Param("bookingUid") bookingUid: string, + @GetUser() user: UserWithProfile + ): Promise { + const booking = await this.bookingsService.reassignBooking(bookingUid, user); + + return { + status: SUCCESS_STATUS, + data: booking, + }; + } + + @Post("/:bookingUid/reassign/:userId") + @HttpCode(HttpStatus.OK) + @Permissions([BOOKING_WRITE]) + @UseGuards(ApiAuthGuard, BookingUidGuard) + @ApiHeader({ + name: "Authorization", + description: + "value must be `Bearer ` where `` either managed user access token or api key prefixed with cal_", + required: true, + }) + @ApiOperation({ summary: "Reassign a booking to a specific user" }) + async reassignBookingToUser( + @Param("bookingUid") bookingUid: string, + @Param("userId") userId: number, + @GetUser("id") reassignedById: number, + @Body() body: ReassignToUserBookingInput_2024_08_13 + ): Promise { + const booking = await this.bookingsService.reassignBookingToUser( + bookingUid, + userId, + reassignedById, + body + ); + + return { + status: SUCCESS_STATUS, + data: booking, + }; + } } diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/reassign-bookings.e2e-spec.ts b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/reassign-bookings.e2e-spec.ts new file mode 100644 index 0000000000..adf6ea69b7 --- /dev/null +++ b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/reassign-bookings.e2e-spec.ts @@ -0,0 +1,337 @@ +import { bootstrap } from "@/app"; +import { AppModule } from "@/app.module"; +import { CreateBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/create-booking.output"; +import { ReassignBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/reassign-booking.output"; +import { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input"; +import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module"; +import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service"; +import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard"; +import { PrismaModule } from "@/modules/prisma/prisma.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 { Booking, User } from "@prisma/client"; +import * as request from "supertest"; +import { BookingsRepositoryFixture } from "test/fixtures/repository/bookings.repository.fixture"; +import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture"; +import { HostsRepositoryFixture } from "test/fixtures/repository/hosts.repository.fixture"; +import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture"; +import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.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 { withApiAuth } from "test/utils/withApiAuth"; + +import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants"; +import { + CreateBookingInput_2024_08_13, + BookingOutput_2024_08_13, + RecurringBookingOutput_2024_08_13, + GetBookingsOutput_2024_08_13, + GetSeatedBookingOutput_2024_08_13, +} from "@calcom/platform-types"; +import { PlatformOAuthClient, Team } from "@calcom/prisma/client"; + +describe("Bookings Endpoints 2024-08-13", () => { + describe("Reassign bookings", () => { + let app: INestApplication; + let organization: Team; + let team: Team; + + let userRepositoryFixture: UserRepositoryFixture; + let bookingsRepositoryFixture: BookingsRepositoryFixture; + let schedulesService: SchedulesService_2024_04_15; + let eventTypesRepositoryFixture: EventTypesRepositoryFixture; + let oauthClientRepositoryFixture: OAuthClientRepositoryFixture; + let oAuthClient: PlatformOAuthClient; + let teamRepositoryFixture: TeamRepositoryFixture; + let membershipsRepositoryFixture: MembershipRepositoryFixture; + let hostsRepositoryFixture: HostsRepositoryFixture; + let organizationsRepositoryFixture: OrganizationRepositoryFixture; + let profileRepositoryFixture: ProfileRepositoryFixture; + + const teamUserEmail = "orgUser1team1@api.com"; + const teamUserEmail2 = "orgUser2team1@api.com"; + let teamUser1: User; + let teamUser2: User; + + let teamRoundRobinEventTypeId: number; + + let roundRobinBooking: Booking; + + beforeAll(async () => { + const moduleRef = await withApiAuth( + teamUserEmail, + Test.createTestingModule({ + imports: [AppModule, PrismaModule, UsersModule, SchedulesModule_2024_04_15], + }) + ) + .overrideGuard(PermissionsGuard) + .useValue({ + canActivate: () => true, + }) + .compile(); + + userRepositoryFixture = new UserRepositoryFixture(moduleRef); + bookingsRepositoryFixture = new BookingsRepositoryFixture(moduleRef); + eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef); + oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef); + teamRepositoryFixture = new TeamRepositoryFixture(moduleRef); + organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef); + profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef); + membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef); + hostsRepositoryFixture = new HostsRepositoryFixture(moduleRef); + schedulesService = moduleRef.get(SchedulesService_2024_04_15); + + organization = await organizationsRepositoryFixture.create({ name: "organization team bookings" }); + team = await teamRepositoryFixture.create({ + name: "team 1", + isOrganization: false, + parent: { connect: { id: organization.id } }, + }); + + oAuthClient = await createOAuthClient(organization.id); + + teamUser1 = await userRepositoryFixture.create({ + email: teamUserEmail, + locale: "it", + name: "orgUser1team1", + }); + + teamUser2 = await userRepositoryFixture.create({ + email: teamUserEmail2, + locale: "it", + name: "orgUser2team1", + }); + + const userSchedule: CreateScheduleInput_2024_04_15 = { + name: "working time", + timeZone: "Europe/Rome", + isDefault: true, + }; + await schedulesService.createUserSchedule(teamUser1.id, userSchedule); + await schedulesService.createUserSchedule(teamUser2.id, userSchedule); + + await profileRepositoryFixture.create({ + uid: `usr-${teamUser1.id}`, + username: teamUserEmail, + organization: { + connect: { + id: organization.id, + }, + }, + user: { + connect: { + id: teamUser1.id, + }, + }, + }); + + await profileRepositoryFixture.create({ + uid: `usr-${teamUser2.id}`, + username: teamUserEmail2, + organization: { + connect: { + id: organization.id, + }, + }, + user: { + connect: { + id: teamUser2.id, + }, + }, + }); + + await membershipsRepositoryFixture.create({ + role: "MEMBER", + team: { connect: { id: team.id } }, + user: { connect: { id: teamUser1.id } }, + accepted: true, + }); + + await membershipsRepositoryFixture.create({ + role: "MEMBER", + team: { connect: { id: team.id } }, + user: { connect: { id: teamUser2.id } }, + accepted: true, + }); + + const team1EventType = await eventTypesRepositoryFixture.createTeamEventType({ + schedulingType: "ROUND_ROBIN", + team: { + connect: { id: team.id }, + }, + users: { + connect: [{ id: teamUser1.id }, { id: teamUser2.id }], + }, + title: "Round Robin Event Type", + slug: "round-robin-event-type", + length: 60, + assignAllTeamMembers: false, + bookingFields: [], + locations: [{ type: "inPerson", address: "via 10, rome, italy" }], + }); + + teamRoundRobinEventTypeId = team1EventType.id; + + await hostsRepositoryFixture.create({ + isFixed: false, + user: { + connect: { + id: teamUser1.id, + }, + }, + eventType: { + connect: { + id: team1EventType.id, + }, + }, + }); + + await hostsRepositoryFixture.create({ + isFixed: false, + user: { + connect: { + id: teamUser2.id, + }, + }, + eventType: { + connect: { + id: team1EventType.id, + }, + }, + }); + + roundRobinBooking = await bookingsRepositoryFixture.create({ + user: { + connect: { + id: teamUser1.id, + }, + }, + startTime: new Date(Date.UTC(2050, 0, 7, 13, 0, 0)), + endTime: new Date(Date.UTC(2050, 0, 7, 14, 0, 0)), + title: "round robin coding lets goo", + uid: "round-robin-coding", + eventType: { + connect: { + id: teamRoundRobinEventTypeId, + }, + }, + location: "via 10, rome, italy", + customInputs: {}, + metadata: {}, + responses: { + name: "Bob", + email: "bob@gmail.com", + }, + attendees: { + create: { + email: "bob@gmail.com", + name: "Bob", + locale: "it", + timeZone: "Europe/Rome", + }, + }, + }); + + app = moduleRef.createNestApplication(); + bootstrap(app as NestExpressApplication); + + await app.init(); + }); + + it("should reassign round robin booking", async () => { + const booking = await bookingsRepositoryFixture.getByUid(roundRobinBooking.uid); + expect(booking?.userId).toEqual(teamUser1.id); + + return request(app.getHttpServer()) + .post(`/v2/bookings/${roundRobinBooking.uid}/reassign`) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .expect(200) + .then(async (response) => { + const responseBody: ReassignBookingOutput_2024_08_13 = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + + const data: ReassignBookingOutput_2024_08_13["data"] = responseBody.data; + expect(data.bookingUid).toEqual(roundRobinBooking.uid); + expect(data.reassignedTo).toEqual({ + id: teamUser2.id, + name: teamUser2.name, + email: teamUser2.email, + }); + + const reassigned = await bookingsRepositoryFixture.getByUid(roundRobinBooking.uid); + expect(reassigned?.userId).toEqual(teamUser2.id); + }); + }); + + it("should reassign round robin booking to a specific user", async () => { + const booking = await bookingsRepositoryFixture.getByUid(roundRobinBooking.uid); + expect(booking?.userId).toEqual(teamUser2.id); + + const body: CreateBookingInput_2024_08_13 = { + start: new Date(Date.UTC(2030, 0, 8, 13, 0, 0)).toISOString(), + eventTypeId: teamRoundRobinEventTypeId, + attendee: { + name: "alice", + email: "alice@gmail.com", + timeZone: "Europe/Madrid", + language: "es", + }, + meetingUrl: "https://meet.google.com/abc-def-ghi", + }; + + return request(app.getHttpServer()) + .post(`/v2/bookings/${roundRobinBooking.uid}/reassign/${teamUser1.id}`) + .send(body) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .expect(200) + .then(async (response) => { + const responseBody: ReassignBookingOutput_2024_08_13 = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + + const data: ReassignBookingOutput_2024_08_13["data"] = responseBody.data; + expect(data.bookingUid).toEqual(roundRobinBooking.uid); + expect(data.reassignedTo).toEqual({ + id: teamUser1.id, + name: teamUser1.name, + email: teamUser1.email, + }); + + const reassigned = await bookingsRepositoryFixture.getByUid(roundRobinBooking.uid); + expect(reassigned?.userId).toEqual(teamUser1.id); + }); + }); + + async function createOAuthClient(organizationId: number) { + const data = { + logo: "logo-url", + name: "name", + redirectUris: ["http://localhost:5555"], + permissions: 32, + }; + const secret = "secret"; + + const client = await oauthClientRepositoryFixture.create(organizationId, data, secret); + return client; + } + + function responseDataIsBooking(data: any): data is BookingOutput_2024_08_13 { + return !Array.isArray(data) && typeof data === "object" && data && "id" in data; + } + + afterAll(async () => { + await oauthClientRepositoryFixture.delete(oAuthClient.id); + await teamRepositoryFixture.delete(organization.id); + await userRepositoryFixture.deleteByEmail(teamUser1.email); + await userRepositoryFixture.deleteByEmail(teamUserEmail2); + await bookingsRepositoryFixture.deleteAllBookings(teamUser1.id, teamUser1.email); + await bookingsRepositoryFixture.deleteAllBookings(teamUser2.id, teamUser2.email); + await app.close(); + }); + }); +}); diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/outputs/reassign-booking.output.ts b/apps/api/v2/src/ee/bookings/2024-08-13/outputs/reassign-booking.output.ts new file mode 100644 index 0000000000..8c01e8777e --- /dev/null +++ b/apps/api/v2/src/ee/bookings/2024-08-13/outputs/reassign-booking.output.ts @@ -0,0 +1,21 @@ +import { ApiProperty, getSchemaPath } from "@nestjs/swagger"; +import { Type } from "class-transformer"; +import { IsEnum, ValidateNested } from "class-validator"; + +import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants"; +import { ReassignBookingOutput_2024_08_13 as ReassignBookingOutputData_2024_08_13 } from "@calcom/platform-types"; + +export class ReassignBookingOutput_2024_08_13 { + @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) + @IsEnum([SUCCESS_STATUS, ERROR_STATUS]) + status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS; + + @ApiProperty({ + oneOf: [{ $ref: getSchemaPath(ReassignBookingOutputData_2024_08_13) }], + description: + "Booking data, which can be either a ReassignAutoBookingOutput object or a ReassignManualBookingOutput object", + }) + @ValidateNested() + @Type(() => Object) + data!: ReassignBookingOutputData_2024_08_13; +} diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts index 2ed6c4d90e..6395b1ff97 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts @@ -5,6 +5,8 @@ import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_20 import { BillingService } from "@/modules/billing/services/billing.service"; import { BookingSeatRepository } from "@/modules/booking-seat/booking-seat.repository"; import { PrismaReadService } from "@/modules/prisma/prisma-read.service"; +import { UsersService } from "@/modules/users/services/users.service"; +import { UsersRepository, UserWithProfile } from "@/modules/users/users.repository"; import { Injectable, Logger, NotFoundException } from "@nestjs/common"; import { BadRequestException } from "@nestjs/common"; import { Request } from "express"; @@ -15,6 +17,8 @@ import { getAllUserBookings, handleInstantMeeting, handleCancelBooking, + roundRobinReassignment, + roundRobinManualReassignment, handleMarkNoShow, } from "@calcom/platform-libraries"; import { @@ -23,8 +27,8 @@ import { CreateRecurringBookingInput_2024_08_13, GetBookingsInput_2024_08_13, CreateInstantBookingInput_2024_08_13, - CancelBookingInput_2024_08_13, MarkAbsentBookingInput_2024_08_13, + ReassignToUserBookingInput_2024_08_13, BookingOutput_2024_08_13, RecurringBookingOutput_2024_08_13, GetSeatedBookingOutput_2024_08_13, @@ -50,7 +54,9 @@ export class BookingsService_2024_08_13 { private readonly bookingSeatRepository: BookingSeatRepository, private readonly eventTypesRepository: EventTypesRepository_2024_06_14, private readonly prismaReadService: PrismaReadService, - private readonly billingService: BillingService + private readonly billingService: BillingService, + private readonly usersService: UsersService, + private readonly usersRepository: UsersRepository ) {} async createBooking(request: Request, body: CreateBookingInput) { @@ -358,4 +364,54 @@ export class BookingsService_2024_08_13 { fromReschedule: oldBookingUid, }); } + + async reassignBooking(bookingUid: string, requestUser: UserWithProfile) { + const booking = await this.bookingsRepository.getByUid(bookingUid); + if (!booking) { + throw new NotFoundException(`Booking with uid=${bookingUid} was not found in the database`); + } + + const profile = this.usersService.getUserMainProfile(requestUser); + + await roundRobinReassignment({ + bookingId: booking.id, + orgId: profile?.organizationId || null, + }); + + const reassigned = await this.bookingsRepository.getByUidWithUser(bookingUid); + if (!reassigned) { + throw new NotFoundException(`Reassigned booking with uid=${bookingUid} was not found in the database`); + } + + return this.outputService.getOutputReassignedBooking(reassigned); + } + + async reassignBookingToUser( + bookingUid: string, + newUserId: number, + reassignedById: number, + body: ReassignToUserBookingInput_2024_08_13 + ) { + const booking = await this.bookingsRepository.getByUid(bookingUid); + if (!booking) { + throw new NotFoundException(`Booking with uid=${bookingUid} was not found in the database`); + } + + const user = await this.usersRepository.findByIdWithProfile(newUserId); + if (!user) { + throw new NotFoundException(`User with id=${newUserId} was not found in the database`); + } + + const profile = this.usersService.getUserMainProfile(user); + + const reassigned = await roundRobinManualReassignment({ + bookingId: booking.id, + newUserId, + orgId: profile?.organizationId || null, + reassignReason: body.reason, + reassignedById, + }); + + return this.outputService.getOutputReassignedBooking(reassigned); + } } diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/output.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/output.service.ts index 2ddd572e6f..b515f9ce35 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/services/output.service.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/services/output.service.ts @@ -10,6 +10,7 @@ import { CreateSeatedBookingOutput_2024_08_13, GetRecurringSeatedBookingOutput_2024_08_13, GetSeatedBookingOutput_2024_08_13, + ReassignBookingOutput_2024_08_13, RecurringBookingOutput_2024_08_13, SeatedAttendee, } from "@calcom/platform-types"; @@ -50,6 +51,8 @@ type DatabaseBooking = Booking & { }[]; } & { user: { id: number; name: string | null; email: string } | null }; +type BookingWithUser = Booking & { user: { id: number; name: string | null; email: string } | null }; + @Injectable() export class OutputBookingsService_2024_08_13 { constructor(private readonly bookingsRepository: BookingsRepository_2024_08_13) {} @@ -308,4 +311,17 @@ export class OutputBookingsService_2024_08_13 { return parsed; } + + getOutputReassignedBooking( + databaseBooking: Pick + ): ReassignBookingOutput_2024_08_13 { + return { + bookingUid: databaseBooking.uid, + reassignedTo: { + id: databaseBooking?.user?.id || 0, + name: databaseBooking?.user?.name || "unknown", + email: databaseBooking?.user?.email || "unknown", + }, + }; + } } diff --git a/packages/platform/libraries/index.ts b/packages/platform/libraries/index.ts index 1b6ccfc3d7..b54c9691c3 100644 --- a/packages/platform/libraries/index.ts +++ b/packages/platform/libraries/index.ts @@ -161,6 +161,9 @@ export { getTranslation }; export { updateNewTeamMemberEventTypes } from "@calcom/lib/server/queries"; +export { roundRobinReassignment } from "@calcom/features/ee/round-robin/roundRobinReassignment"; +export { roundRobinManualReassignment } from "@calcom/features/ee/round-robin/roundRobinManualReassignment"; + export { ErrorCode } from "@calcom/lib/errorCodes"; export { IcsFeedCalendarService }; diff --git a/packages/platform/types/bookings/2024-08-13/inputs/index.ts b/packages/platform/types/bookings/2024-08-13/inputs/index.ts index 0f1e66b8d8..ca9fb7b155 100644 --- a/packages/platform/types/bookings/2024-08-13/inputs/index.ts +++ b/packages/platform/types/bookings/2024-08-13/inputs/index.ts @@ -4,5 +4,6 @@ export * from "./get-bookings.input"; export * from "./reschedule-booking.input"; export * from "./cancel-booking.input"; export * from "./mark-absent.input"; +export * from "./reassign-to-user.input"; export * from "./reschedule-booking-input.pipe"; export * from "./cancel-booking-input.pipe"; diff --git a/packages/platform/types/bookings/2024-08-13/inputs/reassign-to-user.input.ts b/packages/platform/types/bookings/2024-08-13/inputs/reassign-to-user.input.ts new file mode 100644 index 0000000000..f36fc2c48c --- /dev/null +++ b/packages/platform/types/bookings/2024-08-13/inputs/reassign-to-user.input.ts @@ -0,0 +1,13 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { IsOptional, IsString } from "class-validator"; + +export class ReassignToUserBookingInput_2024_08_13 { + @IsString() + @IsOptional() + @ApiProperty({ + example: "Host has to take another call", + required: false, + description: "Reason for reassigning the booking", + }) + reason?: string; +} diff --git a/packages/platform/types/bookings/2024-08-13/outputs/booking.output.ts b/packages/platform/types/bookings/2024-08-13/outputs/booking.output.ts index e759dd7b1d..8eed00c155 100644 --- a/packages/platform/types/bookings/2024-08-13/outputs/booking.output.ts +++ b/packages/platform/types/bookings/2024-08-13/outputs/booking.output.ts @@ -9,6 +9,7 @@ import { IsObject, IsOptional, IsString, + IsEmail, IsTimeZone, IsUrl, ValidateNested, @@ -291,3 +292,33 @@ export class CreateRecurringSeatedBookingOutput_2024_08_13 extends BaseBookingOu @Expose() recurringBookingUid!: string; } + +class ReassignedToDto { + @ApiProperty({ type: Number, example: 123 }) + @IsInt() + @Expose() + id!: number; + + @ApiProperty({ type: String, example: "John Doe" }) + @IsString() + @Expose() + name!: string; + + @ApiProperty({ type: String, example: "john.doe@example.com" }) + @IsEmail() + @Expose() + email!: string; +} + +export class ReassignBookingOutput_2024_08_13 { + @ApiProperty({ type: String, example: "booking_uid_123" }) + @IsString() + @Expose() + bookingUid!: string; + + @ApiProperty({ type: ReassignedToDto }) + @ValidateNested() + @Type(() => ReassignedToDto) + @Expose() + reassignedTo!: ReassignedToDto; +} diff --git a/yarn.lock b/yarn.lock index 50ebb88bcc..48d9ba4530 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4125,7 +4125,7 @@ __metadata: dependencies: "@calcom/platform-constants": "*" "@calcom/platform-enums": "*" - "@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.53" + "@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.57" "@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2" "@calcom/platform-types": "*" "@calcom/platform-utils": "*" @@ -5141,14 +5141,14 @@ __metadata: languageName: node linkType: hard -"@calcom/platform-libraries@npm:@calcom/platform-libraries@0.0.53": - version: 0.0.53 - resolution: "@calcom/platform-libraries@npm:0.0.53" +"@calcom/platform-libraries@npm:@calcom/platform-libraries@0.0.57": + version: 0.0.57 + resolution: "@calcom/platform-libraries@npm:0.0.57" dependencies: "@calcom/core": "*" "@calcom/features": "*" "@calcom/lib": "*" - checksum: 5a9dadad9f1ffdb831353594acd40bbc96e12130eff1685c9be49dfdc7ce2e44b2086696273b3bb74a28db20d863e0814d95f50043a4ba5b451987bd6f401294 + checksum: 2d2bbf5dc964c390b354de01f124329ae7e75313bde9a8971541e05b55c26c36fa6f580dbff5b90f8bcd74f3864e6f6385ff3efd2d0cf749dfaec698b3262c98 languageName: node linkType: hard @@ -5763,6 +5763,8 @@ __metadata: nuqs: ^1.20.0 otplib: ^12.0.1 postcss: ^8.4.18 + posthog-js: ^1.164.1 + posthog-node: ^4.2.0 qrcode: ^1.5.1 raw-body: ^2.5.1 react: ^18.2.0 @@ -20856,7 +20858,7 @@ __metadata: languageName: node linkType: hard -"axios@npm:^1.6.1, axios@npm:^1.6.8": +"axios@npm:^1.6.1, axios@npm:^1.6.8, axios@npm:^1.7.4": version: 1.7.7 resolution: "axios@npm:1.7.7" dependencies: @@ -23690,6 +23692,13 @@ __metadata: languageName: node linkType: hard +"core-js@npm:^3.38.1": + version: 3.39.0 + resolution: "core-js@npm:3.39.0" + checksum: 7a3670e9a2a89e0a049daa288d742d09f6e16d27a8945c5e2ef6fc45dc57e5c4bc5db589da05947486f54ae978d14cf27bd3fb1db0b9907000a611e8af37355b + languageName: node + linkType: hard + "core-util-is@npm:1.0.2": version: 1.0.2 resolution: "core-util-is@npm:1.0.2" @@ -27823,6 +27832,13 @@ __metadata: languageName: node linkType: hard +"fflate@npm:^0.4.8": + version: 0.4.8 + resolution: "fflate@npm:0.4.8" + checksum: 29d8cbe44d5e7f53e7f5a160ac7f9cc025480c7b3bfd85c5f898cbe20dfa2dad4732daa534982664bf30b35896a90af44ea33ede5d94c5ffd1b8b0c0a0a56ca2 + languageName: node + linkType: hard + "fflate@npm:^0.7.3": version: 0.7.4 resolution: "fflate@npm:0.7.4" @@ -40001,6 +40017,28 @@ __metadata: languageName: node linkType: hard +"posthog-js@npm:^1.164.1": + version: 1.181.0 + resolution: "posthog-js@npm:1.181.0" + dependencies: + core-js: ^3.38.1 + fflate: ^0.4.8 + preact: ^10.19.3 + web-vitals: ^4.2.0 + checksum: e48b53844701c9db6c3c3da7af3f251e04e1572a1079c673e7302452c84e0f0e27c4b6b22ddb215afc1996a7f091b69218e927f83d7c0c18f7f7463fef06ee1a + languageName: node + linkType: hard + +"posthog-node@npm:^4.2.0": + version: 4.2.1 + resolution: "posthog-node@npm:4.2.1" + dependencies: + axios: ^1.7.4 + rusha: ^0.8.14 + checksum: f6871399069f329cd4d4c156e82a59c7ab222229d6f49fb62159738f0161a170d723b6707c229af2ca91ba3674f3bb243759d1a615333c23bec188b8122d35ab + languageName: node + linkType: hard + "preact-render-to-string@npm:^5.1.19": version: 5.2.6 resolution: "preact-render-to-string@npm:5.2.6" @@ -40012,6 +40050,13 @@ __metadata: languageName: node linkType: hard +"preact@npm:^10.19.3": + version: 10.24.3 + resolution: "preact@npm:10.24.3" + checksum: 372f601576f52d6417a750a8732cd83c4fc133b0b136f82ea69f013092266ad0213c160b71ae421a0fc7ab04caacb651c29dbf515e3aec26d82b0a8675e8786e + languageName: node + linkType: hard + "preact@npm:^10.6.3": version: 10.11.3 resolution: "preact@npm:10.11.3" @@ -43467,6 +43512,13 @@ __metadata: languageName: node linkType: hard +"rusha@npm:^0.8.14": + version: 0.8.14 + resolution: "rusha@npm:0.8.14" + checksum: d76d7b4b5df1191712eca1e2465d07054dc9bd5e9ec357e1e0f3ddf95006a762330a3c0348e96ac39853656a3fbe02e65e780aa5f61b9355ad914fa296185623 + languageName: node + linkType: hard + "rxjs@npm:7.8.1, rxjs@npm:^7.0.0, rxjs@npm:^7.8.1": version: 7.8.1 resolution: "rxjs@npm:7.8.1" @@ -49058,6 +49110,13 @@ __metadata: languageName: node linkType: hard +"web-vitals@npm:^4.2.0": + version: 4.2.4 + resolution: "web-vitals@npm:4.2.4" + checksum: 5b3ffe1db33f23aebf8cc8560ac574401a95939baafde5841835c1bb1c01f9a2478442f319f77aa0d7914739fc2f6b020c5d5b128c16c5c77ca6be2f9dfbbde6 + languageName: node + linkType: hard + "webcrypto-core@npm:^1.8.0": version: 1.8.0 resolution: "webcrypto-core@npm:1.8.0"