1aae57daae
* 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>
165 lines
5.7 KiB
TypeScript
165 lines
5.7 KiB
TypeScript
import { z } from "zod";
|
|
import { StringChangeSchema } from "../common/changeSchemas";
|
|
import type { DataRequirements } from "../service/EnrichmentDataStore";
|
|
import { AuditActionServiceHelper } from "./AuditActionServiceHelper";
|
|
import type {
|
|
BaseStoredAuditData,
|
|
DisplayField,
|
|
DisplayFieldValue,
|
|
GetDisplayFieldsParams,
|
|
GetDisplayJsonParams,
|
|
GetDisplayTitleParams,
|
|
IAuditActionService,
|
|
TranslationWithParams,
|
|
} from "./IAuditActionService";
|
|
|
|
/**
|
|
* Reassignment Audit Action Service
|
|
* Handles REASSIGNMENT action with per-action versioning
|
|
*/
|
|
|
|
// Module-level because it is passed to IAuditActionService type outside the class scope
|
|
const fieldsSchemaV1 = z.object({
|
|
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,
|
|
});
|
|
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() {
|
|
this.helper = new AuditActionServiceHelper({
|
|
latestVersion: this.VERSION,
|
|
latestFieldsSchema: ReassignmentAuditActionService.latestFieldsSchema,
|
|
storedDataSchema: ReassignmentAuditActionService.storedDataSchema,
|
|
});
|
|
}
|
|
|
|
getVersionedData(fields: unknown) {
|
|
return this.helper.getVersionedData(fields);
|
|
}
|
|
|
|
parseStored(data: unknown) {
|
|
return this.helper.parseStored(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 };
|
|
}
|
|
|
|
getDataRequirements(storedData: BaseStoredAuditData): DataRequirements {
|
|
const { fields } = this.parseStored(storedData);
|
|
const userUuids: string[] = [];
|
|
const { newHostUuid, previousHostUuid } = this.getHostUuids(fields);
|
|
if (newHostUuid) userUuids.push(newHostUuid);
|
|
if (previousHostUuid) userUuids.push(previousHostUuid);
|
|
return { userUuids };
|
|
}
|
|
|
|
private getHostUuids(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 { newHostUuid, previousHostUuid };
|
|
}
|
|
|
|
async getDisplayTitle({ storedData, dbStore }: GetDisplayTitleParams): Promise<TranslationWithParams> {
|
|
const { fields } = this.parseStored(storedData);
|
|
const { newHostUuid } = this.getHostUuids(fields);
|
|
const newUser = newHostUuid ? dbStore.getUserByUuid(newHostUuid) : null;
|
|
const reassignedToName = newUser?.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,
|
|
}
|
|
: {}),
|
|
|
|
...(fields.hostAttendeeUpdated
|
|
? {
|
|
hostAttendeeIdUpdated: fields.hostAttendeeUpdated.id,
|
|
hostAttendeeUserUuidNew: fields.hostAttendeeUpdated.withUserUuid?.new,
|
|
hostAttendeeUserUuidOld: fields.hostAttendeeUpdated.withUserUuid?.old,
|
|
}
|
|
: {}),
|
|
reassignmentReason: fields.reassignmentReason ?? null,
|
|
};
|
|
}
|
|
|
|
async getDisplayFields({ storedData, dbStore }: GetDisplayFieldsParams): Promise<DisplayField[]> {
|
|
const { fields } = this.parseStored(storedData);
|
|
const map = {
|
|
manual: "manual",
|
|
roundRobin: "round_robin",
|
|
};
|
|
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",
|
|
fieldValue: { type: "translationKey", valueKey: typeTranslationKey },
|
|
},
|
|
{
|
|
labelKey: "booking_audit_action.previous_assignee",
|
|
fieldValue: previousAssigneeFieldValue,
|
|
},
|
|
];
|
|
}
|
|
}
|
|
|
|
export type ReassignmentAuditData = z.infer<typeof fieldsSchemaV1>;
|
|
|
|
export type ReassignmentAuditDisplayData = {
|
|
previousOrganizerUuid?: string | null;
|
|
newOrganizerUuid?: string | null;
|
|
hostAttendeeIdUpdated?: number | null;
|
|
hostAttendeeUserUuidNew?: string | null;
|
|
hostAttendeeUserUuidOld?: string | null;
|
|
reassignmentReason: string | null;
|
|
};
|