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:
+463
@@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
+368
@@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
+474
@@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user