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:
@@ -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
|
## [1.0.1-SNAPSHOT] — 2026-04-05
|
||||||
|
|
||||||
### Geändert
|
### Geändert
|
||||||
|
|||||||
+13
-1
@@ -259,7 +259,18 @@ class ZnsImportService(
|
|||||||
var aktualisiert = 0
|
var aktualisiert = 0
|
||||||
zeilen.forEachIndexed { index, zeile ->
|
zeilen.forEachIndexed { index, zeile ->
|
||||||
runCatching {
|
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 satzID = funktionaer.satzId
|
||||||
val satzNummer = funktionaer.satzNummer
|
val satzNummer = funktionaer.satzNummer
|
||||||
val vorhanden = funktionaerRepository.findBySatz(satzID, satzNummer)
|
val vorhanden = funktionaerRepository.findBySatz(satzID, satzNummer)
|
||||||
@@ -269,6 +280,7 @@ class ZnsImportService(
|
|||||||
} else {
|
} else {
|
||||||
funktionaerRepository.save(
|
funktionaerRepository.save(
|
||||||
vorhanden.copy(
|
vorhanden.copy(
|
||||||
|
reiterId = funktionaer.reiterId,
|
||||||
name = funktionaer.name,
|
name = funktionaer.name,
|
||||||
qualifikationen = funktionaer.qualifikationen,
|
qualifikationen = funktionaer.qualifikationen,
|
||||||
istAktiv = funktionaer.istAktiv,
|
istAktiv = funktionaer.istAktiv,
|
||||||
|
|||||||
+4
-13
@@ -38,6 +38,10 @@ data class Funktionaer(
|
|||||||
@Serializable(with = UuidSerializer::class)
|
@Serializable(with = UuidSerializer::class)
|
||||||
val personId: Uuid? = null,
|
val personId: Uuid? = null,
|
||||||
|
|
||||||
|
// Reference to Reiter
|
||||||
|
@Serializable(with = UuidSerializer::class)
|
||||||
|
val reiterId: Uuid? = null,
|
||||||
|
|
||||||
// === ZNS.zip RICHT01.DAT === ANFANG ===
|
// === ZNS.zip RICHT01.DAT === ANFANG ===
|
||||||
|
|
||||||
// Alphanumerisch (1) WERT "X" = RICHTER, "Y" = PARCOURSBAUER
|
// Alphanumerisch (1) WERT "X" = RICHTER, "Y" = PARCOURSBAUER
|
||||||
@@ -54,19 +58,6 @@ data class Funktionaer(
|
|||||||
|
|
||||||
// === ZNS.zip RICHT01.DAT === ENDE ===
|
// === 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
|
// Status & Verwaltung
|
||||||
var istAktiv: Boolean = true,
|
var istAktiv: Boolean = true,
|
||||||
var bemerkungen: String? = null,
|
var bemerkungen: String? = null,
|
||||||
|
|||||||
+5
@@ -23,6 +23,11 @@ interface ReiterRepository {
|
|||||||
*/
|
*/
|
||||||
suspend fun findBySatznummer(satznummer: String?): Reiter?
|
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).
|
* Gibt alle Reiter zurück (paginiert).
|
||||||
*/
|
*/
|
||||||
|
|||||||
+3
@@ -30,6 +30,7 @@ class FunktionaerExposedRepository : FunktionaerRepository {
|
|||||||
return Funktionaer(
|
return Funktionaer(
|
||||||
funktionaerId = row[FunktionaerTable.id],
|
funktionaerId = row[FunktionaerTable.id],
|
||||||
personId = row[FunktionaerTable.personId],
|
personId = row[FunktionaerTable.personId],
|
||||||
|
reiterId = row[FunktionaerTable.reiterId],
|
||||||
satzId = row[FunktionaerTable.satzId],
|
satzId = row[FunktionaerTable.satzId],
|
||||||
satzNummer = row[FunktionaerTable.satzNummer] ?: 0,
|
satzNummer = row[FunktionaerTable.satzNummer] ?: 0,
|
||||||
name = row[FunktionaerTable.name],
|
name = row[FunktionaerTable.name],
|
||||||
@@ -84,6 +85,7 @@ class FunktionaerExposedRepository : FunktionaerRepository {
|
|||||||
if (exists) {
|
if (exists) {
|
||||||
FunktionaerTable.update({ FunktionaerTable.id eq funktionaer.funktionaerId }) {
|
FunktionaerTable.update({ FunktionaerTable.id eq funktionaer.funktionaerId }) {
|
||||||
it[personId] = funktionaer.personId
|
it[personId] = funktionaer.personId
|
||||||
|
it[reiterId] = funktionaer.reiterId
|
||||||
it[satzId] = funktionaer.satzId
|
it[satzId] = funktionaer.satzId
|
||||||
it[satzNummer] = funktionaer.satzNummer
|
it[satzNummer] = funktionaer.satzNummer
|
||||||
it[name] = funktionaer.name
|
it[name] = funktionaer.name
|
||||||
@@ -96,6 +98,7 @@ class FunktionaerExposedRepository : FunktionaerRepository {
|
|||||||
FunktionaerTable.insert {
|
FunktionaerTable.insert {
|
||||||
it[id] = funktionaer.funktionaerId
|
it[id] = funktionaer.funktionaerId
|
||||||
it[personId] = funktionaer.personId
|
it[personId] = funktionaer.personId
|
||||||
|
it[reiterId] = funktionaer.reiterId
|
||||||
it[satzId] = funktionaer.satzId
|
it[satzId] = funktionaer.satzId
|
||||||
it[satzNummer] = funktionaer.satzNummer
|
it[satzNummer] = funktionaer.satzNummer
|
||||||
it[name] = funktionaer.name
|
it[name] = funktionaer.name
|
||||||
|
|||||||
+2
-13
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
package at.mocode.masterdata.infrastructure.persistence.funktionaer
|
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.core.Table
|
||||||
import org.jetbrains.exposed.v1.datetime.CurrentTimestamp
|
import org.jetbrains.exposed.v1.datetime.CurrentTimestamp
|
||||||
import org.jetbrains.exposed.v1.datetime.timestamp
|
import org.jetbrains.exposed.v1.datetime.timestamp
|
||||||
@@ -15,6 +16,7 @@ import kotlin.uuid.ExperimentalUuidApi
|
|||||||
object FunktionaerTable : Table("funktionaer") {
|
object FunktionaerTable : Table("funktionaer") {
|
||||||
val id = uuid("funktionaer_id")
|
val id = uuid("funktionaer_id")
|
||||||
val personId = uuid("person_id").nullable()
|
val personId = uuid("person_id").nullable()
|
||||||
|
val reiterId = uuid("reiter_id").references(ReiterTable.id).nullable()
|
||||||
|
|
||||||
// === ZNS.zip RICHT01.DAT (Zentrales Nennungssystem) === ANFANG ===
|
// === ZNS.zip RICHT01.DAT (Zentrales Nennungssystem) === ANFANG ===
|
||||||
|
|
||||||
@@ -29,19 +31,6 @@ object FunktionaerTable : Table("funktionaer") {
|
|||||||
|
|
||||||
// === ZNS.zip RICHT01.DAT === ENDE ===
|
// === 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
|
// Status & Verwaltung
|
||||||
val istAktiv = bool("ist_aktiv").default(true)
|
val istAktiv = bool("ist_aktiv").default(true)
|
||||||
val bemerkungen = text("bemerkungen").nullable()
|
val bemerkungen = text("bemerkungen").nullable()
|
||||||
|
|||||||
+11
@@ -8,7 +8,9 @@ import at.mocode.core.utils.database.DatabaseFactory
|
|||||||
import at.mocode.masterdata.domain.model.Reiter
|
import at.mocode.masterdata.domain.model.Reiter
|
||||||
import at.mocode.masterdata.domain.repository.ReiterRepository
|
import at.mocode.masterdata.domain.repository.ReiterRepository
|
||||||
import org.jetbrains.exposed.v1.core.ResultRow
|
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.eq
|
||||||
|
import org.jetbrains.exposed.v1.core.lowerCase
|
||||||
import org.jetbrains.exposed.v1.jdbc.*
|
import org.jetbrains.exposed.v1.jdbc.*
|
||||||
import kotlin.uuid.ExperimentalUuidApi
|
import kotlin.uuid.ExperimentalUuidApi
|
||||||
import kotlin.uuid.Uuid
|
import kotlin.uuid.Uuid
|
||||||
@@ -63,6 +65,15 @@ class ReiterExposedRepository : ReiterRepository {
|
|||||||
.singleOrNull()
|
.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 {
|
override suspend fun findAll(limit: Int, offset: Int): List<Reiter> = DatabaseFactory.dbQuery {
|
||||||
ReiterTable.selectAll()
|
ReiterTable.selectAll()
|
||||||
.limit(limit).offset(offset.toLong())
|
.limit(limit).offset(offset.toLong())
|
||||||
|
|||||||
+18
@@ -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;
|
||||||
@@ -106,7 +106,8 @@ und über definierte Schnittstellen kommunizieren.
|
|||||||
|
|
||||||
#### 🧐 Agent: QA Specialist
|
#### 🧐 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] **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.
|
* [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
|
→ Note: `IdempotencyApiIntegrationTest` bleibt vorerst @Disabled, da das Hochfahren des Spring-Contexts in der
|
||||||
|
|||||||
Reference in New Issue
Block a user