Skip to content

feat: take into account guest availability when host reschedules#28636

Open
bcornish1797 wants to merge 29 commits into
calcom:mainfrom
bcornish1797:feat/16378-guest-availability-reschedule
Open

feat: take into account guest availability when host reschedules#28636
bcornish1797 wants to merge 29 commits into
calcom:mainfrom
bcornish1797:feat/16378-guest-availability-reschedule

Conversation

@bcornish1797
Copy link
Copy Markdown

/claim #16378

What does this PR do?

When a host reschedules a booking, the system now checks whether attendees are Cal.com users and fetches their busy times. Only mutually available slots are shown in the reschedule calendar.

Before: Host could freely pick any time when rescheduling, even if the guest was busy.
After: Guest's existing bookings are fetched and blocked automatically.

How it works

  1. BookingRepository.findByUidIncludeAttendeeEmails() — gets attendee emails from the original booking
  2. UserRepository.findByEmails() — resolves emails to Cal.com users (primary + verified secondary emails, case-insensitive, deduplicated via Promise.all)
  3. BookingRepository.findByUserIdsAndDateRange() — fetches guest bookings in the date range, with excludeUid filtering at the database level
  4. Guest busy times merged into host's availability view

Scope

Per @CarinaWolli's clarification: this applies only when the event type owner reschedules. If attendees reschedule, all slots are shown.

  • COLLECTIVE scheduling: skipped (already coordinated)
  • Non-Cal.com guests: ignored (no availability to check)
  • Multiple Cal.com guests: all busy times merged
  • ROUND_ROBIN: fully supported
  • Error handling: graceful degradation (returns [] on failure, never blocks rescheduling)

Demo

▶️ Watch Demo Video

Tests

30 tests passing (3 new test files, 631 lines added):

  • UserRepository.findByEmails — primary/secondary email lookup, dedup, normalization (6 tests)
  • BookingRepository.findByUserIdsAndDateRange — userId/email query, excludeUid, empty input (6 tests)
  • BookingRepository.findByUidIncludeAttendeeEmails — uid lookup, null handling (2 tests)
  • _getGuestBusyTimesForReschedule — early exits, busy time collection, multi-guest, error handling (13 tests)
  • Existing tests — unchanged and passing (6 tests)

Self-review notes

  • Uses select (not include) per cal.com conventions
  • Uses withReporting wrapper for observability
  • Queries run in Promise.all for zero added latency
  • excludeUid filtering at database level (not JS post-filter)

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

2 issues found across 7 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/trpc/server/routers/viewer/slots/util.ts">

<violation number="1" location="packages/trpc/server/routers/viewer/slots/util.ts:847">
P2: Guest busy-time filtering is applied to all reschedules with a UID, but the feature is host-reschedule-specific; missing initiator gating can incorrectly restrict attendee reschedules.</violation>

<violation number="2" location="packages/trpc/server/routers/viewer/slots/util.ts:867">
P2: Non‑Cal.com guest emails are still used in the OR attendee-email filter, so their bookings can be pulled into guest busy times despite the Cal.com user filter. Narrow userEmails to emails of resolved Cal.com users (or omit it) to avoid including non‑Cal.com guests.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread packages/trpc/server/routers/viewer/slots/util.ts
Comment thread packages/trpc/server/routers/viewer/slots/util.ts Outdated
@bcornish1797 bcornish1797 requested a review from a team as a code owner March 28, 2026 19:32
@bcornish1797
Copy link
Copy Markdown
Author

Both issues addressed in 416ecc9:

P2 (host-initiator gating): Added hostUserIds parameter. The function now checks if the original booking's userId is in the current event type's host list. If not (attendee-initiated reschedule), returns [] immediately — all slots shown per CarinaWolli's spec.

P2 (email filter scope): userEmails now only contains emails of resolved Cal.com users (calUsers.map(u => u.email)), not all original attendee emails. Non-Cal.com guests no longer pollute the OR filter.

Tests added for both fixes.

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

2 issues found across 2 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/trpc/server/routers/viewer/slots/util.ts">

<violation number="1" location="packages/trpc/server/routers/viewer/slots/util.ts:862">
P2: Reschedule initiator detection is incorrect: comparing `original.userId` to `hostUserIds` does not tell whether host or attendee initiated the reschedule.</violation>
</file>

<file name="packages/trpc/server/routers/viewer/slots/getGuestBusyTimesForReschedule.test.ts">

<violation number="1" location="packages/trpc/server/routers/viewer/slots/getGuestBusyTimesForReschedule.test.ts:261">
P2: Newly added Cal.com-only email filtering test conflicts with an existing test that still expects external attendee emails, leaving contradictory test contracts.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread packages/trpc/server/routers/viewer/slots/util.ts Outdated
@bcornish1797
Copy link
Copy Markdown
Author

Addressed in b8628c5:

P2 (initiator detection): Removed the broken hostUserIds check. The slots API does not receive rescheduledBy context, so host-vs-attendee gating cannot be done at this layer. Guest availability is now always checked as the safe default (fewer slots > double-booking risk). Added inline comment with the path forward.

P2 (test conflict): Fixed the "pass correct userIds and emails" test to expect only Cal.com user emails, consistent with the email filtering logic. Removed the broken initiator-gating test.

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 2 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/trpc/server/routers/viewer/slots/util.ts">

<violation number="1" location="packages/trpc/server/routers/viewer/slots/util.ts:857">
P2: Guest busy-time blocking now applies to all non-collective reschedules because host/attendee gating was removed, causing attendee-initiated reschedules to incorrectly hide slots.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread packages/trpc/server/routers/viewer/slots/util.ts Outdated
@CLAassistant
Copy link
Copy Markdown

CLAassistant commented Mar 29, 2026

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you all sign our Contributor License Agreement before we can accept your contribution.
1 out of 2 committers have signed the CLA.

✅ bcornish1797
❌ sochanged
You have signed the CLA already but the status is still pending? Let us recheck it.

@bcornish1797
Copy link
Copy Markdown
Author

Addressed in c4fc209 and 25061e9:

P2 (host/attendee gating): Added rescheduledBy parameter through the full chain (URL search params → useEventuseSchedule → tRPC slots input → _getGuestBusyTimesForReschedule). The function now fetches the host email from the booking and compares it against rescheduledBy:

  • Host reschedules → guest busy-time blocking applies (correct behavior)
  • Attendee reschedules → returns early, no guest blocking (fixes the P2)
  • rescheduledBy not provided (older clients) → falls through to apply guest blocking as safe default (backwards compatible)

Comparison is case-insensitive. Orphaned bookings (no user) are handled safely.

Type safety fix: EventBusyDetails.source was being assigned undefined when withSource is false, but the type requires string. Changed to empty string "".

Test updates: 5 new test cases covering host-initiated, attendee-initiated, case-insensitive match, and null/undefined backwards compatibility. Existing test data updated to match the new BookingRepository select shape.

@bcornish1797
Copy link
Copy Markdown
Author

@cla-bot check

@bcornish1797
Copy link
Copy Markdown
Author

recheck

@bcornish1797
Copy link
Copy Markdown
Author

I have read the CLA Document and I hereby sign the CLA

@bcornish1797
Copy link
Copy Markdown
Author

Hi team 👋 All review feedback from cubic-dev-ai has been addressed (see inline replies). The CLA is signed and passing.

Could a maintainer please add the run-ci label to trigger CI? Happy to address any formatting/linting issues that come up from the auto-fix pipeline.

