From 7bf89c58d3cd998709becac41eb601efa1909e91 Mon Sep 17 00:00:00 2001 From: StefanMoCoAt Date: Mon, 6 Apr 2026 23:51:56 +0200 Subject: [PATCH] Refactor license matrix and tokenizer logic: rename `LicenseTable` to `LizenzTable`, replace `LicenseMatrixService` with `LizenzMatrixService`, enhance tokenizer with normalized and fallback token handling, improve ZNS import for license extraction, and update related documentation. --- CHANGELOG.md | 32 ++++ .../mocode/zns/importer/ZnsImportService.kt | 2 +- .../masterdata/docs/REITER_LIZENZEN.md | 142 +++++++++++++++--- .../mocode/masterdata/domain/model/Reiter.kt | 18 ++- .../model/{ReitLizenz.kt => Reiterlizenz.kt} | 2 +- ...censeRepository.kt => LizenzRepository.kt} | 6 +- .../service/LicenseMatrixServiceImpl.kt | 57 ------- ...atrixService.kt => LizenzMatrixService.kt} | 2 +- .../domain/service/LizenzMatrixServiceImpl.kt | 72 +++++++++ ...viceTest.kt => LiznezMatrixServiceTest.kt} | 4 +- .../ExposedRegulationRepository.kt | 20 +-- .../{LicenseTable.kt => LizenzTable.kt} | 2 +- .../MasterdataLicenseExposedRepository.kt | 12 +- .../RegulationSeedVerificationTest.kt | 4 +- .../config/MasterdataDatabaseConfiguration.kt | 4 +- .../service/config/ReiterlizenzenSeeder.kt | 79 ++++++++++ ...13__Cleanup_and_Standardize_Masterdata.sql | 76 +++++++--- .../service/ZnsImportServiceApplication.kt | 2 +- .../service/config/RepositoryConfiguration.kt | 2 +- .../at/mocode/zns/parser/ZnsReiterParser.kt | 30 +++- .../zns/parser/ZnsReiterlicenseTokenizer.kt | 88 +++++++++++ .../mocode/zns/parser/LicenseTokenizerTest.kt | 47 ++++++ docs/03_Domain/00_Glossary.md | 4 +- .../01_Glossary/Ubiquitous_Language.md | 2 +- ...2026_A-Teil-Allgemeiner-Teil_16-12-2025.md | 41 +++++ 25 files changed, 612 insertions(+), 138 deletions(-) rename backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/model/{ReitLizenz.kt => Reiterlizenz.kt} (94%) rename backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/repository/{MasterdataLicenseRepository.kt => LizenzRepository.kt} (72%) delete mode 100644 backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/service/LicenseMatrixServiceImpl.kt rename backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/service/{LicenseMatrixService.kt => LizenzMatrixService.kt} (97%) create mode 100644 backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/service/LizenzMatrixServiceImpl.kt rename backend/services/masterdata/masterdata-domain/src/commonTest/kotlin/at/mocode/masterdata/domain/service/{LicenseMatrixServiceTest.kt => LiznezMatrixServiceTest.kt} (97%) rename backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/{LicenseTable.kt => LizenzTable.kt} (96%) create mode 100644 backend/services/masterdata/masterdata-service/src/main/kotlin/at/mocode/masterdata/service/config/ReiterlizenzenSeeder.kt create mode 100644 core/zns-parser/src/commonMain/kotlin/at/mocode/zns/parser/ZnsReiterlicenseTokenizer.kt create mode 100644 core/zns-parser/src/commonTest/kotlin/at/mocode/zns/parser/LicenseTokenizerTest.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f22ab71..c87f5f89 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,8 +15,25 @@ Versionierung folgt [Semantic Versioning](https://semver.org/lang/de/). ## [Unreleased] +### Geändert +- Masterdata/Domain: Umbenennungen zur Vereinheitlichung der Terminologie (DE): + - `MasterdataLicenseRepository` → `LizenzRepository` + - `LicenseMatrixService` → `LizenzMatrixService` + - `LicenseMatrixServiceImpl` → `LizenzMatrixServiceImpl` + - Test: `LicenseMatrixServiceTest` → `LiznezMatrixServiceTest` (exakt nach Vorgabe) + - Infrastructure (Exposed): `LicenseTable` → `LizenzTable` +- Docs: Begriff „reit_lizenzen“ → „reiterlizenzen“ in Glossar/UL konsolidiert. + ### Hinzugefügt +- Masterdata: Automatisches Seeding aller Reiterlizenzen (license_matrix) beim Start des `masterdata-service` via `ReiterlizenzenSeeder` (idempotent; SPRINGEN: LIZENZFREI,R1–R4; DRESSUR: LIZENZFREI,RD1–RD3). + +- **ZNS-Import (LIZENZ01.dat):** Robuster Lizenz-Tokenizer und Normalizer implementiert. + - Erkennung: `RD1..RD4`, `R1..R4`, `S1..S4`, `D2..D4`, Kombis `R{n}D{m}`, `R{n}S{k}`, `RDS4` (rechts-/letztes Vorkommen gewinnt). + - Normalisierung: `S*→R*`, `D*→RD*`, `RD4→RD3` (bis Enum verfügbar), `R{n}S{k}→Rmax(n,k)`, `R{n}D{m}→R{n}+RD{m}`. + - Integration: `ZnsReiterParser` füllt `lizenzen`-Liste (1:n) entsprechend und leitet `lizenzKlasse` bei fehlendem 4‑Spalten‑Code aus Token ab. + - QA: Neue Unit-Tests (Tokenizer) für Beispiele `R2S3`, `R2D4`, `RD2` u. a.; alle Parser-Tests grün. + - **Core:** Modularisierte ZNS-Parser eingeführt (`ZnsVereinParser`, `ZnsReiterParser`, `ZnsPferdParser`, `ZnsFunktionaerParser`) zur Verbesserung der Wartbarkeit und Unterstützung von Einzelimporten. - **Fix:** SQL-Migrationsfehler in `V010` behoben, indem die Umbenennung der Spalte `name` in `verein_name` durch einen idempotenten `DO`-Block abgesichert wurde (behebt "Unable to resolve column 'name'"). - **Infrastructure:** Datenbank-Migration `V010` hinzugefügt, um das Schema final mit den `Exposed`-Modellen zu synchronisieren. @@ -44,6 +61,21 @@ Versionierung folgt [Semantic Versioning](https://semver.org/lang/de/). - **Masterdata/API:** Fehlendes Interface-Mapping ergänzt: `RegulationRepository` enthält nun `findAllTurnierklassen()`; `ExposedRegulationRepository` implementiert die Methode und `RegulationController` kompiliert wieder. - **ZNS-Import:** `AltersklassenExposedRepository` korrigiert (richtiger Domain-Typ `AltersklasseDefinition`, Mapping von `SparteE` und Zeitstempeln). +- **Migration V013:** Idempotent und robust gemacht. Alle `ALTER TABLE ... RENAME`-Operationen laufen nun nur, wenn die Quell-Tabelle existiert (Fix für "Unable to resolve table 'bundesland'/'turnierklasse'"). +- **Lizenz-Validierung:** `LicenseMatrixServiceImpl` um Cross-Discipline-Mapping R↔RD (ÖTO-Äquivalenzen) erweitert. Damit funktionieren Fälle wie Dressur-Starts mit Spring-Lizenz (R1→RD1, R2→RD2, R3/R4→RD3) bzw. umgekehrt konsistent. + +- **Domain:** Striktere Spartenlizenz-Prüfung in `Reiter.hasLizenzForSparte` implementiert (RD1..RD3 nur DRESSUR; R1..R4 nur SPRINGEN). Behebt Testfehler „isEligible verweigert Start ohne passende Spartenlizenz“ im `LicenseMatrixServiceTest`. + +### Dokumentation +- **Masterdata/Docs:** `REITER_LIZENZEN.md` überarbeitet: + - Strikte Sparten-Trennung dokumentiert (RD1..RD3 nur Dressur; R1..R4 nur Springen). + - Dressur-Tabelle korrigiert (R-Lizenzen entfernen; RD-Pflicht je Klasse). + - Validierungslogik ergänzt (2-stufig: Spartenlizenz → Max-Turnierklasse; R↔RD Mapping nur zur Kappung, nicht zur Eligibility). + - Vielseitigkeit (CCN/CCI) ergänzt: kumulative Anforderungen (Dressur RD* UND Springen R* je Klasse); Startkartenregel für Einsteiger. + - Fahren (CAN/CAI) ergänzt: aktueller Systemzustand ohne `F*`‑Lizenzen dokumentiert; Teilnahme über Startkarte/Ausschreibung, geplante Enum‑Erweiterung vermerkt. + - §15‑Tabelle (kompakt) integriert und auf ÖTO‑Referenz verlinkt; Bedeutungen „B,C“ und „LP“ erläutert. Hinweis aufgenommen, dass `RD4` derzeit nicht als Enum vorhanden ist und wie `RD3` behandelt wird. + - Kombinationsreihen gemäß §15 ergänzt: `R1S2`, `R1S3`, `R1S4`, `R2S3`, `R2S4`, `R3S4` (neuer Unterabschnitt 2.6 mit Tabelle, identische Spalten wie 2.5). + ### Behoben - **Masterdata:** Qualifikations-Management für Funktionäre (Richter/Parcoursbauer) professionalisiert: Umstellung von unstrukturiertem Text auf offizielle ÖTO/FEI Master-Daten Referenzen (`QualifikationMasterTable`). diff --git a/backend/infrastructure/zns-importer/src/main/kotlin/at/mocode/zns/importer/ZnsImportService.kt b/backend/infrastructure/zns-importer/src/main/kotlin/at/mocode/zns/importer/ZnsImportService.kt index e45c9699..cf263023 100644 --- a/backend/infrastructure/zns-importer/src/main/kotlin/at/mocode/zns/importer/ZnsImportService.kt +++ b/backend/infrastructure/zns-importer/src/main/kotlin/at/mocode/zns/importer/ZnsImportService.kt @@ -40,7 +40,7 @@ class ZnsImportService( private val funktionaerRepository: FunktionaerRepository, private val landRepository: LandRepository, private val bundeslandRepository: BundeslandRepository, - private val licenseRepository: MasterdataLicenseRepository? = null, + private val licenseRepository: LizenzRepository? = null, private val altersklassenRepository: AltersklassenRepository? = null ) { diff --git a/backend/services/masterdata/docs/REITER_LIZENZEN.md b/backend/services/masterdata/docs/REITER_LIZENZEN.md index 248288cf..d1d738c6 100644 --- a/backend/services/masterdata/docs/REITER_LIZENZEN.md +++ b/backend/services/masterdata/docs/REITER_LIZENZEN.md @@ -1,7 +1,7 @@ # 📜 Reiter-Lizenzen & Startberechtigungen (OEPS) Diese Dokumentation beschreibt die verschiedenen Lizenzstufen des **OEPS (Österreichischer Pferdesportverband)** und die -daraus resultierenden Startberechtigungen für die Sparten **Dressur (CDN)** und **Springen (CSN)** gemäß ÖTO 2026. +daraus resultierenden Startberechtigungen für die Sparten **Dressur (CDN)**, **Springen (CSN)**, **Vielseitigkeit (CCN/CCI)** und **Fahren (CAN/CAI)** gemäß ÖTO 2026. ## 1. Lizenztypen & Klassen @@ -11,10 +11,10 @@ Prüfungen antreten darf. | Code | Bezeichnung | Beschreibung | ZNS-Mapping | |:--------|:-----------------|:-------------------------------------------------------|:-------------| | **LZF** | Lizenzfrei | Nur Startkarte oder Reiterpass vorhanden. | `LIZENZFREI` | -| **R1** | Reiter-Lizenz 1 | Einstiegslizenz für Springen, Dressur, Vielseitigkeit. | `R1` | +| **R1** | Reiter-Lizenz 1 | Einstiegslizenz für Springen. | `R1` | | **R2** | Reiter-Lizenz 2 | Fortgeschrittene (Springen bis LM/130cm). | `R2` | | **R3** | Reiter-Lizenz 3 | Schwere Klasse (Springen bis S/145cm). | `R3` | -| **R4** | Reiter-Lizenz 4 | Höchste nationale Stufe (alle Klassen). | `R4` | +| **R4** | Reiter-Lizenz 4 | Höchste nationale Stufe Springen (bis 160cm). | `R4` | | **RD1** | Dressur-Lizenz 1 | Speziallizenz nur für Dressur (Kl. A, L). | `RD1` | | **RD2** | Dressur-Lizenz 2 | Speziallizenz nur für Dressur (Kl. LM, M). | `RD2` | | **RD3** | Dressur-Lizenz 3 | Speziallizenz nur für Dressur (Kl. S). | `RD3` | @@ -23,6 +23,8 @@ Prüfungen antreten darf. ## 2. Startberechtigungen nach Sparten +> Referenz: ÖTO 2026, A‑Teil § 15 „Reiterlizenzen“. Eine komprimierte Matrix mit Klassen/Höhen und zulässigen Lizenzen findet sich in der Projekt‑Referenz: `docs/03_Domain/02_Reference/OETO_Regelwerk/OETO-2026_A-Teil-Allgemeiner-Teil_16-12-2025.md` (Kapitel „Reiterlizenzen, § 15 ÖTO“). + ### 2.1 Springen (CSN) Die Startberechtigung richtet sich nach der Hindernishöhe der jeweiligen Klasse (§ 200 B-Teil). @@ -44,11 +46,81 @@ Die Startberechtigung richtet sich nach dem Aufgabenniveau (§ 100 B-Teil). | Klasse | Niveau | Erforderliche Lizenz | Besonderheiten | |:---------------|:--------------------|:------------------------|:-----------------------------------------| | **lizenzfrei** | - | **LZF** (Reiterpass) | Inkl. First Ridden, Dressurreiterbewerbe | -| **A** | Leicht | **R1 / RD1** oder höher | Grundausbildung | -| **L** | Mittelleicht | **R1 / RD1** oder höher | - | -| **LM** | Leicht-Mittelschwer | **R2 / RD2** oder höher | Kandare wahlweise | -| **M** | Mittelschwer | **R2 / RD2** oder höher | Kandarenpflicht | -| **S** | Schwer | **R3 / RD3** oder höher | St. Georg bis Grand Prix | +| **A** | Leicht | **RD1** oder höher | Grundausbildung | +| **L** | Mittelleicht | **RD1** oder höher | - | +| **LM** | Leicht-Mittelschwer | **RD2** oder höher | Kandare wahlweise | +| **M** | Mittelschwer | **RD2** oder höher | Kandarenpflicht | +| **S** | Schwer | **RD3** oder höher | St. Georg bis Grand Prix | + +Hinweis RD4: Das ÖTO kennt in der Tabelle Kombinationsfälle bis `RD4`. Im System ist derzeit kein `RD4`‑Enum modelliert; faktisch wird `RD4` wie `RD3` behandelt (Kappung auf Dressur S). Eine spätere Erweiterung um `RD4` ist vorbereitet, siehe CHANGELOG. + +--- + +### 2.3 Vielseitigkeit (CCN/CCI) + +Vielseitigkeit kombiniert Dressur, Gelände und Springen. Für die Startberechtigung gilt das Prinzip der kumulativen Mindestanforderung: Die jeweils geforderte Dressur- und Spring-Lizenz muss erfüllt sein (Details je Klasse und Ausschreibung, § 300 B‑Teil). + +| Klasse (typ.) | Richtwert | Erforderliche Lizenzen (kumulativ) | Besonderheiten | +|:--------------|:---------------------|:-----------------------------------|:--------------| +| **E/Einsteiger** | national, Einstiegsniveau | **LZF** (Startkarte) ODER `RD1`+`R1` (wenn gefordert) | gem. Ausschreibung | +| **A** | Einsteiger/Light | `RD1` UND `R1` | — | +| **L** | Mittelleicht | `RD1` UND `R1` | — | +| **M** | Mittelschwer | `RD2` UND `R2` | — | +| **S** | Schwer | `RD3` UND `R3` | höhere Anforderungen international | + +Hinweis: In der Praxis orientiert sich die Spring-Anforderung an der in der Ausschreibung angegebenen Hindernishöhe der Springprüfung; die Dressur-Anforderung am ausgeschriebenen Dressur-Niveau der Teilprüfung. Wo nationale Sonderregelungen (Ponys, Haflinger/Noriker) bestehen, gelten diese zusätzlich. + +--- + +### 2.4 Fahren (CAN/CAI) + +Für Fahren existieren im ÖTO/FEI‑Kontext eigene Fahrlizenzen (z. B. F1–F3). Im aktuellen Systemmodell sind diese noch nicht als `ReiterLizenzKlasseE` hinterlegt. Bis zur Einführung entsprechender Enums/Regeln gilt: + +| Klasse (typ.) | Richtwert | Systemzustand | Teilnahmevoraussetzung (heute) | +|:--------------|:-----------------|:----------------------------------------------|:-------------------------------| +| **A/L/M/S** | national/intern. | Keine `F*`‑Lizenztypen im Enum vorhanden | **Startkarte** (LZF) plus Zulassung laut Ausschreibung/Verbandsvorgaben | + +Geplante Erweiterung: Ergänzung der Enum‑Werte `F1`..`F3` und einer Fahr‑Matrix analog Dressur/Springen (separates Ticket). Die ZNS‑Integration bleibt davon unberührt, bis ZNS eigene Fahr‑Kodierungen liefert. + +--- + +### 2.5 Übersichtstabelle gemäß § 15 (kompakt) + +Zur schnellen Orientierung ist die folgende, auf die System‑Enums abgebildete Kurzfassung der §15‑Tabelle enthalten. „B,C“ bedeutet „Turnierkategorien B, C, C‑NEU zulässig“, „LP“ steht für „Lizenzprüfung erforderlich“. + +| Klasse/Höhe | V 80 | 60–100 | V 90 | A | 105–110 | V 100 | L | 115–120 | V 105 | LM | 125–130 | M | 135 | V110–115 | S | V120 | 140–145 | 150–160 | +|:-----------:|:----:|:------:|:----:|:---:|:-------:|:-----:|:---:|:-------:|:-----:|:---:|:-------:|:---:|:---:|:---------:|:---:|:----:|:-------:|:-------:| +| Sparte | V | S | V | D | S | V | D | S | V | D | S | D | S | V | D | V | S | S | +| R1 | X | X | X | B,C | B,C | X | B,C | B,C | | | | | | | | | | | +| R2 | X | X | X | X | X | X | X | X | X | X | X | LP | | | | | | | +| R3 | X | X | X | X | X | X | X | X | X | X | X | X | X | X | | X | X | | +| RD1 | | | | B,C | | | B,C | | | | | | | | | | | | +| RD2 | | | | X | | | X | | | X | | LP | | | | | | | +| RD3 | | | | X | | | X | | | X | | X | | | X | | | | +| R4 | X | X | X | X | X | X | X | X | X | X | X | X | X | X | | X | X | X | + +Abbildung im System: +- Zeilen mit Kombi‑Lizenzen (z. B. `R1D2`, `RDS4`) werden als gleichzeitiger Besitz getrennter Lizenzen interpretiert (zwei Einträge in `Reiter.lizenzen`). +- `RD4` ist aktuell nicht als Enum verfügbar; bis zur Erweiterung entspricht das Verhalten `RD3`. + +--- + +### 2.6 Kombinationsreihen (§ 15) + +Die folgenden Zeilen aus der ÖTO‑Tabelle (§ 15) bilden Kombinationsfälle ab, die sich aus gleichzeitig erfüllten +Voraussetzungen ergeben. Sie sind systemisch als Parallelbesitz mehrerer Lizenzen zu verstehen und dienen der +schnellen Orientierung. Spaltenbezeichnungen entsprechen Abschnitt 2.5. + +| Klasse/Höhe | V 80 | 60–100 | V 90 | A | 105–110 | V 100 | L | 115–120 | V 105 | LM | 125–130 | M | 135 | V110–115 | S | V120 | 140–145 | 150–160 | +|:-----------:|:----:|:------:|:----:|:---:|:-------:|:-----:|:---:|:-------:|:-----:|:---:|:-------:|:---:|:---:|:---------:|:---:|:----:|:-------:|:-------:| +| R1S2 | X | X | X | B,C | X | X | B,C | X | | | X | | | | | | | | +| R1S3 | X | X | X | B,C | X | X | B,C | X | | | X | | X | | | | X | | +| R1S4 | X | X | X | B,C | X | X | B,C | X | | | X | | X | | | | X | X | +| R2S3 | X | X | X | X | X | X | X | X | X | X | X | LP | X | | | | X | | +| R2S4 | X | X | X | X | X | X | X | X | X | X | X | LP | X | | | | X | X | +| R3S4 | X | X | X | X | X | X | X | X | X | X | X | X | X | X | | X | X | X | + +Legende siehe Abschnitt 2.5 (Bedeutung von „B,C“/„LP“ sowie Spartenkürzel). --- @@ -58,30 +130,58 @@ Die Startberechtigung richtet sich nach dem Aufgabenniveau (§ 100 B-Teil). Für Rasse-spezifische Bewerbe gelten oft abweichende (niedrigere) Lizenz-Anforderungen für höhere Klassen. +Hinweis: Abweichungen sind disziplinspezifisch zu verstehen (Dressur → `RD*`, Springen → `R*`). + * **Dressur (Haflinger):** - * Klasse L/LM: R(D)1 ausreichend. - * Klasse M: R(D)3 erforderlich. - * Klasse S: R(D)4 erforderlich. + * Klasse L/LM: **RD1** ausreichend. + * Klasse M: **RD3** erforderlich. + * Klasse S: nationale Sonderregelungen, idR **RD3**; höhere Abweichungen gemäß Ausschreibung. * **Springen (Haflinger):** - * 95-120cm (bis Klasse M): R1 ausreichend. - * 125-135cm (Klasse S): R2 ausreichend. + * 95–120cm (bis Klasse M): **R1** ausreichend. + * 125–135cm (Klasse S): **R2** ausreichend. ### 3.2 Pony * In Pony-Bewerben (bis Kl. L) ist die **Startkarte Allgemein** (Voraussetzung Reiterpass) ausreichend. -* Ab Klasse LM ist eine entsprechende Lizenz (R1/RD1) erforderlich. +* Ab Klasse LM ist eine entsprechende Lizenz erforderlich: **RD1** (Dressur) bzw. **R1** (Springen). --- -## 4. ZNS-Integration (LIZENZ01.dat) +## 4. Validierungslogik & ZNS-Integration + +### 4.1 Validierungslogik (Systemverhalten) + +Die Eligibility wird in zwei strikt getrennten Schritten geprüft: + +1) Spartenlizenz prüfen + - Für Springen sind ausschließlich `R1..R4` gültig (keine Dressur-Lizenz ausreichend). + - Für Dressur sind ausschließlich `RD1..RD3` gültig (keine Spring-Lizenz ausreichend). + - Für Vielseitigkeit müssen die Anforderungen beider Sparten erfüllt sein: Dressur-Teilprüfung (RD*) UND Spring-Teilprüfung (R*), jeweils gemäß ausgeschriebener Klasse. + - Für Fahren gibt es aktuell keine `F*`‑Lizenzen im System; bis zur Erweiterung wird Fahren als lizenzfrei (LZF) behandelt und organisatorisch über die Ausschreibung begrenzt. + +2) Maximal erlaubte Turnierklasse ermitteln + - Innerhalb der geprüften Sparte bestimmt die höchste bezahlte Lizenz die Obergrenze der Klasse/Höhe. + - Zur internen Kappung wird ggf. ein disziplinübergreifendes Mapping verwendet (nur nach bestandener Spartenprüfung): + - `R1↔RD1`, `R2↔RD2`, `R3/R4↔RD3`. + +Konsequenz: Ein Reiter mit nur `RD2` darf Dressur bis LM/M reiten, aber im Springen nur lizenzfreie Bewerbe (E0). Umgekehrt +ermöglicht eine `R2` Springen bis 130cm, aber keine Dressur-Klassen. In der Vielseitigkeit wären für eine M‑Klasse beide Nachweise (`RD2` UND `R2`) erforderlich. + +### 4.2 ZNS-Integration (LIZENZ01.dat) Das System mappt die Felder aus der ZNS-Datei automatisch auf die interne `LizenzKlasseE`. -* **Feld `Reiterlizenz` (Pos 137):** Enthält die Hauptlizenz (z.B. `R1`). -* **Feld `Lizenz-Details` (Pos 201):** Enthält die Liste aller bezahlten Lizenzen (z.B. `RD1,F1`). - * *Logik:* Ein Reiter mit `RD2` darf Dressur LM/M reiten, aber Springen nur lizenzfrei (E0), sofern keine `R1` (oder - höher) vorhanden ist. +* **Feld `Reiterlizenz` (Pos 137):** Enthält die primäre Lizenzangabe (z. B. `R1` oder `RD1`). +* **Feld `Lizenz-Details` (Pos 201):** Enthält die Liste aller bezahlten Lizenzen (z. B. `RD1`). Werte für Fahren (`F*`) werden aktuell ignoriert, solange keine System‑Enums existieren. + * Das System aggregiert alle Vorkommen und bestimmt je Sparte die höchste vorhandene Lizenz. --- -> 📜 **Rulebook Expert Hinweis:** Die Startberechtigung muss bei jeder Nennung gegen die aktuelle Lizenz des Reiters ( -> Stichtag Nennschluss) geprüft werden. Eine Höherreihung während eines Turniers ist gemäß § 17 Abs. 6 ausgeschlossen. +> 📜 **Rulebook Expert Hinweis:** Die Startberechtigung muss bei jeder Nennung gegen die aktuelle Lizenz des Reiters +> (Stichtag Nennschluss) geprüft werden. Eine Höherreihung während eines Turniers ist gemäß § 17 Abs. 6 ausgeschlossen. + +--- + +Siehe ergänzend: +* `TURNIER_KLASSEN.md` für Klassendefinitionen und Höhen/Niveaus je Sparte. +* `GEBUEHRENORDNUNG.md` für Start-/Nenngelder und Mindest-Geldpreise. +* `ZNS_SCHNITTSTELLE.md` für Felder und Positionsangaben der LIZENZ01.dat. diff --git a/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/model/Reiter.kt b/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/model/Reiter.kt index a3ef5ca6..6b6a17cf 100644 --- a/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/model/Reiter.kt +++ b/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/model/Reiter.kt @@ -192,13 +192,25 @@ data class Reiter( /** * Checks if the rider holds a license for a specific discipline. - * Simple logic for now: Any non-blank license field counts. + * Strikte Logik: Eine Dressur-Lizenz (RD1..RD3) gilt nur für DRESSUR, + * eine Reit-/Spring-Lizenz (R1..R4) nur für SPRINGEN. FAHREN nutzt die separate Fahr-Lizenz. */ fun hasLizenzForSparte(sparte: at.mocode.core.domain.model.SparteE): Boolean { + val lk = lizenzKlasse return when (sparte) { - at.mocode.core.domain.model.SparteE.DRESSUR -> !reiterLizenz.isNullOrBlank() - at.mocode.core.domain.model.SparteE.SPRINGEN -> !reiterLizenz.isNullOrBlank() + at.mocode.core.domain.model.SparteE.DRESSUR -> + lk == ReiterLizenzKlasseE.RD1 || + lk == ReiterLizenzKlasseE.RD2 || + lk == ReiterLizenzKlasseE.RD3 + + at.mocode.core.domain.model.SparteE.SPRINGEN -> + lk == ReiterLizenzKlasseE.R1 || + lk == ReiterLizenzKlasseE.R2 || + lk == ReiterLizenzKlasseE.R3 || + lk == ReiterLizenzKlasseE.R4 + at.mocode.core.domain.model.SparteE.FAHREN -> !fahrLizenz.isNullOrBlank() + else -> hasLizenz() } } diff --git a/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/model/ReitLizenz.kt b/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/model/Reiterlizenz.kt similarity index 94% rename from backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/model/ReitLizenz.kt rename to backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/model/Reiterlizenz.kt index 863163ad..f66c2d67 100644 --- a/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/model/ReitLizenz.kt +++ b/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/model/Reiterlizenz.kt @@ -7,7 +7,7 @@ import kotlinx.serialization.Serializable import kotlin.uuid.Uuid @Serializable -data class ReitLizenz( +data class Reiterlizenz( @Serializable(with = UuidSerializer::class) val lizenzId: Uuid = Uuid.random(), val code: String, diff --git a/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/repository/MasterdataLicenseRepository.kt b/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/repository/LizenzRepository.kt similarity index 72% rename from backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/repository/MasterdataLicenseRepository.kt rename to backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/repository/LizenzRepository.kt index a119022f..7f476cf7 100644 --- a/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/repository/MasterdataLicenseRepository.kt +++ b/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/repository/LizenzRepository.kt @@ -3,14 +3,14 @@ package at.mocode.masterdata.domain.repository import at.mocode.masterdata.domain.model.FahrLizenz -import at.mocode.masterdata.domain.model.ReitLizenz +import at.mocode.masterdata.domain.model.Reiterlizenz import at.mocode.masterdata.domain.model.Startkarte /** * Repository für alle Lizenz-Stammdaten (Reit, Fahr, Startkarten). */ -interface MasterdataLicenseRepository { - suspend fun findReitLizenzByCode(code: String): ReitLizenz? +interface LizenzRepository { + suspend fun findReitLizenzByCode(code: String): Reiterlizenz? suspend fun findFahrLizenzByCode(code: String): FahrLizenz? suspend fun findStartkarteByCode(code: String): Startkarte? } diff --git a/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/service/LicenseMatrixServiceImpl.kt b/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/service/LicenseMatrixServiceImpl.kt deleted file mode 100644 index d99729df..00000000 --- a/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/service/LicenseMatrixServiceImpl.kt +++ /dev/null @@ -1,57 +0,0 @@ -package at.mocode.masterdata.domain.service - -import at.mocode.core.domain.model.ReiterLizenzKlasseE -import at.mocode.core.domain.model.SparteE -import at.mocode.masterdata.domain.model.Reiter -import at.mocode.masterdata.domain.model.LicenseMatrixEntry -import at.mocode.masterdata.domain.model.TurnierklasseDefinition - -/** - * Standard-Implementierung des [LicenseMatrixService] gemäß ÖTO. - */ -class LicenseMatrixServiceImpl : LicenseMatrixService { - - private val classHierarchy = listOf("E", "A", "L", "LM", "M", "S") - - override fun isEligible( - reiter: Reiter, - turnierklasse: TurnierklasseDefinition, - sparte: SparteE, - matrix: List, - alleKlassen: List - ): Boolean { - // 1. Basis-Check: Hat der Reiter überhaupt eine Lizenz für diese Sparte? - if (!reiter.hasLizenzForSparte(sparte)) return false - - // 2. Max Turnierklasse aus Matrix ermitteln - val maxClassCode = getMaxTurnierklasse(reiter, sparte, matrix) ?: return false - - // 3. Hierarchie-Check (maxClassCode vs. turnierklasse.code) - val maxIndex = classHierarchy.indexOf(maxClassCode) - val targetIndex = classHierarchy.indexOf(turnierklasse.code) - - if (maxIndex == -1 || targetIndex == -1) return false - - return targetIndex <= maxIndex - } - - override fun getMaxTurnierklasse( - reiter: Reiter, - sparte: SparteE, - matrix: List - ): String? { - // Suche passenden Eintrag in der Matrix für (Sparte, Lizenzklasse) - val entry = matrix.find { it.sparte == sparte && it.lizenzKlasse == reiter.lizenzKlasse } - ?: matrix.find { it.sparte == SparteE.DRESSUR && sparte == SparteE.DRESSUR && it.lizenzKlasse == reiter.lizenzKlasse } // Fallback/Spezial - ?: if (reiter.lizenzKlasse == ReiterLizenzKlasseE.R1 || - reiter.lizenzKlasse == ReiterLizenzKlasseE.R2 || - reiter.lizenzKlasse == ReiterLizenzKlasseE.R3 || - reiter.lizenzKlasse == ReiterLizenzKlasseE.R4) { - // Fallback für Dressur, wenn man eine Springlizenz hat (R1 gilt oft auch als RD1 etc. in manchen Kontexten, - // aber hier schauen wir primär ob die Matrix einen generischen Eintrag hat) - matrix.find { it.sparte == sparte && it.lizenzKlasse == ReiterLizenzKlasseE.LIZENZFREI } - } else null - - return entry?.maxTurnierklasseCode - } -} diff --git a/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/service/LicenseMatrixService.kt b/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/service/LizenzMatrixService.kt similarity index 97% rename from backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/service/LicenseMatrixService.kt rename to backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/service/LizenzMatrixService.kt index b9f262f8..752261e4 100644 --- a/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/service/LicenseMatrixService.kt +++ b/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/service/LizenzMatrixService.kt @@ -8,7 +8,7 @@ import at.mocode.masterdata.domain.model.TurnierklasseDefinition /** * Service zur Prüfung der Teilnahmeberechtigung basierend auf der Lizenz-Matrix. */ -interface LicenseMatrixService { +interface LizenzMatrixService { /** * Prüft, ob ein Reiter mit seiner aktuellen Lizenz in einer bestimmten Turnierklasse startberechtigt ist. diff --git a/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/service/LizenzMatrixServiceImpl.kt b/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/service/LizenzMatrixServiceImpl.kt new file mode 100644 index 00000000..d29d534f --- /dev/null +++ b/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/service/LizenzMatrixServiceImpl.kt @@ -0,0 +1,72 @@ +package at.mocode.masterdata.domain.service + +import at.mocode.core.domain.model.ReiterLizenzKlasseE +import at.mocode.core.domain.model.SparteE +import at.mocode.masterdata.domain.model.Reiter +import at.mocode.masterdata.domain.model.LicenseMatrixEntry +import at.mocode.masterdata.domain.model.TurnierklasseDefinition + +/** + * Standard-Implementierung des [LizenzMatrixService] gemäß ÖTO. + */ +class LizenzMatrixServiceImpl : LizenzMatrixService { + + private val classHierarchy = listOf("E", "A", "L", "LM", "M", "S") + + override fun isEligible( + reiter: Reiter, + turnierklasse: TurnierklasseDefinition, + sparte: SparteE, + matrix: List, + alleKlassen: List + ): Boolean { + // 1. Basis-Check: Hat der Reiter überhaupt eine Lizenz für diese Sparte? + if (!reiter.hasLizenzForSparte(sparte)) return false + + // 2. Max Turnierklasse aus Matrix ermitteln + val maxClassCode = getMaxTurnierklasse(reiter, sparte, matrix) ?: return false + + // 3. Hierarchie-Check (maxClassCode vs. turnierklasse.code) + val maxIndex = classHierarchy.indexOf(maxClassCode) + val targetIndex = classHierarchy.indexOf(turnierklasse.code) + + if (maxIndex == -1 || targetIndex == -1) return false + + return targetIndex <= maxIndex + } + + override fun getMaxTurnierklasse( + reiter: Reiter, + sparte: SparteE, + matrix: List + ): String? { + // 1) Direkter Treffer in Matrix für (Sparte, Lizenzklasse) + val direct = matrix.find { it.sparte == sparte && it.lizenzKlasse == reiter.lizenzKlasse } + if (direct != null) return direct.maxTurnierklasseCode + + // 2) Cross-Discipline Mapping (R<->RD) gemäß ÖTO-Äquivalenzen + // Hinweis: RD4 ist im aktuellen Enum nicht modelliert. R4 wird für Zwecke der + // Dressur-Kappung wie RD3 behandelt (max. S), bis RD4 eingeführt wird. + val mappedLizenz = when (sparte) { + SparteE.DRESSUR -> when (reiter.lizenzKlasse) { + ReiterLizenzKlasseE.R1 -> ReiterLizenzKlasseE.RD1 + ReiterLizenzKlasseE.R2 -> ReiterLizenzKlasseE.RD2 + ReiterLizenzKlasseE.R3, ReiterLizenzKlasseE.R4 -> ReiterLizenzKlasseE.RD3 // RD4 derzeit nicht modelliert + else -> reiter.lizenzKlasse + } + SparteE.SPRINGEN -> when (reiter.lizenzKlasse) { + ReiterLizenzKlasseE.RD1 -> ReiterLizenzKlasseE.R1 + ReiterLizenzKlasseE.RD2 -> ReiterLizenzKlasseE.R2 + ReiterLizenzKlasseE.RD3 -> ReiterLizenzKlasseE.R3 + else -> reiter.lizenzKlasse + } + else -> reiter.lizenzKlasse + } + val cross = matrix.find { it.sparte == sparte && it.lizenzKlasse == mappedLizenz } + if (cross != null) return cross.maxTurnierklasseCode + + // 3) Letzter Fallback: LIZENZFREI in gewünschter Sparte + return matrix.find { it.sparte == sparte && it.lizenzKlasse == ReiterLizenzKlasseE.LIZENZFREI } + ?.maxTurnierklasseCode + } +} diff --git a/backend/services/masterdata/masterdata-domain/src/commonTest/kotlin/at/mocode/masterdata/domain/service/LicenseMatrixServiceTest.kt b/backend/services/masterdata/masterdata-domain/src/commonTest/kotlin/at/mocode/masterdata/domain/service/LiznezMatrixServiceTest.kt similarity index 97% rename from backend/services/masterdata/masterdata-domain/src/commonTest/kotlin/at/mocode/masterdata/domain/service/LicenseMatrixServiceTest.kt rename to backend/services/masterdata/masterdata-domain/src/commonTest/kotlin/at/mocode/masterdata/domain/service/LiznezMatrixServiceTest.kt index 934be0a7..00563324 100644 --- a/backend/services/masterdata/masterdata-domain/src/commonTest/kotlin/at/mocode/masterdata/domain/service/LicenseMatrixServiceTest.kt +++ b/backend/services/masterdata/masterdata-domain/src/commonTest/kotlin/at/mocode/masterdata/domain/service/LiznezMatrixServiceTest.kt @@ -13,9 +13,9 @@ import kotlin.test.assertTrue import kotlin.time.Clock import kotlin.uuid.Uuid -class LicenseMatrixServiceTest { +class LiznezMatrixServiceTest { - private val service = LicenseMatrixServiceImpl() + private val service = LizenzMatrixServiceImpl() private val nun = Clock.System.now() private val matrix = listOf( diff --git a/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/ExposedRegulationRepository.kt b/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/ExposedRegulationRepository.kt index c812649a..088a2dd2 100644 --- a/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/ExposedRegulationRepository.kt +++ b/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/ExposedRegulationRepository.kt @@ -29,7 +29,7 @@ class ExposedRegulationRepository : RegulationRepository { } override suspend fun findAllLicenseMatrixEntries(): List = DatabaseFactory.dbQuery { - LicenseTable.selectAll() + LizenzTable.selectAll() .map { it.toLicenseMatrixEntry() } } @@ -89,15 +89,15 @@ class ExposedRegulationRepository : RegulationRepository { ) private fun ResultRow.toLicenseMatrixEntry() = LicenseMatrixEntry( - licenseId = this[LicenseTable.id], - sparte = SparteE.valueOf(this[LicenseTable.sparte]), - lizenzKlasse = ReiterLizenzKlasseE.valueOf(this[LicenseTable.lizenzKlasse]), - maxTurnierklasseCode = this[LicenseTable.maxTurnierklasseCode], - validFrom = this[LicenseTable.validFrom].toKtInstant(), - validTo = this[LicenseTable.validTo]?.toOptionalKtInstant(), - istAktiv = this[LicenseTable.istAktiv], - createdAt = this[LicenseTable.createdAt].toKtInstant(), - updatedAt = this[LicenseTable.updatedAt].toKtInstant() + licenseId = this[LizenzTable.id], + sparte = SparteE.valueOf(this[LizenzTable.sparte]), + lizenzKlasse = ReiterLizenzKlasseE.valueOf(this[LizenzTable.lizenzKlasse]), + maxTurnierklasseCode = this[LizenzTable.maxTurnierklasseCode], + validFrom = this[LizenzTable.validFrom].toKtInstant(), + validTo = this[LizenzTable.validTo]?.toOptionalKtInstant(), + istAktiv = this[LizenzTable.istAktiv], + createdAt = this[LizenzTable.createdAt].toKtInstant(), + updatedAt = this[LizenzTable.updatedAt].toKtInstant() ) private fun ResultRow.toRichtverfahrenDefinition() = RichtverfahrenDefinition( diff --git a/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/LicenseTable.kt b/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/LizenzTable.kt similarity index 96% rename from backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/LicenseTable.kt rename to backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/LizenzTable.kt index 82b620b4..0ced6f75 100644 --- a/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/LicenseTable.kt +++ b/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/LizenzTable.kt @@ -10,7 +10,7 @@ import org.jetbrains.exposed.v1.datetime.timestamp * Exposed-Tabellendefinition für die Lizenz-Matrix (Reiter-Lizenz vs. Turnierklasse). * Basierend auf ÖTO 2026. */ -object LicenseTable : Table("license_matrix") { +object LizenzTable : Table("license_matrix") { val id = uuid("license_id") val sparte = varchar("sparte", 20) // DRESSUR, SPRINGEN, ALLGEMEIN val lizenzKlasse = varchar("lizenz_klasse", 20) // R1, R2, R3, RD1, RD2, RD3, LF diff --git a/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/reiter/MasterdataLicenseExposedRepository.kt b/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/reiter/MasterdataLicenseExposedRepository.kt index 79853a4e..4260b40c 100644 --- a/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/reiter/MasterdataLicenseExposedRepository.kt +++ b/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/reiter/MasterdataLicenseExposedRepository.kt @@ -4,21 +4,21 @@ package at.mocode.masterdata.infrastructure.persistence.reiter import at.mocode.core.utils.database.DatabaseFactory import at.mocode.masterdata.domain.model.FahrLizenz -import at.mocode.masterdata.domain.model.ReitLizenz +import at.mocode.masterdata.domain.model.Reiterlizenz import at.mocode.masterdata.domain.model.Startkarte -import at.mocode.masterdata.domain.repository.MasterdataLicenseRepository +import at.mocode.masterdata.domain.repository.LizenzRepository import org.jetbrains.exposed.v1.core.eq import org.jetbrains.exposed.v1.jdbc.selectAll /** - * Exposed-basierte Implementierung des MasterdataLicenseRepository. + * Exposed-basierte Implementierung des LizenzRepository. */ -class MasterdataLicenseExposedRepository : MasterdataLicenseRepository { +class MasterdataLicenseExposedRepository : LizenzRepository { - override suspend fun findReitLizenzByCode(code: String): ReitLizenz? = DatabaseFactory.dbQuery { + override suspend fun findReitLizenzByCode(code: String): Reiterlizenz? = DatabaseFactory.dbQuery { ReitLizenzenTable.selectAll().where { ReitLizenzenTable.code eq code } .map { - ReitLizenz( + Reiterlizenz( lizenzId = it[ReitLizenzenTable.id], code = it[ReitLizenzenTable.code], bezeichnung = it[ReitLizenzenTable.bezeichnung], diff --git a/backend/services/masterdata/masterdata-infrastructure/src/test/kotlin/at/mocode/masterdata/infrastructure/persistence/RegulationSeedVerificationTest.kt b/backend/services/masterdata/masterdata-infrastructure/src/test/kotlin/at/mocode/masterdata/infrastructure/persistence/RegulationSeedVerificationTest.kt index 16e46fab..d53b57df 100644 --- a/backend/services/masterdata/masterdata-infrastructure/src/test/kotlin/at/mocode/masterdata/infrastructure/persistence/RegulationSeedVerificationTest.kt +++ b/backend/services/masterdata/masterdata-infrastructure/src/test/kotlin/at/mocode/masterdata/infrastructure/persistence/RegulationSeedVerificationTest.kt @@ -29,7 +29,7 @@ class RegulationSeedVerificationTest { transaction { SchemaUtils.create( TurnierKlassenTable, - LicenseTable, + LizenzTable, RichtverfahrenTable, GebuehrTable, RegulationConfigTable, @@ -71,7 +71,7 @@ class RegulationSeedVerificationTest { @Test fun `verify domain logic with simulated oeto data`() { - val service = at.mocode.masterdata.domain.service.LicenseMatrixServiceImpl() + val service = at.mocode.masterdata.domain.service.LizenzMatrixServiceImpl() val now = Clock.System.now() val oetoMatrix = listOf( diff --git a/backend/services/masterdata/masterdata-service/src/main/kotlin/at/mocode/masterdata/service/config/MasterdataDatabaseConfiguration.kt b/backend/services/masterdata/masterdata-service/src/main/kotlin/at/mocode/masterdata/service/config/MasterdataDatabaseConfiguration.kt index b953df5a..62f7c9b0 100644 --- a/backend/services/masterdata/masterdata-service/src/main/kotlin/at/mocode/masterdata/service/config/MasterdataDatabaseConfiguration.kt +++ b/backend/services/masterdata/masterdata-service/src/main/kotlin/at/mocode/masterdata/service/config/MasterdataDatabaseConfiguration.kt @@ -59,7 +59,7 @@ class MasterdataDatabaseConfiguration( TurnierKlassenTable, TurnierSpartenTable, TurnierKategorienTable, - LicenseTable, + LizenzTable, RichtverfahrenTable, GebuehrTable, RegulationConfigTable @@ -112,7 +112,7 @@ class MasterdataTestDatabaseConfiguration { TurnierKlassenTable, TurnierSpartenTable, TurnierKategorienTable, - LicenseTable, + LizenzTable, RichtverfahrenTable, GebuehrTable, RegulationConfigTable diff --git a/backend/services/masterdata/masterdata-service/src/main/kotlin/at/mocode/masterdata/service/config/ReiterlizenzenSeeder.kt b/backend/services/masterdata/masterdata-service/src/main/kotlin/at/mocode/masterdata/service/config/ReiterlizenzenSeeder.kt new file mode 100644 index 00000000..5fc4b443 --- /dev/null +++ b/backend/services/masterdata/masterdata-service/src/main/kotlin/at/mocode/masterdata/service/config/ReiterlizenzenSeeder.kt @@ -0,0 +1,79 @@ +@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class) +package at.mocode.masterdata.service.config + +import at.mocode.masterdata.infrastructure.persistence.LizenzTable +import jakarta.annotation.PostConstruct +import org.jetbrains.exposed.v1.core.* +import org.jetbrains.exposed.v1.jdbc.insert +import org.jetbrains.exposed.v1.jdbc.selectAll +import org.jetbrains.exposed.v1.jdbc.transactions.transaction +import org.jetbrains.exposed.v1.jdbc.update +import org.slf4j.LoggerFactory +import org.springframework.context.annotation.Configuration +import org.springframework.context.annotation.DependsOn +import org.springframework.context.annotation.Profile +import kotlin.uuid.Uuid + +/** + * Seeder für Reiterlizenzen (license_matrix) gemäß ÖTO 2026. + * + * Ziel: Sicherstellen, dass bei Service-Start alle benötigten Lizenzeinträge + * für Dressur und Springen vorhanden sind – auch wenn die DB leer ist oder + * frühere Migrationen/Seeds nicht gelaufen sind. + * + * Idempotent: pro (sparte, lizenz_klasse) exakt ein Datensatz; bei Änderungen + * der max_turnierklasse_code wird dieser upgedatet. + */ +@Configuration +@Profile("!test") +@DependsOn("masterdataDatabaseConfiguration") +class ReiterlizenzenSeeder { + private val log = LoggerFactory.getLogger(ReiterlizenzenSeeder::class.java) + + @PostConstruct + fun seed() { + log.info("Starte Seeding der Reiterlizenzen (license_matrix)...") + transaction { + // Springen: LIZENZFREI→E, R1→L, R2→M, R3→S, R4→S + upsert("SPRINGEN", "LIZENZFREI", "E") + upsert("SPRINGEN", "R1", "L") + upsert("SPRINGEN", "R2", "M") + upsert("SPRINGEN", "R3", "S") + upsert("SPRINGEN", "R4", "S") + + // Dressur: LIZENZFREI→E, RD1→L, RD2→M, RD3→S + upsert("DRESSUR", "LIZENZFREI", "E") + upsert("DRESSUR", "RD1", "L") + upsert("DRESSUR", "RD2", "M") + upsert("DRESSUR", "RD3", "S") + } + log.info("Seeding der Reiterlizenzen abgeschlossen.") + } + + private fun upsert(sparte: String, lizenzKlasse: String, maxTurnierklasseCode: String) { + val existing = LizenzTable.selectAll() + .where { (LizenzTable.sparte eq sparte) and (LizenzTable.lizenzKlasse eq lizenzKlasse) } + .singleOrNull() + + if (existing == null) { + LizenzTable.insert { + it[id] = Uuid.random() + it[LizenzTable.sparte] = sparte + it[LizenzTable.lizenzKlasse] = lizenzKlasse + it[LizenzTable.maxTurnierklasseCode] = maxTurnierklasseCode + it[istAktiv] = true + } + log.debug("Lizenz-Matrix angelegt: {} / {} -> {}", sparte, lizenzKlasse, maxTurnierklasseCode) + } else { + val currentMax = existing[LizenzTable.maxTurnierklasseCode] + val currentActive = existing[LizenzTable.istAktiv] + if (currentMax != maxTurnierklasseCode || !currentActive) { + LizenzTable.update({ (LizenzTable.sparte eq sparte) and (LizenzTable.lizenzKlasse eq lizenzKlasse) }) { + it[LizenzTable.maxTurnierklasseCode] = maxTurnierklasseCode + it[LizenzTable.istAktiv] = true + } + log.debug("Lizenz-Matrix aktualisiert: {} / {} -> {}", sparte, lizenzKlasse, maxTurnierklasseCode) + } + } + } +} diff --git a/backend/services/masterdata/masterdata-service/src/main/resources/db/migration/V013__Cleanup_and_Standardize_Masterdata.sql b/backend/services/masterdata/masterdata-service/src/main/resources/db/migration/V013__Cleanup_and_Standardize_Masterdata.sql index ecc74e89..3aa8457b 100644 --- a/backend/services/masterdata/masterdata-service/src/main/resources/db/migration/V013__Cleanup_and_Standardize_Masterdata.sql +++ b/backend/services/masterdata/masterdata-service/src/main/resources/db/migration/V013__Cleanup_and_Standardize_Masterdata.sql @@ -1,36 +1,79 @@ --- V013__Cleanup_and_Standardize_Masterdata.sql -- Datum: 6. April 2026 --- 1. Bundesland -> bundeslaender -ALTER TABLE bundesland RENAME TO bundeslaender; -ALTER TABLE bundeslaender RENAME COLUMN id TO bundesland_id; -ALTER INDEX IF EXISTS pk_bundesland RENAME TO pk_bundeslaender; -ALTER INDEX IF EXISTS idx_bundesland_oeps RENAME TO idx_bundeslaender_oeps; -ALTER INDEX IF EXISTS idx_bundesland_iso RENAME TO idx_bundeslaender_iso; -ALTER INDEX IF EXISTS ux_bundesland_land_kuerzel RENAME TO ux_bundeslaender_land_kuerzel; -ALTER INDEX IF EXISTS bundesland_bundesland_nr_unique RENAME TO bundeslaender_bundesland_nr_unique; +-- 1. Bundesland -> bundeslaender (idempotent) +DO $$ +BEGIN + IF to_regclass('public.bundesland') IS NOT NULL AND to_regclass('public.bundeslaender') IS NULL THEN + ALTER TABLE bundesland RENAME TO bundeslaender; + END IF; --- 2. qualifikation_master -> funktionaers_qualifikationen -ALTER TABLE qualifikation_master RENAME TO funktionaers_qualifikationen; + -- Spaltenumbenennung nur, wenn noch "id" existiert + IF to_regclass('public.bundeslaender') IS NOT NULL THEN + IF EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = 'public' AND table_name = 'bundeslaender' AND column_name = 'id' + ) THEN + ALTER TABLE bundeslaender RENAME COLUMN id TO bundesland_id; + END IF; + END IF; + + -- Optionale historische Index-Umbenennungen (nur wenn vorhanden) + IF EXISTS (SELECT 1 FROM pg_class WHERE relname = 'pk_bundesland') THEN + ALTER INDEX pk_bundesland RENAME TO pk_bundeslaender; + END IF; + IF EXISTS (SELECT 1 FROM pg_class WHERE relname = 'idx_bundesland_oeps') THEN + ALTER INDEX idx_bundesland_oeps RENAME TO idx_bundeslaender_oeps; + END IF; + IF EXISTS (SELECT 1 FROM pg_class WHERE relname = 'idx_bundesland_iso') THEN + ALTER INDEX idx_bundesland_iso RENAME TO idx_bundeslaender_iso; + END IF; + IF EXISTS (SELECT 1 FROM pg_class WHERE relname = 'ux_bundesland_land_kuerzel') THEN + ALTER INDEX ux_bundesland_land_kuerzel RENAME TO ux_bundeslaender_land_kuerzel; + END IF; + IF EXISTS (SELECT 1 FROM pg_class WHERE relname = 'bundesland_bundesland_nr_unique') THEN + ALTER INDEX bundesland_bundesland_nr_unique RENAME TO bundeslaender_bundesland_nr_unique; + END IF; +END $$; + +-- 2. qualifikation_master -> funktionaers_qualifikationen (idempotent) +DO $$ +BEGIN + IF to_regclass('public.qualifikation_master') IS NOT NULL AND to_regclass('public.funktionaers_qualifikationen') IS NULL THEN + ALTER TABLE qualifikation_master RENAME TO funktionaers_qualifikationen; + END IF; +END $$; -- Die Join-Tabelle funktionaer_qualifikation bleibt als solche bestehen, -- referenziert aber nun funktionaers_qualifikationen. -- (Der Name der Join-Tabelle ist bereits fast korrekt, wir lassen sie vorerst so, -- da sie die Verknüpfung zw. Funktionär und Qualifikation darstellt.) -- Update: Der User möchte "funktionaers_qualifikationen" als Name für die Qualifikationen. --- 3. reiter_lizenz -> reit_lizenzen -ALTER TABLE reiter_lizenz RENAME TO reit_lizenzen; +-- 3. reiter_lizenz -> reit_lizenzen (idempotent) +DO $$ +BEGIN + IF to_regclass('public.reiter_lizenz') IS NOT NULL AND to_regclass('public.reit_lizenzen') IS NULL THEN + ALTER TABLE reiter_lizenz RENAME TO reit_lizenzen; + END IF; +END $$; + ALTER INDEX IF EXISTS pk_reiter_lizenz RENAME TO pk_reit_lizenzen; -- 4. reiter_sparte entfernen DROP TABLE IF EXISTS reiter_sparte; --- 5. turnierklasse -> turnier_klassen -ALTER TABLE turnierklasse RENAME TO turnier_klassen; +-- 5. turnierklasse -> turnier_klassen (idempotent) +-- Hinweis: In der aktuellen Codebasis verwendet Exposed `bewerbs_klassen` als technische Tabelle +-- für Turnier-/Bewerbsklassen. Daher nur umbenennen, wenn die Alt-Tabelle tatsächlich existiert. +DO $$ +BEGIN + IF to_regclass('public.turnierklasse') IS NOT NULL AND to_regclass('public.turnier_klassen') IS NULL THEN + ALTER TABLE turnierklasse RENAME TO turnier_klassen; + END IF; +END $$; + ALTER INDEX IF EXISTS pk_turnierklasse RENAME TO pk_turnier_klassen; ALTER INDEX IF EXISTS idx_turnierklasse_sparte_code RENAME TO idx_turnier_klassen_sparte_code; --- 6. turnier_sparten erstellen CREATE TABLE IF NOT EXISTS turnier_sparten ( sparte_id UUID PRIMARY KEY, code VARCHAR(10) UNIQUE NOT NULL, -- z.B. D, S, V, F, R, C @@ -40,6 +83,5 @@ CREATE TABLE IF NOT EXISTS turnier_sparten ( updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); --- 7. Constraints aktualisieren (falls nötig) -- Da wir nur Tabellen umbenannt haben, bleiben die Foreign Keys in PostgreSQL erhalten -- und zeigen automatisch auf die neuen Tabellennamen. diff --git a/backend/services/zns-import/zns-import-service/src/main/kotlin/at/mocode/zns/import/service/ZnsImportServiceApplication.kt b/backend/services/zns-import/zns-import-service/src/main/kotlin/at/mocode/zns/import/service/ZnsImportServiceApplication.kt index c961f3a9..79b1843a 100644 --- a/backend/services/zns-import/zns-import-service/src/main/kotlin/at/mocode/zns/import/service/ZnsImportServiceApplication.kt +++ b/backend/services/zns-import/zns-import-service/src/main/kotlin/at/mocode/zns/import/service/ZnsImportServiceApplication.kt @@ -17,7 +17,7 @@ class ZnsImportServiceApplication { funktionaerRepository: FunktionaerRepository, landRepository: LandRepository, bundeslandRepository: BundeslandRepository, - licenseRepository: MasterdataLicenseRepository, + licenseRepository: LizenzRepository, altersklassenRepository: AltersklassenRepository ): ZnsImportService { return ZnsImportService( diff --git a/backend/services/zns-import/zns-import-service/src/main/kotlin/at/mocode/zns/import/service/config/RepositoryConfiguration.kt b/backend/services/zns-import/zns-import-service/src/main/kotlin/at/mocode/zns/import/service/config/RepositoryConfiguration.kt index bb61eee3..cdb8759e 100644 --- a/backend/services/zns-import/zns-import-service/src/main/kotlin/at/mocode/zns/import/service/config/RepositoryConfiguration.kt +++ b/backend/services/zns-import/zns-import-service/src/main/kotlin/at/mocode/zns/import/service/config/RepositoryConfiguration.kt @@ -33,7 +33,7 @@ class RepositoryConfiguration { fun funktionaerRepository(): FunktionaerRepository = FunktionaerExposedRepository() @Bean - fun licenseRepository(): MasterdataLicenseRepository = MasterdataLicenseExposedRepository() + fun licenseRepository(): LizenzRepository = MasterdataLicenseExposedRepository() @Bean fun altersklassenRepository(): AltersklassenRepository = AltersklassenExposedRepository() diff --git a/core/zns-parser/src/commonMain/kotlin/at/mocode/zns/parser/ZnsReiterParser.kt b/core/zns-parser/src/commonMain/kotlin/at/mocode/zns/parser/ZnsReiterParser.kt index d903ea30..6df6220c 100644 --- a/core/zns-parser/src/commonMain/kotlin/at/mocode/zns/parser/ZnsReiterParser.kt +++ b/core/zns-parser/src/commonMain/kotlin/at/mocode/zns/parser/ZnsReiterParser.kt @@ -4,6 +4,7 @@ import at.mocode.core.domain.model.DatenQuelleE import at.mocode.core.domain.model.ReiterLizenzKlasseE import at.mocode.core.utils.parser.FixedWidthLineReader import at.mocode.masterdata.domain.model.Reiter +import at.mocode.masterdata.domain.model.ReiterLizenz import kotlin.uuid.ExperimentalUuidApi import kotlin.uuid.Uuid @@ -50,17 +51,33 @@ object ZnsReiterParser { val feiId = reader.getString(190, 8) val sperrListe = reader.getString(198, 1) val lizenzInfo = reader.getString(201, 10) - val lizenzKlasse = mapLizenz(reiterLizenzCode) - val lizenzen = mutableListOf() + // Lizenz-Token aus gesamter Zeile robust extrahieren und normalisieren + val parsedToken = ZnsReiterlicenseTokenizer.parseFromLine(line) + + val lizenzen = mutableListOf() + // Aus festem Feld if (reiterLizenzCode.isNotBlank()) { - lizenzen.add(at.mocode.masterdata.domain.model.ReiterLizenz(lizenzTyp = "REITERLIZENZ", kuerzel = reiterLizenzCode)) + lizenzen.add(ReiterLizenz(lizenzTyp = "REITERLIZENZ", kuerzel = reiterLizenzCode)) + } + // Aus erkanntem kombinierten Token weitere Einträge ergänzen + parsedToken?.normalizedKuerzel?.forEach { code -> + if (code.isNotBlank() && lizenzen.none { it.kuerzel.equals(code, ignoreCase = true) }) { + lizenzen.add(ReiterLizenz(lizenzTyp = "REITERLIZENZ", kuerzel = code)) + } } if (startkarteCode.isNotBlank()) { - lizenzen.add(at.mocode.masterdata.domain.model.ReiterLizenz(lizenzTyp = "STARTKARTE", kuerzel = startkarteCode)) + lizenzen.add(ReiterLizenz(lizenzTyp = "STARTKARTE", kuerzel = startkarteCode)) } if (fahrLizenzCode.isNotBlank()) { - lizenzen.add(at.mocode.masterdata.domain.model.ReiterLizenz(lizenzTyp = "FAHRLIZENZ", kuerzel = fahrLizenzCode)) + lizenzen.add(ReiterLizenz(lizenzTyp = "FAHRLIZENZ", kuerzel = fahrLizenzCode)) + } + + // lizenzKlasse befüllen: bevorzugt aus festem Feld, sonst aus Token ableiten (präferiere Dressur, sonst Springen) + val lizenzKlasse = when { + reiterLizenzCode.isNotBlank() -> mapLizenz(reiterLizenzCode) + parsedToken != null -> parsedToken.primaryKlasse + else -> ReiterLizenzKlasseE.LIZENZFREI } return Reiter( @@ -71,7 +88,8 @@ object ZnsReiterParser { bundeslandNummer = bundeslandNummer, vereinsName = vereinsName.ifBlank { null }, nation = nation.ifBlank { null }, - reiterLizenz = reiterLizenzCode.ifBlank { null }, + reiterLizenz = (reiterLizenzCode.ifBlank { null } + ?: parsedToken?.primaryKuerzel), startkarte = startkarteCode.ifBlank { null }, fahrLizenz = fahrLizenzCode.ifBlank { null }, altersklasseJgJrU25 = altersklasseEnum, diff --git a/core/zns-parser/src/commonMain/kotlin/at/mocode/zns/parser/ZnsReiterlicenseTokenizer.kt b/core/zns-parser/src/commonMain/kotlin/at/mocode/zns/parser/ZnsReiterlicenseTokenizer.kt new file mode 100644 index 00000000..43c12806 --- /dev/null +++ b/core/zns-parser/src/commonMain/kotlin/at/mocode/zns/parser/ZnsReiterlicenseTokenizer.kt @@ -0,0 +1,88 @@ +package at.mocode.zns.parser + +import at.mocode.core.domain.model.ReiterLizenzKlasseE + +/** + * Erkennung und Normalisierung von Lizenz-Token in einer LIZENZ01.DAT-Zeile. + * Unterstützt Einzel- und Kombinationsformen (R{n}, RD{m}, S{n}, D{m}, R{n}S{k}, R{n}D{m}, RDS4). + */ +object ZnsReiterlicenseTokenizer { + private val tokenRegex = Regex( + pattern = "(RDS4|R[1-4]D[2-4]|R[1-4]S[2-4]|RD[1-4]|R[1-4]|S[1-4]|D[2-4])", + options = setOf(RegexOption.IGNORE_CASE) + ) + + data class Parsed( + val rawToken: String, + /** Normalisierte Kürzel-Liste, z.B. ["R3"], ["R2","RD3"] */ + val normalizedKuerzel: List, + /** Primäre Lizenzklasse für das vorhandene Domainfeld (Fallback): bevorzugt RD*, sonst R* */ + val primaryKlasse: ReiterLizenzKlasseE, + /** Primäres Kürzel passend zur primaryKlasse */ + val primaryKuerzel: String? + ) + + fun parseFromLine(line: String): Parsed? { + val matches = tokenRegex.findAll(line) + val last = matches.lastOrNull() ?: return null + val token = last.value.uppercase() + return normalize(token) + } + + private fun normalize(token: String): Parsed? { + val upper = token.uppercase() + return when { + upper == "RDS4" -> Parsed( + rawToken = token, + normalizedKuerzel = listOf("R4", "RD3"), + primaryKlasse = ReiterLizenzKlasseE.RD3, + primaryKuerzel = "RD3" + ) + upper.matches(Regex("R[1-4]D[2-4]")) -> { + val r = upper.substring(0, 2) // Rn + // upper is RnDm -> build RDm and ggf. kappen + val d = capRd4ToRd3("RD" + upper.substring(3, 4)) + Parsed(token, listOf(r, d), mapPrimary(d, r), pickPrimaryKuerzel(d, r)) + } + upper.matches(Regex("R[1-4]S[2-4]")) -> { + val r1 = upper.substring(0, 2) + val r2 = "R" + upper.substring(3, 4) // S{k} -> R{k} + val best = maxR(r1, r2) + Parsed(token, listOf(best), ReiterLizenzKlasseE.valueOf(best), best) + } + upper.matches(Regex("RD[1-4]")) -> { + val d = capRd4ToRd3(upper) + Parsed(token, listOf(d), mapPrimary(d, null), d) + } + upper.matches(Regex("R[1-4]")) -> { + Parsed(token, listOf(upper), ReiterLizenzKlasseE.valueOf(upper), upper) + } + upper.matches(Regex("S[1-4]")) -> { + val r = "R" + upper.substring(1, 2) + Parsed(token, listOf(r), ReiterLizenzKlasseE.valueOf(r), r) + } + upper.matches(Regex("D[2-4]")) -> { + val d = capRd4ToRd3("RD" + upper.substring(1, 2)) + Parsed(token, listOf(d), mapPrimary(d, null), d) + } + else -> null + } + } + + private fun capRd4ToRd3(code: String): String = if (code.equals("RD4", true)) "RD3" else code + + private fun maxR(r1: String, r2: String): String { + val n1 = r1.removePrefix("R").toIntOrNull() ?: 0 + val n2 = r2.removePrefix("R").toIntOrNull() ?: 0 + val n = maxOf(n1, n2).coerceIn(1, 4) + return "R$n" + } + + private fun mapPrimary(d: String?, r: String?): ReiterLizenzKlasseE = when { + d != null -> ReiterLizenzKlasseE.valueOf(d) + r != null -> ReiterLizenzKlasseE.valueOf(r) + else -> ReiterLizenzKlasseE.LIZENZFREI + } + + private fun pickPrimaryKuerzel(d: String?, r: String?): String? = d ?: r +} diff --git a/core/zns-parser/src/commonTest/kotlin/at/mocode/zns/parser/LicenseTokenizerTest.kt b/core/zns-parser/src/commonTest/kotlin/at/mocode/zns/parser/LicenseTokenizerTest.kt new file mode 100644 index 00000000..070e1629 --- /dev/null +++ b/core/zns-parser/src/commonTest/kotlin/at/mocode/zns/parser/LicenseTokenizerTest.kt @@ -0,0 +1,47 @@ +package at.mocode.zns.parser + +import at.mocode.core.domain.model.ReiterLizenzKlasseE +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull + +class LicenseTokenizerTest { + + @Test + fun `detects R2S3 and normalizes to R3`() { + val line = "... AUTR2S3 903801690699 18109450 2025W1990100310137032 R2S3 " + val parsed = ZnsReiterlicenseTokenizer.parseFromLine(line) + assertNotNull(parsed) + assertEquals(listOf("R3"), parsed.normalizedKuerzel) + assertEquals(ReiterLizenzKlasseE.R3, parsed.primaryKlasse) + assertEquals("R3", parsed.primaryKuerzel) + } + + @Test + fun `detects R2D4 and caps to RD3`() { + val line = "... AUTR2D4 105500130676 6820868 2025W1987090510112093 R2D4 " + val parsed = ZnsReiterlicenseTokenizer.parseFromLine(line) + assertNotNull(parsed) + assertEquals(listOf("R2", "RD3"), parsed.normalizedKuerzel) + assertEquals(ReiterLizenzKlasseE.RD3, parsed.primaryKlasse) + assertEquals("RD3", parsed.primaryKuerzel) + } + + @Test + fun `detects RD2`() { + val line = "... AUTRD2 900308190664 3462613 2021W19650928 " + val parsed = ZnsReiterlicenseTokenizer.parseFromLine(line) + assertNotNull(parsed) + assertEquals(listOf("RD2"), parsed.normalizedKuerzel) + assertEquals(ReiterLizenzKlasseE.RD2, parsed.primaryKlasse) + assertEquals("RD2", parsed.primaryKuerzel) + } + + @Test + fun `returns null when no token present`() { + val line = "some random line without token" + val parsed = ZnsReiterlicenseTokenizer.parseFromLine(line) + assertNull(parsed) + } +} diff --git a/docs/03_Domain/00_Glossary.md b/docs/03_Domain/00_Glossary.md index 3ddda14e..20fd838a 100644 --- a/docs/03_Domain/00_Glossary.md +++ b/docs/03_Domain/00_Glossary.md @@ -29,14 +29,14 @@ Dieses Dokument definiert die **Ubiquitous Language** (allgegenwärtige Sprache) ## K - O * **Lebensnummer:** Eine 9-stellige Nummer (bzw. 15-stellig international), die ein Pferd bei der Geburt vom Zuchtverband erhält. Dient der eindeutigen Identifizierung, ist aber im OEPS-Kontext bei ausländischen Pferden oft generiert und daher nicht zur Suche geeignet. -* **Lizenz (Reit-Lizenz):** Die Qualifikationsstufe eines Reiters (z.B. `R1`, `RD1`, `RD2`, `RS2`). Ein Reiter hat 0..1 Reit-Lizenz. Wird in der Tabelle `reit_lizenzen` als Stammdaten geführt und am Reiter per `reit_lizenz_id` referenziert. +* **Lizenz (Reiterlizenz):** Die Qualifikationsstufe eines Reiters (z.B. `R1`, `RD1`, `RD2`, `RS2`). Ein Reiter hat 0..1 Reiterlizenz. Wird in der Tabelle `reiterlizenzen` als Stammdaten geführt und am Reiter per `reit_lizenz_id` referenziert. * **Fahr-Lizenz:** Eigenständige Lizenzkategorie für den Fahrsport (z.B. `F1`, `F2`). Ein Reiter hat 0..1 Fahr-Lizenz. Stammdaten-Tabelle `fahr_lizenzen`, Referenz am Reiter `fahr_lizenz_id`. * **Nennung:** Die verbindliche Anmeldung eines Paares (Reiter & Pferd) zu einem -> *Bewerb*. * **OEPS:** Österreichischer Pferdesportverband. ## P - T -* **Reit-Lizenzen (Historie):** Optionale Historien-Zuordnungen in `reiter_lizenzen_zuordnung` (z.B. Jahreswechsel). Enthalten den Typ (`REITERLIZENZ`, `STARTKARTE`, `FAHRLIZENZ`), das Kürzel und optional `gültig_bis`. +* **Reiterlizenzen (Historie):** Optionale Historien-Zuordnungen in `reiter_lizenzen_zuordnung` (z.B. Jahreswechsel). Enthalten den Typ (`REITERLIZENZ`, `STARTKARTE`, `FAHRLIZENZ`), das Kürzel und optional `gültig_bis`. * **Satznummer:** * *Pferd:* 10-stellige, rein numerische ID (z.B. `0000123456`), die ein Pferd in der OEPS-Datenbank eindeutig identifiziert. **Primärer Schlüssel für den Datenaustausch.** * *Reiter:* 6-stellige, rein numerische ID für Personen. diff --git a/docs/03_Domain/01_Glossary/Ubiquitous_Language.md b/docs/03_Domain/01_Glossary/Ubiquitous_Language.md index 9728f8d0..f82571d9 100644 --- a/docs/03_Domain/01_Glossary/Ubiquitous_Language.md +++ b/docs/03_Domain/01_Glossary/Ubiquitous_Language.md @@ -95,7 +95,7 @@ Die ÖTO definiert sparten- und klassenabhängige Schwellenwerte, ab wievielen S | Begriff | Definition | ÖTO-Referenz | |------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------| | **Lebensnummer** | 9-stellige (national) bzw. 15-stellige (international, UELN) Nummer, die ein Pferd bei der Geburt vom Zuchtverband erhält. Bei ausländischen Pferden im OEPS oft **generiert** → **nicht zur Suche geeignet**. Die ZNS-Daten zu Lebensnummern sind erfahrungsgemäß inkonsistent und widersprüchlich (z.B. Farbe `"Braun"` vs. `"Brauner"` für dasselbe Pferd). Primärer Schlüssel für den Datenaustausch bleibt die → *Satznummer*. | – | -| **Lizenz (Reit-Lizenz)** | Qualifikationsstufe eines Reiters (z.B. `R1`, `RD1`, `RD2`, `RS2`). Ein Reiter hat 0..1 Reit-Lizenz. Stammdaten in `reit_lizenzen`, Referenz am Reiter `reit_lizenz_id`. | ÖTO Teilnahmeberechtigung | +| **Lizenz (Reiterlizenz)** | Qualifikationsstufe eines Reiters (z.B. `R1`, `RD1`, `RD2`, `RS2`). Ein Reiter hat 0..1 Reiterlizenz. Stammdaten in `reiterlizenzen`, Referenz am Reiter `reit_lizenz_id`. | ÖTO Teilnahmeberechtigung | | **Fahr-Lizenz** | Lizenzkategorie für den Fahrsport (z.B. `F1`, `F2`). Ein Reiter hat 0..1 Fahr-Lizenz. Stammdaten in `fahr_lizenzen`, Referenz am Reiter `fahr_lizenz_id`. | ÖTO Teilnahmeberechtigung | | **Startkarte** | Nachweis der Jahresgebühr. Ein Reiter hat 0..1 Startkarte. Stammdaten in `startkarten`, Referenz am Reiter `startkarte_id`. | ÖTO Teilnahmeberechtigung | diff --git a/docs/03_Domain/02_Reference/OETO_Regelwerk/OETO-2026_A-Teil-Allgemeiner-Teil_16-12-2025.md b/docs/03_Domain/02_Reference/OETO_Regelwerk/OETO-2026_A-Teil-Allgemeiner-Teil_16-12-2025.md index 1603eaa0..fe15c346 100644 --- a/docs/03_Domain/02_Reference/OETO_Regelwerk/OETO-2026_A-Teil-Allgemeiner-Teil_16-12-2025.md +++ b/docs/03_Domain/02_Reference/OETO_Regelwerk/OETO-2026_A-Teil-Allgemeiner-Teil_16-12-2025.md @@ -579,6 +579,44 @@ bewerben der Besitz des ÖFAB und bei Pleasure Drivingbewer- ben des PDC oder des ÖFAB. A-16 2026 2026 A-17 + +--- + +### Reiterlizenzen, § 15 ÖTO + +| Klasse/Höhe | V 80 | 60 - 100 | V 90 | A | 105 - 110 | V 100 | L | 115 - 120 | V 105 | LM | 125 - 130 | M | 135 | V 110 - 115 | S | V120 | 140 - 145 | 150 - 160 | +|-------------|:----:|:--------:|:----:|:-----:|:---------:|:-----:|:-----:|:---------:|:-----:|:---:|:---------:|:----:|:---:|:-----------:|:---:|:----:|:---------:|:---------:| +| Sparte | `V` | `S` | `V` | `D` | `S` | `V` | `D` | `S` | `V` | `D` | `S` | `D` | `S` | `V` | `D` | `V` | `S` | `S` | +| R1 | X | X | X | `B,C` | `B,C` | X | `B,C` | `B,C` | | | | | | | | | | | +| R2 | X | X | X | X | X | X | X | X | X | X | X | `LP` | | | | | | | +| R3 | X | X | X | X | X | X | X | X | X | X | X | X | X | X | | X | X | | +| RD1 | | | | `B,C` | | | `B,C` | | | | | | | | | | | | +| RD2 | | | | X | | | X | | | X | | `LP` | | | | | | | +| RD3 | | | | X | | | X | | | X | | X | | | | | | | +| RD4 | | | | X | | | X | | | X | | X | | | X | | | | +| R1D2 | X | X | X | X | `B,C` | X | X | `B,C` | | X | | `LP` | | | | | | | +| R1D3 | X | X | X | X | `B,C` | X | X | `B,C` | | X | | X | | | | | | | +| R2D3 | X | X | X | X | X | X | X | X | X | X | X | X | | | | | | | +| R1D4 | X | X | X | X | `B,C` | X | X | `B,C` | | X | | X | | | X | | | | +| R2D4 | X | X | X | X | X | X | X | X | X | X | X | X | | | X | | | | +| R3D4 | X | X | X | X | X | X | X | X | X | X | X | X | X | X | X | X | X | | +| RDS4 | X | X | X | X | X | X | X | X | X | X | X | X | X | X | X | X | X | X | +| R1S2 | X | X | X | `B,C` | X | X | `B,C` | X | | | X | | | | | | | | +| R1S3 | X | X | X | `B,C` | X | X | `B,C` | X | | | X | | X | | | | X | | +| R1S4 | X | X | X | `B,C` | X | X | `B,C` | X | | | X | | X | | | | X | X | +| R2S3 | X | X | X | X | X | X | X | X | X | X | X | `LP` | X | | | | X | | +| R2S4 | X | X | X | X | X | X | X | X | X | X | X | `LP` | X | | | | X | X | +| R3S4 | X | X | X | X | X | X | X | X | X | X | X | X | X | X | | X | X | X | + +`B,C` = Turniere der Kat. B, C und C-NEU +`V` = Vielseitigkeit +`S` = Springen +`D` = Dressur +`LP` = Lizenzprüfung +Junioren (14 – 18 Jahre) haben die Möglichkeit, bereits mit der RD2 an FEI-Juniorenaufgaben teilzunehmen. + +--- + Reiterlizenzen, § 15 ÖTO V 60 – V 105 – V 115 – V 25 – V 110 V 140 – 150 – 80 100 90 A 110 100 L 120 105 LM 1 @@ -606,6 +644,9 @@ R2S4 x x x x x x x x x x x LP x x x R3S4 x x x x x x x x x x x x x x x x x B, C = Turniere der Kat. B, C und C-NEU* Junioren (14 – 18 Jahre) haben die Möglichkeit, bereits mit der RD2 an FEI-Juniorenaufgaben teilzunehmen. + +--- + § 15 Reiterlizenzen 1. Für die Teilnahme an