Skip to content

Commit 90f97c3

Browse files
authored
fix: preserve attendee names during round robin host reassignment when reassignmentReason is required (calcom#24771)
1 parent 1407886 commit 90f97c3

4 files changed

Lines changed: 186 additions & 18 deletions

File tree

apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/reassign-bookings.e2e-spec.ts

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,11 +58,15 @@ describe("Bookings Endpoints 2024-08-13", () => {
5858
let teamRoundRobinEventTypeId: number;
5959
let teamRoundRobinFixedHostEventTypeId: number;
6060
let teamRoundRobinNonFixedEventTypeId: number;
61+
let teamRoundRobinWithRescheduleReasonEventTypeId: number;
6162

6263
let teamRoundRobinNonFixedEventTypeTitle: string;
6364
let teamRoundRobinFixedHostEventTypeTitle: string;
65+
let teamRoundRobinWithRescheduleReasonEventTypeTitle: string;
6466

6567
let roundRobinBooking: Booking;
68+
let rescheduleReasonBookingUid: string;
69+
let rescheduleReasonBookingInitialHostId: number;
6670

6771
beforeAll(async () => {
6872
const moduleRef = await withApiAuth(
@@ -389,6 +393,74 @@ describe("Bookings Endpoints 2024-08-13", () => {
389393
},
390394
});
391395

396+
const teamEventTypeWithRescheduleReason = await eventTypesRepositoryFixture.createTeamEventType({
397+
schedulingType: "ROUND_ROBIN",
398+
team: {
399+
connect: { id: team.id },
400+
},
401+
users: {
402+
connect: [{ id: teamUser1.id }, { id: teamUser2.id }],
403+
},
404+
title: `reassign-bookings-2024-08-13-reschedule-reason-event-type-${randomString()}`,
405+
slug: `reassign-bookings-2024-08-13-reschedule-reason-event-type-${randomString()}`,
406+
length: 60,
407+
assignAllTeamMembers: false,
408+
bookingFields: [
409+
{
410+
name: "rescheduleReason",
411+
type: "textarea",
412+
defaultLabel: "Reason for rescheduling",
413+
required: true,
414+
sources: [
415+
{
416+
id: "default",
417+
type: "default",
418+
label: "Default",
419+
},
420+
],
421+
editable: "system",
422+
views: [
423+
{
424+
id: "reschedule",
425+
label: "Reschedule View",
426+
},
427+
],
428+
},
429+
],
430+
locations: [{ type: "inPerson", address: "via 10, rome, italy" }],
431+
});
432+
433+
teamRoundRobinWithRescheduleReasonEventTypeId = teamEventTypeWithRescheduleReason.id;
434+
teamRoundRobinWithRescheduleReasonEventTypeTitle = teamEventTypeWithRescheduleReason.title;
435+
436+
await hostsRepositoryFixture.create({
437+
isFixed: false,
438+
user: {
439+
connect: {
440+
id: teamUser1.id,
441+
},
442+
},
443+
eventType: {
444+
connect: {
445+
id: teamEventTypeWithRescheduleReason.id,
446+
},
447+
},
448+
});
449+
450+
await hostsRepositoryFixture.create({
451+
isFixed: false,
452+
user: {
453+
connect: {
454+
id: teamUser2.id,
455+
},
456+
},
457+
eventType: {
458+
connect: {
459+
id: teamEventTypeWithRescheduleReason.id,
460+
},
461+
},
462+
});
463+
392464
app = moduleRef.createNestApplication();
393465
bootstrap(app as NestExpressApplication);
394466

@@ -558,6 +630,100 @@ describe("Bookings Endpoints 2024-08-13", () => {
558630
});
559631
});
560632

633+
it("should preserve attendee name when reassigning round robin host manually with rescheduleReason required", async () => {
634+
const bookingBody: CreateBookingInput_2024_08_13 = {
635+
start: new Date(Date.UTC(2050, 0, 10, 13, 0, 0)).toISOString(),
636+
eventTypeId: teamRoundRobinWithRescheduleReasonEventTypeId,
637+
attendee: {
638+
name: "David",
639+
email: "david@gmail.com",
640+
timeZone: "Europe/Rome",
641+
language: "en",
642+
},
643+
meetingUrl: "https://meet.google.com/abc-def-ghi",
644+
};
645+
646+
const createResponse = await request(app.getHttpServer())
647+
.post("/v2/bookings")
648+
.send(bookingBody)
649+
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
650+
.expect(201);
651+
652+
const bookingUid = createResponse.body.data.uid;
653+
rescheduleReasonBookingUid = bookingUid;
654+
const booking = await bookingsRepositoryFixture.getByUid(bookingUid);
655+
656+
expect(booking).toBeDefined();
657+
expect(booking?.userId).toBeDefined();
658+
const initialHostId = booking!.userId!;
659+
rescheduleReasonBookingInitialHostId = initialHostId;
660+
661+
const expectedInitialTitle = `${teamRoundRobinWithRescheduleReasonEventTypeTitle} between ${
662+
booking?.userId === teamUser1.id ? teamUser1.name : teamUser2.name
663+
} and David`;
664+
expect(booking?.title).toEqual(expectedInitialTitle);
665+
expect(booking?.title).not.toContain("Nameless");
666+
667+
const reassignToHostId = initialHostId === teamUser1.id ? teamUser2.id : teamUser1.id;
668+
const reassignToHostName = reassignToHostId === teamUser1.id ? teamUser1.name : teamUser2.name;
669+
670+
return request(app.getHttpServer())
671+
.post(`/v2/bookings/${bookingUid}/reassign/${reassignToHostId}`)
672+
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
673+
.expect(200)
674+
.then(async (response) => {
675+
const responseBody: ReassignBookingOutput_2024_08_13 = response.body;
676+
expect(responseBody.status).toEqual(SUCCESS_STATUS);
677+
expect(responseBody.data).toBeDefined();
678+
679+
const data: ReassignBookingOutput_2024_08_13["data"] = responseBody.data;
680+
expect(data.bookingUid).toEqual(bookingUid);
681+
expect(data.reassignedTo.id).toEqual(reassignToHostId);
682+
683+
const reassigned = await bookingsRepositoryFixture.getByUid(bookingUid);
684+
expect(reassigned?.userId).toEqual(reassignToHostId);
685+
686+
const expectedReassignedTitle = `${teamRoundRobinWithRescheduleReasonEventTypeTitle} between ${reassignToHostName} and David`;
687+
expect(reassigned?.title).toEqual(expectedReassignedTitle);
688+
expect(reassigned?.title).not.toContain("Nameless");
689+
});
690+
});
691+
692+
it("should preserve attendee name when reassigning round robin host automatically with rescheduleReason required", async () => {
693+
const bookingUid = rescheduleReasonBookingUid;
694+
const booking = await bookingsRepositoryFixture.getByUid(bookingUid);
695+
696+
expect(booking).toBeDefined();
697+
expect(booking?.userId).toBeDefined();
698+
699+
const currentHostId = booking!.userId!;
700+
const initialHostId = rescheduleReasonBookingInitialHostId;
701+
const initialHostName = initialHostId === teamUser1.id ? teamUser1.name : teamUser2.name;
702+
703+
expect(currentHostId).not.toEqual(initialHostId);
704+
705+
return request(app.getHttpServer())
706+
.post(`/v2/bookings/${bookingUid}/reassign`)
707+
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
708+
.expect(200)
709+
.then(async (response) => {
710+
const responseBody: ReassignBookingOutput_2024_08_13 = response.body;
711+
expect(responseBody.status).toEqual(SUCCESS_STATUS);
712+
expect(responseBody.data).toBeDefined();
713+
714+
const data: ReassignBookingOutput_2024_08_13["data"] = responseBody.data;
715+
expect(data.bookingUid).toEqual(bookingUid);
716+
expect(data.reassignedTo.id).toEqual(initialHostId);
717+
718+
const autoReassigned = await bookingsRepositoryFixture.getByUid(bookingUid);
719+
expect(autoReassigned?.userId).toEqual(initialHostId);
720+
721+
const expectedAutoReassignedTitle = `${teamRoundRobinWithRescheduleReasonEventTypeTitle} between ${initialHostName} and David`;
722+
expect(autoReassigned?.title).toEqual(expectedAutoReassignedTitle);
723+
expect(autoReassigned?.title).not.toContain("Nameless");
724+
});
725+
});
726+
561727
async function createOAuthClient(organizationId: number) {
562728
const data = {
563729
logo: "logo-url",

docs/api-reference/v2/openapi.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8483,7 +8483,7 @@
84838483
],
84848484
"requestBody": {
84858485
"required": true,
8486-
"description": "Accepts different types of reschedule booking input: Reschedule Booking (Option 1) or Reschedule Seated Booking (Option 2).\n\n If you are rescheduling a seated booking for an event type with 'show attendees' disabled, then to retrieve attendees in the response either set 'show attendees' to true on event type level or\n you have to provide an authentication method of event type owner, host, team admin or owner or org admin or owner.",
8486+
"description": "Accepts different types of reschedule booking input: Reschedule Booking (Option 1) or Reschedule Seated Booking (Option 2). If you're rescheduling a seated booking as org admin of booking host, pass booking input for Reschedule Booking (Option 1) along with your access token in the request header.\n\n If you are rescheduling a seated booking for an event type with 'show attendees' disabled, then to retrieve attendees in the response either set 'show attendees' to true on event type level or\n you have to provide an authentication method of event type owner, host, team admin or owner or org admin or owner.",
84878487
"content": {
84888488
"application/json": {
84898489
"schema": {
@@ -8518,7 +8518,7 @@
85188518
"post": {
85198519
"operationId": "BookingsController_2024_08_13_cancelBooking",
85208520
"summary": "Cancel a booking",
8521-
"description": ":bookingUid can be :bookingUid of an usual booking, individual recurrence or recurring booking to cancel all recurrences.\n \n \nCancelling normal bookings:\n If the booking is not seated and not recurring, simply pass :bookingUid in the request URL `/bookings/:bookingUid/cancel` and optionally cancellationReason in the request body `{\"cancellationReason\": \"Will travel\"}`.\n\n \nCancelling seated bookings:\n It is possible to cancel specific seat within a booking as an attendee or all of the seats as the host.\n \n1. As an attendee - provide :bookingUid in the request URL `/bookings/:bookingUid/cancel` and seatUid in the request body `{\"seatUid\": \"123-123-123\"}` . This will remove this particular attendance from the booking.\n \n2. As the host - host can cancel booking for all attendees aka for every seat. Provide :bookingUid in the request URL `/bookings/:bookingUid/cancel` and cancellationReason in the request body `{\"cancellationReason\": \"Will travel\"}` and `Authorization: Bearer token` request header where token is event type owner (host) credential. This will cancel the booking for all attendees.\n \n \nCancelling recurring seated bookings:\n For recurring seated bookings it is not possible to cancel all of them with 1 call\n like with non-seated recurring bookings by providing recurring bookind uid - you have to cancel each recurrence booking by its bookingUid + seatUid.\n \n If you are cancelling a seated booking for an event type with 'show attendees' disabled, then to retrieve attendees in the response either set 'show attendees' to true on event type level or\n you have to provide an authentication method of event type owner, host, team admin or owner or org admin or owner.\n\n <Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>\n ",
8521+
"description": ":bookingUid can be :bookingUid of an usual booking, individual recurrence or recurring booking to cancel all recurrences.\n \n \nCancelling normal bookings:\n If the booking is not seated and not recurring, simply pass :bookingUid in the request URL `/bookings/:bookingUid/cancel` and optionally cancellationReason in the request body `{\"cancellationReason\": \"Will travel\"}`.\n\n \nCancelling seated bookings:\n It is possible to cancel specific seat within a booking as an attendee or all of the seats as the host.\n \n1. As an attendee - provide :bookingUid in the request URL `/bookings/:bookingUid/cancel` and seatUid in the request body `{\"seatUid\": \"123-123-123\"}` . This will remove this particular attendance from the booking.\n \n2. As the host or org admin of host - host can cancel booking for all attendees aka for every seat, this also applies to org admins. Provide :bookingUid in the request URL `/bookings/:bookingUid/cancel` and cancellationReason in the request body `{\"cancellationReason\": \"Will travel\"}` and `Authorization: Bearer token` request header where token is event type owner (host) credential. This will cancel the booking for all attendees.\n \n \nCancelling recurring seated bookings:\n For recurring seated bookings it is not possible to cancel all of them with 1 call\n like with non-seated recurring bookings by providing recurring bookind uid - you have to cancel each recurrence booking by its bookingUid + seatUid.\n \n If you are cancelling a seated booking for an event type with 'show attendees' disabled, then to retrieve attendees in the response either set 'show attendees' to true on event type level or\n you have to provide an authentication method of event type owner, host, team admin or owner or org admin or owner.\n\n <Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>\n ",
85228522
"parameters": [
85238523
{
85248524
"name": "cal-api-version",

packages/features/ee/round-robin/roundRobinManualReassignment.ts

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,13 @@ import {
1212
} from "@calcom/emails";
1313
import EventManager from "@calcom/features/bookings/lib/EventManager";
1414
import { getAllCredentialsIncludeServiceAccountKey } from "@calcom/features/bookings/lib/getAllCredentialsForUsersOnEvent/getAllCredentials";
15-
import getBookingResponsesSchema from "@calcom/features/bookings/lib/getBookingResponsesSchema";
15+
import { getBookingResponsesPartialSchema } from "@calcom/features/bookings/lib/getBookingResponsesSchema";
1616
import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses";
1717
import { getEventTypesFromDB } from "@calcom/features/bookings/lib/handleNewBooking/getEventTypesFromDB";
1818
import { getBookerBaseUrl } from "@calcom/features/ee/organizations/lib/getBookerUrlServer";
19-
import AssignmentReasonRecorder, {
20-
RRReassignmentType,
21-
} from "@calcom/features/ee/round-robin/assignmentReason/AssignmentReasonRecorder";
19+
import AssignmentReasonRecorder, { RRReassignmentType } from "@calcom/features/ee/round-robin/assignmentReason/AssignmentReasonRecorder";
2220
import { BookingLocationService } from "@calcom/features/ee/round-robin/lib/bookingLocationService";
23-
import {
24-
scheduleEmailReminder,
25-
deleteScheduledEmailReminder,
26-
} from "@calcom/features/ee/workflows/lib/reminders/emailReminderManager";
21+
import { scheduleEmailReminder, deleteScheduledEmailReminder } from "@calcom/features/ee/workflows/lib/reminders/emailReminderManager";
2722
import { scheduleWorkflowReminders } from "@calcom/features/ee/workflows/lib/reminders/reminderScheduler";
2823
import { getEventName } from "@calcom/features/eventtypes/lib/eventNaming";
2924
import { getVideoCallUrlFromCalEvent } from "@calcom/lib/CalEventParser";
@@ -168,7 +163,7 @@ export const roundRobinManualReassignment = async ({
168163

169164
if (hasOrganizerChanged) {
170165
const bookingResponses = booking.responses;
171-
const responseSchema = getBookingResponsesSchema({
166+
const responseSchema = getBookingResponsesPartialSchema({
172167
bookingFields: eventType.bookingFields,
173168
view: "reschedule",
174169
});
@@ -391,7 +386,7 @@ export const roundRobinManualReassignment = async ({
391386
bookingMetadata: booking.metadata,
392387
});
393388

394-
const { cancellationReason, ...evtWithoutCancellationReason } = evtWithAdditionalInfo;
389+
const { cancellationReason: _cancellationReason, ...evtWithoutCancellationReason } = evtWithAdditionalInfo;
395390

396391
// Send emails
397392
if (emailsEnabled) {

packages/features/ee/round-robin/roundRobinReassignment.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
21
import { cloneDeep } from "lodash";
32

3+
4+
45
import {
56
enrichHostsWithDelegationCredentials,
67
enrichUserWithDelegationCredentialsIncludeServiceAccountKey,
@@ -15,7 +16,7 @@ import {
1516
} from "@calcom/emails";
1617
import EventManager from "@calcom/features/bookings/lib/EventManager";
1718
import { getAllCredentialsIncludeServiceAccountKey } from "@calcom/features/bookings/lib/getAllCredentialsForUsersOnEvent/getAllCredentials";
18-
import getBookingResponsesSchema from "@calcom/features/bookings/lib/getBookingResponsesSchema";
19+
import { getBookingResponsesPartialSchema } from "@calcom/features/bookings/lib/getBookingResponsesSchema";
1920
import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses";
2021
import { ensureAvailableUsers } from "@calcom/features/bookings/lib/handleNewBooking/ensureAvailableUsers";
2122
import { getEventTypesFromDB } from "@calcom/features/bookings/lib/handleNewBooking/getEventTypesFromDB";
@@ -208,7 +209,7 @@ export const roundRobinReassignment = async ({
208209
if (hasOrganizerChanged) {
209210
const bookingResponses = booking.responses;
210211

211-
const responseSchema = getBookingResponsesSchema({
212+
const responseSchema = getBookingResponsesPartialSchema({
212213
bookingFields: eventType.bookingFields,
213214
view: "reschedule",
214215
});
@@ -255,6 +256,7 @@ export const roundRobinReassignment = async ({
255256
userId: reassignedRRHost.id,
256257
userPrimaryEmail: reassignedRRHost.email,
257258
title: newBookingTitle,
259+
reassignById: reassignedById,
258260
idempotencyKey: IdempotencyKeyService.generate({
259261
startTime: booking.startTime,
260262
endTime: booking.endTime,
@@ -341,7 +343,7 @@ export const roundRobinReassignment = async ({
341343
...(platformClientParams ? platformClientParams : {}),
342344
};
343345

344-
if(hasOrganizerChanged){
346+
if (hasOrganizerChanged) {
345347
// location might changed and will be new created in eventManager.create (organizer default location)
346348
evt.videoCallData = undefined;
347349
// To prevent "The requested identifier already exists" error while updating event, we need to remove iCalUID
@@ -416,7 +418,7 @@ export const roundRobinReassignment = async ({
416418
bookingMetadata: booking.metadata,
417419
});
418420

419-
const { cancellationReason, ...evtWithoutCancellationReason } = evtWithAdditionalInfo;
421+
const { cancellationReason: _cancellationReason, ...evtWithoutCancellationReason } = evtWithAdditionalInfo;
420422

421423
// Send to new RR host
422424
if (emailsEnabled) {
@@ -431,6 +433,11 @@ export const roundRobinReassignment = async ({
431433
language: { translate: reassignedRRHostT, locale: reassignedRRHost.locale || "en" },
432434
},
433435
],
436+
reassigned: {
437+
name: reassignedRRHost.name,
438+
email: reassignedRRHost.email,
439+
byUser: originalOrganizer.name || undefined,
440+
},
434441
});
435442
}
436443

@@ -512,4 +519,4 @@ export const roundRobinReassignment = async ({
512519
};
513520
};
514521

515-
export default roundRobinReassignment;
522+
export default roundRobinReassignment;

0 commit comments

Comments
 (0)