Summary of changes (10 files, 16 tests):

  • Guest availability check during host-initiated reschedules ([CAL-4531] Take into account guest's availability when rescheduling #16378)
  • rescheduledBy parameter flows from frontend URL → tRPC → slot computation
  • Only Cal.com users among attendees are checked (external guests ignored)
  • Attendee-initiated reschedules are properly gated (no slot blocking)
  • Graceful degradation on errors (returns [])

Thank you!

@bcornish1797
Copy link
Copy Markdown
Author

Hi team, friendly follow-up — this PR is ready for CI. Could a maintainer please add the run-ci label? All review feedback has been addressed and CLA is signed. Thank you!

@bcornish1797
Copy link
Copy Markdown
Author

Hi team 👋 Just a friendly follow-up — all review comments have been addressed and the PR is mergeable. Would appreciate a re-review when you get a chance. Happy to make any further adjustments if needed!

@bcornish1797
Copy link
Copy Markdown
Author

Friendly follow-up — all review comments have been addressed and the PR is ready for re-review. Could a maintainer please add the run-ci label so the CI checks can complete? Thank you!

@bcornish1797
Copy link
Copy Markdown
Author

@dhairyashiil Could you please review or help get this PR reviewed? All previous review comments have been addressed, and the PR is mergeable. The only blocker is the run-ci label needed to trigger CI. Thank you!

@bcornish1797
Copy link
Copy Markdown
Author

Hi maintainers, friendly ping — could someone please approve the CI run for this fork PR? The required gate is currently blocking all other checks from executing, and it needs a maintainer to trigger it.

Current status:

  • All cubic-dev-ai P2 review comments on packages/trpc/server/routers/viewer/slots/util.ts have been addressed across commits 416ecc9b8628c5c4fc209 → 443f085 (rescheduledBy email comparison + calUserEmails filtering).
  • cubic AI code reviewer: ✅ passed
  • CLA: ✅ signed
  • Trust Check, labeler, Validate PR title: ✅ passed
  • PR is MERGEABLE

Thanks for your time!

@bcornish1797
Copy link
Copy Markdown
Author

Hi — following up on this. If I understand the CI setup correctly, fork PRs here need a maintainer to add the run-ci label to trigger the workflow. The code is ready and all cubic-dev-ai review comments have been addressed across the 416ecc9b8628c5c4fc209 → 443f085 commit chain. Could a maintainer please add the run-ci label when you get a chance? Thanks!

@bcornish1797
Copy link
Copy Markdown
Author

Hi @sahitya-chandra — I noticed from the cal.com PR history that you've been handling run-ci label triggers for fork PRs over the last week (#28833, #28832, #28827, #28783 all by you). Could you take a look at this PR when you get a chance?

PR is OPEN since 2026-03-28, mergeable, all cubic-dev-ai P2 review comments have been addressed across commits 416ecc9b8628c5c4fc209 → 443f085. The only remaining gate is the run-ci label to trigger the CI workflow.

Thanks for your help with the cal.com bounty contributor flow!

@bcornish1797 bcornish1797 force-pushed the feat/16378-guest-availability-reschedule branch from 443f085 to 8a27404 Compare April 15, 2026 11:53
@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 15, 2026

Someone is attempting to deploy a commit to the cal-diy Team on Vercel.

A member of the Team first needs to authorize it.

@bcornish1797
Copy link
Copy Markdown
Author

Update — pushed 8a27404:

  1. Rebased onto latest main (49 commits forward; no conflicts on any of the 10 changed files in this PR).
  2. Added observability to the graceful-degradation path in _getGuestBusyTimesForReschedule. The catch arm previously returned [] silently on any failure (correct runtime behaviour — never block rescheduling), but a silent swallow makes upstream regressions look like 'no Cal.com guests found' instead of a real fault. Now emits a structured log.warn via the existing slots/util logger with safeStringify (same pattern used elsewhere in this file).

Status snapshot

  • ✅ cubic AI code reviewer: passed (all P2 feedback addressed across 416ecc9b8628c5c4fc209 → 443f085, now rebased to 8a27404)
  • ✅ CLA: signed
  • ✅ Trust Check, labeler, Validate PR title, semgrep: passed
  • ✅ Mergeable, no conflicts
  • ⏸️ required check: failure is purely because run-ci label hasn't been applied — fork PRs need it to trigger the workflow

CODEOWNERS for the changed paths

The two production files in this PR (packages/trpc/server/routers/viewer/slots/util.ts and packages/features/availability/lib/getUserAvailability.ts) are both owned by @calcom/Foundation. Recent contributors to slots/util.ts: @emrysal @hbjORbj @hariombalhara @Udit-takkar @anikdhabal @eunjae-lee — would any of you have a few minutes to apply the run-ci label and take a pass when you get a chance? Happy to address whatever the CI surfaces.

Thanks!

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 15, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Hooks were updated to accept an optional restrictionSchedule and to treat useBookerTime()'s timezone as rawTimezone, deriving an effectiveTimezone via useStableTimezone before calling useSchedule; rescheduledBy (from search params) is forwarded into useSchedule. useSchedule and its args now accept rescheduledBy and prefer the V1 schedule path when both rescheduleUid and rescheduledBy are present. Server-side: TRPC input gains rescheduledBy, AvailableSlotsService adds _getGuestBusyTimesForReschedule and threads guestBusyTimes into availability computation and getUserAvailability. New repository methods UserRepository.findByEmails, BookingRepository.findByUidIncludeAttendeeEmails, and findByUserIdsAndDateRange were added. Multiple unit tests were added or extended.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: take into account guest availability when host reschedules' accurately summarizes the main change—adding guest busy-time blocking to host-initiated reschedules.
Description check ✅ Passed The description is detailed and directly related to the changeset, explaining the business logic, implementation approach, scope, testing, and linking to related issues.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai 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: 1

🧹 Nitpick comments (1)
packages/features/availability/lib/getUserAvailability.ts (1)

621-626: Use native ISO serialization for guest busy slots.

guestBusyTimes is already typed as Date, so the Day.js round-trip here is unnecessary work in a hot path.

♻️ Proposed refactor
-    const guestBusyTimesFormatted: EventBusyDetails[] = (initialData?.guestBusyTimes ?? []).map((t) => ({
-      start: dayjs.utc(t.start).toISOString(),
-      end: dayjs.utc(t.end).toISOString(),
+    const guestBusyTimesFormatted: EventBusyDetails[] = (initialData?.guestBusyTimes ?? []).map((t) => ({
+      start: t.start.toISOString(),
+      end: t.end.toISOString(),
       title: "Guest busy",
       source: withSource ? "guest-availability" : "",
     }));

As per coding guidelines "Use date-fns or native Date instead of Day.js when timezone awareness isn't needed".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/features/availability/lib/getUserAvailability.ts` around lines 621 -
626, guestBusyTimesFormatted currently round-trips guestBusyTimes through
dayjs.utc which is unnecessary and costly; change the mapping to use native Date
ISO serialization instead: for each item in initialData?.guestBusyTimes use new
Date(t.start).toISOString() and new Date(t.end).toISOString() to populate
start/end, preserve title ("Guest busy") and source logic (withSource ?
"guest-availability" : ""), and keep the EventBusyDetails shape so you avoid
Day.js in this hot path (update the mapping inside guestBusyTimesFormatted).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/features/users/repositories/UserRepository.ts`:
- Around line 1524-1546: The current aggregation collapses every hit to {id,
email} and loses which secondary email(s) matched; change the logic in
UserRepository (the byPrimary/bySecondary queries and the dedupe loop around
seen) to return the matched secondary address(es) along with the user id (e.g.,
include matchedSecondaryEmails: string[] or an emails array), by selecting the
matching secondaryEmails in the bySecondary query (and preserving the primary
email for byPrimary hits), and when merging [...byPrimary, ...bySecondary]
combine/dedupe per user id so the returned entries include all matched emails
used later by findByUserIdsAndDateRange/viewer slots lookup.

---

Nitpick comments:
In `@packages/features/availability/lib/getUserAvailability.ts`:
- Around line 621-626: guestBusyTimesFormatted currently round-trips
guestBusyTimes through dayjs.utc which is unnecessary and costly; change the
mapping to use native Date ISO serialization instead: for each item in
initialData?.guestBusyTimes use new Date(t.start).toISOString() and new
Date(t.end).toISOString() to populate start/end, preserve title ("Guest busy")
and source logic (withSource ? "guest-availability" : ""), and keep the
EventBusyDetails shape so you avoid Day.js in this hot path (update the mapping
inside guestBusyTimesFormatted).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 821e2db1-208e-428f-bcab-a2d0420dae14

📥 Commits

Reviewing files that changed from the base of the PR and between a17f28e and 8a27404.

📒 Files selected for processing (10)
  • apps/web/modules/schedules/hooks/useEvent.ts
  • apps/web/modules/schedules/hooks/useSchedule.ts
  • packages/features/availability/lib/getUserAvailability.ts
  • packages/features/bookings/repositories/BookingRepository.test.ts
  • packages/features/bookings/repositories/BookingRepository.ts
  • packages/features/users/repositories/UserRepository.test.ts
  • packages/features/users/repositories/UserRepository.ts
  • packages/trpc/server/routers/viewer/slots/getGuestBusyTimesForReschedule.test.ts
  • packages/trpc/server/routers/viewer/slots/types.ts
  • packages/trpc/server/routers/viewer/slots/util.ts

Comment thread packages/features/users/repositories/UserRepository.ts
bcornish1797 pushed a commit to bcornish1797/cal.com that referenced this pull request Apr 15, 2026
Split of calcom#28636 (Part A of 3). Pure additive infra — no call sites change
and no existing behaviour is altered. This layer is the data-access
foundation that Parts B (frontend `rescheduledBy` plumbing) and C
(slots/util.ts business logic) build on.

BookingRepository:
  - findByUidIncludeAttendeeEmails(uid): fetches an original booking's
    attendee emails and the host user's email, used to detect who the
    reschedule initiator is and resolve attendees to Cal.com users.
  - findByUserIdsAndDateRange({ userIds, userEmails, dateFrom, dateTo,
    excludeUid? }): finds ACCEPTED/PENDING bookings overlapping a date
    range by userId or attendee email (case-insensitive), with an
    excludeUid parameter applied at the database level so the caller
    cannot accidentally include the very booking being rescheduled.

UserRepository:
  - findByEmails({ emails }): resolves a list of emails to Cal.com
    users, checking both primary email and verified secondary emails,
    case-insensitively, with input deduplication before the query and
    output deduplication by user id. Uses Promise.all to fan out the two
    lookups concurrently.

Tests cover: empty-input short-circuits, primary vs secondary lookup,
dedup across both lookups, case-insensitive normalization, excludeUid,
OR clause composition, and the select shape used downstream.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 21, 2026

@bcornish1797 Thank you for the detailed rebase summary and for confirming all previous feedback has been addressed. I'll kick off a fresh full review of the current state of the PR now.


🧠 Learnings used
Learnt from: scopecreepsoap
Repo: calcom/cal.diy PR: 28246
File: packages/trpc/server/routers/viewer/bookings/confirm.handler.ts:186-217
Timestamp: 2026-04-20T02:57:03.744Z
Learning: In `packages/trpc/server/routers/viewer/bookings/confirm.handler.ts`, the `forceConfirm` conflict check (added in PR `#28246`) is intentionally scoped to the primary organizer (`userId`) only. Extending conflict detection to `eventType.hosts` (round-robin / collective team event types) is explicitly out of scope for this PR and should be addressed in a dedicated follow-up with test coverage for those flows. Existing booking-creation guards already cover the team-host conflict path.

Learnt from: pmartin-dev
Repo: calcom/cal.diy PR: 28916
File: packages/features/bookings/lib/service/RegularBookingService.ts:2082-2086
Timestamp: 2026-04-16T13:00:15.886Z
Learning: In `packages/features/bookings/lib/service/RegularBookingService.ts`, when all calendar integrations fail (`allCalendarEventsFailed`), the booking is compensated to `BookingStatus.PENDING` via a `prismaClient.booking.update` call immediately after `eventManager.create()` returns. This is intentionally a compensating-update pattern (not an optimistic-PENDING-then-promote pattern), because `createBooking()` conditions downstream side effects on the initial status; refactoring that is considered too high-regression-risk. All side effects (emails, webhooks, no-show triggers) are gated on `booking.status === ACCEPTED` and fire after the in-memory mutation, so no consumer within the handler observes the transient `ACCEPTED` state. The only residual risk is external DB readers during the narrow window between `createBooking()` and the compensating update — accepted as a known trade-off.
✅ Actions performed

Full review triggered.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai 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: 1

♻️ Duplicate comments (1)
packages/features/users/repositories/UserRepository.ts (1)

1510-1538: ⚠️ Potential issue | 🟠 Major

Secondary-email match is still collapsed to the primary email.

The seen.set(u.id, u) dedup uses whichever row arrives first (primary vs. secondary query), so a user matched only via a verified secondaryEmail still returns { id, email: user.email } (the primary). Callers in slots/util.ts then feed these emails into BookingRepository.findByUserIdsAndDateRange's attendee-email filter — meaning bookings where the guest appears under the secondary address won't match, and the host will be shown slots that actually conflict with the guest.

Recommend selecting the matched secondaryEmails in the bySecondary branch and carrying them alongside (e.g., matchedEmails: string[] or emails: string[]), then merging per id.

Note: there's also an existing findVerifiedUsersByEmailsRaw a few lines above (L293–329) that already handles the exact dual-primary/secondary lookup via a raw SQL UNION and preserves matchedEmail. Consider either reusing/extending that helper or mirroring its shape so downstream consumers get the matched email.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/features/users/repositories/UserRepository.ts` around lines 1510 -
1538, findByEmails currently deduplicates users by id using the first-row email,
so matches found only via a verified secondary email return the primary email
and break downstream attendee-email filters; update findByEmails to include the
matched email(s) from the bySecondary query (e.g., add select { secondaryEmails:
{ where: { email: { in: normalized }, emailVerified: { not: null } }, select: {
email: true } } } or mirror the shape of findVerifiedUsersByEmailsRaw to return
matchedEmail/matchedEmails), then when merging results for the same id
combine/replace the stored email with the matched secondary email(s) (or store
matchedEmails: string[]) so callers get the actual matched address;
alternatively reuse/extend findVerifiedUsersByEmailsRaw to return matchedEmail
and call it from findByEmails and return objects that include id and
matchedEmail(s) instead of always returning the primary email.
🧹 Nitpick comments (1)
packages/features/users/repositories/UserRepository.test.ts (1)

117-215: Tests validate the current (collapsed-email) behavior.

Once the secondary-email preservation issue flagged in UserRepository.ts is fixed, the "secondary (verified) email" test at L152–173 should assert that the matched secondary address is returned alongside the user id (not just the primary). Otherwise the tests will lock in the bug.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/features/users/repositories/UserRepository.test.ts` around lines 117
- 215, Update the "should look up users by secondary (verified) email" test to
assert that findByEmails returns the matched secondary address along with the
user id (not only the primary email). Specifically, adjust the mocked Prisma
responses and the final expect for repo.findByEmails in that test so the
returned user object includes the matched secondary email (e.g., a property like
matchedSecondaryEmail or secondaryEmail equal to "secondary@example.com") and
assert that value along with id; keep the existing expect.objectContaining on
the second findMany call that checks the secondaryEmails.where clause unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/features/bookings/repositories/BookingRepository.ts`:
- Around line 2168-2189: The query can return duplicate booking rows when both
OR branches match the same booking; modify the method that calls
this.prismaClient.booking.findMany (the block using userIds, userEmails,
excludeUid and status filters) to deduplicate results by uid before returning
(e.g., run a unique-by-uid pass over the array returned from
prismaClient.booking.findMany and return only one entry per uid), or
alternatively add a clear comment in BookingRepository.ts documenting that
callers (like getUserAvailability) must dedupe by uid; ensure the change
references the prismaClient.booking.findMany call and preserves the existing
selected fields (uid, startTime, endTime, title, userId, status).

---

Duplicate comments:
In `@packages/features/users/repositories/UserRepository.ts`:
- Around line 1510-1538: findByEmails currently deduplicates users by id using
the first-row email, so matches found only via a verified secondary email return
the primary email and break downstream attendee-email filters; update
findByEmails to include the matched email(s) from the bySecondary query (e.g.,
add select { secondaryEmails: { where: { email: { in: normalized },
emailVerified: { not: null } }, select: { email: true } } } or mirror the shape
of findVerifiedUsersByEmailsRaw to return matchedEmail/matchedEmails), then when
merging results for the same id combine/replace the stored email with the
matched secondary email(s) (or store matchedEmails: string[]) so callers get the
actual matched address; alternatively reuse/extend findVerifiedUsersByEmailsRaw
to return matchedEmail and call it from findByEmails and return objects that
include id and matchedEmail(s) instead of always returning the primary email.

---

Nitpick comments:
In `@packages/features/users/repositories/UserRepository.test.ts`:
- Around line 117-215: Update the "should look up users by secondary (verified)
email" test to assert that findByEmails returns the matched secondary address
along with the user id (not only the primary email). Specifically, adjust the
mocked Prisma responses and the final expect for repo.findByEmails in that test
so the returned user object includes the matched secondary email (e.g., a
property like matchedSecondaryEmail or secondaryEmail equal to
"secondary@example.com") and assert that value along with id; keep the existing
expect.objectContaining on the second findMany call that checks the
secondaryEmails.where clause unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: feb43dd4-b3f2-49aa-ac8b-3562ab914133

📥 Commits

Reviewing files that changed from the base of the PR and between e9c6d0e and 5bcf052.

📒 Files selected for processing (10)
  • apps/web/modules/schedules/hooks/useEvent.ts
  • apps/web/modules/schedules/hooks/useSchedule.ts
  • packages/features/availability/lib/getUserAvailability.ts
  • packages/features/bookings/repositories/BookingRepository.test.ts
  • packages/features/bookings/repositories/BookingRepository.ts
  • packages/features/users/repositories/UserRepository.test.ts
  • packages/features/users/repositories/UserRepository.ts
  • packages/trpc/server/routers/viewer/slots/getGuestBusyTimesForReschedule.test.ts
  • packages/trpc/server/routers/viewer/slots/types.ts
  • packages/trpc/server/routers/viewer/slots/util.ts

Comment thread packages/features/bookings/repositories/BookingRepository.ts Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai 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: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/web/modules/schedules/hooks/useSchedule.ts (1)

106-151: ⚠️ Potential issue | 🟠 Major

Do not route reschedules with rescheduledBy through API V2 until the DTO supports it.

rescheduledBy is added to this payload, but the API V2 available-slots input in the provided context does not define that field. Team-event reschedules using API V2 will therefore lose the initiator context, causing attendee-initiated reschedules to incorrectly apply guest blocking.

Conservative local fallback
-  const isCallingApiV2Slots = useApiV2 && Boolean(isTeamEvent) && options.enabled;
+  const isCallingApiV2Slots =
+    useApiV2 && Boolean(isTeamEvent) && options.enabled && !(rescheduleUid && rescheduledBy);

Alternatively, add rescheduledBy?: string | null to the API V2 input DTO and pass it through the slots service.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/modules/schedules/hooks/useSchedule.ts` around lines 106 - 151, The
API V2 call is receiving rescheduledBy in the payload even though the V2 DTO
doesn't support it, losing initiator context; update the call site so
reschedules with rescheduledBy don't go through V2: modify the
isCallingApiV2Slots guard (used to decide V2 usage) or strip rescheduledBy
before calling useApiV2AvailableSlots so that when input.rescheduledBy is
present you fall back to V1 (i.e., change isCallingApiV2Slots = useApiV2 &&
Boolean(isTeamEvent) && options.enabled && !input.rescheduledBy or remove
rescheduledBy from the spread passed to useApiV2AvailableSlots), or
alternatively add rescheduledBy?: string|null to the API V2 DTO and propagate it
through the slots service if you prefer enabling V2.
♻️ Duplicate comments (1)
packages/features/users/repositories/UserRepository.ts (1)

1510-1538: ⚠️ Potential issue | 🟠 Major

Preserve the matched secondary email addresses.

Secondary-email hits currently return only the user’s primary email, so downstream guest-busy lookup misses bookings where that guest participates under the verified secondary address used on the original booking. Return matched email(s) per user and pass those into findByUserIdsAndDateRange.

Suggested shape
-      this.prismaClient.user.findMany({
+      this.prismaClient.user.findMany({
         where: {
           secondaryEmails: {
             some: {
               email: { in: normalized, mode: "insensitive" },
               emailVerified: { not: null },
             },
           },
         },
-        select: { id: true, email: true },
+        select: {
+          id: true,
+          email: true,
+          secondaryEmails: {
+            where: {
+              email: { in: normalized, mode: "insensitive" },
+              emailVerified: { not: null },
+            },
+            select: { email: true },
+          },
+        },
       }),
     ]);
 
-    const seen = new Map<number, { id: number; email: string }>();
-    for (const u of [...byPrimary, ...bySecondary]) {
-      if (!seen.has(u.id)) seen.set(u.id, u);
+    const seen = new Map<number, { id: number; email: string; matchedEmails: string[] }>();
+    for (const u of byPrimary) {
+      seen.set(u.id, { id: u.id, email: u.email, matchedEmails: [u.email] });
+    }
+    for (const u of bySecondary) {
+      const existing = seen.get(u.id) ?? { id: u.id, email: u.email, matchedEmails: [] };
+      seen.set(u.id, {
+        ...existing,
+        matchedEmails: Array.from(
+          new Set([...existing.matchedEmails, ...u.secondaryEmails.map(({ email }) => email)])
+        ),
+      });
     }
     return Array.from(seen.values());
🧹 Nitpick comments (1)
packages/trpc/server/routers/viewer/slots/getGuestBusyTimesForReschedule.test.ts (1)

222-266: Remove comments that only restate the assertions.

These comments duplicate the expect(...).toHaveBeenCalled() assertions; the test names already capture the behavior.

Suggested cleanup
-      // Should proceed to check guest availability (host email matches case-insensitively)
       expect(mockDependencies.userRepo.findByEmails).toHaveBeenCalled();
...
-      // Without rescheduledBy, should still check guest availability (safe default)
       expect(mockDependencies.userRepo.findByEmails).toHaveBeenCalled();
...
-      // Without rescheduledBy, should still check guest availability (safe default)
       expect(mockDependencies.userRepo.findByEmails).toHaveBeenCalled();

As per coding guidelines, “Never add comments that simply restate what the code does”.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/trpc/server/routers/viewer/slots/getGuestBusyTimesForReschedule.test.ts`
around lines 222 - 266, Remove the inline comments that merely restate the
subsequent assertions in the two tests titled "should check guest busy times
when rescheduledBy is not provided (backwards compat)" and "should check guest
busy times when rescheduledBy is null (backwards compat)"; specifically delete
the comments immediately preceding the
expect(mockDependencies.userRepo.findByEmails).toHaveBeenCalled() lines so the
tests remain the same (they still call callGetGuestBusyTimes and assert via
mockDependencies.userRepo.findByEmails and bookingRepo mocks) but without
duplicate explanatory comments.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/features/bookings/repositories/BookingRepository.test.ts`:
- Around line 120-149: The test for findByUserIdsAndDateRange is missing an
assertion that the query filters by userIds; update the
BookingRepository.test.ts case (the one calling
repository.findByUserIdsAndDateRange and mocking
mockPrismaClient.booking.findMany) to assert that the prisma call's where
includes the userId predicate (e.g. userId: { in: [10] } or equivalent)
alongside the existing status and date overlap assertions so the test fails if
userIds are not applied.

In
`@packages/trpc/server/routers/viewer/slots/getGuestBusyTimesForReschedule.test.ts`:
- Around line 359-489: Add a new test (using callGetGuestBusyTimes) that mocks
bookingRepo.findByUidIncludeAttendeeEmails to return attendees with
duplicate/case-varied emails (e.g., "Alias@Cal.com" and "alias@cal.com"), mocks
userRepo.findByEmails to return a single Cal.com user (resolved via secondary
email), and mocks bookingRepo.findByUserIdsAndDateRange to return []. Then
assert that userRepo.findByEmails was called with the original attendee emails,
and that bookingRepo.findByUserIdsAndDateRange was called with
deduplicated/userId list and with userEmails deduped and case-normalized (only
one email for the resolved Cal.com user) and include excludeUid/dateFrom/dateTo
as in other tests; reference
mockDependencies.bookingRepo.findByUidIncludeAttendeeEmails,
mockDependencies.userRepo.findByEmails,
mockDependencies.bookingRepo.findByUserIdsAndDateRange, and
callGetGuestBusyTimes to locate where to add the test.

---

Outside diff comments:
In `@apps/web/modules/schedules/hooks/useSchedule.ts`:
- Around line 106-151: The API V2 call is receiving rescheduledBy in the payload
even though the V2 DTO doesn't support it, losing initiator context; update the
call site so reschedules with rescheduledBy don't go through V2: modify the
isCallingApiV2Slots guard (used to decide V2 usage) or strip rescheduledBy
before calling useApiV2AvailableSlots so that when input.rescheduledBy is
present you fall back to V1 (i.e., change isCallingApiV2Slots = useApiV2 &&
Boolean(isTeamEvent) && options.enabled && !input.rescheduledBy or remove
rescheduledBy from the spread passed to useApiV2AvailableSlots), or
alternatively add rescheduledBy?: string|null to the API V2 DTO and propagate it
through the slots service if you prefer enabling V2.

---

Nitpick comments:
In
`@packages/trpc/server/routers/viewer/slots/getGuestBusyTimesForReschedule.test.ts`:
- Around line 222-266: Remove the inline comments that merely restate the
subsequent assertions in the two tests titled "should check guest busy times
when rescheduledBy is not provided (backwards compat)" and "should check guest
busy times when rescheduledBy is null (backwards compat)"; specifically delete
the comments immediately preceding the
expect(mockDependencies.userRepo.findByEmails).toHaveBeenCalled() lines so the
tests remain the same (they still call callGetGuestBusyTimes and assert via
mockDependencies.userRepo.findByEmails and bookingRepo mocks) but without
duplicate explanatory comments.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: aea1dcb1-ede6-40ce-8f38-79f857d39545

📥 Commits

Reviewing files that changed from the base of the PR and between e9c6d0e and fb8da3c.

📒 Files selected for processing (10)
  • apps/web/modules/schedules/hooks/useEvent.ts
  • apps/web/modules/schedules/hooks/useSchedule.ts
  • packages/features/availability/lib/getUserAvailability.ts
  • packages/features/bookings/repositories/BookingRepository.test.ts
  • packages/features/bookings/repositories/BookingRepository.ts
  • packages/features/users/repositories/UserRepository.test.ts
  • packages/features/users/repositories/UserRepository.ts
  • packages/trpc/server/routers/viewer/slots/getGuestBusyTimesForReschedule.test.ts
  • packages/trpc/server/routers/viewer/slots/types.ts
  • packages/trpc/server/routers/viewer/slots/util.ts

Comment on lines +120 to +149
it("should query bookings by userId when userIds are provided", async () => {
const mockBookings = [
{
uid: "booking-1",
startTime: new Date("2026-04-10T09:00:00Z"),
endTime: new Date("2026-04-10T10:00:00Z"),
title: "Meeting",
userId: 10,
status: BookingStatus.ACCEPTED,
},
];
mockPrismaClient.booking.findMany.mockResolvedValue(mockBookings);

const result = await repository.findByUserIdsAndDateRange({
userIds: [10],
userEmails: [],
dateFrom,
dateTo,
});

expect(result).toEqual(mockBookings);
expect(mockPrismaClient.booking.findMany).toHaveBeenCalledWith(
expect.objectContaining({
where: expect.objectContaining({
status: { in: [BookingStatus.ACCEPTED, BookingStatus.PENDING] },
AND: [{ startTime: { lt: dateTo } }, { endTime: { gt: dateFrom } }],
}),
})
);
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Assert the userId predicate in the userId-only path.

This test currently verifies status/date overlap but would still pass if the repository forgot to filter by userIds, causing guest busy-time lookups to include unrelated bookings.

Suggested assertion
       expect(mockPrismaClient.booking.findMany).toHaveBeenCalledWith(
         expect.objectContaining({
           where: expect.objectContaining({
             status: { in: [BookingStatus.ACCEPTED, BookingStatus.PENDING] },
             AND: [{ startTime: { lt: dateTo } }, { endTime: { gt: dateFrom } }],
+            OR: expect.arrayContaining([{ userId: { in: [10] } }]),
           }),
         })
       );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/features/bookings/repositories/BookingRepository.test.ts` around
lines 120 - 149, The test for findByUserIdsAndDateRange is missing an assertion
that the query filters by userIds; update the BookingRepository.test.ts case
(the one calling repository.findByUserIdsAndDateRange and mocking
mockPrismaClient.booking.findMany) to assert that the prisma call's where
includes the userId predicate (e.g. userId: { in: [10] } or equivalent)
alongside the existing status and date overlap assertions so the test fails if
userIds are not applied.

…secondary)

Secondary-email hits were collapsing to the primary address, so guest busy-time lookup missed bookings where a Cal.com user attended under a verified secondary email. findByEmails now returns matchedEmails: string[] per user (union of primary and/or secondary matches), and the guest-busy flow feeds those actual matched emails into findByUserIdsAndDateRange.
The API V2 available-slots DTO does not carry rescheduledBy, so routing a reschedule through V2 loses the host/attendee initiator context that the V1 guest busy-time gate relies on. The useApiV2Slots guard now opts back to V1 whenever both rescheduleUid and rescheduledBy are present.
Three inline comments in getGuestBusyTimesForReschedule.test.ts just paraphrased the expect lines below them. Removed per the project coding guideline that discourages comments which only restate what the code does.
@bcornish1797
Copy link
Copy Markdown
Author

Addressed the three actionable items from the two CodeRabbit passes.

  • 989b4197bfindByEmails now returns matchedEmails: string[] per user (union of primary and verified secondary hits). The guest-busy flow feeds those actual matched addresses into findByUserIdsAndDateRange, so a Cal.com user who attended under a verified secondary email is no longer missed. Tests in UserRepository.test.ts and getGuestBusyTimesForReschedule.test.ts updated to the new return shape.
  • 23b360939isCallingApiV2Slots in useSchedule opts back to V1 when both rescheduleUid and rescheduledBy are set, because the V2 available-slots DTO does not carry rescheduledBy. Kept the diff minimal via a guard-side fallback; happy to propagate rescheduledBy through the V2 DTO instead if that is preferred.
  • 5ca694d29 — three inline comments in the reschedule test file that just restated the assertions below them, removed per the repo coding guideline.

Skipped for now, open to adding if wanted: the optional test cases suggested under "prompt for AI agents" (duplicate / case-varied secondary emails, and an explicit userId filter assertion in BookingRepository.test.ts).

CI is still blocked by the run-ci label gate — would appreciate a maintainer re-applying the label so typecheck / tests / build can run against the new head.

@cubic-dev-ai @coderabbitai — please re-review.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 21, 2026

@bcornish1797 Thank you for the detailed summary! All three items look well-reasoned:

  • The matchedEmails return shape ensures secondary-email attendees are correctly covered in the busy-time query — solid fix.
  • The V2 guard-side fallback for isCallingApiV2Slots is a clean minimal diff. Propagating rescheduledBy through the V2 DTO would be the cleaner long-term path, but a follow-up PR for that is reasonable.
  • Removing the redundant inline comments aligns with the repo's coding guidelines.

Kicking off a fresh full review now.


🧠 Learnings used
Learnt from: scopecreepsoap
Repo: calcom/cal.diy PR: 28246
File: packages/trpc/server/routers/viewer/bookings/confirm.handler.ts:186-217
Timestamp: 2026-04-20T02:57:03.744Z
Learning: In `packages/trpc/server/routers/viewer/bookings/confirm.handler.ts`, the `forceConfirm` conflict check (added in PR `#28246`) is intentionally scoped to the primary organizer (`userId`) only. Extending conflict detection to `eventType.hosts` (round-robin / collective team event types) is explicitly out of scope for this PR and should be addressed in a dedicated follow-up with test coverage for those flows. Existing booking-creation guards already cover the team-host conflict path.

Learnt from: pmartin-dev
Repo: calcom/cal.diy PR: 28916
File: packages/features/bookings/lib/service/RegularBookingService.ts:2082-2086
Timestamp: 2026-04-16T13:00:15.886Z
Learning: In `packages/features/bookings/lib/service/RegularBookingService.ts`, when all calendar integrations fail (`allCalendarEventsFailed`), the booking is compensated to `BookingStatus.PENDING` via a `prismaClient.booking.update` call immediately after `eventManager.create()` returns. This is intentionally a compensating-update pattern (not an optimistic-PENDING-then-promote pattern), because `createBooking()` conditions downstream side effects on the initial status; refactoring that is considered too high-regression-risk. All side effects (emails, webhooks, no-show triggers) are gated on `booking.status === ACCEPTED` and fire after the in-memory mutation, so no consumer within the handler observes the transient `ACCEPTED` state. The only residual risk is external DB readers during the narrow window between `createBooking()` and the compensating update — accepted as a known trade-off.
✅ Actions performed

Full review triggered.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai 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: 1

🧹 Nitpick comments (2)
packages/features/users/repositories/UserRepository.test.ts (1)

203-231: Tighten these assertions to cover the full contract.

The last test only checks length, and the normalization test only checks the primary lookup. Assert the secondary lookup input and each returned user’s matchedEmails so regressions in the new mapping behavior fail loudly.

Suggested assertion hardening
     test("should normalize emails to lowercase and deduplicate input", async () => {
       mockPrismaClient.user.findMany
         .mockResolvedValueOnce([{ id: 1, email: "user@example.com" }])
         .mockResolvedValueOnce([]);

       await repo.findByEmails({ emails: ["User@Example.COM", "user@example.com"] });

       expect(mockPrismaClient.user.findMany).toHaveBeenNthCalledWith(
         1,
         expect.objectContaining({
           where: { email: { in: ["user@example.com"], mode: "insensitive" } },
         })
       );
+      expect(mockPrismaClient.user.findMany).toHaveBeenNthCalledWith(
+        2,
+        expect.objectContaining({
+          where: {
+            secondaryEmails: {
+              some: {
+                email: { in: ["user@example.com"], mode: "insensitive" },
+                emailVerified: { not: null },
+              },
+            },
+          },
+        })
+      );
     });

     test("should return multiple distinct users", async () => {
       mockPrismaClient.user.findMany
         .mockResolvedValueOnce([
@@
       });

-      expect(result).toHaveLength(2);
+      expect(result).toEqual(
+        expect.arrayContaining([
+          { id: 1, email: "user1@example.com", matchedEmails: ["user1@example.com"] },
+          { id: 2, email: "user2@example.com", matchedEmails: ["user2@example.com"] },
+        ])
+      );
+      expect(result).toHaveLength(2);
     });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/features/users/repositories/UserRepository.test.ts` around lines 203
- 231, Tighten the tests for repo.findByEmails: in the "normalize emails" test,
assert both calls to mockPrismaClient.user.findMany (the primary and the
secondary lookup) receive the expected where.in arrays (normalized/unique
emails) and include checking the second call's arguments; in the "should return
multiple distinct users" test, assert the returned array contains the exact user
objects and that each user has the correct matchedEmails mapping (e.g.,
matchedEmails for id 1 includes "user1@example.com", for id 2 includes
"user2@example.com") so the mapping/secondary-lookup behavior is verified in
addition to length.
packages/trpc/server/routers/viewer/slots/util.ts (1)

717-720: Keep degradation logs structured and bounded.

safeStringify({ rescheduleUid, error }) produces a string payload and may either drop useful Error fields or include too much from repository failures. Prefer logging explicit, bounded metadata.

Suggested logging adjustment
+      const errorDetails =
+        error instanceof Error
+          ? {
+              name: error.name,
+              message: error.message,
+              code: "code" in error ? String(error.code) : undefined,
+            }
+          : { value: safeStringify(error) };
+
       log.warn(
         "[getGuestBusyTimesForReschedule] degraded to empty result",
-        safeStringify({ rescheduleUid, error })
+        { rescheduleUid, error: errorDetails }
       );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/trpc/server/routers/viewer/slots/util.ts` around lines 717 - 720,
The degradation log currently calls log.warn with safeStringify({ rescheduleUid,
error }) which can either drop Error fields or include unbounded payloads;
change the log to emit a small, structured object instead (inside
getGuestBusyTimesForReschedule and the log.warn call), e.g. include
rescheduleUid and a bounded error object with explicit fields like error.name,
error.message and optionally error.code or a boolean flag (omit error.stack and
large nested properties) so the logged payload is predictable and size-bounded
rather than relying on safeStringify.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/web/modules/schedules/hooks/useSchedule.ts`:
- Around line 106-107: The code forwards an email-valued rescheduledBy from
useSchedule.ts into the slots cache which currently JSON-stringifies inputs and
logs cacheKey in packages/trpc/server/routers/viewer/slots/util.ts (see cache
key handling around the existing log sites), so sanitize before persisting or
logging: update the cache key generation in the slots util to either
remove/redact the rescheduledBy field from the input payload or replace it with
a deterministic hash (e.g., SHA-256) of the email, and ensure the same sanitized
value is used wherever cacheKey is stored or logged (the log calls near the
existing cacheKey creation must log the hashed/redacted key instead of raw
JSON); keep the redaction/hashing logic centralized in the cache key generation
function so all callers (including where rescheduledBy is passed) inherit the
fix.

---

Nitpick comments:
In `@packages/features/users/repositories/UserRepository.test.ts`:
- Around line 203-231: Tighten the tests for repo.findByEmails: in the
"normalize emails" test, assert both calls to mockPrismaClient.user.findMany
(the primary and the secondary lookup) receive the expected where.in arrays
(normalized/unique emails) and include checking the second call's arguments; in
the "should return multiple distinct users" test, assert the returned array
contains the exact user objects and that each user has the correct matchedEmails
mapping (e.g., matchedEmails for id 1 includes "user1@example.com", for id 2
includes "user2@example.com") so the mapping/secondary-lookup behavior is
verified in addition to length.

In `@packages/trpc/server/routers/viewer/slots/util.ts`:
- Around line 717-720: The degradation log currently calls log.warn with
safeStringify({ rescheduleUid, error }) which can either drop Error fields or
include unbounded payloads; change the log to emit a small, structured object
instead (inside getGuestBusyTimesForReschedule and the log.warn call), e.g.
include rescheduleUid and a bounded error object with explicit fields like
error.name, error.message and optionally error.code or a boolean flag (omit
error.stack and large nested properties) so the logged payload is predictable
and size-bounded rather than relying on safeStringify.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8f47e301-dae7-4148-92fa-a6ef7b556989

📥 Commits

Reviewing files that changed from the base of the PR and between fb8da3c and 5ca694d.

📒 Files selected for processing (5)
  • apps/web/modules/schedules/hooks/useSchedule.ts
  • packages/features/users/repositories/UserRepository.test.ts
  • packages/features/users/repositories/UserRepository.ts
  • packages/trpc/server/routers/viewer/slots/getGuestBusyTimesForReschedule.test.ts
  • packages/trpc/server/routers/viewer/slots/util.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/trpc/server/routers/viewer/slots/getGuestBusyTimesForReschedule.test.ts
  • packages/features/users/repositories/UserRepository.ts

Comment on lines 106 to +107
rescheduleUid,
rescheduledBy,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Sanitize cache/log handling before adding email-valued rescheduledBy.

Line 107 forwards rescheduledBy, which is later compared to original.user.email, so this is an email/user identifier. The slots cache currently JSON-stringifies the full input and logs the resulting cacheKey in packages/trpc/server/routers/viewer/slots/util.ts Lines 110, 131, and 139. Please make sure the server hashes or redacts that cache key/log payload so this new identifier is not persisted or emitted in raw form.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/modules/schedules/hooks/useSchedule.ts` around lines 106 - 107, The
code forwards an email-valued rescheduledBy from useSchedule.ts into the slots
cache which currently JSON-stringifies inputs and logs cacheKey in
packages/trpc/server/routers/viewer/slots/util.ts (see cache key handling around
the existing log sites), so sanitize before persisting or logging: update the
cache key generation in the slots util to either remove/redact the rescheduledBy
field from the input payload or replace it with a deterministic hash (e.g.,
SHA-256) of the email, and ensure the same sanitized value is used wherever
cacheKey is stored or logged (the log calls near the existing cacheKey creation
must log the hashed/redacted key instead of raw JSON); keep the
redaction/hashing logic centralized in the cache key generation function so all
callers (including where rescheduledBy is passed) inherit the fix.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
packages/trpc/server/routers/viewer/slots/util.ts (1)

694-710: Consider clarifying email normalization flow, but code is functionally correct.

The three concerns raised need updating:

  1. Line 694 — attendee email normalization: findByEmails already normalizes emails internally (converts to lowercase and deduplicates at the source). While not explicit at the call site, the contract is upheld. This is an optional code clarity improvement, not a functional issue.

  2. Line 702 — case dedup of matchedEmails: While additional case variants could be sent to findByUserIdsAndDateRange, the booking query uses mode: "insensitive" in its Prisma where clause (line 2175 of BookingRepository), so case variants are handled correctly. This is a minor efficiency consideration rather than a bug.

  3. Line 702 — nullish guard for matchedEmails: This concern is invalid. The matchedEmails field is always initialized as a string[] in UserRepository.findByEmails (lines 1545 and 1553) and is guaranteed to exist. The type signature confirms it is never undefined.

The code functions correctly as-is. If desired, normalizing emails at line 694 would make the contract explicit, but it's not required.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/trpc/server/routers/viewer/slots/util.ts` around lines 694 - 710,
Normalize and dedupe attendee emails before calling findByEmails and ensure
matchedEmails are normalized/deduped before passing to
findByUserIdsAndDateRange: transform original.attendees.map(a => a.email) into a
lowercased, filtered and unique list (e.g., map to .toLowerCase(),
filter(Boolean), Array.from(new Set(...))) before calling
this.dependencies.userRepo.findByEmails, and then build calUserEmails from
calUsers.flatMap(u => u.matchedEmails ?? []).map(e => e.toLowerCase()) and
dedupe with Array.from(new Set(...)) before calling
this.dependencies.bookingRepo.findByUserIdsAndDateRange; keep the same userIds
array (calUsers.map(u => u.id)) and preserve excludeUid/date range parameters.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/trpc/server/routers/viewer/slots/util.ts`:
- Around line 694-710: Normalize and dedupe attendee emails before calling
findByEmails and ensure matchedEmails are normalized/deduped before passing to
findByUserIdsAndDateRange: transform original.attendees.map(a => a.email) into a
lowercased, filtered and unique list (e.g., map to .toLowerCase(),
filter(Boolean), Array.from(new Set(...))) before calling
this.dependencies.userRepo.findByEmails, and then build calUserEmails from
calUsers.flatMap(u => u.matchedEmails ?? []).map(e => e.toLowerCase()) and
dedupe with Array.from(new Set(...)) before calling
this.dependencies.bookingRepo.findByUserIdsAndDateRange; keep the same userIds
array (calUsers.map(u => u.id)) and preserve excludeUid/date range parameters.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1132de45-0d42-421c-b183-0b3fa5cf1e97

📥 Commits

Reviewing files that changed from the base of the PR and between e9c6d0e and 5ca694d.

📒 Files selected for processing (10)
  • apps/web/modules/schedules/hooks/useEvent.ts
  • apps/web/modules/schedules/hooks/useSchedule.ts
  • packages/features/availability/lib/getUserAvailability.ts
  • packages/features/bookings/repositories/BookingRepository.test.ts
  • packages/features/bookings/repositories/BookingRepository.ts
  • packages/features/users/repositories/UserRepository.test.ts
  • packages/features/users/repositories/UserRepository.ts
  • packages/trpc/server/routers/viewer/slots/getGuestBusyTimesForReschedule.test.ts
  • packages/trpc/server/routers/viewer/slots/types.ts
  • packages/trpc/server/routers/viewer/slots/util.ts

Add missing assertion on the bySecondary findMany call in the normalization test, and assert the exact { id, email, matchedEmails } shape in the multi-user test so regressions in the matchedEmails mapping fail loudly.
Replace safeStringify({ rescheduleUid, error }) with a structured, size-bounded object that surfaces Error.name/message/code and falls back to safeStringify only for non-Error values. Avoids unbounded nested repository error payloads and dropped Error fields.
@bcornish1797
Copy link
Copy Markdown
Author

Addressed the two nitpicks from the latest pass.

  • efb6cbcb — tightens findByEmails tests so the mapping regressions fail loudly: assertion on the secondary findMany call in the normalization test, and explicit { id, email, matchedEmails } shape check in the multi-user test.
  • c050464 — bounds the guest busy-time degradation log to { rescheduleUid, error: { name, message, code? } } instead of safeStringify over the full error payload, so unbounded repository errors or dropped Error fields do not leak into the warn log.

On the actionable flagged for useSchedule.ts:106-107 (email-valued rescheduledBy flowing into the Redis slots cache key): this is a pre-existing pattern — the cache key already stringifies email and teamMemberEmail from the same input. A principled fix would hash the whole key (or strip all email-valued fields) in withSlotsCache, not just this one field, and that belongs in its own PR. Open to doing that follow-up.

The review on util.ts 694-710 (normalize/dedupe around findByEmails / findByUserIdsAndDateRange) flagged then self-verified: findByEmails already normalizes inside, and findByUserIdsAndDateRange's Prisma where uses mode: "insensitive", so no change needed.

@coderabbitai — please re-review.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 21, 2026

@bcornish1797 Thank you for addressing the two nitpicks and for the clear explanations on the items you intentionally left unchanged:

  • The useSchedule.ts cache-key concern is a pre-existing cross-cutting pattern — scoping the fix to withSlotsCache in a follow-up PR is the right call.
  • The self-verification on util.ts 694-710 is correct: findByEmails normalises internally and Prisma's mode: "insensitive" covers the query side, so no redundant handling is needed at the call site.

Kicking off a fresh full review now.


🧠 Learnings used
Learnt from: scopecreepsoap
Repo: calcom/cal.diy PR: 28246
File: packages/trpc/server/routers/viewer/bookings/confirm.handler.ts:186-217
Timestamp: 2026-04-20T02:57:03.744Z
Learning: In `packages/trpc/server/routers/viewer/bookings/confirm.handler.ts`, the `forceConfirm` conflict check (added in PR `#28246`) is intentionally scoped to the primary organizer (`userId`) only. Extending conflict detection to `eventType.hosts` (round-robin / collective team event types) is explicitly out of scope for this PR and should be addressed in a dedicated follow-up with test coverage for those flows. Existing booking-creation guards already cover the team-host conflict path.

Learnt from: pmartin-dev
Repo: calcom/cal.diy PR: 28916
File: packages/features/bookings/lib/service/RegularBookingService.ts:2082-2086
Timestamp: 2026-04-16T13:00:15.886Z
Learning: In `packages/features/bookings/lib/service/RegularBookingService.ts`, when all calendar integrations fail (`allCalendarEventsFailed`), the booking is compensated to `BookingStatus.PENDING` via a `prismaClient.booking.update` call immediately after `eventManager.create()` returns. This is intentionally a compensating-update pattern (not an optimistic-PENDING-then-promote pattern), because `createBooking()` conditions downstream side effects on the initial status; refactoring that is considered too high-regression-risk. All side effects (emails, webhooks, no-show triggers) are gated on `booking.status === ACCEPTED` and fire after the in-memory mutation, so no consumer within the handler observes the transient `ACCEPTED` state. The only residual risk is external DB readers during the narrow window between `createBooking()` and the compensating update — accepted as a known trade-off.
✅ Actions performed

Full review triggered.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
packages/trpc/server/routers/viewer/slots/util.ts (1)

718-729: Nit: String((error as { code?: unknown }).code) can serialize to the literal "undefined" / "null".

"code" in error only checks presence, not value. If the property exists but is undefined/null (common for wrapped errors), the log emits code: "undefined" / code: "null" which is noisier than simply omitting the field.

♻️ Proposed tweak
       const errorDetails =
         error instanceof Error
           ? {
               name: error.name,
               message: error.message,
-              code: "code" in error ? String((error as { code?: unknown }).code) : undefined,
+              code:
+                "code" in error && (error as { code?: unknown }).code != null
+                  ? String((error as { code?: unknown }).code)
+                  : undefined,
             }
           : { value: safeStringify(error) };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/trpc/server/routers/viewer/slots/util.ts` around lines 718 - 729,
The log currently checks only for the presence of a "code" property and
serializes it which can produce literal "undefined"/"null"; update the
errorDetails construction in getGuestBusyTimesForReschedule so you read the code
value into a local (e.g., codeVal = (error as any).code) and only include code:
String(codeVal) when codeVal != null (i.e., not undefined and not null); keep
the rest of the shape (name, message, fallback to safeStringify) and continue
passing errorDetails into log.warn.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/trpc/server/routers/viewer/slots/util.ts`:
- Around line 718-729: The log currently checks only for the presence of a
"code" property and serializes it which can produce literal "undefined"/"null";
update the errorDetails construction in getGuestBusyTimesForReschedule so you
read the code value into a local (e.g., codeVal = (error as any).code) and only
include code: String(codeVal) when codeVal != null (i.e., not undefined and not
null); keep the rest of the shape (name, message, fallback to safeStringify) and
continue passing errorDetails into log.warn.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7f8a6b83-691c-44f2-8489-238eb1e6d66a

📥 Commits

Reviewing files that changed from the base of the PR and between 5ca694d and c050464.

📒 Files selected for processing (2)
  • packages/features/users/repositories/UserRepository.test.ts
  • packages/trpc/server/routers/viewer/slots/util.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/features/users/repositories/UserRepository.test.ts

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai 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: 2

♻️ Duplicate comments (1)
packages/features/bookings/repositories/BookingRepository.ts (1)

2168-2189: ⚠️ Potential issue | 🟡 Minor

OR branches can return duplicate rows for the same booking.

When a Cal.com guest is both the organizer (userId match) and also listed among attendees for the same booking (or two guests whose one is organizer and another is an attendee on overlapping data), the Prisma OR produces duplicates — the same uid row can appear twice. Downstream subtract() in getUserAvailability is idempotent so correctness is fine, but payloads are inflated and busy-time diagnostics can be misleading. Consider deduping by uid before returning, or documenting the contract for callers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/features/bookings/repositories/BookingRepository.ts` around lines
2168 - 2189, The findMany OR can yield duplicate booking rows (same uid) when a
user is both organizer and an attendee; update the method containing
this.prismaClient.booking.findMany to dedupe results by uid before returning
(e.g., collect returned entries into a map keyed by uid and return the unique
values) so callers like getUserAvailability see a single record per booking;
ensure the return shape (uid, startTime, endTime, title, userId, status) is
preserved.
🧹 Nitpick comments (3)
packages/features/availability/lib/getUserAvailability.ts (1)

621-626: Minor: setting source: "" when withSource is false is redundant.

detailedBusyTimes is already stripped of source via the withSource ? ... : map(({ source, ...rest }) => rest) branch on Lines 641–643, so assigning an empty string here is just noise. The existing busyTimes.map(...) above doesn't do this because those entries come with real source values. Consider only setting source when withSource is true (and leaving it undefined otherwise) to stay consistent with UserAvailabilityBusyDetails's Optional<EventBusyDetails, "source"> typing.

♻️ Proposed tweak
-    const guestBusyTimesFormatted: EventBusyDetails[] = (initialData?.guestBusyTimes ?? []).map((t) => ({
-      start: t.start.toISOString(),
-      end: t.end.toISOString(),
-      title: "Guest busy",
-      source: withSource ? "guest-availability" : "",
-    }));
+    const guestBusyTimesFormatted = (initialData?.guestBusyTimes ?? []).map((t) => ({
+      start: t.start.toISOString(),
+      end: t.end.toISOString(),
+      title: "Guest busy",
+      ...(withSource ? { source: "guest-availability" } : {}),
+    })) as EventBusyDetails[];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/features/availability/lib/getUserAvailability.ts` around lines 621 -
626, The guestBusyTimesFormatted mapping should not assign an empty string to
source when withSource is false; update the mapping in getUserAvailability (the
guestBusyTimesFormatted const that maps initialData?.guestBusyTimes) to only
include source when withSource is true (omit/leave undefined otherwise) so it
matches how detailedBusyTimes is handled and conforms to the
UserAvailabilityBusyDetails Optional<EventBusyDetails, "source"> typing; locate
guestBusyTimesFormatted, initialData, and withSource and conditionally add the
source property instead of setting it to "".
apps/web/modules/schedules/hooks/useSchedule.ts (1)

141-145: V2 fallback condition is sound and appropriately scoped. The V2 Available Slots API indeed lacks a rescheduledBy parameter, so the fallback to V1 when both rescheduleUid and rescheduledBy are present prevents loss of host/attendee initiator gating. Extending the V2 DTO to support rescheduledBy is a valid enhancement but should be a dedicated follow-up with full test coverage for host-initiated reschedule flows, as this impacts guest busy-time lookups and confirmation logic.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/modules/schedules/hooks/useSchedule.ts` around lines 141 - 145, The
V2 fallback logic is correct: keep the computed flag isCallingApiV2Slots
(derived from useApiV2, isTeamEvent, options.enabled, rescheduleUid,
rescheduledBy) as-is, but add a concise TODO noting that supporting
rescheduledBy in the V2 available-slots DTO is a follow-up work item with test
coverage for host-initiated reschedules and guest busy-time lookup; reference
the symbol isCallingApiV2Slots and the variables rescheduleUid/rescheduledBy in
the TODO so the future change can be located easily in useSchedule.ts.
packages/trpc/server/routers/viewer/slots/types.ts (1)

29-29: Consider tightening type validation for rescheduledBy parameter.

The rescheduledBy parameter is part of the documented reschedule flow and controls confirmation behavior (auto-confirm if owner email, manual confirmation otherwise). Instead of generic z.string(), use z.string().email().nullish() for better type safety and to reject malformed inputs early.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/trpc/server/routers/viewer/slots/types.ts` at line 29, The
rescheduledBy field currently uses a permissive z.string().nullish(); tighten
validation by replacing it with z.string().email().nullish() so only valid
emails (or null/undefined) are accepted. Locate the rescheduledBy schema entry
in types.ts (the line defining rescheduledBy) and update the Zod validator to
z.string().email().nullish(), ensuring the rest of the schema stays unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/features/users/repositories/UserRepository.ts`:
- Around line 1510-1557: The primary branch in findByEmails accepts users
regardless of locked/emailVerified state; update the prismaClient.user.findMany
call used for byPrimary to mirror the verified semantics by adding filters
emailVerified: { not: null } and locked: false (same shape as the secondary
query), so both byPrimary and bySecondary only return non-locked, email-verified
users; keep the rest of the merging logic (seen map, matchedEmails) intact.

In
`@packages/trpc/server/routers/viewer/slots/getGuestBusyTimesForReschedule.test.ts`:
- Around line 7-8: The test currently uses typeof
AvailableSlotsService.prototype._getGuestBusyTimesForReschedule which references
the private method and triggers TS2341; replace that with a structural function
type that matches the private method's signature instead of referencing it by
name (e.g. declare type GetGuestBusyTimesForReschedule = (/* same params as
_getGuestBusyTimesForReschedule */) => Promise</* same return type */>), or
reuse an exported public type if one exists; keep references to
AvailableSlotsService and _getGuestBusyTimesForReschedule in the comment for
traceability but do not use typeof on the private member.

---

Duplicate comments:
In `@packages/features/bookings/repositories/BookingRepository.ts`:
- Around line 2168-2189: The findMany OR can yield duplicate booking rows (same
uid) when a user is both organizer and an attendee; update the method containing
this.prismaClient.booking.findMany to dedupe results by uid before returning
(e.g., collect returned entries into a map keyed by uid and return the unique
values) so callers like getUserAvailability see a single record per booking;
ensure the return shape (uid, startTime, endTime, title, userId, status) is
preserved.

---

Nitpick comments:
In `@apps/web/modules/schedules/hooks/useSchedule.ts`:
- Around line 141-145: The V2 fallback logic is correct: keep the computed flag
isCallingApiV2Slots (derived from useApiV2, isTeamEvent, options.enabled,
rescheduleUid, rescheduledBy) as-is, but add a concise TODO noting that
supporting rescheduledBy in the V2 available-slots DTO is a follow-up work item
with test coverage for host-initiated reschedules and guest busy-time lookup;
reference the symbol isCallingApiV2Slots and the variables
rescheduleUid/rescheduledBy in the TODO so the future change can be located
easily in useSchedule.ts.

In `@packages/features/availability/lib/getUserAvailability.ts`:
- Around line 621-626: The guestBusyTimesFormatted mapping should not assign an
empty string to source when withSource is false; update the mapping in
getUserAvailability (the guestBusyTimesFormatted const that maps
initialData?.guestBusyTimes) to only include source when withSource is true
(omit/leave undefined otherwise) so it matches how detailedBusyTimes is handled
and conforms to the UserAvailabilityBusyDetails Optional<EventBusyDetails,
"source"> typing; locate guestBusyTimesFormatted, initialData, and withSource
and conditionally add the source property instead of setting it to "".

In `@packages/trpc/server/routers/viewer/slots/types.ts`:
- Line 29: The rescheduledBy field currently uses a permissive
z.string().nullish(); tighten validation by replacing it with
z.string().email().nullish() so only valid emails (or null/undefined) are
accepted. Locate the rescheduledBy schema entry in types.ts (the line defining
rescheduledBy) and update the Zod validator to z.string().email().nullish(),
ensuring the rest of the schema stays unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2caf2b0d-e097-45f3-92ad-1b530683be57

📥 Commits

Reviewing files that changed from the base of the PR and between e9c6d0e and c050464.

📒 Files selected for processing (10)
  • apps/web/modules/schedules/hooks/useEvent.ts
  • apps/web/modules/schedules/hooks/useSchedule.ts
  • packages/features/availability/lib/getUserAvailability.ts
  • packages/features/bookings/repositories/BookingRepository.test.ts
  • packages/features/bookings/repositories/BookingRepository.ts
  • packages/features/users/repositories/UserRepository.test.ts
  • packages/features/users/repositories/UserRepository.ts
  • packages/trpc/server/routers/viewer/slots/getGuestBusyTimesForReschedule.test.ts
  • packages/trpc/server/routers/viewer/slots/types.ts
  • packages/trpc/server/routers/viewer/slots/util.ts

Comment on lines +1510 to +1557
async findByEmails({ emails }: { emails: string[] }) {
if (!emails.length) return [];

const normalized = Array.from(new Set(emails.map((e) => e.toLowerCase())));

const [byPrimary, bySecondary] = await Promise.all([
this.prismaClient.user.findMany({
where: { email: { in: normalized, mode: "insensitive" } },
select: { id: true, email: true },
}),
this.prismaClient.user.findMany({
where: {
secondaryEmails: {
some: {
email: { in: normalized, mode: "insensitive" },
emailVerified: { not: null },
},
},
},
select: {
id: true,
email: true,
secondaryEmails: {
where: {
email: { in: normalized, mode: "insensitive" },
emailVerified: { not: null },
},
select: { email: true },
},
},
}),
]);

const seen = new Map<number, { id: number; email: string; matchedEmails: string[] }>();
for (const u of byPrimary) {
seen.set(u.id, { id: u.id, email: u.email, matchedEmails: [u.email] });
}
for (const u of bySecondary) {
const existing = seen.get(u.id);
const secondaryMatches = u.secondaryEmails.map((s) => s.email);
if (existing) {
existing.matchedEmails = Array.from(new Set([...existing.matchedEmails, ...secondaryMatches]));
} else {
seen.set(u.id, { id: u.id, email: u.email, matchedEmails: secondaryMatches });
}
}
return Array.from(seen.values());
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

findByEmails does not filter out locked or unverified primary-email users.

The secondary-email branch requires emailVerified: { not: null }, and the sibling findVerifiedUsersByEmailsRaw additionally enforces locked = FALSE for both branches. Here, the primary-email query (Lines 1516–1519) accepts any user regardless of locked or emailVerified status. In the rescheduling flow this means a guest whose account is locked or whose primary email is unverified can still have their bookings pulled as busy time, which is inconsistent with how verified-secondary matches are gated and can surface bookings from disabled accounts.

Consider aligning with the verified-lookup semantics:

🛡️ Proposed fix
       this.prismaClient.user.findMany({
-        where: { email: { in: normalized, mode: "insensitive" } },
+        where: {
+          email: { in: normalized, mode: "insensitive" },
+          emailVerified: { not: null },
+          locked: false,
+        },
         select: { id: true, email: true },
       }),
       this.prismaClient.user.findMany({
         where: {
+          locked: false,
           secondaryEmails: {
             some: {
               email: { in: normalized, mode: "insensitive" },
               emailVerified: { not: null },
             },
           },
         },
📝 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
async findByEmails({ emails }: { emails: string[] }) {
if (!emails.length) return [];
const normalized = Array.from(new Set(emails.map((e) => e.toLowerCase())));
const [byPrimary, bySecondary] = await Promise.all([
this.prismaClient.user.findMany({
where: { email: { in: normalized, mode: "insensitive" } },
select: { id: true, email: true },
}),
this.prismaClient.user.findMany({
where: {
secondaryEmails: {
some: {
email: { in: normalized, mode: "insensitive" },
emailVerified: { not: null },
},
},
},
select: {
id: true,
email: true,
secondaryEmails: {
where: {
email: { in: normalized, mode: "insensitive" },
emailVerified: { not: null },
},
select: { email: true },
},
},
}),
]);
const seen = new Map<number, { id: number; email: string; matchedEmails: string[] }>();
for (const u of byPrimary) {
seen.set(u.id, { id: u.id, email: u.email, matchedEmails: [u.email] });
}
for (const u of bySecondary) {
const existing = seen.get(u.id);
const secondaryMatches = u.secondaryEmails.map((s) => s.email);
if (existing) {
existing.matchedEmails = Array.from(new Set([...existing.matchedEmails, ...secondaryMatches]));
} else {
seen.set(u.id, { id: u.id, email: u.email, matchedEmails: secondaryMatches });
}
}
return Array.from(seen.values());
}
async findByEmails({ emails }: { emails: string[] }) {
if (!emails.length) return [];
const normalized = Array.from(new Set(emails.map((e) => e.toLowerCase())));
const [byPrimary, bySecondary] = await Promise.all([
this.prismaClient.user.findMany({
where: {
email: { in: normalized, mode: "insensitive" },
emailVerified: { not: null },
locked: false,
},
select: { id: true, email: true },
}),
this.prismaClient.user.findMany({
where: {
locked: false,
secondaryEmails: {
some: {
email: { in: normalized, mode: "insensitive" },
emailVerified: { not: null },
},
},
},
select: {
id: true,
email: true,
secondaryEmails: {
where: {
email: { in: normalized, mode: "insensitive" },
emailVerified: { not: null },
},
select: { email: true },
},
},
}),
]);
const seen = new Map<number, { id: number; email: string; matchedEmails: string[] }>();
for (const u of byPrimary) {
seen.set(u.id, { id: u.id, email: u.email, matchedEmails: [u.email] });
}
for (const u of bySecondary) {
const existing = seen.get(u.id);
const secondaryMatches = u.secondaryEmails.map((s) => s.email);
if (existing) {
existing.matchedEmails = Array.from(new Set([...existing.matchedEmails, ...secondaryMatches]));
} else {
seen.set(u.id, { id: u.id, email: u.email, matchedEmails: secondaryMatches });
}
}
return Array.from(seen.values());
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/features/users/repositories/UserRepository.ts` around lines 1510 -
1557, The primary branch in findByEmails accepts users regardless of
locked/emailVerified state; update the prismaClient.user.findMany call used for
byPrimary to mirror the verified semantics by adding filters emailVerified: {
not: null } and locked: false (same shape as the secondary query), so both
byPrimary and bySecondary only return non-locked, email-verified users; keep the
rest of the merging logic (seen map, matchedEmails) intact.

Comment on lines +7 to +8
type GetGuestBusyTimesForReschedule =
typeof AvailableSlotsService.prototype._getGuestBusyTimesForReschedule;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm the method is declared private and see if tsc flags the access.
rg -nP 'private\s+(async\s+)?_getGuestBusyTimesForReschedule\b' -C1
fd -t f 'tsconfig.json' | head -5 | xargs -I{} sh -c 'echo "=== {} ==="; cat "{}"'
# Attempt typecheck of just this file if tsc is available
fd -t f 'getGuestBusyTimesForReschedule.test.ts'

Repository: calcom/cal.diy

Length of output: 1764


🌐 Web query:

Does TypeScript emit TS2341 for typeof ClassName.prototype.privateMember when the member is declared private?

💡 Result:

Yes, TypeScript emits error TS2341 when accessing typeof ClassName.prototype.privateMember for a private member, because ClassName.prototype is typed as the instance type ClassName, and private members are not accessible on the instance type from outside the class declaration context.

Citations:


typeof access to a private method will trigger a TypeScript error.

_getGuestBusyTimesForReschedule is declared private in AvailableSlotsService. TypeScript emits TS2341 ("Property is private and only accessible within class") when accessing a private member via typeof ClassName.prototype.member from outside the class context, regardless of whether the runtime cast works. This will fail during type-checking in CI.

Replace the alias with a structural type that avoids referencing the private member by name:

♻️ Suggested fix
-  type GetGuestBusyTimesForReschedule =
-    typeof AvailableSlotsService.prototype._getGuestBusyTimesForReschedule;
+  type GetGuestBusyTimesForReschedule = (params: {
+    rescheduleUid: string | null | undefined;
+    rescheduledBy?: string | null | undefined;
+    schedulingType: SchedulingType | null;
+    dateFrom: Date;
+    dateTo: Date;
+  }) => Promise<{ start: Date; end: Date }[]>;
📝 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
type GetGuestBusyTimesForReschedule =
typeof AvailableSlotsService.prototype._getGuestBusyTimesForReschedule;
type GetGuestBusyTimesForReschedule = (params: {
rescheduleUid: string | null | undefined;
rescheduledBy?: string | null | undefined;
schedulingType: SchedulingType | null;
dateFrom: Date;
dateTo: Date;
}) => Promise<{ start: Date; end: Date }[]>;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/trpc/server/routers/viewer/slots/getGuestBusyTimesForReschedule.test.ts`
around lines 7 - 8, The test currently uses typeof
AvailableSlotsService.prototype._getGuestBusyTimesForReschedule which references
the private method and triggers TS2341; replace that with a structural function
type that matches the private method's signature instead of referencing it by
name (e.g. declare type GetGuestBusyTimesForReschedule = (/* same params as
_getGuestBusyTimesForReschedule */) => Promise</* same return type */>), or
reuse an exported public type if one exists; keep references to
AvailableSlotsService and _getGuestBusyTimesForReschedule in the comment for
traceability but do not use typeof on the private member.

The OR between userId and attendees.email can surface the same booking twice when a guest is both organizer and attendee on that row. Downstream subtract() in getUserAvailability is idempotent, so correctness holds, but payloads were inflated and busy-time diagnostics misleading. Dedupe by uid in-memory before returning.
- util.ts: guard null/undefined error.code so the log does not emit literal code: "undefined" / "null"
- types.ts: validate rescheduledBy as z.string().email().nullish() to reject malformed inputs early
- getUserAvailability.ts: omit source instead of setting source: "" when withSource is false, matches the Optional<EventBusyDetails, "source"> typing
- useSchedule.ts: TODO breadcrumb for the V2 DTO follow-up so the fallback site is easy to locate later
@bcornish1797
Copy link
Copy Markdown
Author

Addressed most of the round-3 review.

  • 0a83d1a7eBookingRepository.findByUserIdsAndDateRange now dedupes rows by uid. The OR between userId and attendees.email was surfacing the same booking twice when a guest was both organizer and attendee; downstream subtract() was idempotent so correctness held, but the payload was inflated.
  • e8a7275e0 — grouped nits:
    • util.ts degradation log: guard error.code on != null so the structured log does not emit the literal code: "undefined" / code: "null".
    • types.ts: tightened rescheduledBy to z.string().email().nullish().
    • getUserAvailability.ts: omit source when withSource is false instead of setting it to "" — matches the Optional<EventBusyDetails, "source"> typing and the pattern used for busyTimes.
    • useSchedule.ts: added a TODO next to the V2 fallback guard pointing to the V2 DTO follow-up, so the site is easy to locate when that work happens.

Declined for this PR:

  • Adding emailVerified: { not: null } + locked: false to the primary branch of findByEmails. The secondary branch enforces the verified filter because the secondaryEmails relation carries its own emailVerified; applying the equivalent on the primary branch changes existing behavior (would exclude users who have signed up but not verified). Happy to do it if the intent here is "all guest busy-time filters operate only on verified, non-locked users" — want to avoid changing the contract under this PR without confirmation.
  • typeof AvailableSlotsService.prototype._getGuestBusyTimesForReschedule in the test file — pre-existing and compiles under the current tsconfig; would rather not touch unrelated test scaffolding in this PR.

@coderabbitai — please re-review.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 21, 2026

@bcornish1797 Thanks for the thorough round-3 update! All the addressed items look well-reasoned:

  • Dedup by uid in findByUserIdsAndDateRange is the correct fix — the OR between userId and attendees.email is semantically correct for the query but can legitimately surface the same booking row twice when the same user appears in both predicates. Deduping at the repo layer is the right place to handle it.
  • error.code guard — preventing literal "undefined" / "null" from appearing in structured logs is a good hygiene fix.
  • z.string().email().nullish() — tighter and semantically correct since rescheduledBy is always an email address when provided.
  • Omitting source when withSource is false instead of setting "" — cleaner and consistent with the existing busyTimes pattern.
  • TODO for the V2 fallback — good anchor for the follow-up.

On the two declined items:

  • The emailVerified: { not: null } + locked: false on the primary findByEmails branch — your reasoning is sound. The primary email on a User row is the canonical address set at sign-up; tying guest-blocking to verified-only would be a separate behavioral decision. Fine to defer or handle in a focused follow-up.
  • The typeof AvailableSlotsService.prototype._getGuestBusyTimesForReschedule test cast — pre-existing scaffolding that compiles is a reasonable place to draw the scope boundary.

Kicking off a fresh full review now.


🧠 Learnings used
Learnt from: scopecreepsoap
Repo: calcom/cal.diy PR: 28246
File: packages/trpc/server/routers/viewer/bookings/confirm.handler.ts:186-217
Timestamp: 2026-04-20T02:57:03.744Z
Learning: In `packages/trpc/server/routers/viewer/bookings/confirm.handler.ts`, the `forceConfirm` conflict check (added in PR `#28246`) is intentionally scoped to the primary organizer (`userId`) only. Extending conflict detection to `eventType.hosts` (round-robin / collective team event types) is explicitly out of scope for this PR and should be addressed in a dedicated follow-up with test coverage for those flows. Existing booking-creation guards already cover the team-host conflict path.

Learnt from: pmartin-dev
Repo: calcom/cal.diy PR: 28916
File: packages/features/bookings/lib/service/RegularBookingService.ts:2082-2086
Timestamp: 2026-04-16T13:00:15.886Z
Learning: In `packages/features/bookings/lib/service/RegularBookingService.ts`, when all calendar integrations fail (`allCalendarEventsFailed`), the booking is compensated to `BookingStatus.PENDING` via a `prismaClient.booking.update` call immediately after `eventManager.create()` returns. This is intentionally a compensating-update pattern (not an optimistic-PENDING-then-promote pattern), because `createBooking()` conditions downstream side effects on the initial status; refactoring that is considered too high-regression-risk. All side effects (emails, webhooks, no-show triggers) are gated on `booking.status === ACCEPTED` and fire after the in-memory mutation, so no consumer within the handler observes the transient `ACCEPTED` state. The only residual risk is external DB readers during the narrow window between `createBooking()` and the compensating update — accepted as a known trade-off.
✅ Actions performed

Full review triggered.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai 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: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/features/availability/lib/getUserAvailability.ts (1)

621-638: ⚠️ Potential issue | 🟡 Minor

Type inconsistency: source is required on EventBusyDetails.

Per packages/types/Calendar.d.ts, EventBusyDetails.source: string is required (not optional). The new guestBusyTimesFormatted is typed as EventBusyDetails[] but only sets source when withSource is true, so when withSource=false the objects don't satisfy the declared type. Functionally it works because source is later stripped at Line 643, but the inconsistency differs from the sibling entries (busyTimes, busyTimesFromLimits, busyTimesFromTeamLimits) which always carry source.

Recommend always setting source: "guest-availability" here and letting the existing strip step at Line 643 handle the withSource=false path — matching how the other arrays are constructed.

♻️ Proposed fix
-    const guestBusyTimesFormatted: EventBusyDetails[] = (initialData?.guestBusyTimes ?? []).map((t) => ({
-      start: t.start.toISOString(),
-      end: t.end.toISOString(),
-      title: "Guest busy",
-      ...(withSource ? { source: "guest-availability" } : {}),
-    }));
+    const guestBusyTimesFormatted: EventBusyDetails[] = (initialData?.guestBusyTimes ?? []).map((t) => ({
+      start: t.start.toISOString(),
+      end: t.end.toISOString(),
+      title: "Guest busy",
+      source: "guest-availability",
+    }));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/features/availability/lib/getUserAvailability.ts` around lines 621 -
638, guestBusyTimesFormatted is declared as EventBusyDetails[] but only sets
source when withSource is true, causing a type mismatch; always set source:
"guest-availability" on each guestBusyTimesFormatted entry (matching
busyTimes/busyTimesFromLimits/busyTimesFromTeamLimits) and rely on the existing
later strip step to remove source when withSource is false so types remain
consistent and the runtime behavior is unchanged.
🧹 Nitpick comments (3)
packages/features/bookings/repositories/BookingRepository.ts (1)

2141-2197: New repo methods LGTM; one small nit on excludeUid falsy handling.

findByUidIncludeAttendeeEmails and findByUserIdsAndDateRange are well-scoped: DB-level excludeUid, early [] on empty inputs, and post-query dedupe by uid address the prior duplicate-row concern from OR fan-out.

Minor: excludeUid ? { uid: { not: excludeUid } } : {} treats an empty string the same as undefined. If a caller ever passes "" by accident it would silently include the self-booking — consider excludeUid != null or explicit length check. Non-blocking given current call sites.

Including PENDING alongside ACCEPTED is intentional per the PR discussion (reschedule flow treats pending-with-confirmation as blocking), so no change needed there.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/features/bookings/repositories/BookingRepository.ts` around lines
2141 - 2197, The conditional that builds the Prisma where clause in
findByUserIdsAndDateRange treats an empty string excludeUid as a valid value;
change the spread that currently uses "excludeUid ? { uid: { not: excludeUid } }
: {}" to explicitly check for non-null/non-empty (for example excludeUid != null
&& excludeUid !== "" or excludeUid?.length > 0) so that an empty string won't be
applied to the uid not filter; update the condition in
BookingRepository.findByUserIdsAndDateRange accordingly.
packages/features/bookings/repositories/BookingRepository.test.ts (1)

104-242: Good test coverage for the new methods.

Early-return behavior, OR composition for userId/email branches, excludeUid, and select shape are all exercised. The combined-OR test at Line 176–198 also guards the predicate shape.

Nit: findByUserIdsAndDateRange dedup-by-uid (repo Lines 2190–2196) isn't directly asserted — a test that stubs findMany to return two rows with the same uid and asserts a length-1 result would lock in that behavior. Optional.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/features/bookings/repositories/BookingRepository.test.ts` around
lines 104 - 242, Add a test for BookingRepository.findByUserIdsAndDateRange that
verifies deduplication by uid: stub mockPrismaClient.booking.findMany to resolve
with two objects that have the same uid but otherwise identical/different
fields, call repository.findByUserIdsAndDateRange (e.g., with userIds or
userEmails and dateFrom/dateTo), and assert the returned array length is 1 and
the single item matches the expected booking; target the
BookingRepository.findByUserIdsAndDateRange code path that performs the dedupe
(the repository method and mockPrismaClient.booking.findMany) so the test locks
in the dedup-by-uid behavior.
packages/trpc/server/routers/viewer/slots/util.ts (1)

813-829: Guest lookup runs on every calculateHostsAndAvailabilities call, including fallback flows.

calculateHostsAndAvailabilities is invoked up to three times per request when hasFallbackRRHosts triggers re-computation (see lines 1097, 1132, 1168). Each invocation fires getGuestBusyTimesForReschedule, which issues 3 DB queries on the reschedule path. The date ranges differ across calls, so trivial caching isn't safe, but for the common case where only hosts/date-window varies and attendee resolution is identical, consider memoizing at least the findByUidIncludeAttendeeEmails + findByEmails lookups (they don't depend on dateFrom/dateTo) to avoid redundant reads.

Non-blocking; gracefully degrades.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/trpc/server/routers/viewer/slots/util.ts` around lines 813 - 829,
calculateHostsAndAvailabilities repeatedly calls getGuestBusyTimesForReschedule
causing duplicate DB reads; memoize the guest resolution steps (the parts that
call findByUidIncludeAttendeeEmails and findByEmails) per-request so repeated
getGuestBusyTimesForReschedule invocations reuse resolved attendee emails/ids
even if date ranges differ. Add a lightweight request-scoped cache (e.g., a Map
keyed by rescheduleUid+rescheduledBy) accessible to
calculateHostsAndAvailabilities and used inside getGuestBusyTimesForReschedule
(or pass the memo map as an extra param), store the results of
findByUidIncludeAttendeeEmails and findByEmails there, and only run the
date-dependent DB queries when needed. This keeps semantics but avoids redundant
attendee lookups across the three recalculations.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/web/modules/schedules/hooks/useEvent.ts`:
- Around line 107-117: The rescheduledBy query param is passed raw from
searchParams?.get("rescheduledBy") into useSchedule and can be an empty string
or malformed email which will fail server zod validation; normalize this value
before calling useSchedule by converting empty string to null and perform a
lightweight client-side email shape check (e.g., simple regex or indexOf('@')
sanity check) to only pass a non-empty string that looks like an email,
otherwise pass null; update the variable referenced as rescheduledBy and ensure
useSchedule(...) is invoked with that normalized value so bad query params don't
break the schedule query.

In
`@packages/trpc/server/routers/viewer/slots/getGuestBusyTimesForReschedule.test.ts`:
- Around line 204-223: The test only asserts
mockDependencies.userRepo.findByEmails was called, which doesn't prove the
host-branch executed; update the "should handle case-insensitive host email
comparison" test (which calls callGetGuestBusyTimes with rescheduleUid and
rescheduledBy) to also assert
mockDependencies.bookingRepo.findByUserIdsAndDateRange was called (or
alternately assert the test did not early-return by checking a side-effect
unique to the host path) so the case-insensitive host matching flow
(rescheduledBy -> userRepo.findByEmails ->
bookingRepo.findByUserIdsAndDateRange) is fully verified.

