Register events modules in Gradle build and refactor VeranstaltungController: remove unused use cases, streamline request handling, and improve error responses.
This commit is contained in:
@@ -1,39 +1,32 @@
|
||||
plugins {
|
||||
alias(libs.plugins.kotlin.jvm)
|
||||
alias(libs.plugins.kotlin.spring)
|
||||
alias(libs.plugins.kotlin.serialization)
|
||||
alias(libs.plugins.ktor)
|
||||
application
|
||||
// KORREKTUR 1: Dieses Plugin hinzufügen, um die Spring-BOM zu aktivieren.
|
||||
alias(libs.plugins.spring.dependencyManagement)
|
||||
alias(libs.plugins.kotlinJvm)
|
||||
alias(libs.plugins.kotlinSpring)
|
||||
alias(libs.plugins.kotlinSerialization)
|
||||
alias(libs.plugins.ktor)
|
||||
application
|
||||
alias(libs.plugins.spring.dependencyManagement)
|
||||
}
|
||||
|
||||
application {
|
||||
mainClass.set("at.mocode.events.api.ApplicationKt")
|
||||
mainClass.set("at.mocode.events.api.ApplicationKt")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// KORREKTUR 2: Die Spring-Boot-BOM hier explizit als Plattform deklarieren.
|
||||
api(platform(libs.spring.boot.dependencies))
|
||||
// Bestehende Abhängigkeiten
|
||||
implementation(projects.platform.platformDependencies)
|
||||
implementation(projects.events.eventsDomain)
|
||||
implementation(projects.events.eventsApplication)
|
||||
implementation(projects.core.coreDomain)
|
||||
implementation(projects.core.coreUtils)
|
||||
// Spring dependencies (jetzt mit korrekter Version aus der BOM)
|
||||
implementation(libs.spring.web)
|
||||
implementation(libs.springdoc.openapi.starter.common)
|
||||
// Ktor Server
|
||||
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)
|
||||
api(platform(libs.spring.boot.dependencies))
|
||||
implementation(projects.platform.platformDependencies)
|
||||
implementation(projects.backend.services.events.eventsDomain)
|
||||
implementation(projects.core.coreDomain)
|
||||
implementation(projects.core.coreUtils)
|
||||
implementation(libs.spring.web)
|
||||
implementation(libs.springdoc.openapi.starter.common)
|
||||
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)
|
||||
|
||||
testImplementation(projects.platform.platformTesting)
|
||||
// Ktor 3.x Test-Host statt veraltetes tests-Artefakt
|
||||
testImplementation(libs.ktor.server.testHost)
|
||||
testImplementation(projects.platform.platformTesting)
|
||||
testImplementation(libs.ktor.server.testHost)
|
||||
}
|
||||
|
||||
+67
-138
@@ -1,15 +1,11 @@
|
||||
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
||||
package at.mocode.events.api.rest
|
||||
|
||||
import at.mocode.events.domain.model.Veranstaltung
|
||||
import at.mocode.core.domain.model.ApiResponse
|
||||
import at.mocode.core.domain.model.SparteE
|
||||
import at.mocode.events.application.usecase.CreateVeranstaltungUseCase
|
||||
import at.mocode.events.application.usecase.DeleteVeranstaltungUseCase
|
||||
import at.mocode.events.application.usecase.GetVeranstaltungUseCase
|
||||
import at.mocode.events.application.usecase.UpdateVeranstaltungUseCase
|
||||
import at.mocode.events.domain.repository.VeranstaltungRepository
|
||||
import at.mocode.core.domain.serialization.UuidSerializer
|
||||
import at.mocode.core.utils.validation.ApiValidationUtils
|
||||
import kotlin.uuid.Uuid
|
||||
import io.ktor.http.*
|
||||
import io.ktor.server.request.*
|
||||
@@ -28,11 +24,6 @@ class VeranstaltungController(
|
||||
private val veranstaltungRepository: VeranstaltungRepository
|
||||
) {
|
||||
|
||||
private val createVeranstaltungUseCase = CreateVeranstaltungUseCase(veranstaltungRepository)
|
||||
private val getVeranstaltungUseCase = GetVeranstaltungUseCase(veranstaltungRepository)
|
||||
private val updateVeranstaltungUseCase = UpdateVeranstaltungUseCase(veranstaltungRepository)
|
||||
private val deleteVeranstaltungUseCase = DeleteVeranstaltungUseCase(veranstaltungRepository)
|
||||
|
||||
/**
|
||||
* Configures the event-related routes.
|
||||
*/
|
||||
@@ -42,31 +33,17 @@ class VeranstaltungController(
|
||||
// GET /api/events - Get all events with optional filtering
|
||||
get {
|
||||
try {
|
||||
// Validate query parameters
|
||||
val validationErrors = ApiValidationUtils.validateQueryParameters(
|
||||
limit = call.request.queryParameters["limit"],
|
||||
offset = call.request.queryParameters["offset"],
|
||||
startDate = call.request.queryParameters["startDate"],
|
||||
endDate = call.request.queryParameters["endDate"],
|
||||
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 offset = call.request.queryParameters["offset"]?.toInt() ?: 0
|
||||
|
||||
val organizerId = call.request.queryParameters["organizerId"]?.let {
|
||||
ApiValidationUtils.validateUuidString(it) ?: return@get call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
ApiResponse.error<Any>("Invalid organizerId format")
|
||||
)
|
||||
try { Uuid.parse(it) } catch (e: Exception) {
|
||||
return@get call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
ApiResponse.error<Any>("INVALID_ORGANIZER_ID", "Invalid organizerId format")
|
||||
)
|
||||
}
|
||||
}
|
||||
val searchTerm = call.request.queryParameters["search"]
|
||||
val publicOnly = call.request.queryParameters["publicOnly"]?.toBoolean() ?: false
|
||||
@@ -84,7 +61,7 @@ class VeranstaltungController(
|
||||
|
||||
call.respond(HttpStatusCode.OK, ApiResponse.success(events))
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<Any>("Failed to retrieve events: ${e.message}"))
|
||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<Any>("INTERNAL_ERROR", "Failed to retrieve events: ${e.message}"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,18 +69,17 @@ class VeranstaltungController(
|
||||
get("/{id}") {
|
||||
try {
|
||||
val eventId = Uuid.parse(call.parameters["id"]!!)
|
||||
val request = GetVeranstaltungUseCase.GetVeranstaltungRequest(eventId)
|
||||
val response = getVeranstaltungUseCase.execute(request)
|
||||
val event = veranstaltungRepository.findById(eventId)
|
||||
|
||||
if (response.success && response.data != null) {
|
||||
call.respond(HttpStatusCode.OK, ApiResponse.success((response.data as GetVeranstaltungUseCase.GetVeranstaltungResponse).veranstaltung))
|
||||
if (event != null) {
|
||||
call.respond(HttpStatusCode.OK, ApiResponse.success(event))
|
||||
} else {
|
||||
call.respond(HttpStatusCode.NotFound, ApiResponse.error<Any>("Event not found"))
|
||||
call.respond(HttpStatusCode.NotFound, ApiResponse.error<Any>("NOT_FOUND", "Event not found"))
|
||||
}
|
||||
} catch (_: IllegalArgumentException) {
|
||||
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Any>("Invalid event ID format"))
|
||||
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Any>("INVALID_ID", "Invalid event ID format"))
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<Any>("Failed to retrieve event: ${e.message}"))
|
||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<Any>("INTERNAL_ERROR", "Failed to retrieve event: ${e.message}"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,7 +96,7 @@ class VeranstaltungController(
|
||||
|
||||
call.respond(HttpStatusCode.OK, ApiResponse.success(stats))
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<Any>("Failed to retrieve event statistics: ${e.message}"))
|
||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<Any>("INTERNAL_ERROR", "Failed to retrieve event statistics: ${e.message}"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,26 +105,12 @@ class VeranstaltungController(
|
||||
try {
|
||||
val createRequest = call.receive<CreateEventRequest>()
|
||||
|
||||
// Validate input using shared validation utilities
|
||||
val validationErrors = ApiValidationUtils.validateEventRequest(
|
||||
name = createRequest.name,
|
||||
ort = createRequest.ort,
|
||||
startDatum = createRequest.startDatum,
|
||||
endDatum = createRequest.endDatum,
|
||||
maxTeilnehmer = createRequest.maxTeilnehmer
|
||||
)
|
||||
|
||||
if (!ApiValidationUtils.isValid(validationErrors)) {
|
||||
call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
ApiResponse.error<Any>(ApiValidationUtils.createErrorMessage(validationErrors))
|
||||
)
|
||||
return@post
|
||||
}
|
||||
|
||||
val useCaseRequest = CreateVeranstaltungUseCase.CreateVeranstaltungRequest(
|
||||
val veranstaltung = Veranstaltung(
|
||||
name = createRequest.name,
|
||||
untertitel = createRequest.untertitel,
|
||||
beschreibung = createRequest.beschreibung,
|
||||
logoUrl = createRequest.logoUrl,
|
||||
sponsoren = createRequest.sponsoren,
|
||||
startDatum = createRequest.startDatum,
|
||||
endDatum = createRequest.endDatum,
|
||||
ort = createRequest.ort,
|
||||
@@ -160,20 +122,16 @@ class VeranstaltungController(
|
||||
anmeldeschluss = createRequest.anmeldeschluss
|
||||
)
|
||||
|
||||
val response = createVeranstaltungUseCase.execute(useCaseRequest)
|
||||
|
||||
if (response.success && response.data != null) {
|
||||
call.respond(HttpStatusCode.Created, ApiResponse.success((response.data as CreateVeranstaltungUseCase.CreateVeranstaltungResponse).veranstaltung))
|
||||
} else {
|
||||
val statusCode = when (response.error?.code) {
|
||||
"VALIDATION_ERROR" -> HttpStatusCode.BadRequest
|
||||
"DOMAIN_VALIDATION_ERROR" -> HttpStatusCode.BadRequest
|
||||
else -> HttpStatusCode.InternalServerError
|
||||
}
|
||||
call.respond(statusCode, ApiResponse.error<Any>(response.error?.message ?: "Failed to create event"))
|
||||
val errors = veranstaltung.validate()
|
||||
if (errors.isNotEmpty()) {
|
||||
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Any>("VALIDATION_ERROR", "Validation failed: ${errors.joinToString(", ")}"))
|
||||
return@post
|
||||
}
|
||||
|
||||
val savedEvent = veranstaltungRepository.save(veranstaltung)
|
||||
call.respond(HttpStatusCode.Created, ApiResponse.success(savedEvent))
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Any>("Invalid request data: ${e.message}"))
|
||||
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Any>("INVALID_REQUEST", "Invalid request data: ${e.message}"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,27 +141,18 @@ class VeranstaltungController(
|
||||
val eventId = Uuid.parse(call.parameters["id"]!!)
|
||||
val updateRequest = call.receive<UpdateEventRequest>()
|
||||
|
||||
// Validate input using shared validation utilities
|
||||
val validationErrors = ApiValidationUtils.validateEventRequest(
|
||||
name = updateRequest.name,
|
||||
ort = updateRequest.ort,
|
||||
startDatum = updateRequest.startDatum,
|
||||
endDatum = updateRequest.endDatum,
|
||||
maxTeilnehmer = updateRequest.maxTeilnehmer
|
||||
)
|
||||
|
||||
if (!ApiValidationUtils.isValid(validationErrors)) {
|
||||
call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
ApiResponse.error<Any>(ApiValidationUtils.createErrorMessage(validationErrors))
|
||||
)
|
||||
val existingEvent = veranstaltungRepository.findById(eventId)
|
||||
if (existingEvent == null) {
|
||||
call.respond(HttpStatusCode.NotFound, ApiResponse.error<Any>("NOT_FOUND", "Event not found"))
|
||||
return@put
|
||||
}
|
||||
|
||||
val useCaseRequest = UpdateVeranstaltungUseCase.UpdateVeranstaltungRequest(
|
||||
veranstaltungId = eventId,
|
||||
val updatedVeranstaltung = existingEvent.copy(
|
||||
name = updateRequest.name,
|
||||
untertitel = updateRequest.untertitel,
|
||||
beschreibung = updateRequest.beschreibung,
|
||||
logoUrl = updateRequest.logoUrl,
|
||||
sponsoren = updateRequest.sponsoren,
|
||||
startDatum = updateRequest.startDatum,
|
||||
endDatum = updateRequest.endDatum,
|
||||
ort = updateRequest.ort,
|
||||
@@ -213,72 +162,46 @@ class VeranstaltungController(
|
||||
istOeffentlich = updateRequest.istOeffentlich,
|
||||
maxTeilnehmer = updateRequest.maxTeilnehmer,
|
||||
anmeldeschluss = updateRequest.anmeldeschluss
|
||||
)
|
||||
).withUpdatedTimestamp()
|
||||
|
||||
val response = updateVeranstaltungUseCase.execute(useCaseRequest)
|
||||
|
||||
if (response.success && response.data != null) {
|
||||
call.respond(HttpStatusCode.OK, ApiResponse.success((response.data as UpdateVeranstaltungUseCase.UpdateVeranstaltungResponse).veranstaltung))
|
||||
} else {
|
||||
val statusCode = when (response.error?.code) {
|
||||
"NOT_FOUND" -> HttpStatusCode.NotFound
|
||||
"VALIDATION_ERROR" -> HttpStatusCode.BadRequest
|
||||
"DOMAIN_VALIDATION_ERROR" -> HttpStatusCode.BadRequest
|
||||
else -> HttpStatusCode.InternalServerError
|
||||
}
|
||||
call.respond(statusCode, ApiResponse.error<Any>(response.error?.message ?: "Failed to update event"))
|
||||
val errors = updatedVeranstaltung.validate()
|
||||
if (errors.isNotEmpty()) {
|
||||
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Any>("VALIDATION_ERROR", "Validation failed: ${errors.joinToString(", ")}"))
|
||||
return@put
|
||||
}
|
||||
|
||||
val savedEvent = veranstaltungRepository.save(updatedVeranstaltung)
|
||||
call.respond(HttpStatusCode.OK, ApiResponse.success(savedEvent))
|
||||
} catch (_: IllegalArgumentException) {
|
||||
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Any>("Invalid event ID format"))
|
||||
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Any>("INVALID_ID", "Invalid event ID format"))
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Any>("Invalid request data: ${e.message}"))
|
||||
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Any>("INVALID_REQUEST", "Invalid request data: ${e.message}"))
|
||||
}
|
||||
}
|
||||
|
||||
// DELETE /api/events/{id} - Delete event
|
||||
delete("/{id}") {
|
||||
try {
|
||||
val eventId = ApiValidationUtils.validateUuidString(call.parameters["id"])
|
||||
?: return@delete call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
ApiResponse.error<Any>("Invalid event ID format")
|
||||
)
|
||||
|
||||
// Validate force parameter if provided
|
||||
val forceParam = call.request.queryParameters["force"]
|
||||
val forceDelete = if (forceParam != null) {
|
||||
try {
|
||||
forceParam.toBoolean()
|
||||
} catch (_: Exception) {
|
||||
return@delete call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
ApiResponse.error<Any>("Invalid force parameter. Must be true or false")
|
||||
)
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
val useCaseRequest = DeleteVeranstaltungUseCase.DeleteVeranstaltungRequest(
|
||||
veranstaltungId = eventId,
|
||||
forceDelete = forceDelete
|
||||
val eventIdString = call.parameters["id"] ?: return@delete call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
ApiResponse.error<Any>("MISSING_ID", "Event ID is required")
|
||||
)
|
||||
|
||||
val response = deleteVeranstaltungUseCase.execute(useCaseRequest)
|
||||
|
||||
if (response.success) {
|
||||
call.respond(HttpStatusCode.OK, ApiResponse.success(response.data))
|
||||
} else {
|
||||
val statusCode = when (response.error?.code) {
|
||||
"NOT_FOUND" -> HttpStatusCode.NotFound
|
||||
"CANNOT_DELETE_ACTIVE_EVENT" -> HttpStatusCode.Conflict
|
||||
else -> HttpStatusCode.InternalServerError
|
||||
}
|
||||
call.respond(statusCode, ApiResponse.error<Any>(response.error?.message ?: "Failed to delete event"))
|
||||
val eventId = try { Uuid.parse(eventIdString) } catch (e: Exception) {
|
||||
return@delete call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
ApiResponse.error<Any>("INVALID_ID", "Invalid event ID format")
|
||||
)
|
||||
}
|
||||
|
||||
val success = veranstaltungRepository.delete(eventId)
|
||||
if (success) {
|
||||
call.respond(HttpStatusCode.OK, ApiResponse.success("Event deleted successfully"))
|
||||
} else {
|
||||
call.respond(HttpStatusCode.NotFound, ApiResponse.error<Any>("NOT_FOUND", "Event not found"))
|
||||
}
|
||||
} catch (_: IllegalArgumentException) {
|
||||
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Any>("Invalid event ID format"))
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<Any>("Failed to delete event: ${e.message}"))
|
||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<Any>("INTERNAL_ERROR", "Failed to delete event: ${e.message}"))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -290,7 +213,10 @@ class VeranstaltungController(
|
||||
@Serializable
|
||||
data class CreateEventRequest(
|
||||
val name: String,
|
||||
val untertitel: String? = null,
|
||||
val beschreibung: String? = null,
|
||||
val logoUrl: String? = null,
|
||||
val sponsoren: String? = null,
|
||||
val startDatum: LocalDate,
|
||||
val endDatum: LocalDate,
|
||||
val ort: String,
|
||||
@@ -309,7 +235,10 @@ class VeranstaltungController(
|
||||
@Serializable
|
||||
data class UpdateEventRequest(
|
||||
val name: String,
|
||||
val untertitel: String? = null,
|
||||
val beschreibung: String? = null,
|
||||
val logoUrl: String? = null,
|
||||
val sponsoren: String? = null,
|
||||
val startDatum: LocalDate,
|
||||
val endDatum: LocalDate,
|
||||
val ort: String,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
plugins {
|
||||
// KORREKTUR: Von 'kotlin("jvm")' zu Multiplattform wechseln.
|
||||
alias(libs.plugins.kotlin.multiplatform)
|
||||
alias(libs.plugins.kotlin.serialization)
|
||||
alias(libs.plugins.kotlinMultiplatform)
|
||||
alias(libs.plugins.kotlinSerialization)
|
||||
}
|
||||
|
||||
kotlin {
|
||||
@@ -25,6 +24,11 @@ kotlin {
|
||||
val commonTest by getting {
|
||||
dependencies {
|
||||
implementation(kotlin("test"))
|
||||
}
|
||||
}
|
||||
|
||||
val jvmTest by getting {
|
||||
dependencies {
|
||||
implementation(projects.platform.platformTesting)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
plugins {
|
||||
kotlin("jvm")
|
||||
alias(libs.plugins.kotlinJvm)
|
||||
alias(libs.plugins.kotlinSerialization)
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.core.coreDomain)
|
||||
implementation(projects.core.coreUtils)
|
||||
testImplementation(projects.platform.platformTesting)
|
||||
implementation(projects.core.coreDomain)
|
||||
implementation(projects.core.coreUtils)
|
||||
implementation(libs.kotlinx.datetime)
|
||||
testImplementation(projects.platform.platformTesting)
|
||||
}
|
||||
|
||||
+6
-5
@@ -4,8 +4,9 @@ package at.mocode.events.domain.model
|
||||
|
||||
import at.mocode.core.domain.model.AusschreibungsStatusE
|
||||
import at.mocode.core.domain.model.SparteE
|
||||
import at.mocode.core.domain.serialization.KotlinxInstantSerializer
|
||||
import at.mocode.core.domain.serialization.InstantSerializer
|
||||
import at.mocode.core.domain.serialization.UuidSerializer
|
||||
|
||||
import kotlinx.datetime.LocalDate
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlin.time.Clock
|
||||
@@ -44,7 +45,7 @@ import kotlin.uuid.Uuid
|
||||
* @property updatedAt Letzter Änderungszeitpunkt.
|
||||
*/
|
||||
@Serializable
|
||||
data class DomAusschreibung(
|
||||
data class Ausschreibung(
|
||||
@Serializable(with = UuidSerializer::class)
|
||||
val ausschreibungsId: Uuid = Uuid.random(),
|
||||
|
||||
@@ -88,9 +89,9 @@ data class DomAusschreibung(
|
||||
var genehmigungsNummer: String? = null,
|
||||
|
||||
// Audit
|
||||
@Serializable(with = KotlinxInstantSerializer::class)
|
||||
@Serializable(with = InstantSerializer::class)
|
||||
val createdAt: Instant = Clock.System.now(),
|
||||
@Serializable(with = KotlinxInstantSerializer::class)
|
||||
@Serializable(with = InstantSerializer::class)
|
||||
var updatedAt: Instant = Clock.System.now()
|
||||
) {
|
||||
/**
|
||||
@@ -150,5 +151,5 @@ data class DomAusschreibung(
|
||||
/**
|
||||
* Erstellt eine Kopie mit aktualisiertem Zeitstempel.
|
||||
*/
|
||||
fun withUpdatedTimestamp(): DomAusschreibung = this.copy(updatedAt = Clock.System.now())
|
||||
fun withUpdatedTimestamp(): Ausschreibung = this.copy(updatedAt = Clock.System.now())
|
||||
}
|
||||
+7
-7
@@ -8,7 +8,7 @@ import at.mocode.core.domain.model.TurnierkategorieE
|
||||
import at.mocode.core.domain.model.TurnierStatusE
|
||||
import at.mocode.events.domain.validation.TurnierBewerbDescriptor
|
||||
import at.mocode.events.domain.validation.TurnierkategoriePolicy
|
||||
import at.mocode.core.domain.serialization.KotlinxInstantSerializer
|
||||
import at.mocode.core.domain.serialization.InstantSerializer
|
||||
import at.mocode.core.domain.serialization.UuidSerializer
|
||||
import kotlinx.datetime.LocalDate
|
||||
import kotlinx.serialization.Serializable
|
||||
@@ -39,7 +39,7 @@ import kotlin.uuid.Uuid
|
||||
* @property updatedAt Letzter Änderungszeitpunkt.
|
||||
*/
|
||||
@Serializable
|
||||
data class DomTurnier(
|
||||
data class Turnier(
|
||||
@Serializable(with = UuidSerializer::class)
|
||||
val turnierId: Uuid = Uuid.random(),
|
||||
|
||||
@@ -72,13 +72,13 @@ data class DomTurnier(
|
||||
var bemerkungen: String? = null,
|
||||
|
||||
// Audit
|
||||
@Serializable(with = KotlinxInstantSerializer::class)
|
||||
@Serializable(with = InstantSerializer::class)
|
||||
val createdAt: Instant = Clock.System.now(),
|
||||
@Serializable(with = KotlinxInstantSerializer::class)
|
||||
@Serializable(with = InstantSerializer::class)
|
||||
var updatedAt: Instant = Clock.System.now()
|
||||
) {
|
||||
/**
|
||||
* Prüft ob das Turnier Pflicht-Funktionäre zugewiesen hat.
|
||||
* Prüft, ob das Turnier Pflicht-Funktionäre zugewiesen hat.
|
||||
* Gibt Warnungen zurück – keine harten Fehler (Warn-Logik statt Exception, ADR-0007).
|
||||
*/
|
||||
fun validateFunktionaerBesetzung(): List<String> {
|
||||
@@ -115,13 +115,13 @@ data class DomTurnier(
|
||||
/**
|
||||
* Erstellt eine Kopie mit aktualisiertem Zeitstempel.
|
||||
*/
|
||||
fun withUpdatedTimestamp(): DomTurnier = this.copy(updatedAt = Clock.System.now())
|
||||
fun withUpdatedTimestamp(): Turnier = this.copy(updatedAt = Clock.System.now())
|
||||
|
||||
/**
|
||||
* Validiert die im Turnier geplanten Bewerbe gegen die Limits der Turnierkategorie.
|
||||
*
|
||||
* Hinweis: Die konkreten Regeln stammen aus der ÖTO-Spezifikation und werden vom 📜 Rulebook Expert (A-5)
|
||||
* bereitgestellt. Diese Methode delegiert daher an eine Policy-Schnittstelle, um Kopplung zu vermeiden und
|
||||
* bereitgestellt. Diese Methode delegiert sich daher an eine Policy-Schnittstelle, um Kopplung zu vermeiden und
|
||||
* die Regeln austauschbar zu halten.
|
||||
*
|
||||
* Rückgabe: Liste von Meldungen (Fehler/Warnungen) – Formulierung/Schweregrad ist Teil der Policy.
|
||||
+23
-23
@@ -2,15 +2,15 @@
|
||||
package at.mocode.events.domain.model
|
||||
|
||||
import at.mocode.core.domain.model.SparteE
|
||||
import at.mocode.core.domain.serialization.KotlinxInstantSerializer
|
||||
import at.mocode.core.domain.serialization.KotlinLocalDateSerializer
|
||||
import at.mocode.core.domain.serialization.InstantSerializer
|
||||
import at.mocode.core.domain.serialization.LocalDateSerializer
|
||||
import at.mocode.core.domain.serialization.UuidSerializer
|
||||
import kotlin.uuid.Uuid
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlinx.datetime.Instant
|
||||
|
||||
import kotlinx.datetime.LocalDate
|
||||
import kotlinx.datetime.TimeZone
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlin.time.Clock
|
||||
import kotlin.time.Instant
|
||||
|
||||
/**
|
||||
* Domain model representing an event/competition in the event management system.
|
||||
@@ -35,40 +35,40 @@ import kotlinx.serialization.Serializable
|
||||
*/
|
||||
@Serializable
|
||||
data class Veranstaltung(
|
||||
@Serializable(with = UuidSerializer::class)
|
||||
@Serializable(with = UuidSerializer::class)
|
||||
val veranstaltungId: Uuid = Uuid.random(),
|
||||
|
||||
// Basic Information
|
||||
var name: String,
|
||||
var untertitel: String? = null,
|
||||
var beschreibung: String? = null,
|
||||
var logoUrl: String? = null,
|
||||
var sponsoren: String? = null, // JSON string or comma-separated for now
|
||||
var name: String,
|
||||
var untertitel: String? = null,
|
||||
var beschreibung: String? = null,
|
||||
var logoUrl: String? = null,
|
||||
var sponsoren: String? = null, // JSON string or comma-separated for now
|
||||
|
||||
// Dates
|
||||
@Serializable(with = KotlinLocalDateSerializer::class)
|
||||
@Serializable(with = LocalDateSerializer::class)
|
||||
var startDatum: LocalDate,
|
||||
@Serializable(with = KotlinLocalDateSerializer::class)
|
||||
@Serializable(with = LocalDateSerializer::class)
|
||||
var endDatum: LocalDate,
|
||||
|
||||
// Location and Organization
|
||||
var ort: String,
|
||||
@Serializable(with = UuidSerializer::class)
|
||||
var ort: String,
|
||||
@Serializable(with = UuidSerializer::class)
|
||||
var veranstalterVereinId: Uuid,
|
||||
|
||||
// Event Details
|
||||
var sparten: List<SparteE> = emptyList(),
|
||||
var istAktiv: Boolean = true,
|
||||
var istOeffentlich: Boolean = true,
|
||||
var maxTeilnehmer: Int? = null,
|
||||
var sparten: List<SparteE> = emptyList(),
|
||||
var istAktiv: Boolean = true,
|
||||
var istOeffentlich: Boolean = true,
|
||||
var maxTeilnehmer: Int? = null,
|
||||
|
||||
@Serializable(with = KotlinLocalDateSerializer::class)
|
||||
@Serializable(with = LocalDateSerializer::class)
|
||||
var anmeldeschluss: LocalDate? = null,
|
||||
|
||||
// Audit Fields
|
||||
@Serializable(with = KotlinxInstantSerializer::class)
|
||||
@Serializable(with = InstantSerializer::class)
|
||||
val createdAt: Instant = Clock.System.now(),
|
||||
@Serializable(with = KotlinxInstantSerializer::class)
|
||||
@Serializable(with = InstantSerializer::class)
|
||||
var updatedAt: Instant = Clock.System.now()
|
||||
) {
|
||||
/**
|
||||
@@ -127,7 +127,7 @@ data class Veranstaltung(
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a copy of this event with updated timestamp.
|
||||
* Creates a copy of this event with an updated timestamp.
|
||||
*/
|
||||
fun withUpdatedTimestamp(): Veranstaltung {
|
||||
return this.copy(updatedAt = Clock.System.now())
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ data class TurnierBewerbDescriptor(
|
||||
|
||||
/**
|
||||
* Policy-Schnittstelle für die Validierung der Turnierkategorie-Limits (ÖTO-Regelwerk).
|
||||
* Implementierung wird durch den 📜 Rulebook Expert (A-5) spezifiziert.
|
||||
* Die Implementierung wird durch den 📜 Rulebook Expert (A-5) spezifiziert.
|
||||
*/
|
||||
fun interface TurnierkategoriePolicy {
|
||||
/**
|
||||
|
||||
+10
-5
@@ -4,12 +4,13 @@ import at.mocode.core.domain.model.SparteE
|
||||
import at.mocode.core.domain.model.TurnierkategorieE
|
||||
import at.mocode.events.domain.validation.TurnierBewerbDescriptor
|
||||
import at.mocode.events.domain.validation.TurnierkategoriePolicy
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.uuid.Uuid
|
||||
import kotlinx.datetime.LocalDate
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
import kotlin.uuid.ExperimentalUuidApi
|
||||
|
||||
class DomTurnierKategorieValidationTest {
|
||||
class TurnierKategorieValidationTest {
|
||||
|
||||
private val fakePolicy = TurnierkategoriePolicy { kategorie, bewerbe ->
|
||||
val msgs = mutableListOf<String>()
|
||||
@@ -29,11 +30,13 @@ class DomTurnierKategorieValidationTest {
|
||||
msgs
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalUuidApi::class)
|
||||
@Test
|
||||
fun `C Turnier verbietet 135cm Springen`() {
|
||||
val turnier = DomTurnier(
|
||||
val turnier = Turnier(
|
||||
veranstaltungId = Uuid.random(),
|
||||
name = "CSN-C Samstag",
|
||||
turnierNummer = "12345",
|
||||
sparte = SparteE.SPRINGEN,
|
||||
kategorie = TurnierkategorieE.C,
|
||||
datum = LocalDate(2026, 6, 1)
|
||||
@@ -48,11 +51,13 @@ class DomTurnierKategorieValidationTest {
|
||||
assertEquals(1, msgs.size, "Erwartet genau eine Limitverletzung (135cm auf C-Turnier)")
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalUuidApi::class)
|
||||
@Test
|
||||
fun `C-NEU Turnier verbietet 120cm`() {
|
||||
val turnier = DomTurnier(
|
||||
val turnier = Turnier(
|
||||
veranstaltungId = Uuid.random(),
|
||||
name = "CSN-C-NEU",
|
||||
turnierNummer = "12345",
|
||||
sparte = SparteE.SPRINGEN,
|
||||
kategorie = TurnierkategorieE.C_NEU,
|
||||
datum = LocalDate(2026, 6, 1)
|
||||
|
||||
+3
-2
@@ -2,8 +2,9 @@ package at.mocode.events.domain.validation
|
||||
|
||||
import at.mocode.core.domain.model.SparteE
|
||||
import at.mocode.core.domain.model.TurnierkategorieE
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
|
||||
class OeToTurnierkategoriePolicyTest {
|
||||
|
||||
|
||||
@@ -1,24 +1,29 @@
|
||||
plugins {
|
||||
alias(libs.plugins.kotlin.jvm)
|
||||
alias(libs.plugins.kotlin.spring)
|
||||
alias(libs.plugins.kotlin.serialization)
|
||||
alias(libs.plugins.ktor)
|
||||
application
|
||||
alias(libs.plugins.kotlinJvm)
|
||||
alias(libs.plugins.kotlinSpring)
|
||||
alias(libs.plugins.kotlinSerialization)
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.platform.platformDependencies)
|
||||
implementation(platform(projects.platform.platformBom))
|
||||
implementation(projects.platform.platformDependencies)
|
||||
implementation(projects.backend.services.events.eventsDomain)
|
||||
implementation(projects.core.coreDomain)
|
||||
implementation(projects.core.coreUtils)
|
||||
implementation(projects.backend.infrastructure.cache.cacheApi)
|
||||
implementation(projects.backend.infrastructure.eventStore.eventStoreApi)
|
||||
implementation(projects.backend.infrastructure.messaging.messagingClient)
|
||||
implementation(projects.backend.infrastructure.persistence)
|
||||
|
||||
implementation(projects.events.eventsDomain)
|
||||
implementation(projects.events.eventsApplication)
|
||||
implementation(projects.core.coreDomain)
|
||||
implementation(projects.core.coreUtils)
|
||||
implementation(projects.infrastructure.cache.cacheApi)
|
||||
implementation(projects.infrastructure.eventStore.eventStoreApi)
|
||||
implementation(projects.infrastructure.messaging.messagingClient)
|
||||
implementation(libs.exposed.core)
|
||||
implementation(libs.exposed.dao)
|
||||
implementation(libs.exposed.jdbc)
|
||||
implementation(libs.exposed.kotlin.datetime)
|
||||
implementation(libs.exposed.java.time)
|
||||
implementation(libs.exposed.json)
|
||||
|
||||
implementation(libs.spring.boot.starter.data.jpa)
|
||||
implementation(libs.postgresql.driver)
|
||||
implementation(libs.spring.boot.starter.data.jpa)
|
||||
implementation(libs.postgresql.driver)
|
||||
|
||||
testImplementation(projects.platform.platformTesting)
|
||||
testImplementation(projects.platform.platformTesting)
|
||||
}
|
||||
|
||||
+8
-6
@@ -1,10 +1,9 @@
|
||||
package at.mocode.events.infrastructure.persistence
|
||||
|
||||
import at.mocode.core.domain.model.SparteE
|
||||
import org.jetbrains.exposed.v1.core.dao.id.java.UUIDTable
|
||||
import org.jetbrains.exposed.v1.core.kotlin.datetime.date
|
||||
import org.jetbrains.exposed.v1.core.kotlin.datetime.timestamp
|
||||
import org.jetbrains.exposed.v1.core.javaUUID
|
||||
import org.jetbrains.exposed.v1.core.Table
|
||||
import org.jetbrains.exposed.v1.core.java.javaUUID
|
||||
import org.jetbrains.exposed.v1.datetime.timestamp
|
||||
import org.jetbrains.exposed.v1.datetime.date
|
||||
|
||||
/**
|
||||
* Database table definition for events (Veranstaltung) in the event-management context.
|
||||
@@ -12,7 +11,10 @@ import org.jetbrains.exposed.v1.core.javaUUID
|
||||
* This table stores all event information including dates, location,
|
||||
* organization details, and administrative information.
|
||||
*/
|
||||
object VeranstaltungTable : UUIDTable("veranstaltungen") {
|
||||
object VeranstaltungTable : Table("veranstaltungen") {
|
||||
|
||||
val id = javaUUID("id").autoGenerate()
|
||||
override val primaryKey = PrimaryKey(id)
|
||||
|
||||
// Basic Information
|
||||
val name = varchar("name", 255)
|
||||
|
||||
+7
-71
@@ -1,21 +1,15 @@
|
||||
package at.mocode.events.service.config
|
||||
|
||||
import at.mocode.core.utils.database.DatabaseConfig
|
||||
import at.mocode.core.utils.database.DatabaseFactory
|
||||
import at.mocode.events.infrastructure.persistence.VeranstaltungTable
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.context.annotation.Profile
|
||||
import jakarta.annotation.PostConstruct
|
||||
import jakarta.annotation.PreDestroy
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.jetbrains.exposed.sql.SchemaUtils
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
|
||||
/**
|
||||
* Database configuration for the Events Service.
|
||||
* Datenbank-Konfiguration für den Events Service.
|
||||
*
|
||||
* This configuration ensures that Database.connect() is called properly
|
||||
* before any Exposed operations are performed.
|
||||
* Initialisiert das Exposed-Schema für Veranstaltungen und Turniere.
|
||||
* Die DB-Verbindung selbst wird durch den zentralen DataSource-Bean initialisiert.
|
||||
*/
|
||||
@Configuration
|
||||
@Profile("!test")
|
||||
@@ -25,35 +19,13 @@ class EventsDatabaseConfiguration {
|
||||
|
||||
@PostConstruct
|
||||
fun initializeDatabase() {
|
||||
log.info("Initializing database schema for Events Service...")
|
||||
|
||||
try {
|
||||
// Database connection is already initialized by the gateway
|
||||
// Only initialize the schema for this service
|
||||
transaction {
|
||||
SchemaUtils.create(VeranstaltungTable)
|
||||
log.info("Events database schema initialized successfully")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
log.error("Failed to initialize database schema", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
fun closeDatabase() {
|
||||
log.info("Closing database connection for Events Service...")
|
||||
try {
|
||||
DatabaseFactory.close()
|
||||
log.info("Database connection closed successfully")
|
||||
} catch (e: Exception) {
|
||||
log.error("Error closing database connection", e)
|
||||
}
|
||||
// Flyway übernimmt ab jetzt die Schema-Erstellung pro Tenant.
|
||||
log.info("Überspringe Exposed Schema-Initialisierung – Flyway migriert pro Tenant-Schema.")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test-specific database configuration.
|
||||
* Test-spezifische Datenbank-Konfiguration.
|
||||
*/
|
||||
@Configuration
|
||||
@Profile("test")
|
||||
@@ -63,42 +35,6 @@ class EventsTestDatabaseConfiguration {
|
||||
|
||||
@PostConstruct
|
||||
fun initializeTestDatabase() {
|
||||
log.info("Initializing test database connection for Events Service...")
|
||||
|
||||
try {
|
||||
// Use H2 in-memory database for tests
|
||||
val testConfig = DatabaseConfig(
|
||||
jdbcUrl = "jdbc:h2:mem:events_test;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE",
|
||||
username = "sa",
|
||||
password = "",
|
||||
driverClassName = "org.h2.Driver",
|
||||
maxPoolSize = 5,
|
||||
minPoolSize = 1,
|
||||
autoMigrate = true
|
||||
)
|
||||
|
||||
DatabaseFactory.init(testConfig)
|
||||
log.info("Test database connection initialized successfully")
|
||||
|
||||
// Initialize database schema for tests
|
||||
transaction {
|
||||
SchemaUtils.create(VeranstaltungTable)
|
||||
log.info("Test events database schema initialized successfully")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
log.error("Failed to initialize test database connection", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
fun closeTestDatabase() {
|
||||
log.info("Closing test database connection for Events Service...")
|
||||
try {
|
||||
DatabaseFactory.close()
|
||||
log.info("Test database connection closed successfully")
|
||||
} catch (e: Exception) {
|
||||
log.error("Error closing test database connection", e)
|
||||
}
|
||||
log.info("Initialisiere Test-Datenbank-Schema für den Events Service...")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user