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:
Stefan Mogeritsch 2026-04-02 18:36:59 +02:00
parent bbe5b1a357
commit 4ae701d969
8 changed files with 540 additions and 25 deletions

View File

@ -0,0 +1,29 @@
package at.mocode.masterdata.domain.service
/**
* Auflösung von FEI-IDs: akzeptiert numerische IDs (PassThrough) und LegacyReferenzcodes
* und liefert wenn bekannt die zugehörige numerische FEIID zurück.
*/
interface FeiIdResolver {
/**
* Löst eine eingegebene FEIKennung auf.
*
* @param input Benutzer-/Importeingabe (numerisch 78 Stellen oder LegacyCode 3Z+2B+2Z)
* @return [FeiIdResolution] mit normalisierter numerischer ID, oder null wenn unbekannt/ungültig
*/
fun resolve(input: String): FeiIdResolution?
}
/**
* Ergebnis der FEIID Auflösung.
*
* @property normalizedNumericId Numerische FEIID (78 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
)

View File

@ -31,6 +31,7 @@ dependencies {
implementation(libs.spring.boot.starter.web) implementation(libs.spring.boot.starter.web)
implementation(libs.spring.boot.starter.validation) implementation(libs.spring.boot.starter.validation)
implementation(libs.spring.boot.starter.actuator) implementation(libs.spring.boot.starter.actuator)
implementation(libs.jackson.module.kotlin)
//implementation(libs.springdoc.openapi.starter.webmvc.ui) //implementation(libs.springdoc.openapi.starter.webmvc.ui)
// Ktor Server (für SCS: eigener kleiner HTTP-Server pro Kontext) // Ktor Server (für SCS: eigener kleiner HTTP-Server pro Kontext)

View File

@ -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
)
)
}
}
}

View File

@ -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 InMemory Implementierung des [FeiIdResolver],
* lädt ein JSONMapping 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: PassThrough
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
}
}

View File

@ -0,0 +1,6 @@
{
"104FE22": "10011469",
"103RW04": "10019075",
"102UB51": "10028445",
"104UD89": "10011111"
}

View File

@ -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änenModell: `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 (ParagraphenPins zur Nachverfolgung):
- Springen: ÖTO 2026, Kapitel „Springen“, § 231 Abs. 13 (CSNCNEU Teilungsregeln) [PIN: OETO-SPR-231]
- Dressur: ÖTO 2026, Kapitel „Dressur“, § 103 Abs. 25 (Teilnahme/Leistungsstufen) [PIN: OETO-DRS-103]
- Vielseitigkeit: ÖTO 2026, Kapitel „Vielseitigkeit“, §§ 3xx (Teilnahme/Abteilungen) [PIN: OETO-VS-3XX]
Hinweis Rechtslage: Die obigen „ParagraphenPins“ verankern die Stellen im ÖTO. Exakte Absatz-/Ziffernangaben werden nach juristischer Finalisierung ergänzt. Inhaltliche Logik entspricht dem Stand der Praxis (CSNCNEU) 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 CNEU
Gültig für Bewerbe der Kategorie „CSNCNEU“.
### 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 „LizenzGruppe“ markiert.
- Veranstalter dürfen enger teilen (z. B. zusätzliche Jugendabteilungen), nicht jedoch lockern (PflichtZweiteilung muss bestehen bleiben).
### 2.2 Maschinenlesbare Spezifikation
Tabelle: CSNCNEU 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 PflichtAbteilungen):
```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 CSNCNEU 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 (CSNCNEU).“
---
## 3. CDN (Dressur national) — Prüfung weiterer Zwangsteilungen
Status: geprüft. Nach aktuellem Stand bestehen in den Einsteiger/NiedrigKlassen keine zwingenden LizenzZwangsteilungen analog zu CSNCNEU. Ü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 DressurAbschnitte 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 PflichtTeilungsregeln 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 „CSNCNEU → PflichtAbteilungen“ als RegulationasData hinterlegen (z. B. `reg_forced_divisions` mit Feldern: `category`, `height_threshold`, `division_key`, `allowed_licenses`).
- Serverseitige Validierung beim Anlegen/Bearbeiten eines CSNCNEU Bewerbs: genau zwei Abteilungen erzwingen, Labels/AllowedSets prüfen.
- Nennvalidierung: Lizenz des Reiters ∈ `allowedLicenses` der Zielabteilung.
- Frontend:
- Wizard/Editor legt bei CSNCNEU automatisch beide Abteilungen an (konfigurierbare Labels).
- LiveHinweis, wenn eine Abteilung fehlt oder falsche Lizenzen zugeordnet sind.
---
## 6. Einheitliche LabelKonventionen für Abteilungen
Ziel: Einheitliche, i18nfähige Benennung in UI, Exporten und Validierung. Deutsche StandardLabels 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 RegelSet, kein ÖTOPflichtumfang wie bei CSNCNEU. Veranstalter können zusätzlich nach Jahrgängen/Jugendklassen teilen, sofern ÖTOkonform (vgl. Dressur § 103 und disziplinspezifische Jugendbestimmungen).
Modellierung als optionale Regeln:
- Datenmodell (Beispiel als RegulationasData):
- 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/ParagraphenBezug)
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)
```
UXHinweistexte:
- „Optionale Jugendabteilung aktiv: Nur Athlet:innen des Jahrgangsbereichs {label} werden hier gewertet.“
- „Diese Abteilung ist optional; PflichtZwangsteilung (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.
- BackendSeed: `reg_forced_divisions` und `reg_optional_divisions` befüllen; Keys/Labels gemäß Abschnitt 6 verwenden.
- FE/UX: i18nMapping für DivisionLabels bereitstellen; EditorPresets für CSNCNEU und optionale Jugendabteilungen.

View File

@ -86,14 +86,212 @@ fun validateOepsId(input: String): Boolean {
--- ---
## 2. FEIID ## 2. FEIID
ToDo: Wird in A1 (weiterer Unterpunkt) spezifiziert. Status: Draft auf Basis FEI General Regulations (Art. 113114) 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 FEIrelevanten 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: 78stellige numerische FEIIDs (z. B. `10011469`).
- Legacy/Referenzcode (in LegacyDaten 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 FEIID aufgelöst (siehe 2.5).
Nicht erlaubt:
- Leerzeichen, Trennzeichen, gemischte Schreibweisen mit Präfixen (z. B. `FEI-10011469`), alphanumerische Mischformen außerhalb des obigen LegacyMusters.
### 2.3 PflichtfelderRegel (Wann ist FEIID erforderlich?)
- International (FEIEvents: CI/CSI/CDI/CCI/CIO/CH/…):
- Athlet: FEIID Pflicht.
- Pferd: FEIID Pflicht (inkl. FEIPass/Microchip gem. FEIRegeln, vgl. Art. 114, 137 FEI GR).
- National (ÖTOEvents: CN/CSN/CDN/CCN):
- Athlet: FEIID optional (nur wenn FEIregistriert).
- Pferd: FEIID optional (nur wenn FEIregistriert).
- Ausnahme: Wenn eine nationale Prüfung als FEIqualifikationsrelevant ausgewiesen ist, kann FEIID für Datenexporte empfohlen/erforderlich sein (Veranstalterhinweis).
Hinweis: Die konkrete Pflicht koppeln wir im System an das Feld „Turnierkategorie“ und Disziplin, konfigurierbar per RegelSet.
### 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 FEIID (`[0-9]{7,8}`) als String ohne Trennzeichen.
- LegacyReferenzcode wird sofern möglich vor Speicherung via Mapping/Lookup in numerische FEIID überführt. Falls kein Mapping möglich, speichern als eingegeben plus `source_format = LEGACY_CODE`.
### 2.6 PseudocodeValidierung
```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 (UXTexte)
- Kurz: "Ungültige FEIID. Erlaubt sind 78 Ziffern (z. B. 10011469)."
- Lang: "Bitte eine gültige FEIID eingeben: 78 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 FrontendStores (FEIIDs): `frontend/shells/meldestelle-desktop/.../Stores.kt`
### 2.9 BackendLookup (MasterdataSCS)
- Endpoint: `GET /api/fei/resolve/{id}`
- Eingabe: `{id}` numerisch (`^[0-9]{7,8}$`) oder LegacyCode (`^[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 }`
- MappingQuelle: `backend/services/masterdata/masterdata-service/src/main/resources/data/fei-id-mapping.json` (kann später aus DB gespeist werden).
--- ---
## 3. Lizenzklassen (R1R4, RD1RD3, LZF) ## 3. Lizenzklassen (R1R4, RD1RD3, LZF)
ToDo: Vollständige Liste und Zuordnung in A1 (weiterer Unterpunkt). Status: Draft basierend auf ÖTOPraxis und ZNSLizenzdaten. Detaillierte ParagraphenZitate werden nachgereicht (A2/A3 Arbeiten verknüpft).
### 3.1 Katalog gültiger Lizenzklassen
- Reiten Springen (RKlassen): `R1`, `R2`, `R3`, `R4`
- Dressur Reiten (RDKlassen): `RD1`, `RD2`, `RD3`
- Lizenzfrei/ohne Lizenz Kennzeichnung: `LZF` (für bewerbsbezogene Abteilung „ohne Lizenz“)
Erweiterbarkeit: Weitere Spezial/Jugend oder FahrerLizenzen 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 A2 (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 ÖTOParagraphen 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 (UXTexte)
- 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)
- ZNSLizenzdaten: `docs/OePS/ZNS/LIZENZ01.dat` (Datenquelle, strukturierter Export) Parsing/Anlage in MasterdataSCS.
- Teilungs-/Warnlogik: `docs/03_Domain/02_Reference/OETO_Regelwerk/Warn-Logik-Spezifikation-competition-context.md`
### 3.6 LizenzZuordnungstabelle (DRAFT, final mit ParagraphenVerweisen)
- 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 |
| 105110 | R1, R2+ (Empf. R2) | § 231 |
| 115120 | R2+ | § 231 |
| 125135 | 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 CSNCNEU sind in A2 separat spezifiziert und ergänzen die obige Tabelle.
--- ---
## 4. Altersklassen Pferd ## 4. Altersklassen Pferd
ToDo: Mindestalter je Bewerbsklasse / Höhe und Stichtagsregel (1. Jänner) folgt in A1 (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. 56 Jahre (konkret per Tabelle nachzureichen)
- International (FEI, vgl. Art. 136 GR):
- Disziplinspezifische Mindestalter (werden tabellarisch hinterlegt; Abhängig von Disziplin/Testlevel/StarRating).
Hinweis: Konkrete, rechtssichere Tabellen (Disziplin × Klasse/Höhe × Mindestalter) werden nach ParagraphenSichtung ergänzt und in MasterdataSCS versioniert.
### 4.3 Validierungslogik
- Errechne `age = horseAgeOnJan1(geburtsjahr, veranstaltungsjahr)`.
- Prüfe `age >= minAgeFor(discipline, heightCm?, testLevel?)` laut Matrix.
- Fehler, wenn Bedingung nicht erfüllt.
BeispielFehlertext:
- 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 MindestalterTabellen (DRAFT; ParagraphenVerweise finalisieren)
- Springen (national, ÖTO; Bezug § 231, Pferdealter allgemeine Bestimmungen):
| Höhe (cm) | Mindestalter Pferd (Jahre, Stichtag 1.1.) |
|---|---|
| ≤ 100 | 4 |
| 105120 | 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-/StarLevel | Mindestalter |
|---|---|---|
| Jumping | 1*2* | 6 |
| Jumping | 3*5* | 7 |
| Dressage | CDIYH (Young Horses) | gem. FEI YHRegeln |
| Dressage | CDI (Senior) | 7 |
Hinweis: Exakte FEITabellen sind pro Disziplinregelwerk verbindlich zu übernehmen; hier nur Platzhalter bis ParagraphenFinalisierung.
---
## 5. Offene Punkte & Nächste Schritte
- LizenzZuordnungstabelle (Springen/Dressur) mit ParagraphenVerweisen finalisieren und hier einpflegen. (Status: DRAFT Tabellen vorhanden)
- MindestalterTabellen je Disziplin und Klasse/Höhe aus ÖTO & FEI präzise ergänzen. (Status: DRAFT Tabellen vorhanden)
- FEILegacyCode → numerische ID Mappings in MasterdataSCS verankern; BackendLookup implementieren. (Status: erste Version implementiert, JSONMapping, RESTEndpoint)
Meta:
- status: DRAFT (wird auf STABLE angehoben nach Fachfreigabe)
- version: 0.3 (20260402)

View File

@ -7,40 +7,47 @@
## 🔴 Sprint A — Sofort (diese Woche) ## 🔴 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] **OEPS-Mitgliedsnummer**
- [x] Gültiges Format definieren (Länge, erlaubte Zeichen, Präfixe) - [x] Gültiges Format definieren (Länge, erlaubte Zeichen, Präfixe)
- [x] Ungültige Beispiele dokumentieren - [x] Ungültige Beispiele dokumentieren
- Ergebnis: siehe `docs/03_Domain/02_Reference/Validierungsregeln.md` Abschnitt „OEPSMitgliedsnummer“ - Ergebnis: siehe `docs/03_Domain/02_Reference/Validierungsregeln.md` Abschnitt „OEPSMitgliedsnummer“
- [ ] **FEI-ID** - [x] **FEI-ID**
- [ ] Gültiges Format definieren - [x] Gültiges Format definieren (numerisch 78 stellig + LegacyCode `NNNAA NN`)
- [ ] Wann ist FEI-ID Pflicht? (Turnierkategorie-abhängig) - [x] Pflichtregel national/international festhalten (Turnierkategorieabhängig)
- [ ] Ungültige Beispiele dokumentieren - [x] Ungültige Beispiele dokumentieren
- [ ] **Lizenzklassen (R1R4, RD1RD3, LZF)** - Ergebnis: siehe `docs/03_Domain/02_Reference/Validierungsregeln.md` Abschnitt „FEIID“
- [ ] Vollständige Liste aller gültigen Lizenzklassen - BackendLookup: `GET /api/fei/resolve/{id}` (MasterdataSCS), MappingQuelle `data/fei-id-mapping.json` — dokumentiert in Validierungsregeln 2.9
- [ ] Welche Lizenz erlaubt welche Bewerbsklasse? (Zuordnungstabelle Springen + Dressur) - [x] **Lizenzklassen (R1R4, RD1RD3, LZF)**
- [ ] **Altersklassen Pferd** - [x] Vollständige Liste aller gültigen Lizenzklassen
- [ ] Mindestalter je Bewerbsklasse / Höhe (Springen + Dressur) - [x] Erste LizenzZuordnungstabellen (Springen + Dressur) als DRAFT mit ParagraphenPlatzhaltern
- [ ] Berechnungsregel: Stichtag für Pferdealter (1. Jänner des Geburtsjahres) - Ergebnis: siehe `docs/03_Domain/02_Reference/Validierungsregeln.md` Abschnitt „Lizenzklassen“
- [ ] Ergebnis als Dokument `docs/03_Domain/02_Reference/Validierungsregeln.md` ablegen - [x] **Altersklassen Pferd**
- [x] Mindestalter je Disziplin/Klasse als DRAFTTabellen (Ö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 - [x] **A-2** | Abteilungs-Zwangsteilungsregeln vollständig spezifizieren
- [ ] CSN-C-NEU: Bewerb ≤95cm → `ohne Lizenz` | `mit Lizenz` (§ 231 ÖTO) - [x] CSN-C-NEU: Bewerb ≤95cm → `ohne Lizenz` | `mit Lizenz` (§ 231 ÖTO, Platzhalter) — spezifiziert
- [ ] CSN-C-NEU: Bewerb ≥100cm → `R1` | `R2 und höher` (§ 231 ÖTO) - [x] CSN-C-NEU: Bewerb ≥100cm → `R1` | `R2 und höher` (§ 231 ÖTO, Platzhalter) — spezifiziert
- [ ] Gibt es weitere Pflicht-Teilungsregeln in anderen Kategorien? (CDN, CCN prüfen) - [x] Weitere Pflicht-Teilungsregeln geprüft: CDN, CCN — derzeit keine generische Zwangsteilung wie CSN-C-NEU identifiziert (PlatzhalterParagraphen nachtragen)
- [ ] Ergebnis in `TURNIER_KLASSEN.md` ergänzen - [x] Ergebnis dokumentiert in `docs/03_Domain/02_Reference/TURNIER_KLASSEN.md`
- [x] ParagraphenPins ergänzt (Springen § 231, Dressur § 103, CCN Kap. §§3xx) und einheitliche LabelKonventionen definiert ("ohne Lizenz", "mit Lizenz", "R2 und höher"; Keys: `LZF_ONLY`, `R1_PLUS`, `R1_ONLY`, `R2_PLUS`).
- [x] Optionale Jugend-/Jahrgangsteilungen als RegelModell (RegulationasData) ergänzt, keine systemweite Pflicht.
--- ---
## 🟠 Sprint B — Kurzfristig (nächste Woche) ## 🟠 Sprint B — Kurzfristig (nächste Woche)
- [ ] **B-1** | Validierungs-Implementierung Frontend begleiten - [ ] **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? - [ ] Implementierung prüfen: Entspricht die Live-Validierung den Regelwerks-Anforderungen?
- [ ] Fehlermeldungs-Texte auf Korrektheit und Verständlichkeit prüfen - [ ] Fehlermeldungs-Texte auf Korrektheit und Verständlichkeit prüfen
- [ ] **B-2** | Validierungs-Implementierung Backend begleiten - [ ] **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 MasterdataSCS
- [ ] Spezifikation aus Sprint A-1 an 👷 Backend übergeben (Lizenz-/Altersmatrix als RegulationasData)
- [ ] Serverseitige Validierung prüfen: Werden alle Regeln korrekt durchgesetzt? - [ ] Serverseitige Validierung prüfen: Werden alle Regeln korrekt durchgesetzt?
- [ ] Grenzfälle definieren und an 🧐 QA weitergeben - [ ] Grenzfälle definieren und an 🧐 QA weitergeben
@ -82,8 +89,8 @@
| Meine Aufgabe | Blockiert / Ermöglicht wen | | Meine Aufgabe | Blockiert / Ermöglicht wen |
|---------------------------------------|--------------------------------------------------| |---------------------------------------|--------------------------------------------------|
| Validierungs-Spezifikation (A-1) | 👷 Backend: serverseitige Validierung (Blocker) | | Validierungs-Spezifikation (A-1) v0.3 | 👷 Backend: serverseitige Validierung (Blocker) |
| Validierungs-Spezifikation (A-1) | 🎨 Frontend: Live-Feedback in Dialogen (Blocker) | | Validierungs-Spezifikation (A-1) v0.3 | 🎨 Frontend: Live-Feedback in Dialogen (Blocker) |
| Validierungs-Spezifikation (A-1) | 🧐 QA: Testfälle für Validierung | | Validierungs-Spezifikation (A-1) v0.3 | 🧐 QA: Testfälle für Validierung |
| Abteilungs-Zwangsteilungsregeln (A-2) | 👷 Backend: `Bewerb.validate()` (Blocker) | | Abteilungs-Zwangsteilungsregeln (A-2) | 👷 Backend: `Bewerb.validate()` (Blocker) |
| Funktionärs-Qualifikationen (C-2) | 👷 Backend: Enum-Implementierung | | Funktionärs-Qualifikationen (C-2) | 👷 Backend: Enum-Implementierung |