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 @Configuration
class GatewayConfig( 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 @Bean
@@ -27,6 +28,10 @@ class GatewayConfig(
} }
uri(pingServiceUrl) 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 -> .authorizeExchange { exchanges ->
exchanges exchanges
.pathMatchers(*securityProperties.publicPaths.toTypedArray()).permitAll() .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() .anyExchange().authenticated()
} }
.oauth2ResourceServer { oauth2 -> .oauth2ResourceServer { oauth2 ->
@@ -49,6 +49,32 @@ class ZnsImportService(
private const val FILE_RICHT = "RICHT01.DAT" 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]. * Importiert eine ZNS-ZIP-Datei aus einem [InputStream].
* *
@@ -56,18 +82,7 @@ class ZnsImportService(
* @return [ZnsImportResult] mit Statistiken und eventuellen Fehlern. * @return [ZnsImportResult] mit Statistiken und eventuellen Fehlern.
*/ */
suspend fun importiereZip(zipInputStream: InputStream): ZnsImportResult { suspend fun importiereZip(zipInputStream: InputStream): ZnsImportResult {
val dateien = mutableMapOf<String, List<String>>() val dateien = extrahiereDateien(zipInputStream)
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 fehler = mutableListOf<String>() val fehler = mutableListOf<String>()
val warnungen = mutableListOf<String>() val warnungen = mutableListOf<String>()
@@ -75,7 +90,7 @@ class ZnsImportService(
val (vereineNeu, vereineUpd) = importiereVereine(dateien[FILE_VEREIN] ?: emptyList(), fehler) val (vereineNeu, vereineUpd) = importiereVereine(dateien[FILE_VEREIN] ?: emptyList(), fehler)
val (reiterNeu, reiterUpd) = importiereReiter(dateien[FILE_LIZENZ] ?: emptyList(), fehler, warnungen) val (reiterNeu, reiterUpd) = importiereReiter(dateien[FILE_LIZENZ] ?: emptyList(), fehler, warnungen)
val (pferdeNeu, pferdeUpd) = importierePferde(dateien[FILE_PFERDE] ?: emptyList(), fehler) 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( return ZnsImportResult(
vereineImportiert = vereineNeu, vereineImportiert = vereineNeu,
@@ -95,7 +110,7 @@ class ZnsImportService(
// Private Hilfsmethoden // Private Hilfsmethoden
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
private suspend fun importiereVereine( suspend fun importiereVereine(
zeilen: List<String>, zeilen: List<String>,
fehler: MutableList<String> fehler: MutableList<String>
): Pair<Int, Int> { ): Pair<Int, Int> {
@@ -131,7 +146,7 @@ class ZnsImportService(
return Pair(neu, aktualisiert) return Pair(neu, aktualisiert)
} }
private suspend fun importiereReiter( suspend fun importiereReiter(
zeilen: List<String>, zeilen: List<String>,
fehler: MutableList<String>, fehler: MutableList<String>,
warnungen: MutableList<String> warnungen: MutableList<String>
@@ -150,8 +165,23 @@ class ZnsImportService(
vorhanden.copy( vorhanden.copy(
vorname = reiter.vorname, vorname = reiter.vorname,
nachname = reiter.nachname, nachname = reiter.nachname,
bundeslandNummer = reiter.bundeslandNummer,
vereinsName = reiter.vereinsName, vereinsName = reiter.vereinsName,
nation = reiter.nation, 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, lizenzKlasse = reiter.lizenzKlasse,
istAktiv = reiter.istAktiv, istAktiv = reiter.istAktiv,
datenQuelle = reiter.datenQuelle datenQuelle = reiter.datenQuelle
@@ -166,7 +196,7 @@ class ZnsImportService(
return Pair(neu, aktualisiert) return Pair(neu, aktualisiert)
} }
private suspend fun importierePferde( suspend fun importierePferde(
zeilen: List<String>, zeilen: List<String>,
fehler: MutableList<String> fehler: MutableList<String>
): Pair<Int, Int> { ): Pair<Int, Int> {
@@ -175,9 +205,12 @@ class ZnsImportService(
zeilen.forEachIndexed { index, zeile -> zeilen.forEachIndexed { index, zeile ->
runCatching { runCatching {
val pferd = ZnsLegacyParsers.parsePferd(zeile) ?: return@forEachIndexed val pferd = ZnsLegacyParsers.parsePferd(zeile) ?: return@forEachIndexed
val vorhanden = pferd.lebensnummer if (pferd.pferdeName.isBlank()) return@forEachIndexed
?.takeIf { it.isNotBlank() }
?.let { horseRepository.findByLebensnummer(it) } // 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) { if (vorhanden == null) {
horseRepository.save(pferd) horseRepository.save(pferd)
neu++ neu++
@@ -186,10 +219,17 @@ class ZnsImportService(
vorhanden.copy( vorhanden.copy(
pferdeName = pferd.pferdeName, pferdeName = pferd.pferdeName,
geschlecht = pferd.geschlecht, geschlecht = pferd.geschlecht,
geburtsdatum = pferd.geburtsdatum, geburtsjahr = pferd.geburtsjahr,
rasse = pferd.rasse, farbe = pferd.farbe,
abstammung = pferd.abstammung,
vereinNummer = pferd.vereinNummer,
lastPayYear = pferd.lastPayYear,
verantwortlichePersonId = pferd.verantwortlichePersonId,
lebensnummer = pferd.lebensnummer, lebensnummer = pferd.lebensnummer,
oepsNummer = pferd.oepsNummer, kopfnummer = pferd.kopfnummer,
satznummer = pferd.satznummer,
vater = pferd.vater,
feiPass = pferd.feiPass,
istAktiv = pferd.istAktiv, istAktiv = pferd.istAktiv,
datenQuelle = pferd.datenQuelle datenQuelle = pferd.datenQuelle
).withUpdatedTimestamp() ).withUpdatedTimestamp()
@@ -203,7 +243,7 @@ class ZnsImportService(
return Pair(neu, aktualisiert) return Pair(neu, aktualisiert)
} }
private suspend fun importiereRichter( suspend fun importiereFunktionaere(
zeilen: List<String>, zeilen: List<String>,
fehler: MutableList<String>, fehler: MutableList<String>,
warnungen: MutableList<String> warnungen: MutableList<String>
@@ -212,24 +252,22 @@ class ZnsImportService(
var aktualisiert = 0 var aktualisiert = 0
zeilen.forEachIndexed { index, zeile -> zeilen.forEachIndexed { index, zeile ->
runCatching { runCatching {
val richter = ZnsLegacyParsers.parseRichter(zeile) ?: return@forEachIndexed val funktionaer = ZnsLegacyParsers.parseFunktionaer(zeile) ?: return@forEachIndexed
val richterNummer = richter.richterNummer ?: run { val satzID = funktionaer.satzID
warnungen.add("$FILE_RICHT Zeile ${index + 1}: Keine RichterNummer übersprungen.") val satzNummer = funktionaer.satzNummer
return@forEachIndexed val vorhanden = funktionaerRepository.findBySatz(satzID, satzNummer)
}
val vorhanden = funktionaerRepository.findByRichterNummer(richterNummer)
if (vorhanden == null) { if (vorhanden == null) {
funktionaerRepository.save(richter) funktionaerRepository.save(funktionaer)
neu++ neu++
} else { } else {
funktionaerRepository.save( funktionaerRepository.save(
vorhanden.copy( vorhanden.copy(
vorname = richter.vorname, satzID = satzID,
nachname = richter.nachname, satzNummer = satzNummer,
vereinsNummer = richter.vereinsNummer, name = funktionaer.name,
richterNummer = richter.richterNummer, qualifikationen = funktionaer.qualifikationen,
istAktiv = richter.istAktiv, istAktiv = funktionaer.istAktiv,
datenQuelle = richter.datenQuelle datenQuelle = funktionaer.datenQuelle
).withUpdatedTimestamp() ).withUpdatedTimestamp()
) )
aktualisiert++ aktualisiert++
@@ -74,7 +74,8 @@ class ZnsImportServiceTest {
return satznummer.padEnd(6) + return satznummer.padEnd(6) +
nachname.padEnd(50) + nachname.padEnd(50) +
vorname.padEnd(25) + 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). */ /** Erzeugt eine gültige PFERDE01.DAT-Zeile (mind. 211 Zeichen). */
@@ -88,17 +89,19 @@ class ZnsImportServiceTest {
lebensnummer.padEnd(9) + lebensnummer.padEnd(9) +
"W" + // Geschlecht: Wallach "W" + // Geschlecht: Wallach
"2015" + // Geburtsjahr "2015" + // Geburtsjahr
" ".repeat(157) // Auffüllen bis Stelle 201 " ".repeat(157) // Auffüllen bis Stelle 201 (1 bis 201 = 201 Zeichen)
return base + "SAT0000001".padEnd(10) // Satznummer ab Stelle 202 return base + "1234567890".padEnd(10) // Satznummer ab Stelle 202
} }
/** Erzeugt eine gültige RICHT01.DAT-Zeile (mind. 83 Zeichen). */ /** Erzeugt eine gültige RICHT01.DAT-Zeile (mind. 83 Zeichen). */
private fun richterZeile( private fun funktionaerZeile(
satznummer: String = "R00001", typ: String = "X",
name: String = "Huber, Anna" satznummer: String = "123456",
name: String = "Huber, Anna",
qualifikationen: String = "GA"
): String { ): String {
// Stelle 1: Typ, 2-7: Satznummer (6), 8-82: Name (75) // Stelle 1: Typ (X=Richter, Y=Parcoursbauer), 2-7: Satznummer (6), 8-82: Name (75), 83-112: Quali (30)
return "R" + satznummer.padEnd(6) + name.padEnd(75) return typ + satznummer.padEnd(6) + name.padEnd(75) + qualifikationen.padEnd(30)
} }
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
@@ -154,6 +157,7 @@ class ZnsImportServiceTest {
fun `importiereZip - neue Pferde werden gespeichert`() = runTest { fun `importiereZip - neue Pferde werden gespeichert`() = runTest {
val zip = buildZip("PFERDE01.DAT" to pferdeZeile()) val zip = buildZip("PFERDE01.DAT" to pferdeZeile())
coEvery { horseRepository.findBySatznummer(any()) } returns null
coEvery { horseRepository.findByLebensnummer(any()) } returns null coEvery { horseRepository.findByLebensnummer(any()) } returns null
coEvery { horseRepository.save(any()) } answers { firstArg<DomPferd>() } coEvery { horseRepository.save(any()) } answers { firstArg<DomPferd>() }
@@ -166,10 +170,10 @@ class ZnsImportServiceTest {
} }
@Test @Test
fun `importiereZip - neue Richter werden gespeichert`() = runTest { fun `importiereZip - neue Funktionaere werden gespeichert`() = runTest {
val zip = buildZip("RICHT01.DAT" to richterZeile()) 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>() } coEvery { funktionaerRepository.save(any()) } answers { firstArg<DomFunktionaer>() }
val result = service.importiereZip(zip) val result = service.importiereZip(zip)
@@ -180,22 +184,40 @@ class ZnsImportServiceTest {
coVerify(exactly = 1) { funktionaerRepository.save(any<DomFunktionaer>()) } 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 @Test
fun `importiereZip - vollstaendiger Import aller vier Dateien`() = runTest { fun `importiereZip - vollstaendiger Import aller vier Dateien`() = runTest {
val zip = buildZip( val zip = buildZip(
"VEREIN01.DAT" to vereinZeile(), "VEREIN01.DAT" to vereinZeile(),
"LIZENZ01.DAT" to lizenzZeile(), "LIZENZ01.DAT" to lizenzZeile(),
"PFERDE01.DAT" to pferdeZeile(), "PFERDE01.DAT" to pferdeZeile(),
"RICHT01.DAT" to richterZeile() "RICHT01.DAT" to funktionaerZeile()
) )
coEvery { vereinRepository.findByVereinsNummer(any()) } returns null coEvery { vereinRepository.findByVereinsNummer(any()) } returns null
coEvery { vereinRepository.save(any()) } answers { firstArg<DomVerein>() } coEvery { vereinRepository.save(any()) } answers { firstArg<DomVerein>() }
coEvery { reiterRepository.findBySatznummer(any()) } returns null coEvery { reiterRepository.findBySatznummer(any()) } returns null
coEvery { reiterRepository.save(any()) } answers { firstArg<DomReiter>() } coEvery { reiterRepository.save(any()) } answers { firstArg<DomReiter>() }
coEvery { horseRepository.findBySatznummer(any()) } returns null
coEvery { horseRepository.findByLebensnummer(any()) } returns null coEvery { horseRepository.findByLebensnummer(any()) } returns null
coEvery { horseRepository.save(any()) } answers { firstArg<DomPferd>() } 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>() } coEvery { funktionaerRepository.save(any()) } answers { firstArg<DomFunktionaer>() }
val result = service.importiereZip(zip) val result = service.importiereZip(zip)
@@ -24,20 +24,12 @@ import kotlin.uuid.Uuid
*/ */
class FunktionaerController(private val funktionaerRepository: FunktionaerRepository) { class FunktionaerController(private val funktionaerRepository: FunktionaerRepository) {
@Serializable
data class FunktionaerDto( data class FunktionaerDto(
val funktionaerId: String, val funktionaerId: String,
val richterNummer: String? = null, val satzID: String,
val vorname: String, val satzNummer: Int,
val nachname: String, val name: String? = null,
@Serializable(with = LocalDateSerializer::class) val qualifikationen: List<String> = emptyList(),
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 istAktiv: Boolean, val istAktiv: Boolean,
val bemerkungen: String? = null, val bemerkungen: String? = null,
@Serializable(with = InstantSerializer::class) @Serializable(with = InstantSerializer::class)
@@ -46,33 +38,18 @@ class FunktionaerController(private val funktionaerRepository: FunktionaerReposi
@Serializable @Serializable
data class FunktionaerCreateRequest( data class FunktionaerCreateRequest(
val richterNummer: String? = null, val satzID: String,
val vorname: String, val satzNummer: Int,
val nachname: String, val name: String? = null,
@Serializable(with = LocalDateSerializer::class) val qualifikationen: List<String> = emptyList(),
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 istAktiv: Boolean = true, val istAktiv: Boolean = true,
val bemerkungen: String? = null val bemerkungen: String? = null
) )
@Serializable @Serializable
data class FunktionaerUpdateRequest( data class FunktionaerUpdateRequest(
val vorname: String? = null, val name: String? = null,
val nachname: String? = null, val qualifikationen: List<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 istAktiv: Boolean? = null, val istAktiv: Boolean? = null,
val bemerkungen: String? = null val bemerkungen: String? = null
) )
@@ -81,29 +58,22 @@ class FunktionaerController(private val funktionaerRepository: FunktionaerReposi
route("/funktionaer") { route("/funktionaer") {
/** /**
* GET /funktionaer — Alle Funktionäre (paginiert), optional gefiltert nach rolle. * GET /funktionaer — Alle Funktionäre (paginiert).
*/ */
get { get {
val rolleParam = call.request.queryParameters["rolle"]
val limit = call.request.queryParameters["limit"]?.toIntOrNull() ?: 100 val limit = call.request.queryParameters["limit"]?.toIntOrNull() ?: 100
val offset = call.request.queryParameters["offset"]?.toIntOrNull() ?: 0 val offset = call.request.queryParameters["offset"]?.toIntOrNull() ?: 0
val results = if (rolleParam != null) { val results = funktionaerRepository.findAll(limit, offset)
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)
}
call.respond(results.map { it.toDto() }) 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") { get("/search") {
val query = call.request.queryParameters["q"] ?: "" val query = call.request.queryParameters["q"]?.toIntOrNull() ?: 0
val results = funktionaerRepository.findByName(query) val results = funktionaerRepository.findAll(100, 0).filter { it.satzNummer == query }
call.respond(results.map { it.toDto() }) 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}") { get("/satz/{satzID}/{satzNummer}") {
val nr = call.parameters["nr"] ?: return@get call.respond(HttpStatusCode.BadRequest) val satzID = call.parameters["satzID"] ?: return@get call.respond(HttpStatusCode.BadRequest)
val funktionaer = funktionaerRepository.findByRichterNummer(nr) 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) if (funktionaer != null) call.respond(funktionaer.toDto()) else call.respond(HttpStatusCode.NotFound)
} }
@@ -130,29 +101,11 @@ class FunktionaerController(private val funktionaerRepository: FunktionaerReposi
*/ */
post { post {
val req = call.receive<FunktionaerCreateRequest>() 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( val domFunktionaer = DomFunktionaer(
richterNummer = req.richterNummer, satzID = req.satzID,
vorname = req.vorname, satzNummer = req.satzNummer,
nachname = req.nachname, name = req.name,
geburtsdatum = req.geburtsdatum, qualifikationen = req.qualifikationen,
rollen = rollen,
richterQualifikation = richterQualifikation,
qualifiziertFuerSparten = sparten,
email = req.email,
telefon = req.telefon,
vereinsNummer = req.vereinsNummer,
istAktiv = req.istAktiv, istAktiv = req.istAktiv,
bemerkungen = req.bemerkungen 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 existing = funktionaerRepository.findById(id) ?: return@put call.respond(HttpStatusCode.NotFound)
val req = call.receive<FunktionaerUpdateRequest>() 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( val updated = existing.copy(
vorname = req.vorname ?: existing.vorname, name = req.name ?: existing.name,
nachname = req.nachname ?: existing.nachname, qualifikationen = req.qualifikationen ?: existing.qualifikationen,
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,
istAktiv = req.istAktiv ?: existing.istAktiv, istAktiv = req.istAktiv ?: existing.istAktiv,
bemerkungen = req.bemerkungen ?: existing.bemerkungen bemerkungen = req.bemerkungen ?: existing.bemerkungen
) )
@@ -221,16 +146,10 @@ class FunktionaerController(private val funktionaerRepository: FunktionaerReposi
private fun DomFunktionaer.toDto() = FunktionaerDto( private fun DomFunktionaer.toDto() = FunktionaerDto(
funktionaerId = funktionaerId.toString(), funktionaerId = funktionaerId.toString(),
richterNummer = richterNummer, satzID = satzID,
vorname = vorname, satzNummer = satzNummer,
nachname = nachname, name = name,
geburtsdatum = geburtsdatum, qualifikationen = qualifikationen,
rollen = rollen.map { it.name },
richterQualifikation = richterQualifikation?.name,
qualifiziertFuerSparten = qualifiziertFuerSparten.map { it.name },
email = email,
telefon = telefon,
vereinsNummer = vereinsNummer,
istAktiv = istAktiv, istAktiv = istAktiv,
bemerkungen = bemerkungen, bemerkungen = bemerkungen,
updatedAt = updatedAt updatedAt = updatedAt
@@ -25,101 +25,66 @@ class HorseController(private val horseRepository: HorseRepository) {
@Serializable @Serializable
data class HorseDto( data class HorseDto(
val pferdId: String, val pferdId: String,
val kopfnummer: String? = null,
val pferdeName: String, 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 lebensnummer: String? = null,
val chipNummer: String? = null, val geschlecht: String,
val passNummer: String? = null, val geburtsjahr: Int? = null,
val oepsNummer: String? = null, val farbe: String? = null,
val feiNummer: String? = null, val satznummer: String? = null,
val besitzerId: String? = null,
val vaterName: String? = null,
val mutterName: String? = null,
val stockmass: Int? = null,
val istAktiv: Boolean, val istAktiv: Boolean,
val bemerkungen: String? = null,
@Serializable(with = InstantSerializer::class) @Serializable(with = InstantSerializer::class)
val updatedAt: Instant val updatedAt: Instant
) )
@Serializable @Serializable
data class HorseCreateRequest( data class HorseCreateRequest(
val kopfnummer: String? = null,
val pferdeName: String, 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 lebensnummer: String? = null,
val chipNummer: String? = null, val geschlecht: String,
val passNummer: String? = null, val geburtsjahr: Int? = null,
val oepsNummer: String? = null, val farbe: String? = null,
val feiNummer: String? = null, val satznummer: String? = null,
val besitzerId: String? = null, val istAktiv: Boolean = true
val vaterName: String? = null,
val mutterName: String? = null,
val stockmass: Int? = null,
val istAktiv: Boolean = true,
val bemerkungen: String? = null
) )
@Serializable @Serializable
data class HorseUpdateRequest( data class HorseUpdateRequest(
val kopfnummer: String? = null,
val pferdeName: 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 lebensnummer: String? = null,
val chipNummer: String? = null, val geschlecht: String? = null,
val passNummer: String? = null, val geburtsjahr: Int? = null,
val oepsNummer: String? = null, val farbe: String? = null,
val feiNummer: String? = null, val istAktiv: Boolean? = 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
) )
fun Route.registerRoutes() { fun Route.registerRoutes() {
route("/horse") { route("/horse") {
/** /**
* GET /horse — Alle Pferde (paginiert), optional gefiltert nach jahrgang oder besitzerId. * GET /horse — Alle Pferde (paginiert), optional gefiltert nach jahrgang.
*/ */
get { get {
val jahrgang = call.request.queryParameters["jahrgang"]?.toIntOrNull() val jahrgang = call.request.queryParameters["jahrgang"]?.toIntOrNull()
val besitzerId = call.request.queryParameters["besitzerId"]
val limit = call.request.queryParameters["limit"]?.toIntOrNull() ?: 100 val limit = call.request.queryParameters["limit"]?.toIntOrNull() ?: 100
val offset = call.request.queryParameters["offset"]?.toIntOrNull() ?: 0 val offset = call.request.queryParameters["offset"]?.toIntOrNull() ?: 0
val results = when { val results = when {
jahrgang != null -> horseRepository.findByBirthYear(jahrgang) 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) else -> horseRepository.findAllActive(limit)
} }
call.respond(results.map { it.toDto() }) call.respond(results.map { it.toDto() })
} }
/** /**
* GET /horse/search?q=... — Sucht Pferde nach Name. * GET /horse/search?q=... — Sucht Pferde nach Lebensnummer.
*/ */
get("/search") { get("/search") {
val query = call.request.queryParameters["q"] ?: "" val query = call.request.queryParameters["q"] ?: ""
val results = horseRepository.findByName(query) val result = horseRepository.findByLebensnummer(query)
call.respond(results.map { it.toDto() }) 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 req = call.receive<HorseCreateRequest>()
val geschlecht = runCatching { PferdeGeschlechtE.valueOf(req.geschlecht) }.getOrNull() val geschlecht = runCatching { PferdeGeschlechtE.valueOf(req.geschlecht) }.getOrNull()
?: return@post call.respond(HttpStatusCode.BadRequest, "Ungültiges Geschlecht: ${req.geschlecht}") ?: 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( val domPferd = DomPferd(
kopfnummer = req.kopfnummer,
pferdeName = req.pferdeName, pferdeName = req.pferdeName,
geschlecht = geschlecht,
geburtsdatum = req.geburtsdatum,
rasse = req.rasse,
farbe = req.farbe,
lebensnummer = req.lebensnummer, lebensnummer = req.lebensnummer,
chipNummer = req.chipNummer, geschlecht = geschlecht,
passNummer = req.passNummer, geburtsjahr = req.geburtsjahr,
oepsNummer = req.oepsNummer, farbe = req.farbe,
feiNummer = req.feiNummer, satznummer = req.satznummer,
besitzerId = besitzerId, istAktiv = req.istAktiv
vaterName = req.vaterName,
mutterName = req.mutterName,
stockmass = req.stockmass,
istAktiv = req.istAktiv,
bemerkungen = req.bemerkungen
) )
val saved = horseRepository.save(domPferd) val saved = horseRepository.save(domPferd)
call.respond(HttpStatusCode.Created, saved.toDto()) call.respond(HttpStatusCode.Created, saved.toDto())
@@ -184,27 +137,14 @@ class HorseController(private val horseRepository: HorseRepository) {
runCatching { PferdeGeschlechtE.valueOf(it) }.getOrNull() runCatching { PferdeGeschlechtE.valueOf(it) }.getOrNull()
?: return@put call.respond(HttpStatusCode.BadRequest, "Ungültiges Geschlecht: $it") ?: return@put call.respond(HttpStatusCode.BadRequest, "Ungültiges Geschlecht: $it")
} ?: existing.geschlecht } ?: 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( val updated = existing.copy(
kopfnummer = req.kopfnummer ?: existing.kopfnummer,
pferdeName = req.pferdeName ?: existing.pferdeName, 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, lebensnummer = req.lebensnummer ?: existing.lebensnummer,
chipNummer = req.chipNummer ?: existing.chipNummer, geschlecht = geschlecht,
passNummer = req.passNummer ?: existing.passNummer, geburtsjahr = req.geburtsjahr ?: existing.geburtsjahr,
oepsNummer = req.oepsNummer ?: existing.oepsNummer, farbe = req.farbe ?: existing.farbe,
feiNummer = req.feiNummer ?: existing.feiNummer, istAktiv = req.istAktiv ?: existing.istAktiv
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
) )
val saved = horseRepository.save(updated) val saved = horseRepository.save(updated)
call.respond(saved.toDto()) call.respond(saved.toDto())
@@ -225,22 +165,14 @@ class HorseController(private val horseRepository: HorseRepository) {
private fun DomPferd.toDto() = HorseDto( private fun DomPferd.toDto() = HorseDto(
pferdId = pferdId.toString(), pferdId = pferdId.toString(),
kopfnummer = kopfnummer,
pferdeName = pferdeName, pferdeName = pferdeName,
geschlecht = geschlecht.name,
geburtsdatum = geburtsdatum,
rasse = rasse,
farbe = farbe,
lebensnummer = lebensnummer, lebensnummer = lebensnummer,
chipNummer = chipNummer, geschlecht = geschlecht.name,
passNummer = passNummer, geburtsjahr = geburtsjahr,
oepsNummer = oepsNummer, farbe = farbe,
feiNummer = feiNummer, satznummer = satznummer,
besitzerId = besitzerId?.toString(),
vaterName = vaterName,
mutterName = mutterName,
stockmass = stockmass,
istAktiv = istAktiv, istAktiv = istAktiv,
bemerkungen = bemerkungen,
updatedAt = updatedAt updatedAt = updatedAt
) )
} }
@@ -22,22 +22,24 @@ import kotlin.uuid.Uuid
*/ */
class ReiterController(private val reiterRepository: ReiterRepository) { class ReiterController(private val reiterRepository: ReiterRepository) {
@Serializable
data class ReiterDto( data class ReiterDto(
val reiterId: String, val reiterId: String,
val satznummer: String, val satznummer: String?,
val nachname: String, val nachname: String,
val vorname: String, val vorname: String,
@Serializable(with = LocalDateSerializer::class) @Serializable(with = LocalDateSerializer::class)
val geburtsdatum: LocalDate? = null, val geburtsdatum: LocalDate? = null,
val lizenzNummer: String? = null, val bundeslandNummer: Int? = null,
val lizenzKlasse: String,
val startkartAktiv: Boolean,
val nation: String? = null,
val vereinsNummer: String? = null,
val vereinsName: String? = 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 feiId: String? = null,
val istGastreiter: Boolean, val lizenzKlasse: String,
val istAktiv: Boolean, val istAktiv: Boolean,
@Serializable(with = InstantSerializer::class) @Serializable(with = InstantSerializer::class)
val updatedAt: Instant val updatedAt: Instant
@@ -50,14 +52,17 @@ class ReiterController(private val reiterRepository: ReiterRepository) {
val vorname: String, val vorname: String,
@Serializable(with = LocalDateSerializer::class) @Serializable(with = LocalDateSerializer::class)
val geburtsdatum: LocalDate? = null, val geburtsdatum: LocalDate? = null,
val lizenzNummer: String? = null, val bundeslandNummer: Int? = null,
val lizenzKlasse: String = "LIZENZFREI",
val startkartAktiv: Boolean = false,
val nation: String? = null,
val vereinsNummer: String? = null,
val vereinsName: String? = 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 feiId: String? = null,
val istGastreiter: Boolean = false, val lizenzKlasse: String = "LIZENZFREI",
val istAktiv: Boolean = true val istAktiv: Boolean = true
) )
@@ -67,14 +72,17 @@ class ReiterController(private val reiterRepository: ReiterRepository) {
val vorname: String? = null, val vorname: String? = null,
@Serializable(with = LocalDateSerializer::class) @Serializable(with = LocalDateSerializer::class)
val geburtsdatum: LocalDate? = null, val geburtsdatum: LocalDate? = null,
val lizenzNummer: String? = null, val bundeslandNummer: Int? = null,
val lizenzKlasse: String? = null,
val startkartAktiv: Boolean? = null,
val nation: String? = null,
val vereinsNummer: String? = null,
val vereinsName: String? = 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 feiId: String? = null,
val istGastreiter: Boolean? = null, val lizenzKlasse: String? = null,
val istAktiv: Boolean? = null val istAktiv: Boolean? = null
) )
@@ -82,34 +90,23 @@ class ReiterController(private val reiterRepository: ReiterRepository) {
route("/reiter") { route("/reiter") {
/** /**
* GET /reiter — Alle Reiter (paginiert), optional gefiltert nach lizenzKlasse oder vereinId. * GET /reiter — Alle Reiter (paginiert).
*/ */
get { get {
val lizenzKlasse = call.request.queryParameters["lizenzKlasse"]
val vereinId = call.request.queryParameters["vereinId"]
val limit = call.request.queryParameters["limit"]?.toIntOrNull() ?: 100 val limit = call.request.queryParameters["limit"]?.toIntOrNull() ?: 100
val offset = call.request.queryParameters["offset"]?.toIntOrNull() ?: 0 val offset = call.request.queryParameters["offset"]?.toIntOrNull() ?: 0
val results = when { val results = reiterRepository.findAll(limit, offset)
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)
}
call.respond(results.map { it.toDto() }) 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") { get("/search") {
val query = call.request.queryParameters["q"] ?: "" val query = call.request.queryParameters["q"] ?: ""
val results = reiterRepository.findByName(query) val result = reiterRepository.findBySatznummer(query)
call.respond(results.map { it.toDto() }) 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, nachname = req.nachname,
vorname = req.vorname, vorname = req.vorname,
geburtsdatum = req.geburtsdatum, geburtsdatum = req.geburtsdatum,
lizenzNummer = req.lizenzNummer, bundeslandNummer = req.bundeslandNummer,
lizenzKlasse = lizenzKlasse,
startkartAktiv = req.startkartAktiv,
nation = req.nation,
vereinsNummer = req.vereinsNummer,
vereinsName = req.vereinsName, 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, feiId = req.feiId,
istGastreiter = req.istGastreiter, lizenzKlasse = lizenzKlasse,
istAktiv = req.istAktiv istAktiv = req.istAktiv
) )
val saved = reiterRepository.save(domReiter) val saved = reiterRepository.save(domReiter)
@@ -172,14 +172,17 @@ class ReiterController(private val reiterRepository: ReiterRepository) {
nachname = req.nachname ?: existing.nachname, nachname = req.nachname ?: existing.nachname,
vorname = req.vorname ?: existing.vorname, vorname = req.vorname ?: existing.vorname,
geburtsdatum = req.geburtsdatum ?: existing.geburtsdatum, geburtsdatum = req.geburtsdatum ?: existing.geburtsdatum,
lizenzNummer = req.lizenzNummer ?: existing.lizenzNummer, bundeslandNummer = req.bundeslandNummer ?: existing.bundeslandNummer,
lizenzKlasse = lizenzKlasse,
startkartAktiv = req.startkartAktiv ?: existing.startkartAktiv,
nation = req.nation ?: existing.nation,
vereinsNummer = req.vereinsNummer ?: existing.vereinsNummer,
vereinsName = req.vereinsName ?: existing.vereinsName, 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, feiId = req.feiId ?: existing.feiId,
istGastreiter = req.istGastreiter ?: existing.istGastreiter, lizenzKlasse = lizenzKlasse,
istAktiv = req.istAktiv ?: existing.istAktiv istAktiv = req.istAktiv ?: existing.istAktiv
) )
val saved = reiterRepository.save(updated) val saved = reiterRepository.save(updated)
@@ -205,14 +208,17 @@ class ReiterController(private val reiterRepository: ReiterRepository) {
nachname = nachname, nachname = nachname,
vorname = vorname, vorname = vorname,
geburtsdatum = geburtsdatum, geburtsdatum = geburtsdatum,
lizenzNummer = lizenzNummer, bundeslandNummer = bundeslandNummer,
lizenzKlasse = lizenzKlasse.name,
startkartAktiv = startkartAktiv,
nation = nation,
vereinsNummer = vereinsNummer,
vereinsName = vereinsName, vereinsName = vereinsName,
nation = nation,
reiterLizenz = reiterLizenz,
startkarte = startkarte,
fahrLizenz = fahrLizenz,
mitgliedsNummer = mitgliedsNummer,
telefonNummer = telefonNummer,
lastPayYear = lastPayYear,
feiId = feiId, feiId = feiId,
istGastreiter = istGastreiter, lizenzKlasse = lizenzKlasse.name,
istAktiv = istAktiv, istAktiv = istAktiv,
updatedAt = updatedAt updatedAt = updatedAt
) )
@@ -18,21 +18,14 @@ import kotlin.uuid.Uuid
* Domain-Modell für einen Funktionär im actor-context. * Domain-Modell für einen Funktionär im actor-context.
* *
* Repräsentiert eine Person mit einer definierten Rolle bei Turnieren (Richter, TBA, * 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. * Parcoursbauer etc.). Die Qualifikation wird gegen `RICHT01.DAT` oder `PARCO01.DAT`
* * aus dem ZNS geprüft.
* Aggregate Root des `officials`-Bounded Context.
* *
* @property funktionaerId Eindeutige interne ID (UUID). * @property funktionaerId Eindeutige interne ID (UUID).
* @property richterNummer ÖPS-Funktionärsnummer aus ZNS (RICHT01.dat), 6-stellig. * @property satzID Typ des Satzes (X = Richter, Y = Parcoursbauer). Aus ZNS (RICHT01.DAT / PARCO01.DAT).
* @property vorname Vorname der Person. * @property satzNummer Satznummer (6-stellig). Aus ZNS (RICHT01.DAT / PARCO01.DAT).
* @property nachname Nachname der Person. * @property name Vollständiger Name (Nachname, Vorname). Aus ZNS (RICHT01.DAT / PARCO01.DAT).
* @property geburtsdatum Geburtsdatum (optional, für Altersklassen-Prüfung). * @property qualifikation Qualifikationen (getrennt durch `,`). Aus ZNS (RICHT01.DAT / PARCO01.DAT).
* @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 istAktiv Ob der Funktionär aktuell aktiv/einsatzbereit ist. * @property istAktiv Ob der Funktionär aktuell aktiv/einsatzbereit ist.
* @property bemerkungen Interne Notizen. * @property bemerkungen Interne Notizen.
* @property datenQuelle Herkunft des Datensatzes (ZNS-Import oder manuell). * @property datenQuelle Herkunft des Datensatzes (ZNS-Import oder manuell).
@@ -43,26 +36,21 @@ import kotlin.uuid.Uuid
data class DomFunktionaer( data class DomFunktionaer(
@Serializable(with = UuidSerializer::class) @Serializable(with = UuidSerializer::class)
val funktionaerId: Uuid = Uuid.random(), 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 // var vorname: String,
val richterNummer: String? = null, // var nachname: String,
// var geburtsdatum: LocalDate? = null,
// Persönliche Daten // val richterNummer: String? = null,
var vorname: String, // var rollen: Set<FunktionaerRolleE> = emptySet(),
var nachname: String, // var richterQualifikation: RichterQualifikationE? = null,
var geburtsdatum: LocalDate? = null, // var qualifiziertFuerSparten: Set<SparteE> = emptySet(),
// var email: String? = null,
// Qualifikation & Rollen // var telefon: String? = null,
var rollen: Set<FunktionaerRolleE> = emptySet(), // var vereinsNummer: String? = null,
var richterQualifikation: RichterQualifikationE? = null,
var qualifiziertFuerSparten: Set<SparteE> = emptySet(),
// Kontakt
var email: String? = null,
var telefon: String? = null,
// Vereinszugehörigkeit
var vereinsNummer: String? = null,
// Status & Verwaltung // Status & Verwaltung
var istAktiv: Boolean = true, var istAktiv: Boolean = true,
@@ -78,44 +66,35 @@ data class DomFunktionaer(
/** /**
* Gibt den vollständigen Anzeigenamen zurück. * 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). * Gibt den Anzeigenamen mit Funktionärsnummer zurück (falls vorhanden).
*/ */
fun getDisplayNameWithNummer(): String = 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 = fun istRichter(): Boolean = satzID.uppercase() == "X"
rollen.contains(FunktionaerRolleE.RICHTER) && qualifiziertFuerSparten.contains(sparte)
/** /**
* 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. * Validiert die Pflichtfelder für den Turniereinsatz.
* Gibt eine Liste von Warnungen zurück (kein harter Fehler Override-Event möglich). * 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>() val warnings = mutableListOf<String>()
if (!istAktiv) { if (!istAktiv) {
warnings.add("Funktionär ${getDisplayName()} ist nicht aktiv.") 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 return warnings
} }
@@ -22,24 +22,19 @@ import kotlin.uuid.Uuid
* It serves as the core aggregate root for the horse-registry bounded context. * It serves as the core aggregate root for the horse-registry bounded context.
* *
* @property pferdId Unique internal identifier for this horse (UUID). * @property pferdId Unique internal identifier for this horse (UUID).
* @property pferdeName Name of the horse. * @property kopfnummer Head number (Kopfnummer) used at tournaments (4 alphanumeric chars). From PFERDE01.DAT.
* @property geschlecht Gender of the horse (Hengst, Stute, Wallach). * @property pferdeName Name of the horse. From PFERDE01.DAT.
* @property geburtsdatum Birthdate of the horse. * @property lebensnummer Life number (unique identification number). From PFERDE01.DAT.
* @property rasse Breed of the horse. * @property geschlecht Gender of the horse (Hengst, Stute, Wallach). Derived from PFERDE01.DAT.
* @property farbe Color/coat of the horse. * @property geburtsjahr Birth year of the horse. From PFERDE01.DAT.
* @property besitzerId ID of the current owner (Person from member-management context). * @property farbe Color/coat of the horse. From PFERDE01.DAT.
* @property verantwortlichePersonId ID of the responsible person (trainer, rider, etc.). * @property abstammung Breeding/Pedigree information. From PFERDE01.DAT.
* @property zuechterName Name of the breeder. * @property vereinNummer Club number (OEPS). From PFERDE01.DAT.
* @property zuchtbuchNummer Studbook number if registered. * @property lastPayYear Last year the horse's OEPS fee was paid. From PFERDE01.DAT.
* @property lebensnummer Life number (unique identification number). * @property verantwortlichePersonId Reference to the responsible person (Satznummer or ID). From PFERDE01.DAT.
* @property chipNummer Microchip number for identification. * @property vater Name of the sire (father). From PFERDE01.DAT.
* @property passNummer Passport number. * @property feiPass FEI passport information. From PFERDE01.DAT.
* @property oepsNummer OEPS (Austrian Equestrian Federation) number. * @property satznummer 10-digit ZNS primary key for data exchange. From PFERDE01.DAT.
* @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 istAktiv Whether the horse is currently active in the system. * @property istAktiv Whether the horse is currently active in the system.
* @property bemerkungen Additional notes or comments. * @property bemerkungen Additional notes or comments.
* @property datenQuelle Source of the data (manual entry, import, etc.). * @property datenQuelle Source of the data (manual entry, import, etc.).
@@ -51,56 +46,49 @@ data class DomPferd(
@Serializable(with = UuidSerializer::class) @Serializable(with = UuidSerializer::class)
val pferdId: Uuid = Uuid.random(), val pferdId: Uuid = Uuid.random(),
// Basic Information // PFERDE01.DAT Information
var kopfnummer: String? = null,
var pferdeName: String, 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 lebensnummer: String? = null,
var chipNummer: String? = null, var geschlecht: PferdeGeschlechtE,
var passNummer: String? = null, var geburtsjahr: Int? = null,
var oepsNummer: String? = null, var farbe: String? = null,
var feiNummer: 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 geburtsdatum: LocalDate? = null,
var vaterName: String? = null, // var rasse: String? = null,
var mutterName: String? = null, // @Serializable(with = UuidSerializer::class)
var mutterVaterName: String? = null, // 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 istAktiv: Boolean = true,
var bemerkungen: String? = null, var bemerkungen: String? = null,
var datenQuelle: DatenQuelleE = DatenQuelleE.MANUELL, var datenQuelle: DatenQuelleE = DatenQuelleE.MANUELL,
var createdAt: Instant = Clock.System.now(),
// Audit Fields
@Serializable(with = InstantSerializer::class)
val createdAt: Instant = Clock.System.now(),
@Serializable(with = InstantSerializer::class)
var updatedAt: Instant = Clock.System.now() var updatedAt: Instant = Clock.System.now()
) { ) {
/** /**
* Returns the display name for the horse, combining name and birth year if available. * Returns the display name for the horse, combining name and birth year if available.
*/ */
fun getDisplayName(): String { fun getDisplayName(): String {
return geburtsdatum?.let { birthDate -> val basic = geburtsjahr?.let { year ->
"$pferdeName (${birthDate.year})" "$pferdeName ($year)"
} ?: pferdeName } ?: pferdeName
return kopfnummer?.let { "[$it] $basic" } ?: basic
} }
/** /**
@@ -108,40 +96,31 @@ data class DomPferd(
*/ */
fun hasCompleteIdentification(): Boolean { fun hasCompleteIdentification(): Boolean {
return !lebensnummer.isNullOrBlank() || return !lebensnummer.isNullOrBlank() ||
!chipNummer.isNullOrBlank() || !kopfnummer.isNullOrBlank() ||
!passNummer.isNullOrBlank() !satznummer.isNullOrBlank()
} }
/** /**
* Checks if the horse is registered with OEPS. * Checks if the horse is registered with OEPS.
*/ */
fun isOepsRegistered(): Boolean { fun isOepsRegistered(): Boolean {
return !oepsNummer.isNullOrBlank() return false // OEPS registration information currently commented out
} }
/** /**
* Checks if the horse is registered with FEI. * Checks if the horse is registered with FEI.
*/ */
fun isFeiRegistered(): Boolean { 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? { fun getAge(): Int? {
return geburtsdatum?.let { birthDate -> return geburtsjahr?.let { birthYear ->
val today = Clock.System.todayIn(kotlinx.datetime.TimeZone.currentSystemDefault()) val today = Clock.System.todayIn(kotlinx.datetime.TimeZone.currentSystemDefault())
var age = today.year - birthDate.year today.year - birthYear
// 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
} }
} }
@@ -156,11 +135,7 @@ data class DomPferd(
} }
if (!hasCompleteIdentification()) { if (!hasCompleteIdentification()) {
errors.add("At least one identification number (life number, chip number, or passport number) is required") errors.add("At least one identification number (life number, or kopfnummer, or satznummer) is required")
}
if (besitzerId == null) {
errors.add("Owner is required")
} }
return errors 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.LocalDateSerializer
import at.mocode.core.domain.serialization.UuidSerializer import at.mocode.core.domain.serialization.UuidSerializer
import kotlinx.datetime.LocalDate import kotlinx.datetime.LocalDate
import kotlinx.datetime.todayIn
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlin.time.Clock import kotlin.time.Clock
import kotlin.time.Instant import kotlin.time.Instant
@@ -21,27 +22,30 @@ import kotlin.uuid.Uuid
* attributes such as license, start card, and competition eligibility. * attributes such as license, start card, and competition eligibility.
* Data is primarily sourced from the OEPS ZNS (LIZENZ01.DAT). * 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 reiterId Unique internal identifier (UUID).
* @property personId Reference to the base DomPerson record (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 satznummer 6-digit ZNS primary key for data exchange. From LIZENZ01.DAT.
* @property lizenzNummer OEPS license number (from ZNS LIZENZ01.DAT). * @property nachname Surname of the rider. From LIZENZ01.DAT.
* @property lizenzKlasse License class determining competition eligibility (e.g. R1, RD2). * @property vorname First name of the rider. From LIZENZ01.DAT.
* @property lizenzSparten Disciplines for which the license is valid. * @property bundeslandNummer State number (Bundesland). From LIZENZ01.DAT.
* @property startkartAktiv Whether the annual start card fee has been paid. * @property vereinsName Name of the club. From LIZENZ01.DAT.
* @property startkartSaison Season year for which the start card is valid (e.g. 2026). * @property nation Nationality of the rider. From LIZENZ01.DAT.
* @property feiId FEI international rider ID (optional). * @property reiterLizenz Rider license information. From LIZENZ01.DAT.
* @property nation Nation code (e.g. AUT). * @property startkarte Start card information. From LIZENZ01.DAT.
* @property geburtsdatum Date of birth (for age class validation). * @property fahrLizenz Driving license information. From LIZENZ01.DAT.
* @property vereinsNummer Club number (OEPS). * @property altersklasseJgJrU25 Age class Jg/Jr/U25. From LIZENZ01.DAT.
* @property vereinsName Club name. * @property altersklasseY Age class Young Rider. From LIZENZ01.DAT.
* @property istGastreiter Whether the rider is a guest rider (foreign nationality, not in Austrian club). * @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 istAktiv Whether the rider is currently active in the system.
* @property bemerkungen Additional notes or comments.
* @property datenQuelle Source of the data. * @property datenQuelle Source of the data.
* @property createdAt Timestamp when this record was created. * @property createdAt Timestamp when this record was created.
* @property updatedAt Timestamp when this record was last updated. * @property updatedAt Timestamp when this record was last updated.
@@ -56,37 +60,33 @@ data class DomReiter(
val personId: Uuid, val personId: Uuid,
// ZNS Identification // ZNS Identification
val satznummer: String, var satznummer: String?,
val lizenzNummer: String? = null, var nachname: String,
var vorname: String,
// License & Eligibility var bundeslandNummer: Int? = null,
val lizenzKlasse: LizenzKlasseE = LizenzKlasseE.LIZENZFREI, var vereinsName: String? = null,
val lizenzSparten: List<SparteE> = emptyList(), var nation: String? = null,
var reiterLizenz: String? = null,
// Start Card (Startkarte) annual fee proof var startkarte: String? = null,
val startkartAktiv: Boolean = false, var fahrLizenz: String? = null,
val startkartSaison: Int? = null, var altersklasseJgJrU25: String? = null,
var altersklasseY: String? = null,
// International var mitgliedsNummer: Int? = null,
val feiId: String? = null, var telefonNummer: String? = null,
val nation: String? = null, var kader: String? = null,
var lastPayYear: Int? = null,
// Personal Data (denormalized from DomPerson for performance) var geschlecht: String? = null,
val nachname: String,
val vorname: String,
@Serializable(with = LocalDateSerializer::class) @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 var lizenzKlasse: LizenzKlasseE = LizenzKlasseE.LIZENZFREI,
val vereinsNummer: String? = null,
val vereinsName: String? = null,
// Status
val istGastreiter: Boolean = false,
val istAktiv: Boolean = true, val istAktiv: Boolean = true,
var bemerkungen: String? = null,
val datenQuelle: DatenQuelleE = DatenQuelleE.IMPORT_ZNS, val datenQuelle: DatenQuelleE = DatenQuelleE.IMPORT_ZNS,
// Audit Fields
@Serializable(with = InstantSerializer::class) @Serializable(with = InstantSerializer::class)
val createdAt: Instant = Clock.System.now(), val createdAt: Instant = Clock.System.now(),
@Serializable(with = InstantSerializer::class) @Serializable(with = InstantSerializer::class)
@@ -99,31 +99,45 @@ data class DomReiter(
/** /**
* Checks if the rider is eligible to compete nationally. * 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 = fun hasLizenz(): Boolean = !reiterLizenz.isNullOrBlank()
lizenzKlasse == LizenzKlasseE.LIZENZFREI || lizenzSparten.contains(sparte)
/**
* 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. * Validates the rider for competition entry.
* Returns a list of warning messages (never hard errors TBA has final say). * 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>() val warnings = mutableListOf<String>()
if (!istAktiv) { if (!istAktiv) {
warnings.add("Reiter ${getDisplayName()} ist nicht aktiv") warnings.add("Reiter ${getDisplayName()} ist nicht aktiv")
} }
if (!startkartAktiv) {
warnings.add("Reiter ${getDisplayName()} hat keine aktive Startkarte für Saison $startkartSaison") if (!isStartberechtigt()) {
} warnings.add("Reiter ${getDisplayName()} hat keine aktive Startkarte für das aktuelle Jahr")
if (!hasLizenzForSparte(sparte)) {
warnings.add("Reiter ${getDisplayName()} hat keine Lizenz für Sparte $sparte (Lizenzklasse: $lizenzKlasse)")
} }
return warnings return warnings
@@ -22,42 +22,9 @@ interface FunktionaerRepository {
suspend fun findById(id: Uuid): DomFunktionaer? 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? suspend fun findBySatz(satzID: String, satzNummer: Int): 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>
/** /**
* Gibt alle Funktionäre zurück (paginiert). * Gibt alle Funktionäre zurück (paginiert).
@@ -82,12 +49,7 @@ interface FunktionaerRepository {
suspend fun countActive(): Long 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 suspend fun existsBySatz(satzID: String, satzNummer: Int): Boolean
/**
* Prüft ob ein Funktionär mit der gegebenen Richternummer bereits existiert.
*/
suspend fun existsByRichterNummer(richterNummer: String): Boolean
} }
@@ -246,6 +246,28 @@ interface HorseRepository {
*/ */
suspend fun countFeiRegistered(activeOnly: Boolean = true): Long 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). * Speichert ein Pferd basierend auf der Lebensnummer (Upsert).
* Wenn ein Pferd mit der Lebensnummer existiert, wird es aktualisiert, ansonsten neu angelegt. * Wenn ein Pferd mit der Lebensnummer existiert, wird es aktualisiert, ansonsten neu angelegt.
@@ -2,8 +2,6 @@
package at.mocode.masterdata.domain.repository 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 at.mocode.masterdata.domain.model.DomReiter
import kotlin.uuid.Uuid import kotlin.uuid.Uuid
@@ -23,42 +21,7 @@ interface ReiterRepository {
/** /**
* Sucht einen Reiter anhand seiner Satznummer (OEPS-Mitgliedsnummer). * Sucht einen Reiter anhand seiner Satznummer (OEPS-Mitgliedsnummer).
*/ */
suspend fun findBySatznummer(satznummer: String): DomReiter? 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>
/** /**
* Gibt alle Reiter zurück (paginiert). * Gibt alle Reiter zurück (paginiert).
@@ -95,9 +95,7 @@ class LicenseMatrixServiceTest {
satznummer = "1", satznummer = "1",
nachname = "R1", nachname = "R1",
vorname = "Reiter", vorname = "Reiter",
lizenzKlasse = LizenzKlasseE.R1, lizenzKlasse = LizenzKlasseE.R1
lizenzSparten = listOf(SparteE.SPRINGEN),
startkartAktiv = true
) )
val klasseA = turnierklassen.find { it.code == "A" }!! val klasseA = turnierklassen.find { it.code == "A" }!!
@@ -116,9 +114,7 @@ class LicenseMatrixServiceTest {
satznummer = "2", satznummer = "2",
nachname = "RD1", nachname = "RD1",
vorname = "Reiter", vorname = "Reiter",
lizenzKlasse = LizenzKlasseE.RD1, lizenzKlasse = LizenzKlasseE.RD1
lizenzSparten = listOf(SparteE.DRESSUR), // Nur Dressur
startkartAktiv = true
) )
val klasseA = turnierklassen.find { it.code == "A" }!! 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.model.DomFunktionaer
import at.mocode.masterdata.domain.repository.FunktionaerRepository import at.mocode.masterdata.domain.repository.FunktionaerRepository
import org.jetbrains.exposed.v1.core.ResultRow import org.jetbrains.exposed.v1.core.ResultRow
import org.jetbrains.exposed.v1.core.eq import org.jetbrains.exposed.v1.core.*
import org.jetbrains.exposed.v1.core.like
import org.jetbrains.exposed.v1.core.or
import org.jetbrains.exposed.v1.jdbc.* import org.jetbrains.exposed.v1.jdbc.*
import kotlin.uuid.Uuid import kotlin.uuid.Uuid
@@ -21,16 +19,13 @@ import kotlin.uuid.Uuid
*/ */
class ExposedFunktionaerRepository : FunktionaerRepository { class ExposedFunktionaerRepository : FunktionaerRepository {
private fun rowToDomFunktionaer(row: ResultRow): DomFunktionaer { private fun rowToDomFunktionaer(row: ResultRow, qualifikationen: List<String> = emptyList()): DomFunktionaer {
return DomFunktionaer( return DomFunktionaer(
funktionaerId = row[FunktionaerTable.id], funktionaerId = row[FunktionaerTable.id],
richterNummer = row[FunktionaerTable.richterNummer], satzID = row[FunktionaerTable.satzID] ?: "X",
vorname = row[FunktionaerTable.vorname], satzNummer = row[FunktionaerTable.satzNummer] ?: 0,
nachname = row[FunktionaerTable.nachname], name = row[FunktionaerTable.name],
geburtsdatum = row[FunktionaerTable.geburtsdatum], qualifikationen = qualifikationen,
email = row[FunktionaerTable.email],
telefon = row[FunktionaerTable.telefon],
vereinsNummer = row[FunktionaerTable.vereinsNummer],
istAktiv = row[FunktionaerTable.istAktiv], istAktiv = row[FunktionaerTable.istAktiv],
bemerkungen = row[FunktionaerTable.bemerkungen], bemerkungen = row[FunktionaerTable.bemerkungen],
datenQuelle = DatenQuelleE.valueOf(row[FunktionaerTable.datenQuelle]), datenQuelle = DatenQuelleE.valueOf(row[FunktionaerTable.datenQuelle]),
@@ -40,101 +35,78 @@ class ExposedFunktionaerRepository : FunktionaerRepository {
} }
override suspend fun findById(id: Uuid): DomFunktionaer? = DatabaseFactory.dbQuery { 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 } FunktionaerTable.selectAll().where { FunktionaerTable.id eq id }
.map(::rowToDomFunktionaer) .map { rowToDomFunktionaer(it, qualifikationen) }
.singleOrNull() .singleOrNull()
} }
override suspend fun findByRichterNummer(richterNummer: String): DomFunktionaer? = DatabaseFactory.dbQuery { override suspend fun findBySatz(satzID: String, satzNummer: Int): DomFunktionaer? = DatabaseFactory.dbQuery {
FunktionaerTable.selectAll().where { FunktionaerTable.richterNummer eq richterNummer } val row = FunktionaerTable.selectAll()
.map(::rowToDomFunktionaer) .where { (FunktionaerTable.satzID eq satzID) and (FunktionaerTable.satzNummer eq satzNummer) }
.singleOrNull() .singleOrNull() ?: return@dbQuery null
}
override suspend fun findByName(searchTerm: String, limit: Int): List<DomFunktionaer> = DatabaseFactory.dbQuery { val qualifikationen = FunktionaerQualifikationTable
val pattern = "%$searchTerm%" .selectAll().where { FunktionaerQualifikationTable.funktionaerId eq row[FunktionaerTable.id] }
FunktionaerTable.selectAll() .map { it[FunktionaerQualifikationTable.qualifikation] }
.where { (FunktionaerTable.nachname like pattern) or (FunktionaerTable.vorname like pattern) }
.limit(limit)
.map(::rowToDomFunktionaer)
}
override suspend fun findByRolle(rolle: FunktionaerRolleE, activeOnly: Boolean): List<DomFunktionaer> = rowToDomFunktionaer(row, qualifikationen)
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)
} }
override suspend fun findAll(limit: Int, offset: Int): List<DomFunktionaer> = DatabaseFactory.dbQuery { override suspend fun findAll(limit: Int, offset: Int): List<DomFunktionaer> = DatabaseFactory.dbQuery {
FunktionaerTable.selectAll() val funktionaere = FunktionaerTable.selectAll()
.limit(limit).offset(offset.toLong()) .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 { override suspend fun save(funktionaer: DomFunktionaer): DomFunktionaer = DatabaseFactory.dbQuery {
val exists = FunktionaerTable.selectAll().where { FunktionaerTable.id eq funktionaer.funktionaerId }.any() val exists = FunktionaerTable.selectAll().where { FunktionaerTable.id eq funktionaer.funktionaerId }.any()
if (exists) { if (exists) {
FunktionaerTable.update({ FunktionaerTable.id eq funktionaer.funktionaerId }) { FunktionaerTable.update({ FunktionaerTable.id eq funktionaer.funktionaerId }) {
it[richterNummer] = funktionaer.richterNummer it[satzID] = funktionaer.satzID
it[vorname] = funktionaer.vorname it[satzNummer] = funktionaer.satzNummer
it[nachname] = funktionaer.nachname it[name] = funktionaer.name
it[geburtsdatum] = funktionaer.geburtsdatum
it[email] = funktionaer.email
it[telefon] = funktionaer.telefon
it[vereinsNummer] = funktionaer.vereinsNummer
it[istAktiv] = funktionaer.istAktiv it[istAktiv] = funktionaer.istAktiv
it[bemerkungen] = funktionaer.bemerkungen it[bemerkungen] = funktionaer.bemerkungen
it[datenQuelle] = funktionaer.datenQuelle.name it[datenQuelle] = funktionaer.datenQuelle.name
it[updatedAt] = funktionaer.updatedAt it[updatedAt] = funktionaer.updatedAt
} }
funktionaer
} else { } else {
FunktionaerTable.insert { FunktionaerTable.insert {
it[id] = funktionaer.funktionaerId it[id] = funktionaer.funktionaerId
it[richterNummer] = funktionaer.richterNummer it[satzID] = funktionaer.satzID
it[vorname] = funktionaer.vorname it[satzNummer] = funktionaer.satzNummer
it[nachname] = funktionaer.nachname it[name] = funktionaer.name
it[geburtsdatum] = funktionaer.geburtsdatum
it[email] = funktionaer.email
it[telefon] = funktionaer.telefon
it[vereinsNummer] = funktionaer.vereinsNummer
it[istAktiv] = funktionaer.istAktiv it[istAktiv] = funktionaer.istAktiv
it[bemerkungen] = funktionaer.bemerkungen it[bemerkungen] = funktionaer.bemerkungen
it[datenQuelle] = funktionaer.datenQuelle.name it[datenQuelle] = funktionaer.datenQuelle.name
it[createdAt] = funktionaer.createdAt it[createdAt] = funktionaer.createdAt
it[updatedAt] = funktionaer.updatedAt 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 { override suspend fun delete(id: Uuid): Boolean = DatabaseFactory.dbQuery {
@@ -145,13 +117,9 @@ class ExposedFunktionaerRepository : FunktionaerRepository {
FunktionaerTable.selectAll().where { FunktionaerTable.istAktiv eq true }.count() FunktionaerTable.selectAll().where { FunktionaerTable.istAktiv eq true }.count()
} }
override suspend fun countByRichterQualifikation(qualifikation: RichterQualifikationE, activeOnly: Boolean): Long = override suspend fun existsBySatz(satzID: String, satzNummer: Int): Boolean = DatabaseFactory.dbQuery {
DatabaseFactory.dbQuery { FunktionaerTable.selectAll()
// Aktuell keine Qualifikations-Speicherung .where { (FunktionaerTable.satzID eq satzID) and (FunktionaerTable.satzNummer eq satzNummer) }
0L .any()
}
override suspend fun existsByRichterNummer(richterNummer: String): Boolean = DatabaseFactory.dbQuery {
FunktionaerTable.selectAll().where { FunktionaerTable.richterNummer eq richterNummer }.any()
} }
} }
@@ -4,14 +4,11 @@ package at.mocode.masterdata.infrastructure.persistence
import at.mocode.core.domain.model.DatenQuelleE import at.mocode.core.domain.model.DatenQuelleE
import at.mocode.core.domain.model.LizenzKlasseE import at.mocode.core.domain.model.LizenzKlasseE
import at.mocode.core.domain.model.SparteE
import at.mocode.core.utils.database.DatabaseFactory import at.mocode.core.utils.database.DatabaseFactory
import at.mocode.masterdata.domain.model.DomReiter import at.mocode.masterdata.domain.model.DomReiter
import at.mocode.masterdata.domain.repository.ReiterRepository import at.mocode.masterdata.domain.repository.ReiterRepository
import org.jetbrains.exposed.v1.core.ResultRow import org.jetbrains.exposed.v1.core.ResultRow
import org.jetbrains.exposed.v1.core.eq 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 org.jetbrains.exposed.v1.jdbc.*
import kotlin.uuid.Uuid import kotlin.uuid.Uuid
@@ -20,133 +17,55 @@ import kotlin.uuid.Uuid
*/ */
class ExposedReiterRepository : ReiterRepository { class ExposedReiterRepository : ReiterRepository {
private fun rowToDomReiter(row: ResultRow, sparten: List<SparteE> = emptyList()): DomReiter { private fun rowToDomReiter(row: ResultRow): DomReiter {
return DomReiter( return DomReiter(
reiterId = row[ReiterTable.id], reiterId = row[ReiterTable.id],
personId = row[ReiterTable.personId], personId = row[ReiterTable.personId],
satznummer = row[ReiterTable.satznummer], satznummer = row[ReiterTable.satznummer],
nachname = row[ReiterTable.nachname], nachname = row[ReiterTable.nachname],
vorname = row[ReiterTable.vorname], vorname = row[ReiterTable.vorname],
geburtsdatum = row[ReiterTable.geburtsdatum], bundeslandNummer = row[ReiterTable.bundeslandNummer],
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],
vereinsName = row[ReiterTable.vereinsName], 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], istAktiv = row[ReiterTable.istAktiv],
bemerkungen = row[ReiterTable.bemerkungen],
datenQuelle = DatenQuelleE.valueOf(row[ReiterTable.datenQuelle]), datenQuelle = DatenQuelleE.valueOf(row[ReiterTable.datenQuelle]),
createdAt = row[ReiterTable.createdAt], createdAt = row[ReiterTable.createdAt],
updatedAt = row[ReiterTable.updatedAt] 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 { override suspend fun findById(id: Uuid): DomReiter? = DatabaseFactory.dbQuery {
ReiterTable.selectAll().where { ReiterTable.id eq id } ReiterTable.selectAll().where { ReiterTable.id eq id }
.map { rowToDomReiter(it, getSpartenForReiter(id)) } .map { rowToDomReiter(it) }
.singleOrNull() .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 } ReiterTable.selectAll().where { ReiterTable.satznummer eq satznummer }
.map { row -> .map { row -> rowToDomReiter(row) }
val id = row[ReiterTable.id]
rowToDomReiter(row, getSpartenForReiter(id))
}
.singleOrNull() .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 { override suspend fun findAll(limit: Int, offset: Int): List<DomReiter> = DatabaseFactory.dbQuery {
ReiterTable.selectAll() ReiterTable.selectAll()
.limit(limit).offset(offset.toLong()) .limit(limit).offset(offset.toLong())
.map { row -> .map { row -> rowToDomReiter(row) }
val id = row[ReiterTable.id]
rowToDomReiter(row, getSpartenForReiter(id))
}
} }
override suspend fun save(reiter: DomReiter): DomReiter = DatabaseFactory.dbQuery { override suspend fun save(reiter: DomReiter): DomReiter = DatabaseFactory.dbQuery {
@@ -157,17 +76,26 @@ class ExposedReiterRepository : ReiterRepository {
it[satznummer] = reiter.satznummer it[satznummer] = reiter.satznummer
it[nachname] = reiter.nachname it[nachname] = reiter.nachname
it[vorname] = reiter.vorname it[vorname] = reiter.vorname
it[geburtsdatum] = reiter.geburtsdatum it[bundeslandNummer] = reiter.bundeslandNummer
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[vereinsName] = reiter.vereinsName 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[istAktiv] = reiter.istAktiv
it[bemerkungen] = reiter.bemerkungen
it[datenQuelle] = reiter.datenQuelle.name it[datenQuelle] = reiter.datenQuelle.name
it[updatedAt] = reiter.updatedAt it[updatedAt] = reiter.updatedAt
} }
@@ -178,33 +106,32 @@ class ExposedReiterRepository : ReiterRepository {
it[satznummer] = reiter.satznummer it[satznummer] = reiter.satznummer
it[nachname] = reiter.nachname it[nachname] = reiter.nachname
it[vorname] = reiter.vorname it[vorname] = reiter.vorname
it[geburtsdatum] = reiter.geburtsdatum it[bundeslandNummer] = reiter.bundeslandNummer
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[vereinsName] = reiter.vereinsName 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[istAktiv] = reiter.istAktiv
it[bemerkungen] = reiter.bemerkungen
it[datenQuelle] = reiter.datenQuelle.name it[datenQuelle] = reiter.datenQuelle.name
it[createdAt] = reiter.createdAt it[createdAt] = reiter.createdAt
it[updatedAt] = reiter.updatedAt 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 reiter
} }
@@ -221,12 +148,7 @@ class ExposedReiterRepository : ReiterRepository {
} }
override suspend fun upsertBySatznummer(reiter: DomReiter): DomReiter = DatabaseFactory.dbQuery { override suspend fun upsertBySatznummer(reiter: DomReiter): DomReiter = DatabaseFactory.dbQuery {
val existing = ReiterTable.selectAll().where { ReiterTable.satznummer eq reiter.satznummer } val existing = findBySatznummer(reiter.satznummer)
.map { row ->
val id = row[ReiterTable.id]
rowToDomReiter(row, getSpartenForReiter(id))
}
.singleOrNull()
if (existing != null) { if (existing != null) {
val toUpdate = reiter.copy(reiterId = existing.reiterId) val toUpdate = reiter.copy(reiterId = existing.reiterId)
@@ -13,13 +13,9 @@ import org.jetbrains.exposed.v1.datetime.timestamp
*/ */
object FunktionaerTable : Table("funktionaer") { object FunktionaerTable : Table("funktionaer") {
val id = uuid("funktionaer_id") val id = uuid("funktionaer_id")
val richterNummer = varchar("richter_nummer", 10).nullable().uniqueIndex() val satzID = varchar("satz_id", 1).nullable()
val vorname = varchar("vorname", 100) val satzNummer = integer("satz_nummer").nullable()
val nachname = varchar("nachname", 100) val name = varchar("name", 200).nullable()
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 istAktiv = bool("ist_aktiv").default(true) val istAktiv = bool("ist_aktiv").default(true)
val bemerkungen = text("bemerkungen").nullable() val bemerkungen = text("bemerkungen").nullable()
val datenQuelle = varchar("daten_quelle", 50) val datenQuelle = varchar("daten_quelle", 50)
@@ -27,4 +23,18 @@ object FunktionaerTable : Table("funktionaer") {
val updatedAt = timestamp("updated_at").defaultExpression(CurrentTimestamp) val updatedAt = timestamp("updated_at").defaultExpression(CurrentTimestamp)
override val primaryKey = PrimaryKey(id) 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.core.utils.database.DatabaseFactory
import at.mocode.masterdata.domain.model.DomPferd import at.mocode.masterdata.domain.model.DomPferd
import at.mocode.masterdata.domain.repository.HorseRepository import at.mocode.masterdata.domain.repository.HorseRepository
import org.jetbrains.exposed.v1.core.* import org.jetbrains.exposed.v1.core.ResultRow
import org.jetbrains.exposed.v1.jdbc.* 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 import kotlin.uuid.Uuid
/**
* Exposed-basierte Implementierung des Horse-Repositorys.
*/
class HorseRepositoryImpl : HorseRepository { class HorseRepositoryImpl : HorseRepository {
private fun rowToDomPferd(row: ResultRow): DomPferd { private fun rowToDomPferd(row: ResultRow): DomPferd {
return DomPferd( return DomPferd(
pferdId = row[HorseTable.id], pferdId = row[HorseTable.id],
kopfnummer = row[HorseTable.kopfnummer],
pferdeName = row[HorseTable.pferdeName], 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], lebensnummer = row[HorseTable.lebensnummer],
chipNummer = row[HorseTable.chipNummer], geschlecht = PferdeGeschlechtE.valueOf(row[HorseTable.geschlecht]),
passNummer = row[HorseTable.passNummer], geburtsjahr = row[HorseTable.geburtsjahr],
oepsNummer = row[HorseTable.oepsNummer], farbe = row[HorseTable.farbe],
feiNummer = row[HorseTable.feiNummer], abstammung = row[HorseTable.abstammung],
vaterName = row[HorseTable.vaterName], vereinNummer = row[HorseTable.vereinNummer],
mutterName = row[HorseTable.mutterName], lastPayYear = row[HorseTable.lastPayYear],
mutterVaterName = row[HorseTable.mutterVaterName], verantwortlichePersonId = row[HorseTable.verantwortlichePersonId],
stockmass = row[HorseTable.stockmass], vater = row[HorseTable.vater],
feiPass = row[HorseTable.feiPass],
satznummer = row[HorseTable.satznummer],
istAktiv = row[HorseTable.istAktiv], istAktiv = row[HorseTable.istAktiv],
bemerkungen = row[HorseTable.bemerkungen], bemerkungen = row[HorseTable.bemerkungen],
datenQuelle = DatenQuelleE.valueOf(row[HorseTable.datenQuelle]), datenQuelle = DatenQuelleE.valueOf(row[HorseTable.datenQuelle]),
@@ -57,28 +54,15 @@ class HorseRepositoryImpl : HorseRepository {
.singleOrNull() .singleOrNull()
} }
override suspend fun findByChipNummer(chipNummer: String): DomPferd? = DatabaseFactory.dbQuery { override suspend fun findBySatznummer(satznummer: String): DomPferd? = DatabaseFactory.dbQuery {
HorseTable.selectAll().where { HorseTable.chipNummer eq chipNummer } HorseTable.selectAll().where { HorseTable.satznummer eq satznummer }
.map(::rowToDomPferd) .map(::rowToDomPferd)
.singleOrNull() .singleOrNull()
} }
override suspend fun findByPassNummer(passNummer: String): DomPferd? = DatabaseFactory.dbQuery { override suspend fun findByKopfnummer(kopfnummer: String): List<DomPferd> = DatabaseFactory.dbQuery {
HorseTable.selectAll().where { HorseTable.passNummer eq passNummer } HorseTable.selectAll().where { HorseTable.kopfnummer eq kopfnummer }
.map(::rowToDomPferd) .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 { override suspend fun findByName(searchTerm: String, limit: Int): List<DomPferd> = DatabaseFactory.dbQuery {
@@ -88,113 +72,29 @@ class HorseRepositoryImpl : HorseRepository {
.map(::rowToDomPferd) .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 { override suspend fun findAllActive(limit: Int): List<DomPferd> = DatabaseFactory.dbQuery {
HorseTable.selectAll().where { HorseTable.istAktiv eq true } HorseTable.selectAll().where { HorseTable.istAktiv eq true }
.limit(limit) .limit(limit)
.map(::rowToDomPferd) .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 { override suspend fun save(horse: DomPferd): DomPferd = DatabaseFactory.dbQuery {
val exists = HorseTable.selectAll().where { HorseTable.id eq horse.pferdId }.any() val exists = HorseTable.selectAll().where { HorseTable.id eq horse.pferdId }.any()
if (exists) { if (exists) {
HorseTable.update({ HorseTable.id eq horse.pferdId }) { HorseTable.update({ HorseTable.id eq horse.pferdId }) {
it[kopfnummer] = horse.kopfnummer
it[pferdeName] = horse.pferdeName 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[lebensnummer] = horse.lebensnummer
it[chipNummer] = horse.chipNummer it[geschlecht] = horse.geschlecht.name
it[passNummer] = horse.passNummer it[geburtsjahr] = horse.geburtsjahr
it[oepsNummer] = horse.oepsNummer it[farbe] = horse.farbe
it[feiNummer] = horse.feiNummer it[abstammung] = horse.abstammung
it[vaterName] = horse.vaterName it[vereinNummer] = horse.vereinNummer
it[mutterName] = horse.mutterName it[lastPayYear] = horse.lastPayYear
it[mutterVaterName] = horse.mutterVaterName it[verantwortlichePersonId] = horse.verantwortlichePersonId
it[stockmass] = horse.stockmass it[vater] = horse.vater
it[feiPass] = horse.feiPass
it[satznummer] = horse.satznummer
it[istAktiv] = horse.istAktiv it[istAktiv] = horse.istAktiv
it[bemerkungen] = horse.bemerkungen it[bemerkungen] = horse.bemerkungen
it[datenQuelle] = horse.datenQuelle.name it[datenQuelle] = horse.datenQuelle.name
@@ -204,24 +104,19 @@ class HorseRepositoryImpl : HorseRepository {
} else { } else {
HorseTable.insert { HorseTable.insert {
it[id] = horse.pferdId it[id] = horse.pferdId
it[kopfnummer] = horse.kopfnummer
it[pferdeName] = horse.pferdeName 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[lebensnummer] = horse.lebensnummer
it[chipNummer] = horse.chipNummer it[geschlecht] = horse.geschlecht.name
it[passNummer] = horse.passNummer it[geburtsjahr] = horse.geburtsjahr
it[oepsNummer] = horse.oepsNummer it[farbe] = horse.farbe
it[feiNummer] = horse.feiNummer it[abstammung] = horse.abstammung
it[vaterName] = horse.vaterName it[vereinNummer] = horse.vereinNummer
it[mutterName] = horse.mutterName it[lastPayYear] = horse.lastPayYear
it[mutterVaterName] = horse.mutterVaterName it[verantwortlichePersonId] = horse.verantwortlichePersonId
it[stockmass] = horse.stockmass it[vater] = horse.vater
it[feiPass] = horse.feiPass
it[satznummer] = horse.satznummer
it[istAktiv] = horse.istAktiv it[istAktiv] = horse.istAktiv
it[bemerkungen] = horse.bemerkungen it[bemerkungen] = horse.bemerkungen
it[datenQuelle] = horse.datenQuelle.name it[datenQuelle] = horse.datenQuelle.name
@@ -240,50 +135,10 @@ class HorseRepositoryImpl : HorseRepository {
HorseTable.selectAll().where { HorseTable.lebensnummer eq lebensnummer }.any() 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 { override suspend fun countActive(): Long = DatabaseFactory.dbQuery {
HorseTable.selectAll().where { HorseTable.istAktiv eq true }.count() 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 { override suspend fun upsertByLebensnummer(horse: DomPferd): DomPferd = DatabaseFactory.dbQuery {
val lebensnummer = horse.lebensnummer ?: return@dbQuery save(horse) val lebensnummer = horse.lebensnummer ?: return@dbQuery save(horse)
@@ -294,24 +149,19 @@ class HorseRepositoryImpl : HorseRepository {
if (existing != null) { if (existing != null) {
val toUpdate = horse.copy(pferdId = existing.pferdId) val toUpdate = horse.copy(pferdId = existing.pferdId)
HorseTable.update({ HorseTable.id eq existing.pferdId }) { HorseTable.update({ HorseTable.id eq existing.pferdId }) {
it[kopfnummer] = toUpdate.kopfnummer
it[pferdeName] = toUpdate.pferdeName 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[HorseTable.lebensnummer] = toUpdate.lebensnummer
it[chipNummer] = toUpdate.chipNummer it[geschlecht] = toUpdate.geschlecht.name
it[passNummer] = toUpdate.passNummer it[geburtsjahr] = toUpdate.geburtsjahr
it[oepsNummer] = toUpdate.oepsNummer it[farbe] = toUpdate.farbe
it[feiNummer] = toUpdate.feiNummer it[abstammung] = toUpdate.abstammung
it[vaterName] = toUpdate.vaterName it[vereinNummer] = toUpdate.vereinNummer
it[mutterName] = toUpdate.mutterName it[lastPayYear] = toUpdate.lastPayYear
it[mutterVaterName] = toUpdate.mutterVaterName it[verantwortlichePersonId] = toUpdate.verantwortlichePersonId
it[stockmass] = toUpdate.stockmass it[vater] = toUpdate.vater
it[feiPass] = toUpdate.feiPass
it[satznummer] = toUpdate.satznummer
it[istAktiv] = toUpdate.istAktiv it[istAktiv] = toUpdate.istAktiv
it[bemerkungen] = toUpdate.bemerkungen it[bemerkungen] = toUpdate.bemerkungen
it[datenQuelle] = toUpdate.datenQuelle.name it[datenQuelle] = toUpdate.datenQuelle.name
@@ -322,4 +172,68 @@ class HorseRepositoryImpl : HorseRepository {
save(horse) 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.core.Table
import org.jetbrains.exposed.v1.datetime.CurrentTimestamp import org.jetbrains.exposed.v1.datetime.CurrentTimestamp
import org.jetbrains.exposed.v1.datetime.date
import org.jetbrains.exposed.v1.datetime.timestamp 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") { object HorseTable : Table("horse") {
val id = uuid("horse_id") val id = uuid("horse_id")
val kopfnummer = varchar("kopfnummer", 4).nullable().index()
val pferdeName = varchar("pferde_name", 200).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 lebensnummer = varchar("lebensnummer", 50).nullable().index()
val chipNummer = varchar("chip_nummer", 50).nullable() val geschlecht = varchar("geschlecht", 20)
val passNummer = varchar("pass_nummer", 50).nullable() val geburtsjahr = integer("geburtsjahr").nullable()
val oepsNummer = varchar("oeps_nummer", 50).nullable() val farbe = varchar("farbe", 100).nullable()
val feiNummer = varchar("fei_nummer", 50).nullable() val abstammung = varchar("abstammung", 100).nullable()
val vaterName = varchar("vater_name", 200).nullable() val vereinNummer = integer("verein_nummer").nullable()
val mutterName = varchar("mutter_name", 200).nullable() val lastPayYear = integer("last_pay_year").nullable()
val mutterVaterName = varchar("mutter_vater_name", 200).nullable() val verantwortlichePersonId = varchar("verantwortliche_person_id", 100).nullable()
val stockmass = integer("stockmass").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 istAktiv = bool("ist_aktiv").default(true)
val bemerkungen = text("bemerkungen").nullable() val bemerkungen = text("bemerkungen").nullable()
val datenQuelle = varchar("daten_quelle", 50) val datenQuelle = varchar("daten_quelle", 50)
@@ -37,4 +32,8 @@ object HorseTable : Table("horse") {
val updatedAt = timestamp("updated_at").defaultExpression(CurrentTimestamp) val updatedAt = timestamp("updated_at").defaultExpression(CurrentTimestamp)
override val primaryKey = PrimaryKey(id) override val primaryKey = PrimaryKey(id)
init {
index("idx_horse_satznummer", isUnique = true, satznummer)
}
} }
@@ -2,6 +2,7 @@
package at.mocode.masterdata.infrastructure.persistence 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.core.Table
import org.jetbrains.exposed.v1.datetime.CurrentTimestamp import org.jetbrains.exposed.v1.datetime.CurrentTimestamp
import org.jetbrains.exposed.v1.datetime.date import org.jetbrains.exposed.v1.datetime.date
@@ -13,20 +14,30 @@ import org.jetbrains.exposed.v1.datetime.timestamp
object ReiterTable : Table("reiter") { object ReiterTable : Table("reiter") {
val id = uuid("reiter_id") val id = uuid("reiter_id")
val personId = uuid("person_id") val personId = uuid("person_id")
val satznummer = varchar("satznummer", 10).uniqueIndex() val satznummer = varchar("satznummer", 10).nullable()
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 nachname = varchar("nachname", 100) val nachname = varchar("nachname", 100)
val vorname = varchar("vorname", 100) val vorname = varchar("vorname", 100)
val geburtsdatum = date("geburtsdatum").nullable() val bundeslandNummer = integer("bundesland_nummer").nullable()
val vereinsNummer = varchar("vereins_nummer", 10).nullable()
val vereinsName = varchar("vereins_name", 200).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 istAktiv = bool("ist_aktiv").default(true)
val bemerkungen = text("bemerkungen").nullable()
val datenQuelle = varchar("daten_quelle", 50) val datenQuelle = varchar("daten_quelle", 50)
val createdAt = timestamp("created_at").defaultExpression(CurrentTimestamp) val createdAt = timestamp("created_at").defaultExpression(CurrentTimestamp)
val updatedAt = timestamp("updated_at").defaultExpression(CurrentTimestamp) val updatedAt = timestamp("updated_at").defaultExpression(CurrentTimestamp)
@@ -98,9 +98,7 @@ class RegulationSeedVerificationTest {
satznummer = "123456", satznummer = "123456",
nachname = "Müller", nachname = "Müller",
vorname = "Hans", vorname = "Hans",
lizenzKlasse = LizenzKlasseE.R1, lizenzKlasse = LizenzKlasseE.R1
lizenzSparten = listOf(SparteE.SPRINGEN),
startkartAktiv = true
) )
val klasseL = TurnierklasseDefinition( 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.FunktionaerRepository
import at.mocode.masterdata.domain.repository.ReiterRepository import at.mocode.masterdata.domain.repository.ReiterRepository
import at.mocode.zns.importer.ZnsImportService import at.mocode.zns.importer.ZnsImportService
import at.mocode.zns.importer.ZnsImportResult
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -27,8 +28,30 @@ class ZnsImportOrchestrator(
val service = ZnsImportService(vereinRepository, reiterRepository, horseRepository, funktionaerRepository) val service = ZnsImportService(vereinRepository, reiterRepository, horseRepository, funktionaerRepository)
val dateien = service.extrahiereDateien(zipBytes.inputStream())
jobRegistry.aktualisiereStatus(jobId, ImportJobStatus.LADE_VEREINE, "Lade Vereine...", 20) 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( jobRegistry.aktualisiereStatus(
jobId, ImportJobStatus.ABGESCHLOSSEN, jobId, ImportJobStatus.ABGESCHLOSSEN,
@@ -1,5 +1,7 @@
package at.mocode.core.utils.parser 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. * 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. * 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) val str = getString(start1Based, length)
return str.toIntOrNull() 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 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.DatenQuelleE
import at.mocode.core.domain.model.LizenzKlasseE import at.mocode.core.domain.model.LizenzKlasseE
import at.mocode.core.domain.model.PferdeGeschlechtE import at.mocode.core.domain.model.PferdeGeschlechtE
import at.mocode.core.utils.parser.FixedWidthLineReader 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.ExperimentalUuidApi
import kotlin.uuid.Uuid import kotlin.uuid.Uuid
@@ -54,24 +53,56 @@ object ZnsLegacyParsers {
val nachname = reader.getString(7, 50) val nachname = reader.getString(7, 50)
val vorname = reader.getString(57, 25) val vorname = reader.getString(57, 25)
val bundeslandNummer = reader.getIntOrNull(82, 2)
val vereinsName = reader.getString(84, 50) val vereinsName = reader.getString(84, 50)
val nation = reader.getString(134, 3) val nation = reader.getString(134, 3)
val reiterLizenz = reader.getString(137, 4)
val lizenzString = reader.getString(137, 4) // Ab Stelle 137 weicht die Realität der ZNS.zip von der Spec 2.4 ab
val lizenz = mapLizenz(lizenzString) // Die Realität (Aichinger Ewald) zeigt:
// 134-136: AUT
val sperrlisteFlag = reader.getString(200, 1) // 137-140: R2
val gesperrt = sperrlisteFlag == "S" // 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( return DomReiter(
personId = Uuid.random(), personId = Uuid.random(),
satznummer = satznummer, satznummer = satznummer,
nachname = nachname, nachname = nachname,
vorname = vorname, vorname = vorname,
bundeslandNummer = bundeslandNummer,
vereinsName = vereinsName.ifBlank { null }, vereinsName = vereinsName.ifBlank { null },
nation = nation.ifBlank { null }, nation = nation.ifBlank { null },
lizenzKlasse = lizenz, reiterLizenz = reiterLizenz.ifBlank { null },
istAktiv = !gesperrt, 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 datenQuelle = DatenQuelleE.IMPORT_ZNS
) )
} }
@@ -80,52 +111,71 @@ object ZnsLegacyParsers {
* Parses a line from PFERDE01.DAT. * Parses a line from PFERDE01.DAT.
*/ */
fun parsePferd(line: String): DomPferd? { 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 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 kopfnummer = reader.getString(1, 4)
val name = reader.getString(5, 30)
val lebensnummer = reader.getString(35, 9) val lebensnummer = reader.getString(35, 9)
val geschlechtChar = reader.getString(44, 1) val geschlechtChar = reader.getString(44, 1)
val geschlecht = mapGeschlecht(geschlechtChar) val geschlecht = mapGeschlecht(geschlechtChar)
val geburtsjahr = reader.getIntOrNull(45, 4) 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( return DomPferd(
pferdeName = name, pferdeName = name,
geschlecht = geschlecht, geschlecht = geschlecht,
geburtsdatum = geburtsdatum, geburtsjahr = geburtsjahr,
lebensnummer = lebensnummer.ifBlank { null }, 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 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 if (line.isBlank() || line.length < 8) return null
val reader = FixedWidthLineReader(line) val reader = FixedWidthLineReader(line)
val satzID = reader.getString(1, 1).uppercase()
if (satzID != "X" && satzID != "Y") return null
val satznummer = reader.getString(2, 6) val satzNummer = reader.getIntOrNull(2, 6)
if (satznummer.isBlank()) return null if (satzNummer == null) return null
val fullName = reader.getString(8, 75) // Name begins directly after the satzNummer (position 8)
val parts = fullName.split(",").map { it.trim() } val name = reader.getString(8, 75).trim()
val nachname = parts.getOrNull(0) ?: fullName // Qualifikation is much later, probably at 83?
val vorname = parts.getOrNull(1) ?: "" // 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( return DomFunktionaer(
richterNummer = satznummer, satzID = satzID,
nachname = nachname, satzNummer = satzNummer,
vorname = vorname, name = name.ifBlank { null },
qualifikationen = qualifikationen,
datenQuelle = DatenQuelleE.IMPORT_ZNS datenQuelle = DatenQuelleE.IMPORT_ZNS
) )
} }
@@ -21,27 +21,35 @@ class ZnsLegacyParsersTest {
@Test @Test
fun `parseLizenz should extract LIZENZ01 correctly`() { fun `parseLizenz should extract LIZENZ01 correctly`() {
val sb = StringBuilder() val sb = StringBuilder()
sb.append("123456") sb.append("123456") // 1-6
sb.append("Mustermann ") sb.append("Mustermann ") // 7-56
sb.append("Max ") sb.append("Max ") // 57-81
sb.append("01") sb.append("01") // 82-83
sb.append("Reitverein Wien ") sb.append("Reitverein Wien ") // 84-133
sb.append("AUT") sb.append("AUT") // 134-136
sb.append("R1 ") sb.append("R1 ") // 137-140
sb.append(" ") // 141-146 (leer)
while (sb.length < 199) { sb.append("00000001") // 147-154 (mitgliedsNummer)
sb.append(" ") sb.append("0676 12345678 ") // 155-176 (telefonNummer length 22)
} sb.append("2026") // 177-180 (lastPayYear)
sb.append("S") 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()) val result = ZnsLegacyParsers.parseLizenz(sb.toString())
assertNotNull(result) assertNotNull(result)
assertEquals("123456", result.satznummer) assertEquals("123456", result.satznummer)
assertEquals("Mustermann", result.nachname) assertEquals("Mustermann", result.nachname)
assertEquals("Max", result.vorname) assertEquals("Max", result.vorname)
assertEquals(1, result.bundeslandNummer)
assertEquals("Reitverein Wien", result.vereinsName) assertEquals("Reitverein Wien", result.vereinsName)
assertEquals(LizenzKlasseE.R1, result.lizenzKlasse) assertEquals("AUT", result.nation)
assertEquals(false, result.istAktiv) assertEquals("R1", result.reiterLizenz)
assertEquals(2026, result.lastPayYear)
assertEquals("M", result.geschlecht)
assertEquals("1980-01-01", result.geburtsdatum.toString())
} }
@Test @Test
@@ -60,21 +68,144 @@ class ZnsLegacyParsersTest {
val result = ZnsLegacyParsers.parsePferd(sb.toString()) val result = ZnsLegacyParsers.parsePferd(sb.toString())
assertNotNull(result) assertNotNull(result)
assertEquals("A123", result.kopfnummer)
assertEquals("0000000001", result.satznummer)
assertEquals("Black Beauty", result.pferdeName) assertEquals("Black Beauty", result.pferdeName)
assertEquals("123456789", result.lebensnummer) assertEquals("123456789", result.lebensnummer)
assertEquals(PferdeGeschlechtE.WALLACH, result.geschlecht) assertEquals(PferdeGeschlechtE.WALLACH, result.geschlecht)
assertEquals(2010, result.geburtsdatum?.year) assertEquals(2010, result.geburtsjahr)
} }
@Test @Test
fun `parseRichter should extract RICHT01 correctly`() { fun `parseFunktionaer should extract RICHT01 correctly for Richter`() {
val line = // Real example from RICHT01.dat
"X123456Richter, Peter GA " val line = "X010128Zitterbart Rainer PI-A"
val result = ZnsLegacyParsers.parseRichter(line) val result = ZnsLegacyParsers.parseFunktionaer(line)
assertNotNull(result) assertNotNull(result)
assertEquals("123456", result.richterNummer) assertEquals("X", result.satzID)
assertEquals("Richter", result.nachname) assertEquals(10128, result.satzNummer)
assertEquals("Peter", result.vorname) 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 | | MITGLIEDSNUMMER | 147 | 8 | Numerisch | FORMAT: 999999999 |
| TELEFONNUMMER | 155 | 21 | Alphanumerisch (21) | Standard: BLANK | | TELEFONNUMMER | 155 | 21 | Alphanumerisch (21) | Standard: BLANK |
| KADER | 176 | 1 | Alphanumerisch (1) | derzeit immer 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` | | GESCHLECHT | 181 | 1 | Alphanumerisch (1) | Werte: `W`, `M` |
| GEBURTSDATUM | 182 | 8 | Datum | FORMAT: `JJJJMMTT` | | GEBURTSDATUM | 182 | 8 | Datum | FORMAT: `JJJJMMTT` |
| FEI-ID | 190 | 10 | Alphanumerisch (10) | Standard: BLANK (10) | | FEI-ID | 190 | 10 | Alphanumerisch (10) | Standard: BLANK (10) |
+16 -5
View File
@@ -1,6 +1,17 @@
## ToDos und Folgearbeiten # ToDos
- 📜 Rulebook Expert: DetailSpezifikation `SEPARATE_SIEGEREHRUNG` (Preisgeld, Ranking, UIHinweise) ergänzen.
- 🧹 Curator: `Ubiquitous_Language.md` um obige Begriffe/Definitionen erweitern. Bitte analysieren, vervollständigen bzw. korrigieren und optimieren.
- 👷 Backend: SchemaMigrationen pro Tenant gemäß obiger Tabellen; Repositories/Services entsprechend zuschneiden. Anschließend alle betroffene Dokumentationen aktualisieren.
- 🎨 Frontend: ViewModels/Stores entlang dieser Struktur aktualisieren (Navigation: Veranstaltung → Turnier → Bewerb → Abteilung).
## 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