Skip to content

Automated Test: guest-management-enhanced #376

Closed

Conversation

admin-coderabbit
Copy link
Owner

@admin-coderabbit admin-coderabbit commented Feb 4, 2026

This pull request was automatically created by @coderabbitai/e2e-reviewer.

Batch created pull request.

Summary by CodeRabbit

  • New Features
    • Added ability to add guests to existing bookings through a dedicated guest management dialog.
    • Automated email notifications inform organizers and attendees when new guests are added to bookings.
    • Email validation ensures all guest addresses are unique and properly formatted.

* feat: ability to add guests via app.cal.com/bookings

* fix: some update

* fix: minor issue

* fix: final update

* update

* update

* add requested changes

* fix type error

* small update

* final update

* fix type error

* fix location

* update calender event

---------

Co-authored-by: Somay Chauhan <somaychauhan98@gmail.com>
@coderabbit-eval
Copy link

coderabbit-eval bot commented Feb 4, 2026

📝 Walkthrough

Walkthrough

This PR introduces functionality to add additional guests to existing bookings. It includes a new UI dialog component, backend handler with permission validation and attendee creation, email templates for organizers and attendees, input validation schema, TRPC routing, and a multi-email form component. Changes span frontend UI, backend handlers, email infrastructure, and localization.

Changes

Cohort / File(s) Summary
Booking UI Components
apps/web/components/booking/BookingListItem.tsx, apps/web/components/dialog/AddGuestsDialog.tsx, packages/ui/form/MultiEmail.tsx, packages/ui/form/MultiEmailLazy.tsx, packages/ui/index.tsx
Added new "Add Members" action to booking list items that opens a dialog for adding guest emails. Includes MultiEmail input component for managing multiple email entries with add/remove functionality and validation feedback.
Email Templates
packages/emails/src/templates/AttendeeAddGuestsEmail.tsx, packages/emails/src/templates/OrganizerAddGuestsEmail.tsx, packages/emails/src/templates/index.ts, packages/emails/templates/attendee-add-guests-email.ts, packages/emails/templates/organizer-add-guests-email.ts
New email template wrappers and classes for notifying attendees and organizers when guests are added to a booking. Includes ICS event generation and localized subject lines.
Backend Handler
packages/trpc/server/routers/viewer/bookings/addGuests.handler.ts, packages/trpc/server/routers/viewer/bookings/addGuests.schema.ts, packages/trpc/server/routers/viewer/bookings/_router.tsx
New TRPC handler for adding guests with permission validation (team admin, organizer, or existing attendee), email filtering against blacklist, attendee creation, calendar event enrichment, and email dispatch.
Email Infrastructure
packages/emails/email-manager.ts
Added sendAddGuestsEmails function to coordinate email dispatch to organizer and all attendees when guests are added.
Localization
apps/web/public/static/locales/en/common.json
Added new locale strings for guest-related events, UI labels, validation messages, and error states.

Sequence Diagram

sequenceDiagram
    participant User
    participant UI as AddGuestsDialog
    participant TRPC as TRPC Server
    participant Handler as addGuestsHandler
    participant DB as Database
    participant EmailMgr as Email Manager
    participant Calendar as Calendar Sync

    User->>UI: Enters guest emails
    User->>UI: Clicks "Add"
    UI->>UI: Validates emails (unique & format)
    UI->>TRPC: Call addGuests mutation
    TRPC->>Handler: Route to handler
    Handler->>DB: Load booking + attendees + event data
    Handler->>Handler: Check permissions (admin/organizer/attendee)
    Handler->>Handler: Filter emails against blacklist & existing attendees
    Handler->>DB: Create new attendee records
    Handler->>Handler: Enrich event data with all attendees
    Handler->>Calendar: Update calendar with new attendees
    Handler->>EmailMgr: Send notifications
    EmailMgr->>EmailMgr: Render OrganizerAddGuestsEmail
    EmailMgr->>EmailMgr: Render AttendeeAddGuestsEmail (new guests)
    EmailMgr->>EmailMgr: Render AttendeeScheduledEmail (existing attendees)
    Handler->>UI: Return success
    UI->>UI: Show success toast & close dialog
    UI->>User: Confirm guests added
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 A dialog springs forth with a user-plus gleam,
Emails dance and multiply in organized dream,
Attendees multiplying through handlers so keen,
Calendar and inbox both know what's been seen!
New guests now added, the booking's complete—
Another pair of paws makes the meeting sweet! 🎉

