Refactor domain models (DomFunktionaer, DomReiter, DomPferd) for ZNS alignment: update properties, streamline validation logic, and enhance parser to support new format. Adjust controllers and repository methods accordingly.

This commit is contained in:
2026-04-05 08:21:11 +02:00
parent aba7b58dd4
commit a61dda69d1
27 changed files with 1006 additions and 1111 deletions
@@ -49,6 +49,32 @@ class ZnsImportService(
private const val FILE_RICHT = "RICHT01.DAT"
}
/**
* Extrahiert die relevanten Dateien aus dem ZIP-Archiv.
*/
fun extrahiereDateien(zipInputStream: InputStream): Map<String, List<String>> {
val dateien = mutableMapOf<String, List<String>>()
ZipInputStream(zipInputStream).use { zip ->
var entry = zip.nextEntry
while (entry != null) {
val entryPath = entry.name.uppercase()
val fileName = entryPath.substringAfterLast("/")
if (fileName in setOf(FILE_VEREIN, FILE_LIZENZ, FILE_PFERDE, FILE_RICHT)) {
// Read all bytes from the current entry
val bytes = zip.readBytes()
// Convert to string using the correct charset and split into lines
val content = bytes.toString(CP850)
val lines = content.split(Regex("\\r?\\n|\\r")).filter { it.isNotBlank() }
dateien[fileName] = lines
}
zip.closeEntry()
entry = zip.nextEntry
}
}
return dateien
}
/**
* Importiert eine ZNS-ZIP-Datei aus einem [InputStream].
*
@@ -56,18 +82,7 @@ class ZnsImportService(
* @return [ZnsImportResult] mit Statistiken und eventuellen Fehlern.
*/
suspend fun importiereZip(zipInputStream: InputStream): ZnsImportResult {
val dateien = mutableMapOf<String, List<String>>()
ZipInputStream(zipInputStream).use { zip ->
var entry = zip.nextEntry
while (entry != null) {
val name = entry.name.uppercase().substringAfterLast("/")
if (name in setOf(FILE_VEREIN, FILE_LIZENZ, FILE_PFERDE, FILE_RICHT)) {
dateien[name] = zip.readBytes().toString(CP850).lines()
}
zip.closeEntry()
entry = zip.nextEntry
}
}
val dateien = extrahiereDateien(zipInputStream)
val fehler = mutableListOf<String>()
val warnungen = mutableListOf<String>()
@@ -75,7 +90,7 @@ class ZnsImportService(
val (vereineNeu, vereineUpd) = importiereVereine(dateien[FILE_VEREIN] ?: emptyList(), fehler)
val (reiterNeu, reiterUpd) = importiereReiter(dateien[FILE_LIZENZ] ?: emptyList(), fehler, warnungen)
val (pferdeNeu, pferdeUpd) = importierePferde(dateien[FILE_PFERDE] ?: emptyList(), fehler)
val (richterNeu, richterUpd) = importiereRichter(dateien[FILE_RICHT] ?: emptyList(), fehler, warnungen)
val (richterNeu, richterUpd) = importiereFunktionaere(dateien[FILE_RICHT] ?: emptyList(), fehler, warnungen)
return ZnsImportResult(
vereineImportiert = vereineNeu,
@@ -95,7 +110,7 @@ class ZnsImportService(
// Private Hilfsmethoden
// -------------------------------------------------------------------------
private suspend fun importiereVereine(
suspend fun importiereVereine(
zeilen: List<String>,
fehler: MutableList<String>
): Pair<Int, Int> {
@@ -131,7 +146,7 @@ class ZnsImportService(
return Pair(neu, aktualisiert)
}
private suspend fun importiereReiter(
suspend fun importiereReiter(
zeilen: List<String>,
fehler: MutableList<String>,
warnungen: MutableList<String>
@@ -150,8 +165,23 @@ class ZnsImportService(
vorhanden.copy(
vorname = reiter.vorname,
nachname = reiter.nachname,
bundeslandNummer = reiter.bundeslandNummer,
vereinsName = reiter.vereinsName,
nation = reiter.nation,
reiterLizenz = reiter.reiterLizenz,
startkarte = reiter.startkarte,
fahrLizenz = reiter.fahrLizenz,
altersklasseJgJrU25 = reiter.altersklasseJgJrU25,
altersklasseY = reiter.altersklasseY,
mitgliedsNummer = reiter.mitgliedsNummer,
telefonNummer = reiter.telefonNummer,
kader = reiter.kader,
lastPayYear = reiter.lastPayYear,
geschlecht = reiter.geschlecht,
geburtsdatum = reiter.geburtsdatum,
feiId = reiter.feiId,
sperrListe = reiter.sperrListe,
lizenzInfo = reiter.lizenzInfo,
lizenzKlasse = reiter.lizenzKlasse,
istAktiv = reiter.istAktiv,
datenQuelle = reiter.datenQuelle
@@ -166,7 +196,7 @@ class ZnsImportService(
return Pair(neu, aktualisiert)
}
private suspend fun importierePferde(
suspend fun importierePferde(
zeilen: List<String>,
fehler: MutableList<String>
): Pair<Int, Int> {
@@ -175,9 +205,12 @@ class ZnsImportService(
zeilen.forEachIndexed { index, zeile ->
runCatching {
val pferd = ZnsLegacyParsers.parsePferd(zeile) ?: return@forEachIndexed
val vorhanden = pferd.lebensnummer
?.takeIf { it.isNotBlank() }
?.let { horseRepository.findByLebensnummer(it) }
if (pferd.pferdeName.isBlank()) return@forEachIndexed
// Match primarily by satznummer, then by lebensnummer, then by kopfnummer+name
val vorhanden = pferd.satznummer?.takeIf { it.isNotBlank() }?.let { horseRepository.findBySatznummer(it) }
?: pferd.lebensnummer?.takeIf { it.isNotBlank() }?.let { horseRepository.findByLebensnummer(it) }
if (vorhanden == null) {
horseRepository.save(pferd)
neu++
@@ -186,10 +219,17 @@ class ZnsImportService(
vorhanden.copy(
pferdeName = pferd.pferdeName,
geschlecht = pferd.geschlecht,
geburtsdatum = pferd.geburtsdatum,
rasse = pferd.rasse,
geburtsjahr = pferd.geburtsjahr,
farbe = pferd.farbe,
abstammung = pferd.abstammung,
vereinNummer = pferd.vereinNummer,
lastPayYear = pferd.lastPayYear,
verantwortlichePersonId = pferd.verantwortlichePersonId,
lebensnummer = pferd.lebensnummer,
oepsNummer = pferd.oepsNummer,
kopfnummer = pferd.kopfnummer,
satznummer = pferd.satznummer,
vater = pferd.vater,
feiPass = pferd.feiPass,
istAktiv = pferd.istAktiv,
datenQuelle = pferd.datenQuelle
).withUpdatedTimestamp()
@@ -203,7 +243,7 @@ class ZnsImportService(
return Pair(neu, aktualisiert)
}
private suspend fun importiereRichter(
suspend fun importiereFunktionaere(
zeilen: List<String>,
fehler: MutableList<String>,
warnungen: MutableList<String>
@@ -212,24 +252,22 @@ class ZnsImportService(
var aktualisiert = 0
zeilen.forEachIndexed { index, zeile ->
runCatching {
val richter = ZnsLegacyParsers.parseRichter(zeile) ?: return@forEachIndexed
val richterNummer = richter.richterNummer ?: run {
warnungen.add("$FILE_RICHT Zeile ${index + 1}: Keine RichterNummer übersprungen.")
return@forEachIndexed
}
val vorhanden = funktionaerRepository.findByRichterNummer(richterNummer)
val funktionaer = ZnsLegacyParsers.parseFunktionaer(zeile) ?: return@forEachIndexed
val satzID = funktionaer.satzID
val satzNummer = funktionaer.satzNummer
val vorhanden = funktionaerRepository.findBySatz(satzID, satzNummer)
if (vorhanden == null) {
funktionaerRepository.save(richter)
funktionaerRepository.save(funktionaer)
neu++
} else {
funktionaerRepository.save(
vorhanden.copy(
vorname = richter.vorname,
nachname = richter.nachname,
vereinsNummer = richter.vereinsNummer,
richterNummer = richter.richterNummer,
istAktiv = richter.istAktiv,
datenQuelle = richter.datenQuelle
satzID = satzID,
satzNummer = satzNummer,
name = funktionaer.name,
qualifikationen = funktionaer.qualifikationen,
istAktiv = funktionaer.istAktiv,
datenQuelle = funktionaer.datenQuelle
).withUpdatedTimestamp()
)
aktualisiert++
@@ -74,7 +74,8 @@ class ZnsImportServiceTest {
return satznummer.padEnd(6) +
nachname.padEnd(50) +
vorname.padEnd(25) +
" ".repeat(200) // Rest auffüllen
"01" + // 82-83 Bundesland
" ".repeat(250) // Rest auffüllen
}
/** Erzeugt eine gültige PFERDE01.DAT-Zeile (mind. 211 Zeichen). */
@@ -88,17 +89,19 @@ class ZnsImportServiceTest {
lebensnummer.padEnd(9) +
"W" + // Geschlecht: Wallach
"2015" + // Geburtsjahr
" ".repeat(157) // Auffüllen bis Stelle 201
return base + "SAT0000001".padEnd(10) // Satznummer ab Stelle 202
" ".repeat(157) // Auffüllen bis Stelle 201 (1 bis 201 = 201 Zeichen)
return base + "1234567890".padEnd(10) // Satznummer ab Stelle 202
}
/** Erzeugt eine gültige RICHT01.DAT-Zeile (mind. 83 Zeichen). */
private fun richterZeile(
satznummer: String = "R00001",
name: String = "Huber, Anna"
private fun funktionaerZeile(
typ: String = "X",
satznummer: String = "123456",
name: String = "Huber, Anna",
qualifikationen: String = "GA"
): String {
// Stelle 1: Typ, 2-7: Satznummer (6), 8-82: Name (75)
return "R" + satznummer.padEnd(6) + name.padEnd(75)
// 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)
}
// -------------------------------------------------------------------------
@@ -154,6 +157,7 @@ class ZnsImportServiceTest {
fun `importiereZip - neue Pferde werden gespeichert`() = runTest {
val zip = buildZip("PFERDE01.DAT" to pferdeZeile())
coEvery { horseRepository.findBySatznummer(any()) } returns null
coEvery { horseRepository.findByLebensnummer(any()) } returns null
coEvery { horseRepository.save(any()) } answers { firstArg<DomPferd>() }
@@ -166,10 +170,10 @@ class ZnsImportServiceTest {
}
@Test
fun `importiereZip - neue Richter werden gespeichert`() = runTest {
val zip = buildZip("RICHT01.DAT" to richterZeile())
fun `importiereZip - neue Funktionaere werden gespeichert`() = runTest {
val zip = buildZip("RICHT01.DAT" to funktionaerZeile())
coEvery { funktionaerRepository.findByRichterNummer(any()) } returns null
coEvery { funktionaerRepository.findBySatz(any(), any()) } returns null
coEvery { funktionaerRepository.save(any()) } answers { firstArg<DomFunktionaer>() }
val result = service.importiereZip(zip)
@@ -180,22 +184,40 @@ class ZnsImportServiceTest {
coVerify(exactly = 1) { funktionaerRepository.save(any<DomFunktionaer>()) }
}
@Test
fun `importiereZip - Richter und Parcoursbauer mit Mac-Zeilenumbruch werden importiert`() = runTest {
// Nur \r als Umbruch
val zip = buildZip(
"RICHT01.DAT" to "X139552Mc Mullen Elizabeth DIOR\rX014346Schubert Renate DM,DPF,GAR-SP,SPF,SS*\rX001416Lechner-Gebhard Jeannette DPF,DSGP\rY135894Helmreich Marilena GA\r"
)
coEvery { funktionaerRepository.findBySatz(any(), any()) } returns null
coEvery { funktionaerRepository.save(any()) } answers { firstArg<DomFunktionaer>() }
val result = service.importiereZip(zip)
assertThat(result.richterImportiert).isEqualTo(4)
assertThat(result.fehler).isEmpty()
coVerify(exactly = 4) { funktionaerRepository.save(any<DomFunktionaer>()) }
}
@Test
fun `importiereZip - vollstaendiger Import aller vier Dateien`() = runTest {
val zip = buildZip(
"VEREIN01.DAT" to vereinZeile(),
"LIZENZ01.DAT" to lizenzZeile(),
"PFERDE01.DAT" to pferdeZeile(),
"RICHT01.DAT" to richterZeile()
"RICHT01.DAT" to funktionaerZeile()
)
coEvery { vereinRepository.findByVereinsNummer(any()) } returns null
coEvery { vereinRepository.save(any()) } answers { firstArg<DomVerein>() }
coEvery { reiterRepository.findBySatznummer(any()) } returns null
coEvery { reiterRepository.save(any()) } answers { firstArg<DomReiter>() }
coEvery { horseRepository.findBySatznummer(any()) } returns null
coEvery { horseRepository.findByLebensnummer(any()) } returns null
coEvery { horseRepository.save(any()) } answers { firstArg<DomPferd>() }
coEvery { funktionaerRepository.findByRichterNummer(any()) } returns null
coEvery { funktionaerRepository.findBySatz(any(), any()) } returns null
coEvery { funktionaerRepository.save(any()) } answers { firstArg<DomFunktionaer>() }
val result = service.importiereZip(zip)