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
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {}
|
||||
@@ -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<TranslationWithParams> {
|
||||
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<TranslationWithParams> {
|
||||
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<typeof fieldsSchemaV1>;
|
||||
|
||||
+173
@@ -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",
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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) {
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
}),
|
||||
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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<TrpcSessionUser>;
|
||||
} & BookingsProcedureContext;
|
||||
input: TEditLocationInputSchema;
|
||||
actionSource: ValidActionSource;
|
||||
};
|
||||
|
||||
type UserMetadata = z.infer<typeof userMetadata>;
|
||||
@@ -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" };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user