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:
2026-04-08 22:56:15 +02:00
parent df8bce4277
commit 8bc6f8e1df
16 changed files with 501 additions and 319 deletions
@@ -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)
}
@@ -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)
}
@@ -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())
}
@@ -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.
@@ -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())
@@ -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 {
/**
@@ -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)
@@ -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)
}
@@ -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)
@@ -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...")
}
}