feat(masterdata): add controllers, services, and repositories for Reiter, Horse, and Verein domains

- Introduced entities `ReiterController`, `HorseController`, and `VereinController`, with associated REST routes.
- Implemented upsert functionality for `Reiter`, `Horse`, and `Verein` repositories.
- Added services for `Altersklasse` calculations and integrated them into the domain layer.
- Updated database schema to include `ReiterTable`, `HorseTable`, `VereinTable`, and `FunktionaerTable`.
- Refactored `masterdataApiModule` to register new domain controllers.
- Adjusted Ktor server and Spring configurations to support new domains.

Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
This commit is contained in:
2026-03-30 13:16:55 +02:00
parent c576bbd6af
commit 0c870ba2e3
15 changed files with 533 additions and 33 deletions
@@ -1,24 +1,24 @@
package at.mocode.masterdata.api
import at.mocode.masterdata.api.plugins.IdempotencyPlugin
import at.mocode.masterdata.api.rest.AltersklasseController
import at.mocode.masterdata.api.rest.BundeslandController
import at.mocode.masterdata.api.rest.CountryController
import at.mocode.masterdata.api.rest.PlatzController
import io.ktor.server.application.Application
import io.ktor.server.routing.routing
import at.mocode.masterdata.api.rest.*
import io.ktor.server.application.*
import io.ktor.server.routing.*
/**
* Ktor-Modul für den Masterdata-Bounded-Context.
*
* - Installiert das IdempotencyPlugin (Header „Idempotency-Key“) global.
* - Registriert alle Masterdata-Routen (Country, Bundesland, Altersklasse, Platz).
* - Registriert alle Masterdata-Routen (Country, Bundesland, Altersklasse, Platz, Reiter, Horse, Verein).
*/
fun Application.masterdataApiModule(
countryController: CountryController,
bundeslandController: BundeslandController,
altersklasseController: AltersklasseController,
platzController: PlatzController
platzController: PlatzController,
reiterController: ReiterController,
horseController: HorseController,
vereinController: VereinController
) {
// Installiere das Idempotency-Plugin global für alle Routen
IdempotencyPlugin.install(this)
@@ -29,5 +29,8 @@ fun Application.masterdataApiModule(
with(bundeslandController) { registerRoutes() }
with(altersklasseController) { registerRoutes() }
with(platzController) { registerRoutes() }
with(reiterController) { registerRoutes() }
with(horseController) { registerRoutes() }
with(vereinController) { registerRoutes() }
}
}
@@ -0,0 +1,95 @@
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
package at.mocode.masterdata.api.rest
import at.mocode.core.domain.serialization.InstantSerializer
import at.mocode.core.domain.serialization.LocalDateSerializer
import at.mocode.masterdata.domain.model.DomPferd
import at.mocode.masterdata.domain.repository.HorseRepository
import io.ktor.http.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import kotlinx.datetime.LocalDate
import kotlinx.serialization.Serializable
import kotlin.time.Instant
import kotlin.uuid.Uuid
/**
* Controller für Pferde-bezogene REST-Endpunkte.
*/
class HorseController(private val horseRepository: HorseRepository) {
@Serializable
data class HorseDto(
val pferdId: String,
val pferdeName: String,
val geschlecht: String,
@Serializable(with = LocalDateSerializer::class)
val geburtsdatum: LocalDate? = null,
val rasse: String? = null,
val lebensnummer: String? = null,
val oepsNummer: String? = null,
val feiNummer: String? = null,
val istAktiv: Boolean,
@Serializable(with = InstantSerializer::class)
val updatedAt: Instant
)
fun Route.registerRoutes() {
route("/horse") {
/**
* Sucht Pferde nach Name.
*/
get("/search") {
val query = call.request.queryParameters["q"] ?: ""
val results = horseRepository.findByName(query)
call.respond(results.map { it.toDto() })
}
/**
* Ruft ein spezifisches Pferd ab.
*/
get("/{id}") {
val idStr = call.parameters["id"] ?: return@get call.respond(HttpStatusCode.BadRequest)
val id = try {
Uuid.parse(idStr)
} catch (e: Exception) {
return@get call.respond(HttpStatusCode.BadRequest)
}
val pferd = horseRepository.findById(id)
if (pferd != null) {
call.respond(pferd.toDto())
} else {
call.respond(HttpStatusCode.NotFound)
}
}
/**
* Sucht ein Pferd nach seiner Lebensnummer.
*/
get("/lebensnummer/{nr}") {
val nr = call.parameters["nr"] ?: return@get call.respond(HttpStatusCode.BadRequest)
val pferd = horseRepository.findByLebensnummer(nr)
if (pferd != null) {
call.respond(pferd.toDto())
} else {
call.respond(HttpStatusCode.NotFound)
}
}
}
}
private fun DomPferd.toDto() = HorseDto(
pferdId = pferdId.toString(),
pferdeName = pferdeName,
geschlecht = geschlecht.name,
geburtsdatum = geburtsdatum,
rasse = rasse,
lebensnummer = lebensnummer,
oepsNummer = oepsNummer,
feiNummer = feiNummer,
istAktiv = istAktiv,
updatedAt = updatedAt
)
}
@@ -0,0 +1,97 @@
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
package at.mocode.masterdata.api.rest
import at.mocode.core.domain.serialization.InstantSerializer
import at.mocode.core.domain.serialization.LocalDateSerializer
import at.mocode.masterdata.domain.model.DomReiter
import at.mocode.masterdata.domain.repository.ReiterRepository
import io.ktor.http.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import kotlinx.datetime.LocalDate
import kotlinx.serialization.Serializable
import kotlin.time.Instant
import kotlin.uuid.Uuid
/**
* Controller für Reiter-bezogene REST-Endpunkte.
*/
class ReiterController(private val reiterRepository: ReiterRepository) {
@Serializable
data class ReiterDto(
val reiterId: 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 vereinsName: String? = null,
@Serializable(with = InstantSerializer::class)
val updatedAt: Instant
)
fun Route.registerRoutes() {
route("/reiter") {
/**
* Sucht Reiter nach Name oder Satznummer.
*/
get("/search") {
val query = call.request.queryParameters["q"] ?: ""
val results = reiterRepository.findByName(query)
call.respond(results.map { it.toDto() })
}
/**
* Ruft einen spezifischen Reiter ab.
*/
get("/{id}") {
val idStr = call.parameters["id"] ?: return@get call.respond(HttpStatusCode.BadRequest)
val id = try {
Uuid.parse(idStr)
} catch (e: Exception) {
return@get call.respond(HttpStatusCode.BadRequest)
}
val reiter = reiterRepository.findById(id)
if (reiter != null) {
call.respond(reiter.toDto())
} else {
call.respond(HttpStatusCode.NotFound)
}
}
/**
* Sucht einen Reiter nach seiner Satznummer.
*/
get("/satznummer/{nr}") {
val nr = call.parameters["nr"] ?: return@get call.respond(HttpStatusCode.BadRequest)
val reiter = reiterRepository.findBySatznummer(nr)
if (reiter != null) {
call.respond(reiter.toDto())
} else {
call.respond(HttpStatusCode.NotFound)
}
}
}
}
private fun DomReiter.toDto() = ReiterDto(
reiterId = reiterId.toString(),
satznummer = satznummer,
nachname = nachname,
vorname = vorname,
geburtsdatum = geburtsdatum,
lizenzNummer = lizenzNummer,
lizenzKlasse = lizenzKlasse.name,
startkartAktiv = startkartAktiv,
nation = nation,
vereinsName = vereinsName,
updatedAt = updatedAt
)
}
@@ -0,0 +1,89 @@
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
package at.mocode.masterdata.api.rest
import at.mocode.core.domain.serialization.InstantSerializer
import at.mocode.masterdata.domain.model.DomVerein
import at.mocode.masterdata.domain.repository.VereinRepository
import io.ktor.http.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import kotlinx.serialization.Serializable
import kotlin.uuid.Uuid
/**
* Controller für Vereins-bezogene REST-Endpunkte.
*/
class VereinController(private val vereinRepository: VereinRepository) {
@Serializable
data class VereinDto(
val vereinId: String,
val vereinsNummer: String,
val name: String,
val kurzname: String? = null,
val bundesland: String,
val ort: String? = null,
val istVeranstalter: Boolean,
val istAktiv: Boolean,
@Serializable(with = InstantSerializer::class)
val updatedAt: kotlin.time.Instant
)
fun Route.registerRoutes() {
route("/verein") {
/**
* Sucht Vereine nach Name oder Kurzname.
*/
get("/search") {
val query = call.request.queryParameters["q"] ?: ""
val results = vereinRepository.findByName(query)
call.respond(results.map { it.toDto() })
}
/**
* Ruft einen spezifischen Verein ab.
*/
get("/{id}") {
val idStr = call.parameters["id"] ?: return@get call.respond(HttpStatusCode.BadRequest)
val id = try {
Uuid.parse(idStr)
} catch (e: Exception) {
return@get call.respond(HttpStatusCode.BadRequest)
}
val verein = vereinRepository.findById(id)
if (verein != null) {
call.respond(verein.toDto())
} else {
call.respond(HttpStatusCode.NotFound)
}
}
/**
* Sucht einen Verein nach seiner Vereinsnummer.
*/
get("/nummer/{nr}") {
val nr = call.parameters["nr"] ?: return@get call.respond(HttpStatusCode.BadRequest)
val verein = vereinRepository.findByVereinsNummer(nr)
if (verein != null) {
call.respond(verein.toDto())
} else {
call.respond(HttpStatusCode.NotFound)
}
}
}
}
private fun DomVerein.toDto() = VereinDto(
vereinId = vereinId.toString(),
vereinsNummer = vereinsNummer,
name = name,
kurzname = kurzname,
bundesland = bundesland ?: "",
ort = ort,
istVeranstalter = istVeranstalter,
istAktiv = istAktiv,
updatedAt = updatedAt
)
}