fix: preserve attendee names during round robin host reassignment when reassignmentReason is required (#24771)
This commit is contained in:
@@ -58,11 +58,15 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
let teamRoundRobinEventTypeId: number;
|
||||
let teamRoundRobinFixedHostEventTypeId: number;
|
||||
let teamRoundRobinNonFixedEventTypeId: number;
|
||||
let teamRoundRobinWithRescheduleReasonEventTypeId: number;
|
||||
|
||||
let teamRoundRobinNonFixedEventTypeTitle: string;
|
||||
let teamRoundRobinFixedHostEventTypeTitle: string;
|
||||
let teamRoundRobinWithRescheduleReasonEventTypeTitle: string;
|
||||
|
||||
let roundRobinBooking: Booking;
|
||||
let rescheduleReasonBookingUid: string;
|
||||
let rescheduleReasonBookingInitialHostId: number;
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await withApiAuth(
|
||||
@@ -389,6 +393,74 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
},
|
||||
});
|
||||
|
||||
const teamEventTypeWithRescheduleReason = await eventTypesRepositoryFixture.createTeamEventType({
|
||||
schedulingType: "ROUND_ROBIN",
|
||||
team: {
|
||||
connect: { id: team.id },
|
||||
},
|
||||
users: {
|
||||
connect: [{ id: teamUser1.id }, { id: teamUser2.id }],
|
||||
},
|
||||
title: `reassign-bookings-2024-08-13-reschedule-reason-event-type-${randomString()}`,
|
||||
slug: `reassign-bookings-2024-08-13-reschedule-reason-event-type-${randomString()}`,
|
||||
length: 60,
|
||||
assignAllTeamMembers: false,
|
||||
bookingFields: [
|
||||
{
|
||||
name: "rescheduleReason",
|
||||
type: "textarea",
|
||||
defaultLabel: "Reason for rescheduling",
|
||||
required: true,
|
||||
sources: [
|
||||
{
|
||||
id: "default",
|
||||
type: "default",
|
||||
label: "Default",
|
||||
},
|
||||
],
|
||||
editable: "system",
|
||||
views: [
|
||||
{
|
||||
id: "reschedule",
|
||||
label: "Reschedule View",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
locations: [{ type: "inPerson", address: "via 10, rome, italy" }],
|
||||
});
|
||||
|
||||
teamRoundRobinWithRescheduleReasonEventTypeId = teamEventTypeWithRescheduleReason.id;
|
||||
teamRoundRobinWithRescheduleReasonEventTypeTitle = teamEventTypeWithRescheduleReason.title;
|
||||
|
||||
await hostsRepositoryFixture.create({
|
||||
isFixed: false,
|
||||
user: {
|
||||
connect: {
|
||||
id: teamUser1.id,
|
||||
},
|
||||
},
|
||||
eventType: {
|
||||
connect: {
|
||||
id: teamEventTypeWithRescheduleReason.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await hostsRepositoryFixture.create({
|
||||
isFixed: false,
|
||||
user: {
|
||||
connect: {
|
||||
id: teamUser2.id,
|
||||
},
|
||||
},
|
||||
eventType: {
|
||||
connect: {
|
||||
id: teamEventTypeWithRescheduleReason.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
app = moduleRef.createNestApplication();
|
||||
bootstrap(app as NestExpressApplication);
|
||||
|
||||
@@ -558,6 +630,100 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should preserve attendee name when reassigning round robin host manually with rescheduleReason required", async () => {
|
||||
const bookingBody: CreateBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2050, 0, 10, 13, 0, 0)).toISOString(),
|
||||
eventTypeId: teamRoundRobinWithRescheduleReasonEventTypeId,
|
||||
attendee: {
|
||||
name: "David",
|
||||
email: "david@gmail.com",
|
||||
timeZone: "Europe/Rome",
|
||||
language: "en",
|
||||
},
|
||||
meetingUrl: "https://meet.google.com/abc-def-ghi",
|
||||
};
|
||||
|
||||
const createResponse = await request(app.getHttpServer())
|
||||
.post("/v2/bookings")
|
||||
.send(bookingBody)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(201);
|
||||
|
||||
const bookingUid = createResponse.body.data.uid;
|
||||
rescheduleReasonBookingUid = bookingUid;
|
||||
const booking = await bookingsRepositoryFixture.getByUid(bookingUid);
|
||||
|
||||
expect(booking).toBeDefined();
|
||||
expect(booking?.userId).toBeDefined();
|
||||
const initialHostId = booking!.userId!;
|
||||
rescheduleReasonBookingInitialHostId = initialHostId;
|
||||
|
||||
const expectedInitialTitle = `${teamRoundRobinWithRescheduleReasonEventTypeTitle} between ${
|
||||
booking?.userId === teamUser1.id ? teamUser1.name : teamUser2.name
|
||||
} and David`;
|
||||
expect(booking?.title).toEqual(expectedInitialTitle);
|
||||
expect(booking?.title).not.toContain("Nameless");
|
||||
|
||||
const reassignToHostId = initialHostId === teamUser1.id ? teamUser2.id : teamUser1.id;
|
||||
const reassignToHostName = reassignToHostId === teamUser1.id ? teamUser1.name : teamUser2.name;
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${bookingUid}/reassign/${reassignToHostId}`)
|
||||
.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(bookingUid);
|
||||
expect(data.reassignedTo.id).toEqual(reassignToHostId);
|
||||
|
||||
const reassigned = await bookingsRepositoryFixture.getByUid(bookingUid);
|
||||
expect(reassigned?.userId).toEqual(reassignToHostId);
|
||||
|
||||
const expectedReassignedTitle = `${teamRoundRobinWithRescheduleReasonEventTypeTitle} between ${reassignToHostName} and David`;
|
||||
expect(reassigned?.title).toEqual(expectedReassignedTitle);
|
||||
expect(reassigned?.title).not.toContain("Nameless");
|
||||
});
|
||||
});
|
||||
|
||||
it("should preserve attendee name when reassigning round robin host automatically with rescheduleReason required", async () => {
|
||||
const bookingUid = rescheduleReasonBookingUid;
|
||||
const booking = await bookingsRepositoryFixture.getByUid(bookingUid);
|
||||
|
||||
expect(booking).toBeDefined();
|
||||
expect(booking?.userId).toBeDefined();
|
||||
|
||||
const currentHostId = booking!.userId!;
|
||||
const initialHostId = rescheduleReasonBookingInitialHostId;
|
||||
const initialHostName = initialHostId === teamUser1.id ? teamUser1.name : teamUser2.name;
|
||||
|
||||
expect(currentHostId).not.toEqual(initialHostId);
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${bookingUid}/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(bookingUid);
|
||||
expect(data.reassignedTo.id).toEqual(initialHostId);
|
||||
|
||||
const autoReassigned = await bookingsRepositoryFixture.getByUid(bookingUid);
|
||||
expect(autoReassigned?.userId).toEqual(initialHostId);
|
||||
|
||||
const expectedAutoReassignedTitle = `${teamRoundRobinWithRescheduleReasonEventTypeTitle} between ${initialHostName} and David`;
|
||||
expect(autoReassigned?.title).toEqual(expectedAutoReassignedTitle);
|
||||
expect(autoReassigned?.title).not.toContain("Nameless");
|
||||
});
|
||||
});
|
||||
|
||||
async function createOAuthClient(organizationId: number) {
|
||||
const data = {
|
||||
logo: "logo-url",
|
||||
|
||||
@@ -8483,7 +8483,7 @@
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"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.",
|
||||
"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.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
@@ -8518,7 +8518,7 @@
|
||||
"post": {
|
||||
"operationId": "BookingsController_2024_08_13_cancelBooking",
|
||||
"summary": "Cancel a booking",
|
||||
"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 ",
|
||||
"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 ",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "cal-api-version",
|
||||
|
||||
@@ -12,18 +12,13 @@ import {
|
||||
} from "@calcom/emails";
|
||||
import EventManager from "@calcom/features/bookings/lib/EventManager";
|
||||
import { getAllCredentialsIncludeServiceAccountKey } from "@calcom/features/bookings/lib/getAllCredentialsForUsersOnEvent/getAllCredentials";
|
||||
import getBookingResponsesSchema from "@calcom/features/bookings/lib/getBookingResponsesSchema";
|
||||
import { getBookingResponsesPartialSchema } from "@calcom/features/bookings/lib/getBookingResponsesSchema";
|
||||
import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses";
|
||||
import { getEventTypesFromDB } from "@calcom/features/bookings/lib/handleNewBooking/getEventTypesFromDB";
|
||||
import { getBookerBaseUrl } from "@calcom/features/ee/organizations/lib/getBookerUrlServer";
|
||||
import AssignmentReasonRecorder, {
|
||||
RRReassignmentType,
|
||||
} from "@calcom/features/ee/round-robin/assignmentReason/AssignmentReasonRecorder";
|
||||
import AssignmentReasonRecorder, { RRReassignmentType } from "@calcom/features/ee/round-robin/assignmentReason/AssignmentReasonRecorder";
|
||||
import { BookingLocationService } from "@calcom/features/ee/round-robin/lib/bookingLocationService";
|
||||
import {
|
||||
scheduleEmailReminder,
|
||||
deleteScheduledEmailReminder,
|
||||
} from "@calcom/features/ee/workflows/lib/reminders/emailReminderManager";
|
||||
import { scheduleEmailReminder, deleteScheduledEmailReminder } from "@calcom/features/ee/workflows/lib/reminders/emailReminderManager";
|
||||
import { scheduleWorkflowReminders } from "@calcom/features/ee/workflows/lib/reminders/reminderScheduler";
|
||||
import { getEventName } from "@calcom/features/eventtypes/lib/eventNaming";
|
||||
import { getVideoCallUrlFromCalEvent } from "@calcom/lib/CalEventParser";
|
||||
@@ -168,7 +163,7 @@ export const roundRobinManualReassignment = async ({
|
||||
|
||||
if (hasOrganizerChanged) {
|
||||
const bookingResponses = booking.responses;
|
||||
const responseSchema = getBookingResponsesSchema({
|
||||
const responseSchema = getBookingResponsesPartialSchema({
|
||||
bookingFields: eventType.bookingFields,
|
||||
view: "reschedule",
|
||||
});
|
||||
@@ -391,7 +386,7 @@ export const roundRobinManualReassignment = async ({
|
||||
bookingMetadata: booking.metadata,
|
||||
});
|
||||
|
||||
const { cancellationReason, ...evtWithoutCancellationReason } = evtWithAdditionalInfo;
|
||||
const { cancellationReason: _cancellationReason, ...evtWithoutCancellationReason } = evtWithAdditionalInfo;
|
||||
|
||||
// Send emails
|
||||
if (emailsEnabled) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
|
||||
import { cloneDeep } from "lodash";
|
||||
|
||||
|
||||
|
||||
import {
|
||||
enrichHostsWithDelegationCredentials,
|
||||
enrichUserWithDelegationCredentialsIncludeServiceAccountKey,
|
||||
@@ -15,7 +16,7 @@ import {
|
||||
} from "@calcom/emails";
|
||||
import EventManager from "@calcom/features/bookings/lib/EventManager";
|
||||
import { getAllCredentialsIncludeServiceAccountKey } from "@calcom/features/bookings/lib/getAllCredentialsForUsersOnEvent/getAllCredentials";
|
||||
import getBookingResponsesSchema from "@calcom/features/bookings/lib/getBookingResponsesSchema";
|
||||
import { getBookingResponsesPartialSchema } from "@calcom/features/bookings/lib/getBookingResponsesSchema";
|
||||
import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses";
|
||||
import { ensureAvailableUsers } from "@calcom/features/bookings/lib/handleNewBooking/ensureAvailableUsers";
|
||||
import { getEventTypesFromDB } from "@calcom/features/bookings/lib/handleNewBooking/getEventTypesFromDB";
|
||||
@@ -208,7 +209,7 @@ export const roundRobinReassignment = async ({
|
||||
if (hasOrganizerChanged) {
|
||||
const bookingResponses = booking.responses;
|
||||
|
||||
const responseSchema = getBookingResponsesSchema({
|
||||
const responseSchema = getBookingResponsesPartialSchema({
|
||||
bookingFields: eventType.bookingFields,
|
||||
view: "reschedule",
|
||||
});
|
||||
@@ -255,6 +256,7 @@ export const roundRobinReassignment = async ({
|
||||
userId: reassignedRRHost.id,
|
||||
userPrimaryEmail: reassignedRRHost.email,
|
||||
title: newBookingTitle,
|
||||
reassignById: reassignedById,
|
||||
idempotencyKey: IdempotencyKeyService.generate({
|
||||
startTime: booking.startTime,
|
||||
endTime: booking.endTime,
|
||||
@@ -341,7 +343,7 @@ export const roundRobinReassignment = async ({
|
||||
...(platformClientParams ? platformClientParams : {}),
|
||||
};
|
||||
|
||||
if(hasOrganizerChanged){
|
||||
if (hasOrganizerChanged) {
|
||||
// location might changed and will be new created in eventManager.create (organizer default location)
|
||||
evt.videoCallData = undefined;
|
||||
// To prevent "The requested identifier already exists" error while updating event, we need to remove iCalUID
|
||||
@@ -416,7 +418,7 @@ export const roundRobinReassignment = async ({
|
||||
bookingMetadata: booking.metadata,
|
||||
});
|
||||
|
||||
const { cancellationReason, ...evtWithoutCancellationReason } = evtWithAdditionalInfo;
|
||||
const { cancellationReason: _cancellationReason, ...evtWithoutCancellationReason } = evtWithAdditionalInfo;
|
||||
|
||||
// Send to new RR host
|
||||
if (emailsEnabled) {
|
||||
@@ -431,6 +433,11 @@ export const roundRobinReassignment = async ({
|
||||
language: { translate: reassignedRRHostT, locale: reassignedRRHost.locale || "en" },
|
||||
},
|
||||
],
|
||||
reassigned: {
|
||||
name: reassignedRRHost.name,
|
||||
email: reassignedRRHost.email,
|
||||
byUser: originalOrganizer.name || undefined,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -512,4 +519,4 @@ export const roundRobinReassignment = async ({
|
||||
};
|
||||
};
|
||||
|
||||
export default roundRobinReassignment;
|
||||
export default roundRobinReassignment;
|
||||
Reference in New Issue
Block a user