feat: Add consistent iCalUID (#12122)

Co-authored-by: Hariom <hariombalhara@gmail.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
This commit is contained in:
Joe Au-Yeung 2023-12-15 10:28:32 -05:00 committed by GitHub
parent 0e8ac7e4ed
commit 9dfa596e3e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 568 additions and 195 deletions

View File

@ -62,6 +62,7 @@ export const createBookingsFixture = (page: Page) => {
rescheduled,
paid,
status,
iCalUID: `${uid}@cal.com`,
},
});
const bookingFixture = createBookingFixture(booking, store.page);

View File

@ -68,6 +68,7 @@ type ExpectedEmail = {
filename: string;
iCalUID?: string;
recurrence?: Recurrence;
method: string;
};
/**
* Checks that there is no
@ -162,9 +163,14 @@ expect.extend({
}
if (!expectedEmail.noIcs && !isIcsUIDExpected) {
const icsObjectKeys = icsObject ? Object.keys(icsObject) : [];
const icsKey = icsObjectKeys.find((key) => key !== "vcalendar");
if (!icsKey) throw new Error("icsKey not found");
return {
pass: false,
actual: JSON.stringify(icsObject),
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
actual: icsObject[icsKey].uid!,
expected: expectedEmail.ics?.iCalUID,
message: () => `Expected ICS UID ${isNot ? "is" : "isn't"} present in actual`,
};
@ -375,6 +381,7 @@ export function expectSuccessfulBookingCreationEmails({
filename: "event.ics",
iCalUID: `${iCalUID}`,
recurrence,
method: "REQUEST",
},
},
`${organizer.email}`
@ -397,8 +404,9 @@ export function expectSuccessfulBookingCreationEmails({
to: `${booker.name} <${booker.email}>`,
ics: {
filename: "event.ics",
iCalUID: iCalUID,
iCalUID: `${iCalUID}`,
recurrence,
method: "REQUEST",
},
links: recurrence
? [
@ -436,7 +444,8 @@ export function expectSuccessfulBookingCreationEmails({
to: `${otherTeamMember.email}`,
ics: {
filename: "event.ics",
iCalUID: iCalUID,
iCalUID: `${iCalUID}`,
method: "REQUEST",
},
links: [
{
@ -472,7 +481,8 @@ export function expectSuccessfulBookingCreationEmails({
to: `${guest.email}`,
ics: {
filename: "event.ics",
iCalUID: iCalUID,
iCalUID: `${iCalUID}`,
method: "REQUEST",
},
},
`${guest.name} <${guest.email}`
@ -529,6 +539,7 @@ export function expectCalendarEventCreationFailureEmails({
ics: {
filename: "event.ics",
iCalUID,
method: "REQUEST",
},
},
`${organizer.email}`
@ -541,6 +552,7 @@ export function expectCalendarEventCreationFailureEmails({
ics: {
filename: "event.ics",
iCalUID,
method: "REQUEST",
},
},
`${booker.name} <${booker.email}>`
@ -612,6 +624,7 @@ export function expectSuccessfulBookingRescheduledEmails({
ics: {
filename: "event.ics",
iCalUID,
method: "REQUEST",
},
appsStatus,
},
@ -625,6 +638,7 @@ export function expectSuccessfulBookingRescheduledEmails({
ics: {
filename: "event.ics",
iCalUID,
method: "REQUEST",
},
},
`${booker.name} <${booker.email}>`
@ -711,6 +725,7 @@ export function expectBookingRequestRescheduledEmails({
to: `${booker.email}`,
ics: {
filename: "event.ics",
method: "REQUEST",
},
},
`${booker.email}`
@ -724,6 +739,7 @@ export function expectBookingRequestRescheduledEmails({
to: `${loggedInUser.email}`,
ics: {
filename: "event.ics",
method: "REQUEST",
},
},
`${loggedInUser.email}`
@ -1074,3 +1090,11 @@ export async function expectBookingInDBToBeRescheduledFromTo({ from, to }: { fro
...to,
});
}
export function expectICalUIDAsString(iCalUID: string | undefined | null) {
if (typeof iCalUID !== "string") {
throw new Error("iCalUID is not a string");
}
return iCalUID;
}

View File

@ -202,6 +202,7 @@ export default class GoogleCalendarService implements Calendar {
useDefault: true,
},
guestsCanSeeOtherGuests: !!calEventRaw.seatsPerTimeSlot ? calEventRaw.seatsShowAttendees : true,
iCalUID: calEventRaw.iCalUID,
};
if (calEventRaw.location) {
@ -250,7 +251,6 @@ export default class GoogleCalendarService implements Calendar {
type: "google_calendar",
password: "",
url: "",
iCalUID: event.data.iCalUID,
};
} catch (error) {
this.log.error(

View File

@ -179,7 +179,7 @@ export default class EventManager {
}
const isCalendarType = isCalendarResult(result);
if (isCalendarType) {
evt.iCalUID = result.iCalUID || undefined;
evt.iCalUID = result.iCalUID || event.iCalUID || undefined;
}
return {

View File

@ -30,6 +30,7 @@ class CalendarEventClass implements CalendarEvent {
hideCalendarNotes?: boolean;
additionalNotes?: string | null | undefined;
recurrence?: string;
iCalUID?: string | null;
constructor(initProps?: CalendarEvent) {
// If more parameters are given we update this

View File

@ -0,0 +1,105 @@
import type { DateArray, ParticipationStatus, ParticipationRole, EventStatus } 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 type { CalendarEvent, Person } from "@calcom/types/Calendar";
export enum BookingAction {
Create = "create",
Cancel = "cancel",
Reschedule = "reschedule",
RequestReschedule = "request_reschedule",
LocationChange = "location_change",
}
const generateIcsString = ({
event,
title,
subtitle,
status,
role,
isRequestReschedule,
t,
}: {
event: CalendarEvent;
title: string;
subtitle: string;
status: EventStatus;
role: "attendee" | "organizer";
isRequestReschedule?: boolean;
t?: TFunction;
}) => {
// 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 === "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,
start: dayjs(event.startTime)
.utc()
.toArray()
.slice(0, 6)
.map((v, i) => (i === 1 ? v + 1 : v)) as DateArray,
startInputType: "utc",
productId: "calcom/ics",
title: event.title,
description: getTextBody(title, subtitle),
duration: { minutes: dayjs(event.endTime).diff(dayjs(event.startTime), "minute") },
organizer: { name: event.organizer.name, email: event.organizer.email },
...{ recurrenceRule },
attendees: [
...event.attendees.map((attendee: Person) => ({
name: attendee.name,
email: attendee.email,
partstat,
role: icsRole,
rsvp: true,
})),
...(event.team?.members
? event.team?.members.map((member: Person) => ({
name: member.name,
email: member.email,
partstat,
role: icsRole,
rsvp: true,
}))
: []),
],
method: "REQUEST",
status,
});
if (icsEvent.error) {
throw icsEvent.error;
}
return icsEvent.value;
};
export default generateIcsString;

View File

@ -0,0 +1,36 @@
import short from "short-uuid";
import { v5 as uuidv5 } from "uuid";
import { APP_NAME } from "@calcom/lib/constants";
/**
* This function returns the iCalUID if a uid is passed or if it is present in the event that is passed
* @param uid - the uid of the event
* @param event - an event that already has an iCalUID or one that has a uid
* @param defaultToEventUid - if true, will default to the event.uid if present
*
* @returns the iCalUID whether already present or generated
*/
const getICalUID = ({
uid,
event,
defaultToEventUid,
}: {
uid?: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
event?: { iCalUID?: string | null; uid?: string | null; [key: string]: any };
defaultToEventUid?: boolean;
}) => {
if (event?.iCalUID) return event.iCalUID;
if (defaultToEventUid && event?.uid) return `${event.uid}@${APP_NAME}`;
if (uid) return `${uid}@${APP_NAME}`;
const translator = short();
uid = translator.fromUUID(uuidv5(APP_NAME, uuidv5.URL));
return `${uid}@${APP_NAME}`;
};
export default getICalUID;