In `@packages/trpc/server/routers/viewer/slots/util.ts`:
- Around line 686-692: The gating logic currently treats any rescheduledBy value
that is not equal to hostEmail as an attendee-initiated reschedule and returns
[] (see rescheduledBy, original.user?.email, hostEmail, isHostReschedule), which
erroneously excludes host-adjacent initiators; change the check to explicitly
detect whether rescheduledBy matches an attendee email from the slot's attendee
list (only return [] when rescheduledBy is equal to an attendee email), and
ensure you defensively handle original.user being null/undefined when computing
hostEmail; alternatively, if the current behavior is intentional, add a clear
comment above this block explaining that non-host/non-attendee initiators should
fall through.

---

Outside diff comments:
In `@packages/features/availability/lib/getUserAvailability.ts`:
- Around line 621-638: guestBusyTimesFormatted is declared as EventBusyDetails[]
but only sets source when withSource is true, causing a type mismatch; always
set source: "guest-availability" on each guestBusyTimesFormatted entry (matching
busyTimes/busyTimesFromLimits/busyTimesFromTeamLimits) and rely on the existing
later strip step to remove source when withSource is false so types remain
consistent and the runtime behavior is unchanged.

---

Nitpick comments:
In `@packages/features/bookings/repositories/BookingRepository.test.ts`:
- Around line 104-242: Add a test for
BookingRepository.findByUserIdsAndDateRange that verifies deduplication by uid:
stub mockPrismaClient.booking.findMany to resolve with two objects that have the
same uid but otherwise identical/different fields, call
repository.findByUserIdsAndDateRange (e.g., with userIds or userEmails and
dateFrom/dateTo), and assert the returned array length is 1 and the single item
matches the expected booking; target the
BookingRepository.findByUserIdsAndDateRange code path that performs the dedupe
(the repository method and mockPrismaClient.booking.findMany) so the test locks
in the dedup-by-uid behavior.

