From 4ae701d969b092947a1184c8f5167c7dd4dd3fb2 Mon Sep 17 00:00:00 2001 From: StefanMoCoAt Date: Thu, 2 Apr 2026 18:36:59 +0200 Subject: [PATCH] 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. --- .../domain/service/FeiIdResolver.kt | 29 +++ .../masterdata-service/build.gradle.kts | 1 + .../masterdata/service/fei/FeiIdController.kt | 50 +++++ .../service/fei/FeiIdResolverImpl.kt | 43 ++++ .../main/resources/data/fei-id-mapping.json | 6 + .../03_Domain/02_Reference/TURNIER_KLASSEN.md | 181 ++++++++++++++++ .../02_Reference/Validierungsregeln.md | 204 +++++++++++++++++- docs/04_Agents/Roadmaps/Rulebook_Roadmap.md | 51 +++-- 8 files changed, 540 insertions(+), 25 deletions(-) create mode 100644 backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/service/FeiIdResolver.kt create mode 100644 backend/services/masterdata/masterdata-service/src/main/kotlin/at/mocode/masterdata/service/fei/FeiIdController.kt create mode 100644 backend/services/masterdata/masterdata-service/src/main/kotlin/at/mocode/masterdata/service/fei/FeiIdResolverImpl.kt create mode 100644 backend/services/masterdata/masterdata-service/src/main/resources/data/fei-id-mapping.json create mode 100644 docs/03_Domain/02_Reference/TURNIER_KLASSEN.md diff --git a/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/service/FeiIdResolver.kt b/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/service/FeiIdResolver.kt new file mode 100644 index 00000000..3df6a053 --- /dev/null +++ b/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/service/FeiIdResolver.kt @@ -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 +) diff --git a/backend/services/masterdata/masterdata-service/build.gradle.kts b/backend/services/masterdata/masterdata-service/build.gradle.kts index e5951767..9f672105 100644 --- a/backend/services/masterdata/masterdata-service/build.gradle.kts +++ b/backend/services/masterdata/masterdata-service/build.gradle.kts @@ -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) diff --git a/backend/services/masterdata/masterdata-service/src/main/kotlin/at/mocode/masterdata/service/fei/FeiIdController.kt b/backend/services/masterdata/masterdata-service/src/main/kotlin/at/mocode/masterdata/service/fei/FeiIdController.kt new file mode 100644 index 00000000..025618b6 --- /dev/null +++ b/backend/services/masterdata/masterdata-service/src/main/kotlin/at/mocode/masterdata/service/fei/FeiIdController.kt @@ -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 { + 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 + ) + ) + } + } +} diff --git a/backend/services/masterdata/masterdata-service/src/main/kotlin/at/mocode/masterdata/service/fei/FeiIdResolverImpl.kt b/backend/services/masterdata/masterdata-service/src/main/kotlin/at/mocode/masterdata/service/fei/FeiIdResolverImpl.kt new file mode 100644 index 00000000..02507144 --- /dev/null +++ b/backend/services/masterdata/masterdata-service/src/main/kotlin/at/mocode/masterdata/service/fei/FeiIdResolverImpl.kt @@ -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 by lazy { + val res = ClassPathResource("data/fei-id-mapping.json") + if (!res.exists()) return@lazy emptyMap() + val mapper = jacksonObjectMapper() + res.inputStream.use { input -> + mapper.readValue>(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 + } +} diff --git a/backend/services/masterdata/masterdata-service/src/main/resources/data/fei-id-mapping.json b/backend/services/masterdata/masterdata-service/src/main/resources/data/fei-id-mapping.json new file mode 100644 index 00000000..90669809 --- /dev/null +++ b/backend/services/masterdata/masterdata-service/src/main/resources/data/fei-id-mapping.json @@ -0,0 +1,6 @@ +{ + "104FE22": "10011469", + "103RW04": "10019075", + "102UB51": "10028445", + "104UD89": "10011111" +} diff --git a/docs/03_Domain/02_Reference/TURNIER_KLASSEN.md b/docs/03_Domain/02_Reference/TURNIER_KLASSEN.md new file mode 100644 index 00000000..c3b3fa52 --- /dev/null +++ b/docs/03_Domain/02_Reference/TURNIER_KLASSEN.md @@ -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) + +fun forcedDivisionsCsnCNeu(heightCm: Int): List = + 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? = 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. diff --git a/docs/03_Domain/02_Reference/Validierungsregeln.md b/docs/03_Domain/02_Reference/Validierungsregeln.md index cefa32a6..0e519de0 100644 --- a/docs/03_Domain/02_Reference/Validierungsregeln.md +++ b/docs/03_Domain/02_Reference/Validierungsregeln.md @@ -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) diff --git a/docs/04_Agents/Roadmaps/Rulebook_Roadmap.md b/docs/04_Agents/Roadmaps/Rulebook_Roadmap.md index b4e7e6f9..127111e1 100644 --- a/docs/04_Agents/Roadmaps/Rulebook_Roadmap.md +++ b/docs/04_Agents/Roadmaps/Rulebook_Roadmap.md @@ -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 |