View File

@ -0,0 +1,137 @@
import { describe, expect } from "vitest";
import dayjs from "@calcom/dayjs";
import type { CalendarEvent } from "@calcom/types/Calendar";
import { test } from "@calcom/web/test/fixtures/fixtures";
import { buildCalendarEvent, buildPerson } from "../../../lib/test/builder";
import generateIcsString from "../generateIcsString";
const assertHasIcsString = (icsString: string | undefined) => {
if (!icsString) throw new Error("icsString is undefined");
expect(icsString).toBeDefined();
return icsString;
};
const testIcsStringContains = ({
icsString,
event,
status,
}: {
icsString: string;
event: CalendarEvent;
status: string;
}) => {
const DTSTART = event.startTime.split(".")[0].replace(/[-:]/g, "");
const startTime = dayjs(event.startTime);
const endTime = dayjs(event.endTime);
const duration = endTime.diff(startTime, "minute");
expect(icsString).toEqual(expect.stringContaining(`UID:${event.iCalUID}`));
// Sometimes the deeply equal stringMatching error appears. Don't want to add flakey tests
// expect(icsString).toEqual(expect.stringContaining(`SUMMARY:${event.title}`));
expect(icsString).toEqual(expect.stringContaining(`DTSTART:${DTSTART}`));
expect(icsString).toEqual(
expect.stringContaining(`ORGANIZER;CN=${event.organizer.name}:mailto:${event.organizer.email}`)
);
expect(icsString).toEqual(expect.stringContaining(`DURATION:PT${duration}M`));
expect(icsString).toEqual(expect.stringContaining(`STATUS:${status}`));
// Getting an error expected icsString to deeply equal stringMatching
// for (const attendee of event.attendees) {
// expect(icsString).toEqual(
// expect.stringMatching(
// `RSVP=TRUE;ROLE=REQ-PARTICIPANT;PARTSTAT=ACCEPTED;CN=${attendee.name}:mailto:${attendee.email}`
// )
// );
// }
};
describe("generateIcsString", () => {
test("when bookingAction is Create", () => {
const event = buildCalendarEvent({
iCalSequence: 0,
attendees: [buildPerson()],
});
const title = "new_event_scheduled_recurring";
const subtitle = "emailed_you_and_any_other_attendees";
const status = "CONFIRMED";
const icsString = generateIcsString({
event: event,
title,
subtitle,
role: "organizer",
status,
});
const assertedIcsString = assertHasIcsString(icsString);
testIcsStringContains({ icsString: assertedIcsString, event, status });
});
test("when bookingAction is Cancel", () => {
const event = buildCalendarEvent({
iCalSequence: 0,
attendees: [buildPerson()],
});
const title = "event_request_cancelled";
const subtitle = "emailed_you_and_any_other_attendees";
const status = "CANCELLED";
const icsString = generateIcsString({
event: event,
title,
subtitle,
role: "organizer",
status,
});
const assertedIcsString = assertHasIcsString(icsString);
testIcsStringContains({ icsString: assertedIcsString, event, status });
});
test("when bookingAction is Reschedule", () => {
const event = buildCalendarEvent({
iCalSequence: 0,
attendees: [buildPerson()],
});
const title = "event_type_has_been_rescheduled";
const subtitle = "emailed_you_and_any_other_attendees";
const status = "CONFIRMED";
const icsString = generateIcsString({
event: event,
title,
subtitle,
role: "organizer",
status,
});
const assertedIcsString = assertHasIcsString(icsString);
testIcsStringContains({ icsString: assertedIcsString, event, status });
});
test("when bookingAction is RequestReschedule", () => {
const event = buildCalendarEvent({
iCalSequence: 0,
attendees: [buildPerson()],
});
const title = "request_reschedule_title_organizer";
const subtitle = "request_reschedule_subtitle_organizer";
const status = "CANCELLED";
const icsString = generateIcsString({
event: event,
title,
subtitle,
role: "organizer",
status,
});
const assertedIcsString = assertHasIcsString(icsString);
testIcsStringContains({ icsString: assertedIcsString, event, status });
});
});