🚥 Pre-merge checks | ✅ 1 | ❌ 1
❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Title check ❓ Inconclusive The title 'Automated Test: guest-management-enhanced' is vague and generic, using non-descriptive terms that don't convey meaningful information about the actual changeset. Use a more descriptive title that summarizes the main feature being added, such as 'Add guest management feature to bookings' or 'Implement add guests functionality for booking items'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch guest-management-enhanced

Comment @coderabbit eval help to get the list of available commands and usage tips.

@coderabbit-eval coderabbit-eval bot closed this Feb 4, 2026
Copy link

@coderabbit-eval coderabbit-eval bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 8

🤖 Fix all issues with AI agents
In `@apps/web/components/dialog/AddGuestsDialog.tsx`:
- Around line 2-34: The invalid-state banner remains after correcting input
because we never clear isInvalidEmail when the MultiEmail value changes; update
the change handler in AddGuestsDialog that updates multiEmailValue (the place
using setMultiEmailValue / the MultiEmail component) to also call
setIsInvalidEmail(false) so any banner controlled by isInvalidEmail is cleared
as soon as the user edits the input.

In `@packages/emails/email-manager.ts`:
- Around line 525-549: sendAddGuestsEmails is currently passing the full
formatted calendarEvent (from formatCalEvent) to attendee-facing email
constructors which can leak additionalNotes when calendarEvent.hideCalendarNotes
is true; update sendAddGuestsEmails to detect calendarEvent.hideCalendarNotes
and, for calls to AttendeeScheduledEmail and AttendeeAddGuestsEmail, pass an
attendee-safe copy of calEvent with additionalNotes removed/cleared (but
continue passing the full calendarEvent to OrganizerAddGuestsEmail and team
member organizer emails); identify this in the function sendAddGuestsEmails and
where AttendeeScheduledEmail/AttendeeAddGuestsEmail are constructed and ensure
the attendee payload omits additionalNotes when hideCalendarNotes is true.
- Around line 525-549: sendAddGuestsEmails currently sends all organizer and
attendee emails unconditionally; update it to read the event-type email-disable
flags from the formatted calendar event (e.g.
calendarEvent.eventType.emailSettings or similarly named properties) and skip
sending emails when the corresponding flag is disabled. Specifically: before
pushing OrganizerAddGuestsEmail and team-member OrganizerAddGuestsEmail, check
the organizer/email setting flag on calendarEvent; before pushing
AttendeeAddGuestsEmail or AttendeeScheduledEmail for each attendee, check the
attendee/email setting flag on calendarEvent (still preserving the newGuests
branch logic). Also adjust the function signature if needed to accept or compute
the event-type settings so sendAddGuestsEmails, OrganizerAddGuestsEmail,
AttendeeAddGuestsEmail, and AttendeeScheduledEmail all honor the same event-type
disable flags.

In `@packages/emails/templates/organizer-add-guests-email.ts`:
- Around line 26-30: The subject construction uses
this.calEvent.attendees[0].name which will throw if attendees is empty; update
the subject generation in organizer-add-guests-email.ts to safely access the
first attendee (e.g., use optional chaining or a helper) and provide a fallback
name (e.g., 'Guest' or this.t('unknown_attendee')) when this.calEvent.attendees
is missing/empty, while keeping the existing this.t(..., { eventType:
this.calEvent.type, name: fallbackName, date: this.getFormattedDate() }) call.

