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
@@ -1,5 +1,7 @@
package at.mocode.core.utils.parser
import kotlinx.datetime.LocalDate
/**
* A simple utility to parse fixed-width strings based on 1-based start positions and lengths.
* This is particularly useful for parsing legacy data formats like the OePS ZNS formats.
@@ -36,4 +38,26 @@ class FixedWidthLineReader(private val line: String) {
val str = getString(start1Based, length)
return str.toIntOrNull()
}
/**
* Extracts a string and parses it as a LocalDate (format YYYYMMDD).
* Returns null if the field is empty or cannot be parsed.
*/
fun getLocalDateOrNull(start1Based: Int, length: Int): LocalDate? {
val str = getString(start1Based, length)
if (str.length != 8) return null
val year = str.substring(0, 4).toIntOrNull()
val month = str.substring(4, 6).toIntOrNull()
val day = str.substring(6, 8).toIntOrNull()
if (year == null || month == null || day == null) return null
if (month !in 1..12 || day !in 1..31) return null
return try {
LocalDate(year, month, day)
} catch (e: Exception) {
null
}
}
}
@@ -1,14 +1,13 @@
package at.mocode.zns.parser
import at.mocode.masterdata.domain.model.DomVerein
import at.mocode.masterdata.domain.model.DomReiter
import at.mocode.masterdata.domain.model.DomPferd
import at.mocode.masterdata.domain.model.DomFunktionaer
import at.mocode.core.domain.model.DatenQuelleE
import at.mocode.core.domain.model.LizenzKlasseE
import at.mocode.core.domain.model.PferdeGeschlechtE
import at.mocode.core.utils.parser.FixedWidthLineReader
import kotlinx.datetime.LocalDate
import at.mocode.masterdata.domain.model.DomFunktionaer
import at.mocode.masterdata.domain.model.DomPferd
import at.mocode.masterdata.domain.model.DomReiter
import at.mocode.masterdata.domain.model.DomVerein
import kotlin.uuid.ExperimentalUuidApi
import kotlin.uuid.Uuid
@@ -54,24 +53,56 @@ object ZnsLegacyParsers {
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 lizenzString = reader.getString(137, 4)
val lizenz = mapLizenz(lizenzString)
val sperrlisteFlag = reader.getString(200, 1)
val gesperrt = sperrlisteFlag == "S"
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)
return DomReiter(
personId = Uuid.random(),
satznummer = satznummer,
nachname = nachname,
vorname = vorname,
bundeslandNummer = bundeslandNummer,
vereinsName = vereinsName.ifBlank { null },
nation = nation.ifBlank { null },
lizenzKlasse = lizenz,
istAktiv = !gesperrt,
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 = mapLizenz(reiterLizenz),
datenQuelle = DatenQuelleE.IMPORT_ZNS
)
}
@@ -80,52 +111,71 @@ object ZnsLegacyParsers {
* Parses a line from PFERDE01.DAT.
*/
fun parsePferd(line: String): DomPferd? {
if (line.isBlank() || line.length < 202) return null
if (line.isBlank() || line.trim().length < 40) return null
val reader = FixedWidthLineReader(line)
val satznummer = reader.getString(202, 10)
if (satznummer.isBlank()) return null
val name = reader.getString(5, 30)
val kopfnummer = reader.getString(1, 4)
val name = reader.getString(5, 30)
val lebensnummer = reader.getString(35, 9)
val geschlechtChar = reader.getString(44, 1)
val geschlecht = mapGeschlecht(geschlechtChar)
val geburtsjahr = reader.getIntOrNull(45, 4)
val geburtsdatum = geburtsjahr?.let { LocalDate(it, 1, 1) }
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)
// Some lines might not have a satznummer, but we need at least a name to identify it
if (satznummer.isBlank() && name.isBlank()) return null
return DomPferd(
pferdeName = name,
geschlecht = geschlecht,
geburtsdatum = geburtsdatum,
geburtsjahr = geburtsjahr,
lebensnummer = lebensnummer.ifBlank { null },
kopfnummer = kopfnummer.ifBlank { null },
satznummer = satznummer,
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.
* Parses a line from RICHT01.DAT (Richter oder Parcoursbauer).
*/
fun parseRichter(line: String): DomFunktionaer? {
fun parseFunktionaer(line: String): DomFunktionaer? {
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.getString(2, 6)
if (satznummer.isBlank()) return null
val satzNummer = reader.getIntOrNull(2, 6)
if (satzNummer == null) return null
val fullName = reader.getString(8, 75)
val parts = fullName.split(",").map { it.trim() }
val nachname = parts.getOrNull(0) ?: fullName
val vorname = parts.getOrNull(1) ?: ""
// 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 DomFunktionaer(
richterNummer = satznummer,
nachname = nachname,
vorname = vorname,
satzID = satzID,
satzNummer = satzNummer,
name = name.ifBlank { null },
qualifikationen = qualifikationen,
datenQuelle = DatenQuelleE.IMPORT_ZNS
)
}
@@ -21,27 +21,35 @@ class ZnsLegacyParsersTest {
@Test
fun `parseLizenz should extract LIZENZ01 correctly`() {
val sb = StringBuilder()
sb.append("123456")
sb.append("Mustermann ")
sb.append("Max ")
sb.append("01")
sb.append("Reitverein Wien ")
sb.append("AUT")
sb.append("R1 ")
while (sb.length < 199) {
sb.append(" ")
}
sb.append("S")
sb.append("123456") // 1-6
sb.append("Mustermann ") // 7-56
sb.append("Max ") // 57-81
sb.append("01") // 82-83
sb.append("Reitverein Wien ") // 84-133
sb.append("AUT") // 134-136
sb.append("R1 ") // 137-140
sb.append(" ") // 141-146 (leer)
sb.append("00000001") // 147-154 (mitgliedsNummer)
sb.append("0676 12345678 ") // 155-176 (telefonNummer length 22)
sb.append("2026") // 177-180 (lastPayYear)
sb.append("M") // 181 (geschlecht)
sb.append("19800101") // 182-189 (geburtsdatum)
sb.append("1000000001") // 190-199 (feiId length 10)
sb.append("S") // 200 (sperrListe)
sb.append("INFO1 ") // 201-210 (lizenzInfo)
val result = ZnsLegacyParsers.parseLizenz(sb.toString())
assertNotNull(result)
assertEquals("123456", result.satznummer)
assertEquals("Mustermann", result.nachname)
assertEquals("Max", result.vorname)
assertEquals(1, result.bundeslandNummer)
assertEquals("Reitverein Wien", result.vereinsName)
assertEquals(LizenzKlasseE.R1, result.lizenzKlasse)
assertEquals(false, result.istAktiv)
assertEquals("AUT", result.nation)
assertEquals("R1", result.reiterLizenz)
assertEquals(2026, result.lastPayYear)
assertEquals("M", result.geschlecht)
assertEquals("1980-01-01", result.geburtsdatum.toString())
}
@Test
@@ -60,21 +68,144 @@ class ZnsLegacyParsersTest {
val result = ZnsLegacyParsers.parsePferd(sb.toString())
assertNotNull(result)
assertEquals("A123", result.kopfnummer)
assertEquals("0000000001", result.satznummer)
assertEquals("Black Beauty", result.pferdeName)
assertEquals("123456789", result.lebensnummer)
assertEquals(PferdeGeschlechtE.WALLACH, result.geschlecht)
assertEquals(2010, result.geburtsdatum?.year)
assertEquals(2010, result.geburtsjahr)
}
@Test
fun `parseRichter should extract RICHT01 correctly`() {
val line =
"X123456Richter, Peter GA "
val result = ZnsLegacyParsers.parseRichter(line)
fun `parseFunktionaer should extract RICHT01 correctly for Richter`() {
// Real example from RICHT01.dat
val line = "X010128Zitterbart Rainer PI-A"
val result = ZnsLegacyParsers.parseFunktionaer(line)
assertNotNull(result)
assertEquals("123456", result.richterNummer)
assertEquals("Richter", result.nachname)
assertEquals("Peter", result.vorname)
assertEquals("X", result.satzID)
assertEquals(10128, result.satzNummer)
assertEquals("Zitterbart Rainer", result.name)
assertEquals(listOf("PI-A"), result.qualifikationen)
}
@Test
fun `parseFunktionaer should extract RICHT01 correctly with more examples`() {
// X139552Mc Mullen Elizabeth DIOR
val line1 = "X139552Mc Mullen Elizabeth DIOR"
val result1 = ZnsLegacyParsers.parseFunktionaer(line1)
assertNotNull(result1)
assertEquals("X", result1.satzID)
assertEquals(139552, result1.satzNummer)
assertEquals("Mc Mullen Elizabeth", result1.name)
assertEquals(listOf("DIOR"), result1.qualifikationen)
// X014346Schubert Renate DM,DPF,GAR-SP,SPF,SS*
val line2 = "X014346Schubert Renate DM,DPF,GAR-SP,SPF,SS*"
val result2 = ZnsLegacyParsers.parseFunktionaer(line2)
assertNotNull(result2)
assertEquals(14346, result2.satzNummer)
assertEquals("Schubert Renate", result2.name)
assertEquals(listOf("DM", "DPF", "GAR-SP", "SPF", "SS*"), result2.qualifikationen)
// Y002211Salusek Andreas Christian P3,PL2
val line3 = "Y002211Salusek Andreas Christian P3,PL2"
val result3 = ZnsLegacyParsers.parseFunktionaer(line3)
assertNotNull(result3)
assertEquals("Y", result3.satzID)
assertEquals(2211, result3.satzNummer)
assertEquals("Salusek Andreas Christian", result3.name)
assertEquals(listOf("P3", "PL2"), result3.qualifikationen)
}
@Test
fun `parseFunktionaer should return null for invalid lines`() {
assertEquals(null, ZnsLegacyParsers.parseFunktionaer(""))
assertEquals(null, ZnsLegacyParsers.parseFunktionaer("Z123456Test"))
assertEquals(null, ZnsLegacyParsers.parseFunktionaer("XABCDEFTest"))
}
@Test
fun `parsePferd should extract real PFERDE01 correctly`() {
// Real example from PFERDE01.dat (line length approx 211 characters)
val line = "9D56Viola B 000000017S2005Brauner Tschech. WB 10952024Tanja Kuntner 535 Latinus 5637401268"
val result = ZnsLegacyParsers.parsePferd(line)
assertNotNull(result)
assertEquals("9D56", result.kopfnummer)
assertEquals("Viola B", result.pferdeName)
assertEquals("000000017", result.lebensnummer)
assertEquals(PferdeGeschlechtE.STUTE, result.geschlecht)
assertEquals(2005, result.geburtsjahr)
assertEquals("Brauner", result.farbe)
assertEquals("Tschech. WB", result.abstammung)
assertEquals(1095, result.vereinNummer)
assertEquals(2024, result.lastPayYear)
assertEquals("Tanja Kuntner", result.verantwortlichePersonId)
assertEquals("535 Latinus", result.vater)
assertEquals("5637401268", result.satznummer)
}
@Test
fun `parseLizenz should extract real LIZENZ01 correctly for Ebner Sarah`() {
// Real example from user:
// "100365Ebner Sarah 09Hubertus Voltigier Reit- und Fahrverein AUTR2S3 903801690699 18109450 2025W1990100310137032 R2S3 "
val line = "100365Ebner Sarah 09Hubertus Voltigier Reit- und Fahrverein AUTR2S3 903801690699 18109450 2025W1990100310137032 R2S3 "
val result = ZnsLegacyParsers.parseLizenz(line)
assertNotNull(result)
assertEquals("100365", result.satznummer)
assertEquals("Ebner", result.nachname)
assertEquals("Sarah", result.vorname)
assertEquals(9, result.bundeslandNummer)
assertEquals("Hubertus Voltigier Reit- und Fahrverein", result.vereinsName)
assertEquals("AUT", result.nation)
assertEquals("R2S3", result.reiterLizenz)
assertEquals(90380169, result.mitgliedsNummer)
assertEquals("0699 18109450", result.telefonNummer)
assertEquals(2025, result.lastPayYear)
assertEquals("W", result.geschlecht)
assertEquals("1990-10-03", result.geburtsdatum.toString())
assertEquals("10137032", result.feiId)
assertEquals("R2S3", result.lizenzInfo)
}
@Test
fun `parseLizenz should extract real LIZENZ01 correctly`() {
// Real example from LIZENZ01.dat (second line of file)
val sb = StringBuilder()
sb.append("000010") // 1-6
sb.append("Aichinger ") // 7-56
sb.append("Ewald ") // 57-81
sb.append("02") // 82-83
sb.append("Reitverein Geiger-Amstetten ") // 84-133
sb.append("AUT") // 134-136
sb.append("R2 ") // 137-140
sb.append(" ") // 141-146 (leer)
sb.append("20660700") // 147-154 (mitgliedsNummer)
sb.append("0676 4825910 ") // 155-176 (telefon)
sb.append("2023") // 177-180 (lastPayYear)
sb.append("M") // 181 (geschlecht)
sb.append("19571010") // 182-189 (geburtsdatum)
sb.append(" ") // 190-199 (feiId length 10)
sb.append(" ") // 200 (sperrliste)
sb.append(" ") // 201-210 (lizenzinfo)
val result = ZnsLegacyParsers.parseLizenz(sb.toString())
assertNotNull(result)
assertEquals("000010", result.satznummer)
assertEquals("Aichinger", result.nachname)
assertEquals("Ewald", result.vorname)
assertEquals(2, result.bundeslandNummer)
assertEquals("Reitverein Geiger-Amstetten", result.vereinsName)
assertEquals("AUT", result.nation)
assertEquals("R2", result.reiterLizenz)
assertEquals(20660700, result.mitgliedsNummer)
assertEquals("0676 4825910", result.telefonNummer)
assertEquals(2023, result.lastPayYear)
assertEquals("M", result.geschlecht)
assertEquals("1957-10-10", result.geburtsdatum.toString())
}
}