from __future__ import annotations from datetime import datetime, timezone from typing import Iterable from sqlalchemy import Select, func, select, update from sqlalchemy.orm import Session from app.modules.alerts.models import UserAlert from app.modules.core.iam.models import User from app.modules.email_integration.event_service import send_alert_created_email ALERT_PRIORITIES = ("low", "normal", "high", "critical") ALERT_TYPES = ( "general", "task_assigned", "task_due", "task_overdue", "task_review", "document_uploaded", "clarification", "attendance", "leave", "payroll", "consultant", "client", ) def normalize_priority(priority: str | None) -> str: value = (priority or "normal").strip().lower() return value if value in ALERT_PRIORITIES else "normal" def normalize_alert_type(alert_type: str | None) -> str: value = (alert_type or "general").strip().lower() return value or "general" def create_alert( db: Session, *, user_id: int, title: str, message: str | None = None, tenant_id: int | None = None, branch_id: int | None = None, role_context: str | None = None, alert_type: str = "general", priority: str = "normal", target_url: str | None = None, created_by_user_id: int | None = None, commit: bool = True, ) -> UserAlert: alert = UserAlert( tenant_id=tenant_id, branch_id=branch_id, user_id=user_id, role_context=(role_context or None), alert_type=normalize_alert_type(alert_type), priority=normalize_priority(priority), title=(title or "Alert").strip()[:255], message=(message or None), target_url=(target_url or None), created_by_user_id=created_by_user_id, ) db.add(alert) db.flush() try: send_alert_created_email(db, alert) except Exception: # Email notification must never block in-app alert creation. pass if commit: db.commit() db.refresh(alert) return alert def create_bulk_alerts( db: Session, *, user_ids: Iterable[int], title: str, message: str | None = None, tenant_id: int | None = None, branch_id: int | None = None, role_context: str | None = None, alert_type: str = "general", priority: str = "normal", target_url: str | None = None, created_by_user_id: int | None = None, ) -> list[UserAlert]: rows: list[UserAlert] = [] for user_id in sorted({int(uid) for uid in user_ids if uid}): rows.append( create_alert( db, user_id=user_id, title=title, message=message, tenant_id=tenant_id, branch_id=branch_id, role_context=role_context, alert_type=alert_type, priority=priority, target_url=target_url, created_by_user_id=created_by_user_id, commit=False, ) ) db.commit() for row in rows: db.refresh(row) return rows def _user_alert_query(current_user: User) -> Select: return select(UserAlert).where(UserAlert.user_id == current_user.id) def list_my_alerts( db: Session, current_user: User, *, status: str = "all", priority: str = "all", limit: int = 100, ) -> list[UserAlert]: q = _user_alert_query(current_user) if status == "unread": q = q.where(UserAlert.is_read.is_(False)) elif status == "read": q = q.where(UserAlert.is_read.is_(True)) if priority in ALERT_PRIORITIES: q = q.where(UserAlert.priority == priority) q = q.order_by(UserAlert.is_read.asc(), UserAlert.created_at_utc.desc()).limit(max(1, min(limit, 500))) return list(db.execute(q).scalars().all()) def count_unread_alerts(db: Session, current_user: User | None) -> int: if not current_user: return 0 value = db.execute( select(func.count(UserAlert.id)).where(UserAlert.user_id == current_user.id, UserAlert.is_read.is_(False)) ).scalar_one() return int(value or 0) def get_my_alert_or_404(db: Session, current_user: User, alert_id: int) -> UserAlert | None: return db.execute( select(UserAlert).where(UserAlert.id == alert_id, UserAlert.user_id == current_user.id) ).scalar_one_or_none() def mark_alert_read(db: Session, current_user: User, alert_id: int) -> bool: alert = get_my_alert_or_404(db, current_user, alert_id) if not alert: return False if not alert.is_read: alert.is_read = True alert.read_at_utc = datetime.now(timezone.utc) db.commit() return True def mark_all_alerts_read(db: Session, current_user: User) -> int: result = db.execute( update(UserAlert) .where(UserAlert.user_id == current_user.id, UserAlert.is_read.is_(False)) .values(is_read=True, read_at_utc=datetime.now(timezone.utc)) ) db.commit() return int(result.rowcount or 0)