In `@packages/trpc/server/routers/viewer/bookings/addGuests.handler.ts`:
- Around line 70-105: The guest-email filtering is case-sensitive and still uses
the original guests array; normalize and dedupe emails (trim and toLowerCase)
first, build a normalizedUnique list by removing duplicates and excluding
blacklisted emails (use the existing blacklistedGuestEmails variable), then use
that normalizedUnique everywhere: use it in the attendees.some comparison
(compare normalized attendee.email), construct guestsFullDetails from
normalizedUnique, and pass normalizedUnique to prisma.booking.update createMany
so the blacklist/dedup logic is consistently applied.
- Around line 46-48: The permission check assigned to isTeamAdminOrOwner is
wrong: it uses logical AND so it only passes when the user is both admin and
owner. Change the expression to use OR so a user who is either an admin or an
owner is allowed; update the line that calls isTeamAdmin(user.id,
booking.eventType?.teamId ?? 0) and isTeamOwner(user.id,
booking.eventType?.teamId ?? 0) to combine their awaited results with || instead
of && (keeping the same arguments: user.id and booking.eventType?.teamId ?? 0).
- Around line 158-166: The code uses ctx.user credentials when constructing
EventManager which fails if a non-organizer attendee adds guests; change to use
the organizer's credentials instead by calling getUsersCredentials for the
organizer (the user representing the event owner) and pass those credentials
into new EventManager({...user, credentials: [...]}) before calling
eventManager.updateCalendarAttendees(evt, booking); ensure the organizer user
object (not ctx.user) is used to look up credentials so updateCalendarAttendees
runs with organizer access.

In `@packages/ui/form/MultiEmail.tsx`:
- Around line 64-68: The code pushes an empty string into the emails array which
immediately triggers browser validation because each EmailField is rendered with
required; change the behavior so new inputs are not required until the user
interacts or enters text: modify the onClick handler that calls setValue to push
a sentinel (e.g. null or an object like {value: "",touched:false}) instead of ""
and update the EmailField rendering to compute required based on content/touched
(for example required={!!emailValue} or required={email?.touched === true}) or
only set required on blur/when non-empty; update any usages of value[index] to
read the new shape accordingly and adjust setValue updates to mark touched=true
when the user edits.
🧹 Nitpick comments (6)
packages/emails/src/templates/OrganizerAddGuestsEmail.tsx (1)

3-11: Preset props can be overridden by the caller due to spread ordering.

{...props} appears after the preset props (title, headerType, subject, callToAction), so any matching key in props will silently override the intended template values. If these are meant to be fixed for this email type, place the spread before them:

♻️ Suggested reorder
 export const OrganizerAddGuestsEmail = (props: React.ComponentProps<typeof OrganizerScheduledEmail>) => (
   <OrganizerScheduledEmail
+    {...props}
     title="new_guests_added"
     headerType="calendarCircle"
     subject="guests_added_event_type_subject"
     callToAction={null}
-    {...props}
   />
 );
packages/emails/src/templates/AttendeeAddGuestsEmail.tsx (1)

3-10: Same spread-ordering concern as OrganizerAddGuestsEmail.

{...props} after the presets lets callers override title, headerType, and subject. Consider placing {...props} first if these should be fixed. Also note that unlike the Organizer variant, this template does not set callToAction={null} — verify whether the default call-to-action is appropriate for a "guests added" notification to attendees.

packages/ui/form/MultiEmail.tsx (2)

25-26: Using array index as key can cause stale input state when items are removed.

When an item is spliced out (line 41), all subsequent items shift indices, causing React to mismatch DOM nodes with data. This can lead to focus jumps and stale values in uncontrolled internal state of EmailField. Consider using a stable identifier (e.g., a generated ID stored alongside each email).


73-74: Unnecessary empty fragment.

The <></> in the else branch of the ternary is a no-op. You can simplify by converting the ternary into a plain conditional ({value.length > 0 && (...)}), since the empty-list case is already handled at line 77.

