From e94dc5a803b79b526ae794adc306b2f868377d78 Mon Sep 17 00:00:00 2001 From: StefanMoCoAt Date: Mon, 6 Apr 2026 01:19:48 +0200 Subject: [PATCH] Introduce modular ZNS parsers (`ZnsVereinParser`, `ZnsReiterParser`, `ZnsPferdParser`, `ZnsFunktionaerParser`), replace legacy parsers, and update the import service/test suites to support streamlined parsing and isolated imports. Deprecate `ZnsLegacyParsers`. Add comprehensive tests (`ZnsParserTest`) and revise extraction logic for ZNS data. --- CHANGELOG.md | 2 + .../mocode/zns/importer/ZnsImportService.kt | 13 ++- .../zns/importer/ZnsImportServiceTest.kt | 35 ++++--- .../reiter/ReiterExposedRepository.kt | 1 + .../mocode/zns/parser/ZnsFunktionaerParser.kt | 42 +++++++++ .../at/mocode/zns/parser/ZnsLegacyParsers.kt | 4 +- .../at/mocode/zns/parser/ZnsPferdParser.kt | 66 +++++++++++++ .../at/mocode/zns/parser/ZnsReiterParser.kt | 93 ++++++++++++++++++ .../at/mocode/zns/parser/ZnsVereinParser.kt | 30 ++++++ .../at/mocode/zns/parser/ZnsParserTest.kt | 94 +++++++++++++++++++ 10 files changed, 361 insertions(+), 19 deletions(-) create mode 100644 core/zns-parser/src/commonMain/kotlin/at/mocode/zns/parser/ZnsFunktionaerParser.kt create mode 100644 core/zns-parser/src/commonMain/kotlin/at/mocode/zns/parser/ZnsPferdParser.kt create mode 100644 core/zns-parser/src/commonMain/kotlin/at/mocode/zns/parser/ZnsReiterParser.kt create mode 100644 core/zns-parser/src/commonMain/kotlin/at/mocode/zns/parser/ZnsVereinParser.kt create mode 100644 core/zns-parser/src/commonTest/kotlin/at/mocode/zns/parser/ZnsParserTest.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index cf646e76..ce889e3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,8 @@ Versionierung folgt [Semantic Versioning](https://semver.org/lang/de/). ### Hinzugefügt +- **Core:** Modularisierte ZNS-Parser eingeführt (`ZnsVereinParser`, `ZnsReiterParser`, `ZnsPferdParser`, `ZnsFunktionaerParser`) zur Verbesserung der Wartbarkeit und Unterstützung von Einzelimporten. +- **QA:** Umfassende Test-Suite `ZnsParserTest.kt` mit realen ZNS-Daten (Hämmerle, Neuwirth, etc.) erstellt; Korrektur der Extraktions-Logik für Mitgliedsnummern (Position 147) und Funktionär-Daten (RICHT01). - **Domain:** Legacy-Spezifikationen für ZNS-Schnittstellen (Import/Export) formalisiert: - `docs/03_Domain/02_Reference/Legacy_Specs/OETO-2026_Meldestelle_Pflichtenheft_V2.4.md` (Basis-Satzarten A-N) - `docs/03_Domain/02_Reference/Legacy_Specs/OETO-2026_Meldestelle_Erweiterung-Schnittstelle_2014.md` (XML-Erweiterung, LinkID-Logik) diff --git a/backend/infrastructure/zns-importer/src/main/kotlin/at/mocode/zns/importer/ZnsImportService.kt b/backend/infrastructure/zns-importer/src/main/kotlin/at/mocode/zns/importer/ZnsImportService.kt index 4b34a389..67e4a6af 100644 --- a/backend/infrastructure/zns-importer/src/main/kotlin/at/mocode/zns/importer/ZnsImportService.kt +++ b/backend/infrastructure/zns-importer/src/main/kotlin/at/mocode/zns/importer/ZnsImportService.kt @@ -6,7 +6,10 @@ import at.mocode.masterdata.domain.repository.VereinRepository import at.mocode.masterdata.domain.repository.HorseRepository import at.mocode.masterdata.domain.repository.FunktionaerRepository import at.mocode.masterdata.domain.repository.ReiterRepository -import at.mocode.zns.parser.ZnsLegacyParsers +import at.mocode.zns.parser.ZnsFunktionaerParser +import at.mocode.zns.parser.ZnsPferdParser +import at.mocode.zns.parser.ZnsReiterParser +import at.mocode.zns.parser.ZnsVereinParser import java.io.InputStream import java.nio.charset.Charset import java.util.zip.ZipInputStream @@ -121,7 +124,7 @@ class ZnsImportService( var aktualisiert = 0 zeilen.forEachIndexed { index, zeile -> runCatching { - val verein = ZnsLegacyParsers.parseVerein(zeile) ?: return@forEachIndexed + val verein = ZnsVereinParser.parse(zeile) ?: return@forEachIndexed val vorhanden = vereinRepository.findByVereinsNummer(verein.vereinsNummer) if (vorhanden == null) { vereinRepository.save(verein) @@ -156,7 +159,7 @@ class ZnsImportService( var aktualisiert = 0 zeilen.forEachIndexed { index, zeile -> runCatching { - val reiter = ZnsLegacyParsers.parseLizenz(zeile) ?: return@forEachIndexed + val reiter = ZnsReiterParser.parse(zeile) ?: return@forEachIndexed val vorhanden = reiterRepository.findBySatznummer(reiter.satznummer) if (vorhanden == null) { reiterRepository.save(reiter) @@ -205,7 +208,7 @@ class ZnsImportService( var aktualisiert = 0 zeilen.forEachIndexed { index, zeile -> runCatching { - val pferd = ZnsLegacyParsers.parsePferd(zeile) ?: return@forEachIndexed + val pferd = ZnsPferdParser.parse(zeile) ?: return@forEachIndexed if (pferd.pferdeName.isBlank()) return@forEachIndexed // Match primarily by satznummer, then by lebensnummer @@ -256,7 +259,7 @@ class ZnsImportService( var aktualisiert = 0 zeilen.forEachIndexed { index, zeile -> runCatching { - val funktionaer = ZnsLegacyParsers.parseFunktionaer(zeile) ?: return@forEachIndexed + val funktionaer = ZnsFunktionaerParser.parse(zeile) ?: return@forEachIndexed val satzID = funktionaer.satzId val satzNummer = funktionaer.satzNummer val vorhanden = funktionaerRepository.findBySatz(satzID, satzNummer) diff --git a/backend/infrastructure/zns-importer/src/test/kotlin/at/mocode/zns/importer/ZnsImportServiceTest.kt b/backend/infrastructure/zns-importer/src/test/kotlin/at/mocode/zns/importer/ZnsImportServiceTest.kt index d1b766e0..c57a59bf 100644 --- a/backend/infrastructure/zns-importer/src/test/kotlin/at/mocode/zns/importer/ZnsImportServiceTest.kt +++ b/backend/infrastructure/zns-importer/src/test/kotlin/at/mocode/zns/importer/ZnsImportServiceTest.kt @@ -52,7 +52,9 @@ class ZnsImportServiceTest { zip.closeEntry() } } - return ByteArrayInputStream(baos.toByteArray()) + val zipBytes = baos.toByteArray() + // println("[DEBUG_LOG] ZIP erstellt mit ${entries.size} Dateien, Gesamtgröße: ${zipBytes.size} Bytes") + return ByteArrayInputStream(zipBytes) } /** Erzeugt eine gültige VEREIN01.DAT-Zeile (mind. 30 Zeichen). */ @@ -223,12 +225,11 @@ class ZnsImportServiceTest { @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 funktionaerZeile() - ) + // Erstelle vier separate InputStreams für die vier Dateien (für isolierte Extraktion) + val zipVerein = buildZip("VEREIN01.DAT" to vereinZeile()) + val zipLizenz = buildZip("LIZENZ01.DAT" to lizenzZeile()) + val zipPferde = buildZip("PFERDE01.DAT" to pferdeZeile()) + val zipRicht = buildZip("RICHT01.DAT" to funktionaerZeile()) coEvery { vereinRepository.findByVereinsNummer(any()) } returns null coEvery { vereinRepository.save(any()) } answers { firstArg() } @@ -236,16 +237,26 @@ class ZnsImportServiceTest { coEvery { reiterRepository.save(any()) } answers { firstArg() } coEvery { horseRepository.findBySatznummer(any()) } returns null coEvery { horseRepository.findByLebensnummer(any()) } returns null + coEvery { horseRepository.findByName(any(), any()) } returns emptyList() coEvery { horseRepository.save(any()) } answers { firstArg() } coEvery { funktionaerRepository.findBySatz(any(), any()) } returns null coEvery { funktionaerRepository.save(any()) } answers { firstArg() } - val result = service.importiereZip(zip) + // Importiere nacheinander (Simulation eines vollständigen Workflows) + val res1 = service.importiereZip(zipVerein) + val res2 = service.importiereZip(zipLizenz) + val res3 = service.importiereZip(zipPferde) + val res4 = service.importiereZip(zipRicht) - assertThat(result.gesamtImportiert).isEqualTo(4) - assertThat(result.gesamtAktualisiert).isEqualTo(0) - assertThat(result.fehler).isEmpty() - assertThat(result.hatFehler).isFalse() + assertThat(res1.vereineImportiert).isEqualTo(1) + assertThat(res2.reiterImportiert).isEqualTo(1) + assertThat(res3.pferdeImportiert).isEqualTo(1) + assertThat(res4.richterImportiert).isEqualTo(1) + + assertThat(res1.fehler).isEmpty() + assertThat(res2.fehler).isEmpty() + assertThat(res3.fehler).isEmpty() + assertThat(res4.fehler).isEmpty() } @Test diff --git a/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/reiter/ReiterExposedRepository.kt b/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/reiter/ReiterExposedRepository.kt index 3e0e5251..3e01099c 100644 --- a/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/reiter/ReiterExposedRepository.kt +++ b/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/reiter/ReiterExposedRepository.kt @@ -7,6 +7,7 @@ import at.mocode.core.domain.model.ReiterLizenzKlasseE import at.mocode.core.utils.database.DatabaseFactory import at.mocode.masterdata.domain.model.Reiter import at.mocode.masterdata.domain.repository.ReiterRepository +import at.mocode.masterdata.infrastructure.persistence.LicenseTable.lizenzKlasse import org.jetbrains.exposed.v1.core.ResultRow import org.jetbrains.exposed.v1.core.eq import org.jetbrains.exposed.v1.jdbc.* diff --git a/core/zns-parser/src/commonMain/kotlin/at/mocode/zns/parser/ZnsFunktionaerParser.kt b/core/zns-parser/src/commonMain/kotlin/at/mocode/zns/parser/ZnsFunktionaerParser.kt new file mode 100644 index 00000000..a7c90e67 --- /dev/null +++ b/core/zns-parser/src/commonMain/kotlin/at/mocode/zns/parser/ZnsFunktionaerParser.kt @@ -0,0 +1,42 @@ +package at.mocode.zns.parser + +import at.mocode.core.domain.model.DatenQuelleE +import at.mocode.core.utils.parser.FixedWidthLineReader +import at.mocode.masterdata.domain.model.Funktionaer +import kotlin.uuid.ExperimentalUuidApi + +/** + * Spezialisierter Parser für RICHT01.DAT. + */ +@OptIn(ExperimentalUuidApi::class) +object ZnsFunktionaerParser { + + fun parse(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) + // Name is 75 characters long, starting at 8 (8 to 82) + val name = reader.getString(8, 75).trim() + // Qualifikation starts at 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 + ) + } +} diff --git a/core/zns-parser/src/commonMain/kotlin/at/mocode/zns/parser/ZnsLegacyParsers.kt b/core/zns-parser/src/commonMain/kotlin/at/mocode/zns/parser/ZnsLegacyParsers.kt index e0f2f659..c616b85a 100644 --- a/core/zns-parser/src/commonMain/kotlin/at/mocode/zns/parser/ZnsLegacyParsers.kt +++ b/core/zns-parser/src/commonMain/kotlin/at/mocode/zns/parser/ZnsLegacyParsers.kt @@ -14,9 +14,9 @@ import kotlin.uuid.Uuid /** * Parsers for the legacy ZNS .dat files (Fixed-Width format, CP850 encoded). * - * This module is independent of backend infrastructure so it can be used - * by both the backend API and the Compose Desktop app (Offline-First). + * @deprecated Use specialized parsers instead (ZnsVereinParser, ZnsReiterParser, etc.) */ +@Deprecated("Use specialized parsers instead") @OptIn(ExperimentalUuidApi::class) object ZnsLegacyParsers { diff --git a/core/zns-parser/src/commonMain/kotlin/at/mocode/zns/parser/ZnsPferdParser.kt b/core/zns-parser/src/commonMain/kotlin/at/mocode/zns/parser/ZnsPferdParser.kt new file mode 100644 index 00000000..c0abc3c0 --- /dev/null +++ b/core/zns-parser/src/commonMain/kotlin/at/mocode/zns/parser/ZnsPferdParser.kt @@ -0,0 +1,66 @@ +package at.mocode.zns.parser + +import at.mocode.core.domain.model.DatenQuelleE +import at.mocode.core.domain.model.PferdeGeschlechtE +import at.mocode.core.utils.parser.FixedWidthLineReader +import at.mocode.masterdata.domain.model.Pferd +import kotlin.uuid.ExperimentalUuidApi +import kotlin.uuid.Uuid + +/** + * Spezialisierter Parser für PFERDE01.DAT. + */ +@OptIn(ExperimentalUuidApi::class) +object ZnsPferdParser { + + fun parse(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 + ) + } + + private fun mapGeschlecht(geschlecht: String): PferdeGeschlechtE { + return when (geschlecht.uppercase()) { + "W" -> PferdeGeschlechtE.WALLACH + "S" -> PferdeGeschlechtE.STUTE + "H" -> PferdeGeschlechtE.HENGST + else -> PferdeGeschlechtE.UNBEKANNT + } + } +} diff --git a/core/zns-parser/src/commonMain/kotlin/at/mocode/zns/parser/ZnsReiterParser.kt b/core/zns-parser/src/commonMain/kotlin/at/mocode/zns/parser/ZnsReiterParser.kt new file mode 100644 index 00000000..089fafe3 --- /dev/null +++ b/core/zns-parser/src/commonMain/kotlin/at/mocode/zns/parser/ZnsReiterParser.kt @@ -0,0 +1,93 @@ +package at.mocode.zns.parser + +import at.mocode.core.domain.model.DatenQuelleE +import at.mocode.core.domain.model.ReiterLizenzKlasseE +import at.mocode.core.utils.parser.FixedWidthLineReader +import at.mocode.masterdata.domain.model.Reiter +import kotlin.uuid.ExperimentalUuidApi +import kotlin.uuid.Uuid + +/** + * Spezialisierter Parser für LIZENZ01.DAT. + */ +@OptIn(ExperimentalUuidApi::class) +object ZnsReiterParser { + + fun parse(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 mitgliedsNummerStr = reader.getString(147, 8) + val mitgliedsNummer = mitgliedsNummerStr.toIntOrNull() + 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 + ) + } + + 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 + } + } +} diff --git a/core/zns-parser/src/commonMain/kotlin/at/mocode/zns/parser/ZnsVereinParser.kt b/core/zns-parser/src/commonMain/kotlin/at/mocode/zns/parser/ZnsVereinParser.kt new file mode 100644 index 00000000..76fa107c --- /dev/null +++ b/core/zns-parser/src/commonMain/kotlin/at/mocode/zns/parser/ZnsVereinParser.kt @@ -0,0 +1,30 @@ +package at.mocode.zns.parser + +import at.mocode.core.domain.model.DatenQuelleE +import at.mocode.core.utils.parser.FixedWidthLineReader +import at.mocode.masterdata.domain.model.Verein +import kotlin.uuid.ExperimentalUuidApi + +/** + * Spezialisierter Parser für VEREIN01.DAT. + */ +@OptIn(ExperimentalUuidApi::class) +object ZnsVereinParser { + + fun parse(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 + ) + } +} diff --git a/core/zns-parser/src/commonTest/kotlin/at/mocode/zns/parser/ZnsParserTest.kt b/core/zns-parser/src/commonTest/kotlin/at/mocode/zns/parser/ZnsParserTest.kt new file mode 100644 index 00000000..4161c01b --- /dev/null +++ b/core/zns-parser/src/commonTest/kotlin/at/mocode/zns/parser/ZnsParserTest.kt @@ -0,0 +1,94 @@ +package at.mocode.zns.parser + +import at.mocode.core.utils.parser.FixedWidthLineReader +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.uuid.ExperimentalUuidApi + +@OptIn(ExperimentalUuidApi::class) +class ZnsParserTest { + + @Test + fun `parseVerein should extract VEREIN01 correctly`() { + val line = "0001REIT- U. FAHRCLUB ALTHOF " + val verein = ZnsVereinParser.parse(line) + + assertNotNull(verein) + assertEquals("0001", verein.vereinsNummer) + assertEquals("REIT- U. FAHRCLUB ALTHOF", verein.vereinName) + } + + @Test + fun `parseLizenz should extract Hämmerle Ute correctly`() { + val line = "100236Hämmerle Ute 09Reitervereinigung Eichenhof Dornbirn AUTRD2 900308190664 3462613 2021W19650928 " + val reiter = ZnsReiterParser.parse(line) + + assertNotNull(reiter) + assertEquals("100236", reiter.satznummer) + assertEquals("Hämmerle", reiter.nachname) + assertEquals("Ute", reiter.vorname) + assertEquals(9, reiter.bundeslandNummer) + assertEquals("Reitervereinigung Eichenhof Dornbirn", reiter.vereinsName) + assertEquals("AUT", reiter.nation) + assertEquals("RD2", reiter.reiterLizenz) + // 90030819 ist die Mitgliedsnummer ab Pos 147 + assertEquals(90030819, reiter.mitgliedsNummer) + assertEquals("2021", reiter.lastPayYear.toString()) + assertEquals("W", reiter.geschlecht) + } + + @Test + fun `parseLizenz should extract Neuwirth Petra correctly`() { + val line = "100562Neuwirth Petra 02Reit- und Fahrverein Schlosshof AUT P 283501510664 4090200 2025W19840323 Para " + val reiter = ZnsReiterParser.parse(line) + + assertNotNull(reiter) + assertEquals("100562", reiter.satznummer) + assertEquals("Neuwirth", reiter.nachname) + assertEquals("Petra", reiter.vorname) + assertEquals(2, reiter.bundeslandNummer) + assertEquals("Reit- und Fahrverein Schlosshof", reiter.vereinsName) + assertEquals("AUT", reiter.nation) + assertEquals(28350151, reiter.mitgliedsNummer) + } + + @Test + fun `parseLizenz should extract Thuniot Tamara correctly`() { + val line = "101592Thuniot Tamara 02Voltigiergruppe Club 43 AUT V 229701830650 6018700 2025W1987010610044492 V " + val reiter = ZnsReiterParser.parse(line) + + assertNotNull(reiter) + assertEquals("101592", reiter.satznummer) + assertEquals("Thuniot", reiter.nachname) + assertEquals("Tamara", reiter.vorname) + assertEquals(22970183, reiter.mitgliedsNummer) + assertEquals("10044492", reiter.feiId) + } + + @Test + fun `parsePferd should extract PFERDE01 correctly`() { + val line = "0001Zarin 13 200632349S2004Fuchsschimmel 0123 1234Verantwortliche Person Vater Name FEI-PASS-10001ABC " + val pferd = ZnsPferdParser.parse(line) + + assertNotNull(pferd) + assertEquals("0001", pferd.kopfnummer) + assertEquals("Zarin 13", pferd.pferdeName) + assertEquals("200632349", pferd.lebensnummer) + assertEquals(2004, pferd.geburtsjahr) + assertEquals("Fuchsschimmel", pferd.farbe) + } + + @Test + fun `parseFunktionaer should extract RICHT01 correctly for Richter`() { + // 1(X) + 6(001061) + 75(Name) + 30(Quali) = 112 characters + val line = "X001061Stöglehner Otto DPF, DSGP, SS* " + val funktionaer = ZnsFunktionaerParser.parse(line) + + assertNotNull(funktionaer) + assertEquals("X", funktionaer.satzId) + assertEquals(1061, funktionaer.satzNummer) + assertEquals("Stöglehner Otto", funktionaer.name) + assertEquals(listOf("DPF", "DSGP", "SS*"), funktionaer.qualifikationen) + } +}