feat: add booking audit action services for lib/actions folder (#25720)
Extract lib/actions folder changes from booking-audit-more-infra branch: - AcceptedAuditActionService - AttendeeAddedAuditActionService - AttendeeNoShowUpdatedAuditActionService - AttendeeRemovedAuditActionService - CancelledAuditActionService - CreatedAuditActionService (modified) - HostNoShowUpdatedAuditActionService - IAuditActionService (modified) - LocationChangedAuditActionService - ReassignmentAuditActionService - RejectedAuditActionService - RescheduleRequestedAuditActionService - RescheduledAuditActionService Also includes common/changeSchemas.ts required for type-checks to pass. Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
Devin AI
parent
257a49ce88
commit
f350542d0d
@@ -361,20 +361,20 @@ Used when a booking status changes to accepted.
|
||||
#### ATTENDEE_ADDED
|
||||
```typescript
|
||||
{
|
||||
addedAttendees // { old: null, new: ["email@example.com", ...] }
|
||||
attendees // { old: ["email1@example.com", ...], new: ["email1@example.com", "email2@example.com", ...] }
|
||||
}
|
||||
```
|
||||
|
||||
Tracks attendee(s) that were added in this action. Old value is null since we're tracking the delta, not full state.
|
||||
Tracks attendee(s) that were added in this action. The field stores the state change: `old` contains attendees before addition, `new` contains attendees after addition. The actual added attendees are computed as the difference (new - old).
|
||||
|
||||
#### ATTENDEE_REMOVED
|
||||
```typescript
|
||||
{
|
||||
removedAttendees // { old: null, new: ["email@example.com", ...] }
|
||||
attendees // { old: ["email1@example.com", ...], new: ["email2@example.com", ...] }
|
||||
}
|
||||
```
|
||||
|
||||
Tracks attendee(s) that were removed in this action. Old value is null since we're tracking the delta, not full state.
|
||||
Tracks attendee(s) that were removed in this action. The field stores the state change: `old` contains attendees before removal, `new` contains remaining attendees after removal. The actual removed attendees are computed as the difference (old - new).
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { StringChangeSchema } from "../common/changeSchemas";
|
||||
import { AuditActionServiceHelper } from "./AuditActionServiceHelper";
|
||||
import type { IAuditActionService, TranslationWithParams } from "./IAuditActionService";
|
||||
|
||||
/**
|
||||
* Accepted Audit Action Service
|
||||
* Handles ACCEPTED action with per-action versioning
|
||||
*/
|
||||
|
||||
// Module-level because it is passed to IAuditActionService type outside the class scope
|
||||
const fieldsSchemaV1 = z.object({
|
||||
status: StringChangeSchema,
|
||||
});
|
||||
|
||||
export class AcceptedAuditActionService
|
||||
implements IAuditActionService<typeof fieldsSchemaV1, typeof fieldsSchemaV1> {
|
||||
readonly VERSION = 1;
|
||||
public static readonly TYPE = "ACCEPTED" 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 = AcceptedAuditActionService.dataSchemaV1;
|
||||
// Union of all versions
|
||||
public static readonly storedFieldsSchema = AcceptedAuditActionService.fieldsSchemaV1;
|
||||
private helper: AuditActionServiceHelper<
|
||||
typeof AcceptedAuditActionService.latestFieldsSchema,
|
||||
typeof AcceptedAuditActionService.storedDataSchema
|
||||
>;
|
||||
|
||||
constructor() {
|
||||
this.helper = new AuditActionServiceHelper({
|
||||
latestVersion: this.VERSION,
|
||||
latestFieldsSchema: AcceptedAuditActionService.latestFieldsSchema,
|
||||
storedDataSchema: AcceptedAuditActionService.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 };
|
||||
}
|
||||
|
||||
async getDisplayTitle(): Promise<TranslationWithParams> {
|
||||
return { key: "booking_audit_action.accepted" };
|
||||
}
|
||||
|
||||
getDisplayJson(storedData: { version: number; fields: z.infer<typeof fieldsSchemaV1> }): AcceptedAuditDisplayData {
|
||||
const { fields } = storedData;
|
||||
return {
|
||||
previousStatus: fields.status.old ?? null,
|
||||
newStatus: fields.status.new ?? null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export type AcceptedAuditData = z.infer<typeof fieldsSchemaV1>;
|
||||
|
||||
export type AcceptedAuditDisplayData = {
|
||||
previousStatus: string | null;
|
||||
newStatus: string | null;
|
||||
};
|
||||
@@ -0,0 +1,82 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { StringArrayChangeSchema } from "../common/changeSchemas";
|
||||
import { AuditActionServiceHelper } from "./AuditActionServiceHelper";
|
||||
import type { IAuditActionService, TranslationWithParams } from "./IAuditActionService";
|
||||
|
||||
/**
|
||||
* Attendee Added Audit Action Service
|
||||
* Handles ATTENDEE_ADDED action with per-action versioning
|
||||
*/
|
||||
|
||||
// Module-level because it is passed to IAuditActionService type outside the class scope
|
||||
const fieldsSchemaV1 = z.object({
|
||||
attendees: StringArrayChangeSchema,
|
||||
});
|
||||
|
||||
export class AttendeeAddedAuditActionService
|
||||
implements IAuditActionService<typeof fieldsSchemaV1, typeof fieldsSchemaV1> {
|
||||
readonly VERSION = 1;
|
||||
public static readonly TYPE = "ATTENDEE_ADDED" 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 = AttendeeAddedAuditActionService.dataSchemaV1;
|
||||
// Union of all versions
|
||||
public static readonly storedFieldsSchema = AttendeeAddedAuditActionService.fieldsSchemaV1;
|
||||
private helper: AuditActionServiceHelper<
|
||||
typeof AttendeeAddedAuditActionService.latestFieldsSchema,
|
||||
typeof AttendeeAddedAuditActionService.storedDataSchema
|
||||
>;
|
||||
|
||||
constructor() {
|
||||
this.helper = new AuditActionServiceHelper({
|
||||
latestVersion: this.VERSION,
|
||||
latestFieldsSchema: AttendeeAddedAuditActionService.latestFieldsSchema,
|
||||
storedDataSchema: AttendeeAddedAuditActionService.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 };
|
||||
}
|
||||
|
||||
async getDisplayTitle(): Promise<TranslationWithParams> {
|
||||
return { key: "booking_audit_action.attendee_added" };
|
||||
}
|
||||
|
||||
getDisplayJson(storedData: { version: number; fields: z.infer<typeof fieldsSchemaV1> }): AttendeeAddedAuditDisplayData {
|
||||
const { fields } = storedData;
|
||||
const previousAttendeesSet = new Set(fields.attendees.old ?? []);
|
||||
const addedAttendees = fields.attendees.new.filter(
|
||||
(email) => !previousAttendeesSet.has(email)
|
||||
);
|
||||
return {
|
||||
addedAttendees,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export type AttendeeAddedAuditData = z.infer<typeof fieldsSchemaV1>;
|
||||
|
||||
export type AttendeeAddedAuditDisplayData = {
|
||||
addedAttendees: string[];
|
||||
};
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { BooleanChangeSchema } from "../common/changeSchemas";
|
||||
import { AuditActionServiceHelper } from "./AuditActionServiceHelper";
|
||||
import type { IAuditActionService, TranslationWithParams } from "./IAuditActionService";
|
||||
|
||||
/**
|
||||
* Attendee No-Show Updated Audit Action Service
|
||||
* Handles ATTENDEE_NO_SHOW_UPDATED action with per-action versioning
|
||||
*/
|
||||
|
||||
// Module-level because it is passed to IAuditActionService type outside the class scope
|
||||
const fieldsSchemaV1 = z.object({
|
||||
noShowAttendee: BooleanChangeSchema,
|
||||
});
|
||||
|
||||
export class AttendeeNoShowUpdatedAuditActionService
|
||||
implements IAuditActionService<typeof fieldsSchemaV1, typeof fieldsSchemaV1> {
|
||||
readonly VERSION = 1;
|
||||
public static readonly TYPE = "ATTENDEE_NO_SHOW_UPDATED" 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 = AttendeeNoShowUpdatedAuditActionService.dataSchemaV1;
|
||||
// Union of all versions
|
||||
public static readonly storedFieldsSchema = AttendeeNoShowUpdatedAuditActionService.fieldsSchemaV1;
|
||||
private helper: AuditActionServiceHelper<
|
||||
typeof AttendeeNoShowUpdatedAuditActionService.latestFieldsSchema,
|
||||
typeof AttendeeNoShowUpdatedAuditActionService.storedDataSchema
|
||||
>;
|
||||
|
||||
constructor() {
|
||||
this.helper = new AuditActionServiceHelper({
|
||||
latestVersion: this.VERSION,
|
||||
latestFieldsSchema: AttendeeNoShowUpdatedAuditActionService.latestFieldsSchema,
|
||||
storedDataSchema: AttendeeNoShowUpdatedAuditActionService.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 };
|
||||
}
|
||||
|
||||
async getDisplayTitle(): Promise<TranslationWithParams> {
|
||||
return { key: "booking_audit_action.attendee_no_show_updated" };
|
||||
}
|
||||
|
||||
getDisplayJson(storedData: { version: number; fields: z.infer<typeof fieldsSchemaV1> }): AttendeeNoShowUpdatedAuditDisplayData {
|
||||
const { fields } = storedData;
|
||||
return {
|
||||
noShowAttendee: fields.noShowAttendee.new,
|
||||
previousNoShowAttendee: fields.noShowAttendee.old ?? null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export type AttendeeNoShowUpdatedAuditData = z.infer<typeof fieldsSchemaV1>;
|
||||
|
||||
export type AttendeeNoShowUpdatedAuditDisplayData = {
|
||||
noShowAttendee: boolean;
|
||||
previousNoShowAttendee: boolean | null;
|
||||
};
|
||||
@@ -0,0 +1,85 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { StringArrayChangeSchema } from "../common/changeSchemas";
|
||||
import { AuditActionServiceHelper } from "./AuditActionServiceHelper";
|
||||
import type { IAuditActionService, TranslationWithParams } from "./IAuditActionService";
|
||||
|
||||
/**
|
||||
* Attendee Removed Audit Action Service
|
||||
* Handles ATTENDEE_REMOVED action with per-action versioning
|
||||
*/
|
||||
|
||||
// Module-level because it is passed to IAuditActionService type outside the class scope
|
||||
const fieldsSchemaV1 = z.object({
|
||||
attendees: StringArrayChangeSchema,
|
||||
});
|
||||
|
||||
export class AttendeeRemovedAuditActionService
|
||||
implements IAuditActionService<typeof fieldsSchemaV1, typeof fieldsSchemaV1> {
|
||||
readonly VERSION = 1;
|
||||
public static readonly TYPE = "ATTENDEE_REMOVED" 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 = AttendeeRemovedAuditActionService.dataSchemaV1;
|
||||
// Union of all versions
|
||||
public static readonly storedFieldsSchema = AttendeeRemovedAuditActionService.fieldsSchemaV1;
|
||||
private helper: AuditActionServiceHelper<
|
||||
typeof AttendeeRemovedAuditActionService.latestFieldsSchema,
|
||||
typeof AttendeeRemovedAuditActionService.storedDataSchema
|
||||
>;
|
||||
|
||||
constructor() {
|
||||
this.helper = new AuditActionServiceHelper({
|
||||
latestVersion: this.VERSION,
|
||||
latestFieldsSchema: AttendeeRemovedAuditActionService.latestFieldsSchema,
|
||||
storedDataSchema: AttendeeRemovedAuditActionService.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 };
|
||||
}
|
||||
|
||||
async getDisplayTitle(): Promise<TranslationWithParams> {
|
||||
return { key: "booking_audit_action.attendee_removed" };
|
||||
}
|
||||
|
||||
getDisplayJson(storedData: { version: number; fields: z.infer<typeof fieldsSchemaV1> }): AttendeeRemovedAuditDisplayData {
|
||||
const { fields } = storedData;
|
||||
// Note: fields.attendees stores the state change (old -> new), not the removed attendees directly
|
||||
// old = attendees before removal, new = remaining attendees after removal
|
||||
const remainingAttendeesSet = new Set(fields.attendees.new ?? []);
|
||||
// Compute removed attendees: those in old but not in new (remaining)
|
||||
const removedAttendees = (fields.attendees.old ?? []).filter(
|
||||
(email) => !remainingAttendeesSet.has(email)
|
||||
);
|
||||
return {
|
||||
removedAttendees,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export type AttendeeRemovedAuditData = z.infer<typeof fieldsSchemaV1>;
|
||||
|
||||
export type AttendeeRemovedAuditDisplayData = {
|
||||
removedAttendees: string[];
|
||||
};
|
||||
@@ -0,0 +1,90 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { StringChangeSchema } from "../common/changeSchemas";
|
||||
import { AuditActionServiceHelper } from "./AuditActionServiceHelper";
|
||||
import type { IAuditActionService, TranslationWithParams } from "./IAuditActionService";
|
||||
|
||||
/**
|
||||
* Cancelled Audit Action Service
|
||||
* Handles CANCELLED action with per-action versioning
|
||||
*/
|
||||
|
||||
// Module-level because it is passed to IAuditActionService type outside the class scope
|
||||
const fieldsSchemaV1 = z.object({
|
||||
cancellationReason: StringChangeSchema,
|
||||
cancelledBy: StringChangeSchema,
|
||||
status: StringChangeSchema,
|
||||
});
|
||||
|
||||
export class CancelledAuditActionService
|
||||
implements IAuditActionService<typeof fieldsSchemaV1, typeof fieldsSchemaV1> {
|
||||
readonly VERSION = 1;
|
||||
public static readonly TYPE = "CANCELLED" 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 = CancelledAuditActionService.dataSchemaV1;
|
||||
// Union of all versions
|
||||
public static readonly storedFieldsSchema = CancelledAuditActionService.fieldsSchemaV1;
|
||||
private helper: AuditActionServiceHelper<
|
||||
typeof CancelledAuditActionService.latestFieldsSchema,
|
||||
typeof CancelledAuditActionService.storedDataSchema
|
||||
>;
|
||||
|
||||
constructor() {
|
||||
this.helper = new AuditActionServiceHelper({
|
||||
latestVersion: this.VERSION,
|
||||
latestFieldsSchema: CancelledAuditActionService.latestFieldsSchema,
|
||||
storedDataSchema: CancelledAuditActionService.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 };
|
||||
}
|
||||
|
||||
async getDisplayTitle(): Promise<TranslationWithParams> {
|
||||
return { key: "booking_audit_action.cancelled" };
|
||||
}
|
||||
|
||||
getDisplayJson(storedData: { version: number; fields: z.infer<typeof fieldsSchemaV1> }): CancelledAuditDisplayData {
|
||||
const { fields } = storedData;
|
||||
return {
|
||||
cancellationReason: fields.cancellationReason.new ?? null,
|
||||
previousReason: fields.cancellationReason.old ?? null,
|
||||
cancelledBy: fields.cancelledBy.new ?? null,
|
||||
previousCancelledBy: fields.cancelledBy.old ?? null,
|
||||
previousStatus: fields.status.old ?? null,
|
||||
newStatus: fields.status.new ?? null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export type CancelledAuditData = z.infer<typeof fieldsSchemaV1>;
|
||||
|
||||
export type CancelledAuditDisplayData = {
|
||||
cancellationReason: string | null;
|
||||
previousReason: string | null;
|
||||
cancelledBy: string | null;
|
||||
previousCancelledBy: string | null;
|
||||
previousStatus: string | null;
|
||||
newStatus: string | null;
|
||||
};
|
||||
@@ -2,7 +2,7 @@ import { z } from "zod";
|
||||
import { BookingStatus } from "@calcom/prisma/enums";
|
||||
|
||||
import { AuditActionServiceHelper } from "./AuditActionServiceHelper";
|
||||
import type { IAuditActionService } from "./IAuditActionService";
|
||||
import type { IAuditActionService, TranslationWithParams } from "./IAuditActionService";
|
||||
|
||||
/**
|
||||
* Created Audit Action Service
|
||||
@@ -22,7 +22,7 @@ export class CreatedAuditActionService implements IAuditActionService<
|
||||
typeof fieldsSchemaV1
|
||||
> {
|
||||
readonly VERSION = 1;
|
||||
public static readonly TYPE = "CREATED";
|
||||
public static readonly TYPE = "CREATED" as const;
|
||||
private static dataSchemaV1 = z.object({
|
||||
version: z.literal(1),
|
||||
fields: fieldsSchemaV1,
|
||||
@@ -61,11 +61,16 @@ export class CreatedAuditActionService implements IAuditActionService<
|
||||
return { isMigrated: false, latestData: validated };
|
||||
}
|
||||
|
||||
async getDisplayTitle(): Promise<TranslationWithParams> {
|
||||
return { key: "booking_audit_action.created" };
|
||||
}
|
||||
|
||||
getDisplayJson(storedData: { version: number; fields: z.infer<typeof fieldsSchemaV1> }): CreatedAuditDisplayData {
|
||||
const { fields } = storedData;
|
||||
return {
|
||||
startTime: new Date(storedData.fields.startTime).toISOString(),
|
||||
endTime: new Date(storedData.fields.endTime).toISOString(),
|
||||
status: storedData.fields.status,
|
||||
startTime: new Date(fields.startTime).toISOString(),
|
||||
endTime: new Date(fields.endTime).toISOString(),
|
||||
status: fields.status,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { BooleanChangeSchema } from "../common/changeSchemas";
|
||||
import { AuditActionServiceHelper } from "./AuditActionServiceHelper";
|
||||
import type { IAuditActionService, TranslationWithParams } from "./IAuditActionService";
|
||||
|
||||
/**
|
||||
* Host No-Show Updated Audit Action Service
|
||||
* Handles HOST_NO_SHOW_UPDATED action with per-action versioning
|
||||
*/
|
||||
|
||||
// Module-level because it is passed to IAuditActionService type outside the class scope
|
||||
const fieldsSchemaV1 = z.object({
|
||||
noShowHost: BooleanChangeSchema,
|
||||
});
|
||||
|
||||
export class HostNoShowUpdatedAuditActionService
|
||||
implements IAuditActionService<typeof fieldsSchemaV1, typeof fieldsSchemaV1> {
|
||||
readonly VERSION = 1;
|
||||
public static readonly TYPE = "HOST_NO_SHOW_UPDATED" 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 = HostNoShowUpdatedAuditActionService.dataSchemaV1;
|
||||
// Union of all versions
|
||||
public static readonly storedFieldsSchema = HostNoShowUpdatedAuditActionService.fieldsSchemaV1;
|
||||
private helper: AuditActionServiceHelper<
|
||||
typeof HostNoShowUpdatedAuditActionService.latestFieldsSchema,
|
||||
typeof HostNoShowUpdatedAuditActionService.storedDataSchema
|
||||
>;
|
||||
|
||||
constructor() {
|
||||
this.helper = new AuditActionServiceHelper({
|
||||
latestVersion: this.VERSION,
|
||||
latestFieldsSchema: HostNoShowUpdatedAuditActionService.latestFieldsSchema,
|
||||
storedDataSchema: HostNoShowUpdatedAuditActionService.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 };
|
||||
}
|
||||
|
||||
async getDisplayTitle(): Promise<TranslationWithParams> {
|
||||
return { key: "booking_audit_action.host_no_show_updated" };
|
||||
}
|
||||
|
||||
getDisplayJson(storedData: { version: number; fields: z.infer<typeof fieldsSchemaV1> }): HostNoShowUpdatedAuditDisplayData {
|
||||
const { fields } = storedData;
|
||||
return {
|
||||
noShowHost: fields.noShowHost.new,
|
||||
previousNoShowHost: fields.noShowHost.old ?? null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export type HostNoShowUpdatedAuditData = z.infer<typeof fieldsSchemaV1>;
|
||||
|
||||
export type HostNoShowUpdatedAuditDisplayData = {
|
||||
noShowHost: boolean;
|
||||
previousNoShowHost: boolean | null;
|
||||
};
|
||||
@@ -1,5 +1,25 @@
|
||||
import { z } from "zod";
|
||||
|
||||
/**
|
||||
* Represents a component that can be interpolated into translations
|
||||
* Used with react-i18next Trans component for proper i18n support (RTL, word order, etc.)
|
||||
*/
|
||||
export type TranslationComponent = {
|
||||
type: "link";
|
||||
href: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Represents a translation key with optional interpolation params and components
|
||||
* Used for dynamic display titles that need to be translated with context
|
||||
* Components are used for clickable links within translations (e.g., "Rescheduled to <1>New Booking</1>")
|
||||
*/
|
||||
export type TranslationWithParams = {
|
||||
key: string;
|
||||
params?: Record<string, string | number>;
|
||||
components?: TranslationComponent[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Interface for Audit Action Services
|
||||
*
|
||||
@@ -43,10 +63,20 @@ export interface IAuditActionService<
|
||||
|
||||
/**
|
||||
* Get flattened JSON data for display (fields only, no version wrapper)
|
||||
* Optional - implement only if custom display formatting is needed
|
||||
* @param storedData - Parsed stored data { version, fields }
|
||||
* @returns The fields object without version wrapper and we decide what fields to show to the client
|
||||
*/
|
||||
getDisplayJson(storedData: { version: number; fields: z.infer<TStoredFieldsSchema> }): unknown;
|
||||
getDisplayJson?(storedData: { version: number; fields: z.infer<TStoredFieldsSchema> }): unknown;
|
||||
|
||||
/**
|
||||
* Get the display title for the audit action
|
||||
* Returns a translation key with optional interpolation params for dynamic titles
|
||||
* (e.g., "Booking reassigned to John Doe" instead of just "Reassignment")
|
||||
* @param storedData - Parsed stored data { version, fields }
|
||||
* @returns Translation key with optional interpolation params
|
||||
*/
|
||||
getDisplayTitle(storedData: { version: number; fields: z.infer<TStoredFieldsSchema> }): Promise<TranslationWithParams>;
|
||||
|
||||
/**
|
||||
* Migrate old version data to latest version
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { getHumanReadableLocationValue } from "@calcom/app-store/locations";
|
||||
import { StringChangeSchema } from "../common/changeSchemas";
|
||||
import { AuditActionServiceHelper } from "./AuditActionServiceHelper";
|
||||
import type { IAuditActionService, TranslationWithParams } from "./IAuditActionService";
|
||||
import { getTranslation } from "@calcom/lib/server/i18n";
|
||||
/**
|
||||
* Location Changed Audit Action Service
|
||||
* Handles LOCATION_CHANGED action with per-action versioning
|
||||
*/
|
||||
|
||||
// Module-level because it is passed to IAuditActionService type outside the class scope
|
||||
const fieldsSchemaV1 = z.object({
|
||||
location: StringChangeSchema,
|
||||
});
|
||||
|
||||
export class LocationChangedAuditActionService
|
||||
implements IAuditActionService<typeof fieldsSchemaV1, typeof fieldsSchemaV1> {
|
||||
readonly VERSION = 1;
|
||||
public static readonly TYPE = "LOCATION_CHANGED" 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 = LocationChangedAuditActionService.dataSchemaV1;
|
||||
// Union of all versions
|
||||
public static readonly storedFieldsSchema = LocationChangedAuditActionService.fieldsSchemaV1;
|
||||
private helper: AuditActionServiceHelper<
|
||||
typeof LocationChangedAuditActionService.latestFieldsSchema,
|
||||
typeof LocationChangedAuditActionService.storedDataSchema
|
||||
>;
|
||||
|
||||
constructor() {
|
||||
this.helper = new AuditActionServiceHelper({
|
||||
latestVersion: this.VERSION,
|
||||
latestFieldsSchema: LocationChangedAuditActionService.latestFieldsSchema,
|
||||
storedDataSchema: LocationChangedAuditActionService.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 };
|
||||
}
|
||||
|
||||
async getDisplayTitle(storedData: { version: number; fields: z.infer<typeof fieldsSchemaV1> }): Promise<TranslationWithParams> {
|
||||
const { fields } = storedData;
|
||||
// TODO: Ideally we want to translate the location label to the user's locale
|
||||
// We currently don't accept requesting user's translate fn here, fix it later.
|
||||
const t = await getTranslation("en", "common");
|
||||
|
||||
const fromLocation = getHumanReadableLocationValue(fields.location.old, t);
|
||||
const toLocation = getHumanReadableLocationValue(fields.location.new, t);
|
||||
|
||||
return {
|
||||
key: "booking_audit_action.location_changed_from_to",
|
||||
params: { fromLocation, toLocation },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export type LocationChangedAuditData = z.infer<typeof fieldsSchemaV1>;
|
||||
@@ -0,0 +1,91 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import type { UserRepository } from "@calcom/features/users/repositories/UserRepository";
|
||||
import { StringChangeSchema, NumberChangeSchema } from "../common/changeSchemas";
|
||||
import { AuditActionServiceHelper } from "./AuditActionServiceHelper";
|
||||
import type { 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({
|
||||
assignedToId: NumberChangeSchema,
|
||||
assignedById: NumberChangeSchema,
|
||||
reassignmentReason: StringChangeSchema,
|
||||
userPrimaryEmail: StringChangeSchema.optional(),
|
||||
title: StringChangeSchema.optional(),
|
||||
});
|
||||
|
||||
export class ReassignmentAuditActionService
|
||||
implements IAuditActionService<typeof fieldsSchemaV1, typeof fieldsSchemaV1> {
|
||||
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(private userRepository: UserRepository) {
|
||||
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 };
|
||||
}
|
||||
|
||||
async getDisplayTitle(storedData: { version: number; fields: z.infer<typeof fieldsSchemaV1> }): Promise<TranslationWithParams> {
|
||||
const { fields } = storedData;
|
||||
const user = await this.userRepository.findById({ id: fields.assignedToId.new });
|
||||
const reassignedToName = user?.name || "Unknown";
|
||||
return {
|
||||
key: "booking_audit_action.booking_reassigned_to_host",
|
||||
params: { host: reassignedToName },
|
||||
};
|
||||
}
|
||||
|
||||
getDisplayJson(storedData: { version: number; fields: z.infer<typeof fieldsSchemaV1> }): ReassignmentAuditDisplayData {
|
||||
const { fields } = storedData;
|
||||
return {
|
||||
newAssignedToId: fields.assignedToId.new,
|
||||
reassignmentReason: fields.reassignmentReason.new ?? null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export type ReassignmentAuditData = z.infer<typeof fieldsSchemaV1>;
|
||||
|
||||
export type ReassignmentAuditDisplayData = {
|
||||
newAssignedToId: number;
|
||||
reassignmentReason: string | null;
|
||||
};
|
||||
@@ -0,0 +1,85 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { StringChangeSchema } from "../common/changeSchemas";
|
||||
import { AuditActionServiceHelper } from "./AuditActionServiceHelper";
|
||||
import type { IAuditActionService, TranslationWithParams } from "./IAuditActionService";
|
||||
|
||||
/**
|
||||
* Rejected Audit Action Service
|
||||
* Handles REJECTED action with per-action versioning
|
||||
*/
|
||||
|
||||
// Module-level because it is passed to IAuditActionService type outside the class scope
|
||||
const fieldsSchemaV1 = z.object({
|
||||
rejectionReason: StringChangeSchema,
|
||||
status: StringChangeSchema,
|
||||
});
|
||||
|
||||
export class RejectedAuditActionService
|
||||
implements IAuditActionService<typeof fieldsSchemaV1, typeof fieldsSchemaV1> {
|
||||
readonly VERSION = 1;
|
||||
public static readonly TYPE = "REJECTED" 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 = RejectedAuditActionService.dataSchemaV1;
|
||||
// Union of all versions
|
||||
public static readonly storedFieldsSchema = RejectedAuditActionService.fieldsSchemaV1;
|
||||
private helper: AuditActionServiceHelper<
|
||||
typeof RejectedAuditActionService.latestFieldsSchema,
|
||||
typeof RejectedAuditActionService.storedDataSchema
|
||||
>;
|
||||
|
||||
constructor() {
|
||||
this.helper = new AuditActionServiceHelper({
|
||||
latestVersion: this.VERSION,
|
||||
latestFieldsSchema: RejectedAuditActionService.latestFieldsSchema,
|
||||
storedDataSchema: RejectedAuditActionService.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 };
|
||||
}
|
||||
|
||||
async getDisplayTitle(): Promise<TranslationWithParams> {
|
||||
return { key: "booking_audit_action.rejected" };
|
||||
}
|
||||
|
||||
getDisplayJson(storedData: { version: number; fields: z.infer<typeof fieldsSchemaV1> }): RejectedAuditDisplayData {
|
||||
const { fields } = storedData;
|
||||
return {
|
||||
rejectionReason: fields.rejectionReason.new ?? null,
|
||||
previousReason: fields.rejectionReason.old ?? null,
|
||||
previousStatus: fields.status.old ?? null,
|
||||
newStatus: fields.status.new ?? null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export type RejectedAuditData = z.infer<typeof fieldsSchemaV1>;
|
||||
|
||||
export type RejectedAuditDisplayData = {
|
||||
rejectionReason: string | null;
|
||||
previousReason: string | null;
|
||||
previousStatus: string | null;
|
||||
newStatus: string | null;
|
||||
};
|
||||
@@ -0,0 +1,82 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { StringChangeSchema, BooleanChangeSchema } from "../common/changeSchemas";
|
||||
import { AuditActionServiceHelper } from "./AuditActionServiceHelper";
|
||||
import type { IAuditActionService, TranslationWithParams } from "./IAuditActionService";
|
||||
|
||||
/**
|
||||
* Reschedule Requested Audit Action Service
|
||||
* Handles RESCHEDULE_REQUESTED action with per-action versioning
|
||||
*/
|
||||
|
||||
// Module-level because it is passed to IAuditActionService type outside the class scope
|
||||
const fieldsSchemaV1 = z.object({
|
||||
cancellationReason: StringChangeSchema,
|
||||
cancelledBy: StringChangeSchema,
|
||||
rescheduled: BooleanChangeSchema.optional(),
|
||||
});
|
||||
|
||||
export class RescheduleRequestedAuditActionService
|
||||
implements IAuditActionService<typeof fieldsSchemaV1, typeof fieldsSchemaV1> {
|
||||
readonly VERSION = 1;
|
||||
public static readonly TYPE = "RESCHEDULE_REQUESTED" 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 = RescheduleRequestedAuditActionService.dataSchemaV1;
|
||||
// Union of all versions
|
||||
public static readonly storedFieldsSchema = RescheduleRequestedAuditActionService.fieldsSchemaV1;
|
||||
private helper: AuditActionServiceHelper<
|
||||
typeof RescheduleRequestedAuditActionService.latestFieldsSchema,
|
||||
typeof RescheduleRequestedAuditActionService.storedDataSchema
|
||||
>;
|
||||
|
||||
constructor() {
|
||||
this.helper = new AuditActionServiceHelper({
|
||||
latestVersion: this.VERSION,
|
||||
latestFieldsSchema: RescheduleRequestedAuditActionService.latestFieldsSchema,
|
||||
storedDataSchema: RescheduleRequestedAuditActionService.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 };
|
||||
}
|
||||
|
||||
async getDisplayTitle(): Promise<TranslationWithParams> {
|
||||
return { key: "booking_audit_action.reschedule_requested" };
|
||||
}
|
||||
|
||||
getDisplayJson(storedData: { version: number; fields: z.infer<typeof fieldsSchemaV1> }): RescheduleRequestedAuditDisplayData {
|
||||
const { fields } = storedData;
|
||||
return {
|
||||
reason: fields.cancellationReason.new ?? null,
|
||||
requestedBy: fields.cancelledBy.new ?? null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export type RescheduleRequestedAuditData = z.infer<typeof fieldsSchemaV1>;
|
||||
|
||||
export type RescheduleRequestedAuditDisplayData = {
|
||||
reason: string | null;
|
||||
requestedBy: string | null;
|
||||
};
|
||||
@@ -0,0 +1,112 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { StringChangeSchema } from "../common/changeSchemas";
|
||||
import { AuditActionServiceHelper } from "./AuditActionServiceHelper";
|
||||
import type { IAuditActionService, TranslationWithParams } from "./IAuditActionService";
|
||||
|
||||
/**
|
||||
* Rescheduled Audit Action Service
|
||||
* Handles RESCHEDULED action with per-action versioning
|
||||
*/
|
||||
|
||||
// Module-level because it is passed to IAuditActionService type outside the class scope
|
||||
const fieldsSchemaV1 = z.object({
|
||||
startTime: StringChangeSchema,
|
||||
endTime: StringChangeSchema,
|
||||
rescheduledToUid: StringChangeSchema,
|
||||
});
|
||||
|
||||
export class RescheduledAuditActionService
|
||||
implements IAuditActionService<typeof fieldsSchemaV1, typeof fieldsSchemaV1> {
|
||||
readonly VERSION = 1;
|
||||
public static readonly TYPE = "RESCHEDULED" 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 = RescheduledAuditActionService.dataSchemaV1;
|
||||
// Union of all versions
|
||||
public static readonly storedFieldsSchema = RescheduledAuditActionService.fieldsSchemaV1;
|
||||
private helper: AuditActionServiceHelper<
|
||||
typeof RescheduledAuditActionService.latestFieldsSchema,
|
||||
typeof RescheduledAuditActionService.storedDataSchema
|
||||
>;
|
||||
|
||||
constructor() {
|
||||
this.helper = new AuditActionServiceHelper({
|
||||
latestVersion: this.VERSION,
|
||||
latestFieldsSchema: RescheduledAuditActionService.latestFieldsSchema,
|
||||
storedDataSchema: RescheduledAuditActionService.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 };
|
||||
}
|
||||
|
||||
async getDisplayTitle(storedData: { version: number; fields: z.infer<typeof fieldsSchemaV1> }): Promise<TranslationWithParams> {
|
||||
const rescheduledToUid = storedData.fields.rescheduledToUid.new;
|
||||
return {
|
||||
key: "booking_audit_action.rescheduled",
|
||||
components: rescheduledToUid ? [{ type: "link", href: `/booking/${rescheduledToUid}` }] : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
getDisplayJson(storedData: { version: number; fields: z.infer<typeof fieldsSchemaV1> }): RescheduledAuditDisplayData {
|
||||
const { fields } = storedData;
|
||||
return {
|
||||
previousStartTime: fields.startTime.old ?? null,
|
||||
newStartTime: fields.startTime.new ?? null,
|
||||
previousEndTime: fields.endTime.old ?? null,
|
||||
newEndTime: fields.endTime.new ?? null,
|
||||
rescheduledToUid: fields.rescheduledToUid.new ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the rescheduled log that created a specific booking
|
||||
* by matching the rescheduledToUid field with the target booking UID
|
||||
* @param rescheduledLogs - Array of rescheduled audit logs to search through
|
||||
* @param rescheduledToBookingUid - The UID of the booking that was created from the reschedule
|
||||
* @returns The matching log or null if not found
|
||||
*/
|
||||
getMatchingLog<T extends { data: unknown }>({
|
||||
rescheduledLogs,
|
||||
rescheduledToBookingUid,
|
||||
}: {
|
||||
rescheduledLogs: T[];
|
||||
rescheduledToBookingUid: string;
|
||||
}): T | null {
|
||||
return rescheduledLogs.find((log) => {
|
||||
const parsedData = this.parseStored(log.data);
|
||||
return parsedData.fields.rescheduledToUid.new === rescheduledToBookingUid;
|
||||
}) ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
export type RescheduledAuditData = z.infer<typeof fieldsSchemaV1>;
|
||||
|
||||
export type RescheduledAuditDisplayData = {
|
||||
previousStartTime: string | null;
|
||||
newStartTime: string | null;
|
||||
previousEndTime: string | null;
|
||||
newEndTime: string | null;
|
||||
rescheduledToUid: string | null;
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
import { z } from "zod";
|
||||
|
||||
/**
|
||||
* Common change schemas for audit data
|
||||
* These represent old -> new value transitions
|
||||
*/
|
||||
|
||||
export const StringChangeSchema = z.object({
|
||||
old: z.string().nullable(),
|
||||
new: z.string().nullable(),
|
||||
});
|
||||
|
||||
export const BooleanChangeSchema = z.object({
|
||||
old: z.boolean().nullable(),
|
||||
new: z.boolean(),
|
||||
});
|
||||
|
||||
export const StringArrayChangeSchema = z.object({
|
||||
old: z.array(z.string()).nullable(),
|
||||
new: z.array(z.string()),
|
||||
});
|
||||
|
||||
export const NumberChangeSchema = z.object({
|
||||
old: z.number().nullable(),
|
||||
new: z.number(),
|
||||
});
|
||||
|
||||
@@ -56,24 +56,7 @@ describe("Booking Audit Integration", () => {
|
||||
});
|
||||
testEventTypeId = eventType.id;
|
||||
|
||||
// Create test booking
|
||||
const startTime = new Date();
|
||||
const endTime = new Date(startTime.getTime() + 60 * 60 * 1000);
|
||||
testBookingUid = `test-booking-${timestamp}-${randomSuffix}`;
|
||||
|
||||
const testBooking = await prisma.booking.create({
|
||||
data: {
|
||||
uid: testBookingUid,
|
||||
title: "Test Booking",
|
||||
startTime,
|
||||
endTime,
|
||||
userId: testUserId,
|
||||
eventTypeId: testEventTypeId,
|
||||
status: BookingStatus.ACCEPTED,
|
||||
},
|
||||
});
|
||||
|
||||
// Create test attendee user for permission tests
|
||||
// Create test attendee user for permission tests (needed before booking creation)
|
||||
const attendeeUser = await prisma.user.create({
|
||||
data: {
|
||||
email: `test-attendee-${timestamp}-${randomSuffix}@example.com`,
|
||||
@@ -84,13 +67,29 @@ describe("Booking Audit Integration", () => {
|
||||
testAttendeeUserId = attendeeUser.id;
|
||||
testAttendeeEmail = attendeeUser.email;
|
||||
|
||||
// Add attendee to booking
|
||||
await prisma.attendee.create({
|
||||
// Create test booking with attendee in single atomic operation
|
||||
const startTime = new Date();
|
||||
const endTime = new Date(startTime.getTime() + 60 * 60 * 1000);
|
||||
testBookingUid = `test-booking-${timestamp}-${randomSuffix}`;
|
||||
|
||||
await prisma.booking.create({
|
||||
data: {
|
||||
email: testAttendeeEmail,
|
||||
name: "Test Attendee",
|
||||
timeZone: "UTC",
|
||||
bookingId: testBooking.id,
|
||||
uid: testBookingUid,
|
||||
title: "Test Booking",
|
||||
startTime,
|
||||
endTime,
|
||||
userId: testUserId,
|
||||
eventTypeId: testEventTypeId,
|
||||
status: BookingStatus.ACCEPTED,
|
||||
attendees: {
|
||||
create: [
|
||||
{
|
||||
email: testAttendeeEmail,
|
||||
name: "Test Attendee",
|
||||
timeZone: "UTC",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user