diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f8c1a0b..42ae5d71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,8 @@ Versionierung folgt [Semantic Versioning](https://semver.org/lang/de/). ### 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. - **Infrastructure:** Port-Konflikt im `masterdata-service` durch Trennung der Bind-Adressen (Spring: 127.0.0.1, Ktor: 0.0.0.0) und Bereinigung verwaister Prozesse stabilisiert. - **Core:** Veraltete `ZnsLegacyParsersTest.kt` entfernt; alle Tests sind nun in `ZnsParserTest.kt` konsolidiert. 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 41555b7b..df9b2831 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,20 +5,17 @@ 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.jdbc.deleteWhere -import org.jetbrains.exposed.v1.jdbc.insert -import org.jetbrains.exposed.v1.jdbc.selectAll -import org.jetbrains.exposed.v1.jdbc.update +import at.mocode.masterdata.infrastructure.persistence.funktionaer.* +import org.jetbrains.exposed.v1.core.* +import org.jetbrains.exposed.v1.jdbc.* +import org.slf4j.LoggerFactory import kotlin.uuid.Uuid /** * Exposed-basierte Implementierung des Funktionaer-Repositorys. */ class FunktionaerExposedRepository : FunktionaerRepository { + private val log = LoggerFactory.getLogger(FunktionaerExposedRepository::class.java) private fun rowToDomFunktionaer(row: ResultRow, qualifikationen: List = emptyList()): Funktionaer { return Funktionaer( @@ -37,9 +34,9 @@ class FunktionaerExposedRepository : FunktionaerRepository { } override suspend fun findById(id: Uuid): Funktionaer? = DatabaseFactory.dbQuery { - val qualifikationen = FunktionaerQualifikationTable + val qualifikationen = (FunktionaerQualifikationTable innerJoin QualifikationMasterTable) .selectAll().where { FunktionaerQualifikationTable.funktionaerId eq id } - .map { it[FunktionaerQualifikationTable.qualifikation] } + .map { it[QualifikationMasterTable.code] } FunktionaerTable.selectAll().where { FunktionaerTable.id eq id } .map { rowToDomFunktionaer(it, qualifikationen) } @@ -51,9 +48,9 @@ class FunktionaerExposedRepository : FunktionaerRepository { .where { (FunktionaerTable.satzId eq satzID) and (FunktionaerTable.satzNummer eq satzNummer) } .singleOrNull() ?: return@dbQuery null - val qualifikationen = FunktionaerQualifikationTable + val qualifikationen = (FunktionaerQualifikationTable innerJoin QualifikationMasterTable) .selectAll().where { FunktionaerQualifikationTable.funktionaerId eq row[FunktionaerTable.id] } - .map { it[FunktionaerQualifikationTable.qualifikation] } + .map { it[QualifikationMasterTable.code] } rowToDomFunktionaer(row, qualifikationen) } @@ -64,9 +61,9 @@ class FunktionaerExposedRepository : FunktionaerRepository { .toList() val ids = funktionaere.map { it[FunktionaerTable.id] } - val qualisMap = FunktionaerQualifikationTable + val qualisMap = (FunktionaerQualifikationTable innerJoin QualifikationMasterTable) .selectAll().where { FunktionaerQualifikationTable.funktionaerId inList ids } - .groupBy({ it[FunktionaerQualifikationTable.funktionaerId] }) { it[FunktionaerQualifikationTable.qualifikation] } + .groupBy({ it[FunktionaerQualifikationTable.funktionaerId] }) { it[QualifikationMasterTable.code] } funktionaere.map { row -> rowToDomFunktionaer(row, qualisMap[row[FunktionaerTable.id]] ?: emptyList()) @@ -101,12 +98,25 @@ class FunktionaerExposedRepository : FunktionaerRepository { } } - // Qualifikationen synchronisieren + // Qualifikationen synchronisieren (über Master-Daten Auflösung) FunktionaerQualifikationTable.deleteWhere { funktionaerId eq funktionaer.funktionaerId } - funktionaer.qualifikationen.forEach { quali -> - FunktionaerQualifikationTable.insert { - it[funktionaerId] = funktionaer.funktionaerId - it[qualifikation] = quali + + 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] } + .singleOrNull() + + if (masterId != null) { + FunktionaerQualifikationTable.insert { + it[funktionaerId] = funktionaer.funktionaerId + it[qualifikationId] = masterId + } + } else { + log.warn("Qualifikation '{}' für Typ '{}' nicht in Master-Daten gefunden. Überspringe Zuordnung für Funktionär {}.", + code, typ, funktionaer.name) } } 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 54d0fc6f..deb40185 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 @@ -53,11 +53,27 @@ object FunktionaerTable : Table("funktionaer") { } /** - * Exposed-Tabellendefinition für die Qualifikationen eines Funktionärs. + * Exposed-Tabellendefinition für die Qualifikation-Master-Daten. + */ +object QualifikationMasterTable : Table("qualifikation_master") { + val id = uuid("qualifikation_id") + val code = varchar("code", 10) // z.B. "D", "S", "SPF", "P1" + val bezeichnung = varchar("bezeichnung", 100) // z.B. "Dressur", "Springpferde" + val typ = varchar("typ", 20) // "RICHTER" oder "PARCOURSBAUER" + + override val primaryKey = PrimaryKey(id) + + init { + index("idx_qualifikation_code_typ", isUnique = true, code, typ) + } +} + +/** + * Exposed-Tabellendefinition für die Zuordnung von Qualifikationen zu Funktionären (Join-Tabelle). */ object FunktionaerQualifikationTable : Table("funktionaer_qualifikation") { val funktionaerId = uuid("funktionaer_id").references(FunktionaerTable.id) - val qualifikation = varchar("qualifikation", 20) + val qualifikationId = uuid("qualifikation_id").references(QualifikationMasterTable.id) - override val primaryKey = PrimaryKey(funktionaerId, qualifikation) + override val primaryKey = PrimaryKey(funktionaerId, qualifikationId) } 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 473f0749..21f971d9 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 @@ -2,7 +2,9 @@ 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.pferd.HorseTable import at.mocode.masterdata.infrastructure.persistence.reiter.ReiterTable import at.mocode.masterdata.infrastructure.persistence.verein.VereinTable @@ -49,6 +51,8 @@ class MasterdataDatabaseConfiguration( HorseTable, VereinTable, FunktionaerTable, + QualifikationMasterTable, + FunktionaerQualifikationTable, TurnierklasseTable, LicenseTable, RichtverfahrenTable, @@ -95,6 +99,8 @@ class MasterdataTestDatabaseConfiguration { HorseTable, VereinTable, FunktionaerTable, + QualifikationMasterTable, + FunktionaerQualifikationTable, TurnierklasseTable, LicenseTable, RichtverfahrenTable, 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 new file mode 100644 index 00000000..7c8edda8 --- /dev/null +++ b/backend/services/masterdata/masterdata-service/src/main/kotlin/at/mocode/masterdata/service/config/QualifikationMasterSeeder.kt @@ -0,0 +1,86 @@ +@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/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 c377602c..dcfd5786 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 @@ -1,6 +1,8 @@ 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.pferd.HorseTable import at.mocode.masterdata.infrastructure.persistence.reiter.ReiterTable import at.mocode.masterdata.infrastructure.persistence.verein.VereinTable @@ -28,7 +30,12 @@ class ZnsImportDatabaseConfiguration( Database.connect(jdbcUrl, user = username, password = password) transaction { val statements = MigrationUtils.statementsRequiredForDatabaseMigration( - VereinTable, ReiterTable, HorseTable, FunktionaerTable + VereinTable, + ReiterTable, + HorseTable, + FunktionaerTable, + QualifikationMasterTable, + FunktionaerQualifikationTable ) statements.forEach { exec(it) } log.info("Datenbank-Schema erfolgreich initialisiert ({} Statements)", statements.size) diff --git a/docs/01_Architecture/MASTER_ROADMAP.md b/docs/01_Architecture/MASTER_ROADMAP.md index cd402a71..d7ce893a 100644 --- a/docs/01_Architecture/MASTER_ROADMAP.md +++ b/docs/01_Architecture/MASTER_ROADMAP.md @@ -123,6 +123,9 @@ und über definierte Schnittstellen kommunizieren. #### 👷 Agent: Backend Developer +* [x] **ZNS-Importer:** Support für Richter-Import (RICHT01.DAT) vervollständigt. +* [x] **Masterdata:** Qualifikations-System auf professionelle Master-Daten-Referenzierung (`QualifikationMasterTable`) umgestellt. +* [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. * [x] **`event-management-context`:** `DomVeranstaltung`, `DomTurnier`, `DomAusschreibung` implementiert.