Skip to content

Commit c9f9712

Browse files
fix: meeting URL is missing in the email after rescheduling a seated event (calcom#26914)
* fix: missing meeting url * test: verify reschedule emails for seated events include meeting URL Co-Authored-By: anik@cal.com <adhabal2002@gmail.com> --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
1 parent b584f0a commit c9f9712

2 files changed

Lines changed: 326 additions & 2 deletions

File tree

packages/features/bookings/lib/handleSeats/reschedule/attendee/attendeeRescheduleSeatedBooking.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { cloneDeep } from "lodash";
33

44
import { sendRescheduledSeatEmailAndSMS } from "@calcom/emails/email-manager";
55
import type EventManager from "@calcom/features/bookings/lib/EventManager";
6+
import { addVideoCallDataToEvent } from "@calcom/features/bookings/lib/handleNewBooking/addVideoCallDataToEvent";
67
import { getTranslation } from "@calcom/lib/server/i18n";
78
import prisma from "@calcom/prisma";
89
import type { Person, CalendarEvent } from "@calcom/types/Calendar";
@@ -51,10 +52,16 @@ const attendeeRescheduleSeatedBooking = async (
5152
},
5253
});
5354

55+
const originalBookingReferences = originalRescheduledBooking?.references;
56+
5457
// We don't want to trigger rescheduling logic of the original booking
5558
originalRescheduledBooking = null;
5659

57-
await sendRescheduledSeatEmailAndSMS(evt, seatAttendee as Person, eventType.metadata);
60+
const evtWithVideoCallData = originalBookingReferences
61+
? addVideoCallDataToEvent(originalBookingReferences, evt)
62+
: evt;
63+
64+
await sendRescheduledSeatEmailAndSMS(evtWithVideoCallData, seatAttendee as Person, eventType.metadata);
5865

5966
return null;
6067
}
@@ -96,7 +103,11 @@ const attendeeRescheduleSeatedBooking = async (
96103

97104
await eventManager.updateCalendarAttendees(copyEvent, newTimeSlotBooking);
98105

99-
await sendRescheduledSeatEmailAndSMS(copyEvent, seatAttendee as Person, eventType.metadata);
106+
const copyEventWithVideoCallData = newTimeSlotBooking.references
107+
? addVideoCallDataToEvent(newTimeSlotBooking.references, copyEvent)
108+
: copyEvent;
109+
110+
await sendRescheduledSeatEmailAndSMS(copyEventWithVideoCallData, seatAttendee as Person, eventType.metadata);
100111
const filteredAttendees = originalRescheduledBooking?.attendees.filter((attendee) => {
101112
return attendee.email !== bookerEmail;
102113
});

packages/features/bookings/lib/handleSeats/test/handleSeats.test.ts

Lines changed: 313 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
BookingLocations,
1313
getDate,
1414
getMockBookingAttendee,
15+
mockCalendarToHaveNoBusySlots,
1516
} from "@calcom/testing/lib/bookingScenario/bookingScenario";
1617
import { createMockNextJsRequest } from "@calcom/testing/lib/bookingScenario/createMockNextJsRequest";
1718
import { getMockRequestDataForBooking } from "@calcom/testing/lib/bookingScenario/getMockRequestDataForBooking";
@@ -21,6 +22,7 @@ import { setupAndTeardown } from "@calcom/testing/lib/bookingScenario/setupAndTe
2122
import { describe, test, vi, expect } from "vitest";
2223

2324
import { appStoreMetadata } from "@calcom/app-store/apps.metadata.generated";
25+
import * as emailManager from "@calcom/emails/email-manager";
2426
import { ErrorCode } from "@calcom/lib/errorCodes";
2527
import { SchedulingType } from "@calcom/prisma/enums";
2628
import { BookingStatus } from "@calcom/prisma/enums";
@@ -1252,6 +1254,317 @@ describe("handleSeats", () => {
12521254
expect(attendeeSeat?.bookingId).toEqual(secondBookingId);
12531255
});
12541256

