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 072ac65ca4..acf2487c30 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
@@ -1,3 +1,45 @@
+import {
+ confirmBookingHandler,
+ getAllUserBookings,
+ getCalendarLinks,
+ getTranslation,
+ handleCancelBooking,
+ handleMarkNoShow,
+ roundRobinManualReassignment,
+ roundRobinReassignment,
+} from "@calcom/platform-libraries";
+import { makeUserActor, PrismaOrgMembershipRepository } from "@calcom/platform-libraries/bookings";
+import type { RescheduleSeatedBookingInput_2024_08_13 } from "@calcom/platform-types";
+import {
+ BookingOutput_2024_08_13,
+ CancelBookingInput,
+ CreateBookingInput,
+ CreateBookingInput_2024_08_13,
+ CreateInstantBookingInput_2024_08_13,
+ CreateRecurringBookingInput_2024_08_13,
+ GetBookingsInput_2024_08_13,
+ GetRecurringSeatedBookingOutput_2024_08_13,
+ GetSeatedBookingOutput_2024_08_13,
+ MarkAbsentBookingInput_2024_08_13,
+ ReassignToUserBookingInput_2024_08_13,
+ RecurringBookingOutput_2024_08_13,
+ RescheduleBookingInput,
+} from "@calcom/platform-types";
+import type { PrismaClient } from "@calcom/prisma";
+import type { EventType, Team, User } from "@calcom/prisma/client";
+import {
+ BadRequestException,
+ ConflictException,
+ ForbiddenException,
+ Injectable,
+ Logger,
+ NotFoundException,
+ UnauthorizedException,
+ UnprocessableEntityException,
+} from "@nestjs/common";
+import { Request } from "express";
+import { DateTime } from "luxon";
+import { z } from "zod";
import { CalendarLink } from "@/ee/bookings/2024-08-13/outputs/calendar-links.output";
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/repositories/bookings.repository";
import { ErrorsBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/errors.service";
@@ -24,51 +66,8 @@ import { TeamsEventTypesRepository } from "@/modules/teams/event-types/teams-eve
import { TeamsRepository } from "@/modules/teams/teams/teams.repository";
import { UsersService } from "@/modules/users/services/users.service";
import { UsersRepository } from "@/modules/users/users.repository";
-import {
- ConflictException,
- ForbiddenException,
- Injectable,
- Logger,
- NotFoundException,
- UnauthorizedException,
- UnprocessableEntityException,
-} from "@nestjs/common";
-import { BadRequestException } from "@nestjs/common";
-import { Request } from "express";
-import { DateTime } from "luxon";
-import { z } from "zod";
-export const BOOKING_REASSIGN_PERMISSION_ERROR = "You do not have permission to reassign this booking";
-import {
- getTranslation,
- getAllUserBookings,
- handleCancelBooking,
- roundRobinReassignment,
- roundRobinManualReassignment,
- handleMarkNoShow,
- confirmBookingHandler,
- getCalendarLinks,
-} from "@calcom/platform-libraries";
-import { PrismaOrgMembershipRepository } from "@calcom/platform-libraries/bookings";
-import {
- CreateBookingInput_2024_08_13,
- CreateBookingInput,
- CreateRecurringBookingInput_2024_08_13,
- GetBookingsInput_2024_08_13,
- CreateInstantBookingInput_2024_08_13,
- MarkAbsentBookingInput_2024_08_13,
- ReassignToUserBookingInput_2024_08_13,
- BookingOutput_2024_08_13,
- RecurringBookingOutput_2024_08_13,
- GetSeatedBookingOutput_2024_08_13,
- GetRecurringSeatedBookingOutput_2024_08_13,
- RescheduleBookingInput,
- CancelBookingInput,
-} from "@calcom/platform-types";
-import type { RescheduleSeatedBookingInput_2024_08_13 } from "@calcom/platform-types";
-import type { PrismaClient } from "@calcom/prisma";
-import type { EventType, User, Team } from "@calcom/prisma/client";
-import { makeUserActor } from "@calcom/platform-libraries/bookings";
+export const BOOKING_REASSIGN_PERMISSION_ERROR = "You do not have permission to reassign this booking";
type CreatedBooking = {
hosts: { id: number }[];
@@ -115,7 +114,7 @@ export class BookingsService_2024_08_13 {
private readonly recurringBookingService: RecurringBookingService,
private readonly instantBookingCreateService: InstantBookingCreateService,
private readonly eventTypeAccessService: EventTypeAccessService
- ) { }
+ ) {}
async createBooking(request: Request, body: CreateBookingInput, authUser: AuthOptionalUser) {
let bookingTeamEventType = false;
@@ -321,7 +320,8 @@ export class BookingsService_2024_08_13 {
const allowedOptionValues = eventTypeBookingField.options.map((opt) => opt.value);
if (!this.isValidSingleOptionValue(submittedValue, allowedOptionValues)) {
throw new BadRequestException(
- `Invalid option '${submittedValue}' for booking field '${eventTypeBookingField.name
+ `Invalid option '${submittedValue}' for booking field '${
+ eventTypeBookingField.name
}'. Allowed options are: ${allowedOptionValues.join(", ")}.`
);
}
@@ -335,7 +335,8 @@ export class BookingsService_2024_08_13 {
const allowedOptionValues = eventTypeBookingField.options.map((opt) => opt.value);
if (!this.areValidMultipleOptionValues(submittedValues, allowedOptionValues)) {
throw new BadRequestException(
- `One or more invalid options for booking field '${eventTypeBookingField.name
+ `One or more invalid options for booking field '${
+ eventTypeBookingField.name
}'. Allowed options are: ${allowedOptionValues.join(", ")}.`
);
}
@@ -349,7 +350,8 @@ export class BookingsService_2024_08_13 {
const allowedOptionValues = eventTypeBookingField.options.map((opt) => opt.value);
if (!this.areValidMultipleOptionValues(submittedValues, allowedOptionValues)) {
throw new BadRequestException(
- `One or more invalid options for booking field '${eventTypeBookingField.name
+ `One or more invalid options for booking field '${
+ eventTypeBookingField.name
}'. Allowed options are: ${allowedOptionValues.join(", ")}.`
);
}
@@ -364,7 +366,8 @@ export class BookingsService_2024_08_13 {
const allowedOptionValues = eventTypeBookingField.options.map((opt) => opt.value);
if (!this.isValidSingleOptionValue(submittedValue, allowedOptionValues)) {
throw new BadRequestException(
- `Invalid option '${submittedValue}' for booking field '${eventTypeBookingField.name
+ `Invalid option '${submittedValue}' for booking field '${
+ eventTypeBookingField.name
}'. Allowed options are: ${allowedOptionValues.join(", ")}.`
);
}
@@ -1036,6 +1039,8 @@ export class BookingsService_2024_08_13 {
emailsEnabled,
platformClientParams,
reassignedById: reassignedByUser.id,
+ actionSource: "API_V2",
+ reassignedByUuid: reassignedByUser.uuid,
});
} catch (error) {
if (error instanceof Error) {
@@ -1103,6 +1108,8 @@ export class BookingsService_2024_08_13 {
reassignedById: reassignedByUser.id,
emailsEnabled,
platformClientParams,
+ actionSource: "API_V2",
+ reassignedByUuid: reassignedByUser.uuid,
});
return this.outputService.getOutputReassignedBooking(reassigned);
@@ -1216,4 +1223,4 @@ export class BookingsService_2024_08_13 {
t: await getTranslation("en", "common"),
});
}
-}
\ No newline at end of file
+}
diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json
index 511447e311..17ebb8f3ee 100644
--- a/apps/web/public/static/locales/en/common.json
+++ b/apps/web/public/static/locales/en/common.json
@@ -4389,6 +4389,7 @@
"seat_booked": "Seat booked",
"seat_rescheduled": "Seat rescheduled {{oldDate}} -> <0>{{newDate}}0>",
"assignment_type": "Assignment",
+ "previous_assignee": "Previous assignee",
"assignment_type_manual": "Manual",
"assignment_type_round_robin": "Round robin",
"actor_impersonated_by": "Impersonator",
diff --git a/packages/features/booking-audit/client/components/BookingHistory.tsx b/packages/features/booking-audit/client/components/BookingHistory.tsx
index 1ba2a0677e..bc87c0c695 100644
--- a/packages/features/booking-audit/client/components/BookingHistory.tsx
+++ b/packages/features/booking-audit/client/components/BookingHistory.tsx
@@ -1,17 +1,17 @@
"use client";
-import Link from "next/link";
-import { useState } from "react";
-import { useLocale } from "@calcom/lib/hooks/useLocale";
import dayjs from "@calcom/dayjs";
+import type { AuditActorType } from "@calcom/features/booking-audit/lib/repository/IAuditActorRepository";
+import ServerTrans from "@calcom/lib/components/ServerTrans";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
+import { Avatar } from "@calcom/ui/components/avatar";
import { Button } from "@calcom/ui/components/button";
+import { FilterSearchField, Select } from "@calcom/ui/components/form";
import { Icon, type IconName } from "@calcom/ui/components/icon";
import { SkeletonText } from "@calcom/ui/components/skeleton";
-import { FilterSearchField, Select } from "@calcom/ui/components/form";
-import { Avatar } from "@calcom/ui/components/avatar";
-import ServerTrans from "@calcom/lib/components/ServerTrans";
-import type { AuditActorType } from "@calcom/features/booking-audit/lib/repository/IAuditActorRepository";
+import Link from "next/link";
+import { useState } from "react";
interface BookingHistoryProps {
bookingUid: string;
@@ -108,7 +108,11 @@ function BookingLogsFilters({
@@ -229,9 +228,7 @@ function BookingLogsTimeline({ logs }: BookingLogsTimelineProps) {
- {!isLast && (
-
- )}
+ {!isLast && }
@@ -276,32 +273,40 @@ function BookingLogsTimeline({ logs }: BookingLogsTimelineProps) {
{isExpanded && (
{/* Render displayFields if available, otherwise show type */}
- {log.displayFields && log.displayFields.length > 0 ? (
- log.displayFields.map((field, idx) => (
-
-
{t(field.labelKey)}
+ {log.displayFields && log.displayFields.length > 0
+ ? log.displayFields.map((field, idx) => (
+
+
+ {t(field.labelKey)}
+
{t(field.valueKey)}
))
- ) : null}
+ : null}
- {t("actor")}
+ {t("actor")}
{log.actor.displayName || log.actor.type}
{log.impersonatedBy && (
- {t("booking_audit_action.actor_impersonated_by", { actor: log.actor.displayName })}
+
+ {t("booking_audit_action.actor_impersonated_by", {
+ actor: log.actor.displayName,
+ })}
+
{log.impersonatedBy.displayName}
)}
- {t("booking_audit_action.source")}
+
+ {t("booking_audit_action.source")}
+
{log.source}
-
- {t("timestamp")}
-
+ {t("timestamp")}
{dayjs(log.timestamp).format("YYYY-MM-DD HH:mm:ss")}
@@ -318,9 +323,7 @@ function BookingLogsTimeline({ logs }: BookingLogsTimelineProps) {
{t("json")}
-
- {showJson && }
-
+
{showJson && }
)}
@@ -335,16 +338,26 @@ function BookingLogsTimeline({ logs }: BookingLogsTimelineProps) {
);
}
-function useBookingLogsFilters(
- auditLogs: AuditLog[],
- searchTerm: string,
- actorFilter: string | null
-) {
+function useBookingLogsFilters(auditLogs: AuditLog[], searchTerm: string, actorFilter: string | null) {
const filteredLogs = auditLogs.filter((log) => {
+ const doesMatchDisplayFields = (): boolean => {
+ return log.displayFields?.some((field) => {
+ return field.valueKey?.toLowerCase().includes(searchTerm.toLowerCase()) ||
+ field.labelKey?.toLowerCase().includes(searchTerm.toLowerCase());
+ }) ?? false;
+ };
+
+ const doesMatchActionDisplayTitle = (): boolean => {
+ return log.actionDisplayTitle.key?.toLowerCase().includes(searchTerm.toLowerCase()) ||
+ Object.values(log.actionDisplayTitle.params ?? {}).some((param) => param?.toString().toLowerCase().includes(searchTerm.toLowerCase()));
+ };
+
const matchesSearch =
!searchTerm ||
log.action.toLowerCase().includes(searchTerm.toLowerCase()) ||
- log.actor.displayName?.toLowerCase().includes(searchTerm.toLowerCase());
+ log.actor.displayName?.toLowerCase().includes(searchTerm.toLowerCase()) ||
+ doesMatchDisplayFields() ||
+ doesMatchActionDisplayTitle();
const matchesActor = !actorFilter || log.actor.displayName === actorFilter;
@@ -352,11 +365,7 @@ function useBookingLogsFilters(
});
const uniqueActorNames = Array.from(
- new Set(
- auditLogs
- .map((log) => log.actor.displayName)
- .filter((name): name is string => Boolean(name))
- )
+ new Set(auditLogs.map((log) => log.actor.displayName).filter((name): name is string => Boolean(name)))
);
const actorOptions = uniqueActorNames.map((actorName) => ({
@@ -377,11 +386,7 @@ export function BookingHistory({ bookingUid }: BookingHistoryProps) {
const auditLogs = data?.auditLogs || [];
- const { filteredLogs, actorOptions } = useBookingLogsFilters(
- auditLogs,
- searchTerm,
- actorFilter
- );
+ const { filteredLogs, actorOptions } = useBookingLogsFilters(auditLogs, searchTerm, actorFilter);
if (error) {
return (
diff --git a/packages/features/booking-audit/lib/actions/IAuditActionService.ts b/packages/features/booking-audit/lib/actions/IAuditActionService.ts
index ae7f7a7435..a23aaba775 100644
--- a/packages/features/booking-audit/lib/actions/IAuditActionService.ts
+++ b/packages/features/booking-audit/lib/actions/IAuditActionService.ts
@@ -99,12 +99,12 @@ export interface IAuditActionService {
* Returns additional display fields with translation keys for frontend rendering
* Optional - implement only if custom display fields are needed
* @param storedData - Parsed stored data { version, fields }
- * @returns Array of field objects with label and value translation keys
+ * @returns Promise of array of field objects with label and value translation keys
*/
- getDisplayFields?(storedData: BaseStoredAuditData): Array<{
+ getDisplayFields?(storedData: BaseStoredAuditData): Promise
;
+ }>>;
/**
* Migrate old version data to latest version
@@ -126,4 +126,4 @@ export interface IAuditActionService {
*/
latestData: Record;
};
-}
\ No newline at end of file
+}
diff --git a/packages/features/booking-audit/lib/actions/ReassignmentAuditActionService.ts b/packages/features/booking-audit/lib/actions/ReassignmentAuditActionService.ts
index 42cbfc80dc..7dc3f8c3cc 100644
--- a/packages/features/booking-audit/lib/actions/ReassignmentAuditActionService.ts
+++ b/packages/features/booking-audit/lib/actions/ReassignmentAuditActionService.ts
@@ -1,9 +1,14 @@
-import { z } from "zod";
-
import type { UserRepository } from "@calcom/features/users/repositories/UserRepository";
-import { StringChangeSchema, NumberChangeSchema } from "../common/changeSchemas";
+import { z } from "zod";
+import { StringChangeSchema } from "../common/changeSchemas";
import { AuditActionServiceHelper } from "./AuditActionServiceHelper";
-import type { IAuditActionService, TranslationWithParams, GetDisplayTitleParams, GetDisplayJsonParams, BaseStoredAuditData } from "./IAuditActionService";
+import type {
+ BaseStoredAuditData,
+ GetDisplayJsonParams,
+ GetDisplayTitleParams,
+ IAuditActionService,
+ TranslationWithParams,
+} from "./IAuditActionService";
/**
* Reassignment Audit Action Service
@@ -12,101 +17,145 @@ import type { IAuditActionService, TranslationWithParams, GetDisplayTitleParams,
// Module-level because it is passed to IAuditActionService type outside the class scope
const fieldsSchemaV1 = z.object({
- assignedToId: NumberChangeSchema,
- assignedById: NumberChangeSchema,
- reassignmentReason: StringChangeSchema,
- reassignmentType: z.enum(["manual", "roundRobin"]),
- userPrimaryEmail: StringChangeSchema.optional(),
- title: StringChangeSchema.optional(),
+ organizerUuid: StringChangeSchema.optional(),
+ hostAttendeeUpdated: z
+ .object({
+ id: z.number().optional(),
+ withUserUuid: StringChangeSchema.optional(),
+ })
+ .optional(),
+ reassignmentReason: z.string().nullable(),
+ reassignmentType: z.enum(["manual", "roundRobin"]),
});
export class ReassignmentAuditActionService implements IAuditActionService {
- readonly VERSION = 1;
- public static readonly TYPE = "REASSIGNMENT" as const;
- private static dataSchemaV1 = z.object({
- version: z.literal(1),
- fields: fieldsSchemaV1,
+ readonly VERSION = 1;
+ public static readonly TYPE = "REASSIGNMENT" 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 = ReassignmentAuditActionService.dataSchemaV1;
+ // Union of all versions
+ public static readonly storedFieldsSchema = ReassignmentAuditActionService.fieldsSchemaV1;
+ private helper: AuditActionServiceHelper<
+ typeof ReassignmentAuditActionService.latestFieldsSchema,
+ typeof ReassignmentAuditActionService.storedDataSchema
+ >;
+
+ constructor(private userRepository: UserRepository) {
+ this.helper = new AuditActionServiceHelper({
+ latestVersion: this.VERSION,
+ latestFieldsSchema: ReassignmentAuditActionService.latestFieldsSchema,
+ storedDataSchema: ReassignmentAuditActionService.storedDataSchema,
});
- private static fieldsSchemaV1 = fieldsSchemaV1;
- public static readonly latestFieldsSchema = fieldsSchemaV1;
- // Union of all versions
- public static readonly storedDataSchema = ReassignmentAuditActionService.dataSchemaV1;
- // Union of all versions
- public static readonly storedFieldsSchema = ReassignmentAuditActionService.fieldsSchemaV1;
- private helper: AuditActionServiceHelper<
- typeof ReassignmentAuditActionService.latestFieldsSchema,
- typeof ReassignmentAuditActionService.storedDataSchema
- >;
+ }
- constructor(private userRepository: UserRepository) {
- this.helper = new AuditActionServiceHelper({
- latestVersion: this.VERSION,
- latestFieldsSchema: ReassignmentAuditActionService.latestFieldsSchema,
- storedDataSchema: ReassignmentAuditActionService.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);
+ const { newUser } = await this.getPreviousAndNewAssigneeUser(fields);
+ const reassignedToName = newUser?.name || "Unknown";
+ return {
+ key: "booking_audit_action.booking_reassigned_to_host",
+ params: { host: reassignedToName },
+ };
+ }
- async getDisplayTitle({ storedData }: GetDisplayTitleParams): Promise {
- const { fields } = this.parseStored(storedData);
- const user = await this.userRepository.findById({ id: fields.assignedToId.new });
- const reassignedToName = user?.name || "Unknown";
- return {
- key: "booking_audit_action.booking_reassigned_to_host",
- params: { host: reassignedToName },
- };
- }
+ getDisplayJson({ storedData }: GetDisplayJsonParams): ReassignmentAuditDisplayData {
+ const { fields } = this.parseStored(storedData);
+ return {
+ ...(fields.organizerUuid
+ ? {
+ previousOrganizerUuid: fields.organizerUuid.old,
+ newOrganizerUuid: fields.organizerUuid.new,
+ }
+ : {}),
- getDisplayJson({
- storedData,
- }: GetDisplayJsonParams): ReassignmentAuditDisplayData {
- const { fields } = this.parseStored(storedData);
- return {
- newAssignedToId: fields.assignedToId.new,
- reassignmentReason: fields.reassignmentReason.new ?? null,
- };
- }
+ ...(fields.hostAttendeeUpdated
+ ? {
+ hostAttendeeIdUpdated: fields.hostAttendeeUpdated.id,
+ hostAttendeeUserUuidNew: fields.hostAttendeeUpdated.withUserUuid?.new,
+ hostAttendeeUserUuidOld: fields.hostAttendeeUpdated.withUserUuid?.old,
+ }
+ : {}),
+ reassignmentReason: fields.reassignmentReason ?? null,
+ };
+ }
- getDisplayFields(storedData: BaseStoredAuditData): Array<{
- labelKey: string;
- valueKey: string;
- }> {
- const { fields } = this.parseStored(storedData);
- const map = {
- manual: "manual",
- roundRobin: "round_robin",
- }
- const typeTranslationKey = `booking_audit_action.assignment_type_${map[fields.reassignmentType]}`;
+ private async getPreviousAndNewAssigneeUser(fields: ReassignmentAuditData) {
+ const hasAttendeeUpdated = fields.hostAttendeeUpdated != null;
+ const newHostUuid = hasAttendeeUpdated
+ ? fields.hostAttendeeUpdated?.withUserUuid?.new
+ : fields.organizerUuid?.new;
+ const previousHostUuid = hasAttendeeUpdated
+ ? fields.hostAttendeeUpdated?.withUserUuid?.old
+ : fields.organizerUuid?.old;
- return [
- {
- labelKey: "booking_audit_action.assignment_type",
- valueKey: typeTranslationKey,
- }
- ];
- }
+ const newUser = newHostUuid ? await this.userRepository.findByUuid({ uuid: newHostUuid }) : null;
+ const previousUser = previousHostUuid
+ ? await this.userRepository.findByUuid({ uuid: previousHostUuid })
+ : null;
+
+ return {
+ previousUser: previousUser,
+ newUser: newUser,
+ };
+ }
+
+ async getDisplayFields(storedData: BaseStoredAuditData): Promise<
+ Array<{
+ labelKey: string;
+ valueKey: string;
+ }>
+ > {
+ const { fields } = this.parseStored(storedData);
+ const map = {
+ manual: "manual",
+ roundRobin: "round_robin",
+ };
+ const typeTranslationKey = `booking_audit_action.assignment_type_${map[fields.reassignmentType]}`;
+ const { previousUser } = await this.getPreviousAndNewAssigneeUser(fields);
+ return [
+ {
+ labelKey: "booking_audit_action.assignment_type",
+ valueKey: typeTranslationKey,
+ },
+ {
+ labelKey: "booking_audit_action.previous_assignee",
+ valueKey: previousUser?.name ?? "Unknown",
+ },
+ ];
+ }
}
export type ReassignmentAuditData = z.infer;
export type ReassignmentAuditDisplayData = {
- newAssignedToId: number;
- reassignmentReason: string | null;
+ previousOrganizerUuid?: string | null;
+ newOrganizerUuid?: string | null;
+ hostAttendeeIdUpdated?: number | null;
+ hostAttendeeUserUuidNew?: string | null;
+ hostAttendeeUserUuidOld?: string | null;
+ reassignmentReason: string | null;
};
diff --git a/packages/features/booking-audit/lib/actions/__tests__/ReassignmentAuditActionService.test.ts b/packages/features/booking-audit/lib/actions/__tests__/ReassignmentAuditActionService.test.ts
new file mode 100644
index 0000000000..ce7f01cea3
--- /dev/null
+++ b/packages/features/booking-audit/lib/actions/__tests__/ReassignmentAuditActionService.test.ts
@@ -0,0 +1,328 @@
+import type { UserRepository } from "@calcom/features/users/repositories/UserRepository";
+import { beforeEach, describe, expect, it, vi } from "vitest";
+import { ReassignmentAuditActionService } from "../ReassignmentAuditActionService";
+
+describe("ReassignmentAuditActionService", () => {
+ let service: ReassignmentAuditActionService;
+ let mockUserRepository: {
+ findByUuid: ReturnType;
+ };
+
+ beforeEach(() => {
+ mockUserRepository = {
+ findByUuid: vi.fn(),
+ };
+ service = new ReassignmentAuditActionService(mockUserRepository as unknown as UserRepository);
+ });
+
+ describe("getVersionedData", () => {
+ it("should wrap fields with version number", () => {
+ const fields = {
+ organizerUuid: { old: "organizer-old", new: "organizer-new" },
+ reassignmentReason: "Host unavailable",
+ reassignmentType: "manual" as const,
+ };
+
+ const result = service.getVersionedData(fields);
+
+ expect(result).toEqual({
+ version: 1,
+ fields,
+ });
+ });
+
+ it("should handle roundRobin reassignment type with attendee update", () => {
+ const fields = {
+ organizerUuid: { old: "fixed-host", new: "fixed-host" },
+ hostAttendeeUpdated: {
+ id: 123,
+ withUserUuid: { old: "old-rr-host", new: "new-rr-host" },
+ },
+ reassignmentReason: null,
+ reassignmentType: "roundRobin" as const,
+ };
+
+ const result = service.getVersionedData(fields);
+
+ expect(result).toEqual({
+ version: 1,
+ fields,
+ });
+ });
+
+ it("should handle null reassignment reason", () => {
+ const fields = {
+ organizerUuid: { old: "organizer-old", new: "organizer-new" },
+ reassignmentReason: null,
+ reassignmentType: "manual" as const,
+ };
+
+ const result = service.getVersionedData(fields);
+
+ expect(result).toEqual({
+ version: 1,
+ fields,
+ });
+ });
+ });
+
+ describe("parseStored", () => {
+ it("should parse valid stored data", () => {
+ const storedData = {
+ version: 1,
+ fields: {
+ organizerUuid: { old: "organizer-old", new: "organizer-new" },
+ reassignmentReason: "Host unavailable",
+ reassignmentType: "manual",
+ },
+ };
+
+ const result = service.parseStored(storedData);
+
+ expect(result.version).toBe(1);
+ expect(result.fields).toEqual({
+ organizerUuid: { old: "organizer-old", new: "organizer-new" },
+ reassignmentReason: "Host unavailable",
+ reassignmentType: "manual",
+ });
+ });
+
+ it("should throw error for invalid data", () => {
+ const invalidData = {
+ version: 1,
+ fields: {
+ reassignmentReason: "Host unavailable",
+ },
+ };
+
+ expect(() => service.parseStored(invalidData)).toThrow();
+ });
+ });
+
+ describe("getVersion", () => {
+ it("should return version from stored data", () => {
+ const storedData = {
+ version: 1,
+ fields: {
+ organizerUuid: { old: "organizer-old", new: "organizer-new" },
+ reassignmentReason: null,
+ reassignmentType: "manual",
+ },
+ };
+
+ const result = service.getVersion(storedData);
+
+ expect(result).toBe(1);
+ });
+ });
+
+ describe("migrateToLatest", () => {
+ it("should return data as-is for V1 (no migration needed)", () => {
+ const fields = {
+ organizerUuid: { old: "organizer-old", new: "organizer-new" },
+ reassignmentReason: "Host unavailable",
+ reassignmentType: "manual" as const,
+ };
+
+ const result = service.migrateToLatest(fields);
+
+ expect(result.isMigrated).toBe(false);
+ expect(result.latestData).toEqual(fields);
+ });
+ });
+
+ describe("getDisplayTitle", () => {
+ it("should return translation key with host name for manual reassignment (organizer changed)", async () => {
+ const storedData = {
+ version: 1,
+ fields: {
+ organizerUuid: { old: "organizer-old", new: "organizer-new" },
+ reassignmentReason: "Host unavailable",
+ reassignmentType: "manual",
+ },
+ };
+
+ mockUserRepository.findByUuid.mockResolvedValue({ uuid: "organizer-new", name: "New Host" });
+
+ const result = await service.getDisplayTitle({ storedData });
+
+ expect(mockUserRepository.findByUuid).toHaveBeenCalledWith({ uuid: "organizer-new" });
+ expect(result).toEqual({
+ key: "booking_audit_action.booking_reassigned_to_host",
+ params: { host: "New Host" },
+ });
+ });
+
+ it("should return host name when attendee was updated (fixed-host scenario)", async () => {
+ const storedData = {
+ version: 1,
+ fields: {
+ organizerUuid: { old: "fixed-host", new: "fixed-host" },
+ hostAttendeeUpdated: {
+ id: 123,
+ withUserUuid: { old: "old-rr-host", new: "new-rr-host" },
+ },
+ reassignmentReason: null,
+ reassignmentType: "roundRobin",
+ },
+ };
+
+ mockUserRepository.findByUuid.mockResolvedValue({ uuid: "new-rr-host", name: "New RR Host" });
+
+ const result = await service.getDisplayTitle({ storedData });
+
+ expect(mockUserRepository.findByUuid).toHaveBeenCalledWith({ uuid: "new-rr-host" });
+ expect(result).toEqual({
+ key: "booking_audit_action.booking_reassigned_to_host",
+ params: { host: "New RR Host" },
+ });
+ });
+
+ it("should return 'Unknown' when user not found", async () => {
+ const storedData = {
+ version: 1,
+ fields: {
+ organizerUuid: { old: "organizer-old", new: "organizer-new" },
+ reassignmentReason: null,
+ reassignmentType: "roundRobin",
+ },
+ };
+
+ mockUserRepository.findByUuid.mockResolvedValue(null);
+
+ const result = await service.getDisplayTitle({ storedData });
+
+ expect(result).toEqual({
+ key: "booking_audit_action.booking_reassigned_to_host",
+ params: { host: "Unknown" },
+ });
+ });
+
+ it("should return 'Unknown' when user has no name", async () => {
+ const storedData = {
+ version: 1,
+ fields: {
+ organizerUuid: { old: "organizer-old", new: "organizer-new" },
+ reassignmentReason: null,
+ reassignmentType: "manual",
+ },
+ };
+
+ mockUserRepository.findByUuid.mockResolvedValue({ uuid: "organizer-new", name: null });
+
+ const result = await service.getDisplayTitle({ storedData });
+
+ expect(result).toEqual({
+ key: "booking_audit_action.booking_reassigned_to_host",
+ params: { host: "Unknown" },
+ });
+ });
+ });
+
+ describe("getDisplayJson", () => {
+ it("should return display data for manual reassignment (organizer changed)", () => {
+ const storedData = {
+ version: 1,
+ fields: {
+ organizerUuid: { old: "organizer-old", new: "organizer-new" },
+ reassignmentReason: "Host unavailable",
+ reassignmentType: "manual",
+ },
+ };
+
+ const result = service.getDisplayJson({ storedData, userTimeZone: "UTC" });
+
+ expect(result).toEqual({
+ newOrganizerUuid: "organizer-new",
+ previousOrganizerUuid: "organizer-old",
+ reassignmentReason: "Host unavailable",
+ });
+ });
+
+ it("should return display data for fixed-host reassignment (attendee updated)", () => {
+ const storedData = {
+ version: 1,
+ fields: {
+ organizerUuid: { old: "fixed-host-uuid", new: "fixed-host-uuid" },
+ hostAttendeeUpdated: {
+ id: 123,
+ withUserUuid: { old: "old-rr-host-uuid", new: "new-rr-host-uuid" },
+ },
+ reassignmentReason: null,
+ reassignmentType: "roundRobin",
+ },
+ };
+
+ const result = service.getDisplayJson({ storedData, userTimeZone: "UTC" });
+
+ expect(result).toEqual({
+ newOrganizerUuid: "fixed-host-uuid",
+ previousOrganizerUuid: "fixed-host-uuid",
+ hostAttendeeIdUpdated: 123,
+ hostAttendeeUserUuidNew: "new-rr-host-uuid",
+ hostAttendeeUserUuidOld: "old-rr-host-uuid",
+ reassignmentReason: null,
+ });
+ });
+ });
+
+ describe("getDisplayFields", () => {
+ it("should return assignment type field for manual reassignment", async () => {
+ const storedData = {
+ version: 1,
+ fields: {
+ organizerUuid: { old: "organizer-old", new: "organizer-new" },
+ reassignmentReason: "Host unavailable",
+ reassignmentType: "manual",
+ },
+ };
+
+ mockUserRepository.findByUuid.mockResolvedValue({ uuid: "organizer-old", name: "Previous Host" });
+
+ const result = await service.getDisplayFields(storedData);
+
+ expect(result).toEqual([
+ {
+ labelKey: "booking_audit_action.assignment_type",
+ valueKey: "booking_audit_action.assignment_type_manual",
+ },
+ {
+ labelKey: "booking_audit_action.previous_assignee",
+ valueKey: "Previous Host",
+ },
+ ]);
+ });
+
+ it("should return assignment type field for round robin reassignment", async () => {
+ const storedData = {
+ version: 1,
+ fields: {
+ organizerUuid: { old: "organizer-old", new: "organizer-new" },
+ reassignmentReason: null,
+ reassignmentType: "roundRobin",
+ },
+ };
+
+ mockUserRepository.findByUuid.mockResolvedValue({ uuid: "organizer-old", name: "Previous Host" });
+
+ const result = await service.getDisplayFields(storedData);
+
+ expect(result).toEqual([
+ {
+ labelKey: "booking_audit_action.assignment_type",
+ valueKey: "booking_audit_action.assignment_type_round_robin",
+ },
+ {
+ labelKey: "booking_audit_action.previous_assignee",
+ valueKey: "Previous Host",
+ },
+ ]);
+ });
+ });
+
+ describe("TYPE constant", () => {
+ it("should have correct type value", () => {
+ expect(ReassignmentAuditActionService.TYPE).toBe("REASSIGNMENT");
+ });
+ });
+});
diff --git a/packages/features/booking-audit/lib/service/BookingAuditViewerService.ts b/packages/features/booking-audit/lib/service/BookingAuditViewerService.ts
index 5224f04a65..de077da8e0 100644
--- a/packages/features/booking-audit/lib/service/BookingAuditViewerService.ts
+++ b/packages/features/booking-audit/lib/service/BookingAuditViewerService.ts
@@ -151,7 +151,7 @@ export class BookingAuditViewerService {
: null;
const displayFields = actionService.getDisplayFields
- ? actionService.getDisplayFields(parsedData)
+ ? await actionService.getDisplayFields(parsedData)
: null;
const impersonatedBy = await this.enrichImpersonator(log.context);
diff --git a/packages/features/ee/round-robin/roundRobinManualReassignment.test.ts b/packages/features/ee/round-robin/roundRobinManualReassignment.test.ts
index 3c80e86fba..a0bd9454fd 100644
--- a/packages/features/ee/round-robin/roundRobinManualReassignment.test.ts
+++ b/packages/features/ee/round-robin/roundRobinManualReassignment.test.ts
@@ -1,33 +1,35 @@
import prismaMock from "@calcom/testing/lib/__mocks__/prisma";
-
import {
- getDate,
- createBookingScenario,
- getScenarioData,
- getMockBookingAttendee,
- TestData,
addWorkflowReminders,
+ createBookingScenario,
+ getDate,
+ getMockBookingAttendee,
+ getScenarioData,
+ TestData,
} from "@calcom/testing/lib/bookingScenario/bookingScenario";
+import { OrganizerDefaultConferencingAppType } from "@calcom/app-store/locations";
+import { getBookingEventHandlerService } from "@calcom/features/bookings/di/BookingEventHandlerService.container";
+import { BookingRepository } from "@calcom/features/bookings/repositories/BookingRepository";
+import { BookingStatus, SchedulingType, WorkflowMethods } from "@calcom/prisma/enums";
import {
expectBookingToBeInDatabase,
expectSuccessfulRoundRobinReschedulingEmails,
expectWorkflowToBeTriggered,
} from "@calcom/testing/lib/bookingScenario/expects";
import { setupAndTeardown } from "@calcom/testing/lib/bookingScenario/setupAndTeardown";
-
-import { describe, vi, expect } from "vitest";
-import { v4 as uuidv4 } from "uuid";
-import { parse } from "node-html-parser";
-
-import { OrganizerDefaultConferencingAppType } from "@calcom/app-store/locations";
-import { BookingRepository } from "@calcom/features/bookings/repositories/BookingRepository";
-import { SchedulingType, BookingStatus, WorkflowMethods } from "@calcom/prisma/enums";
import { test } from "@calcom/testing/lib/fixtures/fixtures";
+import { parse } from "node-html-parser";
+import { v4 as uuidv4 } from "uuid";
+import { beforeEach, describe, expect, vi } from "vitest";
+import type { BookingEventHandlerService } from "../../bookings/lib/onBookingEvents/BookingEventHandlerService";
vi.mock("@calcom/features/bookings/lib/EventManager");
vi.mock("@calcom/app-store/utils", () => ({
getAppFromSlug: vi.fn(),
}));
+vi.mock("@calcom/features/bookings/di/BookingEventHandlerService.container", () => ({
+ getBookingEventHandlerService: vi.fn(),
+}));
// Type definitions
type TestUserMetadata = {
@@ -58,6 +60,7 @@ const createTestUser = (overrides?: {
email?: string;
destinationCalendar?: { integration: string; externalId: string };
metadata?: TestUserMetadata;
+ uuid?: string;
}) => ({
id: overrides?.id ?? 1,
name: overrides?.name ?? `user-${overrides?.id ?? 1}`,
@@ -79,6 +82,7 @@ const createTestUser = (overrides?: {
completedOnboarding: true,
locked: false,
organizationId: null,
+ uuid: overrides?.uuid ?? `uuid-${overrides?.id ?? 1}`,
});
const createTestDestinationCalendar = (overrides?: { integration?: string; externalId?: string }) => ({
@@ -222,8 +226,8 @@ const mockEventManagerReschedule = async (config?: MockEventManagerConfig) => {
conferenceType === "zoom"
? "https://zoom.us/j/123456789"
: conferenceType === "google"
- ? "https://meet.google.com/test-meeting"
- : `https://${conferenceType}.co/test-room`,
+ ? "https://meet.google.com/test-meeting"
+ : `https://${conferenceType}.co/test-room`,
},
});
}
@@ -243,6 +247,14 @@ const mockGetAppFromSlug = async () => {
describe("roundRobinManualReassignment test", () => {
setupAndTeardown();
+ beforeEach(() => {
+ // Set up default mock for BookingEventHandlerService
+ const mockOnReassignment = vi.fn().mockResolvedValue(undefined);
+ vi.mocked(getBookingEventHandlerService).mockReturnValue({
+ onReassignment: mockOnReassignment,
+ } as unknown as BookingEventHandlerService);
+ });
+
test("should reassign round robin booking to new host and update workflows", async ({ emails }) => {
const roundRobinManualReassignment = (await import("./roundRobinManualReassignment")).default;
const eventManagerRescheduleSpy = await mockEventManagerReschedule();
@@ -302,6 +314,8 @@ describe("roundRobinManualReassignment test", () => {
newUserId: newHost.id,
orgId: null,
reassignedById: 1,
+ actionSource: "WEBAPP",
+ reassignedByUuid: originalHost.uuid,
});
expectEventManagerCalledWith(eventManagerRescheduleSpy, {
@@ -370,6 +384,8 @@ describe("roundRobinManualReassignment test", () => {
newUserId: newHost.id,
orgId: null,
reassignedById: 1,
+ actionSource: "WEBAPP",
+ reassignedByUuid: originalHost.uuid,
});
// Verify that EventManager.reschedule was called with an event containing conferenceCredentialId
@@ -446,6 +462,8 @@ describe("roundRobinManualReassignment test", () => {
newUserId: newHost.id,
orgId: null,
reassignedById: 1,
+ actionSource: "WEBAPP",
+ reassignedByUuid: fixedHost.uuid,
});
expectEventManagerCalledWith(eventManagerRescheduleSpy, {
@@ -526,6 +544,8 @@ describe("roundRobinManualReassignment test", () => {
newUserId: newHost.id,
orgId: null,
reassignedById: 1,
+ actionSource: "WEBAPP",
+ reassignedByUuid: originalHost.uuid,
});
expect(sendReassignedEmailsAndSMSSpy).toHaveBeenCalledTimes(1);
@@ -535,6 +555,14 @@ describe("roundRobinManualReassignment test", () => {
describe("roundRobinManualReassignment - Location Changes", () => {
setupAndTeardown();
+ beforeEach(() => {
+ // Set up default mock for BookingEventHandlerService
+ const mockOnReassignment = vi.fn().mockResolvedValue(undefined);
+ vi.mocked(getBookingEventHandlerService).mockReturnValue({
+ onReassignment: mockOnReassignment,
+ } as any);
+ });
+
const createConferencingUsers = () => {
return {
googleMeetHost: createTestUser({
@@ -655,6 +683,8 @@ describe("roundRobinManualReassignment - Location Changes", () => {
orgId: null,
reassignReason: "Host unavailable",
reassignedById: 999,
+ actionSource: "WEBAPP",
+ reassignedByUuid: "uuid-999",
});
expectEventManagerCalledWith(eventManagerRescheduleSpy, {
@@ -720,6 +750,8 @@ describe("roundRobinManualReassignment - Location Changes", () => {
orgId: null,
reassignReason: "Host unavailable",
reassignedById: 999,
+ actionSource: "WEBAPP",
+ reassignedByUuid: "uuid-999",
});
const calledEvent = eventManagerRescheduleSpy.mock.calls[0][0];
@@ -771,6 +803,8 @@ describe("roundRobinManualReassignment - Location Changes", () => {
orgId: null,
reassignReason: "Host unavailable",
reassignedById: 999,
+ actionSource: "WEBAPP",
+ reassignedByUuid: "uuid-999",
});
const calledEvent = eventManagerRescheduleSpy.mock.calls[0][0];
@@ -821,6 +855,8 @@ describe("roundRobinManualReassignment - Location Changes", () => {
orgId: null,
reassignReason: "Host unavailable",
reassignedById: 999,
+ actionSource: "WEBAPP",
+ reassignedByUuid: "uuid-999",
});
const calledEvent = eventManagerRescheduleSpy.mock.calls[0][0];
@@ -876,6 +912,8 @@ describe("roundRobinManualReassignment - Location Changes", () => {
orgId: null,
reassignReason: "Host unavailable",
reassignedById: 999,
+ actionSource: "WEBAPP",
+ reassignedByUuid: "uuid-999",
})
).rejects.toThrow("Failed to set video conferencing link, but the meeting has been rescheduled");
});
@@ -884,6 +922,14 @@ describe("roundRobinManualReassignment - Location Changes", () => {
describe("roundRobinManualReassignment - Seated Events", () => {
setupAndTeardown();
+ beforeEach(() => {
+ // Set up default mock for BookingEventHandlerService
+ const mockOnReassignment = vi.fn().mockResolvedValue(undefined);
+ vi.mocked(getBookingEventHandlerService).mockReturnValue({
+ onReassignment: mockOnReassignment,
+ } as any);
+ });
+
test("should not expose other attendees in updated email when seatsShowAttendees is disabled for seated round-robin event", async ({
emails,
}) => {
@@ -975,6 +1021,8 @@ describe("roundRobinManualReassignment - Seated Events", () => {
newUserId: newHost.id,
orgId: null,
reassignedById: 1,
+ actionSource: "WEBAPP",
+ reassignedByUuid: originalHost.uuid,
});
// Verify that updated emails were sent to all attendees
@@ -1032,3 +1080,161 @@ describe("roundRobinManualReassignment - Seated Events", () => {
}
});
});
+
+describe("roundRobinManualReassignment - Audit Data Verification", () => {
+ setupAndTeardown();
+
+ test("should call BookingEventHandlerService.onReassignment with correct audit data when organizer changes", async () => {
+ const roundRobinManualReassignment = (await import("./roundRobinManualReassignment")).default;
+ await mockEventManagerReschedule();
+
+ const mockOnReassignment = vi.fn().mockResolvedValue(undefined);
+ vi.mocked(getBookingEventHandlerService).mockReturnValue({
+ onReassignment: mockOnReassignment,
+ } as any);
+
+ const testDestinationCalendar = createTestDestinationCalendar();
+ const originalHost = createTestUser({
+ id: 1,
+ uuid: "uuid-1",
+ destinationCalendar: testDestinationCalendar,
+ });
+ const newHost = createTestUser({ id: 2, uuid: "uuid-2" });
+ const reassigningUser = createTestUser({ id: 3, uuid: "uuid-3" });
+ const users = [originalHost, newHost, reassigningUser];
+
+ const bookingToReassignUid = "booking-audit-test";
+
+ await createBookingScenario(
+ getScenarioData({
+ eventTypes: [
+ createRoundRobinEventType({
+ id: 1,
+ slug: "round-robin-event",
+ users,
+ }),
+ ],
+ bookings: [
+ await createTestBooking({
+ eventTypeId: 1,
+ userId: originalHost.id,
+ bookingId: 123,
+ bookingUid: bookingToReassignUid,
+ }),
+ ],
+ organizer: originalHost,
+ usersApartFromOrganizer: users.slice(1),
+ })
+ );
+
+ await roundRobinManualReassignment({
+ bookingId: 123,
+ newUserId: newHost.id,
+ orgId: null,
+ reassignedById: reassigningUser.id,
+ actionSource: "WEBAPP",
+ reassignedByUuid: reassigningUser.uuid,
+ reassignReason: "Test reassignment reason",
+ });
+
+ expect(mockOnReassignment).toHaveBeenCalledTimes(1);
+ expect(mockOnReassignment).toHaveBeenCalledWith({
+ bookingUid: bookingToReassignUid,
+ actor: { identifiedBy: "user", userUuid: reassigningUser.uuid },
+ organizationId: null,
+ source: "WEBAPP",
+ auditData: {
+ organizerUuid: { old: originalHost.uuid, new: newHost.uuid },
+ reassignmentReason: "Test reassignment reason",
+ reassignmentType: "manual",
+ },
+ });
+ });
+
+ test("should call BookingEventHandlerService.onReassignment with correct audit data when only attendee changes (fixed host scenario)", async () => {
+ const roundRobinManualReassignment = (await import("./roundRobinManualReassignment")).default;
+ await mockEventManagerReschedule();
+
+ const mockOnReassignment = vi.fn().mockResolvedValue(undefined);
+ vi.mocked(getBookingEventHandlerService).mockReturnValue({
+ onReassignment: mockOnReassignment,
+ } as any);
+
+ const testDestinationCalendar = createTestDestinationCalendar();
+ const fixedHost = createTestUser({ id: 1, uuid: "uuid-1", destinationCalendar: testDestinationCalendar });
+ const currentRRHost = createTestUser({ id: 2, uuid: "uuid-2" });
+ const newHost = createTestUser({ id: 3, uuid: "uuid-3" });
+ const reassigningUser = createTestUser({ id: 4, uuid: "uuid-4" });
+ const users = [fixedHost, currentRRHost, newHost, reassigningUser];
+
+ const bookingToReassignUid = "booking-audit-fixed-host-test";
+
+ await createBookingScenario(
+ getScenarioData({
+ eventTypes: [
+ createRoundRobinEventType({
+ id: 1,
+ slug: "round-robin-event",
+ users: [fixedHost, currentRRHost, newHost],
+ hosts: users.slice(0, 3).map((user) => ({
+ userId: user.id,
+ isFixed: user.id === fixedHost.id,
+ })),
+ }),
+ ],
+ bookings: [
+ await createTestBooking({
+ eventTypeId: 1,
+ userId: fixedHost.id,
+ bookingId: 123,
+ bookingUid: bookingToReassignUid,
+ attendees: [
+ getMockBookingAttendee({
+ id: 1,
+ name: "attendee",
+ email: "attendee@test.com",
+ locale: "en",
+ timeZone: "Asia/Kolkata",
+ }),
+ getMockBookingAttendee({
+ id: currentRRHost.id,
+ name: currentRRHost.name,
+ email: currentRRHost.email,
+ locale: "en",
+ timeZone: currentRRHost.timeZone,
+ }),
+ ],
+ }),
+ ],
+ organizer: fixedHost,
+ usersApartFromOrganizer: users.slice(1, 3),
+ })
+ );
+
+ await roundRobinManualReassignment({
+ bookingId: 123,
+ newUserId: newHost.id,
+ orgId: null,
+ reassignedById: reassigningUser.id,
+ actionSource: "WEBAPP",
+ reassignedByUuid: reassigningUser.uuid,
+ reassignReason: "Test reason",
+ });
+
+ expect(mockOnReassignment).toHaveBeenCalledTimes(1);
+ const callArgs = mockOnReassignment.mock.calls[0][0];
+ expect(callArgs.bookingUid).toBe(bookingToReassignUid);
+ expect(callArgs.actor).toEqual({ identifiedBy: "user", userUuid: reassigningUser.uuid });
+ expect(callArgs.organizationId).toBe(null);
+ expect(callArgs.source).toBe("WEBAPP");
+ // organizerUuid should NOT be included when organizer hasn't changed (fixed host scenario)
+ expect(callArgs.auditData.organizerUuid).toBeUndefined();
+ expect(callArgs.auditData.reassignmentType).toBe("manual");
+ expect(callArgs.auditData.reassignmentReason).toBe("Test reason");
+ expect(callArgs.auditData.hostAttendeeUpdated).toBeDefined();
+ expect(callArgs.auditData.hostAttendeeUpdated?.withUserUuid).toEqual({
+ old: currentRRHost.uuid,
+ new: newHost.uuid,
+ });
+ });
+});
diff --git a/packages/features/ee/round-robin/roundRobinManualReassignment.ts b/packages/features/ee/round-robin/roundRobinManualReassignment.ts
index 5b822f57a3..38bde4dcc1 100644
--- a/packages/features/ee/round-robin/roundRobinManualReassignment.ts
+++ b/packages/features/ee/round-robin/roundRobinManualReassignment.ts
@@ -1,5 +1,3 @@
-import { cloneDeep } from "lodash";
-
import { enrichUserWithDelegationCredentialsIncludeServiceAccountKey } from "@calcom/app-store/delegationCredential";
import { eventTypeAppMetadataOptionalSchema } from "@calcom/app-store/zod-utils";
import dayjs from "@calcom/dayjs";
@@ -8,6 +6,9 @@ import {
sendReassignedScheduledEmailsAndSMS,
sendReassignedUpdatedEmailsAndSMS,
} 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 { getAllCredentialsIncludeServiceAccountKey } from "@calcom/features/bookings/lib/getAllCredentialsForUsersOnEvent/getAllCredentials";
import { getBookingResponsesPartialSchema } from "@calcom/features/bookings/lib/getBookingResponsesSchema";
@@ -20,8 +21,8 @@ import AssignmentReasonRecorder, {
} from "@calcom/features/ee/round-robin/assignmentReason/AssignmentReasonRecorder";
import { BookingLocationService } from "@calcom/features/ee/round-robin/lib/bookingLocationService";
import {
- scheduleEmailReminder,
deleteScheduledEmailReminder,
+ scheduleEmailReminder,
} from "@calcom/features/ee/workflows/lib/reminders/emailReminderManager";
import { scheduleWorkflowReminders } from "@calcom/features/ee/workflows/lib/reminders/reminderScheduler";
import { getEventName } from "@calcom/features/eventtypes/lib/eventNaming";
@@ -36,7 +37,7 @@ import { prisma } from "@calcom/prisma";
import { WorkflowActions, WorkflowMethods, WorkflowTriggerEvents } from "@calcom/prisma/enums";
import type { EventTypeMetadata, PlatformClientParams } from "@calcom/prisma/zod-utils";
import type { CalendarEvent } from "@calcom/types/Calendar";
-
+import { cloneDeep } from "lodash";
import { handleRescheduleEventManager } from "./handleRescheduleEventManager";
import type { BookingSelectResult } from "./utils/bookingSelect";
import { bookingSelect } from "./utils/bookingSelect";
@@ -56,6 +57,8 @@ export const roundRobinManualReassignment = async ({
reassignedById,
emailsEnabled = true,
platformClientParams,
+ actionSource,
+ reassignedByUuid,
}: {
bookingId: number;
newUserId: number;
@@ -64,6 +67,8 @@ export const roundRobinManualReassignment = async ({
reassignedById: number;
emailsEnabled?: boolean;
platformClientParams?: PlatformClientParams;
+ actionSource: ValidActionSource;
+ reassignedByUuid: string;
}) => {
const roundRobinReassignLogger = logger.getSubLogger({
prefix: ["roundRobinManualReassign", `${bookingId}`],
@@ -113,9 +118,20 @@ export const roundRobinManualReassignment = async ({
}));
const fixedHost = eventTypeHosts.find((host) => host.isFixed);
- const currentRRHost = booking.attendees.find((attendee) =>
- eventTypeHosts.some((host) => !host.isFixed && host.user.email === attendee.email)
- );
+
+ let currentRRHostUserUuid = null;
+ let attendeeUpdatedId = null;
+
+ const currentRRHostAttendee = booking.attendees.find((attendee) => {
+ const matchingEventTypeHost = eventTypeHosts.find(
+ (host) => !host.isFixed && host.user.email === attendee.email
+ );
+ if (matchingEventTypeHost) {
+ currentRRHostUserUuid = matchingEventTypeHost.user.uuid;
+ return true;
+ }
+ return false;
+ });
const newUserHost = eventTypeHosts.find((host) => host.user.id === newUserId);
if (!newUserHost) {
@@ -152,7 +168,7 @@ export const roundRobinManualReassignment = async ({
let bookingLocation = booking.location;
let conferenceCredentialId: number | null = null;
- const organizer = hasOrganizerChanged ? newUser : booking.user ?? newUser;
+ const organizer = hasOrganizerChanged ? newUser : (booking.user ?? newUser);
const teamMembers = await getTeamMembers({
eventTypeHosts,
@@ -224,10 +240,10 @@ export const roundRobinManualReassignment = async ({
reassignById: reassignedById,
reassignmentType: RRReassignmentType.MANUAL,
});
- } else if (currentRRHost) {
+ } else if (currentRRHostAttendee) {
// Update the round-robin host attendee
await prisma.attendee.update({
- where: { id: currentRRHost.id },
+ where: { id: currentRRHostAttendee.id },
data: {
name: newUser.name || "",
email: newUser.email,
@@ -235,8 +251,36 @@ export const roundRobinManualReassignment = async ({
locale: newUser.locale,
},
});
+ attendeeUpdatedId = currentRRHostAttendee.id;
}
+ const bookingEventHandlerService = getBookingEventHandlerService();
+
+ await bookingEventHandlerService.onReassignment({
+ bookingUid: booking.uid,
+ actor: makeUserActor(reassignedByUuid),
+ organizationId: orgId,
+ source: actionSource,
+ auditData: {
+ ...(hasOrganizerChanged
+ ? { organizerUuid: { old: originalOrganizer.uuid, new: newUser.uuid } }
+ : null),
+ ...(attendeeUpdatedId
+ ? {
+ hostAttendeeUpdated: {
+ id: attendeeUpdatedId,
+ withUserUuid: {
+ old: currentRRHostUserUuid,
+ new: newUser.uuid,
+ },
+ },
+ }
+ : null),
+ reassignmentReason: reassignReason ?? null,
+ reassignmentType: "manual",
+ },
+ });
+
// When organizer hasn't changed, still extract conferenceCredentialId from event type locations
if (!hasOrganizerChanged && bookingLocation) {
const { conferenceCredentialId: extractedCredentialId } =
@@ -257,7 +301,6 @@ export const roundRobinManualReassignment = async ({
});
const organizerT = await getTranslation(organizer?.locale || "en", "common");
-
const attendeePromises = [];
for (const attendee of booking.attendees) {
if (
diff --git a/packages/features/ee/round-robin/roundRobinReassignment.test.ts b/packages/features/ee/round-robin/roundRobinReassignment.test.ts
index cc355125d8..8be59a6f71 100644
--- a/packages/features/ee/round-robin/roundRobinReassignment.test.ts
+++ b/packages/features/ee/round-robin/roundRobinReassignment.test.ts
@@ -1,29 +1,31 @@
import prismaMock from "@calcom/testing/lib/__mocks__/prisma";
-
import {
- getDate,
- createBookingScenario,
- getScenarioData,
- getMockBookingAttendee,
- TestData,
addWorkflowReminders,
+ createBookingScenario,
+ getDate,
+ getMockBookingAttendee,
+ getScenarioData,
+ TestData,
} from "@calcom/testing/lib/bookingScenario/bookingScenario";
+import { getBookingEventHandlerService } from "@calcom/features/bookings/di/BookingEventHandlerService.container";
+import { BookingRepository } from "@calcom/features/bookings/repositories/BookingRepository";
+import { BookingStatus, SchedulingType, WorkflowMethods } from "@calcom/prisma/enums";
import {
expectBookingToBeInDatabase,
expectSuccessfulRoundRobinReschedulingEmails,
expectWorkflowToBeTriggered,
} from "@calcom/testing/lib/bookingScenario/expects";
import { setupAndTeardown } from "@calcom/testing/lib/bookingScenario/setupAndTeardown";
-
-import { describe, vi, expect } from "vitest";
-import { v4 as uuidv4 } from "uuid";
-import { parse } from "node-html-parser";
-
-import { BookingRepository } from "@calcom/features/bookings/repositories/BookingRepository";
-import { SchedulingType, BookingStatus, WorkflowMethods } from "@calcom/prisma/enums";
import { test } from "@calcom/testing/lib/fixtures/fixtures";
+import { parse } from "node-html-parser";
+import { v4 as uuidv4 } from "uuid";
+import { beforeEach, describe, expect, vi } from "vitest";
+import type { BookingEventHandlerService } from "../../bookings/lib/onBookingEvents/BookingEventHandlerService";
vi.mock("@calcom/features/bookings/lib/EventManager");
+vi.mock("@calcom/features/bookings/di/BookingEventHandlerService.container", () => ({
+ getBookingEventHandlerService: vi.fn(),
+}));
const testDestinationCalendar = {
integration: "test-calendar",
@@ -39,6 +41,7 @@ const testUsers = [
email: "host1@test.com",
schedules: [TestData.schedules.IstWorkHours],
destinationCalendar: testDestinationCalendar,
+ uuid: "uuid-1",
},
{
id: 2,
@@ -47,6 +50,7 @@ const testUsers = [
username: "host-2",
email: "host2@test.com",
schedules: [TestData.schedules.IstWorkHours],
+ uuid: "uuid-2",
},
{
id: 3,
@@ -55,12 +59,21 @@ const testUsers = [
username: "host-3",
email: "host3@test.com",
schedules: [TestData.schedules.IstWorkHours],
+ uuid: "uuid-3",
},
];
describe("roundRobinReassignment test", () => {
setupAndTeardown();
+ beforeEach(() => {
+ // Set up default mock for BookingEventHandlerService
+ const mockOnReassignment = vi.fn().mockResolvedValue(undefined);
+ vi.mocked(getBookingEventHandlerService).mockReturnValue({
+ onReassignment: mockOnReassignment,
+ } as unknown as BookingEventHandlerService);
+ });
+
test("reassign new round robin organizer", async ({ emails }) => {
const roundRobinReassignment = (await import("./roundRobinReassignment")).default;
const EventManager = (await import("@calcom/features/bookings/lib/EventManager")).default;
@@ -168,6 +181,8 @@ describe("roundRobinReassignment test", () => {
bookingId: 123,
orgId: null,
reassignedById: originalHost.id,
+ actionSource: "WEBAPP",
+ reassignedByUuid: originalHost.uuid,
});
expect(eventManagerSpy).toBeCalledTimes(1);
@@ -425,6 +440,8 @@ describe("roundRobinReassignment test", () => {
bookingId: 123,
orgId: null,
reassignedById: originalHost.id,
+ actionSource: "WEBAPP",
+ reassignedByUuid: originalHost.uuid,
});
expect(eventManagerSpy).toBeCalledTimes(1);
@@ -484,3 +501,203 @@ describe("roundRobinReassignment test", () => {
}
});
});
+
+describe("roundRobinReassignment - Audit Data Verification", () => {
+ setupAndTeardown();
+
+ test("should call BookingEventHandlerService.onReassignment with correct audit data when organizer changes", async () => {
+ const roundRobinReassignment = (await import("./roundRobinReassignment")).default;
+ const EventManager = (await import("@calcom/features/bookings/lib/EventManager")).default;
+
+ const eventManagerSpy = vi.spyOn(EventManager.prototype as any, "reschedule");
+ eventManagerSpy.mockClear();
+ eventManagerSpy.mockResolvedValue({ referencesToCreate: [] });
+
+ const mockOnReassignment = vi.fn().mockResolvedValue(undefined);
+ vi.mocked(getBookingEventHandlerService).mockReturnValue({
+ onReassignment: mockOnReassignment,
+ } as any);
+
+ const users = testUsers;
+ const originalHost = users[0];
+ const newHost = users[1];
+ const reassigningUser = users[0]; // originalHost is doing the reassignment
+
+ const { dateString: dateStringPlusOne } = getDate({ dateIncrement: 1 });
+ const { dateString: dateStringMinusOne } = getDate({ dateIncrement: -1 });
+ const { dateString: dateStringPlusTwo } = getDate({ dateIncrement: 2 });
+
+ const bookingToReassignUid = "booking-audit-test";
+
+ await createBookingScenario(
+ getScenarioData({
+ eventTypes: [
+ {
+ id: 1,
+ slug: "round-robin-event",
+ schedulingType: SchedulingType.ROUND_ROBIN,
+ length: 45,
+ users: users.map((user) => ({ id: user.id })),
+ hosts: users.map((user) => ({ userId: user.id, isFixed: false })),
+ },
+ ],
+ bookings: [
+ {
+ id: 123,
+ eventTypeId: 1,
+ userId: originalHost.id,
+ uid: bookingToReassignUid,
+ status: BookingStatus.ACCEPTED,
+ startTime: `${dateStringPlusOne}T05:00:00.000Z`,
+ endTime: `${dateStringPlusOne}T05:15:00.000Z`,
+ attendees: [
+ getMockBookingAttendee({
+ id: 2,
+ name: "attendee",
+ email: "attendee@test.com",
+ locale: "en",
+ timeZone: "Asia/Kolkata",
+ }),
+ ],
+ },
+ {
+ id: 456,
+ eventTypeId: 1,
+ userId: users[2].id,
+ uid: "other-booking",
+ status: BookingStatus.ACCEPTED,
+ startTime: `${dateStringMinusOne}T05:00:00.000Z`,
+ endTime: `${dateStringMinusOne}T05:15:00.000Z`,
+ attendees: [
+ getMockBookingAttendee({
+ id: 2,
+ name: "attendee",
+ email: "attendee@test.com",
+ locale: "en",
+ timeZone: "Asia/Kolkata",
+ }),
+ ],
+ },
+ ],
+ organizer: originalHost,
+ usersApartFromOrganizer: users.slice(1),
+ })
+ );
+
+ await roundRobinReassignment({
+ bookingId: 123,
+ orgId: null,
+ reassignedById: reassigningUser.id,
+ actionSource: "WEBAPP",
+ reassignedByUuid: reassigningUser.uuid,
+ });
+
+ expect(mockOnReassignment).toHaveBeenCalledTimes(1);
+ expect(mockOnReassignment).toHaveBeenCalledWith({
+ bookingUid: bookingToReassignUid,
+ actor: { identifiedBy: "user", userUuid: reassigningUser.uuid },
+ organizationId: null,
+ source: "WEBAPP",
+ auditData: {
+ organizerUuid: { old: originalHost.uuid, new: newHost.uuid },
+ reassignmentReason: null,
+ reassignmentType: "roundRobin",
+ },
+ });
+ });
+
+ test("should call BookingEventHandlerService.onReassignment with correct audit data when only attendee changes (fixed host scenario)", async () => {
+ const roundRobinReassignment = (await import("./roundRobinReassignment")).default;
+ const EventManager = (await import("@calcom/features/bookings/lib/EventManager")).default;
+
+ const eventManagerSpy = vi.spyOn(EventManager.prototype as any, "reschedule");
+ eventManagerSpy.mockClear();
+ eventManagerSpy.mockResolvedValue({ referencesToCreate: [] });
+
+ const mockOnReassignment = vi.fn().mockResolvedValue(undefined);
+ vi.mocked(getBookingEventHandlerService).mockReturnValue({
+ onReassignment: mockOnReassignment,
+ } as any);
+
+ const users = testUsers;
+ const fixedHost = users[0];
+ const currentRRHost = users[1];
+ const newHost = users[2];
+ const reassigningUser = fixedHost;
+
+ const { dateString: dateStringPlusOne } = getDate({ dateIncrement: 1 });
+
+ const bookingToReassignUid = "booking-audit-fixed-host-test";
+
+ await createBookingScenario(
+ getScenarioData({
+ eventTypes: [
+ {
+ id: 1,
+ slug: "round-robin-event",
+ schedulingType: SchedulingType.ROUND_ROBIN,
+ length: 45,
+ users: users.map((user) => ({ id: user.id })),
+ hosts: users.map((user) => ({
+ userId: user.id,
+ isFixed: user.id === fixedHost.id,
+ })),
+ },
+ ],
+ bookings: [
+ {
+ id: 123,
+ eventTypeId: 1,
+ userId: fixedHost.id,
+ uid: bookingToReassignUid,
+ status: BookingStatus.ACCEPTED,
+ startTime: `${dateStringPlusOne}T05:00:00.000Z`,
+ endTime: `${dateStringPlusOne}T05:15:00.000Z`,
+ attendees: [
+ getMockBookingAttendee({
+ id: 1,
+ name: "attendee",
+ email: "attendee@test.com",
+ locale: "en",
+ timeZone: "Asia/Kolkata",
+ }),
+ getMockBookingAttendee({
+ id: currentRRHost.id,
+ name: currentRRHost.name,
+ email: currentRRHost.email,
+ locale: "en",
+ timeZone: currentRRHost.timeZone,
+ }),
+ ],
+ },
+ ],
+ organizer: fixedHost,
+ usersApartFromOrganizer: users.slice(1),
+ })
+ );
+
+ await roundRobinReassignment({
+ bookingId: 123,
+ orgId: null,
+ reassignedById: reassigningUser.id,
+ actionSource: "WEBAPP",
+ reassignedByUuid: reassigningUser.uuid,
+ });
+
+ expect(mockOnReassignment).toHaveBeenCalledTimes(1);
+ const callArgs = mockOnReassignment.mock.calls[0][0];
+ expect(callArgs.bookingUid).toBe(bookingToReassignUid);
+ expect(callArgs.actor).toEqual({ identifiedBy: "user", userUuid: reassigningUser.uuid });
+ expect(callArgs.organizationId).toBe(null);
+ expect(callArgs.source).toBe("WEBAPP");
+ // organizerUuid should NOT be included when organizer hasn't changed (fixed host scenario)
+ expect(callArgs.auditData.organizerUuid).toBeUndefined();
+ expect(callArgs.auditData.reassignmentType).toBe("roundRobin");
+ expect(callArgs.auditData.reassignmentReason).toBe(null);
+ expect(callArgs.auditData.hostAttendeeUpdated).toBeDefined();
+ expect(callArgs.auditData.hostAttendeeUpdated?.withUserUuid).toEqual({
+ old: currentRRHost.uuid,
+ new: newHost.uuid,
+ });
+ });
+});
diff --git a/packages/features/ee/round-robin/roundRobinReassignment.ts b/packages/features/ee/round-robin/roundRobinReassignment.ts
index 46792fa80a..90464a23aa 100644
--- a/packages/features/ee/round-robin/roundRobinReassignment.ts
+++ b/packages/features/ee/round-robin/roundRobinReassignment.ts
@@ -1,10 +1,8 @@
-import { cloneDeep } from "lodash";
-
import {
enrichHostsWithDelegationCredentials,
enrichUserWithDelegationCredentialsIncludeServiceAccountKey,
} from "@calcom/app-store/delegationCredential";
-import { OrganizerDefaultConferencingAppType, getLocationValueForDB } from "@calcom/app-store/locations";
+import { getLocationValueForDB, OrganizerDefaultConferencingAppType } from "@calcom/app-store/locations";
import { eventTypeAppMetadataOptionalSchema } from "@calcom/app-store/zod-utils";
import dayjs from "@calcom/dayjs";
import {
@@ -12,6 +10,9 @@ import {
sendReassignedScheduledEmailsAndSMS,
sendReassignedUpdatedEmailsAndSMS,
} 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 { getAllCredentialsIncludeServiceAccountKey } from "@calcom/features/bookings/lib/getAllCredentialsForUsersOnEvent/getAllCredentials";
import { getBookingResponsesPartialSchema } from "@calcom/features/bookings/lib/getBookingResponsesSchema";
@@ -31,10 +32,10 @@ import logger from "@calcom/lib/logger";
import { getTranslation } from "@calcom/lib/server/i18n";
import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat";
import { prisma } from "@calcom/prisma";
-import { userMetadata as userMetadataSchema } from "@calcom/prisma/zod-utils";
import type { EventTypeMetadata, PlatformClientParams } from "@calcom/prisma/zod-utils";
+import { userMetadata as userMetadataSchema } from "@calcom/prisma/zod-utils";
import type { CalendarEvent } from "@calcom/types/Calendar";
-
+import { cloneDeep } from "lodash";
import { handleRescheduleEventManager } from "./handleRescheduleEventManager";
import { handleWorkflowsUpdate } from "./roundRobinManualReassignment";
import { bookingSelect } from "./utils/bookingSelect";
@@ -47,12 +48,16 @@ export const roundRobinReassignment = async ({
emailsEnabled = true,
platformClientParams,
reassignedById,
+ actionSource,
+ reassignedByUuid,
}: {
bookingId: number;
orgId: number | null;
emailsEnabled?: boolean;
platformClientParams?: PlatformClientParams;
reassignedById: number;
+ actionSource: ValidActionSource;
+ reassignedByUuid: string;
}) => {
const roundRobinReassignLogger = logger.getSubLogger({
prefix: ["roundRobinReassign", `${bookingId}`],
@@ -203,6 +208,8 @@ export const roundRobinReassignment = async ({
const attendeeList = await Promise.all(attendeePromises);
let bookingLocation = booking.location;
+ let attendeeUpdatedId = null;
+ let previousRRHostUserUuid = null;
if (hasOrganizerChanged) {
const bookingResponses = booking.responses;
@@ -268,19 +275,49 @@ export const roundRobinReassignment = async ({
const previousRRHostAttendee = booking.attendees.find(
(attendee) => attendee.email === previousRRHost.email
);
- await prisma.attendee.update({
- where: {
- id: previousRRHostAttendee!.id,
- },
- data: {
- name: reassignedRRHost.name || "",
- email: reassignedRRHost.email,
- timeZone: reassignedRRHost.timeZone,
- locale: reassignedRRHost.locale,
- },
- });
+ if (previousRRHostAttendee) {
+ previousRRHostUserUuid = previousRRHost?.uuid ?? null;
+ attendeeUpdatedId = previousRRHostAttendee.id;
+ await prisma.attendee.update({
+ where: {
+ id: previousRRHostAttendee.id,
+ },
+ data: {
+ name: reassignedRRHost.name || "",
+ email: reassignedRRHost.email,
+ timeZone: reassignedRRHost.timeZone,
+ locale: reassignedRRHost.locale,
+ },
+ });
+ }
}
+ const bookingEventHandlerService = getBookingEventHandlerService();
+ await bookingEventHandlerService.onReassignment({
+ bookingUid: booking.uid,
+ actor: makeUserActor(reassignedByUuid),
+ organizationId: orgId,
+ source: actionSource,
+ auditData: {
+ ...(hasOrganizerChanged
+ ? { organizerUuid: { old: originalOrganizer.uuid, new: reassignedRRHost.uuid } }
+ : null),
+ ...(attendeeUpdatedId
+ ? {
+ hostAttendeeUpdated: {
+ id: attendeeUpdatedId,
+ withUserUuid: {
+ old: previousRRHostUserUuid,
+ new: reassignedRRHost.uuid ?? null,
+ },
+ },
+ }
+ : null),
+ reassignmentReason: null,
+ reassignmentType: "roundRobin",
+ },
+ });
+
roundRobinReassignLogger.info(`Successfully reassigned to user ${reassignedRRHost.id}`);
await AssignmentReasonRecorder.roundRobinReassignment({
diff --git a/packages/features/ee/round-robin/utils/bookingSelect.ts b/packages/features/ee/round-robin/utils/bookingSelect.ts
index 2a636108fa..21759a88e1 100644
--- a/packages/features/ee/round-robin/utils/bookingSelect.ts
+++ b/packages/features/ee/round-robin/utils/bookingSelect.ts
@@ -26,6 +26,7 @@ export const bookingSelect = {
credentials: {
select: credentialForCalendarServiceSelect,
},
+ uuid: true,
},
},
attendees: true,
diff --git a/packages/trpc/server/routers/viewer/teams/roundRobin/roundRobinManualReassign.handler.ts b/packages/trpc/server/routers/viewer/teams/roundRobin/roundRobinManualReassign.handler.ts
index aa4cfcb5d6..f7c23184f8 100644
--- a/packages/trpc/server/routers/viewer/teams/roundRobin/roundRobinManualReassign.handler.ts
+++ b/packages/trpc/server/routers/viewer/teams/roundRobin/roundRobinManualReassign.handler.ts
@@ -33,6 +33,8 @@ export const roundRobinManualReassignHandler = async ({ ctx, input }: RoundRobin
orgId: ctx.user.organizationId,
reassignReason,
reassignedById: ctx.user.id,
+ actionSource: "WEBAPP",
+ reassignedByUuid: ctx.user.uuid,
});
return { success: true };
diff --git a/packages/trpc/server/routers/viewer/teams/roundRobin/roundRobinReassign.handler.ts b/packages/trpc/server/routers/viewer/teams/roundRobin/roundRobinReassign.handler.ts
index e160ae3724..5bf20958b7 100644
--- a/packages/trpc/server/routers/viewer/teams/roundRobin/roundRobinReassign.handler.ts
+++ b/packages/trpc/server/routers/viewer/teams/roundRobin/roundRobinReassign.handler.ts
@@ -1,5 +1,5 @@
-import { roundRobinReassignment } from "@calcom/features/ee/round-robin/roundRobinReassignment";
import { getBookingAccessService } from "@calcom/features/di/containers/BookingAccessService";
+import { roundRobinReassignment } from "@calcom/features/ee/round-robin/roundRobinReassignment";
import type { TrpcSessionUser } from "@calcom/trpc/server/types";
import { TRPCError } from "@trpc/server";
@@ -31,6 +31,8 @@ export const roundRobinReassignHandler = async ({ ctx, input }: RoundRobinReassi
bookingId,
orgId: ctx.user.organizationId,
reassignedById: ctx.user.id,
+ actionSource: "WEBAPP",
+ reassignedByUuid: ctx.user.uuid,
});
};