Skip to content

Automated Test: guest-management-enhanced #368

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions apps/web/components/booking/BookingListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import {
Tooltip,
} from "@calcom/ui";

import { AddGuestsDialog } from "@components/dialog/AddGuestsDialog";
import { ChargeCardDialog } from "@components/dialog/ChargeCardDialog";
import { EditLocationDialog } from "@components/dialog/EditLocationDialog";
import { ReassignDialog } from "@components/dialog/ReassignDialog";
Expand Down Expand Up @@ -189,6 +190,14 @@ function BookingListItem(booking: BookingItemProps) {
},
icon: "map-pin" as const,
},
{
id: "add_members",
label: t("additional_guests"),
onClick: () => {
setIsOpenAddGuestsDialog(true);
},
icon: "user-plus" as const,
},
];

if (booking.eventType.schedulingType === SchedulingType.ROUND_ROBIN) {
Expand Down Expand Up @@ -256,6 +265,7 @@ function BookingListItem(booking: BookingItemProps) {
const [isOpenRescheduleDialog, setIsOpenRescheduleDialog] = useState(false);
const [isOpenReassignDialog, setIsOpenReassignDialog] = useState(false);
const [isOpenSetLocationDialog, setIsOpenLocationDialog] = useState(false);
const [isOpenAddGuestsDialog, setIsOpenAddGuestsDialog] = useState(false);
const setLocationMutation = trpc.viewer.bookings.editLocation.useMutation({
onSuccess: () => {
showToast(t("location_updated"), "success");
Expand Down Expand Up @@ -344,6 +354,11 @@ function BookingListItem(booking: BookingItemProps) {
setShowLocationModal={setIsOpenLocationDialog}
teamId={booking.eventType?.team?.id}
/>
<AddGuestsDialog
isOpenDialog={isOpenAddGuestsDialog}
setIsOpenDialog={setIsOpenAddGuestsDialog}
bookingId={booking.id}
/>
{booking.paid && booking.payment[0] && (
<ChargeCardDialog
isOpenDialog={chargeCardDialogIsOpen}
Expand Down
107 changes: 107 additions & 0 deletions apps/web/components/dialog/AddGuestsDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import type { Dispatch, SetStateAction } from "react";
import { useState } from "react";
import { z } from "zod";

import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import {
Button,
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
MultiEmail,
Icon,
showToast,
} from "@calcom/ui";

interface IAddGuestsDialog {
isOpenDialog: boolean;
setIsOpenDialog: Dispatch<SetStateAction<boolean>>;
bookingId: number;
}

export const AddGuestsDialog = (props: IAddGuestsDialog) => {
const { t } = useLocale();
const ZAddGuestsInputSchema = z.array(z.string().email()).refine((emails) => {
const uniqueEmails = new Set(emails);
return uniqueEmails.size === emails.length;
});
const { isOpenDialog, setIsOpenDialog, bookingId } = props;
const utils = trpc.useUtils();
const [multiEmailValue, setMultiEmailValue] = useState<string[]>([""]);
const [isInvalidEmail, setIsInvalidEmail] = useState(false);

const addGuestsMutation = trpc.viewer.bookings.addGuests.useMutation({
onSuccess: async () => {
showToast(t("guests_added"), "success");
setIsOpenDialog(false);
setMultiEmailValue([""]);
utils.viewer.bookings.invalidate();
},
onError: (err) => {
const message = `${err.data?.code}: ${t(err.message)}`;
showToast(message || t("unable_to_add_guests"), "error");
},
Comment on lines +35 to +45

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Avoid emitting “undefined:” in the error toast.

message is always a non-empty string due to the template literal, so the fallback never runs and “undefined:” can leak into the UI.

🐛 Suggested fix
   onError: (err) => {
-    const message = `${err.data?.code}: ${t(err.message)}`;
-    showToast(message || t("unable_to_add_guests"), "error");
+    const code = err.data?.code;
+    const text = err.message ? t(err.message) : t("unable_to_add_guests");
+    const message = code ? `${code}: ${text}` : text;
+    showToast(message, "error");
   },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const addGuestsMutation = trpc.viewer.bookings.addGuests.useMutation({
onSuccess: async () => {
showToast(t("guests_added"), "success");
setIsOpenDialog(false);
setMultiEmailValue([""]);
utils.viewer.bookings.invalidate();
},
onError: (err) => {
const message = `${err.data?.code}: ${t(err.message)}`;
showToast(message || t("unable_to_add_guests"), "error");
},
const addGuestsMutation = trpc.viewer.bookings.addGuests.useMutation({
onSuccess: async () => {
showToast(t("guests_added"), "success");
setIsOpenDialog(false);
setMultiEmailValue([""]);
utils.viewer.bookings.invalidate();
},
onError: (err) => {
const code = err.data?.code;
const text = err.message ? t(err.message) : t("unable_to_add_guests");
const message = code ? `${code}: ${text}` : text;
showToast(message, "error");
},
🤖 Prompt for AI Agents
In `@apps/web/components/dialog/AddGuestsDialog.tsx` around lines 35 - 45, The
error toast currently constructs message with a template literal in
addGuestsMutation.onError which causes "undefined:" to appear when
err.data?.code is missing; change the construction to only prefix the code when
present (e.g., if err.data?.code then `${err.data.code}: ${t(err.message)}` else
use t(err.message) or fallback to t("unable_to_add_guests")), then pass that
safe string to showToast to avoid emitting "undefined:".

});

const handleAdd = () => {
if (multiEmailValue.length === 0) {
return;
}
const validationResult = ZAddGuestsInputSchema.safeParse(multiEmailValue);
if (validationResult.success) {
addGuestsMutation.mutate({ bookingId, guests: multiEmailValue });
} else {
setIsInvalidEmail(true);
}
};
Comment on lines +48 to +58

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Clear invalid state on edits and ignore empty entries before validation.

Once isInvalidEmail flips to true, it never clears when the user fixes inputs. Also the default [""] will always fail validation. Normalize and filter empties, and reset the error on success (or on input change).

🛠️ Suggested adjustment
-const handleAdd = () => {
-  if (multiEmailValue.length === 0) {
+const handleAdd = () => {
+  const emails = multiEmailValue.map((e) => e.trim()).filter(Boolean);
+  if (emails.length === 0) {
     return;
   }
-  const validationResult = ZAddGuestsInputSchema.safeParse(multiEmailValue);
+  const validationResult = ZAddGuestsInputSchema.safeParse(emails);
   if (validationResult.success) {
-    addGuestsMutation.mutate({ bookingId, guests: multiEmailValue });
+    setIsInvalidEmail(false);
+    addGuestsMutation.mutate({ bookingId, guests: emails });
   } else {
     setIsInvalidEmail(true);
   }
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handleAdd = () => {
if (multiEmailValue.length === 0) {
return;
}
const validationResult = ZAddGuestsInputSchema.safeParse(multiEmailValue);
if (validationResult.success) {
addGuestsMutation.mutate({ bookingId, guests: multiEmailValue });
} else {
setIsInvalidEmail(true);
}
};
const handleAdd = () => {
const emails = multiEmailValue.map((e) => e.trim()).filter(Boolean);
if (emails.length === 0) {
return;
}
const validationResult = ZAddGuestsInputSchema.safeParse(emails);
if (validationResult.success) {
setIsInvalidEmail(false);
addGuestsMutation.mutate({ bookingId, guests: emails });
} else {
setIsInvalidEmail(true);
}
};
🤖 Prompt for AI Agents
In `@apps/web/components/dialog/AddGuestsDialog.tsx` around lines 48 - 58, Trim
and filter out empty strings from multiEmailValue before validation and ignore
validation if the resulting array is empty; use the cleaned array when calling
ZAddGuestsInputSchema.safeParse and when invoking addGuestsMutation.mutate({
bookingId, guests }); also ensure isInvalidEmail is cleared (call
setIsInvalidEmail(false)) when the input changes and after a successful mutation
in handleAdd so the error state resets once the user fixes entries.


return (
<Dialog open={isOpenDialog} onOpenChange={setIsOpenDialog}>
<DialogContent enableOverflow>
<div className="flex flex-row space-x-3">
<div className="bg-subtle flex h-10 w-10 flex-shrink-0 justify-center rounded-full ">
<Icon name="user-plus" className="m-auto h-6 w-6" />
</div>
<div className="w-full pt-1">
<DialogHeader title={t("additional_guests")} />
<MultiEmail
label={t("add_emails")}
value={multiEmailValue}
readOnly={false}
setValue={setMultiEmailValue}
/>

{isInvalidEmail && (
<div className="my-4 flex text-sm text-red-700">
<div className="flex-shrink-0">
<Icon name="triangle-alert" className="h-5 w-5" />
</div>
<div className="ml-3">
<p className="font-medium">{t("emails_must_be_unique_valid")}</p>
</div>
</div>
)}

<DialogFooter>
<Button
onClick={() => {
setMultiEmailValue([""]);
setIsInvalidEmail(false);
setIsOpenDialog(false);
}}
Comment on lines +60 to +93

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Reset dialog state when closing via overlay/escape.

Closing the dialog through onOpenChange doesn’t clear emails or the error state, so reopening can show stale inputs/error banners.

🧹 Suggested fix
-<Dialog open={isOpenDialog} onOpenChange={setIsOpenDialog}>
+<Dialog
+  open={isOpenDialog}
+  onOpenChange={(open) => {
+    if (!open) {
+      setMultiEmailValue([""]);
+      setIsInvalidEmail(false);
+    }
+    setIsOpenDialog(open);
+  }}>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return (
<Dialog open={isOpenDialog} onOpenChange={setIsOpenDialog}>
<DialogContent enableOverflow>
<div className="flex flex-row space-x-3">
<div className="bg-subtle flex h-10 w-10 flex-shrink-0 justify-center rounded-full ">
<Icon name="user-plus" className="m-auto h-6 w-6" />
</div>
<div className="w-full pt-1">
<DialogHeader title={t("additional_guests")} />
<MultiEmail
label={t("add_emails")}
value={multiEmailValue}
readOnly={false}
setValue={setMultiEmailValue}
/>
{isInvalidEmail && (
<div className="my-4 flex text-sm text-red-700">
<div className="flex-shrink-0">
<Icon name="triangle-alert" className="h-5 w-5" />
</div>
<div className="ml-3">
<p className="font-medium">{t("emails_must_be_unique_valid")}</p>
</div>
</div>
)}
<DialogFooter>
<Button
onClick={() => {
setMultiEmailValue([""]);
setIsInvalidEmail(false);
setIsOpenDialog(false);
}}
return (
<Dialog
open={isOpenDialog}
onOpenChange={(open) => {
if (!open) {
setMultiEmailValue([""]);
setIsInvalidEmail(false);
}
setIsOpenDialog(open);
}}>
<DialogContent enableOverflow>
<div className="flex flex-row space-x-3">
<div className="bg-subtle flex h-10 w-10 flex-shrink-0 justify-center rounded-full ">
<Icon name="user-plus" className="m-auto h-6 w-6" />
</div>
<div className="w-full pt-1">
<DialogHeader title={t("additional_guests")} />
<MultiEmail
label={t("add_emails")}
value={multiEmailValue}
readOnly={false}
setValue={setMultiEmailValue}
/>
{isInvalidEmail && (
<div className="my-4 flex text-sm text-red-700">
<div className="flex-shrink-0">
<Icon name="triangle-alert" className="h-5 w-5" />
</div>
<div className="ml-3">
<p className="font-medium">{t("emails_must_be_unique_valid")}</p>
</div>
</div>
)}
<DialogFooter>
<Button
onClick={() => {
setMultiEmailValue([""]);
setIsInvalidEmail(false);
setIsOpenDialog(false);
}}
🤖 Prompt for AI Agents
In `@apps/web/components/dialog/AddGuestsDialog.tsx` around lines 60 - 93, The
dialog's onOpenChange currently points directly to setIsOpenDialog so closing
via overlay/escape doesn't clear multiEmailValue or isInvalidEmail; change the
handler for onOpenChange to a wrapper that calls setIsOpenDialog(open) and when
open is false resets state (call setMultiEmailValue([""]) and
setIsInvalidEmail(false)), or alternatively add a useEffect watching
isOpenDialog that clears multiEmailValue and isInvalidEmail when isOpenDialog
becomes false; update the Dialog prop (currently onOpenChange={setIsOpenDialog})
to use this wrapper/behavior and ensure the same reset logic used in the
explicit Close button is reused.

type="button"
color="secondary">
{t("cancel")}
</Button>
<Button data-testid="add_members" loading={addGuestsMutation.isPending} onClick={handleAdd}>
{t("add")}
</Button>
</DialogFooter>
</div>
</div>
</DialogContent>
</Dialog>
);
};
6 changes: 6 additions & 0 deletions apps/web/public/static/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -1119,7 +1119,9 @@
"impersonating_user_warning": "Impersonating username \"{{user}}\".",
"impersonating_stop_instructions": "Click here to stop",
"event_location_changed": "Updated - Your event changed the location",
"new_guests_added": "Added - New guests added to your event",
"location_changed_event_type_subject": "Location Changed: {{eventType}} with {{name}} at {{date}}",
"guests_added_event_type_subject": "Guests Added: {{eventType}} with {{name}} at {{date}}",
"current_location": "Current Location",
"new_location": "New Location",
"session": "Session",
Expand All @@ -1131,7 +1133,10 @@
"set_location": "Set Location",
"update_location": "Update Location",
"location_updated": "Location updated",
"guests_added": "Guests added",
"unable_to_add_guests": "Unable to add guests",
"email_validation_error": "That doesn't look like an email address",
"emails_must_be_unique_valid": "Emails must be unique and valid",
"url_validation_error": "That doesn't look like a URL",
"place_where_cal_widget_appear": "Place this code in your HTML where you want your {{appName}} widget to appear.",
"create_update_react_component": "Create or update an existing React component as shown below.",
Expand Down Expand Up @@ -2426,6 +2431,7 @@
"primary": "Primary",
"make_primary": "Make primary",
"add_email": "Add Email",
"add_emails": "Add Emails",
"add_email_description": "Add an email address to replace your primary or to use as an alternative email on your event types.",
"confirm_email": "Confirm your email",
"scheduler_first_name": "The first name of the person booking",
Expand Down
28 changes: 28 additions & 0 deletions packages/emails/email-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import type { EmailVerifyLink } from "./templates/account-verify-email";
import AccountVerifyEmail from "./templates/account-verify-email";
import type { OrganizationNotification } from "./templates/admin-organization-notification";
import AdminOrganizationNotification from "./templates/admin-organization-notification";
import AttendeeAddGuestsEmail from "./templates/attendee-add-guests-email";
import AttendeeAwaitingPaymentEmail from "./templates/attendee-awaiting-payment-email";
import AttendeeCancelledEmail from "./templates/attendee-cancelled-email";
import AttendeeCancelledSeatEmail from "./templates/attendee-cancelled-seat-email";
Expand Down Expand Up @@ -48,6 +49,7 @@ import type { OrganizationCreation } from "./templates/organization-creation-ema
import OrganizationCreationEmail from "./templates/organization-creation-email";
import type { OrganizationEmailVerify } from "./templates/organization-email-verification";
import OrganizationEmailVerification from "./templates/organization-email-verification";
import OrganizerAddGuestsEmail from "./templates/organizer-add-guests-email";
import OrganizerAttendeeCancelledSeatEmail from "./templates/organizer-attendee-cancelled-seat-email";
import OrganizerCancelledEmail from "./templates/organizer-cancelled-email";
import OrganizerDailyVideoDownloadRecordingEmail from "./templates/organizer-daily-video-download-recording-email";
Expand Down Expand Up @@ -520,6 +522,32 @@ export const sendLocationChangeEmails = async (

await Promise.all(emailsToSend);
};
export const sendAddGuestsEmails = async (calEvent: CalendarEvent, newGuests: string[]) => {
const calendarEvent = formatCalEvent(calEvent);

const emailsToSend: Promise<unknown>[] = [];
emailsToSend.push(sendEmail(() => new OrganizerAddGuestsEmail({ calEvent: calendarEvent })));

if (calendarEvent.team?.members) {
for (const teamMember of calendarEvent.team.members) {
emailsToSend.push(
sendEmail(() => new OrganizerAddGuestsEmail({ calEvent: calendarEvent, teamMember }))
);
}
}

emailsToSend.push(
...calendarEvent.attendees.map((attendee) => {
if (newGuests.includes(attendee.email)) {
return sendEmail(() => new AttendeeScheduledEmail(calendarEvent, attendee));
} else {
return sendEmail(() => new AttendeeAddGuestsEmail(calendarEvent, attendee));
}
})
);

await Promise.all(emailsToSend);
};
Comment on lines +525 to +550

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

rg -n "eventTypeDisableHostEmail|eventTypeDisableAttendeeEmail" packages/emails/email-manager.ts -A 2 -B 2

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 5574


🏁 Script executed:

# Check the function signatures of the compared functions
rg -n "export const send(Scheduled|Rescheduled)Emails" packages/emails/email-manager.ts -A 3

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 381


🏁 Script executed:

# Look for all functions that use eventTypeMetadata parameter
rg -n "eventTypeMetadata" packages/emails/email-manager.ts | head -20

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 1272


Add eventTypeMetadata parameter and email disable checks to maintain consistency.

All other email functions in this file (e.g., sendScheduledEmails, sendRescheduledEmails, sendCancelledEmails) accept eventTypeMetadata and check eventTypeDisableHostEmail and eventTypeDisableAttendeeEmail before sending emails. The sendAddGuestsEmails function should either:

  1. Accept eventTypeMetadata and perform these checks for consistency with the rest of the codebase, or
  2. Add a comment explaining why guest addition notifications bypass these disable settings.
🤖 Prompt for AI Agents
In `@packages/emails/email-manager.ts` around lines 525 - 550, sendAddGuestsEmails
currently lacks the eventTypeMetadata parameter and does not honor
eventTypeDisableHostEmail/eventTypeDisableAttendeeEmail like other email
functions (sendScheduledEmails, sendRescheduledEmails, sendCancelledEmails);
update sendAddGuestsEmails to accept an eventTypeMetadata argument and before
queuing emails, check eventTypeDisableHostEmail to skip organizer/teamMember
emails and eventTypeDisableAttendeeEmail to skip attendee emails (keeping
existing organizer/team member looping and attendee mapping logic but gated by
those flags), or alternatively add a clear comment in sendAddGuestsEmails
explaining why guest-add notifications intentionally bypass those disable flags.

export const sendFeedbackEmail = async (feedback: Feedback) => {
await sendEmail(() => new FeedbackEmail(feedback));
};
Expand Down
10 changes: 10 additions & 0 deletions packages/emails/src/templates/AttendeeAddGuestsEmail.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { AttendeeScheduledEmail } from "./AttendeeScheduledEmail";

export const AttendeeAddGuestsEmail = (props: React.ComponentProps<typeof AttendeeScheduledEmail>) => (
<AttendeeScheduledEmail
title="new_guests_added"
headerType="calendarCircle"
subject="guests_added_event_type_subject"
{...props}
/>
);
11 changes: 11 additions & 0 deletions packages/emails/src/templates/OrganizerAddGuestsEmail.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { OrganizerScheduledEmail } from "./OrganizerScheduledEmail";

export const OrganizerAddGuestsEmail = (props: React.ComponentProps<typeof OrganizerScheduledEmail>) => (
<OrganizerScheduledEmail
title="new_guests_added"
headerType="calendarCircle"
subject="guests_added_event_type_subject"
callToAction={null}
{...props}
/>
);
2 changes: 2 additions & 0 deletions packages/emails/src/templates/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,6 @@ export { AdminOrganizationNotificationEmail } from "./AdminOrganizationNotificat
export { BookingRedirectEmailNotification } from "./BookingRedirectEmailNotification";
export { VerifyEmailChangeEmail } from "./VerifyEmailChangeEmail";
export { OrganizationCreationEmail } from "./OrganizationCreationEmail";
export { OrganizerAddGuestsEmail } from "./OrganizerAddGuestsEmail";
export { AttendeeAddGuestsEmail } from "./AttendeeAddGuestsEmail";
export { OrganizationAdminNoSlotsEmail } from "./OrganizationAdminNoSlots";
34 changes: 34 additions & 0 deletions packages/emails/templates/attendee-add-guests-email.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { renderEmail } from "../";
import generateIcsString from "../lib/generateIcsString";
import AttendeeScheduledEmail from "./attendee-scheduled-email";

export default class AttendeeAddGuestsEmail extends AttendeeScheduledEmail {
protected async getNodeMailerPayload(): Promise<Record<string, unknown>> {
return {
icalEvent: {
filename: "event.ics",
content: generateIcsString({
event: this.calEvent,
title: this.t("new_guests_added"),
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}>`,
replyTo: this.calEvent.organizer.email,
subject: `${this.t("guests_added_event_type_subject", {
eventType: this.calEvent.type,
name: this.calEvent.team?.name || this.calEvent.organizer.name,
date: this.getFormattedDate(),
})}`,
html: await renderEmail("AttendeeAddGuestsEmail", {
calEvent: this.calEvent,
attendee: this.attendee,
}),
text: this.getTextBody("new_guests_added"),
};
}
}
38 changes: 38 additions & 0 deletions packages/emails/templates/organizer-add-guests-email.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { APP_NAME } from "@calcom/lib/constants";

import { renderEmail } from "../";
import generateIcsString from "../lib/generateIcsString";
import OrganizerScheduledEmail from "./organizer-scheduled-email";

export default class OrganizerAddGuestsEmail extends OrganizerScheduledEmail {
protected async getNodeMailerPayload(): Promise<Record<string, unknown>> {
const toAddresses = [this.teamMember?.email || this.calEvent.organizer.email];

return {
icalEvent: {
filename: "event.ics",
content: generateIcsString({
event: this.calEvent,
title: this.t("new_guests_added"),
subtitle: this.t("emailed_you_and_any_other_attendees"),
role: "organizer",
status: "CONFIRMED",
}),
method: "REQUEST",
},
from: `${APP_NAME} <${this.getMailerOptions().from}>`,
to: toAddresses.join(","),
replyTo: [this.calEvent.organizer.email, ...this.calEvent.attendees.map(({ email }) => email)],
subject: `${this.t("guests_added_event_type_subject", {
eventType: this.calEvent.type,
name: this.calEvent.attendees[0].name,
date: this.getFormattedDate(),
})}`,
Comment on lines +26 to +30

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Potential null pointer exception when accessing attendees[0].name.

If this.calEvent.attendees is empty, accessing this.calEvent.attendees[0].name on Line 28 will throw a runtime error. This could occur in edge cases where the calendar event has no attendees.

🛡️ Proposed fix: Add null safety check
       subject: `${this.t("guests_added_event_type_subject", {
         eventType: this.calEvent.type,
-        name: this.calEvent.attendees[0].name,
+        name: this.calEvent.attendees[0]?.name ?? this.calEvent.organizer.name,
         date: this.getFormattedDate(),
       })}`,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
subject: `${this.t("guests_added_event_type_subject", {
eventType: this.calEvent.type,
name: this.calEvent.attendees[0].name,
date: this.getFormattedDate(),
})}`,
subject: `${this.t("guests_added_event_type_subject", {
eventType: this.calEvent.type,
name: this.calEvent.attendees[0]?.name ?? this.calEvent.organizer.name,
date: this.getFormattedDate(),
})}`,
🤖 Prompt for AI Agents
In `@packages/emails/templates/organizer-add-guests-email.ts` around lines 26 -
30, The subject construction in organizer-add-guests-email.ts uses
this.calEvent.attendees[0].name and can throw if attendees is empty; update the
subject building in the class/template to safely access the first attendee
(e.g., use a null-safe check or optional chaining like
this.calEvent.attendees?.[0]?.name) and provide a fallback string (e.g., "Guest"
or this.calEvent.organizer?.name) so subject creation (the template that calls
this.t("guests_added_event_type_subject", { eventType: this.calEvent.type, name:
..., date: this.getFormattedDate() })) never accesses undefined.

html: await renderEmail("OrganizerAddGuestsEmail", {
attendee: this.calEvent.organizer,
calEvent: this.calEvent,
}),
text: this.getTextBody("new_guests_added"),
};
}
}
19 changes: 19 additions & 0 deletions packages/trpc/server/routers/viewer/bookings/_router.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import authedProcedure from "../../../procedures/authedProcedure";
import publicProcedure from "../../../procedures/publicProcedure";
import { router } from "../../../trpc";
import { ZAddGuestsInputSchema } from "./addGuests.schema";
import { ZConfirmInputSchema } from "./confirm.schema";
import { ZEditLocationInputSchema } from "./editLocation.schema";
import { ZFindInputSchema } from "./find.schema";
Expand All @@ -14,6 +15,7 @@ type BookingsRouterHandlerCache = {
get?: typeof import("./get.handler").getHandler;
requestReschedule?: typeof import("./requestReschedule.handler").requestRescheduleHandler;
editLocation?: typeof import("./editLocation.handler").editLocationHandler;
addGuests?: typeof import("./addGuests.handler").addGuestsHandler;
confirm?: typeof import("./confirm.handler").confirmHandler;
getBookingAttendees?: typeof import("./getBookingAttendees.handler").getBookingAttendeesHandler;
find?: typeof import("./find.handler").getHandler;
Expand Down Expand Up @@ -74,6 +76,23 @@ export const bookingsRouter = router({
input,
});
}),
addGuests: authedProcedure.input(ZAddGuestsInputSchema).mutation(async ({ input, ctx }) => {
if (!UNSTABLE_HANDLER_CACHE.addGuests) {
UNSTABLE_HANDLER_CACHE.addGuests = await import("./addGuests.handler").then(
(mod) => mod.addGuestsHandler
);
}

// Unreachable code but required for type safety
if (!UNSTABLE_HANDLER_CACHE.addGuests) {
throw new Error("Failed to load handler");
}

return UNSTABLE_HANDLER_CACHE.addGuests({
ctx,
input,
});
}),

confirm: authedProcedure.input(ZConfirmInputSchema).mutation(async ({ input, ctx }) => {
if (!UNSTABLE_HANDLER_CACHE.confirm) {
Expand Down
Loading