# LogAuditEvent

## Permission Scope

auditEvent

## Overview

LogAuditEvent ingests a structured audit event from an emitting module, creating an immutable AuditEntry with linked ChangeDetail records. The command enforces idempotency on eventId — duplicate submissions are silently discarded. Events are validated against registered auditable entities and active audit policies. The timestamp is system-generated and cannot be supplied by the caller.

This command is the primary ingestion interface for the audit module and is called by other modules after they commit their own transactions.

## Business Rules

- eventId must be a UUID and globally unique; duplicate eventId submissions are silently accepted (no new entry created)
- actorType is required and must be one of USER, SYSTEM, or SERVICE
- actorId is required and must be non-empty
- entityType must reference a registered auditable entity
- entityId is required and must be non-empty
- operationType must be one of CREATE, UPDATE, or DELETE
- companyId is required for company-bound entities and must be null for global entities
- timestamp is system-generated at ingestion time; caller-supplied timestamps are ignored
- An active audit policy must exist for the entity + operation + scope combination; events with no matching active policy are silently discarded
- changes array must contain at least one ChangeDetail for CREATE and DELETE operations
- For UPDATE operations, only fields that actually changed should be included in changes
- For UPDATE operations, if the changes array is empty (no fields actually changed), the event is a no-op: no AuditEntry or ChangeDetail records are created, and the command returns success silently — this prevents audit noise from saves that do not modify any field values
- After policy field-rule filtering and auditable-field filtering, if no processable changes remain:
  - UPDATE: treated as a no-op (same as empty changes array above)
  - CREATE / DELETE: rejected with VALIDATION_ERROR because these operations require at least one auditable change to produce a meaningful entry
- Field values are normalized by type before sensitivity processing:
  - Scalar fields (string, number, boolean, date, enum) are serialized as strings and stored as-is
  - Rich text / HTML field values are stripped to plain text before storage
  - Collection / array field values are serialized as JSON strings
  - Relation field values capture the foreign key ID as a string
  - File attachment fields capture the reference metadata (file ID, filename, size), not the file content
  - Binary / BLOB fields are excluded from audit capture entirely and are not eligible for policy field rules
  - Computed / derived fields are excluded from audit capture
- Fields in the changes array that are not listed in the entity's registered auditableFields are silently dropped — no ChangeDetail is created for them and no error is raised
- Fields in the changes array that are listed in auditableFields but not included in the active policy's field rules (when the policy has explicit field rules) are also silently dropped
- Field values are processed through the applicable policy's sensitivity modes after normalization
- Values exceeding 4,000 characters are truncated with a `[truncated]` suffix after sensitivity processing
- correlationId is optional and groups related events from a single logical operation; if provided, it must be a valid UUID — invalid values are rejected with VALIDATION_ERROR
- actorMetadata (ipAddress, userAgent, sessionId, requestId) is optional
- onBehalfOf is optional and records the delegated user's identity

## Process Flow

```mermaid
flowchart TD
    A[Receive audit event] --> A2{Caller has logAuditEvent permission?}
    A2 -->|No| A3[Return error: INSUFFICIENT_PERMISSION]
    A2 -->|Yes| B{Duplicate eventId?}
    B -->|Yes| C[Return success, no new entry]
    B -->|No| D{entityType registered?}
    D -->|No| E[Return error: ENTITY_TYPE_NOT_REGISTERED]
    D -->|Yes| F{Validate required fields}
    F -->|Invalid| G[Return validation error]
    F -->|Valid| H{Company scope matches entity scope?}
    H -->|No| I[Return error: SCOPE_MISMATCH]
    H -->|Yes| H2{correlationId provided and not valid UUID?}
    H2 -->|Yes| H3[Return error: VALIDATION_ERROR]
    H2 -->|No| J{Active policy exists for entity + operation + scope?}
    J -->|No| K[Silently discard, return success]
    J -->|Yes| J2{UPDATE with empty changes array?}
    J2 -->|Yes| J3[No-op: return success, no entry created]
    J2 -->|No| J4{CREATE or DELETE with empty changes?}
    J4 -->|Yes| J5[Return error: VALIDATION_ERROR]
    J4 -->|No| L[Apply policy field rules and sensitivity modes]
    L --> L2{Any processable changes remain after filtering?}
    L2 -->|No, UPDATE| L3[No-op: return success, no entry created]
    L2 -->|No, CREATE/DELETE| L4[Return error: VALIDATION_ERROR]
    L2 -->|Yes| M[Generate system timestamp]
    M --> N[Create AuditEntry]
    N --> O[Create ChangeDetail records]
    O --> P[Return success]
```

