Skip to content

feat: enforce guest availability on host-initiated reschedules (non-collective) - CAL-4531 #28849

Open
simbabimba-dev wants to merge 8 commits intocalcom:mainfrom
simbabimba-dev:main
Open

feat: enforce guest availability on host-initiated reschedules (non-collective) - CAL-4531 #28849
simbabimba-dev wants to merge 8 commits intocalcom:mainfrom
simbabimba-dev:main

Conversation

@simbabimba-dev
Copy link
Copy Markdown

@simbabimba-dev simbabimba-dev commented Apr 12, 2026

Summary

This PR fixes CAL-4531 (Fixes #16378) by enforcing guest availability when a host reschedules a booking.

When the host initiates a non-collective reschedule, slots are filtered against guest availability to prevent guest double-booking.

This implementation is intentionally scoped and reuses the existing availability engine instead of introducing a separate guestBusyTimes pipeline.

Changes Made

  • Added end-to-end rescheduledBy propagation through the reschedule flow.
  • Added rescheduledBy validation and normalization in schedule input schema.
  • Added host-initiated guard in slot calculation where rescheduledBy must match booking owner.
  • Kept collective scheduling excluded from this behavior in current scope.
  • Added guest resolution from booking attendees using Cal.com user lookup with verified secondary-email support.
  • Injected the resolved guest as a host-equivalent participant into the existing availability pipeline.
  • Preserved attendee alias email after user resolution to keep attendee-based busy-booking matching accurate.
  • Added and updated tests for host-initiated behavior, attendee-initiated regression safety, secondary-email behavior, collective bypass, and current-slot preservation.

Why this approach

  • Lower regression risk by staying inside the existing availability pipeline.
  • Preserves behavior boundaries so attendee-initiated flow remains unchanged.
  • Automatically respects existing constraints already enforced by the engine: calendars, before and after buffers, booking limits, team limits, OOO, and reschedule-aware busy-time handling.

Comparison to PR #26811 and PR #27710

  • This PR keeps host-initiated gating explicit via rescheduledBy.
  • This PR keeps non-collective-only scope explicit.
  • This PR avoids introducing a parallel guestBusyTimes ruleset.
  • This PR centralizes behavior in the existing slot engine instead of splitting logic across two availability paths.
  • This PR is intentionally scoped for safer rollout and easier review.

Type of Change

  • Feature (behavior improvement in existing flow)
  • Bug fix (non-breaking change)

Test Plan

  • Host-initiated reschedule applies guest availability filtering.
  • Attendee-initiated reschedule keeps previous behavior.
  • Verified secondary-email attendee path is handled.
  • Current booking slot remains available during reschedule.
  • Collective scheduling is explicitly bypassed in this scope.
  • Unit coverage for guest resolution guards and alias preservation.
  • Integration coverage for host and attendee reschedule scenarios.

Scope Note

Current scope resolves the first non-organizer attendee for guest enforcement.
If product needs multi-attendee enforcement, that can be added in a follow-up without changing this PR core design.

Demo

I will shortly add a video demo showing:

  • host-initiated reschedule with guest conflict filtering
  • attendee-initiated behavior unchanged
  • current-slot preservation during reschedule

Before the fix:

image Left side shows a pro user who booked a slot with a free user and is now attempting to reschedule that booking. On the right side, the free user has two existing bookings — one with a trial user and another with the pro user.

In this booking, the pro user is the host and the free user is the attendee. When the host initiates the reschedule, available time slots are currently not evaluated against the attendee’s existing availability, so conflicting slots are still returned, resulting in a potential double booking.

After the fix:

image

Now, The host (pro user) is prevented from selecting time slots that conflict with the attendee’s existing bookings. This ensures attendee availability is properly enforced during host-initiated reschedules, eliminating the possibility of double-booking.

Fixes #16378
Refs CAL-4531
Related: #26811, #27710

/claim #16378

@simbabimba-dev simbabimba-dev requested a review from a team as a code owner April 12, 2026 13:16
Copilot AI review requested due to automatic review settings April 12, 2026 13:16
@github-actions github-actions bot added $200 bookings area: bookings, availability, timezones, double booking Medium priority Created by Linear-GitHub Sync ✨ feature New feature or request 💎 Bounty A bounty on Algora.io 🧹 Improvements Improvements to existing features. Mostly UX/UI labels Apr 12, 2026
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR addresses CAL-4531 (#16378) by enforcing guest availability during host-initiated, non-collective reschedules, preventing hosts from selecting slots that would double-book the guest.

Changes:

  • Propagates and normalizes rescheduledBy through the schedule/reschedule flow and input schema.
  • Resolves the reschedule “guest” (first non-organizer attendee), including verified secondary-email matching, and injects them into the existing availability pipeline to enforce conflicts.
  • Adds/updates unit + integration tests for host vs attendee initiated reschedule, secondary-email handling, collective bypass, and current-slot preservation.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
packages/trpc/server/routers/viewer/slots/util.ts Adds guest resolution + host-initiated gating, and injects guest into availability calculation for reschedule slot filtering.
packages/trpc/server/routers/viewer/slots/util.test.ts Adds unit tests for _getRescheduleGuestUser behavior.
packages/trpc/server/routers/viewer/slots/types.ts Adds rescheduledBy to schedule input schema with normalization.
packages/features/users/repositories/UserRepository.ts Adds findAvailabilityUserByEmail with verified secondary-email fallback for guest resolution.
packages/features/bookings/repositories/BookingRepository.ts Adds seat-reference UID lookup to resolve seated-event reschedule UIDs to underlying booking.
apps/web/test/lib/getSchedule.test.ts Adds integration tests covering reschedule guest filtering behavior and edge cases.
apps/web/modules/schedules/hooks/useSchedule.ts Threads rescheduledBy from query params into schedule input.
apps/web/lib/reschedule/[uid]/getServerSideProps.ts Ensures rescheduledBy is derived from session when present (and propagated into the destination URL).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 12, 2026

📝 Walkthrough

Walkthrough

Adds a rescheduledBy input propagated from server-side props through hooks into the TRPC getSchedule input and availability calculations. Adds UserRepository.findAvailabilityUserByEmail and BookingRepository.findBySeatReferenceUidIncludeEventType for resolving users/bookings. Implements AvailableSlotsService._getRescheduleGuestUser, injects a reschedule guest into host/availability aggregation via getAggregatedAvailabilityForEvent, and adjusts host-count logic for event semantics. Adds tests for attendee- and host-initiated reschedules and secondary-email resolution. Several imports were reordered.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: enforcing guest availability during host-initiated reschedules for non-collective events, which is the primary objective of the PR.
Description check ✅ Passed The description provides comprehensive detail on changes made, rationale, approach, testing, and comparisons to related PRs, all directly related to the changeset implementation.
Linked Issues check ✅ Passed The PR fully implements the requirements from #16378 (CAL-4531): propagates rescheduledBy through the flow, checks guest availability via Cal.com user lookup with secondary-email support, filters slots against guest availability, preserves non-Cal.com user behavior, and limits scope to non-collective single-attendee scenarios with comprehensive test coverage.
Out of Scope Changes check ✅ Passed All code changes directly support the linked objective of enforcing guest availability on host-initiated reschedules: rescheduledBy propagation, schema updates, repository methods for guest resolution, availability service logic, and targeted test coverage remain within scope.

✏️ 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.

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

102-323: Extract a helper to avoid repeated private-method casting.

The same service as unknown as { _getRescheduleGuestUser... } block is repeated across tests (for example, Line 102 and Line 148 onward). A single typed helper would reduce duplication and keep each test focused on behavior.

♻️ Suggested cleanup
+const callGetRescheduleGuestUser = (args: {
+  rescheduleUid?: string | null;
+  organizerEmails: string[];
+  schedulingType: SchedulingType | null;
+  rescheduledBy?: string | null;
+}) =>
+  (
+    service as unknown as {
+      _getRescheduleGuestUser: (input: typeof args) => Promise<unknown>;
+    }
+  )._getRescheduleGuestUser(args);

-const result = await (
-  service as unknown as {
-    _getRescheduleGuestUser: (args: {
-      rescheduleUid?: string | null;
-      organizerEmails: string[];
-      schedulingType: SchedulingType | null;
-      rescheduledBy?: string | null;
-    }) => Promise<unknown>;
-  }
-)._getRescheduleGuestUser({
+const result = await callGetRescheduleGuestUser({
   rescheduleUid: "BOOKING_TO_RESCHEDULE_UID",
   organizerEmails: ["host@example.com"],
   schedulingType: SchedulingType.ROUND_ROBIN,
   rescheduledBy: "host@example.com",
 });
🤖 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.test.ts` around lines 102 -
323, Tests repeatedly cast service to an inline type to call the private method
_getRescheduleGuestUser; extract a single typed helper (e.g., const typedService
= service as unknown as { _getRescheduleGuestUser: (...) => Promise<unknown> }
or a function getRescheduleGuestUser(args) that delegates to
(typedService)._getRescheduleGuestUser) at the top of the test file and replace
all repeated casts with calls to that helper so each test calls
typedService._getRescheduleGuestUser(...) or getRescheduleGuestUser(...) instead
of repeating the long cast.
🤖 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.test.ts`:
- Around line 102-323: Tests repeatedly cast service to an inline type to call
the private method _getRescheduleGuestUser; extract a single typed helper (e.g.,
const typedService = service as unknown as { _getRescheduleGuestUser: (...) =>
Promise<unknown> } or a function getRescheduleGuestUser(args) that delegates to
(typedService)._getRescheduleGuestUser) at the top of the test file and replace
all repeated casts with calls to that helper so each test calls
typedService._getRescheduleGuestUser(...) or getRescheduleGuestUser(...) instead
of repeating the long cast.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: bd6991c2-3e9a-4d2b-849b-ab0498d5194c

📥 Commits

Reviewing files that changed from the base of the PR and between d08f4a0 and d27a3fa.

📒 Files selected for processing (8)
  • apps/web/lib/reschedule/[uid]/getServerSideProps.ts
  • apps/web/modules/schedules/hooks/useSchedule.ts
  • apps/web/test/lib/getSchedule.test.ts
  • packages/features/bookings/repositories/BookingRepository.ts
  • packages/features/users/repositories/UserRepository.ts
  • packages/trpc/server/routers/viewer/slots/types.ts
  • packages/trpc/server/routers/viewer/slots/util.test.ts
  • packages/trpc/server/routers/viewer/slots/util.ts

…hedules

Address follow-up review feedback for CAL-4531.

- Prevent injected reschedule guests from forcing team-style aggregation for schedulingType=null

- Aggregate host availability as single-host, then intersect with guest oooExcluded ranges

- Add regression test for single-host OOO semantics

- Keep util.test dependency typing CI-safe via explicit cast
@simbabimba-dev
Copy link
Copy Markdown
Author

simbabimba-dev commented Apr 12, 2026

Hey @dhairyashiil @keithwillcode, PR is ready for review.
Tests + screenshots included. Demo video coming shortly.

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.test.ts (1)

380-404: Add two small edge-case tests to lock the non-collective default path.

You already test SchedulingType.COLLECTIVE and rescheduledBy: null; I’d also add explicit coverage for schedulingType: null (host-initiated should still enforce) and missing rescheduleUid (should early-return null).

Proposed test additions
+  it("returns guest for host-initiated reschedule when schedulingType is null (non-collective)", async () => {
+    findByUidIncludeEventType.mockResolvedValue({
+      user: { email: "host@example.com" },
+      attendees: [{ email: "guest@example.com" }],
+    });
+    findAvailabilityUserByEmail.mockResolvedValue({
+      id: 102,
+      email: "guest@example.com",
+    });
+
+    const result = await getRescheduleGuestUser({
+      rescheduleUid: "BOOKING_TO_RESCHEDULE_UID",
+      organizerEmails: ["host@example.com"],
+      schedulingType: null,
+      rescheduledBy: "host@example.com",
+    });
+
+    expect(result).toEqual({
+      id: 102,
+      email: "guest@example.com",
+    });
+  });
+
+  it("returns null when rescheduleUid is missing", async () => {
+    const result = await getRescheduleGuestUser({
+      organizerEmails: ["host@example.com"],
+      schedulingType: SchedulingType.ROUND_ROBIN,
+      rescheduledBy: "host@example.com",
+    });
+
+    expect(result).toBeNull();
+    expect(findByUidIncludeEventType).not.toHaveBeenCalled();
+    expect(findBySeatReferenceUidIncludeEventType).not.toHaveBeenCalled();
+    expect(findAvailabilityUserByEmail).not.toHaveBeenCalled();
+  });
🤖 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.test.ts` around lines 380 -
404, Tests lack coverage for two edge cases in getRescheduleGuestUser: when
schedulingType is null and when rescheduleUid is missing; add two new tests
mirroring the existing ones that assert the function returns null and does not
call findByUidIncludeEventType or findAvailabilityUserByEmail. Specifically, add
one test calling getRescheduleGuestUser with schedulingType: null
(organizerEmails and rescheduledBy set to host@example.com) and another with
rescheduleUid: null (schedulingType set to SchedulingType.ROUND_ROBIN and
rescheduledBy provided), and ensure both expect null result and that
findByUidIncludeEventType and findAvailabilityUserByEmail were not called.
🤖 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.test.ts`:
- Around line 380-404: Tests lack coverage for two edge cases in
getRescheduleGuestUser: when schedulingType is null and when rescheduleUid is
missing; add two new tests mirroring the existing ones that assert the function
returns null and does not call findByUidIncludeEventType or
findAvailabilityUserByEmail. Specifically, add one test calling
getRescheduleGuestUser with schedulingType: null (organizerEmails and
rescheduledBy set to host@example.com) and another with rescheduleUid: null
(schedulingType set to SchedulingType.ROUND_ROBIN and rescheduledBy provided),
and ensure both expect null result and that findByUidIncludeEventType and
findAvailabilityUserByEmail were not called.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 799cfbad-738e-4da7-a391-2d94573e8e71

📥 Commits

Reviewing files that changed from the base of the PR and between 0ac7ed1 and f5f060d.

📒 Files selected for processing (1)
  • packages/trpc/server/routers/viewer/slots/util.test.ts

@simbabimba-dev
Copy link
Copy Markdown
Author

@cubic-dev-ai review this

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

Labels

bookings area: bookings, availability, timezones, double booking 🙋 Bounty claim 💎 Bounty A bounty on Algora.io ✨ feature New feature or request 🧹 Improvements Improvements to existing features. Mostly UX/UI Medium priority Created by Linear-GitHub Sync size/XL $200

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[CAL-4531] Take into account guest's availability when rescheduling

2 participants