In `@packages/features/bookings/repositories/BookingRepository.ts`:
- Around line 2141-2197: The conditional that builds the Prisma where clause in
findByUserIdsAndDateRange treats an empty string excludeUid as a valid value;
change the spread that currently uses "excludeUid ? { uid: { not: excludeUid } }
: {}" to explicitly check for non-null/non-empty (for example excludeUid != null
&& excludeUid !== "" or excludeUid?.length > 0) so that an empty string won't be
applied to the uid not filter; update the condition in
BookingRepository.findByUserIdsAndDateRange accordingly.

In `@packages/trpc/server/routers/viewer/slots/util.ts`:
- Around line 813-829: calculateHostsAndAvailabilities repeatedly calls
getGuestBusyTimesForReschedule causing duplicate DB reads; memoize the guest
resolution steps (the parts that call findByUidIncludeAttendeeEmails and
findByEmails) per-request so repeated getGuestBusyTimesForReschedule invocations
reuse resolved attendee emails/ids even if date ranges differ. Add a lightweight
request-scoped cache (e.g., a Map keyed by rescheduleUid+rescheduledBy)
accessible to calculateHostsAndAvailabilities and used inside
getGuestBusyTimesForReschedule (or pass the memo map as an extra param), store
the results of findByUidIncludeAttendeeEmails and findByEmails there, and only
run the date-dependent DB queries when needed. This keeps semantics but avoids
redundant attendee lookups across the three recalculations.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d0634495-fb7e-4302-9d11-b1285e72880b

