diff --git a/apps/web/modules/booking-audit/components/BookingHistory.tsx b/apps/web/modules/booking-audit/components/BookingHistory.tsx index 0889689d64..151ef66357 100644 --- a/apps/web/modules/booking-audit/components/BookingHistory.tsx +++ b/apps/web/modules/booking-audit/components/BookingHistory.tsx @@ -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 | 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 ( - - {field.values.map((v, i) => ( - - {v} - - ))} - - ); + switch (fieldValue.type) { + case "translationsWithParams": + return ( + + {fieldValue.valuesWithParams.map((v, i) => ( + + {t(v.key, v.params)} + + ))} + + ); + case "rawValues": + return ( + + {fieldValue.values.map((v, i) => ( + + {v} + + ))} + + ); + 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"> {t(field.labelKey)} - + )) @@ -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 ); }; diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index 9241dd7988..2baacd646a 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -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", diff --git a/packages/features/booking-audit/lib/actions/IAuditActionService.ts b/packages/features/booking-audit/lib/actions/IAuditActionService.ts index c9e3209b37..1dcd92b3ff 100644 --- a/packages/features/booking-audit/lib/actions/IAuditActionService.ts +++ b/packages/features/booking-audit/lib/actions/IAuditActionService.ts @@ -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; /** * Migrate old version data to latest version diff --git a/packages/features/booking-audit/lib/actions/NoShowUpdatedAuditActionService.ts b/packages/features/booking-audit/lib/actions/NoShowUpdatedAuditActionService.ts index 7cec3f7e78..a58ad657dd 100644 --- a/packages/features/booking-audit/lib/actions/NoShowUpdatedAuditActionService.ts +++ b/packages/features/booking-audit/lib/actions/NoShowUpdatedAuditActionService.ts @@ -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 { 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; diff --git a/packages/features/booking-audit/lib/actions/ReassignmentAuditActionService.ts b/packages/features/booking-audit/lib/actions/ReassignmentAuditActionService.ts index 20457fe488..36310f03ce 100644 --- a/packages/features/booking-audit/lib/actions/ReassignmentAuditActionService.ts +++ b/packages/features/booking-audit/lib/actions/ReassignmentAuditActionService.ts @@ -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 { 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, }, ]; } diff --git a/packages/features/booking-audit/lib/actions/__tests__/NoShowUpdatedAuditActionService.test.ts b/packages/features/booking-audit/lib/actions/__tests__/NoShowUpdatedAuditActionService.test.ts index 748b7b3c5a..90bbc8addb 100644 --- a/packages/features/booking-audit/lib/actions/__tests__/NoShowUpdatedAuditActionService.test.ts +++ b/packages/features/booking-audit/lib/actions/__tests__/NoShowUpdatedAuditActionService.test.ts @@ -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"); + }); }); }); diff --git a/packages/features/booking-audit/lib/actions/__tests__/ReassignmentAuditActionService.test.ts b/packages/features/booking-audit/lib/actions/__tests__/ReassignmentAuditActionService.test.ts index 13655bcaef..d8b18f230d 100644 --- a/packages/features/booking-audit/lib/actions/__tests__/ReassignmentAuditActionService.test.ts +++ b/packages/features/booking-audit/lib/actions/__tests__/ReassignmentAuditActionService.test.ts @@ -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" }, }, ]); }); diff --git a/packages/features/booking-audit/lib/service/BookingAuditViewerService.ts b/packages/features/booking-audit/lib/service/BookingAuditViewerService.ts index ffcfcf6ef1..8378e3fd02 100644 --- a/packages/features/booking-audit/lib/service/BookingAuditViewerService.ts +++ b/packages/features/booking-audit/lib/service/BookingAuditViewerService.ts @@ -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 | null; actionDisplayTitle: TranslationWithParams; - displayFields?: Array<{ labelKey: string; valueKey?: string; value?: string; values?: string[] }> | null; + displayFields?: DisplayField[] | null; actor: { id: string; type: AuditActorType; diff --git a/packages/trpc/server/routers/viewer/bookings/getBookingHistory.handler.ts b/packages/trpc/server/routers/viewer/bookings/getBookingHistory.handler.ts index 0958761936..0e05722f47 100644 --- a/packages/trpc/server/routers/viewer/bookings/getBookingHistory.handler.ts +++ b/packages/trpc/server/routers/viewer/bookings/getBookingHistory.handler.ts @@ -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;