# app/core/soft_delete.py """ Soft-delete utility functions. Provides helpers for soft-deleting, restoring, and cascade soft-deleting records that use the SoftDeleteMixin. Usage: from app.core.soft_delete import soft_delete, restore, soft_delete_cascade # Simple soft delete soft_delete(db, user, deleted_by_id=admin.id) # Cascade soft delete (merchant + all stores + their children) soft_delete_cascade(db, merchant, deleted_by_id=admin.id, cascade_rels=[ ("stores", [("products", []), ("customers", []), ("orders", []), ("store_users", [])]), ]) # Restore a soft-deleted record from app.modules.tenancy.models import User restore(db, User, entity_id=42, restored_by_id=admin.id) """ import logging from datetime import UTC, datetime from sqlalchemy import select from sqlalchemy.orm import Session logger = logging.getLogger(__name__) def soft_delete(db: Session, entity, deleted_by_id: int | None = None) -> None: """ Mark an entity as soft-deleted. Sets deleted_at to now and deleted_by_id to the actor. Does NOT call db.commit() — caller is responsible. Args: db: Database session. entity: SQLAlchemy model instance with SoftDeleteMixin. deleted_by_id: ID of the user performing the deletion. """ entity.deleted_at = datetime.now(UTC) entity.deleted_by_id = deleted_by_id db.flush() logger.info( f"Soft-deleted {entity.__class__.__name__} id={entity.id} " f"by user_id={deleted_by_id}" ) def restore( db: Session, model_class, entity_id: int, restored_by_id: int | None = None, ): """ Restore a soft-deleted entity. Queries with include_deleted=True to find the record, then clears deleted_at and deleted_by_id. Args: db: Database session. model_class: SQLAlchemy model class. entity_id: ID of the entity to restore. restored_by_id: ID of the user performing the restore (for logging). Returns: The restored entity. Raises: ValueError: If entity not found. """ entity = db.execute( select(model_class).filter(model_class.id == entity_id), execution_options={"include_deleted": True}, ).scalar_one_or_none() if entity is None: raise ValueError(f"{model_class.__name__} with id={entity_id} not found") if entity.deleted_at is None: raise ValueError(f"{model_class.__name__} with id={entity_id} is not deleted") entity.deleted_at = None entity.deleted_by_id = None db.flush() logger.info( f"Restored {model_class.__name__} id={entity_id} " f"by user_id={restored_by_id}" ) return entity def soft_delete_cascade( db: Session, entity, deleted_by_id: int | None = None, cascade_rels: list[tuple[str, list]] | None = None, ) -> int: """ Soft-delete an entity and recursively soft-delete its children. Args: db: Database session. entity: SQLAlchemy model instance with SoftDeleteMixin. deleted_by_id: ID of the user performing the deletion. cascade_rels: List of (relationship_name, child_cascade_rels) tuples. Example: [("stores", [("products", []), ("customers", [])])] Returns: Total number of records soft-deleted (including the root entity). """ count = 0 # Soft-delete the entity itself soft_delete(db, entity, deleted_by_id) count += 1 # Recursively soft-delete children if cascade_rels: for rel_name, child_cascade in cascade_rels: children = getattr(entity, rel_name, None) if children is None: continue # Handle both collections and single items (uselist=False) if not isinstance(children, list): children = [children] for child in children: if hasattr(child, "deleted_at") and child.deleted_at is None: count += soft_delete_cascade( db, child, deleted_by_id, child_cascade ) return count