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
+158 -31
View File
@@ -2,26 +2,54 @@
type: Roadmap
status: ACTIVE
owner: Lead Architect
last_update: 2026-03-05
last_update: 2026-03-24
---
# MASTER ROADMAP: "Operation Self-Sovereignty"
# MASTER ROADMAP: Meldestelle-Biest
🏗️ **[Lead Architect]** | 24. März 2026
**Strategisches Ziel:**
Vollständige Migration auf Self-Hosted Infrastruktur (Gitea, Pangolin, Zora) und Konsolidierung der Dokumentation. Der Fokus liegt auf Datensouveränität, Offline-Fähigkeit und einer sauberen, aktuellen Wissensbasis.
Entwicklung einer ÖTO-konformen, offline-fähigen Turnier-Meldestelle als Compose Desktop App (KMP).
Vollständige Self-Hosted Infrastruktur (Gitea, Pangolin, Zora). Datensouveränität, Offline-First, saubere Wissensbasis.
**Aktueller technischer Stand (05.03.2026):**
**Aktueller technischer Stand (24.03.2026):**
* **Infrastruktur:** ✅ "Zora" (MS-R1, ARM64) ist live. Gitea & Registry laufen.
* **Networking:** ✅ Pangolin Tunnel ersetzt Cloudflare.
* **CI/CD:** ✅ Gitea Actions mit ARM64-Runner (VM 102) aktiv. Docker-Publish Pipeline grün.
* **Code-Basis:** ✅ Backend (Java 25), Frontend (Kotlin/JS) bauen sauber auf ARM64.
* **Dokumentation:** ✅ Konsolidiert und aufgeräumt.
* **Code-Basis:** ✅ Backend (Java 25 / Spring Boot / Kotlin), Frontend (KMP/Compose Desktop).
* **Domain-Design:** ✅ 6 Bounded Contexts (SCS-Architektur) definiert. Ubiquitous Language erstellt.
* **Domain-Modelle:** ✅ `DomReiter`, `DomNennung`, `DomNennungsTransfer`, `DomPferd`, `DomFunktionaer`, `DomVerein`,
`DomBewerb`, `DomAbteilung`, `DomStartliste`, `DomVeranstaltung`, `DomTurnier`, `DomAusschreibung` implementiert.
Enums ÖTO-konform.
* **Dokumentation:** ✅ Konsolidiert. ÖTO-Regelwerk-Referenzen (Abteilungs-Schwellenwerte) dokumentiert.
---
## 1. Arbeitsaufträge an die AGENTS (Phasenplan)
## Architektur-Übersicht: 6 Bounded Contexts (SCS)
### PHASE 1: Documentation Cleanup (ABGESCHLOSSEN)
Das System ist in **6 Self-Contained Systems (SCS)** aufgeteilt, die fachlich voneinander getrennt sind
und über definierte Schnittstellen kommunizieren.
| SCS | Kontext | Priorität | Status |
|----------------------------|---------------------------------------|-----------|----------------|
| `registration-context` | Nennungs-Workflow (Herzstück) | **P1** | 🟡 In Arbeit |
| `actor-context` | Reiter, Pferde, Funktionäre, ZNS | **P1** | 🟡 In Arbeit |
| `competition-context` | Bewerbe, Startlisten, Ergebnisse | **P2** | ⬜ Geplant |
| `event-management-context` | Veranstaltung, Turnier, Ausschreibung | **P2** | ⬜ Geplant |
| `billing-context` | Abrechnung, Kassa, Gebühren | **P3** | ⬜ Geplant |
| `identity-context` | Auth, Rollen (Keycloak) | **P3** | ⬜ Geplant |
| `series-context` | Cups, Serien, Meisterschaften | Phase 2+ | 🔵 Vorbereitet |
> **Hinweis `series-context`:** Ist Phase 2+, aber die Architektur ist von Anfang an vorbereitet.
> Cups/Serien/Meisterschaften benötigen eigene, konfigurierbare Reglements (kein Hard-Coding).
> Pluggable Berechnungsmodell und konfigurierbare Paar-Bindung (Reiter+Pferd vs. nur Reiter) erforderlich.
---
## 1. Abgeschlossene Phasen
### PHASE 1: Documentation Cleanup ✅ ABGESCHLOSSEN
*Ziel: Eine einzige, vertrauenswürdige Quelle der Wahrheit (SSOT) schaffen.*
#### 🧹 Agent: Curator
@@ -30,38 +58,137 @@ Vollständige Migration auf Self-Hosted Infrastruktur (Gitea, Pangolin, Zora) un
* [x] **Struktur:** `docs/` Ordner aufräumen (unnötige Root-Files in Unterordner).
* [x] **Index:** `README.md` im Root als Einstiegspunkt aktualisieren.
### PHASE 2: Infrastructure Hardening (ABGESCHLOSSEN)
### PHASE 2: Infrastructure Hardening ✅ ABGESCHLOSSEN
*Ziel: Stabilisierung der neuen Self-Hosted Umgebung.*
#### 🐧 Agent: DevOps Engineer
* [x] **Keycloak Fix:** Verbindungsprobleme innerhalb des Docker-Netzwerks (`meldestelle-host`) behoben (Alias `auth.mo-code.at`).
* [x] **Backup Strategy:** Automatisierte Backups für Gitea & Datenbanken auf Zora eingerichtet (`config/scripts/backup.sh`).
* [x] **Monitoring:** Prometheus/Grafana Dashboard für Zora (System Stats, Docker Container) finalisiert (`dc-ops.yaml`).
* [x] **Keycloak Fix:** Verbindungsprobleme innerhalb des Docker-Netzwerks behoben (Alias `auth.mo-code.at`).
* [x] **Backup Strategy:** Automatisierte Backups für Gitea & Datenbanken auf Zora (`config/scripts/backup.sh`).
* [x] **Monitoring:** Prometheus/Grafana Dashboard für Zora finalisiert (`dc-ops.yaml`).
* [x] **Deployment:** Git-basiertes Deployment-Skript (`config/scripts/deploy.sh`) erstellt.
### PHASE 3: Feature Development (ON HOLD)
*Ziel: Neuausrichtung der Fachlichkeit.*
### PHASE 3: Domain-Design & Ubiquitous Language ✅ ABGESCHLOSSEN
#### 🏗️ Agent: Lead Architect & Domain Expert
* [ ] **Domain Analysis:** Workshop mit dem Fachexperten zur Neudefinition der Anforderungen.
* [ ] **Feature Roadmap:** Erstellung eines neuen Plans basierend auf den Ergebnissen.
* [ ] **Entries Service:** Pausiert bis zur Klärung der Anforderungen.
*Ziel: Fachliche Grundlage für die Implementierung schaffen.*
#### 🏗️ Agent: Lead Architect
* [x] **DDD-Analyse:** 6 Bounded Contexts (SCS-Architektur) definiert und priorisiert.
* [x] **Terminologie:** `Veranstaltung` ≠ `Turnier` gemäß ÖTO § 2 Abs. 1 festgelegt (ADR).
* [x] **Design-Baseline:** Vision_03 (Figma) als offizieller Design-Baseline festgelegt.
* [x] **Technologie:** Desktop-First-Strategie mit KMP/Compose Desktop beschlossen.
#### 📜 Agent: ÖTO/FEI Rulebook Expert
* [x] **Ubiquitous Language:** Offizielle Domänen-Terminologie mit ÖTO-Referenzen erstellt.
* [x] **Abteilungs-Schwellenwerte:** Alle Trennungs-Schwellenwerte (§ 39 + spartenspezifisch) dokumentiert.
→ `docs/03_Domain/02_Reference/OETO_Regelwerk/Abteilungs-Trennungs-Schwellenwerte.md`
#### 👷 Agent: Backend Developer
* [x] **Enums:** `SparteE`, `TurnierkategorieE`, `VeranstaltungsTypE`, `LizenzKlasseE`, `NennungsStatusE`,
`StartwunschE` – ÖTO-konform.
* [x] **Domain-Modelle:** `DomReiter` (actor-context), `DomNennung`, `DomNennungsTransfer` (registration-context).
* [x] **Modul:** `entries-domain` als KMP-Modul aufgesetzt und in `settings.gradle.kts` registriert.
---
## 2. Definition of Done (für Phase 1 & 2)
1. [x] `docs/` Root enthält nur noch essentielle Einstiegspunkte (`README.md`, `MASTER_ROADMAP.md`).
2. [x] Alle veralteten Dokumente sind im `_archive` oder gelöscht.
3. [x] Die `Zora_System_Architektur.md` ist korrekt in `docs/07_Infrastructure/` eingeordnet.
4. [x] Ein neuer Entwickler findet sich sofort zurecht.
5. [x] Keycloak ist intern erreichbar.
6. [x] Backups laufen automatisch.
## 2. Aktuelle Phase
### PHASE 4: MVP-Implementierung 🟡 IN ARBEIT
*Ziel: Lauffähiger MVP für `registration-context` und `actor-context` (P1-Contexts).*
#### 🏗️ Agent: Lead Architect
* [x] **ADRs vervollständigen:** Bounded Context Mapping und Context Map dokumentieren.
→ `docs/01_Architecture/adr/0014-bounded-context-mapping-de.md`
→ `docs/01_Architecture/adr/0015-context-map-de.md`
* [x] **API-Design:** Schnittstellen zwischen den Contexts definieren (Anti-Corruption Layer).
→ `docs/01_Architecture/adr/0016-api-design-acl-de.md`
#### 👷 Agent: Backend Developer
* [x] **`actor-context`:** Domain-Modelle für `DomPferd`, `DomFunktionaer`, `DomVerein` implementiert.
* [x] **`registration-context`:** `DomBewerb`, `DomAbteilung`, `DomStartliste` implementiert.
* [x] **`event-management-context`:** `DomVeranstaltung`, `DomTurnier`, `DomAusschreibung` implementiert.
* [x] **Persistenz:** Repository-Interfaces und erste DB-Migrationen (Flyway/Liquibase).
* [x] **API:** REST-Endpunkte für Nennungs-Workflow (Kern-Use-Cases).
#### 🎨 Agent: Frontend Expert
* [x] **KMP/Compose Desktop:** Projektstruktur aufgesetzt (`frontend/shells/meldestelle-desktop`).
* [x] **Navigation:** Sidebar-Navigation gemäß Vision_03 implementiert (Veranstaltungen, Reiter, Pferde, Funktionäre,
Meisterschaften, Cups).
* [x] **Nennungs-Screen:** `TurnierDetailScreen` integriert `NennungsMaske` aus `nennung-feature` (Bewerbe-Tab ⭐).
#### 📜 Agent: ÖTO/FEI Rulebook Expert
* [ ] **Voltigieren (CVN):** Abteilungs-Trennungsregeln aus B-Teil § 400 ff. auswerten (offene Frage #3).
* [ ] **Fahren (CAN):** Starter-Schwellenwerte jenseits der Reitertreffen-Regel klären (offene Frage #4).
* [x] **Warn-Logik:** Spezifikation der `competition-context` Warn-Logik für Abteilungs-Schwellenwerte.
→ `docs/03_Domain/02_Reference/OETO_Regelwerk/Warn-Logik-Spezifikation-competition-context.md`
---
## 3. Wichtige Referenzen
* **Infrastruktur:** `docs/07_Infrastructure/Zora_System_Architektur.md`
* **Deployment Guide:** `docs/07_Infrastructure/Guides/Setup_Git_Deployment_Zora.md`
* **Backup Guide:** `docs/07_Infrastructure/Guides/Setup_Backup_Zora.md`
* **CI/CD:** `.gitea/workflows/docker-publish.yaml`
* **Projekt-Protokoll:** `docs/04_Agents/Playbooks/`
## 3. Geplante Phasen
### PHASE 5: P2-Contexts & Integration ⬜ GEPLANT
*Ziel: `competition-context` und `event-management-context` implementieren.*
* [ ] **`competition-context`:** Bewerbe, Startlisten, Ergebnisse, Abteilungs-Warn-Logik.
* [ ] **`event-management-context`:** Veranstaltungs- und Turnier-Verwaltung, Ausschreibungs-Generator.
* [ ] **ZNS-Integration:** Schnittstelle zum Zentralen Nennungs-System (A-Satz / B-Satz).
* [ ] **Offline-Sync:** Offline-First-Strategie für Desktop-App implementieren.
### PHASE 6: P3-Contexts & Billing ⬜ GEPLANT
*Ziel: `billing-context` und `identity-context` implementieren.*
* [ ] **`billing-context`:** Gebührenberechnung, Kassa, Abrechnung.
* [ ] **`identity-context`:** Rollen-Modell (TBA, Veranstalter, Richter etc.) mit Keycloak.
* [ ] **Reporting:** Startlisten- und Ergebnislisten-Druck (PDF).
### PHASE 7: Series-Context & Erweiterungen 🔵 PHASE 2+
*Ziel: Cups, Serien und Meisterschaften mit konfigurierbaren Reglements.*
* [ ] **`series-context`:** Pluggable Berechnungsmodell, konfigurierbare Paar-Bindung.
* [ ] **Web-Portal:** Shared Module aus Desktop-App extrahieren → Web-Portal aufbauen.
* [ ] **Mobile:** KMP-Sharing auf Android/iOS ausweiten.
---
## 4. Wichtige Architektur-Entscheidungen (ADRs)
| # | Entscheidung | Status | Dokument |
|----|--------------------------------------------------------------|--------|------------------------------|
| 1 | Vision_03 = Design-Baseline | ✅ | Session Log 2026-03-24 |
| 2 | Desktop-First mit KMP/Compose Desktop | ✅ | ADR-0009 |
| 3 | `Veranstaltung` ≠ `Turnier` (ÖTO § 2 Abs. 1) | ✅ | Ubiquitous Language |
| 4 | 6 Bounded Contexts als SCS-Architektur | ✅ | Session Log 2026-03-24 |
| 5 | `series-context` ist Phase 2+ (Architektur vorbereitet) | ✅ | Session Log 2026-03-24 |
| 6 | Cups/Serien benötigen konfigurierbare Reglements | ✅ | Session Log 2026-03-24 |
| 7 | Warn-Logik statt harter Fehler (Override-Event) | ✅ | Abteilungs-Schwellenwerte.md |
| 8 | 6 Bounded Contexts: Mapping & Aggregate Roots | ✅ | ADR-0014 |
| 9 | Context Map: Integration Patterns & ACL-Strategie | ✅ | ADR-0015 |
| 10 | API-Design & ACL: Ports, DTOs, REST-Endpunkte, Domain Events | ✅ | ADR-0016 |
---
## 5. Wichtige Referenzen
| Dokument | Pfad |
|---------------------------|----------------------------------------------------------------------------------------------|
| Ubiquitous Language | `docs/03_Domain/01_Glossary/Ubiquitous_Language.md` |
| Abteilungs-Schwellenwerte | `docs/03_Domain/02_Reference/OETO_Regelwerk/Abteilungs-Trennungs-Schwellenwerte.md` |
| Warn-Logik-Spezifikation | `docs/03_Domain/02_Reference/OETO_Regelwerk/Warn-Logik-Spezifikation-competition-context.md` |
| Session Log (DDD) | `docs/99_Journal/2026-03-24_Session_Log_DDD_Ubiquitous_Language.md` |
| Infrastruktur | `docs/07_Infrastructure/Zora_System_Architektur.md` |
| Deployment Guide | `docs/07_Infrastructure/Guides/Setup_Git_Deployment_Zora.md` |
| Backup Guide | `docs/07_Infrastructure/Guides/Setup_Backup_Zora.md` |
| CI/CD | `.gitea/workflows/docker-publish.yaml` |
| Agent Playbooks | `docs/04_Agents/Playbooks/` |
| ADR-Verzeichnis | `docs/01_Architecture/adr/` |
@@ -0,0 +1,225 @@
---
type: ADR
id: ADR-0014
status: ACTIVE
owner: Lead Architect
last_update: 2026-03-24
---
# ADR-0014: Bounded Context Mapping (SCS-Architektur)
## Status
Akzeptiert
## Kontext
Mit der Entscheidung für Domain-Driven Design (→ ADR-0002) und der modularen Architektur (→ ADR-0001) war es
notwendig, die fachlichen Grenzen des Systems explizit zu definieren. Die ursprünglichen Module (`masterdata`,
`members`, `horses`, `events`) spiegelten technische Kategorien wider, nicht die tatsächlichen Fachdomänen des
österreichischen Turniersports.
Folgende Probleme wurden identifiziert:
1. Fehlende Ausrichtung zwischen Code-Struktur und ÖTO-Regelwerk
2. Unklare Verantwortlichkeiten bei domänenübergreifenden Operationen (z.B. Nennungs-Transfer)
3. Keine explizite Trennung zwischen Kern-Domäne (Nennungs-Workflow) und unterstützenden Domänen
4. Fehlende Grundlage für eine skalierbare, offline-fähige Desktop-Architektur
## Entscheidung
Das System wird in **6 Bounded Contexts** aufgeteilt, die als **Self-Contained Systems (SCS)** implementiert werden.
Jeder Context ist fachlich eigenständig, besitzt seine eigene Ubiquitous Language und kommuniziert über definierte
Schnittstellen.
### Übersicht der 6 Bounded Contexts
| Context | Verantwortlichkeit | Priorität | Phase |
|----------------------------|------------------------------------------------------|-----------|---------|
| `registration-context` | Nennungs-Workflow (Herzstück des Systems) | **P1** | Phase 4 |
| `actor-context` | Reiter, Pferde, Funktionäre, Vereine, ZNS-Stammdaten | **P1** | Phase 4 |
| `competition-context` | Bewerbe, Startlisten, Ergebnisse, Abteilungs-Logik | **P2** | Phase 5 |
| `event-management-context` | Veranstaltung, Turnier, Ausschreibung, Genehmigungen | **P2** | Phase 5 |
| `billing-context` | Abrechnung, Kassa, Gebühren, Konten | **P3** | Phase 6 |
| `identity-context` | Authentifizierung, Rollen, Berechtigungen (Keycloak) | **P3** | Phase 6 |
> **Hinweis `series-context`:** Cups, Serien und Meisterschaften werden in Phase 2+ als eigenständiger Context
> implementiert. Die Architektur ist von Anfang an dafür vorbereitet (pluggable Berechnungsmodell,
> konfigurierbare Paar-Bindung). Kein Hard-Coding von Serien-Logik in anderen Contexts.
---
### Context-Beschreibungen
#### `registration-context` — Kern-Domäne (Core Domain)
**Verantwortlichkeit:** Der gesamte Lebenszyklus einer Nennung – von der Erstanmeldung bis zur Stornierung.
**Aggregate Roots:**
- `DomNennung` – Verbindliche Anmeldung eines Paares (Reiter & Pferd) zu einem Bewerb
- `DomNennungsTransfer` – Transfer-Operation (kein Storno + Neu); Guthaben bleibt erhalten
- `DomAbteilung` – Kleinste Einheit für Startlisten und Ergebnisse (mit Warn-Logik)
**Ubiquitous Language (Auswahl):**
- `Nennung`, `Nennschluss`, `Nachnenngebühr`, `Nennungs-Transfer`, `Override-Event`, `Startwunsch`
**Kern-Invarianten:**
- Eine Nennung ist immer einem Paar (Reiter + Pferd) zugeordnet
- Nennungs-Transfer ist eine atomare Operation – kein Zwischenzustand ohne gültiges Paar
- Regelwerk-Verstöße erzeugen **Warnungen** (niemals harte Fehler) + `Override-Event`
---
#### `actor-context` — Unterstützende Domäne (Supporting Domain)
**Verantwortlichkeit:** Stammdaten aller Akteure und Synchronisation mit dem ZNS (Zentrales Nennungs-System).
**Aggregate Roots:**
- `DomReiter` – Reiter mit Lizenz, Satznummer, Startkarte
- `DomPferd` – Pferd mit Lebensnummer, Kopfnummer, Satznummer
- `DomFunktionär` – Person mit Turnier-Rolle und Qualifikation
- `DomVerein` – OEPS-Mitgliedsverein (Veranstalter)
**Ubiquitous Language (Auswahl):**
- `Satznummer`, `Lebensnummer`, `Kopfnummer`, `FEI-ID`, `Lizenz`, `Startkarte`, `Sperrliste`, `Gastreiter`
**Kern-Invarianten:**
- `Satznummer` ist der primäre Schlüssel für den ZNS-Datenaustausch
- `Lebensnummer` und `Kopfnummer` sind **nicht** als Datenbankschlüssel geeignet (ZNS-Inkonsistenzen)
- ZNS-Daten werden lokal gecacht (Offline-First); Synchronisation im Hintergrund
---
#### `competition-context` — Unterstützende Domäne (Supporting Domain)
**Verantwortlichkeit:** Strukturierung von Bewerben, Erstellung von Startlisten, Erfassung von Ergebnissen.
**Aggregate Roots:**
- `DomBewerb` – Einzelne sportliche Prüfung mit Bewerbsnummer, Sparte, Klasse, Richtverfahren
- `DomAbteilung` – Untereinheit eines Bewerbs mit eigenem Teilnehmerkreis und Platzierung
- `DomStartliste` – Geordnete Liste der Starter einer Abteilung
- `DomErgebnis` – Ergebnis eines Starts (Platzierung, Punkte, Zeit)
**Ubiquitous Language (Auswahl):**
- `Bewerb`, `Prüfung`, `Abteilung`, `Abteilungsnummer`, `Startliste`, `Richtverfahren`, `Klasse/Höhe`
**Kern-Invarianten:**
- Abteilungs-Schwellenwerte gemäß ÖTO § 39 lösen **Warnungen** aus (→ `Override-Event`)
- Vollständige Schwellenwert-Tabellen:
`docs/03_Domain/02_Reference/OETO_Regelwerk/Abteilungs-Trennungs-Schwellenwerte.md`
---
#### `event-management-context` — Unterstützende Domäne (Supporting Domain)
**Verantwortlichkeit:** Verwaltung von Veranstaltungen und Turnieren, Ausschreibungs-Generierung, Genehmigungsprozesse.
**Aggregate Roots:**
- `DomVeranstaltung` – Interne Organisationseinheit des Veranstalters (selbst vergebene ID)
- `DomTurnier` – Offizielles Turnier mit OEPS-vergebener Turniernummer
- `DomAusschreibung` – Offizielles Dokument mit Pflichtfeldern gemäß ÖTO (A-Satz ZNS)
**Ubiquitous Language (Auswahl):**
- `Veranstaltung`, `Turnier`, `Turniernummer`, `Turnierkategorie`, `Ausschreibung`, `Kombination`, `TBA`
**Kern-Invarianten:**
- `Veranstaltung` ≠ `Turnier` (→ ADR-0002, ÖTO § 2 Abs. 1): Eine Veranstaltung kann mehrere Turniere umfassen
- Turniernummern werden von der OEPS vergeben, nicht selbst generiert
- Kombinations-Turniere behalten je eigene Turniernummer
---
#### `billing-context` — Generische Domäne (Generic Domain)
**Verantwortlichkeit:** Gebührenberechnung, Kassenführung, Abrechnung mit Reitern und dem Verband.
**Aggregate Roots:**
- `DomKonto` – Kontobasierte Abrechnung pro Zahler (Basis für „Hansi-Szenario")
- `DomGebühr` – Einzelgebühr (Nenngeld, Nachnenngebühr, Sportförderbeitrag, Tierwohl-Euro)
- `DomAbrechnung` – Zusammenfassung aller Gebühren einer Veranstaltung
**Ubiquitous Language (Auswahl):**
- `Konto`, `Nenngeld`, `Nachnenngebühr`, `Sportförderbeitrag`, `Tierwohl-Euro`, `Gebühren-Verzicht`
**Kern-Invarianten:**
- Sportförderbeitrag und Tierwohl-Euro fallen **pro Start** an (nicht pro Nennung)
- Gebühren-Verzicht wird als explizites Event gespeichert (Audit-Trail)
---
#### `identity-context` — Generische Domäne (Generic Domain)
**Verantwortlichkeit:** Authentifizierung, Rollen-Management, Berechtigungsprüfung (via Keycloak).
**Aggregate Roots:**
- `DomBenutzer` – Systembenutzer mit Rollen (TBA, Veranstalter, Meldestelle, Richter)
- `DomRolle` – Definierte Rolle mit Berechtigungen
**Ubiquitous Language (Auswahl):**
- `TBA`, `Veranstalter`, `Meldestelle`, `Richter`, `Rolle`, `Berechtigung`
**Kern-Invarianten:**
- Keycloak ist der einzige Identity Provider (→ ADR-0006)
- Rollen sind turnierbezogen (ein Benutzer kann bei Turnier A TBA und bei Turnier B Richter sein)
---
## Konsequenzen
### Positive
- **Fachliche Klarheit:** Jeder Context hat eine klar definierte Verantwortlichkeit und eigene Ubiquitous Language
- **Unabhängige Entwicklung:** P1-Contexts (`registration-context`, `actor-context`) können ohne P2/P3 entwickelt werden
- **Offline-First:** Jeder Context kann seinen eigenen lokalen Cache verwalten (SQLDelight)
- **ÖTO-Konformität:** Die Context-Grenzen spiegeln die Struktur des ÖTO-Regelwerks wider
- **Erweiterbarkeit:** `series-context` kann in Phase 2+ ohne Änderungen an bestehenden Contexts hinzugefügt werden
### Negative
- **Koordinationsaufwand:** Domänenübergreifende Use-Cases (z.B. Nennungs-Workflow) erfordern explizite Integration
- **Datenkonsistenz:** Eventual Consistency zwischen Contexts muss bewusst gehandhabt werden
- **Initialer Aufwand:** Vollständige Context-Implementierung erfordert mehr Vorabdesign
### Neutral
- Die Context-Grenzen können sich mit wachsendem Domänenwissen verschieben (Living Architecture)
## Betrachtete Alternativen
### Technische Modulaufteilung (abgelehnt)
Die ursprüngliche Aufteilung in `masterdata`, `members`, `horses`, `events` wurde verworfen, da sie technische
Kategorien statt fachliche Domänen widerspiegelt und keine klare Heimat für den Nennungs-Workflow bietet.
### Monolithische Domäne (abgelehnt)
Ein einzelner großer Domänen-Context würde die Komplexität des ÖTO-Regelwerks nicht beherrschbar machen und
die Offline-First-Strategie erschweren.
## Referenzen
- [ADR-0001: Modulare Architektur](0001-modular-architecture-de.md)
- [ADR-0002: Domain-Driven Design](0002-domain-driven-design-de.md)
- [ADR-0015: Context Map](0015-context-map-de.md)
- [Ubiquitous Language](../../03_Domain/01_Glossary/Ubiquitous_Language.md)
- [Abteilungs-Trennungs-Schwellenwerte](../../03_Domain/02_Reference/OETO_Regelwerk/Abteilungs-Trennungs-Schwellenwerte.md)
- [MASTER_ROADMAP](../MASTER_ROADMAP.md)
- ÖTO 2026, § 2 Abs. 1, § 2 Abs. 7, § 2 Abs. 8, § 39
@@ -0,0 +1,275 @@
---
type: ADR
id: ADR-0015
status: ACTIVE
owner: Lead Architect
last_update: 2026-03-24
---
# ADR-0015: Context Map & Integration Patterns
## Status
Akzeptiert
## Kontext
Nach der Definition der 6 Bounded Contexts (→ ADR-0014) müssen die **Beziehungen zwischen den Contexts** explizit
dokumentiert werden. Eine Context Map beschreibt:
- Welche Contexts miteinander kommunizieren
- In welche Richtung Abhängigkeiten fließen
- Welches Integration Pattern verwendet wird
- Wo Anti-Corruption Layers (ACL) notwendig sind
Ohne eine explizite Context Map entstehen implizite Abhängigkeiten, die die Unabhängigkeit der Contexts untergraben
und die Offline-First-Strategie gefährden.
## Entscheidung
### Context Map (ASCII-Diagramm)
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ EXTERNE SYSTEME │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ ZNS (Zentrales Nennungs-System / OEPS) [Upstream / Big Ball] │ │
│ └──────────────────────────┬──────────────────────────────────────────┘ │
│ │ ACL (A-Satz / B-Satz Import) │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────────────────┐ │
│ │ Keycloak (Identity Provider) [Upstream / Conformist] │ │
│ └──────────────────────────┬───────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
│
│ JWT / OIDC
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ INTERNE CONTEXTS │
│ │
│ ┌──────────────────────┐ ┌──────────────────────────────────────┐ │
│ │ identity-context │◄───────│ (alle Contexts) │ │
│ │ [Generic Domain] │ OHS │ prüfen Berechtigungen via Token │ │
│ └──────────────────────┘ └──────────────────────────────────────┘ │
│ │
│ ┌──────────────────────┐ ACL ┌──────────────────────────────────────┐ │
│ │ actor-context │◄───────│ ZNS (extern) │ │
│ │ [Supporting Domain] │ └──────────────────────────────────────┘ │
│ │ │ │
│ │ DomReiter │ CL/SK ┌──────────────────────────────────────┐ │
│ │ DomPferd │───────►│ registration-context │ │
│ │ DomFunktionär │ │ [Core Domain] │ │
│ │ DomVerein │ │ │ │
│ └──────────────────────┘ │ DomNennung │ │
│ │ DomNennungsTransfer │ │
│ ┌──────────────────────┐ CL/SK │ DomAbteilung │ │
│ │ event-management- │───────►│ │ │
│ │ context │ └──────────────┬───────────────────────┘ │
│ │ [Supporting Domain] │ │ │
│ │ │ │ Domain Events │
│ │ DomVeranstaltung │ │ (NennungErstellt, │
│ │ DomTurnier │ │ NennungStorniert, │
│ │ DomAusschreibung │ │ NennungTransferiert) │
│ └──────────────────────┘ │ │
│ ▼ │
│ ┌──────────────────────┐ ┌──────────────────────────────────────┐ │
│ │ billing-context │◄───────│ competition-context │ │
│ │ [Generic Domain] │ ACL │ [Supporting Domain] │ │
│ │ │ │ │ │
│ │ DomKonto │ │ DomBewerb │ │
│ │ DomGebühr │ │ DomAbteilung │ │
│ │ DomAbrechnung │ │ DomStartliste │ │
│ └──────────────────────┘ │ DomErgebnis │ │
│ └──────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────────────────────┐ │
│ │ series-context [Phase 2+ — Architektur vorbereitet, nicht aktiv] │ │
│ └──────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
```
**Legende:**
- `ACL` = Anti-Corruption Layer
- `CL/SK` = Customer/Supplier mit Shared Kernel (gemeinsame IDs)
- `OHS` = Open Host Service (standardisiertes Interface)
- `►` = Abhängigkeitsrichtung (Downstream → Upstream)
---
### Beziehungen im Detail
#### 1. ZNS (extern) → `actor-context`
**Pattern:** Upstream/Downstream mit **Anti-Corruption Layer (ACL)**
| Eigenschaft | Beschreibung |
|-------------------|--------------------------------------------------------------------------------------------------------------------------------------------|
| Richtung | ZNS ist Upstream (Datenquelle), `actor-context` ist Downstream |
| Protokoll | Datei-Import (A-Satz / B-Satz, proprietäres Format) |
| ACL-Aufgabe | Übersetzung von ZNS-Datenformaten in interne Domänenmodelle |
| Offline-Verhalten | ZNS-Daten werden lokal gecacht; Import läuft asynchron |
| Kritische Regel | `Satznummer` ist primärer Schlüssel; `Lebensnummer` und `Kopfnummer` sind **nicht** als DB-Schlüssel geeignet (ZNS-Inkonsistenzen bekannt) |
**ACL-Verantwortlichkeiten:**
- Normalisierung inkonsistenter Felder (z.B. Farbe `"Braun"` vs. `"Brauner"`)
- Generierung interner IDs für ausländische Pferde ohne UELN
- Validierung und Ablehnung korrupter ZNS-Datensätze mit Protokollierung
---
#### 2. `actor-context` → `registration-context`
**Pattern:** Customer/Supplier mit **Shared Kernel (gemeinsame IDs)**
| Eigenschaft | Beschreibung |
|-------------------|--------------------------------------------------------------------------------|
| Richtung | `actor-context` ist Upstream (Stammdaten-Lieferant) |
| Shared Kernel | `ReiterId` (Satznummer), `PferdId` (Satznummer) als gemeinsame Referenz-IDs |
| Kommunikation | Synchron: Lookup bei Nennungs-Erstellung; Asynchron: Sperrlisten-Updates |
| Offline-Verhalten | `registration-context` hält lokale Kopie der benötigten Akteur-Daten |
| Kritische Regel | `registration-context` darf Akteur-Daten **nicht** direkt mutieren (nur lesen) |
---
#### 3. `event-management-context` → `registration-context`
**Pattern:** Customer/Supplier mit **Shared Kernel (gemeinsame IDs)**
| Eigenschaft | Beschreibung |
|-----------------|---------------------------------------------------------------------|
| Richtung | `event-management-context` ist Upstream (Turnier-/Bewerbs-Struktur) |
| Shared Kernel | `TurnierId`, `BewerbId` als gemeinsame Referenz-IDs |
| Kommunikation | Synchron: Bewerbs-Lookup bei Nennungs-Erstellung |
| Kritische Regel | Nennungen können nur für existierende Bewerbe erstellt werden |
---
#### 4. `registration-context` → `competition-context`
**Pattern:** Upstream/Downstream via **Domain Events**
| Eigenschaft | Beschreibung |
|--------------------|-----------------------------------------------------------------------------|
| Richtung | `registration-context` ist Upstream (Ereignis-Quelle) |
| Events | `NennungErstelltEvent`, `NennungStorniertEvent`, `NennungTransferiertEvent` |
| Kommunikation | Asynchron (Event Bus / lokale Event Queue) |
| Aufgabe Downstream | `competition-context` baut Startlisten aus Nennungs-Events auf |
| Offline-Verhalten | Events werden lokal persistiert und bei Verbindung synchronisiert |
---
#### 5. `registration-context` → `billing-context`
**Pattern:** Upstream/Downstream via **Domain Events** mit ACL
| Eigenschaft | Beschreibung |
|-----------------|-----------------------------------------------------------------------------------------------------------|
| Richtung | `registration-context` ist Upstream (Gebühren-Auslöser) |
| Events | `NennungErstelltEvent` (löst Nenngeld aus), `NennungStorniertEvent` (Gutschrift), `GebührenVerzichtEvent` |
| ACL-Aufgabe | Übersetzung von Nennungs-Events in Gebühren-Buchungen |
| Kritische Regel | Sportförderbeitrag und Tierwohl-Euro fallen **pro Start** an (nicht pro Nennung) |
---
#### 6. `competition-context` → `billing-context`
**Pattern:** Upstream/Downstream via **Domain Events** mit ACL
| Eigenschaft | Beschreibung |
|-------------|------------------------------------------------------------------|
| Richtung | `competition-context` ist Upstream |
| Events | `StartErfolgreich` (löst Sportförderbeitrag + Tierwohl-Euro aus) |
| ACL-Aufgabe | Übersetzung von Start-Events in Gebühren-Buchungen |
---
#### 7. Keycloak → alle Contexts
**Pattern:** Upstream/Downstream, **Conformist** (alle Contexts passen sich Keycloak an)
| Eigenschaft | Beschreibung |
|-------------------|-----------------------------------------------------------------------------------|
| Richtung | Keycloak ist Upstream (Identity Provider) |
| Protokoll | OIDC / JWT-Token |
| Kommunikation | Synchron: Token-Validierung bei jedem Request |
| Offline-Verhalten | Token-Caching mit konfigurierbarer TTL; Offline-Modus mit eingeschränkten Rechten |
---
### Anti-Corruption Layer (ACL) — Implementierungsrichtlinien
Jeder ACL wird als **eigenständiges Modul** innerhalb des Downstream-Contexts implementiert:
```
actor-context/
└── infrastructure/
└── zns/
├── ZnsImportService.kt # Orchestrierung
├── ZnsAkteurMapper.kt # Übersetzung ZNS → Dom*
├── ZnsValidationFilter.kt # Ablehnung korrupter Daten
└── ZnsImportProtokoll.kt # Audit-Log aller Imports
```
**Prinzipien:**
1. Der ACL übersetzt **immer** in die interne Ubiquitous Language — niemals umgekehrt
2. Fehlerhafte externe Daten werden **protokolliert und übersprungen** (kein Systemabsturz)
3. Der ACL ist der einzige Ort, der das externe Datenformat kennt
---
### Offline-First Integration
Da die Anwendung als Desktop-App (Offline-First) betrieben wird, gelten folgende Regeln:
| Szenario | Verhalten |
|-----------------------------|------------------------------------------------------------|
| ZNS nicht erreichbar | Lokaler Cache wird verwendet; Import-Status wird angezeigt |
| Nennungs-Erstellung offline | Lokal gespeichert; Events werden bei Sync übertragen |
| Keycloak nicht erreichbar | Gecachter Token wird verwendet (TTL-basiert) |
| Konflikt bei Sync | Optimistic Locking (409) + manuelle Auflösung (→ ADR-0013) |
---
## Konsequenzen
### Positive
- **Explizite Abhängigkeiten:** Alle Context-Beziehungen sind dokumentiert und nachvollziehbar
- **Schutz der Kern-Domäne:** `registration-context` ist durch ACLs von externen Systemen isoliert
- **Offline-Fähigkeit:** Jede Integration ist auf Offline-Betrieb ausgelegt
- **Erweiterbarkeit:** `series-context` kann in Phase 2+ als reiner Downstream-Consumer hinzugefügt werden
### Negative
- **Komplexität:** ACLs und Event-Übersetzungen erhöhen den initialen Implementierungsaufwand
- **Eventual Consistency:** Zwischen `registration-context` und `competition-context` gibt es keine sofortige Konsistenz
### Neutral
- Die Context Map ist ein **lebendes Dokument** und wird mit jeder neuen Integration aktualisiert
## Betrachtete Alternativen
### Direkte Context-zu-Context-Aufrufe (abgelehnt)
Direkte synchrone Aufrufe zwischen Contexts würden enge Kopplung erzeugen und die Offline-Fähigkeit untergraben.
### Shared Database (abgelehnt)
Eine gemeinsame Datenbank für alle Contexts würde die Context-Grenzen aufweichen und die unabhängige Entwicklung
verhindern.
## Referenzen
- [ADR-0014: Bounded Context Mapping](0014-bounded-context-mapping-de.md)
- [ADR-0004: Event-Driven Communication](0004-event-driven-communication-de.md)
- [ADR-0006: Authentication & Authorization (Keycloak)](0006-authentication-authorization-keycloak-de.md)
- [ADR-0013: Tech Stack Stabilization](0013-tech-stack-stabilization-2026.md)
- [Ubiquitous Language](../../03_Domain/01_Glossary/Ubiquitous_Language.md)
- [MASTER_ROADMAP](../MASTER_ROADMAP.md)
- Vaughn Vernon: „Implementing Domain-Driven Design", Kapitel 3 (Context Maps)
- ÖTO 2026, ZNS-Schnittstellen-Spezifikation (A-Satz / B-Satz)
@@ -0,0 +1,561 @@
---
type: ADR
id: ADR-0016
status: ACTIVE
owner: Lead Architect
last_update: 2026-03-24
---
# ADR-0016: API-Design & Anti-Corruption Layer (ACL)
## Status
Akzeptiert
## Kontext
Die 6 Bounded Contexts (ADR-0014) kommunizieren über definierte Schnittstellen (ADR-0015).
Dieses ADR konkretisiert:
1. **Welche Daten** über Kontextgrenzen fließen (DTOs / Query-Objekte)
2. **Wie** die ACL-Schicht technisch implementiert wird (Ports & Adapters)
3. **Welche REST-Endpunkte** die P1-Contexts nach außen exponieren
4. **Welche Domain Events** asynchron publiziert werden
Grundprinzip: **Kein Context kennt die internen Modelle eines anderen Context.**
Jeder Context übersetzt eingehende Daten in seine eigene Ubiquitous Language.
---
## Entscheidung
### 1. Architektur-Muster: Ports & Adapters (Hexagonal)
Jeder Context implementiert:
```
┌─────────────────────────────────────────────────────┐
│ [Context X] │
│ │
│ ┌──────────┐ ┌──────────────┐ ┌───────────┐ │
│ │ Inbound │ │ Domain │ │ Outbound │ │
│ │ Port │───▶│ Model │───▶│ Port │ │
│ │(REST/UI) │ │ (Aggregate) │ │(ACL/Event)│ │
│ └──────────┘ └──────────────┘ └───────────┘ │
│ ▲ │ │
│ │DTO DTO │ │
└───────┼────────────────────────────────────┼─────────┘
│ │
[Client / [Anderer
Frontend] Context]
```
**Regel:** DTOs sind flache, serialisierbare Datenstrukturen ohne Domänen-Logik.
Domain-Objekte verlassen den Context **niemals**.
---
### 2. Schnittstellen-Katalog: P1-Contexts
#### 2.1 `actor-context` → Inbound REST API
**Base-URL:** `/api/v1/actors`
| Methode | Pfad | Beschreibung | Response-DTO |
|---------|--------------------------------|------------------------------------|--------------------------|
| GET | `/reiter/{satznummer}` | Reiter per Satznummer laden | `ReiterDto` |
| GET | `/reiter/search?name=&verein=` | Reiter suchen (für Nennungs-Maske) | `List<ReiterSummaryDto>` |
| GET | `/pferde/{lebensnummer}` | Pferd per Lebensnummer laden | `PferdDto` |
| GET | `/pferde/search?name=&reiter=` | Pferde suchen (für Nennungs-Maske) | `List<PferdSummaryDto>` |
| GET | `/funktionaere/{id}` | Funktionär laden | `FunktionaerDto` |
| GET | `/vereine/{vereinsnummer}` | Verein laden | `VereinDto` |
| POST | `/reiter` | Reiter anlegen (TBA-Workflow) | `ReiterDto` |
| PUT | `/reiter/{satznummer}` | Reiter aktualisieren | `ReiterDto` |
**DTOs (actor-context → outbound):**
```kotlin
// Vollständiges Reiter-Objekt (für Detail-Ansicht)
data class ReiterDto(
val satznummer: String, // OEPS-Satznummer (eindeutig)
val vorname: String,
val nachname: String,
val geburtsdatum: LocalDate,
val lizenzklasse: String, // "A", "B", "C", "AMATEUR" etc.
val vereinsnummer: String,
val vereinsname: String,
val startkarte: Boolean,
val znsId: String? // ZNS-Referenz (nullable, Offline-Fall)
)
// Kompaktes Objekt für Suchergebnisse / Dropdown
data class ReiterSummaryDto(
val satznummer: String,
val vollname: String, // "Nachname, Vorname"
val lizenzklasse: String,
val vereinsname: String
)
// Vollständiges Pferd-Objekt
data class PferdDto(
val lebensnummer: String, // FEI-Lebensnummer (eindeutig)
val name: String,
val kopfnummer: String?, // Turnier-Kopfnummer (optional)
val satznummer: String?, // OEPS-Satznummer des Besitzers
val rasse: String?,
val farbe: String?,
val geburtsjahr: Int?
)
data class PferdSummaryDto(
val lebensnummer: String,
val name: String,
val kopfnummer: String?
)
data class FunktionaerDto(
val id: String,
val vorname: String,
val nachname: String,
val rolle: String, // "RICHTER", "PARCOURSCHEF" etc.
val qualifikationen: List<String>
)
data class VereinDto(
val vereinsnummer: String,
val name: String,
val oepsNummer: String
)
```
---
#### 2.2 `event-management-context` → Inbound REST API
**Base-URL:** `/api/v1/events`
| Methode | Pfad | Beschreibung | Response-DTO |
|---------|-------------------------------------|-------------------------------------|---------------------------------|
| GET | `/veranstaltungen` | Alle Veranstaltungen (paginiert) | `Page<VeranstaltungSummaryDto>` |
| GET | `/veranstaltungen/{id}` | Veranstaltung mit Turnieren laden | `VeranstaltungDetailDto` |
| GET | `/turniere/{turniernummer}` | Turnier per OEPS-Nummer laden | `TurnierDto` |
| GET | `/turniere/{turniernummer}/bewerbe` | Bewerbe eines Turniers | `List<BewerbSummaryDto>` |
| POST | `/veranstaltungen` | Neue Veranstaltung anlegen | `VeranstaltungDto` |
| POST | `/veranstaltungen/{id}/turniere` | Turnier zu Veranstaltung hinzufügen | `TurnierDto` |
**DTOs (event-management-context → outbound):**
```kotlin
data class VeranstaltungSummaryDto(
val id: String,
val bezeichnung: String,
val datum: LocalDate,
val ort: String,
val veranstalterVereinsnummer: String,
val status: String // "GEPLANT", "AKTIV", "ABGESCHLOSSEN"
)
data class VeranstaltungDetailDto(
val id: String,
val bezeichnung: String,
val datum: LocalDate,
val ort: String,
val veranstalterVereinsnummer: String,
val status: String,
val turniere: List<TurnierSummaryDto>
)
data class TurnierDto(
val turniernummer: String, // OEPS-vergebene Nummer
val veranstaltungId: String, // Referenz auf interne Veranstaltung
val bezeichnung: String,
val datum: LocalDate,
val kategorie: String, // "LT", "RT", "BT", "ST" etc.
val sparten: List<String> // ["DRESSUR", "SPRINGEN"] etc.
)
data class TurnierSummaryDto(
val turniernummer: String,
val bezeichnung: String,
val datum: LocalDate,
val kategorie: String
)
// Wird vom registration-context konsumiert (ACL-Übersetzung)
data class BewerbSummaryDto(
val bewerbId: String,
val bewerbsnummer: String, // z.B. "1", "2A", "2B"
val bezeichnung: String,
val sparte: String,
val klasse: String,
val maxStarter: Int?
)
```
---
#### 2.3 `registration-context` → Inbound REST API
**Base-URL:** `/api/v1/registrations`
| Methode | Pfad | Beschreibung | Response-DTO |
|---------|-------------------------------------------------|-------------------------------------|---------------------------|
| GET | `/nennungen?turniernummer=&status=` | Nennungen filtern | `List<NennungSummaryDto>` |
| GET | `/nennungen/{nennungsId}` | Einzelne Nennung laden | `NennungDetailDto` |
| POST | `/nennungen` | Neue Nennung einreichen | `NennungDetailDto` |
| PUT | `/nennungen/{nennungsId}/status` | Status ändern (Storno, Bestätigung) | `NennungDetailDto` |
| POST | `/nennungen/{nennungsId}/transfer` | Nennung transferieren | `NennungsTransferDto` |
| GET | `/nennungen/{nennungsId}/transfer/{transferId}` | Transfer-Status abfragen | `NennungsTransferDto` |
**DTOs (registration-context):**
```kotlin
// Eingehend: Neue Nennung (Command)
data class NennungErstellenCommand(
val turniernummer: String,
val bewerbId: String,
val reiterSatznummer: String,
val pferdLebensnummer: String,
val startwunsch: String?, // "FRUEH", "SPAET", "EGAL"
val bemerkung: String?
)
// Ausgehend: Kompakte Nennung (für Listen)
data class NennungSummaryDto(
val nennungsId: String,
val turniernummer: String,
val bewerbBezeichnung: String,
val reiterName: String, // Denormalisiert für Performance
val pferdName: String, // Denormalisiert für Performance
val status: String, // "EINGEREICHT", "BESTAETIGT", "STORNIERT"
val eingereichtAm: LocalDateTime
)
// Ausgehend: Vollständige Nennung (für Detail-Ansicht)
data class NennungDetailDto(
val nennungsId: String,
val turniernummer: String,
val bewerbId: String,
val bewerbBezeichnung: String,
val reiterSatznummer: String,
val reiterName: String,
val pferdLebensnummer: String,
val pferdName: String,
val startwunsch: String?,
val status: String,
val bemerkung: String?,
val eingereichtAm: LocalDateTime,
val letzteAenderung: LocalDateTime
)
// Transfer-DTO
data class NennungsTransferDto(
val transferId: String,
val quellNennungsId: String,
val zielBewerbId: String,
val zielReiterSatznummer: String?, // null = gleicher Reiter
val zielPferdLebensnummer: String?, // null = gleiches Pferd
val status: String, // "BEANTRAGT", "GENEHMIGT", "ABGELEHNT"
val guthabenErhalten: Boolean
)
```
---
### 3. ACL-Implementierung: registration-context konsumiert actor-context
Der `registration-context` benötigt Reiter- und Pferd-Daten, darf aber **nicht** direkt auf
die Datenbank des `actor-context` zugreifen.
**ACL-Port (Interface im registration-context):**
```kotlin
// Port-Interface (Teil des registration-context Domain-Layers)
interface AktorReferenzPort {
fun ladeReiter(satznummer: String): ReiterReferenz?
fun ladePferd(lebensnummer: String): PferdReferenz?
fun validiereReiterPferdKombination(
reiterSatznummer: String,
pferdLebensnummer: String
): ValidiertesReiterPferdPaar?
}
// Interne Referenz-Objekte (registration-context eigene Sprache)
data class ReiterReferenz(
val satznummer: String,
val vollname: String,
val lizenzklasse: LizenzKlasseE, // Eigener Enum des registration-context
val vereinsnummer: String,
val istStartberechtigt: Boolean // Abgeleitetes Feld (Startkarte + Lizenz aktiv)
)
data class PferdReferenz(
val lebensnummer: String,
val name: String,
val kopfnummer: String?
)
data class ValidiertesReiterPferdPaar(
val reiter: ReiterReferenz,
val pferd: PferdReferenz,
val paarungGueltig: Boolean,
val warnungen: List<String> // z.B. "Pferd hat keine aktive Kopfnummer"
)
```
**ACL-Adapter (Infrastruktur-Layer, implementiert den Port):**
```kotlin
// Adapter übersetzt actor-context DTO → registration-context Referenz-Objekt
@Component
class AktorReferenzAdapter(
private val aktorClient: AktorContextClient // HTTP-Client oder direkte Bean
) : AktorReferenzPort {
override fun ladeReiter(satznummer: String): ReiterReferenz? {
val dto = aktorClient.getReiter(satznummer) ?: return null
// ACL-Übersetzung: ReiterDto → ReiterReferenz
return ReiterReferenz(
satznummer = dto.satznummer,
vollname = "${dto.nachname}, ${dto.vorname}",
lizenzklasse = LizenzKlasseE.valueOf(dto.lizenzklasse),
vereinsnummer = dto.vereinsnummer,
istStartberechtigt = dto.startkarte
)
}
override fun ladePferd(lebensnummer: String): PferdReferenz? {
val dto = aktorClient.getPferd(lebensnummer) ?: return null
return PferdReferenz(
lebensnummer = dto.lebensnummer,
name = dto.name,
kopfnummer = dto.kopfnummer
)
}
override fun validiereReiterPferdKombination(
reiterSatznummer: String,
pferdLebensnummer: String
): ValidiertesReiterPferdPaar? {
val reiter = ladeReiter(reiterSatznummer) ?: return null
val pferd = ladePferd(pferdLebensnummer) ?: return null
val warnungen = mutableListOf<String>()
if (pferd.kopfnummer == null) warnungen.add("Pferd hat keine aktive Kopfnummer")
if (!reiter.istStartberechtigt) warnungen.add("Reiter hat keine gültige Startkarte")
return ValidiertesReiterPferdPaar(reiter, pferd, warnungen.isEmpty(), warnungen)
}
}
```
---
### 4. ACL-Implementierung: registration-context konsumiert event-management-context
```kotlin
// Port-Interface
interface TurnierReferenzPort {
fun ladeTurnier(turniernummer: String): TurnierReferenz?
fun ladeBewerb(bewerbId: String): BewerbReferenz?
fun ladeBewerbeDesTurniers(turniernummer: String): List<BewerbReferenz>
}
// Interne Referenz-Objekte (registration-context Sprache)
data class TurnierReferenz(
val turniernummer: String,
val bezeichnung: String,
val datum: LocalDate,
val kategorie: TurnierkategorieE, // Eigener Enum
val istNennungMoeglich: Boolean // Abgeleitetes Feld (Datum + Status)
)
data class BewerbReferenz(
val bewerbId: String,
val bewerbsnummer: String,
val bezeichnung: String,
val sparte: SparteE, // Eigener Enum
val klasse: String,
val maxStarter: Int?
)
```
---
### 5. Domain Events (Asynchrone Kommunikation)
Folgende Events werden über den internen Event-Bus publiziert:
#### 5.1 `actor-context` publiziert
| Event | Payload (Schlüsselfelder) | Konsumenten |
|----------------------|--------------------------------------------|----------------------------------------------|
| `ReiterAktualisiert` | `satznummer`, `lizenzklasse`, `startkarte` | `registration-context` (Cache-Invalidierung) |
| `PferdAktualisiert` | `lebensnummer`, `kopfnummer` | `registration-context` (Cache-Invalidierung) |
| `ReiterGesperrt` | `satznummer`, `grund` | `registration-context` (Warn-Logik) |
#### 5.2 `registration-context` publiziert
| Event | Payload (Schlüsselfelder) | Konsumenten |
|-----------------------|------------------------------------------------------------------------------------|------------------------------------------|
| `NennungEingereicht` | `nennungsId`, `turniernummer`, `bewerbId`, `reiterSatznummer`, `pferdLebensnummer` | `billing-context`, `competition-context` |
| `NennungStorniert` | `nennungsId`, `turniernummer`, `grund` | `billing-context` |
| `NennungTransferiert` | `transferId`, `quellNennungsId`, `zielBewerbId` | `billing-context` |
#### 5.3 `event-management-context` publiziert
| Event | Payload (Schlüsselfelder) | Konsumenten |
|---------------------------|-------------------------------------|--------------------------------------------|
| `TurnierEroeffnet` | `turniernummer`, `datum`, `sparten` | `registration-context` (Nennungs-Freigabe) |
| `NennungsschlussErreicht` | `turniernummer`, `zeitpunkt` | `registration-context` (Sperr-Logik) |
| `TurnierAbgesagt` | `turniernummer`, `grund` | `registration-context`, `billing-context` |
**Event-Struktur (Basis):**
```kotlin
// Basis-Event (alle Domain Events erben davon)
abstract class DomainEvent(
val eventId: String = UUID.randomUUID().toString(),
val occurredAt: Instant = Instant.now(),
val contextSource: String // z.B. "actor-context"
)
// Beispiel: NennungEingereicht
data class NennungEingereichtEvent(
val nennungsId: String,
val turniernummer: String,
val bewerbId: String,
val reiterSatznummer: String,
val pferdLebensnummer: String,
val eingereichtVon: String // Benutzer-ID (identity-context Referenz)
) : DomainEvent(contextSource = "registration-context")
```
---
### 6. Offline-First: Lokale Referenz-Caches
Da die Desktop-App offline-fähig sein muss, cachen die ACL-Adapter Referenz-Daten lokal:
```
┌─────────────────────────────────────────────────────────┐
│ registration-context (Desktop) │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ AktorReferenzAdapter │ │
│ │ │ │
│ │ ladeReiter() ──▶ [Lokaler Cache (SQLite)] │ │
│ │ │ │ │
│ │ ▼ Cache-Miss │ │
│ │ [HTTP → actor-context] │ │
│ │ │ │ │
│ │ ▼ Offline │ │
│ │ [Fehler: ReiterNichtGefunden] │ │
│ └─────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
```
**Cache-Strategie:**
- **Reiter/Pferd-Daten:** Werden beim Turnier-Download vollständig gecacht (Bulk-Sync)
- **Cache-Invalidierung:** Via `ReiterAktualisiert`-Event (wenn online) oder manueller Sync
- **Offline-Fallback:** Gecachte Daten sind gültig; neue Reiter können nicht angelegt werden
---
### 7. ZNS-Schnittstelle (Externer Context)
Der `actor-context` implementiert eine ACL zum externen ZNS (Zentrales Nennungs-System):
```kotlin
// Port (actor-context → ZNS)
interface ZnsPort {
fun ladeReiterAusSatz(satznummer: String): ZnsReiterSatz?
fun ladePferdAusSatz(lebensnummer: String): ZnsPferdSatz?
fun synchronisiereStammdaten(turniernummer: String): ZnsSyncErgebnis
}
// ZNS A-Satz (Reiter-Stammdaten)
data class ZnsReiterSatz(
val satznummer: String,
val vorname: String,
val nachname: String,
val geburtsdatum: LocalDate,
val lizenzklasse: String, // ZNS-Kodierung → wird in LizenzKlasseE übersetzt
val vereinsNummer: String,
val startkarte: Boolean
)
// ZNS B-Satz (Pferd-Stammdaten)
data class ZnsPferdSatz(
val lebensnummer: String,
val name: String,
val satznummer: String?, // Besitzer-Satznummer
val rasse: String?,
val farbe: String?
)
```
---
## Konsequenzen
### Positiv
- **Klare Kontraktgrenzen:** Jeder Context hat explizite, versionierbare APIs
- **Unabhängige Deployments:** Contexts können unabhängig deployed werden
- **Testbarkeit:** ACL-Ports können einfach gemockt werden (Unit-Tests)
- **Offline-Fähigkeit:** Lokale Caches ermöglichen Offline-Betrieb ohne Architektur-Bruch
- **ZNS-Isolation:** Änderungen am ZNS-Format betreffen nur den `ZnsAdapter`
### Negativ / Risiken
- **Datenduplizierung:** Denormalisierte Felder in DTOs (z.B. `reiterName` in `NennungSummaryDto`)
- **Cache-Konsistenz:** Lokale Caches können veralten (akzeptiertes Risiko, Warn-Logik)
- **Initialer Aufwand:** ACL-Adapter müssen für jeden Context implementiert werden
### Neutral
- **DTO-Versionierung:** Bei Breaking Changes muss API-Version erhöht werden (`/api/v2/...`)
- **Event-Ordering:** Domain Events sind best-effort; kritische Operationen bleiben synchron
---
## Abgelehnte Alternativen
### Shared Domain Model
Alle Contexts teilen ein gemeinsames Domain-Modell (z.B. `DomReiter` überall).
**Abgelehnt:** Führt zu starker Kopplung; Änderungen an `DomReiter` brechen alle Contexts.
### GraphQL Federation
Einheitliches GraphQL-Schema über alle Contexts.
**Abgelehnt:** Zu komplex für MVP; REST + Domain Events reicht für die aktuelle Skalierung.
### Direkte Datenbank-Joins
`registration-context` liest direkt aus der `actor-context`-Datenbank.
**Abgelehnt:** Verletzt SCS-Prinzip; verhindert unabhängige Deployments und Skalierung.
---
## Implementierungs-Reihenfolge (P1-Priorität)
1. **`actor-context` REST API** (`/api/v1/actors`) – Basis für alle anderen Contexts
2. **`event-management-context` REST API** (`/api/v1/events`) – Turnier/Bewerb-Referenzen
3. **ACL-Adapter im `registration-context`** – `AktorReferenzAdapter` + `TurnierReferenzAdapter`
4. **`registration-context` REST API** (`/api/v1/registrations`) – Kern-Use-Cases
5. **Domain Events** – `NennungEingereicht` als erstes Event (für `billing-context`)
6. **Offline-Cache** – Bulk-Sync beim Turnier-Download
---
## Referenzen
- [ADR-0014: Bounded Context Mapping](0014-bounded-context-mapping-de.md)
- [ADR-0015: Context Map & Integration Patterns](0015-context-map-de.md)
- [ADR-0004: Event-Driven Communication](0004-event-driven-communication-de.md)
- [ADR-0001: Modular Architecture](0001-modular-architecture-de.md)
- [Ubiquitous Language](../../03_Domain/01_Glossary/Ubiquitous_Language.md)
- [MASTER_ROADMAP](../MASTER_ROADMAP.md)
- Vaughn Vernon: „Implementing Domain-Driven Design", Kapitel 13 (Integrating Bounded Contexts)
- ÖTO 2026, ZNS-Schnittstellen-Spezifikation (A-Satz / B-Satz)
+3
View File
@@ -14,5 +14,8 @@ Namensschema: ADR-XXX-title.md mit fortlaufender Nummerierung.
- ADR-003 Optimistic Locking (409) als Konfliktstrategie
- ADR-004 Freshness UI (Ampel)
- ADR-005 Core Domain & Feature Isolation
- ADR-0014 Bounded Context Mapping & Aggregate Roots
- ADR-0015 Context Map & Integration Patterns
- ADR-0016 API-Design & Anti-Corruption Layer (ACL)
Siehe Template: ADR-000-template.md.
@@ -46,11 +46,11 @@ Veranstalter (OEPS-Mitgliedsverein)
### A
| Begriff | Definition | ÖTO-Referenz |
|-------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------|
| **Abteilung** | **Kleinste Einheit für Nennungen, Startlisten und Ergebnisse.** Untereinheit eines Bewerbs mit eigenem Teilnehmerkreis (Lizenz, Pferdealter etc.) und eigener Platzierung/Siegerehrung. Erhält eine fortlaufende **Abteilungsnummer** (1, 2, ...) innerhalb des Bewerbs. Referenz auf Startliste/Ergebnisliste: `BW: 9 Abt: 1` bzw. `9-1`. Die ÖTO definiert sparten- und klassenabhängige Schwellenwerte, ab wievielen Startern eine Abteilung **verpflichtend** getrennt werden muss. Bei Überschreitung gibt das System eine **WARNUNG** (kein harter Fehler) – der TBA hat das letzte Wort (→ *Override-Event*). | ÖTO § 2 Abs. 7 |
| **Akteur** | Oberbegriff für alle Personen (Reiter, Richter, Funktionäre, Besitzer) und Organisationen (Vereine), die im System interagieren. | – |
| **Ausschreibung** | Das offizielle Dokument, das alle Bedingungen eines Turniers festlegt. Pflichtfelder gemäß ÖTO (A-Satz der ZNS-Schnittstelle). | ÖTO Ausschreibungs-Struktur |
| Begriff | Definition | ÖTO-Referenz |
|-------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------|
| **Abteilung** | **Kleinste Einheit für Nennungen, Startlisten und Ergebnisse.** Untereinheit eines Bewerbs mit eigenem Teilnehmerkreis (Lizenz, Pferdealter etc.) und eigener Platzierung/Siegerehrung. Erhält eine fortlaufende **Abteilungsnummer** (1, 2, ...) innerhalb des Bewerbs. Referenz auf Startliste/Ergebnisliste: `BW: 9 Abt: 1` bzw. `9-1`. Die ÖTO definiert sparten- und klassenabhängige Schwellenwerte, ab wievielen Startern eine Abteilung **verpflichtend** getrennt werden muss. Bei Überschreitung gibt das System eine **WARNUNG** (kein harter Fehler) – der TBA hat das letzte Wort (→ *Override-Event*). Vollständige Schwellenwert-Tabellen: → [`Abteilungs-Trennungs-Schwellenwerte.md`](../02_Reference/OETO_Regelwerk/Abteilungs-Trennungs-Schwellenwerte.md) | ÖTO § 2 Abs. 7, § 39 |
| **Akteur** | Oberbegriff für alle Personen (Reiter, Richter, Funktionäre, Besitzer) und Organisationen (Vereine), die im System interagieren. | – |
| **Ausschreibung** | Das offizielle Dokument, das alle Bedingungen eines Turniers festlegt. Pflichtfelder gemäß ÖTO (A-Satz der ZNS-Schnittstelle). | ÖTO Ausschreibungs-Struktur |
### B
@@ -0,0 +1,251 @@
---
type: Reference
status: ACTIVE
owner: ÖTO/FEI Rulebook Expert
last_update: 2026-03-24
sources:
- ÖTO 2026, A-Teil § 39 (Teilen von Bewerben)
- ÖTO 2026, B-Teil § 200 ff. (Springen / CSN)
- ÖTO 2026, B-Teil § 100 ff. (Dressur / CDN)
- ÖTO 2026, B-Teil § 300 ff. (Vielseitigkeit / CCN)
- ÖTO 2026, B-Teil § 803 (Caprilli)
- ÖTO 2026, B-Teil § 850 ff. (Reitertreffen / Fahren)
---
# Abteilungs-Trennungs-Schwellenwerte
📜 **[ÖTO/FEI Rulebook Expert]** | 24. März 2026
Dieses Dokument ist die **Single Source of Truth** für alle Regeln zur Pflicht- und Kann-Teilung
von Bewerben in Abteilungen. Es ergänzt die [Ubiquitous Language](../../01_Glossary/Ubiquitous_Language.md)
und dient als Grundlage für die Implementierung der Warn-Logik im `competition-context`.
> ⚠️ **System-Philosophie:** Das System gibt bei Überschreitung eines Schwellenwerts **niemals**
> einen harten Fehler, sondern immer nur eine **WARNUNG**. Der TBA hat das letzte Wort und kann
> per **Override-Event** bewusst abweichen (→ Ubiquitous Language: *Override-Event*).
---
## 1. Allgemeine Schwellenwerte (§ 39 A-Teil) – Alle Sparten
Diese Regeln gelten für **alle Turniere der Kategorien A\*, A, B\*, B und C** und für alle Sparten,
sofern die spartenspezifischen Bestimmungen (Abschnitt 2) keine strengeren Regeln vorsehen.
### 1.1 Pflicht-Teilung (MUSS)
| Prüfungstyp | Schwellenwert (Starter) | Abteilungs-Kriterium (Standard) | ÖTO-Referenz |
|------------------------------------------------|-------------------------|---------------------------------|--------------|
| Stil- und Springpferdeprüfungen | **> 30** | Lizenzstufen (sonst: Plätze) | § 39 Abs. 2 |
| Vielseitigkeitsprüfungen | **> 40** | Lizenzstufen (sonst: Plätze) | § 39 Abs. 2 |
| Übrige Springprüfungen (Standard, Spezial ...) | **> 80** | Lizenzstufen (sonst: Plätze) | § 39 Abs. 2 |
| Jede Abteilung nach Teilung | **> 80** | Erneute Teilung verpflichtend | § 39 Abs. 2 |
> **Hinweis:** „Übrige Springprüfungen" umfasst alle Springprüfungen, die **nicht** als Stil- oder
> Springpferdeprüfung ausgeschrieben sind (Standardspringprüfung, Zweiphasen, Punkte, Risiko etc.).
### 1.2 Kann-Teilung (KANN / DARF)
| Prüfungstyp | Schwellenwert (Starter) | Abteilungs-Kriterium (Standard) | ÖTO-Referenz |
|------------------|-------------------------|---------------------------------|--------------|
| Dressurprüfungen | **> 30** | Lizenzstufen (sonst: Plätze) | § 39 Abs. 2 |
### 1.3 Teilungs-Kriterien (Priorität)
Sofern in der Ausschreibung kein anderes Kriterium festgelegt ist, gilt folgende Priorität:
1. **Ausschreibungs-Kriterium** (Altersklasse, Pferdealter, Geschlecht etc.) – hat Vorrang
2. **Lizenzstufen** (Standard-Fallback)
3. **Plätze** (wenn Teilung nach Lizenzstufen nicht möglich)
### 1.4 Ausnahmen von der Pflicht-Teilung
| Ausnahme | ÖTO-Referenz |
|---------------------------------------------------------------------|--------------|
| Meisterschaftsbewerbe (Cups und Serien) | § 39 Abs. 4 |
| Bewerbe mit Geldpreisen > Doppeltes der Gebührenordnungs-Geldpreise | § 39 Abs. 2 |
### 1.5 Pflicht-Teilung nach Klasse (unabhängig von Starterzahl)
Diese Teilungen sind **strukturell verpflichtend** und gelten unabhängig von der Starterzahl:
| Prüfungstyp | Pflicht-Teilung | ÖTO-Referenz |
|------------------------------------------------------|-------------------------------------------------------|--------------|
| Dressur- und Springprüfungen Klassen **A und L** | Mindestens 2 Abteilungen; R1 in **eigener** Abteilung | § 39 Abs. 1 |
| Lizenzprüfungsaufgaben | Getrennt nach R2/RD2 und R3/RD3 | § 39 Abs. 1 |
| LM-Springen bei B- und C-Turnieren | Kann getrennt werden: R2/RS2 und R3/RS3/RS4 | § 39 Abs. 1 |
| Pferdeprüfungen (Dressur- und Springpferdeprüfungen) | Teilung nach **Pferdealter** (nicht nach Lizenzen) | § 39 Abs. 1 |
---
## 2. Spartenspezifische Schwellenwerte
### 2.1 Springen (CSN) – Sparte B II
#### 2.1.1 Stil- und Idealzeitspringprüfungen bis 95 cm (alle CSN-Kategorien)
Pflicht-Teilung in **getrennte Abteilungen** nach Lizenzstatus – unabhängig von der Starterzahl:
| Abteilung | Teilnehmerkreis |
|-----------|--------------------------|
| Abt. 1 | Reiter **ohne Lizenz** |
| Abt. 2 | Reiter mit Lizenz **R1** |
> **Referenz:** ÖTO B-Teil § 200 Abs. 5.3
#### 2.1.2 Springpferdeprüfungen – Pflicht-Teilung nach Pferdealter
| Höhe | Erlaubtes Pferdealter | Pflicht-Teilung | ÖTO-Referenz |
|--------------|-----------------------|------------------------------------------|---------------------|
| 95 – 110 cm | 4 – 6 jährig | **Ja:** 4-jährige / 5–6-jährige getrennt | B-Teil § 200 Abs. 6 |
| 115 – 130 cm | 5 – 6 jährig | Nein (keine Teilung erforderlich) | B-Teil § 200 Abs. 6 |
| 135 cm | 6 – 7 jährig | Nein (keine Teilung erforderlich) | B-Teil § 200 Abs. 6 |
#### 2.1.3 CSN-C-NEU – Zwingend 2 Abteilungen (strukturell, unabhängig von Starterzahl)
| Höhenbereich | Abt. 1 | Abt. 2 | ÖTO-Referenz |
|--------------|-----------------|------------------|--------------|
| bis 95 cm | **ohne Lizenz** | **mit Lizenz** | B-Teil § 231 |
| ab 100 cm | **R1** | **R2 und höher** | B-Teil § 231 |
---
### 2.2 Dressur (CDN) – Sparte B I
#### 2.2.1 Allgemeine Schwellenwerte
Für Dressurprüfungen gilt die **Kann-Teilung** aus § 39 Abs. 2 (> 30 Starter).
Es gibt **keine Pflicht-Teilung** allein aufgrund der Starterzahl bei Dressur
(außer den strukturellen Pflichten aus § 39 Abs. 1 für Klassen A/L).
#### 2.2.2 Dressurpferdeprüfungen – Pflicht-Teilung nach Pferdealter
| Klasse | Erlaubtes Pferdealter | Pflicht-Teilung | ÖTO-Referenz |
|--------|-----------------------|------------------------------------------|---------------------|
| A | 4 – 6 jährig | **Ja:** 4-jährige / 5–6-jährige getrennt | B-Teil § 100 Abs. 5 |
| L | 5 – 6 jährig | Nein (keine Teilung erforderlich) | B-Teil § 100 Abs. 5 |
| M | 6 – 7 jährig | Nein (keine Teilung erforderlich) | B-Teil § 100 Abs. 5 |
| S | 7 – 8 jährig | Nein (keine Teilung erforderlich) | B-Teil § 100 Abs. 5 |
---
### 2.3 Vielseitigkeit (CCN) – Sparte B III
#### 2.3.1 Allgemeine Schwellenwerte
Für Vielseitigkeitsprüfungen gilt die **Pflicht-Teilung** aus § 39 Abs. 2 (> 40 Starter).
#### 2.3.2 Klasse Welcome und 80 cm – Lizenz-Abteilung
| Klasse | Regel | ÖTO-Referenz |
|---------|---------------------------------------------------------------------------------------------|-----------------------|
| Welcome | Nur Reiter mit Reiterpass und **höchstens R1**. R2+ sind in **eigener Abteilung** zu werten | B-Teil § 301 Abs. 1.4 |
| 80 cm | R2+ startberechtigt, aber in **eigener Abteilung** zu werten, erhalten keine Ehrenpreise | B-Teil § 301 Abs. 1.4 |
#### 2.3.3 CCN-C-NEU Geländeritte/Geländepferdeprüfungen – Zwingend nach Lizenz
| Höhenbereich | Abt. 1 | Abt. 2 | Abt. 3 | ÖTO-Referenz |
|--------------|-----------------|------------------|------------------|----------------------|
| bis 80 cm | **ohne Lizenz** | **R1-Reiter** | **R2 und höher** | B-Teil § 300 (C-NEU) |
| ab 90 cm | **ohne Lizenz** | **R1 und höher** | – | B-Teil § 300 (C-NEU) |
---
### 2.4 Caprilli-Prüfungen (§ 803) – Alle Sparten
Pflicht-Teilung in **mindestens 2 Abteilungen** – unabhängig von der Starterzahl:
| Abteilung | Teilnehmerkreis |
|-----------|-----------------------|
| Abt. 1 | **lizenzfrei** |
| Abt. 2 | **RD1, R1 und höher** |
> **Referenz:** ÖTO B-Teil § 803 Abs. 2
---
### 2.5 Fahren (CAN) – Reitertreffen
Bei Fahrertreffen (§ 850 ff.) gilt:
| Regel | ÖTO-Referenz |
|-------------------------------------------------------------------------------------|---------------------|
| Fahrer mit Lizenz **höher als F1** werden in einer **separaten Abteilung** gewertet | B-Teil § 850 Abs. 9 |
---
## 3. Zusammenfassung: Schwellenwert-Matrix
| Sparte | Prüfungstyp | Schwellenwert | Typ | Kriterium |
|--------|-------------------------------|---------------|------|-----------------------|
| Alle | Stil- / Springpferdeprüfungen | > 30 Starter | MUSS | Lizenz / Plätze |
| Alle | Vielseitigkeitsprüfungen | > 40 Starter | MUSS | Lizenz / Plätze |
| Alle | Übrige Springprüfungen | > 80 Starter | MUSS | Lizenz / Plätze |
| Alle | Jede Abteilung nach Teilung | > 80 Starter | MUSS | Erneute Teilung |
| CDN | Dressurprüfungen | > 30 Starter | KANN | Lizenz / Plätze |
| CDN | Dressurpferdeprüfung Klasse A | strukturell | MUSS | Pferdealter (4 / 5–6) |
| CSN | Stil-/Idealzeit bis 95 cm | strukturell | MUSS | Lizenz (ohne / R1) |
| CSN | Springpferdeprüfung 95–110 cm | strukturell | MUSS | Pferdealter (4 / 5–6) |
| CSN | C-NEU bis 95 cm | strukturell | MUSS | Lizenz (ohne / mit) |
| CSN | C-NEU ab 100 cm | strukturell | MUSS | Lizenz (R1 / R2+) |
| CCN | C-NEU Gelände bis 80 cm | strukturell | MUSS | Lizenz (3 Abt.) |
| CCN | C-NEU Gelände ab 90 cm | strukturell | MUSS | Lizenz (2 Abt.) |
| CCN | Welcome / 80 cm | strukturell | MUSS | R2+ eigene Abt. |
| Alle | Caprilli (§ 803) | strukturell | MUSS | Lizenz (frei / RD1+) |
| CAN | Fahrertreffen | strukturell | MUSS | F1+ eigene Abt. |
---
## 4. Implementierungs-Hinweise für den `competition-context`
### 4.1 Warn-Logik (keine harten Fehler!)
Das System soll folgende **Warnungen** ausgeben (nie harte Fehler):
```
WARN_ABTEILUNG_PFLICHT_TEILUNG_UEBERSCHRITTEN
→ Bewerb [BW-Nr], Prüfungstyp [Typ], Starter: [N] > Schwellenwert [S]
→ Empfehlung: Teilung in Abteilungen nach [Kriterium]
→ Override möglich (TBA-Entscheidung erforderlich)
WARN_ABTEILUNG_STRUKTURELL_NICHT_GETRENNT
→ Bewerb [BW-Nr], Sparte [Sparte], Klasse/Höhe [X]
→ Strukturelle Pflicht-Teilung fehlt (z.B. CSN-C-NEU ohne Lizenz-Trennung)
→ Override möglich (TBA-Entscheidung erforderlich)
```
### 4.2 Konfigurierbare Parameter
Die Schwellenwerte sind **nicht hard-coded**, sondern als konfigurierbare Werte zu hinterlegen:
| Parameter | Standardwert | Quelle |
|----------------------------------------|--------------|-------------|
| `threshold.stil_springpferd.pflicht` | 30 | § 39 Abs. 2 |
| `threshold.vielseitigkeit.pflicht` | 40 | § 39 Abs. 2 |
| `threshold.springen_uebrig.pflicht` | 80 | § 39 Abs. 2 |
| `threshold.dressur.kann` | 30 | § 39 Abs. 2 |
| `threshold.abteilung.max_nach_teilung` | 80 | § 39 Abs. 2 |
### 4.3 Beziehung zu Domain-Modellen
| Domain-Modell | Relevanz |
|------------------|---------------------------------------------------------------|
| `DomAbteilung` | Trägt `abteilungsNummer`, `teilnehmerkreis`, `starterAnzahl` |
| `DomBewerb` | Kennt `prüfungsTyp`, `sparte`, `turnierkategorie`, `höhe` |
| `DomNennung` | Referenziert `DomAbteilung` als kleinste Einheit |
| `Override-Event` | Wird gespeichert, wenn TBA eine Warn-Überschreibung bestätigt |
---
## 5. Offene Fragen / Klärungsbedarf
| # | Frage | Status |
|---|-----------------------------------------------------------------------------------------------------------------------|----------|
| 1 | Gelten die Schwellenwerte aus § 39 auch für **Reitertreffen** (nicht nur Turniere)? | 🔍 Offen |
| 2 | Wie verhält sich die Pflicht-Teilung bei **kombinierten Turnieren** (CDN + CSN am selben Wochenende, § 4)? | 🔍 Offen |
| 3 | Gibt es für **Voltigieren (CVN)** eigene Abteilungs-Trennungsregeln? (B-Teil § 400 ff. nicht vollständig ausgewertet) | 🔍 Offen |
| 4 | Gibt es für **Fahren (CAN)** eigene Starter-Schwellenwerte jenseits der Reitertreffen-Regel? | 🔍 Offen |
---
*Erstellt: 2026-03-24 | Autor: ÖTO/FEI Rulebook Expert (Junie)*
*Basiert auf: ÖTO 2026 A-Teil § 39, B-Teil §§ 100, 200, 231, 300, 803, 850*
@@ -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, 4–6-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 95–110 cm / Dressurpferdeprüfung Klasse A
**Regel:** 4-jährige in eigener Abteilung, getrennt von 5–6-jährigen.
| Prüfung | Bedingung für `WARN_STRUKTURELLE_TEILUNG_FEHLT` |
|-------------------------------|-------------------------------------------------------------------------|
| Abt. „4-jährige" vorhanden? | Keine Abteilung mit `teilnehmerkreisBeschreibung` ~ „4-jährig" |
| Abt. „5–6-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 / 5–6-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)*
@@ -0,0 +1,75 @@
---
type: SessionLog
date: 2026-03-24
agents:
- Lead Architect
- Curator
status: ABGESCHLOSSEN
---
# Session Log: ADR-0014 & ADR-0015 — Bounded Context Mapping & Context Map
🏗️ **[Lead Architect]** | 🧹 **[Curator]** | 24. März 2026
---
## Ziel der Session
ADRs für Bounded Context Mapping und Context Map vervollständigen (PHASE 4, Task 1).
---
## Ergebnisse
### ADR-0014: Bounded Context Mapping (SCS-Architektur)
**Datei:** `docs/01_Architecture/adr/0014-bounded-context-mapping-de.md`
Dokumentiert die 6 Bounded Contexts als Self-Contained Systems:
| Context | Domänen-Typ | Priorität |
|----------------------------|-------------------|-----------|
| `registration-context` | Core Domain | P1 |
| `actor-context` | Supporting Domain | P1 |
| `competition-context` | Supporting Domain | P2 |
| `event-management-context` | Supporting Domain | P2 |
| `billing-context` | Generic Domain | P3 |
| `identity-context` | Generic Domain | P3 |
Für jeden Context dokumentiert: Aggregate Roots, Ubiquitous Language (Auswahl), Kern-Invarianten.
Begründung für Ablehnung der alten technischen Modulaufteilung (`masterdata`, `members`, `horses`, `events`).
---
### ADR-0015: Context Map & Integration Patterns
**Datei:** `docs/01_Architecture/adr/0015-context-map-de.md`
Dokumentiert 7 Context-Beziehungen mit ASCII-Diagramm und Detailtabellen:
| Beziehung | Pattern |
|-----------------------------------------------------|-----------------------------------|
| ZNS → `actor-context` | Upstream/Downstream + ACL |
| `actor-context` → `registration-context` | Customer/Supplier + Shared Kernel |
| `event-management-context` → `registration-context` | Customer/Supplier + Shared Kernel |
| `registration-context` → `competition-context` | Domain Events (asynchron) |
| `registration-context` → `billing-context` | Domain Events + ACL |
| `competition-context` → `billing-context` | Domain Events + ACL |
| Keycloak → alle Contexts | Conformist (OIDC/JWT) |
Enthält: ACL-Implementierungsrichtlinien, Offline-First-Verhalten pro Szenario.
---
## MASTER_ROADMAP Updates
- `[x]` **ADRs vervollständigen** (PHASE 4, Lead Architect) — abgeschlossen
- ADR-Tabelle: Einträge #8 (ADR-0014) und #9 (ADR-0015) hinzugefügt
---
## Offene Punkte (nächste Session)
- `[ ]` **API-Design:** Schnittstellen zwischen den Contexts definieren (Anti-Corruption Layer) — Lead Architect
- `[ ]` **`actor-context`:** `DomPferd`, `DomFunktionär`, `DomVerein` implementieren — Backend Developer
@@ -0,0 +1,92 @@
---
type: SessionLog
date: 2026-03-24
agent: Lead Architect
phase: PHASE 4
task: API-Design & Anti-Corruption Layer
status: ABGESCHLOSSEN
---
# 🧹 [Curator] Session Log – API-Design & ACL
**Datum:** 24. März 2026
**Agent:** 🏗️ Lead Architect
**Phase:** PHASE 4 – MVP-Implementierung
**Aufgabe:** API-Design: Schnittstellen zwischen den Contexts definieren (Anti-Corruption Layer)
---
## Ergebnisse
### Erstellt
- **`ADR-0016`** (`docs/01_Architecture/adr/0016-api-design-acl-de.md`)
- Architektur-Muster: Ports & Adapters (Hexagonal) für alle Contexts
- REST-API-Katalog für alle 3 P1-Contexts (`actor`, `event-management`, `registration`)
- Vollständige DTO-Definitionen (Inbound Commands + Outbound DTOs)
- ACL-Port-Interfaces: `AktorReferenzPort`, `TurnierReferenzPort`
- ACL-Adapter-Implementierung mit Übersetzungslogik (DTO → Referenz-Objekt)
- Domain Events-Katalog: 8 Events über 3 Contexts
- ZNS-Schnittstelle: `ZnsPort` mit A-Satz / B-Satz
- Offline-First Cache-Strategie für ACL-Adapter
- Implementierungs-Reihenfolge (P1-Priorität)
### Aktualisiert
- **`MASTER_ROADMAP.md`**: Task `[x]` abgehakt, ADR #10 in Tabelle eingetragen
- **`docs/01_Architecture/adr/README.md`**: ADR-0016 eingetragen
---
## Kern-Entscheidungen (ADR-0016)
| Bereich | Entscheidung |
|---------------|--------------------------------------------------|
| Muster | Ports & Adapters (Hexagonal Architecture) |
| Kommunikation | REST (synchron) + Domain Events (asynchron) |
| ACL-Regel | Domain-Objekte verlassen den Context **niemals** |
| DTOs | Flach, serialisierbar, ohne Domänen-Logik |
| Offline | Lokale SQLite-Caches in ACL-Adaptern |
| ZNS | Eigener `ZnsPort` im `actor-context` (isoliert) |
---
## REST-API Übersicht
| Context | Base-URL | Endpunkte |
|----------------------------|-------------------------|------------------------------------------|
| `actor-context` | `/api/v1/actors` | 8 (Reiter, Pferde, Funktionäre, Vereine) |
| `event-management-context` | `/api/v1/events` | 6 (Veranstaltungen, Turniere, Bewerbe) |
| `registration-context` | `/api/v1/registrations` | 6 (Nennungen, Transfer) |
---
## Domain Events Übersicht
| Context | Events |
|----------------------------|------------------------------------------------------------------|
| `actor-context` | `ReiterAktualisiert`, `PferdAktualisiert`, `ReiterGesperrt` |
| `registration-context` | `NennungEingereicht`, `NennungStorniert`, `NennungTransferiert` |
| `event-management-context` | `TurnierEroeffnet`, `NennungsschlussErreicht`, `TurnierAbgesagt` |
---
## Status PHASE 4: Lead Architect Tasks
- [x] ADRs vervollständigen (Bounded Context Mapping + Context Map) → ADR-0014, ADR-0015
- [x] API-Design & ACL definieren → ADR-0016
**Lead Architect Tasks PHASE 4: ✅ ABGESCHLOSSEN**
---
## Nächste Schritte (Backend Developer)
Gemäß Implementierungs-Reihenfolge aus ADR-0016:
1. `actor-context` REST API (`/api/v1/actors`) implementieren
2. `event-management-context` REST API (`/api/v1/events`) implementieren
3. ACL-Adapter im `registration-context` implementieren
4. `registration-context` REST API (`/api/v1/registrations`) implementieren
5. Domain Event `NennungEingereicht` als erstes Event
6. Offline-Cache (Bulk-Sync beim Turnier-Download)
@@ -0,0 +1,70 @@
---
type: Session Log
date: 2026-03-24
agent: Backend Developer
status: ABGESCHLOSSEN
roadmap_phase: PHASE 4 – MVP-Implementierung
---
# Session Log: actor-context Domain-Modelle
👷 **[Backend Developer]** | 24. März 2026
## Ziel
Domain-Modelle für `DomPferd`, `DomFunktionär` und `DomVerein` im `actor-context` implementieren (PHASE 4,
MASTER_ROADMAP).
---
## Ergebnis
### ✅ DomPferd
- Bereits vollständig und ÖTO-konform im Modul `backend/services/horses/horses-domain` vorhanden.
- Keine Änderungen notwendig.
### ✅ DomFunktionaer (neu)
- **Datei:**
`backend/services/officials/officials-domain/src/main/kotlin/at/mocode/officials/domain/model/DomFunktionaer.kt`
- Aggregate Root des `officials`-Bounded Context.
- Felder: `richterNummer` (ZNS RICHT01.dat), `vorname`, `nachname`, `geburtsdatum`, `rollen`, `richterQualifikation`,
`qualifiziertFuerSparten`, `email`, `telefon`, `vereinsNummer`, `istAktiv`, `bemerkungen`, `datenQuelle`.
- Domain-Methoden: `getDisplayName()`, `istRichterFuerSparte()`, `istTba()`, `validateFuerTurniereinsatz()` (Warn-Logik,
kein harter Fehler).
### ✅ DomVerein (neu)
- **Datei:** `backend/services/clubs/clubs-domain/src/main/kotlin/at/mocode/clubs/domain/model/DomVerein.kt`
- Aggregate Root des `clubs`-Bounded Context.
- Felder: `vereinsNummer` (ZNS VEREIN01.dat, 4-stellig), `name`, `kurzname`, `bundesland`, `ort`, `plz`, `strasse`,
`email`, `telefon`, `website`, `oepsRegionNummer`, `istVeranstalter`, `istAktiv`, `bemerkungen`, `datenQuelle`.
- Domain-Methoden: `getDisplayName()`, `getDisplayNameWithNummer()`, `hasCompleteAddress()`,
`validateFuerVeranstaltung()` (Warn-Logik).
### ✅ Neue Enums in core-domain
- **Datei:** `core/core-domain/src/commonMain/kotlin/at/mocode/core/domain/model/Enums.kt`
- `FunktionaerRolleE`: TBA, RICHTER, PARCOURSBAUER, STRECKENDESIGNER, TIERARZT, STEWARD, STARTER, ZEITNEHMER,
PROTOKOLLFUEHRER, SONSTIGE
- `RichterQualifikationE`: GA, G3, G2, G1, INTERNATIONAL, SONSTIGE (gemäß ZNS RICHT01.dat)
---
## Design-Entscheidungen
| Entscheidung | Begründung |
|------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `DomFunktionaer` statt Erweiterung von `DomOfficial` | `DomOfficial` war ein minimaler ZNS-Import-Stub. `DomFunktionaer` ist das vollständige ÖTO-konforme Domain-Modell mit Rollen und Sparten-Qualifikation. |
| `DomVerein` statt Erweiterung von `DomClub` | Analog: `DomClub` war ein ZNS-Import-Stub. `DomVerein` enthält Adresse, Kontakt, Veranstalter-Flag und OEPS-Region. |
| Warn-Logik statt harter Fehler | Konsistent mit ADR-0016 und Override-Event-Prinzip: `validateFuerTurniereinsatz()` und `validateFuerVeranstaltung()` geben Warnungen zurück, kein Exception-Throwing. |
| `rollen: Set<FunktionaerRolleE>` | Eine Person kann mehrere Rollen haben (z.B. TBA + Richter). Set verhindert Duplikate. |
---
## Nächste Schritte (PHASE 4)
- [ ] `registration-context`: `DomBewerb`, `DomAbteilung`, `DomStartliste` implementieren.
- [ ] `event-management-context`: `DomVeranstaltung`, `DomTurnier`, `DomAusschreibung` implementieren.
- [ ] Persistenz: Repository-Interfaces und DB-Migrationen (Flyway/Liquibase).
@@ -67,10 +67,15 @@ die ÖTO-konforme Terminologie und die Erstellung der offiziellen Ubiquitous Lan
## Erstellte / Aktualisierte Dokumente
| Dokument | Aktion | Beschreibung |
|-----------------------------------------------------|----------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `docs/03_Domain/01_Glossary/Ubiquitous_Language.md` | ✅ NEU ERSTELLT | Offizielle Domänen-Terminologie mit ÖTO-Referenzen, Bounded Context Zuordnung, Hierarchie-Diagramm, Reglement-Hinweis für Cups/Serien/Meisterschaften und MVP-Scope-Tabelle |
| `docs/03_Domain/01_Glossary/Ubiquitous_Language.md` | ✅ KORRIGIERT | Drei Korrekturen eingearbeitet (📜 ÖTO/FEI Rulebook Expert Review): **Abteilung** als kleinste Einheit für Nennungen/Startlisten/Ergebnisse mit Abteilungsnummer und Referenzformat `BW:9 Abt:1`; **Bewerb** korrigiert (nicht mehr „kleinste Einheit"); **Kopfnummer** als nicht eindeutige ID markiert; **Lebensnummer** mit Hinweis auf inkonsistente ZNS-Daten ergänzt |
| Dokument | Aktion | Beschreibung |
|----------------------------------------------------------------------|----------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `docs/03_Domain/01_Glossary/Ubiquitous_Language.md` | ✅ NEU ERSTELLT | Offizielle Domänen-Terminologie mit ÖTO-Referenzen, Bounded Context Zuordnung, Hierarchie-Diagramm, Reglement-Hinweis für Cups/Serien/Meisterschaften und MVP-Scope-Tabelle |
| `docs/03_Domain/01_Glossary/Ubiquitous_Language.md` | ✅ KORRIGIERT | Drei Korrekturen eingearbeitet (📜 ÖTO/FEI Rulebook Expert Review): **Abteilung** als kleinste Einheit für Nennungen/Startlisten/Ergebnisse mit Abteilungsnummer und Referenzformat `BW:9 Abt:1`; **Bewerb** korrigiert (nicht mehr „kleinste Einheit"); **Kopfnummer** als nicht eindeutige ID markiert; **Lebensnummer** mit Hinweis auf inkonsistente ZNS-Daten ergänzt |
| `core/core-domain/.../Enums.kt` | ✅ ERWEITERT | Neue Enums: `SparteE`, `TurnierkategorieE`, `VeranstaltungsTypE`, `LizenzKlasseE`, `NennungsStatusE`, `StartwunschE` – alle ÖTO-konform mit KDoc-Kommentaren |
| `backend/services/persons/persons-domain/.../DomReiter.kt` | ✅ NEU ERSTELLT | Reiter-Domänenmodell (actor-context): Satznummer als ZNS-Primärschlüssel, LizenzKlasse, Startkarte, Sparten-Lizenz, Gastreiter-Flag, `validateForNennung()` gibt nur Warnungen (kein harter Fehler) |
| `backend/services/entries/entries-domain/.../DomNennung.kt` | ✅ NEU ERSTELLT | Nennungs-Domänenmodell (registration-context): Referenz auf Abteilung (kleinste Einheit), Reiter, Pferd, Zahler; Nachnennung-Flag, Gebühren-Verzicht, Status-Lifecycle |
| `backend/services/entries/entries-domain/.../DomNennungsTransfer.kt` | ✅ NEU ERSTELLT | Transfer-Domänenmodell: explizites Audit-Trail (alter/neuer Reiter, altes/neues Pferd), Override-Event-Referenz, `isValid()` prüft dass mindestens Reiter oder Pferd getauscht wurde |
| `backend/services/entries/entries-domain/build.gradle.kts` | ✅ NEU ERSTELLT | Neues KMP-Modul nach Muster `horses-domain`; in `settings.gradle.kts` registriert; `compileKotlinJvm` erfolgreich |
---
@@ -87,11 +92,12 @@ die ÖTO-konforme Terminologie und die Erstellung der offiziellen Ubiquitous Lan
## Nächste Schritte (Empfehlung)
- [ ] 👷 **[Backend Developer]**: Kotlin Domain-Modelle für `registration-context` und `actor-context` definieren
- [ ] 🏗️ **[Lead Architect]**: MASTER_ROADMAP mit den 6 Bounded Contexts aktualisieren
- [ ] 🎨 **[Frontend Expert]**: KMP/Compose Desktop Projektstruktur aufsetzen
- [ ] 📜 **[ÖTO/FEI Rulebook Expert]**: Abteilungs-Trennungs-Schwellenwerte (sparten- und klassenabhängig) recherchieren
und dokumentieren
- [x] 👷 **[Backend Developer]**: Kotlin Domain-Modelle für `registration-context` und `actor-context` definieren
- [x] 🏗️ **[Lead Architect]**: MASTER_ROADMAP mit den 6 Bounded Contexts aktualisieren →
`docs/01_Architecture/MASTER_ROADMAP.md`
- [x] 🎨 **[Frontend Expert]**: KMP/Compose Desktop Projektstruktur aufsetzen
- [x] 📜 **[ÖTO/FEI Rulebook Expert]**: Abteilungs-Trennungs-Schwellenwerte (sparten- und klassenabhängig) recherchieren
und dokumentieren → `docs/03_Domain/02_Reference/OETO_Regelwerk/Abteilungs-Trennungs-Schwellenwerte.md`
---
@@ -0,0 +1,117 @@
---
date: 2026-03-24
type: Session Log
agents: Frontend Expert, Curator
status: COMPLETED
---
# Session Log – KMP/Compose Desktop Shell aufsetzen
🎨 **[Frontend Expert]** & 🧹 **[Curator]** | 24. März 2026
---
## Zusammenfassung der Session
Aufsetzen der dedizierten Desktop-Shell `meldestelle-desktop` als eigenständiges
JVM/Compose-Desktop-Modul gemäß Desktop-First-Strategie (ADR-0009) und Vision_03-Design-Baseline.
---
## Durchgeführte Aktivitäten
### 1. Neues Modul: `frontend/shells/meldestelle-desktop`
- Eigenständige Desktop-Shell, getrennt vom Web-Portal (`meldestelle-portal`)
- Reines JVM-Modul (kein JS/WASM) – Desktop-First gemäß MASTER_ROADMAP
- In `settings.gradle.kts` registriert
- `compileKotlinJvm` ✅ BUILD SUCCESSFUL
### 2. Projektstruktur
```
frontend/shells/meldestelle-desktop/
├── build.gradle.kts # JVM-only, compose.desktop, nativeDistributions
└── src/jvmMain/kotlin/at/mocode/desktop/
├── main.kt # application {} Entry-Point, Koin-Init, Window
├── DesktopApp.kt # Login-Gate + Haupt-Composable
├── di/
│ └── DesktopModule.kt # Koin: DesktopNavigationPort, CurrentUserProvider, DeepLinkHandler
├── navigation/
│ └── DesktopNavigationPort.kt # StateFlow-basierte Navigation
└── screens/
├── DesktopMainLayout.kt # Sidebar (220dp) + Content-Bereich
├── PlaceholderContent.kt # Wiederverwendbarer Platzhalter
├── VeranstaltungenScreen.kt # Übersicht + "Neue Veranstaltung"-Button
├── VeranstaltungNeuScreen.kt # Tabs: Übersicht | Stammdaten* | Organisation | Preisliste
├── VeranstaltungDetailScreen.kt # Übersicht-Tab + Turniere-Section
├── TurnierNeuScreen.kt # Tabs: Übersicht | Stammdaten | Organisation | Bewerbe⭐* | Preisliste
├── TurnierDetailScreen.kt # Bewerbe-Tab integriert NennungsMaske (nennung-feature)
└── AktorScreens.kt # Reiter, Pferde, Funktionäre, Meisterschaften, Cups
```
### 3. Navigation gemäß Vision_03
Sidebar-Navigation mit 6 Einträgen (links, 220dp, Material3 `surfaceVariant`):
| Eintrag | Route | Status |
|-----------------|--------------------|---------------------------------|
| Veranstaltungen | `/veranstaltungen` | ✅ Screen implementiert |
| Reiter | `/reiter` | ✅ Placeholder |
| Pferde | `/pferde` | ✅ Placeholder |
| Funktionäre | `/funktionaere` | ✅ Placeholder |
| Meisterschaften | `/meisterschaften` | ✅ Placeholder (Phase 2+) |
| Cups | `/cups` | ✅ Placeholder (Phase 2+) |
| Logout | — | ✅ Löscht Token, zurück zu Login |
### 4. Neue `AppScreen`-Einträge (core/navigation)
Folgende Screens wurden in `AppScreen.kt` ergänzt:
- `Veranstaltungen`, `VeranstaltungNeu`, `VeranstaltungDetail(id)`
- `TurnierNeu(veranstaltungId)`, `TurnierDetail(veranstaltungId, turnierId)`
- `Reiter`, `Pferde`, `Funktionaere`, `Meisterschaften`, `Cups`
### 5. Nennungs-Integration
- `TurnierDetailScreen` → Bewerbe-Tab (⭐ Standard-Tab) integriert `NennungsMaske` aus `nennung-feature`
- Callbacks für Startliste, Ergebnisse, Abrechnung als TODO vorbereitet
---
## Erstellte / Aktualisierte Dokumente
| Dokument | Aktion | Beschreibung |
|-----------------------------------------------------------------------------------|----------------|---------------------------------------------------------------------|
| `frontend/shells/meldestelle-desktop/build.gradle.kts` | ✅ NEU | JVM-only Shell, compose.desktop, nativeDistributions (Deb/Msi/Dmg) |
| `frontend/shells/meldestelle-desktop/src/.../main.kt` | ✅ NEU | application {} Entry-Point, Koin-Init, Window 1400×900 |
| `frontend/shells/meldestelle-desktop/src/.../DesktopApp.kt` | ✅ NEU | Login-Gate, delegiert an DesktopMainLayout |
| `frontend/shells/meldestelle-desktop/src/.../di/DesktopModule.kt` | ✅ NEU | Koin-Modul mit Navigation, CurrentUserProvider, DeepLinkHandler |
| `frontend/shells/meldestelle-desktop/src/.../navigation/DesktopNavigationPort.kt` | ✅ NEU | StateFlow-Navigation |
| `frontend/shells/meldestelle-desktop/src/.../screens/*.kt` | ✅ NEU | 8 Screen-Dateien (Layout, Placeholders, Veranstaltung/Turnier-Flow) |
| `frontend/core/navigation/.../AppScreen.kt` | ✅ ERWEITERT | 10 neue Desktop-Screens ergänzt |
| `settings.gradle.kts` | ✅ ERWEITERT | `:frontend:shells:meldestelle-desktop` registriert |
| `docs/01_Architecture/MASTER_ROADMAP.md` | ✅ AKTUALISIERT | Frontend-Expert-Tasks als abgeschlossen markiert |
---
## Wichtige Entscheidungen
1. **Eigenständiges Modul** statt Erweiterung des Portal-Shells → saubere Trennung Desktop/Web
2. **Login-Gate** in `DesktopApp.kt` → Desktop startet immer mit Login, kein Landing-Screen
3. **`PrimaryTabRow`** statt deprecated `TabRow` verwendet
4. **`NennungsMaske`** direkt im Bewerbe-Tab des `TurnierDetailScreen` integriert
---
## Nächste Schritte (Empfehlung)
- [ ] 👷 **[Backend Developer]**: `DomVeranstaltung`, `DomTurnier`, `DomBewerb`, `DomAbteilung` implementieren
- [ ] 🎨 **[Frontend Expert]**: `VeranstaltungenScreen` mit echten Daten aus Repository befüllen
- [ ] 🎨 **[Frontend Expert]**: Startlisten- und Ergebnisse-Screens implementieren (competition-context)
- [ ] 🎨 **[Frontend Expert]**: Formular-Felder für `VeranstaltungNeuScreen` und `TurnierNeuScreen` ausbauen
---
*Session-Dauer: 24. März 2026*
*Curator: Junie (KI-Agent)*
@@ -0,0 +1,78 @@
---
type: Session Log
date: 2026-03-24
agent: Backend Developer
context: event-management-context
status: ABGESCHLOSSEN
---
# Session Log: event-management-context – Domain-Modelle
👷 **[Backend Developer]** | 24. März 2026
## Ziel
Implementierung der Domain-Modelle `DomVeranstaltung`, `DomTurnier` und `DomAusschreibung`
für den `event-management-context` gemäß ÖTO § 2 Abs. 1 und ADR-0003 (`Veranstaltung ≠ Turnier`).
---
## Implementierte Modelle
### `DomVeranstaltung` (`events-domain`)
- **Aggregate Root** für den organisatorischen Rahmen einer pferdesportlichen Veranstaltung.
- Felder: `veranstaltungId`, `name`, `veranstaltungsTyp`, `sparten`, `veranstalterVereinId`,
`verantwortlicheFunktionaerId`, `startDatum`, `endDatum`, `ort`, `nennschluss`, `status`,
`ausschreibungsId`, `oepsGenehmigungsNummer`, `bemerkungen`, Audit-Felder.
- Warn-Logik: `validateNennungsmoeglichkeit()` (Status GENEHMIGT + Nennschluss vorhanden),
`validateFuerEinreichung()` (Pflichtfelder, Datum-Konsistenz, Ausschreibung verknüpft).
### `DomTurnier` (`events-domain`)
- **Aggregate Root** für ein einzelnes Turnier innerhalb einer Veranstaltung.
- Felder: `turnierId`, `veranstaltungId` (FK), `name`, `sparte`, `kategorie`, `datum`,
`richterObmannId`, `parcoursbauerId`, `status`, `maxBewerbe`, `istMeisterschaft`, `bemerkungen`, Audit-Felder.
- Warn-Logik: `validateFunktionaerBesetzung()` (Richter-Obmann Pflicht; Parcoursbauer Pflicht bei Springen),
`validateFuerPlanung()` (Pflichtfelder, positive maxBewerbe).
### `DomAusschreibung` (`events-domain`)
- **Aggregate Root** für das offizielle Ausschreibungs-Dokument.
- Felder: `ausschreibungsId`, `veranstaltungId` (FK), `titel`, `sparten`, `nennschluss`,
`nachnennung`, `nachnennungBis`, Gebühren in Cent (Integer, kein Float), `tierwohleuroAktiv`,
Veranstaltungsort, Stallplätze, Kontakt, `status`, `eingereichtAm`, `genehmigungsNummer`, Audit-Felder.
- Warn-Logik: `validateFuerEinreichung()` (Pflichtfelder, Gebühren ≥ 0, Nachnennungs-Datum-Konsistenz).
- Hilfsmethoden: `getNenngebuehrAlsEuroString()`, `getGesamtgebuehrCent()`.
---
## Neue Enums in `Enums.kt` (`core-domain`)
| Enum | Werte |
|-------------------------|----------------------------------------------------------------------------------------------------------|
| `VeranstaltungsStatusE` | `IN_PLANUNG`, `EINGEREICHT`, `GENEHMIGT`, `NENNSCHLUSS_ABGELAUFEN`, `AKTIV`, `ABGESCHLOSSEN`, `ABGESAGT` |
| `TurnierStatusE` | `GEPLANT`, `AKTIV`, `ABGESCHLOSSEN`, `ABGESAGT` |
| `AusschreibungsStatusE` | `ENTWURF`, `EINGEREICHT`, `GENEHMIGT`, `ABGELEHNT`, `VEROEFFENTLICHT` |
---
## Design-Entscheidungen
- **Veranstaltung ≠ Turnier** (ADR-0003): `DomVeranstaltung` ist der Rahmen, `DomTurnier` die Durchführungseinheit.
- **Gebühren in Cent (Integer)**: Vermeidung von Floating-Point-Fehlern bei Geldbeträgen.
- **Warn-Logik statt Exceptions** (ADR-0007): Alle Validierungen geben `List<String>` zurück.
- **Bestehende `Veranstaltung.kt`** bleibt als Legacy-Scaffold erhalten; `DomVeranstaltung.kt` ist das neue ÖTO-konforme
Modell.
---
## Geänderte Dateien
| Datei | Aktion |
|------------------------------------------|----------------------|
| `events-domain/.../DomVeranstaltung.kt` | NEU |
| `events-domain/.../DomTurnier.kt` | NEU |
| `events-domain/.../DomAusschreibung.kt` | NEU |
| `core-domain/.../Enums.kt` | ERWEITERT (+3 Enums) |
| `docs/01_Architecture/MASTER_ROADMAP.md` | AKTUALISIERT |
@@ -0,0 +1,121 @@
---
date: 2026-03-24
agent: Backend Developer
phase: PHASE 4 – MVP-Implementierung
status: ABGESCHLOSSEN
---
# Session Log: REST-Endpunkte Nennungs-Workflow
👷 **[Backend Developer]** | 24. März 2026
## Ziel
Implementierung der REST-Endpunkte für den Nennungs-Workflow (registration-context) gemäß MASTER_ROADMAP Phase 4.
---
## Erledigte Aufgaben
### 1. `entries-service` aktiviert
- `settings.gradle.kts`: `:backend:services:entries:entries-service` aus dem ON-HOLD-Kommentar befreit und aktiv
eingebunden.
### 2. `entries-api` – Fachliche DTOs (`NennungDtos.kt`)
Neue Datei: `entries-api/src/commonMain/kotlin/at/mocode/entries/api/NennungDtos.kt`
| DTO | Zweck |
|-------------------------------|-------------------------------------|
| `NennungEinreichenRequest` | POST – Neue Nennung einreichen |
| `NennungStatusAendernRequest` | PUT – Status ändern |
| `NennungTransferRequest` | POST /transfer – Transfer-Operation |
| `NennungSummaryDto` | GET Liste – kompakte Ansicht |
| `NennungDetailDto` | GET Detail / POST Response |
| `NennungsTransferDto` | Transfer-Response mit Audit-Trail |
**Dependency ergänzt:** `projects.core.coreDomain` in `entries-api/build.gradle.kts` (für `NennungsStatusE`,
`StartwunschE`, `UuidSerializer`).
### 3. `entries-domain` – `NennungsTransferRepository`
Neues Interface: `entries-domain/.../repository/NennungsTransferRepository.kt`
- `findById`, `findByUrsprungsNennungId`, `save`
### 4. `entries-service` – Persistence-Schicht
| Datei | Inhalt |
|-------------------------------------|-----------------------------------------------------------------|
| `NennungTable.kt` | Exposed-Tabelle `nennungen` mit allen Feldern + Indizes |
| `NennungsTransferTable.kt` | Exposed-Tabelle `nennungs_transfers` mit Audit-Trail |
| `NennungRepositoryImpl.kt` | Vollständige Implementierung aller `NennungRepository`-Methoden |
| `NennungsTransferRepositoryImpl.kt` | Implementierung aller `NennungsTransferRepository`-Methoden |
### 5. `entries-service` – Use Cases (`NennungUseCases.kt`)
`@Service`-Klasse mit folgenden Use Cases:
| Methode | Fachliche Logik |
|-------------------------------------------------|------------------------------------------------------------------------------------------------------------------------|
| `getNennungById` | Query by ID |
| `getNennungenByTurnier/Bewerb/Abteilung/Reiter` | Gefilterte Listen |
| `nennungEinreichen` | Neue Nennung, Warn-Log bei Nachnennung |
| `statusAendern` | Status-Transition mit Audit-Timestamp |
| `nennungZurueckziehen` | Soft-Delete → Status `ZURUECKGEZOGEN` |
| `nennungTransferieren` | **Atomare Transfer-Operation** (ÖTO-konform): Ursprung → TRANSFERIERT, neue Nennung anlegen, Transfer-Record speichern |
**ÖTO-Konformität:** Warn-Logik statt harter Fehler. TBA hat das letzte Wort.
### 6. `entries-service` – REST-Controller (`NennungController.kt`)
Basis-URL: `/api/v1/registrations/nennungen`
| Methode | Endpunkt | Beschreibung |
|----------|--------------------------|------------------------------------------------------------|
| `GET` | `/` | Liste (Filter: turnierId, bewerbId, abteilungId, reiterId) |
| `GET` | `/{nennungsId}` | Detail |
| `POST` | `/` | Neue Nennung einreichen (201) |
| `PUT` | `/{nennungsId}/status` | Status ändern |
| `DELETE` | `/{nennungsId}` | Zurückziehen |
| `POST` | `/{nennungsId}/transfer` | Transfer (201) |
### 7. `entries-service` – Konfiguration
| Datei | Inhalt |
|-----------------------------------|---------------------------------------------------------------|
| `EntriesBeansConfiguration.kt` | Spring-Beans für Repository-Implementierungen |
| `EntriesDatabaseConfiguration.kt` | Exposed-Schema-Init (`NennungTable`, `NennungsTransferTable`) |
| `EntriesExceptionHandler.kt` | RFC 9457 Problem Details (404, 400, 409) |
### 8. `entries-service` – Build-Dependencies ergänzt
```kotlin
implementation(projects.backend.services.entries.entriesDomain)
implementation(projects.core.coreUtils)
implementation(projects.core.coreDomain)
implementation(libs.exposed.core)
implementation(libs.exposed.jdbc)
implementation(libs.exposed.kotlin.datetime)
```
---
## Architektur-Entscheidungen
- **Warn-Logik:** Nachnennungen und Transfers nach Nennschluss werden geloggt (`log.warn`), aber nicht blockiert – gemäß
ADR-7 (Warn-Logik statt harter Fehler).
- **Transfer = atomare Operation:** Keine Storno + Neunennung, sondern: Ursprung → TRANSFERIERT + neue Nennung +
Transfer-Record. Entspricht ÖTO-Regelwerk.
- **Soft-Delete:** Nennungen werden nie physisch gelöscht, sondern auf `ZURUECKGEZOGEN` gesetzt.
- **Repository-Pattern:** Interfaces in `entries-domain`, Implementierungen in `entries-service` (Dependency Inversion).
---
## Nächste Schritte
- **ÖTO/FEI Rulebook Expert:** Voltigieren (CVN) und Fahren (CAN) Abteilungs-Trennungsregeln auswerten (offene Fragen
#3, #4).
- **QA Specialist:** Integrationstests für den Nennungs-Workflow schreiben.
- **Backend Developer:** `competition-context` (Bewerbe, Startlisten, Ergebnisse) – PHASE 5.
@@ -0,0 +1,107 @@
---
type: Session Log
date: 2026-03-24
agent: Backend Developer
topic: Persistenz – Repository-Interfaces und erste DB-Migrationen
status: ABGESCHLOSSEN
---
# 👷 Session Log: Persistenz – Repository-Interfaces & DB-Migrationen
**Datum:** 24. März 2026
**Agent:** 👷 Backend Developer
**Aufgabe:** Repository-Interfaces und erste DB-Migrationen (Flyway) für `actor-context`, `registration-context`.
---
## Erledigte Aufgaben
### 1. Repository-Interfaces (Domain-Layer)
Vier neue Repository-Interfaces als Ports (Hexagonale Architektur) in den `-domain`-Modulen:
| Interface | Modul | Aggregate Root |
|-------------------------|--------------------|------------------|
| `FunktionaerRepository` | `officials-domain` | `DomFunktionaer` |
| `VereinRepository` | `clubs-domain` | `DomVerein` |
| `ReiterRepository` | `persons-domain` | `DomReiter` |
| `NennungRepository` | `entries-domain` | `DomNennung` |
Alle Interfaces folgen dem gleichen Muster:
- `findById`, `findByXxx`, `findAll`, `findAllActive` (paginiert)
- `save` (Upsert-Semantik)
- `delete`, `countXxx`, `existsByXxx`
- `suspend fun` für Coroutine-Kompatibilität
### 2. Exposed-Tabellendefinitionen (Infrastructure-Layer)
Drei neue `Table`-Objekte (Exposed 1.0.0, `org.jetbrains.exposed.v1.core.Table` + `javaUUID`):
| Table | Modul | DB-Tabelle |
|--------------------|----------------------------|----------------|
| `FunktionaerTable` | `officials-infrastructure` | `funktionaere` |
| `VereinTable` | `clubs-infrastructure` | `vereine` |
| `ReiterTable` | `persons-infrastructure` | `reiter` |
Technische Details:
- UUID-PK via `javaUUID("id").autoGenerate()` (konsistent mit `HorseTable`)
- JSON-Spalten (`TEXT`) für `rollen`, `qualifiziert_fuer_sparten`, `lizenziert_fuer_sparten`
- `timestamp()` aus `org.jetbrains.exposed.v1.datetime` für Audit-Felder
- Partial Unique Indexes für nullable Felder (Richternummer, Satznummer, FEI-ID)
### 3. Repository-Implementierungen (Infrastructure-Layer)
Drei neue `Exposed*Repository`-Klassen:
| Implementierung | Modul |
|--------------------------------|----------------------------|
| `ExposedFunktionaerRepository` | `officials-infrastructure` |
| `ExposedVereinRepository` | `clubs-infrastructure` |
| `ExposedReiterRepository` | `persons-infrastructure` |
Technische Details:
- Imports: `org.jetbrains.exposed.v1.core.eq`, `.like`, `.and`, `.or` (Top-Level-Funktionen in Exposed 1.0.0)
- Pagination: `.limit(n).offset(m.toLong())` (Exposed 1.0.0 API)
- JSON-Serialisierung via `kotlinx.serialization.json.Json`
- `kotlin.time.Clock` (nicht `kotlinx.datetime.Clock`) für Timestamp-Kompatibilität mit Exposed
### 4. Flyway-SQL-Migrationen (Service-Layer)
Drei neue V001-Migrationen im Stil der bestehenden `masterdata-service`-Migrationen:
| Migration | Service | Tabelle |
|---------------------------------------|---------------------|----------------|
| `V001__Create_Funktionaere_Table.sql` | `officials-service` | `funktionaere` |
| `V001__Create_Vereine_Table.sql` | `clubs-service` | `vereine` |
| `V001__Create_Reiter_Table.sql` | `persons-service` | `reiter` |
Alle Migrationen enthalten:
- `CREATE TABLE IF NOT EXISTS` mit vollständiger Spaltendefinition
- Partial Unique Indexes für nullable Identifikationsfelder
- Performance-Indizes für häufige Suchabfragen
- `COMMENT ON TABLE/COLUMN` für Dokumentation
---
## Architektur-Entscheidungen
- **Hexagonale Architektur:** Repository-Interfaces im Domain-Layer, Implementierungen im Infrastructure-Layer – keine
Abhängigkeit der Domain von Exposed.
- **JSON für Collections:** Rollen und Sparten als JSON-Arrays in TEXT-Spalten (kein Join-Table) – pragmatisch für MVP,
erweiterbar.
- **Upsert-Semantik:** `save()` prüft Existenz via `selectAll().where { id eq ... }` und führt Insert oder Update durch.
- **Partial Unique Indexes:** Nullable Felder (Richternummer, Satznummer, FEI-ID) mit `WHERE field IS NOT NULL` –
erlaubt mehrere NULL-Werte.
---
## Offene Punkte
- `entries-infrastructure`-Modul existiert noch nicht → `NennungRepository`-Implementierung ausstehend.
- Flyway-Konfiguration in `officials-service`, `clubs-service`, `persons-service` `application.yml` noch nicht
geprüft/ergänzt.
- Spring-DI-Konfiguration (Bean-Registrierung der Repository-Implementierungen) noch ausstehend.
@@ -0,0 +1,76 @@
---
type: Session Log
date: 2026-03-24
agent: Backend Developer
status: ABGESCHLOSSEN
roadmap_phase: PHASE 4 – MVP-Implementierung
---
# Session Log: registration-context Domain-Modelle
👷 **[Backend Developer]** | 24. März 2026
## Ziel
Domain-Modelle `DomBewerb`, `DomAbteilung` und `DomStartliste` im `registration-context` implementieren (PHASE 4,
MASTER_ROADMAP).
---
## Ergebnis
### ✅ DomBewerb (neu)
- **Datei:** `backend/services/entries/entries-domain/src/main/kotlin/at/mocode/entries/domain/model/DomBewerb.kt`
- Aggregate Root für den Bewerbs-Workflow im `registration-context`.
- Felder: `turnierId`, `bewerbNummer`, `bezeichnung`, `sparte`, `turnierkategorie`, `pruefungsTyp`, `hoeheCm`,
`teilungsTyp`, `maxStarterProAbteilung`, `istMeisterschaft`, `istNachnennungErlaubt`.
- Domain-Methoden: `getDisplayName()`, `getPflichtTeilungsSchwellenwert()` (§ 39-konform),
`getKannTeilungsSchwellenwert()`, `validateAbteilungsSchwellenwerte()` (Warn-Logik).
### ✅ DomAbteilung (neu)
- **Datei:** `backend/services/entries/entries-domain/src/main/kotlin/at/mocode/entries/domain/model/DomAbteilung.kt`
- Kleinste startbare Einheit innerhalb eines Bewerbs.
- Felder: `bewerbId`, `abteilungsNummer`, `bezeichnung`, `teilungsTyp`, `teilnehmerkreisBeschreibung`, `starterAnzahl`,
`maxStarter`, `startzeit`.
- Domain-Methoden: `getDisplayName()`, `hatFreiePlaetze()`, `validateStarterLimit()` (Warn-Logik für > 80 Starter, § 39
Abs. 2).
### ✅ DomStartliste (neu)
- **Datei:** `backend/services/entries/entries-domain/src/main/kotlin/at/mocode/entries/domain/model/DomStartliste.kt`
- Enthält geordnete Liste von `StartlistenEintrag` (Startnummer → Nennung).
- Workflow: `NICHT_ERSTELLT → ENTWURF → VEROEFFENTLICHT → GESPERRT → ARCHIVIERT`.
- Domain-Methoden: `getStarterAnzahl()`, `getEintragByStartnummer()`, `istBearbeitbar()`, `istSichtbar()`,
`veroeffentlichen()`, `sperren()` (Warn-Logik für ungültige Status-Übergänge).
- `StartlistenEintrag`: Denormalisierte Felder `reiterName`, `pferdeName` für schnelle Anzeige; `istGestrichen`-Flag für
Abmeldungen nach Startlistenerstellung.
### ✅ Neue Enums in core-domain
- **Datei:** `core/core-domain/src/commonMain/kotlin/at/mocode/core/domain/model/Enums.kt`
- `PruefungsTypE`: STIL_SPRINGEN, SPRINGPFERDE, DRESSURPFERDE, VIELSEITIGKEIT, SPRINGEN_UEBRIG, DRESSUR, CAPRILLI,
FAHREN, VOLTIGIEREN, SONSTIGE – mit Schwellenwert-Kommentaren gemäß § 39.
- `AbteilungsTeilungsTypE`: KEINE, NACH_LIZENZ, NACH_PLATZ, NACH_PFERDEALTER, STRUKTURELL, NACH_AUSSCHREIBUNG.
- `StartlistenStatusE`: NICHT_ERSTELLT, ENTWURF, VEROEFFENTLICHT, GESPERRT, ARCHIVIERT.
---
## Design-Entscheidungen
| Entscheidung | Begründung |
|----------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `DomBewerb` im `entries-domain`-Modul | Bewerbe sind direkt mit Nennungen verknüpft; `entries-domain` ist das bestehende KMP-Modul für den `registration-context`. |
| Schwellenwerte als Domain-Methoden, nicht hard-coded | Konsistent mit Implementierungs-Hinweisen in `Abteilungs-Trennungs-Schwellenwerte.md` (§ 4.2). Konfigurierbare Parameter für spätere Persistenz vorbereitet. |
| `StartlistenEintrag` als Value Object in `DomStartliste` | Einträge haben keine eigene Identität außerhalb der Startliste; Denormalisierung von `reiterName`/`pferdeName` für performante Anzeige ohne Join. |
| Warn-Logik statt harter Fehler | Konsistent mit ADR-0016 und Override-Event-Prinzip: alle `validate*()`-Methoden geben Warnungen zurück, kein Exception-Throwing. |
| `istMeisterschaft`-Flag auf `DomBewerb` | Meisterschaftsbewerbe sind von der Pflicht-Teilung ausgenommen (§ 39 Abs. 4) – Flag ermöglicht direkte Prüfung in `getPflichtTeilungsSchwellenwert()`. |
---
## Nächste Schritte (PHASE 4)
- [ ] `event-management-context`: `DomVeranstaltung`, `DomTurnier`, `DomAusschreibung` implementieren.
- [ ] Persistenz: Repository-Interfaces und DB-Migrationen (Flyway/Liquibase).
- [ ] API: REST-Endpunkte für Nennungs-Workflow (Kern-Use-Cases).
@@ -0,0 +1,82 @@
---
type: Session Log
date: 2026-03-24
agent: ÖTO/FEI Rulebook Expert
status: ABGESCHLOSSEN
---
# Session Log: Warn-Logik-Spezifikation `competition-context`
🧹 **[Curator]** | 24. März 2026
---
## Aufgabe
Spezifikation der `competition-context` Warn-Logik für Abteilungs-Schwellenwerte
(MASTER_ROADMAP Phase 4, ÖTO/FEI Rulebook Expert).
## Ergebnis
### Neues Dokument erstellt
**`docs/03_Domain/02_Reference/OETO_Regelwerk/Warn-Logik-Spezifikation-competition-context.md`**
Verbindliche Implementierungs-Spezifikation für die Warn-Logik im `competition-context`.
### Inhalt der Spezifikation
**6 Warn-Codes definiert:**
| Warn-Code | Typ |
|--------------------------------------------|----------------------------------------------|
| `WARN_PFLICHT_TEILUNG_UEBERSCHRITTEN` | Starter-Schwellenwert überschritten |
| `WARN_KANN_TEILUNG_EMPFOHLEN` | Kann-Teilung empfohlen (Dressur) |
| `WARN_ABTEILUNG_ZU_GROSS` | Abteilung nach Teilung > 80 Starter |
| `WARN_ABTEILUNG_MAX_UEBERSCHRITTEN` | Konfiguriertes Starter-Limit überschritten |
| `WARN_STRUKTURELLE_TEILUNG_FEHLT` | Strukturelle Pflicht-Teilung nicht vorhanden |
| `WARN_STRUKTURELLE_TEILUNG_UNVOLLSTAENDIG` | Strukturelle Teilung unvollständig |
**3 Warn-Typen mit vollständigem Entscheidungsbaum:**
1. Starter-Schwellenwerte (`DomBewerb.validateAbteilungsSchwellenwerte`)
2. Abteilungs-Größe nach Teilung (`DomAbteilung.validateStarterLimit`)
3. Strukturelle Pflicht-Teilungen (`DomBewerb.validateStrukturellesTeilung`) – NEU
**Strukturelle Prüfungen abgedeckt:**
- CSN Stil-/Idealzeitspringen ≤ 95 cm: ohne Lizenz vs. R1 (§ 200 Abs. 5.3)
- Springpferdeprüfung 95–110 cm / Dressurpferdeprüfung Kl. A: Pferdealter 4 vs. 5–6 (§ 200 Abs. 6 / § 100 Abs. 5)
- CSN-C-NEU ≤ 95 cm: ohne Lizenz vs. mit Lizenz (§ 231)
- CSN-C-NEU ≥ 100 cm: R1 vs. R2+ (§ 231)
- CCN-C-NEU Gelände ≤ 80 cm: 3 Abteilungen (§ 300)
- CCN-C-NEU Gelände ≥ 90 cm: 2 Abteilungen (§ 300)
- CCN Welcome / 80 cm: R2+ eigene Abteilung (§ 301 Abs. 1.4)
- Caprilli: lizenzfrei vs. RD1+ (§ 803 Abs. 2)
- Fahren/Fahrertreffen: F1+ eigene Abteilung (§ 850 Abs. 9)
**Implementierungs-Vorgaben:**
- Typisiertes Value Object `AbteilungsWarnung` (statt roher Strings)
- `AbteilungsWarnungCodeE` Enum
- `AbteilungsWarnungOverrideEvent` mit Pflicht-Begründungsfeld
- `AbteilungsSchwellenwertConfig` (konfigurierbare Schwellenwerte, nicht hard-coded)
- Aufruf-Zeitpunkte (Trigger) definiert
### Aktualisierte Dokumente
- `docs/01_Architecture/MASTER_ROADMAP.md` – Task als `[x]` markiert, Referenz ergänzt, Referenz-Tabelle aktualisiert
## Offene Fragen (weiterhin offen)
| # | Frage |
|---|--------------------------------------------------------------------|
| 1 | Gelten § 39-Schwellenwerte auch für Reitertreffen? |
| 2 | Pflicht-Teilung bei kombinierten Turnieren (§ 4)? |
| 3 | Voltigieren (CVN): Eigene Abteilungs-Trennungsregeln? |
| 4 | Fahren (CAN): Eigene Starter-Schwellenwerte? |
| 5 | Abbildung „Geldpreise > Doppeltes Gebührenordnung" im Datenmodell? |
---
*Session: 2026-03-24 | Agent: ÖTO/FEI Rulebook Expert + Curator (Junie)*