Files
calendar/packages/features/tasker/tasks/triggerNoShow/common.ts
T
Hariom BalharaGitHubhariom@cal.com <hariombalhara@gmail.com>Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
54da93ab02 chore: Integrate mark-no-show booking audit (#26570)
* chore: Integrate mark-no-show booking audit

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* fix: Simplify host no-show audit and add comment for attendee actor

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* fix: Make actionSource required with ValidActionSource type and remove unnecessary comments

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* fix: Remove merge conflict markers from bookings.service.ts

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* feat: implement SYSTEM as a booking audit source for background tasks, including no-show triggers.

* refactor: Move prisma query to BookingRepository for audit logging

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* Conflict resolution

* refactor: introduce `handleMarkHostNoShow` for public viewer and standardize actor and action source parameters for no-show actions.

* refactor: Combine HostNoShowUpdatedAuditActionService and AttendeeNoShowUpdatedAuditActionService into NoShowUpdatedActionService

- Create new NoShowUpdatedAuditActionService with combined schema supporting both noShowHost and noShowAttendee as optional fields
- Update BookingAuditActionServiceRegistry to use combined service with NO_SHOW_UPDATED action type
- Update BookingAuditTaskerProducerService with single queueNoShowUpdatedAudit method
- Update BookingAuditProducerService.interface.ts with combined method
- Update BookingEventHandlerService with single onNoShowUpdated method
- Update handleMarkNoShow.ts to use combined audit service
- Update tasker tasks (triggerGuestNoShow, triggerHostNoShow) to use combined service
- Add NO_SHOW_UPDATED to Prisma BookingAuditAction enum
- Remove old separate HostNoShowUpdatedAuditActionService and AttendeeNoShowUpdatedAuditActionService files

This allows a single API action (e.g., API V2 markAbsent) that updates both host and attendee no-show status to be logged as a single audit event.

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* chore: Add migration for NO_SHOW_UPDATED audit action enum value

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* refactor: Update no-show audit schema to use host and attendees array

- Rename noShowHost to host and noShowAttendee to attendees (remove redundant prefix)
- Change attendees from single value to array to support multiple attendees in single audit entry
- Update handleMarkNoShow.ts to create single audit entry for all attendees
- Update tasker tasks (triggerGuestNoShow, triggerHostNoShow) to use new schema
- Update display data types to reflect new schema structure

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* Update schema to be more clear. Call onNoShowUpdated immediately after DB update as audit only cares about DB update, and this would avoid any accidental Audit update on DB change

* chore: accommodate schema changes and fix type errors for no-show audit

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* fix: Update migration to remove old no-show enum values

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* fix: Use updated attendees in webhook payload for guest no-show

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* fix: Remove duplicate imports and fix actor parameter in bookings.service.ts

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* refactor: Move booking query to BookingRepository and remove unused method

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* refactor: centralize no-show audit event firing and attendee fetching within `handleMarkNoShow` for improved clarity.

* fix: Build attendeesNoShow as Record with attendee IDs as keys

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* Fix and add tests for handleMarkBoShow

* refactor: Move prisma queries to AttendeeRepository and BookingRepository

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* fix: Correct return type for updateNoShow method (noShow can be null)

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* fix: Update handleMarkNoShow tests to mock new repository methods

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* fix triggerGuestNoShow

* refactor: Move triggerGuestNoShow queries to AttendeeRepository and add unit tests

- Add new methods to AttendeeRepository:
  - findByBookingId: Get attendees with id, email, noShow
  - findByBookingIdWithDetails: Get full attendee details
  - updateManyNoShowByBookingIdAndEmails: Update specific attendees
  - updateManyNoShowByBookingIdExcludingEmails: Update all except specific emails
- Refactor triggerGuestNoShow.ts to use AttendeeRepository instead of direct Prisma calls
- Add comprehensive unit tests for triggerGuestNoShow following handleMarkNoShow.test.ts pattern:
  - Mock repositories and external services
  - In-memory DB simulation
  - Test core functionality, audit logging, webhook payload, error handling, and edge cases

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* refactor: Move triggerHostNoShow queries to repositories and delete unit test file

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* refactor: Convert repository methods to use named parameters objects

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* refactor: Enhance no-show handling by consolidating audit logging and updating repository interactions

- Refactor `buildResultPayload` to accept a structured argument for better clarity.
- Update `updateAttendees` to use a named parameter object for improved readability.
- Introduce `fireNoShowUpdatedEvent` to centralize no-show audit logging for both hosts and guests.
- Remove redundant methods and streamline attendee retrieval in `AttendeeRepository`.
- Add comprehensive unit tests for no-show event handling, ensuring accurate audit logging and webhook triggering.

* test: Add tests for handleMarkHostNoShow guest actor and unmarking host no-show

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* fix: update attendeesNoShow validation to handle numeric keys

- Changed attendeesNoShow schema to use z.coerce.number() for key validation, ensuring string keys are correctly coerced to numbers.
- Updated test to validate attendeesNoShow data using numeric key comparison, improving robustness of the audit data checks.

* test: Add integration tests for NoShowUpdatedAuditActionService coerce fix

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* feat: add graceful error handling for audit log enrichment

- Add hasError field to EnrichedAuditLog type
- Create buildFallbackAuditLog() method for failed enrichments
- Wrap enrichAuditLog() in try-catch to handle errors gracefully
- Add booking_audit_action.error_processing translation key
- Update BookingHistory.tsx to show warning icon for error logs
- Hide 'Show details' button for logs with hasError
- Add comprehensive test cases for error handling scenarios

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* feat: Enhance booking audit action services with new display fields and improved validation

- Added "attendee_no_show_updated" and "no_show_updated" translations to common.json.
- Updated IAuditActionService to include new methods for handling display fields and migration.
- Enhanced NoShowUpdatedAuditActionService to differentiate between host and attendee no-show updates.
- Improved ReassignmentAuditActionService to ensure consistent handling of audit data.
- Refactored BookingAuditViewerService for better clarity and maintainability.

* refactor: Use attendeeEmail instead of attendeeId as key in audit data and store host's userUuid

- Updated schema to use attendee email as key instead of attendee ID because attendee records can be reused with different person's data
- Store host's userUuid in audit data with format: host: { userUuid, noShow: { old, new } }
- Updated fireNoShowUpdated to accept hostUserUuid parameter
- Added uuid field to BookingRepository.findByUidIncludeEventTypeAttendeesAndUser
- Updated NoShowUpdatedAuditActionService getDisplayFields to work with email-based keys
- Added attendeeRepository to DI modules for BookingAuditTaskConsumer
- Updated tests to match new schema format

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* test: Update integration tests to use new schema format with host.userUuid and email keys

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* fix: correct audit schema for SYSTEM source and ensure attendee lookup by bookingId

- Fix schema inconsistency: use host.userUuid format instead of hostNoShow
- Add user.uuid to getBooking select for audit logging
- Log warning when hostUserUuid is undefined but host no-show is being updated
- Use email as key for attendeesNoShow (not attendee ID) to match schema
- Fetch attendees by bookingId before updating to ensure correct scoping
- Remove unused safeStringify import

* refactor: Change attendeesNoShow schema from Record to Array format

- Update NoShowUpdatedAuditActionService schema to use array format:
  attendeesNoShow: Array<{attendeeEmail: string, noShow: {old, new}}>
- Update handleMarkNoShow.ts to build attendeesNoShow as array
- Update common.ts fireNoShowUpdatedEvent to convert Map to array
- Update all tests to use new array format with attendeeEmail property

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* refactor: Update attendeesNoShow schema to use array format in triggerNoShow tasks

- Refactor `triggerGuestNoShow` and `triggerHostNoShow` to replace Map with array for `attendeesNoShowAudit`.
- Update `fireNoShowUpdatedEvent` to handle the new array format for attendees.
- Ensure consistent data structure across no-show audit logging for better clarity and maintainability.

* fix: Update ReassignmentAuditActionService to use GetDisplayFieldsParams interface

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* fix: Update ReassignmentAuditActionService tests to use GetDisplayFieldsParams interface

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* fix: Use handleMarkAttendeeNoShow for API v2 mark absent endpoint

- Export handleMarkAttendeeNoShow from platform-libraries
- Update API v2 bookings service to use handleMarkAttendeeNoShow with actionSource
- Add validation for userUuid before calling handleMarkAttendeeNoShow

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* refactor: Enhance display fields structure in booking audit components

- Updated `BookingHistory.tsx` to allow for more flexible display fields, including optional raw values and arrays of values.
- Introduced `DisplayFieldValue` component for rendering display fields, improving clarity and maintainability.
- Adjusted `IAuditActionService` and `NoShowUpdatedAuditActionService` to align with the new display fields structure.
- Ensured consistent handling of display fields across the booking audit service for better data representation.

* fix: Remove debug code that duplicated first attendee in display fields

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* refactor: Update attendee repository methods to use structured parameters

- Modified `updateNoShow`, `updateManyNoShowByBookingIdAndEmails`, and `updateManyNoShowByBookingIdExcludingEmails` methods to accept structured `where` and `data` parameters for better clarity and maintainability.
- Updated calls to these methods throughout the codebase to reflect the new parameter structure.
- Removed unused `findByIdWithNoShow` method and adjusted related logic to streamline attendee data retrieval.

* feat: Add infrastructure for no-show audit integration

- Add Prisma migrations for SYSTEM source and NO_SHOW_UPDATED audit action
- Add NoShowUpdatedAuditActionService with array-based attendeesNoShow schema
- Update BookingAuditActionServiceRegistry to include NO_SHOW_UPDATED
- Update BookingAuditTaskConsumer and BookingAuditViewerService
- Add AttendeeRepository methods for no-show queries
- Update IAuditActionService interface with values array support
- Update locales with no-show audit translation keys

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* fix: Add NO_SHOW_UPDATED to BookingAuditAction and SYSTEM to ActionSource types

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* fix: Remove HOST_NO_SHOW_UPDATED and ATTENDEE_NO_SHOW_UPDATED from BookingAuditAction type to match Prisma schema

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* fix: Update BookingAuditActionSchema to use NO_SHOW_UPDATED instead of HOST_NO_SHOW_UPDATED and ATTENDEE_NO_SHOW_UPDATED

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* refactor: Remove deprecated no-show audit services and unify to NoShowUpdatedAuditActionService

- Delete HostNoShowUpdatedAuditActionService and AttendeeNoShowUpdatedAuditActionService
- Update BookingAuditProducerService.interface.ts to use queueNoShowUpdatedAudit
- Update BookingAuditTaskerProducerService.ts to use queueNoShowUpdatedAudit
- Update BookingEventHandlerService.ts to use onNoShowUpdated
- Add integration tests for NoShowUpdatedAuditActionService

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* fix: Update test mock to match new AttendeeRepository.updateNoShow signature

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* refactor: rename handleMarkAttendeeNoShow to handleMarkNoShow and update related types

- Renamed `handleMarkAttendeeNoShow` to `handleMarkNoShow` for clarity.
- Updated type definitions to reflect the new naming convention.
- Modified the `markNoShow` handler to require `userUuid` and adjusted related logic.
- Enhanced the `fireNoShowUpdatedEvent` to accommodate changes in event type structure.
- Updated references across various files to ensure consistency with the new function name.

* handle null value

* fix: add explicit parentheses for operator precedence clarity

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* Fix cubic reported bug

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-02-06 09:27:25 -03:00

315 lines
10 KiB
TypeScript

import dayjs from "@calcom/dayjs";
import type { NoShowUpdatedAuditData } from "@calcom/features/booking-audit/lib/actions/NoShowUpdatedAuditActionService";
import { makeSystemActor } from "@calcom/features/booking-audit/lib/makeActor";
import { getBookingEventHandlerService } from "@calcom/features/bookings/di/BookingEventHandlerService.container";
import type { Host } from "@calcom/features/bookings/lib/getHostsAndGuests";
import { getHostsAndGuests } from "@calcom/features/bookings/lib/getHostsAndGuests";
import { sendGenericWebhookPayload } from "@calcom/features/webhooks/lib/sendPayload";
import getOrgIdFromMemberOrTeamId from "@calcom/lib/getOrgIdFromMemberOrTeamId";
import logger from "@calcom/lib/logger";
import { safeStringify } from "@calcom/lib/safeStringify";
import prisma from "@calcom/prisma";
import type { TimeUnit } from "@calcom/prisma/enums";
import { BookingStatus, WebhookTriggerEvents } from "@calcom/prisma/enums";
import { getBooking } from "./getBooking";
import { getMeetingSessionsFromRoomName } from "./getMeetingSessionsFromRoomName";
import type { TTriggerNoShowPayloadSchema, TWebhook } from "./schema";
import { ZSendNoShowWebhookPayloadSchema } from "./schema";
type OriginalRescheduledBooking =
| {
rescheduledBy?: string | null;
}
| null
| undefined;
export type Booking = Awaited<ReturnType<typeof getBooking>>;
type Webhook = TWebhook;
export type Participants = TTriggerNoShowPayloadSchema["data"][number]["participants"];
type ParticipantsWithEmail = (Participants[number] & { email?: string; isLoggedIn?: boolean })[];
export function sendWebhookPayload(
webhook: Webhook,
triggerEvent: WebhookTriggerEvents,
booking: Booking & { guests?: Booking["attendees"] },
maxStartTime: number,
participants: ParticipantsWithEmail,
originalRescheduledBooking?: OriginalRescheduledBooking,
hostEmail?: string
): Promise<{ ok: boolean; status: number } | void> {
const maxStartTimeHumanReadable = dayjs.unix(maxStartTime).format("YYYY-MM-DD HH:mm:ss Z");
return sendGenericWebhookPayload({
secretKey: webhook.secret,
triggerEvent,
createdAt: new Date().toISOString(),
webhook: {
subscriberUrl: webhook.subscriberUrl,
appId: webhook.appId,
payloadTemplate: webhook.payloadTemplate,
version: webhook.version,
},
data: {
title: booking.title,
bookingId: booking.id,
bookingUid: booking.uid,
startTime: booking.startTime,
attendees: booking.attendees,
endTime: booking.endTime,
participants,
...(hostEmail ? { hostEmail } : {}),
...(triggerEvent === WebhookTriggerEvents.AFTER_HOSTS_CAL_VIDEO_NO_SHOW
? { noShowHost: booking.noShowHost }
: {}),
...(triggerEvent === WebhookTriggerEvents.AFTER_GUESTS_CAL_VIDEO_NO_SHOW && booking.guests
? { guests: booking.guests }
: {}),
...(originalRescheduledBooking ? { rescheduledBy: originalRescheduledBooking.rescheduledBy } : {}),
eventType: {
...booking.eventType,
id: booking.eventTypeId,
hosts: undefined,
users: undefined,
},
webhook: {
...webhook,
secret: undefined,
},
message:
triggerEvent === WebhookTriggerEvents.AFTER_GUESTS_CAL_VIDEO_NO_SHOW
? `Guest didn't join the call or didn't join before ${maxStartTimeHumanReadable}`
: `Host with email ${hostEmail} didn't join the call or didn't join before ${maxStartTimeHumanReadable}`,
},
}).catch((e) => {
console.error(
`Error executing webhook for event: ${triggerEvent}, URL: ${webhook.subscriberUrl}`,
webhook,
e
);
});
}
export function calculateMaxStartTime(startTime: Date, time: number, timeUnit: TimeUnit): number {
return dayjs(startTime)
.add(time, timeUnit.toLowerCase() as dayjs.ManipulateType)
.unix();
}
function checkIfHostJoinedTheCall(email: string, allParticipants: ParticipantsWithEmail): boolean {
return allParticipants.some(
(participant) => participant.email && participant.isLoggedIn && participant.email === email
);
}
function checkIfGuestJoinedTheCall(email: string, allParticipants: ParticipantsWithEmail): boolean {
return allParticipants.some((participant) => participant.email && participant.email === email);
}
const getUserOrGuestById = async (id: string) => {
// Try User table (numeric IDs)
if (!isNaN(Number(id))) {
const user = await prisma.user.findUnique({
where: { id: parseInt(id) },
select: { email: true },
});
if (user) return { email: user.email, isLoggedIn: true };
}
// Try VideoCallGuest table (UUID)
const guestSession = await prisma.videoCallGuest
.findUnique({
where: { id },
select: { email: true },
})
.catch(() => null);
return { email: guestSession?.email, isLoggedIn: false };
};
export async function getParticipantsWithEmail(
allParticipants: Participants
): Promise<ParticipantsWithEmail> {
const participantsWithEmail = await Promise.all(
allParticipants.map(async (participant) => {
if (!participant.user_id) return participant;
const { email, isLoggedIn } = await getUserOrGuestById(participant.user_id);
return { ...participant, email, isLoggedIn };
})
);
return participantsWithEmail;
}
export const log = logger.getSubLogger({ prefix: ["triggerNoShowTask"] });
export const fireNoShowUpdatedEvent = async ({
booking,
noShowHostAudit,
attendeesNoShowAudit,
}: {
booking: {
id: number;
uid: string;
user?: { id: number; uuid: string } | null;
eventType?: { teamId?: number | null; parent?: { teamId?: number | null } | null } | null;
};
noShowHostAudit?: { old: boolean | null; new: boolean | null };
attendeesNoShowAudit?: NoShowUpdatedAuditData["attendeesNoShow"];
}): Promise<void> => {
const hasHostNoShow = noShowHostAudit && noShowHostAudit.new !== null;
const hasAttendeesNoShow = attendeesNoShowAudit && attendeesNoShowAudit.length > 0;
if (!hasHostNoShow && !hasAttendeesNoShow) {
return;
}
const hostUserUuid = booking.user?.uuid;
if (hasHostNoShow && !hostUserUuid) {
log.warn("Host no-show audit skipped: booking.user.uuid is undefined", { bookingUid: booking.uid });
}
try {
const orgId = await getOrgIdFromMemberOrTeamId({
memberId: booking.user?.id,
teamId: booking.eventType?.teamId || booking.eventType?.parent?.teamId,
});
const bookingEventHandlerService = getBookingEventHandlerService();
await bookingEventHandlerService.onNoShowUpdated({
bookingUid: booking.uid,
// This action is taken by the scheduled tasker job, so we use the system actor
actor: makeSystemActor(),
organizationId: orgId ?? null,
source: "SYSTEM",
auditData: {
...(hasHostNoShow && noShowHostAudit.new !== null && hostUserUuid
? {
host: {
userUuid: hostUserUuid,
noShow: { old: noShowHostAudit.old, new: noShowHostAudit.new },
},
}
: {}),
...(hasAttendeesNoShow ? { attendeesNoShow: attendeesNoShowAudit } : {}),
},
});
} catch (error) {
log.error("Error logging audit for automatic no-show", error);
}
};
export const prepareNoShowTrigger = async (
payload: string
): Promise<{
booking: Booking;
webhook: TWebhook;
hosts: Host[];
hostsThatDidntJoinTheCall: Host[];
hostsThatJoinedTheCall: Host[];
numberOfHostsThatJoined: number;
didGuestJoinTheCall: boolean;
guestsThatJoinedTheCall: { email: string; name: string }[];
guestsThatDidntJoinTheCall: { email: string; name: string }[];
originalRescheduledBooking?: OriginalRescheduledBooking;
participants: ParticipantsWithEmail;
} | void> => {
const { bookingId, webhook } = ZSendNoShowWebhookPayloadSchema.parse(JSON.parse(payload));
const booking = await getBooking(bookingId);
let originalRescheduledBooking = null;
if (booking.fromReschedule) {
originalRescheduledBooking = await prisma.booking.findFirst({
where: {
uid: booking.fromReschedule,
status: {
in: [BookingStatus.ACCEPTED, BookingStatus.CANCELLED, BookingStatus.PENDING],
},
},
select: {
rescheduledBy: true,
},
});
}
if (booking.status !== BookingStatus.ACCEPTED) {
log.debug(
"Booking is not accepted",
safeStringify({
bookingId,
webhook: { id: webhook.id },
})
);
return;
}
const dailyVideoReference =
booking.references?.filter((reference) => reference.type === "daily_video")?.pop() ?? null;
if (!dailyVideoReference) {
log.error(
"Daily video reference not found",
safeStringify({
bookingId,
webhook: { id: webhook.id },
})
);
throw new Error(`Daily video reference not found in triggerHostNoShow with bookingId ${bookingId}`);
}
const meetingDetails = await getMeetingSessionsFromRoomName(dailyVideoReference.uid);
const { hosts, guests } = getHostsAndGuests(booking);
const allParticipants = meetingDetails.data.flatMap((meeting) => meeting.participants);
const participantsWithEmail = await getParticipantsWithEmail(allParticipants);
const hostsThatJoinedTheCall: Host[] = [];
const hostsThatDidntJoinTheCall: Host[] = [];
for (const host of hosts) {
if (checkIfHostJoinedTheCall(host.email, participantsWithEmail)) {
hostsThatJoinedTheCall.push(host);
} else {
hostsThatDidntJoinTheCall.push(host);
}
}
const numberOfHostsThatJoined = hosts.length - hostsThatDidntJoinTheCall.length;
const requireEmailForGuests = booking.eventType?.calVideoSettings?.requireEmailForGuests ?? false;
let didGuestJoinTheCall: boolean;
const guestsThatJoinedTheCall: { email: string; name: string }[] = [];
const guestsThatDidntJoinTheCall: { email: string; name: string }[] = [];
if (requireEmailForGuests) {
for (const guest of guests) {
if (checkIfGuestJoinedTheCall(guest.email, participantsWithEmail)) {
guestsThatJoinedTheCall.push(guest);
} else {
guestsThatDidntJoinTheCall.push(guest);
}
}
didGuestJoinTheCall = guestsThatJoinedTheCall.length > 0;
} else {
didGuestJoinTheCall = meetingDetails.data.some(
(meeting) => meeting.max_participants > numberOfHostsThatJoined
);
}
return {
hosts,
hostsThatDidntJoinTheCall,
hostsThatJoinedTheCall,
booking,
numberOfHostsThatJoined,
webhook,
didGuestJoinTheCall,
guestsThatJoinedTheCall,
guestsThatDidntJoinTheCall,
originalRescheduledBooking,
participants: participantsWithEmail,
};
};