From 599388961642e857c3cf4139e349d057517b5328 Mon Sep 17 00:00:00 2001 From: Hariom Balhara <1780212+hariombalhara@users.noreply.github.com> Date: Mon, 9 Mar 2026 19:23:36 +0530 Subject: [PATCH] feat: make impersonatedByUserUuid required across booking audit flows (#26546) * Integrate creation/rescheduling booking audit * fix: add missing hostUserUuid to booking audit test data Co-Authored-By: hariom@cal.com * fix-ci * feat: enhance booking audit with seat reference - Added support for seat reference in booking audit actions. - Updated localization for booking creation to include seat information. - Modified relevant services to pass attendee seat ID during booking creation. * fix: update test data to match schema requirements - Add seatReferenceUid: null to default mock audit log data - Add seatReferenceUid: null to multiple audit logs test case - Convert SEAT_RESCHEDULED test data to use numeric timestamps instead of ISO strings Co-Authored-By: hariom@cal.com * Allow nullish seatReferenceUid * feat: enhance booking audit to support rescheduledBy information - Updated booking audit actions to include rescheduledBy details, allowing tracking of who rescheduled a booking. - Refactored related services to accommodate the new rescheduledBy parameter in booking events. - Adjusted type definitions and function signatures to reflect the changes in the booking audit context. * Avoid possible run time issue * Fix imoport path * fix failing test due to merge from main\ * Pass useruuid * chore: retrigger CI (flaky unit test) Co-Authored-By: hariom@cal.com * feat: add context parameter to bulk audit methods for impersonation support - Add context parameter to queueBulkCreatedAudit and queueBulkRescheduledAudit in BookingAuditProducerService interface - Add context parameter to BookingAuditTaskerProducerService implementation - Add context parameter to onBulkBookingsCreated and onBulkBookingsRescheduled in BookingEventHandlerService - Update RegularBookingService.fireBookingEvents to accept and pass impersonation context - Update RecurringBookingService.fireBookingEvents to accept and pass impersonation context - Update handleSeats to accept and pass impersonation context Co-Authored-By: hariom@cal.com * chore: Integrate mark-no-show booking audit Co-Authored-By: hariom@cal.com * fix: Simplify host no-show audit and add comment for attendee actor Co-Authored-By: hariom@cal.com * feat: make impersonatedByUserUuid required with explicit null for non-impersonation cases Co-Authored-By: hariom@cal.com * feat: integrate impersonatedByUserUuid into booking cancellation audit flow - Add impersonatedByUserUuid to CancelBookingInput and CancelBookingMeta types - Pass audit context with impersonatedBy to onBookingCancelled and onBulkBookingsCancelled - Update all cancel booking call sites to pass impersonatedByUserUuid: - Web app cancel route: uses session impersonatedBy - API v1 and v2: explicitly set to null (no impersonation support) Co-Authored-By: hariom@cal.com * refactor: make impersonatedByUserUuid optional and remove explicit null assignments - Changed impersonatedByUserUuid from required (string | null) to optional (string?) - Removed explicit null assignments from API v1 and v2 endpoints - Keep impersonatedByUserUuid only where impersonation actually occurs (web app) Co-Authored-By: hariom@cal.com * Apply suggestions from code review Rfemove unnecessary comment * fix: restore bookingMeta variable in createBookingForApiV1 Co-Authored-By: hariom@cal.com * fix: Make actionSource required with ValidActionSource type and remove unnecessary comments Co-Authored-By: hariom@cal.com * fix: Remove merge conflict markers from bookings.service.ts Co-Authored-By: hariom@cal.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 * 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 * chore: Add migration for NO_SHOW_UPDATED audit action enum value Co-Authored-By: hariom@cal.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 * 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 * fix: Update migration to remove old no-show enum values Co-Authored-By: hariom@cal.com * fix: Use updated attendees in webhook payload for guest no-show Co-Authored-By: hariom@cal.com * fix: Remove duplicate imports and fix actor parameter in bookings.service.ts Co-Authored-By: hariom@cal.com * refactor: Move booking query to BookingRepository and remove unused method Co-Authored-By: hariom@cal.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 * Fix and add tests for handleMarkBoShow * refactor: Move prisma queries to AttendeeRepository and BookingRepository Co-Authored-By: hariom@cal.com * fix: Correct return type for updateNoShow method (noShow can be null) Co-Authored-By: hariom@cal.com * fix: Update handleMarkNoShow tests to mock new repository methods Co-Authored-By: hariom@cal.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 * refactor: Move triggerHostNoShow queries to repositories and delete unit test file Co-Authored-By: hariom@cal.com * refactor: Convert repository methods to use named parameters objects Co-Authored-By: hariom@cal.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 * 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 * 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 * 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 * test: Update integration tests to use new schema format with host.userUuid and email keys Co-Authored-By: hariom@cal.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 * 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 * fix: Update ReassignmentAuditActionService tests to use GetDisplayFieldsParams interface Co-Authored-By: hariom@cal.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 * 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 * 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 * fix: Add NO_SHOW_UPDATED to BookingAuditAction and SYSTEM to ActionSource types Co-Authored-By: hariom@cal.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 * 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 * 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 * fix: Update test mock to match new AttendeeRepository.updateNoShow signature Co-Authored-By: hariom@cal.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 * Fix cubic reported bug * feat: add impersonation audit support to no-show flow (#27601) Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * feat: add impersonatedByUserUuid context to all BookingEventHandler calls (#27602) - Add context parameter to confirm.handler.ts (onBulkBookingsRejected, onBookingRejected) - Add context parameter to requestReschedule.handler.ts (onRescheduleRequested) - Add context parameter to editLocation.handler.ts (onLocationChanged) - Add context parameter to addGuests.handler.ts (onAttendeeAdded) - Add context parameter to handleConfirmation.ts (onBulkBookingsAccepted, onBookingAccepted) - Update _router.tsx to pass impersonatedByUserUuid from session to handlers This ensures all BookingEventHandler method calls that have loggedInUser context now properly support impersonation tracking for audit logging. Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix: remove stray merge artifact in RegularBookingService Co-Authored-By: hariom@cal.com * fix: remove duplicate imports in BookingRepository Co-Authored-By: hariom@cal.com * fix: resolve type errors and duplicate declarations from merge Co-Authored-By: hariom@cal.com * fix: remove duplicate test assertions and duplicate test from merge artifacts Co-Authored-By: hariom@cal.com * fix: remove redundant ?? undefined from impersonatedByUserUuid Co-Authored-By: hariom@cal.com * fix: update impersonatedByUserUuid type to string | null for consistency Co-Authored-By: hariom@cal.com * fix: make impersonatedByUserUuid required in booking audit flows Changes impersonatedByUserUuid from optional (?: string | null) to required (: string | null) in all booking-related type definitions. This ensures audit-critical parameters are never accidentally omitted. Fixed all call sites to explicitly pass the value or null. Co-Authored-By: hariom@cal.com * fix: add impersonatedByUserUuid to remaining callers and tests Adds impersonatedByUserUuid: null to API v1, API v2, and test callers of handleCancelBooking that were missing the now-required parameter. Co-Authored-By: hariom@cal.com * fix: require impersonatedByUserUuid in DTOs and pass it across all booking flows (coalesce to null) Co-Authored-By: hariom@cal.com * fix: add impersonatedByUserUuid to remaining call sites (confirm, markNoShow, webhook, tests) Co-Authored-By: hariom@cal.com * fix: add impersonatedByUserUuid to confirm handler test and API v2 confirm/decline calls Co-Authored-By: hariom@cal.com * fix: derive impersonatedByUserUuid from session in reportBooking handler instead of hardcoding null Co-Authored-By: hariom@cal.com * fix: reorder spread to ensure impersonatedByUserUuid null fallback is not overwritten Co-Authored-By: hariom@cal.com * fix: add missing impersonatedByUserUuid to CalendarSyncService cancel and reschedule calls Co-Authored-By: hariom@cal.com * fix: update CalendarSyncService tests to expect impersonatedByUserUuid Co-Authored-By: hariom@cal.com * ci: retrigger failed checks (attempt 1) Co-Authored-By: hariom@cal.com * ci: retrigger failed checks (attempt 2) Co-Authored-By: hariom@cal.com * ci: retrigger failed checks (attempt 3) Co-Authored-By: hariom@cal.com * chore: bust prisma cache with comment change Co-Authored-By: hariom@cal.com * ci: retrigger after cache bust (attempt 2) Co-Authored-By: hariom@cal.com * ci: retrigger after cache bust (attempt 3) Co-Authored-By: hariom@cal.com --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com> Co-authored-by: Volnei Munhoz --- .../api/v1/pages/api/bookings/[id]/_delete.ts | 1 + apps/api/v1/pages/api/bookings/_post.ts | 1 + .../controllers/bookings.controller.ts | 4 +++ .../services/booking-guests.service.ts | 1 + .../2024-08-13/services/bookings.service.ts | 9 +++++ apps/web/app/api/cancel/route.ts | 1 + apps/web/app/api/link/route.ts | 1 + .../web/app/api/verify-booking-token/route.ts | 1 + apps/web/pages/api/book/event.ts | 2 +- apps/web/pages/api/book/recurring-event.ts | 1 + .../test/handlers/requestReschedule.test.ts | 1 + apps/web/test/lib/confirm.handler.test.ts | 5 +++ .../_utils/payments/handlePaymentSuccess.ts | 1 + .../BookingAuditProducerService.interface.ts | 2 ++ .../BookingAuditTaskerProducerService.ts | 2 ++ .../bookings/lib/dto/BookingCancel.d.ts | 1 + packages/features/bookings/lib/dto/types.d.ts | 2 +- .../bookings/lib/handleCancelBooking.ts | 7 ++++ .../test/handleCancelBooking.test.ts | 14 ++++++++ .../bookings/lib/handleConfirmation.ts | 8 +++++ .../bookings/lib/handleSeats/handleSeats.ts | 8 +++++ .../lib/handleSeats/test/handleSeats.test.ts | 3 ++ .../bookings/lib/handleSeats/types.d.ts | 1 + .../BookingEventHandlerService.ts | 8 +++-- .../lib/service/RecurringBookingService.ts | 26 ++++++++++++--- .../lib/service/RegularBookingService.ts | 33 ++++++++++++++++--- .../lib/sync/CalendarSyncService.ts | 2 ++ .../CalendarSyncService.integration.test.ts | 2 ++ .../__tests__/CalendarSyncService.test.ts | 3 ++ packages/features/ee/payments/api/webhook.ts | 1 + packages/features/handleMarkNoShow.ts | 13 ++++++++ packages/prisma/schema.prisma | 2 +- .../loggedInViewer/markNoShow.handler.ts | 5 +++ .../publicViewer/markHostAsNoShow.handler.ts | 1 + .../routers/viewer/bookings/_router.tsx | 13 +++++--- .../viewer/bookings/addGuests.handler.ts | 4 +++ .../viewer/bookings/confirm.handler.ts | 11 ++++++- .../viewer/bookings/editLocation.handler.ts | 5 ++- .../bookings/reportBooking.handler.test.ts | 14 ++++++++ .../viewer/bookings/reportBooking.handler.ts | 4 ++- .../bookings/requestReschedule.handler.ts | 5 ++- 41 files changed, 206 insertions(+), 23 deletions(-) diff --git a/apps/api/v1/pages/api/bookings/[id]/_delete.ts b/apps/api/v1/pages/api/bookings/[id]/_delete.ts index 1756c29467..b7321824ca 100644 --- a/apps/api/v1/pages/api/bookings/[id]/_delete.ts +++ b/apps/api/v1/pages/api/bookings/[id]/_delete.ts @@ -82,6 +82,7 @@ async function handler(req: NextApiRequest) { return await handleCancelBooking({ bookingData: { id, allRemainingBookings, cancellationReason, cancelledBy }, userId: req.userId, + impersonatedByUserUuid: null, actionSource: "API_V1", }); } diff --git a/apps/api/v1/pages/api/bookings/_post.ts b/apps/api/v1/pages/api/bookings/_post.ts index 3b1c1c57f2..a1ab66c754 100644 --- a/apps/api/v1/pages/api/bookings/_post.ts +++ b/apps/api/v1/pages/api/bookings/_post.ts @@ -247,6 +247,7 @@ async function handler(req: NextApiRequest) { userId, hostname: req.headers.host || "", forcedSlug: req.headers["x-cal-force-slug"] as string | undefined, + impersonatedByUserUuid: null, }, bookingDataSchemaGetter: getBookingDataSchemaForApi, }); diff --git a/apps/api/v2/src/ee/bookings/2024-04-15/controllers/bookings.controller.ts b/apps/api/v2/src/ee/bookings/2024-04-15/controllers/bookings.controller.ts index 47a8bd5390..45c8dceac4 100644 --- a/apps/api/v2/src/ee/bookings/2024-04-15/controllers/bookings.controller.ts +++ b/apps/api/v2/src/ee/bookings/2024-04-15/controllers/bookings.controller.ts @@ -223,6 +223,7 @@ export class BookingsController_2024_04_15 { platformBookingUrl: bookingRequest.platformBookingUrl, platformBookingLocation: bookingRequest.platformBookingLocation, areCalendarEventsEnabled: bookingRequest.areCalendarEventsEnabled, + impersonatedByUserUuid: null, }, }); if (booking.userId && booking.uid && booking.startTime && booking.user?.isPlatformManaged) { @@ -279,6 +280,7 @@ export class BookingsController_2024_04_15 { platformCancelUrl: bookingRequest.platformCancelUrl, platformRescheduleUrl: bookingRequest.platformRescheduleUrl, platformBookingUrl: bookingRequest.platformBookingUrl, + impersonatedByUserUuid: null, actionSource: "API_V2", }); if (!res.onlyRemovedAttendee && res.isPlatformManagedUserBooking) { @@ -317,6 +319,7 @@ export class BookingsController_2024_04_15 { userId: user.id, actor: makeUserActor(user.uuid), actionSource: "API_V2", + impersonatedByUserUuid: null, }); return { status: SUCCESS_STATUS, data: markNoShowResponse }; @@ -354,6 +357,7 @@ export class BookingsController_2024_04_15 { platformBookingUrl: bookingRequest.platformBookingUrl, platformBookingLocation: bookingRequest.platformBookingLocation, noEmail: bookingRequest.body.noEmail, + impersonatedByUserUuid: null, }, creationSource: "API_V2", }); diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/booking-guests.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/booking-guests.service.ts index c434cfae1b..d5b7c483f5 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/services/booking-guests.service.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/services/booking-guests.service.ts @@ -48,6 +48,7 @@ export class BookingGuestsService_2024_08_13 { input: { bookingId: booking.id, guests: input.guests }, emailsEnabled, actionSource: "API_V2", + impersonatedByUserUuid: null, }); if (res.message === "Guests added") { return await this.bookingsService.getBooking(bookingUid, user); diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts index c7bf7f40cc..a47ed5826e 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts @@ -468,6 +468,7 @@ export class BookingsService_2024_08_13 { platformBookingLocation: bookingRequest.platformBookingLocation, noEmail: bookingRequest.noEmail, areCalendarEventsEnabled: bookingRequest.areCalendarEventsEnabled, + impersonatedByUserUuid: null, }, creationSource: "API_V2", }); @@ -497,6 +498,7 @@ export class BookingsService_2024_08_13 { platformBookingUrl: bookingRequest.platformBookingUrl, platformBookingLocation: bookingRequest.platformBookingLocation, areCalendarEventsEnabled: bookingRequest.areCalendarEventsEnabled, + impersonatedByUserUuid: null, }, creationSource: "API_V2", }); @@ -527,6 +529,7 @@ export class BookingsService_2024_08_13 { platformBookingUrl: bookingRequest.platformBookingUrl, platformBookingLocation: bookingRequest.platformBookingLocation, areCalendarEventsEnabled: bookingRequest.areCalendarEventsEnabled, + impersonatedByUserUuid: null, }, }); @@ -569,6 +572,7 @@ export class BookingsService_2024_08_13 { platformBookingUrl: bookingRequest.platformBookingUrl, platformBookingLocation: bookingRequest.platformBookingLocation, areCalendarEventsEnabled: bookingRequest.areCalendarEventsEnabled, + impersonatedByUserUuid: null, }, }); @@ -828,6 +832,7 @@ export class BookingsService_2024_08_13 { platformBookingUrl: bookingRequest.platformBookingUrl, platformBookingLocation: bookingRequest.platformBookingLocation, areCalendarEventsEnabled: bookingRequest.areCalendarEventsEnabled, + impersonatedByUserUuid: null, }, }); if (!booking.uid) { @@ -976,6 +981,7 @@ export class BookingsService_2024_08_13 { platformCancelUrl: bookingRequest.platformCancelUrl, platformRescheduleUrl: bookingRequest.platformRescheduleUrl, platformBookingUrl: bookingRequest.platformBookingUrl, + impersonatedByUserUuid: null, }); if (!res.onlyRemovedAttendee && res.isPlatformManagedUserBooking) { @@ -1036,6 +1042,7 @@ export class BookingsService_2024_08_13 { platformClientParams, actor: makeUserActor(userUuid), actionSource: "API_V2", + impersonatedByUserUuid: null, }); const booking = await this.bookingsRepository.getByUidWithAttendeesAndUserAndEvent(bookingUid); @@ -1242,6 +1249,7 @@ export class BookingsService_2024_08_13 { platformClientParams, actionSource: "API_V2", actor: makeUserActor(requestUser.uuid), + impersonatedByUserUuid: null, }, }); @@ -1277,6 +1285,7 @@ export class BookingsService_2024_08_13 { platformClientParams, actionSource: "API_V2", actor: makeUserActor(requestUser.uuid), + impersonatedByUserUuid: null, }, }); diff --git a/apps/web/app/api/cancel/route.ts b/apps/web/app/api/cancel/route.ts index f16ad65ae1..7a2dcfebfa 100644 --- a/apps/web/app/api/cancel/route.ts +++ b/apps/web/app/api/cancel/route.ts @@ -43,6 +43,7 @@ async function handler(req: NextRequest) { userId: session?.user?.id || -1, userUuid: session?.user?.uuid, actionSource: "WEBAPP", + impersonatedByUserUuid: session?.user?.impersonatedBy?.uuid ?? null, }); // const bookingCancelService = getBookingCancelService(); diff --git a/apps/web/app/api/link/route.ts b/apps/web/app/api/link/route.ts index 34c400fa62..2ff82732fa 100644 --- a/apps/web/app/api/link/route.ts +++ b/apps/web/app/api/link/route.ts @@ -94,6 +94,7 @@ async function handler(request: NextRequest) { : undefined, actionSource: "MAGIC_LINK", actor: makeUserActor(user.uuid), + impersonatedByUserUuid: null, }, }); } catch (e) { diff --git a/apps/web/app/api/verify-booking-token/route.ts b/apps/web/app/api/verify-booking-token/route.ts index d8a77175ca..8c6bb944c5 100644 --- a/apps/web/app/api/verify-booking-token/route.ts +++ b/apps/web/app/api/verify-booking-token/route.ts @@ -108,6 +108,7 @@ async function handleBookingAction( emailsEnabled: true, actionSource: "MAGIC_LINK", actor: makeUserActor(user.uuid), + impersonatedByUserUuid: null, }, }); } catch (e) { diff --git a/apps/web/pages/api/book/event.ts b/apps/web/pages/api/book/event.ts index 5f744934a2..20eccb3981 100644 --- a/apps/web/pages/api/book/event.ts +++ b/apps/web/pages/api/book/event.ts @@ -54,7 +54,7 @@ async function handler(req: NextApiRequest & { userId?: number; traceContext: Tr hostname: req.headers.host || "", forcedSlug: req.headers["x-cal-force-slug"] as string | undefined, traceContext: req.traceContext, - impersonatedByUserUuid: session?.user?.impersonatedBy?.uuid, + impersonatedByUserUuid: session?.user?.impersonatedBy?.uuid ?? null, }, }); diff --git a/apps/web/pages/api/book/recurring-event.ts b/apps/web/pages/api/book/recurring-event.ts index 70481c8f5a..55ab38baec 100644 --- a/apps/web/pages/api/book/recurring-event.ts +++ b/apps/web/pages/api/book/recurring-event.ts @@ -53,6 +53,7 @@ async function handler(req: NextApiRequest & RequestMeta) { platformRescheduleUrl: req.platformRescheduleUrl, platformBookingLocation: req.platformBookingLocation, noEmail: req.noEmail, + impersonatedByUserUuid: session?.user?.impersonatedBy?.uuid ?? null, }, creationSource: "WEBAPP", }); diff --git a/apps/web/test/handlers/requestReschedule.test.ts b/apps/web/test/handlers/requestReschedule.test.ts index 9b4ca962db..4ddf98cdf1 100644 --- a/apps/web/test/handlers/requestReschedule.test.ts +++ b/apps/web/test/handlers/requestReschedule.test.ts @@ -580,5 +580,6 @@ function getTrpcHandlerData({ }, input: input, source: "WEBAPP" as const, + impersonatedByUserUuid: null, }; } diff --git a/apps/web/test/lib/confirm.handler.test.ts b/apps/web/test/lib/confirm.handler.test.ts index 4e7c3fab75..fc2ea6723a 100644 --- a/apps/web/test/lib/confirm.handler.test.ts +++ b/apps/web/test/lib/confirm.handler.test.ts @@ -131,6 +131,7 @@ describe("confirmHandler", () => { emailsEnabled: true, actor: makeUserActor(ctx.user.uuid), actionSource: "WEBAPP", + impersonatedByUserUuid: null, }, }); @@ -230,6 +231,7 @@ describe("confirmHandler", () => { emailsEnabled: true, actor, actionSource: "WEBAPP", + impersonatedByUserUuid: null, }, }); @@ -357,6 +359,7 @@ describe("confirmHandler", () => { emailsEnabled: true, actor, actionSource: "WEBAPP", + impersonatedByUserUuid: null, }, }); @@ -467,6 +470,7 @@ describe("confirmHandler", () => { emailsEnabled: false, actor, actionSource: "WEBAPP", + impersonatedByUserUuid: null, }, }); @@ -592,6 +596,7 @@ describe("confirmHandler", () => { emailsEnabled: false, actor, actionSource: "WEBAPP", + impersonatedByUserUuid: null, }, }); diff --git a/packages/app-store/_utils/payments/handlePaymentSuccess.ts b/packages/app-store/_utils/payments/handlePaymentSuccess.ts index c3fc6e33c0..913e236000 100644 --- a/packages/app-store/_utils/payments/handlePaymentSuccess.ts +++ b/packages/app-store/_utils/payments/handlePaymentSuccess.ts @@ -251,6 +251,7 @@ export async function handlePaymentSuccess(params: { platformClientParams, traceContext: updatedTraceContext, actionSource: "WEBHOOK", + impersonatedByUserUuid: null, actor, }); } else { diff --git a/packages/features/booking-audit/lib/service/BookingAuditProducerService.interface.ts b/packages/features/booking-audit/lib/service/BookingAuditProducerService.interface.ts index 1a4aa0c232..de98656c1f 100644 --- a/packages/features/booking-audit/lib/service/BookingAuditProducerService.interface.ts +++ b/packages/features/booking-audit/lib/service/BookingAuditProducerService.interface.ts @@ -208,6 +208,7 @@ export interface BookingAuditProducerService { organizationId: number | null; source: ActionSource; operationId?: string | null; + context?: BookingAuditContext; isBookingAuditEnabled: boolean; }): Promise; @@ -220,6 +221,7 @@ export interface BookingAuditProducerService { organizationId: number | null; source: ActionSource; operationId?: string | null; + context?: BookingAuditContext; isBookingAuditEnabled: boolean; }): Promise; diff --git a/packages/features/booking-audit/lib/service/BookingAuditTaskerProducerService.ts b/packages/features/booking-audit/lib/service/BookingAuditTaskerProducerService.ts index 54d29b5bb8..bb98a1fbe7 100644 --- a/packages/features/booking-audit/lib/service/BookingAuditTaskerProducerService.ts +++ b/packages/features/booking-audit/lib/service/BookingAuditTaskerProducerService.ts @@ -438,6 +438,7 @@ export class BookingAuditTaskerProducerService implements BookingAuditProducerSe organizationId: number | null; source: ActionSource; operationId?: string | null; + context?: BookingAuditContext; isBookingAuditEnabled: boolean; }): Promise { await this.queueBulkTask({ @@ -455,6 +456,7 @@ export class BookingAuditTaskerProducerService implements BookingAuditProducerSe organizationId: number | null; source: ActionSource; operationId?: string | null; + context?: BookingAuditContext; isBookingAuditEnabled: boolean; }): Promise { await this.queueBulkTask({ diff --git a/packages/features/bookings/lib/dto/BookingCancel.d.ts b/packages/features/bookings/lib/dto/BookingCancel.d.ts index 5632fae3a4..313c5a25ef 100644 --- a/packages/features/bookings/lib/dto/BookingCancel.d.ts +++ b/packages/features/bookings/lib/dto/BookingCancel.d.ts @@ -21,6 +21,7 @@ export type CancelBookingMeta = { platformCancelUrl?: string; platformBookingUrl?: string; arePlatformEmailsEnabled?: boolean; + impersonatedByUserUuid: string | null; }; /** diff --git a/packages/features/bookings/lib/dto/types.d.ts b/packages/features/bookings/lib/dto/types.d.ts index d3ed978b24..68df9d4c36 100644 --- a/packages/features/bookings/lib/dto/types.d.ts +++ b/packages/features/bookings/lib/dto/types.d.ts @@ -40,7 +40,7 @@ export type CreateBookingMeta = { forcedSlug?: string; noEmail?: boolean; traceContext?: TraceContext; - impersonatedByUserUuid?: string; + impersonatedByUserUuid: string | null; } & PlatformParams; export type BookingHandlerInput = { diff --git a/packages/features/bookings/lib/handleCancelBooking.ts b/packages/features/bookings/lib/handleCancelBooking.ts index d186daf858..bd4fcec0be 100644 --- a/packages/features/bookings/lib/handleCancelBooking.ts +++ b/packages/features/bookings/lib/handleCancelBooking.ts @@ -88,6 +88,7 @@ export type CancelBookingInput = { userUuid?: string; bookingData: z.infer; actionSource: ValidActionSource; + impersonatedByUserUuid: string | null; } & PlatformParams; type Dependencies = { @@ -179,6 +180,9 @@ async function handler(input: CancelBookingInput, dependencies?: Dependencies) { } = input; const userUuid = input.userUuid ?? null; + const impersonatedByUserUuid = input.impersonatedByUserUuid ?? null; + + const auditContext = impersonatedByUserUuid ? { impersonatedBy: impersonatedByUserUuid } : undefined; const actionSource = input.actionSource; @@ -557,6 +561,7 @@ async function handler(input: CancelBookingInput, dependencies?: Dependencies) { organizationId: orgId ?? null, operationId, source: actionSource, + context: auditContext, isBookingAuditEnabled, }); } else { @@ -592,6 +597,7 @@ async function handler(input: CancelBookingInput, dependencies?: Dependencies) { new: BookingStatus.CANCELLED, }, }, + context: auditContext, isBookingAuditEnabled, }); @@ -756,6 +762,7 @@ export class BookingCancelService implements IBookingCancelService { const cancelBookingInput: CancelBookingInput = { bookingData: input.bookingData, ...(input.bookingMeta || {}), + impersonatedByUserUuid: input.bookingMeta?.impersonatedByUserUuid ?? null, actionSource: input.actionSource, }; diff --git a/packages/features/bookings/lib/handleCancelBooking/test/handleCancelBooking.test.ts b/packages/features/bookings/lib/handleCancelBooking/test/handleCancelBooking.test.ts index 16a34ae261..8bf539d366 100644 --- a/packages/features/bookings/lib/handleCancelBooking/test/handleCancelBooking.test.ts +++ b/packages/features/bookings/lib/handleCancelBooking/test/handleCancelBooking.test.ts @@ -134,6 +134,7 @@ describe("Cancel Booking", () => { cancelledBy: organizer.email, cancellationReason: "No reason", }, + impersonatedByUserUuid: null, actionSource: "WEBAPP", }); @@ -265,6 +266,7 @@ describe("Cancel Booking", () => { cancelledBy: organizer.email, cancellationReason: "No reason", }, + impersonatedByUserUuid: null, actionSource: "WEBAPP", }); @@ -413,6 +415,7 @@ describe("Cancel Booking", () => { cancelledBy: organizer.email, cancellationReason: "Testing round robin cancellation with host as attendee", }, + impersonatedByUserUuid: null, actionSource: "WEBAPP", }); @@ -566,6 +569,7 @@ describe("Cancel Booking", () => { cancelledBy: primaryHost.email, cancellationReason: "Testing EMAIL_HOST workflow sends to secondary host in round robin", }, + impersonatedByUserUuid: null, actionSource: "WEBAPP", }); @@ -644,6 +648,7 @@ describe("Cancel Booking", () => { cancelledBy: organizer.email, cancellationReason: "Testing past booking cancellation", }, + impersonatedByUserUuid: null, actionSource: "WEBAPP", }) ).rejects.toThrow("Cannot cancel a booking that has already ended"); @@ -713,6 +718,7 @@ describe("Cancel Booking", () => { uid: uidOfBookingToBeCancelled, cancelledBy: organizer.email, }, + impersonatedByUserUuid: null, actionSource: "WEBAPP", }) ).rejects.toThrow("Cancellation reason is required"); @@ -825,6 +831,7 @@ describe("Cancel Booking", () => { cancelledBy: organizer.email, cancellationReason: "No reason", }, + impersonatedByUserUuid: null, actionSource: "WEBAPP", }); @@ -952,6 +959,7 @@ describe("Cancel Booking", () => { cancelledBy: organizer.email, cancellationReason: "No reason", }, + impersonatedByUserUuid: null, actionSource: "WEBAPP", }); @@ -1070,6 +1078,7 @@ describe("Cancel Booking", () => { cancellationReason: "Attendee cancelled within time threshold", }, userId: 999, + impersonatedByUserUuid: null, actionSource: "WEBAPP", }); @@ -1179,6 +1188,7 @@ describe("Cancel Booking", () => { cancelledBy: organizer.email, cancellationReason: "Organization booking cancellation test", }, + impersonatedByUserUuid: null, actionSource: "WEBAPP", }); @@ -1302,6 +1312,7 @@ describe("Cancel Booking", () => { cancellationReason: "Cancelling seated event", }, userId: organizer.id, + impersonatedByUserUuid: null, actionSource: "WEBAPP", }); @@ -1424,6 +1435,7 @@ describe("Cancel Booking", () => { allRemainingBookings: true, }, userId: organizer.id, + impersonatedByUserUuid: null, actionSource: "WEBAPP", }); @@ -1546,6 +1558,7 @@ describe("Cancel Booking", () => { cancelSubsequentBookings: true, }, userId: organizer.id, + impersonatedByUserUuid: null, actionSource: "WEBAPP", }); @@ -1660,6 +1673,7 @@ describe("Cancel Booking", () => { cancellationReason: "Testing booking reference cleanup", }, userId: organizer.id, + impersonatedByUserUuid: null, actionSource: "WEBAPP", }); diff --git a/packages/features/bookings/lib/handleConfirmation.ts b/packages/features/bookings/lib/handleConfirmation.ts index 8712660321..46d88d3896 100644 --- a/packages/features/bookings/lib/handleConfirmation.ts +++ b/packages/features/bookings/lib/handleConfirmation.ts @@ -45,6 +45,7 @@ async function fireBookingAcceptedEvent({ acceptedBookings, isBookingAuditEnabled, tracingLogger, + impersonatedByUserUuid, }: { actor: Actor; organizationId: number | null; @@ -55,9 +56,11 @@ async function fireBookingAcceptedEvent({ }[]; isBookingAuditEnabled: boolean; tracingLogger: ISimpleLogger; + impersonatedByUserUuid: string | null; }) { try { const bookingEventHandlerService = getBookingEventHandlerService(); + const context = impersonatedByUserUuid ? { impersonatedBy: impersonatedByUserUuid } : undefined; if (acceptedBookings.length > 1) { const operationId = uuidv4(); await bookingEventHandlerService.onBulkBookingsAccepted({ @@ -71,6 +74,7 @@ async function fireBookingAcceptedEvent({ organizationId, operationId, source: actionSource, + context, isBookingAuditEnabled, }); } else if (acceptedBookings.length === 1) { @@ -83,6 +87,7 @@ async function fireBookingAcceptedEvent({ status: { old: acceptedBooking.oldStatus, new: BookingStatus.ACCEPTED }, }, source: actionSource, + context, isBookingAuditEnabled, }); } @@ -135,6 +140,7 @@ export async function handleConfirmation(args: { traceContext: TraceContext; actionSource: ValidActionSource; actor: Actor; + impersonatedByUserUuid: string | null; }) { const { user, @@ -149,6 +155,7 @@ export async function handleConfirmation(args: { traceContext, actionSource, actor, + impersonatedByUserUuid, } = args; const eventType = booking.eventType; const eventTypeMetadata = EventTypeMetaDataSchema.parse(eventType?.metadata || {}); @@ -428,6 +435,7 @@ export async function handleConfirmation(args: { actionSource, isBookingAuditEnabled, tracingLogger, + impersonatedByUserUuid, }); //Workflows - set reminders for confirmed events diff --git a/packages/features/bookings/lib/handleSeats/handleSeats.ts b/packages/features/bookings/lib/handleSeats/handleSeats.ts index 0d02d9d69b..2db2319afd 100644 --- a/packages/features/bookings/lib/handleSeats/handleSeats.ts +++ b/packages/features/bookings/lib/handleSeats/handleSeats.ts @@ -29,6 +29,7 @@ const fireBookingEvents = async ({ rescheduledBy, actionSource, actorUserUuid, + impersonatedByUserUuid, isBookingAuditEnabled, deps, }: { @@ -42,6 +43,7 @@ const fireBookingEvents = async ({ rescheduledBy: string | undefined; actionSource: ActionSource; actorUserUuid: string | null; + impersonatedByUserUuid: string | null; isBookingAuditEnabled: boolean; deps: { bookingEventHandler: BookingEventHandlerService; @@ -75,6 +77,8 @@ const fireBookingEvents = async ({ return; } + const auditContext = impersonatedByUserUuid ? { impersonatedBy: impersonatedByUserUuid } : undefined; + if (rescheduleUid && originalRescheduledBooking) { const movedToDifferentBooking = newBooking.uid && newBooking.uid !== previousSeatedBooking.uid; const newBookingStartTimeMs = @@ -107,6 +111,7 @@ const fireBookingEvents = async ({ }, }, source: actionSource, + context: auditContext, isBookingAuditEnabled, }); } else { @@ -122,6 +127,7 @@ const fireBookingEvents = async ({ endTime: previousSeatedBooking.endTime.getTime(), }, source: actionSource, + context: auditContext, isBookingAuditEnabled, }); } @@ -159,6 +165,7 @@ const handleSeats = async ( fullName, traceContext, actionSource, + impersonatedByUserUuid, deps: { bookingEventHandler }, } = newSeatedBookingObject; // TODO: We could allow doing more things to support good dry run for seats @@ -244,6 +251,7 @@ const handleSeats = async ( rescheduledBy, actionSource, actorUserUuid: reqUserUuid ?? null, + impersonatedByUserUuid, isBookingAuditEnabled, deps: { bookingEventHandler, logger: loggerWithEventDetails }, }); diff --git a/packages/features/bookings/lib/handleSeats/test/handleSeats.test.ts b/packages/features/bookings/lib/handleSeats/test/handleSeats.test.ts index 961cbfce43..c00b555570 100644 --- a/packages/features/bookings/lib/handleSeats/test/handleSeats.test.ts +++ b/packages/features/bookings/lib/handleSeats/test/handleSeats.test.ts @@ -1976,6 +1976,7 @@ describe("handleSeats", () => { cancellationReason: "test cancellation reason", }, userId: organizer.id, + impersonatedByUserUuid: null, actionSource: "WEBAPP", }); @@ -2116,6 +2117,7 @@ describe("handleSeats", () => { cancellationReason: "test cancellation reason", }, userId: organizer.id, + impersonatedByUserUuid: null, actionSource: "WEBAPP", }); @@ -2969,6 +2971,7 @@ describe("handleSeats", () => { cancellationReason: "test cancellation reason", }, userId: organizer.id, + impersonatedByUserUuid: null, actionSource: "WEBAPP", }); diff --git a/packages/features/bookings/lib/handleSeats/types.d.ts b/packages/features/bookings/lib/handleSeats/types.d.ts index ecf319660f..8c13bb5fc5 100644 --- a/packages/features/bookings/lib/handleSeats/types.d.ts +++ b/packages/features/bookings/lib/handleSeats/types.d.ts @@ -65,6 +65,7 @@ export type NewSeatedBookingObject = { organizationId?: number | null; actionSource: ActionSource; traceContext: TraceContext; + impersonatedByUserUuid: string | null; deps: { bookingEventHandler: BookingEventHandlerService; } diff --git a/packages/features/bookings/lib/onBookingEvents/BookingEventHandlerService.ts b/packages/features/bookings/lib/onBookingEvents/BookingEventHandlerService.ts index 10d54a7843..37744c15a9 100644 --- a/packages/features/bookings/lib/onBookingEvents/BookingEventHandlerService.ts +++ b/packages/features/bookings/lib/onBookingEvents/BookingEventHandlerService.ts @@ -466,9 +466,10 @@ export class BookingEventHandlerService { organizationId: number | null; operationId?: string | null; source: ActionSource; + context?: BookingAuditContext; isBookingAuditEnabled: boolean; }) { - const { bookings, actor, organizationId, operationId, source, isBookingAuditEnabled } = params; + const { bookings, actor, organizationId, operationId, source, context, isBookingAuditEnabled } = params; await this.bookingAuditProducerService.queueBulkCreatedAudit({ bookings: bookings.map((booking) => ({ bookingUid: booking.bookingUid, @@ -478,6 +479,7 @@ export class BookingEventHandlerService { organizationId, source, operationId, + context, isBookingAuditEnabled, }); } @@ -491,9 +493,10 @@ export class BookingEventHandlerService { organizationId: number | null; operationId?: string | null; source: ActionSource; + context?: BookingAuditContext; isBookingAuditEnabled: boolean; }) { - const { bookings, actor, organizationId, operationId, source, isBookingAuditEnabled } = params; + const { bookings, actor, organizationId, operationId, source, context, isBookingAuditEnabled } = params; await this.bookingAuditProducerService.queueBulkRescheduledAudit({ bookings: bookings.map((booking) => ({ bookingUid: booking.bookingUid, @@ -503,6 +506,7 @@ export class BookingEventHandlerService { organizationId, source, operationId, + context, isBookingAuditEnabled, }); } diff --git a/packages/features/bookings/lib/service/RecurringBookingService.ts b/packages/features/bookings/lib/service/RecurringBookingService.ts index 3fcf92aef8..610839949c 100644 --- a/packages/features/bookings/lib/service/RecurringBookingService.ts +++ b/packages/features/bookings/lib/service/RecurringBookingService.ts @@ -77,6 +77,7 @@ export const handleNewRecurringBooking = async function ( bookingMeta: { hostname: input.hostname || "", forcedSlug: input.forcedSlug as string | undefined, + impersonatedByUserUuid: input.impersonatedByUserUuid ?? null, ...handleBookingMeta, }, }); @@ -120,11 +121,12 @@ export const handleNewRecurringBooking = async function ( bookingMeta: { hostname: input.hostname || "", forcedSlug: input.forcedSlug as string | undefined, + impersonatedByUserUuid: input.impersonatedByUserUuid ?? null, ...handleBookingMeta, }, }); - const eachRecurringBooking = await promiseEachRecurringBooking; + const eachRecurringBooking= await promiseEachRecurringBooking; createdBookings.push(eachRecurringBooking); @@ -148,6 +150,7 @@ export const handleNewRecurringBooking = async function ( actorUserUuid: input.userUuid ?? null, rescheduledBy: firstBooking.rescheduledBy ?? null, creationSource, + impersonatedByUserUuid: input.impersonatedByUserUuid ?? null, }); } @@ -164,7 +167,7 @@ export interface IRecurringBookingServiceDependencies { * Recurring Booking Service takes care of creating/rescheduling recurring bookings. */ export class RecurringBookingService implements IBookingService { - constructor(private readonly deps: IRecurringBookingServiceDependencies) {} + constructor(private readonly deps: IRecurringBookingServiceDependencies) { } async fireBookingEvents({ createdBookings, @@ -173,6 +176,7 @@ export class RecurringBookingService implements IBookingService { actorUserUuid, rescheduledBy, creationSource, + impersonatedByUserUuid, }: { createdBookings: BookingResponse[]; eventTypeId: number; @@ -180,6 +184,7 @@ export class RecurringBookingService implements IBookingService { actorUserUuid: string | null; rescheduledBy: string | null; creationSource: CreationSource | undefined; + impersonatedByUserUuid: string | null; }) { try { type ValidBooking = BookingResponse & { @@ -245,6 +250,8 @@ export class RecurringBookingService implements IBookingService { ); }; + const auditContext = impersonatedByUserUuid ? { impersonatedBy: impersonatedByUserUuid } : undefined; + if (isReschedule) { const bulkRescheduledBookings = createdBookings.filter(isValidRescheduledBooking).map((booking) => ({ bookingUid: booking.previousBooking.uid, @@ -261,11 +268,11 @@ export class RecurringBookingService implements IBookingService { organizationId: eventOrganizationId, operationId, source: actionSource, + context: auditContext, isBookingAuditEnabled, }); } } else { - // For new bookings const bulkCreatedBookings = createdBookings.filter(isValidBooking).map((booking) => ({ bookingUid: booking.uid, auditData: buildBookingCreatedAuditData({ booking, attendeeSeatId: null }), @@ -278,6 +285,7 @@ export class RecurringBookingService implements IBookingService { organizationId: eventOrganizationId, operationId, source: actionSource, + context: auditContext, isBookingAuditEnabled, }); } @@ -292,7 +300,11 @@ export class RecurringBookingService implements IBookingService { bookingMeta?: CreateBookingMeta; creationSource: CreationSource; }): Promise { - const handlerInput = { bookingData: input.bookingData, ...(input.bookingMeta || {}) }; + const handlerInput = { + bookingData: input.bookingData, + ...(input.bookingMeta || {}), + impersonatedByUserUuid: input.bookingMeta?.impersonatedByUserUuid ?? null, + }; return handleNewRecurringBooking.bind(this)({ input: handlerInput, deps: this.deps, @@ -305,7 +317,11 @@ export class RecurringBookingService implements IBookingService { bookingMeta?: CreateBookingMeta; creationSource: CreationSource; }): Promise { - const handlerInput = { bookingData: input.bookingData, ...(input.bookingMeta || {}) }; + const handlerInput = { + bookingData: input.bookingData, + ...(input.bookingMeta || {}), + impersonatedByUserUuid: input.bookingMeta?.impersonatedByUserUuid ?? null, + }; return handleNewRecurringBooking.bind(this)({ input: handlerInput, deps: this.deps, diff --git a/packages/features/bookings/lib/service/RegularBookingService.ts b/packages/features/bookings/lib/service/RegularBookingService.ts index 9b95eec6bc..33a66db6f3 100644 --- a/packages/features/bookings/lib/service/RegularBookingService.ts +++ b/packages/features/bookings/lib/service/RegularBookingService.ts @@ -603,6 +603,7 @@ async function handler( skipEventLimitsCheck = false, skipCalendarSyncTaskCreation = false, traceContext: passedTraceContext, + impersonatedByUserUuid, } = input; let bookingEmailsAndSmsTaskerAction: BookingActionType = BookingActionMap.requested; @@ -1832,6 +1833,7 @@ async function handler( organizationId: eventOrganizationId, actionSource, traceContext, + impersonatedByUserUuid: impersonatedByUserUuid ?? null, deps, }, deps.featuresRepository @@ -2518,10 +2520,11 @@ async function handler( isRecurringBooking: !!input.bookingData.allRecurringDates, attendeeSeatId: evt.attendeeSeatId ?? null, tracingLogger, + impersonatedByUserUuid: impersonatedByUserUuid ?? null, isBookingAuditEnabled, }); - const webhookLocation = metadata?.videoCallUrl || evt.location; + const webhookLocation= metadata?.videoCallUrl || evt.location; const { assignmentReason: _emailAssignmentReason, ...evtWithoutAssignmentReason } = evt; const webhookData: EventPayloadType = { @@ -2961,6 +2964,7 @@ export class RegularBookingService implements IBookingService { isRecurringBooking, attendeeSeatId, tracingLogger, + impersonatedByUserUuid, isBookingAuditEnabled, }: { booking: { @@ -2987,6 +2991,7 @@ export class RegularBookingService implements IBookingService { isRecurringBooking: boolean; tracingLogger: ReturnType; attendeeSeatId: string | null; + impersonatedByUserUuid: string | null; isBookingAuditEnabled: boolean; }) { try { @@ -3020,6 +3025,8 @@ export class RegularBookingService implements IBookingService { logger: tracingLogger, }); + const auditContext = impersonatedByUserUuid ? { impersonatedBy: impersonatedByUserUuid } : undefined; + // For recurring bookings we fire the events in the RecurringBookingService if (!isRecurringBooking) { if (originalRescheduledBooking) { @@ -3040,6 +3047,7 @@ export class RegularBookingService implements IBookingService { }), source: actionSource, operationId: null, + context: auditContext, isBookingAuditEnabled, }); } else { @@ -3049,6 +3057,7 @@ export class RegularBookingService implements IBookingService { auditData: buildBookingCreatedAuditData({ booking, attendeeSeatId }), source: actionSource, operationId: null, + context: auditContext, isBookingAuditEnabled, }); } @@ -3059,11 +3068,25 @@ export class RegularBookingService implements IBookingService { } async createBooking(input: { bookingData: CreateRegularBookingData; bookingMeta?: CreateBookingMeta }) { - return handler.bind(this)({ bookingData: input.bookingData, ...input.bookingMeta }, this.deps); + return handler.bind(this)( + { + bookingData: input.bookingData, + ...input.bookingMeta, + impersonatedByUserUuid: input.bookingMeta?.impersonatedByUserUuid ?? null, + }, + this.deps + ); } async rescheduleBooking(input: { bookingData: CreateRegularBookingData; bookingMeta?: CreateBookingMeta }) { - return handler.bind(this)({ bookingData: input.bookingData, ...input.bookingMeta }, this.deps); + return handler.bind(this)( + { + bookingData: input.bookingData, + ...input.bookingMeta, + impersonatedByUserUuid: input.bookingMeta?.impersonatedByUserUuid ?? null, + }, + this.deps + ); } /** @@ -3074,11 +3097,11 @@ export class RegularBookingService implements IBookingService { bookingMeta?: CreateBookingMeta; bookingDataSchemaGetter: BookingDataSchemaGetter; }) { - const bookingMeta = input.bookingMeta ?? {}; return handler.bind(this)( { bookingData: input.bookingData, - ...bookingMeta, + ...input.bookingMeta, + impersonatedByUserUuid: input.bookingMeta?.impersonatedByUserUuid ?? null, }, this.deps, input.bookingDataSchemaGetter diff --git a/packages/features/calendar-subscription/lib/sync/CalendarSyncService.ts b/packages/features/calendar-subscription/lib/sync/CalendarSyncService.ts index 73139a53d7..d5ddb5f972 100644 --- a/packages/features/calendar-subscription/lib/sync/CalendarSyncService.ts +++ b/packages/features/calendar-subscription/lib/sync/CalendarSyncService.ts @@ -119,6 +119,7 @@ export class CalendarSyncService { await handleCancelBooking({ userId: booking.userId, actionSource: "SYSTEM", + impersonatedByUserUuid: null, bookingData: { uid: booking.uid, cancellationReason: "Cancelled on user's calendar", @@ -210,6 +211,7 @@ export class CalendarSyncService { skipCalendarSyncTaskCreation: true, skipAvailabilityCheck: true, skipEventLimitsCheck: true, + impersonatedByUserUuid: null, }, }); log.info("Successfully rescheduled booking from calendar sync", { bookingUid }); diff --git a/packages/features/calendar-subscription/lib/sync/__tests__/CalendarSyncService.integration.test.ts b/packages/features/calendar-subscription/lib/sync/__tests__/CalendarSyncService.integration.test.ts index ccf2b6d03b..f11633f9a7 100644 --- a/packages/features/calendar-subscription/lib/sync/__tests__/CalendarSyncService.integration.test.ts +++ b/packages/features/calendar-subscription/lib/sync/__tests__/CalendarSyncService.integration.test.ts @@ -921,6 +921,7 @@ describe("CalendarSyncService - error isolation", () => { expect(mockHandleCancelBooking).toHaveBeenCalledWith({ userId: 99, actionSource: "SYSTEM", + impersonatedByUserUuid: null, bookingData: { uid: "my-booking-uid", cancellationReason: "Cancelled on user's calendar", @@ -947,6 +948,7 @@ describe("CalendarSyncService - error isolation", () => { skipCalendarSyncTaskCreation: true, skipAvailabilityCheck: true, skipEventLimitsCheck: true, + impersonatedByUserUuid: null, }, }) ); diff --git a/packages/features/calendar-subscription/lib/sync/__tests__/CalendarSyncService.test.ts b/packages/features/calendar-subscription/lib/sync/__tests__/CalendarSyncService.test.ts index ea3b46cbee..da14565605 100644 --- a/packages/features/calendar-subscription/lib/sync/__tests__/CalendarSyncService.test.ts +++ b/packages/features/calendar-subscription/lib/sync/__tests__/CalendarSyncService.test.ts @@ -232,6 +232,7 @@ describe("CalendarSyncService", () => { expect(mockHandleCancelBooking).toHaveBeenCalledWith({ userId: mockBooking.userId, actionSource: "SYSTEM", + impersonatedByUserUuid: null, bookingData: { uid: mockBooking.uid, cancellationReason: "Cancelled on user's calendar", @@ -345,6 +346,7 @@ describe("CalendarSyncService", () => { skipCalendarSyncTaskCreation: true, skipAvailabilityCheck: true, skipEventLimitsCheck: true, + impersonatedByUserUuid: null, }, }); const lastCall = mockCreateBooking.mock.calls.at(-1)!; @@ -437,6 +439,7 @@ describe("CalendarSyncService", () => { skipCalendarSyncTaskCreation: true, skipAvailabilityCheck: true, skipEventLimitsCheck: true, + impersonatedByUserUuid: null, }, }); const baseCall = mockCreateBooking.mock.calls.at(-1)!; diff --git a/packages/features/ee/payments/api/webhook.ts b/packages/features/ee/payments/api/webhook.ts index 2c3979b53c..c3c8af5b67 100644 --- a/packages/features/ee/payments/api/webhook.ts +++ b/packages/features/ee/payments/api/webhook.ts @@ -146,6 +146,7 @@ const handleSetupSuccess = async (event: Stripe.Event, traceContext: TraceContex traceContext: updatedTraceContext, actionSource: "WEBHOOK", actor, + impersonatedByUserUuid: null, }); } else if (areEmailsEnabled) { await sendOrganizerRequestEmail({ ...evt }, eventType.metadata); diff --git a/packages/features/handleMarkNoShow.ts b/packages/features/handleMarkNoShow.ts index 1ca9bb67d6..017a74f36b 100644 --- a/packages/features/handleMarkNoShow.ts +++ b/packages/features/handleMarkNoShow.ts @@ -81,6 +81,7 @@ type HandleMarkHostNoShowArgs = { actionSource: ValidActionSource; locale?: string; platformClientParams?: PlatformClientParams; + impersonatedByUserUuid: string | null; }; type HandleMarkAttendeesAndHostNoShowArgs = { @@ -92,6 +93,7 @@ type HandleMarkAttendeesAndHostNoShowArgs = { actionSource: ValidActionSource; locale?: string; platformClientParams?: PlatformClientParams; + impersonatedByUserUuid: string | null; }; type HandleMarkNoShowArgs = { @@ -104,6 +106,7 @@ type HandleMarkNoShowArgs = { locale?: string; platformClientParams?: PlatformClientParams; actor: Actor; + impersonatedByUserUuid: string | null; }; const buildResultPayload = async ({ @@ -201,6 +204,7 @@ async function fireNoShowUpdated({ actor, orgId, actionSource, + impersonatedByUserUuid, }: { updatedNoShowHost?: boolean; hostUserUuid?: string; @@ -213,6 +217,7 @@ async function fireNoShowUpdated({ actor: Actor; orgId: number | null; actionSource: ValidActionSource; + impersonatedByUserUuid: string | null; }): Promise { const auditData: { host?: { userUuid: string; noShow: { old: boolean | null; new: boolean } }; @@ -248,12 +253,14 @@ async function fireNoShowUpdated({ const isSomethingChanged = auditData.host || (auditData.attendeesNoShow && auditData.attendeesNoShow.length > 0); if (isSomethingChanged) { + const auditContext = impersonatedByUserUuid ? { impersonatedBy: impersonatedByUserUuid } : undefined; await bookingEventHandlerService.onNoShowUpdated({ bookingUid: booking.uid, actor, organizationId: orgId ?? null, source: actionSource, auditData, + context: auditContext, isBookingAuditEnabled, }); } @@ -268,6 +275,7 @@ const handleMarkNoShow = async ({ locale, platformClientParams, actionSource, + impersonatedByUserUuid, }: HandleMarkNoShowArgs): Promise => { const responsePayload = new ResponsePayload(); const t = await getTranslation(locale ?? "en", "common"); @@ -457,6 +465,7 @@ const handleMarkNoShow = async ({ actor, orgId: orgId ?? null, actionSource, + impersonatedByUserUuid, }); return responsePayload.getPayload(); @@ -539,6 +548,7 @@ export const handleMarkHostNoShow = async ({ actionSource, locale, platformClientParams, + impersonatedByUserUuid, }: HandleMarkHostNoShowArgs): Promise => { const actorEmail = buildActorEmail({ identifier: getUniqueIdentifier({ prefix: "attendee" }), @@ -555,6 +565,7 @@ export const handleMarkHostNoShow = async ({ actionSource, locale, platformClientParams, + impersonatedByUserUuid, }); }; @@ -572,6 +583,7 @@ export const handleMarkAttendeesAndHostNoShow = async ({ actionSource, locale, platformClientParams, + impersonatedByUserUuid, }: HandleMarkAttendeesAndHostNoShowArgs): Promise => { const actor = makeUserActor(userUuid); @@ -584,6 +596,7 @@ export const handleMarkAttendeesAndHostNoShow = async ({ actionSource, locale, platformClientParams, + impersonatedByUserUuid, }); }; diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index 230c00df8a..86a550d252 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -1,5 +1,5 @@ // This is your Prisma Schema file -// learn more about it in the docs: https://pris.ly/d/prisma-schema +// Learn more about it in the docs: https://pris.ly/d/prisma-schema datasource db { provider = "postgresql" diff --git a/packages/trpc/server/routers/loggedInViewer/markNoShow.handler.ts b/packages/trpc/server/routers/loggedInViewer/markNoShow.handler.ts index df8b941972..823f4f536a 100644 --- a/packages/trpc/server/routers/loggedInViewer/markNoShow.handler.ts +++ b/packages/trpc/server/routers/loggedInViewer/markNoShow.handler.ts @@ -1,3 +1,5 @@ +import type { Session } from "next-auth"; + import { handleMarkAttendeesAndHostNoShow } from "@calcom/features/handleMarkNoShow"; import type { TrpcSessionUser } from "@calcom/trpc/server/types"; @@ -7,11 +9,13 @@ type NoShowOptions = { input: TNoShowInputSchema; ctx: { user: NonNullable; + session: Session; }; }; export const markNoShow = async ({ ctx, input }: NoShowOptions) => { const { bookingUid, attendees, noShowHost } = input; + const impersonatedByUserUuid = ctx.session.user?.impersonatedBy?.uuid ?? null; return handleMarkAttendeesAndHostNoShow({ bookingUid, @@ -21,5 +25,6 @@ export const markNoShow = async ({ ctx, input }: NoShowOptions) => { userUuid: ctx.user.uuid, locale: ctx.user.locale, actionSource: "WEBAPP", + impersonatedByUserUuid, }); }; diff --git a/packages/trpc/server/routers/publicViewer/markHostAsNoShow.handler.ts b/packages/trpc/server/routers/publicViewer/markHostAsNoShow.handler.ts index 7b58db949d..758c4ef19d 100644 --- a/packages/trpc/server/routers/publicViewer/markHostAsNoShow.handler.ts +++ b/packages/trpc/server/routers/publicViewer/markHostAsNoShow.handler.ts @@ -15,6 +15,7 @@ export const noShowHandler = async ({ input }: NoShowOptions) => { bookingUid, noShowHost, actionSource: "WEBAPP", + impersonatedByUserUuid: null, }); }; diff --git a/packages/trpc/server/routers/viewer/bookings/_router.tsx b/packages/trpc/server/routers/viewer/bookings/_router.tsx index 960d181543..de8e5f7c6f 100644 --- a/packages/trpc/server/routers/viewer/bookings/_router.tsx +++ b/packages/trpc/server/routers/viewer/bookings/_router.tsx @@ -1,5 +1,5 @@ import { makeUserActor } from "@calcom/features/booking-audit/lib/makeActor"; -import type { ActionSource } from "@calcom/features/booking-audit/lib/types/actionSource"; + import { MembershipRole } from "@calcom/prisma/enums"; import authedProcedure from "../../../procedures/authedProcedure"; import { createTeamPbacProcedure } from "../../../procedures/pbacProcedures"; @@ -39,30 +39,33 @@ export const bookingsRouter = router({ ctx, input, source: "WEBAPP", + impersonatedByUserUuid: ctx.session?.user?.impersonatedBy?.uuid ?? null, }); }), - editLocation: bookingsProcedure.input(ZEditLocationInputSchema).mutation(async ({ input, ctx }) => { + editLocation:bookingsProcedure.input(ZEditLocationInputSchema).mutation(async ({ input, ctx }) => { const { editLocationHandler } = await import("./editLocation.handler"); return editLocationHandler({ ctx, input, actionSource: "WEBAPP", + impersonatedByUserUuid: ctx.session?.user?.impersonatedBy?.uuid ?? null, }); }), - addGuests: authedProcedure.input(ZAddGuestsInputSchema).mutation(async ({ input, ctx }) => { + addGuests:authedProcedure.input(ZAddGuestsInputSchema).mutation(async ({ input, ctx }) => { const { addGuestsHandler } = await import("./addGuests.handler"); return addGuestsHandler({ ctx, input, actionSource: "WEBAPP", + impersonatedByUserUuid: ctx.session?.user?.impersonatedBy?.uuid ?? null, }); }), - confirm: authedProcedure.input(ZConfirmInputSchema).mutation(async ({ input, ctx }) => { + confirm:authedProcedure.input(ZConfirmInputSchema).mutation(async ({ input, ctx }) => { const { confirmHandler } = await import("./confirm.handler"); return confirmHandler({ @@ -71,6 +74,7 @@ export const bookingsRouter = router({ ...input, actor: makeUserActor(ctx.user.uuid), actionSource: "WEBAPP", + impersonatedByUserUuid: ctx.session?.user?.impersonatedBy?.uuid ?? null, }, }); }), @@ -121,6 +125,7 @@ export const bookingsRouter = router({ return reportBookingHandler({ ctx, input, + impersonatedByUserUuid: ctx.session?.user?.impersonatedBy?.uuid ?? null, actionSource: "WEBAPP", }); }), diff --git a/packages/trpc/server/routers/viewer/bookings/addGuests.handler.ts b/packages/trpc/server/routers/viewer/bookings/addGuests.handler.ts index d99f95ef9c..a63ce9a8e9 100644 --- a/packages/trpc/server/routers/viewer/bookings/addGuests.handler.ts +++ b/packages/trpc/server/routers/viewer/bookings/addGuests.handler.ts @@ -37,6 +37,7 @@ type AddGuestsOptions = { input: TAddGuestsInputSchema; emailsEnabled?: boolean; actionSource: ValidActionSource; + impersonatedByUserUuid: string | null; }; export type Booking = NonNullable< @@ -49,6 +50,7 @@ export const addGuestsHandler = async ({ input, emailsEnabled = true, actionSource, + impersonatedByUserUuid, }: AddGuestsOptions) => { const { user } = ctx; const { bookingId, guests } = input; @@ -93,6 +95,7 @@ export const addGuestsHandler = async ({ } const bookingEventHandlerService = getBookingEventHandlerService(); + const context = impersonatedByUserUuid ? { impersonatedBy: impersonatedByUserUuid } : undefined; const featuresRepository = getFeaturesRepository(); const organizationId = user.organizationId ?? null; const isBookingAuditEnabled = organizationId @@ -107,6 +110,7 @@ export const addGuestsHandler = async ({ auditData: { added: uniqueGuestEmails, }, + context, isBookingAuditEnabled, }); diff --git a/packages/trpc/server/routers/viewer/bookings/confirm.handler.ts b/packages/trpc/server/routers/viewer/bookings/confirm.handler.ts index d253e7dcbe..8b94cf0485 100644 --- a/packages/trpc/server/routers/viewer/bookings/confirm.handler.ts +++ b/packages/trpc/server/routers/viewer/bookings/confirm.handler.ts @@ -52,7 +52,7 @@ type ConfirmOptions = { >; traceContext: TraceContext; }; - input: TConfirmInputSchema & { actionSource: ValidActionSource; actor: Actor }; + input: TConfirmInputSchema & { actionSource: ValidActionSource; actor: Actor; impersonatedByUserUuid: string | null }; }; async function fireRejectionEvent({ @@ -63,6 +63,7 @@ async function fireRejectionEvent({ rejectionReason, isBookingAuditEnabled, tracingLogger, + impersonatedByUserUuid, }: { actor: Actor; organizationId: number | null; @@ -74,11 +75,13 @@ async function fireRejectionEvent({ }[]; isBookingAuditEnabled: boolean; tracingLogger: ISimpleLogger; + impersonatedByUserUuid: string | null; }): Promise { try { const bookingEventHandlerService = getBookingEventHandlerService(); if (rejectedBookings.length > 1) { const operationId = uuidv4(); + const context = impersonatedByUserUuid ? { impersonatedBy: impersonatedByUserUuid } : undefined; await bookingEventHandlerService.onBulkBookingsRejected({ bookings: rejectedBookings.map((booking) => ({ bookingUid: booking.uid, @@ -91,10 +94,12 @@ async function fireRejectionEvent({ organizationId, operationId, source: actionSource, + context, isBookingAuditEnabled, }); } else if (rejectedBookings.length === 1) { const booking = rejectedBookings[0]; + const context = impersonatedByUserUuid ? { impersonatedBy: impersonatedByUserUuid } : undefined; await bookingEventHandlerService.onBookingRejected({ bookingUid: booking.uid, actor, @@ -104,6 +109,7 @@ async function fireRejectionEvent({ status: { old: booking.oldStatus, new: BookingStatus.REJECTED }, }, source: actionSource, + context, isBookingAuditEnabled, }); } @@ -125,6 +131,7 @@ export const confirmHandler = async ({ ctx, input }: ConfirmOptions) => { platformClientParams, actionSource, actor, + impersonatedByUserUuid, } = input; const booking = await prisma.booking.findUniqueOrThrow({ @@ -450,6 +457,7 @@ export const confirmHandler = async ({ ctx, input }: ConfirmOptions) => { traceContext, actionSource, actor, + impersonatedByUserUuid, }); } else { evt.rejectionReason = rejectionReason; @@ -542,6 +550,7 @@ export const confirmHandler = async ({ ctx, input }: ConfirmOptions) => { rejectedBookings, isBookingAuditEnabled, tracingLogger: log, + impersonatedByUserUuid, }); // send BOOKING_REJECTED webhooks diff --git a/packages/trpc/server/routers/viewer/bookings/editLocation.handler.ts b/packages/trpc/server/routers/viewer/bookings/editLocation.handler.ts index 54cc38eb7a..091d11257e 100644 --- a/packages/trpc/server/routers/viewer/bookings/editLocation.handler.ts +++ b/packages/trpc/server/routers/viewer/bookings/editLocation.handler.ts @@ -36,6 +36,7 @@ type EditLocationOptions = { } & BookingsProcedureContext; input: TEditLocationInputSchema; actionSource: ValidActionSource; + impersonatedByUserUuid: string | null; }; type UserMetadata = z.infer; @@ -262,7 +263,7 @@ export function getLocationForOrganizerDefaultConferencingAppInEvtFormat({ return appLink; } -export async function editLocationHandler({ ctx, input, actionSource }: EditLocationOptions) { +export async function editLocationHandler({ ctx, input, actionSource, impersonatedByUserUuid }: EditLocationOptions) { const { newLocation, credentialId: conferenceCredentialId } = input; const { booking, user: loggedInUser } = ctx; @@ -318,6 +319,7 @@ export async function editLocationHandler({ ctx, input, actionSource }: EditLoca } const bookingEventHandlerService = getBookingEventHandlerService(); + const context = impersonatedByUserUuid ? { impersonatedBy: impersonatedByUserUuid } : undefined; const featuresRepository = getFeaturesRepository(); const isBookingAuditEnabled = organizationId ? await featuresRepository.checkIfTeamHasFeature(organizationId, "booking-audit") @@ -334,6 +336,7 @@ export async function editLocationHandler({ ctx, input, actionSource }: EditLoca new: updatedLocation, }, }, + context, isBookingAuditEnabled, }); diff --git a/packages/trpc/server/routers/viewer/bookings/reportBooking.handler.test.ts b/packages/trpc/server/routers/viewer/bookings/reportBooking.handler.test.ts index 924a028302..8bfe861b95 100644 --- a/packages/trpc/server/routers/viewer/bookings/reportBooking.handler.test.ts +++ b/packages/trpc/server/routers/viewer/bookings/reportBooking.handler.test.ts @@ -81,6 +81,7 @@ describe("reportBookingHandler", () => { bookingUid: "test-booking-uid", reason: BookingReportReason.SPAM, }, + impersonatedByUserUuid: null, }) ).rejects.toThrow(TRPCError); @@ -91,6 +92,7 @@ describe("reportBookingHandler", () => { bookingUid: "test-booking-uid", reason: BookingReportReason.SPAM, }, + impersonatedByUserUuid: null, }) ).rejects.toMatchObject({ code: "FORBIDDEN", @@ -109,6 +111,7 @@ describe("reportBookingHandler", () => { bookingUid: "test-booking-uid", reason: BookingReportReason.SPAM, }, + impersonatedByUserUuid: null, }) ).rejects.toMatchObject({ code: "NOT_FOUND", @@ -132,6 +135,7 @@ describe("reportBookingHandler", () => { bookingUid: "test-booking-uid", reason: BookingReportReason.SPAM, }, + impersonatedByUserUuid: null, }) ).rejects.toMatchObject({ code: "BAD_REQUEST", @@ -161,6 +165,7 @@ describe("reportBookingHandler", () => { reason: BookingReportReason.SPAM, description: "This is spam", }, + impersonatedByUserUuid: null, }); expect(result.success).toBe(true); @@ -190,6 +195,7 @@ describe("reportBookingHandler", () => { bookingUid: "test-booking-uid", reason: BookingReportReason.DONT_KNOW_PERSON, }, + impersonatedByUserUuid: null, }); expect(result.message).toBe("Booking reported successfully"); @@ -226,6 +232,7 @@ describe("reportBookingHandler", () => { reason: BookingReportReason.SPAM, description: "Spam booking", }, + impersonatedByUserUuid: null, }); expect(result.message).toBe("Booking reported and cancelled successfully"); @@ -236,6 +243,7 @@ describe("reportBookingHandler", () => { skipCancellationReasonValidation: true, }, userId: mockUser.id, + impersonatedByUserUuid: null, }); }); @@ -252,6 +260,7 @@ describe("reportBookingHandler", () => { bookingUid: "test-booking-uid", reason: BookingReportReason.SPAM, }, + impersonatedByUserUuid: null, }); expect(handleCancelBooking).not.toHaveBeenCalled(); @@ -272,6 +281,7 @@ describe("reportBookingHandler", () => { bookingUid: "test-booking-uid", reason: BookingReportReason.SPAM, }, + impersonatedByUserUuid: null, }); expect(result.success).toBe(true); @@ -304,6 +314,7 @@ describe("reportBookingHandler", () => { bookingUid: "test-booking-uid", reason: BookingReportReason.SPAM, }, + impersonatedByUserUuid: null, }); expect(handleCancelBooking).toHaveBeenCalledWith({ @@ -311,6 +322,7 @@ describe("reportBookingHandler", () => { cancelSubsequentBookings: true, }), userId: mockUser.id, + impersonatedByUserUuid: null, }); }); @@ -337,6 +349,7 @@ describe("reportBookingHandler", () => { bookingUid: "test-booking-uid", reason: BookingReportReason.SPAM, }, + impersonatedByUserUuid: null, }); expect(handleCancelBooking).toHaveBeenCalledWith({ @@ -344,6 +357,7 @@ describe("reportBookingHandler", () => { seatReferenceUid: "seat-123", }), userId: mockUser.id, + impersonatedByUserUuid: null, }); }); }); diff --git a/packages/trpc/server/routers/viewer/bookings/reportBooking.handler.ts b/packages/trpc/server/routers/viewer/bookings/reportBooking.handler.ts index 401b9be929..28090dd80a 100644 --- a/packages/trpc/server/routers/viewer/bookings/reportBooking.handler.ts +++ b/packages/trpc/server/routers/viewer/bookings/reportBooking.handler.ts @@ -17,12 +17,13 @@ type ReportBookingOptions = { user: NonNullable; }; input: TReportBookingInputSchema; + impersonatedByUserUuid: string | null; actionSource: ValidActionSource; }; const log = logger.getSubLogger({ prefix: ["reportBookingHandler"] }); -export const reportBookingHandler = async ({ ctx, input, actionSource }: ReportBookingOptions) => { +export const reportBookingHandler = async ({ ctx, input, impersonatedByUserUuid, actionSource }: ReportBookingOptions) => { const { user } = ctx; const { bookingUid, reason, description } = input; @@ -92,6 +93,7 @@ export const reportBookingHandler = async ({ ctx, input, actionSource }: ReportB ...(seatReferenceUid ? { seatReferenceUid } : {}), }, userId: user.id, + impersonatedByUserUuid, actionSource, }); didCancel = true; diff --git a/packages/trpc/server/routers/viewer/bookings/requestReschedule.handler.ts b/packages/trpc/server/routers/viewer/bookings/requestReschedule.handler.ts index 9c5ca97107..02d677b354 100644 --- a/packages/trpc/server/routers/viewer/bookings/requestReschedule.handler.ts +++ b/packages/trpc/server/routers/viewer/bookings/requestReschedule.handler.ts @@ -48,9 +48,10 @@ type RequestRescheduleOptions = { }; input: TRequestRescheduleInputSchema; source: ValidActionSource; + impersonatedByUserUuid: string | null; }; const log = logger.getSubLogger({ prefix: ["requestRescheduleHandler"] }); -export const requestRescheduleHandler = async ({ ctx, input, source }: RequestRescheduleOptions) => { +export const requestRescheduleHandler = async ({ ctx, input, source, impersonatedByUserUuid }: RequestRescheduleOptions) => { const { user } = ctx; const { bookingUid, rescheduleReason: cancellationReason } = input; log.debug("Started", safeStringify({ bookingUid })); @@ -316,6 +317,7 @@ export const requestRescheduleHandler = async ({ ctx, input, source }: RequestRe await Promise.all(promises); const bookingEventHandlerService = getBookingEventHandlerService(); + const context = impersonatedByUserUuid ? { impersonatedBy: impersonatedByUserUuid } : undefined; const featuresRepository = getFeaturesRepository(); const isBookingAuditEnabled = orgId ? await featuresRepository.checkIfTeamHasFeature(orgId, "booking-audit") @@ -330,6 +332,7 @@ export const requestRescheduleHandler = async ({ ctx, input, source }: RequestRe rescheduleReason: cancellationReason ?? null, rescheduledRequestedBy: user.email, }, + context, isBookingAuditEnabled, }); };