Integrate advanced filtering, sorting, and pagination logic into Altersklasse, Bundesland, and Platz controllers. Enhance error handling with centralized ErrorCodes and update date serialization for consistent handling of Instant values.

This commit is contained in:
Stefan Mogeritsch 2026-03-28 20:48:36 +01:00
parent f91b067b36
commit 74df3514ae
4 changed files with 313 additions and 43 deletions

View File

@ -3,6 +3,9 @@
package at.mocode.masterdata.api.rest
import at.mocode.core.domain.model.ApiResponse
import at.mocode.core.domain.model.ErrorCodes
import at.mocode.core.domain.model.PagedResponse
import at.mocode.core.domain.model.SortDirection
import at.mocode.core.domain.model.SparteE
import at.mocode.masterdata.application.usecase.CreateAltersklasseUseCase
import at.mocode.masterdata.application.usecase.GetAltersklasseUseCase
@ -13,6 +16,9 @@ import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import kotlinx.serialization.Serializable
import at.mocode.core.domain.serialization.InstantSerializer
import kotlin.time.Instant
import kotlin.math.min
/**
* REST API controller for age class management operations.
@ -34,8 +40,10 @@ class AltersklasseController(
val geschlechtFilter: String? = null,
val oetoRegelReferenzId: String? = null,
val istAktiv: Boolean = true,
val createdAt: String,
val updatedAt: String
@Serializable(with = InstantSerializer::class)
val createdAt: Instant,
@Serializable(with = InstantSerializer::class)
val updatedAt: Instant
)
@Serializable
@ -76,9 +84,93 @@ class AltersklasseController(
}
val geschlecht = call.request.queryParameters["geschlecht"]?.getOrNull(0)
val response = getAltersklasseUseCase.getAllActive(sparte, geschlecht)
val dtos = response.altersklassen.map { it.toDto() }
call.respond(ApiResponse.success(dtos))
val search = call.request.queryParameters["search"]?.trim().takeUnless { it.isNullOrBlank() }
val activeParam = call.request.queryParameters["active"]?.lowercase()
val page = call.request.queryParameters["page"]?.toIntOrNull() ?: 0
val sizeParam = call.request.queryParameters["size"]?.toIntOrNull()
val unpaged = call.request.queryParameters["unpaged"]?.let { v ->
when (v.lowercase()) { "true", "1", "yes" -> true; else -> false }
} ?: false
val size = sizeParam ?: 20
val sortParams = call.request.queryParameters.getAll("sort")?.mapNotNull { token ->
val parts = token.split(",")
if (parts.isEmpty()) null else {
val field = parts[0].trim()
val dir = if (parts.getOrNull(1)?.trim()?.equals("desc", ignoreCase = true) == true) SortDirection.DESC else SortDirection.ASC
field to dir
}
} ?: listOf("bezeichnung" to SortDirection.ASC)
val baseList = when (activeParam) {
null, "true" -> getAltersklasseUseCase.getAllActive(sparte, geschlecht).altersklassen
"false", "all" -> getAltersklasseUseCase.getAllActive(null, null).altersklassen // TODO: getAll wenn verfügbar
else -> getAltersklasseUseCase.getAllActive(sparte, geschlecht).altersklassen
}
val filtered = baseList.asSequence()
.filter { item ->
when (activeParam) {
"true", null -> item.istAktiv
"false" -> !item.istAktiv
"all" -> true
else -> true
}
}
.filter { item ->
val q = search
if (q.isNullOrBlank()) return@filter true
val s = q.lowercase()
item.altersklasseCode.lowercase().contains(s) || item.bezeichnung.lowercase().contains(s)
}
.toList()
var list = filtered
sortParams.forEach { (field, dir) ->
list = when (field) {
"bezeichnung" -> if (dir == SortDirection.ASC) list.sortedBy { it.bezeichnung.lowercase() } else list.sortedByDescending { it.bezeichnung.lowercase() }
"altersklasseCode" -> if (dir == SortDirection.ASC) list.sortedBy { it.altersklasseCode.lowercase() } else list.sortedByDescending { it.altersklasseCode.lowercase() }
else -> list
}
}
val total = list.size.toLong()
val isUnpaged = unpaged || size == -1
val pageContent: List<AltersklasseDefinition>
val effPage: Int
val effSize: Int
val totalPages: Int
val hasNext: Boolean
val hasPrevious: Boolean
if (isUnpaged) {
pageContent = list
effPage = 0
effSize = list.size
totalPages = 1
hasNext = false
hasPrevious = false
} else {
val fromIndex = (page * size).coerceAtLeast(0)
val toIndex = min(fromIndex + size, list.size)
pageContent = if (fromIndex in 0..list.size) list.subList(fromIndex, toIndex) else emptyList()
totalPages = if (size > 0) ((total + size - 1) / size).toInt() else 1
hasNext = page + 1 < totalPages
hasPrevious = page > 0
effPage = page
effSize = size
}
val dtoPage = pageContent.map { it.toDto() }
val paged = PagedResponse.create(
content = dtoPage,
page = effPage,
size = effSize,
totalElements = total,
totalPages = totalPages,
hasNext = hasNext,
hasPrevious = hasPrevious
)
call.respond(ApiResponse.success(paged))
}
get("/{id}") {
@ -92,13 +184,13 @@ class AltersklasseController(
}
?: return@get call.respond(
HttpStatusCode.BadRequest,
ApiResponse.error<Unit>("INVALID_ID", "Missing or invalid ID")
ApiResponse.error<Unit>(ErrorCodes.INVALID_ID, "Missing or invalid ID")
)
val response = getAltersklasseUseCase.getById(id)
response.altersklasse?.let {
call.respond(ApiResponse.success(it.toDto()))
} ?: call.respond(HttpStatusCode.NotFound, ApiResponse.error<Unit>("NOT_FOUND", "Age class not found"))
} ?: call.respond(HttpStatusCode.NotFound, ApiResponse.error<Unit>(ErrorCodes.NOT_FOUND, "Age class not found"))
}
post {
@ -150,7 +242,7 @@ class AltersklasseController(
}
?: return@put call.respond(
HttpStatusCode.BadRequest,
ApiResponse.error<Unit>("INVALID_ID", "Missing or invalid ID")
ApiResponse.error<Unit>(ErrorCodes.INVALID_ID, "Missing or invalid ID")
)
val dto = call.receive<UpdateAltersklasseDto>()
@ -186,7 +278,7 @@ class AltersklasseController(
} else {
call.respond(
HttpStatusCode.BadRequest,
ApiResponse.error<Unit>("UPDATE_FAILED", response.errors.joinToString())
ApiResponse.error<Unit>(ErrorCodes.UPDATE_FAILED, response.errors.joinToString())
)
}
}
@ -202,7 +294,7 @@ class AltersklasseController(
}
?: return@delete call.respond(
HttpStatusCode.BadRequest,
ApiResponse.error<Unit>("INVALID_ID", "Missing or invalid ID")
ApiResponse.error<Unit>(ErrorCodes.INVALID_ID, "Missing or invalid ID")
)
val response = createAltersklasseUseCase.deleteAltersklasse(id)
@ -211,7 +303,7 @@ class AltersklasseController(
} else {
call.respond(
HttpStatusCode.NotFound,
ApiResponse.error<Unit>("DELETE_FAILED", response.errors.joinToString())
ApiResponse.error<Unit>(ErrorCodes.DELETE_FAILED, response.errors.joinToString())
)
}
}
@ -229,7 +321,7 @@ class AltersklasseController(
geschlechtFilter = geschlechtFilter?.toString(),
oetoRegelReferenzId = oetoRegelReferenzId?.toString(),
istAktiv = istAktiv,
createdAt = createdAt.toString(),
updatedAt = updatedAt.toString()
createdAt = createdAt,
updatedAt = updatedAt
)
}

View File

@ -2,6 +2,9 @@
package at.mocode.masterdata.api.rest
import at.mocode.core.domain.model.ApiResponse
import at.mocode.core.domain.model.ErrorCodes
import at.mocode.core.domain.model.PagedResponse
import at.mocode.core.domain.model.SortDirection
import at.mocode.masterdata.application.usecase.CreateBundeslandUseCase
import at.mocode.masterdata.application.usecase.GetBundeslandUseCase
import at.mocode.masterdata.domain.model.BundeslandDefinition
@ -11,6 +14,9 @@ import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import kotlinx.serialization.Serializable
import at.mocode.core.domain.serialization.InstantSerializer
import kotlin.time.Instant
import kotlin.math.min
/**
* REST API controller for federal state (Bundesland) management.
@ -31,8 +37,10 @@ class BundeslandController(
val wappenUrl: String? = null,
val istAktiv: Boolean = true,
val sortierReihenfolge: Int? = null,
val createdAt: String,
val updatedAt: String
@Serializable(with = InstantSerializer::class)
val createdAt: Instant,
@Serializable(with = InstantSerializer::class)
val updatedAt: Instant
)
@Serializable
@ -58,14 +66,97 @@ class BundeslandController(
}
}
val response = if (landId != null) {
getBundeslandUseCase.getByCountry(landId)
} else {
getBundeslandUseCase.getAllActive()
val search = call.request.queryParameters["search"]?.trim().takeUnless { it.isNullOrBlank() }
val activeParam = call.request.queryParameters["active"]?.lowercase()
val page = call.request.queryParameters["page"]?.toIntOrNull() ?: 0
val sizeParam = call.request.queryParameters["size"]?.toIntOrNull()
val unpaged = call.request.queryParameters["unpaged"]?.let {
when (it.lowercase()) { "true", "1", "yes" -> true; else -> false }
} ?: false
val size = sizeParam ?: 20
val sortParams = call.request.queryParameters.getAll("sort")?.mapNotNull { token ->
val parts = token.split(",")
if (parts.isEmpty()) null else {
val field = parts[0].trim()
val dir = if (parts.getOrNull(1)?.trim()?.equals("desc", ignoreCase = true) == true) SortDirection.DESC else SortDirection.ASC
field to dir
}
} ?: listOf("sortierReihenfolge" to SortDirection.ASC, "name" to SortDirection.ASC)
val baseList = when (activeParam) {
null, "true" -> if (landId != null) getBundeslandUseCase.getByCountry(landId).bundeslaender.filter { it.istAktiv } else getBundeslandUseCase.getAllActive().bundeslaender
"false", "all" -> if (landId != null) getBundeslandUseCase.getByCountry(landId).bundeslaender else getBundeslandUseCase.getAllActive().bundeslaender
else -> if (landId != null) getBundeslandUseCase.getByCountry(landId).bundeslaender else getBundeslandUseCase.getAllActive().bundeslaender
}
val dtos = response.bundeslaender.map { it.toDto() }
call.respond(ApiResponse.success(dtos))
val filtered = baseList.asSequence()
.filter { item ->
when (activeParam) {
"true", null -> item.istAktiv
"false" -> !item.istAktiv
"all" -> true
else -> true
}
}
.filter { item ->
val q = search
if (q.isNullOrBlank()) return@filter true
val s = q.lowercase()
item.name.lowercase().contains(s) ||
(item.kuerzel?.lowercase()?.contains(s) == true) ||
(item.oepsCode?.lowercase()?.contains(s) == true) ||
(item.iso3166_2_Code?.lowercase()?.contains(s) == true)
}
.toList()
var list = filtered
sortParams.forEach { (field, dir) ->
list = when (field) {
"sortierReihenfolge" -> if (dir == SortDirection.ASC) list.sortedBy { it.sortierReihenfolge ?: Int.MAX_VALUE } else list.sortedByDescending { it.sortierReihenfolge ?: Int.MIN_VALUE }
"name" -> if (dir == SortDirection.ASC) list.sortedBy { it.name.lowercase() } else list.sortedByDescending { it.name.lowercase() }
"kuerzel" -> if (dir == SortDirection.ASC) list.sortedBy { it.kuerzel?.lowercase() } else list.sortedByDescending { it.kuerzel?.lowercase() }
else -> list
}
}
val total = list.size.toLong()
val isUnpaged = unpaged || size == -1
val pageContent: List<BundeslandDefinition>
val effPage: Int
val effSize: Int
val totalPages: Int
val hasNext: Boolean
val hasPrevious: Boolean
if (isUnpaged) {
pageContent = list
effPage = 0
effSize = list.size
totalPages = 1
hasNext = false
hasPrevious = false
} else {
val fromIndex = (page * size).coerceAtLeast(0)
val toIndex = min(fromIndex + size, list.size)
pageContent = if (fromIndex in 0..list.size) list.subList(fromIndex, toIndex) else emptyList()
totalPages = if (size > 0) ((total + size - 1) / size).toInt() else 1
hasNext = page + 1 < totalPages
hasPrevious = page > 0
effPage = page
effSize = size
}
val dtoPage = pageContent.map { it.toDto() }
val paged = PagedResponse.create(
content = dtoPage,
page = effPage,
size = effSize,
totalElements = total,
totalPages = totalPages,
hasNext = hasNext,
hasPrevious = hasPrevious
)
call.respond(ApiResponse.success(paged))
}
get("/{id}") {
@ -79,7 +170,7 @@ class BundeslandController(
}
?: return@get call.respond(
HttpStatusCode.BadRequest,
ApiResponse.error<Unit>("INVALID_ID", "Missing or invalid ID")
ApiResponse.error<Unit>(ErrorCodes.INVALID_ID, "Missing or invalid ID")
)
val response = getBundeslandUseCase.getById(id)
@ -87,7 +178,7 @@ class BundeslandController(
call.respond(ApiResponse.success(it.toDto()))
} ?: call.respond(
HttpStatusCode.NotFound,
ApiResponse.error<Unit>("NOT_FOUND", "Federal state not found")
ApiResponse.error<Unit>(ErrorCodes.NOT_FOUND, "Federal state not found")
)
}
@ -98,7 +189,7 @@ class BundeslandController(
} catch (e: Exception) {
return@post call.respond(
HttpStatusCode.BadRequest,
ApiResponse.error<Unit>("INVALID_LAND_ID", "Invalid landId format")
ApiResponse.error<Unit>(ErrorCodes.INVALID_PARAMETER, "Invalid landId format")
)
}
@ -120,7 +211,7 @@ class BundeslandController(
} else {
call.respond(
HttpStatusCode.BadRequest,
ApiResponse.error<Unit>("CREATION_FAILED", response.errors.joinToString())
ApiResponse.error<Unit>(ErrorCodes.CREATION_FAILED, response.errors.joinToString())
)
}
}
@ -137,7 +228,7 @@ class BundeslandController(
wappenUrl = wappenUrl,
istAktiv = istAktiv,
sortierReihenfolge = sortierReihenfolge,
createdAt = createdAt.toString(),
updatedAt = updatedAt.toString()
createdAt = createdAt,
updatedAt = updatedAt
)
}

View File

@ -4,21 +4,19 @@ package at.mocode.masterdata.api.rest
import at.mocode.core.domain.model.ApiResponse
import at.mocode.core.domain.model.ErrorCodes
import at.mocode.core.domain.model.PagedResponse
import at.mocode.core.domain.model.PageNumber
import at.mocode.core.domain.model.PageSize
import at.mocode.core.domain.model.SortDirection
import at.mocode.core.domain.serialization.InstantSerializer
import at.mocode.masterdata.application.usecase.CreateCountryUseCase
import at.mocode.masterdata.application.usecase.GetCountryUseCase
import at.mocode.masterdata.domain.model.LandDefinition
import kotlin.uuid.Uuid
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.math.min
import at.mocode.core.domain.serialization.InstantSerializer
import kotlin.time.Instant
import kotlin.uuid.Uuid
/**
* REST API controller for country (Land) management.

View File

@ -2,6 +2,9 @@
package at.mocode.masterdata.api.rest
import at.mocode.core.domain.model.ApiResponse
import at.mocode.core.domain.model.ErrorCodes
import at.mocode.core.domain.model.PagedResponse
import at.mocode.core.domain.model.SortDirection
import at.mocode.core.domain.model.PlatzTypE
import at.mocode.masterdata.application.usecase.CreatePlatzUseCase
import at.mocode.masterdata.application.usecase.GetPlatzUseCase
@ -12,6 +15,9 @@ import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import kotlinx.serialization.Serializable
import at.mocode.core.domain.serialization.InstantSerializer
import kotlin.time.Instant
import kotlin.math.min
/**
* REST API controller for venue/arena (Platz) management.
@ -31,8 +37,10 @@ class PlatzController(
val typ: String,
val istAktiv: Boolean = true,
val sortierReihenfolge: Int? = null,
val createdAt: String,
val updatedAt: String
@Serializable(with = InstantSerializer::class)
val createdAt: Instant,
@Serializable(with = InstantSerializer::class)
val updatedAt: Instant
)
@Serializable
@ -58,12 +66,93 @@ class PlatzController(
}
?: return@get call.respond(
HttpStatusCode.BadRequest,
ApiResponse.error<Unit>("MISSING_TURNIER_ID", "Query parameter turnierId is required")
ApiResponse.error<Unit>(ErrorCodes.MISSING_PARAMETER, "Query parameter turnierId is required")
)
val response = getPlatzUseCase.getByTournament(turnierId)
val dtos = response.plaetze.map { it.toDto() }
call.respond(ApiResponse.success(dtos))
val search = call.request.queryParameters["search"]?.trim().takeUnless { it.isNullOrBlank() }
val activeParam = call.request.queryParameters["active"]?.lowercase()
val page = call.request.queryParameters["page"]?.toIntOrNull() ?: 0
val sizeParam = call.request.queryParameters["size"]?.toIntOrNull()
val unpaged = call.request.queryParameters["unpaged"]?.let { v ->
when (v.lowercase()) { "true", "1", "yes" -> true; else -> false }
} ?: false
val size = sizeParam ?: 20
val sortParams = call.request.queryParameters.getAll("sort")?.mapNotNull { token ->
val parts = token.split(",")
if (parts.isEmpty()) null else {
val field = parts[0].trim()
val dir = if (parts.getOrNull(1)?.trim()?.equals("desc", ignoreCase = true) == true) SortDirection.DESC else SortDirection.ASC
field to dir
}
} ?: listOf("sortierReihenfolge" to SortDirection.ASC, "name" to SortDirection.ASC)
val baseList = getPlatzUseCase.getByTournament(turnierId).plaetze
val filtered = baseList.asSequence()
.filter { item ->
when (activeParam) {
"true", null -> item.istAktiv
"false" -> !item.istAktiv
"all" -> true
else -> true
}
}
.filter { item ->
val q = search
if (q.isNullOrBlank()) return@filter true
val s = q.lowercase()
item.name.lowercase().contains(s) ||
(item.dimension?.lowercase()?.contains(s) == true) ||
(item.boden?.lowercase()?.contains(s) == true)
}
.toList()
var list = filtered
sortParams.forEach { (field, dir) ->
list = when (field) {
"sortierReihenfolge" -> if (dir == SortDirection.ASC) list.sortedBy { it.sortierReihenfolge ?: Int.MAX_VALUE } else list.sortedByDescending { it.sortierReihenfolge ?: Int.MIN_VALUE }
"name" -> if (dir == SortDirection.ASC) list.sortedBy { it.name.lowercase() } else list.sortedByDescending { it.name.lowercase() }
else -> list
}
}
val total = list.size.toLong()
val isUnpaged = unpaged || size == -1
val pageContent: List<Platz>
val effPage: Int
val effSize: Int
val totalPages: Int
val hasNext: Boolean
val hasPrevious: Boolean
if (isUnpaged) {
pageContent = list
effPage = 0
effSize = list.size
totalPages = 1
hasNext = false
hasPrevious = false
} else {
val fromIndex = (page * size).coerceAtLeast(0)
val toIndex = min(fromIndex + size, list.size)
pageContent = if (fromIndex in 0..list.size) list.subList(fromIndex, toIndex) else emptyList()
totalPages = if (size > 0) ((total + size - 1) / size).toInt() else 1
hasNext = page + 1 < totalPages
hasPrevious = page > 0
effPage = page
effSize = size
}
val dtoPage = pageContent.map { it.toDto() }
val paged = PagedResponse.create(
content = dtoPage,
page = effPage,
size = effSize,
totalElements = total,
totalPages = totalPages,
hasNext = hasNext,
hasPrevious = hasPrevious
)
call.respond(ApiResponse.success(paged))
}
get("/{id}") {
@ -77,13 +166,13 @@ class PlatzController(
}
?: return@get call.respond(
HttpStatusCode.BadRequest,
ApiResponse.error<Unit>("INVALID_ID", "Missing or invalid ID")
ApiResponse.error<Unit>(ErrorCodes.INVALID_ID, "Missing or invalid ID")
)
val response = getPlatzUseCase.getById(id)
response.platz?.let {
call.respond(ApiResponse.success(it.toDto()))
} ?: call.respond(HttpStatusCode.NotFound, ApiResponse.error<Unit>("NOT_FOUND", "Venue not found"))
} ?: call.respond(HttpStatusCode.NotFound, ApiResponse.error<Unit>(ErrorCodes.NOT_FOUND, "Venue not found"))
}
post {
@ -93,7 +182,7 @@ class PlatzController(
} catch (e: Exception) {
return@post call.respond(
HttpStatusCode.BadRequest,
ApiResponse.error<Unit>("INVALID_TURNIER_ID", "Invalid turnierId format")
ApiResponse.error<Unit>(ErrorCodes.INVALID_PARAMETER, "Invalid turnierId format")
)
}
@ -118,7 +207,7 @@ class PlatzController(
} else {
call.respond(
HttpStatusCode.BadRequest,
ApiResponse.error<Unit>("CREATION_FAILED", response.errors.joinToString())
ApiResponse.error<Unit>(ErrorCodes.CREATION_FAILED, response.errors.joinToString())
)
}
}
@ -134,7 +223,7 @@ class PlatzController(
typ = typ.name,
istAktiv = istAktiv,
sortierReihenfolge = sortierReihenfolge,
createdAt = createdAt.toString(),
updatedAt = updatedAt.toString()
createdAt = createdAt,
updatedAt = updatedAt
)
}