diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index 6b5d84dd22..6d6aa02533 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -1603,6 +1603,7 @@ "booking_with_payment_cancelled_refunded": "This booking payment has been refunded.", "booking_confirmation_failed": "Booking confirmation failed", "get_started_zapier_templates": "Get started with Zapier templates", + "team_member": "Team member", "a_routing_form": "A Routing Form", "form_description_placeholder": "Form Description", "keep_me_connected_with_form": "Keep me connected with the form", diff --git a/packages/core/EventManager.ts b/packages/core/EventManager.ts index 74ede6f7a0..ead4cf1cf5 100644 --- a/packages/core/EventManager.ts +++ b/packages/core/EventManager.ts @@ -255,7 +255,6 @@ export default class EventManager { results.push(result); } - // Update all calendar events. results.push(...(await this.updateAllCalendarEvents(evt, booking))); const bookingPayment = booking?.payment; @@ -442,7 +441,7 @@ export default class EventManager { // Bookings should only have one calendar reference calendarReference = booking.references.filter((reference) => reference.type.includes("_calendar"))[0]; if (!calendarReference) { - throw new Error("bookingRef"); + return []; } const { uid: bookingRefUid, externalCalendarId: bookingExternalCalendarId } = calendarReference; diff --git a/packages/core/builders/CalendarEvent/class.ts b/packages/core/builders/CalendarEvent/class.ts index 168fb08043..880fde5287 100644 --- a/packages/core/builders/CalendarEvent/class.ts +++ b/packages/core/builders/CalendarEvent/class.ts @@ -1,4 +1,4 @@ -import { DestinationCalendar } from "@prisma/client"; +import type { DestinationCalendar } from "@prisma/client"; import type { AdditionalInformation, @@ -16,7 +16,7 @@ class CalendarEventClass implements CalendarEvent { organizer!: Person; attendees!: Person[]; description?: string | null; - team?: { name: string; members: string[] }; + team?: { name: string; members: Person[] }; location?: string | null; conferenceData?: ConferenceData; additionalInformation?: AdditionalInformation; diff --git a/packages/emails/email-manager.ts b/packages/emails/email-manager.ts index f3d3c4f422..aea2a3a706 100644 --- a/packages/emails/email-manager.ts +++ b/packages/emails/email-manager.ts @@ -1,5 +1,6 @@ -import { TFunction } from "next-i18next"; +import type { TFunction } from "next-i18next"; +import type BaseEmail from "@calcom/emails/templates/_base-email"; import type { CalendarEvent, Person } from "@calcom/types/Calendar"; import AttendeeAwaitingPaymentEmail from "./templates/attendee-awaiting-payment-email"; @@ -12,8 +13,10 @@ import AttendeeScheduledEmail from "./templates/attendee-scheduled-email"; import AttendeeWasRequestedToRescheduleEmail from "./templates/attendee-was-requested-to-reschedule-email"; import BrokenIntegrationEmail from "./templates/broken-integration-email"; import DisabledAppEmail from "./templates/disabled-app-email"; -import FeedbackEmail, { Feedback } from "./templates/feedback-email"; -import ForgotPasswordEmail, { PasswordReset } from "./templates/forgot-password-email"; +import type { Feedback } from "./templates/feedback-email"; +import FeedbackEmail from "./templates/feedback-email"; +import type { PasswordReset } from "./templates/forgot-password-email"; +import ForgotPasswordEmail from "./templates/forgot-password-email"; import OrganizerCancelledEmail from "./templates/organizer-cancelled-email"; import OrganizerLocationChangeEmail from "./templates/organizer-location-change-email"; import OrganizerPaymentRefundFailedEmail from "./templates/organizer-payment-refund-failed-email"; @@ -22,32 +25,34 @@ import OrganizerRequestReminderEmail from "./templates/organizer-request-reminde import OrganizerRequestedToRescheduleEmail from "./templates/organizer-requested-to-reschedule-email"; import OrganizerRescheduledEmail from "./templates/organizer-rescheduled-email"; import OrganizerScheduledEmail from "./templates/organizer-scheduled-email"; -import TeamInviteEmail, { TeamInvite } from "./templates/team-invite-email"; +import type { TeamInvite } from "./templates/team-invite-email"; +import TeamInviteEmail from "./templates/team-invite-email"; + +const sendEmail = (prepare: () => BaseEmail) => { + return new Promise((resolve, reject) => { + try { + const email = prepare(); + resolve(email.sendEmail()); + } catch (e) { + reject(console.error(`${prepare.constructor.name}.sendEmail failed`, e)); + } + }); +}; export const sendScheduledEmails = async (calEvent: CalendarEvent) => { const emailsToSend: Promise[] = []; - emailsToSend.push( - ...calEvent.attendees.map((attendee) => { - return new Promise((resolve, reject) => { - try { - const scheduledEmail = new AttendeeScheduledEmail(calEvent, attendee); - resolve(scheduledEmail.sendEmail()); - } catch (e) { - reject(console.error("AttendeeRescheduledEmail.sendEmail failed", e)); - } - }); - }) - ); + emailsToSend.push(sendEmail(() => new OrganizerScheduledEmail({ calEvent }))); + + if (calEvent.team) { + for (const teamMember of calEvent.team.members) { + emailsToSend.push(sendEmail(() => new OrganizerScheduledEmail({ calEvent, teamMember }))); + } + } emailsToSend.push( - new Promise((resolve, reject) => { - try { - const scheduledEmail = new OrganizerScheduledEmail(calEvent); - resolve(scheduledEmail.sendEmail()); - } catch (e) { - reject(console.error("OrganizerScheduledEmail.sendEmail failed", e)); - } + ...calEvent.attendees.map((attendee) => { + return sendEmail(() => new AttendeeScheduledEmail(calEvent, attendee)); }) ); @@ -56,28 +61,19 @@ export const sendScheduledEmails = async (calEvent: CalendarEvent) => { export const sendRescheduledEmails = async (calEvent: CalendarEvent) => { const emailsToSend: Promise[] = []; + + emailsToSend.push(sendEmail(() => new OrganizerRescheduledEmail({ calEvent }))); + + if (calEvent.team) { + for (const teamMember of calEvent.team.members) { + emailsToSend.push(sendEmail(() => new OrganizerRescheduledEmail({ calEvent, teamMember }))); + } + } + // @TODO: we should obtain who is rescheduling the event and send them a different email emailsToSend.push( ...calEvent.attendees.map((attendee) => { - return new Promise((resolve, reject) => { - try { - const scheduledEmail = new AttendeeRescheduledEmail(calEvent, attendee); - resolve(scheduledEmail.sendEmail()); - } catch (e) { - reject(console.error("AttendeeRescheduledEmail.sendEmail failed", e)); - } - }); - }) - ); - - emailsToSend.push( - new Promise((resolve, reject) => { - try { - const scheduledEmail = new OrganizerRescheduledEmail(calEvent); - resolve(scheduledEmail.sendEmail()); - } catch (e) { - reject(console.error("OrganizerRescheduledEmail.sendEmail failed", e)); - } + return sendEmail(() => new AttendeeRescheduledEmail(calEvent, attendee)); }) ); @@ -92,51 +88,35 @@ export const sendScheduledSeatsEmails = async ( ) => { const emailsToSend: Promise[] = []; - emailsToSend.push( - new Promise((resolve, reject) => { - try { - const scheduledEmail = new AttendeeScheduledEmail(calEvent, invitee, showAttendees); - resolve(scheduledEmail.sendEmail()); - } catch (e) { - reject(console.error("AttendeeScheduledEmail.sendEmail failed", e)); - } - }) - ); + emailsToSend.push(sendEmail(() => new OrganizerScheduledEmail({ calEvent, newSeat }))); - emailsToSend.push( - new Promise((resolve, reject) => { - try { - const scheduledEmail = new OrganizerScheduledEmail(calEvent, newSeat); - resolve(scheduledEmail.sendEmail()); - } catch (e) { - reject(console.error("OrganizerScheduledEmail.sendEmail failed", e)); - } - }) - ); + if (calEvent.team) { + for (const teamMember of calEvent.team.members) { + emailsToSend.push(sendEmail(() => new OrganizerScheduledEmail({ calEvent, newSeat, teamMember }))); + } + } + + emailsToSend.push(sendEmail(() => new AttendeeScheduledEmail(calEvent, invitee, showAttendees))); await Promise.all(emailsToSend); }; export const sendOrganizerRequestEmail = async (calEvent: CalendarEvent) => { - await new Promise((resolve, reject) => { - try { - const organizerRequestEmail = new OrganizerRequestEmail(calEvent); - resolve(organizerRequestEmail.sendEmail()); - } catch (e) { - reject(console.error("OrganizerRequestEmail.sendEmail failed", e)); + const emailsToSend: Promise[] = []; + + emailsToSend.push(sendEmail(() => new OrganizerRequestEmail({ calEvent }))); + + if (calEvent.team?.members) { + for (const teamMember of calEvent.team.members) { + emailsToSend.push(sendEmail(() => new OrganizerRequestEmail({ calEvent, teamMember }))); } - }); + } + + await Promise.all(emailsToSend); }; export const sendAttendeeRequestEmail = async (calEvent: CalendarEvent, attendee: Person) => { - await new Promise((resolve, reject) => { - try { - const attendeeRequestEmail = new AttendeeRequestEmail(calEvent, attendee); - resolve(attendeeRequestEmail.sendEmail()); - } catch (e) { - reject(console.error("AttendRequestEmail.sendEmail failed", e)); - } - }); + await sendEmail(() => new AttendeeRequestEmail(calEvent, attendee)); }; export const sendDeclinedEmails = async (calEvent: CalendarEvent) => { @@ -144,14 +124,7 @@ export const sendDeclinedEmails = async (calEvent: CalendarEvent) => { emailsToSend.push( ...calEvent.attendees.map((attendee) => { - return new Promise((resolve, reject) => { - try { - const declinedEmail = new AttendeeDeclinedEmail(calEvent, attendee); - resolve(declinedEmail.sendEmail()); - } catch (e) { - reject(console.error("AttendeeRescheduledEmail.sendEmail failed", e)); - } - }); + return sendEmail(() => new AttendeeDeclinedEmail(calEvent, attendee)); }) ); @@ -161,27 +134,17 @@ export const sendDeclinedEmails = async (calEvent: CalendarEvent) => { export const sendCancelledEmails = async (calEvent: CalendarEvent) => { const emailsToSend: Promise[] = []; - emailsToSend.push( - ...calEvent.attendees.map((attendee) => { - return new Promise((resolve, reject) => { - try { - const scheduledEmail = new AttendeeCancelledEmail(calEvent, attendee); - resolve(scheduledEmail.sendEmail()); - } catch (e) { - reject(console.error("AttendeeCancelledEmail.sendEmail failed", e)); - } - }); - }) - ); + emailsToSend.push(sendEmail(() => new OrganizerCancelledEmail({ calEvent }))); + + if (calEvent.team?.members) { + for (const teamMember of calEvent.team.members) { + emailsToSend.push(sendEmail(() => new OrganizerCancelledEmail({ calEvent, teamMember }))); + } + } emailsToSend.push( - new Promise((resolve, reject) => { - try { - const scheduledEmail = new OrganizerCancelledEmail(calEvent); - resolve(scheduledEmail.sendEmail()); - } catch (e) { - reject(console.error("OrganizerCancelledEmail.sendEmail failed", e)); - } + ...calEvent.attendees.map((attendee) => { + return sendEmail(() => new AttendeeCancelledEmail(calEvent, attendee)); }) ); @@ -189,14 +152,15 @@ export const sendCancelledEmails = async (calEvent: CalendarEvent) => { }; export const sendOrganizerRequestReminderEmail = async (calEvent: CalendarEvent) => { - await new Promise((resolve, reject) => { - try { - const organizerRequestReminderEmail = new OrganizerRequestReminderEmail(calEvent); - resolve(organizerRequestReminderEmail.sendEmail()); - } catch (e) { - reject(console.error("OrganizerRequestReminderEmail.sendEmail failed", e)); + const emailsToSend: Promise[] = []; + + emailsToSend.push(sendEmail(() => new OrganizerRequestReminderEmail({ calEvent }))); + + if (calEvent.team?.members) { + for (const teamMember of calEvent.team.members) { + emailsToSend.push(sendEmail(() => new OrganizerRequestReminderEmail({ calEvent, teamMember }))); } - }); + } }; export const sendAwaitingPaymentEmail = async (calEvent: CalendarEvent) => { @@ -204,50 +168,31 @@ export const sendAwaitingPaymentEmail = async (calEvent: CalendarEvent) => { emailsToSend.push( ...calEvent.attendees.map((attendee) => { - return new Promise((resolve, reject) => { - try { - const paymentEmail = new AttendeeAwaitingPaymentEmail(calEvent, attendee); - resolve(paymentEmail.sendEmail()); - } catch (e) { - reject(console.error("AttendeeAwaitingPaymentEmail.sendEmail failed", e)); - } - }); + return sendEmail(() => new AttendeeAwaitingPaymentEmail(calEvent, attendee)); }) ); await Promise.all(emailsToSend); }; export const sendOrganizerPaymentRefundFailedEmail = async (calEvent: CalendarEvent) => { - await new Promise((resolve, reject) => { - try { - const paymentRefundFailedEmail = new OrganizerPaymentRefundFailedEmail(calEvent); - resolve(paymentRefundFailedEmail.sendEmail()); - } catch (e) { - reject(console.error("OrganizerPaymentRefundFailedEmail.sendEmail failed", e)); + const emailsToSend: Promise[] = []; + emailsToSend.push(sendEmail(() => new OrganizerPaymentRefundFailedEmail({ calEvent }))); + + if (calEvent.team?.members) { + for (const teamMember of calEvent.team.members) { + emailsToSend.push(sendEmail(() => new OrganizerPaymentRefundFailedEmail({ calEvent, teamMember }))); } - }); + } + + await Promise.all(emailsToSend); }; export const sendPasswordResetEmail = async (passwordResetEvent: PasswordReset) => { - await new Promise((resolve, reject) => { - try { - const passwordResetEmail = new ForgotPasswordEmail(passwordResetEvent); - resolve(passwordResetEmail.sendEmail()); - } catch (e) { - reject(console.error("OrganizerPaymentRefundFailedEmail.sendEmail failed", e)); - } - }); + await sendEmail(() => new ForgotPasswordEmail(passwordResetEvent)); }; export const sendTeamInviteEmail = async (teamInviteEvent: TeamInvite) => { - await new Promise((resolve, reject) => { - try { - const teamInviteEmail = new TeamInviteEmail(teamInviteEvent); - resolve(teamInviteEmail.sendEmail()); - } catch (e) { - reject(console.error("TeamInviteEmail.sendEmail failed", e)); - } - }); + await sendEmail(() => new TeamInviteEmail(teamInviteEvent)); }; export const sendRequestRescheduleEmail = async ( @@ -256,27 +201,9 @@ export const sendRequestRescheduleEmail = async ( ) => { const emailsToSend: Promise[] = []; - emailsToSend.push( - new Promise((resolve, reject) => { - try { - const requestRescheduleEmail = new AttendeeWasRequestedToRescheduleEmail(calEvent, metadata); - resolve(requestRescheduleEmail.sendEmail()); - } catch (e) { - reject(console.error("AttendeeWasRequestedToRescheduleEmail.sendEmail failed", e)); - } - }) - ); + emailsToSend.push(sendEmail(() => new OrganizerRequestedToRescheduleEmail(calEvent, metadata))); - emailsToSend.push( - new Promise((resolve, reject) => { - try { - const requestRescheduleEmail = new OrganizerRequestedToRescheduleEmail(calEvent, metadata); - resolve(requestRescheduleEmail.sendEmail()); - } catch (e) { - reject(console.error("OrganizerRequestedToRescheduleEmail.sendEmail failed", e)); - } - }) - ); + emailsToSend.push(sendEmail(() => new AttendeeWasRequestedToRescheduleEmail(calEvent, metadata))); await Promise.all(emailsToSend); }; @@ -284,52 +211,28 @@ export const sendRequestRescheduleEmail = async ( export const sendLocationChangeEmails = async (calEvent: CalendarEvent) => { const emailsToSend: Promise[] = []; - emailsToSend.push( - ...calEvent.attendees.map((attendee) => { - return new Promise((resolve, reject) => { - try { - const scheduledEmail = new AttendeeLocationChangeEmail(calEvent, attendee); - resolve(scheduledEmail.sendEmail()); - } catch (e) { - reject(console.error("AttendeeLocationChangeEmail.sendEmail failed", e)); - } - }); - }) - ); + emailsToSend.push(sendEmail(() => new OrganizerLocationChangeEmail({ calEvent }))); + + if (calEvent.team?.members) { + for (const teamMember of calEvent.team.members) { + emailsToSend.push(sendEmail(() => new OrganizerLocationChangeEmail({ calEvent, teamMember }))); + } + } emailsToSend.push( - new Promise((resolve, reject) => { - try { - const scheduledEmail = new OrganizerLocationChangeEmail(calEvent); - resolve(scheduledEmail.sendEmail()); - } catch (e) { - reject(console.error("OrganizerLocationChangeEmail.sendEmail failed", e)); - } + ...calEvent.attendees.map((attendee) => { + return sendEmail(() => new AttendeeLocationChangeEmail(calEvent, attendee)); }) ); await Promise.all(emailsToSend); }; export const sendFeedbackEmail = async (feedback: Feedback) => { - await new Promise((resolve, reject) => { - try { - const feedbackEmail = new FeedbackEmail(feedback); - resolve(feedbackEmail.sendEmail()); - } catch (e) { - reject(console.error("FeedbackEmail.sendEmail failed", e)); - } - }); + await sendEmail(() => new FeedbackEmail(feedback)); }; export const sendBrokenIntegrationEmail = async (evt: CalendarEvent, type: "video" | "calendar") => { - await new Promise((resolve, reject) => { - try { - const brokenIntegrationEmail = new BrokenIntegrationEmail(evt, type); - resolve(brokenIntegrationEmail.sendEmail()); - } catch (e) { - reject(console.error("FeedbackEmail.sendEmail failed", e)); - } - }); + await sendEmail(() => new BrokenIntegrationEmail(evt, type)); }; export const sendDisabledAppEmail = async ({ @@ -347,12 +250,5 @@ export const sendDisabledAppEmail = async ({ title?: string; eventTypeId?: number; }) => { - await new Promise((resolve, reject) => { - try { - const disabledPaymentEmail = new DisabledAppEmail(email, appName, appType, t, title, eventTypeId); - resolve(disabledPaymentEmail.sendEmail()); - } catch (e) { - reject(console.error("DisabledPaymentEmail.sendEmail failed", e)); - } - }); + await sendEmail(() => new DisabledAppEmail(email, appName, appType, t, title, eventTypeId)); }; diff --git a/packages/emails/src/components/WhoInfo.tsx b/packages/emails/src/components/WhoInfo.tsx index 41a1bae2ad..c710028d0b 100644 --- a/packages/emails/src/components/WhoInfo.tsx +++ b/packages/emails/src/components/WhoInfo.tsx @@ -1,4 +1,4 @@ -import { TFunction } from "next-i18next"; +import type { TFunction } from "next-i18next"; import type { CalendarEvent } from "@calcom/types/Calendar"; @@ -27,6 +27,9 @@ export function WhoInfo(props: { calEvent: CalendarEvent; t: TFunction }) { role={t("organizer")} email={props.calEvent.organizer.email} /> + {props.calEvent.team?.members.map((member) => ( + + ))} {props.calEvent.attendees.map((attendee) => ( > ) => { let subject; @@ -26,10 +27,10 @@ export const OrganizerScheduledEmail = ( title = "new_event_scheduled"; } - const t = props.calEvent.organizer.language.translate; + const t = props.teamMember?.language.translate || props.calEvent.organizer.language.translate; return ( { - const toAddresses = [this.calEvent.attendees[0].email]; - if (this.calEvent.team) { - this.calEvent.team.members.forEach((member) => { - const memberAttendee = this.calEvent.attendees.find((attendee) => attendee.name === member); - if (memberAttendee) { - toAddresses.push(memberAttendee.email); - } - }); - } + const toAddresses = this.calEvent.attendees.map((attendee) => attendee.email); return { from: `${APP_NAME} <${this.getMailerOptions().from}>`, diff --git a/packages/emails/templates/attendee-scheduled-email.ts b/packages/emails/templates/attendee-scheduled-email.ts index 5a6dcc2a4e..610b3db54e 100644 --- a/packages/emails/templates/attendee-scheduled-email.ts +++ b/packages/emails/templates/attendee-scheduled-email.ts @@ -1,5 +1,6 @@ -import { createEvent, DateArray } from "ics"; -import { TFunction } from "next-i18next"; +import type { DateArray } from "ics"; +import { createEvent } from "ics"; +import type { TFunction } from "next-i18next"; import { RRule } from "rrule"; import dayjs from "@calcom/dayjs"; @@ -47,10 +48,18 @@ export default class AttendeeScheduledEmail extends BaseEmail { description: this.getTextBody(), duration: { minutes: dayjs(this.calEvent.endTime).diff(dayjs(this.calEvent.startTime), "minute") }, organizer: { name: this.calEvent.organizer.name, email: this.calEvent.organizer.email }, - attendees: this.calEvent.attendees.map((attendee: Person) => ({ - name: attendee.name, - email: attendee.email, - })), + attendees: [ + ...this.calEvent.attendees.map((attendee: Person) => ({ + name: attendee.name, + email: attendee.email, + })), + ...(this.calEvent.team?.members + ? this.calEvent.team?.members.map((member: Person) => ({ + name: member.name, + email: member.email, + })) + : []), + ], ...{ recurrenceRule }, status: "CONFIRMED", }); diff --git a/packages/emails/templates/attendee-was-requested-to-reschedule-email.ts b/packages/emails/templates/attendee-was-requested-to-reschedule-email.ts index cfeb6c3d66..97967572bf 100644 --- a/packages/emails/templates/attendee-was-requested-to-reschedule-email.ts +++ b/packages/emails/templates/attendee-was-requested-to-reschedule-email.ts @@ -1,4 +1,5 @@ -import { createEvent, DateArray, Person } from "ics"; +import type { DateArray, Person } from "ics"; +import { createEvent } from "ics"; import dayjs from "@calcom/dayjs"; import { getManageLink } from "@calcom/lib/CalEventParser"; @@ -11,7 +12,7 @@ import OrganizerScheduledEmail from "./organizer-scheduled-email"; export default class AttendeeWasRequestedToRescheduleEmail extends OrganizerScheduledEmail { private metadata: { rescheduleLink: string }; constructor(calEvent: CalendarEvent, metadata: { rescheduleLink: string }) { - super(calEvent); + super({ calEvent }); this.metadata = metadata; } protected getNodeMailerPayload(): Record { diff --git a/packages/emails/templates/broken-integration-email.ts b/packages/emails/templates/broken-integration-email.ts index 349723be45..89258fe16b 100644 --- a/packages/emails/templates/broken-integration-email.ts +++ b/packages/emails/templates/broken-integration-email.ts @@ -1,4 +1,4 @@ -import { TFunction } from "next-i18next"; +import type { TFunction } from "next-i18next"; import { getRichDescription } from "@calcom/lib/CalEventParser"; import { APP_NAME } from "@calcom/lib/constants"; @@ -22,14 +22,6 @@ export default class BrokenIntegrationEmail extends BaseEmail { protected getNodeMailerPayload(): Record { const toAddresses = [this.calEvent.organizer.email]; - if (this.calEvent.team) { - this.calEvent.team.members.forEach((member) => { - const memberAttendee = this.calEvent.attendees.find((attendee) => attendee.name === member); - if (memberAttendee) { - toAddresses.push(memberAttendee.email); - } - }); - } return { from: `${APP_NAME} <${this.getMailerOptions().from}>`, diff --git a/packages/emails/templates/organizer-cancelled-email.ts b/packages/emails/templates/organizer-cancelled-email.ts index b3842b2d53..d0e2fb315c 100644 --- a/packages/emails/templates/organizer-cancelled-email.ts +++ b/packages/emails/templates/organizer-cancelled-email.ts @@ -5,15 +5,7 @@ import OrganizerScheduledEmail from "./organizer-scheduled-email"; export default class OrganizerCancelledEmail extends OrganizerScheduledEmail { protected getNodeMailerPayload(): Record { - const toAddresses = [this.calEvent.organizer.email]; - if (this.calEvent.team) { - this.calEvent.team.members.forEach((member) => { - const memberAttendee = this.calEvent.attendees.find((attendee) => attendee.name === member); - if (memberAttendee) { - toAddresses.push(memberAttendee.email); - } - }); - } + const toAddresses = [this.teamMember?.email || this.calEvent.organizer.email]; return { from: `${APP_NAME} <${this.getMailerOptions().from}>`, diff --git a/packages/emails/templates/organizer-location-change-email.ts b/packages/emails/templates/organizer-location-change-email.ts index 1385d10609..57bb7dce66 100644 --- a/packages/emails/templates/organizer-location-change-email.ts +++ b/packages/emails/templates/organizer-location-change-email.ts @@ -5,15 +5,7 @@ import OrganizerScheduledEmail from "./organizer-scheduled-email"; export default class OrganizerLocationChangeEmail extends OrganizerScheduledEmail { protected getNodeMailerPayload(): Record { - const toAddresses = [this.calEvent.organizer.email]; - if (this.calEvent.team) { - this.calEvent.team.members.forEach((member) => { - const memberAttendee = this.calEvent.attendees.find((attendee) => attendee.name === member); - if (memberAttendee) { - toAddresses.push(memberAttendee.email); - } - }); - } + const toAddresses = [this.teamMember?.email || this.calEvent.organizer.email]; return { icalEvent: { diff --git a/packages/emails/templates/organizer-payment-refund-failed-email.ts b/packages/emails/templates/organizer-payment-refund-failed-email.ts index 1567501f35..26818b1fd7 100644 --- a/packages/emails/templates/organizer-payment-refund-failed-email.ts +++ b/packages/emails/templates/organizer-payment-refund-failed-email.ts @@ -5,15 +5,7 @@ import OrganizerScheduledEmail from "./organizer-scheduled-email"; export default class OrganizerPaymentRefundFailedEmail extends OrganizerScheduledEmail { protected getNodeMailerPayload(): Record { - const toAddresses = [this.calEvent.organizer.email]; - if (this.calEvent.team) { - this.calEvent.team.members.forEach((member) => { - const memberAttendee = this.calEvent.attendees.find((attendee) => attendee.name === member); - if (memberAttendee) { - toAddresses.push(memberAttendee.email); - } - }); - } + const toAddresses = [this.teamMember?.email || this.calEvent.organizer.email]; return { from: `${APP_NAME} <${this.getMailerOptions().from}>`, diff --git a/packages/emails/templates/organizer-request-email.ts b/packages/emails/templates/organizer-request-email.ts index cf9549d825..f6e2c38b0a 100644 --- a/packages/emails/templates/organizer-request-email.ts +++ b/packages/emails/templates/organizer-request-email.ts @@ -5,15 +5,7 @@ import OrganizerScheduledEmail from "./organizer-scheduled-email"; export default class OrganizerRequestEmail extends OrganizerScheduledEmail { protected getNodeMailerPayload(): Record { - const toAddresses = [this.calEvent.organizer.email]; - if (this.calEvent.team) { - this.calEvent.team.members.forEach((member) => { - const memberAttendee = this.calEvent.attendees.find((attendee) => attendee.name === member); - if (memberAttendee) { - toAddresses.push(memberAttendee.email); - } - }); - } + const toAddresses = [this.teamMember?.email || this.calEvent.organizer.email]; return { from: `${APP_NAME} <${this.getMailerOptions().from}>`, diff --git a/packages/emails/templates/organizer-request-reminder-email.ts b/packages/emails/templates/organizer-request-reminder-email.ts index 2a3d0ebff1..20973fc8ac 100644 --- a/packages/emails/templates/organizer-request-reminder-email.ts +++ b/packages/emails/templates/organizer-request-reminder-email.ts @@ -5,15 +5,7 @@ import OrganizerRequestEmail from "./organizer-request-email"; export default class OrganizerRequestReminderEmail extends OrganizerRequestEmail { protected getNodeMailerPayload(): Record { - const toAddresses = [this.calEvent.organizer.email]; - if (this.calEvent.team) { - this.calEvent.team.members.forEach((member) => { - const memberAttendee = this.calEvent.attendees.find((attendee) => attendee.name === member); - if (memberAttendee) { - toAddresses.push(memberAttendee.email); - } - }); - } + const toAddresses = [this.teamMember?.email || this.calEvent.organizer.email]; return { from: `${APP_NAME} <${this.getMailerOptions().from}>`, diff --git a/packages/emails/templates/organizer-requested-to-reschedule-email.ts b/packages/emails/templates/organizer-requested-to-reschedule-email.ts index e3c433f014..8293c7053a 100644 --- a/packages/emails/templates/organizer-requested-to-reschedule-email.ts +++ b/packages/emails/templates/organizer-requested-to-reschedule-email.ts @@ -1,4 +1,5 @@ -import { createEvent, DateArray, Person } from "ics"; +import type { DateArray, Person } from "ics"; +import { createEvent } from "ics"; import dayjs from "@calcom/dayjs"; import { getRichDescription } from "@calcom/lib/CalEventParser"; @@ -11,7 +12,7 @@ import OrganizerScheduledEmail from "./organizer-scheduled-email"; export default class OrganizerRequestedToRescheduleEmail extends OrganizerScheduledEmail { private metadata: { rescheduleLink: string }; constructor(calEvent: CalendarEvent, metadata: { rescheduleLink: string }) { - super(calEvent); + super({ calEvent }); this.metadata = metadata; } protected getNodeMailerPayload(): Record { diff --git a/packages/emails/templates/organizer-rescheduled-email.ts b/packages/emails/templates/organizer-rescheduled-email.ts index 31b1fe45c9..07808a0863 100644 --- a/packages/emails/templates/organizer-rescheduled-email.ts +++ b/packages/emails/templates/organizer-rescheduled-email.ts @@ -5,15 +5,7 @@ import OrganizerScheduledEmail from "./organizer-scheduled-email"; export default class OrganizerRescheduledEmail extends OrganizerScheduledEmail { protected getNodeMailerPayload(): Record { - const toAddresses = [this.calEvent.organizer.email]; - if (this.calEvent.team) { - this.calEvent.team.members.forEach((member) => { - const memberAttendee = this.calEvent.attendees.find((attendee) => attendee.name === member); - if (memberAttendee) { - toAddresses.push(memberAttendee.email); - } - }); - } + const toAddresses = [this.teamMember?.email || this.calEvent.organizer.email]; return { icalEvent: { diff --git a/packages/emails/templates/organizer-scheduled-email.ts b/packages/emails/templates/organizer-scheduled-email.ts index 11af9fccf8..dbd91140f7 100644 --- a/packages/emails/templates/organizer-scheduled-email.ts +++ b/packages/emails/templates/organizer-scheduled-email.ts @@ -1,11 +1,12 @@ -import { createEvent, DateArray, Person } from "ics"; -import { TFunction } from "next-i18next"; +import type { DateArray } from "ics"; +import { createEvent } from "ics"; +import type { TFunction } from "next-i18next"; import { RRule } from "rrule"; import dayjs from "@calcom/dayjs"; import { getRichDescription } from "@calcom/lib/CalEventParser"; import { APP_NAME } from "@calcom/lib/constants"; -import type { CalendarEvent } from "@calcom/types/Calendar"; +import type { CalendarEvent, Person } from "@calcom/types/Calendar"; import { renderEmail } from "../"; import BaseEmail from "./_base-email"; @@ -14,13 +15,15 @@ export default class OrganizerScheduledEmail extends BaseEmail { calEvent: CalendarEvent; t: TFunction; newSeat?: boolean; + teamMember?: Person; - constructor(calEvent: CalendarEvent, newSeat?: boolean) { + constructor(input: { calEvent: CalendarEvent; newSeat?: boolean; teamMember?: Person }) { super(); this.name = "SEND_BOOKING_CONFIRMATION"; - this.calEvent = calEvent; + this.calEvent = input.calEvent; this.t = this.calEvent.organizer.language.translate; - this.newSeat = newSeat; + this.newSeat = input.newSeat; + this.teamMember = input.teamMember; } protected getiCalEventAsString(): string | undefined { @@ -43,10 +46,18 @@ export default class OrganizerScheduledEmail extends BaseEmail { duration: { minutes: dayjs(this.calEvent.endTime).diff(dayjs(this.calEvent.startTime), "minute") }, organizer: { name: this.calEvent.organizer.name, email: this.calEvent.organizer.email }, ...{ recurrenceRule }, - attendees: this.calEvent.attendees.map((attendee: Person) => ({ - name: attendee.name, - email: attendee.email, - })), + attendees: [ + ...this.calEvent.attendees.map((attendee: Person) => ({ + name: attendee.name, + email: attendee.email, + })), + ...(this.calEvent.team?.members + ? this.calEvent.team?.members.map((member: Person) => ({ + name: member.name, + email: member.email, + })) + : []), + ], status: "CONFIRMED", }); if (icsEvent.error) { @@ -56,15 +67,7 @@ export default class OrganizerScheduledEmail extends BaseEmail { } protected getNodeMailerPayload(): Record { - const toAddresses = [this.calEvent.organizer.email]; - if (this.calEvent.team) { - this.calEvent.team.members.forEach((member) => { - const memberAttendee = this.calEvent.attendees.find((attendee) => attendee.name === member); - if (memberAttendee) { - toAddresses.push(memberAttendee.email); - } - }); - } + const toAddresses = [this.teamMember?.email || this.calEvent.organizer.email]; return { icalEvent: { @@ -79,6 +82,7 @@ export default class OrganizerScheduledEmail extends BaseEmail { html: renderEmail("OrganizerScheduledEmail", { calEvent: this.calEvent, attendee: this.calEvent.organizer, + teamMember: this.teamMember, newSeat: this.newSeat, }), text: this.getTextBody(), diff --git a/packages/features/bookings/lib/handleCancelBooking.ts b/packages/features/bookings/lib/handleCancelBooking.ts index acd85d3d12..4ced54cf23 100644 --- a/packages/features/bookings/lib/handleCancelBooking.ts +++ b/packages/features/bookings/lib/handleCancelBooking.ts @@ -1,11 +1,6 @@ -import { - BookingStatus, - MembershipRole, - WebhookTriggerEvents, - WorkflowMethods, - WorkflowReminder, -} from "@prisma/client"; -import { NextApiRequest } from "next"; +import type { WebhookTriggerEvents, WorkflowReminder } from "@prisma/client"; +import { BookingStatus, MembershipRole, WorkflowMethods } from "@prisma/client"; +import type { NextApiRequest } from "next"; import appStore from "@calcom/app-store"; import { getCalendar } from "@calcom/app-store/_utils/getCalendar"; @@ -19,7 +14,8 @@ import { deleteScheduledEmailReminder } from "@calcom/features/ee/workflows/lib/ import { sendCancelledReminders } from "@calcom/features/ee/workflows/lib/reminders/reminderScheduler"; import { deleteScheduledSMSReminder } from "@calcom/features/ee/workflows/lib/reminders/smsReminderManager"; import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks"; -import sendPayload, { EventTypeInfo } from "@calcom/features/webhooks/lib/sendPayload"; +import type { EventTypeInfo } from "@calcom/features/webhooks/lib/sendPayload"; +import sendPayload from "@calcom/features/webhooks/lib/sendPayload"; import { isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib"; import { HttpError } from "@calcom/lib/http-error"; import { handleRefundError } from "@calcom/lib/payment/handleRefundError"; @@ -74,6 +70,11 @@ async function handler(req: NextApiRequest & { userId?: number }) { price: true, currency: true, length: true, + hosts: { + select: { + user: true, + }, + }, workflows: { include: { workflow: { @@ -118,8 +119,12 @@ async function handler(req: NextApiRequest & { userId?: number }) { }, }); - const attendeesListPromises = bookingToDelete.attendees.map(async (attendee) => { - return { + const teamMembersPromises = []; + const attendeesListPromises = []; + const hostsPresent = !!bookingToDelete.eventType?.hosts; + + for (const attendee of bookingToDelete.attendees) { + const attendeeObject = { name: attendee.name, email: attendee.email, timeZone: attendee.timeZone, @@ -128,9 +133,24 @@ async function handler(req: NextApiRequest & { userId?: number }) { locale: attendee.locale ?? "en", }, }; - }); + + // Check for the presence of hosts to determine if it is a team event type + if (hostsPresent) { + // If the attendee is a host then they are a team member + const teamMember = bookingToDelete.eventType?.hosts.some((host) => host.user.email === attendee.email); + if (teamMember) { + teamMembersPromises.push(attendeeObject); + // If not then they are an attendee + } else { + attendeesListPromises.push(attendeeObject); + } + } else { + attendeesListPromises.push(attendeeObject); + } + } const attendeesList = await Promise.all(attendeesListPromises); + const teamMembers = await Promise.all(teamMembersPromises); const tOrganizer = await getTranslation(organizer.locale ?? "en", "common"); const evt: CalendarEvent = { @@ -155,7 +175,9 @@ async function handler(req: NextApiRequest & { userId?: number }) { location: bookingToDelete?.location, destinationCalendar: bookingToDelete?.destinationCalendar || bookingToDelete?.user.destinationCalendar, cancellationReason: cancellationReason, + ...(teamMembers && { team: { name: "", members: teamMembers } }), }; + // Hook up the webhook logic here const eventTrigger: WebhookTriggerEvents = "BOOKING_CANCELLED"; // Send Webhook call if hooked to BOOKING.CANCELLED diff --git a/packages/features/bookings/lib/handleNewBooking.ts b/packages/features/bookings/lib/handleNewBooking.ts index 5687fb20e2..fe8e9edd66 100644 --- a/packages/features/bookings/lib/handleNewBooking.ts +++ b/packages/features/bookings/lib/handleNewBooking.ts @@ -335,6 +335,10 @@ async function handler(req: NextApiRequest & { userId?: number | undefined }) { if (!eventType) throw new HttpError({ statusCode: 404, message: "eventType.notFound" }); + const isTeamEventType = + eventType.schedulingType === SchedulingType.COLLECTIVE || + eventType.schedulingType === SchedulingType.ROUND_ROBIN; + const paymentAppData = getPaymentAppData(eventType); // Check if required custom inputs exist @@ -495,12 +499,23 @@ async function handler(req: NextApiRequest & { userId?: number | undefined }) { language: { translate: tAttendees, locale: language ?? "en" }, }, ]; - const guests = (reqBody.guests || []).map((guest) => ({ - email: guest, - name: "", - timeZone: reqBody.timeZone, - language: { translate: tGuests, locale: "en" }, - })); + + const guests = (reqBody.guests || []).reduce((guestArray, guest) => { + // If it's a team event, remove the team member from guests + if (isTeamEventType) { + if (users.some((user) => user.email === guest)) { + return guestArray; + } else { + guestArray.push({ + email: guest, + name: "", + timeZone: reqBody.timeZone, + language: { translate: tGuests, locale: "en" }, + }); + } + } + return guestArray; + }, [] as typeof invitee); const seed = `${organizerUser.username}:${dayjs(reqBody.start).utc().format()}:${new Date().getTime()}`; const uid = translator.fromUUID(uuidv5(seed, uuidv5.URL)); @@ -539,7 +554,7 @@ async function handler(req: NextApiRequest & { userId?: number | undefined }) { const teamMembers = await Promise.all(teamMemberPromises); - const attendeesList = [...invitee, ...guests, ...teamMembers]; + const attendeesList = [...invitee, ...guests]; const eventNameObject = { attendeeName: reqBody.name || "Nameless", @@ -720,11 +735,11 @@ async function handler(req: NextApiRequest & { userId?: number | undefined }) { }); } - if (eventType.schedulingType === SchedulingType.COLLECTIVE) { + if (isTeamEventType) { evt.team = { - members: users.map((user) => user.name || user.username || "Nameless"), + members: teamMembers, name: eventType.team?.name || "Nameless", - }; // used for invitee emails + }; } if (reqBody.recurringEventId && eventType.recurringEvent) { @@ -810,17 +825,30 @@ async function handler(req: NextApiRequest & { userId?: number | undefined }) { metadata: reqBody.metadata, attendees: { createMany: { - data: evt.attendees.map((attendee) => { - //if attendee is team member, it should fetch their locale not booker's locale - //perhaps make email fetch request to see if his locale is stored, else - const retObj = { - name: attendee.name, - email: attendee.email, - timeZone: attendee.timeZone, - locale: attendee.language.locale, - }; - return retObj; - }), + data: [ + ...evt.attendees.map((attendee) => { + //if attendee is team member, it should fetch their locale not booker's locale + //perhaps make email fetch request to see if his locale is stored, else + const retObj = { + name: attendee.name, + email: attendee.email, + timeZone: attendee.timeZone, + locale: attendee.language.locale, + }; + return retObj; + }), + // Have this for now until we change the relationship between bookings & team members + ...(evt.team?.members + ? evt.team.members.map((member) => { + return { + email: member.email, + name: member.name, + timeZone: member.timeZone, + locale: member.language.locale, + }; + }) + : []), + ], }, }, dynamicEventSlugRef, diff --git a/packages/lib/CalEventParser.ts b/packages/lib/CalEventParser.ts index 5d18c8769e..e48423e4fc 100644 --- a/packages/lib/CalEventParser.ts +++ b/packages/lib/CalEventParser.ts @@ -46,9 +46,18 @@ ${calEvent.organizer.name} - ${calEvent.organizer.language.translate("organizer" ${calEvent.organizer.email} `; + const teamMembers = calEvent.team?.members + ? calEvent.team.members.map((member) => { + return ` +${member.name} - ${calEvent.organizer.language.translate("team_member")} +${member.email} + `; + }) + : []; + return ` ${calEvent.organizer.language.translate("who")}: -${organizer + attendees} +${organizer + attendees + teamMembers.join("")} `; }; diff --git a/packages/types/Calendar.d.ts b/packages/types/Calendar.d.ts index 6d33ae2b3a..9eea7bc2e0 100644 --- a/packages/types/Calendar.d.ts +++ b/packages/types/Calendar.d.ts @@ -27,6 +27,13 @@ export type Person = { locale?: string; }; +export type TeamMember = { + name: string; + email: string; + timeZone: string; + language: { translate: TFunction; locale: string }; +}; + export type EventBusyDate = { start: Date | string; end: Date | string; @@ -137,7 +144,7 @@ export interface CalendarEvent { description?: string | null; team?: { name: string; - members: string[]; + members: TeamMember[]; }; location?: string | null; conferenceData?: ConferenceData;