# Audit Event Logging

## Overview

Audit Event Logging captures create, update, and delete operations performed across all ERP modules as immutable audit entries. Each entry records a unique `eventId` for deduplication and idempotency, an optional `correlationId` for grouping related events within a logical operation (e.g., bulk updates, cascading workflows, saga steps), the actor identity (actorType and actorId, with optional metadata such as IP address, user agent, sessionId, requestId, or onBehalfOf reference), the operation type (CREATE, UPDATE, DELETE), the affected entity reference (entityType and entityId), and a precise timestamp. Entries may optionally be scoped by companyId for multi-company data isolation; entries for global entities (e.g., user accounts, currencies) omit companyId and are queryable without company scope.

Audit entries are strictly append-only — once written, they cannot be modified or deleted. This immutability guarantee is fundamental to compliance, forensic investigation, and regulatory reporting. The audit log serves as a universal change history that is queryable by actor, entity type, specific record, operation type, and date range, enabling both real-time monitoring and historical analysis of system activity.

Other modules emit audit events by calling the audit module's ingestion interface. Every audit event submitted must include: `eventId` (UUID, unique, used for idempotency), `actorType` (USER/SYSTEM/SERVICE), `actorId`, `entityType` (must match a registered auditable entity), `entityId`, `operationType` (CREATE/UPDATE/DELETE), and `changes` (array of ChangeDetail). Optional fields include: `correlationId` (UUID, groups related events from a single logical operation), `onBehalfOf` (delegated userId), `actorMetadata` (ipAddress, userAgent, sessionId, requestId), and `companyId` (required for company-bound entities, must be null for global entities). The timestamp is always system-generated at ingestion time and cannot be supplied or overridden by the caller, ensuring a consistent and tamper-resistant time source.

The audit module enforces idempotency on `eventId` — submitting the same eventId more than once silently discards the duplicate (returns success, no new entry). This allows emitting modules to safely retry on transient failures. Ingestion is eventually consistent with the source operation: emitting modules complete their own database transaction first, then submit the audit event. The audit module does not participate in the emitting module's transaction, preventing audit failures from blocking business operations. If the audit write fails permanently after retries, the emitting module logs the failure for operational alerting. Duplicate eventIds are silently accepted; missing required fields, unknown entityTypes, and scope mismatches are rejected with validation errors; events for entity + operation combinations with no active policy are silently discarded.

Modules that want their entities to be auditable register them with the audit module at initialization time, declaring `entityName`, `entityScope` (COMPANY_BOUND or GLOBAL), and `auditableFields` (list of field names eligible for audit capture). The audit module validates that `entityType` in incoming events matches a registered entity.

## Business Purpose

Audit Event Logging provides an append-only record of data mutations captured by the system. Because ingestion is eventually consistent and decoupled from source transactions, coverage is best-effort — not every mutation is guaranteed to be recorded (see ingestion contract above). However, once an entry is written it is immutable and cannot be altered or deleted. This enables:

- Regulatory compliance: immutable, append-only logs support SOX, GDPR, and industry-specific audit trail requirements (applications requiring guaranteed capture should layer an outbox or replay mechanism on top)
- Forensic investigation: security teams can trace exactly who or what changed data, when, and from where
- Operational accountability: managers can review change history for any business record to understand how it reached its current state
- Multi-company isolation: company-scoped audit entries ensure each legal entity's audit trail is independently queryable
- Global entity auditing: changes to foundational, non-company-scoped entities (users, currencies, UoM) are captured without requiring a company context
- Change attribution: every mutation is linked to a specific actor identity (human user, system process, or service account), eliminating ambiguity about responsibility

The append-only constraint on stored entries ensures:

- Historical integrity of recorded entries is preserved — no entry can be retroactively altered or removed
- Stored audit trails remain admissible for external auditors and regulators
- System administrators cannot suppress evidence of recorded operations

## Process Flow

```mermaid
flowchart TD
    A[Actor Performs Operation] --> B[System Identifies Operation Type]
    B --> C{Operation Type}
    C -->|CREATE| D[Capture New Entity State]
    C -->|UPDATE| E[Capture Changed Entity State]
    C -->|DELETE| F[Capture Deleted Entity Reference]
    D --> G[Resolve Actor Identity]
    E --> G
    F --> G
    G --> H[Record actorType + actorId + Metadata]
    H --> I[Determine Scope: company or global]
    I --> J[Build AuditEntry]
    J --> K[Write Immutable Entry]
    K --> L[Entry Available for Query]
```

Querying the audit log follows this pattern. All query paths enforce authorization — the caller must hold `viewAuditHistory` (for detail queries) or `viewAuditSummary` (for aggregate queries) at the appropriate scope:

```mermaid
flowchart TD
    A[Query Request] --> AA{Caller holds required permission?}
    AA -->|No| AB[Reject: insufficient permissions]
    AA -->|Yes| B{Filter Criteria}
    B --> C[By Actor: actorType + actorId]
    B --> D[By Entity Type + Entity ID]
    B --> E[By Operation Type]
    B --> F[By Date Range]
    C --> G{Scope Filter}
    D --> G
    E --> G
    F --> G
    G -->|companyId provided| H[Return company-scoped entries\nif caller has company-scoped permission]
    G -->|no companyId| I[Return global entries only\nif caller has global-scoped permission]
    G -->|both requested| J[Return combined results\nfor scopes caller is authorized for]
```

