diff --git a/apps/web/pages/api/book/recurring-event.test.ts b/apps/web/pages/api/book/recurring-event.test.ts index d173517bc489eb..53e9d1c2804d0c 100644 --- a/apps/web/pages/api/book/recurring-event.test.ts +++ b/apps/web/pages/api/book/recurring-event.test.ts @@ -202,8 +202,8 @@ describe("handleNewBooking", () => { organizer, location: "integrations:daily", subscriberUrl: "http://my-webhook.example.com", - //FIXME: All recurring bookings seem to have the same URL. https://github.com/calcom/cal.com/issues/11955 - videoCallUrl: `${WEBAPP_URL}/video/${createdBookings[0].uid}`, + videoCallUrl: `${WEBAPP_URL}/video/${createdBooking.uid}`, + uid: createdBooking.uid ?? undefined, }); } @@ -551,8 +551,8 @@ describe("handleNewBooking", () => { organizer, location: "integrations:daily", subscriberUrl: "http://my-webhook.example.com", - //FIXME: File a bug - All recurring bookings seem to have the same URL. They should have same CalVideo URL which could mean that future recurring meetings would have already expired by the time they are needed. - videoCallUrl: `${WEBAPP_URL}/video/${createdBookings[0].uid}`, + videoCallUrl: `${WEBAPP_URL}/video/${createdBooking.uid}`, + uid: createdBooking.uid ?? undefined, }); } @@ -769,8 +769,8 @@ describe("handleNewBooking", () => { organizer, location: "integrations:daily", subscriberUrl: "http://my-webhook.example.com", - //FIXME: File a bug - All recurring bookings seem to have the same URL. They should have same CalVideo URL which could mean that future recurring meetings would have already expired by the time they are needed. - videoCallUrl: `${WEBAPP_URL}/video/${createdBookings[0].uid}`, + videoCallUrl: `${WEBAPP_URL}/video/${createdBooking.uid}`, + uid: createdBooking.uid ?? undefined, }); } diff --git a/packages/features/bookings/lib/handleConfirmation.ts b/packages/features/bookings/lib/handleConfirmation.ts index 46d88d38963c5b..ab14da86f7280f 100644 --- a/packages/features/bookings/lib/handleConfirmation.ts +++ b/packages/features/bookings/lib/handleConfirmation.ts @@ -21,7 +21,7 @@ import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks"; import { scheduleTrigger } from "@calcom/features/webhooks/lib/scheduleTrigger"; import sendPayload from "@calcom/features/webhooks/lib/sendOrSchedulePayload"; import type { EventPayloadType, EventTypeInfo } from "@calcom/features/webhooks/lib/sendPayload"; -import { getVideoCallUrlFromCalEvent } from "@calcom/lib/CalEventParser"; +import { getPublicVideoCallUrl, getVideoCallUrlFromCalEvent, isDailyVideoCall } from "@calcom/lib/CalEventParser"; import getOrgIdFromMemberOrTeamId from "@calcom/lib/getOrgIdFromMemberOrTeamId"; import { getTeamIdFromEventType } from "@calcom/lib/getTeamIdFromEventType"; import { safeStringify } from "@calcom/lib/safeStringify"; @@ -276,8 +276,15 @@ export async function handleConfirmation(args: { uid: booking.uid, })); - const updateBookingsPromise = unconfirmedRecurringBookings.map((recurringBooking) => - prisma.booking.update({ + const updateBookingsPromise = unconfirmedRecurringBookings.map((recurringBooking) => { + // For Cal Video (Daily), each occurrence must use its own booking uid as the room + // identifier — the public URL is /video/. Other providers create a single + // meeting for the whole series, so they share meetingUrl. + const bookingVideoCallUrl = isDailyVideoCall(evt.videoCallData) + ? getPublicVideoCallUrl(recurringBooking.uid) + : meetingUrl; + + return prisma.booking.update({ where: { id: recurringBooking.id, }, @@ -289,7 +296,7 @@ export async function handleConfirmation(args: { paid, metadata: { ...(typeof recurringBooking.metadata === "object" ? recurringBooking.metadata : {}), - videoCallUrl: meetingUrl, + videoCallUrl: bookingVideoCallUrl, }, }, select: { @@ -334,8 +341,8 @@ export async function handleConfirmation(args: { customInputs: true, id: true, }, - }) - ); + }); + }); const updatedBookingsResult = await Promise.all(updateBookingsPromise); updatedBookings = updatedBookings.concat(updatedBookingsResult); @@ -442,10 +449,15 @@ export async function handleConfirmation(args: { try { for (let index = 0; index < updatedBookings.length; index++) { const eventTypeSlug = updatedBookings[index].eventType?.slug || ""; + const bookingUid = updatedBookings[index].uid; const evtOfBooking = { ...evt, rescheduleReason: updatedBookings[index].cancellationReason || null, - metadata: { videoCallUrl: meetingUrl }, + metadata: { + videoCallUrl: isDailyVideoCall(evt.videoCallData) + ? getPublicVideoCallUrl(bookingUid) + : meetingUrl, + }, eventType: { slug: eventTypeSlug, schedulingType: updatedBookings[index].eventType?.schedulingType, @@ -455,7 +467,7 @@ export async function handleConfirmation(args: { }; evtOfBooking.startTime = updatedBookings[index].startTime.toISOString(); evtOfBooking.endTime = updatedBookings[index].endTime.toISOString(); - evtOfBooking.uid = updatedBookings[index].uid; + evtOfBooking.uid = bookingUid; const isFirstBooking = index === 0; if (!eventTypeMetadata?.disableStandardEmails?.all?.attendee) { diff --git a/packages/testing/src/lib/bookingScenario/expects.ts b/packages/testing/src/lib/bookingScenario/expects.ts index f4c180e51cf398..0dc3013aa2c5dc 100644 --- a/packages/testing/src/lib/bookingScenario/expects.ts +++ b/packages/testing/src/lib/bookingScenario/expects.ts @@ -273,6 +273,8 @@ export function expectWebhookToHaveBeenCalledWith( data: { triggerEvent: WebhookTriggerEvents; payload: Record | null; + /** When provided, only the webhook whose payload.uid matches this value is checked. */ + uid?: string; } ) { const fetchCalls = fetchMock.mock.calls; @@ -283,7 +285,9 @@ export function expectWebhookToHaveBeenCalledWith( const webhookFetchCall = webhooksToSubscriberUrl.find((call) => { const body = call[1]?.body; const parsedBody = JSON.parse((body as string) || "{}"); - return parsedBody.triggerEvent === data.triggerEvent; + if (parsedBody.triggerEvent !== data.triggerEvent) return false; + if (data.uid !== undefined && parsedBody.payload?.uid !== data.uid) return false; + return true; }); if (!webhookFetchCall) { @@ -1032,6 +1036,7 @@ export function expectBookingCreatedWebhookToHaveBeenFired({ subscriberUrl, paidEvent, videoCallUrl, + uid, isEmailHidden = false, isAttendeePhoneNumberHidden = false, }: { @@ -1041,6 +1046,8 @@ export function expectBookingCreatedWebhookToHaveBeenFired({ location: string; paidEvent?: boolean; videoCallUrl?: string | null; + /** When provided, only the webhook whose payload.uid matches this value is checked. */ + uid?: string; isEmailHidden?: boolean; isAttendeePhoneNumberHidden?: boolean; }) { @@ -1052,6 +1059,7 @@ export function expectBookingCreatedWebhookToHaveBeenFired({ if (!paidEvent) { expectWebhookToHaveBeenCalledWith(subscriberUrl, { triggerEvent: "BOOKING_CREATED", + uid, payload: { metadata: { ...(videoCallUrl ? { videoCallUrl } : null), @@ -1080,6 +1088,7 @@ export function expectBookingCreatedWebhookToHaveBeenFired({ } else { expectWebhookToHaveBeenCalledWith(subscriberUrl, { triggerEvent: "BOOKING_CREATED", + uid, payload: { // FIXME: File this bug and link ticket here. This is a bug in the code. metadata must be sent here like other BOOKING_CREATED webhook metadata: null,