"""Lightweight operator audit helpers. v4.8 records sensitive/manual operator actions without adding a new table. The durable audit stream is stored in business_events with event_type ``operator_action`` and, when a task_id is present, mirrored to task_events. """ from __future__ import annotations import json from typing import Any, Dict, Optional from sqlalchemy import text from app.db import engine def _json(value: Any) -> str: return json.dumps(value or {}, ensure_ascii=False) def record_operator_action( *, action: str, entity_type: str, entity_id: Optional[str] = None, actor: str = "operator", task_id: Optional[str] = None, opportunity_id: Optional[str] = None, conversation_id: Optional[str] = None, contact_id: Optional[str] = None, before: Optional[Dict[str, Any]] = None, after: Optional[Dict[str, Any]] = None, payload: Optional[Dict[str, Any]] = None, ) -> None: """Record a manual/sensitive operator action. Audit SQL invariant: event_type = 'operator_action'. This intentionally avoids a schema migration. ``business_events.task_id`` is nullable, so it can hold system/outbox/admin events as a generic audit log. """ action = str(action or "operator_action").strip() or "operator_action" entity_type = str(entity_type or "unknown").strip() or "unknown" audit_payload = { "action": action, "entity_type": entity_type, "entity_id": str(entity_id or task_id or opportunity_id or ""), "actor": str(actor or "operator"), "opportunity_id": opportunity_id, "before": before or {}, "after": after or {}, **(payload or {}), } task_uuid = task_id if task_id else None with engine.begin() as conn: conn.execute(text(""" INSERT INTO business_events ( event_type, task_id, conversation_id, contact_id, payload, created_by ) VALUES ( 'operator_action', CAST(:task_id AS UUID), :conversation_id, :contact_id, CAST(:payload AS JSONB), :created_by ) """), { "task_id": task_uuid, "conversation_id": conversation_id, "contact_id": contact_id, "payload": _json(audit_payload), "created_by": str(actor or "operator"), }) if task_id: conn.execute(text(""" INSERT INTO task_events ( task_id, event_type, payload, created_by ) VALUES ( CAST(:task_id AS UUID), 'operator_action', CAST(:payload AS JSONB), :created_by ) """), { "task_id": task_id, "payload": _json(audit_payload), "created_by": str(actor or "operator"), }) def record_operator_action_best_effort(**kwargs: Any) -> None: """Audit helper that must never break the operational action itself.""" try: record_operator_action(**kwargs) except Exception as exc: # pragma: no cover - defensive logging only print(f"ClientFlow operator audit failed: {exc}", flush=True)