## External Dependencies

- None (internal module command; emitting modules call this interface)

## Error Scenarios

- **INSUFFICIENT_PERMISSION**: Caller lacks the required permission for this operation
- **ENTITY_TYPE_NOT_REGISTERED**: Provided entityName/entityType does not match any currently registered auditable entity
- **INVALID_ACTOR**: actorType is missing or not one of USER, SYSTEM, SERVICE; or actorId is empty
- **INVALID_OPERATION_TYPE**: operationType is missing or not one of CREATE, UPDATE, DELETE
- **SCOPE_MISMATCH**: companyId provided for a global entity, or companyId missing for a company-bound entity
- **MISSING_ENTITY_ID**: entityId is empty or not provided
- **MISSING_EVENT_ID**: eventId is empty or not a valid UUID
- **VALIDATION_ERROR**: Input failed validation for this operation

## Test Cases

- creates AuditEntry with all required fields for a CREATE operation
- creates AuditEntry with all required fields for an UPDATE operation
- creates AuditEntry with all required fields for a DELETE operation
- silently skips AuditEntry creation for UPDATE with empty changes array (zero-delta no-op)
- rejects CREATE with empty changes array
- rejects DELETE with empty changes array
- silently discards duplicate eventId without creating a new entry
- rejects event with unregistered entityType
- rejects event with missing actorType
- rejects event with invalid actorType
- rejects event with empty actorId
- rejects event with invalid operationType
- rejects event with companyId on a global entity
- rejects event with null companyId on a company-bound entity
- silently discards event when no active policy matches entity + operation + scope
- generates system timestamp and ignores caller-supplied timestamp
- creates ChangeDetail records linked to the parent AuditEntry
- applies MASK sensitivity mode to field values
- applies HASH sensitivity mode to field values
- applies EXCLUDE sensitivity mode storing null for both oldValue and newValue
- serializes scalar field values as strings
- strips rich text / HTML values to plain text before storage
- serializes collection / array field values as JSON strings
- captures relation field values as foreign key ID strings
- captures file attachment fields as reference metadata only (file ID, filename, size)
- excludes binary / BLOB fields from audit capture
- excludes computed / derived fields from audit capture
- silently drops fields from changes array that are not in entity's registered auditableFields
- silently drops fields from changes array that are not in the active policy's field rules when policy has explicit rules
- truncates values exceeding 4,000 characters with [truncated] suffix
- stores correlationId when provided
- stores actorMetadata (ipAddress, userAgent, sessionId, requestId) when provided
- stores onBehalfOf when provided
- creates ChangeDetail with null oldValue for CREATE operations
- creates ChangeDetail with null newValue for DELETE operations
- filters out unchanged fields for UPDATE operations
- returns no-op when all UPDATE changes are unchanged after normalization
- enforces oldValue=null for CREATE even if caller provides oldValue
- enforces newValue=null for DELETE even if caller provides newValue
- rejects event with empty entityId
- rejects event with empty eventId
- rejects event with non-UUID eventId
- rejects event with non-UUID correlationId
- rejects CREATE when all changes are filtered out by policy rules
- rejects DELETE when all changes are filtered out by policy rules
- returns no-op for UPDATE when all changes are filtered out by policy rules
- rejects caller without logAuditEvent permission
