- Kanäle SMTP (HTML-Mail mit Logos als CID-Anhang) und Apprise, beide blockierenden Bibliotheken laufen in einem Thread - Vier Anlässe: Fälligkeiten im Vorlauf, Kündigungsfristen in drei Stufen, überschrittene Budgets je Monat, Vertragsverlängerungen im Folgemonat - APScheduler im Anwendungsprozess, täglich 07:00 Europe/Berlin, räumt zugleich abgelaufene Sitzungen auf - Duplikatsschutz über den Zieltag statt den Versandtag; fehlgeschlagener Versand wird beim nächsten Lauf erneut versucht - Endpunkte für Regeln, Protokoll, Testversand und sofortigen Lauf - Einstellungsseite mit Einrichtungsstand, Regelpflege und Protokoll - 25 neue Backend-Tests (260 gesamt), 9 neue Frontend-Tests (74 gesamt) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014e7t8UpmoVNMtWivY5LiSH
114 lines
3.0 KiB
Python
114 lines
3.0 KiB
Python
"""Schemata für Benachrichtigungsregeln, Protokoll und Testversand."""
|
||
|
||
from datetime import date, datetime
|
||
|
||
from pydantic import Field
|
||
|
||
from app.models.enums import (
|
||
NotificationChannel,
|
||
NotificationStatus,
|
||
NotificationType,
|
||
)
|
||
from app.schemas.common import ApiModel, InputModel
|
||
|
||
TYPE_HINTS = {
|
||
"due_soon": "Fälligkeiten innerhalb der Vorlaufzeit.",
|
||
"notice_deadline": "Kündigungsfristen, die in 30, 14 oder 7 Tagen ablaufen.",
|
||
"budget_exceeded": "Überschrittene Budgets, einmal je Monat und Kategorie.",
|
||
"contract_renewal": "Verträge, die sich im kommenden Monat verlängern.",
|
||
}
|
||
|
||
|
||
class NotificationRuleCreate(InputModel):
|
||
type: NotificationType = Field(
|
||
description="; ".join(f"{k}: {v}" for k, v in TYPE_HINTS.items())
|
||
)
|
||
channel: NotificationChannel
|
||
lead_days: int = Field(
|
||
default=7, ge=0, le=365, description="Vorlauf in Tagen; nur für `due_soon` maßgeblich."
|
||
)
|
||
target: str | None = Field(
|
||
default=None,
|
||
description="Mailadresse bzw. Apprise-URL. Ohne Angabe gelten SMTP_FROM "
|
||
"beziehungsweise APPRISE_URLS.",
|
||
)
|
||
is_active: bool = True
|
||
|
||
|
||
class NotificationRuleUpdate(InputModel):
|
||
lead_days: int | None = Field(default=None, ge=0, le=365)
|
||
channel: NotificationChannel | None = None
|
||
target: str | None = None
|
||
is_active: bool | None = None
|
||
|
||
|
||
class NotificationRuleOut(ApiModel):
|
||
id: int
|
||
type: NotificationType
|
||
channel: NotificationChannel
|
||
lead_days: int
|
||
target: str | None
|
||
is_active: bool
|
||
created_at: datetime
|
||
|
||
|
||
class NotificationLogOut(ApiModel):
|
||
"""Ein Eintrag des Versandprotokolls."""
|
||
|
||
id: int
|
||
rule_id: int
|
||
ref_type: str
|
||
ref_id: str
|
||
dedupe_day: date = Field(
|
||
description="Zieltag des Ereignisses – Grundlage des Duplikatsschutzes."
|
||
)
|
||
sent_at: datetime
|
||
status: NotificationStatus
|
||
error: str | None
|
||
|
||
|
||
class ChannelStatusOut(ApiModel):
|
||
"""Einrichtungsstand eines Kanals."""
|
||
|
||
channel: NotificationChannel
|
||
configured: bool
|
||
detail: str
|
||
|
||
|
||
class NotificationSettingsOut(ApiModel):
|
||
"""Überblick über Kanäle und Zeitplan."""
|
||
|
||
enabled: bool
|
||
scheduler_enabled: bool
|
||
run_at: str = Field(description="Uhrzeit des täglichen Laufs, etwa '07:00'.")
|
||
timezone: str
|
||
next_run_at: datetime | None
|
||
channels: list[ChannelStatusOut]
|
||
|
||
|
||
class TestSendRequest(InputModel):
|
||
target: str | None = Field(
|
||
default=None, description="Abweichendes Ziel; ohne Angabe gelten die Einstellungen."
|
||
)
|
||
|
||
|
||
class TestResultOut(ApiModel):
|
||
channel: NotificationChannel
|
||
configured: bool
|
||
sent: bool
|
||
error: str | None = None
|
||
|
||
|
||
class TestSendResponse(ApiModel):
|
||
results: list[TestResultOut]
|
||
any_sent: bool = Field(description="True, wenn mindestens ein Kanal zugestellt hat.")
|
||
|
||
|
||
class RunResponse(ApiModel):
|
||
"""Ergebnis eines manuell ausgelösten Laufs."""
|
||
|
||
checked: int
|
||
sent: int
|
||
skipped: int = Field(description="Bereits gemeldete Ereignisse.")
|
||
failed: int
|