fixing web-app
This commit is contained in:
@@ -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).
|
||||
@@ -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)
|
||||
}
|
||||
-333
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-173
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
-108
@@ -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}"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
-68
@@ -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}"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
-185
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
-108
@@ -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)
|
||||
}
|
||||
-188
@@ -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
|
||||
}
|
||||
}
|
||||
-48
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
-19
@@ -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)
|
||||
}
|
||||
-104
@@ -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>
|
||||
Reference in New Issue
Block a user