* 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>
451 lines
14 KiB
TypeScript
451 lines
14 KiB
TypeScript
import { beforeEach, describe, expect, it } from "vitest";
|
|
|
|
import { ReassignmentAuditActionService } from "../ReassignmentAuditActionService";
|
|
import { createMockEnrichmentDataStore, verifyDataRequirementsContract } from "./contractVerification";
|
|
|
|
describe("ReassignmentAuditActionService", () => {
|
|
let service: ReassignmentAuditActionService;
|
|
|
|
beforeEach(() => {
|
|
service = new ReassignmentAuditActionService();
|
|
});
|
|
|
|
describe("getVersionedData", () => {
|
|
it("should wrap fields with version number", () => {
|
|
const fields = {
|
|
organizerUuid: { old: "organizer-old", new: "organizer-new" },
|
|
reassignmentReason: "Host unavailable",
|
|
reassignmentType: "manual" as const,
|
|
};
|
|
|
|
const result = service.getVersionedData(fields);
|
|
|
|
expect(result).toEqual({
|
|
version: 1,
|
|
fields,
|
|
});
|
|
});
|
|
|
|
it("should handle roundRobin reassignment type with attendee update", () => {
|
|
const fields = {
|
|
organizerUuid: { old: "fixed-host", new: "fixed-host" },
|
|
hostAttendeeUpdated: {
|
|
id: 123,
|
|
withUserUuid: { old: "old-rr-host", new: "new-rr-host" },
|
|
},
|
|
reassignmentReason: null,
|
|
reassignmentType: "roundRobin" as const,
|
|
};
|
|
|
|
const result = service.getVersionedData(fields);
|
|
|
|
expect(result).toEqual({
|
|
version: 1,
|
|
fields,
|
|
});
|
|
});
|
|
|
|
it("should handle null reassignment reason", () => {
|
|
const fields = {
|
|
organizerUuid: { old: "organizer-old", new: "organizer-new" },
|
|
reassignmentReason: null,
|
|
reassignmentType: "manual" as const,
|
|
};
|
|
|
|
const result = service.getVersionedData(fields);
|
|
|
|
expect(result).toEqual({
|
|
version: 1,
|
|
fields,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("parseStored", () => {
|
|
it("should parse valid stored data", () => {
|
|
const storedData = {
|
|
version: 1,
|
|
fields: {
|
|
organizerUuid: { old: "organizer-old", new: "organizer-new" },
|
|
reassignmentReason: "Host unavailable",
|
|
reassignmentType: "manual",
|
|
},
|
|
};
|
|
|
|
const result = service.parseStored(storedData);
|
|
|
|
expect(result.version).toBe(1);
|
|
expect(result.fields).toEqual({
|
|
organizerUuid: { old: "organizer-old", new: "organizer-new" },
|
|
reassignmentReason: "Host unavailable",
|
|
reassignmentType: "manual",
|
|
});
|
|
});
|
|
|
|
it("should throw error for invalid data", () => {
|
|
const invalidData = {
|
|
version: 1,
|
|
fields: {
|
|
reassignmentReason: "Host unavailable",
|
|
},
|
|
};
|
|
|
|
expect(() => service.parseStored(invalidData)).toThrow();
|
|
});
|
|
});
|
|
|
|
describe("getVersion", () => {
|
|
it("should return version from stored data", () => {
|
|
const storedData = {
|
|
version: 1,
|
|
fields: {
|
|
organizerUuid: { old: "organizer-old", new: "organizer-new" },
|
|
reassignmentReason: null,
|
|
reassignmentType: "manual",
|
|
},
|
|
};
|
|
|
|
const result = service.getVersion(storedData);
|
|
|
|
expect(result).toBe(1);
|
|
});
|
|
});
|
|
|
|
describe("migrateToLatest", () => {
|
|
it("should return data as-is for V1 (no migration needed)", () => {
|
|
const fields = {
|
|
organizerUuid: { old: "organizer-old", new: "organizer-new" },
|
|
reassignmentReason: "Host unavailable",
|
|
reassignmentType: "manual" as const,
|
|
};
|
|
|
|
const result = service.migrateToLatest(fields);
|
|
|
|
expect(result.isMigrated).toBe(false);
|
|
expect(result.latestData).toEqual(fields);
|
|
});
|
|
});
|
|
|
|
describe("getDataRequirements", () => {
|
|
it("should return userUuids for organizer change", () => {
|
|
const storedData = {
|
|
version: 1,
|
|
fields: {
|
|
organizerUuid: { old: "organizer-old", new: "organizer-new" },
|
|
reassignmentReason: "Host unavailable",
|
|
reassignmentType: "manual",
|
|
},
|
|
};
|
|
|
|
const result = service.getDataRequirements(storedData);
|
|
|
|
expect(result.userUuids).toContain("organizer-new");
|
|
expect(result.userUuids).toContain("organizer-old");
|
|
});
|
|
|
|
it("should return userUuids for attendee update", () => {
|
|
const storedData = {
|
|
version: 1,
|
|
fields: {
|
|
organizerUuid: { old: "fixed-host", new: "fixed-host" },
|
|
hostAttendeeUpdated: {
|
|
id: 123,
|
|
withUserUuid: { old: "old-rr-host", new: "new-rr-host" },
|
|
},
|
|
reassignmentReason: null,
|
|
reassignmentType: "roundRobin",
|
|
},
|
|
};
|
|
|
|
const result = service.getDataRequirements(storedData);
|
|
|
|
expect(result.userUuids).toContain("new-rr-host");
|
|
expect(result.userUuids).toContain("old-rr-host");
|
|
});
|
|
});
|
|
|
|
describe("getDisplayTitle", () => {
|
|
it("should return translation key with host name for manual reassignment (organizer changed)", async () => {
|
|
const storedData = {
|
|
version: 1,
|
|
fields: {
|
|
organizerUuid: { old: "organizer-old", new: "organizer-new" },
|
|
reassignmentReason: "Host unavailable",
|
|
reassignmentType: "manual",
|
|
},
|
|
};
|
|
|
|
const dbStore = createMockEnrichmentDataStore(
|
|
{
|
|
users: [
|
|
{ id: 1, uuid: "organizer-new", name: "New Host", email: "new@example.com", avatarUrl: null },
|
|
{ id: 2, uuid: "organizer-old", name: "Old Host", email: "old@example.com", avatarUrl: null },
|
|
],
|
|
},
|
|
{ userUuids: ["organizer-new", "organizer-old"] }
|
|
);
|
|
|
|
const result = await service.getDisplayTitle({ storedData, dbStore, userTimeZone: "UTC" });
|
|
|
|
expect(result).toEqual({
|
|
key: "booking_audit_action.booking_reassigned_to_host",
|
|
params: { host: "New Host" },
|
|
});
|
|
});
|
|
|
|
it("should return host name when attendee was updated (fixed-host scenario)", async () => {
|
|
const storedData = {
|
|
version: 1,
|
|
fields: {
|
|
organizerUuid: { old: "fixed-host", new: "fixed-host" },
|
|
hostAttendeeUpdated: {
|
|
id: 123,
|
|
withUserUuid: { old: "old-rr-host", new: "new-rr-host" },
|
|
},
|
|
reassignmentReason: null,
|
|
reassignmentType: "roundRobin",
|
|
},
|
|
};
|
|
|
|
const dbStore = createMockEnrichmentDataStore(
|
|
{
|
|
users: [
|
|
{ id: 1, uuid: "new-rr-host", name: "New RR Host", email: "newrr@example.com", avatarUrl: null },
|
|
{ id: 2, uuid: "old-rr-host", name: "Old RR Host", email: "oldrr@example.com", avatarUrl: null },
|
|
],
|
|
},
|
|
{ userUuids: ["new-rr-host", "old-rr-host"] }
|
|
);
|
|
|
|
const result = await service.getDisplayTitle({ storedData, dbStore, userTimeZone: "UTC" });
|
|
|
|
expect(result).toEqual({
|
|
key: "booking_audit_action.booking_reassigned_to_host",
|
|
params: { host: "New RR Host" },
|
|
});
|
|
});
|
|
|
|
it("should return 'Unknown' when user not found", async () => {
|
|
const storedData = {
|
|
version: 1,
|
|
fields: {
|
|
organizerUuid: { old: "organizer-old", new: "organizer-new" },
|
|
reassignmentReason: null,
|
|
reassignmentType: "roundRobin",
|
|
},
|
|
};
|
|
|
|
const dbStore = createMockEnrichmentDataStore(
|
|
{ users: [] },
|
|
{ userUuids: ["organizer-new", "organizer-old"] }
|
|
);
|
|
|
|
const result = await service.getDisplayTitle({ storedData, dbStore, userTimeZone: "UTC" });
|
|
|
|
expect(result).toEqual({
|
|
key: "booking_audit_action.booking_reassigned_to_host",
|
|
params: { host: "Unknown" },
|
|
});
|
|
});
|
|
|
|
it("should return 'Unknown' when user has no name", async () => {
|
|
const storedData = {
|
|
version: 1,
|
|
fields: {
|
|
organizerUuid: { old: "organizer-old", new: "organizer-new" },
|
|
reassignmentReason: null,
|
|
reassignmentType: "manual",
|
|
},
|
|
};
|
|
|
|
const dbStore = createMockEnrichmentDataStore(
|
|
{
|
|
users: [
|
|
{ id: 1, uuid: "organizer-new", name: null, email: "new@example.com", avatarUrl: null },
|
|
{ id: 2, uuid: "organizer-old", name: "Old Host", email: "old@example.com", avatarUrl: null },
|
|
],
|
|
},
|
|
{ userUuids: ["organizer-new", "organizer-old"] }
|
|
);
|
|
|
|
const result = await service.getDisplayTitle({ storedData, dbStore, userTimeZone: "UTC" });
|
|
|
|
expect(result).toEqual({
|
|
key: "booking_audit_action.booking_reassigned_to_host",
|
|
params: { host: "Unknown" },
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("getDisplayJson", () => {
|
|
it("should return display data for manual reassignment (organizer changed)", () => {
|
|
const storedData = {
|
|
version: 1,
|
|
fields: {
|
|
organizerUuid: { old: "organizer-old", new: "organizer-new" },
|
|
reassignmentReason: "Host unavailable",
|
|
reassignmentType: "manual",
|
|
},
|
|
};
|
|
|
|
const result = service.getDisplayJson({ storedData, userTimeZone: "UTC" });
|
|
|
|
expect(result).toEqual({
|
|
newOrganizerUuid: "organizer-new",
|
|
previousOrganizerUuid: "organizer-old",
|
|
reassignmentReason: "Host unavailable",
|
|
});
|
|
});
|
|
|
|
it("should return display data for fixed-host reassignment (attendee updated)", () => {
|
|
const storedData = {
|
|
version: 1,
|
|
fields: {
|
|
organizerUuid: { old: "fixed-host-uuid", new: "fixed-host-uuid" },
|
|
hostAttendeeUpdated: {
|
|
id: 123,
|
|
withUserUuid: { old: "old-rr-host-uuid", new: "new-rr-host-uuid" },
|
|
},
|
|
reassignmentReason: null,
|
|
reassignmentType: "roundRobin",
|
|
},
|
|
};
|
|
|
|
const result = service.getDisplayJson({ storedData, userTimeZone: "UTC" });
|
|
|
|
expect(result).toEqual({
|
|
newOrganizerUuid: "fixed-host-uuid",
|
|
previousOrganizerUuid: "fixed-host-uuid",
|
|
hostAttendeeIdUpdated: 123,
|
|
hostAttendeeUserUuidNew: "new-rr-host-uuid",
|
|
hostAttendeeUserUuidOld: "old-rr-host-uuid",
|
|
reassignmentReason: null,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("getDisplayFields", () => {
|
|
it("should return assignment type field for manual reassignment", async () => {
|
|
const storedData = {
|
|
version: 1,
|
|
fields: {
|
|
organizerUuid: { old: "organizer-old", new: "organizer-new" },
|
|
reassignmentReason: "Host unavailable",
|
|
reassignmentType: "manual",
|
|
},
|
|
};
|
|
|
|
const dbStore = createMockEnrichmentDataStore(
|
|
{
|
|
users: [
|
|
{
|
|
id: 1,
|
|
uuid: "organizer-old",
|
|
name: "Previous Host",
|
|
email: "old@example.com",
|
|
avatarUrl: null,
|
|
},
|
|
{ id: 2, uuid: "organizer-new", name: "New Host", email: "new@example.com", avatarUrl: null },
|
|
],
|
|
},
|
|
{ userUuids: ["organizer-old", "organizer-new"] }
|
|
);
|
|
|
|
const result = await service.getDisplayFields({ storedData, dbStore });
|
|
|
|
expect(result).toEqual([
|
|
{
|
|
labelKey: "booking_audit_action.assignment_type",
|
|
fieldValue: { type: "translationKey", valueKey: "booking_audit_action.assignment_type_manual" },
|
|
},
|
|
{
|
|
labelKey: "booking_audit_action.previous_assignee",
|
|
fieldValue: { type: "rawValue", value: "Previous Host" },
|
|
},
|
|
]);
|
|
});
|
|
|
|
it("should return assignment type field for round robin reassignment", async () => {
|
|
const storedData = {
|
|
version: 1,
|
|
fields: {
|
|
organizerUuid: { old: "organizer-old", new: "organizer-new" },
|
|
reassignmentReason: null,
|
|
reassignmentType: "roundRobin",
|
|
},
|
|
};
|
|
|
|
const dbStore = createMockEnrichmentDataStore(
|
|
{
|
|
users: [
|
|
{
|
|
id: 1,
|
|
uuid: "organizer-old",
|
|
name: "Previous Host",
|
|
email: "old@example.com",
|
|
avatarUrl: null,
|
|
},
|
|
{ id: 2, uuid: "organizer-new", name: "New Host", email: "new@example.com", avatarUrl: null },
|
|
],
|
|
},
|
|
{ userUuids: ["organizer-old", "organizer-new"] }
|
|
);
|
|
|
|
const result = await service.getDisplayFields({ storedData, dbStore });
|
|
|
|
expect(result).toEqual([
|
|
{
|
|
labelKey: "booking_audit_action.assignment_type",
|
|
fieldValue: { type: "translationKey", valueKey: "booking_audit_action.assignment_type_round_robin" },
|
|
},
|
|
{
|
|
labelKey: "booking_audit_action.previous_assignee",
|
|
fieldValue: { type: "rawValue", value: "Previous Host" },
|
|
},
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe("TYPE constant", () => {
|
|
it("should have correct type value", () => {
|
|
expect(ReassignmentAuditActionService.TYPE).toBe("REASSIGNMENT");
|
|
});
|
|
});
|
|
|
|
describe("getDataRequirements contract verification", () => {
|
|
it("should declare exactly the userUuids accessed for organizer change", async () => {
|
|
const storedData = {
|
|
version: 1,
|
|
fields: {
|
|
organizerUuid: { old: "organizer-old", new: "organizer-new" },
|
|
reassignmentReason: "Host unavailable",
|
|
reassignmentType: "manual",
|
|
},
|
|
};
|
|
|
|
const { errors, accessedData } = await verifyDataRequirementsContract(service, storedData);
|
|
expect(errors).toEqual([]);
|
|
expect(accessedData.userUuids.size).toBe(2);
|
|
});
|
|
|
|
it("should declare exactly the userUuids accessed for attendee update", async () => {
|
|
const storedData = {
|
|
version: 1,
|
|
fields: {
|
|
organizerUuid: { old: "fixed-host", new: "fixed-host" },
|
|
hostAttendeeUpdated: {
|
|
id: 123,
|
|
withUserUuid: { old: "old-rr-host", new: "new-rr-host" },
|
|
},
|
|
reassignmentReason: null,
|
|
reassignmentType: "roundRobin",
|
|
},
|
|
};
|
|
|
|
const { errors, accessedData } = await verifyDataRequirementsContract(service, storedData);
|
|
expect(errors).toEqual([]);
|
|
expect(accessedData.userUuids.size).toBe(2);
|
|
});
|
|
});
|
|
});
|