Files
cal.diy/packages/features/booking-audit/lib/service/BookingHistoryViewerService.ts
T
Hariom BalharaandDevin AI 9d39590510 feat: create BookingHistoryViewerService to combine audit logs with routing form submissions (#26045)
* feat: create BookingHistoryViewerService to combine audit logs with routing form submissions

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

* refactor(booking): rename audit log query and enhance type safety

- Updated the booking logs view to use the new getBookingHistory query instead of getAuditLogs.
- Introduced DisplayBookingAuditLog type for improved clarity in BookingAuditViewerService.
- Refactored BookingHistoryViewerService to utilize the new DisplayBookingAuditLog type and added sorting functionality for history logs.
- Adjusted related types and methods to ensure consistency across services.

* refactor(routing-forms): streamline imports and enhance type definitions

- Consolidated type exports and imports from the features library for better organization.
- Removed redundant type definitions and functions in zod.ts, findFieldValueByIdentifier.ts, getFieldIdentifier.ts, and parseRoutingFormResponse.ts.
- Introduced new utility functions for handling field responses and parsing routing form responses.
- Improved type safety and clarity across routing form response handling.

* fix: remove double prefix from uniqueId in form submission entry

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

* 1c97f9cc5d50416788c01876fe539bcc9750e9b2 (#26453)

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-01-05 11:11:05 +00:00

106 lines
4.2 KiB
TypeScript

import type { RoutingFormResponseRepositoryInterface } from "@calcom/lib/server/repository/RoutingFormResponseRepository.interface";
import type { BookingAuditViewerService, DisplayBookingAuditLog } from "./BookingAuditViewerService";
import { getFieldResponseByIdentifier } from "@calcom/features/routing-forms/lib/getFieldResponseByIdentifier";
type GetHistoryForBookingParams = {
bookingUid: string;
userId: number;
userEmail: string;
userTimeZone: string;
organizationId: number | null;
};
type BookingHistoryLog = DisplayBookingAuditLog;
interface BookingHistoryViewerServiceDeps {
bookingAuditViewerService: BookingAuditViewerService;
routingFormResponseRepository: RoutingFormResponseRepositoryInterface;
}
export class BookingHistoryViewerService {
private readonly bookingAuditViewerService: BookingAuditViewerService;
private readonly routingFormResponseRepository: RoutingFormResponseRepositoryInterface;
constructor(private readonly deps: BookingHistoryViewerServiceDeps) {
this.bookingAuditViewerService = deps.bookingAuditViewerService;
this.routingFormResponseRepository = deps.routingFormResponseRepository;
}
private sortLogsReverseChronologically(historyLogs: BookingHistoryLog[]): BookingHistoryLog[] {
return historyLogs.sort((a, b) => {
const timestampA = new Date(a.timestamp).getTime();
const timestampB = new Date(b.timestamp).getTime();
return timestampB - timestampA;
});
}
private async getFormAuditLogsForBooking(bookingUid: string): Promise<BookingHistoryLog[]> {
// TODO: Form doesn't have its Audit Logs yet, so we replicate them using the Form Response directly for now.
const formResponse = await this.routingFormResponseRepository.findByBookingUidIncludeForm(bookingUid);
if (!formResponse) {
return [];
}
return [this.createFormSubmissionEntry({ formResponse, bookingUid })];
}
async getHistoryForBooking(
params: GetHistoryForBookingParams
): Promise<{ bookingUid: string; auditLogs: BookingHistoryLog[] }> {
const { bookingUid } = params;
const { auditLogs: bookingAuditLogs } = await this.bookingAuditViewerService.getAuditLogsForBooking(params);
const historyEntries: BookingHistoryLog[] = [...bookingAuditLogs, ...await this.getFormAuditLogsForBooking(bookingUid)];
const sortedLogs = this.sortLogsReverseChronologically(historyEntries);
return {
bookingUid,
auditLogs: sortedLogs,
};
}
private createFormSubmissionEntry({
formResponse,
bookingUid,
}: {
formResponse: NonNullable<
Awaited<ReturnType<RoutingFormResponseRepositoryInterface["findByBookingUidIncludeForm"]>>
>;
bookingUid: string;
}): BookingHistoryLog {
const timestamp = formResponse.createdAt.toISOString();
const emailFieldResult = getFieldResponseByIdentifier({ responsePayload: formResponse.response, formFields: formResponse.form.fields, identifier: "email" });
const emailFieldValueFromResponse = emailFieldResult.success ? emailFieldResult.data : null;
// A valid string can be the email otherwise we assume it is not an email
const submitterEmail = typeof emailFieldValueFromResponse === "string" ? emailFieldValueFromResponse : null;
const uniqueId = `form-submission-${formResponse.id}`;
return {
id: uniqueId,
bookingUid,
type: "RECORD_CREATED",
action: "CREATED",
timestamp,
createdAt: timestamp,
source: "WEBAPP",
operationId: uniqueId,
displayJson: null,
actionDisplayTitle: { key: "form_submitted" },
displayFields: null,
actor: {
id: `form-submission-actor-${formResponse.id}`,
type: "GUEST",
userUuid: null,
attendeeId: null,
name: null,
createdAt: formResponse.createdAt,
displayName: submitterEmail ? `${submitterEmail}` : "Guest",
displayEmail: submitterEmail || null,
displayAvatar: null,
},
};
}
}