docs: Migrationsplan für Projekt-Restrukturierung hinzugefügt

- Detaillierter Plan zur Migration von alter zu neuer Modulstruktur
- Umfasst Überführung von shared-kernel zu core-Modulen
- Definiert Migration von Fachdomänen zu bounded contexts:
  * master-data → masterdata-Module
  * member-management → members-Module
  * horse-registry → horses-Module
  * event-management → events-Module
- Beschreibt Verlagerung von api-gateway zu infrastructure/gateway
- Strukturiert nach Domain-driven Design Prinzipien
- Berücksichtigt Clean Architecture Layering (domain, application, infrastructure, api)
This commit is contained in:
stefan
2025-07-25 13:05:42 +02:00
parent a4c7d53aa3
commit 65a0084f91
68 changed files with 13107 additions and 101 deletions
@@ -0,0 +1,463 @@
package at.mocode.masterdata.api.rest
import at.mocode.core.domain.model.ApiResponse
import at.mocode.core.domain.model.SparteE
import at.mocode.masterdata.application.usecase.CreateAltersklasseUseCase
import at.mocode.masterdata.application.usecase.GetAltersklasseUseCase
import at.mocode.masterdata.domain.model.AltersklasseDefinition
import at.mocode.core.utils.validation.ApiValidationUtils
import com.benasher44.uuid.uuidFrom
import io.ktor.http.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import kotlinx.serialization.Serializable
/**
* REST API controller for age class management operations.
*
* This controller provides HTTP endpoints for the master-data context's
* age class functionality, following REST conventions and proper error handling.
*/
class AltersklasseController(
private val getAltersklasseUseCase: GetAltersklasseUseCase,
private val createAltersklasseUseCase: CreateAltersklasseUseCase
) {
/**
* DTO for age class API responses.
*/
@Serializable
data class AltersklasseDto(
val altersklasseId: String,
val altersklasseCode: String,
val bezeichnung: String,
val minAlter: Int? = null,
val maxAlter: Int? = null,
val stichtagRegelText: String? = null,
val sparteFilter: String? = null,
val geschlechtFilter: String? = null,
val oetoRegelReferenzId: String? = null,
val istAktiv: Boolean = true,
val createdAt: String,
val updatedAt: String
)
/**
* DTO for creating a new age class.
*/
@Serializable
data class CreateAltersklasseDto(
val altersklasseCode: String,
val bezeichnung: String,
val minAlter: Int? = null,
val maxAlter: Int? = null,
val stichtagRegelText: String? = "31.12. des laufenden Kalenderjahres",
val sparteFilter: String? = null,
val geschlechtFilter: String? = null,
val oetoRegelReferenzId: String? = null,
val istAktiv: Boolean = true
)
/**
* DTO for updating an existing age class.
*/
@Serializable
data class UpdateAltersklasseDto(
val altersklasseCode: String,
val bezeichnung: String,
val minAlter: Int? = null,
val maxAlter: Int? = null,
val stichtagRegelText: String? = "31.12. des laufenden Kalenderjahres",
val sparteFilter: String? = null,
val geschlechtFilter: String? = null,
val oetoRegelReferenzId: String? = null,
val istAktiv: Boolean = true
)
/**
* Configures the routing for age class endpoints.
*/
fun configureRouting(routing: Routing) {
routing.route("/api/masterdata/altersklassen") {
// GET /api/masterdata/altersklassen - Get all active age classes
get {
try {
val sparteFilterParam = call.request.queryParameters["sparte"]
val sparteFilter = sparteFilterParam?.let {
try {
SparteE.valueOf(it.uppercase())
} catch (e: Exception) {
return@get call.respond(
HttpStatusCode.BadRequest,
ApiResponse.error<List<AltersklasseDto>>("Invalid sparte parameter: $it")
)
}
}
val geschlechtFilterParam = call.request.queryParameters["geschlecht"]
val geschlechtFilter = geschlechtFilterParam?.let { gender ->
if (gender.length == 1 && (gender == "M" || gender == "W")) {
gender[0]
} else {
return@get call.respond(
HttpStatusCode.BadRequest,
ApiResponse.error<List<AltersklasseDto>>("Invalid geschlecht parameter. Must be 'M' or 'W'")
)
}
}
val altersklassen = getAltersklasseUseCase.getAllActive(sparteFilter, geschlechtFilter)
val altersklasseDtos = altersklassen.map { it.toDto() }
call.respond(HttpStatusCode.OK, ApiResponse.success(altersklasseDtos))
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<List<AltersklasseDto>>("Failed to retrieve age classes: ${e.message}"))
}
}
// GET /api/masterdata/altersklassen/{id} - Get age class by ID
get("/{id}") {
try {
val altersklasseId = call.parameters["id"]?.let { uuidFrom(it) }
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<AltersklasseDto>("Invalid age class ID"))
val altersklasse = getAltersklasseUseCase.getById(altersklasseId)
if (altersklasse != null) {
call.respond(HttpStatusCode.OK, ApiResponse.success(altersklasse.toDto()))
} else {
call.respond(HttpStatusCode.NotFound, ApiResponse.error<AltersklasseDto>("Age class not found"))
}
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<AltersklasseDto>("Failed to retrieve age class: ${e.message}"))
}
}
// GET /api/masterdata/altersklassen/code/{code} - Get age class by code
get("/code/{code}") {
try {
val altersklasseCode = call.parameters["code"]
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<AltersklasseDto>("Age class code is required"))
val altersklasse = getAltersklasseUseCase.getByCode(altersklasseCode)
if (altersklasse != null) {
call.respond(HttpStatusCode.OK, ApiResponse.success(altersklasse.toDto()))
} else {
call.respond(HttpStatusCode.NotFound, ApiResponse.error<AltersklasseDto>("Age class not found"))
}
} catch (e: IllegalArgumentException) {
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<AltersklasseDto>(e.message ?: "Invalid age class code"))
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<AltersklasseDto>("Failed to retrieve age class: ${e.message}"))
}
}
// GET /api/masterdata/altersklassen/search - Search age classes by name
get("/search") {
try {
val validationErrors = ApiValidationUtils.validateQueryParameters(
limit = call.request.queryParameters["limit"],
q = call.request.queryParameters["q"]
)
if (!ApiValidationUtils.isValid(validationErrors)) {
call.respond(
HttpStatusCode.BadRequest,
ApiResponse.error<List<AltersklasseDto>>(ApiValidationUtils.createErrorMessage(validationErrors))
)
return@get
}
val searchTerm = call.request.queryParameters["q"]
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<List<AltersklasseDto>>("Search term 'q' is required"))
val limit = call.request.queryParameters["limit"]?.toIntOrNull() ?: 50
val altersklassen = getAltersklasseUseCase.searchByName(searchTerm, limit)
val altersklasseDtos = altersklassen.map { it.toDto() }
call.respond(HttpStatusCode.OK, ApiResponse.success(altersklasseDtos))
} catch (e: IllegalArgumentException) {
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<List<AltersklasseDto>>(e.message ?: "Invalid search parameters"))
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<List<AltersklasseDto>>("Failed to search age classes: ${e.message}"))
}
}
// GET /api/masterdata/altersklassen/age/{age} - Get age classes applicable for specific age
get("/age/{age}") {
try {
val age = call.parameters["age"]?.toIntOrNull()
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<List<AltersklasseDto>>("Invalid age parameter"))
if (age < 0) {
return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<List<AltersklasseDto>>("Age must be non-negative"))
}
val sparteFilterParam = call.request.queryParameters["sparte"]
val sparteFilter = sparteFilterParam?.let { SparteE.valueOf(it.uppercase()) }
val geschlechtFilterParam = call.request.queryParameters["geschlecht"]
val geschlechtFilter = geschlechtFilterParam?.let { gender ->
if (gender.length == 1 && (gender == "M" || gender == "W")) {
gender[0]
} else {
return@get call.respond(
HttpStatusCode.BadRequest,
ApiResponse.error<List<AltersklasseDto>>("Invalid geschlecht parameter. Must be 'M' or 'W'")
)
}
}
val altersklassen = getAltersklasseUseCase.getApplicableForAge(age, sparteFilter, geschlechtFilter)
val altersklasseDtos = altersklassen.map { it.toDto() }
call.respond(HttpStatusCode.OK, ApiResponse.success(altersklasseDtos))
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<List<AltersklasseDto>>("Failed to retrieve age classes: ${e.message}"))
}
}
// GET /api/masterdata/altersklassen/sparte/{sparte} - Get age classes by sport type
get("/sparte/{sparte}") {
try {
val sparteParam = call.parameters["sparte"]
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<List<AltersklasseDto>>("Sport type is required"))
val sparte = try {
SparteE.valueOf(sparteParam.uppercase())
} catch (e: Exception) {
return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<List<AltersklasseDto>>("Invalid sport type: $sparteParam"))
}
val activeOnlyParam = call.request.queryParameters["activeOnly"]
val activeOnly = activeOnlyParam?.toBoolean() ?: true
val altersklassen = getAltersklasseUseCase.getBySparte(sparte, activeOnly)
val altersklasseDtos = altersklassen.map { it.toDto() }
call.respond(HttpStatusCode.OK, ApiResponse.success(altersklasseDtos))
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<List<AltersklasseDto>>("Failed to retrieve age classes: ${e.message}"))
}
}
// POST /api/masterdata/altersklassen - Create new age class
post {
try {
val createDto = call.receive<CreateAltersklasseDto>()
// Basic validation
if (createDto.altersklasseCode.isBlank()) {
call.respond(
HttpStatusCode.BadRequest,
ApiResponse.error<AltersklasseDto>("Age class code is required")
)
return@post
}
if (createDto.bezeichnung.isBlank()) {
call.respond(
HttpStatusCode.BadRequest,
ApiResponse.error<AltersklasseDto>("Bezeichnung is required")
)
return@post
}
val sparteFilter = createDto.sparteFilter?.let {
try {
SparteE.valueOf(it.uppercase())
} catch (e: Exception) {
return@post call.respond(
HttpStatusCode.BadRequest,
ApiResponse.error<AltersklasseDto>("Invalid sparte filter: $it")
)
}
}
val geschlechtFilter = createDto.geschlechtFilter?.let { gender ->
if (gender.length == 1 && (gender == "M" || gender == "W")) {
gender[0]
} else {
return@post call.respond(
HttpStatusCode.BadRequest,
ApiResponse.error<AltersklasseDto>("Invalid geschlecht filter. Must be 'M' or 'W'")
)
}
}
val oetoRegelReferenzId = createDto.oetoRegelReferenzId?.let {
try {
uuidFrom(it)
} catch (e: Exception) {
return@post call.respond(
HttpStatusCode.BadRequest,
ApiResponse.error<AltersklasseDto>("Invalid OETO regel referenz ID format")
)
}
}
val request = CreateAltersklasseUseCase.CreateAltersklasseRequest(
altersklasseCode = createDto.altersklasseCode,
bezeichnung = createDto.bezeichnung,
minAlter = createDto.minAlter,
maxAlter = createDto.maxAlter,
stichtagRegelText = createDto.stichtagRegelText,
sparteFilter = sparteFilter,
geschlechtFilter = geschlechtFilter,
oetoRegelReferenzId = oetoRegelReferenzId,
istAktiv = createDto.istAktiv
)
val result = createAltersklasseUseCase.createAltersklasse(request)
if (result.success) {
call.respond(HttpStatusCode.Created, ApiResponse.success(result.altersklasse!!.toDto()))
} else {
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<AltersklasseDto>("Validation failed: ${result.errors.joinToString(", ")}"))
}
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<AltersklasseDto>("Failed to create age class: ${e.message}"))
}
}
// PUT /api/masterdata/altersklassen/{id} - Update existing age class
put("/{id}") {
try {
val altersklasseId = call.parameters["id"]?.let { uuidFrom(it) }
?: return@put call.respond(HttpStatusCode.BadRequest, ApiResponse.error<AltersklasseDto>("Invalid age class ID"))
val updateDto = call.receive<UpdateAltersklasseDto>()
// Basic validation
if (updateDto.altersklasseCode.isBlank()) {
call.respond(
HttpStatusCode.BadRequest,
ApiResponse.error<AltersklasseDto>("Age class code is required")
)
return@put
}
if (updateDto.bezeichnung.isBlank()) {
call.respond(
HttpStatusCode.BadRequest,
ApiResponse.error<AltersklasseDto>("Bezeichnung is required")
)
return@put
}
val sparteFilter = updateDto.sparteFilter?.let {
try {
SparteE.valueOf(it.uppercase())
} catch (e: Exception) {
return@put call.respond(
HttpStatusCode.BadRequest,
ApiResponse.error<AltersklasseDto>("Invalid sparte filter: $it")
)
}
}
val geschlechtFilter = updateDto.geschlechtFilter?.let { gender ->
if (gender.length == 1 && (gender == "M" || gender == "W")) {
gender[0]
} else {
return@put call.respond(
HttpStatusCode.BadRequest,
ApiResponse.error<AltersklasseDto>("Invalid geschlecht filter. Must be 'M' or 'W'")
)
}
}
val oetoRegelReferenzId = updateDto.oetoRegelReferenzId?.let {
try {
uuidFrom(it)
} catch (e: Exception) {
return@put call.respond(
HttpStatusCode.BadRequest,
ApiResponse.error<AltersklasseDto>("Invalid OETO regel referenz ID format")
)
}
}
val request = CreateAltersklasseUseCase.UpdateAltersklasseRequest(
altersklasseId = altersklasseId,
altersklasseCode = updateDto.altersklasseCode,
bezeichnung = updateDto.bezeichnung,
minAlter = updateDto.minAlter,
maxAlter = updateDto.maxAlter,
stichtagRegelText = updateDto.stichtagRegelText,
sparteFilter = sparteFilter,
geschlechtFilter = geschlechtFilter,
oetoRegelReferenzId = oetoRegelReferenzId,
istAktiv = updateDto.istAktiv
)
val result = createAltersklasseUseCase.updateAltersklasse(request)
if (result.success) {
call.respond(HttpStatusCode.OK, ApiResponse.success(result.altersklasse!!.toDto()))
} else {
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<AltersklasseDto>("Validation failed: ${result.errors.joinToString(", ")}"))
}
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<AltersklasseDto>("Failed to update age class: ${e.message}"))
}
}
// DELETE /api/masterdata/altersklassen/{id} - Delete age class
delete("/{id}") {
try {
val altersklasseId = call.parameters["id"]?.let { uuidFrom(it) }
?: return@delete call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Unit>("Invalid age class ID"))
val result = createAltersklasseUseCase.deleteAltersklasse(altersklasseId)
if (result.success) {
call.respond(HttpStatusCode.NoContent)
} else {
call.respond(HttpStatusCode.NotFound, ApiResponse.error<Unit>("Age class not found: ${result.errors.joinToString(", ")}"))
}
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<Unit>("Failed to delete age class: ${e.message}"))
}
}
// GET /api/masterdata/altersklassen/eligible/{id} - Check eligibility for age class
get("/eligible/{id}") {
try {
val altersklasseId = call.parameters["id"]?.let { uuidFrom(it) }
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Boolean>("Invalid age class ID"))
val ageParam = call.request.queryParameters["age"]?.toIntOrNull()
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Boolean>("Age parameter is required"))
val geschlechtParam = call.request.queryParameters["geschlecht"]
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Boolean>("Gender parameter is required"))
if (geschlechtParam.length != 1 || (geschlechtParam != "M" && geschlechtParam != "W")) {
return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Boolean>("Gender must be 'M' or 'W'"))
}
val isEligible = getAltersklasseUseCase.isEligible(altersklasseId, ageParam, geschlechtParam[0])
call.respond(HttpStatusCode.OK, ApiResponse.success(isEligible))
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<Boolean>("Failed to check eligibility: ${e.message}"))
}
}
}
}
/**
* Extension function to convert AltersklasseDefinition domain object to AltersklasseDto.
*/
private fun AltersklasseDefinition.toDto(): AltersklasseDto {
return AltersklasseDto(
altersklasseId = this.altersklasseId.toString(),
altersklasseCode = this.altersklasseCode,
bezeichnung = this.bezeichnung,
minAlter = this.minAlter,
maxAlter = this.maxAlter,
stichtagRegelText = this.stichtagRegelText,
sparteFilter = this.sparteFilter?.name,
geschlechtFilter = this.geschlechtFilter?.toString(),
oetoRegelReferenzId = this.oetoRegelReferenzId?.toString(),
istAktiv = this.istAktiv,
createdAt = this.createdAt.toString(),
updatedAt = this.updatedAt.toString()
)
}
}
@@ -0,0 +1,368 @@
package at.mocode.masterdata.api.rest
import at.mocode.core.domain.model.ApiResponse
import at.mocode.masterdata.application.usecase.CreateBundeslandUseCase
import at.mocode.masterdata.application.usecase.GetBundeslandUseCase
import at.mocode.masterdata.domain.model.BundeslandDefinition
import at.mocode.core.utils.validation.ApiValidationUtils
import com.benasher44.uuid.uuidFrom
import io.ktor.http.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import kotlinx.serialization.Serializable
/**
* REST API controller for federal state management operations.
*
* This controller provides HTTP endpoints for the master-data context's
* federal state functionality, following REST conventions and proper error handling.
*/
class BundeslandController(
private val getBundeslandUseCase: GetBundeslandUseCase,
private val createBundeslandUseCase: CreateBundeslandUseCase
) {
/**
* DTO for federal state API responses.
*/
@Serializable
data class BundeslandDto(
val bundeslandId: String,
val landId: String,
val oepsCode: String? = null,
val iso3166_2_Code: String? = null,
val name: String,
val kuerzel: String? = null,
val wappenUrl: String? = null,
val istAktiv: Boolean = true,
val sortierReihenfolge: Int? = null,
val createdAt: String,
val updatedAt: String
)
/**
* DTO for creating a new federal state.
*/
@Serializable
data class CreateBundeslandDto(
val landId: String,
val oepsCode: String? = null,
val iso3166_2_Code: String? = null,
val name: String,
val kuerzel: String? = null,
val wappenUrl: String? = null,
val istAktiv: Boolean = true,
val sortierReihenfolge: Int? = null
)
/**
* DTO for updating an existing federal state.
*/
@Serializable
data class UpdateBundeslandDto(
val landId: String,
val oepsCode: String? = null,
val iso3166_2_Code: String? = null,
val name: String,
val kuerzel: String? = null,
val wappenUrl: String? = null,
val istAktiv: Boolean = true,
val sortierReihenfolge: Int? = null
)
/**
* Configures the routing for federal state endpoints.
*/
fun configureRouting(routing: Routing) {
routing.route("/api/masterdata/bundeslaender") {
// GET /api/masterdata/bundeslaender - Get all active federal states
get {
try {
val orderBySortierungParam = call.request.queryParameters["orderBySortierung"]
val orderBySortierung = if (orderBySortierungParam != null) {
try {
orderBySortierungParam.toBoolean()
} catch (_: Exception) {
return@get call.respond(
HttpStatusCode.BadRequest,
ApiResponse.error<List<BundeslandDto>>("Invalid orderBySortierung parameter. Must be true or false")
)
}
} else {
true
}
val bundeslaender = getBundeslandUseCase.getAllActive(orderBySortierung)
val bundeslandDtos = bundeslaender.map { it.toDto() }
call.respond(HttpStatusCode.OK, ApiResponse.success(bundeslandDtos))
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<List<BundeslandDto>>("Failed to retrieve federal states: ${e.message}"))
}
}
// GET /api/masterdata/bundeslaender/{id} - Get federal state by ID
get("/{id}") {
try {
val bundeslandId = call.parameters["id"]?.let { uuidFrom(it) }
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<BundeslandDto>("Invalid federal state ID"))
val bundesland = getBundeslandUseCase.getById(bundeslandId)
if (bundesland != null) {
call.respond(HttpStatusCode.OK, ApiResponse.success(bundesland.toDto()))
} else {
call.respond(HttpStatusCode.NotFound, ApiResponse.error<BundeslandDto>("Federal state not found"))
}
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<BundeslandDto>("Failed to retrieve federal state: ${e.message}"))
}
}
// GET /api/masterdata/bundeslaender/oeps/{code} - Get federal state by OEPS code
get("/oeps/{code}") {
try {
val oepsCode = call.parameters["code"]
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<BundeslandDto>("OEPS code is required"))
val landIdParam = call.request.queryParameters["landId"]
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<BundeslandDto>("Country ID (landId) is required"))
val landId = try {
uuidFrom(landIdParam)
} catch (e: Exception) {
return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<BundeslandDto>("Invalid country ID format"))
}
val bundesland = getBundeslandUseCase.getByOepsCode(oepsCode, landId)
if (bundesland != null) {
call.respond(HttpStatusCode.OK, ApiResponse.success(bundesland.toDto()))
} else {
call.respond(HttpStatusCode.NotFound, ApiResponse.error<BundeslandDto>("Federal state not found"))
}
} catch (e: IllegalArgumentException) {
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<BundeslandDto>(e.message ?: "Invalid OEPS code"))
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<BundeslandDto>("Failed to retrieve federal state: ${e.message}"))
}
}
// GET /api/masterdata/bundeslaender/iso/{code} - Get federal state by ISO 3166-2 code
get("/iso/{code}") {
try {
val isoCode = call.parameters["code"]
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<BundeslandDto>("ISO 3166-2 code is required"))
val bundesland = getBundeslandUseCase.getByIso3166_2_Code(isoCode)
if (bundesland != null) {
call.respond(HttpStatusCode.OK, ApiResponse.success(bundesland.toDto()))
} else {
call.respond(HttpStatusCode.NotFound, ApiResponse.error<BundeslandDto>("Federal state not found"))
}
} catch (e: IllegalArgumentException) {
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<BundeslandDto>(e.message ?: "Invalid ISO code"))
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<BundeslandDto>("Failed to retrieve federal state: ${e.message}"))
}
}
// GET /api/masterdata/bundeslaender/country/{countryId} - Get federal states by country
get("/country/{countryId}") {
try {
val landId = call.parameters["countryId"]?.let { uuidFrom(it) }
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<List<BundeslandDto>>("Invalid country ID"))
val activeOnlyParam = call.request.queryParameters["activeOnly"]
val activeOnly = activeOnlyParam?.toBoolean() ?: true
val orderBySortierungParam = call.request.queryParameters["orderBySortierung"]
val orderBySortierung = orderBySortierungParam?.toBoolean() ?: true
val bundeslaender = getBundeslandUseCase.getByCountry(landId, activeOnly, orderBySortierung)
val bundeslandDtos = bundeslaender.map { it.toDto() }
call.respond(HttpStatusCode.OK, ApiResponse.success(bundeslandDtos))
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<List<BundeslandDto>>("Failed to retrieve federal states: ${e.message}"))
}
}
// GET /api/masterdata/bundeslaender/search - Search federal states by name
get("/search") {
try {
val validationErrors = ApiValidationUtils.validateQueryParameters(
limit = call.request.queryParameters["limit"],
q = call.request.queryParameters["q"]
)
if (!ApiValidationUtils.isValid(validationErrors)) {
call.respond(
HttpStatusCode.BadRequest,
ApiResponse.error<List<BundeslandDto>>(ApiValidationUtils.createErrorMessage(validationErrors))
)
return@get
}
val searchTerm = call.request.queryParameters["q"]
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<List<BundeslandDto>>("Search term 'q' is required"))
val limit = call.request.queryParameters["limit"]?.toIntOrNull() ?: 50
val landIdParam = call.request.queryParameters["landId"]
val landId = landIdParam?.let { uuidFrom(it) }
val bundeslaender = getBundeslandUseCase.searchByName(searchTerm, landId, limit)
val bundeslandDtos = bundeslaender.map { it.toDto() }
call.respond(HttpStatusCode.OK, ApiResponse.success(bundeslandDtos))
} catch (e: IllegalArgumentException) {
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<List<BundeslandDto>>(e.message ?: "Invalid search parameters"))
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<List<BundeslandDto>>("Failed to search federal states: ${e.message}"))
}
}
// POST /api/masterdata/bundeslaender - Create new federal state
post {
try {
val createDto = call.receive<CreateBundeslandDto>()
// Basic validation
if (createDto.name.isBlank()) {
call.respond(
HttpStatusCode.BadRequest,
ApiResponse.error<BundeslandDto>("Name is required")
)
return@post
}
try {
uuidFrom(createDto.landId)
} catch (e: Exception) {
call.respond(
HttpStatusCode.BadRequest,
ApiResponse.error<BundeslandDto>("Invalid country ID format")
)
return@post
}
val request = CreateBundeslandUseCase.CreateBundeslandRequest(
landId = uuidFrom(createDto.landId),
oepsCode = createDto.oepsCode,
iso3166_2_Code = createDto.iso3166_2_Code,
name = createDto.name,
kuerzel = createDto.kuerzel,
wappenUrl = createDto.wappenUrl,
istAktiv = createDto.istAktiv,
sortierReihenfolge = createDto.sortierReihenfolge
)
val result = createBundeslandUseCase.createBundesland(request)
if (result.success) {
call.respond(HttpStatusCode.Created, ApiResponse.success(result.bundesland!!.toDto()))
} else {
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<BundeslandDto>("Validation failed: ${result.errors.joinToString(", ")}"))
}
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<BundeslandDto>("Failed to create federal state: ${e.message}"))
}
}
// PUT /api/masterdata/bundeslaender/{id} - Update existing federal state
put("/{id}") {
try {
val bundeslandId = call.parameters["id"]?.let { uuidFrom(it) }
?: return@put call.respond(HttpStatusCode.BadRequest, ApiResponse.error<BundeslandDto>("Invalid federal state ID"))
val updateDto = call.receive<UpdateBundeslandDto>()
// Basic validation
if (updateDto.name.isBlank()) {
call.respond(
HttpStatusCode.BadRequest,
ApiResponse.error<BundeslandDto>("Name is required")
)
return@put
}
try {
uuidFrom(updateDto.landId)
} catch (e: Exception) {
call.respond(
HttpStatusCode.BadRequest,
ApiResponse.error<BundeslandDto>("Invalid country ID format")
)
return@put
}
val request = CreateBundeslandUseCase.UpdateBundeslandRequest(
bundeslandId = bundeslandId,
landId = uuidFrom(updateDto.landId),
oepsCode = updateDto.oepsCode,
iso3166_2_Code = updateDto.iso3166_2_Code,
name = updateDto.name,
kuerzel = updateDto.kuerzel,
wappenUrl = updateDto.wappenUrl,
istAktiv = updateDto.istAktiv,
sortierReihenfolge = updateDto.sortierReihenfolge
)
val result = createBundeslandUseCase.updateBundesland(request)
if (result.success) {
call.respond(HttpStatusCode.OK, ApiResponse.success(result.bundesland!!.toDto()))
} else {
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<BundeslandDto>("Validation failed: ${result.errors.joinToString(", ")}"))
}
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<BundeslandDto>("Failed to update federal state: ${e.message}"))
}
}
// DELETE /api/masterdata/bundeslaender/{id} - Delete federal state
delete("/{id}") {
try {
val bundeslandId = call.parameters["id"]?.let { uuidFrom(it) }
?: return@delete call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Unit>("Invalid federal state ID"))
val result = createBundeslandUseCase.deleteBundesland(bundeslandId)
if (result.success) {
call.respond(HttpStatusCode.NoContent)
} else {
call.respond(HttpStatusCode.NotFound, ApiResponse.error<Unit>("Federal state not found: ${result.errors.joinToString(", ")}"))
}
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<Unit>("Failed to delete federal state: ${e.message}"))
}
}
// GET /api/masterdata/bundeslaender/count/{countryId} - Count active federal states by country
get("/count/{countryId}") {
try {
val landId = call.parameters["countryId"]?.let { uuidFrom(it) }
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Long>("Invalid country ID"))
val count = getBundeslandUseCase.countActiveByCountry(landId)
call.respond(HttpStatusCode.OK, ApiResponse.success(count))
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<Long>("Failed to count federal states: ${e.message}"))
}
}
}
}
/**
* Extension function to convert BundeslandDefinition domain object to BundeslandDto.
*/
private fun BundeslandDefinition.toDto(): BundeslandDto {
return BundeslandDto(
bundeslandId = this.bundeslandId.toString(),
landId = this.landId.toString(),
oepsCode = this.oepsCode,
iso3166_2_Code = this.iso3166_2_Code,
name = this.name,
kuerzel = this.kuerzel,
wappenUrl = this.wappenUrl,
istAktiv = this.istAktiv,
sortierReihenfolge = this.sortierReihenfolge,
createdAt = this.createdAt.toString(),
updatedAt = this.updatedAt.toString()
)
}
}
@@ -0,0 +1,474 @@
package at.mocode.masterdata.api.rest
import at.mocode.core.domain.model.ApiResponse
import at.mocode.core.domain.model.PlatzTypE
import at.mocode.masterdata.application.usecase.CreatePlatzUseCase
import at.mocode.masterdata.application.usecase.GetPlatzUseCase
import at.mocode.masterdata.domain.model.Platz
import at.mocode.core.utils.validation.ApiValidationUtils
import com.benasher44.uuid.uuidFrom
import io.ktor.http.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import kotlinx.serialization.Serializable
/**
* REST API controller for venue/arena management operations.
*
* This controller provides HTTP endpoints for the master-data context's
* venue functionality, following REST conventions and proper error handling.
*/
class PlatzController(
private val getPlatzUseCase: GetPlatzUseCase,
private val createPlatzUseCase: CreatePlatzUseCase
) {
/**
* DTO for venue API responses.
*/
@Serializable
data class PlatzDto(
val id: String,
val turnierId: String,
val name: String,
val dimension: String? = null,
val boden: String? = null,
val typ: String,
val istAktiv: Boolean = true,
val sortierReihenfolge: Int? = null,
val createdAt: String,
val updatedAt: String
)
/**
* DTO for creating a new venue.
*/
@Serializable
data class CreatePlatzDto(
val turnierId: String,
val name: String,
val dimension: String? = null,
val boden: String? = null,
val typ: String,
val istAktiv: Boolean = true,
val sortierReihenfolge: Int? = null
)
/**
* DTO for updating an existing venue.
*/
@Serializable
data class UpdatePlatzDto(
val turnierId: String,
val name: String,
val dimension: String? = null,
val boden: String? = null,
val typ: String,
val istAktiv: Boolean = true,
val sortierReihenfolge: Int? = null
)
/**
* Configures the routing for venue endpoints.
*/
fun configureRouting(routing: Routing) {
routing.route("/api/masterdata/plaetze") {
// GET /api/masterdata/plaetze/{id} - Get venue by ID
get("/{id}") {
try {
val platzId = call.parameters["id"]?.let { uuidFrom(it) }
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<PlatzDto>("Invalid venue ID"))
val platz = getPlatzUseCase.getById(platzId)
if (platz != null) {
call.respond(HttpStatusCode.OK, ApiResponse.success(platz.toDto()))
} else {
call.respond(HttpStatusCode.NotFound, ApiResponse.error<PlatzDto>("Venue not found"))
}
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<PlatzDto>("Failed to retrieve venue: ${e.message}"))
}
}
// GET /api/masterdata/plaetze/tournament/{turnierId} - Get venues by tournament
get("/tournament/{turnierId}") {
try {
val turnierId = call.parameters["turnierId"]?.let { uuidFrom(it) }
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<List<PlatzDto>>("Invalid tournament ID"))
val activeOnlyParam = call.request.queryParameters["activeOnly"]
val activeOnly = activeOnlyParam?.toBoolean() ?: true
val orderBySortierungParam = call.request.queryParameters["orderBySortierung"]
val orderBySortierung = orderBySortierungParam?.toBoolean() ?: true
val plaetze = getPlatzUseCase.getByTournament(turnierId, activeOnly, orderBySortierung)
val platzDtos = plaetze.map { it.toDto() }
call.respond(HttpStatusCode.OK, ApiResponse.success(platzDtos))
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<List<PlatzDto>>("Failed to retrieve venues: ${e.message}"))
}
}
// GET /api/masterdata/plaetze/search - Search venues by name
get("/search") {
try {
val validationErrors = ApiValidationUtils.validateQueryParameters(
limit = call.request.queryParameters["limit"],
q = call.request.queryParameters["q"]
)
if (!ApiValidationUtils.isValid(validationErrors)) {
call.respond(
HttpStatusCode.BadRequest,
ApiResponse.error<List<PlatzDto>>(ApiValidationUtils.createErrorMessage(validationErrors))
)
return@get
}
val searchTerm = call.request.queryParameters["q"]
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<List<PlatzDto>>("Search term 'q' is required"))
val limit = call.request.queryParameters["limit"]?.toIntOrNull() ?: 50
val turnierIdParam = call.request.queryParameters["turnierId"]
val turnierId = turnierIdParam?.let { uuidFrom(it) }
val plaetze = getPlatzUseCase.searchByName(searchTerm, turnierId, limit)
val platzDtos = plaetze.map { it.toDto() }
call.respond(HttpStatusCode.OK, ApiResponse.success(platzDtos))
} catch (e: IllegalArgumentException) {
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<List<PlatzDto>>(e.message ?: "Invalid search parameters"))
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<List<PlatzDto>>("Failed to search venues: ${e.message}"))
}
}
// GET /api/masterdata/plaetze/type/{typ} - Get venues by type
get("/type/{typ}") {
try {
val typParam = call.parameters["typ"]
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<List<PlatzDto>>("Venue type is required"))
val typ = try {
PlatzTypE.valueOf(typParam.uppercase())
} catch (e: Exception) {
return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<List<PlatzDto>>("Invalid venue type: $typParam"))
}
val turnierIdParam = call.request.queryParameters["turnierId"]
val turnierId = turnierIdParam?.let { uuidFrom(it) }
val activeOnlyParam = call.request.queryParameters["activeOnly"]
val activeOnly = activeOnlyParam?.toBoolean() ?: true
val plaetze = getPlatzUseCase.getByType(typ, turnierId, activeOnly)
val platzDtos = plaetze.map { it.toDto() }
call.respond(HttpStatusCode.OK, ApiResponse.success(platzDtos))
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<List<PlatzDto>>("Failed to retrieve venues: ${e.message}"))
}
}
// GET /api/masterdata/plaetze/ground/{boden} - Get venues by ground type
get("/ground/{boden}") {
try {
val boden = call.parameters["boden"]
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<List<PlatzDto>>("Ground type is required"))
val turnierIdParam = call.request.queryParameters["turnierId"]
val turnierId = turnierIdParam?.let { uuidFrom(it) }
val activeOnlyParam = call.request.queryParameters["activeOnly"]
val activeOnly = activeOnlyParam?.toBoolean() ?: true
val plaetze = getPlatzUseCase.getByGroundType(boden, turnierId, activeOnly)
val platzDtos = plaetze.map { it.toDto() }
call.respond(HttpStatusCode.OK, ApiResponse.success(platzDtos))
} catch (e: IllegalArgumentException) {
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<List<PlatzDto>>(e.message ?: "Invalid ground type"))
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<List<PlatzDto>>("Failed to retrieve venues: ${e.message}"))
}
}
// GET /api/masterdata/plaetze/dimension/{dimension} - Get venues by dimensions
get("/dimension/{dimension}") {
try {
val dimension = call.parameters["dimension"]
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<List<PlatzDto>>("Dimension is required"))
val turnierIdParam = call.request.queryParameters["turnierId"]
val turnierId = turnierIdParam?.let { uuidFrom(it) }
val activeOnlyParam = call.request.queryParameters["activeOnly"]
val activeOnly = activeOnlyParam?.toBoolean() ?: true
val plaetze = getPlatzUseCase.getByDimensions(dimension, turnierId, activeOnly)
val platzDtos = plaetze.map { it.toDto() }
call.respond(HttpStatusCode.OK, ApiResponse.success(platzDtos))
} catch (e: IllegalArgumentException) {
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<List<PlatzDto>>(e.message ?: "Invalid dimension"))
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<List<PlatzDto>>("Failed to retrieve venues: ${e.message}"))
}
}
// GET /api/masterdata/plaetze/suitable - Get venues suitable for discipline
get("/suitable") {
try {
val typParam = call.request.queryParameters["typ"]
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<List<PlatzDto>>("Required venue type parameter is missing"))
val requiredType = try {
PlatzTypE.valueOf(typParam.uppercase())
} catch (e: Exception) {
return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<List<PlatzDto>>("Invalid venue type: $typParam"))
}
val requiredDimensions = call.request.queryParameters["dimension"]
val turnierIdParam = call.request.queryParameters["turnierId"]
val turnierId = turnierIdParam?.let { uuidFrom(it) }
val plaetze = getPlatzUseCase.getSuitableForDiscipline(requiredType, requiredDimensions, turnierId)
val platzDtos = plaetze.map { it.toDto() }
call.respond(HttpStatusCode.OK, ApiResponse.success(platzDtos))
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<List<PlatzDto>>("Failed to retrieve suitable venues: ${e.message}"))
}
}
// POST /api/masterdata/plaetze - Create new venue
post {
try {
val createDto = call.receive<CreatePlatzDto>()
// Basic validation
if (createDto.name.isBlank()) {
call.respond(
HttpStatusCode.BadRequest,
ApiResponse.error<PlatzDto>("Name is required")
)
return@post
}
val turnierId = try {
uuidFrom(createDto.turnierId)
} catch (e: Exception) {
return@post call.respond(
HttpStatusCode.BadRequest,
ApiResponse.error<PlatzDto>("Invalid tournament ID format")
)
}
val typ = try {
PlatzTypE.valueOf(createDto.typ.uppercase())
} catch (e: Exception) {
return@post call.respond(
HttpStatusCode.BadRequest,
ApiResponse.error<PlatzDto>("Invalid venue type: ${createDto.typ}")
)
}
val request = CreatePlatzUseCase.CreatePlatzRequest(
turnierId = turnierId,
name = createDto.name,
dimension = createDto.dimension,
boden = createDto.boden,
typ = typ,
istAktiv = createDto.istAktiv,
sortierReihenfolge = createDto.sortierReihenfolge
)
val result = createPlatzUseCase.createPlatz(request)
if (result.success) {
call.respond(HttpStatusCode.Created, ApiResponse.success(result.platz!!.toDto()))
} else {
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<PlatzDto>("Validation failed: ${result.errors.joinToString(", ")}"))
}
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<PlatzDto>("Failed to create venue: ${e.message}"))
}
}
// PUT /api/masterdata/plaetze/{id} - Update existing venue
put("/{id}") {
try {
val platzId = call.parameters["id"]?.let { uuidFrom(it) }
?: return@put call.respond(HttpStatusCode.BadRequest, ApiResponse.error<PlatzDto>("Invalid venue ID"))
val updateDto = call.receive<UpdatePlatzDto>()
// Basic validation
if (updateDto.name.isBlank()) {
call.respond(
HttpStatusCode.BadRequest,
ApiResponse.error<PlatzDto>("Name is required")
)
return@put
}
val turnierId = try {
uuidFrom(updateDto.turnierId)
} catch (e: Exception) {
return@put call.respond(
HttpStatusCode.BadRequest,
ApiResponse.error<PlatzDto>("Invalid tournament ID format")
)
}
val typ = try {
PlatzTypE.valueOf(updateDto.typ.uppercase())
} catch (e: Exception) {
return@put call.respond(
HttpStatusCode.BadRequest,
ApiResponse.error<PlatzDto>("Invalid venue type: ${updateDto.typ}")
)
}
val request = CreatePlatzUseCase.UpdatePlatzRequest(
platzId = platzId,
turnierId = turnierId,
name = updateDto.name,
dimension = updateDto.dimension,
boden = updateDto.boden,
typ = typ,
istAktiv = updateDto.istAktiv,
sortierReihenfolge = updateDto.sortierReihenfolge
)
val result = createPlatzUseCase.updatePlatz(request)
if (result.success) {
call.respond(HttpStatusCode.OK, ApiResponse.success(result.platz!!.toDto()))
} else {
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<PlatzDto>("Validation failed: ${result.errors.joinToString(", ")}"))
}
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<PlatzDto>("Failed to update venue: ${e.message}"))
}
}
// DELETE /api/masterdata/plaetze/{id} - Delete venue
delete("/{id}") {
try {
val platzId = call.parameters["id"]?.let { uuidFrom(it) }
?: return@delete call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Unit>("Invalid venue ID"))
val result = createPlatzUseCase.deletePlatz(platzId)
if (result.success) {
call.respond(HttpStatusCode.NoContent)
} else {
call.respond(HttpStatusCode.NotFound, ApiResponse.error<Unit>("Venue not found: ${result.errors.joinToString(", ")}"))
}
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<Unit>("Failed to delete venue: ${e.message}"))
}
}
// GET /api/masterdata/plaetze/count/tournament/{turnierId} - Count venues by tournament
get("/count/tournament/{turnierId}") {
try {
val turnierId = call.parameters["turnierId"]?.let { uuidFrom(it) }
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Long>("Invalid tournament ID"))
val count = getPlatzUseCase.countActiveByTournament(turnierId)
call.respond(HttpStatusCode.OK, ApiResponse.success(count))
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<Long>("Failed to count venues: ${e.message}"))
}
}
// GET /api/masterdata/plaetze/count/type/{typ}/tournament/{turnierId} - Count venues by type and tournament
get("/count/type/{typ}/tournament/{turnierId}") {
try {
val typParam = call.parameters["typ"]
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Long>("Venue type is required"))
val typ = try {
PlatzTypE.valueOf(typParam.uppercase())
} catch (e: Exception) {
return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Long>("Invalid venue type: $typParam"))
}
val turnierId = call.parameters["turnierId"]?.let { uuidFrom(it) }
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Long>("Invalid tournament ID"))
val activeOnlyParam = call.request.queryParameters["activeOnly"]
val activeOnly = activeOnlyParam?.toBoolean() ?: true
val count = getPlatzUseCase.countByTypeAndTournament(typ, turnierId, activeOnly)
call.respond(HttpStatusCode.OK, ApiResponse.success(count))
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<Long>("Failed to count venues: ${e.message}"))
}
}
// GET /api/masterdata/plaetze/grouped/tournament/{turnierId} - Get venues grouped by type
get("/grouped/tournament/{turnierId}") {
try {
val turnierId = call.parameters["turnierId"]?.let { uuidFrom(it) }
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Map<String, List<PlatzDto>>>("Invalid tournament ID"))
val activeOnlyParam = call.request.queryParameters["activeOnly"]
val activeOnly = activeOnlyParam?.toBoolean() ?: true
val groupedVenues = getPlatzUseCase.getGroupedByTypeForTournament(turnierId, activeOnly)
val groupedDtos = groupedVenues.mapKeys { it.key.name }.mapValues { entry ->
entry.value.map { it.toDto() }
}
call.respond(HttpStatusCode.OK, ApiResponse.success(groupedDtos))
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<Map<String, List<PlatzDto>>>("Failed to retrieve grouped venues: ${e.message}"))
}
}
// GET /api/masterdata/plaetze/validate/{id} - Validate venue suitability
get("/validate/{id}") {
try {
val platzId = call.parameters["id"]?.let { uuidFrom(it) }
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Map<String, Any>>("Invalid venue ID"))
val requiredTypeParam = call.request.queryParameters["requiredType"]
val requiredType = requiredTypeParam?.let {
try {
PlatzTypE.valueOf(it.uppercase())
} catch (e: Exception) {
return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Map<String, Any>>("Invalid required type: $it"))
}
}
val requiredDimensions = call.request.queryParameters["requiredDimensions"]
val requiredGroundType = call.request.queryParameters["requiredGroundType"]
val (isValid, reasons) = getPlatzUseCase.validateVenueSuitability(platzId, requiredType, requiredDimensions, requiredGroundType)
val response = mapOf(
"isValid" to isValid,
"reasons" to reasons
)
call.respond(HttpStatusCode.OK, ApiResponse.success(response))
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<Map<String, Any>>("Failed to validate venue: ${e.message}"))
}
}
}
}
/**
* Extension function to convert Platz domain object to PlatzDto.
*/
private fun Platz.toDto(): PlatzDto {
return PlatzDto(
id = this.id.toString(),
turnierId = this.turnierId.toString(),
name = this.name,
dimension = this.dimension,
boden = this.boden,
typ = this.typ.name,
istAktiv = this.istAktiv,
sortierReihenfolge = this.sortierReihenfolge,
createdAt = this.createdAt.toString(),
updatedAt = this.updatedAt.toString()
)
}
}