from __future__ import annotations import uuid from sqlalchemy import delete, func, select from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import selectinload from app.models.enums import NotificationType, SeverityLevel from app.models.notification_destination import ( NotificationDestination, NotificationDestinationCompany, ) def _with_companies(stmt): return stmt.options( selectinload(NotificationDestination.company_links).selectinload( NotificationDestinationCompany.company ) ) class NotificationDestinationRepository: def __init__(self, db: AsyncSession) -> None: self.db = db async def list_for_user(self, user_id: uuid.UUID) -> list[NotificationDestination]: result = await self.db.execute( _with_companies( select(NotificationDestination) .where(NotificationDestination.user_id == user_id) .order_by(NotificationDestination.created_at.desc()) ) ) return list(result.scalars().all()) async def list_for_company(self, company_id: uuid.UUID) -> list[NotificationDestination]: """Only destinations actually linked to this company - what alert dispatch notifies, as opposed to list_for_user's "everything this user owns" (used by the Settings page).""" result = await self.db.execute( select(NotificationDestination) .join( NotificationDestinationCompany, NotificationDestinationCompany.destination_id == NotificationDestination.id, ) .where(NotificationDestinationCompany.company_id == company_id) ) return list(result.scalars().all()) async def get_for_user( self, destination_id: uuid.UUID, user_id: uuid.UUID ) -> NotificationDestination | None: result = await self.db.execute( _with_companies( select(NotificationDestination).where( NotificationDestination.id == destination_id, NotificationDestination.user_id == user_id, ) ) ) return result.scalar_one_or_none() async def find_by_value( self, user_id: uuid.UUID, type: NotificationType, destination_value: str ) -> NotificationDestination | None: """Case-insensitive for email (RFC-technically case-sensitive local parts exist, but no real provider treats them that way and users retype casing inconsistently), exact for everything else.""" normalized = destination_value.strip() stmt = select(NotificationDestination).where( NotificationDestination.user_id == user_id, NotificationDestination.type == type ) if type == NotificationType.EMAIL: stmt = stmt.where( func.lower(NotificationDestination.destination_value) == normalized.lower() ) else: stmt = stmt.where(NotificationDestination.destination_value == normalized) result = await self.db.execute(_with_companies(stmt)) return result.scalar_one_or_none() async def create( self, *, user_id: uuid.UUID, type: NotificationType, destination_value: str, minimum_severity: SeverityLevel, enabled: bool, ) -> NotificationDestination: destination = NotificationDestination( user_id=user_id, type=type, destination_value=destination_value, minimum_severity=minimum_severity, enabled=enabled, ) self.db.add(destination) await self.db.flush() return destination async def link_company(self, destination_id: uuid.UUID, company_id: uuid.UUID) -> None: exists = await self.db.execute( select(func.count()) .select_from(NotificationDestinationCompany) .where( NotificationDestinationCompany.destination_id == destination_id, NotificationDestinationCompany.company_id == company_id, ) ) if int(exists.scalar_one()) > 0: return self.db.add( NotificationDestinationCompany(destination_id=destination_id, company_id=company_id) ) await self.db.flush() async def unlink_company(self, destination_id: uuid.UUID, company_id: uuid.UUID) -> None: await self.db.execute( delete(NotificationDestinationCompany).where( NotificationDestinationCompany.destination_id == destination_id, NotificationDestinationCompany.company_id == company_id, ) ) await self.db.flush() async def company_link_count(self, destination_id: uuid.UUID) -> int: result = await self.db.execute( select(func.count()) .select_from(NotificationDestinationCompany) .where(NotificationDestinationCompany.destination_id == destination_id) ) return int(result.scalar_one()) async def delete_orphaned_for_user(self, user_id: uuid.UUID) -> None: """Deletes any of this user's destinations that ended up linked to zero companies - called after a company delete, since that cascades the join rows for it but leaves the destination row itself behind even when it was the destination's only remaining link.""" destinations = await self.list_for_user(user_id) for destination in destinations: if await self.company_link_count(destination.id) == 0: await self.delete(destination) async def delete(self, destination: NotificationDestination) -> None: await self.db.delete(destination) await self.db.flush()