packages/trpc/server/routers/viewer/bookings/addGuests.schema.ts (1)

3-6: Add basic input constraints (positive bookingId, non-empty guests).

Prevents empty payloads and invalid booking ids earlier in the pipeline.

♻️ Proposed tightening
 export const ZAddGuestsInputSchema = z.object({
-  bookingId: z.number(),
-  guests: z.array(z.string().email()),
+  bookingId: z.number().int().positive(),
+  guests: z.array(z.string().email()).min(1),
 });
apps/web/components/dialog/AddGuestsDialog.tsx (1)

48-57: Filter empty/whitespace emails before validation and mutation.

This avoids placeholder/empty entries triggering a validation failure.

🧹 Proposed normalization
 const handleAdd = () => {
-  if (multiEmailValue.length === 0) {
+  const normalizedGuests = multiEmailValue.map((email) => email.trim()).filter(Boolean);
+  if (normalizedGuests.length === 0) {
     return;
   }
-  const validationResult = ZAddGuestsInputSchema.safeParse(multiEmailValue);
+  const validationResult = ZAddGuestsInputSchema.safeParse(normalizedGuests);
   if (validationResult.success) {
-    addGuestsMutation.mutate({ bookingId, guests: multiEmailValue });
+    addGuestsMutation.mutate({ bookingId, guests: normalizedGuests });
   } else {
     setIsInvalidEmail(true);
   }
 };

Comment on lines +2 to +34
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);

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Clear the invalid-state banner once input changes.

Currently the warning stays visible after correction until submission/cancel.

🧩 Suggested reset on change
-import { useState } from "react";
+import { useEffect, useState } from "react";
 ...
 const [multiEmailValue, setMultiEmailValue] = useState<string[]>([""]);
 const [isInvalidEmail, setIsInvalidEmail] = useState(false);
+
+useEffect(() => {
+  if (isInvalidEmail) setIsInvalidEmail(false);
+}, [multiEmailValue, isInvalidEmail]);
🤖 Prompt for AI Agents
In `@apps/web/components/dialog/AddGuestsDialog.tsx` around lines 2 - 34, The
invalid-state banner remains after correcting input because we never clear
isInvalidEmail when the MultiEmail value changes; update the change handler in
AddGuestsDialog that updates multiEmailValue (the place using setMultiEmailValue
/ the MultiEmail component) to also call setIsInvalidEmail(false) so any banner
controlled by isInvalidEmail is cleared as soon as the user edits the input.

Comment on lines +525 to +549
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);

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Prevent leakage of hidden notes in attendee emails.

When hideCalendarNotes is set, attendee-facing emails should not include additionalNotes (consistent with the scheduling flow).

🛡️ Suggested attendee-safe payload
 export const sendAddGuestsEmails = async (calEvent: CalendarEvent, newGuests: string[]) => {
   const calendarEvent = formatCalEvent(calEvent);
+  const attendeeEvent = {
+    ...calendarEvent,
+    ...(calendarEvent.hideCalendarNotes && { additionalNotes: undefined }),
+  };

   const emailsToSend: Promise<unknown>[] = [];
   ...
   emailsToSend.push(
     ...calendarEvent.attendees.map((attendee) => {
       if (newGuests.includes(attendee.email)) {
-        return sendEmail(() => new AttendeeScheduledEmail(calendarEvent, attendee));
+        return sendEmail(() => new AttendeeScheduledEmail(attendeeEvent, attendee));
       } else {
-        return sendEmail(() => new AttendeeAddGuestsEmail(calendarEvent, attendee));
+        return sendEmail(() => new AttendeeAddGuestsEmail(attendeeEvent, attendee));
       }
     })
   );
🤖 Prompt for AI Agents
In `@packages/emails/email-manager.ts` around lines 525 - 549, sendAddGuestsEmails
is currently passing the full formatted calendarEvent (from formatCalEvent) to
attendee-facing email constructors which can leak additionalNotes when
calendarEvent.hideCalendarNotes is true; update sendAddGuestsEmails to detect
calendarEvent.hideCalendarNotes and, for calls to AttendeeScheduledEmail and
AttendeeAddGuestsEmail, pass an attendee-safe copy of calEvent with
additionalNotes removed/cleared (but continue passing the full calendarEvent to
OrganizerAddGuestsEmail and team member organizer emails); identify this in the
function sendAddGuestsEmails and where
AttendeeScheduledEmail/AttendeeAddGuestsEmail are constructed and ensure the
attendee payload omits additionalNotes when hideCalendarNotes is true.

⚠️ Potential issue | 🟠 Major

Honor event-type email disable flags for add-guests sends.

Currently all organizer/attendee emails are sent regardless of event-type settings, which diverges from other flows.

🛠️ Proposed guard and signature update
-export const sendAddGuestsEmails = async (calEvent: CalendarEvent, newGuests: string[]) => {
+export const sendAddGuestsEmails = async (
+  calEvent: CalendarEvent,
+  newGuests: string[],
+  eventTypeMetadata?: EventTypeMetadata
+) => {
   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 }))
