fixing web-app

This commit is contained in:
stefan
2025-09-24 14:21:57 +02:00
parent cd2b0796a6
commit 1c4184809a
156 changed files with 440 additions and 1708 deletions
-457
View File
@@ -1,457 +0,0 @@
# Events Module
## Überblick
Das Events-Modul ist eine umfassende Lösung zur Verwaltung von Pferdesportveranstaltungen. Es implementiert eine saubere Architektur mit Domain-Driven Design und bietet vollständige CRUD-Operationen sowie erweiterte Geschäftslogik für die Veranstaltungsplanung und -verwaltung.
## Funktionalität
### Verwaltete Entität
#### Veranstaltung (Event)
- **Grundinformationen**: Name, Beschreibung
- **Terminverwaltung**: Startdatum, Enddatum, Anmeldeschluss
- **Ort und Organisation**: Veranstaltungsort, Veranstalter-Verein-ID
- **Veranstaltungsdetails**: Sparten, Aktivitätsstatus, Öffentlichkeit, maximale Teilnehmerzahl
- **Audit-Felder**: Erstellungs- und Aktualisierungszeitstempel
- **Geschäftslogik**: Validierung, Anmeldestatus, Dauernberechnung
### Geschäftsoperationen
Das Modul bietet 10+ spezialisierte Repository-Operationen:
#### Basis-CRUD-Operationen
- `findById(id)` - Veranstaltung nach UUID suchen
- `save(veranstaltung)` - Veranstaltung speichern (erstellen/aktualisieren)
- `delete(id)` - Veranstaltung löschen
#### Such-Operationen
- `findByName(searchTerm, limit)` - Nach Namen suchen (Teilübereinstimmung)
- `findByVeranstalterVereinId(vereinId, activeOnly)` - Veranstaltungen eines Vereins
- `findAllActive(limit, offset)` - Alle aktiven Veranstaltungen
- `findPublicEvents(activeOnly)` - Öffentliche Veranstaltungen
#### Datumsbasierte Abfragen
- `findByDateRange(startDate, endDate, activeOnly)` - Veranstaltungen in Datumsbereich
- `findByStartDate(date, activeOnly)` - Veranstaltungen nach Startdatum
#### Zähl-Operationen
- `countActive()` - Anzahl aktiver Veranstaltungen
- `countByVeranstalterVereinId(vereinId, activeOnly)` - Anzahl Veranstaltungen pro Verein
## Architektur
Das Modul folgt der Clean Architecture mit klarer Trennung der Verantwortlichkeiten:
```
events/
├── events-domain/ # Domain Layer
│ ├── model/ # Domain Models
│ │ └── Veranstaltung.kt # Veranstaltungs-Entität mit Geschäftslogik
│ ├── repository/ # Repository Interfaces
│ │ └── VeranstaltungRepository.kt # 10+ Geschäftsoperationen
│ └── EventManagement.kt # Domain Service/Facade
├── events-application/ # Application Layer
│ └── usecase/ # Use Cases
│ ├── CreateVeranstaltungUseCase.kt
│ ├── GetVeranstaltungUseCase.kt
│ ├── UpdateVeranstaltungUseCase.kt
│ └── DeleteVeranstaltungUseCase.kt
├── events-infrastructure/ # Infrastructure Layer
│ └── persistence/ # Database Implementation
│ ├── VeranstaltungRepositoryImpl.kt
│ └── VeranstaltungTable.kt
├── events-api/ # API Layer
│ └── rest/ # REST Controllers
│ └── VeranstaltungController.kt
└── events-service/ # Service Layer
└── EventsServiceApplication.kt
```
### Domain Layer
- **1 Domain Model** mit reichhaltiger Geschäftslogik
- **1 Repository Interface** mit 10+ Geschäftsoperationen
- **Domain Service** für komplexe Veranstaltungslogik
- **Keine Abhängigkeiten** zu anderen Layern
### Application Layer
- **Use Cases** für CRUD-Operationen
- **Orchestrierung** von Domain-Services
- **Anwendungslogik** ohne UI-Abhängigkeiten
### Infrastructure Layer
- **Datenbankzugriff** mit Exposed ORM
- **Repository-Implementierung** mit PostgreSQL
- **Datenbankschema** und Migrationen
### API Layer
- **REST-Controller** für HTTP-Endpunkte
- **DTO-Mapping** zwischen Domain und API
- **Validierung** und Fehlerbehandlung
### Service Layer
- **Spring Boot Anwendung**
- **Dependency Injection** Konfiguration
- **Service-Konfiguration**
## Domain Model Details
### Veranstaltung-Entität
```kotlin
data class Veranstaltung(
val veranstaltungId: Uuid,
// Grundinformationen
var name: String,
var beschreibung: String? = null,
// Termine
var startDatum: LocalDate,
var endDatum: LocalDate,
// Ort und Organisation
var ort: String,
var veranstalterVereinId: Uuid,
// Veranstaltungsdetails
var sparten: List<SparteE> = emptyList(),
var istAktiv: Boolean = true,
var istOeffentlich: Boolean = true,
var maxTeilnehmer: Int? = null,
var anmeldeschluss: LocalDate? = null,
// Audit-Felder
val createdAt: Instant,
var updatedAt: Instant
)
```
### Geschäftslogik-Methoden
- `isRegistrationOpen()` - Prüfung ob Anmeldung noch möglich ist
- `getDurationInDays()` - Berechnung der Veranstaltungsdauer in Tagen
- `isMultiDay()` - Prüfung ob mehrtägige Veranstaltung
- `validate()` - Datenvalidierung mit Fehlerliste
- `withUpdatedTimestamp()` - Kopie mit aktualisiertem Zeitstempel
### Enumerationen
#### SparteE (Sportsparten)
- `DRESSUR` - Dressurreiten
- `SPRINGEN` - Springreiten
- `VIELSEITIGKEIT` - Vielseitigkeitsreiten
- `FAHREN` - Fahrsport
- `VOLTIGIEREN` - Voltigieren
- `WESTERN` - Westernreiten
- `DISTANZ` - Distanzreiten
## Repository-Operationen
### Erweiterte Such-Features
```kotlin
// Veranstaltungen nach Namen suchen
val events = veranstaltungRepository.findByName("Turnier", limit = 10)
// Veranstaltungen eines Vereins finden
val clubEvents = veranstaltungRepository.findByVeranstalterVereinId(
vereinId = clubId,
activeOnly = true
)
// Veranstaltungen in Datumsbereich suchen
val summerEvents = veranstaltungRepository.findByDateRange(
startDate = LocalDate(2024, 6, 1),
endDate = LocalDate(2024, 8, 31),
activeOnly = true
)
// Öffentliche Veranstaltungen finden
val publicEvents = veranstaltungRepository.findPublicEvents(activeOnly = true)
```
### Datumsbasierte Abfragen
```kotlin
// Veranstaltungen an einem bestimmten Tag
val todayEvents = veranstaltungRepository.findByStartDate(
date = LocalDate.now(),
activeOnly = true
)
// Alle aktiven Veranstaltungen
val activeEvents = veranstaltungRepository.findAllActive(limit = 100)
```
### Statistiken und Zählungen
```kotlin
// Anzahl aktiver Veranstaltungen
val totalActive = veranstaltungRepository.countActive()
// Anzahl Veranstaltungen pro Verein
val clubEventCount = veranstaltungRepository.countByVeranstalterVereinId(
vereinId = clubId,
activeOnly = true
)
```
## Use Cases
### CreateVeranstaltungUseCase
Erstellt eine neue Veranstaltung mit Validierung und Geschäftsregeln.
```kotlin
class CreateVeranstaltungUseCase(
private val veranstaltungRepository: VeranstaltungRepository
) {
suspend fun execute(veranstaltung: Veranstaltung): Veranstaltung {
// Validierung
val errors = veranstaltung.validate()
if (errors.isNotEmpty()) {
throw ValidationException(errors)
}
// Geschäftsregeln prüfen
if (veranstaltung.anmeldeschluss != null &&
veranstaltung.anmeldeschluss!! > veranstaltung.startDatum) {
throw BusinessRuleException("Anmeldeschluss muss vor Veranstaltungsbeginn liegen")
}
return veranstaltungRepository.save(veranstaltung)
}
}
```
### GetVeranstaltungUseCase
Ruft Veranstaltungsinformationen ab mit verschiedenen Suchkriterien.
### UpdateVeranstaltungUseCase
Aktualisiert Veranstaltungsinformationen mit Validierung.
### DeleteVeranstaltungUseCase
Löscht eine Veranstaltung (soft delete durch Deaktivierung).
## API-Endpunkte
Das Events-Modul stellt REST-Endpunkte über den VeranstaltungController bereit:
- `GET /api/events` - Alle aktiven Veranstaltungen abrufen
- `GET /api/events/{id}` - Veranstaltung nach ID abrufen
- `GET /api/events/search?name={name}` - Veranstaltungen nach Namen suchen
- `GET /api/events/club/{clubId}` - Veranstaltungen eines Vereins
- `GET /api/events/public` - Öffentliche Veranstaltungen
- `GET /api/events/date-range?start={start}&end={end}` - Veranstaltungen in Datumsbereich
- `GET /api/events/date/{date}` - Veranstaltungen an einem bestimmten Tag
- `POST /api/events` - Neue Veranstaltung erstellen
- `PUT /api/events/{id}` - Veranstaltung aktualisieren
- `DELETE /api/events/{id}` - Veranstaltung löschen
## Konfiguration
### Datenbankschema
Das Modul verwendet eine `events`-Tabelle mit folgenden Spalten:
- `veranstaltung_id` (UUID, Primary Key)
- `name` (Required)
- `beschreibung` (Text, Optional)
- `start_datum`, `end_datum` (Date, Required)
- `ort` (Required)
- `veranstalter_verein_id` (UUID, Foreign Key)
- `sparten` (JSON Array)
- `ist_aktiv`, `ist_oeffentlich` (Boolean)
- `max_teilnehmer` (Integer, Optional)
- `anmeldeschluss` (Date, Optional)
- `created_at`, `updated_at` (Timestamps)
### Service-Konfiguration
```yaml
# application.yml
events:
service:
name: events-service
port: 8084
database:
url: jdbc:postgresql://localhost:5432/meldestelle
table: events
business-rules:
max-duration-days: 30
min-registration-period-days: 7
allow-past-events: false
```
## Tests
### Integration Tests
Das Modul enthält umfassende Integrationstests:
```kotlin
@Test
fun `should create event with valid data`() {
// Test für Veranstaltungserstellung
}
@Test
fun `should find events by date range`() {
// Test für datumsbasierte Suche
}
@Test
fun `should validate registration deadline`() {
// Test für Anmeldeschluss-Validierung
}
@Test
fun `should find public events only`() {
// Test für öffentliche Veranstaltungen
}
```
### Test-Datenbank
Verwendet H2 In-Memory-Datenbank für Tests mit automatischem Schema-Setup.
## Deployment
### Docker
```dockerfile
FROM openjdk:21-jre-slim
COPY events-service.jar app.jar
EXPOSE 8084
ENTRYPOINT ["java", "-jar", "/app.jar"]
```
### Kubernetes
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: events-service
spec:
replicas: 2
selector:
matchLabels:
app: events-service
template:
spec:
containers:
- name: events-service
image: meldestelle/events-service:latest
ports:
- containerPort: 8084
```
## Monitoring
### Metriken
- Anzahl aktiver Veranstaltungen
- Anzahl öffentlicher Veranstaltungen
- Durchschnittliche Veranstaltungsdauer
- API-Response-Zeiten
- Datenbankverbindungs-Pool
- Validierungsfehler-Rate
### Health Checks
- Datenbankverbindung
- Service-Verfügbarkeit
- Speicherverbrauch
- Externe System-Verbindungen
## Entwicklung
### Lokale Entwicklung
```bash
# Service starten
./gradlew :events:events-service:bootRun
# Tests ausführen
./gradlew :events:test
# Integration Tests
./gradlew :events:events-service:test
```
### Code-Qualität
- **Kotlin Coding Standards**
- **100% Test Coverage** für Domain Layer
- **Integration Tests** für alle Use Cases
- **API-Dokumentation** mit OpenAPI
## Geschäftsregeln
### Veranstaltungsplanung
1. **Datumsvalidierung**: Enddatum muss nach oder gleich Startdatum sein
2. **Anmeldeschluss**: Muss vor Veranstaltungsbeginn liegen
3. **Teilnehmerbegrenzung**: Maximale Teilnehmerzahl muss positiv sein
4. **Öffentlichkeit**: Private Veranstaltungen nur für Vereinsmitglieder
### Sparten-Management
- Unterstützung für alle österreichischen Pferdesport-Sparten
- Mehrfachauswahl möglich für kombinierte Veranstaltungen
- Sparten-spezifische Validierungsregeln
### Vereins-Integration
- Verknüpfung mit Vereinsverwaltung
- Berechtigung zur Veranstaltungserstellung
- Vereins-spezifische Konfigurationen
## Integration
### Externe Systeme
#### OEPS-Integration
- Synchronisation mit OEPS-Veranstaltungskalender
- Automatische Meldung bei OEPS-relevanten Veranstaltungen
- Import von OEPS-Veranstaltungsdaten
#### FEI-Integration
- Unterstützung für internationale Veranstaltungen
- FEI-Regularien und -Standards
- Automatische Klassifizierung
### Interne Module
#### Members-Modul
- Teilnehmerverwaltung
- Anmeldestatus-Tracking
- Mitgliedschaftsvalidierung
#### Horses-Modul
- Pferdeanmeldungen
- Eignung für Sparten
- Registrierungsstatus
## Zukünftige Erweiterungen
1. **Anmeldungssystem** - Vollständiges Teilnehmeranmeldungssystem
2. **Zeitplanung** - Detaillierte Zeitpläne und Startlisten
3. **Ergebniserfassung** - Integration mit Bewertungssystemen
4. **Livestreaming** - Integration mit Streaming-Plattformen
5. **Mobile App** - Mobile Anwendung für Teilnehmer
6. **Zahlungsintegration** - Startgebühren und Zahlungsabwicklung
7. **Wetterintegration** - Wettervorhersage und -warnungen
8. **Kapazitätsmanagement** - Stallplätze und Parkplätze
9. **Catering-Management** - Verpflegung und Bewirtung
10. **Sponsoring** - Sponsoren-Management und -präsentation
---
**Letzte Aktualisierung**: 25. Juli 2025
Für weitere Informationen zur Gesamtarchitektur siehe [README.md](../README.md).
-42
View File
@@ -1,42 +0,0 @@
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)
}
application {
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)
testImplementation(projects.platform.platformTesting)
testImplementation(libs.ktor.server.tests)
}
@@ -1,333 +0,0 @@
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
)
}
@@ -1,32 +0,0 @@
plugins {
// KORREKTUR: Von 'kotlin("jvm")' zu Multiplattform wechseln.
alias(libs.plugins.kotlin.multiplatform)
alias(libs.plugins.kotlin.serialization)
}
kotlin {
jvm()
js(IR) {
browser()
}
sourceSets {
val commonMain by getting {
dependencies {
// Hier die jeweiligen Modul-Abhängigkeiten eintragen
// z.B. für events-domain:
implementation(projects.core.coreDomain)
// z.B. für events-application:
// implementation(projects.events.eventsDomain)
}
}
val commonTest by getting {
dependencies {
implementation(kotlin("test"))
implementation(projects.platform.platformTesting)
}
}
}
}
@@ -1,173 +0,0 @@
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.domain.model.ValidationResult
import at.mocode.core.domain.model.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)
}
}
}
@@ -1,108 +0,0 @@
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}"
)
)
}
}
}
@@ -1,68 +0,0 @@
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}"
)
)
}
}
}
@@ -1,185 +0,0 @@
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.domain.model.ValidationResult
import at.mocode.core.domain.model.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)
}
}
}
-9
View File
@@ -1,9 +0,0 @@
plugins {
kotlin("jvm")
}
dependencies {
implementation(projects.core.coreDomain)
implementation(projects.core.coreUtils)
testImplementation(projects.platform.platformTesting)
}
@@ -1,15 +0,0 @@
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"))
}
@@ -1,132 +0,0 @@
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())
}
}
@@ -1,108 +0,0 @@
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
}
@@ -1,24 +0,0 @@
plugins {
alias(libs.plugins.kotlin.jvm)
alias(libs.plugins.kotlin.spring)
alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.ktor)
application
}
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(libs.spring.boot.starter.data.jpa)
implementation(libs.postgresql.driver)
testImplementation(projects.platform.platformTesting)
}
@@ -1,188 +0,0 @@
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
}
}
@@ -1,48 +0,0 @@
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)
}
}
-51
View File
@@ -1,51 +0,0 @@
plugins {
alias(libs.plugins.kotlin.jvm)
alias(libs.plugins.kotlin.spring)
// KORREKTUR: Dieses Plugin ist entscheidend. Es schaltet den `springBoot`-Block
// und alle Spring-Boot-spezifischen Gradle-Tasks frei.
alias(libs.plugins.spring.boot)
// Dependency Management für konsistente Spring-Versionen
alias(libs.plugins.spring.dependencyManagement)
}
// Dieser Block funktioniert jetzt, weil das `springBoot`-Plugin oben aktiviert ist.
springBoot {
mainClass.set("at.mocode.events.service.EventsServiceApplicationKt")
}
dependencies {
// Interne Module
implementation(projects.platform.platformDependencies)
implementation(projects.core.coreUtils)
implementation(projects.events.eventsDomain)
implementation(projects.events.eventsApplication)
implementation(projects.events.eventsInfrastructure)
implementation(projects.events.eventsApi)
// Infrastruktur-Clients
implementation(projects.infrastructure.auth.authClient)
implementation(projects.infrastructure.cache.redisCache)
implementation(projects.infrastructure.messaging.messagingClient)
implementation(projects.infrastructure.monitoring.monitoringClient)
// KORREKTUR: Alle externen Abhängigkeiten werden jetzt über den Version Catalog bezogen.
// Spring Boot Starters
implementation(libs.spring.boot.starter.web)
implementation(libs.spring.boot.starter.validation)
implementation(libs.spring.boot.starter.actuator)
// Datenbank-Treiber
runtimeOnly(libs.postgresql.driver)
// Testing
testImplementation(projects.platform.platformTesting)
testImplementation(libs.spring.boot.starter.test)
testImplementation(libs.logback.classic) // SLF4J provider for tests
}
tasks.test {
useJUnitPlatform()
}
@@ -1,19 +0,0 @@
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)
}
@@ -1,104 +0,0 @@
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.
*
* This configuration ensures that Database.connect() is called properly
* before any Exposed operations are performed.
*/
@Configuration
@Profile("!test")
class EventsDatabaseConfiguration {
private val log = LoggerFactory.getLogger(EventsDatabaseConfiguration::class.java)
@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.createMissingTablesAndColumns(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)
}
}
}
/**
* Test-specific database configuration.
*/
@Configuration
@Profile("test")
class EventsTestDatabaseConfiguration {
private val log = LoggerFactory.getLogger(EventsTestDatabaseConfiguration::class.java)
@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.createMissingTablesAndColumns(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)
}
}
}
@@ -1,10 +0,0 @@
<configuration>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="CONSOLE" />
</root>
</configuration>