Document and implement CSN-C-NEU mandatory division rules: Add TURNIER_KLASSEN.md detailing forced divisions by license categories; establish uniform labels and keys (e.g., LZF_ONLY and R1_PLUS); draft optional youth division rules. Extend validation in Validierungsregeln.md. Implement FEI-ID resolver and mapping endpoint in Masterdata service.

This commit is contained in:
2026-04-02 18:36:59 +02:00
parent bbe5b1a357
commit 4ae701d969
8 changed files with 540 additions and 25 deletions
@@ -0,0 +1,29 @@
package at.mocode.masterdata.domain.service
/**
* Auflösung von FEI-IDs: akzeptiert numerische IDs (Pass‑Through) und Legacy‑Referenzcodes
* und liefert – wenn bekannt – die zugehörige numerische FEI‑ID zurück.
*/
interface FeiIdResolver {
/**
* Löst eine eingegebene FEI‑Kennung auf.
*
* @param input Benutzer-/Importeingabe (numerisch 7–8 Stellen oder Legacy‑Code 3Z+2B+2Z)
* @return [FeiIdResolution] mit normalisierter numerischer ID, oder null wenn unbekannt/ungültig
*/
fun resolve(input: String): FeiIdResolution?
}
/**
* Ergebnis der FEI‑ID Auflösung.
*
* @property normalizedNumericId Numerische FEI‑ID (7–8 Ziffern) als String
* @property sourceFormat "NUMERIC" | "LEGACY_CODE"
* @property wasMapped true, wenn aus Legacy nach numerisch gemappt wurde
*/
data class FeiIdResolution(
val normalizedNumericId: String,
val sourceFormat: String,
val wasMapped: Boolean
)
@@ -31,6 +31,7 @@ dependencies {
implementation(libs.spring.boot.starter.web)
implementation(libs.spring.boot.starter.validation)
implementation(libs.spring.boot.starter.actuator)
implementation(libs.jackson.module.kotlin)
//implementation(libs.springdoc.openapi.starter.webmvc.ui)
// Ktor Server (für SCS: eigener kleiner HTTP-Server pro Kontext)
@@ -0,0 +1,50 @@
package at.mocode.masterdata.service.fei
import at.mocode.masterdata.domain.service.FeiIdResolution
import at.mocode.masterdata.domain.service.FeiIdResolver
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
@RestController
@RequestMapping("/api/fei")
class FeiIdController(
private val resolver: FeiIdResolver
) {
data class ResolveResponse(
val input: String,
val normalizedNumericId: String?,
val sourceFormat: String?,
val wasMapped: Boolean,
val found: Boolean
)
@GetMapping("/resolve/{id}")
fun resolve(@PathVariable("id") id: String): ResponseEntity<ResolveResponse> {
val res: FeiIdResolution? = resolver.resolve(id)
return if (res != null) {
ResponseEntity.ok(
ResolveResponse(
input = id,
normalizedNumericId = res.normalizedNumericId,
sourceFormat = res.sourceFormat,
wasMapped = res.wasMapped,
found = true
)
)
} else {
ResponseEntity.status(404).body(
ResolveResponse(
input = id,
normalizedNumericId = null,
sourceFormat = null,
wasMapped = false,
found = false
)
)
}
}
}
@@ -0,0 +1,43 @@
package at.mocode.masterdata.service.fei
import at.mocode.masterdata.domain.service.FeiIdResolution
import at.mocode.masterdata.domain.service.FeiIdResolver
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import org.springframework.core.io.ClassPathResource
import org.springframework.stereotype.Component
/**
* Einfache In‑Memory Implementierung des [FeiIdResolver],
* lädt ein JSON‑Mapping aus Ressourcen: `data/fei-id-mapping.json`.
*/
@Component
class FeiIdResolverImpl : FeiIdResolver {
private val legacyToNumeric: Map<String, String> by lazy {
val res = ClassPathResource("data/fei-id-mapping.json")
if (!res.exists()) return@lazy emptyMap<String, String>()
val mapper = jacksonObjectMapper()
res.inputStream.use { input ->
mapper.readValue<Map<String, String>>(input)
.mapKeys { it.key.trim().uppercase() }
}
}
private val numericRegex = Regex("^[0-9]{7,8}$")
private val legacyRegex = Regex("^[0-9]{3}[A-Z]{2}[0-9]{2}$")
override fun resolve(input: String): FeiIdResolution? {
val s = input.trim().uppercase()
// Numerisch: Pass‑Through
if (numericRegex.matches(s)) {
return FeiIdResolution(normalizedNumericId = s, sourceFormat = "NUMERIC", wasMapped = false)
}
// Legacy: lookup
if (legacyRegex.matches(s)) {
val mapped = legacyToNumeric[s] ?: return null
return FeiIdResolution(normalizedNumericId = mapped, sourceFormat = "LEGACY_CODE", wasMapped = true)
}
return null
}
}
@@ -0,0 +1,6 @@
{
"104FE22": "10011469",
"103RW04": "10019075",
"102UB51": "10028445",
"104UD89": "10011111"
}
@@ -0,0 +1,181 @@
---
type: RULE_SPEC
status: DRAFT
owner: Rulebook Expert
last_update: 2026-04-02
---
# Turnier-Klassen und Abteilungs-Zwangsteilung (ÖTO)
Ziel: Einheitliche, maschinenlesbare Spezifikation der Klassensystematik und der verpflichtenden Abteilungs-Teilungsregeln ("Zwangsteilung") gemäß ÖTO. Diese Regeln steuern Backend-Validierungen, Frontend-UX (Hinweise/Warnungen) und Exportlogik.
Quellen/Verweise:
- Roadmap: `docs/04_Agents/Roadmaps/Rulebook_Roadmap.md` (A-2)
- Domänen‑Modell: `docs/03_Domain/01_Core_Model/Domain_Model_Veranstaltung_Turnier_Bewerb_Abteilung.md`
- Validierungsregeln (Lizenz/Alter): `docs/03_Domain/02_Reference/Validierungsregeln.md`
- ÖTO Referenzstellen (Paragraphen‑Pins zur Nachverfolgung):
- Springen: ÖTO 2026, Kapitel „Springen“, § 231 Abs. 1–3 (CSN‑C‑NEU Teilungsregeln) [PIN: OETO-SPR-231]
- Dressur: ÖTO 2026, Kapitel „Dressur“, § 103 Abs. 2–5 (Teilnahme/Leistungsstufen) [PIN: OETO-DRS-103]
- Vielseitigkeit: ÖTO 2026, Kapitel „Vielseitigkeit“, §§ 3xx (Teilnahme/Abteilungen) [PIN: OETO-VS-3XX]
Hinweis Rechtslage: Die obigen „Paragraphen‑Pins“ verankern die Stellen im ÖTO. Exakte Absatz-/Ziffernangaben werden nach juristischer Finalisierung ergänzt. Inhaltliche Logik entspricht dem Stand der Praxis (CSN‑C‑NEU) und wird bei Abweichungen angepasst.
---
## 1. Begriffe (Auszug)
- Bewerb: Sportliche Ausschreibungseinheit innerhalb eines Turniers (z. B. Springen 95 cm, Stilspringen). Enthält 1..N Abteilungen.
- Abteilung: Startgruppe innerhalb eines Bewerbs. Kann organisatorische oder regelbedingte Gründe haben (Zwangsteilung, Lizenz, Jugend usw.).
- Zwangsteilung: Verpflichtende Abteilungsbildung anhand lizenz-/leistungsbezogener Kriterien gemäß ÖTO.
---
## 2. CSN (Springen national) — Zwangsteilung C‑NEU
Gültig für Bewerbe der Kategorie „CSN‑C‑NEU“.
### 2.1 Regelübersicht (Zwangsteilung)
- Rechtsgrundlage: ÖTO § 231 (vgl. [PIN: OETO-SPR-231])
- Bewerbe mit Höhe ≤ 95 cm:
- Abteilung A: Label: „ohne Lizenz“ — Key: `LZF_ONLY` — Allowed: `LZF`
- Abteilung B: Label: „mit Lizenz“ — Key: `R1_PLUS` — Allowed: `R1|R2|R3|R4`
- Bewerbe mit Höhe ≥ 100 cm:
- Abteilung A: Label: „R1“ — Key: `R1_ONLY` — Allowed: `R1`
- Abteilung B: Label: „R2 und höher“ — Key: `R2_PLUS` — Allowed: `R2|R3|R4`
Erläuterungen:
- Die Abteilungsbezeichnungen dienen dem Frontend (Label) und der Preisgeld-/Siegerehrungslogik. Technisch werden die Abteilungen per Attribut „Lizenz‑Gruppe“ markiert.
- Veranstalter dürfen enger teilen (z. B. zusätzliche Jugendabteilungen), nicht jedoch lockern (Pflicht‑Zweiteilung muss bestehen bleiben).
### 2.2 Maschinenlesbare Spezifikation
Tabelle: CSN‑C‑NEU Zwangsteilung nach Höhe
| Höhe (cm) | Abteilung 1 (Label · Key) | Abteilung 2 (Label · Key) | Bemerkung |
|---|---|---|---|
| ≤ 95 | „ohne Lizenz“ · `LZF_ONLY` | „mit Lizenz“ · `R1_PLUS` | `R1_PLUS` umfasst `R1|R2|R3|R4` |
| ≥ 100 | „R1“ · `R1_ONLY` | „R2 und höher“ · `R2_PLUS` | `R2_PLUS` umfasst `R2|R3|R4` |
Pseudocode (Ableitung der Pflicht‑Abteilungen):
```kotlin
data class ForcedDivision(val label: String, val allowedLicenses: Set<String>)
fun forcedDivisionsCsnCNeu(heightCm: Int): List<ForcedDivision> =
if (heightCm <= 95) listOf(
ForcedDivision(label = DivisionLabels.OHNE_LIZENZ, allowedLicenses = setOf("LZF")),
ForcedDivision(label = DivisionLabels.MIT_LIZENZ, allowedLicenses = setOf("R1","R2","R3","R4"))
) else listOf(
ForcedDivision(label = DivisionLabels.R1, allowedLicenses = setOf("R1")),
ForcedDivision(label = DivisionLabels.R2_UND_HOEHER, allowedLicenses = setOf("R2","R3","R4"))
)
```
Validierung (vereinfachte Regel):
- Wenn Bewerbskategorie = `CSN-C-NEU`, dann müssen genau zwei Abteilungen gemäß obiger Ableitung vorhanden sein. Jede Nennung muss in einer Abteilung landen, deren `allowedLicenses` die Lizenz des Reiters enthält.
Fehlermeldungen (UX):
- „Für CSN‑C‑NEU Bewerbe ist eine Zwangsteilung nach Lizenz vorgeschrieben (ÖTO § 231). Bitte beide Abteilungen anlegen.“
- „Die Abteilung ‚R2 und höher‘ darf nur Lizenzen R2/R3/R4 enthalten.“
- Hinweistext (Quelle): „Rechtsgrundlage: ÖTO § 231 (CSN‑C‑NEU).“
---
## 3. CDN (Dressur national) — Prüfung weiterer Zwangsteilungen
Status: geprüft. Nach aktuellem Stand bestehen in den Einsteiger‑/Niedrig‑Klassen keine zwingenden Lizenz‑Zwangsteilungen analog zu CSN‑C‑NEU. Übliche Praxis ist die optionale Teilung nach Leistungsklassen/Jahrgängen (z. B. Jugendliche), jedoch keine verpflichtende Zweiteilung „ohne/mit Lizenz“.
- Ergebnis: Keine allgemeine, disziplinweite Zwangsteilung identifiziert. Veranstalter können freiwillig teilen (z. B. RD1 vs. RD2+), sofern ÖTO konform. Bezug: ÖTO § 103 (vgl. [PIN: OETO-DRS-103]).
Folgeaktion: Bei Veröffentlichung der finalen Dressur‑Abschnitte erneut prüfen. Bis dahin: Keine systemweite Pflichtregel hinterlegen.
---
## 4. CCN (Vielseitigkeit national) — Prüfung weiterer Zwangsteilungen
Status: vorläufig geprüft. In den nationalen Vielseitigkeitsklassen (CCN) ist keine generische Zwangsteilung nach Lizenzgruppen („ohne/mit“ bzw. `R1` vs. `R2+`) als Pflicht verankert. Teilungen erfolgen eher nach Leistungsniveau, Altersklassen oder organisatorischen Gründen.
- Ergebnis: Keine disziplinweiten Pflicht‑Teilungsregeln identifiziert. Konkrete Ausnahmen sind turnierspezifisch. Bezug: ÖTO Kapitel „Vielseitigkeit“, §§ 3xx (vgl. [PIN: OETO-VS-3XX]); exakte Absätze folgen nach Finalisierung.
---
## 5. Implementierungshinweise (Backend/Frontend)
- Backend:
- Regel „CSN‑C‑NEU → Pflicht‑Abteilungen“ als Regulation‑as‑Data hinterlegen (z. B. `reg_forced_divisions` mit Feldern: `category`, `height_threshold`, `division_key`, `allowed_licenses`).
- Serverseitige Validierung beim Anlegen/Bearbeiten eines CSN‑C‑NEU Bewerbs: genau zwei Abteilungen erzwingen, Labels/Allowed‑Sets prüfen.
- Nennvalidierung: Lizenz des Reiters ∈ `allowedLicenses` der Zielabteilung.
- Frontend:
- Wizard/Editor legt bei CSN‑C‑NEU automatisch beide Abteilungen an (konfigurierbare Labels).
- Live‑Hinweis, wenn eine Abteilung fehlt oder falsche Lizenzen zugeordnet sind.
---
## 6. Einheitliche Label‑Konventionen für Abteilungen
Ziel: Einheitliche, i18n‑fähige Benennung in UI, Exporten und Validierung. Deutsche Standard‑Labels und technische Keys:
- DivisionLabels (Deutsch):
- OHNE_LIZENZ → „ohne Lizenz“ (Key: `LZF_ONLY`)
- MIT_LIZENZ → „mit Lizenz“ (Key: `R1_PLUS`)
- R1 → „R1“ (Key: `R1_ONLY`)
- R2_UND_HOEHER → „R2 und höher“ (Key: `R2_PLUS`)
Richtlinien:
- Labels in UI exakt wie oben; keine Varianten („R2+“ nur in Klammern/Hinweisen, offizielles Label: „R2 und höher“).
- Keys sind stabil und werden in Datenpersistenz/Exports verwendet. Übersetzungen erfolgen per i18n.
Pseudocode (Konstanten):
```kotlin
object DivisionLabels {
const val OHNE_LIZENZ = "ohne Lizenz" // key: LZF_ONLY
const val MIT_LIZENZ = "mit Lizenz" // key: R1_PLUS
const val R1 = "R1" // key: R1_ONLY
const val R2_UND_HOEHER = "R2 und höher" // key: R2_PLUS
}
```
---
## 7. Erweiterungen: Jugend‑/Jahrgangsteilungen (optional)
Status: Optionales Regel‑Set, kein ÖTO‑Pflichtumfang wie bei CSN‑C‑NEU. Veranstalter können zusätzlich nach Jahrgängen/Jugendklassen teilen, sofern ÖTO‑konform (vgl. Dressur § 103 und disziplin‑spezifische Jugendbestimmungen).
Modellierung als optionale Regeln:
- Datenmodell (Beispiel als Regulation‑as‑Data):
- Tabelle `reg_optional_divisions`:
- `category` (z. B. `CSN`, `CDN`)
- `discipline` (SPRINGEN, DRESSUR, VIELSEITIGKEIT)
- `division_key` (z. B. `U16`, `U18`, `U25`, `AMATEURE`)
- `label` (z. B. „Jugend U16“, „Jugend U18“)
- `age_range` (z. B. `14-16` Jahre, berechnet gem. Stichtag 1.1.; vgl. Validierungsregeln § „Altersklassen Pferd/Reiter“)
- `license_filter` (optional, Menge erlaubter Lizenzen)
- `notes` (Freitext/Paragraphen‑Bezug)
Beispiel (Pseudocode):
```kotlin
data class OptionalDivisionRule(
val category: String, // CSN, CDN
val discipline: String, // SPRINGEN, DRESSUR
val divisionKey: String, // U16, U18
val label: String, // "Jugend U16"
val ageFrom: Int, val ageTo: Int?, // inklusiv, To=null = open ended
val allowedLicenses: Set<String>? = null // null = alle
)
fun applies(rule: OptionalDivisionRule, athleteAge: Int, license: String): Boolean =
(athleteAge >= rule.ageFrom) && (rule.ageTo?.let { athleteAge <= it } ?: true) &&
(rule.allowedLicenses?.contains(license) ?: true)
```
UX‑Hinweistexte:
- „Optionale Jugendabteilung aktiv: Nur Athlet:innen des Jahrgangsbereichs {label} werden hier gewertet.“
- „Diese Abteilung ist optional; Pflicht‑Zwangsteilung (falls vorhanden) bleibt unberührt.“
---
## 8. Offene Punkte / ToDos
- Juristische Finalisierung: Exakte Absatz-/Ziffernangaben zu [PIN: OETO-SPR-231], [PIN: OETO-DRS-103], [PIN: OETO-VS-3XX] nachtragen.
- Backend‑Seed: `reg_forced_divisions` und `reg_optional_divisions` befüllen; Keys/Labels gemäß Abschnitt 6 verwenden.
- FE/UX: i18n‑Mapping für DivisionLabels bereitstellen; Editor‑Presets für CSN‑C‑NEU und optionale Jugendabteilungen.
@@ -86,14 +86,212 @@ fun validateOepsId(input: String): Boolean {
---
## 2. FEI‑ID
ToDo: Wird in A‑1 (weiterer Unterpunkt) spezifiziert.
Status: Draft – auf Basis FEI General Regulations (Art. 113–114) und vorhandener Systemdaten. Endgültige Bestätigung via FEI Lookup/API steht aus.
### 2.1 Zweck
- Eindeutige Identifikation international registrierter Athlet:innen und Pferde bei FEI‑relevanten Bewerben/Kategorien (CI/CIO/CSI/CDI/CCI etc.).
### 2.2 Gültige Formate (Eingabe)
- Primär (numerisch, aktuell üblich):
- Regex: `^[0-9]{7,8}$`
Erläuterung: 7–8‑stellige numerische FEI‑IDs (z. B. `10011469`).
- Legacy/Referenzcode (in Legacy‑Daten sichtbar):
- Regex: `^[0-9]{3}[A-Z]{2}[0-9]{2}$`
Beispiel: `104FE22`. Diese Codes werden akzeptiert, aber bei Speicherung nach Möglichkeit gegen die numerische FEI‑ID aufgelöst (siehe 2.5).
Nicht erlaubt:
- Leerzeichen, Trennzeichen, gemischte Schreibweisen mit Präfixen (z. B. `FEI-10011469`), alphanumerische Mischformen außerhalb des obigen Legacy‑Musters.
### 2.3 Pflichtfelder‑Regel (Wann ist FEI‑ID erforderlich?)
- International (FEI‑Events: CI/CSI/CDI/CCI/CIO/CH/…):
- Athlet: FEI‑ID Pflicht.
- Pferd: FEI‑ID Pflicht (inkl. FEI‑Pass/Microchip gem. FEI‑Regeln, vgl. Art. 114, 137 FEI GR).
- National (ÖTO‑Events: CN/CSN/CDN/CCN):
- Athlet: FEI‑ID optional (nur wenn FEI‑registriert).
- Pferd: FEI‑ID optional (nur wenn FEI‑registriert).
- Ausnahme: Wenn eine nationale Prüfung als FEI‑qualifikationsrelevant ausgewiesen ist, kann FEI‑ID für Datenexporte empfohlen/erforderlich sein (Veranstalterhinweis).
Hinweis: Die konkrete Pflicht koppeln wir im System an das Feld „Turnierkategorie“ und Disziplin, konfigurierbar per Regel‑Set.
### 2.4 Beispiele
- Gültig: `10011469`, `10019075`, `10028445`, `104FE22` (Legacy).
- Ungültig: `FEI10011469` (Präfix), `10011 469` (Leerzeichen), `10A11469` (Buchstabe in numerischem Format), `104F-E22` (Sonderzeichen).
### 2.5 Normalisierung (Speicherformat)
- Bevorzugtes Speicherformat: numerische FEI‑ID (`[0-9]{7,8}`) als String ohne Trennzeichen.
- Legacy‑Referenzcode wird – sofern möglich – vor Speicherung via Mapping/Lookup in numerische FEI‑ID überführt. Falls kein Mapping möglich, speichern als eingegeben plus `source_format = LEGACY_CODE`.
### 2.6 Pseudocode‑Validierung
```kotlin
fun validateFeiId(input: String): Boolean {
val s = input.trim().uppercase()
val numeric = Regex("^[0-9]{7,8}$")
val legacy = Regex("^[0-9]{3}[A-Z]{2}[0-9]{2}$")
return numeric.matches(s) || legacy.matches(s)
}
```
### 2.7 Fehlermeldungen (UX‑Texte)
- Kurz: "Ungültige FEI‑ID. Erlaubt sind 7–8 Ziffern (z. B. 10011469)."
- Lang: "Bitte eine gültige FEI‑ID eingeben: 7–8 Ziffern (z. B. 10011469). Historische Referenzcodes (z. B. 104FE22) werden akzeptiert und – wenn möglich – automatisch aufgelöst."
### 2.8 Quellen/Verweise
- FEI General Regulations, insbesondere Art. 113 (Registration and Eligibility) und Art. 114 (Horse Identification) — `docs/03_Domain/02_Reference/FEI_Regelwerk/FEI-2026_General-Regulations_…md`
- Systembeispiele/Fixtures in Frontend‑Stores (FEI‑IDs): `frontend/shells/meldestelle-desktop/.../Stores.kt`
### 2.9 Backend‑Lookup (Masterdata‑SCS)
- Endpoint: `GET /api/fei/resolve/{id}`
- Eingabe: `{id}` numerisch (`^[0-9]{7,8}$`) oder Legacy‑Code (`^[0-9]{3}[A-Z]{2}[0-9]{2}$`).
- Erfolg 200: `{ input, normalizedNumericId, sourceFormat, wasMapped, found: true }`
- Nicht gefunden 404: `{ input, normalizedNumericId: null, sourceFormat: null, wasMapped: false, found: false }`
- Mapping‑Quelle: `backend/services/masterdata/masterdata-service/src/main/resources/data/fei-id-mapping.json` (kann später aus DB gespeist werden).
---
## 3. Lizenzklassen (R1–R4, RD1–RD3, LZF)
ToDo: Vollständige Liste und Zuordnung in A‑1 (weiterer Unterpunkt).
Status: Draft – basierend auf ÖTO‑Praxis und ZNS‑Lizenzdaten. Detaillierte Paragraphen‑Zitate werden nachgereicht (A‑2/A‑3 Arbeiten verknüpft).
### 3.1 Katalog gültiger Lizenzklassen
- Reiten Springen (R‑Klassen): `R1`, `R2`, `R3`, `R4`
- Dressur Reiten (RD‑Klassen): `RD1`, `RD2`, `RD3`
- Lizenzfrei/ohne Lizenz Kennzeichnung: `LZF` (für bewerbsbezogene Abteilung „ohne Lizenz“)
Erweiterbarkeit: Weitere Spezial‑/Jugend‑ oder Fahrer‑Lizenzen können ergänzt werden, sobald in ÖTO/ZNS erforderlich.
### 3.2 Grundregeln der Zuordnung (vereinfachte Erstfassung)
- Springen (CSN):
- Bewerbe bis inkl. 95 cm: Teilnahme mit `LZF` (Abt. „ohne Lizenz“) oder `R1` (Abt. „mit Lizenz`).
- Ab 100 cm: mindestens `R1` erforderlich; ab bestimmten Höhen empfohlen/erforderlich `R2+` (veranstalter‑/ausschreibungsabhängig).
- Zwangsteilungsregeln siehe Roadmap A‑2 (eigener Abschnitt).
- Dressur (CDN):
- Einsteigerprüfungen (z. B. Dressurreiterprüfungen niedrig): `LZF` oder `RD1`.
- Ab definiertem Schwierigkeitsgrad: `RD1+`, höhere Klassen `RD2/RD3` gemäß Ausschreibung.
Hinweis: Die exakte Matrix „Lizenzklasse × Bewerbsklasse (Disziplin, Höhe/Schwierigkeit)“ wird als Tabelle hinterlegt und aus ÖTO‑Paragraphen abgeleitet. Nach Bestätigung durch Fachreferat wird diese Spezifikation von „Draft“ auf „Stable“ gehoben.
### 3.3 Validierungslogik (Platzhalter bis zur finalen Matrix)
- Eingabe muss in obiger Katalogliste vorkommen (`R1|R2|R3|R4|RD1|RD2|RD3|LZF`).
- Bei Auswahl eines Bewerbs wird die erlaubte(n) Lizenzklasse(n) aus der Disziplin/Höhe/Schwierigkeit abgeleitet.
- Fehler, wenn gewählte Lizenzklasse nicht in der erlaubten Menge liegt.
Pseudocode (vereinfacht):
```kotlin
fun isLicenseAllowed(discipline: Discipline, heightCm: Int?, testLevel: DressageLevel?, license: String): Boolean {
val allowed = allowedLicensesFor(discipline, heightCm, testLevel) // Tabelle/Regel-Engine
return license in allowed
}
```
### 3.4 Fehlermeldungen (UX‑Texte)
- Kurz: "Diese Lizenzklasse ist für den ausgewählten Bewerb nicht zugelassen."
- Lang: "Bitte eine für diesen Bewerb zugelassene Lizenz auswählen. Die Zulassung richtet sich nach Disziplin und Höhe/Schwierigkeitsgrad (ÖTO)."
### 3.5 Quellen/Verweise
- ÖTO (Abschnitte zu Lizenzen, Springen/Dressur Teilnahmevoraussetzungen)
- ZNS‑Lizenzdaten: `docs/OePS/ZNS/LIZENZ01.dat` (Datenquelle, strukturierter Export) – Parsing/Anlage in Masterdata‑SCS.
- Teilungs-/Warnlogik: `docs/03_Domain/02_Reference/OETO_Regelwerk/Warn-Logik-Spezifikation-competition-context.md`
### 3.6 Lizenz‑Zuordnungstabelle (DRAFT, final mit Paragraphen‑Verweisen)
- Springen (CSN) — Bezug ÖTO § 231 ff. (finale Paragraphennummern nachreichen):
| Höhe (cm) | Zulässige Lizenz-Abteilungen | Primär-Bezug ÖTO |
|---|---|---|
| ≤ 95 | LZF „ohne Lizenz“ | § 231 (Zwangsteilung Einsteiger) |
| ≤ 95 | R1 „mit Lizenz“ | § 231 |
| 100 | R1+ | § 231 |
| 105–110 | R1, R2+ (Empf. R2) | § 231 |
| 115–120 | R2+ | § 231 |
| 125–135 | R3+ | § 231 |
| ≥ 140 | R4 | § 231 |
- Dressur (CDN) — Bezug ÖTO § 103 ff. (finale Paragraphennummern nachreichen):
| Prüfungsniveau (national, äquiv.) | Zulässige Lizenzen | Primär-Bezug ÖTO |
|---|---|---|
| Einsteiger/Dressurreiter (niedrig) | LZF, RD1 | § 103 |
| A/L | RD1+ | § 103 |
| LM/M | RD2+ | § 103 |
| S | RD3 | § 103 |
Hinweise:
- Veranstalter/Ausschreibung kann engere Anforderungen definieren, jedoch nicht lockern.
- Zwangsteilungsregeln für CSN‑C‑NEU sind in A‑2 separat spezifiziert und ergänzen die obige Tabelle.
---
## 4. Altersklassen Pferd
ToDo: Mindestalter je Bewerbsklasse / Höhe und Stichtagsregel (1. Jänner) – folgt in A‑1 (weiterer Unterpunkt).
Status: Draft – FEI/ÖTO konsolidiert; Detailtabellen pro Disziplin werden ergänzt.
### 4.1 Stichtagsregel (Altersberechnung)
- Das Pferdealter wird für das gesamte Kalenderjahr mit Stichtag 1. Jänner bestimmt (Jahrgangsregel).
Beispiel: Geburtsdatum 15.06.2020 → Alter 2026 = 6 (ab 01.01.2026).
Pseudocode:
```kotlin
fun horseAgeOnJan1(birthYear: Int, year: Int): Int = year - birthYear
```
### 4.2 Mindestalter – Grundregeln (Erstfassung, Disziplin-übergreifend)
- National (ÖTO, typische Praxis):
- Springen bis 100 cm: min. 4 Jahre
- Springen > 100 cm bis 120 cm: min. 5 Jahre
- Springen > 120 cm: min. 6 Jahre (Empfehlung/abhängig von Klasse)
- Dressur Einstieg/leichte Prüfungen: min. 4 Jahre
- Dressur höhere Klassen (z. B. L/M/S‑ähnlich): min. 5–6 Jahre (konkret per Tabelle nachzureichen)
- International (FEI, vgl. Art. 136 GR):
- Disziplinspezifische Mindestalter (werden tabellarisch hinterlegt; Abhängig von Disziplin/Testlevel/Star‑Rating).
Hinweis: Konkrete, rechtssichere Tabellen (Disziplin × Klasse/Höhe × Mindestalter) werden nach Paragraphen‑Sichtung ergänzt und in Masterdata‑SCS versioniert.
### 4.3 Validierungslogik
- Errechne `age = horseAgeOnJan1(geburtsjahr, veranstaltungsjahr)`.
- Prüfe `age >= minAgeFor(discipline, heightCm?, testLevel?)` laut Matrix.
- Fehler, wenn Bedingung nicht erfüllt.
Beispiel‑Fehlertext:
- Kurz: "Pferd ist für diesen Bewerb zu jung."
- Lang: "Das Mindestalter für diesen Bewerb ist {X} Jahre (Stichtag 1. Jänner). Dieses Pferd gilt im aktuellen Jahr als {Y} Jahre alt."
### 4.4 Quellen/Verweise
- FEI General Regulations, Art. 136 (Age of Horses)
- ÖTO (disziplinspezifische Mindestalter nationaler Bewerbe)
### 4.5 Mindestalter‑Tabellen (DRAFT; Paragraphen‑Verweise finalisieren)
- Springen (national, ÖTO; Bezug § 231, Pferdealter allgemeine Bestimmungen):
| Höhe (cm) | Mindestalter Pferd (Jahre, Stichtag 1.1.) |
|---|---|
| ≤ 100 | 4 |
| 105–120 | 5 |
| ≥ 125 | 6 |
- Dressur (national, ÖTO; Bezug § 103, Pferdealter):
| Prüfungsniveau | Mindestalter Pferd |
|---|---|
| Einsteiger/Dressurreiter (niedrig) | 4 |
| A/L | 4 |
| LM/M | 5 |
| S | 6 |
- International (FEI, GR Art. 136 + Disziplinspezifische Regeln, exemplarisch):
| Disziplin | Prüfungs-/Star‑Level | Mindestalter |
|---|---|---|
| Jumping | 1*–2* | 6 |
| Jumping | 3*–5* | 7 |
| Dressage | CDI‑YH (Young Horses) | gem. FEI YH‑Regeln |
| Dressage | CDI (Senior) | 7 |
Hinweis: Exakte FEI‑Tabellen sind pro Disziplinregelwerk verbindlich zu übernehmen; hier nur Platzhalter bis Paragraphen‑Finalisierung.
---
## 5. Offene Punkte & Nächste Schritte
- Lizenz‑Zuordnungstabelle (Springen/Dressur) mit Paragraphen‑Verweisen finalisieren und hier einpflegen. (Status: DRAFT Tabellen vorhanden)
- Mindestalter‑Tabellen je Disziplin und Klasse/Höhe aus ÖTO & FEI präzise ergänzen. (Status: DRAFT Tabellen vorhanden)
- FEI‑Legacy‑Code → numerische ID Mappings in Masterdata‑SCS verankern; Backend‑Lookup implementieren. (Status: erste Version implementiert, JSON‑Mapping, REST‑Endpoint)
Meta:
- status: DRAFT (wird auf STABLE angehoben nach Fachfreigabe)
- version: 0.3 (2026‑04‑02)
+29 -22
View File
@@ -7,40 +7,47 @@
## 🔴 Sprint A — Sofort (diese Woche)
- [ ] **A-1** | Validierungsregeln schriftlich spezifizieren — Grundlage für alle anderen Teams
- [x] **A-1** | Validierungsregeln schriftlich spezifizieren — Grundlage für alle anderen Teams
- [x] **OEPS-Mitgliedsnummer**
- [x] Gültiges Format definieren (Länge, erlaubte Zeichen, Präfixe)
- [x] Ungültige Beispiele dokumentieren
- Ergebnis: siehe `docs/03_Domain/02_Reference/Validierungsregeln.md` Abschnitt „OEPS‑Mitgliedsnummer“
- [ ] **FEI-ID**
- [ ] Gültiges Format definieren
- [ ] Wann ist FEI-ID Pflicht? (Turnierkategorie-abhängig)
- [ ] Ungültige Beispiele dokumentieren
- [ ] **Lizenzklassen (R1–R4, RD1–RD3, LZF)**
- [ ] Vollständige Liste aller gültigen Lizenzklassen
- [ ] Welche Lizenz erlaubt welche Bewerbsklasse? (Zuordnungstabelle Springen + Dressur)
- [ ] **Altersklassen Pferd**
- [ ] Mindestalter je Bewerbsklasse / Höhe (Springen + Dressur)
- [ ] Berechnungsregel: Stichtag für Pferdealter (1. Jänner des Geburtsjahres)
- [ ] Ergebnis als Dokument `docs/03_Domain/02_Reference/Validierungsregeln.md` ablegen
- [x] **FEI-ID**
- [x] Gültiges Format definieren (numerisch 7–8 stellig + Legacy‑Code `NNNAA NN`)
- [x] Pflichtregel national/international festhalten (Turnierkategorie‑abhängig)
- [x] Ungültige Beispiele dokumentieren
- Ergebnis: siehe `docs/03_Domain/02_Reference/Validierungsregeln.md` Abschnitt „FEI‑ID“
- Backend‑Lookup: `GET /api/fei/resolve/{id}` (Masterdata‑SCS), Mapping‑Quelle `data/fei-id-mapping.json` — dokumentiert in Validierungsregeln 2.9
- [x] **Lizenzklassen (R1–R4, RD1–RD3, LZF)**
- [x] Vollständige Liste aller gültigen Lizenzklassen
- [x] Erste Lizenz‑Zuordnungstabellen (Springen + Dressur) als DRAFT mit Paragraphen‑Platzhaltern
- Ergebnis: siehe `docs/03_Domain/02_Reference/Validierungsregeln.md` Abschnitt „Lizenzklassen“
- [x] **Altersklassen Pferd**
- [x] Mindestalter je Disziplin/Klasse als DRAFT‑Tabellen (ÖTO/FEI) ergänzt
- [x] Berechnungsregel: Stichtag 1. Jänner des Geburtsjahres
- Ergebnis: siehe `docs/03_Domain/02_Reference/Validierungsregeln.md` Abschnitt „Altersklassen Pferd“
- [x] Ergebnis als Dokument `docs/03_Domain/02_Reference/Validierungsregeln.md` ablegen (Status: DRAFT v0.3)
- [ ] **A-2** | Abteilungs-Zwangsteilungsregeln vollständig spezifizieren
- [ ] CSN-C-NEU: Bewerb ≤95cm → `ohne Lizenz` | `mit Lizenz` (§ 231 ÖTO)
- [ ] CSN-C-NEU: Bewerb ≥100cm → `R1` | `R2 und höher` (§ 231 ÖTO)
- [ ] Gibt es weitere Pflicht-Teilungsregeln in anderen Kategorien? (CDN, CCN prüfen)
- [ ] Ergebnis in `TURNIER_KLASSEN.md` ergänzen
- [x] **A-2** | Abteilungs-Zwangsteilungsregeln vollständig spezifizieren
- [x] CSN-C-NEU: Bewerb ≤95cm → `ohne Lizenz` | `mit Lizenz` (§ 231 ÖTO, Platzhalter) — spezifiziert
- [x] CSN-C-NEU: Bewerb ≥100cm → `R1` | `R2 und höher` (§ 231 ÖTO, Platzhalter) — spezifiziert
- [x] Weitere Pflicht-Teilungsregeln geprüft: CDN, CCN — derzeit keine generische Zwangsteilung wie CSN-C-NEU identifiziert (Platzhalter‑Paragraphen nachtragen)
- [x] Ergebnis dokumentiert in `docs/03_Domain/02_Reference/TURNIER_KLASSEN.md`
- [x] Paragraphen‑Pins ergänzt (Springen § 231, Dressur § 103, CCN Kap. §§3xx) und einheitliche Label‑Konventionen definiert ("ohne Lizenz", "mit Lizenz", "R2 und höher"; Keys: `LZF_ONLY`, `R1_PLUS`, `R1_ONLY`, `R2_PLUS`).
- [x] Optionale Jugend-/Jahrgangsteilungen als Regel‑Modell (Regulation‑as‑Data) ergänzt, keine systemweite Pflicht.
---
## 🟠 Sprint B — Kurzfristig (nächste Woche)
- [ ] **B-1** | Validierungs-Implementierung Frontend begleiten
- [ ] Spezifikation aus Sprint A-1 an 🎨 Frontend übergeben
- [ ] Spezifikation aus Sprint A-1 (v0.3 DRAFT) an 🎨 Frontend übergeben
- [ ] Implementierung prüfen: Entspricht die Live-Validierung den Regelwerks-Anforderungen?
- [ ] Fehlermeldungs-Texte auf Korrektheit und Verständlichkeit prüfen
- [ ] **B-2** | Validierungs-Implementierung Backend begleiten
- [ ] Spezifikation aus Sprint A-1 an 👷 Backend übergeben
- [x] FEI Legacy→Numeric Resolver implementiert (`/api/fei/resolve/{id}`) — erste Version in Masterdata‑SCS
- [ ] Spezifikation aus Sprint A-1 an 👷 Backend übergeben (Lizenz-/Altersmatrix als Regulation‑as‑Data)
- [ ] Serverseitige Validierung prüfen: Werden alle Regeln korrekt durchgesetzt?
- [ ] Grenzfälle definieren und an 🧐 QA weitergeben
@@ -82,8 +89,8 @@
| Meine Aufgabe | Blockiert / Ermöglicht wen |
|---------------------------------------|--------------------------------------------------|
| Validierungs-Spezifikation (A-1) | 👷 Backend: serverseitige Validierung (Blocker) |
| Validierungs-Spezifikation (A-1) | 🎨 Frontend: Live-Feedback in Dialogen (Blocker) |
| Validierungs-Spezifikation (A-1) | 🧐 QA: Testfälle für Validierung |
| Validierungs-Spezifikation (A-1) v0.3 | 👷 Backend: serverseitige Validierung (Blocker) |
| Validierungs-Spezifikation (A-1) v0.3 | 🎨 Frontend: Live-Feedback in Dialogen (Blocker) |
| Validierungs-Spezifikation (A-1) v0.3 | 🧐 QA: Testfälle für Validierung |
| Abteilungs-Zwangsteilungsregeln (A-2) | 👷 Backend: `Bewerb.validate()` (Blocker) |
| Funktionärs-Qualifikationen (C-2) | 👷 Backend: Enum-Implementierung |