-      );
-    }
-  }
+  if (!eventTypeDisableHostEmail(eventTypeMetadata)) {
+    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));
-      }
-    })
-  );
+  if (!eventTypeDisableAttendeeEmail(eventTypeMetadata)) {
+    emailsToSend.push(
+      ...calendarEvent.attendees.map((attendee) => {
+        if (newGuests.includes(attendee.email)) {
+          return sendEmail(() => new AttendeeScheduledEmail(calendarEvent, attendee));
+        }
+        return sendEmail(() => new AttendeeAddGuestsEmail(calendarEvent, attendee));
+      })
+    );
+  }

   await Promise.all(emailsToSend);
 };
🤖 Prompt for AI Agents
In `@packages/emails/email-manager.ts` around lines 525 - 549, sendAddGuestsEmails
currently sends all organizer and attendee emails unconditionally; update it to
read the event-type email-disable flags from the formatted calendar event (e.g.
calendarEvent.eventType.emailSettings or similarly named properties) and skip
sending emails when the corresponding flag is disabled. Specifically: before
pushing OrganizerAddGuestsEmail and team-member OrganizerAddGuestsEmail, check
the organizer/email setting flag on calendarEvent; before pushing
AttendeeAddGuestsEmail or AttendeeScheduledEmail for each attendee, check the
attendee/email setting flag on calendarEvent (still preserving the newGuests
branch logic). Also adjust the function signature if needed to accept or compute
the event-type settings so sendAddGuestsEmails, OrganizerAddGuestsEmail,
AttendeeAddGuestsEmail, and AttendeeScheduledEmail all honor the same event-type
disable flags.

Comment on lines +26 to +30
subject: `${this.t("guests_added_event_type_subject", {
eventType: this.calEvent.type,
name: this.calEvent.attendees[0].name,
date: this.getFormattedDate(),
})}`,

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

this.calEvent.attendees[0] will throw if the attendees array is empty.

While the "add guests" flow should always have attendees, a defensive check or fallback avoids a runtime crash if this email is ever triggered with unexpected data.

🛡️ Suggested defensive fallback
       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 uses this.calEvent.attendees[0].name which will
throw if attendees is empty; update the subject generation in
organizer-add-guests-email.ts to safely access the first attendee (e.g., use
optional chaining or a helper) and provide a fallback name (e.g., 'Guest' or
this.t('unknown_attendee')) when this.calEvent.attendees is missing/empty, while
keeping the existing this.t(..., { eventType: this.calEvent.type, name:
fallbackName, date: this.getFormattedDate() }) call.

Comment on lines +46 to +48
const isTeamAdminOrOwner =
(await isTeamAdmin(user.id, booking.eventType?.teamId ?? 0)) &&
(await isTeamOwner(user.id, booking.eventType?.teamId ?? 0));

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Team admin/owner check should be OR, not AND.

