refactor: Migrate from monolithic to modular architecture
- Restructure project into domain-specific modules (core, masterdata, members, horses, events, infrastructure) - Create shared client components in common-ui module - Implement CI/CD workflows with GitHub Actions - Consolidate documentation in docs directory - Remove deprecated modules and documentation files - Add cleanup and migration scripts for transition - Update README with new project structure and setup instructions
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
plugins {
|
||||
kotlin("jvm")
|
||||
kotlin("plugin.spring")
|
||||
alias(libs.plugins.kotlin.serialization)
|
||||
alias(libs.plugins.ktor)
|
||||
application
|
||||
}
|
||||
|
||||
application {
|
||||
mainClass.set("at.mocode.events.api.ApplicationKt")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.platform.platformDependencies)
|
||||
|
||||
implementation(projects.events.eventsDomain)
|
||||
implementation(projects.events.eventsApplication)
|
||||
implementation(projects.core.coreDomain)
|
||||
implementation(projects.core.coreUtils)
|
||||
|
||||
// Spring dependencies
|
||||
implementation("org.springframework:spring-web")
|
||||
implementation("org.springdoc: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.serializationKotlinxJson)
|
||||
implementation(libs.ktor.server.statusPages)
|
||||
implementation(libs.ktor.server.auth)
|
||||
implementation(libs.ktor.server.authJwt)
|
||||
|
||||
testImplementation(projects.platform.platformTesting)
|
||||
testImplementation(libs.ktor.server.tests)
|
||||
}
|
||||
+333
@@ -0,0 +1,333 @@
|
||||
package at.mocode.events.api.rest
|
||||
|
||||
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 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.datetime.LocalDate
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* REST API controller for event management operations.
|
||||
*
|
||||
* This controller provides HTTP endpoints for all event-related operations
|
||||
* following REST conventions and proper HTTP status codes.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
fun configureRoutes(routing: Routing) {
|
||||
routing.route("/api/events") {
|
||||
|
||||
// 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")
|
||||
)
|
||||
}
|
||||
val searchTerm = call.request.queryParameters["search"]
|
||||
val publicOnly = call.request.queryParameters["publicOnly"]?.toBoolean() ?: false
|
||||
val startDate = call.request.queryParameters["startDate"]?.let { LocalDate.parse(it) }
|
||||
val endDate = call.request.queryParameters["endDate"]?.let { LocalDate.parse(it) }
|
||||
|
||||
val events = when {
|
||||
searchTerm != null -> veranstaltungRepository.findByName(searchTerm, limit)
|
||||
organizerId != null -> veranstaltungRepository.findByVeranstalterVereinId(organizerId, activeOnly)
|
||||
publicOnly -> veranstaltungRepository.findPublicEvents(activeOnly)
|
||||
startDate != null && endDate != null -> veranstaltungRepository.findByDateRange(startDate, endDate, activeOnly)
|
||||
startDate != null -> veranstaltungRepository.findByStartDate(startDate, activeOnly)
|
||||
else -> veranstaltungRepository.findAllActive(limit, offset)
|
||||
}
|
||||
|
||||
call.respond(HttpStatusCode.OK, ApiResponse.success(events))
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<Any>("Failed to retrieve events: ${e.message}"))
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/events/{id} - Get event by ID
|
||||
get("/{id}") {
|
||||
try {
|
||||
val eventId = uuidFrom(call.parameters["id"]!!)
|
||||
val request = GetVeranstaltungUseCase.GetVeranstaltungRequest(eventId)
|
||||
val response = getVeranstaltungUseCase.execute(request)
|
||||
|
||||
if (response.success && response.data != null) {
|
||||
call.respond(HttpStatusCode.OK, ApiResponse.success((response.data as GetVeranstaltungUseCase.GetVeranstaltungResponse).veranstaltung))
|
||||
} else {
|
||||
call.respond(HttpStatusCode.NotFound, ApiResponse.error<Any>("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 retrieve event: ${e.message}"))
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/events/stats - Get event statistics
|
||||
get("/stats") {
|
||||
try {
|
||||
val activeCount = veranstaltungRepository.countActive()
|
||||
val publicCount = veranstaltungRepository.findPublicEvents(true).size
|
||||
|
||||
val stats = EventStats(
|
||||
totalActive = activeCount,
|
||||
totalPublic = publicCount.toLong()
|
||||
)
|
||||
|
||||
call.respond(HttpStatusCode.OK, ApiResponse.success(stats))
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<Any>("Failed to retrieve event statistics: ${e.message}"))
|
||||
}
|
||||
}
|
||||
|
||||
// POST /api/events - Create new event
|
||||
post {
|
||||
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(
|
||||
name = createRequest.name,
|
||||
beschreibung = createRequest.beschreibung,
|
||||
startDatum = createRequest.startDatum,
|
||||
endDatum = createRequest.endDatum,
|
||||
ort = createRequest.ort,
|
||||
veranstalterVereinId = createRequest.veranstalterVereinId,
|
||||
sparten = createRequest.sparten,
|
||||
istAktiv = createRequest.istAktiv,
|
||||
istOeffentlich = createRequest.istOeffentlich,
|
||||
maxTeilnehmer = createRequest.maxTeilnehmer,
|
||||
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"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Any>("Invalid request data: ${e.message}"))
|
||||
}
|
||||
}
|
||||
|
||||
// PUT /api/events/{id} - Update event
|
||||
put("/{id}") {
|
||||
try {
|
||||
val eventId = uuidFrom(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))
|
||||
)
|
||||
return@put
|
||||
}
|
||||
|
||||
val useCaseRequest = UpdateVeranstaltungUseCase.UpdateVeranstaltungRequest(
|
||||
veranstaltungId = eventId,
|
||||
name = updateRequest.name,
|
||||
beschreibung = updateRequest.beschreibung,
|
||||
startDatum = updateRequest.startDatum,
|
||||
endDatum = updateRequest.endDatum,
|
||||
ort = updateRequest.ort,
|
||||
veranstalterVereinId = updateRequest.veranstalterVereinId,
|
||||
sparten = updateRequest.sparten,
|
||||
istAktiv = updateRequest.istAktiv,
|
||||
istOeffentlich = updateRequest.istOeffentlich,
|
||||
maxTeilnehmer = updateRequest.maxTeilnehmer,
|
||||
anmeldeschluss = updateRequest.anmeldeschluss
|
||||
)
|
||||
|
||||
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"))
|
||||
}
|
||||
} catch (_: IllegalArgumentException) {
|
||||
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Any>("Invalid event ID format"))
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Any>("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 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"))
|
||||
}
|
||||
} 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}"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Request DTO for creating events.
|
||||
*/
|
||||
@Serializable
|
||||
data class CreateEventRequest(
|
||||
val name: String,
|
||||
val beschreibung: String? = null,
|
||||
val startDatum: LocalDate,
|
||||
val endDatum: LocalDate,
|
||||
val ort: String,
|
||||
@Serializable(with = UuidSerializer::class)
|
||||
val veranstalterVereinId: Uuid,
|
||||
val sparten: List<SparteE> = emptyList(),
|
||||
val istAktiv: Boolean = true,
|
||||
val istOeffentlich: Boolean = true,
|
||||
val maxTeilnehmer: Int? = null,
|
||||
val anmeldeschluss: LocalDate? = null
|
||||
)
|
||||
|
||||
/**
|
||||
* Request DTO for updating events.
|
||||
*/
|
||||
@Serializable
|
||||
data class UpdateEventRequest(
|
||||
val name: String,
|
||||
val beschreibung: String? = null,
|
||||
val startDatum: LocalDate,
|
||||
val endDatum: LocalDate,
|
||||
val ort: String,
|
||||
@Serializable(with = UuidSerializer::class)
|
||||
val veranstalterVereinId: Uuid,
|
||||
val sparten: List<SparteE> = emptyList(),
|
||||
val istAktiv: Boolean = true,
|
||||
val istOeffentlich: Boolean = true,
|
||||
val maxTeilnehmer: Int? = null,
|
||||
val anmeldeschluss: LocalDate? = null
|
||||
)
|
||||
|
||||
/**
|
||||
* Response DTO for event statistics.
|
||||
*/
|
||||
@Serializable
|
||||
data class EventStats(
|
||||
val totalActive: Long,
|
||||
val totalPublic: Long
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
plugins {
|
||||
kotlin("jvm")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.events.eventsDomain)
|
||||
implementation(projects.core.coreDomain)
|
||||
implementation(projects.core.coreUtils)
|
||||
testImplementation(projects.platform.platformTesting)
|
||||
}
|
||||
+173
@@ -0,0 +1,173 @@
|
||||
package at.mocode.events.application.usecase
|
||||
|
||||
import at.mocode.core.domain.model.ApiResponse
|
||||
import at.mocode.core.domain.model.ErrorDto
|
||||
import at.mocode.events.domain.model.Veranstaltung
|
||||
import at.mocode.events.domain.repository.VeranstaltungRepository
|
||||
import at.mocode.core.domain.model.SparteE
|
||||
import at.mocode.core.utils.validation.ValidationResult
|
||||
import at.mocode.core.utils.validation.ValidationError
|
||||
import com.benasher44.uuid.Uuid
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlinx.datetime.LocalDate
|
||||
|
||||
/**
|
||||
* Use case for creating new events (Veranstaltung).
|
||||
*
|
||||
* This use case handles the business logic for creating events,
|
||||
* including validation and persistence.
|
||||
*/
|
||||
class CreateVeranstaltungUseCase(
|
||||
private val veranstaltungRepository: VeranstaltungRepository
|
||||
) {
|
||||
|
||||
/**
|
||||
* Request data for creating a new event.
|
||||
*/
|
||||
data class CreateVeranstaltungRequest(
|
||||
val name: String,
|
||||
val beschreibung: String? = null,
|
||||
val startDatum: LocalDate,
|
||||
val endDatum: LocalDate,
|
||||
val ort: String,
|
||||
val veranstalterVereinId: Uuid,
|
||||
val sparten: List<SparteE> = emptyList(),
|
||||
val istAktiv: Boolean = true,
|
||||
val istOeffentlich: Boolean = true,
|
||||
val maxTeilnehmer: Int? = null,
|
||||
val anmeldeschluss: LocalDate? = null
|
||||
)
|
||||
|
||||
/**
|
||||
* Response data containing the created event.
|
||||
*/
|
||||
data class CreateVeranstaltungResponse(
|
||||
val veranstaltung: Veranstaltung
|
||||
)
|
||||
|
||||
/**
|
||||
* Executes the create event use case.
|
||||
*
|
||||
* @param request The request containing event data
|
||||
* @return ApiResponse with the created event or error information
|
||||
*/
|
||||
suspend fun execute(request: CreateVeranstaltungRequest): ApiResponse<CreateVeranstaltungResponse> {
|
||||
return try {
|
||||
// Validate the request
|
||||
val validationResult = validateRequest(request)
|
||||
if (!validationResult.isValid()) {
|
||||
val errors = (validationResult as ValidationResult.Invalid).errors
|
||||
return ApiResponse(
|
||||
success = false,
|
||||
error = ErrorDto(
|
||||
code = "VALIDATION_ERROR",
|
||||
message = "Invalid input data",
|
||||
details = errors.associate { it.field to it.message }
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// Create the domain object
|
||||
val veranstaltung = Veranstaltung(
|
||||
name = request.name.trim(),
|
||||
beschreibung = request.beschreibung?.trim(),
|
||||
startDatum = request.startDatum,
|
||||
endDatum = request.endDatum,
|
||||
ort = request.ort.trim(),
|
||||
veranstalterVereinId = request.veranstalterVereinId,
|
||||
sparten = request.sparten,
|
||||
istAktiv = request.istAktiv,
|
||||
istOeffentlich = request.istOeffentlich,
|
||||
maxTeilnehmer = request.maxTeilnehmer,
|
||||
anmeldeschluss = request.anmeldeschluss,
|
||||
createdAt = Clock.System.now(),
|
||||
updatedAt = Clock.System.now()
|
||||
)
|
||||
|
||||
// Validate the domain object
|
||||
val domainValidationErrors = veranstaltung.validate()
|
||||
if (domainValidationErrors.isNotEmpty()) {
|
||||
return ApiResponse(
|
||||
success = false,
|
||||
error = ErrorDto(
|
||||
code = "DOMAIN_VALIDATION_ERROR",
|
||||
message = "Domain validation failed",
|
||||
details = domainValidationErrors.mapIndexed { index, error ->
|
||||
"error_$index" to error
|
||||
}.toMap()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// Save the event
|
||||
val savedVeranstaltung = veranstaltungRepository.save(veranstaltung)
|
||||
|
||||
ApiResponse(
|
||||
success = true,
|
||||
data = CreateVeranstaltungResponse(savedVeranstaltung)
|
||||
)
|
||||
|
||||
} catch (e: Exception) {
|
||||
ApiResponse(
|
||||
success = false,
|
||||
error = ErrorDto(
|
||||
code = "INTERNAL_ERROR",
|
||||
message = "Failed to create event: ${e.message}"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the create event request.
|
||||
*/
|
||||
private fun validateRequest(request: CreateVeranstaltungRequest): ValidationResult {
|
||||
val errors = mutableListOf<ValidationError>()
|
||||
|
||||
// Validate name
|
||||
if (request.name.isBlank()) {
|
||||
errors.add(ValidationError("name", "Event name is required"))
|
||||
} else if (request.name.length > 255) {
|
||||
errors.add(ValidationError("name", "Event name must not exceed 255 characters"))
|
||||
}
|
||||
|
||||
// Validate location
|
||||
if (request.ort.isBlank()) {
|
||||
errors.add(ValidationError("ort", "Event location is required"))
|
||||
} else if (request.ort.length > 255) {
|
||||
errors.add(ValidationError("ort", "Event location must not exceed 255 characters"))
|
||||
}
|
||||
|
||||
// Validate dates
|
||||
if (request.endDatum < request.startDatum) {
|
||||
errors.add(ValidationError("endDatum", "End date cannot be before start date"))
|
||||
}
|
||||
|
||||
// Validate registration deadline
|
||||
request.anmeldeschluss?.let { deadline ->
|
||||
if (deadline > request.startDatum) {
|
||||
errors.add(ValidationError("anmeldeschluss", "Registration deadline cannot be after event start date"))
|
||||
}
|
||||
}
|
||||
|
||||
// Validate max participants
|
||||
request.maxTeilnehmer?.let { max ->
|
||||
if (max <= 0) {
|
||||
errors.add(ValidationError("maxTeilnehmer", "Maximum participants must be positive"))
|
||||
}
|
||||
}
|
||||
|
||||
// Validate description length
|
||||
request.beschreibung?.let { desc ->
|
||||
if (desc.length > 5000) {
|
||||
errors.add(ValidationError("beschreibung", "Description must not exceed 5000 characters"))
|
||||
}
|
||||
}
|
||||
|
||||
return if (errors.isEmpty()) {
|
||||
ValidationResult.Valid
|
||||
} else {
|
||||
ValidationResult.Invalid(errors)
|
||||
}
|
||||
}
|
||||
}
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
package at.mocode.events.application.usecase
|
||||
|
||||
import at.mocode.core.domain.model.ApiResponse
|
||||
import at.mocode.core.domain.model.ErrorDto
|
||||
import at.mocode.events.domain.repository.VeranstaltungRepository
|
||||
import com.benasher44.uuid.Uuid
|
||||
|
||||
/**
|
||||
* Use case for deleting events (Veranstaltung).
|
||||
*
|
||||
* This use case handles the business logic for deleting events,
|
||||
* including validation and cleanup.
|
||||
*/
|
||||
class DeleteVeranstaltungUseCase(
|
||||
private val veranstaltungRepository: VeranstaltungRepository
|
||||
) {
|
||||
|
||||
/**
|
||||
* Request data for deleting an event.
|
||||
*/
|
||||
data class DeleteVeranstaltungRequest(
|
||||
val veranstaltungId: Uuid,
|
||||
val forceDelete: Boolean = false
|
||||
)
|
||||
|
||||
/**
|
||||
* Response data for successful deletion.
|
||||
*/
|
||||
data class DeleteVeranstaltungResponse(
|
||||
val deleted: Boolean,
|
||||
val message: String
|
||||
)
|
||||
|
||||
/**
|
||||
* Executes the delete event use case.
|
||||
*
|
||||
* @param request The request containing the event ID to delete
|
||||
* @return ApiResponse with deletion result or error information
|
||||
*/
|
||||
suspend fun execute(request: DeleteVeranstaltungRequest): ApiResponse<DeleteVeranstaltungResponse> {
|
||||
return try {
|
||||
// Check if event exists
|
||||
val existingVeranstaltung = veranstaltungRepository.findById(request.veranstaltungId)
|
||||
if (existingVeranstaltung == null) {
|
||||
return ApiResponse(
|
||||
success = false,
|
||||
error = ErrorDto(
|
||||
code = "NOT_FOUND",
|
||||
message = "Event not found"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// Check if event can be safely deleted
|
||||
if (!request.forceDelete) {
|
||||
// In a real implementation, you might check for:
|
||||
// - Active registrations
|
||||
// - Related competitions
|
||||
// - Financial transactions
|
||||
// For now, we'll allow deletion if the event is not active or is in the future
|
||||
|
||||
if (existingVeranstaltung.istAktiv) {
|
||||
return ApiResponse(
|
||||
success = false,
|
||||
error = ErrorDto(
|
||||
code = "CANNOT_DELETE_ACTIVE_EVENT",
|
||||
message = "Cannot delete active event. Use forceDelete=true to override.",
|
||||
details = mapOf(
|
||||
"eventId" to request.veranstaltungId.toString(),
|
||||
"eventName" to existingVeranstaltung.name
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Perform the deletion
|
||||
val deleted = veranstaltungRepository.delete(request.veranstaltungId)
|
||||
|
||||
if (deleted) {
|
||||
ApiResponse(
|
||||
success = true,
|
||||
data = DeleteVeranstaltungResponse(
|
||||
deleted = true,
|
||||
message = "Event '${existingVeranstaltung.name}' has been successfully deleted"
|
||||
)
|
||||
)
|
||||
} else {
|
||||
ApiResponse(
|
||||
success = false,
|
||||
error = ErrorDto(
|
||||
code = "DELETE_FAILED",
|
||||
message = "Failed to delete event from database"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
} catch (e: Exception) {
|
||||
ApiResponse(
|
||||
success = false,
|
||||
error = ErrorDto(
|
||||
code = "INTERNAL_ERROR",
|
||||
message = "Failed to delete event: ${e.message}"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
package at.mocode.events.application.usecase
|
||||
|
||||
import at.mocode.core.domain.model.ApiResponse
|
||||
import at.mocode.core.domain.model.ErrorDto
|
||||
import at.mocode.events.domain.model.Veranstaltung
|
||||
import at.mocode.events.domain.repository.VeranstaltungRepository
|
||||
import com.benasher44.uuid.Uuid
|
||||
|
||||
/**
|
||||
* Use case for retrieving events (Veranstaltung) by ID.
|
||||
*
|
||||
* This use case handles the business logic for fetching events
|
||||
* from the repository.
|
||||
*/
|
||||
class GetVeranstaltungUseCase(
|
||||
private val veranstaltungRepository: VeranstaltungRepository
|
||||
) {
|
||||
|
||||
/**
|
||||
* Request data for retrieving an event.
|
||||
*/
|
||||
data class GetVeranstaltungRequest(
|
||||
val veranstaltungId: Uuid
|
||||
)
|
||||
|
||||
/**
|
||||
* Response data containing the retrieved event.
|
||||
*/
|
||||
data class GetVeranstaltungResponse(
|
||||
val veranstaltung: Veranstaltung
|
||||
)
|
||||
|
||||
/**
|
||||
* Executes the get event use case.
|
||||
*
|
||||
* @param request The request containing the event ID
|
||||
* @return ApiResponse with the event or error information
|
||||
*/
|
||||
suspend fun execute(request: GetVeranstaltungRequest): ApiResponse<GetVeranstaltungResponse> {
|
||||
return try {
|
||||
val veranstaltung = veranstaltungRepository.findById(request.veranstaltungId)
|
||||
|
||||
if (veranstaltung != null) {
|
||||
ApiResponse(
|
||||
success = true,
|
||||
data = GetVeranstaltungResponse(veranstaltung)
|
||||
)
|
||||
} else {
|
||||
ApiResponse(
|
||||
success = false,
|
||||
error = ErrorDto(
|
||||
code = "NOT_FOUND",
|
||||
message = "Event not found"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
} catch (e: Exception) {
|
||||
ApiResponse(
|
||||
success = false,
|
||||
error = ErrorDto(
|
||||
code = "INTERNAL_ERROR",
|
||||
message = "Failed to retrieve event: ${e.message}"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+185
@@ -0,0 +1,185 @@
|
||||
package at.mocode.events.application.usecase
|
||||
|
||||
import at.mocode.core.domain.model.ApiResponse
|
||||
import at.mocode.core.domain.model.ErrorDto
|
||||
import at.mocode.events.domain.model.Veranstaltung
|
||||
import at.mocode.events.domain.repository.VeranstaltungRepository
|
||||
import at.mocode.core.domain.model.SparteE
|
||||
import at.mocode.core.utils.validation.ValidationResult
|
||||
import at.mocode.core.utils.validation.ValidationError
|
||||
import com.benasher44.uuid.Uuid
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlinx.datetime.LocalDate
|
||||
|
||||
/**
|
||||
* Use case for updating existing events (Veranstaltung).
|
||||
*
|
||||
* This use case handles the business logic for updating events,
|
||||
* including validation and persistence.
|
||||
*/
|
||||
class UpdateVeranstaltungUseCase(
|
||||
private val veranstaltungRepository: VeranstaltungRepository
|
||||
) {
|
||||
|
||||
/**
|
||||
* Request data for updating an event.
|
||||
*/
|
||||
data class UpdateVeranstaltungRequest(
|
||||
val veranstaltungId: Uuid,
|
||||
val name: String,
|
||||
val beschreibung: String? = null,
|
||||
val startDatum: LocalDate,
|
||||
val endDatum: LocalDate,
|
||||
val ort: String,
|
||||
val veranstalterVereinId: Uuid,
|
||||
val sparten: List<SparteE> = emptyList(),
|
||||
val istAktiv: Boolean = true,
|
||||
val istOeffentlich: Boolean = true,
|
||||
val maxTeilnehmer: Int? = null,
|
||||
val anmeldeschluss: LocalDate? = null
|
||||
)
|
||||
|
||||
/**
|
||||
* Response data containing the updated event.
|
||||
*/
|
||||
data class UpdateVeranstaltungResponse(
|
||||
val veranstaltung: Veranstaltung
|
||||
)
|
||||
|
||||
/**
|
||||
* Executes the update event use case.
|
||||
*
|
||||
* @param request The request containing updated event data
|
||||
* @return ApiResponse with the updated event or error information
|
||||
*/
|
||||
suspend fun execute(request: UpdateVeranstaltungRequest): ApiResponse<UpdateVeranstaltungResponse> {
|
||||
return try {
|
||||
// Check if event exists
|
||||
val existingVeranstaltung = veranstaltungRepository.findById(request.veranstaltungId)
|
||||
if (existingVeranstaltung == null) {
|
||||
return ApiResponse(
|
||||
success = false,
|
||||
error = ErrorDto(
|
||||
code = "NOT_FOUND",
|
||||
message = "Event not found"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// Validate the request
|
||||
val validationResult = validateRequest(request)
|
||||
if (!validationResult.isValid()) {
|
||||
val errors = (validationResult as ValidationResult.Invalid).errors
|
||||
return ApiResponse(
|
||||
success = false,
|
||||
error = ErrorDto(
|
||||
code = "VALIDATION_ERROR",
|
||||
message = "Invalid input data",
|
||||
details = errors.associate { it.field to it.message }
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// Create updated domain object
|
||||
val updatedVeranstaltung = existingVeranstaltung.copy(
|
||||
name = request.name.trim(),
|
||||
beschreibung = request.beschreibung?.trim(),
|
||||
startDatum = request.startDatum,
|
||||
endDatum = request.endDatum,
|
||||
ort = request.ort.trim(),
|
||||
veranstalterVereinId = request.veranstalterVereinId,
|
||||
sparten = request.sparten,
|
||||
istAktiv = request.istAktiv,
|
||||
istOeffentlich = request.istOeffentlich,
|
||||
maxTeilnehmer = request.maxTeilnehmer,
|
||||
anmeldeschluss = request.anmeldeschluss,
|
||||
updatedAt = Clock.System.now()
|
||||
)
|
||||
|
||||
// Validate the domain object
|
||||
val domainValidationErrors = updatedVeranstaltung.validate()
|
||||
if (domainValidationErrors.isNotEmpty()) {
|
||||
return ApiResponse(
|
||||
success = false,
|
||||
error = ErrorDto(
|
||||
code = "DOMAIN_VALIDATION_ERROR",
|
||||
message = "Domain validation failed",
|
||||
details = domainValidationErrors.mapIndexed { index, error ->
|
||||
"error_$index" to error
|
||||
}.toMap()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// Save the updated event
|
||||
val savedVeranstaltung = veranstaltungRepository.save(updatedVeranstaltung)
|
||||
|
||||
ApiResponse(
|
||||
success = true,
|
||||
data = UpdateVeranstaltungResponse(savedVeranstaltung)
|
||||
)
|
||||
|
||||
} catch (e: Exception) {
|
||||
ApiResponse(
|
||||
success = false,
|
||||
error = ErrorDto(
|
||||
code = "INTERNAL_ERROR",
|
||||
message = "Failed to update event: ${e.message}"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the update event request.
|
||||
*/
|
||||
private fun validateRequest(request: UpdateVeranstaltungRequest): ValidationResult {
|
||||
val errors = mutableListOf<ValidationError>()
|
||||
|
||||
// Validate name
|
||||
if (request.name.isBlank()) {
|
||||
errors.add(ValidationError("name", "Event name is required"))
|
||||
} else if (request.name.length > 255) {
|
||||
errors.add(ValidationError("name", "Event name must not exceed 255 characters"))
|
||||
}
|
||||
|
||||
// Validate location
|
||||
if (request.ort.isBlank()) {
|
||||
errors.add(ValidationError("ort", "Event location is required"))
|
||||
} else if (request.ort.length > 255) {
|
||||
errors.add(ValidationError("ort", "Event location must not exceed 255 characters"))
|
||||
}
|
||||
|
||||
// Validate dates
|
||||
if (request.endDatum < request.startDatum) {
|
||||
errors.add(ValidationError("endDatum", "End date cannot be before start date"))
|
||||
}
|
||||
|
||||
// Validate registration deadline
|
||||
request.anmeldeschluss?.let { deadline ->
|
||||
if (deadline > request.startDatum) {
|
||||
errors.add(ValidationError("anmeldeschluss", "Registration deadline cannot be after event start date"))
|
||||
}
|
||||
}
|
||||
|
||||
// Validate max participants
|
||||
request.maxTeilnehmer?.let { max ->
|
||||
if (max <= 0) {
|
||||
errors.add(ValidationError("maxTeilnehmer", "Maximum participants must be positive"))
|
||||
}
|
||||
}
|
||||
|
||||
// Validate description length
|
||||
request.beschreibung?.let { desc ->
|
||||
if (desc.length > 5000) {
|
||||
errors.add(ValidationError("beschreibung", "Description must not exceed 5000 characters"))
|
||||
}
|
||||
}
|
||||
|
||||
return if (errors.isEmpty()) {
|
||||
ValidationResult.Valid
|
||||
} else {
|
||||
ValidationResult.Invalid(errors)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
plugins {
|
||||
kotlin("jvm")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.core.coreDomain)
|
||||
implementation(projects.core.coreUtils)
|
||||
testImplementation(projects.platform.platformTesting)
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package at.mocode.events
|
||||
|
||||
/**
|
||||
* Simple Event Management class for testing KMP configuration
|
||||
*/
|
||||
class EventManagement {
|
||||
fun createEvent(name: String): String {
|
||||
return "Event created: $name"
|
||||
}
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val eventManager = EventManagement()
|
||||
println(eventManager.createEvent("Test Event"))
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
package at.mocode.events.domain.model
|
||||
|
||||
import at.mocode.core.domain.model.SparteE
|
||||
import at.mocode.core.domain.serialization.KotlinInstantSerializer
|
||||
import at.mocode.core.domain.serialization.KotlinLocalDateSerializer
|
||||
import at.mocode.core.domain.serialization.UuidSerializer
|
||||
import com.benasher44.uuid.Uuid
|
||||
import com.benasher44.uuid.uuid4
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlinx.datetime.Instant
|
||||
import kotlinx.datetime.LocalDate
|
||||
import kotlinx.datetime.TimeZone
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Domain model representing an event/competition in the event management system.
|
||||
*
|
||||
* This entity represents a sporting event that can contain multiple tournaments
|
||||
* and competitions. It serves as the main aggregate root for event planning.
|
||||
*
|
||||
* @property veranstaltungId Unique internal identifier for this event (UUID).
|
||||
* @property name Name of the event.
|
||||
* @property beschreibung Description of the event.
|
||||
* @property startDatum Start date of the event.
|
||||
* @property endDatum End date of the event.
|
||||
* @property ort Location where the event takes place.
|
||||
* @property veranstalterVereinId ID of the organizing club/association.
|
||||
* @property sparten List of sport disciplines included in this event.
|
||||
* @property istAktiv Whether the event is currently active.
|
||||
* @property istOeffentlich Whether the event is public.
|
||||
* @property maxTeilnehmer Maximum number of participants (optional).
|
||||
* @property anmeldeschluss Registration deadline.
|
||||
* @property createdAt Timestamp when this record was created.
|
||||
* @property updatedAt Timestamp when this record was last updated.
|
||||
*/
|
||||
@Serializable
|
||||
data class Veranstaltung(
|
||||
@Serializable(with = UuidSerializer::class)
|
||||
val veranstaltungId: Uuid = uuid4(),
|
||||
|
||||
// Basic Information
|
||||
var name: String,
|
||||
var beschreibung: String? = null,
|
||||
|
||||
// Dates
|
||||
@Serializable(with = KotlinLocalDateSerializer::class)
|
||||
var startDatum: LocalDate,
|
||||
@Serializable(with = KotlinLocalDateSerializer::class)
|
||||
var endDatum: LocalDate,
|
||||
|
||||
// Location and Organization
|
||||
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,
|
||||
|
||||
@Serializable(with = KotlinLocalDateSerializer::class)
|
||||
var anmeldeschluss: LocalDate? = null,
|
||||
|
||||
// Audit Fields
|
||||
@Serializable(with = KotlinInstantSerializer::class)
|
||||
val createdAt: Instant = Clock.System.now(),
|
||||
@Serializable(with = KotlinInstantSerializer::class)
|
||||
var updatedAt: Instant = Clock.System.now()
|
||||
) {
|
||||
/**
|
||||
* Checks if the event is currently accepting registrations.
|
||||
*/
|
||||
fun isRegistrationOpen(): Boolean {
|
||||
// Simplified implementation - can be enhanced with proper date comparison
|
||||
return istAktiv && anmeldeschluss != null
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the duration of the event in days.
|
||||
*/
|
||||
fun getDurationInDays(): Int {
|
||||
return (endDatum.toEpochDays() - startDatum.toEpochDays()).toInt() + 1
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the event spans multiple days.
|
||||
*/
|
||||
fun isMultiDay(): Boolean {
|
||||
return startDatum != endDatum
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that the event data is consistent.
|
||||
*/
|
||||
fun validate(): List<String> {
|
||||
val errors = mutableListOf<String>()
|
||||
|
||||
if (name.isBlank()) {
|
||||
errors.add("Event name is required")
|
||||
}
|
||||
|
||||
if (ort.isBlank()) {
|
||||
errors.add("Event location is required")
|
||||
}
|
||||
|
||||
if (endDatum < startDatum) {
|
||||
errors.add("End date cannot be before start date")
|
||||
}
|
||||
|
||||
anmeldeschluss?.let { deadline ->
|
||||
if (deadline > startDatum) {
|
||||
errors.add("Registration deadline cannot be after event start date")
|
||||
}
|
||||
}
|
||||
|
||||
maxTeilnehmer?.let { max ->
|
||||
if (max <= 0) {
|
||||
errors.add("Maximum participants must be positive")
|
||||
}
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a copy of this event with updated timestamp.
|
||||
*/
|
||||
fun withUpdatedTimestamp(): Veranstaltung {
|
||||
return this.copy(updatedAt = Clock.System.now())
|
||||
}
|
||||
}
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
package at.mocode.events.domain.repository
|
||||
|
||||
import at.mocode.events.domain.model.Veranstaltung
|
||||
import com.benasher44.uuid.Uuid
|
||||
import kotlinx.datetime.LocalDate
|
||||
|
||||
/**
|
||||
* Repository interface for Veranstaltung (Event) entities.
|
||||
*
|
||||
* This interface defines the contract for data access operations
|
||||
* related to events in the event management bounded context.
|
||||
*/
|
||||
interface VeranstaltungRepository {
|
||||
|
||||
/**
|
||||
* Finds an event by its unique identifier.
|
||||
*
|
||||
* @param id The unique identifier of the event
|
||||
* @return The event if found, null otherwise
|
||||
*/
|
||||
suspend fun findById(id: Uuid): Veranstaltung?
|
||||
|
||||
/**
|
||||
* Finds events by name (partial match).
|
||||
*
|
||||
* @param searchTerm The search term to match against event names
|
||||
* @param limit Maximum number of results to return
|
||||
* @return List of matching events
|
||||
*/
|
||||
suspend fun findByName(searchTerm: String, limit: Int = 50): List<Veranstaltung>
|
||||
|
||||
/**
|
||||
* Finds events organized by a specific club/association.
|
||||
*
|
||||
* @param vereinId The ID of the organizing club
|
||||
* @param activeOnly Whether to return only active events
|
||||
* @return List of events organized by the specified club
|
||||
*/
|
||||
suspend fun findByVeranstalterVereinId(vereinId: Uuid, activeOnly: Boolean = true): List<Veranstaltung>
|
||||
|
||||
/**
|
||||
* Finds events within a date range.
|
||||
*
|
||||
* @param startDate The earliest start date to include
|
||||
* @param endDate The latest end date to include
|
||||
* @param activeOnly Whether to return only active events
|
||||
* @return List of events within the specified date range
|
||||
*/
|
||||
suspend fun findByDateRange(startDate: LocalDate, endDate: LocalDate, activeOnly: Boolean = true): List<Veranstaltung>
|
||||
|
||||
/**
|
||||
* Finds events starting on a specific date.
|
||||
*
|
||||
* @param date The date to search for
|
||||
* @param activeOnly Whether to return only active events
|
||||
* @return List of events starting on the specified date
|
||||
*/
|
||||
suspend fun findByStartDate(date: LocalDate, activeOnly: Boolean = true): List<Veranstaltung>
|
||||
|
||||
/**
|
||||
* Finds all active events.
|
||||
*
|
||||
* @param limit Maximum number of results to return
|
||||
* @param offset Number of results to skip
|
||||
* @return List of active events
|
||||
*/
|
||||
suspend fun findAllActive(limit: Int = 100, offset: Int = 0): List<Veranstaltung>
|
||||
|
||||
/**
|
||||
* Finds public events (events that are open to public registration).
|
||||
*
|
||||
* @param activeOnly Whether to return only active events
|
||||
* @return List of public events
|
||||
*/
|
||||
suspend fun findPublicEvents(activeOnly: Boolean = true): List<Veranstaltung>
|
||||
|
||||
/**
|
||||
* Saves an event (insert or update).
|
||||
*
|
||||
* @param veranstaltung The event to save
|
||||
* @return The saved event
|
||||
*/
|
||||
suspend fun save(veranstaltung: Veranstaltung): Veranstaltung
|
||||
|
||||
/**
|
||||
* Deletes an event by its ID.
|
||||
*
|
||||
* @param id The unique identifier of the event to delete
|
||||
* @return True if the event was deleted, false if not found
|
||||
*/
|
||||
suspend fun delete(id: Uuid): Boolean
|
||||
|
||||
/**
|
||||
* Counts the number of active events.
|
||||
*
|
||||
* @return The number of active events
|
||||
*/
|
||||
suspend fun countActive(): Long
|
||||
|
||||
/**
|
||||
* Counts events organized by a specific club.
|
||||
*
|
||||
* @param vereinId The ID of the organizing club
|
||||
* @param activeOnly Whether to count only active events
|
||||
* @return The number of events organized by the specified club
|
||||
*/
|
||||
suspend fun countByVeranstalterVereinId(vereinId: Uuid, activeOnly: Boolean = true): Long
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
plugins {
|
||||
kotlin("jvm")
|
||||
kotlin("plugin.spring")
|
||||
kotlin("plugin.jpa") version "2.1.20"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.platform.platformDependencies)
|
||||
|
||||
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("org.springframework.boot:spring-boot-starter-data-jpa")
|
||||
implementation("org.postgresql:postgresql")
|
||||
|
||||
testImplementation(projects.platform.platformTesting)
|
||||
}
|
||||
+188
@@ -0,0 +1,188 @@
|
||||
package at.mocode.events.infrastructure.persistence
|
||||
|
||||
import at.mocode.core.domain.model.SparteE
|
||||
import at.mocode.events.domain.model.Veranstaltung
|
||||
import at.mocode.events.domain.repository.VeranstaltungRepository
|
||||
import at.mocode.core.utils.database.DatabaseFactory
|
||||
import com.benasher44.uuid.Uuid
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlinx.datetime.LocalDate
|
||||
import kotlinx.serialization.json.Json
|
||||
import org.jetbrains.exposed.sql.*
|
||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
||||
import org.jetbrains.exposed.sql.statements.UpdateBuilder
|
||||
|
||||
/**
|
||||
* Exposed-based implementation of VeranstaltungRepository.
|
||||
*
|
||||
* This implementation provides data persistence for Veranstaltung entities
|
||||
* using the Exposed SQL framework and PostgreSQL database.
|
||||
*/
|
||||
class VeranstaltungRepositoryImpl : VeranstaltungRepository {
|
||||
|
||||
override suspend fun findById(id: Uuid): Veranstaltung? = DatabaseFactory.dbQuery {
|
||||
VeranstaltungTable.selectAll().where { VeranstaltungTable.id eq id }
|
||||
.map { rowToVeranstaltung(it) }
|
||||
.singleOrNull()
|
||||
}
|
||||
|
||||
override suspend fun findByName(searchTerm: String, limit: Int): List<Veranstaltung> = DatabaseFactory.dbQuery {
|
||||
val searchPattern = "%$searchTerm%"
|
||||
VeranstaltungTable.selectAll().where { VeranstaltungTable.name like searchPattern }
|
||||
.orderBy(VeranstaltungTable.startDatum, SortOrder.DESC)
|
||||
.limit(limit)
|
||||
.map { rowToVeranstaltung(it) }
|
||||
}
|
||||
|
||||
override suspend fun findByVeranstalterVereinId(vereinId: Uuid, activeOnly: Boolean): List<Veranstaltung> = DatabaseFactory.dbQuery {
|
||||
val query = VeranstaltungTable.selectAll().where { VeranstaltungTable.veranstalterVereinId eq vereinId }
|
||||
|
||||
if (activeOnly) {
|
||||
query.andWhere { VeranstaltungTable.istAktiv eq true }
|
||||
} else {
|
||||
query
|
||||
}.orderBy(VeranstaltungTable.startDatum, SortOrder.DESC)
|
||||
.map { rowToVeranstaltung(it) }
|
||||
}
|
||||
|
||||
override suspend fun findByDateRange(startDate: LocalDate, endDate: LocalDate, activeOnly: Boolean): List<Veranstaltung> = DatabaseFactory.dbQuery {
|
||||
val query = VeranstaltungTable.selectAll().where {
|
||||
(VeranstaltungTable.startDatum greaterEq startDate) and
|
||||
(VeranstaltungTable.endDatum lessEq endDate)
|
||||
}
|
||||
|
||||
if (activeOnly) {
|
||||
query.andWhere { VeranstaltungTable.istAktiv eq true }
|
||||
} else {
|
||||
query
|
||||
}.orderBy(VeranstaltungTable.startDatum)
|
||||
.map { rowToVeranstaltung(it) }
|
||||
}
|
||||
|
||||
override suspend fun findByStartDate(date: LocalDate, activeOnly: Boolean): List<Veranstaltung> = DatabaseFactory.dbQuery {
|
||||
val query = VeranstaltungTable.selectAll().where { VeranstaltungTable.startDatum eq date }
|
||||
|
||||
if (activeOnly) {
|
||||
query.andWhere { VeranstaltungTable.istAktiv eq true }
|
||||
} else {
|
||||
query
|
||||
}.orderBy(VeranstaltungTable.name)
|
||||
.map { rowToVeranstaltung(it) }
|
||||
}
|
||||
|
||||
override suspend fun findAllActive(limit: Int, offset: Int): List<Veranstaltung> = DatabaseFactory.dbQuery {
|
||||
VeranstaltungTable.selectAll().where { VeranstaltungTable.istAktiv eq true }
|
||||
.orderBy(VeranstaltungTable.startDatum, SortOrder.DESC)
|
||||
.limit(limit, offset.toLong())
|
||||
.map { rowToVeranstaltung(it) }
|
||||
}
|
||||
|
||||
override suspend fun findPublicEvents(activeOnly: Boolean): List<Veranstaltung> = DatabaseFactory.dbQuery {
|
||||
val query = VeranstaltungTable.selectAll().where { VeranstaltungTable.istOeffentlich eq true }
|
||||
|
||||
if (activeOnly) {
|
||||
query.andWhere { VeranstaltungTable.istAktiv eq true }
|
||||
} else {
|
||||
query
|
||||
}.orderBy(VeranstaltungTable.startDatum, SortOrder.DESC)
|
||||
.map { rowToVeranstaltung(it) }
|
||||
}
|
||||
|
||||
override suspend fun save(veranstaltung: Veranstaltung): Veranstaltung = DatabaseFactory.dbQuery {
|
||||
val now = Clock.System.now()
|
||||
val updatedVeranstaltung = veranstaltung.copy(updatedAt = now)
|
||||
|
||||
// Check if a record exists
|
||||
val existingRecord = VeranstaltungTable.selectAll()
|
||||
.where { VeranstaltungTable.id eq veranstaltung.veranstaltungId }
|
||||
.singleOrNull()
|
||||
|
||||
if (existingRecord != null) {
|
||||
// Update existing record
|
||||
VeranstaltungTable.update({ VeranstaltungTable.id eq veranstaltung.veranstaltungId }) {
|
||||
veranstaltungToStatement(it, updatedVeranstaltung)
|
||||
}
|
||||
updatedVeranstaltung
|
||||
} else {
|
||||
// Insert a new record
|
||||
VeranstaltungTable.insert {
|
||||
it[id] = veranstaltung.veranstaltungId
|
||||
veranstaltungToStatement(it, updatedVeranstaltung)
|
||||
}
|
||||
updatedVeranstaltung
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun delete(id: Uuid): Boolean = DatabaseFactory.dbQuery {
|
||||
val deletedRows = VeranstaltungTable.deleteWhere { VeranstaltungTable.id eq id }
|
||||
deletedRows > 0
|
||||
}
|
||||
|
||||
override suspend fun countActive(): Long = DatabaseFactory.dbQuery {
|
||||
VeranstaltungTable.selectAll().where { VeranstaltungTable.istAktiv eq true }
|
||||
.count()
|
||||
}
|
||||
|
||||
override suspend fun countByVeranstalterVereinId(vereinId: Uuid, activeOnly: Boolean): Long = DatabaseFactory.dbQuery {
|
||||
val query = VeranstaltungTable.selectAll().where { VeranstaltungTable.veranstalterVereinId eq vereinId }
|
||||
|
||||
if (activeOnly) {
|
||||
query.andWhere { VeranstaltungTable.istAktiv eq true }
|
||||
} else {
|
||||
query
|
||||
}.count()
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a database row to a Veranstaltung domain object.
|
||||
*/
|
||||
private fun rowToVeranstaltung(row: ResultRow): Veranstaltung {
|
||||
// Parse sparten from JSON string
|
||||
val spartenJson = row[VeranstaltungTable.sparten]
|
||||
val sparten = if (spartenJson.isNotBlank()) {
|
||||
try {
|
||||
Json.decodeFromString<List<SparteE>>(spartenJson)
|
||||
} catch (_: Exception) {
|
||||
emptyList()
|
||||
}
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
|
||||
return Veranstaltung(
|
||||
veranstaltungId = row[VeranstaltungTable.id].value,
|
||||
name = row[VeranstaltungTable.name],
|
||||
beschreibung = row[VeranstaltungTable.beschreibung],
|
||||
startDatum = row[VeranstaltungTable.startDatum],
|
||||
endDatum = row[VeranstaltungTable.endDatum],
|
||||
ort = row[VeranstaltungTable.ort],
|
||||
veranstalterVereinId = row[VeranstaltungTable.veranstalterVereinId],
|
||||
sparten = sparten,
|
||||
istAktiv = row[VeranstaltungTable.istAktiv],
|
||||
istOeffentlich = row[VeranstaltungTable.istOeffentlich],
|
||||
maxTeilnehmer = row[VeranstaltungTable.maxTeilnehmer],
|
||||
anmeldeschluss = row[VeranstaltungTable.anmeldeschluss],
|
||||
createdAt = row[VeranstaltungTable.createdAt],
|
||||
updatedAt = row[VeranstaltungTable.updatedAt]
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps a Veranstaltung domain object to database statement values.
|
||||
*/
|
||||
private fun veranstaltungToStatement(statement: UpdateBuilder<*>, veranstaltung: Veranstaltung) {
|
||||
statement[VeranstaltungTable.name] = veranstaltung.name
|
||||
statement[VeranstaltungTable.beschreibung] = veranstaltung.beschreibung
|
||||
statement[VeranstaltungTable.startDatum] = veranstaltung.startDatum
|
||||
statement[VeranstaltungTable.endDatum] = veranstaltung.endDatum
|
||||
statement[VeranstaltungTable.ort] = veranstaltung.ort
|
||||
statement[VeranstaltungTable.veranstalterVereinId] = veranstaltung.veranstalterVereinId
|
||||
statement[VeranstaltungTable.sparten] = Json.encodeToString(veranstaltung.sparten)
|
||||
statement[VeranstaltungTable.istAktiv] = veranstaltung.istAktiv
|
||||
statement[VeranstaltungTable.istOeffentlich] = veranstaltung.istOeffentlich
|
||||
statement[VeranstaltungTable.maxTeilnehmer] = veranstaltung.maxTeilnehmer
|
||||
statement[VeranstaltungTable.anmeldeschluss] = veranstaltung.anmeldeschluss
|
||||
statement[VeranstaltungTable.createdAt] = veranstaltung.createdAt
|
||||
statement[VeranstaltungTable.updatedAt] = veranstaltung.updatedAt
|
||||
}
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
package at.mocode.events.infrastructure.persistence
|
||||
|
||||
import at.mocode.core.domain.model.SparteE
|
||||
import org.jetbrains.exposed.dao.id.UUIDTable
|
||||
import org.jetbrains.exposed.sql.kotlin.datetime.date
|
||||
import org.jetbrains.exposed.sql.kotlin.datetime.timestamp
|
||||
|
||||
/**
|
||||
* Database table definition for events (Veranstaltung) in the event-management context.
|
||||
*
|
||||
* This table stores all event information including dates, location,
|
||||
* organization details, and administrative information.
|
||||
*/
|
||||
object VeranstaltungTable : UUIDTable("veranstaltungen") {
|
||||
|
||||
// Basic Information
|
||||
val name = varchar("name", 255)
|
||||
val beschreibung = text("beschreibung").nullable()
|
||||
|
||||
// Dates
|
||||
val startDatum = date("start_datum")
|
||||
val endDatum = date("end_datum")
|
||||
val anmeldeschluss = date("anmeldeschluss").nullable()
|
||||
|
||||
// Location and Organization
|
||||
val ort = varchar("ort", 255)
|
||||
val veranstalterVereinId = uuid("veranstalter_verein_id")
|
||||
|
||||
// Event Details
|
||||
val sparten = text("sparten") // JSON array of SparteE values
|
||||
val istAktiv = bool("ist_aktiv").default(true)
|
||||
val istOeffentlich = bool("ist_oeffentlich").default(true)
|
||||
val maxTeilnehmer = integer("max_teilnehmer").nullable()
|
||||
|
||||
// Audit Fields
|
||||
val createdAt = timestamp("created_at")
|
||||
val updatedAt = timestamp("updated_at")
|
||||
|
||||
init {
|
||||
// Indexes for performance
|
||||
index(false, name)
|
||||
index(false, startDatum)
|
||||
index(false, endDatum)
|
||||
index(false, veranstalterVereinId)
|
||||
index(false, istAktiv)
|
||||
index(false, istOeffentlich)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
plugins {
|
||||
kotlin("jvm")
|
||||
kotlin("plugin.spring")
|
||||
id("org.springframework.boot")
|
||||
}
|
||||
|
||||
springBoot {
|
||||
mainClass.set("at.mocode.events.service.EventsServiceApplicationKt")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.platform.platformDependencies)
|
||||
|
||||
implementation(projects.events.eventsDomain)
|
||||
implementation(projects.events.eventsApplication)
|
||||
implementation(projects.events.eventsInfrastructure)
|
||||
implementation(projects.events.eventsApi)
|
||||
|
||||
implementation(projects.infrastructure.auth.authClient)
|
||||
implementation(projects.infrastructure.cache.redisCache)
|
||||
implementation(projects.infrastructure.messaging.messagingClient)
|
||||
implementation(projects.infrastructure.monitoring.monitoringClient)
|
||||
|
||||
implementation("org.springframework.boot:spring-boot-starter-web")
|
||||
implementation("org.springframework.boot:spring-boot-starter-validation")
|
||||
implementation("org.springframework.boot:spring-boot-starter-actuator")
|
||||
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui")
|
||||
|
||||
runtimeOnly("org.postgresql:postgresql")
|
||||
|
||||
testImplementation(projects.platform.platformTesting)
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package at.mocode.events.service
|
||||
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication
|
||||
import org.springframework.boot.runApplication
|
||||
|
||||
/**
|
||||
* Main application class for the Events Service.
|
||||
*
|
||||
* This service provides APIs for managing events and competitions.
|
||||
*/
|
||||
@SpringBootApplication
|
||||
class EventsServiceApplication
|
||||
|
||||
/**
|
||||
* Main entry point for the Events Service application.
|
||||
*/
|
||||
fun main(args: Array<String>) {
|
||||
runApplication<EventsServiceApplication>(*args)
|
||||
}
|
||||
Reference in New Issue
Block a user