"""Benachrichtigungsregeln, Versandprotokoll und Testversand.""" from datetime import date from fastapi import APIRouter, Query, status from sqlalchemy import select from sqlalchemy.exc import IntegrityError from app.api.deps import DbSession from app.core.config import settings from app.core.errors import ConflictError from app.models import NotificationLog, NotificationRule from app.models.enums import NotificationChannel, NotificationStatus from app.scheduler import next_run_time from app.schemas.common import ErrorResponse, MessageResponse from app.schemas.notification import ( ChannelStatusOut, NotificationLogOut, NotificationRuleCreate, NotificationRuleOut, NotificationRuleUpdate, NotificationSettingsOut, RunResponse, TestResultOut, TestSendRequest, TestSendResponse, ) from app.services.channels import get_channel from app.services.crud import apply_updates, get_or_404 from app.services.notifications import run_all, send_test router = APIRouter(prefix="/notifications", tags=["notifications"]) NOT_FOUND = {status.HTTP_404_NOT_FOUND: {"model": ErrorResponse}} CHANNEL_HINTS = { NotificationChannel.SMTP: "SMTP_HOST und SMTP_FROM setzen.", NotificationChannel.APPRISE: "APPRISE_URLS setzen, komma-separiert.", } @router.get( "/settings", response_model=NotificationSettingsOut, summary="Einrichtungsstand", description="Zeigt, welche Kanäle einsatzbereit sind und wann der nächste Lauf ansteht.", ) async def read_settings() -> NotificationSettingsOut: kanaele = [] for art, hinweis in CHANNEL_HINTS.items(): eingerichtet = get_channel(art).is_configured() kanaele.append( ChannelStatusOut( channel=art, configured=eingerichtet, detail="Einsatzbereit." if eingerichtet else hinweis, ) ) return NotificationSettingsOut( enabled=settings.notifications_enabled, scheduler_enabled=settings.scheduler_enabled, run_at=f"{settings.notification_hour:02d}:{settings.notification_minute:02d}", timezone=settings.timezone, next_run_at=next_run_time(), channels=kanaele, ) @router.get("/rules", response_model=list[NotificationRuleOut], summary="Regeln auflisten") async def list_rules(session: DbSession) -> list[NotificationRule]: stmt = select(NotificationRule).order_by(NotificationRule.type, NotificationRule.channel) return list((await session.execute(stmt)).scalars().all()) @router.post( "/rules", response_model=NotificationRuleOut, status_code=status.HTTP_201_CREATED, summary="Regel anlegen", ) async def create_rule(payload: NotificationRuleCreate, session: DbSession) -> NotificationRule: regel = NotificationRule(**payload.model_dump()) session.add(regel) try: await session.commit() except IntegrityError as exc: await session.rollback() raise ConflictError("Diese Regel existiert bereits.") from exc await session.refresh(regel) return regel @router.patch( "/rules/{rule_id}", response_model=NotificationRuleOut, responses=NOT_FOUND, summary="Regel ändern", ) async def update_rule( rule_id: int, payload: NotificationRuleUpdate, session: DbSession ) -> NotificationRule: regel = await get_or_404(session, NotificationRule, rule_id) apply_updates(regel, payload) await session.commit() await session.refresh(regel) return regel @router.delete( "/rules/{rule_id}", response_model=MessageResponse, responses=NOT_FOUND, summary="Regel löschen", description="Entfernt die Regel samt ihrem Versandprotokoll.", ) async def delete_rule(rule_id: int, session: DbSession) -> MessageResponse: regel = await get_or_404(session, NotificationRule, rule_id) await session.delete(regel) await session.commit() return MessageResponse(detail="Regel gelöscht.") @router.get( "/log", response_model=list[NotificationLogOut], summary="Versandprotokoll", description="Neueste Einträge zuerst.", ) async def read_log( session: DbSession, rule_id: int | None = Query(default=None), status_filter: NotificationStatus | None = Query(default=None, alias="status"), limit: int = Query(default=100, ge=1, le=500), ) -> list[NotificationLog]: stmt = select(NotificationLog).order_by(NotificationLog.sent_at.desc()).limit(limit) if rule_id is not None: stmt = stmt.where(NotificationLog.rule_id == rule_id) if status_filter is not None: stmt = stmt.where(NotificationLog.status == status_filter) return list((await session.execute(stmt)).scalars().all()) @router.post( "/test", response_model=TestSendResponse, summary="Testnachricht senden", description="Verschickt eine Testnachricht über beide Kanäle. Nicht eingerichtete " "Kanäle werden gemeldet, gelten aber nicht als Fehler.", ) async def send_test_notification(payload: TestSendRequest | None = None) -> TestSendResponse: ergebnisse = await send_test(payload.target if payload else None) return TestSendResponse( results=[TestResultOut.model_validate(eintrag) for eintrag in ergebnisse], any_sent=any(eintrag.sent for eintrag in ergebnisse), ) @router.post( "/run", response_model=RunResponse, summary="Lauf sofort ausführen", description="Führt alle aktiven Regeln aus, ohne auf den Zeitplan zu warten. " "Bereits gemeldete Ereignisse werden dabei übersprungen.", ) async def run_now( session: DbSession, as_of: date | None = Query(default=None, description="Stichtag; Vorgabe ist heute."), ) -> RunResponse: ergebnis = await run_all(session, as_of) return RunResponse( checked=ergebnis.checked, sent=ergebnis.sent, skipped=ergebnis.skipped, failed=ergebnis.failed, )