diff --git a/packages/emails/lib/generateIcsFile.ts b/packages/emails/lib/generateIcsFile.ts index 6f3a99fcef..53f2e32f09 100644 --- a/packages/emails/lib/generateIcsFile.ts +++ b/packages/emails/lib/generateIcsFile.ts @@ -12,20 +12,14 @@ export enum GenerateIcsRole { export default function generateIcsFile({ calEvent, - title, - subtitle, role, status, t, - isRequestReschedule, }: { calEvent: CalendarEvent; - title: string; - subtitle: string; role: GenerateIcsRole; status: EventStatus; t?: TFunction; - isRequestReschedule?: boolean; }) { // O365 deletes emails if the calendar event is selected. Currently no option to disable this on the web if ( @@ -39,12 +33,8 @@ export default function generateIcsFile({ filename: "event.ics", content: generateIcsString({ event: calEvent, - title, - subtitle, - role, status, t, - isRequestReschedule, }), method: "REQUEST", }; diff --git a/packages/emails/lib/generateIcsString.ts b/packages/emails/lib/generateIcsString.ts index df2fa51875..323d537652 100644 --- a/packages/emails/lib/generateIcsString.ts +++ b/packages/emails/lib/generateIcsString.ts @@ -1,16 +1,13 @@ -import type { DateArray, ParticipationStatus, ParticipationRole, EventStatus } from "ics"; +import type { DateArray, ParticipationRole, EventStatus, ParticipationStatus } 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 { getWhen } from "@calcom/lib/CalEventParser"; import { getVideoCallUrlFromCalEvent } from "@calcom/lib/CalEventParser"; import type { CalendarEvent, Person } from "@calcom/types/Calendar"; -import { GenerateIcsRole } from "./generateIcsFile"; - export enum BookingAction { Create = "create", Cancel = "cancel", @@ -19,52 +16,44 @@ export enum BookingAction { LocationChange = "location_change", } +export type ICSCalendarEvent = Pick< + CalendarEvent, + | "uid" + | "iCalUID" + | "iCalSequence" + | "startTime" + | "endTime" + | "title" + | "organizer" + | "attendees" + | "location" + | "recurringEvent" + | "team" + | "type" + | "hideCalendarEventDetails" +>; + const generateIcsString = ({ event, - title, - subtitle, status, - role, - isRequestReschedule, + partstat = "ACCEPTED", t, }: { - event: CalendarEvent; - title: string; - subtitle: string; + event: ICSCalendarEvent; status: EventStatus; - role: GenerateIcsRole; - isRequestReschedule?: boolean; + partstat?: ParticipationStatus; t?: TFunction; }) => { const location = getVideoCallUrlFromCalEvent(event) || event.location; // Taking care of recurrence rule let recurrenceRule: string | undefined = undefined; - const partstat: ParticipationStatus = "ACCEPTED"; const icsRole: ParticipationRole = "REQ-PARTICIPANT"; if (event.recurringEvent?.count) { // ics appends "RRULE:" already, so removing it from RRule generated string recurrenceRule = new RRule(event.recurringEvent).toString().replace("RRULE:", ""); } - const getTextBody = (title: string, subtitle: string): string => { - let body: string; - if (isRequestReschedule && role === GenerateIcsRole.ATTENDEE && t) { - body = ` - ${title} - ${getWhen(event, t)} - ${subtitle}`; - } - body = ` - ${title} - ${subtitle} - - ${getRichDescription(event, t)} - `.trim(); - - return body; - }; - const icsEvent = createEvent({ uid: event.iCalUID || event.uid!, sequence: event.iCalSequence || 0, @@ -76,7 +65,7 @@ const generateIcsString = ({ startInputType: "utc", productId: "calcom/ics", title: event.title, - description: getTextBody(title, subtitle), + description: getRichDescription(event, t), duration: { minutes: dayjs(event.endTime).diff(dayjs(event.startTime), "minute") }, organizer: { name: event.organizer.name, email: event.organizer.email }, ...{ recurrenceRule }, diff --git a/packages/emails/templates/attendee-add-guests-email.ts b/packages/emails/templates/attendee-add-guests-email.ts index 91db4da39d..4281743630 100644 --- a/packages/emails/templates/attendee-add-guests-email.ts +++ b/packages/emails/templates/attendee-add-guests-email.ts @@ -7,8 +7,6 @@ export default class AttendeeAddGuestsEmail extends AttendeeScheduledEmail { return { icalEvent: generateIcsFile({ calEvent: this.calEvent, - title: this.t("new_guests_added"), - subtitle: this.t("emailed_you_and_any_other_attendees"), role: GenerateIcsRole.ATTENDEE, status: "CONFIRMED", }), diff --git a/packages/emails/templates/attendee-cancelled-email.ts b/packages/emails/templates/attendee-cancelled-email.ts index a082a348f0..6433fdeae7 100644 --- a/packages/emails/templates/attendee-cancelled-email.ts +++ b/packages/emails/templates/attendee-cancelled-email.ts @@ -7,8 +7,6 @@ export default class AttendeeCancelledEmail extends AttendeeScheduledEmail { return { icalEvent: generateIcsFile({ calEvent: this.calEvent, - title: this.t("event_request_cancelled"), - subtitle: this.t("emailed_you_and_any_other_attendees"), role: GenerateIcsRole.ATTENDEE, status: "CANCELLED", }), diff --git a/packages/emails/templates/attendee-location-change-email.ts b/packages/emails/templates/attendee-location-change-email.ts index d23d7704de..996410738f 100644 --- a/packages/emails/templates/attendee-location-change-email.ts +++ b/packages/emails/templates/attendee-location-change-email.ts @@ -7,8 +7,6 @@ export default class AttendeeLocationChangeEmail extends AttendeeScheduledEmail return { icalEvent: generateIcsFile({ calEvent: this.calEvent, - title: this.t("event_location_changed"), - subtitle: this.t("emailed_you_and_any_other_attendees"), role: GenerateIcsRole.ATTENDEE, status: "CONFIRMED", }), diff --git a/packages/emails/templates/attendee-rescheduled-email.ts b/packages/emails/templates/attendee-rescheduled-email.ts index 896cbabae8..540179f496 100644 --- a/packages/emails/templates/attendee-rescheduled-email.ts +++ b/packages/emails/templates/attendee-rescheduled-email.ts @@ -7,8 +7,6 @@ export default class AttendeeRescheduledEmail extends AttendeeScheduledEmail { return { icalEvent: generateIcsFile({ calEvent: this.calEvent, - title: this.t("event_type_has_been_rescheduled"), - subtitle: this.t("emailed_you_and_any_other_attendees"), role: GenerateIcsRole.ATTENDEE, status: "CONFIRMED", }), diff --git a/packages/emails/templates/attendee-scheduled-email.ts b/packages/emails/templates/attendee-scheduled-email.ts index 528a35dfb1..db00016387 100644 --- a/packages/emails/templates/attendee-scheduled-email.ts +++ b/packages/emails/templates/attendee-scheduled-email.ts @@ -35,11 +35,7 @@ export default class AttendeeScheduledEmail extends BaseEmail { return { icalEvent: generateIcsFile({ calEvent: this.calEvent, - title: this.calEvent.recurringEvent?.count - ? this.t("your_event_has_been_scheduled_recurring") - : this.t("your_event_has_been_scheduled"), role: GenerateIcsRole.ATTENDEE, - subtitle: this.t("emailed_you_and_any_other_attendees"), status: "CONFIRMED", }), to: `${this.attendee.name} <${this.attendee.email}>`, 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 4c7be20986..ad427baceb 100644 --- a/packages/emails/templates/attendee-was-requested-to-reschedule-email.ts +++ b/packages/emails/templates/attendee-was-requested-to-reschedule-email.ts @@ -19,10 +19,6 @@ export default class AttendeeWasRequestedToRescheduleEmail extends OrganizerSche return { icalEvent: generateIcsFile({ calEvent: this.calEvent, - title: this.t("request_reschedule_booking"), - subtitle: this.t("request_reschedule_subtitle", { - organizer: this.calEvent.organizer.name, - }), role: GenerateIcsRole.ATTENDEE, status: "CANCELLED", }), diff --git a/packages/emails/templates/organizer-add-guests-email.ts b/packages/emails/templates/organizer-add-guests-email.ts index 39ebcdf3ae..5ea3b51c78 100644 --- a/packages/emails/templates/organizer-add-guests-email.ts +++ b/packages/emails/templates/organizer-add-guests-email.ts @@ -11,8 +11,6 @@ export default class OrganizerAddGuestsEmail extends OrganizerScheduledEmail { return { icalEvent: generateIcsFile({ calEvent: this.calEvent, - title: this.t("new_guests_added"), - subtitle: this.t("emailed_you_and_any_other_attendees"), role: GenerateIcsRole.ORGANIZER, status: "CONFIRMED", }), diff --git a/packages/emails/templates/organizer-cancelled-email.ts b/packages/emails/templates/organizer-cancelled-email.ts index 2990278d1c..2b981b4be5 100644 --- a/packages/emails/templates/organizer-cancelled-email.ts +++ b/packages/emails/templates/organizer-cancelled-email.ts @@ -11,8 +11,6 @@ export default class OrganizerCancelledEmail extends OrganizerScheduledEmail { return { icalEvent: generateIcsFile({ calEvent: this.calEvent, - title: this.t("event_request_cancelled"), - subtitle: this.t("emailed_you_and_any_other_attendees"), status: "CANCELLED", role: GenerateIcsRole.ORGANIZER, }), diff --git a/packages/emails/templates/organizer-location-change-email.ts b/packages/emails/templates/organizer-location-change-email.ts index 6f2b7d8b8c..13709a66ee 100644 --- a/packages/emails/templates/organizer-location-change-email.ts +++ b/packages/emails/templates/organizer-location-change-email.ts @@ -11,8 +11,6 @@ export default class OrganizerLocationChangeEmail extends OrganizerScheduledEmai return { icalEvent: generateIcsFile({ calEvent: this.calEvent, - title: this.t("event_location_changed"), - subtitle: this.t("emailed_you_and_any_other_attendees"), role: GenerateIcsRole.ORGANIZER, status: "CONFIRMED", }), diff --git a/packages/emails/templates/organizer-requested-to-reschedule-email.ts b/packages/emails/templates/organizer-requested-to-reschedule-email.ts index be15708bb6..72089be427 100644 --- a/packages/emails/templates/organizer-requested-to-reschedule-email.ts +++ b/packages/emails/templates/organizer-requested-to-reschedule-email.ts @@ -1,8 +1,3 @@ -import type { DateArray, Person } from "ics"; -import { createEvent } from "ics"; - -import dayjs from "@calcom/dayjs"; -import { getRichDescription } from "@calcom/lib/CalEventParser"; import { EMAIL_FROM_NAME } from "@calcom/lib/constants"; import type { CalendarEvent } from "@calcom/types/Calendar"; @@ -22,12 +17,6 @@ export default class OrganizerRequestedToRescheduleEmail extends OrganizerSchedu return { icalEvent: generateIcsFile({ calEvent: this.calEvent, - title: this.t("request_reschedule_title_organizer", { - attendee: this.calEvent.attendees[0].name, - }), - subtitle: this.t("request_reschedule_subtitle_organizer", { - attendee: this.calEvent.attendees[0].name, - }), role: GenerateIcsRole.ORGANIZER, status: "CANCELLED", }), @@ -52,50 +41,4 @@ export default class OrganizerRequestedToRescheduleEmail extends OrganizerSchedu ), }; } - - // @OVERRIDE - protected getiCalEventAsString(): string | undefined { - const icsEvent = createEvent({ - start: dayjs(this.calEvent.startTime) - .utc() - .toArray() - .slice(0, 6) - .map((v, i) => (i === 1 ? v + 1 : v)) as DateArray, - startInputType: "utc", - productId: "calcom/ics", - title: this.t("ics_event_title", { - eventType: this.calEvent.type, - name: this.calEvent.attendees[0].name, - }), - description: this.getTextBody( - this.t("request_reschedule_title_organizer", { - attendee: this.calEvent.attendees[0].name, - }), - this.t("request_reschedule_subtitle_organizer", { - attendee: this.calEvent.attendees[0].name, - }) - ), - 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, - })), - status: "CANCELLED", - method: "CANCEL", - }); - if (icsEvent.error) { - throw icsEvent.error; - } - return icsEvent.value; - } - - // @OVERRIDE - protected getTextBody(title = "", subtitle = ""): string { - return ` -${this.t(title)} -${this.t(subtitle)} -${getRichDescription(this.calEvent, this.t, true)} -`.trim(); - } } diff --git a/packages/emails/templates/organizer-rescheduled-email.ts b/packages/emails/templates/organizer-rescheduled-email.ts index 047d5151e4..2a073a92d7 100644 --- a/packages/emails/templates/organizer-rescheduled-email.ts +++ b/packages/emails/templates/organizer-rescheduled-email.ts @@ -11,8 +11,6 @@ export default class OrganizerRescheduledEmail extends OrganizerScheduledEmail { return { icalEvent: generateIcsFile({ calEvent: this.calEvent, - title: this.t("event_type_has_been_rescheduled"), - subtitle: this.t("emailed_you_and_any_other_attendees"), role: GenerateIcsRole.ORGANIZER, status: "CONFIRMED", }), diff --git a/packages/emails/templates/organizer-scheduled-email.ts b/packages/emails/templates/organizer-scheduled-email.ts index c7089c6637..d55fd76048 100644 --- a/packages/emails/templates/organizer-scheduled-email.ts +++ b/packages/emails/templates/organizer-scheduled-email.ts @@ -34,10 +34,6 @@ export default class OrganizerScheduledEmail extends BaseEmail { return { icalEvent: generateIcsFile({ calEvent: this.calEvent, - title: this.calEvent.recurringEvent?.count - ? this.t("new_event_scheduled_recurring") - : this.t("new_event_scheduled"), - subtitle: this.t("emailed_you_and_any_other_attendees"), role: GenerateIcsRole.ORGANIZER, status: "CONFIRMED", }), diff --git a/packages/features/bookings/lib/handleCancelBooking.ts b/packages/features/bookings/lib/handleCancelBooking.ts index a8bfe4530f..044187b262 100644 --- a/packages/features/bookings/lib/handleCancelBooking.ts +++ b/packages/features/bookings/lib/handleCancelBooking.ts @@ -378,7 +378,7 @@ async function handler(req: CustomRequest) { bookerUrl, ...{ eventType: { - slug: bookingToDelete.eventType?.slug, + slug: bookingToDelete.eventType?.slug as string, schedulingType: bookingToDelete.eventType?.schedulingType, hosts: bookingToDelete.eventType?.hosts, }, diff --git a/packages/features/ee/workflows/api/scheduleEmailReminders.ts b/packages/features/ee/workflows/api/scheduleEmailReminders.ts index ec699d4f5e..44777eced3 100644 --- a/packages/features/ee/workflows/api/scheduleEmailReminders.ts +++ b/packages/features/ee/workflows/api/scheduleEmailReminders.ts @@ -3,10 +3,12 @@ import type { NextApiRequest, NextApiResponse } from "next"; import { v4 as uuidv4 } from "uuid"; import dayjs from "@calcom/dayjs"; +import generateIcsString from "@calcom/emails/lib/generateIcsString"; import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses"; import { getBookerBaseUrl } from "@calcom/lib/getBookerUrl/server"; import logger from "@calcom/lib/logger"; import { defaultHandler } from "@calcom/lib/server"; +import { getTranslation } from "@calcom/lib/server/i18n"; import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat"; import prisma from "@calcom/prisma"; import { SchedulingType, WorkflowActions, WorkflowMethods, WorkflowTemplates } from "@calcom/prisma/enums"; @@ -18,7 +20,6 @@ import { getAllRemindersToDelete, getAllUnscheduledReminders, } from "../lib/getWorkflowReminders"; -import { getiCalEventAsString } from "../lib/getiCalEventAsString"; import { cancelScheduledEmail, deleteScheduledSend, @@ -271,6 +272,37 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { if (emailContent.emailSubject.length > 0 && !emailBodyEmpty && sendTo) { const batchId = await getBatchId(); + const booking = reminder.booking; + + const t = await getTranslation(booking.user?.locale ?? "en", "common"); + + const attendeePromises = []; + + for (const attendee of booking.attendees) { + attendeePromises.push( + getTranslation(attendee.locale ?? "en", "common").then((tAttendee) => ({ + ...attendee, + language: { locale: attendee.locale ?? "en", translate: tAttendee }, + })) + ); + } + + const attendees = await Promise.all(attendeePromises); + + const event = { + ...booking, + startTime: dayjs(booking.startTime).utc().format(), + endTime: dayjs(booking.endTime).utc().format(), + type: booking.eventType?.slug ?? "", + organizer: { + name: booking.user?.name ?? "", + email: booking.user?.email ?? "", + timeZone: booking.user?.timeZone ?? "", + language: { translate: t, locale: booking.user?.locale ?? "en" }, + }, + attendees, + }; + sendEmailPromises.push( sendSendgridMail( { @@ -283,7 +315,9 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { attachments: reminder.workflowStep.includeCalendarEvent ? [ { - content: Buffer.from(getiCalEventAsString(reminder.booking) || "").toString("base64"), + content: Buffer.from( + generateIcsString({ event, status: "CONFIRMED" }) || "" + ).toString("base64"), filename: "event.ics", type: "text/calendar; method=REQUEST", disposition: "attachment", diff --git a/packages/features/ee/workflows/lib/getWorkflowReminders.ts b/packages/features/ee/workflows/lib/getWorkflowReminders.ts index 70ddde3e1a..a5cf438c4b 100644 --- a/packages/features/ee/workflows/lib/getWorkflowReminders.ts +++ b/packages/features/ee/workflows/lib/getWorkflowReminders.ts @@ -27,9 +27,11 @@ type PartialBooking = | "attendees" | "userPrimaryEmail" | "smsReminderNumber" + | "title" > & { eventType: | (Partial & { + slug: string; team: { parentId?: number }; hosts: { user: { email: string; destinationCalendar?: { primaryEmail: string } } }[] | undefined; }) diff --git a/packages/features/ee/workflows/lib/reminders/emailReminderManager.ts b/packages/features/ee/workflows/lib/reminders/emailReminderManager.ts index 6b7dca1705..3f4162ca8f 100644 --- a/packages/features/ee/workflows/lib/reminders/emailReminderManager.ts +++ b/packages/features/ee/workflows/lib/reminders/emailReminderManager.ts @@ -1,15 +1,13 @@ import type { MailData } from "@sendgrid/helpers/classes/mail"; -import { createEvent } from "ics"; -import type { ParticipationStatus } from "ics"; -import type { DateArray } from "ics"; -import { RRule } from "rrule"; +import type { EventStatus } from "ics"; import { v4 as uuidv4 } from "uuid"; -import { guessEventLocationType } from "@calcom/app-store/locations"; import dayjs from "@calcom/dayjs"; +import generateIcsString from "@calcom/emails/lib/generateIcsString"; import { preprocessNameFieldDataWithVariant } from "@calcom/features/form-builder/utils"; import { WEBSITE_URL } from "@calcom/lib/constants"; import logger from "@calcom/lib/logger"; +import { getTranslation } from "@calcom/lib/server/i18n"; import prisma from "@calcom/prisma"; import type { TimeUnit } from "@calcom/prisma/enums"; import { @@ -29,53 +27,6 @@ import emailReminderTemplate from "./templates/emailReminderTemplate"; const log = logger.getSubLogger({ prefix: ["[emailReminderManager]"] }); -function getiCalEventAsString(evt: BookingInfo, status?: ParticipationStatus) { - const uid = uuidv4(); - let recurrenceRule: string | undefined = undefined; - if (evt.eventType.recurringEvent?.count) { - recurrenceRule = new RRule(evt.eventType.recurringEvent).toString().replace("RRULE:", ""); - } - - let location = bookingMetadataSchema.parse(evt.metadata || {})?.videoCallUrl; - - if (!location) { - location = guessEventLocationType(location)?.label || evt.location || ""; - } - - const icsEvent = createEvent({ - uid, - startInputType: "utc", - start: dayjs(evt.startTime) - .utc() - .toArray() - .slice(0, 6) - .map((v, i) => (i === 1 ? v + 1 : v)) as DateArray, - duration: { minutes: dayjs(evt.endTime).diff(dayjs(evt.startTime), "minute") }, - title: evt.title, - description: evt.additionalNotes || "", - location, - organizer: { email: evt.organizer.email || "", name: evt.organizer.name }, - attendees: [ - { - name: preprocessNameFieldDataWithVariant("fullName", evt.attendees[0].name) as string, - email: evt.attendees[0].email, - partstat: status, - role: "REQ-PARTICIPANT", - rsvp: true, - }, - ], - method: "REQUEST", - ...{ recurrenceRule }, - status: "CONFIRMED", - }); - - if (icsEvent.error) { - throw icsEvent.error; - } - - return icsEvent.value; -} - type ScheduleEmailReminderAction = Extract< WorkflowActions, "EMAIL_HOST" | "EMAIL_ATTENDEE" | "EMAIL_ADDRESS" @@ -95,6 +46,7 @@ export interface ScheduleReminderArgs { } interface scheduleEmailReminderArgs extends ScheduleReminderArgs { + evt: BookingInfo; sendTo: MailData["to"]; action: ScheduleEmailReminderAction; emailSubject?: string; @@ -260,13 +212,26 @@ export const scheduleEmailReminder = async (args: scheduleEmailReminderArgs) => const batchId = await getBatchId(); - function sendEmail(data: Partial, triggerEvent?: WorkflowTriggerEvents) { - const status: ParticipationStatus = - triggerEvent === WorkflowTriggerEvents.AFTER_EVENT - ? "COMPLETED" - : triggerEvent === WorkflowTriggerEvents.EVENT_CANCELLED - ? "DECLINED" - : "ACCEPTED"; + async function sendEmail(data: Partial, triggerEvent?: WorkflowTriggerEvents) { + const status: EventStatus = + triggerEvent === WorkflowTriggerEvents.EVENT_CANCELLED ? "CANCELLED" : "CONFIRMED"; + + const organizerT = await getTranslation(evt.organizer.language.locale || "en", "common"); + + const attendeeT = await getTranslation(evt.attendees[0].language.locale || "en", "common"); + + const attendee = { + ...evt.attendees[0], + name: preprocessNameFieldDataWithVariant("fullName", evt.attendees[0].name) as string, + language: { ...evt.attendees[0].language, translate: attendeeT }, + }; + + const emailEvent = { + ...evt, + type: evt.eventType?.slug || "", + organizer: { ...evt.organizer, language: { ...evt.organizer.language, translate: organizerT } }, + attendees: [attendee], + }; return sendSendgridMail( { @@ -278,7 +243,12 @@ export const scheduleEmailReminder = async (args: scheduleEmailReminderArgs) => attachments: includeCalendarEvent ? [ { - content: Buffer.from(getiCalEventAsString(evt, status) || "").toString("base64"), + content: Buffer.from( + generateIcsString({ + event: emailEvent, + status, + }) || "" + ).toString("base64"), filename: "event.ics", type: "text/calendar; method=REQUEST", disposition: "attachment", diff --git a/packages/features/ee/workflows/lib/reminders/reminderScheduler.ts b/packages/features/ee/workflows/lib/reminders/reminderScheduler.ts index 7db5f0bd06..a345f514e8 100644 --- a/packages/features/ee/workflows/lib/reminders/reminderScheduler.ts +++ b/packages/features/ee/workflows/lib/reminders/reminderScheduler.ts @@ -17,7 +17,7 @@ import { scheduleWhatsappReminder } from "./whatsappReminderManager"; export type ExtendedCalendarEvent = Omit & { metadata?: { videoCallUrl: string | undefined }; eventType: { - slug?: string; + slug: string; schedulingType?: SchedulingType | null; hosts?: { user: { email: string; destinationCalendar?: { primaryEmail: string | null } | null } }[]; }; diff --git a/packages/features/ee/workflows/lib/reminders/smsReminderManager.ts b/packages/features/ee/workflows/lib/reminders/smsReminderManager.ts index 27ebf063de..6d1b3111bc 100644 --- a/packages/features/ee/workflows/lib/reminders/smsReminderManager.ts +++ b/packages/features/ee/workflows/lib/reminders/smsReminderManager.ts @@ -46,8 +46,8 @@ export type BookingInfo = { timeFormat?: TimeFormat; username?: string; }; - eventType: { - slug?: string; + eventType?: { + slug: string; recurringEvent?: RecurringEvent | null; }; startTime: string; diff --git a/packages/features/ee/workflows/lib/test/workflows.test.ts b/packages/features/ee/workflows/lib/test/workflows.test.ts index f238205689..94fd9d89ad 100644 --- a/packages/features/ee/workflows/lib/test/workflows.test.ts +++ b/packages/features/ee/workflows/lib/test/workflows.test.ts @@ -11,6 +11,7 @@ import { expectSMSWorkflowToBeTriggered, expectSMSWorkflowToBeNotTriggered, } from "@calcom/web/test/utils/bookingScenario/expects"; +import { setupAndTeardown } from "@calcom/web/test/utils/bookingScenario/setupAndTeardown"; import { describe, expect, beforeAll, vi } from "vitest"; @@ -87,7 +88,7 @@ const mockBookings = [ status: BookingStatus.ACCEPTED, startTime: `2024-05-22T04:00:00.000Z`, endTime: `2024-05-22T04:30:00.000Z`, - attendees: [{ email: "attendee@example.com" }], + attendees: [{ email: "attendee@example.com", locale: "en" }], }, { uid: "mL4Dx9jTkQbnWEu3pR7yNcF", @@ -96,7 +97,7 @@ const mockBookings = [ status: BookingStatus.ACCEPTED, startTime: `2024-05-23T04:00:00.000Z`, endTime: `2024-05-23T04:30:00.000Z`, - attendees: [{ email: "attendee@example.com" }], + attendees: [{ email: "attendee@example.com", locale: "en" }], }, { uid: "Fd9Rf8iYsOpmQUw9hB1vKd8", @@ -105,7 +106,7 @@ const mockBookings = [ status: BookingStatus.ACCEPTED, startTime: `2024-06-01T04:30:00.000Z`, endTime: `2024-06-01T05:00:00.000Z`, - attendees: [{ email: "attendee@example.com" }], + attendees: [{ email: "attendee@example.com", locale: "en" }], }, { uid: "Kd8Dx9jTkQbnWEu3pR7yKdl", @@ -114,7 +115,7 @@ const mockBookings = [ status: BookingStatus.ACCEPTED, startTime: `2024-06-02T04:30:00.000Z`, endTime: `2024-06-02T05:00:00.000Z`, - attendees: [{ email: "attendee@example.com" }], + attendees: [{ email: "attendee@example.com", locale: "en" }], }, ]; @@ -366,6 +367,8 @@ describe("deleteRemindersOfActiveOnIds", () => { }); describe("scheduleBookingReminders", () => { + setupAndTeardown(); + test("schedules workflow notifications with before event trigger and email to host action", async ({}) => { // organizer is part of org and two teams const organizer = getOrganizer({ diff --git a/packages/lib/CalEventParser.ts b/packages/lib/CalEventParser.ts index b6d8229e0d..5afa4ebd2b 100644 --- a/packages/lib/CalEventParser.ts +++ b/packages/lib/CalEventParser.ts @@ -12,14 +12,17 @@ const translator = short(); // The odd indentation in this file is necessary because otherwise the leading tabs will be applied into the event description. -export const getWhat = (calEvent: CalendarEvent, t: TFunction) => { +export const getWhat = (calEvent: Pick, t: TFunction) => { return ` ${t("what")}: ${calEvent.title} `; }; -export const getWhen = (calEvent: CalendarEvent, t: TFunction) => { +export const getWhen = ( + calEvent: Pick, + t: TFunction +) => { return calEvent.seatsPerTimeSlot ? ` ${t("organizer_timezone")}: @@ -31,7 +34,13 @@ ${calEvent.attendees[0].timeZone} `; }; -export const getWho = (calEvent: CalendarEvent, t: TFunction) => { +export const getWho = ( + calEvent: Pick< + CalendarEvent, + "attendees" | "seatsPerTimeSlot" | "seatsShowAttendees" | "organizer" | "team" + >, + t: TFunction +) => { let attendeesFromCalEvent = [...calEvent.attendees]; if (calEvent.seatsPerTimeSlot && !calEvent.seatsShowAttendees) { attendeesFromCalEvent = []; @@ -67,7 +76,7 @@ ${organizer + attendees + teamMembers.join("")} `; }; -export const getAdditionalNotes = (calEvent: CalendarEvent, t: TFunction) => { +export const getAdditionalNotes = (calEvent: Pick, t: TFunction) => { if (!calEvent.additionalNotes) { return ""; } @@ -77,7 +86,7 @@ ${calEvent.additionalNotes} `; }; -export const getUserFieldsResponses = (calEvent: CalendarEvent) => { +export const getUserFieldsResponses = (calEvent: Parameters[0]) => { const labelValueMap = getLabelValueMapFromResponses(calEvent); if (!labelValueMap) { @@ -98,7 +107,7 @@ ${labelValueMap[key]} return responsesString; }; -export const getAppsStatus = (calEvent: CalendarEvent, t: TFunction) => { +export const getAppsStatus = (calEvent: Pick, t: TFunction) => { if (!calEvent.appsStatus) { return ""; } @@ -115,7 +124,7 @@ export const getAppsStatus = (calEvent: CalendarEvent, t: TFunction) => { `; }; -export const getDescription = (calEvent: CalendarEvent, t: TFunction) => { +export const getDescription = (calEvent: Pick, t: TFunction) => { if (!calEvent.description) { return ""; } @@ -123,7 +132,9 @@ export const getDescription = (calEvent: CalendarEvent, t: TFunction) => { ${calEvent.description} `; }; -export const getLocation = (calEvent: CalendarEvent) => { +export const getLocation = ( + calEvent: Parameters[0] & Parameters[0] +) => { const meetingUrl = getVideoCallUrlFromCalEvent(calEvent); if (meetingUrl) { return meetingUrl; @@ -132,7 +143,7 @@ export const getLocation = (calEvent: CalendarEvent) => { return providerName || calEvent.location || ""; }; -export const getProviderName = (calEvent: CalendarEvent): string => { +export const getProviderName = (calEvent: Pick): string => { // TODO: use getAppName from @calcom/app-store if (calEvent.location && calEvent.location.includes("integrations:")) { let location = calEvent.location.split(":")[1]; @@ -148,12 +159,12 @@ export const getProviderName = (calEvent: CalendarEvent): string => { return ""; }; -export const getUid = (calEvent: CalendarEvent): string => { +export const getUid = (calEvent: Pick): string => { const uid = calEvent.uid; return uid ?? translator.fromUUID(uuidv5(JSON.stringify(calEvent), uuidv5.URL)); }; -const getSeatReferenceId = (calEvent: CalendarEvent): string => { +const getSeatReferenceId = (calEvent: Pick): string => { return calEvent.attendeeSeatId ? calEvent.attendeeSeatId : ""; }; @@ -168,7 +179,12 @@ export const getBookingUrl = (calEvent: CalendarEvent) => { return `${calEvent.bookerUrl ?? WEBAPP_URL}/booking/${getUid(calEvent)}?changes=true`; }; -export const getPlatformManageLink = (calEvent: CalendarEvent, t: TFunction) => { +export const getPlatformManageLink = ( + calEvent: Parameters[0] & + Parameters[0]["calEvent"] & + Pick, + t: TFunction +) => { const shouldDisplayReschedule = !calEvent.recurringEvent && calEvent.platformRescheduleUrl; let res = calEvent.platformBookingUrl || shouldDisplayReschedule || calEvent.platformCancelUrl @@ -194,7 +210,11 @@ export const getPlatformManageLink = (calEvent: CalendarEvent, t: TFunction) => return res; }; -export const getManageLink = (calEvent: CalendarEvent, t: TFunction) => { +export const getManageLink = ( + calEvent: Parameters[0] & + Pick, + t: TFunction +) => { if (calEvent.platformClientId) { return getPlatformManageLink(calEvent, t); } @@ -205,7 +225,7 @@ export const getManageLink = (calEvent: CalendarEvent, t: TFunction) => { }; export const getPlatformCancelLink = ( - calEvent: CalendarEvent, + calEvent: Pick, bookingUid: string, seatUid?: string ): string => { @@ -223,7 +243,13 @@ export const getPlatformCancelLink = ( return ""; }; -export const getCancelLink = (calEvent: CalendarEvent, attendee?: Person): string => { +export const getCancelLink = ( + calEvent: Parameters[0] & + Parameters[0] & + Parameters[0] & + Pick, + attendee?: Person +): string => { const Uid = getUid(calEvent); const seatReferenceUid = getSeatReferenceId(calEvent); if (calEvent.platformClientId) { @@ -241,7 +267,7 @@ export const getCancelLink = (calEvent: CalendarEvent, attendee?: Person): strin }; export const getPlatformRescheduleLink = ( - calEvent: CalendarEvent, + calEvent: Pick, bookingUid: string, seatUid?: string ): string => { @@ -264,7 +290,10 @@ export const getRescheduleLink = ({ allowRescheduleForCancelledBooking = false, attendee, }: { - calEvent: CalendarEvent; + calEvent: Parameters[0] & + Parameters[0] & + Parameters[0] & + Pick; allowRescheduleForCancelledBooking?: boolean; attendee?: Person; }): string => { @@ -286,8 +315,19 @@ export const getRescheduleLink = ({ return url.toString(); }; +type RichDescriptionCalEvent = Parameters[0] & + Parameters[0] & + Parameters[0] & + Parameters[0] & + Parameters[0] & + Parameters[0] & + Parameters[0] & + Parameters[0] & + Parameters[0] & + Pick; + export const getRichDescription = ( - calEvent: CalendarEvent, + calEvent: RichDescriptionCalEvent, t_?: TFunction /*, attendee?: Person*/, includeAppStatus = false ) => { @@ -320,7 +360,7 @@ ${calEvent.paymentInfo.link} `.trim(); }; -export const getCancellationReason = (calEvent: CalendarEvent, t: TFunction) => { +export const getCancellationReason = (calEvent: Pick, t: TFunction) => { if (!calEvent.cancellationReason) return ""; return ` ${t("cancellation_reason")}: @@ -328,15 +368,18 @@ ${calEvent.cancellationReason} `; }; -export const isDailyVideoCall = (calEvent: CalendarEvent): boolean => { +export const isDailyVideoCall = (calEvent: Pick): boolean => { return calEvent?.videoCallData?.type === "daily_video"; }; -export const getPublicVideoCallUrl = (calEvent: CalendarEvent): string => { +export const getPublicVideoCallUrl = (calEvent: Pick): string => { return `${WEBAPP_URL}/video/${getUid(calEvent)}`; }; -export const getVideoCallUrlFromCalEvent = (calEvent: CalendarEvent): string => { +export const getVideoCallUrlFromCalEvent = ( + calEvent: Parameters[0] & + Pick +): string => { if (calEvent.videoCallData) { if (isDailyVideoCall(calEvent)) { return getPublicVideoCallUrl(calEvent); diff --git a/packages/lib/getLabelValueMapFromResponses.ts b/packages/lib/getLabelValueMapFromResponses.ts index 488a3defb0..9473c8f26b 100644 --- a/packages/lib/getLabelValueMapFromResponses.ts +++ b/packages/lib/getLabelValueMapFromResponses.ts @@ -4,7 +4,10 @@ import { TITLE_FIELD } from "@calcom/features/bookings/lib/SystemField"; import type { bookingResponse } from "@calcom/features/bookings/lib/getBookingResponsesSchema"; import type { CalendarEvent } from "@calcom/types/Calendar"; -export default function getLabelValueMapFromResponses(calEvent: CalendarEvent, isOrganizer = false) { +export default function getLabelValueMapFromResponses( + calEvent: Pick, + isOrganizer = false +) { const { customInputs, userFieldsResponses, responses, eventTypeId } = calEvent; const isDynamicEvent = !eventTypeId; diff --git a/packages/trpc/server/routers/viewer/workflows/activateEventType.handler.ts b/packages/trpc/server/routers/viewer/workflows/activateEventType.handler.ts index 999335a5a7..2191a80dc4 100644 --- a/packages/trpc/server/routers/viewer/workflows/activateEventType.handler.ts +++ b/packages/trpc/server/routers/viewer/workflows/activateEventType.handler.ts @@ -251,7 +251,7 @@ export const activateEventTypeHandler = async ({ ctx, input }: ActivateEventType title: booking.title, language: { locale: booking?.user?.locale || defaultLocale }, eventType: { - slug: booking.eventType?.slug, + slug: booking.eventType?.slug || "", schedulingType: booking.eventType?.schedulingType, hosts: booking.eventType?.hosts, }, diff --git a/packages/trpc/server/routers/viewer/workflows/util.ts b/packages/trpc/server/routers/viewer/workflows/util.ts index dd95681928..bc9e339fc9 100644 --- a/packages/trpc/server/routers/viewer/workflows/util.ts +++ b/packages/trpc/server/routers/viewer/workflows/util.ts @@ -629,7 +629,7 @@ export async function scheduleBookingReminders( title: booking.title, language: { locale: booking?.user?.locale || defaultLocale }, eventType: { - slug: booking.eventType?.slug, + slug: booking.eventType?.slug || "", schedulingType: booking.eventType?.schedulingType, hosts: booking.eventType?.hosts, }, @@ -664,6 +664,7 @@ export async function scheduleBookingReminders( sendTo = [step.sendTo || ""]; break; } + await scheduleEmailReminder({ evt: bookingInfo, triggerEvent: trigger,