Skip to content

fix: use correct videoCallUrl per recurring booking occurrence#28932

Open
ashif323 wants to merge 3 commits intocalcom:mainfrom
ashif323:fix/recurring-booking-unique-video-url
Open

fix: use correct videoCallUrl per recurring booking occurrence#28932
ashif323 wants to merge 3 commits intocalcom:mainfrom
ashif323:fix/recurring-booking-unique-video-url

Conversation

@ashif323
Copy link
Copy Markdown

What does this PR do?

Fixes two bugs that caused all recurring bookings to share the same videoCallUrl.

Root causes

Bug 1 — RegularBookingService.ts (new booking flow):
getVideoCallUrlFromCalEvent(evt) was called when building booking metadata. For daily_video, this function always returns the public Cal Video URL derived from the booking uid (/video/<uid>) instead of the actual meeting URL from the video adapter. All occurrences ended up with the first booking's uid in their URL.

Fix: use the already-correct videoCallUrl variable directly, which is populated from getVideoCallDetails()updatedVideoEvent?.url.

Bug 2 — handleConfirmation.ts (confirm flow):
A single meetingUrl was computed once before the recurring booking loop and stamped onto every occurrence's metadata.

Fix: derive occurrenceMeetingUrl from scheduleResult.referencesToCreate so each occurrence gets the correct URL from its video reference.

Changes

  • packages/features/bookings/lib/service/RegularBookingService.ts — use videoCallUrl directly in metadata instead of re-calling getVideoCallUrlFromCalEvent
  • packages/features/bookings/lib/handleConfirmation.ts — derive meeting URL from video reference per occurrence in recurring confirmation loop
  • apps/web/pages/api/book/recurring-event.test.ts — remove 3 FIXME comments, update assertions to verify correct video URLs

Testing

All recurring booking tests pass:

@github-actions
Copy link
Copy Markdown
Contributor

Welcome to Cal.diy, @ashif323! Thanks for opening this pull request.

A few things to keep in mind:

  • This is Cal.diy, not Cal.com. Cal.diy is a community-driven, fully open-source fork of Cal.com licensed under MIT. Your changes here will be part of Cal.diy — they will not be deployed to the Cal.com production app.
  • Please review our Contributing Guidelines if you haven't already.
  • Make sure your PR title follows the Conventional Commits format.

A maintainer will review your PR soon. Thanks for contributing!

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 19, 2026

📝 Walkthrough

Walkthrough

The pull request refactors video call URL handling for recurring bookings across test expectations and webhook dispatch logic. Test assertions were updated to expect fixed mock URLs instead of deriving them from booking UIDs. In the confirmation handler, per-occurrence video call URLs are now computed from schedule references and stored in each booking's metadata, with BOOKING_CREATED webhooks dispatched separately per occurrence using occurrence-specific room URLs. The RegularBookingService was modified to source metadata video call URLs from pre-computed values rather than re-parsing from calendar event data, ensuring consistent URL propagation through the booking workflow.

