mirror of
https://github.com/complexcaresolutions/dak.c2s.git
synced 2026-03-17 17:13:42 +00:00
feat: SQLAlchemy models for users, cases, reports, audit
11 models across 4 files matching the MariaDB schema: - user.py: User, RefreshToken, InvitationLink, AllowedDomain - case.py: Case, CaseICDCode - report.py: WeeklyReport, YearlySummary - audit.py: ImportLog, AuditLog, Notification All CHECK constraints, indexes (incl. prefix index), foreign keys, and server defaults match the SQL DDL specification exactly. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
5d57b1f349
commit
e7befe78b6
5 changed files with 703 additions and 0 deletions
|
|
@ -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",
|
||||||
|
]
|
||||||
130
backend/app/models/audit.py
Normal file
130
backend/app/models/audit.py
Normal file
|
|
@ -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",
|
||||||
|
),
|
||||||
|
)
|
||||||
198
backend/app/models/case.py
Normal file
198
backend/app/models/case.py
Normal file
|
|
@ -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"),
|
||||||
|
)
|
||||||
189
backend/app/models/report.py
Normal file
189
backend/app/models/report.py
Normal file
|
|
@ -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"),
|
||||||
|
)
|
||||||
162
backend/app/models/user.py
Normal file
162
backend/app/models/user.py
Normal file
|
|
@ -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"),
|
||||||
|
)
|
||||||
Loading…
Reference in a new issue