From 02e5ec37e72f980a42d8270d3c87c9b7ce7f1c06 Mon Sep 17 00:00:00 2001 From: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com> Date: Sat, 25 Jun 2022 01:16:20 -0400 Subject: [PATCH] Broken Integration Alarm System (#3135) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 * Remove iCal event * Locale fixes * Clean up trans tag * Apply suggested translation Co-authored-by: Bailey Pumfleet * 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 Co-authored-by: Bailey Pumfleet --- apps/console | 2 +- apps/web/pages/api/book/confirm.ts | 2 + apps/web/pages/api/book/event.ts | 2 + apps/web/pages/api/email.ts | 5 +- apps/web/playwright/integrations.test.ts | 1 + .../webhookResponse-chromium.txt | 2 +- apps/web/public/static/locales/en/common.json | 7 +- apps/web/public/static/locales/es/common.json | 3 + packages/core/CalendarManager.ts | 7 +- packages/core/videoClient.ts | 7 +- packages/emails/email-manager.ts | 12 ++ .../emails/src/components/BaseEmailHtml.tsx | 2 +- .../EmailScheduledBodyHeaderContent.tsx | 2 +- .../src/templates/AttendeeRequestEmail.tsx | 16 ++- .../AttendeeRequestRescheduledEmail.tsx | 10 +- .../src/templates/BaseScheduledEmail.tsx | 2 +- .../src/templates/BrokenIntegrationEmail.tsx | 117 ++++++++++++++++++ .../OrganizerPaymentRefundFailedEmail.tsx | 10 +- .../OrganizerRequestRescheduledEmail.tsx | 10 +- packages/emails/src/templates/index.ts | 1 + .../templates/broken-integration-email.ts | 96 ++++++++++++++ packages/types/Calendar.d.ts | 1 + 22 files changed, 290 insertions(+), 27 deletions(-) create mode 100644 packages/emails/src/templates/BrokenIntegrationEmail.tsx create mode 100644 packages/emails/templates/broken-integration-email.ts diff --git a/apps/console b/apps/console index bf9484c64b..3fe93861ba 160000 --- a/apps/console +++ b/apps/console @@ -1 +1 @@ -Subproject commit bf9484c64bab7c23e994086a45fbbb0c21cf4a7f +Subproject commit 3fe93861ba0567f75dcad8f949f46795e887194f diff --git a/apps/web/pages/api/book/confirm.ts b/apps/web/pages/api/book/confirm.ts index 204c99a9bb..ee925a3d39 100644 --- a/apps/web/pages/api/book/confirm.ts +++ b/apps/web/pages/api/book/confirm.ts @@ -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); diff --git a/apps/web/pages/api/book/event.ts b/apps/web/pages/api/book/event.ts index db49907feb..d6051935b5 100644 --- a/apps/web/pages/api/book/event.ts +++ b/apps/web/pages/api/book/event.ts @@ -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); }) diff --git a/apps/web/pages/api/email.ts b/apps/web/pages/api/email.ts index 193978b31d..57c81afa61 100644 --- a/apps/web/pages/api/email.ts +++ b/apps/web/pages/api/email.ts @@ -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", diff --git a/apps/web/playwright/integrations.test.ts b/apps/web/playwright/integrations.test.ts index b078d9dc35..cf641886d7 100644 --- a/apps/web/playwright/integrations.test.ts +++ b/apps/web/playwright/integrations.test.ts @@ -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); diff --git a/apps/web/playwright/integrations.test.ts-snapshots/webhookResponse-chromium.txt b/apps/web/playwright/integrations.test.ts-snapshots/webhookResponse-chromium.txt index 283a523db6..e295671895 100644 --- a/apps/web/playwright/integrations.test.ts-snapshots/webhookResponse-chromium.txt +++ b/apps/web/playwright/integrations.test.ts-snapshots/webhookResponse-chromium.txt @@ -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]"}} \ No newline at end of file +{"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]"}} \ No newline at end of file diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index 84e3b47d3b..c563329e45 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -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}} 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 or try <5>removing and adding the app again.", + "broken_calendar_action": "We could not update your <1>{{calendar}}. <2> Please check your calendar settings or remove and add your calendar again ", + "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" } diff --git a/apps/web/public/static/locales/es/common.json b/apps/web/public/static/locales/es/common.json index 3f86f351cd..678fbef865 100644 --- a/apps/web/public/static/locales/es/common.json +++ b/apps/web/public/static/locales/es/common.json @@ -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}} 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 o <5>remover la App y agregarla de nuevo.", + "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" } diff --git a/packages/core/CalendarManager.ts b/packages/core/CalendarManager.ts index 86e996afb0..db4610944f 100644 --- a/packages/core/CalendarManager.ts +++ b/packages/core/CalendarManager.ts @@ -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; }) diff --git a/packages/core/videoClient.ts b/packages/core/videoClient.ts index 9f6e4e736a..83c6e49822 100644 --- a/packages/core/videoClient.ts +++ b/packages/core/videoClient.ts @@ -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; diff --git a/packages/emails/email-manager.ts b/packages/emails/email-manager.ts index 12d264805a..add3d0f9e8 100644 --- a/packages/emails/email-manager.ts +++ b/packages/emails/email-manager.ts @@ -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)); + } + }); +}; diff --git a/packages/emails/src/components/BaseEmailHtml.tsx b/packages/emails/src/components/BaseEmailHtml.tsx index adeb44c7e5..80f0424e0b 100644 --- a/packages/emails/src/components/BaseEmailHtml.tsx +++ b/packages/emails/src/components/BaseEmailHtml.tsx @@ -20,7 +20,7 @@ export const BaseEmailHtml = (props: { callToAction?: React.ReactNode; subject: string; title?: string; - subtitle?: string; + subtitle?: React.ReactNode; headerType?: BodyHeadType; }) => { return ( diff --git a/packages/emails/src/components/EmailScheduledBodyHeaderContent.tsx b/packages/emails/src/components/EmailScheduledBodyHeaderContent.tsx index 8e36765a9d..13e8c8b663 100644 --- a/packages/emails/src/components/EmailScheduledBodyHeaderContent.tsx +++ b/packages/emails/src/components/EmailScheduledBodyHeaderContent.tsx @@ -1,6 +1,6 @@ import EmailCommonDivider from "./EmailCommonDivider"; -const EmailScheduledBodyHeaderContent = (props: { title: string; subtitle?: string }) => ( +const EmailScheduledBodyHeaderContent = (props: { title: string; subtitle?: React.ReactNode }) => ( + {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} diff --git a/packages/emails/src/templates/AttendeeRequestRescheduledEmail.tsx b/packages/emails/src/templates/AttendeeRequestRescheduledEmail.tsx index 67b29b5cd3..77cdba9237 100644 --- a/packages/emails/src/templates/AttendeeRequestRescheduledEmail.tsx +++ b/packages/emails/src/templates/AttendeeRequestRescheduledEmail.tsx @@ -6,9 +6,13 @@ export const AttendeeRequestRescheduledEmail = ( ) => ( + {props.calEvent.organizer.language.translate("request_reschedule_subtitle", { + organizer: props.calEvent.organizer.name, + })} + + } headerType="calendarCircle" subject="rescheduled_event_type_subject" callToAction={ diff --git a/packages/emails/src/templates/BaseScheduledEmail.tsx b/packages/emails/src/templates/BaseScheduledEmail.tsx index 2a15f83e29..882e673921 100644 --- a/packages/emails/src/templates/BaseScheduledEmail.tsx +++ b/packages/emails/src/templates/BaseScheduledEmail.tsx @@ -64,7 +64,7 @@ export const BaseScheduledEmail = ( ? null : props.callToAction || } - subtitle={t(props.subtitle || "emailed_you_and_any_other_attendees")}> + subtitle={props.subtitle || <>{t("emailed_you_and_any_other_attendees")}}> diff --git a/packages/emails/src/templates/BrokenIntegrationEmail.tsx b/packages/emails/src/templates/BrokenIntegrationEmail.tsx new file mode 100644 index 0000000000..58c1a5ef7f --- /dev/null +++ b/packages/emails/src/templates/BrokenIntegrationEmail.tsx @@ -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 ( + + We could not add the {props.location} meeting link to your scheduled event. Contact your + invitees or update your calendar event to add the details. You can either  + + change your location on the event type + +  or try  + removing and adding the app again. + + ); +}; + +const BrokenCalendarIntegration = (props: { + calendar: string; + eventTypeId?: number | null; + t: TFunction; +}) => { + const { t } = props; + + return ( + + We could not update your {props.calendar}.{" "} + + Please check your calendar settings or remove and add your calendar again + + + ); +}; + +export const BrokenIntegrationEmail = ( + props: { + calEvent: CalendarEvent; + attendee: Person; + type: "video" | "calendar"; + } & Partial> +) => { + 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 ( + } + 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 ( + } + headerType="xCircle" + {...props} + /> + ); + } + + return ( + + ); +}; diff --git a/packages/emails/src/templates/OrganizerPaymentRefundFailedEmail.tsx b/packages/emails/src/templates/OrganizerPaymentRefundFailedEmail.tsx index 5f9606ced2..3fc4938f44 100644 --- a/packages/emails/src/templates/OrganizerPaymentRefundFailedEmail.tsx +++ b/packages/emails/src/templates/OrganizerPaymentRefundFailedEmail.tsx @@ -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, + })} + + }> ); diff --git a/packages/emails/src/templates/OrganizerRequestRescheduledEmail.tsx b/packages/emails/src/templates/OrganizerRequestRescheduledEmail.tsx index f9169dea0b..07175867d3 100644 --- a/packages/emails/src/templates/OrganizerRequestRescheduledEmail.tsx +++ b/packages/emails/src/templates/OrganizerRequestRescheduledEmail.tsx @@ -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} diff --git a/packages/emails/src/templates/index.ts b/packages/emails/src/templates/index.ts index 697b3b1279..918f6b76e4 100644 --- a/packages/emails/src/templates/index.ts +++ b/packages/emails/src/templates/index.ts @@ -17,3 +17,4 @@ export { OrganizerRequestRescheduledEmail } from "./OrganizerRequestRescheduledE export { OrganizerRescheduledEmail } from "./OrganizerRescheduledEmail"; export { OrganizerScheduledEmail } from "./OrganizerScheduledEmail"; export { TeamInviteEmail } from "./TeamInviteEmail"; +export { BrokenIntegrationEmail } from "./BrokenIntegrationEmail"; diff --git a/packages/emails/templates/broken-integration-email.ts b/packages/emails/templates/broken-integration-email.ts new file mode 100644 index 0000000000..dc83db13be --- /dev/null +++ b/packages/emails/templates/broken-integration-email.ts @@ -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 { + 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")}`; + } +} diff --git a/packages/types/Calendar.d.ts b/packages/types/Calendar.d.ts index 16ecd52030..bd378bf0f5 100644 --- a/packages/types/Calendar.d.ts +++ b/packages/types/Calendar.d.ts @@ -123,6 +123,7 @@ export interface CalendarEvent { hideCalendarNotes?: boolean; recurrence?: string; recurringEvent?: RecurringEvent | null; + eventTypeId?: number | null; } export interface EntryPoint {