"""Notification service — create and manage user notifications.""" from __future__ import annotations import uuid from datetime import UTC from typing import Any from sqlalchemy import func, select, update from sqlalchemy.ext.asyncio import AsyncSession from app.models.notification import Notification async def create_notification( db: AsyncSession, tenant_id: uuid.UUID, user_id: uuid.UUID, type: str, title: str, body: str | None = None, ) -> Notification: """Create a new notification for a user.""" notif = Notification( tenant_id=tenant_id, user_id=user_id, type=type, title=title, body=body, ) db.add(notif) await db.flush() return notif async def list_notifications( db: AsyncSession, tenant_id: uuid.UUID, user_id: uuid.UUID, page: int = 1, page_size: int = 25, ) -> dict[str, Any]: """List notifications for a user, unread first, then by created_at desc.""" offset = (page - 1) * page_size # Count total count_q = ( select(func.count()) .select_from(Notification) .where( Notification.tenant_id == tenant_id, Notification.user_id == user_id, ) ) total = (await db.execute(count_q)).scalar() or 0 # Query — unread first (read_at IS NULL), then newest q = ( select(Notification) .where( Notification.tenant_id == tenant_id, Notification.user_id == user_id, ) .order_by( Notification.read_at.isnot(None), # False (unread) sorts first Notification.created_at.desc(), ) .offset(offset) .limit(page_size) ) result = await db.execute(q) items = result.scalars().all() return { "items": [_notification_to_dict(n) for n in items], "total": total, "page": page, "page_size": page_size, } async def mark_notification_read( db: AsyncSession, tenant_id: uuid.UUID, user_id: uuid.UUID, notification_id: uuid.UUID, ) -> Notification | None: """Mark a notification as read.""" from datetime import datetime q = ( update(Notification) .where( Notification.id == notification_id, Notification.tenant_id == tenant_id, Notification.user_id == user_id, ) .values(read_at=datetime.now(UTC)) .returning(Notification) ) result = await db.execute(q) row = result.scalar_one_or_none() return row async def get_unread_count( db: AsyncSession, tenant_id: uuid.UUID, user_id: uuid.UUID, ) -> int: """Get unread notification count for a user.""" q = ( select(func.count()) .select_from(Notification) .where( Notification.tenant_id == tenant_id, Notification.user_id == user_id, Notification.read_at.is_(None), ) ) result = await db.execute(q) return result.scalar() or 0 def _notification_to_dict(n: Notification) -> dict[str, Any]: """Convert a notification to a dict.""" return { "id": str(n.id), "type": n.type, "title": n.title, "body": n.body, "read_at": n.read_at.isoformat() if n.read_at else None, "created_at": n.created_at.isoformat() if n.created_at else None, }