refactor(booking-audit): discriminated union for displayFields and i18n param support (#27373)

* feat: Add infrastructure for no-show audit integration

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

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

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

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

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

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

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

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

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

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

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

* fix: Add data migration step for deprecated no-show enum values

Addresses Cubic AI review feedback (confidence 9/10): The migration now
includes an UPDATE statement to convert existing records using the
deprecated 'host_no_show_updated' or 'attendee_no_show_updated' enum
values to the new unified 'no_show_updated' value before the type cast.
This prevents migration failures if any existing data uses the old values.

Co-Authored-By: unknown <>

* fix: Use CASE expression in USING clause for enum migration

Fixes PostgreSQL error 'unsafe use of new value of enum type' by avoiding
the ADD VALUE statement and instead using a CASE expression in the ALTER
TABLE USING clause to convert deprecated enum values (host_no_show_updated,
attendee_no_show_updated) to the new unified value (no_show_updated) during
the type conversion.

Co-Authored-By: unknown <>

* fix: Replace hardcoded color with semantic text-success class

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

* fix: Remove color class completely from display fields

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

* feat: Add valuesWithParams support for translatable complex field values

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

* refactor(booking-audit): use discriminated union for displayFields and update consumers

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

* fix: add explicit return type to getBookingHistoryHandler to bust stale tRPC build cache

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

* refactor: replace $t() nested interpolation with separate translation keys and add translationsWithParams tests

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

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Hariom Balhara
2026-02-20 14:34:12 +05:30
committed by GitHub
co-authored by unknown hariom@cal.com Devin AI
parent 12e95a1f47
commit 1aae57daae
9 changed files with 413 additions and 114 deletions
@@ -29,6 +29,17 @@ type TranslationWithParams = {
components?: TranslationComponent[];
};
type DisplayFieldValue =
| { type: "translationKey"; valueKey: string }
| { type: "rawValue"; value: string }
| { type: "rawValues"; values: string[] }
| { type: "translationsWithParams"; valuesWithParams: TranslationWithParams[] };
type DisplayField = {
labelKey: string;
fieldValue: DisplayFieldValue;
};
type AuditLog = {
id: string;
action: string;
@@ -37,7 +48,7 @@ type AuditLog = {
source: string;
displayJson?: Record<string, unknown> | null;
actionDisplayTitle: TranslationWithParams;
displayFields?: Array<{ labelKey: string; valueKey?: string; value?: string; values?: string[] }> | null;
displayFields?: DisplayField[] | null;
actor: {
type: AuditActorType;
displayName: string | null;
@@ -188,30 +199,39 @@ function JsonViewer({ data }: JsonViewerProps) {
);
}
interface DisplayFieldValueProps {
field: {
valueKey?: string;
value?: string;
values?: string[];
};
interface DisplayFieldValueComponentProps {
fieldValue: DisplayFieldValue;
}
function DisplayFieldValue({ field }: DisplayFieldValueProps) {
function DisplayFieldValueComponent({ fieldValue }: DisplayFieldValueComponentProps) {
const { t } = useLocale();
if (field.values) {
return (
<span className="flex flex-col">
{field.values.map((v, i) => (
<span className="p-0.5" key={i}>
{v}
</span>
))}
</span>
);
switch (fieldValue.type) {
case "translationsWithParams":
return (
<span className="flex flex-col">
{fieldValue.valuesWithParams.map((v, i) => (
<span className="p-0.5" key={i}>
{t(v.key, v.params)}
</span>
))}
</span>
);
case "rawValues":
return (
<span className="flex flex-col">
{fieldValue.values.map((v, i) => (
<span className="p-0.5" key={i}>
{v}
</span>
))}
</span>
);
case "rawValue":
return <>{fieldValue.value}</>;
case "translationKey":
return <>{t(fieldValue.valueKey)}</>;
}
return <>{field.value ?? (field.valueKey ? t(field.valueKey) : "")}</>;
}
function BookingLogsTimeline({ logs }: BookingLogsTimelineProps) {
@@ -318,7 +338,7 @@ function BookingLogsTimeline({ logs }: BookingLogsTimelineProps) {
className="flex items-start gap-2 py-2 border-b px-3 border-subtle">
<span className="font-medium text-emphasis w-[140px]">{t(field.labelKey)}</span>
<span className="font-medium">
<DisplayFieldValue field={field} />
<DisplayFieldValueComponent fieldValue={field.fieldValue} />
</span>
</div>
))
@@ -384,18 +404,27 @@ function useBookingLogsFilters(auditLogs: AuditLog[], searchTerm: string, actorF
return (
log.displayFields?.some((field) => {
const searchLower = searchTerm.toLowerCase();
const translatedLabel = field.labelKey ? t(field.labelKey) : "";
const translatedValue = field.valueKey ? t(field.valueKey) : "";
const displayValue = field.value ?? "";
const displayValues = field.values ?? [];
return (
translatedLabel.toLowerCase().includes(searchLower) ||
translatedValue.toLowerCase().includes(searchLower) ||
displayValue.toLowerCase().includes(searchLower) ||
displayValues.some((v) => v.toLowerCase().includes(searchLower))
);
if (translatedLabel.toLowerCase().includes(searchLower)) {
return true;
}
const { fieldValue } = field;
switch (fieldValue.type) {
case "translationKey":
return t(fieldValue.valueKey).toLowerCase().includes(searchLower);
case "rawValue":
return fieldValue.value.toLowerCase().includes(searchLower);
case "rawValues":
return fieldValue.values.some((v) => v.toLowerCase().includes(searchLower));
case "translationsWithParams":
return fieldValue.valuesWithParams.some(
(v) =>
t(v.key, v.params).toLowerCase().includes(searchLower) ||
Object.values(v.params ?? {}).some((param) =>
param?.toString().toLowerCase().includes(searchLower)
)
);
}
}) ?? false
);
};
@@ -4563,7 +4563,14 @@
"assignment_type_round_robin": "Round robin",
"actor_impersonated_by": "Impersonator",
"source": "Source",
"error_processing": "Unable to load details - {{actionType}}"
"error_processing": "Unable to load details - {{actionType}}",
"attendees": "Attendees",
"host": "Host",
"attendee_no_show_status_yes": "{{email}}: Yes",
"attendee_no_show_status_no": "{{email}}: No",
"host_no_show_status_yes": "{{name}}: Yes",
"host_no_show_status_no": "{{name}}: No",
"unknown_user": "Unknown"
},
"error_loading_booking_logs": "Error loading booking logs",
"no_audit_logs_found": "No audit logs found",
@@ -44,6 +44,21 @@ export type GetDisplayFieldsParams = {
dbStore: EnrichmentDataStore;
};
/**
* Discriminated union for display field values
* Each variant represents a different way to render the field value
*/
export type DisplayFieldValue =
| { type: "translationKey"; valueKey: string }
| { type: "rawValue"; value: string }
| { type: "rawValues"; values: string[] }
| { type: "translationsWithParams"; valuesWithParams: TranslationWithParams[] };
export type DisplayField = {
labelKey: string;
fieldValue: DisplayFieldValue;
};
/**
* Interface for Audit Action Services
*
@@ -116,16 +131,9 @@ export interface IAuditActionService {
* Optional - implement only if custom display fields are needed
* @param params - Object containing storedData
* @param params.storedData - Parsed stored data { version, fields }
* @returns Promise of array of field objects with label and value (either translation key or raw value)
* @returns Promise of array of DisplayField objects with label and discriminated value type
*/
getDisplayFields?(params: GetDisplayFieldsParams): Promise<
Array<{
labelKey: string; // Translation key for field label
valueKey?: string; // Translation key for field value (will be translated)
value?: string; // Raw value for field value (will NOT be translated)
values?: string[]; // Array of raw values (will NOT be translated, rendered as separate lines)
}>
>;
getDisplayFields?(params: GetDisplayFieldsParams): Promise<DisplayField[]>;
/**
* Migrate old version data to latest version
@@ -5,6 +5,7 @@ import type { DataRequirements } from "../service/EnrichmentDataStore";
import { AuditActionServiceHelper } from "./AuditActionServiceHelper";
import type {
BaseStoredAuditData,
DisplayField,
GetDisplayFieldsParams,
GetDisplayJsonParams,
GetDisplayTitleParams,
@@ -111,30 +112,47 @@ export class NoShowUpdatedAuditActionService implements IAuditActionService {
throw new Error("Audit action data is invalid");
}
async getDisplayFields({ storedData, dbStore }: GetDisplayFieldsParams): Promise<
Array<{
labelKey: string;
valueKey?: string;
value?: string;
values?: string[];
}>
> {
async getDisplayFields({ storedData, dbStore }: GetDisplayFieldsParams): Promise<DisplayField[]> {
const { fields: parsedFields } = this.parseStored(storedData);
const displayFields: { labelKey: string; valueKey?: string; value?: string; values?: string[] }[] = [];
const displayFields: DisplayField[] = [];
if (this.isAttendeesNoShowSet(parsedFields)) {
const attendeesFieldValues = parsedFields.attendeesNoShow.map((attendee) => {
const noShowStatus = attendee.noShow.new ? "Yes" : "No";
return `${attendee.attendeeEmail}: ${noShowStatus}`;
const attendeesValuesWithParams: TranslationWithParams[] = parsedFields.attendeesNoShow.map((attendee) => ({
key: attendee.noShow.new
? "booking_audit_action.attendee_no_show_status_yes"
: "booking_audit_action.attendee_no_show_status_no",
params: {
email: attendee.attendeeEmail,
},
}));
displayFields.push({
labelKey: "booking_audit_action.attendees",
fieldValue: {
type: "translationsWithParams",
valuesWithParams: attendeesValuesWithParams,
},
});
displayFields.push({ labelKey: "Attendees", values: attendeesFieldValues });
}
if (this.isHostSet(parsedFields)) {
const user = dbStore.getUserByUuid(parsedFields.host.userUuid);
const hostName = user?.name || "Unknown";
const hostFieldValue = `${hostName}:${parsedFields.host.noShow.new ? "Yes" : "No"}`;
displayFields.push({ labelKey: "Host", value: hostFieldValue });
displayFields.push({
labelKey: "booking_audit_action.host",
fieldValue: {
type: "translationsWithParams",
valuesWithParams: [
{
key: parsedFields.host.noShow.new
? "booking_audit_action.host_no_show_status_yes"
: "booking_audit_action.host_no_show_status_no",
params: {
name: hostName,
},
},
],
},
});
}
return displayFields;
@@ -1,9 +1,11 @@
import { z } from "zod";
import { StringChangeSchema } from "../common/changeSchemas";
import type { DataRequirements, EnrichmentDataStore } from "../service/EnrichmentDataStore";
import type { DataRequirements } from "../service/EnrichmentDataStore";
import { AuditActionServiceHelper } from "./AuditActionServiceHelper";
import type {
BaseStoredAuditData,
DisplayField,
DisplayFieldValue,
GetDisplayFieldsParams,
GetDisplayJsonParams,
GetDisplayTitleParams,
@@ -125,12 +127,7 @@ export class ReassignmentAuditActionService implements IAuditActionService {
};
}
async getDisplayFields({ storedData, dbStore }: GetDisplayFieldsParams): Promise<
Array<{
labelKey: string;
valueKey: string;
}>
> {
async getDisplayFields({ storedData, dbStore }: GetDisplayFieldsParams): Promise<DisplayField[]> {
const { fields } = this.parseStored(storedData);
const map = {
manual: "manual",
@@ -139,14 +136,17 @@ export class ReassignmentAuditActionService implements IAuditActionService {
const typeTranslationKey = `booking_audit_action.assignment_type_${map[fields.reassignmentType]}`;
const { previousHostUuid } = this.getHostUuids(fields);
const previousUser = previousHostUuid ? dbStore.getUserByUuid(previousHostUuid) : null;
const previousAssigneeFieldValue: DisplayFieldValue = previousUser?.name
? { type: "rawValue", value: previousUser.name }
: { type: "translationKey", valueKey: "booking_audit_action.unknown_user" };
return [
{
labelKey: "booking_audit_action.assignment_type",
valueKey: typeTranslationKey,
fieldValue: { type: "translationKey", valueKey: typeTranslationKey },
},
{
labelKey: "booking_audit_action.previous_assignee",
valueKey: previousUser?.name ?? "Unknown",
fieldValue: previousAssigneeFieldValue,
},
];
}
@@ -1,58 +1,288 @@
import { describe, expect, it, beforeEach } from "vitest";
import { beforeEach, describe, expect, it } from "vitest";
import { verifyDataRequirementsContract } from "./contractVerification";
import { createMockEnrichmentDataStore, verifyDataRequirementsContract } from "./contractVerification";
import { NoShowUpdatedAuditActionService } from "../NoShowUpdatedAuditActionService";
describe("NoShowUpdatedAuditActionService - getDataRequirements contract", () => {
describe("NoShowUpdatedAuditActionService", () => {
let service: NoShowUpdatedAuditActionService;
beforeEach(() => {
service = new NoShowUpdatedAuditActionService();
});
it("should declare exactly the userUuids accessed when host is set", async () => {
const storedData = {
version: 1,
fields: {
host: {
userUuid: "host-uuid-789",
noShow: { old: false, new: true },
describe("getDataRequirements contract", () => {
it("should declare exactly the userUuids accessed when host is set", async () => {
const storedData = {
version: 1,
fields: {
host: {
userUuid: "host-uuid-789",
noShow: { old: false, new: true },
},
},
},
};
};
const { errors, accessedData } = await verifyDataRequirementsContract(service, storedData);
expect(errors).toEqual([]);
expect(accessedData.userUuids.size).toBe(1);
const { errors, accessedData } = await verifyDataRequirementsContract(service, storedData);
expect(errors).toEqual([]);
expect(accessedData.userUuids.size).toBe(1);
});
it("should declare empty userUuids when only attendees are set", async () => {
const storedData = {
version: 1,
fields: {
attendeesNoShow: [{ attendeeEmail: "attendee@example.com", noShow: { old: false, new: true } }],
},
};
const { errors, accessedData } = await verifyDataRequirementsContract(service, storedData);
expect(errors).toEqual([]);
expect(accessedData.userUuids.size).toBe(0);
});
it("should declare exactly the userUuids accessed when both host and attendees are set", async () => {
const storedData = {
version: 1,
fields: {
host: {
userUuid: "host-uuid-abc",
noShow: { old: null, new: true },
},
attendeesNoShow: [{ attendeeEmail: "attendee@example.com", noShow: { old: false, new: true } }],
},
};
const { errors, accessedData } = await verifyDataRequirementsContract(service, storedData);
expect(errors).toEqual([]);
expect(accessedData.userUuids.size).toBe(1);
});
});
it("should declare empty userUuids when only attendees are set", async () => {
const storedData = {
version: 1,
fields: {
attendeesNoShow: [{ attendeeEmail: "attendee@example.com", noShow: { old: false, new: true } }],
},
};
const { errors, accessedData } = await verifyDataRequirementsContract(service, storedData);
expect(errors).toEqual([]);
expect(accessedData.userUuids.size).toBe(0);
});
it("should declare exactly the userUuids accessed when both host and attendees are set", async () => {
const storedData = {
version: 1,
fields: {
host: {
userUuid: "host-uuid-abc",
noShow: { old: null, new: true },
describe("getDisplayFields", () => {
it("should return attendee no-show fields with translationsWithParams using _yes key", async () => {
const storedData = {
version: 1,
fields: {
attendeesNoShow: [{ attendeeEmail: "alice@example.com", noShow: { old: false, new: true } }],
},
attendeesNoShow: [{ attendeeEmail: "attendee@example.com", noShow: { old: false, new: true } }],
},
};
};
const { errors, accessedData } = await verifyDataRequirementsContract(service, storedData);
expect(errors).toEqual([]);
expect(accessedData.userUuids.size).toBe(1);
const dbStore = createMockEnrichmentDataStore({ users: [] }, {});
const result = await service.getDisplayFields({ storedData, dbStore });
expect(result).toEqual([
{
labelKey: "booking_audit_action.attendees",
fieldValue: {
type: "translationsWithParams",
valuesWithParams: [
{
key: "booking_audit_action.attendee_no_show_status_yes",
params: { email: "alice@example.com" },
},
],
},
},
]);
});
it("should return attendee no-show fields with _no key when noShow is false", async () => {
const storedData = {
version: 1,
fields: {
attendeesNoShow: [
{ attendeeEmail: "bob@example.com", noShow: { old: true, new: false } },
],
},
};
const dbStore = createMockEnrichmentDataStore({ users: [] }, {});
const result = await service.getDisplayFields({ storedData, dbStore });
expect(result).toEqual([
{
labelKey: "booking_audit_action.attendees",
fieldValue: {
type: "translationsWithParams",
valuesWithParams: [
{
key: "booking_audit_action.attendee_no_show_status_no",
params: { email: "bob@example.com" },
},
],
},
},
]);
});
it("should return multiple attendee entries with correct keys per attendee", async () => {
const storedData = {
version: 1,
fields: {
attendeesNoShow: [
{ attendeeEmail: "alice@example.com", noShow: { old: false, new: true } },
{ attendeeEmail: "bob@example.com", noShow: { old: true, new: false } },
],
},
};
const dbStore = createMockEnrichmentDataStore({ users: [] }, {});
const result = await service.getDisplayFields({ storedData, dbStore });
expect(result).toEqual([
{
labelKey: "booking_audit_action.attendees",
fieldValue: {
type: "translationsWithParams",
valuesWithParams: [
{
key: "booking_audit_action.attendee_no_show_status_yes",
params: { email: "alice@example.com" },
},
{
key: "booking_audit_action.attendee_no_show_status_no",
params: { email: "bob@example.com" },
},
],
},
},
]);
});
it("should return host no-show field with _yes key when host marked as no-show", async () => {
const storedData = {
version: 1,
fields: {
host: {
userUuid: "host-uuid",
noShow: { old: false, new: true },
},
},
};
const dbStore = createMockEnrichmentDataStore(
{
users: [{ id: 1, uuid: "host-uuid", name: "Host Name", email: "host@example.com", avatarUrl: null }],
},
{ userUuids: ["host-uuid"] }
);
const result = await service.getDisplayFields({ storedData, dbStore });
expect(result).toEqual([
{
labelKey: "booking_audit_action.host",
fieldValue: {
type: "translationsWithParams",
valuesWithParams: [
{
key: "booking_audit_action.host_no_show_status_yes",
params: { name: "Host Name" },
},
],
},
},
]);
});
it("should return host no-show field with _no key when no-show cleared", async () => {
const storedData = {
version: 1,
fields: {
host: {
userUuid: "host-uuid",
noShow: { old: true, new: false },
},
},
};
const dbStore = createMockEnrichmentDataStore(
{
users: [{ id: 1, uuid: "host-uuid", name: "Host Name", email: "host@example.com", avatarUrl: null }],
},
{ userUuids: ["host-uuid"] }
);
const result = await service.getDisplayFields({ storedData, dbStore });
expect(result).toEqual([
{
labelKey: "booking_audit_action.host",
fieldValue: {
type: "translationsWithParams",
valuesWithParams: [
{
key: "booking_audit_action.host_no_show_status_no",
params: { name: "Host Name" },
},
],
},
},
]);
});
it("should use 'Unknown' for host name when user not found", async () => {
const storedData = {
version: 1,
fields: {
host: {
userUuid: "missing-uuid",
noShow: { old: false, new: true },
},
},
};
const dbStore = createMockEnrichmentDataStore(
{ users: [] },
{ userUuids: ["missing-uuid"] }
);
const result = await service.getDisplayFields({ storedData, dbStore });
expect(result).toEqual([
{
labelKey: "booking_audit_action.host",
fieldValue: {
type: "translationsWithParams",
valuesWithParams: [
{
key: "booking_audit_action.host_no_show_status_yes",
params: { name: "Unknown" },
},
],
},
},
]);
});
it("should return both attendee and host fields when both are set", async () => {
const storedData = {
version: 1,
fields: {
host: {
userUuid: "host-uuid",
noShow: { old: null, new: true },
},
attendeesNoShow: [
{ attendeeEmail: "alice@example.com", noShow: { old: false, new: true } },
],
},
};
const dbStore = createMockEnrichmentDataStore(
{
users: [{ id: 1, uuid: "host-uuid", name: "Host Name", email: "host@example.com", avatarUrl: null }],
},
{ userUuids: ["host-uuid"] }
);
const result = await service.getDisplayFields({ storedData, dbStore });
expect(result).toHaveLength(2);
expect(result[0].labelKey).toBe("booking_audit_action.attendees");
expect(result[0].fieldValue.type).toBe("translationsWithParams");
expect(result[1].labelKey).toBe("booking_audit_action.host");
expect(result[1].fieldValue.type).toBe("translationsWithParams");
});
});
});
@@ -356,11 +356,11 @@ describe("ReassignmentAuditActionService", () => {
expect(result).toEqual([
{
labelKey: "booking_audit_action.assignment_type",
valueKey: "booking_audit_action.assignment_type_manual",
fieldValue: { type: "translationKey", valueKey: "booking_audit_action.assignment_type_manual" },
},
{
labelKey: "booking_audit_action.previous_assignee",
valueKey: "Previous Host",
fieldValue: { type: "rawValue", value: "Previous Host" },
},
]);
});
@@ -396,11 +396,11 @@ describe("ReassignmentAuditActionService", () => {
expect(result).toEqual([
{
labelKey: "booking_audit_action.assignment_type",
valueKey: "booking_audit_action.assignment_type_round_robin",
fieldValue: { type: "translationKey", valueKey: "booking_audit_action.assignment_type_round_robin" },
},
{
labelKey: "booking_audit_action.previous_assignee",
valueKey: "Previous Host",
fieldValue: { type: "rawValue", value: "Previous Host" },
},
]);
});
@@ -4,7 +4,7 @@ import type { CredentialRepository } from "@calcom/features/credentials/reposito
import type { ISimpleLogger } from "@calcom/features/di/shared/services/logger.service";
import type { MembershipRepository } from "@calcom/features/membership/repositories/MembershipRepository";
import type { UserRepository } from "@calcom/features/users/repositories/UserRepository";
import type { TranslationWithParams } from "../actions/IAuditActionService";
import type { DisplayField, TranslationWithParams } from "../actions/IAuditActionService";
import { RescheduledAuditActionService } from "../actions/RescheduledAuditActionService";
import type { BookingAuditContext } from "../dto/types";
import type { AuditActorType } from "../repository/IAuditActorRepository";
@@ -41,7 +41,7 @@ type EnrichedAuditLog = {
operationId: string;
displayJson?: Record<string, unknown> | null;
actionDisplayTitle: TranslationWithParams;
displayFields?: Array<{ labelKey: string; valueKey?: string; value?: string; values?: string[] }> | null;
displayFields?: DisplayField[] | null;
actor: {
id: string;
type: AuditActorType;
@@ -4,6 +4,7 @@ import type { PrismaClient } from "@calcom/prisma/client";
import { TRPCError } from "@trpc/server";
import { getBookingHistoryViewerService } from "@calcom/features/booking-audit/di/BookingHistoryViewerService.container";
import type { DisplayBookingAuditLog } from "@calcom/features/booking-audit/lib/service/BookingAuditViewerService";
import {
BookingAuditErrorCode,
BookingAuditPermissionError,
@@ -38,7 +39,13 @@ const getErrorMessage = (code: BookingAuditErrorCode, t: TFunction): string => {
}
};
export const getBookingHistoryHandler = async ({ ctx, input }: GetBookingHistoryOptions) => {
export const getBookingHistoryHandler = async ({
ctx,
input,
}: GetBookingHistoryOptions): Promise<{
bookingUid: string;
auditLogs: DisplayBookingAuditLog[];
}> => {
const { user } = ctx;
const { bookingUid } = input;