diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py index e69de29..2bdb7c5 100644 --- a/backend/app/models/__init__.py +++ b/backend/app/models/__init__.py @@ -0,0 +1,24 @@ +"""SQLAlchemy models for the DAK Zweitmeinungs-Portal.""" + +from app.models.user import AllowedDomain, InvitationLink, RefreshToken, User +from app.models.case import Case, CaseICDCode +from app.models.report import WeeklyReport, YearlySummary +from app.models.audit import AuditLog, ImportLog, Notification + +__all__ = [ + # User & Auth + "User", + "RefreshToken", + "InvitationLink", + "AllowedDomain", + # Cases + "Case", + "CaseICDCode", + # Reports + "WeeklyReport", + "YearlySummary", + # Logging & Notifications + "ImportLog", + "AuditLog", + "Notification", +] diff --git a/backend/app/models/audit.py b/backend/app/models/audit.py new file mode 100644 index 0000000..9e8b460 --- /dev/null +++ b/backend/app/models/audit.py @@ -0,0 +1,130 @@ +"""Import log, audit log, and notification models.""" + +from __future__ import annotations + +import datetime as dt +from typing import Optional + +from sqlalchemy import ( + Boolean, + CheckConstraint, + DateTime, + ForeignKey, + Index, + Integer, + String, + Text, + func, +) +from sqlalchemy.dialects.mysql import JSON +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from app.database import Base + + +class ImportLog(Base): + __tablename__ = "import_log" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + filename: Mapped[str] = mapped_column(String(255), nullable=False) + import_type: Mapped[str] = mapped_column(String(50), nullable=False) + cases_imported: Mapped[int] = mapped_column( + Integer, nullable=False, server_default="0" + ) + cases_skipped: Mapped[int] = mapped_column( + Integer, nullable=False, server_default="0" + ) + cases_updated: Mapped[int] = mapped_column( + Integer, nullable=False, server_default="0" + ) + errors: Mapped[Optional[str]] = mapped_column(Text, nullable=True) + details: Mapped[Optional[dict]] = mapped_column(JSON, nullable=True) + imported_by: Mapped[Optional[int]] = mapped_column( + Integer, ForeignKey("users.id"), nullable=True + ) + imported_at: Mapped[dt.datetime] = mapped_column( + DateTime, nullable=False, server_default=func.now() + ) + + # Relationships + imported_by_user: Mapped[Optional["User"]] = relationship( + foreign_keys=[imported_by] + ) + + __table_args__ = ( + CheckConstraint( + "import_type IN ('csv_crm','icd_xlsx','historical_excel','excel_sync')", + name="chk_imp_type", + ), + ) + + +class AuditLog(Base): + __tablename__ = "audit_log" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + user_id: Mapped[Optional[int]] = mapped_column( + Integer, ForeignKey("users.id"), nullable=True + ) + action: Mapped[str] = mapped_column(String(100), nullable=False) + entity_type: Mapped[Optional[str]] = mapped_column(String(50), nullable=True) + entity_id: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) + old_values: Mapped[Optional[dict]] = mapped_column(JSON, nullable=True) + new_values: Mapped[Optional[dict]] = mapped_column(JSON, nullable=True) + ip_address: Mapped[Optional[str]] = mapped_column(String(45), nullable=True) + user_agent: Mapped[Optional[str]] = mapped_column(Text, nullable=True) + created_at: Mapped[dt.datetime] = mapped_column( + DateTime, nullable=False, server_default=func.now() + ) + + # Relationships + user: Mapped[Optional["User"]] = relationship(foreign_keys=[user_id]) + + __table_args__ = ( + Index("idx_user", "user_id"), + Index("idx_entity", "entity_type", "entity_id"), + Index("idx_created", "created_at"), + ) + + +class Notification(Base): + __tablename__ = "notifications" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + recipient_id: Mapped[int] = mapped_column( + Integer, ForeignKey("users.id"), nullable=False + ) + notification_type: Mapped[str] = mapped_column(String(50), nullable=False) + title: Mapped[str] = mapped_column(String(255), nullable=False) + message: Mapped[Optional[str]] = mapped_column(Text, nullable=True) + related_entity_type: Mapped[Optional[str]] = mapped_column( + String(50), nullable=True + ) + related_entity_id: Mapped[Optional[int]] = mapped_column( + Integer, nullable=True + ) + is_read: Mapped[bool] = mapped_column( + Boolean, nullable=False, server_default="0" + ) + email_sent: Mapped[bool] = mapped_column( + Boolean, nullable=False, server_default="0" + ) + email_sent_at: Mapped[Optional[dt.datetime]] = mapped_column( + DateTime, nullable=True + ) + created_at: Mapped[dt.datetime] = mapped_column( + DateTime, nullable=False, server_default=func.now() + ) + + # Relationships + recipient: Mapped["User"] = relationship(foreign_keys=[recipient_id]) + + __table_args__ = ( + Index("idx_recipient", "recipient_id", "is_read"), + CheckConstraint( + "notification_type IN (" + "'new_cases_uploaded','icd_entered','icd_uploaded'," + "'report_ready','coding_completed')", + name="chk_notif", + ), + ) diff --git a/backend/app/models/case.py b/backend/app/models/case.py new file mode 100644 index 0000000..c007603 --- /dev/null +++ b/backend/app/models/case.py @@ -0,0 +1,198 @@ +"""Case and ICD code models.""" + +from __future__ import annotations + +import datetime as dt +from typing import Optional + +from sqlalchemy import ( + Boolean, + CheckConstraint, + Date, + DateTime, + ForeignKey, + Index, + Integer, + SmallInteger, + String, + Text, + UniqueConstraint, + func, + text, +) +from sqlalchemy.orm import Mapped, mapped_column, relationship +from sqlalchemy.schema import FetchedValue + +from app.database import Base + + +class Case(Base): + __tablename__ = "cases" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + fall_id: Mapped[Optional[str]] = mapped_column(String(100), nullable=True) + crm_ticket_id: Mapped[Optional[str]] = mapped_column(String(20), nullable=True) + jahr: Mapped[int] = mapped_column(SmallInteger, nullable=False) + kw: Mapped[int] = mapped_column(SmallInteger, nullable=False) + datum: Mapped[dt.date] = mapped_column(Date, nullable=False) + anrede: Mapped[Optional[str]] = mapped_column(String(20), nullable=True) + vorname: Mapped[Optional[str]] = mapped_column(String(100), nullable=True) + nachname: Mapped[str] = mapped_column(String(100), nullable=False) + geburtsdatum: Mapped[Optional[dt.date]] = mapped_column(Date, nullable=True) + kvnr: Mapped[Optional[str]] = mapped_column(String(20), nullable=True) + versicherung: Mapped[str] = mapped_column( + String(50), nullable=False, server_default="DAK" + ) + icd: Mapped[Optional[str]] = mapped_column(Text, nullable=True) + fallgruppe: Mapped[str] = mapped_column(String(20), nullable=False) + strasse: Mapped[Optional[str]] = mapped_column(String(255), nullable=True) + plz: Mapped[Optional[str]] = mapped_column(String(10), nullable=True) + ort: Mapped[Optional[str]] = mapped_column(String(100), nullable=True) + email: Mapped[Optional[str]] = mapped_column(String(255), nullable=True) + ansprechpartner: Mapped[Optional[str]] = mapped_column(String(200), nullable=True) + telefonnummer: Mapped[Optional[str]] = mapped_column(String(50), nullable=True) + mobiltelefon: Mapped[Optional[str]] = mapped_column(String(50), nullable=True) + email2: Mapped[Optional[str]] = mapped_column(String(255), nullable=True) + telefon2: Mapped[Optional[str]] = mapped_column(String(50), nullable=True) + unterlagen: Mapped[bool] = mapped_column( + Boolean, nullable=False, server_default="0" + ) + unterlagen_verschickt: Mapped[Optional[dt.date]] = mapped_column( + Date, nullable=True + ) + erhalten: Mapped[Optional[bool]] = mapped_column(Boolean, nullable=True) + unterlagen_erhalten: Mapped[Optional[dt.date]] = mapped_column( + Date, nullable=True + ) + unterlagen_an_gutachter: Mapped[Optional[dt.date]] = mapped_column( + Date, nullable=True + ) + gutachten: Mapped[bool] = mapped_column( + Boolean, nullable=False, server_default="0" + ) + gutachter: Mapped[Optional[str]] = mapped_column(String(100), nullable=True) + gutachten_erstellt: Mapped[Optional[dt.date]] = mapped_column( + Date, nullable=True + ) + gutachten_versendet: Mapped[Optional[dt.date]] = mapped_column( + Date, nullable=True + ) + schweigepflicht: Mapped[bool] = mapped_column( + Boolean, nullable=False, server_default="0" + ) + ablehnung: Mapped[bool] = mapped_column( + Boolean, nullable=False, server_default="0" + ) + abbruch: Mapped[bool] = mapped_column( + Boolean, nullable=False, server_default="0" + ) + abbruch_datum: Mapped[Optional[dt.date]] = mapped_column(Date, nullable=True) + gutachten_typ: Mapped[Optional[str]] = mapped_column(String(20), nullable=True) + therapieaenderung: Mapped[Optional[str]] = mapped_column(String(5), nullable=True) + ta_diagnosekorrektur: Mapped[bool] = mapped_column( + Boolean, nullable=False, server_default="0" + ) + ta_unterversorgung: Mapped[bool] = mapped_column( + Boolean, nullable=False, server_default="0" + ) + ta_uebertherapie: Mapped[bool] = mapped_column( + Boolean, nullable=False, server_default="0" + ) + kurzbeschreibung: Mapped[Optional[str]] = mapped_column(Text, nullable=True) + fragestellung: Mapped[Optional[str]] = mapped_column(Text, nullable=True) + kommentar: Mapped[Optional[str]] = mapped_column(Text, nullable=True) + sonstiges: Mapped[Optional[str]] = mapped_column(Text, nullable=True) + abgerechnet: Mapped[bool] = mapped_column( + Boolean, nullable=False, server_default="0" + ) + abrechnung_datum: Mapped[Optional[dt.date]] = mapped_column(Date, nullable=True) + import_source: Mapped[Optional[str]] = mapped_column(String(255), nullable=True) + imported_at: Mapped[dt.datetime] = mapped_column( + DateTime, nullable=False, server_default=func.now() + ) + updated_at: Mapped[dt.datetime] = mapped_column( + DateTime, + nullable=False, + server_default=func.now(), + onupdate=func.now(), + server_onupdate=FetchedValue(), + ) + updated_by: Mapped[Optional[int]] = mapped_column( + Integer, ForeignKey("users.id"), nullable=True + ) + icd_entered_by: Mapped[Optional[int]] = mapped_column( + Integer, ForeignKey("users.id"), nullable=True + ) + icd_entered_at: Mapped[Optional[dt.datetime]] = mapped_column( + DateTime, nullable=True + ) + coding_completed_by: Mapped[Optional[int]] = mapped_column( + Integer, ForeignKey("users.id"), nullable=True + ) + coding_completed_at: Mapped[Optional[dt.datetime]] = mapped_column( + DateTime, nullable=True + ) + + # Relationships + icd_codes: Mapped[list[CaseICDCode]] = relationship( + back_populates="case", cascade="all, delete-orphan" + ) + updated_by_user: Mapped[Optional["User"]] = relationship( + foreign_keys=[updated_by] + ) + icd_entered_by_user: Mapped[Optional["User"]] = relationship( + foreign_keys=[icd_entered_by] + ) + coding_completed_by_user: Mapped[Optional["User"]] = relationship( + foreign_keys=[coding_completed_by] + ) + + __table_args__ = ( + UniqueConstraint("fall_id", name="uk_fall_id"), + Index("idx_jahr_kw", "jahr", "kw"), + Index("idx_kvnr", "kvnr"), + Index("idx_fallgruppe", "fallgruppe"), + Index("idx_datum", "datum"), + Index("idx_nachname_vorname", "nachname", "vorname"), + # Note: idx_pending_icd uses a prefix length icd(20) which SQLAlchemy + # does not support natively. We use text() for the prefix column. + Index("idx_pending_icd", "jahr", "kw", "fallgruppe", text("icd(20)")), + Index("idx_pending_coding", "gutachten", "gutachten_typ"), + CheckConstraint( + "fallgruppe IN ('onko','kardio','intensiv','galle','sd')", + name="chk_fallgruppe", + ), + CheckConstraint( + "gutachten_typ IS NULL OR gutachten_typ IN ('Bestätigung','Alternative')", + name="chk_gutachten_typ", + ), + CheckConstraint( + "therapieaenderung IS NULL OR therapieaenderung IN ('Ja','Nein')", + name="chk_ta", + ), + ) + + +class CaseICDCode(Base): + __tablename__ = "case_icd_codes" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + case_id: Mapped[int] = mapped_column( + Integer, + ForeignKey("cases.id", ondelete="CASCADE"), + nullable=False, + ) + icd_code: Mapped[str] = mapped_column(String(20), nullable=False) + icd_hauptgruppe: Mapped[Optional[str]] = mapped_column(String(10), nullable=True) + created_at: Mapped[dt.datetime] = mapped_column( + DateTime, nullable=False, server_default=func.now() + ) + + # Relationships + case: Mapped[Case] = relationship(back_populates="icd_codes") + + __table_args__ = ( + Index("idx_case", "case_id"), + Index("idx_code", "icd_code"), + Index("idx_haupt", "icd_hauptgruppe"), + ) diff --git a/backend/app/models/report.py b/backend/app/models/report.py new file mode 100644 index 0000000..1912291 --- /dev/null +++ b/backend/app/models/report.py @@ -0,0 +1,189 @@ +"""Weekly report and yearly summary models.""" + +from __future__ import annotations + +import datetime as dt +from typing import Optional + +from sqlalchemy import ( + Date, + DateTime, + ForeignKey, + Index, + Integer, + SmallInteger, + String, + UniqueConstraint, + func, +) +from sqlalchemy.dialects.mysql import JSON +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from app.database import Base + + +class WeeklyReport(Base): + __tablename__ = "weekly_reports" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + jahr: Mapped[int] = mapped_column(SmallInteger, nullable=False) + kw: Mapped[int] = mapped_column(SmallInteger, nullable=False) + report_date: Mapped[dt.date] = mapped_column(Date, nullable=False) + report_file_path: Mapped[Optional[str]] = mapped_column( + String(500), nullable=True + ) + report_data: Mapped[Optional[dict]] = mapped_column(JSON, nullable=True) + generated_by: Mapped[Optional[int]] = mapped_column( + Integer, ForeignKey("users.id"), nullable=True + ) + generated_at: Mapped[dt.datetime] = mapped_column( + DateTime, nullable=False, server_default=func.now() + ) + + # Relationships + generated_by_user: Mapped[Optional["User"]] = relationship( + foreign_keys=[generated_by] + ) + + __table_args__ = ( + UniqueConstraint("jahr", "kw", name="uk_jahr_kw"), + ) + + +class YearlySummary(Base): + __tablename__ = "yearly_summary" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + jahr: Mapped[int] = mapped_column(SmallInteger, nullable=False) + kw: Mapped[int] = mapped_column(SmallInteger, nullable=False) + + # Overall counts + erstberatungen: Mapped[Optional[int]] = mapped_column( + Integer, server_default="0" + ) + ablehnungen: Mapped[Optional[int]] = mapped_column( + Integer, server_default="0" + ) + unterlagen: Mapped[Optional[int]] = mapped_column( + Integer, server_default="0" + ) + keine_rueckmeldung: Mapped[Optional[int]] = mapped_column( + Integer, server_default="0" + ) + gutachten_gesamt: Mapped[Optional[int]] = mapped_column( + Integer, server_default="0" + ) + gutachten_alternative: Mapped[Optional[int]] = mapped_column( + Integer, server_default="0" + ) + gutachten_bestaetigung: Mapped[Optional[int]] = mapped_column( + Integer, server_default="0" + ) + + # Per-Fallgruppe counts: onko + onko_anzahl: Mapped[Optional[int]] = mapped_column( + Integer, server_default="0" + ) + onko_gutachten: Mapped[Optional[int]] = mapped_column( + Integer, server_default="0" + ) + onko_keine_rm: Mapped[Optional[int]] = mapped_column( + Integer, server_default="0" + ) + + # Per-Fallgruppe counts: kardio + kardio_anzahl: Mapped[Optional[int]] = mapped_column( + Integer, server_default="0" + ) + kardio_gutachten: Mapped[Optional[int]] = mapped_column( + Integer, server_default="0" + ) + kardio_keine_rm: Mapped[Optional[int]] = mapped_column( + Integer, server_default="0" + ) + + # Per-Fallgruppe counts: intensiv + intensiv_anzahl: Mapped[Optional[int]] = mapped_column( + Integer, server_default="0" + ) + intensiv_gutachten: Mapped[Optional[int]] = mapped_column( + Integer, server_default="0" + ) + intensiv_keine_rm: Mapped[Optional[int]] = mapped_column( + Integer, server_default="0" + ) + + # Per-Fallgruppe counts: galle + galle_anzahl: Mapped[Optional[int]] = mapped_column( + Integer, server_default="0" + ) + galle_gutachten: Mapped[Optional[int]] = mapped_column( + Integer, server_default="0" + ) + galle_keine_rm: Mapped[Optional[int]] = mapped_column( + Integer, server_default="0" + ) + + # Per-Fallgruppe counts: sd + sd_anzahl: Mapped[Optional[int]] = mapped_column( + Integer, server_default="0" + ) + sd_gutachten: Mapped[Optional[int]] = mapped_column( + Integer, server_default="0" + ) + sd_keine_rm: Mapped[Optional[int]] = mapped_column( + Integer, server_default="0" + ) + + # Gutachten-Typ per Fallgruppe + onko_alternative: Mapped[Optional[int]] = mapped_column( + Integer, server_default="0" + ) + onko_bestaetigung: Mapped[Optional[int]] = mapped_column( + Integer, server_default="0" + ) + kardio_alternative: Mapped[Optional[int]] = mapped_column( + Integer, server_default="0" + ) + kardio_bestaetigung: Mapped[Optional[int]] = mapped_column( + Integer, server_default="0" + ) + intensiv_alternative: Mapped[Optional[int]] = mapped_column( + Integer, server_default="0" + ) + intensiv_bestaetigung: Mapped[Optional[int]] = mapped_column( + Integer, server_default="0" + ) + galle_alternative: Mapped[Optional[int]] = mapped_column( + Integer, server_default="0" + ) + galle_bestaetigung: Mapped[Optional[int]] = mapped_column( + Integer, server_default="0" + ) + sd_alternative: Mapped[Optional[int]] = mapped_column( + Integer, server_default="0" + ) + sd_bestaetigung: Mapped[Optional[int]] = mapped_column( + Integer, server_default="0" + ) + + # Therapieaenderung counts + ta_ja: Mapped[Optional[int]] = mapped_column( + Integer, server_default="0" + ) + ta_nein: Mapped[Optional[int]] = mapped_column( + Integer, server_default="0" + ) + ta_diagnosekorrektur: Mapped[Optional[int]] = mapped_column( + Integer, server_default="0" + ) + ta_unterversorgung: Mapped[Optional[int]] = mapped_column( + Integer, server_default="0" + ) + ta_uebertherapie: Mapped[Optional[int]] = mapped_column( + Integer, server_default="0" + ) + + __table_args__ = ( + UniqueConstraint("jahr", "kw", name="uk_jahr_kw"), + ) diff --git a/backend/app/models/user.py b/backend/app/models/user.py new file mode 100644 index 0000000..7f3adfa --- /dev/null +++ b/backend/app/models/user.py @@ -0,0 +1,162 @@ +"""User, authentication, and access control models.""" + +from __future__ import annotations + +from datetime import datetime +from typing import Optional + +from sqlalchemy import ( + Boolean, + CheckConstraint, + DateTime, + ForeignKey, + Index, + Integer, + String, + UniqueConstraint, + func, +) +from sqlalchemy.orm import Mapped, mapped_column, relationship +from sqlalchemy.schema import FetchedValue + +from app.database import Base + + +class User(Base): + __tablename__ = "users" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + username: Mapped[str] = mapped_column(String(100), nullable=False) + email: Mapped[str] = mapped_column(String(255), nullable=False) + password_hash: Mapped[str] = mapped_column(String(255), nullable=False) + role: Mapped[str] = mapped_column( + String(20), nullable=False, server_default="dak_mitarbeiter" + ) + mfa_secret: Mapped[Optional[str]] = mapped_column(String(255), nullable=True) + mfa_enabled: Mapped[bool] = mapped_column( + Boolean, nullable=False, server_default="0" + ) + is_active: Mapped[bool] = mapped_column( + Boolean, nullable=False, server_default="1" + ) + must_change_password: Mapped[bool] = mapped_column( + Boolean, nullable=False, server_default="0" + ) + last_login: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True) + failed_login_attempts: Mapped[int] = mapped_column( + Integer, nullable=False, server_default="0" + ) + locked_until: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True) + created_at: Mapped[datetime] = mapped_column( + DateTime, nullable=False, server_default=func.now() + ) + updated_at: Mapped[datetime] = mapped_column( + DateTime, + nullable=False, + server_default=func.now(), + onupdate=func.now(), + server_onupdate=FetchedValue(), + ) + + # Relationships + refresh_tokens: Mapped[list[RefreshToken]] = relationship( + back_populates="user", cascade="all, delete-orphan" + ) + invitations_created: Mapped[list[InvitationLink]] = relationship( + foreign_keys="InvitationLink.created_by", back_populates="creator" + ) + invitations_used: Mapped[list[InvitationLink]] = relationship( + foreign_keys="InvitationLink.used_by", back_populates="used_by_user" + ) + + __table_args__ = ( + UniqueConstraint("username", name="uk_username"), + UniqueConstraint("email", name="uk_email"), + CheckConstraint( + "role IN ('admin', 'dak_mitarbeiter')", name="chk_role" + ), + ) + + +class RefreshToken(Base): + __tablename__ = "refresh_tokens" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + user_id: Mapped[int] = mapped_column( + Integer, + ForeignKey("users.id", ondelete="CASCADE"), + nullable=False, + ) + token_hash: Mapped[str] = mapped_column(String(255), nullable=False) + expires_at: Mapped[datetime] = mapped_column(DateTime, nullable=False) + revoked: Mapped[bool] = mapped_column( + Boolean, nullable=False, server_default="0" + ) + created_at: Mapped[datetime] = mapped_column( + DateTime, nullable=False, server_default=func.now() + ) + + # Relationships + user: Mapped[User] = relationship(back_populates="refresh_tokens") + + __table_args__ = ( + Index("idx_user", "user_id"), + Index("idx_token", "token_hash"), + ) + + +class InvitationLink(Base): + __tablename__ = "invitation_links" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + token: Mapped[str] = mapped_column(String(255), nullable=False) + email: Mapped[Optional[str]] = mapped_column(String(255), nullable=True) + role: Mapped[str] = mapped_column( + String(20), nullable=False, server_default="dak_mitarbeiter" + ) + created_by: Mapped[Optional[int]] = mapped_column( + Integer, ForeignKey("users.id"), nullable=True + ) + expires_at: Mapped[datetime] = mapped_column(DateTime, nullable=False) + used_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True) + used_by: Mapped[Optional[int]] = mapped_column( + Integer, ForeignKey("users.id"), nullable=True + ) + is_active: Mapped[bool] = mapped_column( + Boolean, nullable=False, server_default="1" + ) + created_at: Mapped[datetime] = mapped_column( + DateTime, nullable=False, server_default=func.now() + ) + + # Relationships + creator: Mapped[Optional[User]] = relationship( + foreign_keys=[created_by], back_populates="invitations_created" + ) + used_by_user: Mapped[Optional[User]] = relationship( + foreign_keys=[used_by], back_populates="invitations_used" + ) + + __table_args__ = ( + UniqueConstraint("token", name="uk_token"), + ) + + +class AllowedDomain(Base): + __tablename__ = "allowed_domains" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + domain: Mapped[str] = mapped_column(String(255), nullable=False) + role: Mapped[str] = mapped_column( + String(20), nullable=False, server_default="dak_mitarbeiter" + ) + is_active: Mapped[bool] = mapped_column( + Boolean, nullable=False, server_default="1" + ) + created_at: Mapped[datetime] = mapped_column( + DateTime, nullable=False, server_default=func.now() + ) + + __table_args__ = ( + UniqueConstraint("domain", name="uk_domain"), + )