Files
Udit TakkarGitHubDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
f2a44279fe refactor: evt calendar event builder (#27203)
* refactor: evt creation

* refactor: evt creation

* refactor: evt creation

* fix: type

* test: update unit tests

* chore: add comments

* fix: handle undefined in withConditional to match NonNullable<T> type

Co-Authored-By: unknown <>

* refactor: evt

* fix: type err

* refactor: remove add videoCallData

* refactor: use error with code

* test: update unit test

* refactor: feedback

* refactor: remove conditional

* refactor: more strict types

* fix: type err

* chor: remove un used

* refactor:  improve recurring event handling in RegularBookingService

Removed unnecessary whitespace and clarified comments regarding the attachment of recurring configurations in the booking handler. Adjusted logic to ensure that recurring settings are only applied when relevant.

* refactor: test

* fix: type error

* refactor: improve code

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-03-19 03:50:48 +09:00

166 lines
5.2 KiB
TypeScript

import { cloneDeep } from "lodash";
import { uuid } from "short-uuid";
import { sendRescheduledEmailsAndSMS } from "@calcom/emails/email-manager";
import type EventManager from "@calcom/features/bookings/lib/EventManager";
import { ErrorCode } from "@calcom/lib/errorCodes";
import { HttpError } from "@calcom/lib/http-error";
import prisma from "@calcom/prisma";
import { BookingStatus } from "@calcom/prisma/enums";
import { CalendarEventBuilder } from "@calcom/features/CalendarEventBuilder";
import { findBookingQuery } from "../../../handleNewBooking/findBookingQuery";
import type { createLoggerWithEventDetails } from "../../../handleNewBooking/logger";
import type { SeatedBooking, RescheduleSeatedBookingObject, NewTimeSlotBooking } from "../../types";
const combineTwoSeatedBookings = async (
rescheduleSeatedBookingObject: RescheduleSeatedBookingObject,
seatedBooking: SeatedBooking,
newTimeSlotBooking: NewTimeSlotBooking,
eventManager: EventManager,
loggerWithEventDetails: ReturnType<typeof createLoggerWithEventDetails>
) => {
const {
eventType,
tAttendees,
attendeeLanguage,
rescheduleUid,
noEmail,
isConfirmedByDefault,
additionalNotes,
rescheduleReason,
} = rescheduleSeatedBookingObject;
let { evt } = rescheduleSeatedBookingObject;
// Merge two bookings together
const attendeesToMove = [],
attendeesToDelete = [];
for (const attendee of seatedBooking.attendees) {
// If the attendee already exists on the new booking then delete the attendee record of the old booking
if (
newTimeSlotBooking.attendees.some((newBookingAttendee) => newBookingAttendee.email === attendee.email)
) {
attendeesToDelete.push(attendee.id);
// If the attendee does not exist on the new booking then move that attendee record to the new booking
} else {
attendeesToMove.push({ id: attendee.id, seatReferenceId: attendee.bookingSeat?.id });
}
}
// Confirm that the new event will have enough available seats
if (
!eventType.seatsPerTimeSlot ||
attendeesToMove.length + newTimeSlotBooking.attendees.filter((attendee) => attendee.bookingSeat).length >
eventType.seatsPerTimeSlot
) {
throw new HttpError({ statusCode: 409, message: ErrorCode.NotEnoughAvailableSeats });
}
const moveAttendeeCalls = [];
for (const attendeeToMove of attendeesToMove) {
moveAttendeeCalls.push(
prisma.attendee.update({
where: {
id: attendeeToMove.id,
},
data: {
bookingId: newTimeSlotBooking.id,
bookingSeat: {
upsert: {
create: {
referenceUid: uuid(),
bookingId: newTimeSlotBooking.id,
},
update: {
bookingId: newTimeSlotBooking.id,
},
},
},
},
})
);
}
await prisma.$transaction([
...moveAttendeeCalls,
// Delete any attendees that are already a part of that new time slot booking
prisma.attendee.deleteMany({
where: {
id: {
in: attendeesToDelete,
},
},
}),
]);
const updatedNewBooking = await prisma.booking.findUnique({
where: {
id: newTimeSlotBooking.id,
},
include: {
attendees: true,
references: true,
},
});
if (!updatedNewBooking) {
throw new HttpError({ statusCode: 404, message: "Updated booking not found" });
}
// Update the evt object with the new attendees
const updatedBookingAttendees = updatedNewBooking.attendees.map((attendee) => {
const evtAttendee = {
...attendee,
language: { translate: tAttendees, locale: attendeeLanguage ?? "en" },
};
return evtAttendee;
});
evt.attendees = updatedBookingAttendees;
evt = CalendarEventBuilder.fromEvent(evt)
.withVideoCallDataFromReferences(updatedNewBooking.references)
.build();
const copyEvent = cloneDeep(evt);
const updateManager = await eventManager.reschedule(copyEvent, rescheduleUid, newTimeSlotBooking.id);
const results = updateManager.results;
const calendarResult = results.find((result) => result.type.includes("_calendar"));
evt.iCalUID = Array.isArray(calendarResult?.updatedEvent)
? calendarResult?.updatedEvent[0]?.iCalUID
: calendarResult?.updatedEvent?.iCalUID || undefined;
if (noEmail !== true && isConfirmedByDefault) {
// TODO send reschedule emails to attendees of the old booking
loggerWithEventDetails.debug("Emails: Sending reschedule emails - handleSeats");
await sendRescheduledEmailsAndSMS(
{
...copyEvent,
additionalNotes, // Resets back to the additionalNote input and not the override value
cancellationReason: `$RCH$${rescheduleReason ? rescheduleReason : ""}`, // Removable code prefix to differentiate cancellation from rescheduling for email
},
eventType.metadata
);
}
// Update the old booking with the cancelled status
await prisma.booking.update({
where: {
id: seatedBooking.id,
},
data: {
status: BookingStatus.CANCELLED,
},
});
const foundBooking = await findBookingQuery(newTimeSlotBooking.id);
return { ...foundBooking };
};
export default combineTwoSeatedBookings;