import asyncio import logging from sqlalchemy import select, func, update from sqlalchemy.ext.asyncio import AsyncSession from app.db.models import ClassMembership, Notification, User from app.services.email_service import send_class_notification_email logger = logging.getLogger(__name__) async def create_notification( db: AsyncSession, user_id: int, type: str, title: str, content: str | None = None, related_id: int | None = None, ) -> Notification: notification = Notification( user_id=user_id, type=type, title=title, content=content, related_id=related_id, ) db.add(notification) await db.commit() await db.refresh(notification) return notification async def create_notifications_for_class( db: AsyncSession, class_id: int, type: str, title: str, content: str | None = None, related_id: int | None = None, exclude_user_id: int | None = None, email_subject: str | None = None, email_body: str | None = None, email_action_path: str | None = None, ): """Create in-app notifications + send email for all active users in a class.""" result = await db.execute( select(User.id, User.email) .join(ClassMembership, ClassMembership.user_id == User.id) .where( ClassMembership.class_id == class_id, User.status == "approved", ) ) rows = result.all() emails: list[str] = [] for uid, email in rows: notification = Notification( user_id=uid, type=type, title=title, content=content, related_id=related_id, ) db.add(notification) emails.append(email) await db.commit() # Send email notification in background (fire-and-forget) if email_subject and emails: from app.config import settings action_url = f"{settings.frontend_url}{email_action_path}" if email_action_path else None asyncio.create_task(_safe_send_emails(emails, email_subject, title, email_body or content or "", action_url)) async def _safe_send_emails( emails: list[str], subject: str, title: str, body: str, action_url: str | None ): """Fire-and-forget email sending with error logging.""" try: await send_class_notification_email(emails, subject, title, body, action_url) except Exception as e: logger.error(f"Failed to send class notification emails: {e}") async def list_notifications( db: AsyncSession, user_id: int, page: int = 1, page_size: int = 20 ) -> tuple[list[Notification], int]: total_result = await db.execute( select(func.count(Notification.id)).where(Notification.user_id == user_id) ) total = total_result.scalar() or 0 result = await db.execute( select(Notification) .where(Notification.user_id == user_id) .order_by(Notification.created_at.desc()) .offset((page - 1) * page_size) .limit(page_size) ) notifications = list(result.scalars().all()) return notifications, total async def get_unread_count(db: AsyncSession, user_id: int) -> int: result = await db.execute( select(func.count(Notification.id)).where( Notification.user_id == user_id, Notification.is_read == False, ) ) return result.scalar() or 0 async def mark_as_read(db: AsyncSession, notification_id: int, user_id: int) -> bool: result = await db.execute( update(Notification) .where(Notification.id == notification_id, Notification.user_id == user_id) .values(is_read=True) ) await db.commit() return result.rowcount > 0 async def mark_all_as_read(db: AsyncSession, user_id: int) -> int: result = await db.execute( update(Notification) .where(Notification.user_id == user_id, Notification.is_read == False) .values(is_read=True) ) await db.commit() return result.rowcount