mirror of
https://github.com/hanzoai/calendar.git
synced 2026-10-30 14:46:43 +00:00
* refactor: apply biome formatting to packages/features (batch 1 - small subdirs) Format small subdirectories in packages/features: di, flags, holidays, oauth, settings, users, assignment-reason, selectedCalendar, hashedLink, host, form, form-builder, availability, data-table, pbac, schedules, troubleshooter, eventtypes, calendar-subscription, and root-level files. Also includes straggler apps/web BookEventForm.tsx. Co-Authored-By: Claude Opus 4.6 <[email protected]> * refactor: apply biome formatting to packages/features (batch 2 - medium subdirs) Format medium subdirectories in packages/features: auth, credentials, calendars, routing-forms, routing-trace, attributes, watchlist, calAIPhone, tasker, and webhooks. Co-Authored-By: Claude Opus 4.6 <[email protected]> * refactor: apply biome formatting to packages/features (batch 3 - bookings + insights) Format bookings and insights subdirectories in packages/features. Co-Authored-By: Claude Opus 4.6 <[email protected]> * refactor: apply biome formatting to packages/features (batch 4 - ee) Format packages/features/ee subdirectory covering billing, workflows, organizations, teams, managed-event-types, round-robin, dsync, integration-attribute-sync, and payments. Co-Authored-By: Claude Opus 4.6 <[email protected]> * refactor: apply biome formatting to packages/features (batch 5 - booking-audit part 1) Format booking-audit di, actions, common, dto, repository, and types subdirectories in packages/features/booking-audit. Co-Authored-By: Claude Opus 4.6 <[email protected]> * refactor: apply biome formatting to packages/features (batch 6 - booking-audit part 2) Format booking-audit service subdirectory in packages/features/booking-audit. Co-Authored-By: Claude Opus 4.6 <[email protected]> --------- Co-authored-by: Claude Opus 4.6 <[email protected]>
484 lines
15 KiB
TypeScript
484 lines
15 KiB
TypeScript
import { z } from "zod";
|
|
import { v4 as uuidv4 } from "uuid";
|
|
import type { Tasker } from "@calcom/features/tasker/tasker";
|
|
import { safeStringify } from "@calcom/lib/safeStringify";
|
|
import type { ISimpleLogger } from "@calcom/features/di/shared/services/logger.service";
|
|
|
|
import type { BookingAuditAction } from "../types/bookingAuditTask";
|
|
import type { ActionSource } from "../types/actionSource";
|
|
import type { PiiFreeActor, Actor, BookingAuditContext } from "../dto/types";
|
|
import { makeActorById, buildActorEmail } from "../makeActor";
|
|
import type { IAuditActorRepository } from "../repository/IAuditActorRepository";
|
|
import { AcceptedAuditActionService } from "../actions/AcceptedAuditActionService";
|
|
import { AttendeeAddedAuditActionService } from "../actions/AttendeeAddedAuditActionService";
|
|
import { AttendeeRemovedAuditActionService } from "../actions/AttendeeRemovedAuditActionService";
|
|
import { CancelledAuditActionService } from "../actions/CancelledAuditActionService";
|
|
import { CreatedAuditActionService } from "../actions/CreatedAuditActionService";
|
|
import { LocationChangedAuditActionService } from "../actions/LocationChangedAuditActionService";
|
|
import { NoShowUpdatedAuditActionService } from "../actions/NoShowUpdatedAuditActionService";
|
|
import { ReassignmentAuditActionService } from "../actions/ReassignmentAuditActionService";
|
|
import { RejectedAuditActionService } from "../actions/RejectedAuditActionService";
|
|
import { RescheduleRequestedAuditActionService } from "../actions/RescheduleRequestedAuditActionService";
|
|
import { RescheduledAuditActionService } from "../actions/RescheduledAuditActionService";
|
|
import { SeatBookedAuditActionService } from "../actions/SeatBookedAuditActionService";
|
|
import { SeatRescheduledAuditActionService } from "../actions/SeatRescheduledAuditActionService";
|
|
import type { BookingAuditProducerService } from "./BookingAuditProducerService.interface";
|
|
|
|
interface BookingAuditTaskerProducerServiceDeps {
|
|
tasker: Tasker;
|
|
log: ISimpleLogger;
|
|
auditActorRepository: IAuditActorRepository;
|
|
}
|
|
|
|
/**
|
|
* BookingAuditTaskerProducerService - Tasker-based implementation of BookingAuditProducerService
|
|
*
|
|
* Producer that uses Tasker for local/background job processing.
|
|
* Task processing is handled by BookingAuditTaskConsumer.
|
|
*
|
|
* For future migration to trigger.dev, create BookingAuditTriggerProducerService
|
|
* that implements the same BookingAuditProducerService interface.
|
|
*/
|
|
export class BookingAuditTaskerProducerService implements BookingAuditProducerService {
|
|
private readonly tasker: Tasker;
|
|
private readonly log: BookingAuditTaskerProducerServiceDeps["log"];
|
|
private readonly auditActorRepository: IAuditActorRepository;
|
|
|
|
constructor(private readonly deps: BookingAuditTaskerProducerServiceDeps) {
|
|
this.tasker = deps.tasker;
|
|
this.log = deps.log;
|
|
this.auditActorRepository = deps.auditActorRepository;
|
|
}
|
|
|
|
private async getPIIFreeBookingAuditActor(params: { actor: Actor }): Promise<PiiFreeActor> {
|
|
const { actor } = params;
|
|
|
|
if (actor.identifiedBy === "user" || actor.identifiedBy === "attendee" || actor.identifiedBy === "id") {
|
|
return actor;
|
|
}
|
|
|
|
if (actor.identifiedBy === "app") {
|
|
const piiFreeActor = await this.auditActorRepository.createIfNotExistsAppActor({
|
|
credentialId: actor.credentialId,
|
|
});
|
|
return makeActorById(piiFreeActor.id);
|
|
}
|
|
|
|
if (actor.identifiedBy === "appSlug") {
|
|
const email = buildActorEmail({ identifier: actor.appSlug, actorType: "app" });
|
|
const piiFreeActor = await this.auditActorRepository.createIfNotExistsAppActor({
|
|
email,
|
|
name: actor.name,
|
|
});
|
|
return makeActorById(piiFreeActor.id);
|
|
}
|
|
|
|
// Must be guest actor at this point
|
|
const piiFreeActor = await this.auditActorRepository.createIfNotExistsGuestActor({
|
|
email: actor.email,
|
|
name: actor.name ?? null,
|
|
phone: null,
|
|
});
|
|
return makeActorById(piiFreeActor.id);
|
|
}
|
|
|
|
/**
|
|
* Internal helper to queue audit task to Tasker
|
|
* @param params.action - Must be a valid BookingAuditAction value (TYPE from action services are string-typed)
|
|
* @param params.operationId - Optional operation ID for correlating bulk operations. If null, will be auto-generated.
|
|
* @param params.isBookingAuditEnabled - Flag indicating if booking audit is enabled for the organization.
|
|
* When false, skips queueing.
|
|
*/
|
|
private async queueTask(params: {
|
|
bookingUid: string;
|
|
actor: Actor;
|
|
organizationId: number | null;
|
|
action: string;
|
|
source: ActionSource;
|
|
operationId?: string | null;
|
|
data: unknown;
|
|
context?: BookingAuditContext;
|
|
isBookingAuditEnabled: boolean;
|
|
}): Promise<void> {
|
|
// Skip queueing for non-organization bookings
|
|
if (params.organizationId === null) {
|
|
return;
|
|
}
|
|
// Skip queueing if booking audit is disabled for this organization
|
|
if (!params.isBookingAuditEnabled) {
|
|
this.log.debug(`Skipping ${params.action} audit: booking-audit feature is disabled for organization`, {
|
|
organizationId: params.organizationId,
|
|
bookingUid: params.bookingUid,
|
|
action: params.action,
|
|
});
|
|
return;
|
|
}
|
|
try {
|
|
const piiFreeActor = await this.getPIIFreeBookingAuditActor({
|
|
actor: params.actor,
|
|
});
|
|
|
|
const operationId = params.operationId ?? uuidv4();
|
|
|
|
await this.tasker.create("bookingAudit", {
|
|
isBulk: false,
|
|
bookingUid: params.bookingUid,
|
|
actor: piiFreeActor,
|
|
organizationId: params.organizationId,
|
|
timestamp: Date.now(),
|
|
action: params.action as BookingAuditAction,
|
|
source: params.source,
|
|
operationId,
|
|
data: params.data,
|
|
context: params.context,
|
|
});
|
|
} catch (error) {
|
|
this.log.error(`Error while queueing ${params.action} audit`, safeStringify(error));
|
|
}
|
|
}
|
|
|
|
async queueCreatedAudit(params: {
|
|
bookingUid: string;
|
|
actor: Actor;
|
|
organizationId: number | null;
|
|
source: ActionSource;
|
|
operationId?: string | null;
|
|
data: z.infer<typeof CreatedAuditActionService.latestFieldsSchema>;
|
|
context?: BookingAuditContext;
|
|
isBookingAuditEnabled: boolean;
|
|
}): Promise<void> {
|
|
await this.queueTask({
|
|
...params,
|
|
action: CreatedAuditActionService.TYPE,
|
|
});
|
|
}
|
|
|
|
async queueRescheduledAudit(params: {
|
|
bookingUid: string;
|
|
actor: Actor;
|
|
organizationId: number | null;
|
|
source: ActionSource;
|
|
operationId?: string | null;
|
|
data: z.infer<typeof RescheduledAuditActionService.latestFieldsSchema>;
|
|
context?: BookingAuditContext;
|
|
isBookingAuditEnabled: boolean;
|
|
}): Promise<void> {
|
|
await this.queueTask({
|
|
...params,
|
|
action: RescheduledAuditActionService.TYPE,
|
|
});
|
|
}
|
|
|
|
async queueAcceptedAudit(params: {
|
|
bookingUid: string;
|
|
actor: Actor;
|
|
organizationId: number | null;
|
|
source: ActionSource;
|
|
operationId?: string | null;
|
|
data: z.infer<typeof AcceptedAuditActionService.latestFieldsSchema>;
|
|
context?: BookingAuditContext;
|
|
isBookingAuditEnabled: boolean;
|
|
}): Promise<void> {
|
|
await this.queueTask({
|
|
...params,
|
|
action: AcceptedAuditActionService.TYPE,
|
|
});
|
|
}
|
|
|
|
async queueCancelledAudit(params: {
|
|
bookingUid: string;
|
|
actor: Actor;
|
|
organizationId: number | null;
|
|
source: ActionSource;
|
|
operationId?: string | null;
|
|
data: z.infer<typeof CancelledAuditActionService.latestFieldsSchema>;
|
|
context?: BookingAuditContext;
|
|
isBookingAuditEnabled: boolean;
|
|
}): Promise<void> {
|
|
await this.queueTask({
|
|
...params,
|
|
action: CancelledAuditActionService.TYPE,
|
|
});
|
|
}
|
|
|
|
async queueRescheduleRequestedAudit(params: {
|
|
bookingUid: string;
|
|
actor: Actor;
|
|
organizationId: number | null;
|
|
source: ActionSource;
|
|
operationId?: string | null;
|
|
data: z.infer<typeof RescheduleRequestedAuditActionService.latestFieldsSchema>;
|
|
context?: BookingAuditContext;
|
|
isBookingAuditEnabled: boolean;
|
|
}): Promise<void> {
|
|
await this.queueTask({
|
|
...params,
|
|
action: RescheduleRequestedAuditActionService.TYPE,
|
|
});
|
|
}
|
|
|
|
async queueAttendeeAddedAudit(params: {
|
|
bookingUid: string;
|
|
actor: Actor;
|
|
organizationId: number | null;
|
|
source: ActionSource;
|
|
operationId?: string | null;
|
|
data: z.infer<typeof AttendeeAddedAuditActionService.latestFieldsSchema>;
|
|
context?: BookingAuditContext;
|
|
isBookingAuditEnabled: boolean;
|
|
}): Promise<void> {
|
|
await this.queueTask({
|
|
...params,
|
|
action: AttendeeAddedAuditActionService.TYPE,
|
|
});
|
|
}
|
|
|
|
async queueNoShowUpdatedAudit(params: {
|
|
bookingUid: string;
|
|
actor: Actor;
|
|
organizationId: number | null;
|
|
source: ActionSource;
|
|
operationId?: string | null;
|
|
data: z.infer<typeof NoShowUpdatedAuditActionService.latestFieldsSchema>;
|
|
context?: BookingAuditContext;
|
|
isBookingAuditEnabled: boolean;
|
|
}): Promise<void> {
|
|
await this.queueTask({
|
|
...params,
|
|
action: NoShowUpdatedAuditActionService.TYPE,
|
|
});
|
|
}
|
|
|
|
async queueRejectedAudit(params: {
|
|
bookingUid: string;
|
|
actor: Actor;
|
|
organizationId: number | null;
|
|
source: ActionSource;
|
|
operationId?: string | null;
|
|
data: z.infer<typeof RejectedAuditActionService.latestFieldsSchema>;
|
|
context?: BookingAuditContext;
|
|
isBookingAuditEnabled: boolean;
|
|
}): Promise<void> {
|
|
await this.queueTask({
|
|
...params,
|
|
action: RejectedAuditActionService.TYPE,
|
|
});
|
|
}
|
|
|
|
async queueAttendeeRemovedAudit(params: {
|
|
bookingUid: string;
|
|
actor: Actor;
|
|
organizationId: number | null;
|
|
source: ActionSource;
|
|
operationId?: string | null;
|
|
data: z.infer<typeof AttendeeRemovedAuditActionService.latestFieldsSchema>;
|
|
context?: BookingAuditContext;
|
|
isBookingAuditEnabled: boolean;
|
|
}): Promise<void> {
|
|
await this.queueTask({
|
|
...params,
|
|
action: AttendeeRemovedAuditActionService.TYPE,
|
|
});
|
|
}
|
|
|
|
async queueReassignmentAudit(params: {
|
|
bookingUid: string;
|
|
actor: Actor;
|
|
organizationId: number | null;
|
|
source: ActionSource;
|
|
operationId?: string | null;
|
|
data: z.infer<typeof ReassignmentAuditActionService.latestFieldsSchema>;
|
|
context?: BookingAuditContext;
|
|
isBookingAuditEnabled: boolean;
|
|
}): Promise<void> {
|
|
await this.queueTask({
|
|
...params,
|
|
action: ReassignmentAuditActionService.TYPE,
|
|
});
|
|
}
|
|
|
|
async queueLocationChangedAudit(params: {
|
|
bookingUid: string;
|
|
actor: Actor;
|
|
organizationId: number | null;
|
|
source: ActionSource;
|
|
operationId?: string | null;
|
|
data: z.infer<typeof LocationChangedAuditActionService.latestFieldsSchema>;
|
|
context?: BookingAuditContext;
|
|
isBookingAuditEnabled: boolean;
|
|
}): Promise<void> {
|
|
await this.queueTask({
|
|
...params,
|
|
action: LocationChangedAuditActionService.TYPE,
|
|
});
|
|
}
|
|
|
|
async queueSeatBookedAudit(params: {
|
|
bookingUid: string;
|
|
actor: Actor;
|
|
organizationId: number | null;
|
|
source: ActionSource;
|
|
operationId?: string | null;
|
|
data: z.infer<typeof SeatBookedAuditActionService.latestFieldsSchema>;
|
|
context?: BookingAuditContext;
|
|
isBookingAuditEnabled: boolean;
|
|
}): Promise<void> {
|
|
await this.queueTask({
|
|
...params,
|
|
action: SeatBookedAuditActionService.TYPE,
|
|
});
|
|
}
|
|
|
|
async queueSeatRescheduledAudit(params: {
|
|
bookingUid: string;
|
|
actor: Actor;
|
|
organizationId: number | null;
|
|
source: ActionSource;
|
|
operationId?: string | null;
|
|
data: z.infer<typeof SeatRescheduledAuditActionService.latestFieldsSchema>;
|
|
context?: BookingAuditContext;
|
|
isBookingAuditEnabled: boolean;
|
|
}): Promise<void> {
|
|
await this.queueTask({
|
|
...params,
|
|
action: SeatRescheduledAuditActionService.TYPE,
|
|
});
|
|
}
|
|
|
|
private async queueBulkTask(params: {
|
|
bookings: Array<{
|
|
bookingUid: string;
|
|
data: unknown;
|
|
}>;
|
|
actor: Actor;
|
|
organizationId: number | null;
|
|
action: string;
|
|
source: ActionSource;
|
|
operationId?: string | null;
|
|
context?: BookingAuditContext;
|
|
isBookingAuditEnabled: boolean;
|
|
}): Promise<void> {
|
|
// Skip queueing for non-organization bookings
|
|
if (params.organizationId === null) {
|
|
return;
|
|
}
|
|
// Skip queueing if booking audit is disabled for this organization
|
|
if (!params.isBookingAuditEnabled) {
|
|
this.log.debug(
|
|
`Skipping bulk ${params.action} audit: booking-audit feature is disabled for organization`,
|
|
{ organizationId: params.organizationId, bookingCount: params.bookings.length, action: params.action }
|
|
);
|
|
return;
|
|
}
|
|
try {
|
|
const piiFreeActor = await this.getPIIFreeBookingAuditActor({
|
|
actor: params.actor,
|
|
});
|
|
|
|
const operationId = params.operationId ?? uuidv4();
|
|
|
|
await this.tasker.create("bookingAudit", {
|
|
isBulk: true,
|
|
bookings: params.bookings,
|
|
actor: piiFreeActor,
|
|
organizationId: params.organizationId,
|
|
timestamp: Date.now(),
|
|
action: params.action as BookingAuditAction,
|
|
source: params.source,
|
|
operationId,
|
|
context: params.context,
|
|
});
|
|
} catch (error) {
|
|
this.log.error(`Error while queueing bulk ${params.action} audit`, safeStringify(error));
|
|
}
|
|
}
|
|
|
|
async queueBulkAcceptedAudit(params: {
|
|
bookings: Array<{
|
|
bookingUid: string;
|
|
data: z.infer<typeof AcceptedAuditActionService.latestFieldsSchema>;
|
|
}>;
|
|
actor: Actor;
|
|
organizationId: number | null;
|
|
source: ActionSource;
|
|
operationId?: string | null;
|
|
context?: BookingAuditContext;
|
|
isBookingAuditEnabled: boolean;
|
|
}): Promise<void> {
|
|
await this.queueBulkTask({
|
|
...params,
|
|
action: AcceptedAuditActionService.TYPE,
|
|
});
|
|
}
|
|
|
|
async queueBulkCancelledAudit(params: {
|
|
bookings: Array<{
|
|
bookingUid: string;
|
|
data: z.infer<typeof CancelledAuditActionService.latestFieldsSchema>;
|
|
}>;
|
|
actor: Actor;
|
|
organizationId: number | null;
|
|
source: ActionSource;
|
|
operationId?: string | null;
|
|
context?: BookingAuditContext;
|
|
isBookingAuditEnabled: boolean;
|
|
}): Promise<void> {
|
|
await this.queueBulkTask({
|
|
...params,
|
|
action: CancelledAuditActionService.TYPE,
|
|
});
|
|
}
|
|
|
|
async queueBulkCreatedAudit(params: {
|
|
bookings: Array<{
|
|
bookingUid: string;
|
|
data: z.infer<typeof CreatedAuditActionService.latestFieldsSchema>;
|
|
}>;
|
|
actor: Actor;
|
|
organizationId: number | null;
|
|
source: ActionSource;
|
|
operationId?: string | null;
|
|
isBookingAuditEnabled: boolean;
|
|
}): Promise<void> {
|
|
await this.queueBulkTask({
|
|
...params,
|
|
action: CreatedAuditActionService.TYPE,
|
|
});
|
|
}
|
|
|
|
async queueBulkRescheduledAudit(params: {
|
|
bookings: Array<{
|
|
bookingUid: string;
|
|
data: z.infer<typeof RescheduledAuditActionService.latestFieldsSchema>;
|
|
}>;
|
|
actor: Actor;
|
|
organizationId: number | null;
|
|
source: ActionSource;
|
|
operationId?: string | null;
|
|
isBookingAuditEnabled: boolean;
|
|
}): Promise<void> {
|
|
await this.queueBulkTask({
|
|
...params,
|
|
action: RescheduledAuditActionService.TYPE,
|
|
});
|
|
}
|
|
|
|
async queueBulkRejectedAudit(params: {
|
|
bookings: Array<{
|
|
bookingUid: string;
|
|
data: z.infer<typeof RejectedAuditActionService.latestFieldsSchema>;
|
|
}>;
|
|
actor: Actor;
|
|
organizationId: number | null;
|
|
source: ActionSource;
|
|
operationId?: string | null;
|
|
context?: BookingAuditContext;
|
|
isBookingAuditEnabled: boolean;
|
|
}): Promise<void> {
|
|
await this.queueBulkTask({
|
|
...params,
|
|
action: RejectedAuditActionService.TYPE,
|
|
});
|
|
}
|
|
}
|