* Integrate creation/rescheduling booking audit * fix: add missing hostUserUuid to booking audit test data Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com> * fix-ci * feat: enhance booking audit with seat reference - Added support for seat reference in booking audit actions. - Updated localization for booking creation to include seat information. - Modified relevant services to pass attendee seat ID during booking creation. * fix: update test data to match schema requirements - Add seatReferenceUid: null to default mock audit log data - Add seatReferenceUid: null to multiple audit logs test case - Convert SEAT_RESCHEDULED test data to use numeric timestamps instead of ISO strings Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com> * Allow nullish seatReferenceUid * feat: enhance booking audit to support rescheduledBy information - Updated booking audit actions to include rescheduledBy details, allowing tracking of who rescheduled a booking. - Refactored related services to accommodate the new rescheduledBy parameter in booking events. - Adjusted type definitions and function signatures to reflect the changes in the booking audit context. * Avoid possible run time issue * Fix imoport path * fix failing test due to merge from main\ * Pass useruuid * Refactor getDisplayTitle method signatures to use _params for consistency across audit action services. Update IAuditActionService to include dbStore in GetDisplayTitleParams for improved data handling during title retrieval. * fix: extend bulk-fetching optimization to actor and impersonator enrichment - Collect actor userUuids (for USER type actors) in collectDataRequirements - Collect impersonator UUIDs from log.context.impersonatedBy - Update enrichActorInformation to use dbStore.getUserByUuid() for USER actors - Update enrichImpersonator to use dbStore.getUserByUuid() - Update tests to verify findByUuids is called with correct aggregated UUIDs Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com> * 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: Remove dbStore usage from audit action services after merge - Update CreatedAuditActionService to use userRepository.findByUuid directly - Remove unused dbStore parameter from RescheduledAuditActionService - Sync test file with prepare-for-no-show-audit branch Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com> * fix: Restore dbStore bulk-fetching optimization pattern - Add dbStore parameter and getDataRequirements method to IAuditActionService interface - Update CreatedAuditActionService to use dbStore.getUserByUuid() for bulk-fetched data - Update RescheduledAuditActionService to accept dbStore parameter - Update BookingAuditViewerService to: - Collect data requirements from all audit logs - Bulk-fetch users with findByUuids() before enrichment - Pass dbStore to action services and enrichment methods - Update tests to verify findByUuids is called for bulk-fetching optimization Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com> * fix: Remove duplicate fireBookingEvents method in RecurringBookingService Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com> * revert: Remove formatting-only changes in audit action services Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com> * feat: Make getDataRequirements required and fetch rescheduled logs early - Make getDataRequirements a required method in IAuditActionService interface - Add getDataRequirements to all action services that didn't have it - Fetch rescheduled logs early to include their data requirements in bulk fetch - Update ReassignmentAuditActionService to use dbStore pattern instead of repository - Update NoShowUpdatedAuditActionService to use dbStore pattern - Update tests to use new dbStore pattern - Handle invalid data gracefully in collectDataRequirements Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com> * Remve unncessary variable * feat: Implement Actor Strategy pattern for bulk-fetching attendees and credentials - Add ActorStrategies.ts with strategy pattern for all 5 actor types - Extend EnrichmentDataStore with StoredAttendee and StoredCredential types - Add findByIds method to CredentialRepository for bulk-fetching - Update BookingAuditViewerService to use strategy pattern: - Replace switch statement in enrichActorInformation with strategy dispatch - Update collectDataRequirements to collect attendeeIds and credentialIds - Update buildEnrichmentDataStore to bulk-fetch in parallel - Update tests to use findByIds mock and reflect graceful handling of missing data Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com> * fix: Restore error-throwing for actors with missing required IDs - USER actor now throws error when userUuid is missing - ATTENDEE actor now throws error when attendeeId is missing - Updated tests to expect hasError: true for fallback logs Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com> * fix: Restore original test assertions, only update mocks for bulk methods Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com> * test: Add contract verification tests for getDataRequirements Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com> * refactor: Clean up imports and improve type definitions in BookingAuditTaskConsumer and BookingAuditViewerService - Consolidated import statements for better readability - Enhanced type definitions for clarity and consistency - Updated method signatures to use destructured parameters for improved maintainability Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com> * test: Split contract verification tests into separate files per action service Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com> * test: Rename contract test files to .test.ts and merge into existing test file Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com> * feat: Add runtime validation to EnrichmentDataStore for undeclared data access Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com> * refactor: Make declaredRequirements required and add ensureFetched helper Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com> * refactor: Simplify EnrichmentDataStore with constructor + fetch() pattern - Remove unused userIds and bookingUids from DataRequirements - Remove unused getUserById and getBookingByUid methods - Refactor constructor to accept requirements and repositories - Add fetch() method that fetches data and populates maps - Use Map keys as source of truth for declared requirements - Update getters to throw when accessing undeclared data - Add comprehensive unit tests for EnrichmentDataStore - Update BookingAuditViewerService to use new API - Update contractVerification.ts to remove unused tracking Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com> * refactor: Clean up imports and improve type definitions in BookingAuditTaskConsumer and BookingAuditViewerService - Consolidated import statements for better readability - Enhanced type definitions for clarity and consistency - Updated method signatures to use destructured parameters for improved maintainability Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com> * fix: Include contextRequirements in mergeDataRequirements and add accessedData assertions - Fix bug where contextRequirements (impersonator UUIDs) were not merged - Add accessedData assertions to CreatedAuditActionService.test.ts - Add accessedData assertions to ReassignmentAuditActionService.test.ts Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com> --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
466 lines
15 KiB
TypeScript
466 lines
15 KiB
TypeScript
import type { IAttendeeRepository } from "@calcom/features/bookings/repositories/IAttendeeRepository";
|
|
import type { IFeaturesRepository } from "@calcom/features/flags/features.repository.interface";
|
|
import { Task } from "@calcom/features/tasker/repository";
|
|
import type { UserRepository } from "@calcom/features/users/repositories/UserRepository";
|
|
import logger from "@calcom/lib/logger";
|
|
import { safeStringify } from "@calcom/lib/safeStringify";
|
|
import type { JsonValue } from "@calcom/types/Json";
|
|
import type { BookingAuditContext, PiiFreeActor } from "../dto/types";
|
|
import type { IAuditActorRepository } from "../repository/IAuditActorRepository";
|
|
import type {
|
|
BookingAuditAction,
|
|
BookingAuditCreateInput,
|
|
BookingAuditType,
|
|
IBookingAuditRepository,
|
|
} from "../repository/IBookingAuditRepository";
|
|
import type { ActionSource } from "../types/actionSource";
|
|
import type {
|
|
BookingAuditTaskConsumerPayload,
|
|
BulkBookingAuditTaskConsumerPayload,
|
|
SingleBookingAuditTaskConsumerPayload,
|
|
} from "../types/bookingAuditTask";
|
|
import { BookingAuditActionServiceRegistry } from "./BookingAuditActionServiceRegistry";
|
|
|
|
interface BookingAuditTaskConsumerDeps {
|
|
bookingAuditRepository: IBookingAuditRepository;
|
|
auditActorRepository: IAuditActorRepository;
|
|
featuresRepository: IFeaturesRepository;
|
|
attendeeRepository: IAttendeeRepository;
|
|
userRepository: UserRepository;
|
|
}
|
|
|
|
type CreateBookingAuditInput = {
|
|
bookingUid: string;
|
|
actorId: string;
|
|
type: BookingAuditType;
|
|
action: BookingAuditAction;
|
|
source: ActionSource;
|
|
operationId: string;
|
|
data: JsonValue;
|
|
timestamp: Date; // Required: actual time of the booking change (business event)
|
|
context?: BookingAuditContext;
|
|
};
|
|
|
|
type BookingAudit = {
|
|
id: string;
|
|
bookingUid: string;
|
|
actorId: string;
|
|
type: BookingAuditType;
|
|
action: BookingAuditAction;
|
|
timestamp: Date;
|
|
createdAt: Date;
|
|
updatedAt: Date;
|
|
data: JsonValue;
|
|
};
|
|
|
|
/**
|
|
* BookingAuditTaskConsumer - Task consumer for processing booking audit tasks
|
|
* Handles all audit processing logic including feature flag checks and routing to action handlers
|
|
* Designed to be deployed separately (e.g., as trigger.dev job) with minimal dependencies
|
|
*
|
|
* Note: PENDING and AWAITING_HOST actions are intentionally not implemented.
|
|
* These represent initial booking states captured by the CREATED action.
|
|
*
|
|
* Dependency Injection Note:
|
|
* - `userRepository` is included in this service's dependencies because it's required by
|
|
* ActionServices (e.g., ReassignmentAuditActionService) that need to fetch user data.
|
|
* - Currently, dependencies are passed down the flow: TaskConsumer → Registry → ActionService
|
|
* - Future improvement: Consider creating a container/module system for action services
|
|
* that can build instances directly with their dependencies, eliminating the need to
|
|
* pass dependencies through intermediate layers.
|
|
*/
|
|
export class BookingAuditTaskConsumer {
|
|
private readonly actionServiceRegistry: BookingAuditActionServiceRegistry;
|
|
private readonly bookingAuditRepository: IBookingAuditRepository;
|
|
private readonly auditActorRepository: IAuditActorRepository;
|
|
private readonly featuresRepository: IFeaturesRepository;
|
|
|
|
constructor(deps: BookingAuditTaskConsumerDeps) {
|
|
this.bookingAuditRepository = deps.bookingAuditRepository;
|
|
this.auditActorRepository = deps.auditActorRepository;
|
|
this.featuresRepository = deps.featuresRepository;
|
|
|
|
this.actionServiceRegistry = new BookingAuditActionServiceRegistry();
|
|
}
|
|
|
|
/**
|
|
* Process single booking Audit Task
|
|
*/
|
|
async processAuditTask(payload: SingleBookingAuditTaskConsumerPayload, taskId: string): Promise<void> {
|
|
const { action, bookingUid, actor, organizationId, data, timestamp, source, operationId, context } =
|
|
payload;
|
|
|
|
if (
|
|
!(await this.shouldProcessAudit({
|
|
organizationId,
|
|
action,
|
|
bookingUids: [bookingUid],
|
|
}))
|
|
) {
|
|
return;
|
|
}
|
|
|
|
const dataInLatestFormat = await this.migrateIfNeeded({ action, data, payload, taskId });
|
|
|
|
await this.onBookingAction({
|
|
bookingUid,
|
|
actor,
|
|
action,
|
|
source,
|
|
operationId,
|
|
data: dataInLatestFormat,
|
|
timestamp,
|
|
context,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Process Bulk bookings Audit Task
|
|
*/
|
|
async processBulkAuditTask(payload: BulkBookingAuditTaskConsumerPayload, taskId: string): Promise<void> {
|
|
const { bookings, action, actor, organizationId, timestamp, source, operationId, context } = payload;
|
|
|
|
if (
|
|
!(await this.shouldProcessAudit({
|
|
organizationId,
|
|
action,
|
|
bookingUids: bookings.map((booking) => booking.bookingUid),
|
|
}))
|
|
) {
|
|
return;
|
|
}
|
|
|
|
const migratedBookings = await this.bulkMigrateIfNeeded({
|
|
bookings,
|
|
action,
|
|
payload,
|
|
taskId,
|
|
});
|
|
|
|
await this.onBulkBookingActions({
|
|
bookings: migratedBookings,
|
|
actor,
|
|
action,
|
|
source,
|
|
operationId,
|
|
timestamp,
|
|
context,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Migrate If Needed - Validates and migrates data to latest version
|
|
*
|
|
* This is where action-specific data validation happens.
|
|
* The action service's migrateToLatest method:
|
|
* - Validates the data against all supported versions
|
|
* - Migrates to latest version if needed
|
|
* - Returns validated data with migration status
|
|
*
|
|
* If migration occurred, updates the task payload in DB for retries.
|
|
*/
|
|
private async migrateIfNeeded(params: {
|
|
action: BookingAuditAction;
|
|
data: unknown;
|
|
payload: SingleBookingAuditTaskConsumerPayload;
|
|
taskId: string;
|
|
}) {
|
|
const { action, data, payload, taskId } = params;
|
|
const actionService = this.actionServiceRegistry.getActionService(action);
|
|
|
|
// migrateToLatest validates data with action-specific schema and migrates if needed
|
|
const migrationResult = actionService.migrateToLatest(data);
|
|
|
|
// If migrated, update task payload in DB
|
|
if (migrationResult.isMigrated) {
|
|
logger.info(`Schema migration performed: action=${action}`);
|
|
await this.updateTaskPayload(payload, migrationResult.latestData, taskId);
|
|
}
|
|
|
|
return migrationResult.latestData;
|
|
}
|
|
|
|
/**
|
|
* Update Task Payload - Updates the task payload in DB with migrated data
|
|
*
|
|
* This ensures that task retries use the latest schema version.
|
|
* When a task fails and retries, it will use the already-migrated payload.
|
|
*
|
|
* @param payload - Original task payload
|
|
* @param latestData - Migrated data in latest schema version
|
|
* @param taskId - Task ID (required for DB update)
|
|
*/
|
|
private async updateTaskPayload(
|
|
payload: BookingAuditTaskConsumerPayload,
|
|
latestData: unknown,
|
|
taskId: string
|
|
): Promise<void> {
|
|
try {
|
|
const updatedPayload = { ...payload, data: latestData };
|
|
await Task.updatePayload(taskId, JSON.stringify(updatedPayload));
|
|
|
|
logger.info(`Successfully updated task payload in DB: taskId=${taskId}, action=${payload.action}`);
|
|
} catch (error) {
|
|
// Warning because nothing functionally failed, we have a risk in future that if we completely remove a version support, the particular task if retried will fail with a schema error.
|
|
logger.warn(`Failed to update task payload in DB: taskId=${taskId}, error=${safeStringify(error)}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Bulk Migrate If Needed - Migrates all bookings in a bulk operation
|
|
*
|
|
* Since all bookings in a bulk operation share the same action type,
|
|
* they all use the same action service and schema version.
|
|
* If any booking is migrated, updates the DB task payload for retry consistency.
|
|
*
|
|
* @param params - Bookings, action, payload, and task ID
|
|
* @returns Array of migrated bookings with latest data
|
|
*/
|
|
private async bulkMigrateIfNeeded(params: {
|
|
bookings: BulkBookingAuditTaskConsumerPayload["bookings"];
|
|
action: BookingAuditAction;
|
|
payload: BulkBookingAuditTaskConsumerPayload;
|
|
taskId: string;
|
|
}): Promise<Array<{ bookingUid: string; data: Record<string, unknown> }>> {
|
|
const { bookings, action, payload, taskId } = params;
|
|
const actionService = this.actionServiceRegistry.getActionService(action);
|
|
|
|
let anyMigrated = false;
|
|
const migratedBookings = await Promise.all(
|
|
bookings.map(async (booking) => {
|
|
const migrationResult = actionService.migrateToLatest(booking.data);
|
|
if (migrationResult.isMigrated) {
|
|
anyMigrated = true;
|
|
}
|
|
return {
|
|
bookingUid: booking.bookingUid,
|
|
data: migrationResult.latestData,
|
|
};
|
|
})
|
|
);
|
|
|
|
if (anyMigrated) {
|
|
logger.info(`Schema migration performed for bulk audit: action=${action}`);
|
|
await this.updateBulkTaskPayload(payload, migratedBookings, taskId);
|
|
}
|
|
|
|
return migratedBookings;
|
|
}
|
|
|
|
private async updateBulkTaskPayload(
|
|
payload: BulkBookingAuditTaskConsumerPayload,
|
|
migratedBookings: Array<{ bookingUid: string; data: Record<string, unknown> }>,
|
|
taskId: string
|
|
): Promise<void> {
|
|
try {
|
|
const updatedPayload = { ...payload, bookings: migratedBookings };
|
|
await Task.updatePayload(taskId, JSON.stringify(updatedPayload));
|
|
|
|
logger.info(`Successfully updated bulk task payload in DB: taskId=${taskId}, action=${payload.action}`);
|
|
} catch (error) {
|
|
// Warning because nothing functionally failed, we have a risk in future that if we completely remove a version support, the particular task if retried will fail with a schema error.
|
|
logger.warn(
|
|
`Failed to update bulk task payload in DB: taskId=${taskId}, error=${safeStringify(error)}`
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if audit should be processed based on organization and feature flag
|
|
*
|
|
* @param params - Organization ID, action, and context for logging
|
|
* @returns true if audit should be processed, false otherwise
|
|
*/
|
|
private async shouldProcessAudit(params: {
|
|
organizationId: number | null;
|
|
action: BookingAuditAction;
|
|
bookingUids: string[];
|
|
}): Promise<boolean> {
|
|
const { organizationId, action, bookingUids } = params;
|
|
|
|
if (organizationId === null) {
|
|
logger.debug(
|
|
`Skipping audit for non-organization booking: action=${action}, bookingUids=${bookingUids.join(",")}`
|
|
);
|
|
return false;
|
|
}
|
|
|
|
const isFeatureEnabled = await this.featuresRepository.checkIfTeamHasFeature(
|
|
organizationId,
|
|
"booking-audit"
|
|
);
|
|
|
|
if (!isFeatureEnabled) {
|
|
logger.debug(
|
|
`booking-audit feature is disabled for organization: action=${action}, bookingUids=${bookingUids.join(",")}, organizationId=${organizationId}`
|
|
);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Resolves an Actor to an actor ID in the AuditActor table
|
|
* Handles different actor types appropriately (upsert, lookup, or direct ID)
|
|
*/
|
|
private async resolveActorId(actor: PiiFreeActor): Promise<string> {
|
|
switch (actor.identifiedBy) {
|
|
case "id":
|
|
return actor.id;
|
|
case "user": {
|
|
const userActor = await this.auditActorRepository.createIfNotExistsUserActor({
|
|
userUuid: actor.userUuid,
|
|
});
|
|
return userActor.id;
|
|
}
|
|
case "attendee": {
|
|
const attendeeActor = await this.auditActorRepository.createIfNotExistsAttendeeActor({
|
|
attendeeId: actor.attendeeId,
|
|
});
|
|
return attendeeActor.id;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates a booking audit record
|
|
* Action services handle their own version wrapping
|
|
*/
|
|
private async createAuditRecord(input: CreateBookingAuditInput): Promise<BookingAudit> {
|
|
logger.info(
|
|
"Creating audit record",
|
|
safeStringify({
|
|
bookingUid: input.bookingUid,
|
|
actorId: input.actorId,
|
|
type: input.type,
|
|
action: input.action,
|
|
source: input.source,
|
|
timestamp: input.timestamp,
|
|
context: input.context,
|
|
})
|
|
);
|
|
|
|
return this.bookingAuditRepository.create({
|
|
bookingUid: input.bookingUid,
|
|
actorId: input.actorId,
|
|
type: input.type,
|
|
action: input.action,
|
|
source: input.source,
|
|
timestamp: input.timestamp,
|
|
operationId: input.operationId,
|
|
data: input.data ?? null,
|
|
context: input.context,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get Record Type - Derives the record type from the action
|
|
*
|
|
* Maps booking actions to their corresponding audit record types:
|
|
* - CREATED → RECORD_CREATED
|
|
* - All other actions (CANCELLED, RESCHEDULED, etc.) → RECORD_UPDATED
|
|
* - Future actions like DELETED → RECORD_DELETED
|
|
*
|
|
* @param action - The booking audit action
|
|
* @returns The corresponding record type
|
|
*/
|
|
private getRecordType(params: { action: BookingAuditAction }): BookingAuditType {
|
|
const { action } = params;
|
|
|
|
switch (action) {
|
|
case "CREATED":
|
|
return "RECORD_CREATED";
|
|
|
|
// All update actions fall through to return RECORD_UPDATED
|
|
case "CANCELLED":
|
|
case "ACCEPTED":
|
|
case "REJECTED":
|
|
case "RESCHEDULED":
|
|
case "RESCHEDULE_REQUESTED":
|
|
case "ATTENDEE_ADDED":
|
|
case "ATTENDEE_REMOVED":
|
|
case "REASSIGNMENT":
|
|
case "LOCATION_CHANGED":
|
|
case "NO_SHOW_UPDATED":
|
|
case "SEAT_BOOKED":
|
|
case "SEAT_RESCHEDULED":
|
|
return "RECORD_UPDATED";
|
|
|
|
// PENDING and AWAITING_HOST are not implemented as they represent initial states
|
|
case "PENDING":
|
|
case "AWAITING_HOST":
|
|
throw new Error(
|
|
`Action ${action} is not supported - it represents an initial booking state captured by CREATED`
|
|
);
|
|
}
|
|
}
|
|
|
|
private async onBulkBookingActions(params: {
|
|
bookings: Array<{ bookingUid: string; data: Record<string, unknown> }>;
|
|
actor: PiiFreeActor;
|
|
action: BookingAuditAction;
|
|
source: ActionSource;
|
|
operationId: string;
|
|
timestamp: number;
|
|
context?: BookingAuditContext;
|
|
}): Promise<void> {
|
|
const { bookings, actor, action, source, operationId, timestamp, context } = params;
|
|
|
|
const actorId = await this.resolveActorId(actor);
|
|
const recordType = this.getRecordType({ action });
|
|
const actionService = this.actionServiceRegistry.getActionService(action);
|
|
|
|
const auditRecordsToCreate: BookingAuditCreateInput[] = bookings.map((booking) => {
|
|
const versionedData = actionService.getVersionedData(booking.data);
|
|
return {
|
|
bookingUid: booking.bookingUid,
|
|
actorId,
|
|
type: recordType,
|
|
action,
|
|
source,
|
|
operationId,
|
|
data: versionedData as JsonValue,
|
|
timestamp: new Date(timestamp),
|
|
context,
|
|
};
|
|
});
|
|
|
|
await this.bookingAuditRepository.createMany(auditRecordsToCreate);
|
|
|
|
logger.info(
|
|
`Successfully created ${auditRecordsToCreate.length} bulk audit records: action=${action}, operationId=${operationId}`
|
|
);
|
|
}
|
|
|
|
async onBookingAction(params: {
|
|
bookingUid: string;
|
|
actor: PiiFreeActor;
|
|
action: BookingAuditAction;
|
|
source: ActionSource;
|
|
operationId: string;
|
|
data: Record<string, unknown>;
|
|
timestamp: number;
|
|
context?: BookingAuditContext;
|
|
}): Promise<BookingAudit> {
|
|
const { bookingUid, actor, action, source, operationId, data, timestamp, context } = params;
|
|
const actionService = this.actionServiceRegistry.getActionService(action);
|
|
const versionedData = actionService.getVersionedData(data);
|
|
const actorId = await this.resolveActorId(actor);
|
|
const recordType = this.getRecordType({ action });
|
|
|
|
return this.createAuditRecord({
|
|
bookingUid,
|
|
actorId,
|
|
type: recordType,
|
|
action,
|
|
source,
|
|
operationId,
|
|
// versionedData is { version: number; fields: unknown } which is JsonValue-compatible
|
|
data: versionedData as JsonValue,
|
|
timestamp: new Date(timestamp),
|
|
context,
|
|
});
|
|
}
|
|
}
|