"""Recording and reading editor change history. The Mix Editor and Ingredients Editor write a row here on every create/edit so each mix and ingredient carries an auditable history (who changed what, when). """ from __future__ import annotations from typing import Any from sqlalchemy import select from sqlalchemy.orm import Session from app.api.deps import AuthSession from app.models.change_event import EditorChangeEvent # Entity types — these match the History buttons on the two editors. ENTITY_MIX = "mix" ENTITY_INGREDIENT = "ingredient" def _stringify(value: Any) -> str | None: if value is None: return None if isinstance(value, bool): return "Yes" if value else "No" if isinstance(value, float): # Trim trailing zeros so 12.50 reads as 12.5 and 12.0 as 12. text = f"{value:.4f}".rstrip("0").rstrip(".") return text or "0" return str(value) def diff_fields(before: dict[str, Any], after: dict[str, Any], labels: dict[str, str]) -> list[dict[str, Any]]: """Build a list of ``{field, label, before, after}`` deltas for changed fields. Only keys present in ``labels`` are considered, and only those whose value actually changed are emitted. """ deltas: list[dict[str, Any]] = [] for field, label in labels.items(): if field not in after: continue old = before.get(field) new = after.get(field) if old == new: continue deltas.append({"field": field, "label": label, "before": _stringify(old), "after": _stringify(new)}) return deltas def record_change( db: Session, *, session: AuthSession, entity_type: str, entity_id: int, action: str, summary: str, changes: list[dict[str, Any]] | None = None, ) -> None: """Append a change event. Caller is responsible for committing the session.""" db.add( EditorChangeEvent( tenant_id=session.tenant_id or "", entity_type=entity_type, entity_id=entity_id, action=action, actor_name=session.name or session.email or "Unknown", actor_email=session.email or "", actor_role=session.client_role or session.role, summary=summary, changes=changes or [], ) ) def list_changes( db: Session, *, tenant_id: str, entity_type: str, entity_id: int, limit: int = 200, ) -> list[EditorChangeEvent]: return list( db.scalars( select(EditorChangeEvent) .where( EditorChangeEvent.tenant_id == tenant_id, EditorChangeEvent.entity_type == entity_type, EditorChangeEvent.entity_id == entity_id, ) .order_by(EditorChangeEvent.created_at.desc(), EditorChangeEvent.id.desc()) .limit(limit) ).all() )