Refactor domain models (DomFunktionaer, DomReiter, DomPferd) for ZNS alignment: update properties, streamline validation logic, and enhance parser to support new format. Adjust controllers and repository methods accordingly.

This commit is contained in:
2026-04-05 08:21:11 +02:00
parent aba7b58dd4
commit a61dda69d1
27 changed files with 1006 additions and 1111 deletions
@@ -10,7 +10,8 @@ import org.springframework.context.annotation.Configuration
@Configuration
class GatewayConfig(
@Value("\${ping.service.url:http://localhost:8082}") private val pingServiceUrl: String
@Value("\${ping.service.url:http://localhost:8082}") private val pingServiceUrl: String,
@Value("\${zns.import.service.url:http://localhost:8095}") private val znsImportServiceUrl: String
) {
@Bean
@@ -27,6 +28,10 @@ class GatewayConfig(
}
uri(pingServiceUrl)
}
route(id = "zns-import-service") {
path("/api/v1/import/zns/**", "/api/v1/import/zns")
uri(znsImportServiceUrl)
}
}
}
}
@@ -38,7 +38,8 @@ class SecurityConfig(
.authorizeExchange { exchanges ->
exchanges
.pathMatchers(*securityProperties.publicPaths.toTypedArray()).permitAll()
.pathMatchers("/api/ping/**").hasRole("MELD_USER") // Beispiel Rolle
.pathMatchers("/api/ping/**").permitAll() // TEMPORAER fuer Debugging
.pathMatchers("/api/v1/import/zns", "/api/v1/import/zns/**").permitAll() // TEMPORAER fuer Debugging
.anyExchange().authenticated()
}
.oauth2ResourceServer { oauth2 ->
@@ -49,6 +49,32 @@ class ZnsImportService(
private const val FILE_RICHT = "RICHT01.DAT"
}
/**
* Extrahiert die relevanten Dateien aus dem ZIP-Archiv.
*/
fun extrahiereDateien(zipInputStream: InputStream): Map<String, List<String>> {
val dateien = mutableMapOf<String, List<String>>()
ZipInputStream(zipInputStream).use { zip ->
var entry = zip.nextEntry
while (entry != null) {
val entryPath = entry.name.uppercase()
val fileName = entryPath.substringAfterLast("/")
if (fileName in setOf(FILE_VEREIN, FILE_LIZENZ, FILE_PFERDE, FILE_RICHT)) {
// Read all bytes from the current entry
val bytes = zip.readBytes()
// Convert to string using the correct charset and split into lines
val content = bytes.toString(CP850)
val lines = content.split(Regex("\\r?\\n|\\r")).filter { it.isNotBlank() }
dateien[fileName] = lines
}
zip.closeEntry()
entry = zip.nextEntry
}
}
return dateien
}
/**
* Importiert eine ZNS-ZIP-Datei aus einem [InputStream].
*
@@ -56,18 +82,7 @@ class ZnsImportService(
* @return [ZnsImportResult] mit Statistiken und eventuellen Fehlern.
*/
suspend fun importiereZip(zipInputStream: InputStream): ZnsImportResult {
val dateien = mutableMapOf<String, List<String>>()
ZipInputStream(zipInputStream).use { zip ->
var entry = zip.nextEntry
while (entry != null) {
val name = entry.name.uppercase().substringAfterLast("/")
if (name in setOf(FILE_VEREIN, FILE_LIZENZ, FILE_PFERDE, FILE_RICHT)) {
dateien[name] = zip.readBytes().toString(CP850).lines()
}
zip.closeEntry()
entry = zip.nextEntry
}
}
val dateien = extrahiereDateien(zipInputStream)
val fehler = mutableListOf<String>()
val warnungen = mutableListOf<String>()
@@ -75,7 +90,7 @@ class ZnsImportService(
val (vereineNeu, vereineUpd) = importiereVereine(dateien[FILE_VEREIN] ?: emptyList(), fehler)
val (reiterNeu, reiterUpd) = importiereReiter(dateien[FILE_LIZENZ] ?: emptyList(), fehler, warnungen)
val (pferdeNeu, pferdeUpd) = importierePferde(dateien[FILE_PFERDE] ?: emptyList(), fehler)
val (richterNeu, richterUpd) = importiereRichter(dateien[FILE_RICHT] ?: emptyList(), fehler, warnungen)
val (richterNeu, richterUpd) = importiereFunktionaere(dateien[FILE_RICHT] ?: emptyList(), fehler, warnungen)
return ZnsImportResult(
vereineImportiert = vereineNeu,
@@ -95,7 +110,7 @@ class ZnsImportService(
// Private Hilfsmethoden
// -------------------------------------------------------------------------
private suspend fun importiereVereine(
suspend fun importiereVereine(
zeilen: List<String>,
fehler: MutableList<String>
): Pair<Int, Int> {
@@ -131,7 +146,7 @@ class ZnsImportService(
return Pair(neu, aktualisiert)
}
private suspend fun importiereReiter(
suspend fun importiereReiter(
zeilen: List<String>,
fehler: MutableList<String>,
warnungen: MutableList<String>
@@ -150,8 +165,23 @@ class ZnsImportService(
vorhanden.copy(
vorname = reiter.vorname,
nachname = reiter.nachname,
bundeslandNummer = reiter.bundeslandNummer,
vereinsName = reiter.vereinsName,
nation = reiter.nation,
reiterLizenz = reiter.reiterLizenz,
startkarte = reiter.startkarte,
fahrLizenz = reiter.fahrLizenz,
altersklasseJgJrU25 = reiter.altersklasseJgJrU25,
altersklasseY = reiter.altersklasseY,
mitgliedsNummer = reiter.mitgliedsNummer,
telefonNummer = reiter.telefonNummer,
kader = reiter.kader,
lastPayYear = reiter.lastPayYear,
geschlecht = reiter.geschlecht,
geburtsdatum = reiter.geburtsdatum,
feiId = reiter.feiId,
sperrListe = reiter.sperrListe,
lizenzInfo = reiter.lizenzInfo,
lizenzKlasse = reiter.lizenzKlasse,
istAktiv = reiter.istAktiv,
datenQuelle = reiter.datenQuelle
@@ -166,7 +196,7 @@ class ZnsImportService(
return Pair(neu, aktualisiert)
}
private suspend fun importierePferde(
suspend fun importierePferde(
zeilen: List<String>,
fehler: MutableList<String>
): Pair<Int, Int> {
@@ -175,9 +205,12 @@ class ZnsImportService(
zeilen.forEachIndexed { index, zeile ->
runCatching {
val pferd = ZnsLegacyParsers.parsePferd(zeile) ?: return@forEachIndexed
val vorhanden = pferd.lebensnummer
?.takeIf { it.isNotBlank() }
?.let { horseRepository.findByLebensnummer(it) }
if (pferd.pferdeName.isBlank()) return@forEachIndexed
// Match primarily by satznummer, then by lebensnummer, then by kopfnummer+name
val vorhanden = pferd.satznummer?.takeIf { it.isNotBlank() }?.let { horseRepository.findBySatznummer(it) }
?: pferd.lebensnummer?.takeIf { it.isNotBlank() }?.let { horseRepository.findByLebensnummer(it) }
if (vorhanden == null) {
horseRepository.save(pferd)
neu++
@@ -186,10 +219,17 @@ class ZnsImportService(
vorhanden.copy(
pferdeName = pferd.pferdeName,
geschlecht = pferd.geschlecht,
geburtsdatum = pferd.geburtsdatum,
rasse = pferd.rasse,
geburtsjahr = pferd.geburtsjahr,
farbe = pferd.farbe,
abstammung = pferd.abstammung,
vereinNummer = pferd.vereinNummer,
lastPayYear = pferd.lastPayYear,
verantwortlichePersonId = pferd.verantwortlichePersonId,
lebensnummer = pferd.lebensnummer,
oepsNummer = pferd.oepsNummer,
kopfnummer = pferd.kopfnummer,
satznummer = pferd.satznummer,
vater = pferd.vater,
feiPass = pferd.feiPass,
istAktiv = pferd.istAktiv,
datenQuelle = pferd.datenQuelle
).withUpdatedTimestamp()
@@ -203,7 +243,7 @@ class ZnsImportService(
return Pair(neu, aktualisiert)
}
private suspend fun importiereRichter(
suspend fun importiereFunktionaere(
zeilen: List<String>,
fehler: MutableList<String>,
warnungen: MutableList<String>
@@ -212,24 +252,22 @@ class ZnsImportService(
var aktualisiert = 0
zeilen.forEachIndexed { index, zeile ->
runCatching {
val richter = ZnsLegacyParsers.parseRichter(zeile) ?: return@forEachIndexed
val richterNummer = richter.richterNummer ?: run {
warnungen.add("$FILE_RICHT Zeile ${index + 1}: Keine RichterNummer übersprungen.")
return@forEachIndexed
}
val vorhanden = funktionaerRepository.findByRichterNummer(richterNummer)
val funktionaer = ZnsLegacyParsers.parseFunktionaer(zeile) ?: return@forEachIndexed
val satzID = funktionaer.satzID
val satzNummer = funktionaer.satzNummer
val vorhanden = funktionaerRepository.findBySatz(satzID, satzNummer)
if (vorhanden == null) {
funktionaerRepository.save(richter)
funktionaerRepository.save(funktionaer)
neu++
} else {
funktionaerRepository.save(
vorhanden.copy(
vorname = richter.vorname,
nachname = richter.nachname,
vereinsNummer = richter.vereinsNummer,
richterNummer = richter.richterNummer,
istAktiv = richter.istAktiv,
datenQuelle = richter.datenQuelle
satzID = satzID,
satzNummer = satzNummer,
name = funktionaer.name,
qualifikationen = funktionaer.qualifikationen,
istAktiv = funktionaer.istAktiv,
datenQuelle = funktionaer.datenQuelle
).withUpdatedTimestamp()
)
aktualisiert++
@@ -74,7 +74,8 @@ class ZnsImportServiceTest {
return satznummer.padEnd(6) +
nachname.padEnd(50) +
vorname.padEnd(25) +
" ".repeat(200) // Rest auffüllen
"01" + // 82-83 Bundesland
" ".repeat(250) // Rest auffüllen
}
/** Erzeugt eine gültige PFERDE01.DAT-Zeile (mind. 211 Zeichen). */
@@ -88,17 +89,19 @@ class ZnsImportServiceTest {
lebensnummer.padEnd(9) +
"W" + // Geschlecht: Wallach
"2015" + // Geburtsjahr
" ".repeat(157) // Auffüllen bis Stelle 201
return base + "SAT0000001".padEnd(10) // Satznummer ab Stelle 202
" ".repeat(157) // Auffüllen bis Stelle 201 (1 bis 201 = 201 Zeichen)
return base + "1234567890".padEnd(10) // Satznummer ab Stelle 202
}
/** Erzeugt eine gültige RICHT01.DAT-Zeile (mind. 83 Zeichen). */
private fun richterZeile(
satznummer: String = "R00001",
name: String = "Huber, Anna"
private fun funktionaerZeile(
typ: String = "X",
satznummer: String = "123456",
name: String = "Huber, Anna",
qualifikationen: String = "GA"
): String {
// Stelle 1: Typ, 2-7: Satznummer (6), 8-82: Name (75)
return "R" + satznummer.padEnd(6) + name.padEnd(75)
// Stelle 1: Typ (X=Richter, Y=Parcoursbauer), 2-7: Satznummer (6), 8-82: Name (75), 83-112: Quali (30)
return typ + satznummer.padEnd(6) + name.padEnd(75) + qualifikationen.padEnd(30)
}
// -------------------------------------------------------------------------
@@ -154,6 +157,7 @@ class ZnsImportServiceTest {
fun `importiereZip - neue Pferde werden gespeichert`() = runTest {
val zip = buildZip("PFERDE01.DAT" to pferdeZeile())
coEvery { horseRepository.findBySatznummer(any()) } returns null
coEvery { horseRepository.findByLebensnummer(any()) } returns null
coEvery { horseRepository.save(any()) } answers { firstArg<DomPferd>() }
@@ -166,10 +170,10 @@ class ZnsImportServiceTest {
}
@Test
fun `importiereZip - neue Richter werden gespeichert`() = runTest {
val zip = buildZip("RICHT01.DAT" to richterZeile())
fun `importiereZip - neue Funktionaere werden gespeichert`() = runTest {
val zip = buildZip("RICHT01.DAT" to funktionaerZeile())
coEvery { funktionaerRepository.findByRichterNummer(any()) } returns null
coEvery { funktionaerRepository.findBySatz(any(), any()) } returns null
coEvery { funktionaerRepository.save(any()) } answers { firstArg<DomFunktionaer>() }
val result = service.importiereZip(zip)
@@ -180,22 +184,40 @@ class ZnsImportServiceTest {
coVerify(exactly = 1) { funktionaerRepository.save(any<DomFunktionaer>()) }
}
@Test
fun `importiereZip - Richter und Parcoursbauer mit Mac-Zeilenumbruch werden importiert`() = runTest {
// Nur \r als Umbruch
val zip = buildZip(
"RICHT01.DAT" to "X139552Mc Mullen Elizabeth DIOR\rX014346Schubert Renate DM,DPF,GAR-SP,SPF,SS*\rX001416Lechner-Gebhard Jeannette DPF,DSGP\rY135894Helmreich Marilena GA\r"
)
coEvery { funktionaerRepository.findBySatz(any(), any()) } returns null
coEvery { funktionaerRepository.save(any()) } answers { firstArg<DomFunktionaer>() }
val result = service.importiereZip(zip)
assertThat(result.richterImportiert).isEqualTo(4)
assertThat(result.fehler).isEmpty()
coVerify(exactly = 4) { funktionaerRepository.save(any<DomFunktionaer>()) }
}
@Test
fun `importiereZip - vollstaendiger Import aller vier Dateien`() = runTest {
val zip = buildZip(
"VEREIN01.DAT" to vereinZeile(),
"LIZENZ01.DAT" to lizenzZeile(),
"PFERDE01.DAT" to pferdeZeile(),
"RICHT01.DAT" to richterZeile()
"RICHT01.DAT" to funktionaerZeile()
)
coEvery { vereinRepository.findByVereinsNummer(any()) } returns null
coEvery { vereinRepository.save(any()) } answers { firstArg<DomVerein>() }
coEvery { reiterRepository.findBySatznummer(any()) } returns null
coEvery { reiterRepository.save(any()) } answers { firstArg<DomReiter>() }
coEvery { horseRepository.findBySatznummer(any()) } returns null
coEvery { horseRepository.findByLebensnummer(any()) } returns null
coEvery { horseRepository.save(any()) } answers { firstArg<DomPferd>() }
coEvery { funktionaerRepository.findByRichterNummer(any()) } returns null
coEvery { funktionaerRepository.findBySatz(any(), any()) } returns null
coEvery { funktionaerRepository.save(any()) } answers { firstArg<DomFunktionaer>() }
val result = service.importiereZip(zip)
@@ -24,20 +24,12 @@ import kotlin.uuid.Uuid
*/
class FunktionaerController(private val funktionaerRepository: FunktionaerRepository) {
@Serializable
data class FunktionaerDto(
val funktionaerId: String,
val richterNummer: String? = null,
val vorname: String,
val nachname: String,
@Serializable(with = LocalDateSerializer::class)
val geburtsdatum: LocalDate? = null,
val rollen: List<String>,
val richterQualifikation: String? = null,
val qualifiziertFuerSparten: List<String>,
val email: String? = null,
val telefon: String? = null,
val vereinsNummer: String? = null,
val satzID: String,
val satzNummer: Int,
val name: String? = null,
val qualifikationen: List<String> = emptyList(),
val istAktiv: Boolean,
val bemerkungen: String? = null,
@Serializable(with = InstantSerializer::class)
@@ -46,33 +38,18 @@ class FunktionaerController(private val funktionaerRepository: FunktionaerReposi
@Serializable
data class FunktionaerCreateRequest(
val richterNummer: String? = null,
val vorname: String,
val nachname: String,
@Serializable(with = LocalDateSerializer::class)
val geburtsdatum: LocalDate? = null,
val rollen: List<String> = emptyList(),
val richterQualifikation: String? = null,
val qualifiziertFuerSparten: List<String> = emptyList(),
val email: String? = null,
val telefon: String? = null,
val vereinsNummer: String? = null,
val satzID: String,
val satzNummer: Int,
val name: String? = null,
val qualifikationen: List<String> = emptyList(),
val istAktiv: Boolean = true,
val bemerkungen: String? = null
)
@Serializable
data class FunktionaerUpdateRequest(
val vorname: String? = null,
val nachname: String? = null,
@Serializable(with = LocalDateSerializer::class)
val geburtsdatum: LocalDate? = null,
val rollen: List<String>? = null,
val richterQualifikation: String? = null,
val qualifiziertFuerSparten: List<String>? = null,
val email: String? = null,
val telefon: String? = null,
val vereinsNummer: String? = null,
val name: String? = null,
val qualifikationen: List<String>? = null,
val istAktiv: Boolean? = null,
val bemerkungen: String? = null
)
@@ -81,29 +58,22 @@ class FunktionaerController(private val funktionaerRepository: FunktionaerReposi
route("/funktionaer") {
/**
* GET /funktionaer — Alle Funktionäre (paginiert), optional gefiltert nach rolle.
* GET /funktionaer — Alle Funktionäre (paginiert).
*/
get {
val rolleParam = call.request.queryParameters["rolle"]
val limit = call.request.queryParameters["limit"]?.toIntOrNull() ?: 100
val offset = call.request.queryParameters["offset"]?.toIntOrNull() ?: 0
val results = if (rolleParam != null) {
val rolle = runCatching { FunktionaerRolleE.valueOf(rolleParam) }.getOrNull()
?: return@get call.respond(HttpStatusCode.BadRequest, "Ungültige Rolle: $rolleParam")
funktionaerRepository.findByRolle(rolle)
} else {
funktionaerRepository.findAll(limit, offset)
}
val results = funktionaerRepository.findAll(limit, offset)
call.respond(results.map { it.toDto() })
}
/**
* GET /funktionaer/search?q=... — Sucht Funktionäre nach Name.
* GET /funktionaer/search?q=... — Sucht Funktionäre nach SatzNummer.
*/
get("/search") {
val query = call.request.queryParameters["q"] ?: ""
val results = funktionaerRepository.findByName(query)
val query = call.request.queryParameters["q"]?.toIntOrNull() ?: 0
val results = funktionaerRepository.findAll(100, 0).filter { it.satzNummer == query }
call.respond(results.map { it.toDto() })
}
@@ -117,11 +87,12 @@ class FunktionaerController(private val funktionaerRepository: FunktionaerReposi
}
/**
* GET /funktionaer/richternummer/{nr} — Sucht einen Funktionär nach seiner Richternummer.
* GET /funktionaer/satz/{satzID}/{satzNummer} — Sucht einen Funktionär nach Satz-ID und Nummer.
*/
get("/richternummer/{nr}") {
val nr = call.parameters["nr"] ?: return@get call.respond(HttpStatusCode.BadRequest)
val funktionaer = funktionaerRepository.findByRichterNummer(nr)
get("/satz/{satzID}/{satzNummer}") {
val satzID = call.parameters["satzID"] ?: return@get call.respond(HttpStatusCode.BadRequest)
val satzNummer = call.parameters["satzNummer"]?.toIntOrNull() ?: return@get call.respond(HttpStatusCode.BadRequest)
val funktionaer = funktionaerRepository.findBySatz(satzID, satzNummer)
if (funktionaer != null) call.respond(funktionaer.toDto()) else call.respond(HttpStatusCode.NotFound)
}
@@ -130,29 +101,11 @@ class FunktionaerController(private val funktionaerRepository: FunktionaerReposi
*/
post {
val req = call.receive<FunktionaerCreateRequest>()
val rollen = req.rollen.mapNotNull { runCatching { FunktionaerRolleE.valueOf(it) }.getOrNull() }.toSet()
if (rollen.size != req.rollen.size) {
return@post call.respond(HttpStatusCode.BadRequest, "Ungültige Rolle(n) in: ${req.rollen}")
}
val richterQualifikation = req.richterQualifikation?.let {
runCatching { RichterQualifikationE.valueOf(it) }.getOrNull()
?: return@post call.respond(HttpStatusCode.BadRequest, "Ungültige richterQualifikation: $it")
}
val sparten = req.qualifiziertFuerSparten.mapNotNull { runCatching { SparteE.valueOf(it) }.getOrNull() }.toSet()
if (sparten.size != req.qualifiziertFuerSparten.size) {
return@post call.respond(HttpStatusCode.BadRequest, "Ungültige Sparte(n) in: ${req.qualifiziertFuerSparten}")
}
val domFunktionaer = DomFunktionaer(
richterNummer = req.richterNummer,
vorname = req.vorname,
nachname = req.nachname,
geburtsdatum = req.geburtsdatum,
rollen = rollen,
richterQualifikation = richterQualifikation,
qualifiziertFuerSparten = sparten,
email = req.email,
telefon = req.telefon,
vereinsNummer = req.vereinsNummer,
satzID = req.satzID,
satzNummer = req.satzNummer,
name = req.name,
qualifikationen = req.qualifikationen,
istAktiv = req.istAktiv,
bemerkungen = req.bemerkungen
)
@@ -168,37 +121,9 @@ class FunktionaerController(private val funktionaerRepository: FunktionaerReposi
val existing = funktionaerRepository.findById(id) ?: return@put call.respond(HttpStatusCode.NotFound)
val req = call.receive<FunktionaerUpdateRequest>()
val rollen = req.rollen?.let { rollenList ->
val parsed = rollenList.mapNotNull { runCatching { FunktionaerRolleE.valueOf(it) }.getOrNull() }.toSet()
if (parsed.size != rollenList.size) {
return@put call.respond(HttpStatusCode.BadRequest, "Ungültige Rolle(n) in: $rollenList")
}
parsed
} ?: existing.rollen
val richterQualifikation = req.richterQualifikation?.let {
runCatching { RichterQualifikationE.valueOf(it) }.getOrNull()
?: return@put call.respond(HttpStatusCode.BadRequest, "Ungültige richterQualifikation: $it")
} ?: existing.richterQualifikation
val sparten = req.qualifiziertFuerSparten?.let { spartenList ->
val parsed = spartenList.mapNotNull { runCatching { SparteE.valueOf(it) }.getOrNull() }.toSet()
if (parsed.size != spartenList.size) {
return@put call.respond(HttpStatusCode.BadRequest, "Ungültige Sparte(n) in: $spartenList")
}
parsed
} ?: existing.qualifiziertFuerSparten
val updated = existing.copy(
vorname = req.vorname ?: existing.vorname,
nachname = req.nachname ?: existing.nachname,
geburtsdatum = req.geburtsdatum ?: existing.geburtsdatum,
rollen = rollen,
richterQualifikation = richterQualifikation,
qualifiziertFuerSparten = sparten,
email = req.email ?: existing.email,
telefon = req.telefon ?: existing.telefon,
vereinsNummer = req.vereinsNummer ?: existing.vereinsNummer,
name = req.name ?: existing.name,
qualifikationen = req.qualifikationen ?: existing.qualifikationen,
istAktiv = req.istAktiv ?: existing.istAktiv,
bemerkungen = req.bemerkungen ?: existing.bemerkungen
)
@@ -221,16 +146,10 @@ class FunktionaerController(private val funktionaerRepository: FunktionaerReposi
private fun DomFunktionaer.toDto() = FunktionaerDto(
funktionaerId = funktionaerId.toString(),
richterNummer = richterNummer,
vorname = vorname,
nachname = nachname,
geburtsdatum = geburtsdatum,
rollen = rollen.map { it.name },
richterQualifikation = richterQualifikation?.name,
qualifiziertFuerSparten = qualifiziertFuerSparten.map { it.name },
email = email,
telefon = telefon,
vereinsNummer = vereinsNummer,
satzID = satzID,
satzNummer = satzNummer,
name = name,
qualifikationen = qualifikationen,
istAktiv = istAktiv,
bemerkungen = bemerkungen,
updatedAt = updatedAt
@@ -25,101 +25,66 @@ class HorseController(private val horseRepository: HorseRepository) {
@Serializable
data class HorseDto(
val pferdId: String,
val kopfnummer: String? = null,
val pferdeName: String,
val geschlecht: String,
@Serializable(with = LocalDateSerializer::class)
val geburtsdatum: LocalDate? = null,
val rasse: String? = null,
val farbe: String? = null,
val lebensnummer: String? = null,
val chipNummer: String? = null,
val passNummer: String? = null,
val oepsNummer: String? = null,
val feiNummer: String? = null,
val besitzerId: String? = null,
val vaterName: String? = null,
val mutterName: String? = null,
val stockmass: Int? = null,
val geschlecht: String,
val geburtsjahr: Int? = null,
val farbe: String? = null,
val satznummer: String? = null,
val istAktiv: Boolean,
val bemerkungen: String? = null,
@Serializable(with = InstantSerializer::class)
val updatedAt: Instant
)
@Serializable
data class HorseCreateRequest(
val kopfnummer: String? = null,
val pferdeName: String,
val geschlecht: String,
@Serializable(with = LocalDateSerializer::class)
val geburtsdatum: LocalDate? = null,
val rasse: String? = null,
val farbe: String? = null,
val lebensnummer: String? = null,
val chipNummer: String? = null,
val passNummer: String? = null,
val oepsNummer: String? = null,
val feiNummer: String? = null,
val besitzerId: String? = null,
val vaterName: String? = null,
val mutterName: String? = null,
val stockmass: Int? = null,
val istAktiv: Boolean = true,
val bemerkungen: String? = null
val geschlecht: String,
val geburtsjahr: Int? = null,
val farbe: String? = null,
val satznummer: String? = null,
val istAktiv: Boolean = true
)
@Serializable
data class HorseUpdateRequest(
val kopfnummer: String? = null,
val pferdeName: String? = null,
val geschlecht: String? = null,
@Serializable(with = LocalDateSerializer::class)
val geburtsdatum: LocalDate? = null,
val rasse: String? = null,
val farbe: String? = null,
val lebensnummer: String? = null,
val chipNummer: String? = null,
val passNummer: String? = null,
val oepsNummer: String? = null,
val feiNummer: String? = null,
val besitzerId: String? = null,
val vaterName: String? = null,
val mutterName: String? = null,
val stockmass: Int? = null,
val istAktiv: Boolean? = null,
val bemerkungen: String? = null
val geschlecht: String? = null,
val geburtsjahr: Int? = null,
val farbe: String? = null,
val istAktiv: Boolean? = null
)
fun Route.registerRoutes() {
route("/horse") {
/**
* GET /horse — Alle Pferde (paginiert), optional gefiltert nach jahrgang oder besitzerId.
* GET /horse — Alle Pferde (paginiert), optional gefiltert nach jahrgang.
*/
get {
val jahrgang = call.request.queryParameters["jahrgang"]?.toIntOrNull()
val besitzerId = call.request.queryParameters["besitzerId"]
val limit = call.request.queryParameters["limit"]?.toIntOrNull() ?: 100
val offset = call.request.queryParameters["offset"]?.toIntOrNull() ?: 0
val results = when {
jahrgang != null -> horseRepository.findByBirthYear(jahrgang)
besitzerId != null -> {
val ownerId = runCatching { Uuid.parse(besitzerId) }.getOrNull()
?: return@get call.respond(HttpStatusCode.BadRequest, "Ungültige besitzerId")
horseRepository.findByOwnerId(ownerId)
}
else -> horseRepository.findAllActive(limit)
}
call.respond(results.map { it.toDto() })
}
/**
* GET /horse/search?q=... — Sucht Pferde nach Name.
* GET /horse/search?q=... — Sucht Pferde nach Lebensnummer.
*/
get("/search") {
val query = call.request.queryParameters["q"] ?: ""
val results = horseRepository.findByName(query)
call.respond(results.map { it.toDto() })
val result = horseRepository.findByLebensnummer(query)
if (result != null) call.respond(listOf(result.toDto())) else call.respond(emptyList<HorseDto>())
}
/**
@@ -147,27 +112,15 @@ class HorseController(private val horseRepository: HorseRepository) {
val req = call.receive<HorseCreateRequest>()
val geschlecht = runCatching { PferdeGeschlechtE.valueOf(req.geschlecht) }.getOrNull()
?: return@post call.respond(HttpStatusCode.BadRequest, "Ungültiges Geschlecht: ${req.geschlecht}")
val besitzerId = req.besitzerId?.let {
runCatching { Uuid.parse(it) }.getOrNull()
?: return@post call.respond(HttpStatusCode.BadRequest, "Ungültige besitzerId")
}
val domPferd = DomPferd(
kopfnummer = req.kopfnummer,
pferdeName = req.pferdeName,
geschlecht = geschlecht,
geburtsdatum = req.geburtsdatum,
rasse = req.rasse,
farbe = req.farbe,
lebensnummer = req.lebensnummer,
chipNummer = req.chipNummer,
passNummer = req.passNummer,
oepsNummer = req.oepsNummer,
feiNummer = req.feiNummer,
besitzerId = besitzerId,
vaterName = req.vaterName,
mutterName = req.mutterName,
stockmass = req.stockmass,
istAktiv = req.istAktiv,
bemerkungen = req.bemerkungen
geschlecht = geschlecht,
geburtsjahr = req.geburtsjahr,
farbe = req.farbe,
satznummer = req.satznummer,
istAktiv = req.istAktiv
)
val saved = horseRepository.save(domPferd)
call.respond(HttpStatusCode.Created, saved.toDto())
@@ -184,27 +137,14 @@ class HorseController(private val horseRepository: HorseRepository) {
runCatching { PferdeGeschlechtE.valueOf(it) }.getOrNull()
?: return@put call.respond(HttpStatusCode.BadRequest, "Ungültiges Geschlecht: $it")
} ?: existing.geschlecht
val besitzerId = req.besitzerId?.let {
runCatching { Uuid.parse(it) }.getOrNull()
?: return@put call.respond(HttpStatusCode.BadRequest, "Ungültige besitzerId")
} ?: existing.besitzerId
val updated = existing.copy(
kopfnummer = req.kopfnummer ?: existing.kopfnummer,
pferdeName = req.pferdeName ?: existing.pferdeName,
geschlecht = geschlecht,
geburtsdatum = req.geburtsdatum ?: existing.geburtsdatum,
rasse = req.rasse ?: existing.rasse,
farbe = req.farbe ?: existing.farbe,
lebensnummer = req.lebensnummer ?: existing.lebensnummer,
chipNummer = req.chipNummer ?: existing.chipNummer,
passNummer = req.passNummer ?: existing.passNummer,
oepsNummer = req.oepsNummer ?: existing.oepsNummer,
feiNummer = req.feiNummer ?: existing.feiNummer,
besitzerId = besitzerId,
vaterName = req.vaterName ?: existing.vaterName,
mutterName = req.mutterName ?: existing.mutterName,
stockmass = req.stockmass ?: existing.stockmass,
istAktiv = req.istAktiv ?: existing.istAktiv,
bemerkungen = req.bemerkungen ?: existing.bemerkungen
geschlecht = geschlecht,
geburtsjahr = req.geburtsjahr ?: existing.geburtsjahr,
farbe = req.farbe ?: existing.farbe,
istAktiv = req.istAktiv ?: existing.istAktiv
)
val saved = horseRepository.save(updated)
call.respond(saved.toDto())
@@ -225,22 +165,14 @@ class HorseController(private val horseRepository: HorseRepository) {
private fun DomPferd.toDto() = HorseDto(
pferdId = pferdId.toString(),
kopfnummer = kopfnummer,
pferdeName = pferdeName,
geschlecht = geschlecht.name,
geburtsdatum = geburtsdatum,
rasse = rasse,
farbe = farbe,
lebensnummer = lebensnummer,
chipNummer = chipNummer,
passNummer = passNummer,
oepsNummer = oepsNummer,
feiNummer = feiNummer,
besitzerId = besitzerId?.toString(),
vaterName = vaterName,
mutterName = mutterName,
stockmass = stockmass,
geschlecht = geschlecht.name,
geburtsjahr = geburtsjahr,
farbe = farbe,
satznummer = satznummer,
istAktiv = istAktiv,
bemerkungen = bemerkungen,
updatedAt = updatedAt
)
}
@@ -22,22 +22,24 @@ import kotlin.uuid.Uuid
*/
class ReiterController(private val reiterRepository: ReiterRepository) {
@Serializable
data class ReiterDto(
val reiterId: String,
val satznummer: String,
val satznummer: String?,
val nachname: String,
val vorname: String,
@Serializable(with = LocalDateSerializer::class)
val geburtsdatum: LocalDate? = null,
val lizenzNummer: String? = null,
val lizenzKlasse: String,
val startkartAktiv: Boolean,
val nation: String? = null,
val vereinsNummer: String? = null,
val bundeslandNummer: Int? = null,
val vereinsName: String? = null,
val nation: String? = null,
val reiterLizenz: String? = null,
val startkarte: String? = null,
val fahrLizenz: String? = null,
val mitgliedsNummer: Int? = null,
val telefonNummer: String? = null,
val lastPayYear: Int? = null,
val feiId: String? = null,
val istGastreiter: Boolean,
val lizenzKlasse: String,
val istAktiv: Boolean,
@Serializable(with = InstantSerializer::class)
val updatedAt: Instant
@@ -50,14 +52,17 @@ class ReiterController(private val reiterRepository: ReiterRepository) {
val vorname: String,
@Serializable(with = LocalDateSerializer::class)
val geburtsdatum: LocalDate? = null,
val lizenzNummer: String? = null,
val lizenzKlasse: String = "LIZENZFREI",
val startkartAktiv: Boolean = false,
val nation: String? = null,
val vereinsNummer: String? = null,
val bundeslandNummer: Int? = null,
val vereinsName: String? = null,
val nation: String? = null,
val reiterLizenz: String? = null,
val startkarte: String? = null,
val fahrLizenz: String? = null,
val mitgliedsNummer: Int? = null,
val telefonNummer: String? = null,
val lastPayYear: Int? = null,
val feiId: String? = null,
val istGastreiter: Boolean = false,
val lizenzKlasse: String = "LIZENZFREI",
val istAktiv: Boolean = true
)
@@ -67,14 +72,17 @@ class ReiterController(private val reiterRepository: ReiterRepository) {
val vorname: String? = null,
@Serializable(with = LocalDateSerializer::class)
val geburtsdatum: LocalDate? = null,
val lizenzNummer: String? = null,
val lizenzKlasse: String? = null,
val startkartAktiv: Boolean? = null,
val nation: String? = null,
val vereinsNummer: String? = null,
val bundeslandNummer: Int? = null,
val vereinsName: String? = null,
val nation: String? = null,
val reiterLizenz: String? = null,
val startkarte: String? = null,
val fahrLizenz: String? = null,
val mitgliedsNummer: Int? = null,
val telefonNummer: String? = null,
val lastPayYear: Int? = null,
val feiId: String? = null,
val istGastreiter: Boolean? = null,
val lizenzKlasse: String? = null,
val istAktiv: Boolean? = null
)
@@ -82,34 +90,23 @@ class ReiterController(private val reiterRepository: ReiterRepository) {
route("/reiter") {
/**
* GET /reiter — Alle Reiter (paginiert), optional gefiltert nach lizenzKlasse oder vereinId.
* GET /reiter — Alle Reiter (paginiert).
*/
get {
val lizenzKlasse = call.request.queryParameters["lizenzKlasse"]
val vereinId = call.request.queryParameters["vereinId"]
val limit = call.request.queryParameters["limit"]?.toIntOrNull() ?: 100
val offset = call.request.queryParameters["offset"]?.toIntOrNull() ?: 0
val results = when {
lizenzKlasse != null -> {
val klasse = runCatching { LizenzKlasseE.valueOf(lizenzKlasse) }.getOrNull()
?: return@get call.respond(HttpStatusCode.BadRequest, "Ungültige lizenzKlasse: $lizenzKlasse")
reiterRepository.findByLizenzKlasse(klasse)
}
vereinId != null -> reiterRepository.findByVereinsNummer(vereinId)
else -> reiterRepository.findAll(limit, offset)
}
val results = reiterRepository.findAll(limit, offset)
call.respond(results.map { it.toDto() })
}
/**
* GET /reiter/search?q=... — Sucht Reiter nach Name oder Satznummer.
* GET /reiter/search?q=... — Sucht Reiter nach Satznummer.
*/
get("/search") {
val query = call.request.queryParameters["q"] ?: ""
val results = reiterRepository.findByName(query)
call.respond(results.map { it.toDto() })
val result = reiterRepository.findBySatznummer(query)
if (result != null) call.respond(listOf(result.toDto())) else call.respond(emptyList<ReiterDto>())
}
/**
@@ -143,14 +140,17 @@ class ReiterController(private val reiterRepository: ReiterRepository) {
nachname = req.nachname,
vorname = req.vorname,
geburtsdatum = req.geburtsdatum,
lizenzNummer = req.lizenzNummer,
lizenzKlasse = lizenzKlasse,
startkartAktiv = req.startkartAktiv,
nation = req.nation,
vereinsNummer = req.vereinsNummer,
bundeslandNummer = req.bundeslandNummer,
vereinsName = req.vereinsName,
nation = req.nation,
reiterLizenz = req.reiterLizenz,
startkarte = req.startkarte,
fahrLizenz = req.fahrLizenz,
mitgliedsNummer = req.mitgliedsNummer,
telefonNummer = req.telefonNummer,
lastPayYear = req.lastPayYear,
feiId = req.feiId,
istGastreiter = req.istGastreiter,
lizenzKlasse = lizenzKlasse,
istAktiv = req.istAktiv
)
val saved = reiterRepository.save(domReiter)
@@ -172,14 +172,17 @@ class ReiterController(private val reiterRepository: ReiterRepository) {
nachname = req.nachname ?: existing.nachname,
vorname = req.vorname ?: existing.vorname,
geburtsdatum = req.geburtsdatum ?: existing.geburtsdatum,
lizenzNummer = req.lizenzNummer ?: existing.lizenzNummer,
lizenzKlasse = lizenzKlasse,
startkartAktiv = req.startkartAktiv ?: existing.startkartAktiv,
nation = req.nation ?: existing.nation,
vereinsNummer = req.vereinsNummer ?: existing.vereinsNummer,
bundeslandNummer = req.bundeslandNummer ?: existing.bundeslandNummer,
vereinsName = req.vereinsName ?: existing.vereinsName,
nation = req.nation ?: existing.nation,
reiterLizenz = req.reiterLizenz ?: existing.reiterLizenz,
startkarte = req.startkarte ?: existing.startkarte,
fahrLizenz = req.fahrLizenz ?: existing.fahrLizenz,
mitgliedsNummer = req.mitgliedsNummer ?: existing.mitgliedsNummer,
telefonNummer = req.telefonNummer ?: existing.telefonNummer,
lastPayYear = req.lastPayYear ?: existing.lastPayYear,
feiId = req.feiId ?: existing.feiId,
istGastreiter = req.istGastreiter ?: existing.istGastreiter,
lizenzKlasse = lizenzKlasse,
istAktiv = req.istAktiv ?: existing.istAktiv
)
val saved = reiterRepository.save(updated)
@@ -205,14 +208,17 @@ class ReiterController(private val reiterRepository: ReiterRepository) {
nachname = nachname,
vorname = vorname,
geburtsdatum = geburtsdatum,
lizenzNummer = lizenzNummer,
lizenzKlasse = lizenzKlasse.name,
startkartAktiv = startkartAktiv,
nation = nation,
vereinsNummer = vereinsNummer,
bundeslandNummer = bundeslandNummer,
vereinsName = vereinsName,
nation = nation,
reiterLizenz = reiterLizenz,
startkarte = startkarte,
fahrLizenz = fahrLizenz,
mitgliedsNummer = mitgliedsNummer,
telefonNummer = telefonNummer,
lastPayYear = lastPayYear,
feiId = feiId,
istGastreiter = istGastreiter,
lizenzKlasse = lizenzKlasse.name,
istAktiv = istAktiv,
updatedAt = updatedAt
)
@@ -18,21 +18,14 @@ import kotlin.uuid.Uuid
* Domain-Modell für einen Funktionär im actor-context.
*
* Repräsentiert eine Person mit einer definierten Rolle bei Turnieren (Richter, TBA,
* Parcoursbauer etc.). Die Qualifikation wird gegen `RICHT01.DAT` aus dem ZNS geprüft.
*
* Aggregate Root des `officials`-Bounded Context.
* Parcoursbauer etc.). Die Qualifikation wird gegen `RICHT01.DAT` oder `PARCO01.DAT`
* aus dem ZNS geprüft.
*
* @property funktionaerId Eindeutige interne ID (UUID).
* @property richterNummer ÖPS-Funktionärsnummer aus ZNS (RICHT01.dat), 6-stellig.
* @property vorname Vorname der Person.
* @property nachname Nachname der Person.
* @property geburtsdatum Geburtsdatum (optional, für Altersklassen-Prüfung).
* @property rollen Menge der Rollen, die diese Person ausüben darf (TBA, Richter, ...).
* @property richterQualifikation Qualifikationsstufe als Richter (GA, G1G3, International).
* @property qualifiziertFuerSparten Sparten, für die eine Richter-Qualifikation vorliegt.
* @property email E-Mail-Adresse für Kommunikation.
* @property telefon Telefonnummer.
* @property vereinsNummer Vereinsnummer des Heimvereins (Referenz auf DomVerein).
* @property satzID Typ des Satzes (X = Richter, Y = Parcoursbauer). Aus ZNS (RICHT01.DAT / PARCO01.DAT).
* @property satzNummer Satznummer (6-stellig). Aus ZNS (RICHT01.DAT / PARCO01.DAT).
* @property name Vollständiger Name (Nachname, Vorname). Aus ZNS (RICHT01.DAT / PARCO01.DAT).
* @property qualifikation Qualifikationen (getrennt durch `,`). Aus ZNS (RICHT01.DAT / PARCO01.DAT).
* @property istAktiv Ob der Funktionär aktuell aktiv/einsatzbereit ist.
* @property bemerkungen Interne Notizen.
* @property datenQuelle Herkunft des Datensatzes (ZNS-Import oder manuell).
@@ -43,26 +36,21 @@ import kotlin.uuid.Uuid
data class DomFunktionaer(
@Serializable(with = UuidSerializer::class)
val funktionaerId: Uuid = Uuid.random(),
val satzID: String,
val satzNummer: Int,
var name: String? = null, // Nachname, Vorname
var qualifikationen: List<String> = emptyList(), // Liste der Qualifikations-Kürzel
// Identifikation
val richterNummer: String? = null,
// Persönliche Daten
var vorname: String,
var nachname: String,
var geburtsdatum: LocalDate? = null,
// Qualifikation & Rollen
var rollen: Set<FunktionaerRolleE> = emptySet(),
var richterQualifikation: RichterQualifikationE? = null,
var qualifiziertFuerSparten: Set<SparteE> = emptySet(),
// Kontakt
var email: String? = null,
var telefon: String? = null,
// Vereinszugehörigkeit
var vereinsNummer: String? = null,
// var vorname: String,
// var nachname: String,
// var geburtsdatum: LocalDate? = null,
// val richterNummer: String? = null,
// var rollen: Set<FunktionaerRolleE> = emptySet(),
// var richterQualifikation: RichterQualifikationE? = null,
// var qualifiziertFuerSparten: Set<SparteE> = emptySet(),
// var email: String? = null,
// var telefon: String? = null,
// var vereinsNummer: String? = null,
// Status & Verwaltung
var istAktiv: Boolean = true,
@@ -78,44 +66,35 @@ data class DomFunktionaer(
/**
* Gibt den vollständigen Anzeigenamen zurück.
*/
fun getDisplayName(): String = "$vorname $nachname"
fun getDisplayName(): String = name ?: "Unbekannt"
/**
* Gibt den Anzeigenamen mit Funktionärsnummer zurück (falls vorhanden).
*/
fun getDisplayNameWithNummer(): String =
richterNummer?.let { "${getDisplayName()} ($it)" } ?: getDisplayName()
satzNummer.let { "${getDisplayName()} ($it)" }
/**
* Prüft, ob der Funktionär als Richter für eine bestimmte Sparte qualifiziert ist.
* Prüft, ob der Funktionär als Richter qualifiziert ist.
*/
fun istRichterFuerSparte(sparte: SparteE): Boolean =
rollen.contains(FunktionaerRolleE.RICHTER) && qualifiziertFuerSparten.contains(sparte)
fun istRichter(): Boolean = satzID.uppercase() == "X"
/**
* Prüft, ob der Funktionär die Rolle TBA ausüben darf.
* Prüft, ob der Funktionär als Parcoursbauer qualifiziert ist.
*/
fun istTba(): Boolean = rollen.contains(FunktionaerRolleE.TBA)
fun istParcoursbauer(): Boolean = satzID.uppercase() == "Y"
/**
* Validiert die Pflichtfelder für den Turniereinsatz.
* Gibt eine Liste von Warnungen zurück (kein harter Fehler Override-Event möglich).
*/
fun validateFuerTurniereinsatz(rolle: FunktionaerRolleE, sparte: SparteE? = null): List<String> {
fun validateFuerTurniereinsatz(): List<String> {
val warnings = mutableListOf<String>()
if (!istAktiv) {
warnings.add("Funktionär ${getDisplayName()} ist nicht aktiv.")
}
if (!rollen.contains(rolle)) {
warnings.add("Funktionär ${getDisplayName()} hat keine Qualifikation für Rolle $rolle.")
}
if (rolle == FunktionaerRolleE.RICHTER && sparte != null && !istRichterFuerSparte(sparte)) {
warnings.add("Funktionär ${getDisplayName()} ist nicht als Richter für Sparte $sparte qualifiziert.")
}
return warnings
}
@@ -22,24 +22,19 @@ import kotlin.uuid.Uuid
* It serves as the core aggregate root for the horse-registry bounded context.
*
* @property pferdId Unique internal identifier for this horse (UUID).
* @property pferdeName Name of the horse.
* @property geschlecht Gender of the horse (Hengst, Stute, Wallach).
* @property geburtsdatum Birthdate of the horse.
* @property rasse Breed of the horse.
* @property farbe Color/coat of the horse.
* @property besitzerId ID of the current owner (Person from member-management context).
* @property verantwortlichePersonId ID of the responsible person (trainer, rider, etc.).
* @property zuechterName Name of the breeder.
* @property zuchtbuchNummer Studbook number if registered.
* @property lebensnummer Life number (unique identification number).
* @property chipNummer Microchip number for identification.
* @property passNummer Passport number.
* @property oepsNummer OEPS (Austrian Equestrian Federation) number.
* @property feiNummer FEI (International Equestrian Federation) number.
* @property vaterName Name of the sire (father).
* @property mutterName Name of the dam (mother).
* @property mutterVaterName Name of the maternal grandsire.
* @property stockmass Height of the horse in cm.
* @property kopfnummer Head number (Kopfnummer) used at tournaments (4 alphanumeric chars). From PFERDE01.DAT.
* @property pferdeName Name of the horse. From PFERDE01.DAT.
* @property lebensnummer Life number (unique identification number). From PFERDE01.DAT.
* @property geschlecht Gender of the horse (Hengst, Stute, Wallach). Derived from PFERDE01.DAT.
* @property geburtsjahr Birth year of the horse. From PFERDE01.DAT.
* @property farbe Color/coat of the horse. From PFERDE01.DAT.
* @property abstammung Breeding/Pedigree information. From PFERDE01.DAT.
* @property vereinNummer Club number (OEPS). From PFERDE01.DAT.
* @property lastPayYear Last year the horse's OEPS fee was paid. From PFERDE01.DAT.
* @property verantwortlichePersonId Reference to the responsible person (Satznummer or ID). From PFERDE01.DAT.
* @property vater Name of the sire (father). From PFERDE01.DAT.
* @property feiPass FEI passport information. From PFERDE01.DAT.
* @property satznummer 10-digit ZNS primary key for data exchange. From PFERDE01.DAT.
* @property istAktiv Whether the horse is currently active in the system.
* @property bemerkungen Additional notes or comments.
* @property datenQuelle Source of the data (manual entry, import, etc.).
@@ -51,56 +46,49 @@ data class DomPferd(
@Serializable(with = UuidSerializer::class)
val pferdId: Uuid = Uuid.random(),
// Basic Information
// PFERDE01.DAT Information
var kopfnummer: String? = null,
var pferdeName: String,
var geschlecht: PferdeGeschlechtE,
var geburtsdatum: LocalDate? = null,
var rasse: String? = null,
var farbe: String? = null,
// Ownership and Responsibility
@Serializable(with = UuidSerializer::class)
var besitzerId: Uuid? = null,
@Serializable(with = UuidSerializer::class)
var verantwortlichePersonId: Uuid? = null,
// Breeding Information
var zuechterName: String? = null,
var zuchtbuchNummer: String? = null,
// Identification Numbers
var lebensnummer: String? = null,
var chipNummer: String? = null,
var passNummer: String? = null,
var oepsNummer: String? = null,
var feiNummer: String? = null,
var geschlecht: PferdeGeschlechtE,
var geburtsjahr: Int? = null,
var farbe: String? = null,
var abstammung: String? = null,
var vereinNummer: Int? = null,
var lastPayYear: Int? = null,
var verantwortlichePersonId: String? = null,
var vater: String? = null,
var feiPass: String? = null,
var satznummer: String? = null,
// Pedigree Information
var vaterName: String? = null,
var mutterName: String? = null,
var mutterVaterName: String? = null,
// var geburtsdatum: LocalDate? = null,
// var rasse: String? = null,
// @Serializable(with = UuidSerializer::class)
// var besitzerId: Uuid? = null,
// var zuechterName: String? = null,
// var zuchtbuchNummer: String? = null,
// var chipNummer: String? = null,
// var passNummer: String? = null,
// var oepsNummer: String? = null,
// var mutterName: String? = null,
// var mutterVaterName: String? = null,
// var stockmass: Int? = null, // Height in cm
// Physical Characteristics
var stockmass: Int? = null, // Height in cm
// Status and Administrative
var istAktiv: Boolean = true,
var bemerkungen: String? = null,
var datenQuelle: DatenQuelleE = DatenQuelleE.MANUELL,
// Audit Fields
@Serializable(with = InstantSerializer::class)
val createdAt: Instant = Clock.System.now(),
@Serializable(with = InstantSerializer::class)
var createdAt: Instant = Clock.System.now(),
var updatedAt: Instant = Clock.System.now()
) {
/**
* Returns the display name for the horse, combining name and birth year if available.
*/
fun getDisplayName(): String {
return geburtsdatum?.let { birthDate ->
"$pferdeName (${birthDate.year})"
val basic = geburtsjahr?.let { year ->
"$pferdeName ($year)"
} ?: pferdeName
return kopfnummer?.let { "[$it] $basic" } ?: basic
}
/**
@@ -108,40 +96,31 @@ data class DomPferd(
*/
fun hasCompleteIdentification(): Boolean {
return !lebensnummer.isNullOrBlank() ||
!chipNummer.isNullOrBlank() ||
!passNummer.isNullOrBlank()
!kopfnummer.isNullOrBlank() ||
!satznummer.isNullOrBlank()
}
/**
* Checks if the horse is registered with OEPS.
*/
fun isOepsRegistered(): Boolean {
return !oepsNummer.isNullOrBlank()
return false // OEPS registration information currently commented out
}
/**
* Checks if the horse is registered with FEI.
*/
fun isFeiRegistered(): Boolean {
return !feiNummer.isNullOrBlank()
return !feiPass.isNullOrBlank()
}
/**
* Returns the age of the horse in years, or null if birth date is unknown.
* Returns the age of the horse in years, or null if birth year is unknown.
*/
fun getAge(): Int? {
return geburtsdatum?.let { birthDate ->
return geburtsjahr?.let { birthYear ->
val today = Clock.System.todayIn(kotlinx.datetime.TimeZone.currentSystemDefault())
var age = today.year - birthDate.year
// Check if a birthday has occurred this year
if (today.month.number < birthDate.month.number ||
(today.month.number == birthDate.month.number && today.day < birthDate.day)
) {
age--
}
age
today.year - birthYear
}
}
@@ -156,11 +135,7 @@ data class DomPferd(
}
if (!hasCompleteIdentification()) {
errors.add("At least one identification number (life number, chip number, or passport number) is required")
}
if (besitzerId == null) {
errors.add("Owner is required")
errors.add("At least one identification number (life number, or kopfnummer, or satznummer) is required")
}
return errors
@@ -9,6 +9,7 @@ import at.mocode.core.domain.serialization.InstantSerializer
import at.mocode.core.domain.serialization.LocalDateSerializer
import at.mocode.core.domain.serialization.UuidSerializer
import kotlinx.datetime.LocalDate
import kotlinx.datetime.todayIn
import kotlinx.serialization.Serializable
import kotlin.time.Clock
import kotlin.time.Instant
@@ -21,27 +22,30 @@ import kotlin.uuid.Uuid
* attributes such as license, start card, and competition eligibility.
* Data is primarily sourced from the OEPS ZNS (LIZENZ01.DAT).
*
* Key rules (ÖTO):
* - A rider requires an active Startkarte (annual fee paid) to compete nationally.
* - LizenzKlasse determines which competition classes the rider may enter.
* - Satznummer (6-digit) is the primary key for ZNS data exchange.
* - Kopfnummer is NOT a unique identifier it can change.
*
* @property reiterId Unique internal identifier (UUID).
* @property personId Reference to the base DomPerson record (UUID).
* @property satznummer 6-digit ZNS primary key for data exchange. Primary key for ZNS.
* @property lizenzNummer OEPS license number (from ZNS LIZENZ01.DAT).
* @property lizenzKlasse License class determining competition eligibility (e.g. R1, RD2).
* @property lizenzSparten Disciplines for which the license is valid.
* @property startkartAktiv Whether the annual start card fee has been paid.
* @property startkartSaison Season year for which the start card is valid (e.g. 2026).
* @property feiId FEI international rider ID (optional).
* @property nation Nation code (e.g. AUT).
* @property geburtsdatum Date of birth (for age class validation).
* @property vereinsNummer Club number (OEPS).
* @property vereinsName Club name.
* @property istGastreiter Whether the rider is a guest rider (foreign nationality, not in Austrian club).
* @property satznummer 6-digit ZNS primary key for data exchange. From LIZENZ01.DAT.
* @property nachname Surname of the rider. From LIZENZ01.DAT.
* @property vorname First name of the rider. From LIZENZ01.DAT.
* @property bundeslandNummer State number (Bundesland). From LIZENZ01.DAT.
* @property vereinsName Name of the club. From LIZENZ01.DAT.
* @property nation Nationality of the rider. From LIZENZ01.DAT.
* @property reiterLizenz Rider license information. From LIZENZ01.DAT.
* @property startkarte Start card information. From LIZENZ01.DAT.
* @property fahrLizenz Driving license information. From LIZENZ01.DAT.
* @property altersklasseJgJrU25 Age class Jg/Jr/U25. From LIZENZ01.DAT.
* @property altersklasseY Age class Young Rider. From LIZENZ01.DAT.
* @property mitgliedsNummer Membership number. From LIZENZ01.DAT.
* @property telefonNummer Phone number. From LIZENZ01.DAT.
* @property kader Squad status. From LIZENZ01.DAT.
* @property lastPayYear Last year the license was paid. From LIZENZ01.DAT.
* @property geschlecht Gender of the rider. From LIZENZ01.DAT.
* @property geburtsdatum Date of birth. From LIZENZ01.DAT (JJJJMMTT).
* @property feiId FEI ID. From LIZENZ01.DAT.
* @property sperrListe Suspension list information. From LIZENZ01.DAT.
* @property lizenzInfo License info details. From LIZENZ01.DAT.
* @property istAktiv Whether the rider is currently active in the system.
* @property bemerkungen Additional notes or comments.
* @property datenQuelle Source of the data.
* @property createdAt Timestamp when this record was created.
* @property updatedAt Timestamp when this record was last updated.
@@ -56,37 +60,33 @@ data class DomReiter(
val personId: Uuid,
// ZNS Identification
val satznummer: String,
val lizenzNummer: String? = null,
// License & Eligibility
val lizenzKlasse: LizenzKlasseE = LizenzKlasseE.LIZENZFREI,
val lizenzSparten: List<SparteE> = emptyList(),
// Start Card (Startkarte) annual fee proof
val startkartAktiv: Boolean = false,
val startkartSaison: Int? = null,
// International
val feiId: String? = null,
val nation: String? = null,
// Personal Data (denormalized from DomPerson for performance)
val nachname: String,
val vorname: String,
var satznummer: String?,
var nachname: String,
var vorname: String,
var bundeslandNummer: Int? = null,
var vereinsName: String? = null,
var nation: String? = null,
var reiterLizenz: String? = null,
var startkarte: String? = null,
var fahrLizenz: String? = null,
var altersklasseJgJrU25: String? = null,
var altersklasseY: String? = null,
var mitgliedsNummer: Int? = null,
var telefonNummer: String? = null,
var kader: String? = null,
var lastPayYear: Int? = null,
var geschlecht: String? = null,
@Serializable(with = LocalDateSerializer::class)
val geburtsdatum: LocalDate? = null,
var geburtsdatum: LocalDate? = null,
var feiId: String? = null,
var sperrListe: String? = null,
var lizenzInfo: String? = null,
// Club Affiliation
val vereinsNummer: String? = null,
val vereinsName: String? = null,
var lizenzKlasse: LizenzKlasseE = LizenzKlasseE.LIZENZFREI,
// Status
val istGastreiter: Boolean = false,
val istAktiv: Boolean = true,
var bemerkungen: String? = null,
val datenQuelle: DatenQuelleE = DatenQuelleE.IMPORT_ZNS,
// Audit Fields
@Serializable(with = InstantSerializer::class)
val createdAt: Instant = Clock.System.now(),
@Serializable(with = InstantSerializer::class)
@@ -99,31 +99,45 @@ data class DomReiter(
/**
* Checks if the rider is eligible to compete nationally.
* Requires an active start card (Startkarte).
* Based on the last pay year.
*/
fun isStartberechtigt(): Boolean = istAktiv && startkartAktiv
fun isStartberechtigt(): Boolean {
val currentYear = Clock.System.todayIn(kotlinx.datetime.TimeZone.currentSystemDefault()).year
return istAktiv && (lastPayYear ?: 0) >= currentYear
}
/**
* Checks if the rider holds a license for the given discipline.
* Checks if the rider holds a license.
*/
fun hasLizenzForSparte(sparte: SparteE): Boolean =
lizenzKlasse == LizenzKlasseE.LIZENZFREI || lizenzSparten.contains(sparte)
fun hasLizenz(): Boolean = !reiterLizenz.isNullOrBlank()
/**
* Checks if the rider has a license for a specific sparte.
*/
fun hasLizenzForSparte(sparte: SparteE): Boolean {
// If we have a license class, check if it's applicable for the sparte
if (lizenzKlasse == LizenzKlasseE.LIZENZFREI) return false
return when (sparte) {
SparteE.DRESSUR -> true // Everyone with a license can do dressage (simplified)
SparteE.SPRINGEN -> !listOf(LizenzKlasseE.RD1, LizenzKlasseE.RD2, LizenzKlasseE.RD3).contains(lizenzKlasse)
else -> true
}
}
/**
* Validates the rider for competition entry.
* Returns a list of warning messages (never hard errors TBA has final say).
*/
fun validateForNennung(sparte: SparteE): List<String> {
fun validateForNennung(): List<String> {
val warnings = mutableListOf<String>()
if (!istAktiv) {
warnings.add("Reiter ${getDisplayName()} ist nicht aktiv")
}
if (!startkartAktiv) {
warnings.add("Reiter ${getDisplayName()} hat keine aktive Startkarte für Saison $startkartSaison")
}
if (!hasLizenzForSparte(sparte)) {
warnings.add("Reiter ${getDisplayName()} hat keine Lizenz für Sparte $sparte (Lizenzklasse: $lizenzKlasse)")
if (!isStartberechtigt()) {
warnings.add("Reiter ${getDisplayName()} hat keine aktive Startkarte für das aktuelle Jahr")
}
return warnings
@@ -22,42 +22,9 @@ interface FunktionaerRepository {
suspend fun findById(id: Uuid): DomFunktionaer?
/**
* Sucht einen Funktionär anhand seiner Richternummer.
* Sucht einen Funktionär anhand seiner Satz-ID und Satznummer.
*/
suspend fun findByRichterNummer(richterNummer: String): DomFunktionaer?
/**
* Sucht Funktionäre anhand von Vor- und/oder Nachname (Teilübereinstimmung).
*/
suspend fun findByName(searchTerm: String, limit: Int = 50): List<DomFunktionaer>
/**
* Sucht alle Funktionäre mit einer bestimmten Rolle.
*/
suspend fun findByRolle(rolle: FunktionaerRolleE, activeOnly: Boolean = true): List<DomFunktionaer>
/**
* Sucht alle Richter mit einer bestimmten Qualifikation.
*/
suspend fun findByRichterQualifikation(
qualifikation: RichterQualifikationE,
activeOnly: Boolean = true
): List<DomFunktionaer>
/**
* Sucht alle Funktionäre, die für eine bestimmte Sparte qualifiziert sind.
*/
suspend fun findBySparte(sparte: SparteE, activeOnly: Boolean = true): List<DomFunktionaer>
/**
* Sucht alle Funktionäre eines bestimmten Vereins.
*/
suspend fun findByVereinsNummer(vereinsNummer: String, activeOnly: Boolean = true): List<DomFunktionaer>
/**
* Gibt alle aktiven Funktionäre zurück (paginiert).
*/
suspend fun findAllActive(limit: Int = 100, offset: Int = 0): List<DomFunktionaer>
suspend fun findBySatz(satzID: String, satzNummer: Int): DomFunktionaer?
/**
* Gibt alle Funktionäre zurück (paginiert).
@@ -82,12 +49,7 @@ interface FunktionaerRepository {
suspend fun countActive(): Long
/**
* Zählt alle Richter (Rolle = RICHTER) mit einer bestimmten Qualifikation.
* Prüft ob ein Funktionär mit der gegebenen Satz-ID und Satznummer bereits existiert.
*/
suspend fun countByRichterQualifikation(qualifikation: RichterQualifikationE, activeOnly: Boolean = true): Long
/**
* Prüft ob ein Funktionär mit der gegebenen Richternummer bereits existiert.
*/
suspend fun existsByRichterNummer(richterNummer: String): Boolean
suspend fun existsBySatz(satzID: String, satzNummer: Int): Boolean
}
@@ -246,6 +246,28 @@ interface HorseRepository {
*/
suspend fun countFeiRegistered(activeOnly: Boolean = true): Long
/**
* Finds horses by their head number (Kopfnummer).
*
* @param kopfnummer The head number to search for
* @return The list of horses found
*/
suspend fun findByKopfnummer(kopfnummer: String): List<DomPferd>
/**
* Finds a horse by its ZNS satznummer.
*
* @param satznummer The ZNS satznummer to search for
* @return The horse if found, null otherwise
*/
suspend fun findBySatznummer(satznummer: String): DomPferd?
/**
* Speichert ein Pferd basierend auf der ZNS satznummer (Upsert).
* Wenn ein Pferd mit der satznummer existiert, wird es aktualisiert, ansonsten neu angelegt.
*/
suspend fun upsertBySatznummer(horse: DomPferd): DomPferd
/**
* Speichert ein Pferd basierend auf der Lebensnummer (Upsert).
* Wenn ein Pferd mit der Lebensnummer existiert, wird es aktualisiert, ansonsten neu angelegt.
@@ -2,8 +2,6 @@
package at.mocode.masterdata.domain.repository
import at.mocode.core.domain.model.LizenzKlasseE
import at.mocode.core.domain.model.SparteE
import at.mocode.masterdata.domain.model.DomReiter
import kotlin.uuid.Uuid
@@ -23,42 +21,7 @@ interface ReiterRepository {
/**
* Sucht einen Reiter anhand seiner Satznummer (OEPS-Mitgliedsnummer).
*/
suspend fun findBySatznummer(satznummer: String): DomReiter?
/**
* Sucht einen Reiter anhand seiner FEI-ID.
*/
suspend fun findByFeiId(feiId: String): DomReiter?
/**
* Sucht Reiter anhand von Vor- und/oder Nachname (Teilübereinstimmung).
*/
suspend fun findByName(searchTerm: String, limit: Int = 50): List<DomReiter>
/**
* Sucht alle Reiter eines bestimmten Vereins.
*/
suspend fun findByVereinsNummer(vereinsNummer: String, activeOnly: Boolean = true): List<DomReiter>
/**
* Sucht alle Reiter mit einer bestimmten Lizenzklasse.
*/
suspend fun findByLizenzKlasse(lizenzKlasse: LizenzKlasseE, activeOnly: Boolean = true): List<DomReiter>
/**
* Sucht alle Reiter, die für eine bestimmte Sparte lizenziert sind.
*/
suspend fun findBySparte(sparte: SparteE, activeOnly: Boolean = true): List<DomReiter>
/**
* Sucht alle Gastreiter.
*/
suspend fun findGastreiter(activeOnly: Boolean = true): List<DomReiter>
/**
* Gibt alle aktiven Reiter zurück (paginiert).
*/
suspend fun findAllActive(limit: Int = 100, offset: Int = 0): List<DomReiter>
suspend fun findBySatznummer(satznummer: String?): DomReiter?
/**
* Gibt alle Reiter zurück (paginiert).
@@ -95,9 +95,7 @@ class LicenseMatrixServiceTest {
satznummer = "1",
nachname = "R1",
vorname = "Reiter",
lizenzKlasse = LizenzKlasseE.R1,
lizenzSparten = listOf(SparteE.SPRINGEN),
startkartAktiv = true
lizenzKlasse = LizenzKlasseE.R1
)
val klasseA = turnierklassen.find { it.code == "A" }!!
@@ -116,9 +114,7 @@ class LicenseMatrixServiceTest {
satznummer = "2",
nachname = "RD1",
vorname = "Reiter",
lizenzKlasse = LizenzKlasseE.RD1,
lizenzSparten = listOf(SparteE.DRESSUR), // Nur Dressur
startkartAktiv = true
lizenzKlasse = LizenzKlasseE.RD1
)
val klasseA = turnierklassen.find { it.code == "A" }!!
@@ -10,9 +10,7 @@ import at.mocode.core.utils.database.DatabaseFactory
import at.mocode.masterdata.domain.model.DomFunktionaer
import at.mocode.masterdata.domain.repository.FunktionaerRepository
import org.jetbrains.exposed.v1.core.ResultRow
import org.jetbrains.exposed.v1.core.eq
import org.jetbrains.exposed.v1.core.like
import org.jetbrains.exposed.v1.core.or
import org.jetbrains.exposed.v1.core.*
import org.jetbrains.exposed.v1.jdbc.*
import kotlin.uuid.Uuid
@@ -21,16 +19,13 @@ import kotlin.uuid.Uuid
*/
class ExposedFunktionaerRepository : FunktionaerRepository {
private fun rowToDomFunktionaer(row: ResultRow): DomFunktionaer {
private fun rowToDomFunktionaer(row: ResultRow, qualifikationen: List<String> = emptyList()): DomFunktionaer {
return DomFunktionaer(
funktionaerId = row[FunktionaerTable.id],
richterNummer = row[FunktionaerTable.richterNummer],
vorname = row[FunktionaerTable.vorname],
nachname = row[FunktionaerTable.nachname],
geburtsdatum = row[FunktionaerTable.geburtsdatum],
email = row[FunktionaerTable.email],
telefon = row[FunktionaerTable.telefon],
vereinsNummer = row[FunktionaerTable.vereinsNummer],
satzID = row[FunktionaerTable.satzID] ?: "X",
satzNummer = row[FunktionaerTable.satzNummer] ?: 0,
name = row[FunktionaerTable.name],
qualifikationen = qualifikationen,
istAktiv = row[FunktionaerTable.istAktiv],
bemerkungen = row[FunktionaerTable.bemerkungen],
datenQuelle = DatenQuelleE.valueOf(row[FunktionaerTable.datenQuelle]),
@@ -40,101 +35,78 @@ class ExposedFunktionaerRepository : FunktionaerRepository {
}
override suspend fun findById(id: Uuid): DomFunktionaer? = DatabaseFactory.dbQuery {
val qualifikationen = FunktionaerQualifikationTable
.selectAll().where { FunktionaerQualifikationTable.funktionaerId eq id }
.map { it[FunktionaerQualifikationTable.qualifikation] }
FunktionaerTable.selectAll().where { FunktionaerTable.id eq id }
.map(::rowToDomFunktionaer)
.map { rowToDomFunktionaer(it, qualifikationen) }
.singleOrNull()
}
override suspend fun findByRichterNummer(richterNummer: String): DomFunktionaer? = DatabaseFactory.dbQuery {
FunktionaerTable.selectAll().where { FunktionaerTable.richterNummer eq richterNummer }
.map(::rowToDomFunktionaer)
.singleOrNull()
}
override suspend fun findBySatz(satzID: String, satzNummer: Int): DomFunktionaer? = DatabaseFactory.dbQuery {
val row = FunktionaerTable.selectAll()
.where { (FunktionaerTable.satzID eq satzID) and (FunktionaerTable.satzNummer eq satzNummer) }
.singleOrNull() ?: return@dbQuery null
override suspend fun findByName(searchTerm: String, limit: Int): List<DomFunktionaer> = DatabaseFactory.dbQuery {
val pattern = "%$searchTerm%"
FunktionaerTable.selectAll()
.where { (FunktionaerTable.nachname like pattern) or (FunktionaerTable.vorname like pattern) }
.limit(limit)
.map(::rowToDomFunktionaer)
}
val qualifikationen = FunktionaerQualifikationTable
.selectAll().where { FunktionaerQualifikationTable.funktionaerId eq row[FunktionaerTable.id] }
.map { it[FunktionaerQualifikationTable.qualifikation] }
override suspend fun findByRolle(rolle: FunktionaerRolleE, activeOnly: Boolean): List<DomFunktionaer> =
DatabaseFactory.dbQuery {
// Rolle wird aktuell nicht in FunktionaerTable gespeichert.
// Falls benötigt, muss die Tabelle erweitert werden.
emptyList()
}
override suspend fun findByRichterQualifikation(
qualifikation: RichterQualifikationE,
activeOnly: Boolean
): List<DomFunktionaer> = DatabaseFactory.dbQuery {
// Qualifikationen werden aktuell nicht in FunktionaerTable gespeichert.
emptyList()
}
override suspend fun findBySparte(sparte: SparteE, activeOnly: Boolean): List<DomFunktionaer> =
DatabaseFactory.dbQuery {
emptyList()
}
override suspend fun findByVereinsNummer(vereinsNummer: String, activeOnly: Boolean): List<DomFunktionaer> =
DatabaseFactory.dbQuery {
val query = FunktionaerTable.selectAll().where { FunktionaerTable.vereinsNummer eq vereinsNummer }
if (activeOnly) {
query.andWhere { FunktionaerTable.istAktiv eq true }
}
query.map(::rowToDomFunktionaer)
}
override suspend fun findAllActive(limit: Int, offset: Int): List<DomFunktionaer> = DatabaseFactory.dbQuery {
FunktionaerTable.selectAll().where { FunktionaerTable.istAktiv eq true }
.limit(limit).offset(offset.toLong())
.map(::rowToDomFunktionaer)
rowToDomFunktionaer(row, qualifikationen)
}
override suspend fun findAll(limit: Int, offset: Int): List<DomFunktionaer> = DatabaseFactory.dbQuery {
FunktionaerTable.selectAll()
val funktionaere = FunktionaerTable.selectAll()
.limit(limit).offset(offset.toLong())
.map(::rowToDomFunktionaer)
.toList()
val ids = funktionaere.map { it[FunktionaerTable.id] }
val qualisMap = FunktionaerQualifikationTable
.selectAll().where { FunktionaerQualifikationTable.funktionaerId inList ids }
.groupBy({ it[FunktionaerQualifikationTable.funktionaerId] }) { it[FunktionaerQualifikationTable.qualifikation] }
funktionaere.map { row ->
rowToDomFunktionaer(row, qualisMap[row[FunktionaerTable.id]] ?: emptyList())
}
}
override suspend fun save(funktionaer: DomFunktionaer): DomFunktionaer = DatabaseFactory.dbQuery {
val exists = FunktionaerTable.selectAll().where { FunktionaerTable.id eq funktionaer.funktionaerId }.any()
if (exists) {
FunktionaerTable.update({ FunktionaerTable.id eq funktionaer.funktionaerId }) {
it[richterNummer] = funktionaer.richterNummer
it[vorname] = funktionaer.vorname
it[nachname] = funktionaer.nachname
it[geburtsdatum] = funktionaer.geburtsdatum
it[email] = funktionaer.email
it[telefon] = funktionaer.telefon
it[vereinsNummer] = funktionaer.vereinsNummer
it[satzID] = funktionaer.satzID
it[satzNummer] = funktionaer.satzNummer
it[name] = funktionaer.name
it[istAktiv] = funktionaer.istAktiv
it[bemerkungen] = funktionaer.bemerkungen
it[datenQuelle] = funktionaer.datenQuelle.name
it[updatedAt] = funktionaer.updatedAt
}
funktionaer
} else {
FunktionaerTable.insert {
it[id] = funktionaer.funktionaerId
it[richterNummer] = funktionaer.richterNummer
it[vorname] = funktionaer.vorname
it[nachname] = funktionaer.nachname
it[geburtsdatum] = funktionaer.geburtsdatum
it[email] = funktionaer.email
it[telefon] = funktionaer.telefon
it[vereinsNummer] = funktionaer.vereinsNummer
it[satzID] = funktionaer.satzID
it[satzNummer] = funktionaer.satzNummer
it[name] = funktionaer.name
it[istAktiv] = funktionaer.istAktiv
it[bemerkungen] = funktionaer.bemerkungen
it[datenQuelle] = funktionaer.datenQuelle.name
it[createdAt] = funktionaer.createdAt
it[updatedAt] = funktionaer.updatedAt
}
funktionaer
}
// Qualifikationen synchronisieren
FunktionaerQualifikationTable.deleteWhere { funktionaerId eq funktionaer.funktionaerId }
funktionaer.qualifikationen.forEach { quali ->
FunktionaerQualifikationTable.insert {
it[funktionaerId] = funktionaer.funktionaerId
it[qualifikation] = quali
}
}
funktionaer
}
override suspend fun delete(id: Uuid): Boolean = DatabaseFactory.dbQuery {
@@ -145,13 +117,9 @@ class ExposedFunktionaerRepository : FunktionaerRepository {
FunktionaerTable.selectAll().where { FunktionaerTable.istAktiv eq true }.count()
}
override suspend fun countByRichterQualifikation(qualifikation: RichterQualifikationE, activeOnly: Boolean): Long =
DatabaseFactory.dbQuery {
// Aktuell keine Qualifikations-Speicherung
0L
}
override suspend fun existsByRichterNummer(richterNummer: String): Boolean = DatabaseFactory.dbQuery {
FunktionaerTable.selectAll().where { FunktionaerTable.richterNummer eq richterNummer }.any()
override suspend fun existsBySatz(satzID: String, satzNummer: Int): Boolean = DatabaseFactory.dbQuery {
FunktionaerTable.selectAll()
.where { (FunktionaerTable.satzID eq satzID) and (FunktionaerTable.satzNummer eq satzNummer) }
.any()
}
}
@@ -4,14 +4,11 @@ package at.mocode.masterdata.infrastructure.persistence
import at.mocode.core.domain.model.DatenQuelleE
import at.mocode.core.domain.model.LizenzKlasseE
import at.mocode.core.domain.model.SparteE
import at.mocode.core.utils.database.DatabaseFactory
import at.mocode.masterdata.domain.model.DomReiter
import at.mocode.masterdata.domain.repository.ReiterRepository
import org.jetbrains.exposed.v1.core.ResultRow
import org.jetbrains.exposed.v1.core.eq
import org.jetbrains.exposed.v1.core.like
import org.jetbrains.exposed.v1.core.or
import org.jetbrains.exposed.v1.jdbc.*
import kotlin.uuid.Uuid
@@ -20,133 +17,55 @@ import kotlin.uuid.Uuid
*/
class ExposedReiterRepository : ReiterRepository {
private fun rowToDomReiter(row: ResultRow, sparten: List<SparteE> = emptyList()): DomReiter {
private fun rowToDomReiter(row: ResultRow): DomReiter {
return DomReiter(
reiterId = row[ReiterTable.id],
personId = row[ReiterTable.personId],
satznummer = row[ReiterTable.satznummer],
nachname = row[ReiterTable.nachname],
vorname = row[ReiterTable.vorname],
geburtsdatum = row[ReiterTable.geburtsdatum],
lizenzNummer = row[ReiterTable.lizenzNummer],
lizenzKlasse = LizenzKlasseE.valueOf(row[ReiterTable.lizenzKlasse]),
lizenzSparten = sparten,
startkartAktiv = row[ReiterTable.startkartAktiv],
startkartSaison = row[ReiterTable.startkartSaison],
feiId = row[ReiterTable.feiId],
nation = row[ReiterTable.nation],
vereinsNummer = row[ReiterTable.vereinsNummer],
bundeslandNummer = row[ReiterTable.bundeslandNummer],
vereinsName = row[ReiterTable.vereinsName],
istGastreiter = row[ReiterTable.istGastreiter],
nation = row[ReiterTable.nation],
reiterLizenz = row[ReiterTable.reiterLizenz],
startkarte = row[ReiterTable.startkarte],
fahrLizenz = row[ReiterTable.fahrLizenz],
altersklasseJgJrU25 = row[ReiterTable.altersklasseJgJrU25],
altersklasseY = row[ReiterTable.altersklasseY],
mitgliedsNummer = row[ReiterTable.mitgliedsNummer],
telefonNummer = row[ReiterTable.telefonNummer],
kader = row[ReiterTable.kader],
lastPayYear = row[ReiterTable.lastPayYear],
geschlecht = row[ReiterTable.geschlecht],
geburtsdatum = row[ReiterTable.geburtsdatum],
feiId = row[ReiterTable.feiId],
sperrListe = row[ReiterTable.sperrListe],
lizenzInfo = row[ReiterTable.lizenzInfo],
lizenzKlasse = LizenzKlasseE.valueOf(row[ReiterTable.lizenzKlasse]),
istAktiv = row[ReiterTable.istAktiv],
bemerkungen = row[ReiterTable.bemerkungen],
datenQuelle = DatenQuelleE.valueOf(row[ReiterTable.datenQuelle]),
createdAt = row[ReiterTable.createdAt],
updatedAt = row[ReiterTable.updatedAt]
)
}
private fun getSpartenForReiter(reiterId: Uuid): List<SparteE> {
return ReiterSparteTable.selectAll().where { ReiterSparteTable.reiterId eq reiterId }
.map { SparteE.valueOf(it[ReiterSparteTable.sparte]) }
}
override suspend fun findById(id: Uuid): DomReiter? = DatabaseFactory.dbQuery {
ReiterTable.selectAll().where { ReiterTable.id eq id }
.map { rowToDomReiter(it, getSpartenForReiter(id)) }
.map { rowToDomReiter(it) }
.singleOrNull()
}
override suspend fun findBySatznummer(satznummer: String): DomReiter? = DatabaseFactory.dbQuery {
override suspend fun findBySatznummer(satznummer: String?): DomReiter? = DatabaseFactory.dbQuery {
ReiterTable.selectAll().where { ReiterTable.satznummer eq satznummer }
.map { row ->
val id = row[ReiterTable.id]
rowToDomReiter(row, getSpartenForReiter(id))
}
.map { row -> rowToDomReiter(row) }
.singleOrNull()
}
override suspend fun findByFeiId(feiId: String): DomReiter? = DatabaseFactory.dbQuery {
ReiterTable.selectAll().where { ReiterTable.feiId eq feiId }
.map { row ->
val id = row[ReiterTable.id]
rowToDomReiter(row, getSpartenForReiter(id))
}
.singleOrNull()
}
override suspend fun findByName(searchTerm: String, limit: Int): List<DomReiter> = DatabaseFactory.dbQuery {
val pattern = "%$searchTerm%"
ReiterTable.selectAll().where { (ReiterTable.nachname like pattern) or (ReiterTable.vorname like pattern) }
.limit(limit)
.map { row ->
val id = row[ReiterTable.id]
rowToDomReiter(row, getSpartenForReiter(id))
}
}
override suspend fun findByVereinsNummer(vereinsNummer: String, activeOnly: Boolean): List<DomReiter> =
DatabaseFactory.dbQuery {
val query = ReiterTable.selectAll().where { ReiterTable.vereinsNummer eq vereinsNummer }
if (activeOnly) {
query.andWhere { ReiterTable.istAktiv eq true }
}
query.map { row ->
val id = row[ReiterTable.id]
rowToDomReiter(row, getSpartenForReiter(id))
}
}
override suspend fun findByLizenzKlasse(lizenzKlasse: LizenzKlasseE, activeOnly: Boolean): List<DomReiter> =
DatabaseFactory.dbQuery {
val query = ReiterTable.selectAll().where { ReiterTable.lizenzKlasse eq lizenzKlasse.name }
if (activeOnly) {
query.andWhere { ReiterTable.istAktiv eq true }
}
query.map { row ->
val id = row[ReiterTable.id]
rowToDomReiter(row, getSpartenForReiter(id))
}
}
override suspend fun findBySparte(sparte: SparteE, activeOnly: Boolean): List<DomReiter> = DatabaseFactory.dbQuery {
val query = (ReiterTable innerJoin ReiterSparteTable)
.selectAll().where { ReiterSparteTable.sparte eq sparte.name }
if (activeOnly) {
query.andWhere { ReiterTable.istAktiv eq true }
}
query.map { row ->
val id = row[ReiterTable.id]
rowToDomReiter(row, getSpartenForReiter(id))
}
}
override suspend fun findGastreiter(activeOnly: Boolean): List<DomReiter> = DatabaseFactory.dbQuery {
val query = ReiterTable.selectAll().where { ReiterTable.istGastreiter eq true }
if (activeOnly) {
query.andWhere { ReiterTable.istAktiv eq true }
}
query.map { row ->
val id = row[ReiterTable.id]
rowToDomReiter(row, getSpartenForReiter(id))
}
}
override suspend fun findAllActive(limit: Int, offset: Int): List<DomReiter> = DatabaseFactory.dbQuery {
ReiterTable.selectAll().where { ReiterTable.istAktiv eq true }
.limit(limit).offset(offset.toLong())
.map { row ->
val id = row[ReiterTable.id]
rowToDomReiter(row, getSpartenForReiter(id))
}
}
override suspend fun findAll(limit: Int, offset: Int): List<DomReiter> = DatabaseFactory.dbQuery {
ReiterTable.selectAll()
.limit(limit).offset(offset.toLong())
.map { row ->
val id = row[ReiterTable.id]
rowToDomReiter(row, getSpartenForReiter(id))
}
.map { row -> rowToDomReiter(row) }
}
override suspend fun save(reiter: DomReiter): DomReiter = DatabaseFactory.dbQuery {
@@ -157,17 +76,26 @@ class ExposedReiterRepository : ReiterRepository {
it[satznummer] = reiter.satznummer
it[nachname] = reiter.nachname
it[vorname] = reiter.vorname
it[geburtsdatum] = reiter.geburtsdatum
it[lizenzNummer] = reiter.lizenzNummer
it[lizenzKlasse] = reiter.lizenzKlasse.name
it[startkartAktiv] = reiter.startkartAktiv
it[startkartSaison] = reiter.startkartSaison
it[feiId] = reiter.feiId
it[nation] = reiter.nation
it[vereinsNummer] = reiter.vereinsNummer
it[bundeslandNummer] = reiter.bundeslandNummer
it[vereinsName] = reiter.vereinsName
it[istGastreiter] = reiter.istGastreiter
it[nation] = reiter.nation
it[reiterLizenz] = reiter.reiterLizenz
it[startkarte] = reiter.startkarte
it[fahrLizenz] = reiter.fahrLizenz
it[altersklasseJgJrU25] = reiter.altersklasseJgJrU25
it[altersklasseY] = reiter.altersklasseY
it[mitgliedsNummer] = reiter.mitgliedsNummer
it[telefonNummer] = reiter.telefonNummer
it[kader] = reiter.kader
it[lastPayYear] = reiter.lastPayYear
it[geschlecht] = reiter.geschlecht
it[geburtsdatum] = reiter.geburtsdatum
it[feiId] = reiter.feiId
it[sperrListe] = reiter.sperrListe
it[lizenzInfo] = reiter.lizenzInfo
it[lizenzKlasse] = reiter.lizenzKlasse.name
it[istAktiv] = reiter.istAktiv
it[bemerkungen] = reiter.bemerkungen
it[datenQuelle] = reiter.datenQuelle.name
it[updatedAt] = reiter.updatedAt
}
@@ -178,33 +106,32 @@ class ExposedReiterRepository : ReiterRepository {
it[satznummer] = reiter.satznummer
it[nachname] = reiter.nachname
it[vorname] = reiter.vorname
it[geburtsdatum] = reiter.geburtsdatum
it[lizenzNummer] = reiter.lizenzNummer
it[lizenzKlasse] = reiter.lizenzKlasse.name
it[startkartAktiv] = reiter.startkartAktiv
it[startkartSaison] = reiter.startkartSaison
it[feiId] = reiter.feiId
it[nation] = reiter.nation
it[vereinsNummer] = reiter.vereinsNummer
it[bundeslandNummer] = reiter.bundeslandNummer
it[vereinsName] = reiter.vereinsName
it[istGastreiter] = reiter.istGastreiter
it[nation] = reiter.nation
it[reiterLizenz] = reiter.reiterLizenz
it[startkarte] = reiter.startkarte
it[fahrLizenz] = reiter.fahrLizenz
it[altersklasseJgJrU25] = reiter.altersklasseJgJrU25
it[altersklasseY] = reiter.altersklasseY
it[mitgliedsNummer] = reiter.mitgliedsNummer
it[telefonNummer] = reiter.telefonNummer
it[kader] = reiter.kader
it[lastPayYear] = reiter.lastPayYear
it[geschlecht] = reiter.geschlecht
it[geburtsdatum] = reiter.geburtsdatum
it[feiId] = reiter.feiId
it[sperrListe] = reiter.sperrListe
it[lizenzInfo] = reiter.lizenzInfo
it[lizenzKlasse] = reiter.lizenzKlasse.name
it[istAktiv] = reiter.istAktiv
it[bemerkungen] = reiter.bemerkungen
it[datenQuelle] = reiter.datenQuelle.name
it[createdAt] = reiter.createdAt
it[updatedAt] = reiter.updatedAt
}
}
// Sparten aktualisieren
ReiterSparteTable.deleteWhere { ReiterSparteTable.reiterId eq reiter.reiterId }
reiter.lizenzSparten.forEach { sparte ->
ReiterSparteTable.insert {
it[ReiterSparteTable.id] = Uuid.random()
it[ReiterSparteTable.reiterId] = reiter.reiterId
it[ReiterSparteTable.sparte] = sparte.name
}
}
reiter
}
@@ -221,12 +148,7 @@ class ExposedReiterRepository : ReiterRepository {
}
override suspend fun upsertBySatznummer(reiter: DomReiter): DomReiter = DatabaseFactory.dbQuery {
val existing = ReiterTable.selectAll().where { ReiterTable.satznummer eq reiter.satznummer }
.map { row ->
val id = row[ReiterTable.id]
rowToDomReiter(row, getSpartenForReiter(id))
}
.singleOrNull()
val existing = findBySatznummer(reiter.satznummer)
if (existing != null) {
val toUpdate = reiter.copy(reiterId = existing.reiterId)
@@ -13,13 +13,9 @@ import org.jetbrains.exposed.v1.datetime.timestamp
*/
object FunktionaerTable : Table("funktionaer") {
val id = uuid("funktionaer_id")
val richterNummer = varchar("richter_nummer", 10).nullable().uniqueIndex()
val vorname = varchar("vorname", 100)
val nachname = varchar("nachname", 100)
val geburtsdatum = date("geburtsdatum").nullable()
val email = varchar("email", 200).nullable()
val telefon = varchar("telefon", 50).nullable()
val vereinsNummer = varchar("vereins_nummer", 10).nullable()
val satzID = varchar("satz_id", 1).nullable()
val satzNummer = integer("satz_nummer").nullable()
val name = varchar("name", 200).nullable()
val istAktiv = bool("ist_aktiv").default(true)
val bemerkungen = text("bemerkungen").nullable()
val datenQuelle = varchar("daten_quelle", 50)
@@ -27,4 +23,18 @@ object FunktionaerTable : Table("funktionaer") {
val updatedAt = timestamp("updated_at").defaultExpression(CurrentTimestamp)
override val primaryKey = PrimaryKey(id)
init {
index("idx_funktionaer_satz", isUnique = true, satzID, satzNummer)
}
}
/**
* Exposed-Tabellendefinition für die Qualifikationen eines Funktionärs.
*/
object FunktionaerQualifikationTable : Table("funktionaer_qualifikation") {
val funktionaerId = uuid("funktionaer_id").references(FunktionaerTable.id)
val qualifikation = varchar("qualifikation", 20)
override val primaryKey = PrimaryKey(funktionaerId, qualifikation)
}
@@ -7,36 +7,33 @@ import at.mocode.core.domain.model.PferdeGeschlechtE
import at.mocode.core.utils.database.DatabaseFactory
import at.mocode.masterdata.domain.model.DomPferd
import at.mocode.masterdata.domain.repository.HorseRepository
import org.jetbrains.exposed.v1.core.*
import org.jetbrains.exposed.v1.jdbc.*
import org.jetbrains.exposed.v1.core.ResultRow
import org.jetbrains.exposed.v1.core.eq
import org.jetbrains.exposed.v1.core.like
import org.jetbrains.exposed.v1.jdbc.deleteWhere
import org.jetbrains.exposed.v1.jdbc.insert
import org.jetbrains.exposed.v1.jdbc.selectAll
import org.jetbrains.exposed.v1.jdbc.update
import kotlin.uuid.Uuid
/**
* Exposed-basierte Implementierung des Horse-Repositorys.
*/
class HorseRepositoryImpl : HorseRepository {
private fun rowToDomPferd(row: ResultRow): DomPferd {
return DomPferd(
pferdId = row[HorseTable.id],
kopfnummer = row[HorseTable.kopfnummer],
pferdeName = row[HorseTable.pferdeName],
geschlecht = PferdeGeschlechtE.valueOf(row[HorseTable.geschlecht]),
geburtsdatum = row[HorseTable.geburtsdatum],
rasse = row[HorseTable.rasse],
farbe = row[HorseTable.farbe],
besitzerId = row[HorseTable.besitzerId],
verantwortlichePersonId = row[HorseTable.verantwortlichePersonId],
zuechterName = row[HorseTable.zuechterName],
zuchtbuchNummer = row[HorseTable.zuchtbuchNummer],
lebensnummer = row[HorseTable.lebensnummer],
chipNummer = row[HorseTable.chipNummer],
passNummer = row[HorseTable.passNummer],
oepsNummer = row[HorseTable.oepsNummer],
feiNummer = row[HorseTable.feiNummer],
vaterName = row[HorseTable.vaterName],
mutterName = row[HorseTable.mutterName],
mutterVaterName = row[HorseTable.mutterVaterName],
stockmass = row[HorseTable.stockmass],
geschlecht = PferdeGeschlechtE.valueOf(row[HorseTable.geschlecht]),
geburtsjahr = row[HorseTable.geburtsjahr],
farbe = row[HorseTable.farbe],
abstammung = row[HorseTable.abstammung],
vereinNummer = row[HorseTable.vereinNummer],
lastPayYear = row[HorseTable.lastPayYear],
verantwortlichePersonId = row[HorseTable.verantwortlichePersonId],
vater = row[HorseTable.vater],
feiPass = row[HorseTable.feiPass],
satznummer = row[HorseTable.satznummer],
istAktiv = row[HorseTable.istAktiv],
bemerkungen = row[HorseTable.bemerkungen],
datenQuelle = DatenQuelleE.valueOf(row[HorseTable.datenQuelle]),
@@ -57,28 +54,15 @@ class HorseRepositoryImpl : HorseRepository {
.singleOrNull()
}
override suspend fun findByChipNummer(chipNummer: String): DomPferd? = DatabaseFactory.dbQuery {
HorseTable.selectAll().where { HorseTable.chipNummer eq chipNummer }
override suspend fun findBySatznummer(satznummer: String): DomPferd? = DatabaseFactory.dbQuery {
HorseTable.selectAll().where { HorseTable.satznummer eq satznummer }
.map(::rowToDomPferd)
.singleOrNull()
}
override suspend fun findByPassNummer(passNummer: String): DomPferd? = DatabaseFactory.dbQuery {
HorseTable.selectAll().where { HorseTable.passNummer eq passNummer }
override suspend fun findByKopfnummer(kopfnummer: String): List<DomPferd> = DatabaseFactory.dbQuery {
HorseTable.selectAll().where { HorseTable.kopfnummer eq kopfnummer }
.map(::rowToDomPferd)
.singleOrNull()
}
override suspend fun findByOepsNummer(oepsNummer: String): DomPferd? = DatabaseFactory.dbQuery {
HorseTable.selectAll().where { HorseTable.oepsNummer eq oepsNummer }
.map(::rowToDomPferd)
.singleOrNull()
}
override suspend fun findByFeiNummer(feiNummer: String): DomPferd? = DatabaseFactory.dbQuery {
HorseTable.selectAll().where { HorseTable.feiNummer eq feiNummer }
.map(::rowToDomPferd)
.singleOrNull()
}
override suspend fun findByName(searchTerm: String, limit: Int): List<DomPferd> = DatabaseFactory.dbQuery {
@@ -88,113 +72,29 @@ class HorseRepositoryImpl : HorseRepository {
.map(::rowToDomPferd)
}
override suspend fun findByOwnerId(ownerId: Uuid, activeOnly: Boolean): List<DomPferd> = DatabaseFactory.dbQuery {
val query = HorseTable.selectAll().where { HorseTable.besitzerId eq ownerId }
if (activeOnly) {
query.andWhere { HorseTable.istAktiv eq true }
}
query.map(::rowToDomPferd)
}
override suspend fun findByResponsiblePersonId(responsiblePersonId: Uuid, activeOnly: Boolean): List<DomPferd> =
DatabaseFactory.dbQuery {
val query = HorseTable.selectAll().where { HorseTable.verantwortlichePersonId eq responsiblePersonId }
if (activeOnly) {
query.andWhere { HorseTable.istAktiv eq true }
}
query.map(::rowToDomPferd)
}
override suspend fun findByGeschlecht(
geschlecht: PferdeGeschlechtE,
activeOnly: Boolean,
limit: Int
): List<DomPferd> = DatabaseFactory.dbQuery {
val query = HorseTable.selectAll().where { HorseTable.geschlecht eq geschlecht.name }
if (activeOnly) {
query.andWhere { HorseTable.istAktiv eq true }
}
query.limit(limit).map(::rowToDomPferd)
}
override suspend fun findByRasse(rasse: String, activeOnly: Boolean, limit: Int): List<DomPferd> =
DatabaseFactory.dbQuery {
val query = HorseTable.selectAll().where { HorseTable.rasse eq rasse }
if (activeOnly) {
query.andWhere { HorseTable.istAktiv eq true }
}
query.limit(limit).map(::rowToDomPferd)
}
override suspend fun findByBirthYear(birthYear: Int, activeOnly: Boolean): List<DomPferd> = DatabaseFactory.dbQuery {
// In Exposed v1 gibt es kein direktes year() für date Spalten ohne extra Extension.
// Wir suchen im Datumsbereich nach.
val startDate = kotlinx.datetime.LocalDate(birthYear, 1, 1)
val endDate = kotlinx.datetime.LocalDate(birthYear, 12, 31)
val query = HorseTable.selectAll()
.where { (HorseTable.geburtsdatum greaterEq startDate) and (HorseTable.geburtsdatum lessEq endDate) }
if (activeOnly) {
query.andWhere { HorseTable.istAktiv eq true }
}
query.map(::rowToDomPferd)
}
override suspend fun findByBirthYearRange(fromYear: Int, toYear: Int, activeOnly: Boolean): List<DomPferd> =
DatabaseFactory.dbQuery {
val startDate = kotlinx.datetime.LocalDate(fromYear, 1, 1)
val endDate = kotlinx.datetime.LocalDate(toYear, 12, 31)
val query = HorseTable.selectAll()
.where { (HorseTable.geburtsdatum greaterEq startDate) and (HorseTable.geburtsdatum lessEq endDate) }
if (activeOnly) {
query.andWhere { HorseTable.istAktiv eq true }
}
query.map(::rowToDomPferd)
}
override suspend fun findAllActive(limit: Int): List<DomPferd> = DatabaseFactory.dbQuery {
HorseTable.selectAll().where { HorseTable.istAktiv eq true }
.limit(limit)
.map(::rowToDomPferd)
}
override suspend fun findOepsRegistered(activeOnly: Boolean): List<DomPferd> = DatabaseFactory.dbQuery {
val query = HorseTable.selectAll().where { HorseTable.oepsNummer.isNotNull() }
if (activeOnly) {
query.andWhere { HorseTable.istAktiv eq true }
}
query.map(::rowToDomPferd)
}
override suspend fun findFeiRegistered(activeOnly: Boolean): List<DomPferd> = DatabaseFactory.dbQuery {
val query = HorseTable.selectAll().where { HorseTable.feiNummer.isNotNull() }
if (activeOnly) {
query.andWhere { HorseTable.istAktiv eq true }
}
query.map(::rowToDomPferd)
}
override suspend fun save(horse: DomPferd): DomPferd = DatabaseFactory.dbQuery {
val exists = HorseTable.selectAll().where { HorseTable.id eq horse.pferdId }.any()
if (exists) {
HorseTable.update({ HorseTable.id eq horse.pferdId }) {
it[kopfnummer] = horse.kopfnummer
it[pferdeName] = horse.pferdeName
it[geschlecht] = horse.geschlecht.name
it[geburtsdatum] = horse.geburtsdatum
it[rasse] = horse.rasse
it[farbe] = horse.farbe
it[besitzerId] = horse.besitzerId
it[verantwortlichePersonId] = horse.verantwortlichePersonId
it[zuechterName] = horse.zuechterName
it[zuchtbuchNummer] = horse.zuchtbuchNummer
it[lebensnummer] = horse.lebensnummer
it[chipNummer] = horse.chipNummer
it[passNummer] = horse.passNummer
it[oepsNummer] = horse.oepsNummer
it[feiNummer] = horse.feiNummer
it[vaterName] = horse.vaterName
it[mutterName] = horse.mutterName
it[mutterVaterName] = horse.mutterVaterName
it[stockmass] = horse.stockmass
it[geschlecht] = horse.geschlecht.name
it[geburtsjahr] = horse.geburtsjahr
it[farbe] = horse.farbe
it[abstammung] = horse.abstammung
it[vereinNummer] = horse.vereinNummer
it[lastPayYear] = horse.lastPayYear
it[verantwortlichePersonId] = horse.verantwortlichePersonId
it[vater] = horse.vater
it[feiPass] = horse.feiPass
it[satznummer] = horse.satznummer
it[istAktiv] = horse.istAktiv
it[bemerkungen] = horse.bemerkungen
it[datenQuelle] = horse.datenQuelle.name
@@ -204,24 +104,19 @@ class HorseRepositoryImpl : HorseRepository {
} else {
HorseTable.insert {
it[id] = horse.pferdId
it[kopfnummer] = horse.kopfnummer
it[pferdeName] = horse.pferdeName
it[geschlecht] = horse.geschlecht.name
it[geburtsdatum] = horse.geburtsdatum
it[rasse] = horse.rasse
it[farbe] = horse.farbe
it[besitzerId] = horse.besitzerId
it[verantwortlichePersonId] = horse.verantwortlichePersonId
it[zuechterName] = horse.zuechterName
it[zuchtbuchNummer] = horse.zuchtbuchNummer
it[lebensnummer] = horse.lebensnummer
it[chipNummer] = horse.chipNummer
it[passNummer] = horse.passNummer
it[oepsNummer] = horse.oepsNummer
it[feiNummer] = horse.feiNummer
it[vaterName] = horse.vaterName
it[mutterName] = horse.mutterName
it[mutterVaterName] = horse.mutterVaterName
it[stockmass] = horse.stockmass
it[geschlecht] = horse.geschlecht.name
it[geburtsjahr] = horse.geburtsjahr
it[farbe] = horse.farbe
it[abstammung] = horse.abstammung
it[vereinNummer] = horse.vereinNummer
it[lastPayYear] = horse.lastPayYear
it[verantwortlichePersonId] = horse.verantwortlichePersonId
it[vater] = horse.vater
it[feiPass] = horse.feiPass
it[satznummer] = horse.satznummer
it[istAktiv] = horse.istAktiv
it[bemerkungen] = horse.bemerkungen
it[datenQuelle] = horse.datenQuelle.name
@@ -240,50 +135,10 @@ class HorseRepositoryImpl : HorseRepository {
HorseTable.selectAll().where { HorseTable.lebensnummer eq lebensnummer }.any()
}
override suspend fun existsByChipNummer(chipNummer: String): Boolean = DatabaseFactory.dbQuery {
HorseTable.selectAll().where { HorseTable.chipNummer eq chipNummer }.any()
}
override suspend fun existsByPassNummer(passNummer: String): Boolean = DatabaseFactory.dbQuery {
HorseTable.selectAll().where { HorseTable.passNummer eq passNummer }.any()
}
override suspend fun existsByOepsNummer(oepsNummer: String): Boolean = DatabaseFactory.dbQuery {
HorseTable.selectAll().where { HorseTable.oepsNummer eq oepsNummer }.any()
}
override suspend fun existsByFeiNummer(feiNummer: String): Boolean = DatabaseFactory.dbQuery {
HorseTable.selectAll().where { HorseTable.feiNummer eq feiNummer }.any()
}
override suspend fun countActive(): Long = DatabaseFactory.dbQuery {
HorseTable.selectAll().where { HorseTable.istAktiv eq true }.count()
}
override suspend fun countByOwnerId(ownerId: Uuid, activeOnly: Boolean): Long = DatabaseFactory.dbQuery {
val query = HorseTable.selectAll().where { HorseTable.besitzerId eq ownerId }
if (activeOnly) {
query.andWhere { HorseTable.istAktiv eq true }
}
query.count()
}
override suspend fun countOepsRegistered(activeOnly: Boolean): Long = DatabaseFactory.dbQuery {
val query = HorseTable.selectAll().where { HorseTable.oepsNummer.isNotNull() }
if (activeOnly) {
query.andWhere { HorseTable.istAktiv eq true }
}
query.count()
}
override suspend fun countFeiRegistered(activeOnly: Boolean): Long = DatabaseFactory.dbQuery {
val query = HorseTable.selectAll().where { HorseTable.feiNummer.isNotNull() }
if (activeOnly) {
query.andWhere { HorseTable.istAktiv eq true }
}
query.count()
}
override suspend fun upsertByLebensnummer(horse: DomPferd): DomPferd = DatabaseFactory.dbQuery {
val lebensnummer = horse.lebensnummer ?: return@dbQuery save(horse)
@@ -294,24 +149,19 @@ class HorseRepositoryImpl : HorseRepository {
if (existing != null) {
val toUpdate = horse.copy(pferdId = existing.pferdId)
HorseTable.update({ HorseTable.id eq existing.pferdId }) {
it[kopfnummer] = toUpdate.kopfnummer
it[pferdeName] = toUpdate.pferdeName
it[geschlecht] = toUpdate.geschlecht.name
it[geburtsdatum] = toUpdate.geburtsdatum
it[rasse] = toUpdate.rasse
it[farbe] = toUpdate.farbe
it[besitzerId] = toUpdate.besitzerId
it[verantwortlichePersonId] = toUpdate.verantwortlichePersonId
it[zuechterName] = toUpdate.zuechterName
it[zuchtbuchNummer] = toUpdate.zuchtbuchNummer
it[HorseTable.lebensnummer] = toUpdate.lebensnummer
it[chipNummer] = toUpdate.chipNummer
it[passNummer] = toUpdate.passNummer
it[oepsNummer] = toUpdate.oepsNummer
it[feiNummer] = toUpdate.feiNummer
it[vaterName] = toUpdate.vaterName
it[mutterName] = toUpdate.mutterName
it[mutterVaterName] = toUpdate.mutterVaterName
it[stockmass] = toUpdate.stockmass
it[geschlecht] = toUpdate.geschlecht.name
it[geburtsjahr] = toUpdate.geburtsjahr
it[farbe] = toUpdate.farbe
it[abstammung] = toUpdate.abstammung
it[vereinNummer] = toUpdate.vereinNummer
it[lastPayYear] = toUpdate.lastPayYear
it[verantwortlichePersonId] = toUpdate.verantwortlichePersonId
it[vater] = toUpdate.vater
it[feiPass] = toUpdate.feiPass
it[satznummer] = toUpdate.satznummer
it[istAktiv] = toUpdate.istAktiv
it[bemerkungen] = toUpdate.bemerkungen
it[datenQuelle] = toUpdate.datenQuelle.name
@@ -322,4 +172,68 @@ class HorseRepositoryImpl : HorseRepository {
save(horse)
}
}
override suspend fun upsertBySatznummer(horse: DomPferd): DomPferd = DatabaseFactory.dbQuery {
val satznummer = horse.satznummer ?: return@dbQuery save(horse)
val existing = HorseTable.selectAll().where { HorseTable.satznummer eq satznummer }
.map(::rowToDomPferd)
.singleOrNull()
if (existing != null) {
val toUpdate = horse.copy(pferdId = existing.pferdId)
HorseTable.update({ HorseTable.id eq existing.pferdId }) {
it[kopfnummer] = toUpdate.kopfnummer
it[pferdeName] = toUpdate.pferdeName
it[lebensnummer] = toUpdate.lebensnummer
it[geschlecht] = toUpdate.geschlecht.name
it[geburtsjahr] = toUpdate.geburtsjahr
it[farbe] = toUpdate.farbe
it[abstammung] = toUpdate.abstammung
it[vereinNummer] = toUpdate.vereinNummer
it[lastPayYear] = toUpdate.lastPayYear
it[verantwortlichePersonId] = toUpdate.verantwortlichePersonId
it[vater] = toUpdate.vater
it[feiPass] = toUpdate.feiPass
it[HorseTable.satznummer] = toUpdate.satznummer
it[istAktiv] = toUpdate.istAktiv
it[bemerkungen] = toUpdate.bemerkungen
it[datenQuelle] = toUpdate.datenQuelle.name
it[updatedAt] = toUpdate.updatedAt
}
toUpdate
} else {
save(horse)
}
}
// Not implemented or needed based on current requirements/DomPferd state
override suspend fun findByChipNummer(chipNummer: String): DomPferd? = null
override suspend fun findByPassNummer(passNummer: String): DomPferd? = null
override suspend fun findByOepsNummer(oepsNummer: String): DomPferd? = null
override suspend fun findByFeiNummer(feiNummer: String): DomPferd? = null
override suspend fun findByOwnerId(ownerId: Uuid, activeOnly: Boolean): List<DomPferd> = emptyList()
override suspend fun findByResponsiblePersonId(responsiblePersonId: Uuid, activeOnly: Boolean): List<DomPferd> =
emptyList()
override suspend fun findByGeschlecht(
geschlecht: PferdeGeschlechtE,
activeOnly: Boolean,
limit: Int
): List<DomPferd> = emptyList()
override suspend fun findByRasse(rasse: String, activeOnly: Boolean, limit: Int): List<DomPferd> = emptyList()
override suspend fun findByBirthYear(birthYear: Int, activeOnly: Boolean): List<DomPferd> = emptyList()
override suspend fun findByBirthYearRange(fromYear: Int, toYear: Int, activeOnly: Boolean): List<DomPferd> =
emptyList()
override suspend fun findOepsRegistered(activeOnly: Boolean): List<DomPferd> = emptyList()
override suspend fun findFeiRegistered(activeOnly: Boolean): List<DomPferd> = emptyList()
override suspend fun existsByChipNummer(chipNummer: String): Boolean = false
override suspend fun existsByPassNummer(passNummer: String): Boolean = false
override suspend fun existsByOepsNummer(oepsNummer: String): Boolean = false
override suspend fun existsByFeiNummer(feiNummer: String): Boolean = false
override suspend fun countByOwnerId(ownerId: Uuid, activeOnly: Boolean): Long = 0
override suspend fun countOepsRegistered(activeOnly: Boolean): Long = 0
override suspend fun countFeiRegistered(activeOnly: Boolean): Long = 0
}
@@ -4,32 +4,27 @@ package at.mocode.masterdata.infrastructure.persistence
import org.jetbrains.exposed.v1.core.Table
import org.jetbrains.exposed.v1.datetime.CurrentTimestamp
import org.jetbrains.exposed.v1.datetime.date
import org.jetbrains.exposed.v1.datetime.timestamp
/**
* Exposed-Tabellendefinition für die Pferd-Entität.
* Exposed-Tabellendefinition für die Pferd-Entität basierend auf PFERDE01.DAT.
*/
object HorseTable : Table("horse") {
val id = uuid("horse_id")
val kopfnummer = varchar("kopfnummer", 4).nullable().index()
val pferdeName = varchar("pferde_name", 200).index()
val geschlecht = varchar("geschlecht", 20)
val geburtsdatum = date("geburtsdatum").nullable()
val rasse = varchar("rasse", 100).nullable()
val farbe = varchar("farbe", 100).nullable()
val besitzerId = uuid("besitzer_id").nullable()
val verantwortlichePersonId = uuid("verantwortliche_person_id").nullable()
val zuechterName = varchar("zuechter_name", 200).nullable()
val zuchtbuchNummer = varchar("zuchtbuch_nummer", 50).nullable()
val lebensnummer = varchar("lebensnummer", 50).nullable().index()
val chipNummer = varchar("chip_nummer", 50).nullable()
val passNummer = varchar("pass_nummer", 50).nullable()
val oepsNummer = varchar("oeps_nummer", 50).nullable()
val feiNummer = varchar("fei_nummer", 50).nullable()
val vaterName = varchar("vater_name", 200).nullable()
val mutterName = varchar("mutter_name", 200).nullable()
val mutterVaterName = varchar("mutter_vater_name", 200).nullable()
val stockmass = integer("stockmass").nullable()
val geschlecht = varchar("geschlecht", 20)
val geburtsjahr = integer("geburtsjahr").nullable()
val farbe = varchar("farbe", 100).nullable()
val abstammung = varchar("abstammung", 100).nullable()
val vereinNummer = integer("verein_nummer").nullable()
val lastPayYear = integer("last_pay_year").nullable()
val verantwortlichePersonId = varchar("verantwortliche_person_id", 100).nullable()
val vater = varchar("vater", 200).nullable()
val feiPass = varchar("fei_pass", 50).nullable()
val satznummer = varchar("satznummer", 10).nullable()
val istAktiv = bool("ist_aktiv").default(true)
val bemerkungen = text("bemerkungen").nullable()
val datenQuelle = varchar("daten_quelle", 50)
@@ -37,4 +32,8 @@ object HorseTable : Table("horse") {
val updatedAt = timestamp("updated_at").defaultExpression(CurrentTimestamp)
override val primaryKey = PrimaryKey(id)
init {
index("idx_horse_satznummer", isUnique = true, satznummer)
}
}
@@ -2,6 +2,7 @@
package at.mocode.masterdata.infrastructure.persistence
import at.mocode.core.domain.model.LizenzKlasseE
import org.jetbrains.exposed.v1.core.Table
import org.jetbrains.exposed.v1.datetime.CurrentTimestamp
import org.jetbrains.exposed.v1.datetime.date
@@ -13,20 +14,30 @@ import org.jetbrains.exposed.v1.datetime.timestamp
object ReiterTable : Table("reiter") {
val id = uuid("reiter_id")
val personId = uuid("person_id")
val satznummer = varchar("satznummer", 10).uniqueIndex()
val lizenzNummer = varchar("lizenz_nummer", 20).nullable()
val lizenzKlasse = varchar("lizenz_klasse", 20)
val startkartAktiv = bool("startkart_aktiv").default(false)
val startkartSaison = integer("startkart_saison").nullable()
val feiId = varchar("fei_id", 20).nullable()
val nation = varchar("nation", 3).nullable()
val satznummer = varchar("satznummer", 10).nullable()
val nachname = varchar("nachname", 100)
val vorname = varchar("vorname", 100)
val geburtsdatum = date("geburtsdatum").nullable()
val vereinsNummer = varchar("vereins_nummer", 10).nullable()
val bundeslandNummer = integer("bundesland_nummer").nullable()
val vereinsName = varchar("vereins_name", 200).nullable()
val istGastreiter = bool("ist_gastreiter").default(false)
val nation = varchar("nation", 10).nullable()
val reiterLizenz = varchar("reiter_lizenz", 20).nullable()
val startkarte = varchar("startkarte", 20).nullable()
val fahrLizenz = varchar("fahr_lizenz", 20).nullable()
val altersklasseJgJrU25 = varchar("altersklasse_jg_jr_u25", 10).nullable()
val altersklasseY = varchar("altersklasse_y", 10).nullable()
val mitgliedsNummer = integer("mitglieds_nummer").nullable()
val telefonNummer = varchar("telefon_nummer", 50).nullable()
val kader = varchar("kader", 50).nullable()
val lastPayYear = integer("last_pay_year").nullable()
val geschlecht = varchar("geschlecht", 10).nullable()
val geburtsdatum = date("geburtsdatum").nullable()
val feiId = varchar("fei_id", 20).nullable()
val sperrListe = varchar("sperr_liste", 50).nullable()
val lizenzInfo = varchar("lizenz_info", 100).nullable()
val lizenzKlasse = varchar("lizenz_klasse", 50).default(LizenzKlasseE.LIZENZFREI.name)
val istAktiv = bool("ist_aktiv").default(true)
val bemerkungen = text("bemerkungen").nullable()
val datenQuelle = varchar("daten_quelle", 50)
val createdAt = timestamp("created_at").defaultExpression(CurrentTimestamp)
val updatedAt = timestamp("updated_at").defaultExpression(CurrentTimestamp)
@@ -98,9 +98,7 @@ class RegulationSeedVerificationTest {
satznummer = "123456",
nachname = "Müller",
vorname = "Hans",
lizenzKlasse = LizenzKlasseE.R1,
lizenzSparten = listOf(SparteE.SPRINGEN),
startkartAktiv = true
lizenzKlasse = LizenzKlasseE.R1
)
val klasseL = TurnierklasseDefinition(
@@ -5,6 +5,7 @@ import at.mocode.masterdata.domain.repository.HorseRepository
import at.mocode.masterdata.domain.repository.FunktionaerRepository
import at.mocode.masterdata.domain.repository.ReiterRepository
import at.mocode.zns.importer.ZnsImportService
import at.mocode.zns.importer.ZnsImportResult
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -27,8 +28,30 @@ class ZnsImportOrchestrator(
val service = ZnsImportService(vereinRepository, reiterRepository, horseRepository, funktionaerRepository)
val dateien = service.extrahiereDateien(zipBytes.inputStream())
jobRegistry.aktualisiereStatus(jobId, ImportJobStatus.LADE_VEREINE, "Lade Vereine...", 20)
val result = service.importiereZip(zipBytes.inputStream())
val vereineResult = service.importiereVereine(dateien["VEREIN01.DAT"] ?: emptyList(), mutableListOf())
jobRegistry.aktualisiereStatus(jobId, ImportJobStatus.LADE_REITER, "Lade Reiter...", 40)
val reiterResult = service.importiereReiter(dateien["LIZENZ01.DAT"] ?: emptyList(), mutableListOf(), mutableListOf())
jobRegistry.aktualisiereStatus(jobId, ImportJobStatus.LADE_PFERDE, "Lade Pferde...", 60)
val pferdeResult = service.importierePferde(dateien["PFERDE01.DAT"] ?: emptyList(), mutableListOf())
jobRegistry.aktualisiereStatus(jobId, ImportJobStatus.LADE_RICHTER, "Lade Funktionäre...", 80)
val richterResult = service.importiereFunktionaere(dateien["RICHT01.DAT"] ?: emptyList(), mutableListOf(), mutableListOf())
val result = ZnsImportResult(
vereineImportiert = vereineResult.first,
vereineAktualisiert = vereineResult.second,
reiterImportiert = reiterResult.first,
reiterAktualisiert = reiterResult.second,
pferdeImportiert = pferdeResult.first,
pferdeAktualisiert = pferdeResult.second,
richterImportiert = richterResult.first,
richterAktualisiert = richterResult.second
)
jobRegistry.aktualisiereStatus(
jobId, ImportJobStatus.ABGESCHLOSSEN,