View File

@ -0,0 +1,29 @@
import { describe, expect } from "vitest";
import { APP_NAME } from "@calcom/lib/constants";
import { buildCalendarEvent } from "@calcom/lib/test/builder";
import { test } from "@calcom/web/test/fixtures/fixtures";
import getICalUID from "../getICalUID";
describe("getICalUid", () => {
test("returns iCalUID when passing a uid", () => {
const iCalUID = getICalUID({ uid: "123" });
expect(iCalUID).toEqual(`123@${APP_NAME}`);
});
test("returns iCalUID when passing an event", () => {
const event = buildCalendarEvent({ iCalUID: `123@${APP_NAME}` });
const iCalUID = getICalUID({ event });
expect(iCalUID).toEqual(`123@${APP_NAME}`);
});
test("returns new iCalUID when passing in an event with no iCalUID but has an uid", () => {
const event = buildCalendarEvent({ iCalUID: "" });
const iCalUID = getICalUID({ event, defaultToEventUid: true });
expect(iCalUID).toEqual(`${event.uid}@${APP_NAME}`);
});
test("returns new iCalUID when passing in an event with no iCalUID and uses uid passed", () => {
const event = buildCalendarEvent({ iCalUID: "" });
const iCalUID = getICalUID({ event, uid: "123" });
expect(iCalUID).toEqual(`123@${APP_NAME}`);
});
});

View File