1257+
test("When rescheduling to an existing booking, reschedule email includes meeting URL", async () => {
1258+
const handleNewBooking = getNewBookingHandler();
1259+
const sendRescheduledSeatEmailSpy = vi.spyOn(emailManager, "sendRescheduledSeatEmailAndSMS");
1260+
1261+
const attendeeToReschedule = getMockBookingAttendee({
1262+
id: 2,
1263+
name: "Seat 2",
1264+
email: "seat2@test.com",
1265+
locale: "en",
1266+
timeZone: "America/Toronto",
1267+
bookingSeat: {
1268+
referenceUid: "booking-seat-2",
1269+
data: {},
1270+
},
1271+
});
1272+
1273+
const booker = getBooker({
1274+
email: attendeeToReschedule.email,
1275+
name: attendeeToReschedule.name,
1276+
});
1277+
1278+
const organizer = getOrganizer({
1279+
name: "Organizer",
1280+
email: "organizer@example.com",
1281+
id: 101,
1282+
schedules: [TestData.schedules.IstWorkHours],
1283+
});
1284+
1285+
const firstBookingId = 1;
1286+
const firstBookingUid = "abc123";
1287+
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
1288+
const firstBookingStartTime = `${plus1DateString}T04:00:00Z`;
1289+
const firstBookingEndTime = `${plus1DateString}T04:30:00Z`;
1290+
1291+
const secondBookingId = 2;
1292+
const secondBookingUid = "def456";
1293+
const { dateString: plus2DateString } = getDate({ dateIncrement: 2 });
1294+
const secondBookingStartTime = `${plus2DateString}T04:00:00Z`;
1295+
const secondBookingEndTime = `${plus2DateString}T04:30:00Z`;
1296+
1297+
const meetingUrl = "http://mock-dailyvideo.example.com/meeting-with-url";
1298+
1299+
await createBookingScenario(
1300+
getScenarioData({
1301+
eventTypes: [
1302+
{
1303+
id: 1,
1304+
slug: "seated-event",
1305+
slotInterval: 30,
1306+
length: 30,
1307+
users: [
1308+
{
1309+
id: 101,
1310+
},
1311+
],
1312+
seatsPerTimeSlot: 3,
1313+
seatsShowAttendees: false,
1314+
},
1315+
],
1316+
bookings: [
1317+
{
1318+
id: firstBookingId,
1319+
uid: firstBookingUid,
1320+
eventTypeId: 1,
1321+
status: BookingStatus.ACCEPTED,
1322+
startTime: firstBookingStartTime,
1323+
endTime: firstBookingEndTime,
1324+
metadata: {
1325+
videoCallUrl: meetingUrl,
1326+
},
1327+
references: [
1328+
{
1329+
type: appStoreMetadata.dailyvideo.type,
1330+
uid: "MOCK_ID",
1331+
meetingId: "MOCK_ID",
1332+
meetingPassword: "MOCK_PASS",
1333+
meetingUrl: meetingUrl,
1334+
credentialId: null,
1335+
},
1336+
],
1337+
attendees: [
1338+
getMockBookingAttendee({
1339+
id: 1,
1340+
name: "Seat 1",
1341+
email: "seat1@test.com",
1342+
locale: "en",
1343+
timeZone: "America/Toronto",
1344+
bookingSeat: {
1345+
referenceUid: "booking-seat-1",
1346+
data: {},
1347+
},
1348+
}),
1349+
attendeeToReschedule,
1350+
],
1351+
},
1352+
{
1353+
id: secondBookingId,
1354+
uid: secondBookingUid,
1355+
eventTypeId: 1,
1356+
status: BookingStatus.ACCEPTED,
1357+
startTime: secondBookingStartTime,
1358+
endTime: secondBookingEndTime,
1359+
metadata: {
1360+
videoCallUrl: meetingUrl,
1361+
},
1362+
references: [
1363+
{
1364+
type: appStoreMetadata.dailyvideo.type,
1365+
uid: "MOCK_ID_2",
1366+
meetingId: "MOCK_ID_2",
1367+
meetingPassword: "MOCK_PASS_2",
1368+
meetingUrl: meetingUrl,
1369+
credentialId: null,
1370+
},
1371+
],
1372+
attendees: [
1373+
getMockBookingAttendee({
1374+
id: 3,
1375+
name: "Seat 3",
1376+
email: "seat3@test.com",
1377+
locale: "en",
1378+
timeZone: "America/Toronto",
1379+
bookingSeat: {
1380+
referenceUid: "booking-seat-3",
1381+
data: {},
1382+
},
1383+
}),
1384+
],
1385+
},
1386+
],
1387+
organizer,
1388+
})
1389+
);
1390+
1391+
mockSuccessfulVideoMeetingCreation({
1392+
metadataLookupKey: "dailyvideo",
1393+
videoMeetingData: {
1394+
id: "MOCK_ID",
1395+
password: "MOCK_PASS",
1396+
url: meetingUrl,
1397+
},
1398+
});
1399+
1400+
const reqBookingUser = "seatedAttendee";
1401+
1402+
const mockBookingData = getMockRequestDataForBooking({
1403+
data: {
1404+
eventTypeId: 1,
1405+
responses: {
1406+
email: booker.email,
1407+
name: booker.name,
1408+
location: { optionValue: "", value: BookingLocations.CalVideo },
1409+
},
1410+
rescheduleUid: "booking-seat-2",
1411+
start: secondBookingStartTime,
1412+
end: secondBookingEndTime,
1413+
user: reqBookingUser,
1414+
},
1415+
});
1416+
1417+
await handleNewBooking({
1418+
bookingData: mockBookingData,
1419+
});
1420+
1421+
expect(sendRescheduledSeatEmailSpy).toHaveBeenCalled();
1422+
const calEvent = sendRescheduledSeatEmailSpy.mock.calls[0][0];
1423+
expect(calEvent.videoCallData).toBeDefined();
1424+
expect(calEvent.videoCallData?.url).toBe(meetingUrl);
1425+
});
1426+
1427+
test("When rescheduling to an empty timeslot, reschedule email includes meeting URL", async () => {
1428+
const handleNewBooking = getNewBookingHandler();
1429+
const sendRescheduledSeatEmailSpy = vi.spyOn(emailManager, "sendRescheduledSeatEmailAndSMS");
1430+
1431+
const attendeeToReschedule = getMockBookingAttendee({
1432+
id: 2,
1433+
name: "Seat 2",
1434+
email: "seat2@test.com",
1435+
locale: "en",
1436+
timeZone: "America/Toronto",
1437+
bookingSeat: {
1438+
referenceUid: "booking-seat-2",
1439+
data: {},
1440+
},
1441+
});
1442+
1443+
const booker = getBooker({
1444+
email: attendeeToReschedule.email,
1445+
name: attendeeToReschedule.name,
1446+
});
1447+
1448+
const organizer = getOrganizer({
1449+
name: "Organizer",
1450+
email: "organizer@example.com",
1451+
id: 101,
1452+
schedules: [TestData.schedules.IstWorkHours],
1453+
});
1454+
1455+
const firstBookingId = 1;
1456+
const firstBookingUid = "abc123";
1457+
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
1458+
const firstBookingStartTime = `${plus1DateString}T04:00:00Z`;
1459+
const firstBookingEndTime = `${plus1DateString}T04:30:00Z`;
1460+
1461+
const { dateString: plus2DateString } = getDate({ dateIncrement: 2 });
1462+
const secondBookingStartTime = `${plus2DateString}T04:00:00Z`;
1463+
const secondBookingEndTime = `${plus2DateString}T04:30:00Z`;
1464+
1465+
const meetingUrl = "http://mock-dailyvideo.example.com/meeting-with-url";
1466+
1467+
await createBookingScenario(
1468+
getScenarioData({
1469+
eventTypes: [
1470+
{
1471+
id: 1,
1472+
slug: "seated-event",
1473+
slotInterval: 30,
1474+
length: 30,
1475+
users: [
1476+
{
1477+
id: 101,
1478+
},
1479+
],
1480+
seatsPerTimeSlot: 3,
1481+
seatsShowAttendees: false,
1482+
},
1483+
],
1484+
bookings: [
1485+
{
1486+
id: firstBookingId,
1487+
uid: firstBookingUid,
1488+
eventTypeId: 1,
1489+
status: BookingStatus.ACCEPTED,
1490+
startTime: firstBookingStartTime,
1491+
endTime: firstBookingEndTime,
1492+
metadata: {
1493+
videoCallUrl: meetingUrl,
1494+
},
1495+
references: [
1496+
{
1497+
type: appStoreMetadata.dailyvideo.type,
1498+
uid: "MOCK_ID",
1499+
meetingId: "MOCK_ID",
1500+
meetingPassword: "MOCK_PASS",
1501+
meetingUrl: meetingUrl,
1502+
credentialId: null,
1503+
},
1504+
],
1505+
attendees: [
1506+
getMockBookingAttendee({
1507+
id: 1,
1508+
name: "Seat 1",
1509+
email: "seat1@test.com",
1510+
locale: "en",
1511+
timeZone: "America/Toronto",
1512+
bookingSeat: {
1513+
referenceUid: "booking-seat-1",
1514+
data: {},
1515+
},
1516+
}),
1517+
attendeeToReschedule,
1518+
],
1519+
},
1520+
],
1521+
organizer,
1522+
})
1523+
);
1524+
1525+
mockSuccessfulVideoMeetingCreation({
1526+
metadataLookupKey: "dailyvideo",
1527+
videoMeetingData: {
1528+
id: "NEW_MOCK_ID",
1529+
password: "NEW_MOCK_PASS",
1530+
url: "http://mock-dailyvideo.example.com/new-meeting",
1531+
},
1532+
});
1533+
1534+
mockCalendarToHaveNoBusySlots("googlecalendar", {
1535+
create: {
1536+
id: "GOOGLE_CALENDAR_EVENT_ID",
1537+
iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID",
1538+
},
1539+
});
1540+
1541+
const reqBookingUser = "seatedAttendee";
1542+
1543+
const mockBookingData = getMockRequestDataForBooking({
1544+
data: {
1545+
eventTypeId: 1,
1546+
responses: {
1547+
email: booker.email,
1548+
name: booker.name,
1549+
location: { optionValue: "", value: BookingLocations.CalVideo },
1550+
},
1551+
rescheduleUid: "booking-seat-2",
1552+
start: secondBookingStartTime,
1553+
end: secondBookingEndTime,
1554+
user: reqBookingUser,
1555+
},
1556+
});
1557+
1558+
await handleNewBooking({
1559+
bookingData: mockBookingData,
1560+
});
1561+
1562+
expect(sendRescheduledSeatEmailSpy).toHaveBeenCalled();
1563+
const calEvent = sendRescheduledSeatEmailSpy.mock.calls[0][0];
1564+
expect(calEvent.videoCallData).toBeDefined();
1565+
expect(calEvent.videoCallData?.url).toBe(meetingUrl);
1566+
});
1567+
12551568
test("When rescheduling to an empty timeslot, create a new booking", async () => {
12561569
const handleNewBooking = getNewBookingHandler();
12571570

0 commit comments

Comments
 (0)