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
@@ -10,7 +10,8 @@ import org.springframework.context.annotation.Configuration
@Configuration
class GatewayConfig(
@Value("\${ping.service.url:http://localhost:8082}") private val pingServiceUrl: String
@Value("\${ping.service.url:http://localhost:8082}") private val pingServiceUrl: String,
@Value("\${zns.import.service.url:http://localhost:8095}") private val znsImportServiceUrl: String
) {
@Bean
@@ -27,6 +28,10 @@ class GatewayConfig(
}
uri(pingServiceUrl)
}
route(id = "zns-import-service") {
path("/api/v1/import/zns/**", "/api/v1/import/zns")
uri(znsImportServiceUrl)
}
}
}
}
@@ -38,7 +38,8 @@ class SecurityConfig(
.authorizeExchange { exchanges ->
exchanges
.pathMatchers(*securityProperties.publicPaths.toTypedArray()).permitAll()
.pathMatchers("/api/ping/**").hasRole("MELD_USER") // Beispiel Rolle
.pathMatchers("/api/ping/**").permitAll() // TEMPORAER fuer Debugging
.pathMatchers("/api/v1/import/zns", "/api/v1/import/zns/**").permitAll() // TEMPORAER fuer Debugging
.anyExchange().authenticated()
}
.oauth2ResourceServer { oauth2 ->
@@ -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)
@@ -24,20 +24,12 @@ import kotlin.uuid.Uuid
*/
class FunktionaerController(private val funktionaerRepository: FunktionaerRepository) {
@Serializable
data class FunktionaerDto(
val funktionaerId: String,
val richterNummer: String? = null,
val vorname: String,
val nachname: String,
@Serializable(with = LocalDateSerializer::class)
val geburtsdatum: LocalDate? = null,
val rollen: List<String>,
val richterQualifikation: String? = null,
val qualifiziertFuerSparten: List<String>,
val email: String? = null,
val telefon: String? = null,
val vereinsNummer: String? = null,
val satzID: String,
val satzNummer: Int,
val name: String? = null,
val qualifikationen: List<String> = emptyList(),
val istAktiv: Boolean,
val bemerkungen: String? = null,
@Serializable(with = InstantSerializer::class)
@@ -46,33 +38,18 @@ class FunktionaerController(private val funktionaerRepository: FunktionaerReposi
@Serializable
data class FunktionaerCreateRequest(
val richterNummer: String? = null,
val vorname: String,
val nachname: String,
@Serializable(with = LocalDateSerializer::class)
val geburtsdatum: LocalDate? = null,
val rollen: List<String> = emptyList(),
val richterQualifikation: String? = null,
val qualifiziertFuerSparten: List<String> = emptyList(),
val email: String? = null,
val telefon: String? = null,
val vereinsNummer: String? = null,
val satzID: String,
val satzNummer: Int,
val name: String? = null,
val qualifikationen: List<String> = emptyList(),
val istAktiv: Boolean = true,
val bemerkungen: String? = null
)
@Serializable
data class FunktionaerUpdateRequest(
val vorname: String? = null,
val nachname: String? = null,
@Serializable(with = LocalDateSerializer::class)
val geburtsdatum: LocalDate? = null,
val rollen: List<String>? = null,
val richterQualifikation: String? = null,
val qualifiziertFuerSparten: List<String>? = null,
val email: String? = null,
val telefon: String? = null,
val vereinsNummer: String? = null,
val name: String? = null,
val qualifikationen: List<String>? = null,
val istAktiv: Boolean? = null,
val bemerkungen: String? = null
)
@@ -81,29 +58,22 @@ class FunktionaerController(private val funktionaerRepository: FunktionaerReposi
route("/funktionaer") {
/**
* GET /funktionaer — Alle Funktionäre (paginiert), optional gefiltert nach rolle.
* GET /funktionaer — Alle Funktionäre (paginiert).
*/
get {
val rolleParam = call.request.queryParameters["rolle"]
val limit = call.request.queryParameters["limit"]?.toIntOrNull() ?: 100
val offset = call.request.queryParameters["offset"]?.toIntOrNull() ?: 0
val results = if (rolleParam != null) {
val rolle = runCatching { FunktionaerRolleE.valueOf(rolleParam) }.getOrNull()
?: return@get call.respond(HttpStatusCode.BadRequest, "Ungültige Rolle: $rolleParam")
funktionaerRepository.findByRolle(rolle)
} else {
funktionaerRepository.findAll(limit, offset)
}
val results = funktionaerRepository.findAll(limit, offset)
call.respond(results.map { it.toDto() })
}
/**
* GET /funktionaer/search?q=... — Sucht Funktionäre nach Name.
* GET /funktionaer/search?q=... — Sucht Funktionäre nach SatzNummer.
*/
get("/search") {
val query = call.request.queryParameters["q"] ?: ""
val results = funktionaerRepository.findByName(query)
val query = call.request.queryParameters["q"]?.toIntOrNull() ?: 0
val results = funktionaerRepository.findAll(100, 0).filter { it.satzNummer == query }
call.respond(results.map { it.toDto() })
}
@@ -117,11 +87,12 @@ class FunktionaerController(private val funktionaerRepository: FunktionaerReposi
}
/**
* GET /funktionaer/richternummer/{nr} — Sucht einen Funktionär nach seiner Richternummer.
* GET /funktionaer/satz/{satzID}/{satzNummer} — Sucht einen Funktionär nach Satz-ID und Nummer.
*/
get("/richternummer/{nr}") {
val nr = call.parameters["nr"] ?: return@get call.respond(HttpStatusCode.BadRequest)
val funktionaer = funktionaerRepository.findByRichterNummer(nr)
get("/satz/{satzID}/{satzNummer}") {
val satzID = call.parameters["satzID"] ?: return@get call.respond(HttpStatusCode.BadRequest)
val satzNummer = call.parameters["satzNummer"]?.toIntOrNull() ?: return@get call.respond(HttpStatusCode.BadRequest)
val funktionaer = funktionaerRepository.findBySatz(satzID, satzNummer)
if (funktionaer != null) call.respond(funktionaer.toDto()) else call.respond(HttpStatusCode.NotFound)
}
@@ -130,29 +101,11 @@ class FunktionaerController(private val funktionaerRepository: FunktionaerReposi
*/
post {
val req = call.receive<FunktionaerCreateRequest>()
val rollen = req.rollen.mapNotNull { runCatching { FunktionaerRolleE.valueOf(it) }.getOrNull() }.toSet()
if (rollen.size != req.rollen.size) {
return@post call.respond(HttpStatusCode.BadRequest, "Ungültige Rolle(n) in: ${req.rollen}")
}
val richterQualifikation = req.richterQualifikation?.let {
runCatching { RichterQualifikationE.valueOf(it) }.getOrNull()
?: return@post call.respond(HttpStatusCode.BadRequest, "Ungültige richterQualifikation: $it")
}
val sparten = req.qualifiziertFuerSparten.mapNotNull { runCatching { SparteE.valueOf(it) }.getOrNull() }.toSet()
if (sparten.size != req.qualifiziertFuerSparten.size) {
return@post call.respond(HttpStatusCode.BadRequest, "Ungültige Sparte(n) in: ${req.qualifiziertFuerSparten}")
}
val domFunktionaer = DomFunktionaer(
richterNummer = req.richterNummer,
vorname = req.vorname,
nachname = req.nachname,
geburtsdatum = req.geburtsdatum,
rollen = rollen,
richterQualifikation = richterQualifikation,
qualifiziertFuerSparten = sparten,
email = req.email,
telefon = req.telefon,
vereinsNummer = req.vereinsNummer,
satzID = req.satzID,
satzNummer = req.satzNummer,
name = req.name,
qualifikationen = req.qualifikationen,
istAktiv = req.istAktiv,
bemerkungen = req.bemerkungen
)
@@ -168,37 +121,9 @@ class FunktionaerController(private val funktionaerRepository: FunktionaerReposi
val existing = funktionaerRepository.findById(id) ?: return@put call.respond(HttpStatusCode.NotFound)
val req = call.receive<FunktionaerUpdateRequest>()
val rollen = req.rollen?.let { rollenList ->
val parsed = rollenList.mapNotNull { runCatching { FunktionaerRolleE.valueOf(it) }.getOrNull() }.toSet()
if (parsed.size != rollenList.size) {
return@put call.respond(HttpStatusCode.BadRequest, "Ungültige Rolle(n) in: $rollenList")
}
parsed
} ?: existing.rollen
val richterQualifikation = req.richterQualifikation?.let {
runCatching { RichterQualifikationE.valueOf(it) }.getOrNull()
?: return@put call.respond(HttpStatusCode.BadRequest, "Ungültige richterQualifikation: $it")
} ?: existing.richterQualifikation
val sparten = req.qualifiziertFuerSparten?.let { spartenList ->
val parsed = spartenList.mapNotNull { runCatching { SparteE.valueOf(it) }.getOrNull() }.toSet()
if (parsed.size != spartenList.size) {
return@put call.respond(HttpStatusCode.BadRequest, "Ungültige Sparte(n) in: $spartenList")
}
parsed
} ?: existing.qualifiziertFuerSparten
val updated = existing.copy(
vorname = req.vorname ?: existing.vorname,
nachname = req.nachname ?: existing.nachname,
geburtsdatum = req.geburtsdatum ?: existing.geburtsdatum,
rollen = rollen,
richterQualifikation = richterQualifikation,
qualifiziertFuerSparten = sparten,
email = req.email ?: existing.email,
telefon = req.telefon ?: existing.telefon,
vereinsNummer = req.vereinsNummer ?: existing.vereinsNummer,
name = req.name ?: existing.name,
qualifikationen = req.qualifikationen ?: existing.qualifikationen,
istAktiv = req.istAktiv ?: existing.istAktiv,
bemerkungen = req.bemerkungen ?: existing.bemerkungen
)
@@ -221,16 +146,10 @@ class FunktionaerController(private val funktionaerRepository: FunktionaerReposi
private fun DomFunktionaer.toDto() = FunktionaerDto(
funktionaerId = funktionaerId.toString(),
richterNummer = richterNummer,
vorname = vorname,
nachname = nachname,
geburtsdatum = geburtsdatum,
rollen = rollen.map { it.name },
richterQualifikation = richterQualifikation?.name,
qualifiziertFuerSparten = qualifiziertFuerSparten.map { it.name },
email = email,
telefon = telefon,
vereinsNummer = vereinsNummer,
satzID = satzID,
satzNummer = satzNummer,
name = name,
qualifikationen = qualifikationen,
istAktiv = istAktiv,
bemerkungen = bemerkungen,
updatedAt = updatedAt
@@ -25,101 +25,66 @@ class HorseController(private val horseRepository: HorseRepository) {
@Serializable
data class HorseDto(
val pferdId: String,
val kopfnummer: String? = null,
val pferdeName: String,
val geschlecht: String,
@Serializable(with = LocalDateSerializer::class)
val geburtsdatum: LocalDate? = null,
val rasse: String? = null,
val farbe: String? = null,
val lebensnummer: String? = null,
val chipNummer: String? = null,
val passNummer: String? = null,
val oepsNummer: String? = null,
val feiNummer: String? = null,
val besitzerId: String? = null,
val vaterName: String? = null,
val mutterName: String? = null,
val stockmass: Int? = null,
val geschlecht: String,
val geburtsjahr: Int? = null,
val farbe: String? = null,
val satznummer: String? = null,
val istAktiv: Boolean,
val bemerkungen: String? = null,
@Serializable(with = InstantSerializer::class)
val updatedAt: Instant
)
@Serializable
data class HorseCreateRequest(
val kopfnummer: String? = null,
val pferdeName: String,
val geschlecht: String,
@Serializable(with = LocalDateSerializer::class)
val geburtsdatum: LocalDate? = null,
val rasse: String? = null,
val farbe: String? = null,
val lebensnummer: String? = null,
val chipNummer: String? = null,
val passNummer: String? = null,
val oepsNummer: String? = null,
val feiNummer: String? = null,
val besitzerId: String? = null,
val vaterName: String? = null,
val mutterName: String? = null,
val stockmass: Int? = null,
val istAktiv: Boolean = true,
val bemerkungen: String? = null
val geschlecht: String,
val geburtsjahr: Int? = null,
val farbe: String? = null,
val satznummer: String? = null,
val istAktiv: Boolean = true
)
@Serializable
data class HorseUpdateRequest(
val kopfnummer: String? = null,
val pferdeName: String? = null,
val geschlecht: String? = null,
@Serializable(with = LocalDateSerializer::class)
val geburtsdatum: LocalDate? = null,
val rasse: String? = null,
val farbe: String? = null,
val lebensnummer: String? = null,
val chipNummer: String? = null,
val passNummer: String? = null,
val oepsNummer: String? = null,
val feiNummer: String? = null,
val besitzerId: String? = null,
val vaterName: String? = null,
val mutterName: String? = null,
val stockmass: Int? = null,
val istAktiv: Boolean? = null,
val bemerkungen: String? = null
val geschlecht: String? = null,
val geburtsjahr: Int? = null,
val farbe: String? = null,
val istAktiv: Boolean? = null
)
fun Route.registerRoutes() {
route("/horse") {
/**
* GET /horse — Alle Pferde (paginiert), optional gefiltert nach jahrgang oder besitzerId.
* GET /horse — Alle Pferde (paginiert), optional gefiltert nach jahrgang.
*/
get {
val jahrgang = call.request.queryParameters["jahrgang"]?.toIntOrNull()
val besitzerId = call.request.queryParameters["besitzerId"]
val limit = call.request.queryParameters["limit"]?.toIntOrNull() ?: 100
val offset = call.request.queryParameters["offset"]?.toIntOrNull() ?: 0
val results = when {
jahrgang != null -> horseRepository.findByBirthYear(jahrgang)
besitzerId != null -> {
val ownerId = runCatching { Uuid.parse(besitzerId) }.getOrNull()
?: return@get call.respond(HttpStatusCode.BadRequest, "Ungültige besitzerId")
horseRepository.findByOwnerId(ownerId)
}
else -> horseRepository.findAllActive(limit)
}
call.respond(results.map { it.toDto() })
}
/**
* GET /horse/search?q=... — Sucht Pferde nach Name.
* GET /horse/search?q=... — Sucht Pferde nach Lebensnummer.
*/
get("/search") {
val query = call.request.queryParameters["q"] ?: ""
val results = horseRepository.findByName(query)
call.respond(results.map { it.toDto() })
val result = horseRepository.findByLebensnummer(query)
if (result != null) call.respond(listOf(result.toDto())) else call.respond(emptyList<HorseDto>())
}
/**
@@ -147,27 +112,15 @@ class HorseController(private val horseRepository: HorseRepository) {
val req = call.receive<HorseCreateRequest>()
val geschlecht = runCatching { PferdeGeschlechtE.valueOf(req.geschlecht) }.getOrNull()
?: return@post call.respond(HttpStatusCode.BadRequest, "Ungültiges Geschlecht: ${req.geschlecht}")
val besitzerId = req.besitzerId?.let {
runCatching { Uuid.parse(it) }.getOrNull()
?: return@post call.respond(HttpStatusCode.BadRequest, "Ungültige besitzerId")
}
val domPferd = DomPferd(
kopfnummer = req.kopfnummer,
pferdeName = req.pferdeName,
geschlecht = geschlecht,
geburtsdatum = req.geburtsdatum,
rasse = req.rasse,
farbe = req.farbe,
lebensnummer = req.lebensnummer,
chipNummer = req.chipNummer,
passNummer = req.passNummer,
oepsNummer = req.oepsNummer,
feiNummer = req.feiNummer,
besitzerId = besitzerId,
vaterName = req.vaterName,
mutterName = req.mutterName,
stockmass = req.stockmass,
istAktiv = req.istAktiv,
bemerkungen = req.bemerkungen
geschlecht = geschlecht,
geburtsjahr = req.geburtsjahr,
farbe = req.farbe,
satznummer = req.satznummer,
istAktiv = req.istAktiv
)
val saved = horseRepository.save(domPferd)
call.respond(HttpStatusCode.Created, saved.toDto())
@@ -184,27 +137,14 @@ class HorseController(private val horseRepository: HorseRepository) {
runCatching { PferdeGeschlechtE.valueOf(it) }.getOrNull()
?: return@put call.respond(HttpStatusCode.BadRequest, "Ungültiges Geschlecht: $it")
} ?: existing.geschlecht
val besitzerId = req.besitzerId?.let {
runCatching { Uuid.parse(it) }.getOrNull()
?: return@put call.respond(HttpStatusCode.BadRequest, "Ungültige besitzerId")
} ?: existing.besitzerId
val updated = existing.copy(
kopfnummer = req.kopfnummer ?: existing.kopfnummer,
pferdeName = req.pferdeName ?: existing.pferdeName,
geschlecht = geschlecht,
geburtsdatum = req.geburtsdatum ?: existing.geburtsdatum,
rasse = req.rasse ?: existing.rasse,
farbe = req.farbe ?: existing.farbe,
lebensnummer = req.lebensnummer ?: existing.lebensnummer,
chipNummer = req.chipNummer ?: existing.chipNummer,
passNummer = req.passNummer ?: existing.passNummer,
oepsNummer = req.oepsNummer ?: existing.oepsNummer,
feiNummer = req.feiNummer ?: existing.feiNummer,
besitzerId = besitzerId,
vaterName = req.vaterName ?: existing.vaterName,
mutterName = req.mutterName ?: existing.mutterName,
stockmass = req.stockmass ?: existing.stockmass,
istAktiv = req.istAktiv ?: existing.istAktiv,
bemerkungen = req.bemerkungen ?: existing.bemerkungen
geschlecht = geschlecht,
geburtsjahr = req.geburtsjahr ?: existing.geburtsjahr,
farbe = req.farbe ?: existing.farbe,
istAktiv = req.istAktiv ?: existing.istAktiv
)
val saved = horseRepository.save(updated)
call.respond(saved.toDto())
@@ -225,22 +165,14 @@ class HorseController(private val horseRepository: HorseRepository) {
private fun DomPferd.toDto() = HorseDto(
pferdId = pferdId.toString(),
kopfnummer = kopfnummer,
pferdeName = pferdeName,
geschlecht = geschlecht.name,
geburtsdatum = geburtsdatum,
rasse = rasse,
farbe = farbe,
lebensnummer = lebensnummer,
chipNummer = chipNummer,
passNummer = passNummer,
oepsNummer = oepsNummer,
feiNummer = feiNummer,
besitzerId = besitzerId?.toString(),
vaterName = vaterName,
mutterName = mutterName,
stockmass = stockmass,
geschlecht = geschlecht.name,
geburtsjahr = geburtsjahr,
farbe = farbe,
satznummer = satznummer,
istAktiv = istAktiv,
bemerkungen = bemerkungen,
updatedAt = updatedAt
)
}
@@ -22,22 +22,24 @@ import kotlin.uuid.Uuid
*/
class ReiterController(private val reiterRepository: ReiterRepository) {
@Serializable
data class ReiterDto(
val reiterId: String,
val satznummer: String,
val satznummer: String?,
val nachname: String,
val vorname: String,
@Serializable(with = LocalDateSerializer::class)
val geburtsdatum: LocalDate? = null,
val lizenzNummer: String? = null,
val lizenzKlasse: String,
val startkartAktiv: Boolean,
val nation: String? = null,
val vereinsNummer: String? = null,
val bundeslandNummer: Int? = null,
val vereinsName: String? = null,
val nation: String? = null,
val reiterLizenz: String? = null,
val startkarte: String? = null,
val fahrLizenz: String? = null,
val mitgliedsNummer: Int? = null,
val telefonNummer: String? = null,
val lastPayYear: Int? = null,
val feiId: String? = null,
val istGastreiter: Boolean,
val lizenzKlasse: String,
val istAktiv: Boolean,
@Serializable(with = InstantSerializer::class)
val updatedAt: Instant
@@ -50,14 +52,17 @@ class ReiterController(private val reiterRepository: ReiterRepository) {
val vorname: String,
@Serializable(with = LocalDateSerializer::class)
val geburtsdatum: LocalDate? = null,
val lizenzNummer: String? = null,
val lizenzKlasse: String = "LIZENZFREI",
val startkartAktiv: Boolean = false,
val nation: String? = null,
val vereinsNummer: String? = null,
val bundeslandNummer: Int? = null,
val vereinsName: String? = null,
val nation: String? = null,
val reiterLizenz: String? = null,
val startkarte: String? = null,
val fahrLizenz: String? = null,
val mitgliedsNummer: Int? = null,
val telefonNummer: String? = null,
val lastPayYear: Int? = null,
val feiId: String? = null,
val istGastreiter: Boolean = false,
val lizenzKlasse: String = "LIZENZFREI",
val istAktiv: Boolean = true
)
@@ -67,14 +72,17 @@ class ReiterController(private val reiterRepository: ReiterRepository) {
val vorname: String? = null,
@Serializable(with = LocalDateSerializer::class)
val geburtsdatum: LocalDate? = null,
val lizenzNummer: String? = null,
val lizenzKlasse: String? = null,
val startkartAktiv: Boolean? = null,
val nation: String? = null,
val vereinsNummer: String? = null,
val bundeslandNummer: Int? = null,
val vereinsName: String? = null,
val nation: String? = null,
val reiterLizenz: String? = null,
val startkarte: String? = null,
val fahrLizenz: String? = null,
val mitgliedsNummer: Int? = null,
val telefonNummer: String? = null,
val lastPayYear: Int? = null,
val feiId: String? = null,
val istGastreiter: Boolean? = null,
val lizenzKlasse: String? = null,
val istAktiv: Boolean? = null
)
@@ -82,34 +90,23 @@ class ReiterController(private val reiterRepository: ReiterRepository) {
route("/reiter") {
/**
* GET /reiter — Alle Reiter (paginiert), optional gefiltert nach lizenzKlasse oder vereinId.
* GET /reiter — Alle Reiter (paginiert).
*/
get {
val lizenzKlasse = call.request.queryParameters["lizenzKlasse"]
val vereinId = call.request.queryParameters["vereinId"]
val limit = call.request.queryParameters["limit"]?.toIntOrNull() ?: 100
val offset = call.request.queryParameters["offset"]?.toIntOrNull() ?: 0
val results = when {
lizenzKlasse != null -> {
val klasse = runCatching { LizenzKlasseE.valueOf(lizenzKlasse) }.getOrNull()
?: return@get call.respond(HttpStatusCode.BadRequest, "Ungültige lizenzKlasse: $lizenzKlasse")
reiterRepository.findByLizenzKlasse(klasse)
}
vereinId != null -> reiterRepository.findByVereinsNummer(vereinId)
else -> reiterRepository.findAll(limit, offset)
}
val results = reiterRepository.findAll(limit, offset)
call.respond(results.map { it.toDto() })
}
/**
* GET /reiter/search?q=... — Sucht Reiter nach Name oder Satznummer.
* GET /reiter/search?q=... — Sucht Reiter nach Satznummer.
*/
get("/search") {
val query = call.request.queryParameters["q"] ?: ""
val results = reiterRepository.findByName(query)
call.respond(results.map { it.toDto() })
val result = reiterRepository.findBySatznummer(query)
if (result != null) call.respond(listOf(result.toDto())) else call.respond(emptyList<ReiterDto>())
}
/**
@@ -143,14 +140,17 @@ class ReiterController(private val reiterRepository: ReiterRepository) {
nachname = req.nachname,
vorname = req.vorname,
geburtsdatum = req.geburtsdatum,
lizenzNummer = req.lizenzNummer,
lizenzKlasse = lizenzKlasse,
startkartAktiv = req.startkartAktiv,
nation = req.nation,
vereinsNummer = req.vereinsNummer,
bundeslandNummer = req.bundeslandNummer,
vereinsName = req.vereinsName,
nation = req.nation,
reiterLizenz = req.reiterLizenz,
startkarte = req.startkarte,
fahrLizenz = req.fahrLizenz,
mitgliedsNummer = req.mitgliedsNummer,
telefonNummer = req.telefonNummer,
lastPayYear = req.lastPayYear,
feiId = req.feiId,
istGastreiter = req.istGastreiter,
lizenzKlasse = lizenzKlasse,
istAktiv = req.istAktiv
)
val saved = reiterRepository.save(domReiter)
@@ -172,14 +172,17 @@ class ReiterController(private val reiterRepository: ReiterRepository) {
nachname = req.nachname ?: existing.nachname,
vorname = req.vorname ?: existing.vorname,
geburtsdatum = req.geburtsdatum ?: existing.geburtsdatum,
lizenzNummer = req.lizenzNummer ?: existing.lizenzNummer,
lizenzKlasse = lizenzKlasse,
startkartAktiv = req.startkartAktiv ?: existing.startkartAktiv,
nation = req.nation ?: existing.nation,
vereinsNummer = req.vereinsNummer ?: existing.vereinsNummer,
bundeslandNummer = req.bundeslandNummer ?: existing.bundeslandNummer,
vereinsName = req.vereinsName ?: existing.vereinsName,
nation = req.nation ?: existing.nation,
reiterLizenz = req.reiterLizenz ?: existing.reiterLizenz,
startkarte = req.startkarte ?: existing.startkarte,
fahrLizenz = req.fahrLizenz ?: existing.fahrLizenz,
mitgliedsNummer = req.mitgliedsNummer ?: existing.mitgliedsNummer,
telefonNummer = req.telefonNummer ?: existing.telefonNummer,
lastPayYear = req.lastPayYear ?: existing.lastPayYear,
feiId = req.feiId ?: existing.feiId,
istGastreiter = req.istGastreiter ?: existing.istGastreiter,
lizenzKlasse = lizenzKlasse,
istAktiv = req.istAktiv ?: existing.istAktiv
)
val saved = reiterRepository.save(updated)
@@ -205,14 +208,17 @@ class ReiterController(private val reiterRepository: ReiterRepository) {
nachname = nachname,
vorname = vorname,
geburtsdatum = geburtsdatum,
lizenzNummer = lizenzNummer,
lizenzKlasse = lizenzKlasse.name,
startkartAktiv = startkartAktiv,
nation = nation,
vereinsNummer = vereinsNummer,
bundeslandNummer = bundeslandNummer,
vereinsName = vereinsName,
nation = nation,
reiterLizenz = reiterLizenz,
startkarte = startkarte,
fahrLizenz = fahrLizenz,
mitgliedsNummer = mitgliedsNummer,
telefonNummer = telefonNummer,
lastPayYear = lastPayYear,
feiId = feiId,
istGastreiter = istGastreiter,
lizenzKlasse = lizenzKlasse.name,
istAktiv = istAktiv,
updatedAt = updatedAt
)
@@ -18,21 +18,14 @@ import kotlin.uuid.Uuid
* Domain-Modell für einen Funktionär im actor-context.
*
* Repräsentiert eine Person mit einer definierten Rolle bei Turnieren (Richter, TBA,
* Parcoursbauer etc.). Die Qualifikation wird gegen `RICHT01.DAT` aus dem ZNS geprüft.
*
* Aggregate Root des `officials`-Bounded Context.
* Parcoursbauer etc.). Die Qualifikation wird gegen `RICHT01.DAT` oder `PARCO01.DAT`
* aus dem ZNS geprüft.
*
* @property funktionaerId Eindeutige interne ID (UUID).
* @property richterNummer ÖPS-Funktionärsnummer aus ZNS (RICHT01.dat), 6-stellig.
* @property vorname Vorname der Person.
* @property nachname Nachname der Person.
* @property geburtsdatum Geburtsdatum (optional, für Altersklassen-Prüfung).
* @property rollen Menge der Rollen, die diese Person ausüben darf (TBA, Richter, ...).
* @property richterQualifikation Qualifikationsstufe als Richter (GA, G1G3, International).
* @property qualifiziertFuerSparten Sparten, für die eine Richter-Qualifikation vorliegt.
* @property email E-Mail-Adresse für Kommunikation.
* @property telefon Telefonnummer.
* @property vereinsNummer Vereinsnummer des Heimvereins (Referenz auf DomVerein).
* @property satzID Typ des Satzes (X = Richter, Y = Parcoursbauer). Aus ZNS (RICHT01.DAT / PARCO01.DAT).
* @property satzNummer Satznummer (6-stellig). Aus ZNS (RICHT01.DAT / PARCO01.DAT).
* @property name Vollständiger Name (Nachname, Vorname). Aus ZNS (RICHT01.DAT / PARCO01.DAT).
* @property qualifikation Qualifikationen (getrennt durch `,`). Aus ZNS (RICHT01.DAT / PARCO01.DAT).
* @property istAktiv Ob der Funktionär aktuell aktiv/einsatzbereit ist.
* @property bemerkungen Interne Notizen.
* @property datenQuelle Herkunft des Datensatzes (ZNS-Import oder manuell).
@@ -43,26 +36,21 @@ import kotlin.uuid.Uuid
data class DomFunktionaer(
@Serializable(with = UuidSerializer::class)
val funktionaerId: Uuid = Uuid.random(),
val satzID: String,
val satzNummer: Int,
var name: String? = null, // Nachname, Vorname
var qualifikationen: List<String> = emptyList(), // Liste der Qualifikations-Kürzel
// Identifikation
val richterNummer: String? = null,
// Persönliche Daten
var vorname: String,
var nachname: String,
var geburtsdatum: LocalDate? = null,
// Qualifikation & Rollen
var rollen: Set<FunktionaerRolleE> = emptySet(),
var richterQualifikation: RichterQualifikationE? = null,
var qualifiziertFuerSparten: Set<SparteE> = emptySet(),
// Kontakt
var email: String? = null,
var telefon: String? = null,
// Vereinszugehörigkeit
var vereinsNummer: String? = null,
// var vorname: String,
// var nachname: String,
// var geburtsdatum: LocalDate? = null,
// val richterNummer: String? = null,
// var rollen: Set<FunktionaerRolleE> = emptySet(),
// var richterQualifikation: RichterQualifikationE? = null,
// var qualifiziertFuerSparten: Set<SparteE> = emptySet(),
// var email: String? = null,
// var telefon: String? = null,
// var vereinsNummer: String? = null,
// Status & Verwaltung
var istAktiv: Boolean = true,
@@ -78,44 +66,35 @@ data class DomFunktionaer(
/**
* Gibt den vollständigen Anzeigenamen zurück.
*/
fun getDisplayName(): String = "$vorname $nachname"
fun getDisplayName(): String = name ?: "Unbekannt"
/**
* Gibt den Anzeigenamen mit Funktionärsnummer zurück (falls vorhanden).
*/
fun getDisplayNameWithNummer(): String =
richterNummer?.let { "${getDisplayName()} ($it)" } ?: getDisplayName()
satzNummer.let { "${getDisplayName()} ($it)" }
/**
* Prüft, ob der Funktionär als Richter für eine bestimmte Sparte qualifiziert ist.
* Prüft, ob der Funktionär als Richter qualifiziert ist.
*/
fun istRichterFuerSparte(sparte: SparteE): Boolean =
rollen.contains(FunktionaerRolleE.RICHTER) && qualifiziertFuerSparten.contains(sparte)
fun istRichter(): Boolean = satzID.uppercase() == "X"
/**
* Prüft, ob der Funktionär die Rolle TBA ausüben darf.
* Prüft, ob der Funktionär als Parcoursbauer qualifiziert ist.
*/
fun istTba(): Boolean = rollen.contains(FunktionaerRolleE.TBA)
fun istParcoursbauer(): Boolean = satzID.uppercase() == "Y"
/**
* Validiert die Pflichtfelder für den Turniereinsatz.
* Gibt eine Liste von Warnungen zurück (kein harter Fehler Override-Event möglich).
*/
fun validateFuerTurniereinsatz(rolle: FunktionaerRolleE, sparte: SparteE? = null): List<String> {
fun validateFuerTurniereinsatz(): List<String> {
val warnings = mutableListOf<String>()
if (!istAktiv) {
warnings.add("Funktionär ${getDisplayName()} ist nicht aktiv.")
}
if (!rollen.contains(rolle)) {
warnings.add("Funktionär ${getDisplayName()} hat keine Qualifikation für Rolle $rolle.")
}
if (rolle == FunktionaerRolleE.RICHTER && sparte != null && !istRichterFuerSparte(sparte)) {
warnings.add("Funktionär ${getDisplayName()} ist nicht als Richter für Sparte $sparte qualifiziert.")
}
return warnings
}
@@ -22,24 +22,19 @@ import kotlin.uuid.Uuid
* It serves as the core aggregate root for the horse-registry bounded context.
*
* @property pferdId Unique internal identifier for this horse (UUID).
* @property pferdeName Name of the horse.
* @property geschlecht Gender of the horse (Hengst, Stute, Wallach).
* @property geburtsdatum Birthdate of the horse.
* @property rasse Breed of the horse.
* @property farbe Color/coat of the horse.
* @property besitzerId ID of the current owner (Person from member-management context).
* @property verantwortlichePersonId ID of the responsible person (trainer, rider, etc.).
* @property zuechterName Name of the breeder.
* @property zuchtbuchNummer Studbook number if registered.
* @property lebensnummer Life number (unique identification number).
* @property chipNummer Microchip number for identification.
* @property passNummer Passport number.
* @property oepsNummer OEPS (Austrian Equestrian Federation) number.
* @property feiNummer FEI (International Equestrian Federation) number.
* @property vaterName Name of the sire (father).
* @property mutterName Name of the dam (mother).
* @property mutterVaterName Name of the maternal grandsire.
* @property stockmass Height of the horse in cm.
* @property kopfnummer Head number (Kopfnummer) used at tournaments (4 alphanumeric chars). From PFERDE01.DAT.
* @property pferdeName Name of the horse. From PFERDE01.DAT.
* @property lebensnummer Life number (unique identification number). From PFERDE01.DAT.
* @property geschlecht Gender of the horse (Hengst, Stute, Wallach). Derived from PFERDE01.DAT.
* @property geburtsjahr Birth year of the horse. From PFERDE01.DAT.
* @property farbe Color/coat of the horse. From PFERDE01.DAT.
* @property abstammung Breeding/Pedigree information. From PFERDE01.DAT.
* @property vereinNummer Club number (OEPS). From PFERDE01.DAT.
* @property lastPayYear Last year the horse's OEPS fee was paid. From PFERDE01.DAT.
* @property verantwortlichePersonId Reference to the responsible person (Satznummer or ID). From PFERDE01.DAT.
* @property vater Name of the sire (father). From PFERDE01.DAT.
* @property feiPass FEI passport information. From PFERDE01.DAT.
* @property satznummer 10-digit ZNS primary key for data exchange. From PFERDE01.DAT.
* @property istAktiv Whether the horse is currently active in the system.
* @property bemerkungen Additional notes or comments.
* @property datenQuelle Source of the data (manual entry, import, etc.).
@@ -51,56 +46,49 @@ data class DomPferd(
@Serializable(with = UuidSerializer::class)
val pferdId: Uuid = Uuid.random(),
// Basic Information
// PFERDE01.DAT Information
var kopfnummer: String? = null,
var pferdeName: String,
var geschlecht: PferdeGeschlechtE,
var geburtsdatum: LocalDate? = null,
var rasse: String? = null,
var farbe: String? = null,
// Ownership and Responsibility
@Serializable(with = UuidSerializer::class)
var besitzerId: Uuid? = null,
@Serializable(with = UuidSerializer::class)
var verantwortlichePersonId: Uuid? = null,
// Breeding Information
var zuechterName: String? = null,
var zuchtbuchNummer: String? = null,
// Identification Numbers
var lebensnummer: String? = null,
var chipNummer: String? = null,
var passNummer: String? = null,
var oepsNummer: String? = null,
var feiNummer: String? = null,
var geschlecht: PferdeGeschlechtE,
var geburtsjahr: Int? = null,
var farbe: String? = null,
var abstammung: String? = null,
var vereinNummer: Int? = null,
var lastPayYear: Int? = null,
var verantwortlichePersonId: String? = null,
var vater: String? = null,
var feiPass: String? = null,
var satznummer: String? = null,
// Pedigree Information
var vaterName: String? = null,
var mutterName: String? = null,
var mutterVaterName: String? = null,
// var geburtsdatum: LocalDate? = null,
// var rasse: String? = null,
// @Serializable(with = UuidSerializer::class)
// var besitzerId: Uuid? = null,
// var zuechterName: String? = null,
// var zuchtbuchNummer: String? = null,
// var chipNummer: String? = null,
// var passNummer: String? = null,
// var oepsNummer: String? = null,
// var mutterName: String? = null,
// var mutterVaterName: String? = null,
// var stockmass: Int? = null, // Height in cm
// Physical Characteristics
var stockmass: Int? = null, // Height in cm
// Status and Administrative
var istAktiv: Boolean = true,
var bemerkungen: String? = null,
var datenQuelle: DatenQuelleE = DatenQuelleE.MANUELL,
// Audit Fields
@Serializable(with = InstantSerializer::class)
val createdAt: Instant = Clock.System.now(),
@Serializable(with = InstantSerializer::class)
var createdAt: Instant = Clock.System.now(),
var updatedAt: Instant = Clock.System.now()
) {
/**
* Returns the display name for the horse, combining name and birth year if available.
*/
fun getDisplayName(): String {
return geburtsdatum?.let { birthDate ->
"$pferdeName (${birthDate.year})"
val basic = geburtsjahr?.let { year ->
"$pferdeName ($year)"
} ?: pferdeName
return kopfnummer?.let { "[$it] $basic" } ?: basic
}
/**
@@ -108,40 +96,31 @@ data class DomPferd(
*/
fun hasCompleteIdentification(): Boolean {
return !lebensnummer.isNullOrBlank() ||
!chipNummer.isNullOrBlank() ||
!passNummer.isNullOrBlank()
!kopfnummer.isNullOrBlank() ||
!satznummer.isNullOrBlank()
}
/**
* Checks if the horse is registered with OEPS.
*/
fun isOepsRegistered(): Boolean {
return !oepsNummer.isNullOrBlank()
return false // OEPS registration information currently commented out
}
/**
* Checks if the horse is registered with FEI.
*/
fun isFeiRegistered(): Boolean {
return !feiNummer.isNullOrBlank()
return !feiPass.isNullOrBlank()
}
/**
* Returns the age of the horse in years, or null if birth date is unknown.
* Returns the age of the horse in years, or null if birth year is unknown.
*/
fun getAge(): Int? {
return geburtsdatum?.let { birthDate ->
return geburtsjahr?.let { birthYear ->
val today = Clock.System.todayIn(kotlinx.datetime.TimeZone.currentSystemDefault())
var age = today.year - birthDate.year
// Check if a birthday has occurred this year
if (today.month.number < birthDate.month.number ||
(today.month.number == birthDate.month.number && today.day < birthDate.day)
) {
age--
}
age
today.year - birthYear
}
}
@@ -156,11 +135,7 @@ data class DomPferd(
}
if (!hasCompleteIdentification()) {
errors.add("At least one identification number (life number, chip number, or passport number) is required")
}
if (besitzerId == null) {
errors.add("Owner is required")
errors.add("At least one identification number (life number, or kopfnummer, or satznummer) is required")
}
return errors
@@ -9,6 +9,7 @@ import at.mocode.core.domain.serialization.InstantSerializer
import at.mocode.core.domain.serialization.LocalDateSerializer
import at.mocode.core.domain.serialization.UuidSerializer
import kotlinx.datetime.LocalDate
import kotlinx.datetime.todayIn
import kotlinx.serialization.Serializable
import kotlin.time.Clock
import kotlin.time.Instant
@@ -21,27 +22,30 @@ import kotlin.uuid.Uuid
* attributes such as license, start card, and competition eligibility.
* Data is primarily sourced from the OEPS ZNS (LIZENZ01.DAT).
*
* Key rules (ÖTO):
* - A rider requires an active Startkarte (annual fee paid) to compete nationally.
* - LizenzKlasse determines which competition classes the rider may enter.
* - Satznummer (6-digit) is the primary key for ZNS data exchange.
* - Kopfnummer is NOT a unique identifier it can change.
*
* @property reiterId Unique internal identifier (UUID).
* @property personId Reference to the base DomPerson record (UUID).
* @property satznummer 6-digit ZNS primary key for data exchange. Primary key for ZNS.
* @property lizenzNummer OEPS license number (from ZNS LIZENZ01.DAT).
* @property lizenzKlasse License class determining competition eligibility (e.g. R1, RD2).
* @property lizenzSparten Disciplines for which the license is valid.
* @property startkartAktiv Whether the annual start card fee has been paid.
* @property startkartSaison Season year for which the start card is valid (e.g. 2026).
* @property feiId FEI international rider ID (optional).
* @property nation Nation code (e.g. AUT).
* @property geburtsdatum Date of birth (for age class validation).
* @property vereinsNummer Club number (OEPS).
* @property vereinsName Club name.
* @property istGastreiter Whether the rider is a guest rider (foreign nationality, not in Austrian club).
* @property satznummer 6-digit ZNS primary key for data exchange. From LIZENZ01.DAT.
* @property nachname Surname of the rider. From LIZENZ01.DAT.
* @property vorname First name of the rider. From LIZENZ01.DAT.
* @property bundeslandNummer State number (Bundesland). From LIZENZ01.DAT.
* @property vereinsName Name of the club. From LIZENZ01.DAT.
* @property nation Nationality of the rider. From LIZENZ01.DAT.
* @property reiterLizenz Rider license information. From LIZENZ01.DAT.
* @property startkarte Start card information. From LIZENZ01.DAT.
* @property fahrLizenz Driving license information. From LIZENZ01.DAT.
* @property altersklasseJgJrU25 Age class Jg/Jr/U25. From LIZENZ01.DAT.
* @property altersklasseY Age class Young Rider. From LIZENZ01.DAT.
* @property mitgliedsNummer Membership number. From LIZENZ01.DAT.
* @property telefonNummer Phone number. From LIZENZ01.DAT.
* @property kader Squad status. From LIZENZ01.DAT.
* @property lastPayYear Last year the license was paid. From LIZENZ01.DAT.
* @property geschlecht Gender of the rider. From LIZENZ01.DAT.
* @property geburtsdatum Date of birth. From LIZENZ01.DAT (JJJJMMTT).
* @property feiId FEI ID. From LIZENZ01.DAT.
* @property sperrListe Suspension list information. From LIZENZ01.DAT.
* @property lizenzInfo License info details. From LIZENZ01.DAT.
* @property istAktiv Whether the rider is currently active in the system.
* @property bemerkungen Additional notes or comments.
* @property datenQuelle Source of the data.
* @property createdAt Timestamp when this record was created.
* @property updatedAt Timestamp when this record was last updated.
@@ -56,37 +60,33 @@ data class DomReiter(
val personId: Uuid,
// ZNS Identification
val satznummer: String,
val lizenzNummer: String? = null,
// License & Eligibility
val lizenzKlasse: LizenzKlasseE = LizenzKlasseE.LIZENZFREI,
val lizenzSparten: List<SparteE> = emptyList(),
// Start Card (Startkarte) annual fee proof
val startkartAktiv: Boolean = false,
val startkartSaison: Int? = null,
// International
val feiId: String? = null,
val nation: String? = null,
// Personal Data (denormalized from DomPerson for performance)
val nachname: String,
val vorname: String,
var satznummer: String?,
var nachname: String,
var vorname: String,
var bundeslandNummer: Int? = null,
var vereinsName: String? = null,
var nation: String? = null,
var reiterLizenz: String? = null,
var startkarte: String? = null,
var fahrLizenz: String? = null,
var altersklasseJgJrU25: String? = null,
var altersklasseY: String? = null,
var mitgliedsNummer: Int? = null,
var telefonNummer: String? = null,
var kader: String? = null,
var lastPayYear: Int? = null,
var geschlecht: String? = null,
@Serializable(with = LocalDateSerializer::class)
val geburtsdatum: LocalDate? = null,
var geburtsdatum: LocalDate? = null,
var feiId: String? = null,
var sperrListe: String? = null,
var lizenzInfo: String? = null,
// Club Affiliation
val vereinsNummer: String? = null,
val vereinsName: String? = null,
var lizenzKlasse: LizenzKlasseE = LizenzKlasseE.LIZENZFREI,
// Status
val istGastreiter: Boolean = false,
val istAktiv: Boolean = true,
var bemerkungen: String? = null,
val datenQuelle: DatenQuelleE = DatenQuelleE.IMPORT_ZNS,
// Audit Fields
@Serializable(with = InstantSerializer::class)
val createdAt: Instant = Clock.System.now(),
@Serializable(with = InstantSerializer::class)
@@ -99,31 +99,45 @@ data class DomReiter(
/**
* Checks if the rider is eligible to compete nationally.
* Requires an active start card (Startkarte).
* Based on the last pay year.
*/
fun isStartberechtigt(): Boolean = istAktiv && startkartAktiv
fun isStartberechtigt(): Boolean {
val currentYear = Clock.System.todayIn(kotlinx.datetime.TimeZone.currentSystemDefault()).year
return istAktiv && (lastPayYear ?: 0) >= currentYear
}
/**
* Checks if the rider holds a license for the given discipline.
* Checks if the rider holds a license.
*/
fun hasLizenzForSparte(sparte: SparteE): Boolean =
lizenzKlasse == LizenzKlasseE.LIZENZFREI || lizenzSparten.contains(sparte)
fun hasLizenz(): Boolean = !reiterLizenz.isNullOrBlank()
/**
* Checks if the rider has a license for a specific sparte.
*/
fun hasLizenzForSparte(sparte: SparteE): Boolean {
// If we have a license class, check if it's applicable for the sparte
if (lizenzKlasse == LizenzKlasseE.LIZENZFREI) return false
return when (sparte) {
SparteE.DRESSUR -> true // Everyone with a license can do dressage (simplified)
SparteE.SPRINGEN -> !listOf(LizenzKlasseE.RD1, LizenzKlasseE.RD2, LizenzKlasseE.RD3).contains(lizenzKlasse)
else -> true
}
}
/**
* Validates the rider for competition entry.
* Returns a list of warning messages (never hard errors TBA has final say).
*/
fun validateForNennung(sparte: SparteE): List<String> {
fun validateForNennung(): List<String> {
val warnings = mutableListOf<String>()
if (!istAktiv) {
warnings.add("Reiter ${getDisplayName()} ist nicht aktiv")
}
if (!startkartAktiv) {
warnings.add("Reiter ${getDisplayName()} hat keine aktive Startkarte für Saison $startkartSaison")
}
if (!hasLizenzForSparte(sparte)) {
warnings.add("Reiter ${getDisplayName()} hat keine Lizenz für Sparte $sparte (Lizenzklasse: $lizenzKlasse)")
if (!isStartberechtigt()) {
warnings.add("Reiter ${getDisplayName()} hat keine aktive Startkarte für das aktuelle Jahr")
}
return warnings
@@ -22,42 +22,9 @@ interface FunktionaerRepository {
suspend fun findById(id: Uuid): DomFunktionaer?
/**
* Sucht einen Funktionär anhand seiner Richternummer.
* Sucht einen Funktionär anhand seiner Satz-ID und Satznummer.
*/
suspend fun findByRichterNummer(richterNummer: String): DomFunktionaer?
/**
* Sucht Funktionäre anhand von Vor- und/oder Nachname (Teilübereinstimmung).
*/
suspend fun findByName(searchTerm: String, limit: Int = 50): List<DomFunktionaer>
/**
* Sucht alle Funktionäre mit einer bestimmten Rolle.
*/
suspend fun findByRolle(rolle: FunktionaerRolleE, activeOnly: Boolean = true): List<DomFunktionaer>
/**
* Sucht alle Richter mit einer bestimmten Qualifikation.
*/
suspend fun findByRichterQualifikation(
qualifikation: RichterQualifikationE,
activeOnly: Boolean = true
): List<DomFunktionaer>
/**
* Sucht alle Funktionäre, die für eine bestimmte Sparte qualifiziert sind.
*/
suspend fun findBySparte(sparte: SparteE, activeOnly: Boolean = true): List<DomFunktionaer>
/**
* Sucht alle Funktionäre eines bestimmten Vereins.
*/
suspend fun findByVereinsNummer(vereinsNummer: String, activeOnly: Boolean = true): List<DomFunktionaer>
/**
* Gibt alle aktiven Funktionäre zurück (paginiert).
*/
suspend fun findAllActive(limit: Int = 100, offset: Int = 0): List<DomFunktionaer>
suspend fun findBySatz(satzID: String, satzNummer: Int): DomFunktionaer?
/**
* Gibt alle Funktionäre zurück (paginiert).
@@ -82,12 +49,7 @@ interface FunktionaerRepository {
suspend fun countActive(): Long
/**
* Zählt alle Richter (Rolle = RICHTER) mit einer bestimmten Qualifikation.
* Prüft ob ein Funktionär mit der gegebenen Satz-ID und Satznummer bereits existiert.
*/
suspend fun countByRichterQualifikation(qualifikation: RichterQualifikationE, activeOnly: Boolean = true): Long
/**
* Prüft ob ein Funktionär mit der gegebenen Richternummer bereits existiert.
*/
suspend fun existsByRichterNummer(richterNummer: String): Boolean
suspend fun existsBySatz(satzID: String, satzNummer: Int): Boolean
}
@@ -246,6 +246,28 @@ interface HorseRepository {
*/
suspend fun countFeiRegistered(activeOnly: Boolean = true): Long
/**
* Finds horses by their head number (Kopfnummer).
*
* @param kopfnummer The head number to search for
* @return The list of horses found
*/
suspend fun findByKopfnummer(kopfnummer: String): List<DomPferd>
/**
* Finds a horse by its ZNS satznummer.
*
* @param satznummer The ZNS satznummer to search for
* @return The horse if found, null otherwise
*/
suspend fun findBySatznummer(satznummer: String): DomPferd?
/**
* Speichert ein Pferd basierend auf der ZNS satznummer (Upsert).
* Wenn ein Pferd mit der satznummer existiert, wird es aktualisiert, ansonsten neu angelegt.
*/
suspend fun upsertBySatznummer(horse: DomPferd): DomPferd
/**
* Speichert ein Pferd basierend auf der Lebensnummer (Upsert).
* Wenn ein Pferd mit der Lebensnummer existiert, wird es aktualisiert, ansonsten neu angelegt.
@@ -2,8 +2,6 @@
package at.mocode.masterdata.domain.repository
import at.mocode.core.domain.model.LizenzKlasseE
import at.mocode.core.domain.model.SparteE
import at.mocode.masterdata.domain.model.DomReiter
import kotlin.uuid.Uuid
@@ -23,42 +21,7 @@ interface ReiterRepository {
/**
* Sucht einen Reiter anhand seiner Satznummer (OEPS-Mitgliedsnummer).
*/
suspend fun findBySatznummer(satznummer: String): DomReiter?
/**
* Sucht einen Reiter anhand seiner FEI-ID.
*/
suspend fun findByFeiId(feiId: String): DomReiter?
/**
* Sucht Reiter anhand von Vor- und/oder Nachname (Teilübereinstimmung).
*/
suspend fun findByName(searchTerm: String, limit: Int = 50): List<DomReiter>
/**
* Sucht alle Reiter eines bestimmten Vereins.
*/
suspend fun findByVereinsNummer(vereinsNummer: String, activeOnly: Boolean = true): List<DomReiter>
/**
* Sucht alle Reiter mit einer bestimmten Lizenzklasse.
*/
suspend fun findByLizenzKlasse(lizenzKlasse: LizenzKlasseE, activeOnly: Boolean = true): List<DomReiter>
/**
* Sucht alle Reiter, die für eine bestimmte Sparte lizenziert sind.
*/
suspend fun findBySparte(sparte: SparteE, activeOnly: Boolean = true): List<DomReiter>
/**
* Sucht alle Gastreiter.
*/
suspend fun findGastreiter(activeOnly: Boolean = true): List<DomReiter>
/**
* Gibt alle aktiven Reiter zurück (paginiert).
*/
suspend fun findAllActive(limit: Int = 100, offset: Int = 0): List<DomReiter>
suspend fun findBySatznummer(satznummer: String?): DomReiter?
/**
* Gibt alle Reiter zurück (paginiert).
@@ -95,9 +95,7 @@ class LicenseMatrixServiceTest {
satznummer = "1",
nachname = "R1",
vorname = "Reiter",
lizenzKlasse = LizenzKlasseE.R1,
lizenzSparten = listOf(SparteE.SPRINGEN),
startkartAktiv = true
lizenzKlasse = LizenzKlasseE.R1
)
val klasseA = turnierklassen.find { it.code == "A" }!!
@@ -116,9 +114,7 @@ class LicenseMatrixServiceTest {
satznummer = "2",
nachname = "RD1",
vorname = "Reiter",
lizenzKlasse = LizenzKlasseE.RD1,
lizenzSparten = listOf(SparteE.DRESSUR), // Nur Dressur
startkartAktiv = true
lizenzKlasse = LizenzKlasseE.RD1
)
val klasseA = turnierklassen.find { it.code == "A" }!!
@@ -10,9 +10,7 @@ import at.mocode.core.utils.database.DatabaseFactory
import at.mocode.masterdata.domain.model.DomFunktionaer
import at.mocode.masterdata.domain.repository.FunktionaerRepository
import org.jetbrains.exposed.v1.core.ResultRow
import org.jetbrains.exposed.v1.core.eq
import org.jetbrains.exposed.v1.core.like
import org.jetbrains.exposed.v1.core.or
import org.jetbrains.exposed.v1.core.*
import org.jetbrains.exposed.v1.jdbc.*
import kotlin.uuid.Uuid
@@ -21,16 +19,13 @@ import kotlin.uuid.Uuid
*/
class ExposedFunktionaerRepository : FunktionaerRepository {
private fun rowToDomFunktionaer(row: ResultRow): DomFunktionaer {
private fun rowToDomFunktionaer(row: ResultRow, qualifikationen: List<String> = emptyList()): DomFunktionaer {
return DomFunktionaer(
funktionaerId = row[FunktionaerTable.id],
richterNummer = row[FunktionaerTable.richterNummer],
vorname = row[FunktionaerTable.vorname],
nachname = row[FunktionaerTable.nachname],
geburtsdatum = row[FunktionaerTable.geburtsdatum],
email = row[FunktionaerTable.email],
telefon = row[FunktionaerTable.telefon],
vereinsNummer = row[FunktionaerTable.vereinsNummer],
satzID = row[FunktionaerTable.satzID] ?: "X",
satzNummer = row[FunktionaerTable.satzNummer] ?: 0,
name = row[FunktionaerTable.name],
qualifikationen = qualifikationen,
istAktiv = row[FunktionaerTable.istAktiv],
bemerkungen = row[FunktionaerTable.bemerkungen],
datenQuelle = DatenQuelleE.valueOf(row[FunktionaerTable.datenQuelle]),
@@ -40,101 +35,78 @@ class ExposedFunktionaerRepository : FunktionaerRepository {
}
override suspend fun findById(id: Uuid): DomFunktionaer? = DatabaseFactory.dbQuery {
val qualifikationen = FunktionaerQualifikationTable
.selectAll().where { FunktionaerQualifikationTable.funktionaerId eq id }
.map { it[FunktionaerQualifikationTable.qualifikation] }
FunktionaerTable.selectAll().where { FunktionaerTable.id eq id }
.map(::rowToDomFunktionaer)
.map { rowToDomFunktionaer(it, qualifikationen) }
.singleOrNull()
}
override suspend fun findByRichterNummer(richterNummer: String): DomFunktionaer? = DatabaseFactory.dbQuery {
FunktionaerTable.selectAll().where { FunktionaerTable.richterNummer eq richterNummer }
.map(::rowToDomFunktionaer)
.singleOrNull()
}
override suspend fun findBySatz(satzID: String, satzNummer: Int): DomFunktionaer? = DatabaseFactory.dbQuery {
val row = FunktionaerTable.selectAll()
.where { (FunktionaerTable.satzID eq satzID) and (FunktionaerTable.satzNummer eq satzNummer) }
.singleOrNull() ?: return@dbQuery null
override suspend fun findByName(searchTerm: String, limit: Int): List<DomFunktionaer> = DatabaseFactory.dbQuery {
val pattern = "%$searchTerm%"
FunktionaerTable.selectAll()
.where { (FunktionaerTable.nachname like pattern) or (FunktionaerTable.vorname like pattern) }
.limit(limit)
.map(::rowToDomFunktionaer)
}
val qualifikationen = FunktionaerQualifikationTable
.selectAll().where { FunktionaerQualifikationTable.funktionaerId eq row[FunktionaerTable.id] }
.map { it[FunktionaerQualifikationTable.qualifikation] }
override suspend fun findByRolle(rolle: FunktionaerRolleE, activeOnly: Boolean): List<DomFunktionaer> =
DatabaseFactory.dbQuery {
// Rolle wird aktuell nicht in FunktionaerTable gespeichert.
// Falls benötigt, muss die Tabelle erweitert werden.
emptyList()
}
override suspend fun findByRichterQualifikation(
qualifikation: RichterQualifikationE,
activeOnly: Boolean
): List<DomFunktionaer> = DatabaseFactory.dbQuery {
// Qualifikationen werden aktuell nicht in FunktionaerTable gespeichert.
emptyList()
}
override suspend fun findBySparte(sparte: SparteE, activeOnly: Boolean): List<DomFunktionaer> =
DatabaseFactory.dbQuery {
emptyList()
}
override suspend fun findByVereinsNummer(vereinsNummer: String, activeOnly: Boolean): List<DomFunktionaer> =
DatabaseFactory.dbQuery {
val query = FunktionaerTable.selectAll().where { FunktionaerTable.vereinsNummer eq vereinsNummer }
if (activeOnly) {
query.andWhere { FunktionaerTable.istAktiv eq true }
}
query.map(::rowToDomFunktionaer)
}
override suspend fun findAllActive(limit: Int, offset: Int): List<DomFunktionaer> = DatabaseFactory.dbQuery {
FunktionaerTable.selectAll().where { FunktionaerTable.istAktiv eq true }
.limit(limit).offset(offset.toLong())
.map(::rowToDomFunktionaer)
rowToDomFunktionaer(row, qualifikationen)
}
override suspend fun findAll(limit: Int, offset: Int): List<DomFunktionaer> = DatabaseFactory.dbQuery {
FunktionaerTable.selectAll()
val funktionaere = FunktionaerTable.selectAll()
.limit(limit).offset(offset.toLong())
.map(::rowToDomFunktionaer)
.toList()
val ids = funktionaere.map { it[FunktionaerTable.id] }
val qualisMap = FunktionaerQualifikationTable
.selectAll().where { FunktionaerQualifikationTable.funktionaerId inList ids }
.groupBy({ it[FunktionaerQualifikationTable.funktionaerId] }) { it[FunktionaerQualifikationTable.qualifikation] }
funktionaere.map { row ->
rowToDomFunktionaer(row, qualisMap[row[FunktionaerTable.id]] ?: emptyList())
}
}
override suspend fun save(funktionaer: DomFunktionaer): DomFunktionaer = DatabaseFactory.dbQuery {
val exists = FunktionaerTable.selectAll().where { FunktionaerTable.id eq funktionaer.funktionaerId }.any()
if (exists) {
FunktionaerTable.update({ FunktionaerTable.id eq funktionaer.funktionaerId }) {
it[richterNummer] = funktionaer.richterNummer
it[vorname] = funktionaer.vorname
it[nachname] = funktionaer.nachname
it[geburtsdatum] = funktionaer.geburtsdatum
it[email] = funktionaer.email
it[telefon] = funktionaer.telefon
it[vereinsNummer] = funktionaer.vereinsNummer
it[satzID] = funktionaer.satzID
it[satzNummer] = funktionaer.satzNummer
it[name] = funktionaer.name
it[istAktiv] = funktionaer.istAktiv
it[bemerkungen] = funktionaer.bemerkungen
it[datenQuelle] = funktionaer.datenQuelle.name
it[updatedAt] = funktionaer.updatedAt
}
funktionaer
} else {
FunktionaerTable.insert {
it[id] = funktionaer.funktionaerId
it[richterNummer] = funktionaer.richterNummer
it[vorname] = funktionaer.vorname
it[nachname] = funktionaer.nachname
it[geburtsdatum] = funktionaer.geburtsdatum
it[email] = funktionaer.email
it[telefon] = funktionaer.telefon
it[vereinsNummer] = funktionaer.vereinsNummer
it[satzID] = funktionaer.satzID
it[satzNummer] = funktionaer.satzNummer
it[name] = funktionaer.name
it[istAktiv] = funktionaer.istAktiv
it[bemerkungen] = funktionaer.bemerkungen
it[datenQuelle] = funktionaer.datenQuelle.name
it[createdAt] = funktionaer.createdAt
it[updatedAt] = funktionaer.updatedAt
}
funktionaer
}
// Qualifikationen synchronisieren
FunktionaerQualifikationTable.deleteWhere { funktionaerId eq funktionaer.funktionaerId }
funktionaer.qualifikationen.forEach { quali ->
FunktionaerQualifikationTable.insert {
it[funktionaerId] = funktionaer.funktionaerId
it[qualifikation] = quali
}
}
funktionaer
}
override suspend fun delete(id: Uuid): Boolean = DatabaseFactory.dbQuery {
@@ -145,13 +117,9 @@ class ExposedFunktionaerRepository : FunktionaerRepository {
FunktionaerTable.selectAll().where { FunktionaerTable.istAktiv eq true }.count()
}
override suspend fun countByRichterQualifikation(qualifikation: RichterQualifikationE, activeOnly: Boolean): Long =
DatabaseFactory.dbQuery {
// Aktuell keine Qualifikations-Speicherung
0L
}
override suspend fun existsByRichterNummer(richterNummer: String): Boolean = DatabaseFactory.dbQuery {
FunktionaerTable.selectAll().where { FunktionaerTable.richterNummer eq richterNummer }.any()
override suspend fun existsBySatz(satzID: String, satzNummer: Int): Boolean = DatabaseFactory.dbQuery {
FunktionaerTable.selectAll()
.where { (FunktionaerTable.satzID eq satzID) and (FunktionaerTable.satzNummer eq satzNummer) }
.any()
}
}
@@ -4,14 +4,11 @@ package at.mocode.masterdata.infrastructure.persistence
import at.mocode.core.domain.model.DatenQuelleE
import at.mocode.core.domain.model.LizenzKlasseE
import at.mocode.core.domain.model.SparteE
import at.mocode.core.utils.database.DatabaseFactory
import at.mocode.masterdata.domain.model.DomReiter
import at.mocode.masterdata.domain.repository.ReiterRepository
import org.jetbrains.exposed.v1.core.ResultRow
import org.jetbrains.exposed.v1.core.eq
import org.jetbrains.exposed.v1.core.like
import org.jetbrains.exposed.v1.core.or
import org.jetbrains.exposed.v1.jdbc.*
import kotlin.uuid.Uuid
@@ -20,133 +17,55 @@ import kotlin.uuid.Uuid
*/
class ExposedReiterRepository : ReiterRepository {
private fun rowToDomReiter(row: ResultRow, sparten: List<SparteE> = emptyList()): DomReiter {
private fun rowToDomReiter(row: ResultRow): DomReiter {
return DomReiter(
reiterId = row[ReiterTable.id],
personId = row[ReiterTable.personId],
satznummer = row[ReiterTable.satznummer],
nachname = row[ReiterTable.nachname],
vorname = row[ReiterTable.vorname],
geburtsdatum = row[ReiterTable.geburtsdatum],
lizenzNummer = row[ReiterTable.lizenzNummer],
lizenzKlasse = LizenzKlasseE.valueOf(row[ReiterTable.lizenzKlasse]),
lizenzSparten = sparten,
startkartAktiv = row[ReiterTable.startkartAktiv],
startkartSaison = row[ReiterTable.startkartSaison],
feiId = row[ReiterTable.feiId],
nation = row[ReiterTable.nation],
vereinsNummer = row[ReiterTable.vereinsNummer],
bundeslandNummer = row[ReiterTable.bundeslandNummer],
vereinsName = row[ReiterTable.vereinsName],
istGastreiter = row[ReiterTable.istGastreiter],
nation = row[ReiterTable.nation],
reiterLizenz = row[ReiterTable.reiterLizenz],
startkarte = row[ReiterTable.startkarte],
fahrLizenz = row[ReiterTable.fahrLizenz],
altersklasseJgJrU25 = row[ReiterTable.altersklasseJgJrU25],
altersklasseY = row[ReiterTable.altersklasseY],
mitgliedsNummer = row[ReiterTable.mitgliedsNummer],
telefonNummer = row[ReiterTable.telefonNummer],
kader = row[ReiterTable.kader],
lastPayYear = row[ReiterTable.lastPayYear],
geschlecht = row[ReiterTable.geschlecht],
geburtsdatum = row[ReiterTable.geburtsdatum],
feiId = row[ReiterTable.feiId],
sperrListe = row[ReiterTable.sperrListe],
lizenzInfo = row[ReiterTable.lizenzInfo],
lizenzKlasse = LizenzKlasseE.valueOf(row[ReiterTable.lizenzKlasse]),
istAktiv = row[ReiterTable.istAktiv],
bemerkungen = row[ReiterTable.bemerkungen],
datenQuelle = DatenQuelleE.valueOf(row[ReiterTable.datenQuelle]),
createdAt = row[ReiterTable.createdAt],
updatedAt = row[ReiterTable.updatedAt]
)
}
private fun getSpartenForReiter(reiterId: Uuid): List<SparteE> {
return ReiterSparteTable.selectAll().where { ReiterSparteTable.reiterId eq reiterId }
.map { SparteE.valueOf(it[ReiterSparteTable.sparte]) }
}
override suspend fun findById(id: Uuid): DomReiter? = DatabaseFactory.dbQuery {
ReiterTable.selectAll().where { ReiterTable.id eq id }
.map { rowToDomReiter(it, getSpartenForReiter(id)) }
.map { rowToDomReiter(it) }
.singleOrNull()
}
override suspend fun findBySatznummer(satznummer: String): DomReiter? = DatabaseFactory.dbQuery {
override suspend fun findBySatznummer(satznummer: String?): DomReiter? = DatabaseFactory.dbQuery {
ReiterTable.selectAll().where { ReiterTable.satznummer eq satznummer }
.map { row ->
val id = row[ReiterTable.id]
rowToDomReiter(row, getSpartenForReiter(id))
}
.map { row -> rowToDomReiter(row) }
.singleOrNull()
}
override suspend fun findByFeiId(feiId: String): DomReiter? = DatabaseFactory.dbQuery {
ReiterTable.selectAll().where { ReiterTable.feiId eq feiId }
.map { row ->
val id = row[ReiterTable.id]
rowToDomReiter(row, getSpartenForReiter(id))
}
.singleOrNull()
}
override suspend fun findByName(searchTerm: String, limit: Int): List<DomReiter> = DatabaseFactory.dbQuery {
val pattern = "%$searchTerm%"
ReiterTable.selectAll().where { (ReiterTable.nachname like pattern) or (ReiterTable.vorname like pattern) }
.limit(limit)
.map { row ->
val id = row[ReiterTable.id]
rowToDomReiter(row, getSpartenForReiter(id))
}
}
override suspend fun findByVereinsNummer(vereinsNummer: String, activeOnly: Boolean): List<DomReiter> =
DatabaseFactory.dbQuery {
val query = ReiterTable.selectAll().where { ReiterTable.vereinsNummer eq vereinsNummer }
if (activeOnly) {
query.andWhere { ReiterTable.istAktiv eq true }
}
query.map { row ->
val id = row[ReiterTable.id]
rowToDomReiter(row, getSpartenForReiter(id))
}
}
override suspend fun findByLizenzKlasse(lizenzKlasse: LizenzKlasseE, activeOnly: Boolean): List<DomReiter> =
DatabaseFactory.dbQuery {
val query = ReiterTable.selectAll().where { ReiterTable.lizenzKlasse eq lizenzKlasse.name }
if (activeOnly) {
query.andWhere { ReiterTable.istAktiv eq true }
}
query.map { row ->
val id = row[ReiterTable.id]
rowToDomReiter(row, getSpartenForReiter(id))
}
}
override suspend fun findBySparte(sparte: SparteE, activeOnly: Boolean): List<DomReiter> = DatabaseFactory.dbQuery {
val query = (ReiterTable innerJoin ReiterSparteTable)
.selectAll().where { ReiterSparteTable.sparte eq sparte.name }
if (activeOnly) {
query.andWhere { ReiterTable.istAktiv eq true }
}
query.map { row ->
val id = row[ReiterTable.id]
rowToDomReiter(row, getSpartenForReiter(id))
}
}
override suspend fun findGastreiter(activeOnly: Boolean): List<DomReiter> = DatabaseFactory.dbQuery {
val query = ReiterTable.selectAll().where { ReiterTable.istGastreiter eq true }
if (activeOnly) {
query.andWhere { ReiterTable.istAktiv eq true }
}
query.map { row ->
val id = row[ReiterTable.id]
rowToDomReiter(row, getSpartenForReiter(id))
}
}
override suspend fun findAllActive(limit: Int, offset: Int): List<DomReiter> = DatabaseFactory.dbQuery {
ReiterTable.selectAll().where { ReiterTable.istAktiv eq true }
.limit(limit).offset(offset.toLong())
.map { row ->
val id = row[ReiterTable.id]
rowToDomReiter(row, getSpartenForReiter(id))
}
}
override suspend fun findAll(limit: Int, offset: Int): List<DomReiter> = DatabaseFactory.dbQuery {
ReiterTable.selectAll()
.limit(limit).offset(offset.toLong())
.map { row ->
val id = row[ReiterTable.id]
rowToDomReiter(row, getSpartenForReiter(id))
}
.map { row -> rowToDomReiter(row) }
}
override suspend fun save(reiter: DomReiter): DomReiter = DatabaseFactory.dbQuery {
@@ -157,17 +76,26 @@ class ExposedReiterRepository : ReiterRepository {
it[satznummer] = reiter.satznummer
it[nachname] = reiter.nachname
it[vorname] = reiter.vorname
it[geburtsdatum] = reiter.geburtsdatum
it[lizenzNummer] = reiter.lizenzNummer
it[lizenzKlasse] = reiter.lizenzKlasse.name
it[startkartAktiv] = reiter.startkartAktiv
it[startkartSaison] = reiter.startkartSaison
it[feiId] = reiter.feiId
it[nation] = reiter.nation
it[vereinsNummer] = reiter.vereinsNummer
it[bundeslandNummer] = reiter.bundeslandNummer
it[vereinsName] = reiter.vereinsName
it[istGastreiter] = reiter.istGastreiter
it[nation] = reiter.nation
it[reiterLizenz] = reiter.reiterLizenz
it[startkarte] = reiter.startkarte
it[fahrLizenz] = reiter.fahrLizenz
it[altersklasseJgJrU25] = reiter.altersklasseJgJrU25
it[altersklasseY] = reiter.altersklasseY
it[mitgliedsNummer] = reiter.mitgliedsNummer
it[telefonNummer] = reiter.telefonNummer
it[kader] = reiter.kader
it[lastPayYear] = reiter.lastPayYear
it[geschlecht] = reiter.geschlecht
it[geburtsdatum] = reiter.geburtsdatum
it[feiId] = reiter.feiId
it[sperrListe] = reiter.sperrListe
it[lizenzInfo] = reiter.lizenzInfo
it[lizenzKlasse] = reiter.lizenzKlasse.name
it[istAktiv] = reiter.istAktiv
it[bemerkungen] = reiter.bemerkungen
it[datenQuelle] = reiter.datenQuelle.name
it[updatedAt] = reiter.updatedAt
}
@@ -178,33 +106,32 @@ class ExposedReiterRepository : ReiterRepository {
it[satznummer] = reiter.satznummer
it[nachname] = reiter.nachname
it[vorname] = reiter.vorname
it[geburtsdatum] = reiter.geburtsdatum
it[lizenzNummer] = reiter.lizenzNummer
it[lizenzKlasse] = reiter.lizenzKlasse.name
it[startkartAktiv] = reiter.startkartAktiv
it[startkartSaison] = reiter.startkartSaison
it[feiId] = reiter.feiId
it[nation] = reiter.nation
it[vereinsNummer] = reiter.vereinsNummer
it[bundeslandNummer] = reiter.bundeslandNummer
it[vereinsName] = reiter.vereinsName
it[istGastreiter] = reiter.istGastreiter
it[nation] = reiter.nation
it[reiterLizenz] = reiter.reiterLizenz
it[startkarte] = reiter.startkarte
it[fahrLizenz] = reiter.fahrLizenz
it[altersklasseJgJrU25] = reiter.altersklasseJgJrU25
it[altersklasseY] = reiter.altersklasseY
it[mitgliedsNummer] = reiter.mitgliedsNummer
it[telefonNummer] = reiter.telefonNummer
it[kader] = reiter.kader
it[lastPayYear] = reiter.lastPayYear
it[geschlecht] = reiter.geschlecht
it[geburtsdatum] = reiter.geburtsdatum
it[feiId] = reiter.feiId
it[sperrListe] = reiter.sperrListe
it[lizenzInfo] = reiter.lizenzInfo
it[lizenzKlasse] = reiter.lizenzKlasse.name
it[istAktiv] = reiter.istAktiv
it[bemerkungen] = reiter.bemerkungen
it[datenQuelle] = reiter.datenQuelle.name
it[createdAt] = reiter.createdAt
it[updatedAt] = reiter.updatedAt
}
}
// Sparten aktualisieren
ReiterSparteTable.deleteWhere { ReiterSparteTable.reiterId eq reiter.reiterId }
reiter.lizenzSparten.forEach { sparte ->
ReiterSparteTable.insert {
it[ReiterSparteTable.id] = Uuid.random()
it[ReiterSparteTable.reiterId] = reiter.reiterId
it[ReiterSparteTable.sparte] = sparte.name
}
}
reiter
}
@@ -221,12 +148,7 @@ class ExposedReiterRepository : ReiterRepository {
}
override suspend fun upsertBySatznummer(reiter: DomReiter): DomReiter = DatabaseFactory.dbQuery {
val existing = ReiterTable.selectAll().where { ReiterTable.satznummer eq reiter.satznummer }
.map { row ->
val id = row[ReiterTable.id]
rowToDomReiter(row, getSpartenForReiter(id))
}
.singleOrNull()
val existing = findBySatznummer(reiter.satznummer)
if (existing != null) {
val toUpdate = reiter.copy(reiterId = existing.reiterId)
@@ -13,13 +13,9 @@ import org.jetbrains.exposed.v1.datetime.timestamp
*/
object FunktionaerTable : Table("funktionaer") {
val id = uuid("funktionaer_id")
val richterNummer = varchar("richter_nummer", 10).nullable().uniqueIndex()
val vorname = varchar("vorname", 100)
val nachname = varchar("nachname", 100)
val geburtsdatum = date("geburtsdatum").nullable()
val email = varchar("email", 200).nullable()
val telefon = varchar("telefon", 50).nullable()
val vereinsNummer = varchar("vereins_nummer", 10).nullable()
val satzID = varchar("satz_id", 1).nullable()
val satzNummer = integer("satz_nummer").nullable()
val name = varchar("name", 200).nullable()
val istAktiv = bool("ist_aktiv").default(true)
val bemerkungen = text("bemerkungen").nullable()
val datenQuelle = varchar("daten_quelle", 50)
@@ -27,4 +23,18 @@ object FunktionaerTable : Table("funktionaer") {
val updatedAt = timestamp("updated_at").defaultExpression(CurrentTimestamp)
override val primaryKey = PrimaryKey(id)
init {
index("idx_funktionaer_satz", isUnique = true, satzID, satzNummer)
}
}
/**
* Exposed-Tabellendefinition für die Qualifikationen eines Funktionärs.
*/
object FunktionaerQualifikationTable : Table("funktionaer_qualifikation") {
val funktionaerId = uuid("funktionaer_id").references(FunktionaerTable.id)
val qualifikation = varchar("qualifikation", 20)
override val primaryKey = PrimaryKey(funktionaerId, qualifikation)
}
@@ -7,36 +7,33 @@ import at.mocode.core.domain.model.PferdeGeschlechtE
import at.mocode.core.utils.database.DatabaseFactory
import at.mocode.masterdata.domain.model.DomPferd
import at.mocode.masterdata.domain.repository.HorseRepository
import org.jetbrains.exposed.v1.core.*
import org.jetbrains.exposed.v1.jdbc.*
import org.jetbrains.exposed.v1.core.ResultRow
import org.jetbrains.exposed.v1.core.eq
import org.jetbrains.exposed.v1.core.like
import org.jetbrains.exposed.v1.jdbc.deleteWhere
import org.jetbrains.exposed.v1.jdbc.insert
import org.jetbrains.exposed.v1.jdbc.selectAll
import org.jetbrains.exposed.v1.jdbc.update
import kotlin.uuid.Uuid
/**
* Exposed-basierte Implementierung des Horse-Repositorys.
*/
class HorseRepositoryImpl : HorseRepository {
private fun rowToDomPferd(row: ResultRow): DomPferd {
return DomPferd(
pferdId = row[HorseTable.id],
kopfnummer = row[HorseTable.kopfnummer],
pferdeName = row[HorseTable.pferdeName],
geschlecht = PferdeGeschlechtE.valueOf(row[HorseTable.geschlecht]),
geburtsdatum = row[HorseTable.geburtsdatum],
rasse = row[HorseTable.rasse],
farbe = row[HorseTable.farbe],
besitzerId = row[HorseTable.besitzerId],
verantwortlichePersonId = row[HorseTable.verantwortlichePersonId],
zuechterName = row[HorseTable.zuechterName],
zuchtbuchNummer = row[HorseTable.zuchtbuchNummer],
lebensnummer = row[HorseTable.lebensnummer],
chipNummer = row[HorseTable.chipNummer],
passNummer = row[HorseTable.passNummer],
oepsNummer = row[HorseTable.oepsNummer],
feiNummer = row[HorseTable.feiNummer],
vaterName = row[HorseTable.vaterName],
mutterName = row[HorseTable.mutterName],
mutterVaterName = row[HorseTable.mutterVaterName],
stockmass = row[HorseTable.stockmass],
geschlecht = PferdeGeschlechtE.valueOf(row[HorseTable.geschlecht]),
geburtsjahr = row[HorseTable.geburtsjahr],
farbe = row[HorseTable.farbe],
abstammung = row[HorseTable.abstammung],
vereinNummer = row[HorseTable.vereinNummer],
lastPayYear = row[HorseTable.lastPayYear],
verantwortlichePersonId = row[HorseTable.verantwortlichePersonId],
vater = row[HorseTable.vater],
feiPass = row[HorseTable.feiPass],
satznummer = row[HorseTable.satznummer],
istAktiv = row[HorseTable.istAktiv],
bemerkungen = row[HorseTable.bemerkungen],
datenQuelle = DatenQuelleE.valueOf(row[HorseTable.datenQuelle]),
@@ -57,28 +54,15 @@ class HorseRepositoryImpl : HorseRepository {
.singleOrNull()
}
override suspend fun findByChipNummer(chipNummer: String): DomPferd? = DatabaseFactory.dbQuery {
HorseTable.selectAll().where { HorseTable.chipNummer eq chipNummer }
override suspend fun findBySatznummer(satznummer: String): DomPferd? = DatabaseFactory.dbQuery {
HorseTable.selectAll().where { HorseTable.satznummer eq satznummer }
.map(::rowToDomPferd)
.singleOrNull()
}
override suspend fun findByPassNummer(passNummer: String): DomPferd? = DatabaseFactory.dbQuery {
HorseTable.selectAll().where { HorseTable.passNummer eq passNummer }
override suspend fun findByKopfnummer(kopfnummer: String): List<DomPferd> = DatabaseFactory.dbQuery {
HorseTable.selectAll().where { HorseTable.kopfnummer eq kopfnummer }
.map(::rowToDomPferd)
.singleOrNull()
}
override suspend fun findByOepsNummer(oepsNummer: String): DomPferd? = DatabaseFactory.dbQuery {
HorseTable.selectAll().where { HorseTable.oepsNummer eq oepsNummer }
.map(::rowToDomPferd)
.singleOrNull()
}
override suspend fun findByFeiNummer(feiNummer: String): DomPferd? = DatabaseFactory.dbQuery {
HorseTable.selectAll().where { HorseTable.feiNummer eq feiNummer }
.map(::rowToDomPferd)
.singleOrNull()
}
override suspend fun findByName(searchTerm: String, limit: Int): List<DomPferd> = DatabaseFactory.dbQuery {
@@ -88,113 +72,29 @@ class HorseRepositoryImpl : HorseRepository {
.map(::rowToDomPferd)
}
override suspend fun findByOwnerId(ownerId: Uuid, activeOnly: Boolean): List<DomPferd> = DatabaseFactory.dbQuery {
val query = HorseTable.selectAll().where { HorseTable.besitzerId eq ownerId }
if (activeOnly) {
query.andWhere { HorseTable.istAktiv eq true }
}
query.map(::rowToDomPferd)
}
override suspend fun findByResponsiblePersonId(responsiblePersonId: Uuid, activeOnly: Boolean): List<DomPferd> =
DatabaseFactory.dbQuery {
val query = HorseTable.selectAll().where { HorseTable.verantwortlichePersonId eq responsiblePersonId }
if (activeOnly) {
query.andWhere { HorseTable.istAktiv eq true }
}
query.map(::rowToDomPferd)
}
override suspend fun findByGeschlecht(
geschlecht: PferdeGeschlechtE,
activeOnly: Boolean,
limit: Int
): List<DomPferd> = DatabaseFactory.dbQuery {
val query = HorseTable.selectAll().where { HorseTable.geschlecht eq geschlecht.name }
if (activeOnly) {
query.andWhere { HorseTable.istAktiv eq true }
}
query.limit(limit).map(::rowToDomPferd)
}
override suspend fun findByRasse(rasse: String, activeOnly: Boolean, limit: Int): List<DomPferd> =
DatabaseFactory.dbQuery {
val query = HorseTable.selectAll().where { HorseTable.rasse eq rasse }
if (activeOnly) {
query.andWhere { HorseTable.istAktiv eq true }
}
query.limit(limit).map(::rowToDomPferd)
}
override suspend fun findByBirthYear(birthYear: Int, activeOnly: Boolean): List<DomPferd> = DatabaseFactory.dbQuery {
// In Exposed v1 gibt es kein direktes year() für date Spalten ohne extra Extension.
// Wir suchen im Datumsbereich nach.
val startDate = kotlinx.datetime.LocalDate(birthYear, 1, 1)
val endDate = kotlinx.datetime.LocalDate(birthYear, 12, 31)
val query = HorseTable.selectAll()
.where { (HorseTable.geburtsdatum greaterEq startDate) and (HorseTable.geburtsdatum lessEq endDate) }
if (activeOnly) {
query.andWhere { HorseTable.istAktiv eq true }
}
query.map(::rowToDomPferd)
}
override suspend fun findByBirthYearRange(fromYear: Int, toYear: Int, activeOnly: Boolean): List<DomPferd> =
DatabaseFactory.dbQuery {
val startDate = kotlinx.datetime.LocalDate(fromYear, 1, 1)
val endDate = kotlinx.datetime.LocalDate(toYear, 12, 31)
val query = HorseTable.selectAll()
.where { (HorseTable.geburtsdatum greaterEq startDate) and (HorseTable.geburtsdatum lessEq endDate) }
if (activeOnly) {
query.andWhere { HorseTable.istAktiv eq true }
}
query.map(::rowToDomPferd)
}
override suspend fun findAllActive(limit: Int): List<DomPferd> = DatabaseFactory.dbQuery {
HorseTable.selectAll().where { HorseTable.istAktiv eq true }
.limit(limit)
.map(::rowToDomPferd)
}
override suspend fun findOepsRegistered(activeOnly: Boolean): List<DomPferd> = DatabaseFactory.dbQuery {
val query = HorseTable.selectAll().where { HorseTable.oepsNummer.isNotNull() }
if (activeOnly) {
query.andWhere { HorseTable.istAktiv eq true }
}
query.map(::rowToDomPferd)
}
override suspend fun findFeiRegistered(activeOnly: Boolean): List<DomPferd> = DatabaseFactory.dbQuery {
val query = HorseTable.selectAll().where { HorseTable.feiNummer.isNotNull() }
if (activeOnly) {
query.andWhere { HorseTable.istAktiv eq true }
}
query.map(::rowToDomPferd)
}
override suspend fun save(horse: DomPferd): DomPferd = DatabaseFactory.dbQuery {
val exists = HorseTable.selectAll().where { HorseTable.id eq horse.pferdId }.any()
if (exists) {
HorseTable.update({ HorseTable.id eq horse.pferdId }) {
it[kopfnummer] = horse.kopfnummer
it[pferdeName] = horse.pferdeName
it[geschlecht] = horse.geschlecht.name
it[geburtsdatum] = horse.geburtsdatum
it[rasse] = horse.rasse
it[farbe] = horse.farbe
it[besitzerId] = horse.besitzerId
it[verantwortlichePersonId] = horse.verantwortlichePersonId
it[zuechterName] = horse.zuechterName
it[zuchtbuchNummer] = horse.zuchtbuchNummer
it[lebensnummer] = horse.lebensnummer
it[chipNummer] = horse.chipNummer
it[passNummer] = horse.passNummer
it[oepsNummer] = horse.oepsNummer
it[feiNummer] = horse.feiNummer
it[vaterName] = horse.vaterName
it[mutterName] = horse.mutterName
it[mutterVaterName] = horse.mutterVaterName
it[stockmass] = horse.stockmass
it[geschlecht] = horse.geschlecht.name
it[geburtsjahr] = horse.geburtsjahr
it[farbe] = horse.farbe
it[abstammung] = horse.abstammung
it[vereinNummer] = horse.vereinNummer
it[lastPayYear] = horse.lastPayYear
it[verantwortlichePersonId] = horse.verantwortlichePersonId
it[vater] = horse.vater
it[feiPass] = horse.feiPass
it[satznummer] = horse.satznummer
it[istAktiv] = horse.istAktiv
it[bemerkungen] = horse.bemerkungen
it[datenQuelle] = horse.datenQuelle.name
@@ -204,24 +104,19 @@ class HorseRepositoryImpl : HorseRepository {
} else {
HorseTable.insert {
it[id] = horse.pferdId
it[kopfnummer] = horse.kopfnummer
it[pferdeName] = horse.pferdeName
it[geschlecht] = horse.geschlecht.name
it[geburtsdatum] = horse.geburtsdatum
it[rasse] = horse.rasse
it[farbe] = horse.farbe
it[besitzerId] = horse.besitzerId
it[verantwortlichePersonId] = horse.verantwortlichePersonId
it[zuechterName] = horse.zuechterName
it[zuchtbuchNummer] = horse.zuchtbuchNummer
it[lebensnummer] = horse.lebensnummer
it[chipNummer] = horse.chipNummer
it[passNummer] = horse.passNummer
it[oepsNummer] = horse.oepsNummer
it[feiNummer] = horse.feiNummer
it[vaterName] = horse.vaterName
it[mutterName] = horse.mutterName
it[mutterVaterName] = horse.mutterVaterName
it[stockmass] = horse.stockmass
it[geschlecht] = horse.geschlecht.name
it[geburtsjahr] = horse.geburtsjahr
it[farbe] = horse.farbe
it[abstammung] = horse.abstammung
it[vereinNummer] = horse.vereinNummer
it[lastPayYear] = horse.lastPayYear
it[verantwortlichePersonId] = horse.verantwortlichePersonId
it[vater] = horse.vater
it[feiPass] = horse.feiPass
it[satznummer] = horse.satznummer
it[istAktiv] = horse.istAktiv
it[bemerkungen] = horse.bemerkungen
it[datenQuelle] = horse.datenQuelle.name
@@ -240,50 +135,10 @@ class HorseRepositoryImpl : HorseRepository {
HorseTable.selectAll().where { HorseTable.lebensnummer eq lebensnummer }.any()
}
override suspend fun existsByChipNummer(chipNummer: String): Boolean = DatabaseFactory.dbQuery {
HorseTable.selectAll().where { HorseTable.chipNummer eq chipNummer }.any()
}
override suspend fun existsByPassNummer(passNummer: String): Boolean = DatabaseFactory.dbQuery {
HorseTable.selectAll().where { HorseTable.passNummer eq passNummer }.any()
}
override suspend fun existsByOepsNummer(oepsNummer: String): Boolean = DatabaseFactory.dbQuery {
HorseTable.selectAll().where { HorseTable.oepsNummer eq oepsNummer }.any()
}
override suspend fun existsByFeiNummer(feiNummer: String): Boolean = DatabaseFactory.dbQuery {
HorseTable.selectAll().where { HorseTable.feiNummer eq feiNummer }.any()
}
override suspend fun countActive(): Long = DatabaseFactory.dbQuery {
HorseTable.selectAll().where { HorseTable.istAktiv eq true }.count()
}
override suspend fun countByOwnerId(ownerId: Uuid, activeOnly: Boolean): Long = DatabaseFactory.dbQuery {
val query = HorseTable.selectAll().where { HorseTable.besitzerId eq ownerId }
if (activeOnly) {
query.andWhere { HorseTable.istAktiv eq true }
}
query.count()
}
override suspend fun countOepsRegistered(activeOnly: Boolean): Long = DatabaseFactory.dbQuery {
val query = HorseTable.selectAll().where { HorseTable.oepsNummer.isNotNull() }
if (activeOnly) {
query.andWhere { HorseTable.istAktiv eq true }
}
query.count()
}
override suspend fun countFeiRegistered(activeOnly: Boolean): Long = DatabaseFactory.dbQuery {
val query = HorseTable.selectAll().where { HorseTable.feiNummer.isNotNull() }
if (activeOnly) {
query.andWhere { HorseTable.istAktiv eq true }
}
query.count()
}
override suspend fun upsertByLebensnummer(horse: DomPferd): DomPferd = DatabaseFactory.dbQuery {
val lebensnummer = horse.lebensnummer ?: return@dbQuery save(horse)
@@ -294,24 +149,19 @@ class HorseRepositoryImpl : HorseRepository {
if (existing != null) {
val toUpdate = horse.copy(pferdId = existing.pferdId)
HorseTable.update({ HorseTable.id eq existing.pferdId }) {
it[kopfnummer] = toUpdate.kopfnummer
it[pferdeName] = toUpdate.pferdeName
it[geschlecht] = toUpdate.geschlecht.name
it[geburtsdatum] = toUpdate.geburtsdatum
it[rasse] = toUpdate.rasse
it[farbe] = toUpdate.farbe
it[besitzerId] = toUpdate.besitzerId
it[verantwortlichePersonId] = toUpdate.verantwortlichePersonId
it[zuechterName] = toUpdate.zuechterName
it[zuchtbuchNummer] = toUpdate.zuchtbuchNummer
it[HorseTable.lebensnummer] = toUpdate.lebensnummer
it[chipNummer] = toUpdate.chipNummer
it[passNummer] = toUpdate.passNummer
it[oepsNummer] = toUpdate.oepsNummer
it[feiNummer] = toUpdate.feiNummer
it[vaterName] = toUpdate.vaterName
it[mutterName] = toUpdate.mutterName
it[mutterVaterName] = toUpdate.mutterVaterName
it[stockmass] = toUpdate.stockmass
it[geschlecht] = toUpdate.geschlecht.name
it[geburtsjahr] = toUpdate.geburtsjahr
it[farbe] = toUpdate.farbe
it[abstammung] = toUpdate.abstammung
it[vereinNummer] = toUpdate.vereinNummer
it[lastPayYear] = toUpdate.lastPayYear
it[verantwortlichePersonId] = toUpdate.verantwortlichePersonId
it[vater] = toUpdate.vater
it[feiPass] = toUpdate.feiPass
it[satznummer] = toUpdate.satznummer
it[istAktiv] = toUpdate.istAktiv
it[bemerkungen] = toUpdate.bemerkungen
it[datenQuelle] = toUpdate.datenQuelle.name
@@ -322,4 +172,68 @@ class HorseRepositoryImpl : HorseRepository {
save(horse)
}
}
override suspend fun upsertBySatznummer(horse: DomPferd): DomPferd = DatabaseFactory.dbQuery {
val satznummer = horse.satznummer ?: return@dbQuery save(horse)
val existing = HorseTable.selectAll().where { HorseTable.satznummer eq satznummer }
.map(::rowToDomPferd)
.singleOrNull()
if (existing != null) {
val toUpdate = horse.copy(pferdId = existing.pferdId)
HorseTable.update({ HorseTable.id eq existing.pferdId }) {
it[kopfnummer] = toUpdate.kopfnummer
it[pferdeName] = toUpdate.pferdeName
it[lebensnummer] = toUpdate.lebensnummer
it[geschlecht] = toUpdate.geschlecht.name
it[geburtsjahr] = toUpdate.geburtsjahr
it[farbe] = toUpdate.farbe
it[abstammung] = toUpdate.abstammung
it[vereinNummer] = toUpdate.vereinNummer
it[lastPayYear] = toUpdate.lastPayYear
it[verantwortlichePersonId] = toUpdate.verantwortlichePersonId
it[vater] = toUpdate.vater
it[feiPass] = toUpdate.feiPass
it[HorseTable.satznummer] = toUpdate.satznummer
it[istAktiv] = toUpdate.istAktiv
it[bemerkungen] = toUpdate.bemerkungen
it[datenQuelle] = toUpdate.datenQuelle.name
it[updatedAt] = toUpdate.updatedAt
}
toUpdate
} else {
save(horse)
}
}
// Not implemented or needed based on current requirements/DomPferd state
override suspend fun findByChipNummer(chipNummer: String): DomPferd? = null
override suspend fun findByPassNummer(passNummer: String): DomPferd? = null
override suspend fun findByOepsNummer(oepsNummer: String): DomPferd? = null
override suspend fun findByFeiNummer(feiNummer: String): DomPferd? = null
override suspend fun findByOwnerId(ownerId: Uuid, activeOnly: Boolean): List<DomPferd> = emptyList()
override suspend fun findByResponsiblePersonId(responsiblePersonId: Uuid, activeOnly: Boolean): List<DomPferd> =
emptyList()
override suspend fun findByGeschlecht(
geschlecht: PferdeGeschlechtE,
activeOnly: Boolean,
limit: Int
): List<DomPferd> = emptyList()
override suspend fun findByRasse(rasse: String, activeOnly: Boolean, limit: Int): List<DomPferd> = emptyList()
override suspend fun findByBirthYear(birthYear: Int, activeOnly: Boolean): List<DomPferd> = emptyList()
override suspend fun findByBirthYearRange(fromYear: Int, toYear: Int, activeOnly: Boolean): List<DomPferd> =
emptyList()
override suspend fun findOepsRegistered(activeOnly: Boolean): List<DomPferd> = emptyList()
override suspend fun findFeiRegistered(activeOnly: Boolean): List<DomPferd> = emptyList()
override suspend fun existsByChipNummer(chipNummer: String): Boolean = false
override suspend fun existsByPassNummer(passNummer: String): Boolean = false
override suspend fun existsByOepsNummer(oepsNummer: String): Boolean = false
override suspend fun existsByFeiNummer(feiNummer: String): Boolean = false
override suspend fun countByOwnerId(ownerId: Uuid, activeOnly: Boolean): Long = 0
override suspend fun countOepsRegistered(activeOnly: Boolean): Long = 0
override suspend fun countFeiRegistered(activeOnly: Boolean): Long = 0
}
@@ -4,32 +4,27 @@ package at.mocode.masterdata.infrastructure.persistence
import org.jetbrains.exposed.v1.core.Table
import org.jetbrains.exposed.v1.datetime.CurrentTimestamp
import org.jetbrains.exposed.v1.datetime.date
import org.jetbrains.exposed.v1.datetime.timestamp
/**
* Exposed-Tabellendefinition für die Pferd-Entität.
* Exposed-Tabellendefinition für die Pferd-Entität basierend auf PFERDE01.DAT.
*/
object HorseTable : Table("horse") {
val id = uuid("horse_id")
val kopfnummer = varchar("kopfnummer", 4).nullable().index()
val pferdeName = varchar("pferde_name", 200).index()
val geschlecht = varchar("geschlecht", 20)
val geburtsdatum = date("geburtsdatum").nullable()
val rasse = varchar("rasse", 100).nullable()
val farbe = varchar("farbe", 100).nullable()
val besitzerId = uuid("besitzer_id").nullable()
val verantwortlichePersonId = uuid("verantwortliche_person_id").nullable()
val zuechterName = varchar("zuechter_name", 200).nullable()
val zuchtbuchNummer = varchar("zuchtbuch_nummer", 50).nullable()
val lebensnummer = varchar("lebensnummer", 50).nullable().index()
val chipNummer = varchar("chip_nummer", 50).nullable()
val passNummer = varchar("pass_nummer", 50).nullable()
val oepsNummer = varchar("oeps_nummer", 50).nullable()
val feiNummer = varchar("fei_nummer", 50).nullable()
val vaterName = varchar("vater_name", 200).nullable()
val mutterName = varchar("mutter_name", 200).nullable()
val mutterVaterName = varchar("mutter_vater_name", 200).nullable()
val stockmass = integer("stockmass").nullable()
val geschlecht = varchar("geschlecht", 20)
val geburtsjahr = integer("geburtsjahr").nullable()
val farbe = varchar("farbe", 100).nullable()
val abstammung = varchar("abstammung", 100).nullable()
val vereinNummer = integer("verein_nummer").nullable()
val lastPayYear = integer("last_pay_year").nullable()
val verantwortlichePersonId = varchar("verantwortliche_person_id", 100).nullable()
val vater = varchar("vater", 200).nullable()
val feiPass = varchar("fei_pass", 50).nullable()
val satznummer = varchar("satznummer", 10).nullable()
val istAktiv = bool("ist_aktiv").default(true)
val bemerkungen = text("bemerkungen").nullable()
val datenQuelle = varchar("daten_quelle", 50)
@@ -37,4 +32,8 @@ object HorseTable : Table("horse") {
val updatedAt = timestamp("updated_at").defaultExpression(CurrentTimestamp)
override val primaryKey = PrimaryKey(id)
init {
index("idx_horse_satznummer", isUnique = true, satznummer)
}
}
@@ -2,6 +2,7 @@
package at.mocode.masterdata.infrastructure.persistence
import at.mocode.core.domain.model.LizenzKlasseE
import org.jetbrains.exposed.v1.core.Table
import org.jetbrains.exposed.v1.datetime.CurrentTimestamp
import org.jetbrains.exposed.v1.datetime.date
@@ -13,20 +14,30 @@ import org.jetbrains.exposed.v1.datetime.timestamp
object ReiterTable : Table("reiter") {
val id = uuid("reiter_id")
val personId = uuid("person_id")
val satznummer = varchar("satznummer", 10).uniqueIndex()
val lizenzNummer = varchar("lizenz_nummer", 20).nullable()
val lizenzKlasse = varchar("lizenz_klasse", 20)
val startkartAktiv = bool("startkart_aktiv").default(false)
val startkartSaison = integer("startkart_saison").nullable()
val feiId = varchar("fei_id", 20).nullable()
val nation = varchar("nation", 3).nullable()
val satznummer = varchar("satznummer", 10).nullable()
val nachname = varchar("nachname", 100)
val vorname = varchar("vorname", 100)
val geburtsdatum = date("geburtsdatum").nullable()
val vereinsNummer = varchar("vereins_nummer", 10).nullable()
val bundeslandNummer = integer("bundesland_nummer").nullable()
val vereinsName = varchar("vereins_name", 200).nullable()
val istGastreiter = bool("ist_gastreiter").default(false)
val nation = varchar("nation", 10).nullable()
val reiterLizenz = varchar("reiter_lizenz", 20).nullable()
val startkarte = varchar("startkarte", 20).nullable()
val fahrLizenz = varchar("fahr_lizenz", 20).nullable()
val altersklasseJgJrU25 = varchar("altersklasse_jg_jr_u25", 10).nullable()
val altersklasseY = varchar("altersklasse_y", 10).nullable()
val mitgliedsNummer = integer("mitglieds_nummer").nullable()
val telefonNummer = varchar("telefon_nummer", 50).nullable()
val kader = varchar("kader", 50).nullable()
val lastPayYear = integer("last_pay_year").nullable()
val geschlecht = varchar("geschlecht", 10).nullable()
val geburtsdatum = date("geburtsdatum").nullable()
val feiId = varchar("fei_id", 20).nullable()
val sperrListe = varchar("sperr_liste", 50).nullable()
val lizenzInfo = varchar("lizenz_info", 100).nullable()
val lizenzKlasse = varchar("lizenz_klasse", 50).default(LizenzKlasseE.LIZENZFREI.name)
val istAktiv = bool("ist_aktiv").default(true)
val bemerkungen = text("bemerkungen").nullable()
val datenQuelle = varchar("daten_quelle", 50)
val createdAt = timestamp("created_at").defaultExpression(CurrentTimestamp)
val updatedAt = timestamp("updated_at").defaultExpression(CurrentTimestamp)
@@ -98,9 +98,7 @@ class RegulationSeedVerificationTest {
satznummer = "123456",
nachname = "Müller",
vorname = "Hans",
lizenzKlasse = LizenzKlasseE.R1,
lizenzSparten = listOf(SparteE.SPRINGEN),
startkartAktiv = true
lizenzKlasse = LizenzKlasseE.R1
)
val klasseL = TurnierklasseDefinition(
@@ -5,6 +5,7 @@ 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.importer.ZnsImportService
import at.mocode.zns.importer.ZnsImportResult
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -27,8 +28,30 @@ class ZnsImportOrchestrator(
val service = ZnsImportService(vereinRepository, reiterRepository, horseRepository, funktionaerRepository)
val dateien = service.extrahiereDateien(zipBytes.inputStream())
jobRegistry.aktualisiereStatus(jobId, ImportJobStatus.LADE_VEREINE, "Lade Vereine...", 20)
val result = service.importiereZip(zipBytes.inputStream())
val vereineResult = service.importiereVereine(dateien["VEREIN01.DAT"] ?: emptyList(), mutableListOf())
jobRegistry.aktualisiereStatus(jobId, ImportJobStatus.LADE_REITER, "Lade Reiter...", 40)
val reiterResult = service.importiereReiter(dateien["LIZENZ01.DAT"] ?: emptyList(), mutableListOf(), mutableListOf())
jobRegistry.aktualisiereStatus(jobId, ImportJobStatus.LADE_PFERDE, "Lade Pferde...", 60)
val pferdeResult = service.importierePferde(dateien["PFERDE01.DAT"] ?: emptyList(), mutableListOf())
jobRegistry.aktualisiereStatus(jobId, ImportJobStatus.LADE_RICHTER, "Lade Funktionäre...", 80)
val richterResult = service.importiereFunktionaere(dateien["RICHT01.DAT"] ?: emptyList(), mutableListOf(), mutableListOf())
val result = ZnsImportResult(
vereineImportiert = vereineResult.first,
vereineAktualisiert = vereineResult.second,
reiterImportiert = reiterResult.first,
reiterAktualisiert = reiterResult.second,
pferdeImportiert = pferdeResult.first,
pferdeAktualisiert = pferdeResult.second,
richterImportiert = richterResult.first,
richterAktualisiert = richterResult.second
)
jobRegistry.aktualisiereStatus(
jobId, ImportJobStatus.ABGESCHLOSSEN,
@@ -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())
}
}
@@ -373,7 +373,7 @@ Kopfzeile - **KKARTEI**
| MITGLIEDSNUMMER | 147 | 8 | Numerisch | FORMAT: 999999999 |
| TELEFONNUMMER | 155 | 21 | Alphanumerisch (21) | Standard: BLANK |
| KADER | 176 | 1 | Alphanumerisch (1) | derzeit immer BLANK |
| JAHR (letzte Zahlung) | 177 | 4 | Numerisch FORMAT: 9999 |
| JAHR (letzte Zahlung) | 177 | 4 | Numerisch FORMAT: 9999 | |
| GESCHLECHT | 181 | 1 | Alphanumerisch (1) | Werte: `W`, `M` |
| GEBURTSDATUM | 182 | 8 | Datum | FORMAT: `JJJJMMTT` |
| FEI-ID | 190 | 10 | Alphanumerisch (10) | Standard: BLANK (10) |
+16 -5
View File
@@ -1,6 +1,17 @@
## ToDos und Folgearbeiten
- 📜 Rulebook Expert: DetailSpezifikation `SEPARATE_SIEGEREHRUNG` (Preisgeld, Ranking, UIHinweise) ergänzen.
- 🧹 Curator: `Ubiquitous_Language.md` um obige Begriffe/Definitionen erweitern.
- 👷 Backend: SchemaMigrationen pro Tenant gemäß obiger Tabellen; Repositories/Services entsprechend zuschneiden.
- 🎨 Frontend: ViewModels/Stores entlang dieser Struktur aktualisieren (Navigation: Veranstaltung → Turnier → Bewerb → Abteilung).
# ToDos
Bitte analysieren, vervollständigen bzw. korrigieren und optimieren.
Anschließend alle betroffene Dokumentationen aktualisieren.
## ZNS-Importer
Die Aufgabe des ZNS-Importer ist die vom OEPS zur Verfügung gestellten Daten
- eingeben zu nehmen
- diese sauber in unsere Datenbank zu übertragen
- die ZNS-Daten aus unserer Datenbank im System zur Verfügung stellen
Welche Daten und in welcher Form die ZNS-Daten vom Verband zur Verfügung gestellt werden, ist im Pflichtenheft genau Dokumentiert