hunk.code_window:         <td>${this.calEvent.attendees[0].name}<br /><small><a href="mailto:${
           this.calEvent.attendees[0].email
         }">${this.calEvent.attendees[0].email}</a></small></td>
         </tr>
         <tr>
           <td>${this.calEvent.language("where")}</td>
           <td>${this.getLocation()}</td>
         </tr>
         <tr>
           <td>${this.calEvent.language("notes")}</td>
           <td>${this.calEvent.description}</td>
         </tr>
  
hunk.labels: keep keep keep keep replace replace replace replace keep keep keep keep
hunk.after_edit:       ${this.getLocation()}
  
hunk.file_path: lib/emails/EventOrganizerMail.ts
hunk.type: replace
file: import { Credential } from "@prisma/client";
  import async from "async";
  import merge from "lodash/merge";
  import { v5 as uuidv5 } from "uuid";
  
  import { AdditionInformation, CalendarEvent, createEvent, updateEvent } from "@lib/calendarClient";
  import EventAttendeeMail from "@lib/emails/EventAttendeeMail";
  import EventAttendeeRescheduledMail from "@lib/emails/EventAttendeeRescheduledMail";
  import { DailyEventResult, FAKE_DAILY_CREDENTIAL } from "@lib/integrations/Daily/DailyVideoApiAdapter";
  import { ZoomEventResult } from "@lib/integrations/Zoom/ZoomVideoApiAdapter";
  import { LocationType } from "@lib/location";
  import prisma from "@lib/prisma";
  import { Ensure } from "@lib/types/utils";
  import { createMeeting, updateMeeting, VideoCallData } from "@lib/videoClient";
  
  export type Event = AdditionInformation & { name: string; id: string; disableConfirmationEmail?: boolean } & (
      | ZoomEventResult
      | DailyEventResult
    );
  
  export interface EventResult {
    type: string;
    success: boolean;
    uid: string;
    createdEvent?: Event;
    updatedEvent?: Event;
    originalEvent: CalendarEvent;
    videoCallData?: VideoCallData;
  }
  
  export interface CreateUpdateResult {
    results: Array<EventResult>;
    referencesToCreate: Array<PartialReference>;
  }
  
  export interface PartialBooking {
    id: number;
    references: Array<PartialReference>;
  }
  
  export interface PartialReference {
    id?: number;
    type: string;
    uid: string;
    meetingId?: string | null;
    meetingPassword?: string | null;
    meetingUrl?: string | null;
  }
  
  interface GetLocationRequestFromIntegrationRequest {
    location: string;
  }
  
  export default class EventManager {
    calendarCredentials: Array<Credential>;
    videoCredentials: Array<Credential>;
  
    /**
     * Takes an array of credentials and initializes a new instance of the EventManager.
     *
     * @param credentials
     */
    constructor(credentials: Array<Credential>) {
      this.calendarCredentials = credentials.filter((cred) => cred.type.endsWith("_calendar"));
      this.videoCredentials = credentials.filter((cred) => cred.type.endsWith("_video"));
  
      //for  Daily.co video, temporarily pushes a credential for the daily-video-client
      const hasDailyIntegration = process.env.DAILY_API_KEY;
      if (hasDailyIntegration) {
        this.videoCredentials.push(FAKE_DAILY_CREDENTIAL);
      }
    }
  
    /**
     * Takes a CalendarEvent and creates all necessary integration entries for it.
     * When a video integration is chosen as the event's location, a video integration
     * event will be scheduled for it as well.
     *
     * @param event
     */
    public async create(event: Ensure<CalendarEvent, "language">): Promise<CreateUpdateResult> {
      const evt = EventManager.processLocation(event);
      const isDedicated = evt.location ? EventManager.isDedicatedIntegration(evt.location) : null;
  
      // First, create all calendar events. If this is a dedicated integration event, don't send a mail right here.
      const results: Array<EventResult> = await this.createAllCalendarEvents(evt, isDedicated);
      // If and only if event type is a dedicated meeting, create a dedicated video meeting.
      if (isDedicated) {
        const result = await this.createVideoEvent(evt);
        if (result.videoCallData) {
          evt.videoCallData = result.videoCallData;
        }
        results.push(result);
      } else {
        await EventManager.sendAttendeeMail("new", results, evt);
      }
  
      const referencesToCreate: Array<PartialReference> = results.map((result: EventResult) => {
        let uid = "";
        if (result.createdEvent) {
          const isDailyResult = result.type === "daily_video";
          if (isDailyResult) {
            uid = (result.createdEvent as DailyEventResult).name.toString();
          } else {
            uid = (result.createdEvent as ZoomEventResult).id.toString();
          }
        }
        return {
          type: result.type,
          uid,
          meetingId: result.videoCallData?.id.toString(),
          meetingPassword: result.videoCallData?.password,
          meetingUrl: result.videoCallData?.url,
        };
      });
  
      return {
        results,
        referencesToCreate,
      };
    }
  
    /**
     * Takes a calendarEvent and a rescheduleUid and updates the event that has the
     * given uid using the data delivered in the given CalendarEvent.
     *
     * @param event
     */
    public async update(
      event: Ensure<CalendarEvent, "language">,
      rescheduleUid: string
    ): Promise<CreateUpdateResult> {
      const evt = EventManager.processLocation(event);
  
      if (!rescheduleUid) {
        throw new Error("You called eventManager.update without an `rescheduleUid`. This should never happen.");
      }
  
      // Get details of existing booking.
      const booking = await prisma.booking.findFirst({
        where: {
          uid: rescheduleUid,
        },
        select: {
          id: true,
          references: {
            select: {
              id: true,
              type: true,
              uid: true,
              meetingId: true,
              meetingPassword: true,
              meetingUrl: true,
            },
          },
        },
      });
  
      if (!booking) {
        throw new Error("booking not found");
      }
  
      const isDedicated = evt.location ? EventManager.isDedicatedIntegration(evt.location) : null;
      // First, create all calendar events. If this is a dedicated integration event, don't send a mail right here.
      const results: Array<EventResult> = await this.updateAllCalendarEvents(evt, booking, isDedicated);
      // If and only if event type is a dedicated meeting, update the dedicated video meeting.
      if (isDedicated) {
        const result = await this.updateVideoEvent(evt, booking);
        if (result.videoCallData) {
          evt.videoCallData = result.videoCallData;
        }
        results.push(result);
      } else {
        await EventManager.sendAttendeeMail("reschedule", results, evt);
      }
      // Now we can delete the old booking and its references.
      const bookingReferenceDeletes = prisma.bookingReference.deleteMany({
        where: {
          bookingId: booking.id,
        },
      });
      const attendeeDeletes = prisma.attendee.deleteMany({
        where: {
          bookingId: booking.id,
        },
      });
  
      const bookingDeletes = prisma.booking.delete({
        where: {
          id: booking.id,
        },
      });
  
      // Wait for all deletions to be applied.
      await Promise.all([bookingReferenceDeletes, attendeeDeletes, bookingDeletes]);
  
      return {
        results,
        referencesToCreate: [...booking.references],
      };
    }
  
    /**
     * Creates event entries for all calendar integrations given in the credentials.
     * When noMail is true, no mails will be sent. This is used when the event is
     * a video meeting because then the mail containing the video credentials will be
     * more important than the mails created for these bare calendar events.
     *
     * When the optional uid is set, it will be used instead of the auto generated uid.
     *
     * @param event
     * @param noMail
     * @private
     */
  
    private async createAllCalendarEvents(
      event: CalendarEvent,
      noMail: boolean | null
    ): Promise<Array<EventResult>> {
      const [firstCalendar] = this.calendarCredentials;
      if (!firstCalendar) {
        return [];
      }
      return [await createEvent(firstCalendar, event, noMail)];
    }
  
    /**
     * Checks which video integration is needed for the event's location and returns
     * credentials for that - if existing.
     * @param event
     * @private
     */
  
    private getVideoCredential(event: CalendarEvent): Credential | undefined {
      if (!event.location) {
        return undefined;
      }
  
      const integrationName = event.location.replace("integrations:", "");
  
      return this.videoCredentials.find((credential: Credential) => credential.type.includes(integrationName));
    }
  
    /**
     * Creates a video event entry for the selected integration location.
     *
     * When optional uid is set, it will be used instead of the auto generated uid.
     *
     * @param event
     * @private
     */
    private createVideoEvent(event: Ensure<CalendarEvent, "language">): Promise<EventResult> {
      const credential = this.getVideoCredential(event);
  
      if (credential) {
        return createMeeting(credential, event);
      } else {
        return Promise.reject("No suitable credentials given for the requested integration name.");
      }
    }
  
    /**
     * Updates the event entries for all calendar integrations given in the credentials.
     * When noMail is true, no mails will be sent. This is used when the event is
     * a video meeting because then the mail containing the video credentials will be
     * more important than the mails created for these bare calendar events.
     *
     * @param event
     * @param booking
     * @param noMail
     * @private
     */
    private updateAllCalendarEvents(
      event: CalendarEvent,
      booking: PartialBooking | null,
      noMail: boolean | null
    ): Promise<Array<EventResult>> {
      return async.mapLimit(this.calendarCredentials, 5, async (credential) => {
        const bookingRefUid = booking
          ? booking.references.filter((ref) => ref.type === credential.type)[0]?.uid
          : null;
        return updateEvent(credential, event, noMail, bookingRefUid);
      });
    }
  
    /**
     * Updates a single video event.
     *
     * @param event
     * @param booking
     * @private
     */
    private updateVideoEvent(event: CalendarEvent, booking: PartialBooking) {
      const credential = this.getVideoCredential(event);
  
      if (credential) {
        const bookingRef = booking ? booking.references.filter((ref) => ref.type === credential.type)[0] : null;
        const bookingRefUid = bookingRef ? bookingRef.uid : null;
        return updateMeeting(credential, event, bookingRefUid).then((returnVal: EventResult) => {
          // Some video integrations, such as Zoom, don't return any data about the booking when updating it.
          if (returnVal.videoCallData === undefined) {
            returnVal.videoCallData = EventManager.bookingReferenceToVideoCallData(bookingRef);
          }
          return returnVal;
        });
      } else {
        return Promise.reject("No suitable credentials given for the requested integration name.");
      }
    }
  
    /**
     * Returns true if the given location describes a dedicated integration that
     * delivers meeting credentials. Zoom, for example, is dedicated, because it
     * needs to be called independently from any calendar APIs to receive meeting
     * credentials. Google Meetings, in contrast, are not dedicated, because they
     * are created while scheduling a regular calendar event by simply adding some
     * attributes to the payload JSON.
     *
     * @param location
     * @private
     */
    private static isDedicatedIntegration(location: string): boolean {
      // Hard-coded for now, because Zoom and Google Meet are both integrations, but one is dedicated, the other one isn't.
  
      return location === "integrations:zoom" || location === "integrations:daily";
    }
  
    /**
     * Helper function for processLocation: Returns the conferenceData object to be merged
     * with the CalendarEvent.
     *
     * @param locationObj
     * @private
     */
    private static getLocationRequestFromIntegration(locationObj: GetLocationRequestFromIntegrationRequest) {
      const location = locationObj.location;
  
      if (
        location === LocationType.GoogleMeet.valueOf() ||
        location === LocationType.Zoom.valueOf() ||
        location === LocationType.Daily.valueOf()
      ) {
        const requestId = uuidv5(location, uuidv5.URL);
  
        return {
          conferenceData: {
            createRequest: {
              requestId: requestId,
            },
          },
          location,
        };
      }
  
      return null;
    }
  
    /**
     * Takes a CalendarEvent and adds a ConferenceData object to the event
     * if the event has an integration-related location.
     *
     * @param event
     * @private
     */
    private static processLocation<T extends CalendarEvent>(event: T): T {
      // If location is set to an integration location
      // Build proper transforms for evt object
      // Extend evt object with those transformations
      if (event.location?.includes("integration")) {
        const maybeLocationRequestObject = EventManager.getLocationRequestFromIntegration({
          location: event.location,
        });
  
        event = merge(event, maybeLocationRequestObject);
      }
  
      return event;
    }
  
    /**
     * Accepts a PartialReference object and, if all data is complete,
     * returns a VideoCallData object containing the meeting information.
     *
     * @param reference
     * @private
     */
    private static bookingReferenceToVideoCallData(
      reference: PartialReference | null
    ): VideoCallData | undefined {
      let isComplete = true;
  
      if (!reference) {
        throw new Error("missing reference");
      }
  
      switch (reference.type) {
        case "zoom_video":
          // Zoom meetings in our system should always have an ID, a password and a join URL. In the
          // future, it might happen that we consider making passwords for Zoom meetings optional.
          // Then, this part below (where the password existence is checked) needs to be adapted.
          isComplete =
            reference.meetingId != undefined &&
            reference.meetingPassword != undefined &&
            reference.meetingUrl != undefined;
          break;
        default:
          isComplete = true;
      }
  
      if (isComplete) {
        return {
          type: reference.type,
          // The null coalescing operator should actually never be used here, because we checked if it's defined beforehand.
          id: reference.meetingId ?? "",
          password: reference.meetingPassword ?? "",
          url: reference.meetingUrl ?? "",
        };
      } else {
        return undefined;
      }
    }
  
    /**
     * Conditionally sends an email to the attendee.
     *
     * @param type
     * @param results
     * @param event
     * @private
     */
    private static async sendAttendeeMail(
      type: "new" | "reschedule",
      results: Array<EventResult>,
      event: CalendarEvent
    ) {
      if (
        !results.length ||
        !results.some((eRes) => (eRes.createdEvent || eRes.updatedEvent)?.disableConfirmationEmail)
      ) {
        const metadata: AdditionInformation = {};
        if (results.length) {
          // TODO: Handle created event metadata more elegantly
          metadata.hangoutLink = results[0].createdEvent?.hangoutLink;
          metadata.conferenceData = results[0].createdEvent?.conferenceData;
          metadata.entryPoints = results[0].createdEvent?.entryPoints;
        }
  
        event.additionInformation = metadata;
  
        let attendeeMail;
        switch (type) {
          case "reschedule":
            attendeeMail = new EventAttendeeRescheduledMail(event);
            break;
          case "new":
            attendeeMail = new EventAttendeeMail(event);
            break;
        }
        try {
          await attendeeMail.sendEmail();
        } catch (e) {
          console.error("attendeeMail.sendEmail failed", e);
        }
      }
    }
  }
  