feat(api): extend Masterdata API with CRUD endpoints for Pferde and Funktionäre
- Added full CRUD support for Pferde (list, search, get by ID, create, update, delete) with filters for Jahrgang and BesitzerId. - Introduced FunktionaerController, offering CRUD operations for Funktionäre (list, search, get by ID, create, update, delete) with filtering by Rolle. - Enhanced HorseController and ReiterController with updated data models, additional request validation, and detailed filtering options. - Extended backend configurations to register new controllers and route handlers. - Updated roadmaps and progress documents to reflect completed Sprint B-1 tasks. Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
This commit is contained in:
+3
-2
@@ -4,7 +4,6 @@ import at.mocode.masterdata.api.plugins.IdempotencyPlugin
|
||||
import at.mocode.masterdata.api.rest.*
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.metrics.micrometer.*
|
||||
import io.ktor.server.plugins.openapi.*
|
||||
import io.ktor.server.plugins.swagger.*
|
||||
import io.ktor.server.routing.*
|
||||
import io.micrometer.core.instrument.MeterRegistry
|
||||
@@ -16,7 +15,7 @@ import io.micrometer.prometheusmetrics.PrometheusMeterRegistry
|
||||
*
|
||||
* - Installiert Micrometer für Metriken (Prometheus).
|
||||
* - Installiert das IdempotencyPlugin (Header „Idempotency-Key“) global.
|
||||
* - Registriert alle Masterdata-Routen (Country, Bundesland, Altersklasse, Platz, Reiter, Horse, Verein).
|
||||
* - Registriert alle Masterdata-Routen (Country, Bundesland, Altersklasse, Platz, Reiter, Horse, Verein, Funktionaer).
|
||||
*/
|
||||
fun Application.masterdataApiModule(
|
||||
countryController: CountryController,
|
||||
@@ -26,6 +25,7 @@ fun Application.masterdataApiModule(
|
||||
reiterController: ReiterController,
|
||||
horseController: HorseController,
|
||||
vereinController: VereinController,
|
||||
funktionaerController: FunktionaerController,
|
||||
regulationController: RegulationController,
|
||||
meterRegistry: MeterRegistry = PrometheusMeterRegistry(PrometheusConfig.DEFAULT)
|
||||
) {
|
||||
@@ -48,6 +48,7 @@ fun Application.masterdataApiModule(
|
||||
with(reiterController) { registerRoutes() }
|
||||
with(horseController) { registerRoutes() }
|
||||
with(vereinController) { registerRoutes() }
|
||||
with(funktionaerController) { registerRoutes() }
|
||||
with(regulationController) { registerRoutes() }
|
||||
}
|
||||
}
|
||||
|
||||
+238
@@ -0,0 +1,238 @@
|
||||
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
||||
|
||||
package at.mocode.masterdata.api.rest
|
||||
|
||||
import at.mocode.core.domain.model.FunktionaerRolleE
|
||||
import at.mocode.core.domain.model.RichterQualifikationE
|
||||
import at.mocode.core.domain.model.SparteE
|
||||
import at.mocode.core.domain.serialization.InstantSerializer
|
||||
import at.mocode.core.domain.serialization.LocalDateSerializer
|
||||
import at.mocode.masterdata.domain.model.DomFunktionaer
|
||||
import at.mocode.masterdata.domain.repository.FunktionaerRepository
|
||||
import io.ktor.http.*
|
||||
import io.ktor.server.request.*
|
||||
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 Funktionärs-bezogene REST-Endpunkte.
|
||||
* Bietet vollständiges CRUD: GET (Liste/Suche/Einzeln), POST, PUT, DELETE.
|
||||
*/
|
||||
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 istAktiv: Boolean,
|
||||
val bemerkungen: String? = null,
|
||||
@Serializable(with = InstantSerializer::class)
|
||||
val updatedAt: Instant
|
||||
)
|
||||
|
||||
@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 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 istAktiv: Boolean? = null,
|
||||
val bemerkungen: String? = null
|
||||
)
|
||||
|
||||
fun Route.registerRoutes() {
|
||||
route("/funktionaer") {
|
||||
|
||||
/**
|
||||
* GET /funktionaer — Alle Funktionäre (paginiert), optional gefiltert nach rolle.
|
||||
*/
|
||||
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)
|
||||
}
|
||||
call.respond(results.map { it.toDto() })
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /funktionaer/search?q=... — Sucht Funktionäre nach Name.
|
||||
*/
|
||||
get("/search") {
|
||||
val query = call.request.queryParameters["q"] ?: ""
|
||||
val results = funktionaerRepository.findByName(query)
|
||||
call.respond(results.map { it.toDto() })
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /funktionaer/{id} — Ruft einen spezifischen Funktionär ab.
|
||||
*/
|
||||
get("/{id}") {
|
||||
val id = parseUuid(call.parameters["id"]) ?: return@get call.respond(HttpStatusCode.BadRequest)
|
||||
val funktionaer = funktionaerRepository.findById(id)
|
||||
if (funktionaer != null) call.respond(funktionaer.toDto()) else call.respond(HttpStatusCode.NotFound)
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /funktionaer/richternummer/{nr} — Sucht einen Funktionär nach seiner Richternummer.
|
||||
*/
|
||||
get("/richternummer/{nr}") {
|
||||
val nr = call.parameters["nr"] ?: return@get call.respond(HttpStatusCode.BadRequest)
|
||||
val funktionaer = funktionaerRepository.findByRichterNummer(nr)
|
||||
if (funktionaer != null) call.respond(funktionaer.toDto()) else call.respond(HttpStatusCode.NotFound)
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /funktionaer — Erstellt einen neuen Funktionär.
|
||||
*/
|
||||
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,
|
||||
istAktiv = req.istAktiv,
|
||||
bemerkungen = req.bemerkungen
|
||||
)
|
||||
val saved = funktionaerRepository.save(domFunktionaer)
|
||||
call.respond(HttpStatusCode.Created, saved.toDto())
|
||||
}
|
||||
|
||||
/**
|
||||
* PUT /funktionaer/{id} — Aktualisiert einen bestehenden Funktionär.
|
||||
*/
|
||||
put("/{id}") {
|
||||
val id = parseUuid(call.parameters["id"]) ?: return@put call.respond(HttpStatusCode.BadRequest)
|
||||
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,
|
||||
istAktiv = req.istAktiv ?: existing.istAktiv,
|
||||
bemerkungen = req.bemerkungen ?: existing.bemerkungen
|
||||
)
|
||||
val saved = funktionaerRepository.save(updated)
|
||||
call.respond(saved.toDto())
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE /funktionaer/{id} — Löscht einen Funktionär.
|
||||
*/
|
||||
delete("/{id}") {
|
||||
val id = parseUuid(call.parameters["id"]) ?: return@delete call.respond(HttpStatusCode.BadRequest)
|
||||
val deleted = funktionaerRepository.delete(id)
|
||||
if (deleted) call.respond(HttpStatusCode.NoContent) else call.respond(HttpStatusCode.NotFound)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseUuid(value: String?): Uuid? = value?.let { runCatching { Uuid.parse(it) }.getOrNull() }
|
||||
|
||||
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,
|
||||
istAktiv = istAktiv,
|
||||
bemerkungen = bemerkungen,
|
||||
updatedAt = updatedAt
|
||||
)
|
||||
}
|
||||
+170
-19
@@ -2,11 +2,13 @@
|
||||
|
||||
package at.mocode.masterdata.api.rest
|
||||
|
||||
import at.mocode.core.domain.model.PferdeGeschlechtE
|
||||
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.request.*
|
||||
import io.ktor.server.response.*
|
||||
import io.ktor.server.routing.*
|
||||
import kotlinx.datetime.LocalDate
|
||||
@@ -16,6 +18,7 @@ import kotlin.uuid.Uuid
|
||||
|
||||
/**
|
||||
* Controller für Pferde-bezogene REST-Endpunkte.
|
||||
* Bietet vollständiges CRUD: GET (Liste/Suche/Einzeln), POST, PUT, DELETE.
|
||||
*/
|
||||
class HorseController(private val horseRepository: HorseRepository) {
|
||||
|
||||
@@ -27,18 +30,91 @@ class HorseController(private val horseRepository: HorseRepository) {
|
||||
@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,
|
||||
val bemerkungen: String? = null,
|
||||
@Serializable(with = InstantSerializer::class)
|
||||
val updatedAt: Instant
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class HorseCreateRequest(
|
||||
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
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class HorseUpdateRequest(
|
||||
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
|
||||
)
|
||||
|
||||
fun Route.registerRoutes() {
|
||||
route("/horse") {
|
||||
|
||||
/**
|
||||
* Sucht Pferde nach Name.
|
||||
* GET /horse — Alle Pferde (paginiert), optional gefiltert nach jahrgang oder besitzerId.
|
||||
*/
|
||||
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("/search") {
|
||||
val query = call.request.queryParameters["q"] ?: ""
|
||||
@@ -47,49 +123,124 @@ class HorseController(private val horseRepository: HorseRepository) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Ruft ein spezifisches Pferd ab.
|
||||
* GET /horse/{id} — 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 id = parseUuid(call.parameters["id"]) ?: return@get call.respond(HttpStatusCode.BadRequest)
|
||||
val pferd = horseRepository.findById(id)
|
||||
if (pferd != null) {
|
||||
call.respond(pferd.toDto())
|
||||
} else {
|
||||
call.respond(HttpStatusCode.NotFound)
|
||||
}
|
||||
if (pferd != null) call.respond(pferd.toDto()) else call.respond(HttpStatusCode.NotFound)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sucht ein Pferd nach seiner Lebensnummer.
|
||||
* GET /horse/lebensnummer/{nr} — 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)
|
||||
if (pferd != null) call.respond(pferd.toDto()) else call.respond(HttpStatusCode.NotFound)
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /horse — Erstellt ein neues Pferd.
|
||||
*/
|
||||
post {
|
||||
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(
|
||||
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
|
||||
)
|
||||
val saved = horseRepository.save(domPferd)
|
||||
call.respond(HttpStatusCode.Created, saved.toDto())
|
||||
}
|
||||
|
||||
/**
|
||||
* PUT /horse/{id} — Aktualisiert ein bestehendes Pferd.
|
||||
*/
|
||||
put("/{id}") {
|
||||
val id = parseUuid(call.parameters["id"]) ?: return@put call.respond(HttpStatusCode.BadRequest)
|
||||
val existing = horseRepository.findById(id) ?: return@put call.respond(HttpStatusCode.NotFound)
|
||||
val req = call.receive<HorseUpdateRequest>()
|
||||
val geschlecht = req.geschlecht?.let {
|
||||
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(
|
||||
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
|
||||
)
|
||||
val saved = horseRepository.save(updated)
|
||||
call.respond(saved.toDto())
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE /horse/{id} — Löscht ein Pferd.
|
||||
*/
|
||||
delete("/{id}") {
|
||||
val id = parseUuid(call.parameters["id"]) ?: return@delete call.respond(HttpStatusCode.BadRequest)
|
||||
val deleted = horseRepository.delete(id)
|
||||
if (deleted) call.respond(HttpStatusCode.NoContent) else call.respond(HttpStatusCode.NotFound)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseUuid(value: String?): Uuid? = value?.let { runCatching { Uuid.parse(it) }.getOrNull() }
|
||||
|
||||
private fun DomPferd.toDto() = HorseDto(
|
||||
pferdId = pferdId.toString(),
|
||||
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,
|
||||
istAktiv = istAktiv,
|
||||
bemerkungen = bemerkungen,
|
||||
updatedAt = updatedAt
|
||||
)
|
||||
}
|
||||
|
||||
+142
-20
@@ -2,11 +2,13 @@
|
||||
|
||||
package at.mocode.masterdata.api.rest
|
||||
|
||||
import at.mocode.core.domain.model.LizenzKlasseE
|
||||
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.request.*
|
||||
import io.ktor.server.response.*
|
||||
import io.ktor.server.routing.*
|
||||
import kotlinx.datetime.LocalDate
|
||||
@@ -16,6 +18,7 @@ import kotlin.uuid.Uuid
|
||||
|
||||
/**
|
||||
* Controller für Reiter-bezogene REST-Endpunkte.
|
||||
* Bietet vollständiges CRUD: GET (Liste/Suche/Einzeln), POST, PUT, DELETE.
|
||||
*/
|
||||
class ReiterController(private val reiterRepository: ReiterRepository) {
|
||||
|
||||
@@ -31,15 +34,77 @@ class ReiterController(private val reiterRepository: ReiterRepository) {
|
||||
val lizenzKlasse: String,
|
||||
val startkartAktiv: Boolean,
|
||||
val nation: String? = null,
|
||||
val vereinsNummer: String? = null,
|
||||
val vereinsName: String? = null,
|
||||
val feiId: String? = null,
|
||||
val istGastreiter: Boolean,
|
||||
val istAktiv: Boolean,
|
||||
@Serializable(with = InstantSerializer::class)
|
||||
val updatedAt: Instant
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ReiterCreateRequest(
|
||||
val satznummer: String,
|
||||
val nachname: String,
|
||||
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 vereinsName: String? = null,
|
||||
val feiId: String? = null,
|
||||
val istGastreiter: Boolean = false,
|
||||
val istAktiv: Boolean = true
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ReiterUpdateRequest(
|
||||
val nachname: String? = null,
|
||||
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 vereinsName: String? = null,
|
||||
val feiId: String? = null,
|
||||
val istGastreiter: Boolean? = null,
|
||||
val istAktiv: Boolean? = null
|
||||
)
|
||||
|
||||
fun Route.registerRoutes() {
|
||||
route("/reiter") {
|
||||
|
||||
/**
|
||||
* Sucht Reiter nach Name oder Satznummer.
|
||||
* GET /reiter — Alle Reiter (paginiert), optional gefiltert nach lizenzKlasse oder vereinId.
|
||||
*/
|
||||
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)
|
||||
}
|
||||
call.respond(results.map { it.toDto() })
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /reiter/search?q=... — Sucht Reiter nach Name oder Satznummer.
|
||||
*/
|
||||
get("/search") {
|
||||
val query = call.request.queryParameters["q"] ?: ""
|
||||
@@ -48,39 +113,92 @@ class ReiterController(private val reiterRepository: ReiterRepository) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Ruft einen spezifischen Reiter ab.
|
||||
* GET /reiter/{id} — 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 id = parseUuid(call.parameters["id"]) ?: return@get call.respond(HttpStatusCode.BadRequest)
|
||||
val reiter = reiterRepository.findById(id)
|
||||
if (reiter != null) {
|
||||
call.respond(reiter.toDto())
|
||||
} else {
|
||||
call.respond(HttpStatusCode.NotFound)
|
||||
}
|
||||
if (reiter != null) call.respond(reiter.toDto()) else call.respond(HttpStatusCode.NotFound)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sucht einen Reiter nach seiner Satznummer.
|
||||
* GET /reiter/satznummer/{nr} — 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)
|
||||
}
|
||||
if (reiter != null) call.respond(reiter.toDto()) else call.respond(HttpStatusCode.NotFound)
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /reiter — Erstellt einen neuen Reiter.
|
||||
*/
|
||||
post {
|
||||
val req = call.receive<ReiterCreateRequest>()
|
||||
val lizenzKlasse = runCatching { LizenzKlasseE.valueOf(req.lizenzKlasse) }.getOrNull()
|
||||
?: return@post call.respond(HttpStatusCode.BadRequest, "Ungültige lizenzKlasse: ${req.lizenzKlasse}")
|
||||
val domReiter = DomReiter(
|
||||
personId = Uuid.random(),
|
||||
satznummer = req.satznummer,
|
||||
nachname = req.nachname,
|
||||
vorname = req.vorname,
|
||||
geburtsdatum = req.geburtsdatum,
|
||||
lizenzNummer = req.lizenzNummer,
|
||||
lizenzKlasse = lizenzKlasse,
|
||||
startkartAktiv = req.startkartAktiv,
|
||||
nation = req.nation,
|
||||
vereinsNummer = req.vereinsNummer,
|
||||
vereinsName = req.vereinsName,
|
||||
feiId = req.feiId,
|
||||
istGastreiter = req.istGastreiter,
|
||||
istAktiv = req.istAktiv
|
||||
)
|
||||
val saved = reiterRepository.save(domReiter)
|
||||
call.respond(HttpStatusCode.Created, saved.toDto())
|
||||
}
|
||||
|
||||
/**
|
||||
* PUT /reiter/{id} — Aktualisiert einen bestehenden Reiter.
|
||||
*/
|
||||
put("/{id}") {
|
||||
val id = parseUuid(call.parameters["id"]) ?: return@put call.respond(HttpStatusCode.BadRequest)
|
||||
val existing = reiterRepository.findById(id) ?: return@put call.respond(HttpStatusCode.NotFound)
|
||||
val req = call.receive<ReiterUpdateRequest>()
|
||||
val lizenzKlasse = req.lizenzKlasse?.let {
|
||||
runCatching { LizenzKlasseE.valueOf(it) }.getOrNull()
|
||||
?: return@put call.respond(HttpStatusCode.BadRequest, "Ungültige lizenzKlasse: $it")
|
||||
} ?: existing.lizenzKlasse
|
||||
val updated = existing.copy(
|
||||
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,
|
||||
vereinsName = req.vereinsName ?: existing.vereinsName,
|
||||
feiId = req.feiId ?: existing.feiId,
|
||||
istGastreiter = req.istGastreiter ?: existing.istGastreiter,
|
||||
istAktiv = req.istAktiv ?: existing.istAktiv
|
||||
)
|
||||
val saved = reiterRepository.save(updated)
|
||||
call.respond(saved.toDto())
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE /reiter/{id} — Löscht einen Reiter.
|
||||
*/
|
||||
delete("/{id}") {
|
||||
val id = parseUuid(call.parameters["id"]) ?: return@delete call.respond(HttpStatusCode.BadRequest)
|
||||
val deleted = reiterRepository.delete(id)
|
||||
if (deleted) call.respond(HttpStatusCode.NoContent) else call.respond(HttpStatusCode.NotFound)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseUuid(value: String?): Uuid? = value?.let { runCatching { Uuid.parse(it) }.getOrNull() }
|
||||
|
||||
private fun DomReiter.toDto() = ReiterDto(
|
||||
reiterId = reiterId.toString(),
|
||||
satznummer = satznummer,
|
||||
@@ -91,7 +209,11 @@ class ReiterController(private val reiterRepository: ReiterRepository) {
|
||||
lizenzKlasse = lizenzKlasse.name,
|
||||
startkartAktiv = startkartAktiv,
|
||||
nation = nation,
|
||||
vereinsNummer = vereinsNummer,
|
||||
vereinsName = vereinsName,
|
||||
feiId = feiId,
|
||||
istGastreiter = istGastreiter,
|
||||
istAktiv = istAktiv,
|
||||
updatedAt = updatedAt
|
||||
)
|
||||
}
|
||||
|
||||
+140
-23
@@ -6,13 +6,16 @@ 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.request.*
|
||||
import io.ktor.server.response.*
|
||||
import io.ktor.server.routing.*
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlin.time.Instant
|
||||
import kotlin.uuid.Uuid
|
||||
|
||||
/**
|
||||
* Controller für Vereins-bezogene REST-Endpunkte.
|
||||
* Bietet vollständiges CRUD: GET (Liste/Suche/Einzeln), POST, PUT, DELETE.
|
||||
*/
|
||||
class VereinController(private val vereinRepository: VereinRepository) {
|
||||
|
||||
@@ -22,18 +25,77 @@ class VereinController(private val vereinRepository: VereinRepository) {
|
||||
val vereinsNummer: String,
|
||||
val name: String,
|
||||
val kurzname: String? = null,
|
||||
val bundesland: String,
|
||||
val bundesland: String? = null,
|
||||
val ort: String? = null,
|
||||
val plz: String? = null,
|
||||
val strasse: String? = null,
|
||||
val email: String? = null,
|
||||
val telefon: String? = null,
|
||||
val website: String? = null,
|
||||
val oepsRegionNummer: String? = null,
|
||||
val istVeranstalter: Boolean,
|
||||
val istAktiv: Boolean,
|
||||
val bemerkungen: String? = null,
|
||||
@Serializable(with = InstantSerializer::class)
|
||||
val updatedAt: kotlin.time.Instant
|
||||
val updatedAt: Instant
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class VereinCreateRequest(
|
||||
val vereinsNummer: String,
|
||||
val name: String,
|
||||
val kurzname: String? = null,
|
||||
val bundesland: String? = null,
|
||||
val ort: String? = null,
|
||||
val plz: String? = null,
|
||||
val strasse: String? = null,
|
||||
val email: String? = null,
|
||||
val telefon: String? = null,
|
||||
val website: String? = null,
|
||||
val oepsRegionNummer: String? = null,
|
||||
val istVeranstalter: Boolean = false,
|
||||
val istAktiv: Boolean = true,
|
||||
val bemerkungen: String? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class VereinUpdateRequest(
|
||||
val name: String? = null,
|
||||
val kurzname: String? = null,
|
||||
val bundesland: String? = null,
|
||||
val ort: String? = null,
|
||||
val plz: String? = null,
|
||||
val strasse: String? = null,
|
||||
val email: String? = null,
|
||||
val telefon: String? = null,
|
||||
val website: String? = null,
|
||||
val oepsRegionNummer: String? = null,
|
||||
val istVeranstalter: Boolean? = null,
|
||||
val istAktiv: Boolean? = null,
|
||||
val bemerkungen: String? = null
|
||||
)
|
||||
|
||||
fun Route.registerRoutes() {
|
||||
route("/verein") {
|
||||
|
||||
/**
|
||||
* Sucht Vereine nach Name oder Kurzname.
|
||||
* GET /verein — Alle Vereine (paginiert), optional gefiltert nach verband/bundesland.
|
||||
*/
|
||||
get {
|
||||
val verband = call.request.queryParameters["verband"]
|
||||
val limit = call.request.queryParameters["limit"]?.toIntOrNull() ?: 100
|
||||
val offset = call.request.queryParameters["offset"]?.toIntOrNull() ?: 0
|
||||
|
||||
val results = if (verband != null) {
|
||||
vereinRepository.findByBundesland(verband)
|
||||
} else {
|
||||
vereinRepository.findAll(limit, offset)
|
||||
}
|
||||
call.respond(results.map { it.toDto() })
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /verein/search?q=... — Sucht Vereine nach Name oder Kurzname.
|
||||
*/
|
||||
get("/search") {
|
||||
val query = call.request.queryParameters["q"] ?: ""
|
||||
@@ -42,48 +104,103 @@ class VereinController(private val vereinRepository: VereinRepository) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Ruft einen spezifischen Verein ab.
|
||||
* GET /verein/{id} — 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 id = parseUuid(call.parameters["id"]) ?: return@get call.respond(HttpStatusCode.BadRequest)
|
||||
val verein = vereinRepository.findById(id)
|
||||
if (verein != null) {
|
||||
call.respond(verein.toDto())
|
||||
} else {
|
||||
call.respond(HttpStatusCode.NotFound)
|
||||
}
|
||||
if (verein != null) call.respond(verein.toDto()) else call.respond(HttpStatusCode.NotFound)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sucht einen Verein nach seiner Vereinsnummer.
|
||||
* GET /verein/nummer/{nr} — 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)
|
||||
}
|
||||
if (verein != null) call.respond(verein.toDto()) else call.respond(HttpStatusCode.NotFound)
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /verein — Erstellt einen neuen Verein.
|
||||
*/
|
||||
post {
|
||||
val req = call.receive<VereinCreateRequest>()
|
||||
val domVerein = DomVerein(
|
||||
vereinsNummer = req.vereinsNummer,
|
||||
name = req.name,
|
||||
kurzname = req.kurzname,
|
||||
bundesland = req.bundesland,
|
||||
ort = req.ort,
|
||||
plz = req.plz,
|
||||
strasse = req.strasse,
|
||||
email = req.email,
|
||||
telefon = req.telefon,
|
||||
website = req.website,
|
||||
oepsRegionNummer = req.oepsRegionNummer,
|
||||
istVeranstalter = req.istVeranstalter,
|
||||
istAktiv = req.istAktiv,
|
||||
bemerkungen = req.bemerkungen
|
||||
)
|
||||
val saved = vereinRepository.save(domVerein)
|
||||
call.respond(HttpStatusCode.Created, saved.toDto())
|
||||
}
|
||||
|
||||
/**
|
||||
* PUT /verein/{id} — Aktualisiert einen bestehenden Verein.
|
||||
*/
|
||||
put("/{id}") {
|
||||
val id = parseUuid(call.parameters["id"]) ?: return@put call.respond(HttpStatusCode.BadRequest)
|
||||
val existing = vereinRepository.findById(id) ?: return@put call.respond(HttpStatusCode.NotFound)
|
||||
val req = call.receive<VereinUpdateRequest>()
|
||||
val updated = existing.copy(
|
||||
name = req.name ?: existing.name,
|
||||
kurzname = req.kurzname ?: existing.kurzname,
|
||||
bundesland = req.bundesland ?: existing.bundesland,
|
||||
ort = req.ort ?: existing.ort,
|
||||
plz = req.plz ?: existing.plz,
|
||||
strasse = req.strasse ?: existing.strasse,
|
||||
email = req.email ?: existing.email,
|
||||
telefon = req.telefon ?: existing.telefon,
|
||||
website = req.website ?: existing.website,
|
||||
oepsRegionNummer = req.oepsRegionNummer ?: existing.oepsRegionNummer,
|
||||
istVeranstalter = req.istVeranstalter ?: existing.istVeranstalter,
|
||||
istAktiv = req.istAktiv ?: existing.istAktiv,
|
||||
bemerkungen = req.bemerkungen ?: existing.bemerkungen
|
||||
)
|
||||
val saved = vereinRepository.save(updated)
|
||||
call.respond(saved.toDto())
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE /verein/{id} — Löscht einen Verein.
|
||||
*/
|
||||
delete("/{id}") {
|
||||
val id = parseUuid(call.parameters["id"]) ?: return@delete call.respond(HttpStatusCode.BadRequest)
|
||||
val deleted = vereinRepository.delete(id)
|
||||
if (deleted) call.respond(HttpStatusCode.NoContent) else call.respond(HttpStatusCode.NotFound)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseUuid(value: String?): Uuid? = value?.let { runCatching { Uuid.parse(it) }.getOrNull() }
|
||||
|
||||
private fun DomVerein.toDto() = VereinDto(
|
||||
vereinId = vereinId.toString(),
|
||||
vereinsNummer = vereinsNummer,
|
||||
name = name,
|
||||
kurzname = kurzname,
|
||||
bundesland = bundesland ?: "",
|
||||
bundesland = bundesland,
|
||||
ort = ort,
|
||||
plz = plz,
|
||||
strasse = strasse,
|
||||
email = email,
|
||||
telefon = telefon,
|
||||
website = website,
|
||||
oepsRegionNummer = oepsRegionNummer,
|
||||
istVeranstalter = istVeranstalter,
|
||||
istAktiv = istAktiv,
|
||||
bemerkungen = bemerkungen,
|
||||
updatedAt = updatedAt
|
||||
)
|
||||
}
|
||||
|
||||
+1
@@ -53,6 +53,7 @@ class RegulationControllerTest {
|
||||
reiterController = mockk(relaxed = true),
|
||||
horseController = mockk(relaxed = true),
|
||||
vereinController = mockk(relaxed = true),
|
||||
funktionaerController = mockk(relaxed = true),
|
||||
regulationController = controller
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user