feat: integrate new desktop shell and extend backend & ADRs

- Added `meldestelle-desktop` module using JVM/Compose Desktop, registered in `settings.gradle.kts`.
- Integrated new screens and desktop navigation into core: `Veranstaltungen`, `TurnierDetail`, etc.
- Expanded backend with `ExposedFunktionaerRepository` in `officials-infrastructure`.
- Completed ADRs for bounded context mapping (`ADR-0014`) and context map (`ADR-0015`).
- Updated and extended project documentation with session logs and architecture decisions.

Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
This commit is contained in:
2026-03-24 18:22:15 +01:00
parent c624df8744
commit 354bd49de6
75 changed files with 7616 additions and 48 deletions
@@ -0,0 +1,401 @@
---
type: Specification
status: ACTIVE
owner: ÖTO/FEI Rulebook Expert
last_update: 2026-03-24
related:
- Abteilungs-Trennungs-Schwellenwerte.md
- docs/01_Architecture/adr/0016-api-design-acl-de.md
---
# Warn-Logik-Spezifikation: `competition-context` Abteilungs-Schwellenwerte
📜 **[ÖTO/FEI Rulebook Expert]** | 24. März 2026
Dieses Dokument ist die **verbindliche Implementierungs-Spezifikation** für die Warn-Logik
im `competition-context` bezüglich Abteilungs-Schwellenwerte. Es baut auf der
[Schwellenwert-Referenz](./Abteilungs-Trennungs-Schwellenwerte.md) auf und definiert
präzise, **wann**, **was** und **wie** gewarnt wird.
> ⚠️ **Grundprinzip (ADR-0007):** Das System gibt **niemals** harte Fehler bei
> Schwellenwert-Überschreitungen. Jede Warnung ist **overridebar** per Override-Event
> (TBA-Entscheidung). Warnungen werden gespeichert und sind auditierbar.
---
## 1. Warn-Typen: Übersicht
| Warn-Code | Typ | Auslöser | Betrifft |
|--------------------------------------------|-----------------------|--------------------------------------------------------------------------|------------------------------------|
| `WARN_PFLICHT_TEILUNG_UEBERSCHRITTEN` | Starter-Schwellenwert | Starterzahl > Pflicht-Schwellenwert | `DomBewerb` |
| `WARN_KANN_TEILUNG_EMPFOHLEN` | Starter-Schwellenwert | Starterzahl > Kann-Schwellenwert, keine Teilung konfiguriert | `DomBewerb` |
| `WARN_ABTEILUNG_ZU_GROSS` | Abteilungs-Limit | Abteilung nach Teilung > 80 Starter | `DomAbteilung` |
| `WARN_ABTEILUNG_MAX_UEBERSCHRITTEN` | Konfigurations-Limit | Starter > konfiguriertes `maxStarter`-Limit | `DomAbteilung` |
| `WARN_STRUKTURELLE_TEILUNG_FEHLT` | Strukturelle Pflicht | Vorgeschriebene Abteilungs-Struktur nicht vorhanden | `DomBewerb` + `List<DomAbteilung>` |
| `WARN_STRUKTURELLE_TEILUNG_UNVOLLSTAENDIG` | Strukturelle Pflicht | Abteilungs-Struktur vorhanden, aber Teilnehmerkreis falsch/unvollständig | `DomBewerb` + `List<DomAbteilung>` |
---
## 2. Warn-Typ 1: Starter-Schwellenwerte (`DomBewerb`)
### 2.1 Pflicht-Teilung überschritten
**Auslöser:** `DomBewerb.validateAbteilungsSchwellenwerte(aktuelleStarterAnzahl)`
| Bedingung | Warn-Code | Schwellenwert |
|----------------------------------------------------------------------------------------------------------------------|---------------------------------------|---------------|
| `pruefungsTyp` ∈ {`STIL_SPRINGEN`, `SPRINGPFERDE`, `DRESSURPFERDE`} UND `starterAnzahl > 30` UND `!istMeisterschaft` | `WARN_PFLICHT_TEILUNG_UEBERSCHRITTEN` | 30 |
| `pruefungsTyp == VIELSEITIGKEIT` UND `starterAnzahl > 40` UND `!istMeisterschaft` | `WARN_PFLICHT_TEILUNG_UEBERSCHRITTEN` | 40 |
| `pruefungsTyp == SPRINGEN_UEBRIG` UND `starterAnzahl > 80` UND `!istMeisterschaft` | `WARN_PFLICHT_TEILUNG_UEBERSCHRITTEN` | 80 |
**Warn-Nachricht (Format):**
```
WARN_PFLICHT_TEILUNG_UEBERSCHRITTEN:
Bewerb: [bewerbNummer] [bezeichnung]
Prüfungstyp: [pruefungsTyp]
Starter: [N] > Schwellenwert [S]
Empfehlung: Teilung nach [teilungsTyp] (Standard: NACH_LIZENZ)
Referenz: ÖTO § 39 Abs. 2
Override möglich (TBA-Entscheidung erforderlich)
```
### 2.2 Kann-Teilung empfohlen
**Auslöser:** `DomBewerb.validateAbteilungsSchwellenwerte(aktuelleStarterAnzahl)`
| Bedingung | Warn-Code | Schwellenwert |
|-------------------------------------------------------------------------------------------------------|-------------------------------|---------------|
| `pruefungsTyp == DRESSUR` UND `starterAnzahl > 30` UND `teilungsTyp == KEINE` UND `!istMeisterschaft` | `WARN_KANN_TEILUNG_EMPFOHLEN` | 30 |
**Warn-Nachricht (Format):**
```
WARN_KANN_TEILUNG_EMPFOHLEN:
Bewerb: [bewerbNummer] [bezeichnung]
Prüfungstyp: DRESSUR
Starter: [N] > 30
Empfehlung: Kann-Teilung nach NACH_LIZENZ möglich (§ 39 Abs. 2)
Override möglich (TBA-Entscheidung)
```
---
## 3. Warn-Typ 2: Abteilungs-Größe nach Teilung (`DomAbteilung`)
### 3.1 Abteilung nach Teilung zu groß
**Auslöser:** `DomAbteilung.validateStarterLimit()`
| Bedingung | Warn-Code |
|----------------------|---------------------------|
| `starterAnzahl > 80` | `WARN_ABTEILUNG_ZU_GROSS` |
**Warn-Nachricht (Format):**
```
WARN_ABTEILUNG_ZU_GROSS:
Abteilung: [abteilungsNummer] [bezeichnung]
Starter: [N] > 80
Erneute Teilung verpflichtend (§ 39 Abs. 2)
Override möglich (TBA-Entscheidung erforderlich)
```
### 3.2 Konfiguriertes Starter-Limit überschritten
**Auslöser:** `DomAbteilung.validateStarterLimit()`
| Bedingung | Warn-Code |
|---------------------------------------------------|-------------------------------------|
| `maxStarter > 0` UND `starterAnzahl > maxStarter` | `WARN_ABTEILUNG_MAX_UEBERSCHRITTEN` |
**Warn-Nachricht (Format):**
```
WARN_ABTEILUNG_MAX_UEBERSCHRITTEN:
Abteilung: [abteilungsNummer] [bezeichnung]
Starter: [N] > Limit [maxStarter]
Override möglich (TBA-Entscheidung erforderlich)
```
---
## 4. Warn-Typ 3: Strukturelle Pflicht-Teilungen (`DomBewerb` + `List<DomAbteilung>`)
Strukturelle Teilungen sind **unabhängig von der Starterzahl** verpflichtend.
Sie werden durch `DomBewerb.validateStrukturellesTeilung(abteilungen)` geprüft.
### 4.1 Entscheidungsbaum: Wann greift welche strukturelle Prüfung?
```
DomBewerb
├── sparte == SPRINGEN (CSN)
│ ├── pruefungsTyp == STIL_SPRINGEN UND hoeheCm <= 95
│ │ → Prüfung: LIZENZ_OHNE_VS_R1 (§ 200 Abs. 5.3)
│ ├── pruefungsTyp == SPRINGPFERDE UND hoeheCm IN [95..110]
│ │ → Prüfung: PFERDEALTER_4_VS_5_6 (§ 200 Abs. 6)
│ └── turnierkategorie == C_NEU
│ ├── hoeheCm <= 95
│ │ → Prüfung: C_NEU_OHNE_VS_MIT_LIZENZ (§ 231)
│ └── hoeheCm >= 100
│ → Prüfung: C_NEU_R1_VS_R2PLUS (§ 231)
├── sparte == VIELSEITIGKEIT (CCN)
│ ├── turnierkategorie == C_NEU
│ │ ├── hoeheCm <= 80
│ │ │ → Prüfung: CCN_C_NEU_3_ABT (§ 300 C-NEU)
│ │ └── hoeheCm >= 90
│ │ → Prüfung: CCN_C_NEU_2_ABT (§ 300 C-NEU)
│ └── pruefungsTyp == VIELSEITIGKEIT UND bezeichnung enthält "Welcome" ODER hoeheCm == 80
│ → Prüfung: CCN_WELCOME_80_R2PLUS_EIGENE_ABT (§ 301 Abs. 1.4)
├── sparte == DRESSUR (CDN)
│ └── pruefungsTyp == DRESSURPFERDE UND hoeheCm == null (Klasse A, 46-jährig)
│ → Prüfung: PFERDEALTER_4_VS_5_6 (§ 100 Abs. 5)
├── pruefungsTyp == CAPRILLI
│ → Prüfung: CAPRILLI_LIZENSFREI_VS_RD1PLUS (§ 803 Abs. 2)
└── sparte == FAHREN (CAN) UND pruefungsTyp == FAHREN
→ Prüfung: FAHREN_F1PLUS_EIGENE_ABT (§ 850 Abs. 9)
```
### 4.2 Strukturelle Prüfungen im Detail
#### LIZENZ_OHNE_VS_R1 CSN Stil-/Idealzeitspringen bis 95 cm
**Regel:** Mindestens 2 Abteilungen: Abt. ohne Lizenz + Abt. R1. Unabhängig von Starterzahl.
| Prüfung | Bedingung für `WARN_STRUKTURELLE_TEILUNG_FEHLT` |
|-------------------------------|-------------------------------------------------------------------|
| Abt. „ohne Lizenz" vorhanden? | Keine Abteilung mit `teilnehmerkreisBeschreibung` ~ „ohne Lizenz" |
| Abt. „R1" vorhanden? | Keine Abteilung mit `teilnehmerkreisBeschreibung` ~ „R1" |
```
WARN_STRUKTURELLE_TEILUNG_FEHLT:
Bewerb: [bewerbNummer] [bezeichnung]
Sparte: CSN, Prüfungstyp: STIL_SPRINGEN, Höhe: ≤ 95 cm
Fehlende Abteilung(en): [ohne Lizenz / R1]
Referenz: ÖTO B-Teil § 200 Abs. 5.3
Override möglich (TBA-Entscheidung erforderlich)
```
#### PFERDEALTER_4_VS_5_6 Springpferdeprüfung 95110 cm / Dressurpferdeprüfung Klasse A
**Regel:** 4-jährige in eigener Abteilung, getrennt von 56-jährigen.
| Prüfung | Bedingung für `WARN_STRUKTURELLE_TEILUNG_FEHLT` |
|-------------------------------|-------------------------------------------------------------------------|
| Abt. „4-jährige" vorhanden? | Keine Abteilung mit `teilnehmerkreisBeschreibung` ~ „4-jährig" |
| Abt. „56-jährige" vorhanden? | Keine Abteilung mit `teilnehmerkreisBeschreibung` ~ „5" oder „6-jährig" |
```
WARN_STRUKTURELLE_TEILUNG_FEHLT:
Bewerb: [bewerbNummer] [bezeichnung]
Sparte: [CSN/CDN], Prüfungstyp: [SPRINGPFERDE/DRESSURPFERDE], Höhe: [X] cm
Fehlende Abteilung(en): [4-jährige / 56-jährige]
Referenz: ÖTO B-Teil § 200 Abs. 6 / § 100 Abs. 5
Override möglich (TBA-Entscheidung erforderlich)
```
#### C_NEU_OHNE_VS_MIT_LIZENZ CSN-C-NEU bis 95 cm
**Regel:** Abt. 1 = ohne Lizenz, Abt. 2 = mit Lizenz.
```
WARN_STRUKTURELLE_TEILUNG_FEHLT:
Bewerb: [bewerbNummer] [bezeichnung]
Sparte: CSN, Kategorie: C-NEU, Höhe: ≤ 95 cm
Fehlende Abteilung(en): [ohne Lizenz / mit Lizenz]
Referenz: ÖTO B-Teil § 231
Override möglich (TBA-Entscheidung erforderlich)
```
#### C_NEU_R1_VS_R2PLUS CSN-C-NEU ab 100 cm
**Regel:** Abt. 1 = R1, Abt. 2 = R2 und höher.
```
WARN_STRUKTURELLE_TEILUNG_FEHLT:
Bewerb: [bewerbNummer] [bezeichnung]
Sparte: CSN, Kategorie: C-NEU, Höhe: ≥ 100 cm
Fehlende Abteilung(en): [R1 / R2+]
Referenz: ÖTO B-Teil § 231
Override möglich (TBA-Entscheidung erforderlich)
```
#### CCN_C_NEU_3_ABT CCN-C-NEU Gelände bis 80 cm
**Regel:** 3 Abteilungen: ohne Lizenz / R1 / R2+.
```
WARN_STRUKTURELLE_TEILUNG_FEHLT:
Bewerb: [bewerbNummer] [bezeichnung]
Sparte: CCN, Kategorie: C-NEU, Höhe: ≤ 80 cm
Fehlende Abteilung(en): [ohne Lizenz / R1 / R2+]
Referenz: ÖTO B-Teil § 300 (C-NEU)
Override möglich (TBA-Entscheidung erforderlich)
```
#### CCN_C_NEU_2_ABT CCN-C-NEU Gelände ab 90 cm
**Regel:** 2 Abteilungen: ohne Lizenz / R1+.
```
WARN_STRUKTURELLE_TEILUNG_FEHLT:
Bewerb: [bewerbNummer] [bezeichnung]
Sparte: CCN, Kategorie: C-NEU, Höhe: ≥ 90 cm
Fehlende Abteilung(en): [ohne Lizenz / R1+]
Referenz: ÖTO B-Teil § 300 (C-NEU)
Override möglich (TBA-Entscheidung erforderlich)
```
#### CCN_WELCOME_80_R2PLUS_EIGENE_ABT CCN Welcome / 80 cm
**Regel:** R2+ Reiter müssen in eigener Abteilung gewertet werden.
```
WARN_STRUKTURELLE_TEILUNG_FEHLT:
Bewerb: [bewerbNummer] [bezeichnung]
Sparte: CCN, Klasse: Welcome / 80 cm
R2+-Reiter ohne eigene Abteilung
Referenz: ÖTO B-Teil § 301 Abs. 1.4
Override möglich (TBA-Entscheidung erforderlich)
```
#### CAPRILLI_LIZENSFREI_VS_RD1PLUS Caprilli (§ 803)
**Regel:** Mindestens 2 Abteilungen: lizenzfrei / RD1 und höher. Unabhängig von Starterzahl.
```
WARN_STRUKTURELLE_TEILUNG_FEHLT:
Bewerb: [bewerbNummer] [bezeichnung]
Prüfungstyp: CAPRILLI
Fehlende Abteilung(en): [lizenzfrei / RD1+]
Referenz: ÖTO B-Teil § 803 Abs. 2
Override möglich (TBA-Entscheidung erforderlich)
```
#### FAHREN_F1PLUS_EIGENE_ABT Fahrertreffen (§ 850)
**Regel:** Fahrer mit Lizenz höher als F1 in eigener Abteilung.
```
WARN_STRUKTURELLE_TEILUNG_FEHLT:
Bewerb: [bewerbNummer] [bezeichnung]
Sparte: CAN, Prüfungstyp: FAHREN
F1+-Fahrer ohne eigene Abteilung
Referenz: ÖTO B-Teil § 850 Abs. 9
Override möglich (TBA-Entscheidung erforderlich)
```
---
## 5. Implementierungs-Vorgaben
### 5.1 Methoden-Signaturen (Kotlin)
```kotlin
// In DomBewerb bereits vorhanden, vollständig implementieren:
fun validateAbteilungsSchwellenwerte(aktuelleStarterAnzahl: Int): List<AbteilungsWarnung>
// In DomBewerb NEU zu implementieren:
fun validateStrukturellesTeilung(abteilungen: List<DomAbteilung>): List<AbteilungsWarnung>
// In DomAbteilung bereits vorhanden, vollständig implementieren:
fun validateStarterLimit(): List<AbteilungsWarnung>
```
### 5.2 `AbteilungsWarnung` Value Object
Statt roher Strings soll ein typisiertes Value Object verwendet werden:
```kotlin
@Serializable
data class AbteilungsWarnung(
val code: AbteilungsWarnungCodeE, // Maschinenlesbarer Warn-Code
val bewerbId: Uuid, // Betroffener Bewerb
val abteilungId: Uuid? = null, // Betroffene Abteilung (wenn relevant)
val nachricht: String, // Menschenlesbare Beschreibung
val oetoParagraph: String, // z.B. "§ 39 Abs. 2"
val istOverridebar: Boolean = true, // Immer true (ADR-0007)
val timestamp: Instant = Clock.System.now()
)
enum class AbteilungsWarnungCodeE {
WARN_PFLICHT_TEILUNG_UEBERSCHRITTEN,
WARN_KANN_TEILUNG_EMPFOHLEN,
WARN_ABTEILUNG_ZU_GROSS,
WARN_ABTEILUNG_MAX_UEBERSCHRITTEN,
WARN_STRUKTURELLE_TEILUNG_FEHLT,
WARN_STRUKTURELLE_TEILUNG_UNVOLLSTAENDIG
}
```
### 5.3 Override-Event
Wenn der TBA eine Warnung bestätigt/überschreibt, wird ein `AbteilungsWarnungOverrideEvent` gespeichert:
```kotlin
@Serializable
data class AbteilungsWarnungOverrideEvent(
val overrideId: Uuid = Uuid.random(),
val warnungCode: AbteilungsWarnungCodeE,
val bewerbId: Uuid,
val abteilungId: Uuid? = null,
val begruendung: String, // Pflichtfeld TBA muss Begründung angeben
val tbaUserId: Uuid,
val timestamp: Instant = Clock.System.now()
)
```
### 5.4 Konfigurierbare Schwellenwerte
Die Schwellenwerte sind **nicht hard-coded**, sondern über `AbteilungsSchwellenwertConfig` konfigurierbar:
```kotlin
data class AbteilungsSchwellenwertConfig(
val stilSpringpferdPflicht: Int = 30, // § 39 Abs. 2
val vielseitigkeitPflicht: Int = 40, // § 39 Abs. 2
val springenUebrigPflicht: Int = 80, // § 39 Abs. 2
val dressurKann: Int = 30, // § 39 Abs. 2
val abteilungMaxNachTeilung: Int = 80 // § 39 Abs. 2
)
```
### 5.5 Aufruf-Zeitpunkte (Trigger)
| Ereignis | Aufgerufene Validierung |
|-------------------------------------------------------------|----------------------------------------------------------|
| Neue Nennung wird einem Bewerb zugeordnet | `DomBewerb.validateAbteilungsSchwellenwerte(neueAnzahl)` |
| Abteilung wird erstellt oder geändert | `DomAbteilung.validateStarterLimit()` |
| Bewerb wird gespeichert / Abteilungs-Konfiguration geändert | `DomBewerb.validateStrukturellesTeilung(abteilungen)` |
| Startliste wird aus ENTWURF → VEROEFFENTLICHT überführt | Alle drei Validierungen als Gesamt-Check |
---
## 6. Ausnahmen (nicht warnen)
| Bedingung | Begründung |
|--------------------------------------------------------|-----------------------------------------------------|
| `istMeisterschaft == true` | § 39 Abs. 4: Meisterschaftsbewerbe sind ausgenommen |
| Bewerb mit Geldpreisen > Doppeltes der Gebührenordnung | § 39 Abs. 2: Ausnahme von Pflicht-Teilung |
| `turnierkategorie` nicht in {A*, A, B*, B, C, C-NEU} | Schwellenwerte gelten nur für diese Kategorien |
---
## 7. Offene Fragen (Klärungsbedarf)
| # | Frage | Status |
|---|----------------------------------------------------------------------------------------------|----------|
| 1 | Gelten § 39-Schwellenwerte auch für **Reitertreffen** (nicht nur Turniere)? | 🔍 Offen |
| 2 | Pflicht-Teilung bei **kombinierten Turnieren** (CDN + CSN, § 4)? | 🔍 Offen |
| 3 | **Voltigieren (CVN):** Eigene Abteilungs-Trennungsregeln? (§ 400 ff. nicht ausgewertet) | 🔍 Offen |
| 4 | **Fahren (CAN):** Eigene Starter-Schwellenwerte jenseits der Reitertreffen-Regel? | 🔍 Offen |
| 5 | Wie wird „Bewerb mit Geldpreisen > Doppeltes der Gebührenordnung" im Datenmodell abgebildet? | 🔍 Offen |
---
*Erstellt: 2026-03-24 | Autor: ÖTO/FEI Rulebook Expert (Junie)*
*Basiert auf: ÖTO 2026 A-Teil § 39, B-Teil §§ 100, 200, 231, 300, 301, 803, 850*
*Implementierungs-Ziel: `competition-context` (PHASE 5)*