Files
calendar/packages/features/booking-audit/lib/actions/__tests__/ReassignmentAuditActionService.test.ts
T
Hariom BalharaGitHubunknown <>hariom@cal.com <hariombalhara@gmail.com>hariom@cal.com <hariombalhara@gmail.com>hariom@cal.com <hariombalhara@gmail.com>hariom@cal.com <hariombalhara@gmail.com>hariom@cal.com <hariombalhara@gmail.com>hariom@cal.com <hariombalhara@gmail.com>Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
1aae57daae 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>
2026-02-20 14:34:12 +05:30

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);
});
});
});