- Repo-Struktur für Backend, Frontend, Dokumentation und Gitea-Workflows - FastAPI-Skeleton mit pydantic-settings und RFC-7807-artigem Fehlerformat - Vollständiges Datenmodell (15 Tabellen) als SQLAlchemy-2.0-Modelle - Alembic gegen die Async-Engine inklusive Erstmigration - Idempotenter Seed für den deutschen Kategoriebaum und Standardregeln - Endpunkte /api/health und /api/version - pytest-Infrastruktur mit transaktionsisolierten Fixtures Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014e7t8UpmoVNMtWivY5LiSH
74 lines
2.4 KiB
Python
74 lines
2.4 KiB
Python
"""Benachrichtigungsregeln und Versandprotokoll."""
|
|
|
|
from datetime import date, datetime
|
|
|
|
from sqlalchemy import (
|
|
Boolean,
|
|
Date,
|
|
DateTime,
|
|
ForeignKey,
|
|
Index,
|
|
Integer,
|
|
String,
|
|
Text,
|
|
UniqueConstraint,
|
|
)
|
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
|
|
from app.db.base import Base, CreatedAtMixin
|
|
from app.models.enums import (
|
|
NotificationChannel,
|
|
NotificationStatus,
|
|
NotificationType,
|
|
pg_enum,
|
|
)
|
|
|
|
|
|
class NotificationRule(Base, CreatedAtMixin):
|
|
"""Eine Regel pro Ereignistyp und Kanal."""
|
|
|
|
__tablename__ = "notification_rule"
|
|
|
|
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
|
type: Mapped[NotificationType] = mapped_column(
|
|
pg_enum(NotificationType, "notification_type"), nullable=False
|
|
)
|
|
lead_days: Mapped[int] = mapped_column(Integer, nullable=False, default=7)
|
|
channel: Mapped[NotificationChannel] = mapped_column(
|
|
pg_enum(NotificationChannel, "notification_channel"), nullable=False
|
|
)
|
|
target: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
|
|
|
|
logs: Mapped[list["NotificationLog"]] = relationship(
|
|
back_populates="rule", cascade="all, delete-orphan"
|
|
)
|
|
|
|
|
|
class NotificationLog(Base):
|
|
"""Versandprotokoll. `dedupe_day` verhindert Doppelversand am selben Tag."""
|
|
|
|
__tablename__ = "notification_log"
|
|
|
|
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
|
rule_id: Mapped[int] = mapped_column(
|
|
ForeignKey("notification_rule.id", ondelete="CASCADE"), nullable=False
|
|
)
|
|
ref_type: Mapped[str] = mapped_column(String(40), nullable=False)
|
|
ref_id: Mapped[str] = mapped_column(String(80), nullable=False)
|
|
dedupe_day: Mapped[date] = mapped_column(Date, nullable=False)
|
|
sent_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
|
|
status: Mapped[NotificationStatus] = mapped_column(
|
|
pg_enum(NotificationStatus, "notification_status"), nullable=False
|
|
)
|
|
error: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
|
|
rule: Mapped[NotificationRule] = relationship(back_populates="logs")
|
|
|
|
__table_args__ = (
|
|
UniqueConstraint(
|
|
"rule_id", "ref_type", "ref_id", "dedupe_day", name="uq_notification_log_rule_id"
|
|
),
|
|
Index("ix_notification_log_sent_at", "sent_at"),
|
|
)
|