Remove deprecated ZnsLegacyParsers, update MASTER_ROADMAP to reflect cleanup of parser remnants, stabilize ZNS import tests, and improve ZnsImportService with refined ZipInputStream management and enhanced functionary-reiter matching logic.
This commit is contained in:
@@ -60,6 +60,8 @@ Versionierung folgt [Semantic Versioning](https://semver.org/lang/de/).
|
|||||||
## [1.0.2-SNAPSHOT] — 2026-04-06
|
## [1.0.2-SNAPSHOT] — 2026-04-06
|
||||||
|
|
||||||
### Geändert
|
### Geändert
|
||||||
|
- **ZNS-Import:** `ZnsImportService` stabilisiert (ZipInputStream-Management korrigiert), um sequentielle Imports in Tests zu ermöglichen.
|
||||||
|
- **Test-Vollständigkeit:** `ZnsImportServiceTest` korrigiert (Mocking für Reiter-Suche ergänzt, Testdaten für Funktionäre an Int-Parser angepasst). Alle 9 Tests nun grün.
|
||||||
- **Data Modeling:** Redundante Kontakt- und Adressdaten aus `FunktionaerTable` entfernt; stattdessen Verknüpfung zu `ReiterTable` via `reiter_id` hinzugefügt. (Bereinigung der Felder erfolgte in `V010`).
|
- **Data Modeling:** Redundante Kontakt- und Adressdaten aus `FunktionaerTable` entfernt; stattdessen Verknüpfung zu `ReiterTable` via `reiter_id` hinzugefügt. (Bereinigung der Felder erfolgte in `V010`).
|
||||||
- **Import:** ZNS-Importer verknüpft nun Funktionäre automatisch mit vorhandenen Reitern anhand des Namens (Nachname, Vorname).
|
- **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.
|
- **Infrastructure:** `findByName` in `ReiterRepository` implementiert für effiziente Suche während des Imports.
|
||||||
|
|||||||
+8
-2
@@ -57,7 +57,8 @@ class ZnsImportService(
|
|||||||
*/
|
*/
|
||||||
fun extrahiereDateien(zipInputStream: InputStream): Map<String, List<String>> {
|
fun extrahiereDateien(zipInputStream: InputStream): Map<String, List<String>> {
|
||||||
val dateien = mutableMapOf<String, List<String>>()
|
val dateien = mutableMapOf<String, List<String>>()
|
||||||
ZipInputStream(zipInputStream).use { zip ->
|
val zip = ZipInputStream(zipInputStream)
|
||||||
|
try {
|
||||||
var entry = zip.nextEntry
|
var entry = zip.nextEntry
|
||||||
while (entry != null) {
|
while (entry != null) {
|
||||||
val fileName = entry.name.uppercase().substringAfterLast("/")
|
val fileName = entry.name.uppercase().substringAfterLast("/")
|
||||||
@@ -73,8 +74,14 @@ class ZnsImportService(
|
|||||||
val lines = content.split(Regex("\\r?\\n|\\r")).filter { it.isNotBlank() }
|
val lines = content.split(Regex("\\r?\\n|\\r")).filter { it.isNotBlank() }
|
||||||
dateien[fileName] = lines
|
dateien[fileName] = lines
|
||||||
}
|
}
|
||||||
|
zip.closeEntry()
|
||||||
entry = zip.nextEntry
|
entry = zip.nextEntry
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
// Wir schließen den ZipInputStream NICHT mit use,
|
||||||
|
// um den zugrunde liegenden zipInputStream nicht vorzeitig zu schließen.
|
||||||
|
// Falls der Aufrufer den Stream schließen will, soll er das tun.
|
||||||
|
// Aber wir müssen sicherstellen, dass wir alle Entries gelesen haben.
|
||||||
}
|
}
|
||||||
return dateien
|
return dateien
|
||||||
}
|
}
|
||||||
@@ -261,7 +268,6 @@ class ZnsImportService(
|
|||||||
runCatching {
|
runCatching {
|
||||||
val funktionaerRaw = 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 nameParts = funktionaerRaw.name?.split(",")?.map { it.trim() }
|
||||||
val reiterId = if (nameParts != null && nameParts.size >= 2) {
|
val reiterId = if (nameParts != null && nameParts.size >= 2) {
|
||||||
val nachname = nameParts[0]
|
val nachname = nameParts[0]
|
||||||
|
|||||||
+4
-1
@@ -103,7 +103,8 @@ class ZnsImportServiceTest {
|
|||||||
qualifikationen: String = "GA"
|
qualifikationen: String = "GA"
|
||||||
): String {
|
): String {
|
||||||
// Stelle 1: Typ (X=Richter, Y=Parcoursbauer), 2-7: Satznummer (6), 8-82: Name (75), 83-112: Quali (30)
|
// Stelle 1: Typ (X=Richter, Y=Parcoursbauer), 2-7: Satznummer (6), 8-82: Name (75), 83-112: Quali (30)
|
||||||
return typ + satznummer.padEnd(6) + name.padEnd(75) + qualifikationen.padEnd(30)
|
// WICHTIG: satznummer muss genau 6 Stellen lang sein, ohne abschließende Leerzeichen für den Int-Parser
|
||||||
|
return typ + satznummer.padStart(6, '0') + name.padEnd(75) + qualifikationen.padEnd(30)
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
@@ -197,6 +198,7 @@ class ZnsImportServiceTest {
|
|||||||
|
|
||||||
coEvery { funktionaerRepository.findBySatz(any(), any()) } returns null
|
coEvery { funktionaerRepository.findBySatz(any(), any()) } returns null
|
||||||
coEvery { funktionaerRepository.save(any()) } answers { firstArg<Funktionaer>() }
|
coEvery { funktionaerRepository.save(any()) } answers { firstArg<Funktionaer>() }
|
||||||
|
coEvery { reiterRepository.findByName(any(), any()) } returns emptyList()
|
||||||
|
|
||||||
val result = service.importiereZip(zip)
|
val result = service.importiereZip(zip)
|
||||||
|
|
||||||
@@ -234,6 +236,7 @@ class ZnsImportServiceTest {
|
|||||||
coEvery { vereinRepository.findByVereinsNummer(any()) } returns null
|
coEvery { vereinRepository.findByVereinsNummer(any()) } returns null
|
||||||
coEvery { vereinRepository.save(any()) } answers { firstArg<Verein>() }
|
coEvery { vereinRepository.save(any()) } answers { firstArg<Verein>() }
|
||||||
coEvery { reiterRepository.findBySatznummer(any()) } returns null
|
coEvery { reiterRepository.findBySatznummer(any()) } returns null
|
||||||
|
coEvery { reiterRepository.findByName(any(), any()) } returns emptyList()
|
||||||
coEvery { reiterRepository.save(any()) } answers { firstArg<Reiter>() }
|
coEvery { reiterRepository.save(any()) } answers { firstArg<Reiter>() }
|
||||||
coEvery { horseRepository.findBySatznummer(any()) } returns null
|
coEvery { horseRepository.findBySatznummer(any()) } returns null
|
||||||
coEvery { horseRepository.findByLebensnummer(any()) } returns null
|
coEvery { horseRepository.findByLebensnummer(any()) } returns null
|
||||||
|
|||||||
@@ -1,208 +0,0 @@
|
|||||||
package at.mocode.zns.parser
|
|
||||||
|
|
||||||
import at.mocode.core.domain.model.DatenQuelleE
|
|
||||||
import at.mocode.core.domain.model.ReiterLizenzKlasseE
|
|
||||||
import at.mocode.core.domain.model.PferdeGeschlechtE
|
|
||||||
import at.mocode.core.utils.parser.FixedWidthLineReader
|
|
||||||
import at.mocode.masterdata.domain.model.Funktionaer
|
|
||||||
import at.mocode.masterdata.domain.model.Pferd
|
|
||||||
import at.mocode.masterdata.domain.model.Reiter
|
|
||||||
import at.mocode.masterdata.domain.model.Verein
|
|
||||||
import kotlin.uuid.ExperimentalUuidApi
|
|
||||||
import kotlin.uuid.Uuid
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parsers for the legacy ZNS .dat files (Fixed-Width format, CP850 encoded).
|
|
||||||
*
|
|
||||||
* @deprecated Use specialized parsers instead (ZnsVereinParser, ZnsReiterParser, etc.)
|
|
||||||
*/
|
|
||||||
@Deprecated("Use specialized parsers instead")
|
|
||||||
@OptIn(ExperimentalUuidApi::class)
|
|
||||||
object ZnsLegacyParsers {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses a line from VEREIN01.DAT.
|
|
||||||
*/
|
|
||||||
fun parseVerein(line: String): Verein? {
|
|
||||||
if (line.isBlank() || line.length < 5) return null
|
|
||||||
|
|
||||||
val reader = FixedWidthLineReader(line)
|
|
||||||
|
|
||||||
val vereinsNummer = reader.getString(1, 4)
|
|
||||||
val vereinsName = reader.getString(5, 50)
|
|
||||||
|
|
||||||
if (vereinsNummer.isBlank() || vereinsName.isBlank()) return null
|
|
||||||
|
|
||||||
return Verein(
|
|
||||||
vereinsNummer = vereinsNummer,
|
|
||||||
vereinName = vereinsName,
|
|
||||||
datenQuelle = DatenQuelleE.IMPORT_ZNS
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses a line from LIZENZ01.DAT.
|
|
||||||
*/
|
|
||||||
fun parseLizenz(line: String): Reiter? {
|
|
||||||
if (line.isBlank() || line.length < 57) return null
|
|
||||||
|
|
||||||
val reader = FixedWidthLineReader(line)
|
|
||||||
|
|
||||||
val satznummer = reader.getString(1, 6)
|
|
||||||
if (satznummer.isBlank()) return null
|
|
||||||
|
|
||||||
val nachname = reader.getString(7, 50)
|
|
||||||
val vorname = reader.getString(57, 25)
|
|
||||||
val bundeslandNummer = reader.getIntOrNull(82, 2)
|
|
||||||
val vereinsName = reader.getString(84, 50)
|
|
||||||
val nation = reader.getString(134, 3)
|
|
||||||
val reiterLizenz = reader.getString(137, 4)
|
|
||||||
// Ab Stelle 137 weicht die Realität der ZNS.zip von der Spec 2.4 ab
|
|
||||||
// Die Realität (Aichinger Ewald) zeigt:
|
|
||||||
// 134-136: AUT
|
|
||||||
// 137-140: R2
|
|
||||||
// 147-158: 206607000676 (Mitgliedsnummer 8 Stellen ab 147?)
|
|
||||||
// 160-166: 4825910 (Telefonnummer?)
|
|
||||||
// 177-180: 2023 (LastPayYear)
|
|
||||||
// 181: M (Geschlecht)
|
|
||||||
// 182-189: 19571010 (Geburtsdatum)
|
|
||||||
val startkarte = reader.getString(141, 1)
|
|
||||||
val fahrLizenz = reader.getString(142, 2)
|
|
||||||
val altersklasseJgJrU25 = reader.getString(144, 2)
|
|
||||||
val altersklasseY = reader.getString(146, 1)
|
|
||||||
val mitgliedsNummer = reader.getIntOrNull(147, 8)
|
|
||||||
val telefonNummer = reader.getString(155, 22).trim()
|
|
||||||
val kader = reader.getString(177, 1)
|
|
||||||
val lastPayYear = reader.getIntOrNull(177, 4)
|
|
||||||
val geschlecht = reader.getString(181, 1)
|
|
||||||
val geburtsdatum = reader.getLocalDateOrNull(182, 8)
|
|
||||||
val feiId = reader.getString(190, 8)
|
|
||||||
val sperrListe = reader.getString(198, 1)
|
|
||||||
val lizenzInfo = reader.getString(201, 10)
|
|
||||||
val lizenzKlasse = mapLizenz(reiterLizenz)
|
|
||||||
|
|
||||||
return Reiter(
|
|
||||||
personId = Uuid.random(),
|
|
||||||
satznummer = satznummer,
|
|
||||||
nachname = nachname,
|
|
||||||
vorname = vorname,
|
|
||||||
bundeslandNummer = bundeslandNummer,
|
|
||||||
vereinsName = vereinsName.ifBlank { null },
|
|
||||||
nation = nation.ifBlank { null },
|
|
||||||
reiterLizenz = reiterLizenz.ifBlank { null },
|
|
||||||
startkarte = startkarte.ifBlank { null },
|
|
||||||
fahrLizenz = fahrLizenz.ifBlank { null },
|
|
||||||
altersklasseJgJrU25 = altersklasseJgJrU25.ifBlank { null },
|
|
||||||
altersklasseY = altersklasseY.ifBlank { null },
|
|
||||||
mitgliedsNummer = mitgliedsNummer,
|
|
||||||
telefonNummer = telefonNummer.ifBlank { null },
|
|
||||||
kader = kader.ifBlank { null },
|
|
||||||
lastPayYear = lastPayYear,
|
|
||||||
geschlecht = geschlecht.ifBlank { null },
|
|
||||||
geburtsdatum = geburtsdatum,
|
|
||||||
feiId = feiId.ifBlank { null },
|
|
||||||
sperrListe = sperrListe.ifBlank { null },
|
|
||||||
lizenzInfo = lizenzInfo.ifBlank { null },
|
|
||||||
lizenzKlasse = lizenzKlasse,
|
|
||||||
datenQuelle = DatenQuelleE.IMPORT_ZNS
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses a line from PFERDE01.DAT.
|
|
||||||
*/
|
|
||||||
fun parsePferd(line: String): Pferd? {
|
|
||||||
if (line.isBlank() || line.trim().length < 4) return null
|
|
||||||
|
|
||||||
val reader = FixedWidthLineReader(line)
|
|
||||||
val kopfnummer = reader.getString(1, 4)
|
|
||||||
val name = reader.getString(5, 30)
|
|
||||||
|
|
||||||
// We need at least a name to identify a horse record
|
|
||||||
if (name.isBlank()) return null
|
|
||||||
|
|
||||||
val lebensnummer = reader.getString(35, 9)
|
|
||||||
val geschlechtChar = reader.getString(44, 1)
|
|
||||||
val geschlecht = mapGeschlecht(geschlechtChar)
|
|
||||||
val geburtsjahr = reader.getIntOrNull(45, 4)
|
|
||||||
val farbe = reader.getString(49, 15)
|
|
||||||
val abstammung = reader.getString(64, 15)
|
|
||||||
val vereinNummer = reader.getIntOrNull(79, 4)
|
|
||||||
val lastPayYear = reader.getIntOrNull(83, 4)
|
|
||||||
val verantwortlichePersonId = reader.getString(87, 75)
|
|
||||||
val vaterName = reader.getString(162, 30)
|
|
||||||
val feiPass = reader.getString(192, 10)
|
|
||||||
val satznummer = reader.getString(202, 10)
|
|
||||||
|
|
||||||
return Pferd(
|
|
||||||
personId = Uuid.random(),
|
|
||||||
pferdeName = name,
|
|
||||||
geschlecht = geschlecht,
|
|
||||||
geburtsjahr = geburtsjahr,
|
|
||||||
lebensnummer = lebensnummer.ifBlank { null },
|
|
||||||
kopfnummer = kopfnummer.ifBlank { null },
|
|
||||||
satznummer = satznummer.ifBlank { null },
|
|
||||||
farbe = farbe.ifBlank { null },
|
|
||||||
abstammung = abstammung.ifBlank { null },
|
|
||||||
vereinNummer = vereinNummer,
|
|
||||||
lastPayYear = lastPayYear,
|
|
||||||
verantwortlichePersonId = verantwortlichePersonId.ifBlank { null },
|
|
||||||
vater = vaterName.ifBlank { null },
|
|
||||||
feiPass = feiPass.ifBlank { null },
|
|
||||||
datenQuelle = DatenQuelleE.IMPORT_ZNS
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses a line from RICHT01.DAT (Richter oder Parcoursbauer).
|
|
||||||
*/
|
|
||||||
fun parseFunktionaer(line: String): Funktionaer? {
|
|
||||||
if (line.isBlank() || line.length < 8) return null
|
|
||||||
|
|
||||||
val reader = FixedWidthLineReader(line)
|
|
||||||
val satzID = reader.getString(1, 1).uppercase()
|
|
||||||
if (satzID != "X" && satzID != "Y") return null
|
|
||||||
|
|
||||||
val satzNummer = reader.getIntOrNull(2, 6)
|
|
||||||
if (satzNummer == null) return null
|
|
||||||
|
|
||||||
// Name begins directly after the satzNummer (position 8)
|
|
||||||
val name = reader.getString(8, 75).trim()
|
|
||||||
// Qualifikation is much later, probably at 83?
|
|
||||||
// Wait, name is 75 chars, so 8 + 75 = 83.
|
|
||||||
val qualifikationenRaw = reader.getString(83, 30).trim()
|
|
||||||
val qualifikationen = qualifikationenRaw.split(",")
|
|
||||||
.map { it.trim() }
|
|
||||||
.filter { it.isNotBlank() }
|
|
||||||
|
|
||||||
return Funktionaer(
|
|
||||||
personId = null,
|
|
||||||
satzId = satzID,
|
|
||||||
satzNummer = satzNummer,
|
|
||||||
name = name.ifBlank { null },
|
|
||||||
qualifikationen = qualifikationen,
|
|
||||||
datenQuelle = DatenQuelleE.IMPORT_ZNS
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun mapLizenz(lizenz: String): ReiterLizenzKlasseE {
|
|
||||||
return when (lizenz.uppercase()) {
|
|
||||||
"R1" -> ReiterLizenzKlasseE.R1
|
|
||||||
"R2" -> ReiterLizenzKlasseE.R2
|
|
||||||
"R3" -> ReiterLizenzKlasseE.R3
|
|
||||||
"RD1" -> ReiterLizenzKlasseE.RD1
|
|
||||||
"RD2" -> ReiterLizenzKlasseE.RD2
|
|
||||||
"RD3" -> ReiterLizenzKlasseE.RD3
|
|
||||||
else -> ReiterLizenzKlasseE.LIZENZFREI
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun mapGeschlecht(geschlecht: String): PferdeGeschlechtE {
|
|
||||||
return when (geschlecht.uppercase()) {
|
|
||||||
"W" -> PferdeGeschlechtE.WALLACH
|
|
||||||
"S" -> PferdeGeschlechtE.STUTE
|
|
||||||
"H" -> PferdeGeschlechtE.HENGST
|
|
||||||
else -> PferdeGeschlechtE.UNBEKANNT
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -106,8 +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 und mit Reiter-Daten verknüpft (Import-Idempotenz via `satzNummer`). Redundante Kontakt-/Adressdaten entfernt.
|
* [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. Alle ZNS-Import-Tests (9/9) stabilisiert und verwaiste Parser-Reste entfernt.
|
||||||
* [x] **ZNS-Import Optimization:** Automatische Verknüpfung von Funktionären mit Reitern (Reihenfolge: VEREIN -> LIZENZ -> PFERDE -> RICHT).
|
* [x] **ZNS-Import Optimization:** Automatische Verknüpfung von Funktionären mit Reitern (Reihenfolge: VEREIN -> LIZENZ -> PFERDE -> RICHT). `ZnsImportService` für sequentielle Imports in Tests gehärtet.
|
||||||
* [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