diff --git a/packages/core/EventManager.ts b/packages/core/EventManager.ts index ab1da403fa..1389cd7b8f 100644 --- a/packages/core/EventManager.ts +++ b/packages/core/EventManager.ts @@ -96,6 +96,11 @@ export type EventManagerUser = { type createdEventSchema = z.infer; +export type EventManagerInitParams = { + user: EventManagerUser; + eventTypeAppMetadata?: z.infer; +}; + export default class EventManager { calendarCredentials: CredentialPayload[]; videoCredentials: CredentialPayload[]; diff --git a/packages/features/ee/round-robin/handleRescheduleEventManager.ts b/packages/features/ee/round-robin/handleRescheduleEventManager.ts new file mode 100644 index 0000000000..afda6e3f9d --- /dev/null +++ b/packages/features/ee/round-robin/handleRescheduleEventManager.ts @@ -0,0 +1,174 @@ +import type { DestinationCalendar } from "@prisma/client"; +import type { Prisma } from "@prisma/client"; + +import { metadata as GoogleMeetMetadata } from "@calcom/app-store/googlevideo/_metadata"; +import { MeetLocationType } from "@calcom/app-store/locations"; +import EventManager from "@calcom/core/EventManager"; +import type { EventManagerInitParams } from "@calcom/core/EventManager"; +import { getVideoCallDetails } from "@calcom/features/bookings/lib/handleNewBooking/getVideoCallDetails"; +import { getVideoCallUrlFromCalEvent } from "@calcom/lib/CalEventParser"; +import logger from "@calcom/lib/logger"; +import { getTranslation } from "@calcom/lib/server/i18n"; +import { BookingReferenceRepository } from "@calcom/lib/server/repository/bookingReference"; +import { prisma } from "@calcom/prisma"; +import type { CalendarEvent, AdditionalInformation } from "@calcom/types/Calendar"; + +export const handleRescheduleEventManager = async ({ + evt, + rescheduleUid, + newBookingId, + changedOrganizer, + previousHostDestinationCalendar, + initParams, + bookingLocation, + bookingId, + bookingICalUID, + bookingMetadata, +}: { + evt: CalendarEvent; + rescheduleUid: string; + newBookingId?: number; + changedOrganizer?: boolean; + previousHostDestinationCalendar?: DestinationCalendar[] | null; + initParams: EventManagerInitParams; + bookingLocation: string | null; + bookingId: number; + bookingICalUID?: string | null; + bookingMetadata?: Prisma.JsonValue; +}) => { + const handleRescheduleEventManager = logger.getSubLogger({ + prefix: ["handleRescheduleEventManager", `${bookingId}`], + }); + + const eventManager = new EventManager(initParams.user, initParams?.eventTypeAppMetadata); + + const updateManager = await eventManager.reschedule( + evt, + rescheduleUid, + newBookingId, + changedOrganizer, + previousHostDestinationCalendar + ); + + const results = updateManager.results ?? []; + + const { metadata: videoMetadata, videoCallUrl: _videoCallUrl } = getVideoCallDetails({ + results: results, + }); + + let videoCallUrl = _videoCallUrl; + let metadata: AdditionalInformation = {}; + metadata = videoMetadata; + + if (results.length) { + // Handle Google Meet results + if (bookingLocation === MeetLocationType) { + const googleMeetResult = { + appName: GoogleMeetMetadata.name, + type: "conferencing", + uid: results[0].uid, + originalEvent: results[0].originalEvent, + }; + + // Find index of google_calendar inside createManager.referencesToCreate + const googleCalIndex = updateManager.referencesToCreate.findIndex( + (ref) => ref.type === "google_calendar" + ); + const googleCalResult = results[googleCalIndex]; + + const t = await getTranslation("en", "common"); + + if (!googleCalResult) { + handleRescheduleEventManager.warn("Google Calendar not installed but using Google Meet as location"); + results.push({ + ...googleMeetResult, + success: false, + calWarnings: [t("google_meet_warning")], + }); + } + + const googleHangoutLink = Array.isArray(googleCalResult?.updatedEvent) + ? googleCalResult.updatedEvent[0]?.hangoutLink + : googleCalResult?.updatedEvent?.hangoutLink ?? googleCalResult?.createdEvent?.hangoutLink; + + if (googleHangoutLink) { + results.push({ + ...googleMeetResult, + success: true, + }); + + // Add google_meet to referencesToCreate in the same index as google_calendar + updateManager.referencesToCreate[googleCalIndex] = { + ...updateManager.referencesToCreate[googleCalIndex], + meetingUrl: googleHangoutLink, + }; + + // Also create a new referenceToCreate with type video for google_meet + updateManager.referencesToCreate.push({ + type: "google_meet_video", + meetingUrl: googleHangoutLink, + uid: googleCalResult.uid, + credentialId: updateManager.referencesToCreate[googleCalIndex].credentialId, + }); + } else if (googleCalResult && !googleHangoutLink) { + results.push({ + ...googleMeetResult, + success: false, + }); + } + } + const createdOrUpdatedEvent = Array.isArray(results[0]?.updatedEvent) + ? results[0]?.updatedEvent[0] + : results[0]?.updatedEvent ?? results[0]?.createdEvent; + metadata.hangoutLink = createdOrUpdatedEvent?.hangoutLink; + metadata.conferenceData = createdOrUpdatedEvent?.conferenceData; + metadata.entryPoints = createdOrUpdatedEvent?.entryPoints; + + videoCallUrl = + metadata.hangoutLink || createdOrUpdatedEvent?.url || getVideoCallUrlFromCalEvent(evt) || videoCallUrl; + + const calendarResult = results.find((result) => result.type.includes("_calendar")); + + evt.iCalUID = Array.isArray(calendarResult?.updatedEvent) + ? calendarResult?.updatedEvent[0]?.iCalUID + : calendarResult?.updatedEvent?.iCalUID || undefined; + } + const newReferencesToCreate = structuredClone(updateManager.referencesToCreate); + + await BookingReferenceRepository.replaceBookingReferences({ + bookingId, + newReferencesToCreate, + }); + + try { + if (bookingLocation?.startsWith("http")) { + videoCallUrl = bookingLocation; + } + + const newBookingMetaData = videoCallUrl + ? { + videoCallUrl: getVideoCallUrlFromCalEvent(evt) || videoCallUrl, + } + : undefined; + + await prisma.booking.update({ + where: { + id: bookingId, + }, + data: { + location: bookingLocation, + iCalUID: evt.iCalUID !== bookingICalUID ? evt.iCalUID : bookingICalUID, + metadata: { ...(typeof bookingMetadata === "object" && bookingMetadata), ...newBookingMetaData }, + }, + }); + } catch (error) { + handleRescheduleEventManager.error("Error while updating booking metadata", JSON.stringify({ error })); + } + + const evtWithAdditionalInfo = { + ...evt, + additionalInformation: metadata, + }; + + return { evtWithAdditionalInfo }; +}; diff --git a/packages/features/ee/round-robin/roundRobinManualReassignment.ts b/packages/features/ee/round-robin/roundRobinManualReassignment.ts index fada127c42..e46f957d2a 100644 --- a/packages/features/ee/round-robin/roundRobinManualReassignment.ts +++ b/packages/features/ee/round-robin/roundRobinManualReassignment.ts @@ -2,7 +2,6 @@ import { cloneDeep } from "lodash"; import { OrganizerDefaultConferencingAppType, getLocationValueForDB } from "@calcom/app-store/locations"; -import EventManager from "@calcom/core/EventManager"; import { getEventName } from "@calcom/core/event"; import dayjs from "@calcom/dayjs"; import { sendRoundRobinCancelledEmailsAndSMS, sendRoundRobinScheduledEmailsAndSMS } from "@calcom/emails"; @@ -20,7 +19,6 @@ import { SENDER_NAME } from "@calcom/lib/constants"; import { getBookerBaseUrl } from "@calcom/lib/getBookerUrl/server"; import logger from "@calcom/lib/logger"; import { getTranslation } from "@calcom/lib/server/i18n"; -import { BookingReferenceRepository } from "@calcom/lib/server/repository/bookingReference"; import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat"; import { prisma } from "@calcom/prisma"; import { WorkflowActions, WorkflowMethods, WorkflowTriggerEvents } from "@calcom/prisma/enums"; @@ -28,6 +26,7 @@ import { userMetadata as userMetadataSchema } from "@calcom/prisma/zod-utils"; import type { EventTypeMetadata } from "@calcom/prisma/zod-utils"; import type { CalendarEvent } from "@calcom/types/Calendar"; +import { handleRescheduleEventManager } from "./handleRescheduleEventManager"; import type { BookingSelectResult } from "./utils/bookingSelect"; import { bookingSelect } from "./utils/bookingSelect"; import { getDestinationCalendar } from "./utils/getDestinationCalendar"; @@ -124,6 +123,7 @@ export const roundRobinManualReassignment = async ({ })(); const previousRRHostT = await getTranslation(previousRRHost?.locale || "en", "common"); + let bookingLocation = booking.location; if (hasOrganizerChanged) { const bookingResponses = booking.responses; @@ -134,8 +134,7 @@ export const roundRobinManualReassignment = async ({ const responseSafeParse = await responseSchema.safeParseAsync(bookingResponses); const responses = responseSafeParse.success ? responseSafeParse.data : undefined; - let bookingLocation = booking.location; - if (eventType.locations.includes({ type: OrganizerDefaultConferencingAppType })) { + if (eventType.locations.some((location) => location.type === OrganizerDefaultConferencingAppType)) { const newUserMetadataSafeParse = userMetadataSchema.safeParse(newUser.metadata); const defaultLocationUrl = newUserMetadataSafeParse.success ? newUserMetadataSafeParse?.data?.defaultConferencingApp?.appLink @@ -248,6 +247,7 @@ export const roundRobinManualReassignment = async ({ bookingFields: eventType.bookingFields ?? null, booking, }), + location: bookingLocation, }; const credentials = await prisma.credential.findMany({ @@ -255,29 +255,28 @@ export const roundRobinManualReassignment = async ({ include: { user: { select: { email: true } } }, }); - const eventManager = new EventManager({ ...newUser, credentials }); const previousHostDestinationCalendar = hasOrganizerChanged ? await prisma.destinationCalendar.findFirst({ where: { userId: originalOrganizer.id }, }) : null; - const results = await eventManager.reschedule( + const { evtWithAdditionalInfo } = await handleRescheduleEventManager({ evt, - booking.uid, - undefined, - hasOrganizerChanged, - previousHostDestinationCalendar ? [previousHostDestinationCalendar] : [] - ); - - const newReferencesToCreate = structuredClone(results.referencesToCreate); - - await BookingReferenceRepository.replaceBookingReferences({ + rescheduleUid: booking.uid, + newBookingId: undefined, + changedOrganizer: hasOrganizerChanged, + previousHostDestinationCalendar: previousHostDestinationCalendar ? [previousHostDestinationCalendar] : [], + initParams: { + user: { ...newUser, credentials }, + }, bookingId, - newReferencesToCreate, + bookingLocation, + bookingICalUID: booking.iCalUID, + bookingMetadata: booking.metadata, }); - const { cancellationReason, ...evtWithoutCancellationReason } = evt; + const { cancellationReason, ...evtWithoutCancellationReason } = evtWithAdditionalInfo; // Send emails await sendRoundRobinScheduledEmailsAndSMS({ @@ -300,7 +299,7 @@ export const roundRobinManualReassignment = async ({ }); // Send cancellation email to previous RR host - const cancelledEvt = cloneDeep(evt); + const cancelledEvt = cloneDeep(evtWithAdditionalInfo); cancelledEvt.organizer = { email: originalOrganizer.email, name: originalOrganizer.name || "", @@ -330,7 +329,7 @@ export const roundRobinManualReassignment = async ({ await handleWorkflowsUpdate({ booking, newUser, - evt, + evt: evtWithAdditionalInfo, eventType, orgId, }); diff --git a/packages/features/ee/round-robin/roundRobinReassignment.ts b/packages/features/ee/round-robin/roundRobinReassignment.ts index eaf0367224..a6f7153ec2 100644 --- a/packages/features/ee/round-robin/roundRobinReassignment.ts +++ b/packages/features/ee/round-robin/roundRobinReassignment.ts @@ -2,7 +2,6 @@ import { cloneDeep } from "lodash"; import { OrganizerDefaultConferencingAppType, getLocationValueForDB } from "@calcom/app-store/locations"; -import EventManager from "@calcom/core/EventManager"; import { getEventName } from "@calcom/core/event"; import dayjs from "@calcom/dayjs"; import { sendRoundRobinCancelledEmailsAndSMS, sendRoundRobinScheduledEmailsAndSMS } from "@calcom/emails"; @@ -23,7 +22,6 @@ import { getBookerBaseUrl } from "@calcom/lib/getBookerUrl/server"; import logger from "@calcom/lib/logger"; import { getLuckyUser } from "@calcom/lib/server"; import { getTranslation } from "@calcom/lib/server/i18n"; -import { BookingReferenceRepository } from "@calcom/lib/server/repository/bookingReference"; import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat"; import { prisma } from "@calcom/prisma"; import { WorkflowActions, WorkflowMethods, WorkflowTriggerEvents } from "@calcom/prisma/enums"; @@ -31,6 +29,7 @@ import { userMetadata as userMetadataSchema } from "@calcom/prisma/zod-utils"; import type { EventTypeMetadata } from "@calcom/prisma/zod-utils"; import type { CalendarEvent } from "@calcom/types/Calendar"; +import { handleRescheduleEventManager } from "./handleRescheduleEventManager"; import { bookingSelect } from "./utils/bookingSelect"; import { getDestinationCalendar } from "./utils/getDestinationCalendar"; import { getTeamMembers } from "./utils/getTeamMembers"; @@ -174,6 +173,7 @@ export const roundRobinReassignment = async ({ } const attendeeList = await Promise.all(attendeePromises); + let bookingLocation = booking.location; if (hasOrganizerChanged) { const bookingResponses = booking.responses; @@ -187,9 +187,7 @@ export const roundRobinReassignment = async ({ const responses = responseSafeParse.success ? responseSafeParse.data : undefined; - let bookingLocation = booking.location; - - if (eventType.locations.includes({ type: OrganizerDefaultConferencingAppType })) { + if (eventType.locations.some((location) => location.type === OrganizerDefaultConferencingAppType)) { const organizerMetadataSafeParse = userMetadataSchema.safeParse(reassignedRRHost.metadata); const defaultLocationUrl = organizerMetadataSafeParse.success @@ -291,6 +289,7 @@ export const roundRobinReassignment = async ({ bookingFields: eventType?.bookingFields ?? null, booking, }), + location: bookingLocation, }; const credentials = await prisma.credential.findMany({ @@ -305,27 +304,27 @@ export const roundRobinReassignment = async ({ }, }, }); - const eventManager = new EventManager({ ...organizer, credentials: [...credentials] }); - const results = await eventManager.reschedule( + const { evtWithAdditionalInfo } = await handleRescheduleEventManager({ evt, - booking.uid, - undefined, - hasOrganizerChanged, - previousHostDestinationCalendar ? [previousHostDestinationCalendar] : [] - ); - - let newReferencesToCreate = []; - newReferencesToCreate = structuredClone(results.referencesToCreate); - - await BookingReferenceRepository.replaceBookingReferences({ + rescheduleUid: booking.uid, + newBookingId: undefined, + changedOrganizer: hasOrganizerChanged, + previousHostDestinationCalendar: previousHostDestinationCalendar ? [previousHostDestinationCalendar] : [], + initParams: { + user: { ...organizer, credentials: [...credentials] }, + }, bookingId, - newReferencesToCreate, + bookingLocation, + bookingICalUID: booking.iCalUID, + bookingMetadata: booking.metadata, }); + const { cancellationReason, ...evtWithoutCancellationReason } = evtWithAdditionalInfo; + // Send to new RR host await sendRoundRobinScheduledEmailsAndSMS({ - calEvent: evt, + calEvent: evtWithoutCancellationReason, members: [ { ...reassignedRRHost, @@ -340,7 +339,7 @@ export const roundRobinReassignment = async ({ if (previousRRHost) { // Send to cancelled RR host // First we need to replace the new RR host with the old RR host in the evt object - const cancelledRRHostEvt = cloneDeep(evt); + const cancelledRRHostEvt = cloneDeep(evtWithAdditionalInfo); cancelledRRHostEvt.title = currentBookingTitle; if (hasOrganizerChanged) { cancelledRRHostEvt.organizer = { @@ -427,7 +426,7 @@ export const roundRobinReassignment = async ({ }, }); - const workflowEventMetadata = { videoCallUrl: getVideoCallUrlFromCalEvent(evt) }; + const workflowEventMetadata = { videoCallUrl: getVideoCallUrlFromCalEvent(evtWithAdditionalInfo) }; const bookerUrl = await getBookerBaseUrl(orgId); @@ -438,7 +437,7 @@ export const roundRobinReassignment = async ({ if (workflowStep && workflow) { await scheduleEmailReminder({ evt: { - ...evt, + ...evtWithAdditionalInfo, metadata: workflowEventMetadata, eventType, bookerUrl, @@ -512,7 +511,7 @@ export const roundRobinReassignment = async ({ workflows: newEventWorkflows, smsReminderNumber: null, calendarEvent: { - ...evt, + ...evtWithAdditionalInfo, metadata: workflowEventMetadata, eventType: { slug: eventType.slug }, bookerUrl, diff --git a/packages/features/ee/round-robin/utils/bookingSelect.ts b/packages/features/ee/round-robin/utils/bookingSelect.ts index 29ec47a538..536d6727a5 100644 --- a/packages/features/ee/round-robin/utils/bookingSelect.ts +++ b/packages/features/ee/round-robin/utils/bookingSelect.ts @@ -19,6 +19,8 @@ export const bookingSelect = { }, attendees: true, references: true, + metadata: true, + iCalUID: true, }; export type BookingSelectResult = Prisma.BookingGetPayload<{