diff --git a/CHANGELOG.md b/CHANGELOG.md index 69bf6529..d0e18ac4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -56,6 +56,14 @@ Versionierung folgt [Semantic Versioning](https://semver.org/lang/de/). --- +## [1.0.2-SNAPSHOT] — 2026-04-06 + +### Geändert +- **Data Modeling:** Redundante Kontakt- und Adressdaten aus `FunktionaerTable` entfernt; stattdessen Verknüpfung zu `ReiterTable` via `reiter_id` hinzugefügt. +- **Import:** ZNS-Importer verknüpft nun Funktionäre automatisch mit vorhandenen Reitern anhand des Namens (Nachname, Vorname). +- **Infrastructure:** `findByName` in `ReiterRepository` implementiert für effiziente Suche während des Imports. +- **Datenbank:** Migration `V011` hinzugefügt, um das Schema zu bereinigen und die Fremdschlüsselbeziehung zu etablieren. + ## [1.0.1-SNAPSHOT] — 2026-04-05 ### Geändert 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 67e4a6af..89205932 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 @@ -259,7 +259,18 @@ class ZnsImportService( var aktualisiert = 0 zeilen.forEachIndexed { index, zeile -> runCatching { - val funktionaer = ZnsFunktionaerParser.parse(zeile) ?: return@forEachIndexed + val funktionaerRaw = ZnsFunktionaerParser.parse(zeile) ?: return@forEachIndexed + + // Versuch, den Reiter anhand des Namens (Nachname, Vorname) zu finden + val nameParts = funktionaerRaw.name?.split(",")?.map { it.trim() } + val reiterId = if (nameParts != null && nameParts.size >= 2) { + val nachname = nameParts[0] + val vorname = nameParts[1] + reiterRepository.findByName(vorname, nachname).firstOrNull()?.reiterId + } else null + + val funktionaer = funktionaerRaw.copy(reiterId = reiterId) + val satzID = funktionaer.satzId val satzNummer = funktionaer.satzNummer val vorhanden = funktionaerRepository.findBySatz(satzID, satzNummer) @@ -269,6 +280,7 @@ class ZnsImportService( } else { funktionaerRepository.save( vorhanden.copy( + reiterId = funktionaer.reiterId, name = funktionaer.name, qualifikationen = funktionaer.qualifikationen, istAktiv = funktionaer.istAktiv, diff --git a/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/model/Funktionaer.kt b/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/model/Funktionaer.kt index 9c1f8ca2..8f3df055 100644 --- a/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/model/Funktionaer.kt +++ b/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/model/Funktionaer.kt @@ -38,6 +38,10 @@ data class Funktionaer( @Serializable(with = UuidSerializer::class) val personId: Uuid? = null, + // Reference to Reiter + @Serializable(with = UuidSerializer::class) + val reiterId: Uuid? = null, + // === ZNS.zip RICHT01.DAT === ANFANG === // Alphanumerisch (1) WERT "X" = RICHTER, "Y" = PARCOURSBAUER @@ -54,19 +58,6 @@ data class Funktionaer( // === ZNS.zip RICHT01.DAT === ENDE === - // Kontakt - var imageUrl: String? = null, - var email: String? = null, - var telefon: String? = null, - var website: String? = null, - - // Adresse - var strasse: String? = null, - var hausnummer: String? = null, - var ort: String? = null, - var plz: String? = null, - var bundesland: String? = null, - // Status & Verwaltung var istAktiv: Boolean = true, var bemerkungen: String? = null, diff --git a/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/repository/ReiterRepository.kt b/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/repository/ReiterRepository.kt index 81f506f4..95dbb33a 100644 --- a/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/repository/ReiterRepository.kt +++ b/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/repository/ReiterRepository.kt @@ -23,6 +23,11 @@ interface ReiterRepository { */ suspend fun findBySatznummer(satznummer: String?): Reiter? + /** + * Sucht Reiter nach Vorname und Nachname (Case-Insensitive). + */ + suspend fun findByName(vorname: String, nachname: String): List + /** * Gibt alle Reiter zurück (paginiert). */ 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 de2a0277..066e46c2 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 @@ -30,6 +30,7 @@ class FunktionaerExposedRepository : FunktionaerRepository { return Funktionaer( funktionaerId = row[FunktionaerTable.id], personId = row[FunktionaerTable.personId], + reiterId = row[FunktionaerTable.reiterId], satzId = row[FunktionaerTable.satzId], satzNummer = row[FunktionaerTable.satzNummer] ?: 0, name = row[FunktionaerTable.name], @@ -84,6 +85,7 @@ class FunktionaerExposedRepository : FunktionaerRepository { if (exists) { FunktionaerTable.update({ FunktionaerTable.id eq funktionaer.funktionaerId }) { it[personId] = funktionaer.personId + it[reiterId] = funktionaer.reiterId it[satzId] = funktionaer.satzId it[satzNummer] = funktionaer.satzNummer it[name] = funktionaer.name @@ -96,6 +98,7 @@ class FunktionaerExposedRepository : FunktionaerRepository { FunktionaerTable.insert { it[id] = funktionaer.funktionaerId it[personId] = funktionaer.personId + it[reiterId] = funktionaer.reiterId it[satzId] = funktionaer.satzId it[satzNummer] = funktionaer.satzNummer it[name] = 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 d4d49c0c..978deeb1 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 @@ -2,6 +2,7 @@ package at.mocode.masterdata.infrastructure.persistence.funktionaer +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 @@ -15,6 +16,7 @@ import kotlin.uuid.ExperimentalUuidApi object FunktionaerTable : Table("funktionaer") { val id = uuid("funktionaer_id") val personId = uuid("person_id").nullable() + val reiterId = uuid("reiter_id").references(ReiterTable.id).nullable() // === ZNS.zip RICHT01.DAT (Zentrales Nennungssystem) === ANFANG === @@ -29,19 +31,6 @@ object FunktionaerTable : Table("funktionaer") { // === ZNS.zip RICHT01.DAT === ENDE === - // Kontakt - val imageUrl = varchar("image_url", 255).nullable() - val email = varchar("email", 200).nullable() - val telefon = varchar("telefon", 50).nullable() - val website = varchar("website", 255).nullable() - - // Adresse - val strasse = varchar("strasse", 200).nullable() - val hausnummer = varchar("hausnummer", 10).nullable() - val plz = varchar("plz", 10).nullable() - val ort = varchar("ort", 100).nullable() - val bundesland = varchar("bundesland", 100).nullable() - // Status & Verwaltung val istAktiv = bool("ist_aktiv").default(true) val bemerkungen = text("bemerkungen").nullable() 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 3e0e5251..22015603 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 @@ -8,7 +8,9 @@ import at.mocode.core.utils.database.DatabaseFactory import at.mocode.masterdata.domain.model.Reiter 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 kotlin.uuid.ExperimentalUuidApi import kotlin.uuid.Uuid @@ -63,6 +65,15 @@ class ReiterExposedRepository : ReiterRepository { .singleOrNull() } + override suspend fun findByName(vorname: String, nachname: String): List = DatabaseFactory.dbQuery { + ReiterTable.selectAll() + .where { + (ReiterTable.vorname.lowerCase() eq vorname.lowercase()) and + (ReiterTable.nachname.lowerCase() eq nachname.lowercase()) + } + .map { row -> rowToDomReiter(row) } + } + override suspend fun findAll(limit: Int, offset: Int): List = DatabaseFactory.dbQuery { ReiterTable.selectAll() .limit(limit).offset(offset.toLong()) diff --git a/backend/services/masterdata/masterdata-service/src/main/resources/db/migration/V011__Remove_Redundant_Funktionaer_Fields.sql b/backend/services/masterdata/masterdata-service/src/main/resources/db/migration/V011__Remove_Redundant_Funktionaer_Fields.sql new file mode 100644 index 00000000..1a6fc55a --- /dev/null +++ b/backend/services/masterdata/masterdata-service/src/main/resources/db/migration/V011__Remove_Redundant_Funktionaer_Fields.sql @@ -0,0 +1,18 @@ +-- Flyway Migration V011: Redundante Felder aus Funktionaer entfernen und Verknüpfung zu Reiter hinzufügen + +-- 1. Neue Spalte reiter_id hinzufügen +ALTER TABLE funktionaer ADD COLUMN reiter_id UUID; + +-- 2. Fremdschlüssel-Constraint hinzufügen +ALTER TABLE funktionaer ADD CONSTRAINT fk_funktionaer_reiter FOREIGN KEY (reiter_id) REFERENCES reiter(reiter_id); + +-- 3. Redundante Felder entfernen +ALTER TABLE funktionaer DROP COLUMN image_url; +ALTER TABLE funktionaer DROP COLUMN email; +ALTER TABLE funktionaer DROP COLUMN telefon; +ALTER TABLE funktionaer DROP COLUMN website; +ALTER TABLE funktionaer DROP COLUMN strasse; +ALTER TABLE funktionaer DROP COLUMN hausnummer; +ALTER TABLE funktionaer DROP COLUMN plz; +ALTER TABLE funktionaer DROP COLUMN ort; +ALTER TABLE funktionaer DROP COLUMN bundesland; diff --git a/docs/01_Architecture/MASTER_ROADMAP.md b/docs/01_Architecture/MASTER_ROADMAP.md index d925b195..84cba9eb 100644 --- a/docs/01_Architecture/MASTER_ROADMAP.md +++ b/docs/01_Architecture/MASTER_ROADMAP.md @@ -106,7 +106,8 @@ und über definierte Schnittstellen kommunizieren. #### 🧐 Agent: QA Specialist -* [x] **Actor Context Stabilization:** Funktionär-Datenmodell (Richter/Parcoursbauer) auf professionelle Master-Daten-Referenzierung umgestellt. Qualifikations-Kürzel (ÖTO/FEI) werden nun zentral validiert und über einen Seeder befüllt. SQL-Schema (V010) harmonisiert. +* [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. +* [x] **ZNS-Import Optimization:** Automatische Verknüpfung von Funktionären mit Reitern (Reihenfolge: VEREIN -> LIZENZ -> PFERDE -> RICHT). * [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. → Note: `IdempotencyApiIntegrationTest` bleibt vorerst @Disabled, da das Hochfahren des Spring-Contexts in der