"""Entity history service — record, query, restore, and undo entity snapshots.""" from __future__ import annotations import uuid from datetime import datetime, timezone from typing import Any from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.models.entity_history import EntityHistory async def record_history( db: AsyncSession, tenant_id: uuid.UUID, user_id: uuid.UUID | None, entity_type: str, entity_id: uuid.UUID, action: str, snapshot_before: dict[str, Any] | None = None, snapshot_after: dict[str, Any] | None = None, changes: dict[str, Any] | None = None, ) -> EntityHistory: """Create a history entry for a CRUD action.""" entry = EntityHistory( tenant_id=tenant_id, user_id=user_id, entity_type=entity_type, entity_id=entity_id, action=action, snapshot_before=snapshot_before, snapshot_after=snapshot_after, changes=changes, ) db.add(entry) await db.flush() return entry async def get_entity_history( db: AsyncSession, tenant_id: uuid.UUID, entity_type: str, entity_id: uuid.UUID, limit: int = 50, ) -> list[EntityHistory]: """Get all history entries for an entity, newest first.""" q = ( select(EntityHistory) .where( EntityHistory.tenant_id == tenant_id, EntityHistory.entity_type == entity_type, EntityHistory.entity_id == entity_id, ) .order_by(EntityHistory.created_at.desc()) .limit(limit) ) result = await db.execute(q) return list(result.scalars().all()) async def get_history_entry( db: AsyncSession, tenant_id: uuid.UUID, history_id: uuid.UUID, ) -> EntityHistory | None: """Get a specific history entry by ID.""" q = select(EntityHistory).where( EntityHistory.id == history_id, EntityHistory.tenant_id == tenant_id, ) result = await db.execute(q) return result.scalar_one_or_none() async def restore_from_history( db: AsyncSession, tenant_id: uuid.UUID, history_id: uuid.UUID, user_id: uuid.UUID, ) -> dict[str, Any]: """Restore an entity to a previous snapshot state. For 'delete' actions: un-delete the entity (clear deleted_at). For 'update' actions: revert entity fields to snapshot_before. For 'create' actions: soft-delete the entity (undo creation). Returns the restored data dict. """ entry = await get_history_entry(db, tenant_id, history_id) if entry is None: raise ValueError("History entry not found") entity_type = entry.entity_type entity_id = entry.entity_id action = entry.action # Import here to avoid circular imports from app.models.contact import Contact if entity_type == "contact": q = select(Contact).where( Contact.id == entity_id, Contact.tenant_id == tenant_id, ) result = await db.execute(q) contact = result.scalar_one_or_none() if action == "delete": # Un-delete: clear deleted_at if contact is None: raise ValueError("Entity not found for restore") contact.deleted_at = None contact.updated_by = user_id await db.flush() from app.services.contact_service import _serialize_contact_detail from sqlalchemy.orm import selectinload q2 = ( select(Contact) .options(selectinload(Contact.contact_persons)) .where(Contact.id == contact.id) ) result2 = await db.execute(q2) contact = result2.scalar_one() return _serialize_contact_detail(contact) elif action == "update": # Revert to snapshot_before if contact is None: raise ValueError("Entity not found for restore") if entry.snapshot_before is None: raise ValueError("No snapshot_before available for restore") for key, value in entry.snapshot_before.items(): if hasattr(contact, key) and key not in ("id", "tenant_id", "created_at", "updated_at", "deleted_at"): setattr(contact, key, value) contact.updated_by = user_id await db.flush() from app.services.contact_service import _serialize_contact_detail from sqlalchemy.orm import selectinload q2 = ( select(Contact) .options(selectinload(Contact.contact_persons)) .where(Contact.id == contact.id) ) result2 = await db.execute(q2) contact = result2.scalar_one() return _serialize_contact_detail(contact) elif action == "create": # Undo creation: soft-delete the entity if contact is None: raise ValueError("Entity not found for restore") contact.deleted_at = datetime.now(timezone.utc) contact.updated_by = user_id await db.flush() from app.services.contact_service import _serialize_contact_detail from sqlalchemy.orm import selectinload q2 = ( select(Contact) .options(selectinload(Contact.contact_persons)) .where(Contact.id == contact.id) ) result2 = await db.execute(q2) contact = result2.scalar_one() return _serialize_contact_detail(contact) raise ValueError(f"Unsupported entity type for restore: {entity_type}") async def undo_last_action( db: AsyncSession, tenant_id: uuid.UUID, user_id: uuid.UUID, entity_type: str, entity_id: uuid.UUID, ) -> dict[str, Any]: """Undo the most recent action for an entity. Returns the restored entity data. Raises ValueError if no history exists. """ q = ( select(EntityHistory) .where( EntityHistory.tenant_id == tenant_id, EntityHistory.entity_type == entity_type, EntityHistory.entity_id == entity_id, ) .order_by(EntityHistory.created_at.desc()) .limit(1) ) result = await db.execute(q) entry = result.scalar_one_or_none() if entry is None: raise ValueError("No history found for this entity") return await restore_from_history(db, tenant_id, entry.id, user_id)