Fix - add team members to emails (#7207)

* On booking add team members & translation

* Add team members to round robin create

* Only update calendars on reschedule if there is a calendar reference

* Send email on reschedules

* Send team email on cancelled event

* Add team members to calendar event description

* Clean up

* Convert other emails to organizer & teams

* Type check fixes

* More type fixes

* Change organizer scheduled input to an object

* early return updateCalendarEvent

* Introduce team member type

* Fix type errors

* Put team members before attendees

* Remove lodash cloneDeep

* Update packages/core/EventManager.ts

Co-authored-by: Omar López <zomars@me.com>

* Remove booking select object

* Revert "Remove booking select object"

This reverts commit 9f121ff4eb.

* Refactor email manager (#7270)

Co-authored-by: zomars <zomars@me.com>

* Type change

* Remove conditional check for updateAllCalendarEvents

---------

Co-authored-by: zomars <zomars@me.com>
This commit is contained in:
Joe Au-Yeung 2023-02-27 15:45:40 -05:00 committed by zomars
parent c7d22a851c
commit 50be9255cb
23 changed files with 267 additions and 349 deletions

View File

@ -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",

View File

@ -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;

View File

@ -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;

View File

@ -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<unknown>[] = [];
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<unknown>[] = [];
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<unknown>[] = [];
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<unknown>[] = [];
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<unknown>[] = [];
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<unknown>[] = [];
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<unknown>[] = [];
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<unknown>[] = [];
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<unknown>[] = [];
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));
};

View File

@ -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) => (
<PersonInfo key={member.name} name={member.name} role={t("team_member")} email={member.email} />
))}
{props.calEvent.attendees.map((attendee) => (
<PersonInfo
key={attendee.id || attendee.name}

View File

@ -7,6 +7,7 @@ export const OrganizerScheduledEmail = (
calEvent: CalendarEvent;
attendee: Person;
newSeat?: boolean;
teamMember?: Person;
} & Partial<React.ComponentProps<typeof BaseScheduledEmail>>
) => {
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 (
<BaseScheduledEmail
timeZone={props.calEvent.organizer.timeZone}
timeZone={props.teamMember?.timeZone || props.calEvent.organizer.timeZone}
t={t}
subject={t(subject)}
title={t(title)}

View File

@ -1,6 +1,7 @@
import nodemailer from "nodemailer";
import dayjs, { Dayjs } from "@calcom/dayjs";
import type { Dayjs } from "@calcom/dayjs";
import dayjs from "@calcom/dayjs";
import { getErrorFromUnknown } from "@calcom/lib/errors";
import { serverConfig } from "@calcom/lib/serverConfig";

View File

@ -5,15 +5,7 @@ import AttendeeScheduledEmail from "./attendee-scheduled-email";
export default class AttendeeRequestEmail extends AttendeeScheduledEmail {
protected getNodeMailerPayload(): Record<string, unknown> {
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}>`,

View File

@ -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",
});

View File

@ -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<string, unknown> {

View File

@ -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<string, unknown> {
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}>`,

View File

@ -5,15 +5,7 @@ import OrganizerScheduledEmail from "./organizer-scheduled-email";
export default class OrganizerCancelledEmail extends OrganizerScheduledEmail {
protected getNodeMailerPayload(): Record<string, unknown> {
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}>`,

View File

@ -5,15 +5,7 @@ import OrganizerScheduledEmail from "./organizer-scheduled-email";
export default class OrganizerLocationChangeEmail extends OrganizerScheduledEmail {
protected getNodeMailerPayload(): Record<string, unknown> {
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: {

View File

@ -5,15 +5,7 @@ import OrganizerScheduledEmail from "./organizer-scheduled-email";
export default class OrganizerPaymentRefundFailedEmail extends OrganizerScheduledEmail {
protected getNodeMailerPayload(): Record<string, unknown> {
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}>`,

View File

@ -5,15 +5,7 @@ import OrganizerScheduledEmail from "./organizer-scheduled-email";
export default class OrganizerRequestEmail extends OrganizerScheduledEmail {
protected getNodeMailerPayload(): Record<string, unknown> {
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}>`,

View File

@ -5,15 +5,7 @@ import OrganizerRequestEmail from "./organizer-request-email";
export default class OrganizerRequestReminderEmail extends OrganizerRequestEmail {
protected getNodeMailerPayload(): Record<string, unknown> {
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}>`,

View File

@ -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<string, unknown> {

View File

@ -5,15 +5,7 @@ import OrganizerScheduledEmail from "./organizer-scheduled-email";
export default class OrganizerRescheduledEmail extends OrganizerScheduledEmail {
protected getNodeMailerPayload(): Record<string, unknown> {
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: {

View File

@ -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<string, unknown> {
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(),

View File

@ -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

View File

@ -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,

View File

@ -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("")}
`;
};

View File

@ -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;