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:
co-authored by
unknown
hariom@cal.com
Devin AI
parent
12e95a1f47
commit
1aae57daae
@@ -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,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
+271
-41
@@ -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");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+4
-4
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user