Link Funktionaer to Reiter via reiter_id, implement findByName in ReiterRepository, optimize ZNS import for functionary-reiter matching, remove redundant fields from FunktionaerTable, and add database migration V011.

This commit is contained in:
2026-04-06 14:21:04 +02:00
parent 9237882437
commit 3cab4c4f47
9 changed files with 66 additions and 28 deletions
+8
View File
@@ -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
@@ -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,
@@ -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,
@@ -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<Reiter>
/**
* Gibt alle Reiter zurück (paginiert).
*/
@@ -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
@@ -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()
@@ -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<Reiter> = 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<Reiter> = DatabaseFactory.dbQuery {
ReiterTable.selectAll()
.limit(limit).offset(offset.toLong())
@@ -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;
+2 -1
View File
@@ -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