""" Soft delete utilities. Provides reusable functions to convert hard-delete operations into soft-deletes. The `soft_delete_record` function works with any model that inherits `SoftDeleteMixin` (which is included in `DefaultMixin`). Usage in service files: from src.soft_delete import soft_delete_record, apply_not_deleted_filter async def delete(*, db_session, record_id: str): await soft_delete_record(db_session=db_session, model=MyModel, record_id=record_id) async def get_all(*, db_session, ...): query = Select(MyModel) query = apply_not_deleted_filter(query, MyModel) ... """ import logging from datetime import datetime from typing import TypeVar, Type from sqlalchemy import Select, Update from sqlalchemy.ext.asyncio import AsyncSession import pytz from src.config import TIMEZONE log = logging.getLogger(__name__) T = TypeVar("T") def apply_not_deleted_filter(query: Select, model) -> Select: """ Appends a WHERE clause to exclude soft-deleted records. Use this on all "get" queries to hide deleted data. Example: query = Select(OverhaulScope) query = apply_not_deleted_filter(query, OverhaulScope) """ if hasattr(model, "deleted_at"): return query.filter(model.deleted_at.is_(None)) return query async def soft_delete_record( *, db_session: AsyncSession, model: Type[T], record_id: str, id_column: str = "id", ) -> None: """ Soft-delete a record by setting its `deleted_at` timestamp. Instead of DELETE, this performs an UPDATE. Args: db_session: The async database session. model: The SQLAlchemy model class. record_id: The value of the ID to match. id_column: The name of the ID column (default: "id"). """ now = datetime.now(pytz.timezone(TIMEZONE)) id_col = getattr(model, id_column) stmt = ( Update(model) .where(id_col == record_id) .values(deleted_at=now) ) await db_session.execute(stmt) await db_session.commit() log.info(f"Soft-deleted {model.__tablename__} record: {record_id}")