## Scenario Patterns

- **Record Creation Tracking**: A user creates a new sales order. The system writes an AuditEntry with operation CREATE, actorType = USER, the actor's actorId (userId), the entityType (e.g., SalesOrder), the new record's entityId, companyId, and a timestamp — providing a permanent record of who created the order and when
- **Field Update History**: A user modifies the status of a purchase order. An AuditEntry with operation UPDATE is appended, capturing the actor identity and the affected entity reference, enabling reviewers to see the full change timeline for that record
- **Deletion Accountability**: An operator deletes a draft invoice. An AuditEntry with operation DELETE is written, ensuring the deletion is permanently recorded even though the original record no longer exists
- **System-Initiated Change**: A scheduled job recalculates exchange rates. The system writes an AuditEntry with actorType = SYSTEM, actorId referencing the job identifier, and no companyId (global scope), capturing automated changes with the same fidelity as human-initiated ones
- **Delegated Action**: An administrator performs an action on behalf of another user. The AuditEntry records actorType = USER, actorId = the administrator's userId, and onBehalfOf = the target user's userId, preserving the full delegation chain
- **Security Investigation**: A security team investigates suspicious activity by querying all audit entries for a specific actorId within a date range, across all operation types, to reconstruct the actor's actions
- **Compliance Audit**: An external auditor requests the full change history for a specific entity (e.g., all mutations to a particular general ledger account) by querying audit entries filtered by entityType and entityId within a fiscal period
- **Multi-Company Audit Isolation**: A parent organization runs an audit for one subsidiary. Queries are scoped by companyId, returning only audit entries belonging to that legal entity without leaking data from other companies
- **Global Entity Audit**: An auditor reviews all changes to user accounts (a non-company-scoped entity). Queries omit companyId to retrieve the full global change history for user-management entities

## Test Cases

- Every accepted audit event for an entity and operation type that has an active policy produces exactly one AuditEntry — except UPDATE events with an empty changes array (zero-delta), which are silently accepted as a no-op without creating an AuditEntry
- AuditEntry contains required fields: eventId, actorType, actorId, entityType, entityId, operationType, and timestamp
- companyId is required for company-scoped entities and optional (null) for global entities
- actorType is required and must be one of USER, SYSTEM, or SERVICE
- actorId is required and must be non-empty regardless of actorType
- onBehalfOf is optional and stores the delegated user's identity when an action is performed on behalf of another actor
- AuditEntries cannot be modified after creation — update attempts are rejected
- AuditEntries cannot be deleted — delete attempts are rejected
- Actor metadata (IP address, user agent) is optional and stored when provided
- Querying by actorId returns only entries for that actor
- Querying by entityType and entityId returns the full change history for a specific record
- Querying by operationType filters entries to only the specified operation (CREATE, UPDATE, or DELETE)
- Querying by date range returns only entries whose timestamp falls within the range
- Querying with companyId returns only entries scoped to that company
- Querying without companyId returns only global (company-unscoped) entries
- An operation performed without a valid actor identity (actorType + actorId) is rejected
- Timestamp is system-generated and cannot be overridden by the caller
- Audit entries are returned in chronological order by default
- Large result sets support pagination for efficient retrieval
- Every audit event must include a unique eventId (UUID)
- Submitting an event with a duplicate eventId is silently accepted without creating a new entry
- correlationId is optional and links related events from a single logical operation
- Events referencing an unregistered entityType are rejected with a validation error
- Events with companyId on a global entity are rejected with a validation error
- Events with null companyId on a company-bound entity are rejected with a validation error
- Events for an entity + operation with no active policy are silently discarded
- Entity registration requires entityName, entityScope, and auditableFields
- actorMetadata (ipAddress, userAgent, sessionId, requestId) is optional and stored when provided
- Querying audit entries requires the `viewAuditHistory` permission at the appropriate scope
- A caller with company-scoped `viewAuditHistory` can only retrieve entries for their assigned companies
- A caller with global-scoped `viewAuditHistory` can retrieve global (company-unscoped) entries
- A caller without `viewAuditHistory` permission receives an authorization error when querying audit entries
- Querying audit summaries requires the `viewAuditSummary` permission at the appropriate scope

## Reference Links

- [Odoo Mail Tracking (mail.thread)](https://www.odoo.com/documentation/19.0/developer/reference/backend/orm.html#odoo.models.Model)
- [Dynamics 365 Database Logging (sysdatabaselog)](https://learn.microsoft.com/en-us/dynamics365/fin-ops-core/dev-itpro/sysadmin/configure-manage-database-log)
- [SAP Change Document Header (CDHDR)](https://help.sap.com/docs/SAP_S4HANA_ON-PREMISE/f76052846e404ea8811eb06534b6e099/5e13e82b8ca111d1a6090000e8353423.html)
- [Oracle Financials Audit Trail](https://docs.oracle.com/en/cloud/saas/financials/24d/faigl/overview-of-financials.html)
- [AWS CloudTrail Documentation](https://docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudtrail-user-guide.html)
- [AWS CloudTrail userIdentity](https://docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudtrail-event-reference-user-identity.html)