The current logic only allows users who are both admin and owner.

✅ Correct permission logic
-const isTeamAdminOrOwner =
-  (await isTeamAdmin(user.id, booking.eventType?.teamId ?? 0)) &&
-  (await isTeamOwner(user.id, booking.eventType?.teamId ?? 0));
+const isTeamAdminOrOwner =
+  (await isTeamAdmin(user.id, booking.eventType?.teamId ?? 0)) ||
+  (await isTeamOwner(user.id, booking.eventType?.teamId ?? 0));
📝 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 isTeamAdminOrOwner =
(await isTeamAdmin(user.id, booking.eventType?.teamId ?? 0)) &&
(await isTeamOwner(user.id, booking.eventType?.teamId ?? 0));
const isTeamAdminOrOwner =
(await isTeamAdmin(user.id, booking.eventType?.teamId ?? 0)) ||
(await isTeamOwner(user.id, booking.eventType?.teamId ?? 0));
🤖 Prompt for AI Agents
In `@packages/trpc/server/routers/viewer/bookings/addGuests.handler.ts` around
lines 46 - 48, The permission check assigned to isTeamAdminOrOwner is wrong: it
uses logical AND so it only passes when the user is both admin and owner. Change
the expression to use OR so a user who is either an admin or an owner is
allowed; update the line that calls isTeamAdmin(user.id,
booking.eventType?.teamId ?? 0) and isTeamOwner(user.id,
booking.eventType?.teamId ?? 0) to combine their awaited results with || instead
of && (keeping the same arguments: user.id and booking.eventType?.teamId ?? 0).

Comment on lines +70 to +105
const blacklistedGuestEmails = process.env.BLACKLISTED_GUEST_EMAILS
? process.env.BLACKLISTED_GUEST_EMAILS.split(",").map((email) => email.toLowerCase())
: [];

const uniqueGuests = guests.filter(
(guest) =>
!booking.attendees.some((attendee) => guest === attendee.email) &&
!blacklistedGuestEmails.includes(guest)
);

if (uniqueGuests.length === 0)
throw new TRPCError({ code: "BAD_REQUEST", message: "emails_must_be_unique_valid" });

const guestsFullDetails = uniqueGuests.map((guest) => {
return {
name: "",
email: guest,
timeZone: organizer.timeZone,
locale: organizer.locale,
};
});

