feat(zns-importer): add ZNSImportService with tests and REST controller
- Created `ZnsImportService` to handle uploading, parsing, and persisting ZNS data from legacy `.zip` files. - Introduced corresponding test cases in `ZnsImportServiceTest` for handling edge cases including imports and updates. - Added REST controller `ZnsImportController` for initiating import jobs and retrieving their status. - Defined `ZnsImportResult` data structure for reporting results of import operations. - Established database configuration specific to ZNS importer for development profile. - Updated utility libraries with `FixedWidthLineReader` for fixed-width string parsing. - Refactored architecture by placing parser logic in `core:zns-parser` for reuse across backend and Compose Desktop app. Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
plugins {
|
||||
alias(libs.plugins.kotlinMultiplatform)
|
||||
alias(libs.plugins.kotlinSerialization)
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvm()
|
||||
sourceSets {
|
||||
commonMain {
|
||||
dependencies {
|
||||
implementation(projects.core.coreDomain)
|
||||
implementation(projects.core.coreUtils)
|
||||
|
||||
// Domänen-Modelle für das Parsing
|
||||
implementation(projects.backend.services.clubs.clubsDomain)
|
||||
implementation(projects.backend.services.persons.personsDomain)
|
||||
implementation(projects.backend.services.horses.horsesDomain)
|
||||
implementation(projects.backend.services.officials.officialsDomain)
|
||||
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
implementation(libs.kotlinx.datetime)
|
||||
}
|
||||
}
|
||||
commonTest {
|
||||
dependencies {
|
||||
implementation(libs.kotlin.test)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
package at.mocode.zns.parser
|
||||
|
||||
import at.mocode.clubs.domain.model.DomVerein
|
||||
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 at.mocode.horses.domain.model.DomPferd
|
||||
import at.mocode.officials.domain.model.DomFunktionaer
|
||||
import at.mocode.persons.domain.model.DomReiter
|
||||
import kotlinx.datetime.LocalDate
|
||||
import kotlin.uuid.ExperimentalUuidApi
|
||||
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).
|
||||
*/
|
||||
@OptIn(ExperimentalUuidApi::class)
|
||||
object ZnsLegacyParsers {
|
||||
|
||||
/**
|
||||
* Parses a line from VEREIN01.DAT.
|
||||
*/
|
||||
fun parseVerein(line: String): DomVerein? {
|
||||
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 DomVerein(
|
||||
vereinsNummer = vereinsNummer,
|
||||
name = vereinsName,
|
||||
datenQuelle = DatenQuelleE.IMPORT_ZNS
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a line from LIZENZ01.DAT.
|
||||
*/
|
||||
fun parseLizenz(line: String): DomReiter? {
|
||||
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 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"
|
||||
|
||||
return DomReiter(
|
||||
personId = Uuid.random(),
|
||||
satznummer = satznummer,
|
||||
nachname = nachname,
|
||||
vorname = vorname,
|
||||
vereinsName = vereinsName.ifBlank { null },
|
||||
nation = nation.ifBlank { null },
|
||||
lizenzKlasse = lizenz,
|
||||
istAktiv = !gesperrt,
|
||||
datenQuelle = DatenQuelleE.IMPORT_ZNS
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a line from PFERDE01.DAT.
|
||||
*/
|
||||
fun parsePferd(line: String): DomPferd? {
|
||||
if (line.isBlank() || line.length < 202) 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 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) }
|
||||
|
||||
return DomPferd(
|
||||
pferdeName = name,
|
||||
geschlecht = geschlecht,
|
||||
geburtsdatum = geburtsdatum,
|
||||
lebensnummer = lebensnummer.ifBlank { null },
|
||||
datenQuelle = DatenQuelleE.IMPORT_ZNS
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a line from RICHT01.DAT.
|
||||
*/
|
||||
fun parseRichter(line: String): DomFunktionaer? {
|
||||
if (line.isBlank() || line.length < 8) return null
|
||||
|
||||
val reader = FixedWidthLineReader(line)
|
||||
|
||||
val satznummer = reader.getString(2, 6)
|
||||
if (satznummer.isBlank()) 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) ?: ""
|
||||
|
||||
return DomFunktionaer(
|
||||
richterNummer = satznummer,
|
||||
nachname = nachname,
|
||||
vorname = vorname,
|
||||
datenQuelle = DatenQuelleE.IMPORT_ZNS
|
||||
)
|
||||
}
|
||||
|
||||
private fun mapLizenz(lizenz: String): LizenzKlasseE {
|
||||
return when (lizenz.uppercase()) {
|
||||
"R1" -> LizenzKlasseE.R1
|
||||
"R2" -> LizenzKlasseE.R2
|
||||
"R3" -> LizenzKlasseE.R3
|
||||
"RD1" -> LizenzKlasseE.RD1
|
||||
"RD2" -> LizenzKlasseE.RD2
|
||||
"RD3" -> LizenzKlasseE.RD3
|
||||
"JN" -> LizenzKlasseE.JN
|
||||
"JG" -> LizenzKlasseE.JG
|
||||
"YR" -> LizenzKlasseE.YR
|
||||
else -> LizenzKlasseE.LIZENZFREI
|
||||
}
|
||||
}
|
||||
|
||||
private fun mapGeschlecht(geschlecht: String): PferdeGeschlechtE {
|
||||
return when (geschlecht.uppercase()) {
|
||||
"W" -> PferdeGeschlechtE.WALLACH
|
||||
"S" -> PferdeGeschlechtE.STUTE
|
||||
"H" -> PferdeGeschlechtE.HENGST
|
||||
else -> PferdeGeschlechtE.UNBEKANNT
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package at.mocode.zns.parser
|
||||
|
||||
import at.mocode.core.domain.model.LizenzKlasseE
|
||||
import at.mocode.core.domain.model.PferdeGeschlechtE
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotNull
|
||||
|
||||
class ZnsLegacyParsersTest {
|
||||
|
||||
@Test
|
||||
fun `parseVerein should extract VEREIN01 correctly`() {
|
||||
val line = "1234Reitverein Test "
|
||||
val result = ZnsLegacyParsers.parseVerein(line)
|
||||
|
||||
assertNotNull(result)
|
||||
assertEquals("1234", result.vereinsNummer)
|
||||
assertEquals("Reitverein Test", result.name)
|
||||
}
|
||||
|
||||
@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")
|
||||
|
||||
val result = ZnsLegacyParsers.parseLizenz(sb.toString())
|
||||
assertNotNull(result)
|
||||
assertEquals("123456", result.satznummer)
|
||||
assertEquals("Mustermann", result.nachname)
|
||||
assertEquals("Max", result.vorname)
|
||||
assertEquals("Reitverein Wien", result.vereinsName)
|
||||
assertEquals(LizenzKlasseE.R1, result.lizenzKlasse)
|
||||
assertEquals(false, result.istAktiv)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `parsePferd should extract PFERDE01 correctly`() {
|
||||
val sb = StringBuilder()
|
||||
sb.append("A123")
|
||||
sb.append("Black Beauty ")
|
||||
sb.append("123456789")
|
||||
sb.append("W")
|
||||
sb.append("2010")
|
||||
|
||||
while (sb.length < 201) {
|
||||
sb.append(" ")
|
||||
}
|
||||
sb.append("0000000001")
|
||||
|
||||
val result = ZnsLegacyParsers.parsePferd(sb.toString())
|
||||
assertNotNull(result)
|
||||
assertEquals("Black Beauty", result.pferdeName)
|
||||
assertEquals("123456789", result.lebensnummer)
|
||||
assertEquals(PferdeGeschlechtE.WALLACH, result.geschlecht)
|
||||
assertEquals(2010, result.geburtsdatum?.year)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `parseRichter should extract RICHT01 correctly`() {
|
||||
val line =
|
||||
"X123456Richter, Peter GA "
|
||||
val result = ZnsLegacyParsers.parseRichter(line)
|
||||
|
||||
assertNotNull(result)
|
||||
assertEquals("123456", result.richterNummer)
|
||||
assertEquals("Richter", result.nachname)
|
||||
assertEquals("Peter", result.vorname)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user