* Add DB table for wrong assignment reports * When report is submitted write to the db * Prevent duplicate reportings * test: add migration and tests for WrongAssignmentReport table Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com> * fix: add unique constraint on bookingUid and booking access check for hasWrongAssignmentReport - Add @unique constraint on bookingUid in WrongAssignmentReport model to prevent duplicate reports at DB level - Add booking ownership check using BookingAccessService in hasWrongAssignmentReport endpoint - Refactor hasWrongAssignmentReport into separate handler and schema files Addresses Cubic AI review feedback on PR #27405 Co-Authored-By: unknown <> * feat: add routingFormId to WrongAssignmentReport and fix Select clearing - Add routingFormId field to WrongAssignmentReport model in schema.prisma - Add relation to App_RoutingForms_Form with SetNull on delete - Update WrongAssignmentReportRepository.createReport to accept routingFormId - Update BookingRepository.findByUidIncludeEventTypeAndTeamAndAssignmentReason to include routedFromRoutingFormReponse - Extract routingFormId from booking in reportWrongAssignment handler - Fix Select clearing issue: handle null case when user clears team member selection - Update tests to include routingFormId field Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com> * chore: add migration for routingFormId in WrongAssignmentReport Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com> * feat: add wrong assignment reports dashboard under routing tab - Add reviewedById and reviewedAt fields to WrongAssignmentReport model - Add repository methods for listing reports by status and updating status - Create tRPC endpoints for fetching reports and updating status - Create dashboard UI with pending/reviewed tabs showing routing form name - Add translation keys for dashboard UI - Integrate dashboard into routing insights page Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com> * fix: move hooks before early return and fix indentation Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com> * fix: address Udit's review comments - hasWrongAssignmentReport: throw UNAUTHORIZED error instead of returning false - reportWrongAssignment: add try-catch for Prisma P2002 unique constraint error - WrongAssignmentReport: add Team relation to teamId field - WrongAssignmentReportRepository: use findUnique instead of findFirst - reportWrongAssignment: use i18n for error messages Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com> * fix: revert hasWrongAssignmentReport to return false when user lacks access Per PR checklist, hasWrongAssignmentReport should return { hasReport: false } when user lacks access to booking, not throw an error. This allows the UI to gracefully treat 'no access' as 'no report exists'. Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com> * test: update mocks for findUnique and i18n in unit tests Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com> * fix: throw UNAUTHORIZED in hasWrongAssignmentReport and squash migrations Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com> * feat: move wrong assignment reports to its own tab under Insights Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com> * Use data table for wrong reports * Add option to view routing trace * feat: add view routing form submission action to wrong assignment reports Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com> * feat: add RoutingFormResponseSheet component for viewing form submissions - Create slide-out sheet to display routing form responses - Map option IDs to display labels for select/multiselect fields - Handle both legacy and modern option formats - Add i18n strings: form_submission, no_responses_found Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: improve wrong assignment reports UX - Integrate RoutingFormResponseSheet as slide-out panel instead of new tab - Fix dropdown padding by using StartIcon prop instead of manual Icon - Allow direct status changes for reviewed reports (no need to reopen first) - Remove unused Icon import Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: change User relation onDelete from Cascade to SetNull in WrongAssignmentReport Address Hariom's review feedback: - Changed reportedById from Int to Int? (nullable) - Changed reportedBy relation from onDelete: Cascade to onDelete: SetNull - Updated migration SQL to reflect these changes This preserves wrong assignment reports even when the reporting user is deleted, as the data is still useful for analysis. Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com> * feat: add missing i18n strings for wrong assignment reports dashboard Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com> * refactor: move form response display value resolution server-side Replace client-side option ID to label resolution in RoutingFormResponseSheet with a new lean tRPC endpoint (getFormResponseDisplay) that resolves values server-side using the existing getHumanReadableFieldResponseValue utility. This enforces DTO boundaries by returning a clean pre-resolved payload instead of leaking internal option format details to the client. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add report wrong assignment button to routing trace sheet Wire up the WrongAssignmentDialog from the routing trace sheet header so users can flag wrong assignments directly while viewing the trace. The report button is disabled with a tooltip when a report already exists. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: reuse existing WrongAssignmentDialog from parent Replace the duplicate WrongAssignmentDialog in RoutingTraceSheet with a callback to the existing instance in BookingActionsDropdown. This reduces the prop surface from a 6-field reportContext object to an onReport callback and hasExistingReport boolean. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: use repository instead of direct Prisma in getFormResponseDisplay Replace direct Prisma query with PrismaRoutingFormResponseRepository's findByIdIncludeForm method. Extend the method to also select form name, description, userId, and teamId needed for display and auth checks. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: replace direct Prisma calls with repository methods Use MembershipRepository.hasMembership() for auth checks and TeamRepository.findAllByParentId() for child team queries instead of direct Prisma calls. Replace direct user query with UserRepository.getTimeZoneAndDefaultScheduleId(). Remove unused seed script. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: use PBAC services for wrong assignment report auth Replace manual MembershipRepository.hasMembership() checks with PBAC-aware permission checking. getWrongAssignmentReports uses createTeamPbacProcedure middleware since teamId is in input. updateWrongAssignmentReportStatus uses PermissionCheckService directly since teamId is discovered from the report entity. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: resolve type errors in RoutingFormResponseSheet and wrong-routing view Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com> * feat: add seed script for wrong assignment reports test data Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com> * Revert "feat: add seed script for wrong assignment reports test data" This reverts commit 0bd60e9661858a59aab1573d14d57d81733b7991. * Only update reviewed fields when not pending Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com> * refactor: wrap handleStatusChange in useCallback to fix useMemo recalculation Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com> * Fix routing sheet UI * fix: use appropriate error message in getFormResponseDisplay handler Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com> * refactor: extract WrongAssignmentReportService from tRPC handlers Move business logic (booking lookup, duplicate check, report creation, webhook dispatch, org-level team resolution) into a dedicated service in packages/features. Handlers become thin controllers that only handle auth checks and delegate to the service. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: restrict report status updates to admin/owner roles Remove MembershipRole.MEMBER from fallbackRoles in updateWrongAssignmentReportStatus permission check. Updating report status is an administrative action that should be limited to team admins and owners. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: localize hard-coded success message in WrongAssignmentReportService Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com> * fix: rename reviewed tab to handled and add missing translations Rename the "Reviewed" tab to "Handled" since it groups three distinct statuses (Reviewed, Resolved, Dismissed). Also add missing translation keys for "resolved" and "dismissed" status badges. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * merge: resolve conflicts with main branch Merge main into devin/1769747741-wrong-assignment-dashboard, resolving conflicts in: - BookingActionsDropdown.tsx: use main's booking prop with PR's fragment structure - reportWrongAssignment.handler.ts: keep PR's service-based approach - reportWrongAssignment.handler.test.ts: use main's class-based mocks with PR's additions - WrongAssignmentReportService.ts: align with main's repo method and field names Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com> * Add guard when accessing assignmentReasonSortedByCreatedAt Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> * fix: address Cubic AI review feedback - select projection and useLocale refactor Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com> * fix: remove stale TRPCError assertion in test (service throws ErrorWithCode) Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com> * refactor: remove unused findByTeamIdAndStatus and findByTeamIdAndStatuses methods Co-Authored-By: alex@cal.com <me@alexvanandel.com> * perf: add composite index on WrongAssignmentReport and narrow findByIdIncludeForm select Co-Authored-By: alex@cal.com <me@alexvanandel.com> * perf: use lightweight findTeamIdById in update-status handler instead of findById Co-Authored-By: alex@cal.com <me@alexvanandel.com> --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: hariom@cal.com <hariombalhara@gmail.com> Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> Co-authored-by: alex@cal.com <me@alexvanandel.com>
Booking Audit System
Overview
The Booking Audit System tracks all actions and changes related to bookings in Cal.com. The architecture is built around two core tables (AuditActor and BookingAudit) that work together to maintain a complete, immutable audit trail.
Database Architecture
Core Tables
- AuditActor: Stores information about entities that perform actions on bookings. Maintains historical records even after users are deleted.
- BookingAudit: Stores audit records for all booking-related actions.
Key Design Decisions
AuditActor Table:
- Uses UUID primary keys for distributed system compatibility
- Soft references to
User(viauserUuid) andAttendee(viaattendeeId) without foreign key constraints - Audit trail persists independently even after users/attendees are deleted
- Unique constraints prevent duplicate audit actors
- Supports actor types: USER, GUEST, ATTENDEE, SYSTEM, APP
- Identity fields (email, phone, name) can be anonymized when source records are deleted
BookingAudit Table:
- Uses UUID v7 primary keys for time-sortable IDs
bookingUidstored as plain string (no foreign key) to preserve audit trail after booking deletiononDelete: Restrictprevents actor deletion if audit records exist- Explicit
timestampfield represents business event time (may differ fromcreatedAtif processed asynchronously) operationIdrequired field for correlating audit logs from a single user action across different audit types (BookingAudit, UserAudit, etc.)- JSON
datafield stores action-specific contextual data - Indexed for efficient queries by
bookingUid,actorId,timestamp, andoperationId
Protecting the Audit Trail:
- Database rejects deletion of
AuditActorrecords with associatedBookingAuditrecords - When a
Useris deleted, theirAuditActorrecord persists withuserUuidset to null
Actor Types
- USER: Registered Cal.com users
- GUEST: Non-registered users (typically booking guests)
- ATTENDEE: Guests who have an Attendee record associated with a booking
- SYSTEM: Automated system actions
Source and Actor Design Pattern
The booking audit system uses two complementary fields:
Source: The Channel
source identifies how the action was initiated:
- WEBAPP: Cal.com web application
- API_V1: API v1 endpoint
- API_V2: API v2 endpoint
- WEBHOOK: External webhook (e.g., Stripe)
- SYSTEM: Background job (e.g., Tasker's task, trigger.dev job for automatic no-show detection)
- UNKNOWN: Source cannot be determined
Actor: The Entity
actor identifies who or what performed the action:
- User Actor: Registered Cal.com user
- Guest Actor: Non-registered guest
- Attendee Actor: Attendee associated with a booking
- System Actor: Automated system action (generic or named for specific webhooks/services)
This separation enables clear compliance trails, easier debugging, better analytics, and security by distinguishing user-initiated vs automated actions.
Audit Actions
The system tracks various booking actions including:
- CREATED: Initial booking creation
- RESCHEDULED: Booking time/date changed
- ACCEPTED: Booking request approved
- CANCELLED: Booking cancelled
- REJECTED: Booking request declined
- RESCHEDULE_REQUESTED: Request to reschedule
- ATTENDEE_ADDED: New attendee added
- ATTENDEE_REMOVED: Attendee removed
- REASSIGNMENT: Booking reassigned to different host
- LOCATION_CHANGED: Meeting location updated
- NO_SHOW_UPDATED: Host or attendee no-show status changed
- SEAT_BOOKED: Seat reserved in group booking
- SEAT_RESCHEDULED: Seat rescheduled in group booking
Data Structure Pattern
Most audit actions track changes using a consistent structure:
- Each field tracks both old and new values:
{ old: T | null, new: T } old: Previous value (null if field didn't exist before)new: New value after the change- Complete before/after state captured in every record
Exception: The CREATED action captures initial booking state at creation using a flat object with initial values.
Schema Versioning
The audit system uses per-action versioning. Each action maintains its own schema version independently.
Benefits:
- Update one action's schema without affecting others
- Old records handled via discriminated unions (no migration required)
- Strongly-typed schemas for input and storage
Storage Structure:
Version stored separately from audit data: { version, data: {} }
Table Relationships
AuditActor (1) ──────< (many) BookingAudit
↑
│ (soft reference, no FK)
├──────────── User (via userUuid)
│ (soft reference, no FK)
└──────────── Attendee (via attendeeId)
Relationship Details:
- AuditActor → BookingAudit: One-to-Many with FK constraint and
onDelete: Restrict - AuditActor → User: Soft reference (no FK) - nullable to preserve audits after user deletion
- AuditActor → Attendee: Soft reference (no FK) - nullable to preserve audits after attendee deletion
Indexing Strategy
AuditActor Table:
- Indexed on
email,userUuid,attendeeIdfor fast lookups - Indexed on
pseudonymizedAtfor compliance cleanup jobs
BookingAudit Table:
- Indexed on
bookingUid(primary query pattern) - Indexed on
actorId(secondary query pattern) - Indexed on
timestamp(time-based sorting and filtering) - Indexed on
operationId(correlating multi-booking operations)
Special Actors
SYSTEM Actor:
- Fixed UUID representing automated actions
- Used for automated status changes, system-generated meeting URLs, scheduled operations
- Single instance across the entire system
- No userUuid, attendeeId, email, or phone
Design Principles
1. Immutability
Audit records are append-only. Once created, they are never modified or deleted. This ensures complete historical accuracy and a tamper-proof audit trail.
2. Historical Preservation
Actor information is preserved even after source records are deleted. AuditActor records persist with anonymized identity fields. The audit trail remains complete and queryable even after user/attendee deletion.
3. Flexibility
The JSON data field provides schema flexibility for action-specific context without database schema changes. Backward compatible with versioning.
4. Traceability
Every action is fully traceable with who (actor), what (action), when (timestamp), and contextual data.
5. Integrity
Database constraints ensure data quality through foreign keys, onDelete: Restrict protection, unique constraints, and strategic indexes.
6. Reality Over Enforcement
The audit system records actual state, not expected state. It captures what actually happened without enforcing business rules:
- Store actual values from the database
- Record all actions, including anomalies
- Business logic layer enforces rules; audit layer records faithfully
Benefits:
- Shows real system behavior including anomalies
- Easier debugging
- No silent failures
7. Compliance & Data Privacy
GDPR & HIPAA Compliance:
- AuditActor records persist with PII fields nullified when users are deleted
- Maintains immutable audit trail as required by HIPAA §164.312(b)
- GDPR Article 17 compliance through anonymization:
userUuid,email,phone,nameset to null - Application-level logic handles retention policies
8. Queue Privacy
Zero PII in Queue: The audit system works with third-party queue providers without exposing PII:
- UserActor: Only
userUuidis queued - AttendeeActor: Only
attendeeIdis queued - GuestActor: Actor record created before queueing; only
actorIdis queued
Benefits:
- Safe to use third-party queue providers
- GDPR and HIPAA compliant
- Audit trail remains complete even if queue is compromised
Service Architecture
BookingEventHandlerService is the primary entry point for tracking booking changes. It:
- Receives booking events from various parts of the application
- Queues audit tasks via BookingAuditProducerService
- Handles other side effects such as webhooks and notifications
Queue Payload Structure:
bookingUid: String identifier for the bookingactor: ID-only actor object (userUuid,attendeeId, oractorId)organizationId: Number (for feature flag checks)action: Enum value (e.g., "CREATED", "CANCELLED")operationId: Required string for correlating related audit logsdata: Action-specific datatimestamp: Number (milliseconds since epoch)source: Action source (API_V1, API_V2, WEBAPP, WEBHOOK, SYSTEM, UNKNOWN)
BookingAuditTaskConsumer processes audit records:
- Validates queue payload structure
- Resolves actor IDs to AuditActor records
- Routes to appropriate action service for data validation and formatting
- Creates immutable audit records in BookingAudit table
Operation ID
The operationId field correlates audit logs that result from a single user action affecting multiple bookings or across different audit types (BookingAudit, UserAudit, etc.).
Benefits:
- Easy to identify all audits from a single user action
- Track bulk vs individual operations
- Improved debugging and understanding of action scope
- Indexed field for fast lookups
Summary
The Booking Audit System provides a robust, scalable architecture for tracking all booking-related actions:
- Complete Audit Trail: Every action tracked with full context
- Historical Preservation: Data retained even after deletions through PII anonymization
- Flexible Schema: JSON data supports evolution without migrations
- Strong Integrity: Database constraints ensure data quality
- Performance: Strategic indexes, UUID v7 for time-sortable IDs
- HIPAA & GDPR Compliant: Immutable audit records, anonymized actors
- Reality-Based Recording: Captures actual state for debugging
- Independent Audit Trail: Persists after booking deletion
- Operation Correlation: Links related audit logs across different audit types
This architecture supports compliance requirements, debugging, analytics, and provides transparency for users and administrators.