From 7ec8601bd6827c0462fd780a4693f74268ef3500 Mon Sep 17 00:00:00 2001 From: Hariom Balhara Date: Tue, 20 Jan 2026 18:40:48 +0530 Subject: [PATCH] chore: Integrate edit location booking audit (#26569) ## What does this PR do? Integrates booking audit logging for the edit location functionality, following the pattern established in PR #26046 (booking creation/rescheduling audit). This PR adds audit logging when a booking's location is changed through: 1. **Web app** (tRPC handler): `packages/trpc/server/routers/viewer/bookings/editLocation.handler.ts` 2. **API v2**: `apps/api/v2/src/ee/bookings/2024-08-13/services/booking-location.service.ts` ### Changes: - Added `actionSource` as a **required** parameter to `editLocationHandler` (no fallback) - Added optional `userUuid` parameter (defaults to logged-in user's uuid) - Added `ValidActionSource` type that excludes "UNKNOWN" for client-facing APIs - Captures old location before update for audit data - Calls `BookingEventHandlerService.onLocationChanged()` after successful location update - Web app uses `actionSource: "WEBAPP"`, API v2 uses `actionSource: "API_V2"` - Updated router to explicitly pass `actionSource: "WEBAPP"` - Updated test to pass `actionSource: "WEBAPP"` - **API v2**: Uses NestJS dependency injection pattern with `BookingEventHandlerService` injected via constructor ### Updates since last revision: - **Created `BookingEventHandlerModule`** (`apps/api/v2/src/lib/modules/booking-event-handler.module.ts`) to encapsulate `BookingEventHandlerService` and its dependencies (Logger, TaskerService, HashedLinkService, BookingAuditProducerService) - Updated both bookings modules (2024-04-15 and 2024-08-13) to import `BookingEventHandlerModule` instead of listing individual providers - This reduces code duplication and makes dependency management cleaner ## Mandatory Tasks (DO NOT REMOVE) - [x] I have self-reviewed the code (A decent size PR without self-review might be rejected). - [x] I have updated the developer docs in /docs if this PR makes changes that would require a [documentation change](https://cal.com/docs). N/A - no documentation changes needed. - [x] I confirm automated tests are in place that prove my fix is effective or that my feature works. N/A - using existing audit infrastructure that is already tested. ## How should this be tested? 1. Update a booking's location through the web app 2. Update a booking's location through API v2 3. Verify audit logs are created with: - Correct `bookingUid` - Correct `actor` (user who made the change) - Correct `source` ("WEBAPP" or "API_V2") - Correct `auditData.location.old` and `auditData.location.new` values ## Checklist - [x] My code follows the style guidelines of this project - [x] I have commented my code, particularly in hard-to-understand areas - [x] I have checked if my changes generate no new warnings ## Human Review Checklist - [ ] Verify all callers of `editLocationHandler` pass `actionSource` (router updated, test updated) - [ ] Verify `organizationId` derivation is correct in both handlers (tRPC uses `booking.user?.profiles?.[0]?.organizationId`, API v2 uses `existingBookingHost.organizationId`) - [ ] Confirm audit call placement after location update is intentional (audit failures would fail the operation even though location was already updated) - [ ] Note: API v2 has its own implementation and does NOT reuse `editLocationHandler` - this is correct - [ ] Verify `BookingEventHandlerModule` properly exports `BookingEventHandlerService` and is imported in both bookings modules - [ ] Verify the `updateBookingLocationInDb` return value (`{ updatedLocation }`) is destructured and used correctly for audit data - [ ] Verify API v2 uses `bookingLocation` for audit `new` value, while tRPC uses `updatedLocation` from DB update --- Link to Devin run: https://app.devin.ai/sessions/fd1d439779674050a26ea3fa7d799943 Requested by: @hariombalhara --- .../ee/bookings/2024-04-15/bookings.module.ts | 2 + .../ee/bookings/2024-08-13/bookings.module.ts | 2 + .../services/booking-location.service.ts | 36 +++- .../modules/booking-event-handler.module.ts | 24 +++ .../LocationChangedAuditActionService.ts | 106 +++++------ ...ocation-changed-action.integration-test.ts | 173 ++++++++++++++++++ .../BookingEventHandlerService.ts | 24 ++- .../routers/viewer/bookings/_router.tsx | 3 +- .../bookings/editLocation.handler.test.ts | 4 +- .../viewer/bookings/editLocation.handler.ts | 34 +++- 10 files changed, 326 insertions(+), 82 deletions(-) create mode 100644 apps/api/v2/src/lib/modules/booking-event-handler.module.ts create mode 100644 packages/features/booking-audit/lib/service/__tests__/location-changed-action.integration-test.ts diff --git a/apps/api/v2/src/ee/bookings/2024-04-15/bookings.module.ts b/apps/api/v2/src/ee/bookings/2024-04-15/bookings.module.ts index 47be637c0a..03e52b78c5 100644 --- a/apps/api/v2/src/ee/bookings/2024-04-15/bookings.module.ts +++ b/apps/api/v2/src/ee/bookings/2024-04-15/bookings.module.ts @@ -27,6 +27,7 @@ import { TokensModule } from "@/modules/tokens/tokens.module"; import { TokensRepository } from "@/modules/tokens/tokens.repository"; import { UsersModule } from "@/modules/users/users.module"; import { Module } from "@nestjs/common"; +import { BookingEventHandlerModule } from "@/lib/modules/booking-event-handler.module"; @Module({ imports: [ @@ -43,6 +44,7 @@ import { Module } from "@nestjs/common"; RegularBookingModule, RecurringBookingModule, InstantBookingModule, + BookingEventHandlerModule, ], providers: [ TokensRepository, diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/bookings.module.ts b/apps/api/v2/src/ee/bookings/2024-08-13/bookings.module.ts index 597758201a..ff1f25c63e 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/bookings.module.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/bookings.module.ts @@ -50,6 +50,7 @@ import { TokensModule } from "@/modules/tokens/tokens.module"; import { TokensRepository } from "@/modules/tokens/tokens.repository"; import { UsersModule } from "@/modules/users/users.module"; import { Module } from "@nestjs/common"; +import { BookingEventHandlerModule } from "@/lib/modules/booking-event-handler.module"; @Module({ imports: [ @@ -71,6 +72,7 @@ import { Module } from "@nestjs/common"; RegularBookingModule, RecurringBookingModule, InstantBookingModule, + BookingEventHandlerModule, ], providers: [ TokensRepository, diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/booking-location.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/booking-location.service.ts index 844164cd60..44318b530c 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/services/booking-location.service.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/services/booking-location.service.ts @@ -1,18 +1,19 @@ +import { makeUserActor } from "@calcom/platform-libraries/bookings"; +import type { + BookingInputLocation_2024_08_13, + UpdateBookingInputLocation_2024_08_13, + UpdateBookingLocationInput_2024_08_13, +} from "@calcom/platform-types"; +import { Booking } from "@calcom/prisma/client"; +import { ForbiddenException, Injectable, Logger, NotFoundException } from "@nestjs/common"; import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/repositories/bookings.repository"; import { BookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/bookings.service"; import { InputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/input.service"; -import { ApiAuthGuardUser } from "@/modules/auth/strategies/api-auth/api-auth.strategy"; import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository"; +import { BookingEventHandlerService } from "@/lib/services/booking-event-handler.service"; +import { ApiAuthGuardUser } from "@/modules/auth/strategies/api-auth/api-auth.strategy"; import { EventTypeAccessService } from "@/modules/event-types/services/event-type-access.service"; import { UsersRepository } from "@/modules/users/users.repository"; -import { Injectable, Logger, NotFoundException, ForbiddenException } from "@nestjs/common"; - -import type { - UpdateBookingLocationInput_2024_08_13, - BookingInputLocation_2024_08_13, - UpdateBookingInputLocation_2024_08_13, -} from "@calcom/platform-types"; -import { Booking } from "@calcom/prisma/client"; @Injectable() export class BookingLocationService_2024_08_13 { @@ -24,7 +25,8 @@ export class BookingLocationService_2024_08_13 { private readonly usersRepository: UsersRepository, private readonly inputService: InputBookingsService_2024_08_13, private readonly eventTypesRepository: EventTypesRepository_2024_06_14, - private readonly eventTypeAccessService: EventTypeAccessService + private readonly eventTypeAccessService: EventTypeAccessService, + private readonly bookingEventHandlerService: BookingEventHandlerService ) {} async updateBookingLocation( @@ -66,6 +68,7 @@ export class BookingLocationService_2024_08_13 { user: ApiAuthGuardUser ) { const bookingUid = existingBooking.uid; + const oldLocation = existingBooking.location; const bookingLocation = this.getLocationValue(inputLocation) ?? existingBooking.location; if (!existingBooking.userId) { @@ -96,6 +99,19 @@ export class BookingLocationService_2024_08_13 { responses: updatedBookingResponses, }); + await this.bookingEventHandlerService.onLocationChanged({ + bookingUid: existingBooking.uid, + actor: makeUserActor(user.uuid), + organizationId: existingBookingHost.organizationId ?? null, + source: "API_V2", + auditData: { + location: { + old: oldLocation, + new: bookingLocation, + }, + }, + }); + return this.bookingsService.getBooking(updatedBooking.uid, user); } diff --git a/apps/api/v2/src/lib/modules/booking-event-handler.module.ts b/apps/api/v2/src/lib/modules/booking-event-handler.module.ts new file mode 100644 index 0000000000..d9092ee02e --- /dev/null +++ b/apps/api/v2/src/lib/modules/booking-event-handler.module.ts @@ -0,0 +1,24 @@ +import { Logger } from "@/lib/logger.bridge"; +import { BookingAuditProducerService } from "@/lib/services/booking-audit-producer.service"; +import { BookingEventHandlerService } from "@/lib/services/booking-event-handler.service"; +import { HashedLinkService } from "@/lib/services/hashed-link.service"; +import { TaskerService } from "@/lib/services/tasker.service"; +import { Module, Scope } from "@nestjs/common"; + +@Module({ + providers: [ + { + provide: Logger, + useFactory: () => { + return new Logger(); + }, + scope: Scope.TRANSIENT, + }, + TaskerService, + HashedLinkService, + BookingAuditProducerService, + BookingEventHandlerService, + ], + exports: [BookingEventHandlerService], +}) +export class BookingEventHandlerModule {} diff --git a/packages/features/booking-audit/lib/actions/LocationChangedAuditActionService.ts b/packages/features/booking-audit/lib/actions/LocationChangedAuditActionService.ts index 4e218d8d3d..947848b4bb 100644 --- a/packages/features/booking-audit/lib/actions/LocationChangedAuditActionService.ts +++ b/packages/features/booking-audit/lib/actions/LocationChangedAuditActionService.ts @@ -3,7 +3,11 @@ import { z } from "zod"; import { getHumanReadableLocationValue } from "@calcom/app-store/locations"; import { StringChangeSchema } from "../common/changeSchemas"; import { AuditActionServiceHelper } from "./AuditActionServiceHelper"; -import type { IAuditActionService, TranslationWithParams, GetDisplayTitleParams } from "./IAuditActionService"; +import type { + IAuditActionService, + TranslationWithParams, + GetDisplayTitleParams, +} from "./IAuditActionService"; import { getTranslation } from "@calcom/lib/server/i18n"; /** * Location Changed Audit Action Service @@ -12,67 +16,67 @@ import { getTranslation } from "@calcom/lib/server/i18n"; // Module-level because it is passed to IAuditActionService type outside the class scope const fieldsSchemaV1 = z.object({ - location: StringChangeSchema, + location: StringChangeSchema, }); export class LocationChangedAuditActionService implements IAuditActionService { - readonly VERSION = 1; - public static readonly TYPE = "LOCATION_CHANGED" as const; - private static dataSchemaV1 = z.object({ - version: z.literal(1), - fields: fieldsSchemaV1, + readonly VERSION = 1; + public static readonly TYPE = "LOCATION_CHANGED" as const; + private static dataSchemaV1 = z.object({ + version: z.literal(1), + fields: fieldsSchemaV1, + }); + private static fieldsSchemaV1 = fieldsSchemaV1; + public static readonly latestFieldsSchema = fieldsSchemaV1; + // Union of all versions + public static readonly storedDataSchema = LocationChangedAuditActionService.dataSchemaV1; + // Union of all versions + public static readonly storedFieldsSchema = LocationChangedAuditActionService.fieldsSchemaV1; + private helper: AuditActionServiceHelper< + typeof LocationChangedAuditActionService.latestFieldsSchema, + typeof LocationChangedAuditActionService.storedDataSchema + >; + + constructor() { + this.helper = new AuditActionServiceHelper({ + latestVersion: this.VERSION, + latestFieldsSchema: LocationChangedAuditActionService.latestFieldsSchema, + storedDataSchema: LocationChangedAuditActionService.storedDataSchema, }); - private static fieldsSchemaV1 = fieldsSchemaV1; - public static readonly latestFieldsSchema = fieldsSchemaV1; - // Union of all versions - public static readonly storedDataSchema = LocationChangedAuditActionService.dataSchemaV1; - // Union of all versions - public static readonly storedFieldsSchema = LocationChangedAuditActionService.fieldsSchemaV1; - private helper: AuditActionServiceHelper< - typeof LocationChangedAuditActionService.latestFieldsSchema, - typeof LocationChangedAuditActionService.storedDataSchema - >; + } - constructor() { - this.helper = new AuditActionServiceHelper({ - latestVersion: this.VERSION, - latestFieldsSchema: LocationChangedAuditActionService.latestFieldsSchema, - storedDataSchema: LocationChangedAuditActionService.storedDataSchema, - }); - } + getVersionedData(fields: unknown) { + return this.helper.getVersionedData(fields); + } - getVersionedData(fields: unknown) { - return this.helper.getVersionedData(fields); - } + parseStored(data: unknown) { + return this.helper.parseStored(data); + } - parseStored(data: unknown) { - return this.helper.parseStored(data); - } + getVersion(data: unknown): number { + return this.helper.getVersion(data); + } - getVersion(data: unknown): number { - return this.helper.getVersion(data); - } + migrateToLatest(data: unknown) { + // V1-only: validate and return as-is (no migration needed) + const validated = fieldsSchemaV1.parse(data); + return { isMigrated: false, latestData: validated }; + } - migrateToLatest(data: unknown) { - // V1-only: validate and return as-is (no migration needed) - const validated = fieldsSchemaV1.parse(data); - return { isMigrated: false, latestData: validated }; - } + async getDisplayTitle({ storedData }: GetDisplayTitleParams): Promise { + const { fields } = this.parseStored(storedData); + // TODO: Ideally we want to translate the location label to the user's locale + // We currently don't accept requesting user's translate fn here, fix it later. + const t = await getTranslation("en", "common"); - async getDisplayTitle({ storedData }: GetDisplayTitleParams): Promise { - const { fields } = this.parseStored(storedData); - // TODO: Ideally we want to translate the location label to the user's locale - // We currently don't accept requesting user's translate fn here, fix it later. - const t = await getTranslation("en", "common"); + const fromLocation = getHumanReadableLocationValue(fields.location.old, t); + const toLocation = getHumanReadableLocationValue(fields.location.new, t); - const fromLocation = getHumanReadableLocationValue(fields.location.old, t); - const toLocation = getHumanReadableLocationValue(fields.location.new, t); - - return { - key: "booking_audit_action.location_changed_from_to", - params: { fromLocation, toLocation }, - }; - } + return { + key: "booking_audit_action.location_changed_from_to", + params: { fromLocation, toLocation }, + }; + } } export type LocationChangedAuditData = z.infer; diff --git a/packages/features/booking-audit/lib/service/__tests__/location-changed-action.integration-test.ts b/packages/features/booking-audit/lib/service/__tests__/location-changed-action.integration-test.ts new file mode 100644 index 0000000000..8defc96763 --- /dev/null +++ b/packages/features/booking-audit/lib/service/__tests__/location-changed-action.integration-test.ts @@ -0,0 +1,173 @@ +import { describe, expect, it, beforeEach, afterEach } from "vitest"; + +import type { BookingAuditTaskConsumer } from "../BookingAuditTaskConsumer"; +import type { BookingAuditViewerService } from "../BookingAuditViewerService"; +import { makeUserActor } from "../../makeActor"; +import { getBookingAuditTaskConsumer } from "../../../di/BookingAuditTaskConsumer.container"; +import { getBookingAuditViewerService } from "../../../di/BookingAuditViewerService.container"; +import { + createTestUser, + createTestOrganization, + createTestMembership, + createTestEventType, + createTestBooking, + enableFeatureForOrganization, + cleanupTestData, +} from "./integration-utils"; +import { BookingStatus } from "@calcom/prisma/enums"; + +describe("Location Changed Action Integration", () => { + let bookingAuditTaskConsumer: BookingAuditTaskConsumer; + let bookingAuditViewerService: BookingAuditViewerService; + + let testData: { + owner: { id: number; uuid: string; email: string }; + attendee: { id: number; email: string }; + organization: { id: number }; + eventType: { id: number }; + booking: { uid: string; startTime: Date; endTime: Date; status: BookingStatus }; + }; + + beforeEach(async () => { + bookingAuditTaskConsumer = getBookingAuditTaskConsumer(); + bookingAuditViewerService = getBookingAuditViewerService(); + + // Setup basic test data using Test Data Builder pattern + const owner = await createTestUser({ name: "Location Audit User" }); + const organization = await createTestOrganization(); + await createTestMembership(owner.id, organization.id); + await enableFeatureForOrganization(organization.id, "booking-audit"); + const eventType = await createTestEventType(owner.id); + const attendee = await createTestUser({ name: "Test Attendee" }); + + const booking = await createTestBooking(owner.id, eventType.id, { + attendees: [ + { + email: attendee.email, + name: attendee.name || "Test Attendee", + timeZone: "UTC", + }, + ], + }); + + testData = { + owner: { id: owner.id, uuid: owner.uuid, email: owner.email }, + attendee: { id: attendee.id, email: attendee.email }, + organization: { id: organization.id }, + eventType: { id: eventType.id }, + booking: { + uid: booking.uid, + startTime: booking.startTime, + endTime: booking.endTime, + status: booking.status, + }, + }; + }); + + afterEach(async () => { + if (!testData) return; + + await cleanupTestData({ + bookingUid: testData.booking?.uid, + userUuids: testData.owner?.uuid ? [testData.owner.uuid] : [], + attendeeEmails: testData.attendee?.email ? [testData.attendee.email] : [], + eventTypeId: testData.eventType?.id, + organizationId: testData.organization?.id, + userIds: [testData.owner?.id, testData.attendee?.id].filter((id): id is number => id !== undefined), + featureSlug: "booking-audit", + }); + }); + + describe("when location is changed", () => { + it("should create audit record and retrieve it with correct location data", async () => { + const actor = makeUserActor(testData.owner.uuid); + const operationId = `op-${Date.now()}`; + + // Simulate LOCATION_CHANGED action + await bookingAuditTaskConsumer.onBookingAction({ + bookingUid: testData.booking.uid, + actor, + action: "LOCATION_CHANGED", + source: "WEBAPP", + operationId, + data: { + location: { + old: "Zoom", + new: "Google Meet", + }, + }, + timestamp: Date.now(), + }); + + // Retrieve logs + const result = await bookingAuditViewerService.getAuditLogsForBooking({ + bookingUid: testData.booking.uid, + userId: testData.owner.id, + userEmail: testData.owner.email, + userTimeZone: "UTC", + organizationId: testData.organization.id, + }); + + expect(result.bookingUid).toBe(testData.booking.uid); + + // Find the specific log we just created + const auditLog = result.auditLogs.find((log) => log.operationId === operationId); + expect(auditLog).toBeDefined(); + + if (!auditLog) throw new Error("Audit log not found"); + + expect(auditLog.action).toBe("LOCATION_CHANGED"); + // The type identifier from the service + expect(auditLog.type).toBe("RECORD_UPDATED"); + + expect(auditLog.displayJson).toBeNull(); + + // Verify the title params are correct + expect(auditLog.actionDisplayTitle).toBeDefined(); + expect(auditLog.actionDisplayTitle.key).toBe("booking_audit_action.location_changed_from_to"); + expect(auditLog.actionDisplayTitle.params).toEqual({ + fromLocation: "Zoom", + toLocation: "Google Meet", + }); + }); + + it("should handle location change where old value was null", async () => { + const actor = makeUserActor(testData.owner.uuid); + const operationId = `op-initial-${Date.now()}`; + + // Simulate setting location for the first time (old is null) + await bookingAuditTaskConsumer.onBookingAction({ + bookingUid: testData.booking.uid, + actor, + action: "LOCATION_CHANGED", + source: "WEBAPP", + operationId, + data: { + location: { + old: null, + new: "In Person", + }, + }, + timestamp: Date.now(), + }); + + const result = await bookingAuditViewerService.getAuditLogsForBooking({ + bookingUid: testData.booking.uid, + userId: testData.owner.id, + userEmail: testData.owner.email, + userTimeZone: "UTC", + organizationId: testData.organization.id, + }); + + const auditLog = result.auditLogs.find((log) => log.operationId === operationId); + expect(auditLog).toBeDefined(); + + expect(auditLog!.displayJson).toBeNull(); + + expect(auditLog!.actionDisplayTitle.params).toEqual({ + fromLocation: "No location defined", + toLocation: "In Person", + }); + }); + }); +}); diff --git a/packages/features/bookings/lib/onBookingEvents/BookingEventHandlerService.ts b/packages/features/bookings/lib/onBookingEvents/BookingEventHandlerService.ts index 3db6423c5f..7fbee68f52 100644 --- a/packages/features/bookings/lib/onBookingEvents/BookingEventHandlerService.ts +++ b/packages/features/bookings/lib/onBookingEvents/BookingEventHandlerService.ts @@ -248,16 +248,20 @@ export class BookingEventHandlerService { } async onLocationChanged(params: OnLocationChangedParams) { - const { bookingUid, actor, organizationId, auditData, source, operationId, context } = params; - await this.bookingAuditProducerService.queueLocationChangedAudit({ - bookingUid, - actor, - organizationId, - source, - operationId, - data: auditData, - context, - }); + try { + const { bookingUid, actor, organizationId, auditData, source, operationId, context } = params; + await this.bookingAuditProducerService.queueLocationChangedAudit({ + bookingUid, + actor, + organizationId, + source, + operationId, + data: auditData, + context, + }); + } catch (error) { + this.log.error("Error while onLocationChanged", safeStringify(error)); + } } async onAttendeeNoShowUpdated(params: OnAttendeeNoShowUpdatedParams) { diff --git a/packages/trpc/server/routers/viewer/bookings/_router.tsx b/packages/trpc/server/routers/viewer/bookings/_router.tsx index 20a6a0b6ab..f87ad1e1ef 100644 --- a/packages/trpc/server/routers/viewer/bookings/_router.tsx +++ b/packages/trpc/server/routers/viewer/bookings/_router.tsx @@ -8,9 +8,9 @@ import { ZConfirmInputSchema } from "./confirm.schema"; import { ZEditLocationInputSchema } from "./editLocation.schema"; import { ZFindInputSchema } from "./find.schema"; import { ZGetInputSchema } from "./get.schema"; -import { ZGetBookingHistoryInputSchema } from "./getBookingHistory.schema"; import { ZGetBookingAttendeesInputSchema } from "./getBookingAttendees.schema"; import { ZGetBookingDetailsInputSchema } from "./getBookingDetails.schema"; +import { ZGetBookingHistoryInputSchema } from "./getBookingHistory.schema"; import { ZInstantBookingInputSchema } from "./getInstantBookingLocation.schema"; import { ZReportBookingInputSchema } from "./reportBooking.schema"; import { ZRequestRescheduleInputSchema } from "./requestReschedule.schema"; @@ -42,6 +42,7 @@ export const bookingsRouter = router({ return editLocationHandler({ ctx, input, + actionSource: "WEBAPP", }); }), diff --git a/packages/trpc/server/routers/viewer/bookings/editLocation.handler.test.ts b/packages/trpc/server/routers/viewer/bookings/editLocation.handler.test.ts index 9af11a07ce..f8b4ea587f 100644 --- a/packages/trpc/server/routers/viewer/bookings/editLocation.handler.test.ts +++ b/packages/trpc/server/routers/viewer/bookings/editLocation.handler.test.ts @@ -4,7 +4,7 @@ // NOTE: All imports except vitest are deferred to inside the skipped describe blocks // to prevent module loading side effects during test collection (which can cause // "Closing rpc while fetch was pending" errors from watchlist module imports) -import { describe, expect, test, vi, beforeEach } from "vitest"; +import { beforeEach, describe, expect, test, vi } from "vitest"; describe.skip("getLocationForOrganizerDefaultConferencingAppInEvtFormat", () => { const mockTranslate = vi.fn((key: string) => key); @@ -224,7 +224,7 @@ describe.skip("editLocation.handler", () => { input: { newLocation: "conferencing", }, - currentUserId: updatedOrganizer.id, + actionSource: "WEBAPP", }); const updatedBooking = await prisma.booking.findFirstOrThrow({ diff --git a/packages/trpc/server/routers/viewer/bookings/editLocation.handler.ts b/packages/trpc/server/routers/viewer/bookings/editLocation.handler.ts index 808d6b6f83..bf2345654b 100644 --- a/packages/trpc/server/routers/viewer/bookings/editLocation.handler.ts +++ b/packages/trpc/server/routers/viewer/bookings/editLocation.handler.ts @@ -1,28 +1,28 @@ -import type { z } from "zod"; - import { getUsersCredentialsIncludeServiceAccountKey } from "@calcom/app-store/delegationCredential"; import { getEventLocationType, OrganizerDefaultConferencingAppType } from "@calcom/app-store/locations"; import { getAppFromSlug } from "@calcom/app-store/utils"; import { sendLocationChangeEmailsAndSMS } from "@calcom/emails/email-manager"; +import { makeUserActor } from "@calcom/features/booking-audit/lib/makeActor"; +import type { ValidActionSource } from "@calcom/features/booking-audit/lib/types/actionSource"; +import { getBookingEventHandlerService } from "@calcom/features/bookings/di/BookingEventHandlerService.container"; import EventManager from "@calcom/features/bookings/lib/EventManager"; import { BookingRepository } from "@calcom/features/bookings/repositories/BookingRepository"; import { CredentialRepository } from "@calcom/features/credentials/repositories/CredentialRepository"; import { CredentialAccessService } from "@calcom/features/credentials/services/CredentialAccessService"; import { UserRepository } from "@calcom/features/users/repositories/UserRepository"; -import { getVideoCallUrlFromCalEvent } from "@calcom/lib/CalEventParser"; import { buildCalEventFromBooking } from "@calcom/lib/buildCalEventFromBooking"; +import { getVideoCallUrlFromCalEvent } from "@calcom/lib/CalEventParser"; import logger from "@calcom/lib/logger"; import { safeStringify } from "@calcom/lib/safeStringify"; import { getTranslation } from "@calcom/lib/server/i18n"; import { prisma } from "@calcom/prisma"; import type { Booking, BookingReference } from "@calcom/prisma/client"; -import type { userMetadata } from "@calcom/prisma/zod-utils"; -import type { EventTypeMetadata } from "@calcom/prisma/zod-utils"; +import type { EventTypeMetadata, userMetadata } from "@calcom/prisma/zod-utils"; import type { AdditionalInformation, CalendarEvent } from "@calcom/types/Calendar"; import type { PartialReference } from "@calcom/types/EventManager"; import type { Ensure } from "@calcom/types/utils"; - import { TRPCError } from "@trpc/server"; +import type { z } from "zod"; import type { TrpcSessionUser } from "../../../types"; import type { TEditLocationInputSchema } from "./editLocation.schema"; @@ -34,6 +34,7 @@ type EditLocationOptions = { user: NonNullable; } & BookingsProcedureContext; input: TEditLocationInputSchema; + actionSource: ValidActionSource; }; type UserMetadata = z.infer; @@ -121,6 +122,7 @@ async function updateBookingLocationInDb({ iCalSequence: (evt.iCalSequence || 0) + 1, }, }); + return { updatedLocation: evt.location }; } async function getAllCredentialsIncludeServiceAccountKey({ @@ -257,10 +259,12 @@ export function getLocationForOrganizerDefaultConferencingAppInEvtFormat({ return appLink; } -export async function editLocationHandler({ ctx, input }: EditLocationOptions) { +export async function editLocationHandler({ ctx, input, actionSource }: EditLocationOptions) { const { newLocation, credentialId: conferenceCredentialId } = input; const { booking, user: loggedInUser } = ctx; + const oldLocation = booking.location; + const organizer = await new UserRepository(prisma).findByIdOrThrow({ id: booking.userId || 0 }); const organizationId = booking.user?.profiles?.[0]?.organizationId ?? null; @@ -295,7 +299,7 @@ export async function editLocationHandler({ ctx, input }: EditLocationOptions) { const additionalInformation = extractAdditionalInformation(updatedResult.results[0]); - await updateBookingLocationInDb({ + const { updatedLocation } = await updateBookingLocationInDb({ booking, evt: { ...evt, additionalInformation }, references: updatedResult.referencesToCreate, @@ -310,5 +314,19 @@ export async function editLocationHandler({ ctx, input }: EditLocationOptions) { logger.error("Error sending LocationChangeEmails", safeStringify(error)); } + const bookingEventHandlerService = getBookingEventHandlerService(); + await bookingEventHandlerService.onLocationChanged({ + bookingUid: booking.uid, + actor: makeUserActor(loggedInUser.uuid), + organizationId, + source: actionSource, + auditData: { + location: { + old: oldLocation, + new: updatedLocation, + }, + }, + }); + return { message: "Location updated" }; }