@ -1,9 +1,21 @@
import { renderEmail } from "../";
import generateIcsString from "../lib/generateIcsString";
import AttendeeScheduledEmail from "./attendee-scheduled-email";
export default class AttendeeCancelledEmail extends AttendeeScheduledEmail {
protected async getNodeMailerPayload(): Promise<Record<string, unknown>> {
return {
icalEvent: {
filename: "event.ics",
content: generateIcsString({
event: this.calEvent,
title: this.t("event_request_cancelled"),
subtitle: this.t("emailed_you_and_any_other_attendees"),
status: "CANCELLED",
role: "attendee",
}),
method: "REQUEST",
},
to: `${this.attendee.name} <${this.attendee.email}>`,
from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`,
replyTo: this.calEvent.organizer.email,

View File

@ -1,4 +1,5 @@
import { renderEmail } from "../";
import generateIcsString from "../lib/generateIcsString";
import AttendeeScheduledEmail from "./attendee-scheduled-email";
export default class AttendeeLocationChangeEmail extends AttendeeScheduledEmail {
@ -6,7 +7,14 @@ export default class AttendeeLocationChangeEmail extends AttendeeScheduledEmail
return {
icalEvent: {
filename: "event.ics",
content: this.getiCalEventAsString(),
content: generateIcsString({
event: this.calEvent,
title: this.t("event_location_changed"),
subtitle: this.t("emailed_you_and_any_other_attendees"),
role: "attendee",
status: "CONFIRMED",
}),
method: "REQUEST",
},
to: `${this.attendee.name} <${this.attendee.email}>`,
from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`,

View File

@ -1,4 +1,5 @@
import { renderEmail } from "../";
import generateIcsString from "../lib/generateIcsString";
import AttendeeScheduledEmail from "./attendee-scheduled-email";
export default class AttendeeRescheduledEmail extends AttendeeScheduledEmail {
@ -6,7 +7,14 @@ export default class AttendeeRescheduledEmail extends AttendeeScheduledEmail {
return {
icalEvent: {
filename: "event.ics",
content: this.getiCalEventAsString(),
content: generateIcsString({
event: this.calEvent,
title: this.t("event_type_has_been_rescheduled"),
subtitle: this.t("emailed_you_and_any_other_attendees"),
role: "attendee",
status: "CONFIRMED",
}),
method: "REQUEST",
},
to: `${this.attendee.name} <${this.attendee.email}>`,
from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`,

View File

@ -1,16 +1,13 @@
import type { DateArray, ParticipationStatus, ParticipationRole } from "ics";
import { createEvent } from "ics";
// eslint-disable-next-line no-restricted-imports
import { cloneDeep } from "lodash";
import type { TFunction } from "next-i18next";
import { RRule } from "rrule";
import dayjs from "@calcom/dayjs";
import { getRichDescription } from "@calcom/lib/CalEventParser";
import { TimeFormat } from "@calcom/lib/timeFormat";
import type { CalendarEvent, Person } from "@calcom/types/Calendar";
import { renderEmail } from "../";
import generateIcsString from "../lib/generateIcsString";
import BaseEmail from "./_base-email";
export default class AttendeeScheduledEmail extends BaseEmail {
@ -32,65 +29,21 @@ export default class AttendeeScheduledEmail extends BaseEmail {
this.t = attendee.language.translate;
}
protected getiCalEventAsString(): string | undefined {
// Taking care of recurrence rule
let recurrenceRule: string | undefined = undefined;
if (this.calEvent.recurringEvent?.count) {
// ics appends "RRULE:" already, so removing it from RRule generated string
recurrenceRule = new RRule(this.calEvent.recurringEvent).toString().replace("RRULE:", "");
}
const partstat: ParticipationStatus = "ACCEPTED";
const role: ParticipationRole = "REQ-PARTICIPANT";
const icsEvent = createEvent({
uid: this.calEvent.iCalUID || this.calEvent.uid!,
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.calEvent.title,
description: this.getTextBody(),
duration: { minutes: dayjs(this.calEvent.endTime).diff(dayjs(this.calEvent.startTime), "minute") },
organizer: { name: this.calEvent.organizer.name, email: this.calEvent.organizer.email },
attendees: [
...this.calEvent.attendees.map((attendee: Person) => ({
name: attendee.name,
email: attendee.email,
partstat,
role,
rsvp: true,
})),
...(this.calEvent.team?.members
? this.calEvent.team?.members.map((member: Person) => ({
name: member.name,
email: member.email,
partstat,
role,
rsvp: true,
}))
: []),
],
method: "REQUEST",
...{ recurrenceRule },
status: "CONFIRMED",
});
if (icsEvent.error) {
throw icsEvent.error;
}
return icsEvent.value;
}
protected async getNodeMailerPayload(): Promise<Record<string, unknown>> {
const clonedCalEvent = cloneDeep(this.calEvent);
this.getiCalEventAsString();
return {
icalEvent: {
filename: "event.ics",
content: this.getiCalEventAsString(),
content: generateIcsString({
event: this.calEvent,
title: this.calEvent.recurringEvent?.count
? this.t("your_event_has_been_scheduled_recurring")
: this.t("your_event_has_been_scheduled"),
role: "attendee",
subtitle: "emailed_you_and_any_other_attendees",
status: "CONFIRMED",
}),
method: "REQUEST",
},
to: `${this.attendee.name} <${this.attendee.email}>`,

View File

@ -1,12 +1,9 @@
import type { DateArray } from "ics";
import { createEvent } from "ics";
import dayjs from "@calcom/dayjs";
import { getManageLink } from "@calcom/lib/CalEventParser";
import { APP_NAME } from "@calcom/lib/constants";
import type { CalendarEvent, Person } from "@calcom/types/Calendar";
import type { CalendarEvent } from "@calcom/types/Calendar";
import { renderEmail } from "..";
import generateIcsString from "../lib/generateIcsString";
import OrganizerScheduledEmail from "./organizer-scheduled-email";
export default class AttendeeWasRequestedToRescheduleEmail extends OrganizerScheduledEmail {
@ -22,7 +19,16 @@ export default class AttendeeWasRequestedToRescheduleEmail extends OrganizerSche
return {
icalEvent: {
filename: "event.ics",
content: this.getiCalEventAsString(),
content: generateIcsString({
event: this.calEvent,
title: this.t("request_reschedule_booking"),
subtitle: this.t("request_reschedule_subtitle", {
organizer: this.calEvent.organizer.name,
}),
role: "attendee",
status: "CANCELLED",
}),
method: "REQUEST",
},
from: `${APP_NAME} <${this.getMailerOptions().from}>`,
to: toAddresses.join(","),
@ -39,35 +45,6 @@ export default class AttendeeWasRequestedToRescheduleEmail extends OrganizerSche
};
}
// @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(),
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 getWhen(): string {
return `

View File

@ -53,7 +53,7 @@ ${this.t(
)}
${this.t(subtitle)}
${extraInfo}
${getRichDescription(this.calEvent)}
${getRichDescription(this.calEvent, this.t, true)}
${callToAction}
`.trim();
}

View File

@ -1,6 +1,7 @@
import { APP_NAME } from "@calcom/lib/constants";
import { renderEmail } from "../";
import generateIcsString from "../lib/generateIcsString";
import OrganizerScheduledEmail from "./organizer-scheduled-email";
export default class OrganizerCancelledEmail extends OrganizerScheduledEmail {
@ -8,6 +9,17 @@ export default class OrganizerCancelledEmail extends OrganizerScheduledEmail {
const toAddresses = [this.teamMember?.email || this.calEvent.organizer.email];
return {
icalEvent: {
filename: "event.ics",
content: generateIcsString({
event: this.calEvent,
title: this.t("event_request_cancelled"),
subtitle: this.t("emailed_you_and_any_other_attendees"),
status: "CANCELLED",
role: "organizer",
}),
method: "REQUEST",
},
from: `${APP_NAME} <${this.getMailerOptions().from}>`,
to: toAddresses.join(","),
subject: `${this.t("event_cancelled_subject", {

View File

@ -1,6 +1,7 @@
import { APP_NAME } from "@calcom/lib/constants";
import { renderEmail } from "../";
import generateIcsString from "../lib/generateIcsString";
import OrganizerScheduledEmail from "./organizer-scheduled-email";
export default class OrganizerLocationChangeEmail extends OrganizerScheduledEmail {
@ -10,7 +11,14 @@ export default class OrganizerLocationChangeEmail extends OrganizerScheduledEmai
return {
icalEvent: {
filename: "event.ics",
content: this.getiCalEventAsString(),
content: generateIcsString({
event: this.calEvent,
title: this.t("event_location_changed"),
subtitle: this.t("emailed_you_and_any_other_attendees"),
role: "organizer",
status: "CONFIRMED",
}),
method: "REQUEST",
},
from: `${APP_NAME} <${this.getMailerOptions().from}>`,
to: toAddresses.join(","),

View File

@ -7,6 +7,7 @@ import { APP_NAME } from "@calcom/lib/constants";
import type { CalendarEvent } from "@calcom/types/Calendar";
import { renderEmail } from "..";
import generateIcsString from "../lib/generateIcsString";
import OrganizerScheduledEmail from "./organizer-scheduled-email";
export default class OrganizerRequestedToRescheduleEmail extends OrganizerScheduledEmail {
@ -21,7 +22,18 @@ export default class OrganizerRequestedToRescheduleEmail extends OrganizerSchedu
return {
icalEvent: {
filename: "event.ics",
content: this.getiCalEventAsString(),
content: generateIcsString({
event: 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: "organizer",
status: "CANCELLED",
}),
method: "REQUEST",
},
from: `${APP_NAME} <${this.getMailerOptions().from}>`,
to: toAddresses.join(","),
@ -83,13 +95,11 @@ export default class OrganizerRequestedToRescheduleEmail extends OrganizerSchedu
}
// @OVERRIDE
protected getTextBody(title = "", subtitle = "", extraInfo = "", callToAction = ""): string {
protected getTextBody(title = "", subtitle = ""): string {
return `
${this.t(title)}
${this.t(subtitle)}
${extraInfo}
${getRichDescription(this.calEvent)}
${callToAction}
${getRichDescription(this.calEvent, this.t, true)}
`.trim();
}
}

View File

@ -1,6 +1,7 @@
import { APP_NAME } from "@calcom/lib/constants";
import { renderEmail } from "../";
import generateIcsString from "../lib/generateIcsString";
import OrganizerScheduledEmail from "./organizer-scheduled-email";
export default class OrganizerRescheduledEmail extends OrganizerScheduledEmail {
@ -10,7 +11,14 @@ export default class OrganizerRescheduledEmail extends OrganizerScheduledEmail {
return {
icalEvent: {
filename: "event.ics",
content: this.getiCalEventAsString(),
content: generateIcsString({
event: this.calEvent,
title: this.t("event_type_has_been_rescheduled"),
subtitle: this.t("emailed_you_and_any_other_attendees"),
role: "organizer",
status: "CONFIRMED",
}),
method: "REQUEST",
},
from: `${APP_NAME} <${this.getMailerOptions().from}>`,
to: toAddresses.join(","),

View File

@ -1,17 +1,14 @@
import type { DateArray } from "ics";
import { createEvent } from "ics";
// eslint-disable-next-line no-restricted-imports
import { cloneDeep } from "lodash";
import type { TFunction } from "next-i18next";
import { RRule } from "rrule";
import dayjs from "@calcom/dayjs";
import { getRichDescription } from "@calcom/lib/CalEventParser";
import { APP_NAME } from "@calcom/lib/constants";
import { TimeFormat } from "@calcom/lib/timeFormat";
import type { CalendarEvent, Person } from "@calcom/types/Calendar";
import { renderEmail } from "../";
import generateIcsString from "../lib/generateIcsString";
import BaseEmail from "./_base-email";
export default class OrganizerScheduledEmail extends BaseEmail {
@ -29,47 +26,6 @@ export default class OrganizerScheduledEmail extends BaseEmail {
this.teamMember = input.teamMember;
}
protected getiCalEventAsString(): string | undefined {
// Taking care of recurrence rule
let recurrenceRule: string | undefined = undefined;
if (this.calEvent.recurringEvent?.count) {
// ics appends "RRULE:" already, so removing it from RRule generated string
recurrenceRule = new RRule(this.calEvent.recurringEvent).toString().replace("RRULE:", "");
}
const icsEvent = createEvent({
uid: this.calEvent.iCalUID || this.calEvent.uid!,
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.calEvent.title,
description: this.getTextBody(),
duration: { minutes: dayjs(this.calEvent.endTime).diff(dayjs(this.calEvent.startTime), "minute") },
organizer: { name: this.calEvent.organizer.name, email: this.calEvent.organizer.email },
...{ recurrenceRule },
attendees: [
...this.calEvent.attendees.map((attendee: Person) => ({
name: attendee.name,
email: attendee.email,
})),
...(this.calEvent.team?.members
? this.calEvent.team?.members.map((member: Person) => ({
name: member.name,
email: member.email,
}))
: []),
],
status: "CONFIRMED",
});
if (icsEvent.error) {
throw icsEvent.error;
}
return icsEvent.value;
}
protected async getNodeMailerPayload(): Promise<Record<string, unknown>> {
const clonedCalEvent = cloneDeep(this.calEvent);
const toAddresses = [this.teamMember?.email || this.calEvent.organizer.email];
@ -77,7 +33,16 @@ export default class OrganizerScheduledEmail extends BaseEmail {
return {
icalEvent: {
filename: "event.ics",
content: this.getiCalEventAsString(),
content: generateIcsString({
event: 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: "organizer",
status: "CONFIRMED",
}),
method: "REQUEST",
},
from: `${APP_NAME} <${this.getMailerOptions().from}>`,
to: toAddresses.join(","),
@ -105,7 +70,7 @@ ${this.t(
)}
${this.t(subtitle)}
${extraInfo}
${getRichDescription(this.calEvent)}
${getRichDescription(this.calEvent, this.t, true)}
${callToAction}
`.trim();
}

View File

@ -115,6 +115,8 @@ async function getBookingToDelete(id: number | undefined, uid: string | undefine
scheduledJobs: true,
seatsReferences: true,
responses: true,
iCalUID: true,
iCalSequence: true,
},
});
}
@ -264,6 +266,8 @@ async function handler(req: CustomRequest) {
}),
seatsPerTimeSlot: bookingToDelete.eventType?.seatsPerTimeSlot,
seatsShowAttendees: bookingToDelete.eventType?.seatsShowAttendees,
iCalUID: bookingToDelete.iCalUID,
iCalSequence: bookingToDelete.iCalSequence + 1,
};
const dataForWebhooks = { evt, webhooks, eventTypeInfo };
@ -390,6 +394,8 @@ async function handler(req: CustomRequest) {
data: {
status: BookingStatus.CANCELLED,
cancellationReason: cancellationReason,
// Assume that canceling the booking is the last action
iCalSequence: evt.iCalSequence || 100,
},
select: {
startTime: true,

View File

@ -37,6 +37,7 @@ import {
sendScheduledEmails,
sendScheduledSeatsEmails,
} from "@calcom/emails";
import getICalUID from "@calcom/emails/lib/getICalUID";
import { getBookingFieldsWithSystemFields } from "@calcom/features/bookings/lib/getBookingFields";
import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses";
import { handleWebhookTrigger } from "@calcom/features/bookings/lib/handleWebhookTrigger";
@ -717,6 +718,7 @@ async function createBooking({
},
dynamicEventSlugRef,
dynamicGroupSlugRef,
iCalUID: evt.iCalUID ?? "",
user: {
connect: {
id: organizerUser.id,
@ -823,6 +825,21 @@ function getCustomInputsResponses(
return customInputsResponses;
}
function getICalSequence(originalRescheduledBooking: BookingType | null) {
// If new booking set the sequence to 0
if (!originalRescheduledBooking) {
return 0;
}
// If rescheduling and there is no sequence set, assume sequence should be 1
if (!originalRescheduledBooking.iCalSequence) {
return 1;
}
// If rescheduling then increment sequence by 1
return originalRescheduledBooking.iCalSequence + 1;
}
async function handler(
req: NextApiRequest & { userId?: number | undefined },
{
@ -1247,6 +1264,13 @@ async function handler(
const calEventUserFieldsResponses =
"calEventUserFieldsResponses" in reqBody ? reqBody.calEventUserFieldsResponses : null;
const iCalUID = getICalUID({
event: { iCalUID: originalRescheduledBooking?.iCalUID, uid: originalRescheduledBooking?.uid },
uid,
});
// For bookings made before introducing iCalSequence, assume that the sequence should start at 1. For new bookings start at 0.
const iCalSequence = getICalSequence(originalRescheduledBooking);
let evt: CalendarEvent = {
bookerUrl: await getBookerUrl(organizerUser),
type: eventType.slug,
@ -1283,6 +1307,8 @@ async function handler(
seatsPerTimeSlot: eventType.seatsPerTimeSlot,
seatsShowAvailabilityCount: eventType.seatsPerTimeSlot ? eventType.seatsShowAvailabilityCount : true,
schedulingType: eventType.schedulingType,
iCalUID,
iCalSequence,
};
if (isTeamEventType && eventType.schedulingType === "COLLECTIVE") {
@ -2488,6 +2514,18 @@ async function handler(
evt.appsStatus = handleAppsStatus(results, booking);
videoCallUrl =
metadata.hangoutLink || organizerOrFirstDynamicGroupMemberDefaultLocationUrl || videoCallUrl;
if (evt.iCalUID !== booking.iCalUID) {
// The eventManager could change the iCalUID. At this point we can update the DB record
await prisma.booking.update({
where: {
id: booking.id,
},
data: {
iCalUID: evt.iCalUID || booking.iCalUID,
},
});
}
}
if (noEmail !== true) {
let isHostConfirmationEmailsDisabled = false;

View File

@ -51,6 +51,7 @@ import {
expectBookingPaymentIntiatedWebhookToHaveBeenFired,
expectBrokenIntegrationEmails,
expectSuccessfulCalendarEventCreationInCalendar,
expectICalUIDAsString,
} from "@calcom/web/test/utils/bookingScenario/expects";
import { getMockRequestDataForBooking } from "@calcom/web/test/utils/bookingScenario/getMockRequestDataForBooking";
import { setupAndTeardown } from "@calcom/web/test/utils/bookingScenario/setupAndTeardown";
@ -141,7 +142,6 @@ describe("handleNewBooking", () => {
const calendarMock = mockCalendarToHaveNoBusySlots("googlecalendar", {
create: {
id: "MOCKED_GOOGLE_CALENDAR_EVENT_ID",
iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID",
},
});
@ -162,6 +162,7 @@ describe("handleNewBooking", () => {
});
const createdBooking = await handleNewBooking(req);
expect(createdBooking.responses).toContain({
email: booker.email,
name: booker.name,
@ -193,6 +194,7 @@ describe("handleNewBooking", () => {
meetingUrl: "https://UNUSED_URL",
},
],
iCalUID: createdBooking.iCalUID,
});
expectWorkflowToBeTriggered();
@ -201,6 +203,8 @@ describe("handleNewBooking", () => {
videoCallUrl: "http://mock-dailyvideo.example.com/meeting-1",
});
const iCalUID = expectICalUIDAsString(createdBooking.iCalUID);
expectSuccessfulBookingCreationEmails({
booking: {
uid: createdBooking.uid!,
@ -209,7 +213,7 @@ describe("handleNewBooking", () => {
booker,
organizer,
emails,
iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID",
iCalUID,
});
expectBookingCreatedWebhookToHaveBeenFired({
@ -290,7 +294,6 @@ describe("handleNewBooking", () => {
create: {
id: "GOOGLE_CALENDAR_EVENT_ID",
uid: "MOCK_ID",
iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID",
},
});
@ -342,6 +345,7 @@ describe("handleNewBooking", () => {
meetingUrl: "https://UNUSED_URL",
},
],
iCalUID: createdBooking.iCalUID,
});
expectWorkflowToBeTriggered();
@ -353,6 +357,8 @@ describe("handleNewBooking", () => {
calendarId: null,
});
const iCalUID = expectICalUIDAsString(createdBooking.iCalUID);
expectSuccessfulBookingCreationEmails({
booking: {
uid: createdBooking.uid!,
@ -360,7 +366,7 @@ describe("handleNewBooking", () => {
booker,
organizer,
emails,
iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID",
iCalUID,
});
expectBookingCreatedWebhookToHaveBeenFired({
booker,
@ -441,7 +447,6 @@ describe("handleNewBooking", () => {
create: {
uid: "MOCK_ID",
id: "GOOGLE_CALENDAR_EVENT_ID",
iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID",
},
});
@ -493,6 +498,7 @@ describe("handleNewBooking", () => {
meetingUrl: "https://UNUSED_URL",
},
],
iCalUID: createdBooking.iCalUID,
});
expectWorkflowToBeTriggered();
@ -501,6 +507,8 @@ describe("handleNewBooking", () => {
videoCallUrl: "http://mock-dailyvideo.example.com/meeting-1",
});
const iCalUID = expectICalUIDAsString(createdBooking.iCalUID);
expectSuccessfulBookingCreationEmails({
booking: {
uid: createdBooking.uid!,
@ -508,7 +516,7 @@ describe("handleNewBooking", () => {
booker,
organizer,
emails,
iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID",
iCalUID,
});
expectBookingCreatedWebhookToHaveBeenFired({
@ -705,7 +713,6 @@ describe("handleNewBooking", () => {
create: {
uid: "MOCK_ID",
id: "GOOGLE_CALENDAR_EVENT_ID",
iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID",
},
});
@ -757,6 +764,7 @@ describe("handleNewBooking", () => {
meetingUrl: "https://UNUSED_URL",
},
],
iCalUID: createdBooking.iCalUID,
});
expectWorkflowToBeTriggered();
@ -765,6 +773,8 @@ describe("handleNewBooking", () => {
videoCallUrl: "http://mock-dailyvideo.example.com/meeting-1",
});
const iCalUID = expectICalUIDAsString(createdBooking.iCalUID);
expectSuccessfulBookingCreationEmails({
booking: {
uid: createdBooking.uid!,
@ -772,7 +782,7 @@ describe("handleNewBooking", () => {
booker,
organizer,
emails,
iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID",
iCalUID,
});
expectBookingCreatedWebhookToHaveBeenFired({
@ -998,6 +1008,8 @@ describe("handleNewBooking", () => {
});
const createdBooking = await handleNewBooking(req);
const iCalUID = expectICalUIDAsString(createdBooking.iCalUID);
expectSuccessfulBookingCreationEmails({
booking: {
uid: createdBooking.uid!,
@ -1005,8 +1017,7 @@ describe("handleNewBooking", () => {
booker,
organizer,
emails,
// Because no calendar was involved, we don't have an ics UID
iCalUID: createdBooking.uid!,
iCalUID,
});
expectBookingCreatedWebhookToHaveBeenFired({
@ -1561,11 +1572,7 @@ describe("handleNewBooking", () => {
metadataLookupKey: "dailyvideo",
});
mockCalendarToHaveNoBusySlots("googlecalendar", {
create: {
iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID",
},
});
mockCalendarToHaveNoBusySlots("googlecalendar", {});
const mockBookingData = getMockRequestDataForBooking({
data: {
@ -1599,10 +1606,13 @@ describe("handleNewBooking", () => {
uid: createdBooking.uid!,
eventTypeId: mockBookingData.eventTypeId,
status: BookingStatus.ACCEPTED,
iCalUID: createdBooking.iCalUID,
});
expectWorkflowToBeTriggered();
const iCalUID = expectICalUIDAsString(createdBooking.iCalUID);
expectSuccessfulBookingCreationEmails({
booking: {
uid: createdBooking.uid!,
@ -1610,7 +1620,7 @@ describe("handleNewBooking", () => {
booker,
organizer,
emails,
iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID",
iCalUID,
});
expectBookingCreatedWebhookToHaveBeenFired({
@ -1686,11 +1696,7 @@ describe("handleNewBooking", () => {
metadataLookupKey: "dailyvideo",
});
mockCalendarToHaveNoBusySlots("googlecalendar", {
create: {
iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID",
},
});
mockCalendarToHaveNoBusySlots("googlecalendar", {});
const mockBookingData = getMockRequestDataForBooking({
data: {
@ -1724,6 +1730,7 @@ describe("handleNewBooking", () => {
uid: createdBooking.uid!,
eventTypeId: mockBookingData.eventTypeId,
status: BookingStatus.PENDING,
iCalUID: createdBooking.iCalUID,
});
expectWorkflowToBeTriggered();
@ -1873,11 +1880,7 @@ describe("handleNewBooking", () => {
apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]],
});
mockCalendarToHaveNoBusySlots("googlecalendar", {
create: {
iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID",
},
});
mockCalendarToHaveNoBusySlots("googlecalendar", {});
await createBookingScenario(scenarioData);
const createdBooking = await handleNewBooking(req);
@ -1896,10 +1899,13 @@ describe("handleNewBooking", () => {
uid: createdBooking.uid!,
eventTypeId: mockBookingData.eventTypeId,
status: BookingStatus.ACCEPTED,
iCalUID: createdBooking.iCalUID,
});
expectWorkflowToBeTriggered();
const iCalUID = expectICalUIDAsString(createdBooking.iCalUID);
expectSuccessfulBookingCreationEmails({
booking: {
uid: createdBooking.uid!,
@ -1907,7 +1913,7 @@ describe("handleNewBooking", () => {
booker,
organizer,
emails,
iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID",
iCalUID,
});
expectBookingCreatedWebhookToHaveBeenFired({
booker,

View File

@ -75,6 +75,7 @@ describe("handleNewBooking", () => {
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
const uidOfBookingToBeRescheduled = "n5Wv3eHgconAED2j4gcVhP";
const iCalUID = `${uidOfBookingToBeRescheduled}@Cal.com`;
await createBookingScenario(
getScenarioData({
webhooks: [
@ -128,6 +129,7 @@ describe("handleNewBooking", () => {
credentialId: undefined,
},
],
iCalUID,
},
],
organizer,
@ -145,7 +147,7 @@ describe("handleNewBooking", () => {
},
update: {
uid: "UPDATED_MOCK_ID",
iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID",
iCalUID,
},
});
@ -259,7 +261,7 @@ describe("handleNewBooking", () => {
booker,
organizer,
emails,
iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID",
iCalUID,
appsStatus: [
getMockPassingAppStatus({ slug: appStoreMetadata.dailyvideo.slug }),
getMockPassingAppStatus({ slug: appStoreMetadata.googlecalendar.slug }),
@ -278,7 +280,7 @@ describe("handleNewBooking", () => {
);
test(
`should rechedule a booking successfully and update the event in the same externalCalendarId as was used in the booking earlier.
`should reschedule a booking successfully and update the event in the same externalCalendarId as was used in the booking earlier.
1. Should cancel the existing booking
2. Should create a new booking in the database
3. Should send emails to the booker as well as organizer
@ -302,6 +304,7 @@ describe("handleNewBooking", () => {
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
const uidOfBookingToBeRescheduled = "n5Wv3eHgconAED2j4gcVhP";
const iCalUID = `${uidOfBookingToBeRescheduled}@Cal.com`;
await createBookingScenario(
getScenarioData({
webhooks: [
@ -355,6 +358,7 @@ describe("handleNewBooking", () => {
credentialId: undefined,
},
],
iCalUID,
},
],
organizer,
@ -371,7 +375,7 @@ describe("handleNewBooking", () => {
uid: "MOCK_ID",
},
update: {
iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID",
iCalUID,
uid: "UPDATED_MOCK_ID",
},
});
@ -467,7 +471,7 @@ describe("handleNewBooking", () => {
booker,
organizer,
emails,
iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID",
iCalUID,
});
expectBookingRescheduledWebhookToHaveBeenFired({
booker,
@ -1117,6 +1121,7 @@ describe("handleNewBooking", () => {
});
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
const uidOfBookingToBeRescheduled = "n5Wv3eHgconAED2j4gcVhP";
const iCalUID = `${uidOfBookingToBeRescheduled}@Cal.com`;
const scenarioData = getScenarioData({
webhooks: [
@ -1168,6 +1173,7 @@ describe("handleNewBooking", () => {
credentialId: 1,
}),
],
iCalUID,
},
],
organizer,

View File

@ -202,10 +202,12 @@ export default function ApiKeyDialogForm({
);
}}
/>
{!watchNeverExpires && <span className="text-subtle mt-2 text-xs">
{t("api_key_expires_on")}
<span className="font-bold"> {dayjs(expiryDate).format("DD-MM-YYYY")}</span>
</span>}
{!watchNeverExpires && (
<span className="text-subtle mt-2 text-xs">
{t("api_key_expires_on")}
<span className="font-bold"> {dayjs(expiryDate).format("DD-MM-YYYY")}</span>
</span>
)}
</div>
)}

View File

@ -176,7 +176,11 @@ export const getRescheduleLink = (calEvent: CalendarEvent): string => {
return `${calEvent.bookerUrl ?? WEBAPP_URL}/reschedule/${seatUid ? seatUid : Uid}`;
};
export const getRichDescription = (calEvent: CalendarEvent, t_?: TFunction /*, attendee?: Person*/) => {
export const getRichDescription = (
calEvent: CalendarEvent,
t_?: TFunction /*, attendee?: Person*/,
includeAppStatus = false
) => {
const t = t_ ?? calEvent.organizer.language.translate;
return `
@ -189,7 +193,7 @@ ${getLocation(calEvent)}
${getDescription(calEvent, t)}
${getAdditionalNotes(calEvent, t)}
${getUserFieldsResponses(calEvent)}
${getAppsStatus(calEvent, t)}
${includeAppStatus ? getAppsStatus(calEvent, t) : ""}
${
// TODO: Only the original attendee can make changes to the event
// Guests cannot

View File

@ -11,6 +11,7 @@ import { buildCalendarEvent, buildVideoCallData } from "./builder";
vi.mock("@calcom/lib/constants", () => ({
WEBAPP_URL: "http://localhost:3000",
APP_NAME: "Cal.com",
}));
vi.mock("short-uuid", () => ({

View File

@ -2,6 +2,7 @@ import { faker } from "@faker-js/faker";
import type { Booking, EventType, Prisma, Webhook } from "@prisma/client";
import type { TFunction } from "next-i18next";
import getICalUID from "@calcom/emails/lib/getICalUID";
import { BookingStatus } from "@calcom/prisma/enums";
import type { CalendarEvent, Person, VideoCallData } from "@calcom/types/Calendar";
@ -31,9 +32,10 @@ export const buildPerson = (person?: Partial<Person>): Person => {
};
export const buildBooking = (booking?: Partial<Booking>): Booking => {
const uid = faker.datatype.uuid();
return {
id: faker.datatype.number(),
uid: faker.datatype.uuid(),
uid,
userId: null,
eventTypeId: null,
title: faker.lorem.sentence(),
@ -59,6 +61,8 @@ export const buildBooking = (booking?: Partial<Booking>): Booking => {
metadata: null,
responses: null,
isRecorded: false,
iCalUID: getICalUID({ uid }),
iCalSequence: 0,
...booking,
};
};
@ -155,8 +159,10 @@ export const buildSubscriberEvent = (booking?: Partial<Booking>) => {
};
export const buildCalendarEvent = (event?: Partial<CalendarEvent>): CalendarEvent => {
const uid = faker.datatype.uuid();
return {
uid: faker.datatype.uuid(),
uid,
iCalUID: getICalUID({ uid }),
type: faker.helpers.arrayElement(["event", "meeting"]),
title: faker.lorem.sentence(),
startTime: faker.date.future().toISOString(),

View File

@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE "Booking" ADD COLUMN "iCalSequence" INTEGER NOT NULL DEFAULT 0,
ADD COLUMN "iCalUID" TEXT DEFAULT '';

View File

@ -433,6 +433,8 @@ model Booking {
/// @zod.custom(imports.bookingMetadataSchema)
metadata Json?
isRecorded Boolean @default(false)
iCalUID String? @default("")
iCalSequence Int @default(0)
@@index([eventTypeId])
@@index([userId])

View File

@ -133,6 +133,7 @@ export async function createUserAndEventType({
},
},
status: bookingInput.status,
iCalUID: "",
},
});
console.log(

View File

@ -65,6 +65,7 @@ export const requestRescheduleHandler = async ({ ctx, input }: RequestReschedule
scheduledJobs: true,
workflowReminders: true,
responses: true,
iCalUID: true,
},
where: {
uid: bookingId,
@ -186,6 +187,7 @@ export const requestRescheduleHandler = async ({ ctx, input }: RequestReschedule
tAttendees
),
organizer: userAsPeopleType,
iCalUID: bookingToReschedule.iCalUID,
});
const director = new CalendarEventDirector();
@ -257,6 +259,7 @@ export const requestRescheduleHandler = async ({ ctx, input }: RequestReschedule
? [bookingToReschedule?.destinationCalendar]
: [],
cancellationReason: `Please reschedule. ${cancellationReason}`, // TODO::Add i18-next for this
iCalUID: bookingToReschedule?.iCalUID,
};
// Send webhook

View File

@ -185,6 +185,7 @@ export interface CalendarEvent {
seatsPerTimeSlot?: number | null;
schedulingType?: SchedulingType | null;
iCalUID?: string | null;
iCalSequence?: number | null;
// It has responses to all the fields(system + user)
responses?: CalEventResponses | null;