const bookingAttendees = await prisma.booking.update({
where: {
id: bookingId,
},
include: {
attendees: true,
},
data: {
attendees: {
createMany: {
data: guestsFullDetails,
},
},
},

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Normalize/unique guest emails and use the filtered list everywhere.

Current filtering is case-sensitive and uses the raw guests list for emails, which can bypass the blacklist and notify users who weren’t actually added.

🧹 Suggested normalization + consistent usage
-const blacklistedGuestEmails = process.env.BLACKLISTED_GUEST_EMAILS
-  ? process.env.BLACKLISTED_GUEST_EMAILS.split(",").map((email) => email.toLowerCase())
-  : [];
-
-const uniqueGuests = guests.filter(
-  (guest) =>
-    !booking.attendees.some((attendee) => guest === attendee.email) &&
-    !blacklistedGuestEmails.includes(guest)
-);
+const blacklistedGuestEmails = process.env.BLACKLISTED_GUEST_EMAILS
+  ? process.env.BLACKLISTED_GUEST_EMAILS.split(",").map((email) => email.trim().toLowerCase())
+  : [];
+
+const existingEmails = new Set(
+  booking.attendees.map((attendee) => attendee.email.trim().toLowerCase())
+);
+const uniqueGuests = Array.from(
+  new Set(guests.map((guest) => guest.trim().toLowerCase()))
+).filter((guest) => guest && !existingEmails.has(guest) && !blacklistedGuestEmails.includes(guest));

 ...
-const guestsFullDetails = uniqueGuests.map((guest) => {
+const guestsFullDetails = uniqueGuests.map((guest) => {
   return {
     name: "",
     email: guest,
     timeZone: organizer.timeZone,
     locale: organizer.locale,
   };
 });
 ...
-  await sendAddGuestsEmails(evt, guests);
+  await sendAddGuestsEmails(evt, uniqueGuests);

Also applies to: 168-169

🤖 Prompt for AI Agents
In `@packages/trpc/server/routers/viewer/bookings/addGuests.handler.ts` around
lines 70 - 105, The guest-email filtering is case-sensitive and still uses the
original guests array; normalize and dedupe emails (trim and toLowerCase) first,
build a normalizedUnique list by removing duplicates and excluding blacklisted
emails (use the existing blacklistedGuestEmails variable), then use that
normalizedUnique everywhere: use it in the attendees.some comparison (compare
normalized attendee.email), construct guestsFullDetails from normalizedUnique,
and pass normalizedUnique to prisma.booking.update createMany so the
blacklist/dedup logic is consistently applied.

Comment on lines +158 to +166
const credentials = await getUsersCredentials(ctx.user);

const eventManager = new EventManager({
...user,
credentials: [...credentials],
});

await eventManager.updateCalendarAttendees(evt, booking);

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Use organizer credentials to update calendars.

If a non-organizer attendee adds guests, ctx.user credentials may not have access to the organizer’s calendar.

🔐 Suggested organizer-based credentials
-const credentials = await getUsersCredentials(ctx.user);
-
-const eventManager = new EventManager({
-  ...user,
-  credentials: [...credentials],
-});
+const organizerUser = booking.user ?? user;
+const credentials = await getUsersCredentials(organizerUser);
+
+const eventManager = new EventManager({
+  ...organizerUser,
+  credentials: [...credentials],
+});
📝 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 credentials = await getUsersCredentials(ctx.user);
const eventManager = new EventManager({
...user,
credentials: [...credentials],
});
await eventManager.updateCalendarAttendees(evt, booking);
const organizerUser = booking.user ?? user;
const credentials = await getUsersCredentials(organizerUser);
const eventManager = new EventManager({
...organizerUser,
credentials: [...credentials],
});
await eventManager.updateCalendarAttendees(evt, booking);
🤖 Prompt for AI Agents
In `@packages/trpc/server/routers/viewer/bookings/addGuests.handler.ts` around
lines 158 - 166, The code uses ctx.user credentials when constructing
EventManager which fails if a non-organizer attendee adds guests; change to use
the organizer's credentials instead by calling getUsersCredentials for the
organizer (the user representing the event owner) and pass those credentials
into new EventManager({...user, credentials: [...]}) before calling
eventManager.updateCalendarAttendees(evt, booking); ensure the organizer user
object (not ctx.user) is used to look up credentials so updateCalendarAttendees
runs with organizer access.

Comment on lines +64 to +68
onClick={() => {
const updatedValue = [...value];
updatedValue.push("");
setValue(updatedValue);
}}>

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Pushing an empty string into a list of required email fields.

Each EmailField has required (line 38), so appending "" will immediately trigger browser-native validation on the empty field. If that's not the desired UX, consider deferring the required attribute until the field is non-empty, or validating only on form submission.

🤖 Prompt for AI Agents
In `@packages/ui/form/MultiEmail.tsx` around lines 64 - 68, The code pushes an
empty string into the emails array which immediately triggers browser validation
because each EmailField is rendered with required; change the behavior so new
inputs are not required until the user interacts or enters text: modify the
onClick handler that calls setValue to push a sentinel (e.g. null or an object
like {value: "",touched:false}) instead of "" and update the EmailField
rendering to compute required based on content/touched (for example
required={!!emailValue} or required={email?.touched === true}) or only set
required on blur/when non-empty; update any usages of value[index] to read the
new shape accordingly and adjust setValue updates to mark touched=true when the
user edits.

Sign in to join this conversation on GitHub.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

1 participant