fixing web-app
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
plugins {
|
||||
// KORREKTUR: Alle Plugins werden jetzt konsistent über den Version Catalog geladen.
|
||||
alias(libs.plugins.kotlin.jvm)
|
||||
alias(libs.plugins.kotlin.spring)
|
||||
alias(libs.plugins.kotlin.serialization)
|
||||
|
||||
// Das Ktor-Plugin wird hier nicht benötigt, da Ktor als Bibliothek in Spring Boot läuft.
|
||||
// Das 'application'-Plugin wird vom Spring Boot Plugin bereitgestellt.
|
||||
alias(libs.plugins.spring.boot)
|
||||
alias(libs.plugins.spring.dependencyManagement)
|
||||
}
|
||||
|
||||
// Der springBoot-Block konfiguriert die Anwendung, wenn sie als JAR-Datei ausgeführt wird.
|
||||
springBoot {
|
||||
mainClass.set("at.mocode.horses.api.ApplicationKt")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// Interne Module
|
||||
implementation(projects.platform.platformDependencies)
|
||||
implementation(projects.horses.horsesDomain)
|
||||
implementation(projects.horses.horsesApplication)
|
||||
implementation(projects.core.coreDomain)
|
||||
implementation(projects.core.coreUtils)
|
||||
|
||||
// KORREKTUR: Alle externen Abhängigkeiten werden jetzt über den Version Catalog bezogen.
|
||||
|
||||
// Spring dependencies
|
||||
implementation(libs.spring.web)
|
||||
|
||||
// Ktor Server (als embedded Server in Spring)
|
||||
implementation(libs.ktor.server.core)
|
||||
implementation(libs.ktor.server.netty)
|
||||
implementation(libs.ktor.server.contentNegotiation)
|
||||
implementation(libs.ktor.server.serialization.kotlinx.json)
|
||||
implementation(libs.ktor.server.statusPages)
|
||||
implementation(libs.ktor.server.auth)
|
||||
implementation(libs.ktor.server.authJwt)
|
||||
|
||||
// Testing
|
||||
testImplementation(projects.platform.platformTesting)
|
||||
testImplementation(libs.ktor.server.tests)
|
||||
testImplementation(libs.spring.boot.starter.test)
|
||||
}
|
||||
+438
@@ -0,0 +1,438 @@
|
||||
package at.mocode.horses.api.rest
|
||||
|
||||
import at.mocode.core.domain.model.ApiResponse
|
||||
import at.mocode.core.domain.model.PferdeGeschlechtE
|
||||
import at.mocode.horses.application.usecase.CreateHorseUseCase
|
||||
import at.mocode.horses.application.usecase.DeleteHorseUseCase
|
||||
import at.mocode.horses.application.usecase.GetHorseUseCase
|
||||
import at.mocode.horses.application.usecase.UpdateHorseUseCase
|
||||
import at.mocode.horses.domain.repository.HorseRepository
|
||||
import at.mocode.core.utils.validation.ApiValidationUtils
|
||||
import com.benasher44.uuid.Uuid
|
||||
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.Contextual
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* REST API controller for horse registry operations.
|
||||
*
|
||||
* This controller provides HTTP endpoints for all horse-related operations
|
||||
* following REST conventions and proper HTTP status codes.
|
||||
*/
|
||||
class HorseController(
|
||||
private val horseRepository: HorseRepository
|
||||
) {
|
||||
|
||||
private val getHorseUseCase = GetHorseUseCase(horseRepository)
|
||||
private val createHorseUseCase = CreateHorseUseCase(horseRepository)
|
||||
private val updateHorseUseCase = UpdateHorseUseCase(horseRepository)
|
||||
private val deleteHorseUseCase = DeleteHorseUseCase(horseRepository)
|
||||
|
||||
/**
|
||||
* Configures the horse-related routes.
|
||||
*/
|
||||
fun configureRoutes(routing: Routing) {
|
||||
routing.route("/api/horses") {
|
||||
|
||||
// GET /api/horses - Get all horses with optional filtering
|
||||
get {
|
||||
try {
|
||||
// Validate query parameters
|
||||
val validationErrors = ApiValidationUtils.validateQueryParameters(
|
||||
limit = call.request.queryParameters["limit"],
|
||||
search = call.request.queryParameters["search"]
|
||||
)
|
||||
|
||||
if (!ApiValidationUtils.isValid(validationErrors)) {
|
||||
call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
ApiResponse.error<Any>(ApiValidationUtils.createErrorMessage(validationErrors))
|
||||
)
|
||||
return@get
|
||||
}
|
||||
|
||||
val activeOnly = call.request.queryParameters["activeOnly"]?.toBoolean() ?: true
|
||||
val limit = call.request.queryParameters["limit"]?.toInt() ?: 100
|
||||
val ownerId = call.request.queryParameters["ownerId"]?.let {
|
||||
ApiValidationUtils.validateUuidString(it) ?: return@get call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
ApiResponse.error<Any>("Invalid ownerId format")
|
||||
)
|
||||
}
|
||||
val geschlecht = call.request.queryParameters["geschlecht"]?.let {
|
||||
try {
|
||||
PferdeGeschlechtE.valueOf(it)
|
||||
} catch (_: IllegalArgumentException) {
|
||||
return@get call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
ApiResponse.error<Any>("Invalid geschlecht value. Valid values: ${PferdeGeschlechtE.entries.joinToString(", ")}")
|
||||
)
|
||||
}
|
||||
}
|
||||
val rasse = call.request.queryParameters["rasse"]
|
||||
val searchTerm = call.request.queryParameters["search"]
|
||||
|
||||
val horses = when {
|
||||
searchTerm != null -> getHorseUseCase.searchByName(searchTerm, limit)
|
||||
ownerId != null -> getHorseUseCase.getByOwnerId(ownerId, activeOnly)
|
||||
geschlecht != null -> getHorseUseCase.getByGeschlecht(geschlecht, activeOnly, limit)
|
||||
rasse != null -> getHorseUseCase.getByRasse(rasse, activeOnly, limit)
|
||||
else -> getHorseUseCase.getAllActive(limit)
|
||||
}
|
||||
|
||||
call.respond(HttpStatusCode.OK, ApiResponse.success(horses))
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<Any>("Failed to retrieve horses: ${e.message}"))
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/horses/{id} - Get horse by ID
|
||||
get("/{id}") {
|
||||
try {
|
||||
val horseId = uuidFrom(call.parameters["id"]!!)
|
||||
val horse = getHorseUseCase.getById(horseId)
|
||||
|
||||
if (horse != null) {
|
||||
call.respond(HttpStatusCode.OK, ApiResponse.success(horse))
|
||||
} else {
|
||||
call.respond(HttpStatusCode.NotFound, ApiResponse.error<Any>("Horse not found"))
|
||||
}
|
||||
} catch (_: IllegalArgumentException) {
|
||||
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Any>("Invalid horse ID format"))
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<Any>("Failed to retrieve horse: ${e.message}"))
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/horses/search/lebensnummer/{nummer} - Find by life number
|
||||
get("/search/lebensnummer/{nummer}") {
|
||||
try {
|
||||
val lebensnummer = call.parameters["nummer"]!!
|
||||
val horse = getHorseUseCase.getByLebensnummer(lebensnummer)
|
||||
|
||||
if (horse != null) {
|
||||
call.respond(HttpStatusCode.OK, ApiResponse.success(horse))
|
||||
} else {
|
||||
call.respond(HttpStatusCode.NotFound, ApiResponse.error<Any>("Horse with life number '$lebensnummer' not found"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<Any>("Failed to search horse: ${e.message}"))
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/horses/search/chip/{nummer} - Find by chip number
|
||||
get("/search/chip/{nummer}") {
|
||||
try {
|
||||
val chipNummer = call.parameters["nummer"]!!
|
||||
val horse = getHorseUseCase.getByChipNummer(chipNummer)
|
||||
|
||||
if (horse != null) {
|
||||
call.respond(HttpStatusCode.OK, ApiResponse.success(horse))
|
||||
} else {
|
||||
call.respond(HttpStatusCode.NotFound, ApiResponse.error<Any>("Horse with chip number '$chipNummer' not found"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<Any>("Failed to search horse: ${e.message}"))
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/horses/search/passport/{nummer} - Find by passport number
|
||||
get("/search/passport/{nummer}") {
|
||||
try {
|
||||
val passNummer = call.parameters["nummer"]!!
|
||||
val horse = getHorseUseCase.getByPassNummer(passNummer)
|
||||
|
||||
if (horse != null) {
|
||||
call.respond(HttpStatusCode.OK, ApiResponse.success(horse))
|
||||
} else {
|
||||
call.respond(HttpStatusCode.NotFound, ApiResponse.error<Any>("Horse with passport number '$passNummer' not found"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<Any>("Failed to search horse: ${e.message}"))
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/horses/search/oeps/{nummer} - Find by OEPS number
|
||||
get("/search/oeps/{nummer}") {
|
||||
try {
|
||||
val oepsNummer = call.parameters["nummer"]!!
|
||||
val horse = getHorseUseCase.getByOepsNummer(oepsNummer)
|
||||
|
||||
if (horse != null) {
|
||||
call.respond(HttpStatusCode.OK, ApiResponse.success(horse))
|
||||
} else {
|
||||
call.respond(HttpStatusCode.NotFound, ApiResponse.error<Any>("Horse with OEPS number '$oepsNummer' not found"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<Any>("Failed to search horse: ${e.message}"))
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/horses/search/fei/{nummer} - Find by FEI number
|
||||
get("/search/fei/{nummer}") {
|
||||
try {
|
||||
val feiNummer = call.parameters["nummer"]!!
|
||||
val horse = getHorseUseCase.getByFeiNummer(feiNummer)
|
||||
|
||||
if (horse != null) {
|
||||
call.respond(HttpStatusCode.OK, ApiResponse.success(horse))
|
||||
} else {
|
||||
call.respond(HttpStatusCode.NotFound, ApiResponse.error<Any>("Horse with FEI number '$feiNummer' not found"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<Any>("Failed to search horse: ${e.message}"))
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/horses/oeps-registered - Get OEPS registered horses
|
||||
get("/oeps-registered") {
|
||||
try {
|
||||
val activeOnly = call.request.queryParameters["activeOnly"]?.toBoolean() ?: true
|
||||
val horses = getHorseUseCase.getOepsRegistered(activeOnly)
|
||||
call.respond(HttpStatusCode.OK, ApiResponse.success(horses))
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<Any>("Failed to retrieve OEPS horses: ${e.message}"))
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/horses/fei-registered - Get FEI registered horses
|
||||
get("/fei-registered") {
|
||||
try {
|
||||
val activeOnly = call.request.queryParameters["activeOnly"]?.toBoolean() ?: true
|
||||
val horses = getHorseUseCase.getFeiRegistered(activeOnly)
|
||||
call.respond(HttpStatusCode.OK, ApiResponse.success(horses))
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<Any>("Failed to retrieve FEI horses: ${e.message}"))
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/horses/stats - Get horse statistics
|
||||
get("/stats") {
|
||||
try {
|
||||
val activeCount = getHorseUseCase.countActive()
|
||||
val oepsCount = getHorseUseCase.countOepsRegistered(true)
|
||||
val feiCount = getHorseUseCase.countFeiRegistered(true)
|
||||
|
||||
val stats = HorseStats(
|
||||
totalActive = activeCount,
|
||||
oepsRegistered = oepsCount,
|
||||
feiRegistered = feiCount
|
||||
)
|
||||
|
||||
call.respond(HttpStatusCode.OK, ApiResponse.success(stats))
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<Any>("Failed to retrieve statistics: ${e.message}"))
|
||||
}
|
||||
}
|
||||
|
||||
// POST /api/horses - Create new horse
|
||||
post {
|
||||
try {
|
||||
val createRequest = call.receive<CreateHorseUseCase.CreateHorseRequest>()
|
||||
|
||||
// Validate input using shared validation utilities
|
||||
val validationErrors = ApiValidationUtils.validateHorseRequest(
|
||||
pferdeName = createRequest.pferdeName,
|
||||
lebensnummer = createRequest.lebensnummer,
|
||||
chipNummer = createRequest.chipNummer,
|
||||
oepsNummer = createRequest.oepsNummer,
|
||||
feiNummer = createRequest.feiNummer
|
||||
)
|
||||
|
||||
if (!ApiValidationUtils.isValid(validationErrors)) {
|
||||
call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
ApiResponse.error<Any>(ApiValidationUtils.createErrorMessage(validationErrors))
|
||||
)
|
||||
return@post
|
||||
}
|
||||
|
||||
val response = createHorseUseCase.execute(createRequest)
|
||||
|
||||
if (response.success) {
|
||||
call.respond(HttpStatusCode.Created, ApiResponse.success(response.data!!))
|
||||
} else {
|
||||
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Any>("Validation failed"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<Any>("Failed to create horse: ${e.message}"))
|
||||
}
|
||||
}
|
||||
|
||||
// PUT /api/horses/{id} - Update horse
|
||||
put("/{id}") {
|
||||
try {
|
||||
val horseId = uuidFrom(call.parameters["id"]!!)
|
||||
val updateData = call.receive<UpdateHorseRequest>()
|
||||
|
||||
// Validate input using shared validation utilities
|
||||
val validationErrors = ApiValidationUtils.validateHorseRequest(
|
||||
pferdeName = updateData.pferdeName,
|
||||
lebensnummer = updateData.lebensnummer,
|
||||
chipNummer = updateData.chipNummer,
|
||||
oepsNummer = updateData.oepsNummer,
|
||||
feiNummer = updateData.feiNummer
|
||||
)
|
||||
|
||||
if (!ApiValidationUtils.isValid(validationErrors)) {
|
||||
call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
ApiResponse.error<Any>(ApiValidationUtils.createErrorMessage(validationErrors))
|
||||
)
|
||||
return@put
|
||||
}
|
||||
|
||||
val updateRequest = UpdateHorseUseCase.UpdateHorseRequest(
|
||||
pferdId = horseId,
|
||||
pferdeName = updateData.pferdeName,
|
||||
geschlecht = updateData.geschlecht,
|
||||
geburtsdatum = updateData.geburtsdatum,
|
||||
rasse = updateData.rasse,
|
||||
farbe = updateData.farbe,
|
||||
besitzerId = updateData.besitzerId,
|
||||
verantwortlichePersonId = updateData.verantwortlichePersonId,
|
||||
zuechterName = updateData.zuechterName,
|
||||
zuchtbuchNummer = updateData.zuchtbuchNummer,
|
||||
lebensnummer = updateData.lebensnummer,
|
||||
chipNummer = updateData.chipNummer,
|
||||
passNummer = updateData.passNummer,
|
||||
oepsNummer = updateData.oepsNummer,
|
||||
feiNummer = updateData.feiNummer,
|
||||
vaterName = updateData.vaterName,
|
||||
mutterName = updateData.mutterName,
|
||||
mutterVaterName = updateData.mutterVaterName,
|
||||
stockmass = updateData.stockmass,
|
||||
istAktiv = updateData.istAktiv,
|
||||
bemerkungen = updateData.bemerkungen,
|
||||
datenQuelle = updateData.datenQuelle
|
||||
)
|
||||
|
||||
val response = updateHorseUseCase.execute(updateRequest)
|
||||
|
||||
if (response.success && response.horse != null) {
|
||||
call.respond(HttpStatusCode.OK, ApiResponse.success(response.horse))
|
||||
} else {
|
||||
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Any>("Update failed: ${response.errors.joinToString(", ")}"))
|
||||
}
|
||||
} catch (_: IllegalArgumentException) {
|
||||
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Any>("Invalid horse ID format"))
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<Any>("Failed to update horse: ${e.message}"))
|
||||
}
|
||||
}
|
||||
|
||||
// DELETE /api/horses/{id} - Delete horse
|
||||
delete("/{id}") {
|
||||
try {
|
||||
val horseId = uuidFrom(call.parameters["id"]!!)
|
||||
val forceDelete = call.request.queryParameters["force"]?.toBoolean() ?: false
|
||||
|
||||
val deleteRequest = DeleteHorseUseCase.DeleteHorseRequest(horseId, forceDelete)
|
||||
val response = deleteHorseUseCase.execute(deleteRequest)
|
||||
|
||||
if (response.success) {
|
||||
val message = if (response.warnings.isNotEmpty()) {
|
||||
"Horse deleted successfully. Warnings: ${response.warnings.joinToString(", ")}"
|
||||
} else {
|
||||
"Horse deleted successfully"
|
||||
}
|
||||
call.respond(HttpStatusCode.OK, ApiResponse.success(message))
|
||||
} else {
|
||||
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Any>("Delete failed: ${response.errors.joinToString(", ")}"))
|
||||
}
|
||||
} catch (_: IllegalArgumentException) {
|
||||
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Any>("Invalid horse ID format"))
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<Any>("Failed to delete horse: ${e.message}"))
|
||||
}
|
||||
}
|
||||
|
||||
// POST /api/horses/{id}/soft-delete - Soft delete horse (mark as inactive)
|
||||
post("/{id}/soft-delete") {
|
||||
try {
|
||||
val horseId = uuidFrom(call.parameters["id"]!!)
|
||||
val response = deleteHorseUseCase.softDelete(horseId)
|
||||
|
||||
if (response.success) {
|
||||
val message = if (response.warnings.isNotEmpty()) {
|
||||
"Horse marked as inactive. Warnings: ${response.warnings.joinToString(", ")}"
|
||||
} else {
|
||||
"Horse marked as inactive"
|
||||
}
|
||||
call.respond(HttpStatusCode.OK, ApiResponse.success(message))
|
||||
} else {
|
||||
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Any>("Soft delete failed: ${response.errors.joinToString(", ")}"))
|
||||
}
|
||||
} catch (_: IllegalArgumentException) {
|
||||
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Any>("Invalid horse ID format"))
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<Any>("Failed to soft delete horse: ${e.message}"))
|
||||
}
|
||||
}
|
||||
|
||||
// POST /api/horses/batch-delete - Batch delete multiple horses
|
||||
post("/batch-delete") {
|
||||
try {
|
||||
val batchRequest = call.receive<BatchDeleteRequest>()
|
||||
val response = deleteHorseUseCase.batchDelete(batchRequest.horseIds, batchRequest.forceDelete)
|
||||
|
||||
val statusCode = if (response.overallSuccess) HttpStatusCode.OK else HttpStatusCode.PartialContent
|
||||
call.respond(statusCode, ApiResponse.success(response))
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<Any>("Failed to batch delete horses: ${e.message}"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DTO for updating horse data via API.
|
||||
*/
|
||||
@Serializable
|
||||
data class UpdateHorseRequest(
|
||||
val pferdeName: String,
|
||||
val geschlecht: PferdeGeschlechtE,
|
||||
val geburtsdatum: kotlinx.datetime.LocalDate? = null,
|
||||
val rasse: String? = null,
|
||||
val farbe: String? = null,
|
||||
@Contextual val besitzerId: Uuid? = null,
|
||||
@Contextual val verantwortlichePersonId: Uuid? = null,
|
||||
val zuechterName: String? = null,
|
||||
val zuchtbuchNummer: String? = null,
|
||||
val lebensnummer: String? = null,
|
||||
val chipNummer: String? = null,
|
||||
val passNummer: String? = null,
|
||||
val oepsNummer: String? = null,
|
||||
val feiNummer: String? = null,
|
||||
val vaterName: String? = null,
|
||||
val mutterName: String? = null,
|
||||
val mutterVaterName: String? = null,
|
||||
val stockmass: Int? = null,
|
||||
val istAktiv: Boolean = true,
|
||||
val bemerkungen: String? = null,
|
||||
val datenQuelle: at.mocode.core.domain.model.DatenQuelleE = at.mocode.core.domain.model.DatenQuelleE.MANUELL
|
||||
)
|
||||
|
||||
/**
|
||||
* DTO for batch delete request.
|
||||
*/
|
||||
@Serializable
|
||||
data class BatchDeleteRequest(
|
||||
val horseIds: List<@Contextual Uuid>,
|
||||
val forceDelete: Boolean = false
|
||||
)
|
||||
|
||||
/**
|
||||
* DTO for horse statistics.
|
||||
*/
|
||||
@Serializable
|
||||
data class HorseStats(
|
||||
val totalActive: Long,
|
||||
val oepsRegistered: Long,
|
||||
val feiRegistered: Long
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user