//! The audit log (SPEC §14.4.7, FR-12): the [`AuditWriter`] seam (SPEC §6.3) and its implementation //! [`SqlAuditWriter`], which computes the field diff ([`diff::changes`]) and inserts the `audit_log` row. //! //! Callers (sync push, recurring generation in E-28, E-29 and the scheduler, the importer) call //! [`AuditWriter::write`] inside their own `BEGIN IMMEDIATE` transaction, passing the wire object before and after //! the change; the writer derives `changes` per SPEC §14.4.7 and takes the entry's `server_version` from the same //! transaction. use std::future::Future; use std::pin::Pin; use sqlx::SqliteConnection; use uuid::Uuid; use crate::db; use crate::error::AppError; use crate::wire::{AuditAction, AuditSource, EntityType}; pub mod diff; /// A boxed, sendable future (object-safe async trait methods). pub type BoxFuture<'a, T> = Pin + Send + 'a>>; /// One audit event to record. #[derive(Debug, Clone, PartialEq)] pub struct AuditEvent { pub entity_type: EntityType, pub entity_id: Uuid, pub action: AuditAction, pub source: AuditSource, pub actor_user_id: Uuid, /// `None` for the scheduler and the importer. pub actor_device_id: Option, /// The pushed `updated_at` (for `delete`, the pushed `deleted_at`); server now for scheduler and importer. pub client_at: String, /// Server now. pub server_at: String, /// The full wire object (`Expense` or `Settlement` as JSON) before the change; `None` for `create`. pub before: Option, /// The full wire object after the change. pub after: serde_json::Value, } /// Writes audit entries. pub trait AuditWriter: Send + Sync + 'static { /// Records `event` on `conn`, which is inside the caller's write transaction. Returns the entry's id. fn write<'a>( &'a self, conn: &'a mut SqliteConnection, event: AuditEvent, ) -> BoxFuture<'a, Result>; } /// The real writer: derives `changes` per SPEC §14.4.7 and appends one `audit_log` row (never updated or deleted, /// AC-12.1) with a UUIDv7 id and a `server_version` taken on the caller's transaction. #[derive(Debug, Clone, Copy, Default)] pub struct SqlAuditWriter; impl AuditWriter for SqlAuditWriter { fn write<'a>( &'a self, conn: &'a mut SqliteConnection, event: AuditEvent, ) -> BoxFuture<'a, Result> { Box::pin(async move { let changes = diff::changes( event.entity_type, event.action, event.before.as_ref(), &event.after, ); let changes_json = serde_json::to_string(&changes).map_err(AppError::internal)?; let id = Uuid::now_v7(); let version = db::next_server_version(&mut *conn).await?; sqlx::query( "INSERT INTO audit_log (id, entity_type, entity_id, action, source, actor_user_id, actor_device_id, \ client_at, server_at, changes, server_version) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", ) .bind(id.to_string()) .bind(event.entity_type.as_str()) .bind(event.entity_id.to_string()) .bind(event.action.as_str()) .bind(event.source.as_str()) .bind(event.actor_user_id.to_string()) .bind(event.actor_device_id.map(|d| d.to_string())) .bind(&event.client_at) .bind(&event.server_at) .bind(changes_json) .bind(version) .execute(&mut *conn) .await?; Ok(id) }) } }