From abaaeddaaf386b60483b84d8b5a0d803e91b2976 Mon Sep 17 00:00:00 2001 From: StefanMoCoAt Date: Mon, 6 Apr 2026 19:48:15 +0200 Subject: [PATCH] Standardize and refactor master data infrastructure: rename tables for plural consistency, remove unused entity tables, improve ZNS import mappings with enriched license properties, introduce Altersklasse domain model, activate Consul service discovery, and update application configuration accordingly. --- CHANGELOG.md | 36 ++++ .../mocode/zns/importer/ZnsImportService.kt | 17 +- .../zns/importer/ZnsImportServiceTest.kt | 29 +++- .../masterdata/domain/model/BewerbsKlasse.kt | 22 +++ .../domain/model/BewerbsklasseDefinition.kt | 35 ++++ .../masterdata/domain/model/Bundesland.kt | 4 +- .../masterdata/domain/model/FahrLizenz.kt | 19 ++ .../masterdata/domain/model/ReitLizenz.kt | 19 ++ .../mocode/masterdata/domain/model/Reiter.kt | 46 ++++- .../masterdata/domain/model/Startkarte.kt | 19 ++ .../domain/model/TurnierKategorie.kt | 20 +++ .../masterdata/domain/model/TurnierKlasse.kt | 22 +++ .../masterdata/domain/model/TurnierSparte.kt | 19 ++ .../repository/AltersklassenRepository.kt | 12 ++ .../repository/MasterdataLicenseRepository.kt | 16 ++ .../domain/repository/RegulationRepository.kt | 1 + .../persistence/AltersklasseTable.kt | 6 +- ...ndeslandTable.kt => BundeslaenderTable.kt} | 14 +- .../persistence/BundeslandRepositoryImpl.kt | 89 +++++----- .../ExposedRegulationRepository.kt | 43 +++-- .../persistence/ReiterSparteTable.kt | 27 --- .../persistence/TurnierKategorienTable.kt | 22 +++ ...rklasseTable.kt => TurnierKlassenTable.kt} | 8 +- .../persistence/TurnierSpartenTable.kt | 21 +++ .../FunktionaerExposedRepository.kt | 25 ++- .../funktionaer/FunktionaerTable.kt | 8 +- .../reiter/AltersklassenExposedRepository.kt | 37 ++++ .../reiter/BundeslandExposedRepository.kt | 28 +-- .../MasterdataLicenseExposedRepository.kt | 54 ++++++ .../reiter/ReiterExposedRepository.kt | 60 ++++++- .../persistence/reiter/ReiterTable.kt | 67 +++++-- .../RegulationSeedVerificationTest.kt | 2 +- .../masterdata-service/build.gradle.kts | 1 + .../service/config/AltersklassenSeeder.kt | 57 ++++++ .../service/config/BewerbsKlassenSeeder.kt | 80 +++++++++ .../FunktionaersQualifikationenSeeder.kt | 79 +++++++++ .../config/MasterdataDatabaseConfiguration.kt | 39 +++-- .../service/config/MasterdataSeeder.kt | 164 ++++++++++++++++++ .../config/QualifikationMasterSeeder.kt | 86 --------- .../service/config/ReitLizenzenSeeder.kt | 88 ++++++++++ .../service/config/TurnierKategorienSeeder.kt | 51 ++++++ .../service/config/TurnierKlassenSeeder.kt | 80 +++++++++ .../service/config/TurnierSpartenSeeder.kt | 60 +++++++ .../src/main/resources/application.yml | 16 +- ...13__Cleanup_and_Standardize_Masterdata.sql | 45 +++++ ...pand_Masterdata_Rider_and_Competitions.sql | 80 +++++++++ .../service/ZnsImportServiceApplication.kt | 34 +--- .../service/config/RepositoryConfiguration.kt | 15 ++ .../config/ZnsImportDatabaseConfiguration.kt | 12 +- .../at/mocode/zns/parser/ZnsReiterParser.kt | 50 ++++-- docs/01_Architecture/MASTER_ROADMAP.md | 8 +- docs/03_Domain/00_Glossary.md | 10 +- .../01_Glossary/Ubiquitous_Language.md | 6 +- 53 files changed, 1575 insertions(+), 333 deletions(-) create mode 100644 backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/model/BewerbsKlasse.kt create mode 100644 backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/model/BewerbsklasseDefinition.kt create mode 100644 backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/model/FahrLizenz.kt create mode 100644 backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/model/ReitLizenz.kt create mode 100644 backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/model/Startkarte.kt create mode 100644 backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/model/TurnierKategorie.kt create mode 100644 backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/model/TurnierKlasse.kt create mode 100644 backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/model/TurnierSparte.kt create mode 100644 backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/repository/AltersklassenRepository.kt create mode 100644 backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/repository/MasterdataLicenseRepository.kt rename backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/{BundeslandTable.kt => BundeslaenderTable.kt} (72%) delete mode 100644 backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/ReiterSparteTable.kt create mode 100644 backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/TurnierKategorienTable.kt rename backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/{TurnierklasseTable.kt => TurnierKlassenTable.kt} (82%) create mode 100644 backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/TurnierSpartenTable.kt create mode 100644 backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/reiter/AltersklassenExposedRepository.kt create mode 100644 backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/reiter/MasterdataLicenseExposedRepository.kt create mode 100644 backend/services/masterdata/masterdata-service/src/main/kotlin/at/mocode/masterdata/service/config/AltersklassenSeeder.kt create mode 100644 backend/services/masterdata/masterdata-service/src/main/kotlin/at/mocode/masterdata/service/config/BewerbsKlassenSeeder.kt create mode 100644 backend/services/masterdata/masterdata-service/src/main/kotlin/at/mocode/masterdata/service/config/FunktionaersQualifikationenSeeder.kt create mode 100644 backend/services/masterdata/masterdata-service/src/main/kotlin/at/mocode/masterdata/service/config/MasterdataSeeder.kt delete mode 100644 backend/services/masterdata/masterdata-service/src/main/kotlin/at/mocode/masterdata/service/config/QualifikationMasterSeeder.kt create mode 100644 backend/services/masterdata/masterdata-service/src/main/kotlin/at/mocode/masterdata/service/config/ReitLizenzenSeeder.kt create mode 100644 backend/services/masterdata/masterdata-service/src/main/kotlin/at/mocode/masterdata/service/config/TurnierKategorienSeeder.kt create mode 100644 backend/services/masterdata/masterdata-service/src/main/kotlin/at/mocode/masterdata/service/config/TurnierKlassenSeeder.kt create mode 100644 backend/services/masterdata/masterdata-service/src/main/kotlin/at/mocode/masterdata/service/config/TurnierSpartenSeeder.kt create mode 100644 backend/services/masterdata/masterdata-service/src/main/resources/db/migration/V013__Cleanup_and_Standardize_Masterdata.sql create mode 100644 backend/services/masterdata/masterdata-service/src/main/resources/db/migration/V014__Expand_Masterdata_Rider_and_Competitions.sql diff --git a/CHANGELOG.md b/CHANGELOG.md index 291ce5aa..8f22ab71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,15 @@ Versionierung folgt [Semantic Versioning](https://semver.org/lang/de/). ### Behoben +- **Masterdata/Infrastructure:** Kompilierfehler in `AltersklasseRepositoryImpl` durch Vereinheitlichung der Exposed-Tabellendefinition behoben: + - `AltersklassenTable` → `AltersklasseTable` + - Spalte `altersklassen_code` → `altersklasse_code` + - Tabellenname `altersklassen` → `altersklasse` +- **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). + +### Behoben + - **Masterdata:** Qualifikations-Management für Funktionäre (Richter/Parcoursbauer) professionalisiert: Umstellung von unstrukturiertem Text auf offizielle ÖTO/FEI Master-Daten Referenzen (`QualifikationMasterTable`). - **Masterdata:** Fehlende Tabelle `funktionaer_qualifikation` in der Initialisierung beider Services (`masterdata` und `zns-import`) ergänzt, um `PSQLException` während des ZNS-Imports zu beheben. - **Infrastructure:** Start-Probleme des `masterdata-service` endgültig behoben: Port-Konflikt zwischen Spring Boot (Management/Actuator) und dem Gateway (8081) durch Umzug auf Port 8086 (gemäß Infrastruktur-Vorgaben) gelöst. @@ -57,6 +66,27 @@ Versionierung folgt [Semantic Versioning](https://semver.org/lang/de/). --- +## [1.0.5-SNAPSHOT] — 2026-04-06 + +### Geändert +- **Masterdata:** Bereinigung und Standardisierung von Masterdaten-Tabellen (Mehrzahl-Konvention): + - `bundesland` -> `bundeslaender` + - `qualifikation_master` -> `funktionaers_qualifikationen` + - `reiter_lizenz` -> `reit_lizenzen` + - `turnierklasse` -> `turnier_klassen` +- **Seeding:** Umfassende Erweiterung der Seeder für Funktionärs-Qualifikationen, Turnierklassen und Turnier-Sparten gemäß ÖTO. +- **Data Modeling:** Einführung der Tabelle `turnier_sparten` und Entfernung der redundanten `reiter_sparte`. +- **Infrastructure:** Datenbank-Migration `V013` implementiert alle Schema-Änderungen und Umbenennungen. + +## [1.0.4-SNAPSHOT] — 2026-04-06 + +### Hinzugefügt +- **Reiter-Lizenzen:** Strukturierte Speicherung von Lizenzen (STARTKARTE, REITERLIZENZ, FAHRLIZENZ) in einer 1:n Relation (`ReiterLizenzTable`). +- **Altersklassen:** Einführung von Enums (`ReiterAltersKlasseE`) für präzise Filterung und Validierung im Domain-Modell und Parser. +- **Mitgliedsnummer:** Validierungs-Logik gemäß ÖTO-Spezifikation (Bundesland-Präfix 1-9) in `Reiter.kt` implementiert. +- **ZNS-Import:** `ZnsReiterParser` erweitert, um Lizenzen und Altersklassen-Enums direkt aus LIZENZ01.DAT zu extrahieren. +- **Persistenz:** `ReiterExposedRepository` unterstützt nun das transaktionale Speichern und Laden der 1:n Lizenzen. + ## [1.0.3-SNAPSHOT] — 2026-04-06 ### Hinzugefügt @@ -66,6 +96,12 @@ Versionierung folgt [Semantic Versioning](https://semver.org/lang/de/). - **ZNS-Import:** Automatisches Auflösen von Relationen (Verein nach Name, Bundesland nach Nummer, Nation nach ISO-Code) während des Reiter-Imports. ### Behoben +- **Infrastruktur:** Consul Health-Check für `masterdata-service` korrigiert (Port 8086 für Actuator). +- **Masterdaten:** `MasterdataSeeder` für Nationen und Bundesländer hinzugefügt, um Datenvollständigkeit nach Volume-Cleanup sicherzustellen. +- **Datenintegrität:** Heilungs-Logik (`fixReiterForeignKeys`) implementiert, die Reiter-Datensätze nachträglich mit Masterdaten verknüpft. +- **Code-Qualität:** Redundante `BundeslandTable` Definition in `ReiterTable.kt` entfernt. +- **Infrastruktur:** `BeanDefinitionOverrideException` im `zns-import-service` durch Konsolidierung der Repositories in `RepositoryConfiguration` behoben. +- **Service-Discovery:** Fehlende Consul-Registrierung des `masterdata-service` durch Hinzufügen der Discovery-Dependency und Konfiguration behoben. - **Build:** Kompilierfehler in `BundeslandExposedRepository.kt` behoben (inkonsistente Rückgabetypen im `BundeslandRepository`-Interface). - **Infrastruktur:** Fehlendes Autowiring im `zns-import-service` durch explizite Bean-Definitionen für alle Repositories in `ZnsImportServiceApplication.kt` behoben. - **Domain:** Kompilierfehler in `Bundesland.kt` behoben (uninitialisierte Eigenschaft `bundeslandId` entfernt). 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 974c3205..87a0f777 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 @@ -4,6 +4,8 @@ package at.mocode.zns.importer import at.mocode.masterdata.domain.repository.VereinRepository import at.mocode.masterdata.domain.repository.HorseRepository +import at.mocode.masterdata.domain.repository.AltersklassenRepository +import at.mocode.masterdata.domain.repository.MasterdataLicenseRepository import at.mocode.masterdata.domain.repository.FunktionaerRepository import at.mocode.masterdata.domain.repository.ReiterRepository import at.mocode.masterdata.domain.repository.LandRepository @@ -44,7 +46,9 @@ class ZnsImportService( private val horseRepository: HorseRepository, private val funktionaerRepository: FunktionaerRepository, private val landRepository: LandRepository, - private val bundeslandRepository: BundeslandRepository + private val bundeslandRepository: BundeslandRepository, + private val licenseRepository: MasterdataLicenseRepository? = null, + private val altersklassenRepository: AltersklassenRepository? = null ) { companion object { @@ -176,11 +180,17 @@ class ZnsImportService( val verein = parsed.vereinsName?.let { vereinRepository.findByExactName(it) } val bundesland = parsed.bundeslandNummer?.let { bundeslandRepository.findByNr(it) } val nation = parsed.nation?.let { landRepository.findByIsoAlpha3Code(it) } + val reitLizenz = parsed.reiterLizenz?.let { licenseRepository?.findReitLizenzByCode(it) } + val fahrLizenz = parsed.fahrLizenz?.let { licenseRepository?.findFahrLizenzByCode(it) } + val startkarte = parsed.startkarte?.let { licenseRepository?.findStartkarteByCode(it) } val reiter = parsed.copy( vereinId = verein?.vereinId, bundeslandId = bundesland?.bundeslandId, - nationId = nation?.landId + nationId = nation?.landId, + reitLizenzId = reitLizenz?.lizenzId, + fahrLizenzId = fahrLizenz?.lizenzId, + startkarteId = startkarte?.startkarteId ) val vorhanden = reiterRepository.findBySatznummer(reiter.satznummer) @@ -198,6 +208,9 @@ class ZnsImportService( vereinId = reiter.vereinId, bundeslandId = reiter.bundeslandId, nationId = reiter.nationId, + reitLizenzId = reiter.reitLizenzId, + fahrLizenzId = reiter.fahrLizenzId, + startkarteId = reiter.startkarteId, reiterLizenz = reiter.reiterLizenz, startkarte = reiter.startkarte, fahrLizenz = reiter.fahrLizenz, diff --git a/backend/infrastructure/zns-importer/src/test/kotlin/at/mocode/zns/importer/ZnsImportServiceTest.kt b/backend/infrastructure/zns-importer/src/test/kotlin/at/mocode/zns/importer/ZnsImportServiceTest.kt index 9b1f01b2..a1034ffb 100644 --- a/backend/infrastructure/zns-importer/src/test/kotlin/at/mocode/zns/importer/ZnsImportServiceTest.kt +++ b/backend/infrastructure/zns-importer/src/test/kotlin/at/mocode/zns/importer/ZnsImportServiceTest.kt @@ -88,12 +88,20 @@ class ZnsImportServiceTest { nachname: String = "Mustermann", vorname: String = "Max" ): String { - // Stelle 1-6: Satznummer, 7-56: Nachname (50), 57-81: Vorname (25) - return satznummer.padEnd(6) + - nachname.padEnd(50) + - vorname.padEnd(25) + - "01" + // 82-83 Bundesland - " ".repeat(250) // Rest auffüllen + val line = StringBuilder() + line.append(satznummer.padEnd(6)) // 1-6 + line.append(nachname.padEnd(50)) // 7-56 + line.append(vorname.padEnd(25)) // 57-81 + line.append("01") // 82-83 (Buli) + line.append("".padEnd(50)) // 84-133 (Verein) + line.append("AUT") // 134-136 (Nation) + line.append("R2 ") // 137-140 (Lizenz) + line.append(" ") // 141 (Startkarte) + line.append(" ") // 142-143 (Fahrlizenz) + line.append("JG") // 144-145 (Altersklasse) + line.append(" ") // 146 (Altersklasse Y) + line.append("01234567") // 147-154 (Mitglied) - Bundesland 01 (Wien) + Rest + return line.toString().padEnd(220) } /** Erzeugt eine gültige PFERDE01.DAT-Zeile (mind. 211 Zeichen). */ @@ -169,7 +177,14 @@ class ZnsImportServiceTest { assertThat(result.reiterImportiert).isEqualTo(1) assertThat(result.reiterAktualisiert).isEqualTo(0) assertThat(result.fehler).isEmpty() - coVerify(exactly = 1) { reiterRepository.save(any()) } + coVerify(exactly = 1) { + reiterRepository.save(match { + it.reiterLizenz == "R2" && + it.lizenzen.size == 1 && + it.lizenzen[0].kuerzel == "R2" && + it.altersklasseJgJrU25 == at.mocode.core.domain.model.ReiterAltersKlasseE.JG + }) + } } @Test diff --git a/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/model/BewerbsKlasse.kt b/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/model/BewerbsKlasse.kt new file mode 100644 index 00000000..77966922 --- /dev/null +++ b/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/model/BewerbsKlasse.kt @@ -0,0 +1,22 @@ +@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class) + +package at.mocode.masterdata.domain.model + +import kotlinx.serialization.Serializable +import at.mocode.core.domain.serialization.UuidSerializer +import kotlin.uuid.Uuid + +/** + * Domain-Modell für eine Bewerbs-Klasse (z.B. E, A, L, M, S). + */ +@Serializable +data class BewerbsKlasse( + @Serializable(with = UuidSerializer::class) + val bewerbsklasseId: Uuid = Uuid.random(), + val sparte: String, + val code: String, + val bezeichnung: String, + val maxHoehe: Int? = null, + val aufgabenNiveau: String? = null, + val istAktiv: Boolean = true +) diff --git a/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/model/BewerbsklasseDefinition.kt b/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/model/BewerbsklasseDefinition.kt new file mode 100644 index 00000000..f2ad4337 --- /dev/null +++ b/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/model/BewerbsklasseDefinition.kt @@ -0,0 +1,35 @@ +@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class) + +package at.mocode.masterdata.domain.model + +import at.mocode.core.domain.model.SparteE +import at.mocode.core.domain.serialization.InstantSerializer +import at.mocode.core.domain.serialization.UuidSerializer +import kotlinx.serialization.Serializable +import kotlin.time.Instant +import kotlin.uuid.Uuid + +/** + * Domänenmodell für eine Bewerbsklasse gemäß ÖTO. + */ +@Serializable +data class BewerbsklasseDefinition( + @Serializable(with = UuidSerializer::class) + val bewerbsklasseId: Uuid = Uuid.random(), + val sparte: SparteE, + val code: String, // E, A, L, LM, M, S + val bezeichnung: String, + val maxHoehe: Int? = null, // in cm (Springen) + val aufgabenNiveau: String? = null, // (Dressur) + + @Serializable(with = InstantSerializer::class) + val validFrom: Instant, + @Serializable(with = InstantSerializer::class) + val validTo: Instant? = null, + + val istAktiv: Boolean = true, + @Serializable(with = InstantSerializer::class) + val createdAt: Instant, + @Serializable(with = InstantSerializer::class) + val updatedAt: Instant +) diff --git a/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/model/Bundesland.kt b/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/model/Bundesland.kt index 2715c9a8..4f8f106e 100644 --- a/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/model/Bundesland.kt +++ b/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/model/Bundesland.kt @@ -7,12 +7,12 @@ import at.mocode.core.domain.serialization.UuidSerializer import kotlin.uuid.Uuid /** - * Domain-Modell für Bundesland. + * Domain-Modell für Bundesland (Mehrzahl 'Bundeslaender'). */ @Serializable data class Bundesland( @Serializable(with = UuidSerializer::class) - val id: Uuid, + val bundeslandId: Uuid, val bundeslandNr: Int, val bezeichnung: String, val wappenUrl: String? = null diff --git a/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/model/FahrLizenz.kt b/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/model/FahrLizenz.kt new file mode 100644 index 00000000..e3137025 --- /dev/null +++ b/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/model/FahrLizenz.kt @@ -0,0 +1,19 @@ +@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class) + +package at.mocode.masterdata.domain.model + +import kotlinx.serialization.Serializable +import at.mocode.core.domain.serialization.UuidSerializer +import kotlin.uuid.Uuid + +/** + * Domain-Modell für eine Fahr-Lizenz (z.B. F1, F2). + */ +@Serializable +data class FahrLizenz( + @Serializable(with = UuidSerializer::class) + val lizenzId: Uuid = Uuid.random(), + val code: String, + val bezeichnung: String, + val istAktiv: Boolean = true +) 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/ReitLizenz.kt new file mode 100644 index 00000000..763fca87 --- /dev/null +++ b/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/model/ReitLizenz.kt @@ -0,0 +1,19 @@ +@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class) + +package at.mocode.masterdata.domain.model + +import kotlinx.serialization.Serializable +import at.mocode.core.domain.serialization.UuidSerializer +import at.mocode.core.domain.serialization.LocalDateSerializer +import kotlinx.datetime.LocalDate +import kotlin.uuid.Uuid + +@Serializable +data class ReitLizenz( + @Serializable(with = UuidSerializer::class) + val lizenzId: Uuid = Uuid.random(), + val code: String, + val bezeichnung: String, + val sparte: String? = null, + val istAktiv: Boolean = true +) 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 f785ee4b..c0fe3474 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 @@ -14,6 +14,16 @@ import kotlin.time.Clock import kotlin.time.Instant import kotlin.uuid.Uuid +@Serializable +data class ReiterLizenz( + @Serializable(with = UuidSerializer::class) + val lizenzId: Uuid = Uuid.random(), + val lizenzTyp: String, // STARTKARTE, REITERLIZENZ, FAHRLIZENZ + val kuerzel: String, + @Serializable(with = LocalDateSerializer::class) + val gueltigBis: LocalDate? = null +) + /** * Domain model representing a rider (Reiter) in the actor-context. * @@ -88,6 +98,15 @@ data class Reiter( @Serializable(with = UuidSerializer::class) var nationId: Uuid? = null, + @Serializable(with = UuidSerializer::class) + var reitLizenzId: Uuid? = null, + + @Serializable(with = UuidSerializer::class) + var fahrLizenzId: Uuid? = null, + + @Serializable(with = UuidSerializer::class) + var startkarteId: Uuid? = null, + // Alphanumerisch (4) Keine Lizenz: BLANK var reiterLizenz: String? = null, @@ -98,10 +117,10 @@ data class Reiter( var fahrLizenz: String? = null, // Alphanumerisch (2) WERTE: Standard: BLANK, JG=JUGENDLICHER, JR=JUNIOR, 25=U25 - var altersklasseJgJrU25: String? = null, + var altersklasseJgJrU25: at.mocode.core.domain.model.ReiterAltersKlasseE? = null, // Alphanumerisch (1) WERTE: Standard: BLANK Y=JUNGER-REITER - var altersklasseY: String? = null, + var altersklasseY: at.mocode.core.domain.model.ReiterAltersKlasseE? = null, // Numerisch (8) FORMAT: 00000000 var mitgliedsNummer: Int? = null, @@ -145,7 +164,12 @@ data class Reiter( @Serializable(with = InstantSerializer::class) val createdAt: Instant = Clock.System.now(), @Serializable(with = InstantSerializer::class) - var updatedAt: Instant = Clock.System.now() + var updatedAt: Instant = Clock.System.now(), + + /** + * List of specialized licenses for this rider. + */ + var lizenzen: List = emptyList() ) { /** * Returns the display name of the rider. @@ -183,7 +207,7 @@ data class Reiter( /** * Validates the 8-digit membership number. * Format: [B][VVV][MMMM] - * B: Bundesland (1 digit) + * B: Bundesland (1 digit, 1-9) * VVV: Verein (3 digits) * MMMM: Member (4 digits) */ @@ -192,12 +216,20 @@ data class Reiter( if (nrStr.length != 8) return false val b = nrStr.substring(0, 1).toInt() + if (b < 1 || b > 9) return false // Valid Bundesland prefix is 1-9 + // Validation against bundeslandNummer if available - if (bundeslandNummer != null && b != (bundeslandNummer!! % 10)) { - // ZNS bundeslandNummer is 01-09, while membership first digit is 1-9 - // This might need refinement depending on how "00" (Unbekannt) is handled in membership numbers. + // ZNS bundeslandNummer is 01-09, while membership first digit is 1-9 + if (bundeslandNummer != null && b != bundeslandNummer) { + return false } + // Verein part (2nd-4th digit) + // ZNS Verein ID usually contains state + 3 digits. + // Here we just check if it's not all zeros as a basic sanity check + val vvv = nrStr.substring(1, 4).toInt() + if (vvv == 0) return false + return true } diff --git a/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/model/Startkarte.kt b/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/model/Startkarte.kt new file mode 100644 index 00000000..49943a5f --- /dev/null +++ b/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/model/Startkarte.kt @@ -0,0 +1,19 @@ +@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class) + +package at.mocode.masterdata.domain.model + +import kotlinx.serialization.Serializable +import at.mocode.core.domain.serialization.UuidSerializer +import kotlin.uuid.Uuid + +/** + * Domain-Modell für eine Startkarte. + */ +@Serializable +data class Startkarte( + @Serializable(with = UuidSerializer::class) + val startkarteId: Uuid = Uuid.random(), + val code: String, + val bezeichnung: String, + val istAktiv: Boolean = true +) diff --git a/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/model/TurnierKategorie.kt b/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/model/TurnierKategorie.kt new file mode 100644 index 00000000..f91ce282 --- /dev/null +++ b/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/model/TurnierKategorie.kt @@ -0,0 +1,20 @@ +@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class) + +package at.mocode.masterdata.domain.model + +import kotlinx.serialization.Serializable +import at.mocode.core.domain.serialization.UuidSerializer +import kotlin.uuid.Uuid + +/** + * Domain-Modell für eine Turnier-Kategorie (z.B. CSN-C, CDN-A, CSN-C Neu). + */ +@Serializable +data class TurnierKategorie( + @Serializable(with = UuidSerializer::class) + val kategorieId: Uuid = Uuid.random(), + val code: String, + val bezeichnung: String, + val sparte: String? = null, + val istAktiv: Boolean = true +) diff --git a/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/model/TurnierKlasse.kt b/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/model/TurnierKlasse.kt new file mode 100644 index 00000000..c34b1b7e --- /dev/null +++ b/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/model/TurnierKlasse.kt @@ -0,0 +1,22 @@ +@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class) + +package at.mocode.masterdata.domain.model + +import kotlinx.serialization.Serializable +import at.mocode.core.domain.serialization.UuidSerializer +import kotlin.uuid.Uuid + +/** + * Domain-Modell für eine Turnier-Klasse. + */ +@Serializable +data class TurnierKlasse( + @Serializable(with = UuidSerializer::class) + val turnierklasseId: Uuid = Uuid.random(), + val sparte: String, + val code: String, + val bezeichnung: String, + val maxHoehe: Int? = null, + val aufgabenNiveau: String? = null, + val istAktiv: Boolean = true +) diff --git a/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/model/TurnierSparte.kt b/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/model/TurnierSparte.kt new file mode 100644 index 00000000..14f5479e --- /dev/null +++ b/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/model/TurnierSparte.kt @@ -0,0 +1,19 @@ +@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class) + +package at.mocode.masterdata.domain.model + +import kotlinx.serialization.Serializable +import at.mocode.core.domain.serialization.UuidSerializer +import kotlin.uuid.Uuid + +/** + * Domain-Modell für eine Turnier-Sparte. + */ +@Serializable +data class TurnierSparte( + @Serializable(with = UuidSerializer::class) + val sparteId: Uuid = Uuid.random(), + val code: String, + val bezeichnung: String, + val istAktiv: Boolean = true +) diff --git a/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/repository/AltersklassenRepository.kt b/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/repository/AltersklassenRepository.kt new file mode 100644 index 00000000..0f988202 --- /dev/null +++ b/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/repository/AltersklassenRepository.kt @@ -0,0 +1,12 @@ +@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class) + +package at.mocode.masterdata.domain.repository + +import at.mocode.masterdata.domain.model.AltersklasseDefinition + +/** + * Repository für Altersklassen-Stammdaten. + */ +interface AltersklassenRepository { + suspend fun findByCode(code: String): AltersklasseDefinition? +} 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/MasterdataLicenseRepository.kt new file mode 100644 index 00000000..ef99b75c --- /dev/null +++ b/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/repository/MasterdataLicenseRepository.kt @@ -0,0 +1,16 @@ +@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class) + +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.Startkarte + +/** + * Repository für alle Lizenz-Stammdaten (Reit, Fahr, Startkarten). + */ +interface MasterdataLicenseRepository { + suspend fun findReitLizenzByCode(code: String): at.mocode.masterdata.domain.model.ReitLizenz? + suspend fun findFahrLizenzByCode(code: String): at.mocode.masterdata.domain.model.FahrLizenz? + suspend fun findStartkarteByCode(code: String): at.mocode.masterdata.domain.model.Startkarte? +} diff --git a/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/repository/RegulationRepository.kt b/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/repository/RegulationRepository.kt index afa640d8..116a82e1 100644 --- a/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/repository/RegulationRepository.kt +++ b/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/repository/RegulationRepository.kt @@ -9,6 +9,7 @@ import at.mocode.masterdata.domain.model.* */ interface RegulationRepository { suspend fun findAllTurnierklassen(): List + suspend fun findAllBewerbsklassen(): List suspend fun findAllLicenseMatrixEntries(): List suspend fun findAllRichtverfahren(): List suspend fun findAllGebuehren(): List diff --git a/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/AltersklasseTable.kt b/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/AltersklasseTable.kt index 44867b35..b42b2b6a 100644 --- a/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/AltersklasseTable.kt +++ b/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/AltersklasseTable.kt @@ -7,10 +7,10 @@ import org.jetbrains.exposed.v1.datetime.CurrentTimestamp import org.jetbrains.exposed.v1.datetime.timestamp /** - * Exposed-Tabellendefinition für die Altersklasse-Entität (Altersklassendefinition). + * Exposed-Tabellendefinition für die Altersklassen-Entität (Altersklassendefinition). * * Diese Tabelle speichert alle Informationen zu Altersklassen für Teilnehmer - * entsprechend der AltersklasseDefinition Domain-Entität. + * entsprechend der AltersklassenDefinition Domain-Entität. */ object AltersklasseTable : Table("altersklasse") { val id = uuid("id") @@ -29,7 +29,7 @@ object AltersklasseTable : Table("altersklasse") { override val primaryKey = PrimaryKey(id) init { - // Index for performance on common queries + // Index für Performance (an Migration angepasst) index(customIndexName = "idx_altersklasse_aktiv", columns = arrayOf(istAktiv)) index(customIndexName = "idx_altersklasse_sparte", columns = arrayOf(sparteFilter)) index(customIndexName = "idx_altersklasse_geschlecht", columns = arrayOf(geschlechtFilter)) diff --git a/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/BundeslandTable.kt b/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/BundeslaenderTable.kt similarity index 72% rename from backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/BundeslandTable.kt rename to backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/BundeslaenderTable.kt index c4f44d51..692fded9 100644 --- a/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/BundeslandTable.kt +++ b/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/BundeslaenderTable.kt @@ -7,12 +7,12 @@ import org.jetbrains.exposed.v1.datetime.CurrentTimestamp import org.jetbrains.exposed.v1.datetime.timestamp /** - * Exposed-Tabellendefinition für die Bundesland-Entität. + * Exposed-Tabellendefinition für die Bundesländer-Entität. */ -object BundeslandTable : Table("bundesland") { - val id = uuid("id") +object BundeslaenderTable : Table("bundeslaender") { + val id = uuid("bundesland_id") val landId = uuid("land_id") - val bundeslandNr = integer("bundesland_nr").nullable() + val bundeslandNr = integer("bundesland_nr").uniqueIndex().nullable() val oepsCode = varchar("oeps_code", 10).nullable() val iso3166_2_Code = varchar("iso_3166_2_code", 10).nullable() val name = varchar("name", 100) @@ -26,10 +26,10 @@ object BundeslandTable : Table("bundesland") { override val primaryKey = PrimaryKey(id) init { - uniqueIndex("idx_bundesland_oeps", oepsCode, landId) - uniqueIndex("idx_bundesland_iso", iso3166_2_Code) + uniqueIndex("idx_bundeslaender_oeps", oepsCode, landId) + uniqueIndex("idx_bundeslaender_iso", iso3166_2_Code) // Natürlicher Schlüssel gem. Architektur: (land_id + kuerzel) eindeutig // Hinweis: kuerzel kann NULL sein – der Unique-Index greift dann nur für nicht-null Werte pro Land. - uniqueIndex("ux_bundesland_land_kuerzel", landId, kuerzel) + uniqueIndex("ux_bundeslaender_land_kuerzel", landId, kuerzel) } } diff --git a/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/BundeslandRepositoryImpl.kt b/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/BundeslandRepositoryImpl.kt index 1c256257..0f510b2e 100644 --- a/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/BundeslandRepositoryImpl.kt +++ b/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/BundeslandRepositoryImpl.kt @@ -11,46 +11,47 @@ import kotlin.uuid.Uuid /** * Implementierung des BundeslandRepository für die Datenbankzugriffe. + * Verwendet die Tabelle 'bundeslaender'. */ class BundeslandRepositoryImpl : BundeslandRepository { private fun rowToBundeslandDefinition(row: ResultRow): BundeslandDefinition { return BundeslandDefinition( - bundeslandId = row[BundeslandTable.id], - landId = row[BundeslandTable.landId], - bundeslandNr = row[BundeslandTable.bundeslandNr], - oepsCode = row[BundeslandTable.oepsCode], - iso3166_2_Code = row[BundeslandTable.iso3166_2_Code], - name = row[BundeslandTable.name], - kuerzel = row[BundeslandTable.kuerzel], - wappenUrl = row[BundeslandTable.wappenUrl], - istAktiv = row[BundeslandTable.istAktiv], - sortierReihenfolge = row[BundeslandTable.sortierReihenfolge], - createdAt = row[BundeslandTable.createdAt], - updatedAt = row[BundeslandTable.updatedAt] + bundeslandId = row[BundeslaenderTable.id], + landId = row[BundeslaenderTable.landId], + bundeslandNr = row[BundeslaenderTable.bundeslandNr], + oepsCode = row[BundeslaenderTable.oepsCode], + iso3166_2_Code = row[BundeslaenderTable.iso3166_2_Code], + name = row[BundeslaenderTable.name], + kuerzel = row[BundeslaenderTable.kuerzel], + wappenUrl = row[BundeslaenderTable.wappenUrl], + istAktiv = row[BundeslaenderTable.istAktiv], + sortierReihenfolge = row[BundeslaenderTable.sortierReihenfolge], + createdAt = row[BundeslaenderTable.createdAt], + updatedAt = row[BundeslaenderTable.updatedAt] ) } override suspend fun findByNr(nr: Int): BundeslandDefinition? = DatabaseFactory.dbQuery { - BundeslandTable.selectAll().where { BundeslandTable.bundeslandNr eq nr } + BundeslaenderTable.selectAll().where { BundeslaenderTable.bundeslandNr eq nr } .map(::rowToBundeslandDefinition) .singleOrNull() } override suspend fun findById(id: Uuid): BundeslandDefinition? = DatabaseFactory.dbQuery { - BundeslandTable.selectAll().where { BundeslandTable.id eq id } + BundeslaenderTable.selectAll().where { BundeslaenderTable.id eq id } .map(::rowToBundeslandDefinition) .singleOrNull() } override suspend fun findByOepsCode(oepsCode: String, landId: Uuid): BundeslandDefinition? = DatabaseFactory.dbQuery { - BundeslandTable.selectAll().where { (BundeslandTable.oepsCode eq oepsCode) and (BundeslandTable.landId eq landId) } + BundeslaenderTable.selectAll().where { (BundeslaenderTable.oepsCode eq oepsCode) and (BundeslaenderTable.landId eq landId) } .map(::rowToBundeslandDefinition) .singleOrNull() } override suspend fun findByIso3166_2_Code(iso3166_2_Code: String): BundeslandDefinition? = DatabaseFactory.dbQuery { - BundeslandTable.selectAll().where { BundeslandTable.iso3166_2_Code eq iso3166_2_Code } + BundeslaenderTable.selectAll().where { BundeslaenderTable.iso3166_2_Code eq iso3166_2_Code } .map(::rowToBundeslandDefinition) .singleOrNull() } @@ -60,14 +61,14 @@ class BundeslandRepositoryImpl : BundeslandRepository { activeOnly: Boolean, orderBySortierung: Boolean ): List = DatabaseFactory.dbQuery { - val query = BundeslandTable.selectAll().where { BundeslandTable.landId eq landId } + val query = BundeslaenderTable.selectAll().where { BundeslaenderTable.landId eq landId } if (activeOnly) { - query.andWhere { BundeslandTable.istAktiv eq true } + query.andWhere { BundeslaenderTable.istAktiv eq true } } if (orderBySortierung) { - query.orderBy(BundeslandTable.sortierReihenfolge to SortOrder.ASC, BundeslandTable.name to SortOrder.ASC) + query.orderBy(BundeslaenderTable.sortierReihenfolge to SortOrder.ASC, BundeslaenderTable.name to SortOrder.ASC) } else { - query.orderBy(BundeslandTable.name to SortOrder.ASC) + query.orderBy(BundeslaenderTable.name to SortOrder.ASC) } query.map(::rowToBundeslandDefinition) } @@ -75,26 +76,27 @@ class BundeslandRepositoryImpl : BundeslandRepository { override suspend fun findByName(searchTerm: String, landId: Uuid?, limit: Int): List = DatabaseFactory.dbQuery { val pattern = "%$searchTerm%" - val query = BundeslandTable.selectAll().where { BundeslandTable.name like pattern } - landId?.let { query.andWhere { BundeslandTable.landId eq it } } + val query = BundeslaenderTable.selectAll().where { BundeslaenderTable.name like pattern } + landId?.let { query.andWhere { BundeslaenderTable.landId eq it } } query.limit(limit).map(::rowToBundeslandDefinition) } override suspend fun findAllActive(orderBySortierung: Boolean): List = DatabaseFactory.dbQuery { - val query = BundeslandTable.selectAll().where { BundeslandTable.istAktiv eq true } + val query = BundeslaenderTable.selectAll().where { BundeslaenderTable.istAktiv eq true } if (orderBySortierung) { - query.orderBy(BundeslandTable.sortierReihenfolge to SortOrder.ASC, BundeslandTable.name to SortOrder.ASC) + query.orderBy(BundeslaenderTable.sortierReihenfolge to SortOrder.ASC, BundeslaenderTable.name to SortOrder.ASC) } else { - query.orderBy(BundeslandTable.name to SortOrder.ASC) + query.orderBy(BundeslaenderTable.name to SortOrder.ASC) } query.map(::rowToBundeslandDefinition) } override suspend fun save(bundesland: BundeslandDefinition): BundeslandDefinition = DatabaseFactory.dbQuery { - val exists = BundeslandTable.selectAll().where { BundeslandTable.id eq bundesland.bundeslandId }.any() + val exists = BundeslaenderTable.selectAll().where { BundeslaenderTable.id eq bundesland.bundeslandId }.any() if (exists) { - BundeslandTable.update({ BundeslandTable.id eq bundesland.bundeslandId }) { + BundeslaenderTable.update({ BundeslaenderTable.id eq bundesland.bundeslandId }) { it[landId] = bundesland.landId + it[bundeslandNr] = bundesland.bundeslandNr it[oepsCode] = bundesland.oepsCode it[iso3166_2_Code] = bundesland.iso3166_2_Code it[name] = bundesland.name @@ -106,9 +108,10 @@ class BundeslandRepositoryImpl : BundeslandRepository { } bundesland } else { - BundeslandTable.insert { + BundeslaenderTable.insert { it[id] = bundesland.bundeslandId it[landId] = bundesland.landId + it[bundeslandNr] = bundesland.bundeslandNr it[oepsCode] = bundesland.oepsCode it[iso3166_2_Code] = bundesland.iso3166_2_Code it[name] = bundesland.name @@ -124,31 +127,31 @@ class BundeslandRepositoryImpl : BundeslandRepository { } override suspend fun delete(id: Uuid): Boolean = DatabaseFactory.dbQuery { - BundeslandTable.deleteWhere { BundeslandTable.id eq id } > 0 + BundeslaenderTable.deleteWhere { BundeslaenderTable.id eq id } > 0 } override suspend fun existsByOepsCode(oepsCode: String, landId: Uuid): Boolean = DatabaseFactory.dbQuery { - BundeslandTable.selectAll().where { (BundeslandTable.oepsCode eq oepsCode) and (BundeslandTable.landId eq landId) } + BundeslaenderTable.selectAll().where { (BundeslaenderTable.oepsCode eq oepsCode) and (BundeslaenderTable.landId eq landId) } .any() } override suspend fun existsByIso3166_2_Code(iso3166_2_Code: String): Boolean = DatabaseFactory.dbQuery { - BundeslandTable.selectAll().where { BundeslandTable.iso3166_2_Code eq iso3166_2_Code }.any() + BundeslaenderTable.selectAll().where { BundeslaenderTable.iso3166_2_Code eq iso3166_2_Code }.any() } override suspend fun countActiveByCountry(landId: Uuid): Long = DatabaseFactory.dbQuery { - BundeslandTable.selectAll().where { (BundeslandTable.landId eq landId) and (BundeslandTable.istAktiv eq true) } + BundeslaenderTable.selectAll().where { (BundeslaenderTable.landId eq landId) and (BundeslaenderTable.istAktiv eq true) } .count() } override suspend fun upsertByLandIdAndKuerzel(bundesland: BundeslandDefinition): BundeslandDefinition = DatabaseFactory.dbQuery { // 1) Update anhand des natürlichen Schlüssels (landId + kuerzel) val updated = if (bundesland.kuerzel == null) { - // Ohne Kuerzel ist der natürliche Schlüssel nicht definiert → versuche Update via (landId + name) als Fallback nicht, bleib bei None 0 } else { - BundeslandTable.update({ (BundeslandTable.landId eq bundesland.landId) and (BundeslandTable.kuerzel eq bundesland.kuerzel) }) { + BundeslaenderTable.update({ (BundeslaenderTable.landId eq bundesland.landId) and (BundeslaenderTable.kuerzel eq bundesland.kuerzel) }) { it[landId] = bundesland.landId + it[bundeslandNr] = bundesland.bundeslandNr it[oepsCode] = bundesland.oepsCode it[iso3166_2_Code] = bundesland.iso3166_2_Code it[name] = bundesland.name @@ -161,16 +164,17 @@ class BundeslandRepositoryImpl : BundeslandRepository { } if (updated > 0) { - BundeslandTable.selectAll() - .where { (BundeslandTable.landId eq bundesland.landId) and (BundeslandTable.kuerzel eq bundesland.kuerzel) } + BundeslaenderTable.selectAll() + .where { (BundeslaenderTable.landId eq bundesland.landId) and (BundeslaenderTable.kuerzel eq bundesland.kuerzel) } .map(::rowToBundeslandDefinition) .single() } else { // 2) Insert versuchen try { - BundeslandTable.insert { + BundeslaenderTable.insert { it[id] = bundesland.bundeslandId it[landId] = bundesland.landId + it[bundeslandNr] = bundesland.bundeslandNr it[oepsCode] = bundesland.oepsCode it[iso3166_2_Code] = bundesland.iso3166_2_Code it[name] = bundesland.name @@ -184,8 +188,9 @@ class BundeslandRepositoryImpl : BundeslandRepository { } catch (_: Exception) { // Race-Condition → erneut Update if (bundesland.kuerzel != null) { - BundeslandTable.update({ (BundeslandTable.landId eq bundesland.landId) and (BundeslandTable.kuerzel eq bundesland.kuerzel) }) { + BundeslaenderTable.update({ (BundeslaenderTable.landId eq bundesland.landId) and (BundeslaenderTable.kuerzel eq bundesland.kuerzel) }) { it[landId] = bundesland.landId + it[bundeslandNr] = bundesland.bundeslandNr it[oepsCode] = bundesland.oepsCode it[iso3166_2_Code] = bundesland.iso3166_2_Code it[name] = bundesland.name @@ -197,14 +202,14 @@ class BundeslandRepositoryImpl : BundeslandRepository { } } } - // Rückgabe des aktuellen Datensatzes: Falls Kuerzel null greift auf ID zurück + if (bundesland.kuerzel != null) { - BundeslandTable.selectAll() - .where { (BundeslandTable.landId eq bundesland.landId) and (BundeslandTable.kuerzel eq bundesland.kuerzel) } + BundeslaenderTable.selectAll() + .where { (BundeslaenderTable.landId eq bundesland.landId) and (BundeslaenderTable.kuerzel eq bundesland.kuerzel) } .map(::rowToBundeslandDefinition) .single() } else { - BundeslandTable.selectAll().where { BundeslandTable.id eq bundesland.bundeslandId } + BundeslaenderTable.selectAll().where { BundeslaenderTable.id eq bundesland.bundeslandId } .map(::rowToBundeslandDefinition) .single() } 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 5dd5830c..c812649a 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 @@ -19,10 +19,15 @@ import kotlin.time.Instant as KxInstant class ExposedRegulationRepository : RegulationRepository { override suspend fun findAllTurnierklassen(): List = DatabaseFactory.dbQuery { - TurnierklasseTable.selectAll() + TurnierKlassenTable.selectAll() .map { it.toTurnierklasseDefinition() } } + override suspend fun findAllBewerbsklassen(): List = DatabaseFactory.dbQuery { + TurnierKlassenTable.selectAll() + .map { it.toBewerbsklasseDefinition() } + } + override suspend fun findAllLicenseMatrixEntries(): List = DatabaseFactory.dbQuery { LicenseTable.selectAll() .map { it.toLicenseMatrixEntry() } @@ -55,18 +60,32 @@ class ExposedRegulationRepository : RegulationRepository { } } + private fun ResultRow.toBewerbsklasseDefinition() = BewerbsklasseDefinition( + bewerbsklasseId = this[TurnierKlassenTable.id], + sparte = SparteE.valueOf(this[TurnierKlassenTable.sparte]), + code = this[TurnierKlassenTable.code], + bezeichnung = this[TurnierKlassenTable.bezeichnung], + maxHoehe = this[TurnierKlassenTable.maxHoehe], + aufgabenNiveau = this[TurnierKlassenTable.aufgabenNiveau], + validFrom = this[TurnierKlassenTable.validFrom].toKtInstant(), + validTo = this[TurnierKlassenTable.validTo]?.toOptionalKtInstant(), + istAktiv = this[TurnierKlassenTable.istAktiv], + createdAt = this[TurnierKlassenTable.createdAt].toKtInstant(), + updatedAt = this[TurnierKlassenTable.updatedAt].toKtInstant() + ) + private fun ResultRow.toTurnierklasseDefinition() = TurnierklasseDefinition( - turnierklasseId = this[TurnierklasseTable.id], - sparte = SparteE.valueOf(this[TurnierklasseTable.sparte]), - code = this[TurnierklasseTable.code], - bezeichnung = this[TurnierklasseTable.bezeichnung], - maxHoehe = this[TurnierklasseTable.maxHoehe], - aufgabenNiveau = this[TurnierklasseTable.aufgabenNiveau], - validFrom = this[TurnierklasseTable.validFrom].toKtInstant(), - validTo = this[TurnierklasseTable.validTo]?.toOptionalKtInstant(), - istAktiv = this[TurnierklasseTable.istAktiv], - createdAt = this[TurnierklasseTable.createdAt].toKtInstant(), - updatedAt = this[TurnierklasseTable.updatedAt].toKtInstant() + turnierklasseId = this[TurnierKlassenTable.id], + sparte = SparteE.valueOf(this[TurnierKlassenTable.sparte]), + code = this[TurnierKlassenTable.code], + bezeichnung = this[TurnierKlassenTable.bezeichnung], + maxHoehe = this[TurnierKlassenTable.maxHoehe], + aufgabenNiveau = this[TurnierKlassenTable.aufgabenNiveau], + validFrom = this[TurnierKlassenTable.validFrom].toKtInstant(), + validTo = this[TurnierKlassenTable.validTo]?.toOptionalKtInstant(), + istAktiv = this[TurnierKlassenTable.istAktiv], + createdAt = this[TurnierKlassenTable.createdAt].toKtInstant(), + updatedAt = this[TurnierKlassenTable.updatedAt].toKtInstant() ) private fun ResultRow.toLicenseMatrixEntry() = LicenseMatrixEntry( diff --git a/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/ReiterSparteTable.kt b/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/ReiterSparteTable.kt deleted file mode 100644 index 66d80bb3..00000000 --- a/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/ReiterSparteTable.kt +++ /dev/null @@ -1,27 +0,0 @@ -@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class) - -package at.mocode.masterdata.infrastructure.persistence - -import at.mocode.masterdata.infrastructure.persistence.reiter.ReiterTable -import org.jetbrains.exposed.v1.core.Table -import org.jetbrains.exposed.v1.datetime.CurrentTimestamp -import org.jetbrains.exposed.v1.datetime.timestamp - -/** - * Exposed-Tabellendefinition für die Spartenberechtigung eines Reiters. - * Verknüpft einen Reiter mit den Sparten (DRESSUR, SPRINGEN), für die er lizenziert ist. - */ -object ReiterSparteTable : Table("reiter_sparte") { - val id = uuid("id") - val reiterId = uuid("reiter_id").references(ReiterTable.id) - val sparte = varchar("sparte", 20) // DRESSUR, SPRINGEN - - val createdAt = timestamp("created_at").defaultExpression(CurrentTimestamp) - val updatedAt = timestamp("updated_at").defaultExpression(CurrentTimestamp) - - override val primaryKey = PrimaryKey(id) - - init { - uniqueIndex("ux_reiter_sparte", reiterId, sparte) - } -} diff --git a/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/TurnierKategorienTable.kt b/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/TurnierKategorienTable.kt new file mode 100644 index 00000000..63b7d4ec --- /dev/null +++ b/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/TurnierKategorienTable.kt @@ -0,0 +1,22 @@ +@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class) + +package at.mocode.masterdata.infrastructure.persistence + +import org.jetbrains.exposed.v1.core.Table +import org.jetbrains.exposed.v1.datetime.CurrentTimestamp +import org.jetbrains.exposed.v1.datetime.timestamp + +/** + * Exposed-Tabellendefinition für Turnier-Kategorien (z.B. CSN-B, CDN-A). + */ +object TurnierKategorienTable : Table("turnier_kategorien") { + val id = uuid("kategorie_id") + val code = varchar("code", 20).uniqueIndex() + val bezeichnung = varchar("bezeichnung", 100) + val sparte = varchar("sparte", 20).nullable() + val istAktiv = bool("ist_aktiv").default(true) + val createdAt = timestamp("created_at").defaultExpression(CurrentTimestamp) + val updatedAt = timestamp("updated_at").defaultExpression(CurrentTimestamp) + + override val primaryKey = PrimaryKey(id) +} diff --git a/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/TurnierklasseTable.kt b/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/TurnierKlassenTable.kt similarity index 82% rename from backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/TurnierklasseTable.kt rename to backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/TurnierKlassenTable.kt index a6117e35..16bd9a42 100644 --- a/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/TurnierklasseTable.kt +++ b/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/TurnierKlassenTable.kt @@ -7,11 +7,11 @@ import org.jetbrains.exposed.v1.datetime.CurrentTimestamp import org.jetbrains.exposed.v1.datetime.timestamp /** - * Exposed-Tabellendefinition für Turnierklassen (Springen/Dressur). + * Exposed-Tabellendefinition für Bewerbsklassen (Springen/Dressur). * Basierend auf ÖTO 2026. */ -object TurnierklasseTable : Table("turnierklasse") { - val id = uuid("turnierklasse_id") +object TurnierKlassenTable : Table("bewerbs_klassen") { + val id = uuid("bewerbsklasse_id") val sparte = varchar("sparte", 20) // DRESSUR, SPRINGEN val code = varchar("code", 10) // E, A, L, LM, M, S val bezeichnung = varchar("bezeichnung", 100) @@ -29,6 +29,6 @@ object TurnierklasseTable : Table("turnierklasse") { override val primaryKey = PrimaryKey(id) init { - index("idx_turnierklasse_sparte_code", false, sparte, code) + index("idx_bewerbs_klassen_sparte_code", false, sparte, code) } } diff --git a/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/TurnierSpartenTable.kt b/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/TurnierSpartenTable.kt new file mode 100644 index 00000000..b8d3f3d7 --- /dev/null +++ b/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/TurnierSpartenTable.kt @@ -0,0 +1,21 @@ +@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class) + +package at.mocode.masterdata.infrastructure.persistence + +import org.jetbrains.exposed.v1.core.Table +import org.jetbrains.exposed.v1.datetime.CurrentTimestamp +import org.jetbrains.exposed.v1.datetime.timestamp + +/** + * Exposed-Tabellendefinition für Turnier-Sparten (z.B. Dressur, Springen, Fahren). + */ +object TurnierSpartenTable : Table("turnier_sparten") { + val id = uuid("sparte_id") + val code = varchar("code", 10).uniqueIndex() // z.B. D, S, V, F, R, C + val bezeichnung = varchar("bezeichnung", 100) + val istAktiv = bool("ist_aktiv").default(true) + val createdAt = timestamp("created_at").defaultExpression(CurrentTimestamp) + val updatedAt = timestamp("updated_at").defaultExpression(CurrentTimestamp) + + override val primaryKey = PrimaryKey(id) +} diff --git a/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/funktionaer/FunktionaerExposedRepository.kt b/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/funktionaer/FunktionaerExposedRepository.kt index 066e46c2..bf0d427f 100644 --- a/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/funktionaer/FunktionaerExposedRepository.kt +++ b/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/funktionaer/FunktionaerExposedRepository.kt @@ -5,10 +5,7 @@ import at.mocode.core.domain.model.DatenQuelleE import at.mocode.core.utils.database.DatabaseFactory import at.mocode.masterdata.domain.model.Funktionaer import at.mocode.masterdata.domain.repository.FunktionaerRepository -import org.jetbrains.exposed.v1.core.ResultRow -import org.jetbrains.exposed.v1.core.and -import org.jetbrains.exposed.v1.core.eq -import org.jetbrains.exposed.v1.core.inList +import org.jetbrains.exposed.v1.core.* import org.jetbrains.exposed.v1.jdbc.deleteWhere import org.jetbrains.exposed.v1.jdbc.insert import org.jetbrains.exposed.v1.jdbc.selectAll @@ -20,7 +17,7 @@ import kotlin.uuid.Uuid * Exposed-basierte Implementierung des Funktionaer-Repositorys. * * Verwaltet die Persistenz von Funktionären und deren Qualifikationen. - * Die Qualifikationen werden beim Speichern gegen die [QualifikationMasterTable] + * Die Qualifikationen werden beim Speichern gegen die [FunktionaersQualifikationenTable] * aufgelöst, um Datenintegrität bezüglich offizieller ÖTO-Kürzel sicherzustellen. */ class FunktionaerExposedRepository : FunktionaerRepository { @@ -44,9 +41,9 @@ class FunktionaerExposedRepository : FunktionaerRepository { } override suspend fun findById(id: Uuid): Funktionaer? = DatabaseFactory.dbQuery { - val qualifikationen = (FunktionaerQualifikationTable innerJoin QualifikationMasterTable) + val qualifikationen = (FunktionaerQualifikationTable innerJoin FunktionaersQualifikationenTable) .selectAll().where { FunktionaerQualifikationTable.funktionaerId eq id } - .map { it[QualifikationMasterTable.code] } + .map { it[FunktionaersQualifikationenTable.code] } FunktionaerTable.selectAll().where { FunktionaerTable.id eq id } .map { rowToDomFunktionaer(it, qualifikationen) } @@ -58,9 +55,9 @@ class FunktionaerExposedRepository : FunktionaerRepository { .where { (FunktionaerTable.satzId eq satzID) and (FunktionaerTable.satzNummer eq satzNummer) } .singleOrNull() ?: return@dbQuery null - val qualifikationen = (FunktionaerQualifikationTable innerJoin QualifikationMasterTable) + val qualifikationen = (FunktionaerQualifikationTable innerJoin FunktionaersQualifikationenTable) .selectAll().where { FunktionaerQualifikationTable.funktionaerId eq row[FunktionaerTable.id] } - .map { it[QualifikationMasterTable.code] } + .map { it[FunktionaersQualifikationenTable.code] } rowToDomFunktionaer(row, qualifikationen) } @@ -71,9 +68,9 @@ class FunktionaerExposedRepository : FunktionaerRepository { .toList() val ids = funktionaere.map { it[FunktionaerTable.id] } - val qualisMap = (FunktionaerQualifikationTable innerJoin QualifikationMasterTable) + val qualisMap = (FunktionaerQualifikationTable innerJoin FunktionaersQualifikationenTable) .selectAll().where { FunktionaerQualifikationTable.funktionaerId inList ids } - .groupBy({ it[FunktionaerQualifikationTable.funktionaerId] }) { it[QualifikationMasterTable.code] } + .groupBy({ it[FunktionaerQualifikationTable.funktionaerId] }) { it[FunktionaersQualifikationenTable.code] } funktionaere.map { row -> rowToDomFunktionaer(row, qualisMap[row[FunktionaerTable.id]] ?: emptyList()) @@ -118,9 +115,9 @@ class FunktionaerExposedRepository : FunktionaerRepository { val typ = if (funktionaer.istRichter()) "RICHTER" else "PARCOURSBAUER" funktionaer.qualifikationen.forEach { code -> - val masterId = QualifikationMasterTable - .selectAll().where { (QualifikationMasterTable.code eq code) and (QualifikationMasterTable.typ eq typ) } - .map { it[QualifikationMasterTable.id] } + val masterId = FunktionaersQualifikationenTable + .selectAll().where { (FunktionaersQualifikationenTable.code eq code) and (FunktionaersQualifikationenTable.typ eq typ) } + .map { it[FunktionaersQualifikationenTable.id] } .singleOrNull() if (masterId != null) { diff --git a/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/funktionaer/FunktionaerTable.kt b/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/funktionaer/FunktionaerTable.kt index 978deeb1..9ec5bdfc 100644 --- a/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/funktionaer/FunktionaerTable.kt +++ b/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/funktionaer/FunktionaerTable.kt @@ -48,10 +48,10 @@ object FunktionaerTable : Table("funktionaer") { } /** - * Exposed-Tabellendefinition für die Qualifikation-Master-Daten. + * Exposed-Tabellendefinition für die Qualifikation-Master-Daten (Funktionäre). * Enthält offizielle ÖTO/FEI Kürzel (z.B. "D", "S", "P1"). */ -object QualifikationMasterTable : Table("qualifikation_master") { +object FunktionaersQualifikationenTable : Table("funktionaers_qualifikationen") { val id = uuid("qualifikation_id") /** Offizielles Kürzel (z.B. "SPF" für Springpferde) */ @@ -66,7 +66,7 @@ object QualifikationMasterTable : Table("qualifikation_master") { override val primaryKey = PrimaryKey(id) init { - index("idx_qualifikation_code_typ", isUnique = true, code, typ) + index("idx_funktionaers_qualifikationen_code_typ", isUnique = true, code, typ) } } @@ -75,7 +75,7 @@ object QualifikationMasterTable : Table("qualifikation_master") { */ object FunktionaerQualifikationTable : Table("funktionaer_qualifikation") { val funktionaerId = uuid("funktionaer_id").references(FunktionaerTable.id) - val qualifikationId = uuid("qualifikation_id").references(QualifikationMasterTable.id) + val qualifikationId = uuid("qualifikation_id").references(FunktionaersQualifikationenTable.id) override val primaryKey = PrimaryKey(funktionaerId, qualifikationId) } diff --git a/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/reiter/AltersklassenExposedRepository.kt b/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/reiter/AltersklassenExposedRepository.kt new file mode 100644 index 00000000..687cb94d --- /dev/null +++ b/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/reiter/AltersklassenExposedRepository.kt @@ -0,0 +1,37 @@ +@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class) + +package at.mocode.masterdata.infrastructure.persistence.reiter + +import at.mocode.core.domain.model.SparteE +import at.mocode.core.utils.database.DatabaseFactory +import at.mocode.masterdata.domain.model.AltersklasseDefinition +import at.mocode.masterdata.domain.repository.AltersklassenRepository +import at.mocode.masterdata.infrastructure.persistence.AltersklasseTable +import org.jetbrains.exposed.v1.core.eq +import org.jetbrains.exposed.v1.jdbc.selectAll + +/** + * Exposed-basierte Implementierung des AltersklassenRepository. + */ +class AltersklassenExposedRepository : AltersklassenRepository { + + override suspend fun findByCode(code: String): AltersklasseDefinition? = DatabaseFactory.dbQuery { + AltersklasseTable.selectAll().where { AltersklasseTable.altersklasseCode eq code } + .map { + AltersklasseDefinition( + altersklasseId = it[AltersklasseTable.id], + altersklasseCode = it[AltersklasseTable.altersklasseCode], + bezeichnung = it[AltersklasseTable.bezeichnung], + minAlter = it[AltersklasseTable.minAlter], + maxAlter = it[AltersklasseTable.maxAlter], + stichtagRegelText = it[AltersklasseTable.stichtagRegelText], + sparteFilter = it[AltersklasseTable.sparteFilter]?.let { s -> SparteE.valueOf(s) }, + geschlechtFilter = it[AltersklasseTable.geschlechtFilter], + oetoRegelReferenzId = it[AltersklasseTable.oetoRegelReferenzId], + istAktiv = it[AltersklasseTable.istAktiv], + createdAt = it[AltersklasseTable.createdAt], + updatedAt = it[AltersklasseTable.updatedAt] + ) + }.singleOrNull() + } +} diff --git a/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/reiter/BundeslandExposedRepository.kt b/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/reiter/BundeslandExposedRepository.kt index 9f6228c8..fab95a2c 100644 --- a/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/reiter/BundeslandExposedRepository.kt +++ b/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/reiter/BundeslandExposedRepository.kt @@ -5,7 +5,7 @@ package at.mocode.masterdata.infrastructure.persistence.reiter import at.mocode.core.utils.database.DatabaseFactory import at.mocode.masterdata.domain.model.BundeslandDefinition import at.mocode.masterdata.domain.repository.BundeslandRepository -import at.mocode.masterdata.infrastructure.persistence.BundeslandTable +import at.mocode.masterdata.infrastructure.persistence.BundeslaenderTable import org.jetbrains.exposed.v1.core.ResultRow import org.jetbrains.exposed.v1.core.eq import org.jetbrains.exposed.v1.jdbc.selectAll @@ -17,22 +17,22 @@ import kotlin.uuid.Uuid */ class BundeslandExposedRepository : BundeslandRepository { private fun rowToDom(row: ResultRow) = BundeslandDefinition( - bundeslandId = row[BundeslandTable.id], - landId = row[BundeslandTable.landId], - bundeslandNr = row[BundeslandTable.bundeslandNr], - oepsCode = row[BundeslandTable.oepsCode], - iso3166_2_Code = row[BundeslandTable.iso3166_2_Code], - name = row[BundeslandTable.name], - kuerzel = row[BundeslandTable.kuerzel], - wappenUrl = row[BundeslandTable.wappenUrl], - istAktiv = row[BundeslandTable.istAktiv], - sortierReihenfolge = row[BundeslandTable.sortierReihenfolge], - createdAt = row[BundeslandTable.createdAt], - updatedAt = row[BundeslandTable.updatedAt] + bundeslandId = row[BundeslaenderTable.id], + landId = row[BundeslaenderTable.landId], + bundeslandNr = row[BundeslaenderTable.bundeslandNr], + oepsCode = row[BundeslaenderTable.oepsCode], + iso3166_2_Code = row[BundeslaenderTable.iso3166_2_Code], + name = row[BundeslaenderTable.name], + kuerzel = row[BundeslaenderTable.kuerzel], + wappenUrl = row[BundeslaenderTable.wappenUrl], + istAktiv = row[BundeslaenderTable.istAktiv], + sortierReihenfolge = row[BundeslaenderTable.sortierReihenfolge], + createdAt = row[BundeslaenderTable.createdAt], + updatedAt = row[BundeslaenderTable.updatedAt] ) override suspend fun findByNr(nr: Int): BundeslandDefinition? = DatabaseFactory.dbQuery { - BundeslandTable.selectAll().where { BundeslandTable.bundeslandNr eq nr } + BundeslaenderTable.selectAll().where { BundeslaenderTable.bundeslandNr eq nr } .map(::rowToDom) .singleOrNull() } 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 new file mode 100644 index 00000000..79853a4e --- /dev/null +++ b/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/reiter/MasterdataLicenseExposedRepository.kt @@ -0,0 +1,54 @@ +@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class) + +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.Startkarte +import at.mocode.masterdata.domain.repository.MasterdataLicenseRepository +import org.jetbrains.exposed.v1.core.eq +import org.jetbrains.exposed.v1.jdbc.selectAll + +/** + * Exposed-basierte Implementierung des MasterdataLicenseRepository. + */ +class MasterdataLicenseExposedRepository : MasterdataLicenseRepository { + + override suspend fun findReitLizenzByCode(code: String): ReitLizenz? = DatabaseFactory.dbQuery { + ReitLizenzenTable.selectAll().where { ReitLizenzenTable.code eq code } + .map { + ReitLizenz( + lizenzId = it[ReitLizenzenTable.id], + code = it[ReitLizenzenTable.code], + bezeichnung = it[ReitLizenzenTable.bezeichnung], + sparte = it[ReitLizenzenTable.sparte], + istAktiv = it[ReitLizenzenTable.istAktiv] + ) + }.singleOrNull() + } + + override suspend fun findFahrLizenzByCode(code: String): FahrLizenz? = DatabaseFactory.dbQuery { + FahrLizenzenTable.selectAll().where { FahrLizenzenTable.code eq code } + .map { + FahrLizenz( + lizenzId = it[FahrLizenzenTable.id], + code = it[FahrLizenzenTable.code], + bezeichnung = it[FahrLizenzenTable.bezeichnung], + istAktiv = it[FahrLizenzenTable.istAktiv] + ) + }.singleOrNull() + } + + override suspend fun findStartkarteByCode(code: String): Startkarte? = DatabaseFactory.dbQuery { + StartkartenTable.selectAll().where { StartkartenTable.code eq code } + .map { + Startkarte( + startkarteId = it[StartkartenTable.id], + code = it[StartkartenTable.code], + bezeichnung = it[StartkartenTable.bezeichnung], + istAktiv = it[StartkartenTable.istAktiv] + ) + }.singleOrNull() + } +} diff --git a/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/reiter/ReiterExposedRepository.kt b/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/reiter/ReiterExposedRepository.kt index 878d9f90..ceb53e88 100644 --- a/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/reiter/ReiterExposedRepository.kt +++ b/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/reiter/ReiterExposedRepository.kt @@ -3,15 +3,20 @@ package at.mocode.masterdata.infrastructure.persistence.reiter import at.mocode.core.domain.model.DatenQuelleE +import at.mocode.core.domain.model.ReiterAltersKlasseE import at.mocode.core.domain.model.ReiterLizenzKlasseE import at.mocode.core.utils.database.DatabaseFactory import at.mocode.masterdata.domain.model.Reiter +import at.mocode.masterdata.domain.model.ReiterLizenz import at.mocode.masterdata.domain.repository.ReiterRepository import org.jetbrains.exposed.v1.core.ResultRow import org.jetbrains.exposed.v1.core.and import org.jetbrains.exposed.v1.core.eq import org.jetbrains.exposed.v1.core.lowerCase -import org.jetbrains.exposed.v1.jdbc.* +import org.jetbrains.exposed.v1.jdbc.deleteWhere +import org.jetbrains.exposed.v1.jdbc.insert +import org.jetbrains.exposed.v1.jdbc.selectAll +import org.jetbrains.exposed.v1.jdbc.update import kotlin.uuid.ExperimentalUuidApi import kotlin.uuid.Uuid @@ -33,11 +38,14 @@ class ReiterExposedRepository : ReiterRepository { vereinId = row[ReiterTable.vereinId], bundeslandId = row[ReiterTable.bundeslandId], nationId = row[ReiterTable.nationId], + reitLizenzId = row[ReiterTable.reitLizenzId], + fahrLizenzId = row[ReiterTable.fahrLizenzId], + startkarteId = row[ReiterTable.startkarteId], reiterLizenz = row[ReiterTable.reiterLizenz], startkarte = row[ReiterTable.startkarte], fahrLizenz = row[ReiterTable.fahrLizenz], - altersklasseJgJrU25 = row[ReiterTable.altersklasseJgJrU25], - altersklasseY = row[ReiterTable.altersklasseY], + altersklasseJgJrU25 = row[ReiterTable.altersklasseJgJrU25]?.let { ReiterAltersKlasseE.valueOf(it) }, + altersklasseY = row[ReiterTable.altersklasseY]?.let { ReiterAltersKlasseE.valueOf(it) }, mitgliedsNummer = row[ReiterTable.mitgliedsNummer], telefonNummer = row[ReiterTable.telefonNummer], kader = row[ReiterTable.kader], @@ -52,10 +60,23 @@ class ReiterExposedRepository : ReiterRepository { bemerkungen = row[ReiterTable.bemerkungen], datenQuelle = DatenQuelleE.valueOf(row[ReiterTable.datenQuelle]), createdAt = row[ReiterTable.createdAt], - updatedAt = row[ReiterTable.updatedAt] + updatedAt = row[ReiterTable.updatedAt], + lizenzen = loadLizenzen(row[ReiterTable.id]) ) } + private fun loadLizenzen(reiterId: Uuid): List { + return ReiterLizenzenZuordnungTable.selectAll().where { ReiterLizenzenZuordnungTable.reiterId eq reiterId } + .map { + at.mocode.masterdata.domain.model.ReiterLizenz( + lizenzId = it[ReiterLizenzenZuordnungTable.id], + lizenzTyp = it[ReiterLizenzenZuordnungTable.lizenzTyp], + kuerzel = it[ReiterLizenzenZuordnungTable.kuerzel], + gueltigBis = it[ReiterLizenzenZuordnungTable.gueltigBis] + ) + } + } + override suspend fun findById(id: Uuid): Reiter? = DatabaseFactory.dbQuery { ReiterTable.selectAll().where { ReiterTable.id eq id } .map { rowToDomReiter(it) } @@ -72,7 +93,7 @@ class ReiterExposedRepository : ReiterRepository { ReiterTable.selectAll() .where { (ReiterTable.vorname.lowerCase() eq vorname.lowercase()) and - (ReiterTable.nachname.lowerCase() eq nachname.lowercase()) + (ReiterTable.nachname.lowerCase() eq nachname.lowercase()) } .map { row -> rowToDomReiter(row) } } @@ -97,11 +118,14 @@ class ReiterExposedRepository : ReiterRepository { it[vereinId] = reiter.vereinId it[bundeslandId] = reiter.bundeslandId it[nationId] = reiter.nationId + it[reitLizenzId] = reiter.reitLizenzId + it[fahrLizenzId] = reiter.fahrLizenzId + it[startkarteId] = reiter.startkarteId it[reiterLizenz] = reiter.reiterLizenz it[startkarte] = reiter.startkarte it[fahrLizenz] = reiter.fahrLizenz - it[altersklasseJgJrU25] = reiter.altersklasseJgJrU25 - it[altersklasseY] = reiter.altersklasseY + it[altersklasseJgJrU25] = reiter.altersklasseJgJrU25?.name + it[altersklasseY] = reiter.altersklasseY?.name it[mitgliedsNummer] = reiter.mitgliedsNummer it[telefonNummer] = reiter.telefonNummer it[kader] = reiter.kader @@ -117,6 +141,7 @@ class ReiterExposedRepository : ReiterRepository { it[datenQuelle] = reiter.datenQuelle.name it[updatedAt] = reiter.updatedAt } + saveLizenzen(reiter) } else { ReiterTable.insert { it[id] = reiter.reiterId @@ -130,11 +155,14 @@ class ReiterExposedRepository : ReiterRepository { it[vereinId] = reiter.vereinId it[bundeslandId] = reiter.bundeslandId it[nationId] = reiter.nationId + it[reitLizenzId] = reiter.reitLizenzId + it[fahrLizenzId] = reiter.fahrLizenzId + it[startkarteId] = reiter.startkarteId it[reiterLizenz] = reiter.reiterLizenz it[startkarte] = reiter.startkarte it[fahrLizenz] = reiter.fahrLizenz - it[altersklasseJgJrU25] = reiter.altersklasseJgJrU25 - it[altersklasseY] = reiter.altersklasseY + it[altersklasseJgJrU25] = reiter.altersklasseJgJrU25?.name + it[altersklasseY] = reiter.altersklasseY?.name it[mitgliedsNummer] = reiter.mitgliedsNummer it[telefonNummer] = reiter.telefonNummer it[kader] = reiter.kader @@ -151,11 +179,25 @@ class ReiterExposedRepository : ReiterRepository { it[createdAt] = reiter.createdAt it[updatedAt] = reiter.updatedAt } + saveLizenzen(reiter) } reiter } + private fun saveLizenzen(reiter: Reiter) { + ReiterLizenzenZuordnungTable.deleteWhere { reiterId eq reiter.reiterId } + reiter.lizenzen.forEach { lizenz -> + ReiterLizenzenZuordnungTable.insert { + it[id] = lizenz.lizenzId + it[reiterId] = reiter.reiterId + it[lizenzTyp] = lizenz.lizenzTyp + it[kuerzel] = lizenz.kuerzel + it[gueltigBis] = lizenz.gueltigBis + } + } + } + override suspend fun delete(id: Uuid): Boolean = DatabaseFactory.dbQuery { ReiterTable.deleteWhere { ReiterTable.id eq id } > 0 } diff --git a/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/reiter/ReiterTable.kt b/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/reiter/ReiterTable.kt index 156746f2..12a768a3 100644 --- a/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/reiter/ReiterTable.kt +++ b/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/reiter/ReiterTable.kt @@ -2,6 +2,7 @@ package at.mocode.masterdata.infrastructure.persistence.reiter +import at.mocode.masterdata.infrastructure.persistence.BundeslaenderTable import org.jetbrains.exposed.v1.core.Table import org.jetbrains.exposed.v1.datetime.CurrentTimestamp import org.jetbrains.exposed.v1.datetime.date @@ -27,6 +28,9 @@ object ReiterTable : Table("reiter") { val vereinId = uuid("verein_id").nullable() val bundeslandId = uuid("bundesland_id").nullable() val nationId = uuid("nation_id").nullable() + val reitLizenzId = uuid("reit_lizenz_id").nullable() + val fahrLizenzId = uuid("fahr_lizenz_id").nullable() + val startkarteId = uuid("startkarte_id").nullable() val reiterLizenz = varchar("reiter_lizenz", 20).nullable() val startkarte = varchar("startkarte", 20).nullable() @@ -65,24 +69,10 @@ object ReiterTable : Table("reiter") { } /** - * Exposed-Tabellendefinition für die Bundesland-Mastertabelle. + * Exposed-Tabellendefinition für die Reiter-Lizenzen-Zuordnung (historisch/mehrfach). */ -object BundeslandTable : Table("bundesland") { - val id = uuid("bundesland_id") - val bundeslandNr = integer("bundesland_nr").uniqueIndex() - val bezeichnung = varchar("bezeichnung", 100) - val wappenUrl = varchar("wappen_url", 255).nullable() - val createdAt = timestamp("created_at").defaultExpression(CurrentTimestamp) - val updatedAt = timestamp("updated_at").defaultExpression(CurrentTimestamp) - - override val primaryKey = PrimaryKey(id) -} - -/** - * Exposed-Tabellendefinition für die Reiter-Lizenzen. - */ -object ReiterLizenzTable : Table("reiter_lizenz") { - val id = uuid("lizenz_id") +object ReiterLizenzenZuordnungTable : Table("reiter_lizenzen_zuordnung") { + val id = uuid("lizenz_zuordnung_id") val reiterId = uuid("reiter_id") val lizenzTyp = varchar("lizenz_typ", 50) // STARTKARTE, REITERLIZENZ, FAHRLIZENZ val kuerzel = varchar("kuerzel", 20) @@ -92,3 +82,46 @@ object ReiterLizenzTable : Table("reiter_lizenz") { override val primaryKey = PrimaryKey(id) } + +/** + * Exposed-Tabellendefinition für Reit-Lizenzen (Stammdaten). + */ +object ReitLizenzenTable : Table("reit_lizenzen") { + val id = uuid("lizenz_id") + val code = varchar("code", 20).uniqueIndex() + val bezeichnung = varchar("bezeichnung", 100) + val sparte = varchar("sparte", 20).nullable() + val istAktiv = bool("ist_aktiv").default(true) + val createdAt = timestamp("created_at").defaultExpression(CurrentTimestamp) + val updatedAt = timestamp("updated_at").defaultExpression(CurrentTimestamp) + + override val primaryKey = PrimaryKey(id) +} + +/** + * Exposed-Tabellendefinition für Fahr-Lizenzen (Stammdaten). + */ +object FahrLizenzenTable : Table("fahr_lizenzen") { + val id = uuid("lizenz_id") + val code = varchar("code", 20).uniqueIndex() + val bezeichnung = varchar("bezeichnung", 100) + val istAktiv = bool("ist_aktiv").default(true) + val createdAt = timestamp("created_at").defaultExpression(CurrentTimestamp) + val updatedAt = timestamp("updated_at").defaultExpression(CurrentTimestamp) + + override val primaryKey = PrimaryKey(id) +} + +/** + * Exposed-Tabellendefinition für Startkarten (Stammdaten). + */ +object StartkartenTable : Table("startkarten") { + val id = uuid("startkarte_id") + val code = varchar("code", 20).uniqueIndex() + val bezeichnung = varchar("bezeichnung", 100) + val istAktiv = bool("ist_aktiv").default(true) + val createdAt = timestamp("created_at").defaultExpression(CurrentTimestamp) + val updatedAt = timestamp("updated_at").defaultExpression(CurrentTimestamp) + + override val primaryKey = PrimaryKey(id) +} 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 04c14f4b..16e46fab 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 @@ -28,7 +28,7 @@ class RegulationSeedVerificationTest { Database.connect("jdbc:h2:mem:regulationseed;DB_CLOSE_DELAY=-1", driver = "org.h2.Driver") transaction { SchemaUtils.create( - TurnierklasseTable, + TurnierKlassenTable, LicenseTable, RichtverfahrenTable, GebuehrTable, diff --git a/backend/services/masterdata/masterdata-service/build.gradle.kts b/backend/services/masterdata/masterdata-service/build.gradle.kts index 9f672105..f0442bec 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.spring.cloud.starter.consul.discovery) implementation(libs.jackson.module.kotlin) //implementation(libs.springdoc.openapi.starter.webmvc.ui) diff --git a/backend/services/masterdata/masterdata-service/src/main/kotlin/at/mocode/masterdata/service/config/AltersklassenSeeder.kt b/backend/services/masterdata/masterdata-service/src/main/kotlin/at/mocode/masterdata/service/config/AltersklassenSeeder.kt new file mode 100644 index 00000000..d6f487ba --- /dev/null +++ b/backend/services/masterdata/masterdata-service/src/main/kotlin/at/mocode/masterdata/service/config/AltersklassenSeeder.kt @@ -0,0 +1,57 @@ +@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class) +package at.mocode.masterdata.service.config + +import at.mocode.masterdata.infrastructure.persistence.AltersklasseTable +import jakarta.annotation.PostConstruct +import org.jetbrains.exposed.v1.jdbc.transactions.transaction +import org.jetbrains.exposed.v1.jdbc.insert +import org.jetbrains.exposed.v1.jdbc.selectAll +import org.jetbrains.exposed.v1.core.* +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 Altersklassen gemäß LIZENZ01.DAT (JG, JR, 25, Y). + */ +@Configuration +@Profile("!test") +@DependsOn("masterdataDatabaseConfiguration") +class AltersklassenSeeder { + private val log = LoggerFactory.getLogger(AltersklassenSeeder::class.java) + + @PostConstruct + fun seed() { + log.info("Starte Seeding der Altersklassen...") + transaction { + val klassen = listOf( + Triple("JG", "JUGENDLICHER", "Altersklasse Jugend"), + Triple("JR", "JUNIOR", "Altersklasse Junior"), + Triple("25", "U25", "Altersklasse U25"), + Triple("Y", "JUNGER-REITER", "Altersklasse Junger Reiter") + ) + + klassen.forEach { (code, bez, desc) -> + upsertAltersklasse(code, bez) + } + } + } + + private fun upsertAltersklasse(code: String, bezeichnung: String) { + val exists = AltersklasseTable.selectAll() + .where { AltersklasseTable.altersklasseCode eq code } + .any() + + if (!exists) { + AltersklasseTable.insert { + it[id] = Uuid.random() + it[altersklasseCode] = code + it[AltersklasseTable.bezeichnung] = bezeichnung + it[istAktiv] = true + } + log.debug("Altersklasse '{}' angelegt.", code) + } + } +} diff --git a/backend/services/masterdata/masterdata-service/src/main/kotlin/at/mocode/masterdata/service/config/BewerbsKlassenSeeder.kt b/backend/services/masterdata/masterdata-service/src/main/kotlin/at/mocode/masterdata/service/config/BewerbsKlassenSeeder.kt new file mode 100644 index 00000000..90c8114d --- /dev/null +++ b/backend/services/masterdata/masterdata-service/src/main/kotlin/at/mocode/masterdata/service/config/BewerbsKlassenSeeder.kt @@ -0,0 +1,80 @@ +@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class) +package at.mocode.masterdata.service.config + +import at.mocode.masterdata.infrastructure.persistence.TurnierKlassenTable +import jakarta.annotation.PostConstruct +import org.jetbrains.exposed.v1.jdbc.transactions.transaction +import org.jetbrains.exposed.v1.jdbc.insert +import org.jetbrains.exposed.v1.jdbc.selectAll +import org.jetbrains.exposed.v1.core.* +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 Bewerbsklassen (ehemals Turnierklassen) gemäß ÖTO. + */ +@Configuration +@Profile("!test") +@DependsOn("masterdataDatabaseConfiguration") +class BewerbsKlassenSeeder { + private val log = LoggerFactory.getLogger(BewerbsKlassenSeeder::class.java) + + @PostConstruct + fun seed() { + log.info("Starte Seeding der Bewerbsklassen...") + transaction { + seedSpringenKlassen() + seedDressurKlassen() + } + log.info("Seeding der Bewerbsklassen abgeschlossen.") + } + + private fun seedSpringenKlassen() { + val klassen = listOf( + Triple("E", "Einsteiger", 80), + Triple("A", "Anfänger", 105), + Triple("L", "Leicht", 115), + Triple("LM", "Leicht-Mittel", 125), + Triple("M", "Mittelschwer", 135), + Triple("S", "Schwer", 150) + ) + klassen.forEach { (code, bez, hoehe) -> + upsertKlasse("SPRINGEN", code, bez, hoehe, null) + } + } + + private fun seedDressurKlassen() { + val klassen = listOf( + Triple("E", "Einsteiger", "Aufgaben E"), + Triple("A", "Anfänger", "Aufgaben A"), + Triple("L", "Leicht", "Aufgaben L"), + Triple("LM", "Leicht-Mittel", "Aufgaben LM"), + Triple("M", "Mittelschwer", "Aufgaben M"), + Triple("S", "Schwer", "Aufgaben S") + ) + klassen.forEach { (code, bez, niveau) -> + upsertKlasse("DRESSUR", code, bez, null, niveau) + } + } + + private fun upsertKlasse(sparte: String, code: String, bezeichnung: String, hoehe: Int?, niveau: String?) { + val exists = TurnierKlassenTable.selectAll() + .where { (TurnierKlassenTable.sparte eq sparte) and (TurnierKlassenTable.code eq code) } + .any() + + if (!exists) { + TurnierKlassenTable.insert { + it[id] = Uuid.random() + it[TurnierKlassenTable.sparte] = sparte + it[TurnierKlassenTable.code] = code + it[TurnierKlassenTable.bezeichnung] = bezeichnung + it[TurnierKlassenTable.maxHoehe] = hoehe + it[TurnierKlassenTable.aufgabenNiveau] = niveau + } + log.debug("Bewerbsklasse '{}' ({}) angelegt.", code, sparte) + } + } +} diff --git a/backend/services/masterdata/masterdata-service/src/main/kotlin/at/mocode/masterdata/service/config/FunktionaersQualifikationenSeeder.kt b/backend/services/masterdata/masterdata-service/src/main/kotlin/at/mocode/masterdata/service/config/FunktionaersQualifikationenSeeder.kt new file mode 100644 index 00000000..cb1b3f10 --- /dev/null +++ b/backend/services/masterdata/masterdata-service/src/main/kotlin/at/mocode/masterdata/service/config/FunktionaersQualifikationenSeeder.kt @@ -0,0 +1,79 @@ +@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class) +package at.mocode.masterdata.service.config + +import at.mocode.masterdata.infrastructure.persistence.funktionaer.FunktionaersQualifikationenTable +import jakarta.annotation.PostConstruct +import org.jetbrains.exposed.v1.jdbc.transactions.transaction +import org.jetbrains.exposed.v1.jdbc.insert +import org.jetbrains.exposed.v1.jdbc.selectAll +import org.jetbrains.exposed.v1.core.* +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 Funktionärs-Qualifikationen. + * Befüllt die FunktionaersQualifikationenTable mit Standard-Werten. + */ +@Configuration +@Profile("!test") +@DependsOn("masterdataDatabaseConfiguration") +class FunktionaersQualifikationenSeeder { + private val log = LoggerFactory.getLogger(FunktionaersQualifikationenSeeder::class.java) + + @PostConstruct + fun seed() { + log.info("Starte Seeding der Funktionärs-Qualifikationen...") + transaction { + seedRichterQualifikationen() + seedParcoursbauerQualifikationen() + } + log.info("Seeding der Funktionärs-Qualifikationen abgeschlossen.") + } + + private fun seedRichterQualifikationen() { + val qualis = listOf( + "D" to "Dressur", + "S" to "Springen", + "DPF" to "Dressurpferde", + "SPF" to "Springpferde", + "G" to "Gelände", + "STW" to "Steward", + "DM" to "Dressur Master", + "SM" to "Springen Master" + ) + qualis.forEach { (code, bezeichnung) -> + upsertQualifikation(code, bezeichnung, "RICHTER") + } + } + + private fun seedParcoursbauerQualifikationen() { + val qualis = listOf( + "P1" to "Einsteiger", + "P2" to "Fortgeschritten", + "P3" to "National", + "P4" to "Grand Prix" + ) + qualis.forEach { (code, bezeichnung) -> + upsertQualifikation(code, bezeichnung, "PARCOURSBAUER") + } + } + + private fun upsertQualifikation(code: String, bezeichnung: String, typ: String) { + val exists = FunktionaersQualifikationenTable.selectAll() + .where { (FunktionaersQualifikationenTable.code eq code) and (FunktionaersQualifikationenTable.typ eq typ) } + .any() + + if (!exists) { + FunktionaersQualifikationenTable.insert { + it[id] = Uuid.random() + it[FunktionaersQualifikationenTable.code] = code + it[FunktionaersQualifikationenTable.bezeichnung] = bezeichnung + it[FunktionaersQualifikationenTable.typ] = typ + } + log.debug("Qualifikation '{}' ({}) angelegt.", code, typ) + } + } +} 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 8fd77d21..b953df5a 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 @@ -1,14 +1,11 @@ package at.mocode.masterdata.service.config - import at.mocode.masterdata.infrastructure.persistence.* import at.mocode.masterdata.infrastructure.persistence.funktionaer.FunktionaerQualifikationTable import at.mocode.masterdata.infrastructure.persistence.funktionaer.FunktionaerTable -import at.mocode.masterdata.infrastructure.persistence.funktionaer.QualifikationMasterTable +import at.mocode.masterdata.infrastructure.persistence.funktionaer.FunktionaersQualifikationenTable import at.mocode.masterdata.infrastructure.persistence.pferd.HorseTable -import at.mocode.masterdata.infrastructure.persistence.reiter.ReiterTable -import at.mocode.masterdata.infrastructure.persistence.reiter.ReiterLizenzTable -import at.mocode.masterdata.infrastructure.persistence.reiter.BundeslandTable +import at.mocode.masterdata.infrastructure.persistence.reiter.* import at.mocode.masterdata.infrastructure.persistence.verein.VereinTable import jakarta.annotation.PostConstruct import jakarta.annotation.PreDestroy @@ -46,22 +43,26 @@ class MasterdataDatabaseConfiguration( transaction { SchemaUtils.create( LandTable, - BundeslandTable, + BundeslaenderTable, AltersklasseTable, PlatzTable, ReiterTable, HorseTable, VereinTable, FunktionaerTable, - QualifikationMasterTable, + FunktionaersQualifikationenTable, FunktionaerQualifikationTable, - ReiterLizenzTable, - TurnierklasseTable, + ReitLizenzenTable, + FahrLizenzenTable, + StartkartenTable, + ReiterLizenzenZuordnungTable, + TurnierKlassenTable, + TurnierSpartenTable, + TurnierKategorienTable, LicenseTable, RichtverfahrenTable, GebuehrTable, - RegulationConfigTable, - ReiterSparteTable + RegulationConfigTable ) log.info("Masterdata database schema initialized successfully") } @@ -95,22 +96,26 @@ class MasterdataTestDatabaseConfiguration { transaction { SchemaUtils.create( LandTable, - BundeslandTable, + BundeslaenderTable, AltersklasseTable, PlatzTable, ReiterTable, HorseTable, VereinTable, FunktionaerTable, - QualifikationMasterTable, + FunktionaersQualifikationenTable, FunktionaerQualifikationTable, - ReiterLizenzTable, - TurnierklasseTable, + ReitLizenzenTable, + FahrLizenzenTable, + StartkartenTable, + ReiterLizenzenZuordnungTable, + TurnierKlassenTable, + TurnierSpartenTable, + TurnierKategorienTable, LicenseTable, RichtverfahrenTable, GebuehrTable, - RegulationConfigTable, - ReiterSparteTable + RegulationConfigTable ) log.info("Test masterdata database schema initialized successfully") } diff --git a/backend/services/masterdata/masterdata-service/src/main/kotlin/at/mocode/masterdata/service/config/MasterdataSeeder.kt b/backend/services/masterdata/masterdata-service/src/main/kotlin/at/mocode/masterdata/service/config/MasterdataSeeder.kt new file mode 100644 index 00000000..28ebe865 --- /dev/null +++ b/backend/services/masterdata/masterdata-service/src/main/kotlin/at/mocode/masterdata/service/config/MasterdataSeeder.kt @@ -0,0 +1,164 @@ +@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class) +package at.mocode.masterdata.service.config + +import at.mocode.masterdata.infrastructure.persistence.BundeslaenderTable +import at.mocode.masterdata.infrastructure.persistence.LandTable +import at.mocode.masterdata.infrastructure.persistence.reiter.ReiterTable +import jakarta.annotation.PostConstruct +import org.jetbrains.exposed.v1.jdbc.transactions.transaction +import org.jetbrains.exposed.v1.core.* +import org.jetbrains.exposed.v1.jdbc.* +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 Nationen (Länder) und Bundesländer. + * Stellt sicher, dass die für den ZNS-Import notwendigen Masterdaten vorhanden sind. + */ +@Configuration +@Profile("!test") +@DependsOn("masterdataDatabaseConfiguration") +class MasterdataSeeder { + private val log = LoggerFactory.getLogger(MasterdataSeeder::class.java) + + @PostConstruct + fun seed() { + log.info("Starte Seeding der Master-Daten (Länder & Bundesländer)...") + transaction { + seedCountries() + seedAustrianBundeslaender() + fixReiterForeignKeys() + } + log.info("Seeding der Master-Daten abgeschlossen.") + } + + private fun fixReiterForeignKeys() { + // Falls Reiter bereits importiert wurden, bevor die Masterdaten da waren, + // versuchen wir hier die Verknüpfungen (FKs) zu heilen. + log.info("Prüfe und korrigiere Reiter-Fremdschlüssel (Heilung)...") + + // 1. Bundesland-Links heilen + val states = BundeslaenderTable.selectAll().toList() + states.forEach { row -> + val bId = row[BundeslaenderTable.id] + val bNr = row[BundeslaenderTable.bundeslandNr] + if (bNr != null) { + ReiterTable.update({ (ReiterTable.bundeslandNummer eq bNr) and (ReiterTable.bundeslandId.isNull()) }) { + it[bundeslandId] = bId + } + } + } + + // 2. Nation-Links heilen (ISO-Alpha3 Lookup) + val nations = LandTable.selectAll().toList() + nations.forEach { row -> + val nId = row[LandTable.id] + val iso3 = row[LandTable.isoAlpha3Code] + ReiterTable.update({ (ReiterTable.nation eq iso3) and (ReiterTable.nationId.isNull()) }) { + it[nationId] = nId + } + } + + // 3. Verein-Links heilen (Lookup über Name) + log.info("Heile Reiter-Vereins-Verknüpfungen via Name...") + val vt = at.mocode.masterdata.infrastructure.persistence.verein.VereinTable + val vereine = vt.selectAll().toList() + vereine.forEach { row -> + val vId = row[vt.id] + val vName = row[vt.vereinName] + ReiterTable.update({ (ReiterTable.vereinsName eq vName) and (ReiterTable.vereinId.isNull()) }) { + it[vereinId] = vId + } + } + } + + private fun seedCountries() { + val countries = listOf( + // iso2, iso3, num, nameDe, nameEn, eu, ewr, sort + listOf("AT", "AUT", "040", "Österreich", "Austria", true, true, 1), + listOf("DE", "DEU", "276", "Deutschland", "Germany", true, true, 2), + listOf("CH", "CHE", "756", "Schweiz", "Switzerland", false, false, 3), + listOf("IT", "ITA", "380", "Italien", "Italy", true, true, 4), + listOf("CZ", "CZE", "203", "Tschechien", "Czech Republic", true, true, 6), + listOf("SK", "SVK", "703", "Slowakei", "Slovakia", true, true, 7), + listOf("SI", "SVN", "705", "Slowenien", "Slovenia", true, true, 8), + listOf("HU", "HUN", "348", "Ungarn", "Hungary", true, true, 9), + listOf("LI", "LIE", "438", "Liechtenstein", "Liechtenstein", false, true, 11) + ) + + countries.forEach { c -> + val iso2 = c[0] as String + val exists = LandTable.selectAll().where { LandTable.isoAlpha2Code eq iso2 }.any() + if (!exists) { + LandTable.insert { + it[id] = Uuid.random() + it[isoAlpha2Code] = iso2 + it[isoAlpha3Code] = c[1] as String + it[isoNumerischerCode] = c[2] as String + it[nameDeutsch] = c[3] as String + it[nameEnglisch] = c[4] as String + it[istEuMitglied] = c[5] as Boolean + it[istEwrMitglied] = c[6] as Boolean + it[sortierReihenfolge] = c[7] as Int + } + log.debug("Land '{}' angelegt.", iso2) + } + } + } + + private fun seedAustrianBundeslaender() { + val austria = LandTable.selectAll().where { LandTable.isoAlpha2Code eq "AT" }.singleOrNull() + if (austria == null) { + log.error("Österreich (AT) nicht gefunden, überspringe Bundesland-Seeding.") + return + } + val austriaId = austria[LandTable.id] + + val states = listOf( + // nr, name, kuerzel, oeps + listOf(1, "Wien", "W", "09"), + listOf(2, "Niederösterreich", "NÖ", "03"), + listOf(3, "Burgenland", "BGLD", "01"), + listOf(4, "Steiermark", "STMK", "06"), + listOf(5, "Kärnten", "KTN", "02"), + listOf(6, "Oberösterreich", "OÖ", "04"), + listOf(7, "Salzburg", "SBG", "05"), + listOf(8, "Tirol", "T", "07"), + listOf(9, "Vorarlberg", "VBG", "08"), + listOf(0, "Unbekannt", "UNK", "00") + ) + + states.forEach { s -> + val nr = s[0] as Int + val name = s[1] as String + val kuerzel = s[2] as String + val oeps = s[3] as String + + val exists = BundeslaenderTable.selectAll().where { + (BundeslaenderTable.landId eq austriaId) and (BundeslaenderTable.bundeslandNr eq nr) + }.any() + + if (!exists) { + BundeslaenderTable.insert { + it[id] = Uuid.random() + it[landId] = austriaId + it[bundeslandNr] = nr + it[BundeslaenderTable.name] = name + it[BundeslaenderTable.kuerzel] = kuerzel + it[oepsCode] = oeps + } + log.debug("Bundesland '{}' (Nr. {}) angelegt.", name, nr) + } else { + // Update falls vorhanden (Harmonisierung) + BundeslaenderTable.update({ (BundeslaenderTable.landId eq austriaId) and (BundeslaenderTable.bundeslandNr eq nr) }) { + it[BundeslaenderTable.name] = name + it[BundeslaenderTable.kuerzel] = kuerzel + it[oepsCode] = oeps + } + } + } + } +} diff --git a/backend/services/masterdata/masterdata-service/src/main/kotlin/at/mocode/masterdata/service/config/QualifikationMasterSeeder.kt b/backend/services/masterdata/masterdata-service/src/main/kotlin/at/mocode/masterdata/service/config/QualifikationMasterSeeder.kt deleted file mode 100644 index 7c8edda8..00000000 --- a/backend/services/masterdata/masterdata-service/src/main/kotlin/at/mocode/masterdata/service/config/QualifikationMasterSeeder.kt +++ /dev/null @@ -1,86 +0,0 @@ -@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class) -package at.mocode.masterdata.service.config - -import at.mocode.masterdata.infrastructure.persistence.funktionaer.QualifikationMasterTable -import jakarta.annotation.PostConstruct -import org.jetbrains.exposed.v1.jdbc.transactions.transaction -import org.jetbrains.exposed.v1.core.* -import org.jetbrains.exposed.v1.jdbc.* -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 die offiziellen ÖTO/FEI Qualifikations-Kürzel. - * Befüllt die QualifikationMasterTable mit Standard-Werten. - */ -@Configuration -@Profile("!test") -@DependsOn("masterdataDatabaseConfiguration") -class QualifikationMasterSeeder { - private val log = LoggerFactory.getLogger(QualifikationMasterSeeder::class.java) - - @PostConstruct - fun seed() { - log.info("Starte Seeding der Qualifikations-Master-Daten (ÖTO/FEI)...") - transaction { - seedRichter() - seedParcoursbauer() - } - log.info("Seeding der Qualifikations-Master-Daten abgeschlossen.") - } - - private fun seedRichter() { - val richterQualis = listOf( - "D" to "Dressur", - "S" to "Springen", - "DPF" to "Dressurpferde", - "SPF" to "Springpferde", - "G" to "Gelände", - "STW" to "Steward", - "DM" to "Dressur Master", - "SM" to "Springen Master", - "GA" to "Grundausbildung", - "G3" to "Gruppe 3", - "G2" to "Gruppe 2", - "G1" to "Gruppe 1" - ) - - richterQualis.forEach { (code, bezeichnung) -> - upsertQuali(code, bezeichnung, "RICHTER") - } - } - - private fun seedParcoursbauer() { - val pbQualis = listOf( - "P1" to "Einsteiger", - "P2" to "Fortgeschritten", - "P3" to "National", - "P4" to "Grand Prix", - "SP" to "Springen", - "VS" to "Vielseitigkeit" - ) - - pbQualis.forEach { (code, bezeichnung) -> - upsertQuali(code, bezeichnung, "PARCOURSBAUER") - } - } - - private fun upsertQuali(code: String, bezeichnung: String, typ: String) { - val exists = QualifikationMasterTable.selectAll() - .where { (QualifikationMasterTable.code eq code) and (QualifikationMasterTable.typ eq typ) } - .any() - - if (!exists) { - QualifikationMasterTable.insert { - it[id] = Uuid.random() - it[QualifikationMasterTable.code] = code - it[QualifikationMasterTable.bezeichnung] = bezeichnung - it[QualifikationMasterTable.typ] = typ - } - log.debug("QualifikationMaster '{}' ({}) angelegt.", code, typ) - } - } -} diff --git a/backend/services/masterdata/masterdata-service/src/main/kotlin/at/mocode/masterdata/service/config/ReitLizenzenSeeder.kt b/backend/services/masterdata/masterdata-service/src/main/kotlin/at/mocode/masterdata/service/config/ReitLizenzenSeeder.kt new file mode 100644 index 00000000..8c639339 --- /dev/null +++ b/backend/services/masterdata/masterdata-service/src/main/kotlin/at/mocode/masterdata/service/config/ReitLizenzenSeeder.kt @@ -0,0 +1,88 @@ +@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class) +package at.mocode.masterdata.service.config + +import at.mocode.masterdata.infrastructure.persistence.reiter.FahrLizenzenTable +import at.mocode.masterdata.infrastructure.persistence.reiter.StartkartenTable +import at.mocode.masterdata.infrastructure.persistence.reiter.ReitLizenzenTable +import jakarta.annotation.PostConstruct +import org.jetbrains.exposed.v1.jdbc.transactions.transaction +import org.jetbrains.exposed.v1.jdbc.insert +import org.jetbrains.exposed.v1.jdbc.selectAll +import org.jetbrains.exposed.v1.core.* +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 Reit-Lizenzen, Fahr-Lizenzen und Startkarten. + */ +@Configuration +@Profile("!test") +@DependsOn("masterdataDatabaseConfiguration") +class ReitLizenzenSeeder { + private val log = LoggerFactory.getLogger(ReitLizenzenSeeder::class.java) + + @PostConstruct + fun seed() { + log.info("Starte Seeding der Lizenzen...") + transaction { + seedReitLizenzen() + seedFahrLizenzen() + seedStartkarten() + } + log.info("Seeding der Lizenzen abgeschlossen.") + } + + private fun seedReitLizenzen() { + val lizenzen = listOf( + Triple("R1", "Reitlizenz R1", "SPRINGEN"), + Triple("RD1", "Reitlizenz RD1", "DRESSUR"), + Triple("RD2", "Reitlizenz RD2", "DRESSUR"), + Triple("RS2", "Reitlizenz RS2", "SPRINGEN") + ) + lizenzen.forEach { (code, bez, sparte) -> + if (ReitLizenzenTable.selectAll().where { ReitLizenzenTable.code eq code }.none()) { + ReitLizenzenTable.insert { + it[id] = Uuid.random() + it[ReitLizenzenTable.code] = code + it[bezeichnung] = bez + it[ReitLizenzenTable.sparte] = sparte + } + } + } + } + + private fun seedFahrLizenzen() { + val lizenzen = listOf( + Pair("F1", "Fahrlizenz F1"), + Pair("F2", "Fahrlizenz F2") + ) + lizenzen.forEach { (code, bez) -> + if (FahrLizenzenTable.selectAll().where { FahrLizenzenTable.code eq code }.none()) { + FahrLizenzenTable.insert { + it[id] = Uuid.random() + it[FahrLizenzenTable.code] = code + it[bezeichnung] = bez + } + } + } + } + + private fun seedStartkarten() { + val karten = listOf( + Pair("S1", "Startkarte S1"), + Pair("S2", "Startkarte S2") + ) + karten.forEach { (code, bez) -> + if (StartkartenTable.selectAll().where { StartkartenTable.code eq code }.none()) { + StartkartenTable.insert { + it[id] = Uuid.random() + it[StartkartenTable.code] = code + it[bezeichnung] = bez + } + } + } + } +} diff --git a/backend/services/masterdata/masterdata-service/src/main/kotlin/at/mocode/masterdata/service/config/TurnierKategorienSeeder.kt b/backend/services/masterdata/masterdata-service/src/main/kotlin/at/mocode/masterdata/service/config/TurnierKategorienSeeder.kt new file mode 100644 index 00000000..9ce8c146 --- /dev/null +++ b/backend/services/masterdata/masterdata-service/src/main/kotlin/at/mocode/masterdata/service/config/TurnierKategorienSeeder.kt @@ -0,0 +1,51 @@ +@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class) +package at.mocode.masterdata.service.config + +import at.mocode.masterdata.infrastructure.persistence.TurnierKategorienTable +import jakarta.annotation.PostConstruct +import org.jetbrains.exposed.v1.jdbc.transactions.transaction +import org.jetbrains.exposed.v1.jdbc.insert +import org.jetbrains.exposed.v1.jdbc.selectAll +import org.jetbrains.exposed.v1.core.* +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 Turnier-Kategorien (z.B. CSN-C, CDN-A). + */ +@Configuration +@Profile("!test") +@DependsOn("masterdataDatabaseConfiguration") +class TurnierKategorienSeeder { + private val log = LoggerFactory.getLogger(TurnierKategorienSeeder::class.java) + + @PostConstruct + fun seed() { + log.info("Starte Seeding der Turnier-Kategorien...") + transaction { + val kategorien = listOf( + Triple("CSN-C", "Nationales Springturnier Kategorie C", "SPRINGEN"), + Triple("CSN-C Neu", "Nationales Springturnier Kategorie C Neu", "SPRINGEN"), + Triple("CSN-B", "Nationales Springturnier Kategorie B", "SPRINGEN"), + Triple("CSN-A", "Nationales Springturnier Kategorie A", "SPRINGEN"), + Triple("CDN-C", "Nationales Dressurturnier Kategorie C", "DRESSUR"), + Triple("CDN-B", "Nationales Dressurturnier Kategorie B", "DRESSUR"), + Triple("CDN-A", "Nationales Dressurturnier Kategorie A", "DRESSUR") + ) + + kategorien.forEach { (code, bez, sparte) -> + if (TurnierKategorienTable.selectAll().where { TurnierKategorienTable.code eq code }.none()) { + TurnierKategorienTable.insert { + it[id] = Uuid.random() + it[TurnierKategorienTable.code] = code + it[bezeichnung] = bez + it[TurnierKategorienTable.sparte] = sparte + } + } + } + } + } +} diff --git a/backend/services/masterdata/masterdata-service/src/main/kotlin/at/mocode/masterdata/service/config/TurnierKlassenSeeder.kt b/backend/services/masterdata/masterdata-service/src/main/kotlin/at/mocode/masterdata/service/config/TurnierKlassenSeeder.kt new file mode 100644 index 00000000..509b68c4 --- /dev/null +++ b/backend/services/masterdata/masterdata-service/src/main/kotlin/at/mocode/masterdata/service/config/TurnierKlassenSeeder.kt @@ -0,0 +1,80 @@ +@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class) +package at.mocode.masterdata.service.config + +import at.mocode.masterdata.infrastructure.persistence.TurnierKlassenTable +import jakarta.annotation.PostConstruct +import org.jetbrains.exposed.v1.jdbc.transactions.transaction +import org.jetbrains.exposed.v1.jdbc.insert +import org.jetbrains.exposed.v1.jdbc.selectAll +import org.jetbrains.exposed.v1.core.* +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 Turnierklassen (z.B. E, A, L, M, S) gemäß ÖTO. + */ +@Configuration +@Profile("!test") +@DependsOn("masterdataDatabaseConfiguration") +class TurnierKlassenSeeder { + private val log = LoggerFactory.getLogger(TurnierKlassenSeeder::class.java) + + @PostConstruct + fun seed() { + log.info("Starte Seeding der Turnierklassen...") + transaction { + seedSpringenKlassen() + seedDressurKlassen() + } + log.info("Seeding der Turnierklassen abgeschlossen.") + } + + private fun seedSpringenKlassen() { + val klassen = listOf( + Triple("E", "Einsteiger", 80), + Triple("A", "Anfänger", 105), + Triple("L", "Leicht", 115), + Triple("LM", "Leicht-Mittel", 125), + Triple("M", "Mittelschwer", 135), + Triple("S", "Schwer", 150) + ) + klassen.forEach { (code, bez, hoehe) -> + upsertKlasse("SPRINGEN", code, bez, hoehe, null) + } + } + + private fun seedDressurKlassen() { + val klassen = listOf( + Triple("E", "Einsteiger", "Aufgaben E"), + Triple("A", "Anfänger", "Aufgaben A"), + Triple("L", "Leicht", "Aufgaben L"), + Triple("LM", "Leicht-Mittel", "Aufgaben LM"), + Triple("M", "Mittelschwer", "Aufgaben M"), + Triple("S", "Schwer", "Aufgaben S") + ) + klassen.forEach { (code, bez, niveau) -> + upsertKlasse("DRESSUR", code, bez, null, niveau) + } + } + + private fun upsertKlasse(sparte: String, code: String, bezeichnung: String, hoehe: Int?, niveau: String?) { + val exists = TurnierKlassenTable.selectAll() + .where { (TurnierKlassenTable.sparte eq sparte) and (TurnierKlassenTable.code eq code) } + .any() + + if (!exists) { + TurnierKlassenTable.insert { + it[id] = Uuid.random() + it[TurnierKlassenTable.sparte] = sparte + it[TurnierKlassenTable.code] = code + it[TurnierKlassenTable.bezeichnung] = bezeichnung + it[TurnierKlassenTable.maxHoehe] = hoehe + it[TurnierKlassenTable.aufgabenNiveau] = niveau + } + log.debug("Turnierklasse '{}' ({}) angelegt.", code, sparte) + } + } +} diff --git a/backend/services/masterdata/masterdata-service/src/main/kotlin/at/mocode/masterdata/service/config/TurnierSpartenSeeder.kt b/backend/services/masterdata/masterdata-service/src/main/kotlin/at/mocode/masterdata/service/config/TurnierSpartenSeeder.kt new file mode 100644 index 00000000..0f1c9b29 --- /dev/null +++ b/backend/services/masterdata/masterdata-service/src/main/kotlin/at/mocode/masterdata/service/config/TurnierSpartenSeeder.kt @@ -0,0 +1,60 @@ +@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class) +package at.mocode.masterdata.service.config + +import at.mocode.masterdata.infrastructure.persistence.TurnierSpartenTable +import jakarta.annotation.PostConstruct +import org.jetbrains.exposed.v1.jdbc.transactions.transaction +import org.jetbrains.exposed.v1.jdbc.insert +import org.jetbrains.exposed.v1.jdbc.selectAll +import org.jetbrains.exposed.v1.core.* +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 Turnier-Sparten (z.B. Dressur, Springen, Vielseitigkeit) gemäß ÖTO. + */ +@Configuration +@Profile("!test") +@DependsOn("masterdataDatabaseConfiguration") +class TurnierSpartenSeeder { + private val log = LoggerFactory.getLogger(TurnierSpartenSeeder::class.java) + + @PostConstruct + fun seed() { + log.info("Starte Seeding der Turnier-Sparten...") + transaction { + val sparten = listOf( + "D" to "Dressur", + "S" to "Springen", + "V" to "Vielseitigkeit", + "F" to "Fahren", + "R" to "Reiten", + "C" to "Voltigieren", + "W" to "Western", + "O" to "Orientierungsreiten" + ) + sparten.forEach { (code, bezeichnung) -> + upsertSparte(code, bezeichnung) + } + } + log.info("Seeding der Turnier-Sparten abgeschlossen.") + } + + private fun upsertSparte(code: String, bezeichnung: String) { + val exists = TurnierSpartenTable.selectAll() + .where { TurnierSpartenTable.code eq code } + .any() + + if (!exists) { + TurnierSpartenTable.insert { + it[id] = Uuid.random() + it[TurnierSpartenTable.code] = code + it[TurnierSpartenTable.bezeichnung] = bezeichnung + } + log.debug("Turnier-Sparte '{}' angelegt.", bezeichnung) + } + } +} diff --git a/backend/services/masterdata/masterdata-service/src/main/resources/application.yml b/backend/services/masterdata/masterdata-service/src/main/resources/application.yml index 03e0d908..04154179 100644 --- a/backend/services/masterdata/masterdata-service/src/main/resources/application.yml +++ b/backend/services/masterdata/masterdata-service/src/main/resources/application.yml @@ -11,10 +11,24 @@ spring: flyway: enabled: true baseline-on-migrate: true + cloud: + consul: + host: ${SPRING_CLOUD_CONSUL_HOST:localhost} + port: ${SPRING_CLOUD_CONSUL_PORT:8500} + enabled: ${CONSUL_ENABLED:true} + discovery: + enabled: ${CONSUL_ENABLED:true} + register: ${CONSUL_ENABLED:true} + health-check-path: /actuator/health + health-check-interval: 10s + health-check-port: 8086 + instance-id: ${spring.application.name}-${server.port}-${random.uuid} + service-name: ${spring.application.name} + port: 8091 server: port: 8086 # Spring Boot Management Port (Actuator & Tomcat) - address: 127.0.0.1 # Sicherheit: Nur lokal erreichbar + address: 0.0.0.0 # Erreichbar für Consul Health Checks masterdata: http: 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 new file mode 100644 index 00000000..ecc74e89 --- /dev/null +++ b/backend/services/masterdata/masterdata-service/src/main/resources/db/migration/V013__Cleanup_and_Standardize_Masterdata.sql @@ -0,0 +1,45 @@ +-- 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; + +-- 2. qualifikation_master -> funktionaers_qualifikationen +ALTER TABLE qualifikation_master RENAME TO funktionaers_qualifikationen; +-- 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; +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; +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 + bezeichnung VARCHAR(100) NOT NULL, -- z.B. Dressur, Springen, Vielseitigkeit, Fahren, Reiten, Voltigieren + ist_aktiv BOOLEAN DEFAULT true, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + 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/masterdata/masterdata-service/src/main/resources/db/migration/V014__Expand_Masterdata_Rider_and_Competitions.sql b/backend/services/masterdata/masterdata-service/src/main/resources/db/migration/V014__Expand_Masterdata_Rider_and_Competitions.sql new file mode 100644 index 00000000..5cd5834c --- /dev/null +++ b/backend/services/masterdata/masterdata-service/src/main/resources/db/migration/V014__Expand_Masterdata_Rider_and_Competitions.sql @@ -0,0 +1,80 @@ +-- V014__Expand_Masterdata_Rider_and_Competitions.sql +-- Datum: 6. April 2026 + +-- 1. altersklasse -> altersklassen +ALTER TABLE altersklasse RENAME TO altersklassen; +ALTER INDEX IF EXISTS pk_altersklasse RENAME TO pk_altersklassen; +ALTER INDEX IF EXISTS idx_altersklasse_aktiv RENAME TO idx_altersklassen_aktiv; +ALTER INDEX IF EXISTS idx_altersklasse_sparte RENAME TO idx_altersklassen_sparte; +ALTER INDEX IF EXISTS idx_altersklasse_geschlecht RENAME TO idx_altersklassen_geschlecht; +ALTER INDEX IF EXISTS idx_altersklasse_alter RENAME TO idx_altersklassen_alter; + +-- 2. turnier_klassen -> bewerbs_klassen +ALTER TABLE turnier_klassen RENAME TO bewerbs_klassen; +ALTER TABLE bewerbs_klassen RENAME COLUMN turnierklasse_id TO bewerbsklasse_id; +ALTER INDEX IF EXISTS pk_turnier_klassen RENAME TO pk_bewerbs_klassen; +ALTER INDEX IF EXISTS idx_turnier_klassen_sparte_code RENAME TO idx_bewerbs_klassen_sparte_code; + +-- 3. turnier_kategorien erstellen +CREATE TABLE IF NOT EXISTS turnier_kategorien ( + kategorie_id UUID PRIMARY KEY, + code VARCHAR(20) UNIQUE NOT NULL, -- z.B. CSN-C, CDN-A, CSN-C Neu + bezeichnung VARCHAR(100) NOT NULL, + sparte VARCHAR(20), -- SPRINGEN, DRESSUR, etc. + ist_aktiv BOOLEAN DEFAULT true, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- 4. reit_lizenzen anpassen: Ein Reiter hat 0..1 Reit-Lizenz (Kürzel wie R1, RD1, etc.) +-- Die bestehende Tabelle reit_lizenzen (Join-Tabelle/Mehrfach) wird hier +-- beibehalten für fachspezifische Detail-Lizenzen, aber wir fügen +-- in der Reiter-Tabelle direkte Spalten für die Haupt-Lizenzen hinzu. +-- Fahr-Lizenzen und Startkarten ebenfalls als 0..1 Spalten beim Reiter. + +ALTER TABLE reiter ADD COLUMN IF NOT EXISTS fahr_lizenz_id UUID; +ALTER TABLE reiter ADD COLUMN IF NOT EXISTS startkarte_id UUID; + +-- 5. fahr_lizenzen Master-Tabelle +CREATE TABLE IF NOT EXISTS fahr_lizenzen ( + lizenz_id UUID PRIMARY KEY, + code VARCHAR(20) UNIQUE NOT NULL, -- z.B. F1, F2 + bezeichnung VARCHAR(100) NOT NULL, + ist_aktiv BOOLEAN DEFAULT true, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- 6. startkarten Master-Tabelle +CREATE TABLE IF NOT EXISTS startkarten ( + startkarte_id UUID PRIMARY KEY, + code VARCHAR(20) UNIQUE NOT NULL, -- z.B. S1, S2 + bezeichnung VARCHAR(100) NOT NULL, + ist_aktiv BOOLEAN DEFAULT true, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- 7. reit_lizenzen (Master-Tabelle für Kürzel-Definitionen) +-- Wir hatten reit_lizenzen als Join-Tabelle definiert in V013. +-- Wir behalten reit_lizenzen als Join-Tabelle (reiter_id, kuerzel) bei, +-- aber fügen eine Master-Tabelle für die Kürzel selbst hinzu. +-- Umbenennung der in V013 erstellten reit_lizenzen zu reiter_lizenzen_zuordnung +ALTER TABLE reit_lizenzen RENAME TO reiter_lizenzen_zuordnung; + +CREATE TABLE IF NOT EXISTS reit_lizenzen ( + lizenz_id UUID PRIMARY KEY, + code VARCHAR(20) UNIQUE NOT NULL, -- z.B. R1, RD1, RS2 + bezeichnung VARCHAR(100) NOT NULL, + sparte VARCHAR(20), -- SPRINGEN, DRESSUR, VIELSEITIGKEIT + ist_aktiv BOOLEAN DEFAULT true, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- 8. Fremdschlüssel für Reiter Tabelle aktualisieren +ALTER TABLE reiter ADD CONSTRAINT fk_reiter_fahr_lizenz FOREIGN KEY (fahr_lizenz_id) REFERENCES fahr_lizenzen(lizenz_id); +ALTER TABLE reiter ADD CONSTRAINT fk_reiter_startkarte FOREIGN KEY (startkarte_id) REFERENCES startkarten(startkarte_id); +-- reit_lizenz_id hinzufügen für 0..1 Beziehung +ALTER TABLE reiter ADD COLUMN IF NOT EXISTS reit_lizenz_id UUID; +ALTER TABLE reiter ADD CONSTRAINT fk_reiter_reit_lizenz FOREIGN KEY (reit_lizenz_id) REFERENCES reit_lizenzen(lizenz_id); 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 aa694fa8..c961f3a9 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 @@ -1,11 +1,6 @@ package at.mocode.zns.import.service import at.mocode.masterdata.domain.repository.* -import at.mocode.masterdata.infrastructure.persistence.* -import at.mocode.masterdata.infrastructure.persistence.funktionaer.FunktionaerExposedRepository -import at.mocode.masterdata.infrastructure.persistence.pferd.HorseExposedRepository -import at.mocode.masterdata.infrastructure.persistence.reiter.ReiterExposedRepository -import at.mocode.masterdata.infrastructure.persistence.verein.VereinExposedRepository import at.mocode.zns.importer.ZnsImportService import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication @@ -14,27 +9,6 @@ import org.springframework.context.annotation.Bean @SpringBootApplication class ZnsImportServiceApplication { - // Manuelle Bean-Definitionen für die Repositories, da diese in der Infrastruktur - // keine @Repository-Annotationen haben und wir MasterdataConfiguration nicht importieren wollen. - - @Bean - fun landRepository(): LandRepository = LandRepositoryImpl() - - @Bean - fun bundeslandRepository(): BundeslandRepository = BundeslandRepositoryImpl() - - @Bean - fun vereinRepository(): VereinRepository = VereinExposedRepository() - - @Bean - fun reiterRepository(): ReiterRepository = ReiterExposedRepository() - - @Bean - fun horseRepository(): HorseRepository = HorseExposedRepository() - - @Bean - fun funktionaerRepository(): FunktionaerRepository = FunktionaerExposedRepository() - @Bean fun znsImportService( vereinRepository: VereinRepository, @@ -42,7 +16,9 @@ class ZnsImportServiceApplication { horseRepository: HorseRepository, funktionaerRepository: FunktionaerRepository, landRepository: LandRepository, - bundeslandRepository: BundeslandRepository + bundeslandRepository: BundeslandRepository, + licenseRepository: MasterdataLicenseRepository, + altersklassenRepository: AltersklassenRepository ): ZnsImportService { return ZnsImportService( vereinRepository, @@ -50,7 +26,9 @@ class ZnsImportServiceApplication { horseRepository, funktionaerRepository, landRepository, - bundeslandRepository + bundeslandRepository, + licenseRepository, + altersklassenRepository ) } } 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 feb37d99..bb61eee3 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 @@ -1,6 +1,9 @@ package at.mocode.zns.import.service.config import at.mocode.masterdata.domain.repository.* +import at.mocode.masterdata.infrastructure.persistence.* +import at.mocode.masterdata.infrastructure.persistence.reiter.AltersklassenExposedRepository +import at.mocode.masterdata.infrastructure.persistence.reiter.MasterdataLicenseExposedRepository import at.mocode.masterdata.infrastructure.persistence.funktionaer.FunktionaerExposedRepository import at.mocode.masterdata.infrastructure.persistence.pferd.HorseExposedRepository import at.mocode.masterdata.infrastructure.persistence.reiter.ReiterExposedRepository @@ -11,6 +14,12 @@ import org.springframework.context.annotation.Configuration @Configuration class RepositoryConfiguration { + @Bean + fun landRepository(): LandRepository = LandRepositoryImpl() + + @Bean + fun bundeslandRepository(): BundeslandRepository = BundeslandRepositoryImpl() + @Bean fun vereinRepository(): VereinRepository = VereinExposedRepository() @@ -22,4 +31,10 @@ class RepositoryConfiguration { @Bean fun funktionaerRepository(): FunktionaerRepository = FunktionaerExposedRepository() + + @Bean + fun licenseRepository(): MasterdataLicenseRepository = MasterdataLicenseExposedRepository() + + @Bean + fun altersklassenRepository(): AltersklassenRepository = AltersklassenExposedRepository() } diff --git a/backend/services/zns-import/zns-import-service/src/main/kotlin/at/mocode/zns/import/service/config/ZnsImportDatabaseConfiguration.kt b/backend/services/zns-import/zns-import-service/src/main/kotlin/at/mocode/zns/import/service/config/ZnsImportDatabaseConfiguration.kt index dcfd5786..75e195a9 100644 --- a/backend/services/zns-import/zns-import-service/src/main/kotlin/at/mocode/zns/import/service/config/ZnsImportDatabaseConfiguration.kt +++ b/backend/services/zns-import/zns-import-service/src/main/kotlin/at/mocode/zns/import/service/config/ZnsImportDatabaseConfiguration.kt @@ -2,9 +2,9 @@ package at.mocode.zns.import.service.config import at.mocode.masterdata.infrastructure.persistence.funktionaer.FunktionaerQualifikationTable import at.mocode.masterdata.infrastructure.persistence.funktionaer.FunktionaerTable -import at.mocode.masterdata.infrastructure.persistence.funktionaer.QualifikationMasterTable +import at.mocode.masterdata.infrastructure.persistence.funktionaer.FunktionaersQualifikationenTable import at.mocode.masterdata.infrastructure.persistence.pferd.HorseTable -import at.mocode.masterdata.infrastructure.persistence.reiter.ReiterTable +import at.mocode.masterdata.infrastructure.persistence.reiter.* import at.mocode.masterdata.infrastructure.persistence.verein.VereinTable import jakarta.annotation.PostConstruct import org.jetbrains.exposed.v1.jdbc.Database @@ -34,8 +34,12 @@ class ZnsImportDatabaseConfiguration( ReiterTable, HorseTable, FunktionaerTable, - QualifikationMasterTable, - FunktionaerQualifikationTable + FunktionaersQualifikationenTable, + FunktionaerQualifikationTable, + ReitLizenzenTable, + FahrLizenzenTable, + StartkartenTable, + ReiterLizenzenZuordnungTable ) statements.forEach { exec(it) } log.info("Datenbank-Schema erfolgreich initialisiert ({} Statements)", statements.size) 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 089fafe3..d903ea30 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 @@ -26,20 +26,20 @@ object ZnsReiterParser { val bundeslandNummer = reader.getIntOrNull(82, 2) val vereinsName = reader.getString(84, 50) val nation = reader.getString(134, 3) - val reiterLizenz = reader.getString(137, 4) - // Ab Stelle 137 weicht die Realität der ZNS.zip von der Spec 2.4 ab - // Die Realität (Aichinger Ewald) zeigt: - // 134-136: AUT - // 137-140: R2 - // 147-158: 206607000676 (Mitgliedsnummer 8 Stellen ab 147?) - // 160-166: 4825910 (Telefonnummer?) - // 177-180: 2023 (LastPayYear) - // 181: M (Geschlecht) - // 182-189: 19571010 (Geburtsdatum) - val startkarte = reader.getString(141, 1) - val fahrLizenz = reader.getString(142, 2) + val reiterLizenzCode = reader.getString(137, 4).trim() + val startkarteCode = reader.getString(141, 1).trim() + val fahrLizenzCode = reader.getString(142, 2).trim() val altersklasseJgJrU25 = reader.getString(144, 2) val altersklasseY = reader.getString(146, 1) + + val altersklasseEnum = when (altersklasseJgJrU25.uppercase()) { + "JG" -> at.mocode.core.domain.model.ReiterAltersKlasseE.JG + "JR" -> at.mocode.core.domain.model.ReiterAltersKlasseE.JR + "25" -> at.mocode.core.domain.model.ReiterAltersKlasseE.U25 + else -> null + } + val altersklasseYEnum = if (altersklasseY.uppercase() == "Y") at.mocode.core.domain.model.ReiterAltersKlasseE.Y else null + val mitgliedsNummerStr = reader.getString(147, 8) val mitgliedsNummer = mitgliedsNummerStr.toIntOrNull() val telefonNummer = reader.getString(155, 22).trim() @@ -50,7 +50,18 @@ object ZnsReiterParser { val feiId = reader.getString(190, 8) val sperrListe = reader.getString(198, 1) val lizenzInfo = reader.getString(201, 10) - val lizenzKlasse = mapLizenz(reiterLizenz) + val lizenzKlasse = mapLizenz(reiterLizenzCode) + + val lizenzen = mutableListOf() + if (reiterLizenzCode.isNotBlank()) { + lizenzen.add(at.mocode.masterdata.domain.model.ReiterLizenz(lizenzTyp = "REITERLIZENZ", kuerzel = reiterLizenzCode)) + } + if (startkarteCode.isNotBlank()) { + lizenzen.add(at.mocode.masterdata.domain.model.ReiterLizenz(lizenzTyp = "STARTKARTE", kuerzel = startkarteCode)) + } + if (fahrLizenzCode.isNotBlank()) { + lizenzen.add(at.mocode.masterdata.domain.model.ReiterLizenz(lizenzTyp = "FAHRLIZENZ", kuerzel = fahrLizenzCode)) + } return Reiter( personId = Uuid.random(), @@ -60,11 +71,11 @@ object ZnsReiterParser { bundeslandNummer = bundeslandNummer, vereinsName = vereinsName.ifBlank { null }, nation = nation.ifBlank { null }, - reiterLizenz = reiterLizenz.ifBlank { null }, - startkarte = startkarte.ifBlank { null }, - fahrLizenz = fahrLizenz.ifBlank { null }, - altersklasseJgJrU25 = altersklasseJgJrU25.ifBlank { null }, - altersklasseY = altersklasseY.ifBlank { null }, + reiterLizenz = reiterLizenzCode.ifBlank { null }, + startkarte = startkarteCode.ifBlank { null }, + fahrLizenz = fahrLizenzCode.ifBlank { null }, + altersklasseJgJrU25 = altersklasseEnum, + altersklasseY = altersklasseYEnum, mitgliedsNummer = mitgliedsNummer, telefonNummer = telefonNummer.ifBlank { null }, kader = kader.ifBlank { null }, @@ -75,7 +86,8 @@ object ZnsReiterParser { sperrListe = sperrListe.ifBlank { null }, lizenzInfo = lizenzInfo.ifBlank { null }, lizenzKlasse = lizenzKlasse, - datenQuelle = DatenQuelleE.IMPORT_ZNS + datenQuelle = DatenQuelleE.IMPORT_ZNS, + lizenzen = lizenzen ) } diff --git a/docs/01_Architecture/MASTER_ROADMAP.md b/docs/01_Architecture/MASTER_ROADMAP.md index 90cbcb0e..6f0a935d 100644 --- a/docs/01_Architecture/MASTER_ROADMAP.md +++ b/docs/01_Architecture/MASTER_ROADMAP.md @@ -107,6 +107,8 @@ und über definierte Schnittstellen kommunizieren. #### 🧐 Agent: QA Specialist * [x] **Actor Context Stabilization:** Funktionär-Datenmodell (Richter/Parcoursbauer) auf professionelle Master-Daten-Referenzierung umgestellt und mit Reiter-Daten verknüpft (Import-Idempotenz via `satzNummer`). Redundante Kontakt-/Adressdaten entfernt. Alle ZNS-Import-Tests (9/9) stabilisiert und verwaiste Parser-Reste entfernt. +* [x] **Masterdata Standardization:** Bereinigung und Standardisierung der Masterdaten-Tabellen (Mehrzahl-Konvention: `bundeslaender`, `funktionaers_qualifikationen`, `reit_lizenzen`, `turnier_klassen`). +* [x] **ÖTO Data Seeding:** Umfassende Seeder für Funktionärs-Qualifikationen, Turnierklassen und Turnier-Sparten gemäß ÖTO implementiert. Einführung der Tabelle `turnier_sparten`. * [x] **ZNS-Import Optimization:** Automatische Verknüpfung von Funktionären mit Reitern (Reihenfolge: VEREIN -> LIZENZ -> PFERDE -> RICHT). `ZnsImportService` für sequentielle Imports in Tests gehärtet. * [x] **Service Stability:** Port-Konflikt des `masterdata-service` (Spring Management Port 8081 vs. Gateway) durch Umzug auf Port 8086 und explizite Bind-Adressen (Spring: 127.0.0.1, Ktor: 0.0.0.0) dauerhaft gelöst. * [x] **Documentation:** `CHANGELOG.md` aktualisiert und Port-Konfiguration in `application.yml` dokumentiert. @@ -125,8 +127,10 @@ und über definierte Schnittstellen kommunizieren. #### 👷 Agent: Backend Developer -* [x] **ZNS-Importer:** Support für Richter-Import (RICHT01.DAT) und Reiter-Refactoring (LIZENZ01.DAT) vervollständigt. -* [x] **Masterdata:** Qualifikations-System und Personen-Referenzen (Vereine, Bundesländer, Nationen) auf professionelle Master-Daten umgestellt. +* [x] **ZNS-Importer:** Support für Richter-Import (RICHT01.DAT) und Reiter-Refactoring (LIZENZ01.DAT) vervollständigt. ✓ +* [x] **Masterdata:** Qualifikations-System und Personen-Referenzen (Vereine, Bundesländer, Nationen) stabilisiert (Consul & MasterdataSeeder). ✓ +* [x] **Infrastruktur:** Service-Discovery (Consul) für alle Microservices (incl. Masterdata) aktiviert. +* [x] **Infrastruktur:** Bean-Definitionen und Dependency-Injection im `zns-import-service` bereinigt. * [x] **Database:** Initialisierung der Funktionärs-Tabellen stabilisiert (PSQLException Fix). * [x] **`actor-context`:** Domain-Modelle für `Pferd`, `Funktionaer`, `Verein` implementiert. * [x] **`registration-context`:** `DomBewerb`, `DomAbteilung`, `DomStartliste` implementiert. diff --git a/docs/03_Domain/00_Glossary.md b/docs/03_Domain/00_Glossary.md index 68f645b4..3ddda14e 100644 --- a/docs/03_Domain/00_Glossary.md +++ b/docs/03_Domain/00_Glossary.md @@ -12,6 +12,8 @@ Dieses Dokument definiert die **Ubiquitous Language** (allgegenwärtige Sprache) * **Abteilung:** Eine Unterteilung eines -> *Bewerbs*. Oft werden Bewerbe mit vielen Startern in mehrere Abteilungen geteilt (z.B. nach Lizenzklasse oder Rasse), die getrennt gewertet werden. * **Akteur:** Oberbegriff für alle Personen (Reiter, Richter, Besitzer) und Organisationen (Vereine), die im System interagieren. +* **Altersklasse (singular):** Domänenobjekt zur Klassifikation von Teilnehmern nach Alter. Schlüssel-Felder: `altersklasseId` (UUID), `altersklasseCode` (fachlicher Schlüssel, z.B. `JG`, `JR`, `25`, `Y`), `bezeichnung`, optionale Grenzen `minAlter`/`maxAlter`, optionale Filter `sparteFilter`/`geschlechtFilter`. Persistiert in Tabelle `altersklasse` (singular), Spalte `altersklasse_code` (einzigartig). +* **Altersklassen (plural):** Offizielle Altersklassen gemäß LIZENZ01.DAT. Unterstützte Kürzel: `JG` (Jugendlicher), `JR` (Junior), `25` (U25), `Y` (Junger Reiter). Ein Reiter hat 0..1 Altersklasse aus JG/JR/U25 und optional 0..1 aus Y. * **Ausschreibung:** Das offizielle Dokument, das alle Bedingungen eines -> *Turniers* festlegt. * **Bewerb:** Die einzelne sportliche Prüfung (z.B. "Springprüfung Kl. L"). Kleinste Einheit für Nennungen und Ergebnisse. * **Event:** Der organisatorische Rahmen (z.B. "Pferdefest 2026"), der ein oder mehrere -> *Turniere* beinhalten kann. @@ -27,18 +29,22 @@ 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:** Die Qualifikationsstufe eines Reiters (z.B. "R1", "RD3"). Bestimmt, in welchen Klassen er startberechtigt ist. +* **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. +* **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`. * **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. * **Sperrliste:** Eine vom Verband geführte Liste von Personen oder Pferden, die aktuell nicht startberechtigt sind (meist wegen offener Zahlungen). -* **Startkarte:** Der Nachweis, dass die Jahresgebühr für die Lizenz bezahlt wurde. Ohne aktive Startkarte ist (national) kein Start möglich. +* **Startkarte:** Der Nachweis, dass die Jahresgebühr für die Lizenz bezahlt wurde. Ohne aktive Startkarte ist (national) kein Start möglich. Stammdaten-Tabelle `startkarten`, Referenz am Reiter `startkarte_id`. * **Turnier:** Die administrative Einheit (z.B. "CSN-A"), die einem spezifischen Regelwerk (ÖTO oder FEI) unterliegt. +* **Turnier-Kategorie:** Klassifikation eines Turniers (z.B. `CSN-C`, `CSN-C Neu`, `CSN-B`, `CSN-A`, `CDN-C`, `CDN-B`, `CDN-A`). Stammdaten-Tabelle `turnier_kategorien`. +* **Bewerbsklasse:** Früher „Turnierklasse“. Klassifiziert den Schwierigkeitsgrad eines Bewerbs (Springen: `E`, `A`, `L`, `LM`, `M`, `S`; Dressur: `E` bis `S`). Stammdaten-Tabelle `bewerbs_klassen`. API-Endpunkt: `GET /rules/turnierklassen` liefert dieselben Inhalte (Alias), technisch über `RegulationRepository.findAllTurnierklassen()` abgebildet. ## U - Z diff --git a/docs/03_Domain/01_Glossary/Ubiquitous_Language.md b/docs/03_Domain/01_Glossary/Ubiquitous_Language.md index 091f0b87..9728f8d0 100644 --- a/docs/03_Domain/01_Glossary/Ubiquitous_Language.md +++ b/docs/03_Domain/01_Glossary/Ubiquitous_Language.md @@ -85,7 +85,7 @@ Die ÖTO definiert sparten- und klassenabhängige Schwellenwerte, ab wievielen S | Begriff | Definition | ÖTO-Referenz | |-------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------| | **Kategorie** | Das Niveau eines Turniers und/oder seiner Bewerbe bzw. die Teilnahmeberechtigung daran. Nationale Kategorien: `C-NEU`, `C`, `B*`, `B`, `A`, `A*`. | ÖTO § 3 Abs. 4 | -| **Klasse / Höhe** | Schwierigkeitsgrad eines Bewerbs. Springen: E0 (60–90 cm), A (105–110 cm), L, M, S. Dressur: E, A, L, M, S (nach Aufgabe). | ÖTO B-Teil | +| **Bewerbsklasse** | Früher „Turnierklasse“. Schwierigkeitsgrad eines Bewerbs. Springen: E0 (60–90 cm), A (105–110 cm), L, M, S. Dressur: E, A, L, M, S (nach Aufgabe). Stammdaten in `bewerbs_klassen`. | ÖTO B-Teil | | **Kombination** | Zwei oder mehr Turniere (ggf. unterschiedlicher Sparten) die am selben Ort/Datum stattfinden. Jedes Turnier behält seine eigene Turniernummer. Genehmigung durch LFV/OEPS erforderlich. | ÖTO § 4 | | **Kopfnummer** | *National (OEPS):* 4-stellige Registrierungsnummer eines Pferdes beim OEPS. **Nicht als eindeutige ID geeignet** – kann sich ändern. Dient zur schnellen Suche/Eingabe in der Meldestelle (Autocomplete), aber nicht als Datenbankschlüssel. *Turnier:* Temporäre Startnummer für das spezifische Turnier (ebenfalls nicht persistent). | – | | **Konto** | Kontobasierte Abrechnung pro Zahler (nicht nur pro Reiter). Basis für das „Hansi-Szenario" (Guthaben bei Transfer). | Billing Context | @@ -95,7 +95,9 @@ 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** | Qualifikationsstufe eines Reiters (z.B. `R1`, `RD3`). Bestimmt Startberechtigung in bestimmten Klassen. | ÖTO Teilnahmeberechtigung | +| **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 | +| **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 | ### M