🚥 Pre-merge checks | ✅ 2 | ❌ 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 (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: fixing videoCallUrl handling for recurring booking occurrences to ensure each receives the correct URL instead of sharing the same one.
Description check ✅ Passed The description clearly explains the bugs fixed, root causes, changes made to three files, and confirms testing passed—all directly related to the changeset.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch fix/recurring-booking-unique-video-url

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

Caution

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

⚠️ Outside diff range comments (3)
packages/features/bookings/lib/handleConfirmation.ts (2)

451-457: ⚠️ Potential issue | 🟠 Major

Use each updated booking’s video URL for workflows.

Line 456 still passes the shared meetingUrl, so reminders/workflows for later recurring occurrences can get the wrong room even after per-booking metadata is updated. Prefer updatedBookings[index].metadata.videoCallUrl, with meetingUrl only as fallback.

Suggested direction
+      const bookingMetadata = updatedBookings[index].metadata;
+      const bookingMeetingUrl =
+        bookingMetadata &&
+        typeof bookingMetadata === "object" &&
+        "videoCallUrl" in bookingMetadata &&
+        typeof bookingMetadata.videoCallUrl === "string"
+          ? bookingMetadata.videoCallUrl
+          : meetingUrl;
       const evtOfBooking = {
         ...evt,
         rescheduleReason: updatedBookings[index].cancellationReason || null,
-        metadata: { videoCallUrl: meetingUrl },
+        metadata: bookingMeetingUrl ? { videoCallUrl: bookingMeetingUrl } : {},
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/features/bookings/lib/handleConfirmation.ts` around lines 451 - 457,
The evtOfBooking object is using the shared meetingUrl for metadata, causing
later occurrences to get the wrong room; update the metadata assignment in the
loop that builds evtOfBooking to use the per-booking URL
updatedBookings[index].metadata.videoCallUrl and fall back to meetingUrl when
that field is missing (reference updatedBookings, evtOfBooking, meetingUrl, and
metadata.videoCallUrl).

278-299: ⚠️ Potential issue | 🟠 Major

Move occurrence URL selection into the recurring update loop.

Line 280 and Line 283 compute videoReference / occurrenceMeetingUrl once, then Line 298 writes that same URL to every recurring booking. That keeps the shared-URL failure mode for adapters that produce per-occurrence references. Derive the URL for the current recurringBooking inside the map, using the occurrence’s reference or the reference index if that is the EventManager contract.

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

In `@packages/features/bookings/lib/handleConfirmation.ts` around lines 278 - 299,
The code currently computes videoReference and occurrenceMeetingUrl once before
mapping unconfirmedRecurringBookings, causing the same URL to be written to
every recurring booking; instead, move the logic that derives the
occurrenceMeetingUrl into the map callback so each recurringBooking gets its own
URL: inside the unconfirmedRecurringBookings.map callback (where
recurringBooking is available), pick the per-occurrence reference from
scheduleResult.referencesToCreate for that recurringBooking (or use the
reference index per the EventManager contract) to derive videoReference and set
occurrenceMeetingUrl, then use that occurrenceMeetingUrl in the
prisma.booking.update data (metadata.videoCallUrl) and when creating references
so each occurrence receives its correct URL.
apps/web/pages/api/book/recurring-event.test.ts (1)

113-119: ⚠️ Potential issue | 🟡 Minor

Make the regression test use distinct meeting URLs per occurrence.

All three updated assertions still expect meeting-1, and the mock also returns meeting-1 for every booking. That would not catch a regression where recurring occurrences all receive one shared adapter URL. Please make the mock return per-call URLs, e.g. meeting-1, meeting-2, etc., and assert each booking/webhook receives the matching occurrence URL.

Also applies to: 200-207, 463-469, 549-556, 681-687, 767-774

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

In `@apps/web/pages/api/book/recurring-event.test.ts` around lines 113 - 119, The
test currently uses mockSuccessfulVideoMeetingCreation with a fixed url
`meeting-1` for every call causing all occurrences to appear to share one
adapter URL; change the mock used in the recurring-event.test to generate
distinct per-call URLs (e.g., interpolate a call counter into the url:
`meeting-1`, `meeting-2`, ...) in mockSuccessfulVideoMeetingCreation and update
the corresponding assertions that check the created booking/webhook URLs so each
occurrence asserts against its expected per-call URL (reference the
mockSuccessfulVideoMeetingCreation invocation and the booking/webhook assertion
blocks that verify the meeting url for each occurrence).
🤖 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/lib/handleConfirmation.ts`:
- Around line 592-609: The payload construction is reusing
evtWithoutAssignmentReason which leaves the original event's occurrence fields
(uid, startTime, endTime) on every webhook; update the payload so those
per-occurrence fields come from the current updatedBooking instead: when
building payload in the updatedBookings.flatMap loop (symbols: updatedBooking,
evtWithoutAssignmentReason, payload), explicitly set uid, startTime, and endTime
(or override them after spreading evtWithoutAssignmentReason) using
updatedBooking.uid, updatedBooking.startTime, and updatedBooking.endTime so each
payload reflects the correct occurrence for that bookingId.

---

Outside diff comments:
In `@apps/web/pages/api/book/recurring-event.test.ts`:
- Around line 113-119: The test currently uses
mockSuccessfulVideoMeetingCreation with a fixed url `meeting-1` for every call
causing all occurrences to appear to share one adapter URL; change the mock used
in the recurring-event.test to generate distinct per-call URLs (e.g.,
interpolate a call counter into the url: `meeting-1`, `meeting-2`, ...) in
mockSuccessfulVideoMeetingCreation and update the corresponding assertions that
check the created booking/webhook URLs so each occurrence asserts against its
expected per-call URL (reference the mockSuccessfulVideoMeetingCreation
invocation and the booking/webhook assertion blocks that verify the meeting url
for each occurrence).

In `@packages/features/bookings/lib/handleConfirmation.ts`:
- Around line 451-457: The evtOfBooking object is using the shared meetingUrl
for metadata, causing later occurrences to get the wrong room; update the
metadata assignment in the loop that builds evtOfBooking to use the per-booking
URL updatedBookings[index].metadata.videoCallUrl and fall back to meetingUrl
when that field is missing (reference updatedBookings, evtOfBooking, meetingUrl,
and metadata.videoCallUrl).
- Around line 278-299: The code currently computes videoReference and
occurrenceMeetingUrl once before mapping unconfirmedRecurringBookings, causing
the same URL to be written to every recurring booking; instead, move the logic
that derives the occurrenceMeetingUrl into the map callback so each
recurringBooking gets its own URL: inside the unconfirmedRecurringBookings.map
callback (where recurringBooking is available), pick the per-occurrence
reference from scheduleResult.referencesToCreate for that recurringBooking (or
use the reference index per the EventManager contract) to derive videoReference
and set occurrenceMeetingUrl, then use that occurrenceMeetingUrl in the
prisma.booking.update data (metadata.videoCallUrl) and when creating references
so each occurrence receives its correct URL.
🪄 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: fb21402b-2b71-4013-9528-f65d628be70d

📥 Commits

Reviewing files that changed from the base of the PR and between 9efd0e6 and 07ce76f.

📒 Files selected for processing (3)
  • apps/web/pages/api/book/recurring-event.test.ts
  • packages/features/bookings/lib/handleConfirmation.ts
  • packages/features/bookings/lib/service/RegularBookingService.ts

Comment on lines +592 to +609
const promises = updatedBookings.flatMap((updatedBooking) => {
const bookingMeetingUrl =
updatedBooking.metadata &&
typeof updatedBooking.metadata === "object" &&
"videoCallUrl" in updatedBooking.metadata
? (updatedBooking.metadata as { videoCallUrl: string }).videoCallUrl
: meetingUrl;

const payload: EventPayloadType = {
...evtWithoutAssignmentReason,
...eventTypeInfo,
bookingId: updatedBooking.id,
eventTypeId: eventType?.id,
status: "ACCEPTED",
smsReminderNumber: booking.smsReminderNumber || undefined,
metadata: bookingMeetingUrl ? { videoCallUrl: bookingMeetingUrl } : {},
...(platformClientParams ? platformClientParams : {}),
};
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

Override occurrence fields in each webhook payload.

The loop sends one payload per updatedBooking, but spreading evtWithoutAssignmentReason leaves the original event’s uid, startTime, and endTime on every payload. Consumers can receive different bookingIds with the same occurrence details. Set the per-occurrence fields from updatedBooking.

Suggested fix
       const payload: EventPayloadType = {
         ...evtWithoutAssignmentReason,
+        uid: updatedBooking.uid,
+        startTime: updatedBooking.startTime.toISOString(),
+        endTime: updatedBooking.endTime.toISOString(),
+        title: updatedBooking.title,
         ...eventTypeInfo,
         bookingId: updatedBooking.id,
         eventTypeId: eventType?.id,
         status: "ACCEPTED",
-        smsReminderNumber: booking.smsReminderNumber || undefined,
+        smsReminderNumber: updatedBooking.smsReminderNumber || undefined,
         metadata: bookingMeetingUrl ? { videoCallUrl: bookingMeetingUrl } : {},
         ...(platformClientParams ? platformClientParams : {}),
       };
📝 Committable suggestion

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

Suggested change
const promises = updatedBookings.flatMap((updatedBooking) => {
const bookingMeetingUrl =
updatedBooking.metadata &&
typeof updatedBooking.metadata === "object" &&
"videoCallUrl" in updatedBooking.metadata
? (updatedBooking.metadata as { videoCallUrl: string }).videoCallUrl
: meetingUrl;
const payload: EventPayloadType = {
...evtWithoutAssignmentReason,
...eventTypeInfo,
bookingId: updatedBooking.id,
eventTypeId: eventType?.id,
status: "ACCEPTED",
smsReminderNumber: booking.smsReminderNumber || undefined,
metadata: bookingMeetingUrl ? { videoCallUrl: bookingMeetingUrl } : {},
...(platformClientParams ? platformClientParams : {}),
};
const promises = updatedBookings.flatMap((updatedBooking) => {
const bookingMeetingUrl =
updatedBooking.metadata &&
typeof updatedBooking.metadata === "object" &&
"videoCallUrl" in updatedBooking.metadata
? (updatedBooking.metadata as { videoCallUrl: string }).videoCallUrl
: meetingUrl;
const payload: EventPayloadType = {
...evtWithoutAssignmentReason,
uid: updatedBooking.uid,
startTime: updatedBooking.startTime.toISOString(),
endTime: updatedBooking.endTime.toISOString(),
title: updatedBooking.title,
...eventTypeInfo,
bookingId: updatedBooking.id,
eventTypeId: eventType?.id,
status: "ACCEPTED",
smsReminderNumber: updatedBooking.smsReminderNumber || undefined,
metadata: bookingMeetingUrl ? { videoCallUrl: bookingMeetingUrl } : {},
...(platformClientParams ? platformClientParams : {}),
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/features/bookings/lib/handleConfirmation.ts` around lines 592 - 609,
The payload construction is reusing evtWithoutAssignmentReason which leaves the
original event's occurrence fields (uid, startTime, endTime) on every webhook;
update the payload so those per-occurrence fields come from the current
updatedBooking instead: when building payload in the updatedBookings.flatMap
loop (symbols: updatedBooking, evtWithoutAssignmentReason, payload), explicitly
set uid, startTime, and endTime (or override them after spreading
evtWithoutAssignmentReason) using updatedBooking.uid, updatedBooking.startTime,
and updatedBooking.endTime so each payload reflects the correct occurrence for
that bookingId.

Previously, two bugs caused all recurring bookings to share the same
videoCallUrl:

1. RegularBookingService.ts: metadata was built using
   getVideoCallUrlFromCalEvent(evt) which for daily_video always returns
   the public Cal Video URL (derived from the first booking uid)
   instead of the actual meeting URL from the video adapter.
   Fix: use the already-correct videoCallUrl directly.

2. handleConfirmation.ts: the recurring confirmation flow computed a
   single meetingUrl before the loop and stamped it on every occurrence.
   Fix: derive occurrenceMeetingUrl from scheduleResult.referencesToCreate
   so each occurrence gets the correct URL from its video reference.

Removes 3 FIXME comments from recurring-event.test.ts and updates
assertions to verify correct per-occurrence video URLs.

Fixes calcom#28871
@ashif323 ashif323 force-pushed the fix/recurring-booking-unique-video-url branch from 07ce76f to 2c7a04f Compare April 19, 2026 14:39
@pull-request-size pull-request-size Bot added size/M and removed size/L labels Apr 19, 2026
@github-actions
Copy link
Copy Markdown
Contributor

This PR has been marked as stale due to inactivity. If you're still working on it or need any help, please let us know or update the PR to keep it active.

@github-actions github-actions Bot added the Stale label Apr 27, 2026
@Qodo-Free-For-OSS
Copy link
Copy Markdown

Hi, In handleConfirmation, the BOOKING_CREATED webhook code now returns a map of payload promises but still calls Promise.all(promises) even though promises is no longer defined, causing a runtime/compile failure in this path.

Severity: action required | Category: correctness

How to fix: Restore promises array definition

Agent prompt to fix - you can give this to your LLM of choice:

Issue description

handleConfirmation calls await Promise.all(promises) but promises is no longer declared/assigned after the refactor.

Issue Context

The webhook dispatch previously created a promises array from subscribersBookingCreated.map(...). The new code returns a map expression but doesn't store it.

Fix Focus Areas

  • packages/features/bookings/lib/handleConfirmation.ts[411-439]
    • Reintroduce a const promises = ... assignment (or inline await Promise.all(...)) with correct scoping.
    • Ensure the webhook sending promises are actually awaited.

Found by Qodo code review

@github-actions github-actions Bot removed the Stale label Apr 28, 2026
@Qodo-Free-For-OSS
Copy link
Copy Markdown

Hi, In recurring confirmation, occurrenceMeetingUrl is chosen by taking the first reference with a meetingUrl and then applied to all updated bookings, which can select a non-video reference URL and cannot support truly per-occurrence URLs if references differ.

Severity: remediation recommended | Category: correctness

How to fix: Select video reference explicitly

Agent prompt to fix - you can give this to your LLM of choice:

Issue description

Recurring confirmation currently selects occurrenceMeetingUrl using referencesToCreate.find(ref => ref.meetingUrl) and applies it to all occurrences. This can choose the wrong reference type (e.g., a calendar URL) and does not support per-occurrence URLs.

Issue Context

referencesToCreate is built from all integration results, not just the video adapter. Multiple references may have meetingUrl.

Fix Focus Areas

  • packages/features/bookings/lib/handleConfirmation.ts[166-205]
  • packages/features/bookings/lib/EventManager.ts[387-415]

Suggested direction

  • Filter by the expected video reference type(s), e.g. ref.type === evt.videoCallData?.type or ref.type.endsWith('_video')/known conferencing types.
  • If the requirement is truly “unique URL per occurrence”, compute the URL per recurring booking (or store the correct URL per occurrence at creation time and reuse it here), rather than computing once and stamping it onto all bookings.

Found by Qodo code review

@pull-request-size pull-request-size Bot added size/L and removed size/M labels May 5, 2026
@CLAassistant
Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants