"""Data retention service: auto-purge expired presentations per client policy.""" import os import uuid from datetime import datetime, timedelta, timezone from typing import Optional from sqlalchemy import select, and_ from sqlalchemy.ext.asyncio import AsyncSession from models.sql.client import ClientModel from models.sql.presentation import PresentationModel from services import audit_service from services.database import async_session_maker from utils.datetime_utils import get_current_utc_datetime class RetentionService: async def run_cleanup(self) -> dict: """Soft-delete presentations older than each client's retention_days. Intended to be run daily via ARQ cron. Returns summary of actions taken. """ deleted_count = 0 errors = 0 async with async_session_maker() as session: # Find all clients with a retention policy stmt = select(ClientModel).where(ClientModel.retention_days.isnot(None)) result = await session.execute(stmt) clients = list(result.scalars().all()) for client in clients: try: cutoff = datetime.now(timezone.utc) - timedelta( days=client.retention_days ) # Find non-deleted presentations older than cutoff pres_stmt = select(PresentationModel).where( and_( PresentationModel.client_id == client.id, PresentationModel.created_at < cutoff, PresentationModel.deleted_at.is_(None), ) ) pres_result = await session.execute(pres_stmt) expired = list(pres_result.scalars().all()) for presentation in expired: presentation.deleted_at = get_current_utc_datetime() deleted_count += 1 audit_service.log( user_id=None, action="retention_delete", resource_type="presentation", resource_id=presentation.id, client_id=client.id, details={ "retention_days": client.retention_days, "created_at": presentation.created_at.isoformat() if presentation.created_at else None, }, ) await session.commit() except Exception as e: errors += 1 print(f"Retention cleanup error for client {client.id}: {e}") return {"deleted": deleted_count, "errors": errors} async def purge_soft_deleted(self, days_after_soft_delete: int = 30) -> dict: """Permanently delete records that were soft-deleted more than N days ago. Intended to be run weekly via ARQ cron. """ purged_count = 0 async with async_session_maker() as session: cutoff = datetime.now(timezone.utc) - timedelta( days=days_after_soft_delete ) stmt = select(PresentationModel).where( and_( PresentationModel.deleted_at.isnot(None), PresentationModel.deleted_at < cutoff, ) ) result = await session.execute(stmt) expired = list(result.scalars().all()) for presentation in expired: # Delete associated export files if they exist self._cleanup_files(presentation) await session.delete(presentation) purged_count += 1 await session.commit() return {"purged": purged_count} def _cleanup_files(self, presentation: PresentationModel) -> None: """Remove associated files from filesystem.""" try: if presentation.file_paths: for path in presentation.file_paths: if path and os.path.exists(path): os.remove(path) except Exception as e: print(f"File cleanup error for presentation {presentation.id}: {e}")