Integrate qualification master data system (QualifikationMasterTable) for functionaries, refactor mapping logic in repositories, enhance database initialization for ZNS and Masterdata services, and add a seeder for ÖTO/FEI qualification data. Fix PSQLException during ZNS imports.

This commit is contained in:
Stefan Mogeritsch 2026-04-06 13:53:06 +02:00
parent 933ef9cd6c
commit c35869f8ee
7 changed files with 153 additions and 23 deletions

View File

@ -36,6 +36,8 @@ Versionierung folgt [Semantic Versioning](https://semver.org/lang/de/).
### Behoben ### 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:** 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. - **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. - **Core:** Veraltete `ZnsLegacyParsersTest.kt` entfernt; alle Tests sind nun in `ZnsParserTest.kt` konsolidiert.

View File

@ -5,20 +5,17 @@ import at.mocode.core.domain.model.DatenQuelleE
import at.mocode.core.utils.database.DatabaseFactory import at.mocode.core.utils.database.DatabaseFactory
import at.mocode.masterdata.domain.model.Funktionaer import at.mocode.masterdata.domain.model.Funktionaer
import at.mocode.masterdata.domain.repository.FunktionaerRepository import at.mocode.masterdata.domain.repository.FunktionaerRepository
import org.jetbrains.exposed.v1.core.ResultRow import at.mocode.masterdata.infrastructure.persistence.funktionaer.*
import org.jetbrains.exposed.v1.core.and import org.jetbrains.exposed.v1.core.*
import org.jetbrains.exposed.v1.core.eq import org.jetbrains.exposed.v1.jdbc.*
import org.jetbrains.exposed.v1.core.inList import org.slf4j.LoggerFactory
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.Uuid import kotlin.uuid.Uuid
/** /**
* Exposed-basierte Implementierung des Funktionaer-Repositorys. * Exposed-basierte Implementierung des Funktionaer-Repositorys.
*/ */
class FunktionaerExposedRepository : FunktionaerRepository { class FunktionaerExposedRepository : FunktionaerRepository {
private val log = LoggerFactory.getLogger(FunktionaerExposedRepository::class.java)
private fun rowToDomFunktionaer(row: ResultRow, qualifikationen: List<String> = emptyList()): Funktionaer { private fun rowToDomFunktionaer(row: ResultRow, qualifikationen: List<String> = emptyList()): Funktionaer {
return Funktionaer( return Funktionaer(
@ -37,9 +34,9 @@ class FunktionaerExposedRepository : FunktionaerRepository {
} }
override suspend fun findById(id: Uuid): Funktionaer? = DatabaseFactory.dbQuery { override suspend fun findById(id: Uuid): Funktionaer? = DatabaseFactory.dbQuery {
val qualifikationen = FunktionaerQualifikationTable val qualifikationen = (FunktionaerQualifikationTable innerJoin QualifikationMasterTable)
.selectAll().where { FunktionaerQualifikationTable.funktionaerId eq id } .selectAll().where { FunktionaerQualifikationTable.funktionaerId eq id }
.map { it[FunktionaerQualifikationTable.qualifikation] } .map { it[QualifikationMasterTable.code] }
FunktionaerTable.selectAll().where { FunktionaerTable.id eq id } FunktionaerTable.selectAll().where { FunktionaerTable.id eq id }
.map { rowToDomFunktionaer(it, qualifikationen) } .map { rowToDomFunktionaer(it, qualifikationen) }
@ -51,9 +48,9 @@ class FunktionaerExposedRepository : FunktionaerRepository {
.where { (FunktionaerTable.satzId eq satzID) and (FunktionaerTable.satzNummer eq satzNummer) } .where { (FunktionaerTable.satzId eq satzID) and (FunktionaerTable.satzNummer eq satzNummer) }
.singleOrNull() ?: return@dbQuery null .singleOrNull() ?: return@dbQuery null
val qualifikationen = FunktionaerQualifikationTable val qualifikationen = (FunktionaerQualifikationTable innerJoin QualifikationMasterTable)
.selectAll().where { FunktionaerQualifikationTable.funktionaerId eq row[FunktionaerTable.id] } .selectAll().where { FunktionaerQualifikationTable.funktionaerId eq row[FunktionaerTable.id] }
.map { it[FunktionaerQualifikationTable.qualifikation] } .map { it[QualifikationMasterTable.code] }
rowToDomFunktionaer(row, qualifikationen) rowToDomFunktionaer(row, qualifikationen)
} }
@ -64,9 +61,9 @@ class FunktionaerExposedRepository : FunktionaerRepository {
.toList() .toList()
val ids = funktionaere.map { it[FunktionaerTable.id] } val ids = funktionaere.map { it[FunktionaerTable.id] }
val qualisMap = FunktionaerQualifikationTable val qualisMap = (FunktionaerQualifikationTable innerJoin QualifikationMasterTable)
.selectAll().where { FunktionaerQualifikationTable.funktionaerId inList ids } .selectAll().where { FunktionaerQualifikationTable.funktionaerId inList ids }
.groupBy({ it[FunktionaerQualifikationTable.funktionaerId] }) { it[FunktionaerQualifikationTable.qualifikation] } .groupBy({ it[FunktionaerQualifikationTable.funktionaerId] }) { it[QualifikationMasterTable.code] }
funktionaere.map { row -> funktionaere.map { row ->
rowToDomFunktionaer(row, qualisMap[row[FunktionaerTable.id]] ?: emptyList()) 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 } FunktionaerQualifikationTable.deleteWhere { funktionaerId eq funktionaer.funktionaerId }
funktionaer.qualifikationen.forEach { quali ->
FunktionaerQualifikationTable.insert { val typ = if (funktionaer.istRichter()) "RICHTER" else "PARCOURSBAUER"
it[funktionaerId] = funktionaer.funktionaerId
it[qualifikation] = quali 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)
} }
} }

View File

@ -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") { object FunktionaerQualifikationTable : Table("funktionaer_qualifikation") {
val funktionaerId = uuid("funktionaer_id").references(FunktionaerTable.id) 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)
} }

View File

@ -2,7 +2,9 @@ package at.mocode.masterdata.service.config
import at.mocode.masterdata.infrastructure.persistence.* 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.FunktionaerTable
import at.mocode.masterdata.infrastructure.persistence.funktionaer.QualifikationMasterTable
import at.mocode.masterdata.infrastructure.persistence.pferd.HorseTable import at.mocode.masterdata.infrastructure.persistence.pferd.HorseTable
import at.mocode.masterdata.infrastructure.persistence.reiter.ReiterTable import at.mocode.masterdata.infrastructure.persistence.reiter.ReiterTable
import at.mocode.masterdata.infrastructure.persistence.verein.VereinTable import at.mocode.masterdata.infrastructure.persistence.verein.VereinTable
@ -49,6 +51,8 @@ class MasterdataDatabaseConfiguration(
HorseTable, HorseTable,
VereinTable, VereinTable,
FunktionaerTable, FunktionaerTable,
QualifikationMasterTable,
FunktionaerQualifikationTable,
TurnierklasseTable, TurnierklasseTable,
LicenseTable, LicenseTable,
RichtverfahrenTable, RichtverfahrenTable,
@ -95,6 +99,8 @@ class MasterdataTestDatabaseConfiguration {
HorseTable, HorseTable,
VereinTable, VereinTable,
FunktionaerTable, FunktionaerTable,
QualifikationMasterTable,
FunktionaerQualifikationTable,
TurnierklasseTable, TurnierklasseTable,
LicenseTable, LicenseTable,
RichtverfahrenTable, RichtverfahrenTable,

View File

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

View File

@ -1,6 +1,8 @@
package at.mocode.zns.import.service.config 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.FunktionaerTable
import at.mocode.masterdata.infrastructure.persistence.funktionaer.QualifikationMasterTable
import at.mocode.masterdata.infrastructure.persistence.pferd.HorseTable import at.mocode.masterdata.infrastructure.persistence.pferd.HorseTable
import at.mocode.masterdata.infrastructure.persistence.reiter.ReiterTable import at.mocode.masterdata.infrastructure.persistence.reiter.ReiterTable
import at.mocode.masterdata.infrastructure.persistence.verein.VereinTable import at.mocode.masterdata.infrastructure.persistence.verein.VereinTable
@ -28,7 +30,12 @@ class ZnsImportDatabaseConfiguration(
Database.connect(jdbcUrl, user = username, password = password) Database.connect(jdbcUrl, user = username, password = password)
transaction { transaction {
val statements = MigrationUtils.statementsRequiredForDatabaseMigration( val statements = MigrationUtils.statementsRequiredForDatabaseMigration(
VereinTable, ReiterTable, HorseTable, FunktionaerTable VereinTable,
ReiterTable,
HorseTable,
FunktionaerTable,
QualifikationMasterTable,
FunktionaerQualifikationTable
) )
statements.forEach { exec(it) } statements.forEach { exec(it) }
log.info("Datenbank-Schema erfolgreich initialisiert ({} Statements)", statements.size) log.info("Datenbank-Schema erfolgreich initialisiert ({} Statements)", statements.size)

View File

@ -123,6 +123,9 @@ und über definierte Schnittstellen kommunizieren.
#### 👷 Agent: Backend Developer #### 👷 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] **`actor-context`:** Domain-Modelle für `Pferd`, `Funktionaer`, `Verein` implementiert.
* [x] **`registration-context`:** `DomBewerb`, `DomAbteilung`, `DomStartliste` implementiert. * [x] **`registration-context`:** `DomBewerb`, `DomAbteilung`, `DomStartliste` implementiert.
* [x] **`event-management-context`:** `DomVeranstaltung`, `DomTurnier`, `DomAusschreibung` implementiert. * [x] **`event-management-context`:** `DomVeranstaltung`, `DomTurnier`, `DomAusschreibung` implementiert.