chore: Fixing ICS file in workflows & ICS calendar descriptions [CAL-4409] (#16731)
* Remove title and subtitle from ICS calendar description * Remove unused method * Use generateIcsString function for workflows * Type fixes * Refactor typing of CalEventParser * WIP * Type fixes * Fix test * Fix tests * type fixes * Remove duplicate code before `scheduleEmailReminder`
This commit is contained in:
@@ -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",
|
||||
};
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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",
|
||||
}),
|
||||
|
||||
@@ -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",
|
||||
}),
|
||||
|
||||
@@ -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",
|
||||
}),
|
||||
|
||||
@@ -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",
|
||||
}),
|
||||
|
||||
@@ -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}>`,
|
||||
|
||||
@@ -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",
|
||||
}),
|
||||
|
||||
@@ -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",
|
||||
}),
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
|
||||
@@ -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",
|
||||
}),
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
}),
|
||||
|
||||
@@ -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",
|
||||
}),
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -27,9 +27,11 @@ type PartialBooking =
|
||||
| "attendees"
|
||||
| "userPrimaryEmail"
|
||||
| "smsReminderNumber"
|
||||
| "title"
|
||||
> & {
|
||||
eventType:
|
||||
| (Partial<EventType> & {
|
||||
slug: string;
|
||||
team: { parentId?: number };
|
||||
hosts: { user: { email: string; destinationCalendar?: { primaryEmail: string } } }[] | undefined;
|
||||
})
|
||||
|
||||
@@ -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<MailData>, triggerEvent?: WorkflowTriggerEvents) {
|
||||
const status: ParticipationStatus =
|
||||
triggerEvent === WorkflowTriggerEvents.AFTER_EVENT
|
||||
? "COMPLETED"
|
||||
: triggerEvent === WorkflowTriggerEvents.EVENT_CANCELLED
|
||||
? "DECLINED"
|
||||
: "ACCEPTED";
|
||||
async function sendEmail(data: Partial<MailData>, 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",
|
||||
|
||||
@@ -17,7 +17,7 @@ import { scheduleWhatsappReminder } from "./whatsappReminderManager";
|
||||
export type ExtendedCalendarEvent = Omit<CalendarEvent, "bookerUrl"> & {
|
||||
metadata?: { videoCallUrl: string | undefined };
|
||||
eventType: {
|
||||
slug?: string;
|
||||
slug: string;
|
||||
schedulingType?: SchedulingType | null;
|
||||
hosts?: { user: { email: string; destinationCalendar?: { primaryEmail: string | null } | null } }[];
|
||||
};
|
||||
|
||||
@@ -46,8 +46,8 @@ export type BookingInfo = {
|
||||
timeFormat?: TimeFormat;
|
||||
username?: string;
|
||||
};
|
||||
eventType: {
|
||||
slug?: string;
|
||||
eventType?: {
|
||||
slug: string;
|
||||
recurringEvent?: RecurringEvent | null;
|
||||
};
|
||||
startTime: string;
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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<CalendarEvent, "title">, t: TFunction) => {
|
||||
return `
|
||||
${t("what")}:
|
||||
${calEvent.title}
|
||||
`;
|
||||
};
|
||||
|
||||
export const getWhen = (calEvent: CalendarEvent, t: TFunction) => {
|
||||
export const getWhen = (
|
||||
calEvent: Pick<CalendarEvent, "organizer" | "attendees" | "seatsPerTimeSlot">,
|
||||
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<CalendarEvent, "additionalNotes">, t: TFunction) => {
|
||||
if (!calEvent.additionalNotes) {
|
||||
return "";
|
||||
}
|
||||
@@ -77,7 +86,7 @@ ${calEvent.additionalNotes}
|
||||
`;
|
||||
};
|
||||
|
||||
export const getUserFieldsResponses = (calEvent: CalendarEvent) => {
|
||||
export const getUserFieldsResponses = (calEvent: Parameters<typeof getLabelValueMapFromResponses>[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<CalendarEvent, "appsStatus">, 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<CalendarEvent, "description">, 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<typeof getVideoCallUrlFromCalEvent>[0] & Parameters<typeof getProviderName>[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<CalendarEvent, "location">): 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<CalendarEvent, "uid">): string => {
|
||||
const uid = calEvent.uid;
|
||||
return uid ?? translator.fromUUID(uuidv5(JSON.stringify(calEvent), uuidv5.URL));
|
||||
};
|
||||
|
||||
const getSeatReferenceId = (calEvent: CalendarEvent): string => {
|
||||
const getSeatReferenceId = (calEvent: Pick<CalendarEvent, "attendeeSeatId">): 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<typeof getCancelLink>[0] &
|
||||
Parameters<typeof getRescheduleLink>[0]["calEvent"] &
|
||||
Pick<CalendarEvent, "platformBookingUrl" | "platformRescheduleUrl" | "team">,
|
||||
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<typeof getPlatformManageLink>[0] &
|
||||
Pick<CalendarEvent, "platformClientId" | "bookerUrl">,
|
||||
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<CalendarEvent, "platformCancelUrl" | "type" | "organizer" | "recurringEvent" | "team">,
|
||||
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<typeof getUid>[0] &
|
||||
Parameters<typeof getSeatReferenceId>[0] &
|
||||
Parameters<typeof getPlatformCancelLink>[0] &
|
||||
Pick<CalendarEvent, "platformClientId" | "bookerUrl">,
|
||||
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<CalendarEvent, "platformRescheduleUrl" | "type" | "organizer" | "team">,
|
||||
bookingUid: string,
|
||||
seatUid?: string
|
||||
): string => {
|
||||
@@ -264,7 +290,10 @@ export const getRescheduleLink = ({
|
||||
allowRescheduleForCancelledBooking = false,
|
||||
attendee,
|
||||
}: {
|
||||
calEvent: CalendarEvent;
|
||||
calEvent: Parameters<typeof getUid>[0] &
|
||||
Parameters<typeof getSeatReferenceId>[0] &
|
||||
Parameters<typeof getPlatformRescheduleLink>[0] &
|
||||
Pick<CalendarEvent, "bookerUrl" | "platformClientId">;
|
||||
allowRescheduleForCancelledBooking?: boolean;
|
||||
attendee?: Person;
|
||||
}): string => {
|
||||
@@ -286,8 +315,19 @@ export const getRescheduleLink = ({
|
||||
return url.toString();
|
||||
};
|
||||
|
||||
type RichDescriptionCalEvent = Parameters<typeof getCancellationReason>[0] &
|
||||
Parameters<typeof getWhat>[0] &
|
||||
Parameters<typeof getWhen>[0] &
|
||||
Parameters<typeof getLocation>[0] &
|
||||
Parameters<typeof getDescription>[0] &
|
||||
Parameters<typeof getAdditionalNotes>[0] &
|
||||
Parameters<typeof getUserFieldsResponses>[0] &
|
||||
Parameters<typeof getAppsStatus>[0] &
|
||||
Parameters<typeof getManageLink>[0] &
|
||||
Pick<CalendarEvent, "organizer" | "paymentInfo">;
|
||||
|
||||
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<CalendarEvent, "cancellationReason">, 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<CalendarEvent, "videoCallData">): boolean => {
|
||||
return calEvent?.videoCallData?.type === "daily_video";
|
||||
};
|
||||
|
||||
export const getPublicVideoCallUrl = (calEvent: CalendarEvent): string => {
|
||||
export const getPublicVideoCallUrl = (calEvent: Pick<CalendarEvent, "uid">): string => {
|
||||
return `${WEBAPP_URL}/video/${getUid(calEvent)}`;
|
||||
};
|
||||
|
||||
export const getVideoCallUrlFromCalEvent = (calEvent: CalendarEvent): string => {
|
||||
export const getVideoCallUrlFromCalEvent = (
|
||||
calEvent: Parameters<typeof getPublicVideoCallUrl>[0] &
|
||||
Pick<CalendarEvent, "videoCallData" | "additionalInformation">
|
||||
): string => {
|
||||
if (calEvent.videoCallData) {
|
||||
if (isDailyVideoCall(calEvent)) {
|
||||
return getPublicVideoCallUrl(calEvent);
|
||||
|
||||
@@ -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<CalendarEvent, "customInputs" | "userFieldsResponses" | "responses" | "eventTypeId">,
|
||||
isOrganizer = false
|
||||
) {
|
||||
const { customInputs, userFieldsResponses, responses, eventTypeId } = calEvent;
|
||||
|
||||
const isDynamicEvent = !eventTypeId;
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user