from __future__ import annotations import uuid from sqlalchemy.ext.asyncio import AsyncSession from app.core.errors import NotFoundError, ValidationAppError from app.models.notification_destination import NotificationDestination from app.repositories.company_repository import CompanyRepository from app.repositories.notification_destination_repository import ( NotificationDestinationRepository, ) from app.schemas.notification_destination import ( NotificationDestinationCreate, NotificationDestinationUpdate, ) async def list_destinations(db: AsyncSession, user_id: uuid.UUID) -> list[NotificationDestination]: repo = NotificationDestinationRepository(db) return await repo.list_for_user(user_id) async def get_destination( db: AsyncSession, user_id: uuid.UUID, destination_id: uuid.UUID ) -> NotificationDestination: repo = NotificationDestinationRepository(db) destination = await repo.get_for_user(destination_id, user_id) if destination is None: raise NotFoundError("Notification destination not found") return destination async def create_destination( db: AsyncSession, user_id: uuid.UUID, payload: NotificationDestinationCreate ) -> NotificationDestination: """Reuses an existing destination for the same (user, type, value) rather than creating a duplicate row - this is the fix for the wizard previously creating a fresh row per company even when the email/phone was already registered. Either way, the result ends up linked to every company in payload.company_ids.""" company_repo = CompanyRepository(db) for company_id in payload.company_ids: if await company_repo.get_for_user(company_id, user_id) is None: raise ValidationAppError(f"Company {company_id} not found") repo = NotificationDestinationRepository(db) destination = await repo.find_by_value(user_id, payload.type, payload.destination_value) if destination is None: destination = await repo.create( user_id=user_id, type=payload.type, destination_value=payload.destination_value, minimum_severity=payload.minimum_severity, enabled=payload.enabled, ) for company_id in payload.company_ids: await repo.link_company(destination.id, company_id) await db.commit() refreshed = await repo.get_for_user(destination.id, user_id) assert refreshed is not None return refreshed async def update_destination( db: AsyncSession, user_id: uuid.UUID, destination_id: uuid.UUID, payload: NotificationDestinationUpdate, ) -> NotificationDestination: destination = await get_destination(db, user_id, destination_id) updates = payload.model_dump(exclude_unset=True) for field, value in updates.items(): setattr(destination, field, value) if field == "destination_value": # Changing the destination value invalidates any prior verification. destination.verified = False await db.commit() # Re-fetch (rather than db.refresh) so company_links stays eager-loaded - # refresh() would expire it, and a bare lazy-load isn't safe under # SQLAlchemy's async ORM without an active await context. refreshed = await get_destination(db, user_id, destination_id) return refreshed async def delete_destination( db: AsyncSession, user_id: uuid.UUID, destination_id: uuid.UUID ) -> None: repo = NotificationDestinationRepository(db) destination = await get_destination(db, user_id, destination_id) await repo.delete(destination) await db.commit() async def unlink_company( db: AsyncSession, user_id: uuid.UUID, destination_id: uuid.UUID, company_id: uuid.UUID ) -> None: """Removes this one company's link, not the destination itself - a destination can be shared across companies (see create_destination's dedup-by-value). If this was its last remaining link, it's an orphan now and delete_orphaned_for_user removes it outright, same as already happens when a company itself is deleted.""" await get_destination(db, user_id, destination_id) # ownership check repo = NotificationDestinationRepository(db) await repo.unlink_company(destination_id, company_id) await repo.delete_orphaned_for_user(user_id) await db.commit()