Broken Integration Alarm System (#3135)
* Create broken integration email * Send video on broken video link * Add translations * Add Trans components * Remove calendars file * Type fixes * Add translations * Fix type errors * Fix Spanish translation Co-authored-by: Omar López <zomars@me.com> * Remove iCal event * Locale fixes * Clean up trans tag * Apply suggested translation Co-authored-by: Bailey Pumfleet <pumfleet@hey.com> * Change email to en * Fix type error * Type fix * Fix e2e * Fix e2e * Fix e2e * Fix e2e * Fix e2e * Update webhookResponse-chromium.txt Co-authored-by: Omar López <zomars@me.com> Co-authored-by: Bailey Pumfleet <pumfleet@hey.com>
This commit is contained in:
co-authored by
Omar López
Bailey Pumfleet
parent
fd863b2ae7
commit
02e5ec37e7
+1
-1
Submodule apps/console updated: bf9484c64b...3fe93861ba
@@ -104,6 +104,7 @@ async function patchHandler(req: NextApiRequest) {
|
||||
eventTypeId: true,
|
||||
eventType: {
|
||||
select: {
|
||||
id: true,
|
||||
recurringEvent: true,
|
||||
requiresConfirmation: true,
|
||||
},
|
||||
@@ -178,6 +179,7 @@ async function patchHandler(req: NextApiRequest) {
|
||||
uid: booking.uid,
|
||||
destinationCalendar: booking?.destinationCalendar || currentUser.destinationCalendar,
|
||||
requiresConfirmation: booking?.eventType?.requiresConfirmation ?? false,
|
||||
eventTypeId: booking.eventType?.id,
|
||||
};
|
||||
|
||||
const recurringEvent = parseRecurringEvent(booking.eventType?.recurringEvent);
|
||||
|
||||
@@ -403,6 +403,7 @@ async function handler(req: NextApiRequest) {
|
||||
destinationCalendar: eventType.destinationCalendar || organizerUser.destinationCalendar,
|
||||
hideCalendarNotes: eventType.hideCalendarNotes,
|
||||
requiresConfirmation: eventType.requiresConfirmation ?? false,
|
||||
eventTypeId: eventType.id,
|
||||
};
|
||||
|
||||
if (eventType.schedulingType === SchedulingType.COLLECTIVE) {
|
||||
@@ -803,6 +804,7 @@ async function handler(req: NextApiRequest) {
|
||||
bookingId,
|
||||
rescheduleUid,
|
||||
metadata: reqBody.metadata,
|
||||
eventTypeId,
|
||||
}).catch((e) => {
|
||||
console.error(`Error executing webhook for event: ${eventTrigger}, URL: ${sub.subscriberUrl}`, e);
|
||||
})
|
||||
|
||||
@@ -6,6 +6,7 @@ import { getTranslation } from "@calcom/lib/server/i18n";
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
if (process.env.NODE_ENV !== "development") return res.write("Only for development purposes"), res.end();
|
||||
const t = await getTranslation("en", "common");
|
||||
const language = { translate: t, locale: "en" };
|
||||
|
||||
const evt = {
|
||||
type: "30min",
|
||||
@@ -22,14 +23,14 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
name: "Pro Example",
|
||||
email: "pro@example.com",
|
||||
timeZone: "Europe/London",
|
||||
language: { translate: t, locale: "en" },
|
||||
language,
|
||||
},
|
||||
attendees: [
|
||||
{
|
||||
email: "pro@example.com",
|
||||
name: "pro@example.com",
|
||||
timeZone: "America/Chihuahua",
|
||||
language: { translate: t, locale: "en" },
|
||||
language,
|
||||
},
|
||||
],
|
||||
location: "Zoom video",
|
||||
|
||||
@@ -76,6 +76,7 @@ test.describe("Integrations", () => {
|
||||
body.payload.bookingId = dynamic;
|
||||
body.payload.additionalInformation = dynamic;
|
||||
body.payload.requiresConfirmation = dynamic;
|
||||
body.payload.eventTypeId = dynamic;
|
||||
|
||||
// if we change the shape of our webhooks, we can simply update this by clicking `u`
|
||||
// console.log("BODY", body);
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"triggerEvent":"BOOKING_CREATED","createdAt":"[redacted/dynamic]","payload":{"type":"30 min","title":"30 min between PRO and Test Testson","description":"","additionalNotes":"","customInputs":{},"startTime":"[redacted/dynamic]","endTime":"[redacted/dynamic]","organizer":{"name":"PRO","email":"[redacted/dynamic]","timeZone":"[redacted/dynamic]","language":"[redacted/dynamic]"},"attendees":[{"email":"test@example.com","name":"Test Testson","timeZone":"[redacted/dynamic]","language":"[redacted/dynamic]"}],"location":"[redacted/dynamic]","destinationCalendar":null,"hideCalendarNotes":false,"requiresConfirmation":"[redacted/dynamic]","uid":"[redacted/dynamic]","bookingId":"[redacted/dynamic]","metadata":{},"additionalInformation":"[redacted/dynamic]"}}
|
||||
{"triggerEvent":"BOOKING_CREATED","createdAt":"[redacted/dynamic]","payload":{"type":"30 min","title":"30 min between PRO and Test Testson","description":"","additionalNotes":"","customInputs":{},"startTime":"[redacted/dynamic]","endTime":"[redacted/dynamic]","organizer":{"name":"PRO","email":"[redacted/dynamic]","timeZone":"[redacted/dynamic]","language":"[redacted/dynamic]"},"attendees":[{"email":"test@example.com","name":"Test Testson","timeZone":"[redacted/dynamic]","language":"[redacted/dynamic]"}],"location":"[redacted/dynamic]","destinationCalendar":null,"hideCalendarNotes":false,"requiresConfirmation":"[redacted/dynamic]","eventTypeId":"[redacted/dynamic]","uid":"[redacted/dynamic]","bookingId":"[redacted/dynamic]","metadata":{},"additionalInformation":"[redacted/dynamic]"}}
|
||||
@@ -911,5 +911,10 @@
|
||||
"add_exchange2016": "Connect Exchange 2016 Server",
|
||||
"specific_issue": "Have a specific issue",
|
||||
"browse_our_docs": "browse our docs",
|
||||
"attendee_name": "Attendee's name"
|
||||
"broken_video_action": "We could not add the <1>{{location}}</1> meeting link to your scheduled event. Contact your invitees or update your calendar event to add the details. You can either <3> change your location on the event type </3> or try <5>removing and adding the app again.</5>",
|
||||
"broken_calendar_action": "We could not update your <1>{{calendar}}</1>. <2> Please check your calendar settings or remove and add your calendar again </2>",
|
||||
"attendee_name": "Attendee's name",
|
||||
"broken_integration": "Broken integration",
|
||||
"problem_adding_video_link": "There was a problem adding a video link",
|
||||
"problem_updating_calendar": "There was a problem updating your calendar"
|
||||
}
|
||||
|
||||
@@ -903,5 +903,8 @@
|
||||
"requires_confirmation": "Requiere confirmación",
|
||||
"add_exchange2013": "Conectar con Exchange 2013 Server",
|
||||
"add_exchange2016": "Conectar con Exchange 2016 Server",
|
||||
"broken_video_action": "No pudimos agregar el enlace de <1>{{location}}</1> a tu evento agendado. Contacta a tus invitados o actualiza tu calendario para agregar los detalles. Puedes <3>cambiar la ubicación de tu tipo de evento</3> o <5>remover la App y agregarla de nuevo.</5>",
|
||||
"problem_adding_video_link": "Hubo un problema al añadir un enlace de vídeo",
|
||||
"problem_updating_calendar": "Hubo un problema al actualizar su calendario",
|
||||
"attendee_name": "Nombre del asistente"
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import cache from "memory-cache";
|
||||
|
||||
import { getCalendar } from "@calcom/app-store/_utils/getCalendar";
|
||||
import getApps from "@calcom/app-store/utils";
|
||||
import { sendBrokenIntegrationEmail } from "@calcom/emails";
|
||||
import { getUid } from "@calcom/lib/CalEventParser";
|
||||
import { getErrorFromUnknown } from "@calcom/lib/errors";
|
||||
import logger from "@calcom/lib/logger";
|
||||
@@ -141,7 +142,8 @@ export const createEvent = async (
|
||||
|
||||
// TODO: Surfice success/error messages coming from apps to improve end user visibility
|
||||
const creationResult = calendar
|
||||
? await calendar.createEvent(calEvent).catch((e) => {
|
||||
? await calendar.createEvent(calEvent).catch(async (e) => {
|
||||
await sendBrokenIntegrationEmail(calEvent, "calendar");
|
||||
log.error("createEvent failed", e, calEvent);
|
||||
success = false;
|
||||
return undefined;
|
||||
@@ -177,7 +179,8 @@ export const updateEvent = async (
|
||||
success = true;
|
||||
return event;
|
||||
})
|
||||
.catch((e) => {
|
||||
.catch(async (e) => {
|
||||
await sendBrokenIntegrationEmail(calEvent, "calendar");
|
||||
log.error("updateEvent failed", e, calEvent);
|
||||
return undefined;
|
||||
})
|
||||
|
||||
@@ -3,6 +3,7 @@ import short from "short-uuid";
|
||||
import { v5 as uuidv5 } from "uuid";
|
||||
|
||||
import appStore from "@calcom/app-store";
|
||||
import { sendBrokenIntegrationEmail } from "@calcom/emails";
|
||||
import { getUid } from "@calcom/lib/CalEventParser";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import type { CalendarEvent } from "@calcom/types/Calendar";
|
||||
@@ -43,7 +44,8 @@ const createMeeting = async (credential: Credential, calEvent: CalendarEvent) =>
|
||||
|
||||
const videoAdapters = getVideoAdapters([credential]);
|
||||
const [firstVideoAdapter] = videoAdapters;
|
||||
const createdMeeting = await firstVideoAdapter.createMeeting(calEvent).catch((e) => {
|
||||
const createdMeeting = await firstVideoAdapter.createMeeting(calEvent).catch(async (e) => {
|
||||
await sendBrokenIntegrationEmail(calEvent, "video");
|
||||
log.error("createMeeting failed", e, calEvent);
|
||||
});
|
||||
|
||||
@@ -77,7 +79,8 @@ const updateMeeting = async (
|
||||
const [firstVideoAdapter] = getVideoAdapters([credential]);
|
||||
const updatedMeeting =
|
||||
credential && bookingRef
|
||||
? await firstVideoAdapter.updateMeeting(bookingRef, calEvent).catch((e) => {
|
||||
? await firstVideoAdapter.updateMeeting(bookingRef, calEvent).catch(async (e) => {
|
||||
await sendBrokenIntegrationEmail(calEvent, "video");
|
||||
log.error("updateMeeting failed", e, calEvent);
|
||||
success = false;
|
||||
return undefined;
|
||||
|
||||
@@ -8,6 +8,7 @@ import AttendeeRequestEmail from "./templates/attendee-request-email";
|
||||
import AttendeeRequestRescheduledEmail from "./templates/attendee-request-reschedule-email";
|
||||
import AttendeeRescheduledEmail from "./templates/attendee-rescheduled-email";
|
||||
import AttendeeScheduledEmail from "./templates/attendee-scheduled-email";
|
||||
import BrokenIntegrationEmail from "./templates/broken-integration-email";
|
||||
import FeedbackEmail, { Feedback } from "./templates/feedback-email";
|
||||
import ForgotPasswordEmail, { PasswordReset } from "./templates/forgot-password-email";
|
||||
import OrganizerCancelledEmail from "./templates/organizer-cancelled-email";
|
||||
@@ -284,3 +285,14 @@ export const sendFeedbackEmail = async (feedback: 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));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -20,7 +20,7 @@ export const BaseEmailHtml = (props: {
|
||||
callToAction?: React.ReactNode;
|
||||
subject: string;
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
subtitle?: React.ReactNode;
|
||||
headerType?: BodyHeadType;
|
||||
}) => {
|
||||
return (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import EmailCommonDivider from "./EmailCommonDivider";
|
||||
|
||||
const EmailScheduledBodyHeaderContent = (props: { title: string; subtitle?: string }) => (
|
||||
const EmailScheduledBodyHeaderContent = (props: { title: string; subtitle?: React.ReactNode }) => (
|
||||
<EmailCommonDivider headStyles={{ padding: 0 }} mutipleRows>
|
||||
<tr>
|
||||
<td
|
||||
|
||||
@@ -5,12 +5,16 @@ export const AttendeeRequestEmail = (props: React.ComponentProps<typeof Attendee
|
||||
title={props.calEvent.organizer.language.translate(
|
||||
props.calEvent.recurringEvent?.count ? "booking_submitted_recurring" : "booking_submitted"
|
||||
)}
|
||||
subtitle={props.calEvent.organizer.language.translate(
|
||||
props.calEvent.recurringEvent?.count
|
||||
? "user_needs_to_confirm_or_reject_booking_recurring"
|
||||
: "user_needs_to_confirm_or_reject_booking",
|
||||
{ user: props.calEvent.organizer.name }
|
||||
)}
|
||||
subtitle={
|
||||
<>
|
||||
{props.calEvent.organizer.language.translate(
|
||||
props.calEvent.recurringEvent?.count
|
||||
? "user_needs_to_confirm_or_reject_booking_recurring"
|
||||
: "user_needs_to_confirm_or_reject_booking",
|
||||
{ user: props.calEvent.organizer.name }
|
||||
)}
|
||||
</>
|
||||
}
|
||||
headerType="calendarCircle"
|
||||
subject="booking_submitted_subject"
|
||||
{...props}
|
||||
|
||||
@@ -6,9 +6,13 @@ export const AttendeeRequestRescheduledEmail = (
|
||||
) => (
|
||||
<OrganizerScheduledEmail
|
||||
title="request_reschedule_title_attendee"
|
||||
subtitle={props.calEvent.organizer.language.translate("request_reschedule_subtitle", {
|
||||
organizer: props.calEvent.organizer.name,
|
||||
})}
|
||||
subtitle={
|
||||
<>
|
||||
{props.calEvent.organizer.language.translate("request_reschedule_subtitle", {
|
||||
organizer: props.calEvent.organizer.name,
|
||||
})}
|
||||
</>
|
||||
}
|
||||
headerType="calendarCircle"
|
||||
subject="rescheduled_event_type_subject"
|
||||
callToAction={
|
||||
|
||||
@@ -64,7 +64,7 @@ export const BaseScheduledEmail = (
|
||||
? null
|
||||
: props.callToAction || <ManageLink attendee={props.attendee} calEvent={props.calEvent} />
|
||||
}
|
||||
subtitle={t(props.subtitle || "emailed_you_and_any_other_attendees")}>
|
||||
subtitle={props.subtitle || <>{t("emailed_you_and_any_other_attendees")}</>}>
|
||||
<Info label={t("cancellation_reason")} description={props.calEvent.cancellationReason} withSpacer />
|
||||
<Info label={t("rejection_reason")} description={props.calEvent.rejectionReason} withSpacer />
|
||||
<Info label={t("what")} description={props.calEvent.type} withSpacer />
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
import { TFunction } from "next-i18next";
|
||||
import { Trans } from "react-i18next";
|
||||
|
||||
import { AppStoreLocationType } from "@calcom/app-store/locations";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import type { CalendarEvent, Person } from "@calcom/types/Calendar";
|
||||
|
||||
import { BaseScheduledEmail } from "./BaseScheduledEmail";
|
||||
|
||||
// https://stackoverflow.com/questions/56263980/get-key-of-an-enum-from-its-value-in-typescript
|
||||
export function getEnumKeyByEnumValue(myEnum: any, enumValue: number | string): string {
|
||||
const keys = Object.keys(myEnum).filter((x) => myEnum[x] == enumValue);
|
||||
return keys.length > 0 ? keys[0] : "";
|
||||
}
|
||||
|
||||
const BrokenVideoIntegration = (props: { location: string; eventTypeId?: number | null; t: TFunction }) => {
|
||||
return (
|
||||
<Trans i18nKey="broken_video_action" t={props.t}>
|
||||
We could not add the <span>{props.location}</span> meeting link to your scheduled event. Contact your
|
||||
invitees or update your calendar event to add the details. You can either
|
||||
<a
|
||||
href={
|
||||
props.eventTypeId ? `${WEBAPP_URL}/event-types/${props.eventTypeId}` : `${WEBAPP_URL}/event-types`
|
||||
}>
|
||||
change your location on the event type
|
||||
</a>
|
||||
or try
|
||||
<a href={`${WEBAPP_URL}/apps/installed`}>removing and adding the app again.</a>
|
||||
</Trans>
|
||||
);
|
||||
};
|
||||
|
||||
const BrokenCalendarIntegration = (props: {
|
||||
calendar: string;
|
||||
eventTypeId?: number | null;
|
||||
t: TFunction;
|
||||
}) => {
|
||||
const { t } = props;
|
||||
|
||||
return (
|
||||
<Trans i18nKey="broken_calendar_action" t={props.t}>
|
||||
We could not update your <span>{props.calendar}</span>.{" "}
|
||||
<a href={`${WEBAPP_URL}/apps/installed`}>
|
||||
Please check your calendar settings or remove and add your calendar again
|
||||
</a>
|
||||
</Trans>
|
||||
);
|
||||
};
|
||||
|
||||
export const BrokenIntegrationEmail = (
|
||||
props: {
|
||||
calEvent: CalendarEvent;
|
||||
attendee: Person;
|
||||
type: "video" | "calendar";
|
||||
} & Partial<React.ComponentProps<typeof BaseScheduledEmail>>
|
||||
) => {
|
||||
const { calEvent, type } = props;
|
||||
const t = calEvent.organizer.language.translate;
|
||||
|
||||
if (type === "video") {
|
||||
let location = calEvent.location ? getEnumKeyByEnumValue(AppStoreLocationType, calEvent.location) : " ";
|
||||
|
||||
if (location === "Daily") {
|
||||
location = "Cal Video";
|
||||
}
|
||||
if (location === "GoogleMeet") {
|
||||
location = location.slice(0, 5) + " " + location.slice(5);
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseScheduledEmail
|
||||
timeZone={calEvent.organizer.timeZone}
|
||||
t={t}
|
||||
subject={t("broken_integration")}
|
||||
title={t("problem_adding_video_link")}
|
||||
subtitle={<BrokenVideoIntegration location={location} eventTypeId={calEvent.eventTypeId} t={t} />}
|
||||
headerType="xCircle"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (type === "calendar") {
|
||||
// The calendar name is stored as name_calendar
|
||||
let calendar = calEvent.destinationCalendar
|
||||
? calEvent.destinationCalendar?.integration.split("_")
|
||||
: "calendar";
|
||||
|
||||
if (Array.isArray(calendar)) {
|
||||
const calendarCap = calendar.map((name) => name.charAt(0).toUpperCase() + name.slice(1));
|
||||
calendar = calendarCap[0] + " " + calendarCap[1];
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseScheduledEmail
|
||||
timeZone={calEvent.organizer.timeZone}
|
||||
t={t}
|
||||
subject={t("broken_integration")}
|
||||
title={t("problem_updating_calendar")}
|
||||
subtitle={<BrokenCalendarIntegration calendar={calendar} eventTypeId={calEvent.eventTypeId} t={t} />}
|
||||
headerType="xCircle"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseScheduledEmail
|
||||
timeZone={calEvent.organizer.timeZone}
|
||||
t={t}
|
||||
subject={t("broken_integration")}
|
||||
title={t("problem_updating_calendar")}
|
||||
headerType="xCircle"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -12,9 +12,13 @@ export const OrganizerPaymentRefundFailedEmail = (
|
||||
subject="refund_failed_subject"
|
||||
title={t("a_refund_failed")}
|
||||
callToAction={null}
|
||||
subtitle={t("check_with_provider_and_user", {
|
||||
user: props.calEvent.attendees[0].name,
|
||||
})}>
|
||||
subtitle={
|
||||
<>
|
||||
{t("check_with_provider_and_user", {
|
||||
user: props.calEvent.attendees[0].name,
|
||||
})}
|
||||
</>
|
||||
}>
|
||||
<RefundInformation {...props} />
|
||||
</BaseEmailHtml>
|
||||
);
|
||||
|
||||
@@ -7,9 +7,13 @@ export const OrganizerRequestRescheduledEmail = (
|
||||
title={props.calEvent.organizer.language.translate("request_reschedule_title_organizer", {
|
||||
attendee: props.calEvent.attendees[0].name,
|
||||
})}
|
||||
subtitle={props.calEvent.organizer.language.translate("request_reschedule_subtitle_organizer", {
|
||||
attendee: props.calEvent.attendees[0].name,
|
||||
})}
|
||||
subtitle={
|
||||
<>
|
||||
{props.calEvent.organizer.language.translate("request_reschedule_subtitle_organizer", {
|
||||
attendee: props.calEvent.attendees[0].name,
|
||||
})}
|
||||
</>
|
||||
}
|
||||
headerType="calendarCircle"
|
||||
subject="rescheduled_event_type_subject"
|
||||
{...props}
|
||||
|
||||
@@ -17,3 +17,4 @@ export { OrganizerRequestRescheduledEmail } from "./OrganizerRequestRescheduledE
|
||||
export { OrganizerRescheduledEmail } from "./OrganizerRescheduledEmail";
|
||||
export { OrganizerScheduledEmail } from "./OrganizerScheduledEmail";
|
||||
export { TeamInviteEmail } from "./TeamInviteEmail";
|
||||
export { BrokenIntegrationEmail } from "./BrokenIntegrationEmail";
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
import dayjs from "dayjs";
|
||||
import localizedFormat from "dayjs/plugin/localizedFormat";
|
||||
import timezone from "dayjs/plugin/timezone";
|
||||
import toArray from "dayjs/plugin/toArray";
|
||||
import utc from "dayjs/plugin/utc";
|
||||
import { createEvent, DateArray, Person } from "ics";
|
||||
import { TFunction } from "next-i18next";
|
||||
import rrule from "rrule";
|
||||
|
||||
import { getRichDescription } from "@calcom/lib/CalEventParser";
|
||||
import type { CalendarEvent } from "@calcom/types/Calendar";
|
||||
|
||||
import { renderEmail } from "..";
|
||||
import BaseEmail from "./_base-email";
|
||||
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(timezone);
|
||||
dayjs.extend(localizedFormat);
|
||||
dayjs.extend(toArray);
|
||||
|
||||
export default class BrokenIntegrationEmail extends BaseEmail {
|
||||
type: "calendar" | "video";
|
||||
calEvent: CalendarEvent;
|
||||
t: TFunction;
|
||||
|
||||
constructor(calEvent: CalendarEvent, type: "calendar" | "video") {
|
||||
super();
|
||||
this.name = "SEND_BROKEN_INTEGRATION";
|
||||
this.calEvent = calEvent;
|
||||
this.t = this.calEvent.organizer.language.translate;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
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: `Cal.com <${this.getMailerOptions().from}>`,
|
||||
to: toAddresses.join(","),
|
||||
subject: `[Action Required] ${this.t("confirmed_event_type_subject", {
|
||||
eventType: this.calEvent.type,
|
||||
name: this.calEvent.attendees[0].name,
|
||||
date: this.getFormattedDate(),
|
||||
})}`,
|
||||
html: renderEmail("BrokenIntegrationEmail", {
|
||||
calEvent: this.calEvent,
|
||||
attendee: this.calEvent.organizer,
|
||||
type: this.type,
|
||||
}),
|
||||
text: this.getTextBody(),
|
||||
};
|
||||
}
|
||||
|
||||
protected getTextBody(
|
||||
title = "",
|
||||
subtitle = "emailed_you_and_any_other_attendees",
|
||||
extraInfo = "",
|
||||
callToAction = ""
|
||||
): string {
|
||||
return `
|
||||
${this.t(
|
||||
title || this.calEvent.recurringEvent?.count ? "new_event_scheduled_recurring" : "new_event_scheduled"
|
||||
)}
|
||||
${this.t(subtitle)}
|
||||
${extraInfo}
|
||||
${getRichDescription(this.calEvent)}
|
||||
${callToAction}
|
||||
`.trim();
|
||||
}
|
||||
|
||||
protected getTimezone(): string {
|
||||
return this.calEvent.organizer.timeZone;
|
||||
}
|
||||
|
||||
protected getOrganizerStart(format: string) {
|
||||
return this.getRecipientTime(this.calEvent.startTime, format);
|
||||
}
|
||||
|
||||
protected getOrganizerEnd(format: string) {
|
||||
return this.getRecipientTime(this.calEvent.endTime, format);
|
||||
}
|
||||
|
||||
protected getFormattedDate() {
|
||||
return `${this.getOrganizerStart("h:mma")} - ${this.getOrganizerEnd("h:mma")}, ${this.t(
|
||||
this.getOrganizerStart("dddd").toLowerCase()
|
||||
)}, ${this.t(this.getOrganizerStart("MMMM").toLowerCase())} ${this.getOrganizerStart("D, YYYY")}`;
|
||||
}
|
||||
}
|
||||
Vendored
+1
@@ -123,6 +123,7 @@ export interface CalendarEvent {
|
||||
hideCalendarNotes?: boolean;
|
||||
recurrence?: string;
|
||||
recurringEvent?: RecurringEvent | null;
|
||||
eventTypeId?: number | null;
|
||||
}
|
||||
|
||||
export interface EntryPoint {
|
||||
|
||||
Reference in New Issue
Block a user