📥 Commits

Reviewing files that changed from the base of the PR and between e9c6d0e and e8a7275.

📒 Files selected for processing (10)
  • apps/web/modules/schedules/hooks/useEvent.ts
  • apps/web/modules/schedules/hooks/useSchedule.ts
  • packages/features/availability/lib/getUserAvailability.ts
  • packages/features/bookings/repositories/BookingRepository.test.ts
  • packages/features/bookings/repositories/BookingRepository.ts
  • packages/features/users/repositories/UserRepository.test.ts
  • packages/features/users/repositories/UserRepository.ts
  • packages/trpc/server/routers/viewer/slots/getGuestBusyTimesForReschedule.test.ts
  • packages/trpc/server/routers/viewer/slots/types.ts
  • packages/trpc/server/routers/viewer/slots/util.ts

Comment thread apps/web/modules/schedules/hooks/useEvent.ts Outdated
Comment on lines +686 to +692
if (rescheduledBy) {
const hostEmail = original.user?.email;
const isHostReschedule = hostEmail && rescheduledBy.toLowerCase() === hostEmail.toLowerCase();
if (!isHostReschedule) {
return [];
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Gating logic treats any non-host rescheduledBy as "attendee-initiated".

The check rescheduledBy.toLowerCase() === hostEmail.toLowerCase() returns [] when rescheduledBy is anything other than the host email — including admin/team-owner reschedules, delegated-credential flows, or edge cases where original.user is null (making hostEmail undefined, which collapses to isHostReschedule = false and thus silently skips guest blocking).

If the intent is strictly "not attendee", consider matching against the attendee list explicitly (return [] only when rescheduledBy matches an attendee) so host-adjacent initiators still benefit from guest blocking. Otherwise, a brief comment documenting that "non-host, non-attendee" initiators intentionally fall through to no-op would help future readers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/trpc/server/routers/viewer/slots/util.ts` around lines 686 - 692,
The gating logic currently treats any rescheduledBy value that is not equal to
hostEmail as an attendee-initiated reschedule and returns [] (see rescheduledBy,
original.user?.email, hostEmail, isHostReschedule), which erroneously excludes
host-adjacent initiators; change the check to explicitly detect whether
rescheduledBy matches an attendee email from the slot's attendee list (only
return [] when rescheduledBy is equal to an attendee email), and ensure you
defensively handle original.user being null/undefined when computing hostEmail;
alternatively, if the current behavior is intentional, add a clear comment above
this block explaining that non-host/non-attendee initiators should fall through.

…uestBusyTimesFormatted typing

Two regressions surfaced by local type-check:

- util.ts imported a now-removed RoutingFormResponseRepository type; the field that referenced it was refactored out of IAvailableSlotsService in upstream but the import lingered in the rebased branch (TS2307).
- guestBusyTimesFormatted used a conditional spread for source which made the object shape incompatible with EventBusyDetails's required source. Cast the mapped array to EventBusyDetails[] so the conditional spread is accepted (TS2322).
Revert the conditional-spread approach and always set source to guest-availability. The existing strip step in detailedBusyTimes already drops source when withSource is false, and this keeps the array shape consistent with the sibling busyTimes/busyTimesFromLimits/busyTimesFromTeamLimits arrays.
…-by-uid

- findByUserIdsAndDateRange: use excludeUid != null && excludeUid !== "" so an accidental empty string no longer silently includes the self-booking
- Two new tests: assert empty excludeUid is ignored, and assert the OR/uid dedup collapses identical rows to one result
Server zod now rejects non-email values for rescheduledBy; drop empty / at-less URL strings to null client-side so stale or malformed URLs do not break the schedule query.
…nch assertion

- util.ts: explicit comment on the gating semantics (default-safe-for-attendee when the initiator cannot be positively identified as host)
- test: assert findByUserIdsAndDateRange was invoked in the case-insensitive host test, so an accidental early-return would fail loudly
@bcornish1797
Copy link
Copy Markdown
Author

Round 4 follow-through.

  • 45899ba74 — always set source: "guest-availability" on guestBusyTimesFormatted entries and let the existing strip step handle withSource=false. Keeps the shape consistent with busyTimes/busyTimesFromLimits and drops the previous type cast.
  • 37f58e217findByUserIdsAndDateRange now guards excludeUid != null && excludeUid !== "" so an accidental empty string no longer silently includes the self-booking. Added two tests: one for the empty-string guard, one that locks in the dedup-by-uid behaviour when findMany returns identical rows.
  • 7cec9fb1c — normalize rescheduledBy from URL search params on the client: drop empty-string and non-email-shaped values to null before the schedule query fires, so stale or malformed URLs don't trip the tightened server zod validator.
  • 577cb1738 — test strengthened to assert findByUserIdsAndDateRange is invoked in the case-insensitive host test (an accidental early-return would now fail loudly). Added an inline comment in util.ts documenting the attendee-default gating semantics.

On the gating semantics:

Current behaviour is "default-safe-for-attendee": when rescheduledBy is set but does not match the host's email, we skip guest blocking. That covers admin-on-behalf and team-member initiators the same way it covers attendees. The alternative you suggested — "default-safe-for-host" (only skip blocking when rescheduledBy matches an attendee email) — flips the fallback. Both are defensible; I've left the current semantics and documented them so the intent is discoverable. Happy to flip if maintainers think host intent should win by default.

On the memoization nit (util.ts 813–829):

Acknowledged as a real perf win for the 3x calculateHostsAndAvailabilities path, but it's a request-scoped cache plumbing change that reaches beyond this PR's surface. Leaving for a dedicated follow-up unless a maintainer thinks it should land here.

Local yarn type-check runs clean at 577cb1738 (11/11 tasks, 0 TS errors).

@coderabbitai — please re-review.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 21, 2026

Only repository collaborators, contributors, or members can run CodeRabbit commands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants