""" Audit log utility — all services call log_action() to record staff events. Usage: from shared.audit import log_action await log_action( db, actor_id, actor_name, action="CREATE", entity_type="customer", entity_id=cust_id, entity_label="Church of St. George", ) await log_action( db, actor_id, actor_name, action="UPDATE", entity_type="order", entity_id=order_id, entity_label="ORD-0042", changes={"status": {"old": "negotiating", "new": "confirmed"}}, ) Never raises — a logging failure must never break the primary operation. The call is fire-and-forget safe: wrap in try/except internally. """ from datetime import datetime, timezone from typing import Any from sqlalchemy.ext.asyncio import AsyncSession from shared.orm import AuditLog async def log_action( db: AsyncSession, actor_id: str, actor_name: str, action: str, entity_type: str, entity_id: str, entity_label: str | None = None, changes: dict[str, Any] | None = None, meta: dict[str, Any] | None = None, ) -> None: """ Insert one row into audit_log. Never raises — failures are silently swallowed so a logging error never disrupts the primary request. """ try: entry = AuditLog( occurred_at=datetime.now(timezone.utc), actor_id=actor_id, actor_name=actor_name, action=action, entity_type=entity_type, entity_id=entity_id, entity_label=entity_label, changes=changes, meta=meta, ) db.add(entry) # Flush without committing — caller's transaction commits it atomically. # If the caller hasn't started a transaction, flush still works; the # session will auto-commit on the next explicit commit call. await db.flush() except Exception: pass def diff(old: dict, new: dict) -> dict[str, dict]: """ Build a changes dict from two flat dicts. Only includes keys whose values actually changed. Skip internal/unloggable keys (hashed_password, updated_at). Usage: changes = diff(old_record, new_record) await log_action(..., changes=changes or None) """ _SKIP = {"hashed_password", "updated_at", "firestore_id"} return { k: {"old": old.get(k), "new": new.get(k)} for k in new if k not in _SKIP and old.get(k) != new.get(k) }