fixing web-app
This commit is contained in:
@@ -0,0 +1,336 @@
|
||||
# Masterdata Module
|
||||
|
||||
## Überblick
|
||||
|
||||
Das Masterdata-Modul ist eine umfassende Lösung zur Verwaltung von Stammdaten für Pferdesportveranstaltungen. Es implementiert eine saubere Architektur mit Domain-Driven Design und bietet vollständige CRUD-Operationen für alle Stammdaten-Entitäten.
|
||||
|
||||
## Funktionalität
|
||||
|
||||
### Verwaltete Entitäten
|
||||
|
||||
#### 1. Länder (LandDefinition)
|
||||
- **ISO-Codes**: Alpha-2, Alpha-3 und numerische Codes nach ISO 3166-1
|
||||
- **EU/EWR-Mitgliedschaft**: Tracking der Mitgliedschaft in EU und Europäischem Wirtschaftsraum
|
||||
- **Mehrsprachigkeit**: Deutsche und englische Ländernamen
|
||||
- **Validierung**: Duplikatsprüfung und ISO-Code-Validierung
|
||||
|
||||
#### 2. Bundesländer (BundeslandDefinition)
|
||||
- **OEPS-Codes**: Spezielle Codes für österreichische Bundesländer
|
||||
- **ISO 3166-2 Codes**: Internationale Standardcodes für subnationale Einheiten
|
||||
- **Länder-Zuordnung**: Verknüpfung mit übergeordneten Ländern
|
||||
- **Flexible Struktur**: Unterstützt Bundesländer, Kantone, Regionen
|
||||
|
||||
#### 3. Altersklassen (AltersklasseDefinition)
|
||||
- **Berechtigung**: Komplexe Regeln für Teilnahmeberechtigung
|
||||
- **Sparten-Filter**: Disziplinspezifische Altersklassen (Dressur, Springen, etc.)
|
||||
- **Geschlechts-Filter**: Geschlechtsspezifische Kategorien
|
||||
- **Altersvalidierung**: Automatische Überprüfung der Teilnahmeberechtigung
|
||||
- **OETO-Integration**: Verknüpfung mit österreichischen Turnierordnungsregeln
|
||||
|
||||
#### 4. Turnierplätze (Platz)
|
||||
- **Platztypen**: Dressurplatz, Springplatz, Geländestrecke, etc.
|
||||
- **Abmessungen**: Standardisierte Platzgrößen (20x60m, 20x40m, etc.)
|
||||
- **Bodenarten**: Sand, Gras, Kunststoff, etc.
|
||||
- **Eignung**: Validierung der Eignung für spezifische Disziplinen
|
||||
- **Turnier-Zuordnung**: Organisation nach Turnieren
|
||||
|
||||
## Architektur
|
||||
|
||||
Das Modul folgt der Clean Architecture mit klarer Trennung der Verantwortlichkeiten:
|
||||
|
||||
```
|
||||
masterdata/
|
||||
├── masterdata-domain/ # Domain Layer
|
||||
│ ├── model/ # Domain Models
|
||||
│ └── repository/ # Repository Interfaces
|
||||
├── masterdata-application/ # Application Layer
|
||||
│ └── usecase/ # Use Cases
|
||||
├── masterdata-infrastructure/ # Infrastructure Layer
|
||||
│ └── persistence/ # Database Implementation
|
||||
├── masterdata-api/ # API Layer
|
||||
│ └── rest/ # REST Controllers
|
||||
└── masterdata-service/ # Service Layer
|
||||
├── config/ # Configuration
|
||||
└── resources/db/migration/ # Database Migrations
|
||||
```
|
||||
|
||||
### Domain Layer
|
||||
- **4 Domain Models** mit reichhaltiger Geschäftslogik
|
||||
- **4 Repository Interfaces** mit 60+ Geschäftsoperationen
|
||||
- **Keine Abhängigkeiten** zu anderen Layern
|
||||
|
||||
### Application Layer
|
||||
- **8 Use Cases** mit umfassender Funktionalität
|
||||
- **Validierung**: Eingabevalidierung mit spezifischen Fehlercodes
|
||||
- **Geschäftslogik**: Duplikatsprüfung, Berechtigungsvalidierung
|
||||
|
||||
### Infrastructure Layer
|
||||
- **4 Database Tables** mit Indizes und Constraints
|
||||
- **Repository Implementierungen** mit vollständigen CRUD-Operationen
|
||||
- **Migration Scripts** mit Beispieldaten
|
||||
|
||||
### API Layer
|
||||
- **4 REST Controllers** mit 37 Endpunkten
|
||||
- **DTO Pattern** für saubere API-Verträge
|
||||
- **Fehlerbehandlung** mit strukturierten Antworten
|
||||
|
||||
## API Endpunkte
|
||||
|
||||
### Countries (Länder)
|
||||
```
|
||||
GET /api/masterdata/countries # Alle aktiven Länder
|
||||
GET /api/masterdata/countries/{id} # Land nach ID
|
||||
GET /api/masterdata/countries/iso2/{code} # Land nach ISO Alpha-2
|
||||
GET /api/masterdata/countries/iso3/{code} # Land nach ISO Alpha-3
|
||||
GET /api/masterdata/countries/search # Länder suchen
|
||||
GET /api/masterdata/countries/eu # EU-Mitgliedsländer
|
||||
GET /api/masterdata/countries/ewr # EWR-Mitgliedsländer
|
||||
POST /api/masterdata/countries # Neues Land erstellen
|
||||
PUT /api/masterdata/countries/{id} # Land aktualisieren
|
||||
DELETE /api/masterdata/countries/{id} # Land löschen
|
||||
```
|
||||
|
||||
### Federal States (Bundesländer)
|
||||
```
|
||||
GET /api/masterdata/bundeslaender # Alle aktiven Bundesländer
|
||||
GET /api/masterdata/bundeslaender/{id} # Bundesland nach ID
|
||||
GET /api/masterdata/bundeslaender/oeps/{code} # Bundesland nach OEPS-Code
|
||||
GET /api/masterdata/bundeslaender/iso/{code} # Bundesland nach ISO-Code
|
||||
GET /api/masterdata/bundeslaender/country/{id} # Bundesländer nach Land
|
||||
GET /api/masterdata/bundeslaender/search # Bundesländer suchen
|
||||
GET /api/masterdata/bundeslaender/count/{countryId} # Anzahl nach Land
|
||||
POST /api/masterdata/bundeslaender # Neues Bundesland erstellen
|
||||
PUT /api/masterdata/bundeslaender/{id} # Bundesland aktualisieren
|
||||
DELETE /api/masterdata/bundeslaender/{id} # Bundesland löschen
|
||||
```
|
||||
|
||||
### Age Classes (Altersklassen)
|
||||
```
|
||||
GET /api/masterdata/altersklassen # Alle aktiven Altersklassen
|
||||
GET /api/masterdata/altersklassen/{id} # Altersklasse nach ID
|
||||
GET /api/masterdata/altersklassen/code/{code} # Altersklasse nach Code
|
||||
GET /api/masterdata/altersklassen/search # Altersklassen suchen
|
||||
GET /api/masterdata/altersklassen/age/{age} # Altersklassen für Alter
|
||||
GET /api/masterdata/altersklassen/sparte/{sparte} # Altersklassen nach Sparte
|
||||
GET /api/masterdata/altersklassen/eligible/{id} # Berechtigung prüfen
|
||||
POST /api/masterdata/altersklassen # Neue Altersklasse erstellen
|
||||
PUT /api/masterdata/altersklassen/{id} # Altersklasse aktualisieren
|
||||
DELETE /api/masterdata/altersklassen/{id} # Altersklasse löschen
|
||||
```
|
||||
|
||||
### Venues (Turnierplätze)
|
||||
```
|
||||
GET /api/masterdata/plaetze/{id} # Platz nach ID
|
||||
GET /api/masterdata/plaetze/tournament/{turnierId} # Plätze nach Turnier
|
||||
GET /api/masterdata/plaetze/search # Plätze suchen
|
||||
GET /api/masterdata/plaetze/type/{typ} # Plätze nach Typ
|
||||
GET /api/masterdata/plaetze/ground/{boden} # Plätze nach Boden
|
||||
GET /api/masterdata/plaetze/dimension/{dimension} # Plätze nach Abmessung
|
||||
GET /api/masterdata/plaetze/suitable # Geeignete Plätze
|
||||
GET /api/masterdata/plaetze/count/tournament/{turnierId} # Anzahl nach Turnier
|
||||
GET /api/masterdata/plaetze/count/type/{typ}/tournament/{turnierId} # Anzahl nach Typ
|
||||
GET /api/masterdata/plaetze/grouped/tournament/{turnierId} # Gruppiert nach Typ
|
||||
GET /api/masterdata/plaetze/validate/{id} # Eignung validieren
|
||||
POST /api/masterdata/plaetze # Neuen Platz erstellen
|
||||
PUT /api/masterdata/plaetze/{id} # Platz aktualisieren
|
||||
DELETE /api/masterdata/plaetze/{id} # Platz löschen
|
||||
```
|
||||
|
||||
## Datenbank Schema
|
||||
|
||||
### Land Tabelle
|
||||
```sql
|
||||
CREATE TABLE land (
|
||||
id UUID PRIMARY KEY,
|
||||
iso_alpha2_code VARCHAR(2) NOT NULL UNIQUE,
|
||||
iso_alpha3_code VARCHAR(3) NOT NULL UNIQUE,
|
||||
iso_numerischer_code VARCHAR(3),
|
||||
name_deutsch VARCHAR(100) NOT NULL,
|
||||
name_englisch VARCHAR(100),
|
||||
wappen_url VARCHAR(500),
|
||||
ist_eu_mitglied BOOLEAN,
|
||||
ist_ewr_mitglied BOOLEAN,
|
||||
ist_aktiv BOOLEAN DEFAULT true,
|
||||
sortier_reihenfolge INTEGER,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
```
|
||||
|
||||
### Bundesland Tabelle
|
||||
```sql
|
||||
CREATE TABLE bundesland (
|
||||
id UUID PRIMARY KEY,
|
||||
land_id UUID NOT NULL REFERENCES land(id),
|
||||
oeps_code VARCHAR(10),
|
||||
iso_3166_2_code VARCHAR(10),
|
||||
name VARCHAR(100) NOT NULL,
|
||||
kuerzel VARCHAR(10),
|
||||
wappen_url VARCHAR(500),
|
||||
ist_aktiv BOOLEAN DEFAULT true,
|
||||
sortier_reihenfolge INTEGER,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
```
|
||||
|
||||
### Altersklasse Tabelle
|
||||
```sql
|
||||
CREATE TABLE altersklasse (
|
||||
id UUID PRIMARY KEY,
|
||||
altersklasse_code VARCHAR(50) NOT NULL UNIQUE,
|
||||
bezeichnung VARCHAR(200) NOT NULL,
|
||||
min_alter INTEGER,
|
||||
max_alter INTEGER,
|
||||
stichtag_regel_text VARCHAR(500),
|
||||
sparte_filter VARCHAR(50),
|
||||
geschlecht_filter CHAR(1),
|
||||
oeto_regel_referenz_id UUID,
|
||||
ist_aktiv BOOLEAN DEFAULT true,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
```
|
||||
|
||||
### Platz Tabelle
|
||||
```sql
|
||||
CREATE TABLE platz (
|
||||
id UUID PRIMARY KEY,
|
||||
turnier_id UUID NOT NULL,
|
||||
name VARCHAR(200) NOT NULL,
|
||||
dimension VARCHAR(50),
|
||||
boden VARCHAR(100),
|
||||
typ VARCHAR(50) NOT NULL,
|
||||
ist_aktiv BOOLEAN DEFAULT true,
|
||||
sortier_reihenfolge INTEGER,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
```
|
||||
|
||||
## Verwendung
|
||||
|
||||
### Service starten
|
||||
```bash
|
||||
# Masterdata Service starten
|
||||
./gradlew :masterdata:masterdata-service:bootRun
|
||||
|
||||
# Mit spezifischem Profil
|
||||
./gradlew :masterdata:masterdata-service:bootRun --args='--spring.profiles.active=dev'
|
||||
```
|
||||
|
||||
### API Beispiele
|
||||
|
||||
#### Land erstellen
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/api/masterdata/countries \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"isoAlpha2Code": "AT",
|
||||
"isoAlpha3Code": "AUT",
|
||||
"isoNumerischerCode": "040",
|
||||
"nameDeutsch": "Österreich",
|
||||
"nameEnglisch": "Austria",
|
||||
"istEuMitglied": true,
|
||||
"istEwrMitglied": true
|
||||
}'
|
||||
```
|
||||
|
||||
#### Altersklassen für 16-jährigen Dressurreiter abrufen
|
||||
```bash
|
||||
curl "http://localhost:8080/api/masterdata/altersklassen/age/16?sparte=DRESSUR"
|
||||
```
|
||||
|
||||
#### Geeignete Dressurplätze finden
|
||||
```bash
|
||||
curl "http://localhost:8080/api/masterdata/plaetze/suitable?typ=DRESSURPLATZ&dimension=20x60m"
|
||||
```
|
||||
|
||||
## Konfiguration
|
||||
|
||||
### Umgebungsvariablen
|
||||
```bash
|
||||
# Database
|
||||
MASTERDATA_DB_URL=jdbc:postgresql://localhost:5432/meldestelle
|
||||
MASTERDATA_DB_USERNAME=meldestelle
|
||||
MASTERDATA_DB_PASSWORD=password
|
||||
|
||||
# Cache
|
||||
MASTERDATA_CACHE_ENABLED=true
|
||||
MASTERDATA_CACHE_TTL=3600
|
||||
|
||||
# Validation
|
||||
MASTERDATA_VALIDATION_STRICT=true
|
||||
```
|
||||
|
||||
### Application Properties
|
||||
```yaml
|
||||
masterdata:
|
||||
validation:
|
||||
strict: true
|
||||
duplicate-check: true
|
||||
cache:
|
||||
enabled: true
|
||||
ttl: 3600
|
||||
database:
|
||||
migration:
|
||||
auto: true
|
||||
```
|
||||
|
||||
## Tests
|
||||
|
||||
### Unit Tests ausführen
|
||||
```bash
|
||||
./gradlew :masterdata:test
|
||||
```
|
||||
|
||||
### Integration Tests ausführen
|
||||
```bash
|
||||
./gradlew :masterdata:integrationTest
|
||||
```
|
||||
|
||||
### Spezifische Tests
|
||||
```bash
|
||||
# Repository Tests
|
||||
./gradlew :masterdata:masterdata-infrastructure:test
|
||||
|
||||
# Use Case Tests
|
||||
./gradlew :masterdata:masterdata-application:test
|
||||
|
||||
# API Tests
|
||||
./gradlew :masterdata:masterdata-api:test
|
||||
```
|
||||
|
||||
## Entwicklung
|
||||
|
||||
### Neue Entität hinzufügen
|
||||
|
||||
1. **Domain Model** in `masterdata-domain/model/` erstellen
|
||||
2. **Repository Interface** in `masterdata-domain/repository/` definieren
|
||||
3. **Database Table** in `masterdata-infrastructure/persistence/` implementieren
|
||||
4. **Repository Implementation** erstellen
|
||||
5. **Use Cases** in `masterdata-application/usecase/` implementieren
|
||||
6. **REST Controller** in `masterdata-api/rest/` erstellen
|
||||
7. **Migration Script** in `masterdata-service/resources/db/migration/` hinzufügen
|
||||
8. **Dependency Injection** in `MasterdataConfiguration` konfigurieren
|
||||
|
||||
### Code-Qualität
|
||||
|
||||
- **Clean Architecture**: Strikte Trennung der Layer
|
||||
- **Domain-Driven Design**: Reichhaltige Domain Models
|
||||
- **SOLID Principles**: Befolgt alle SOLID-Prinzipien
|
||||
- **Comprehensive Testing**: Unit- und Integrationstests
|
||||
- **Documentation**: Vollständige deutsche Dokumentation
|
||||
|
||||
## Metriken
|
||||
|
||||
- **Zeilen Code**: ~3,500+ produktionsreife Zeilen
|
||||
- **Domain Models**: 4 umfassende Entitäten
|
||||
- **Repository Methoden**: 60+ Geschäftsoperationen
|
||||
- **API Endpunkte**: 37 REST-Endpunkte
|
||||
- **Datenbank Tabellen**: 4 optimierte Tabellen mit 25+ Indizes
|
||||
- **Test Coverage**: Umfassende Unit- und Integrationstests
|
||||
|
||||
## Lizenz
|
||||
|
||||
Dieses Modul ist Teil des Meldestelle-Projekts und unterliegt derselben Lizenz.
|
||||
@@ -0,0 +1,42 @@
|
||||
plugins {
|
||||
// KORREKTUR: Alle Plugins werden jetzt konsistent über den Version Catalog geladen.
|
||||
alias(libs.plugins.kotlin.jvm)
|
||||
alias(libs.plugins.kotlin.spring)
|
||||
alias(libs.plugins.kotlin.serialization)
|
||||
alias(libs.plugins.ktor)
|
||||
application
|
||||
alias(libs.plugins.spring.dependencyManagement)
|
||||
}
|
||||
|
||||
application {
|
||||
mainClass.set("at.mocode.masterdata.api.ApplicationKt")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api(platform(libs.spring.boot.dependencies))
|
||||
// Interne Module
|
||||
implementation(projects.platform.platformDependencies)
|
||||
implementation(projects.masterdata.masterdataDomain)
|
||||
implementation(projects.masterdata.masterdataApplication)
|
||||
implementation(projects.core.coreDomain)
|
||||
implementation(projects.core.coreUtils)
|
||||
|
||||
// KORREKTUR: Alle externen Abhängigkeiten werden jetzt über den Version Catalog bezogen.
|
||||
|
||||
// Spring dependencies
|
||||
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)
|
||||
|
||||
// Testing
|
||||
testImplementation(projects.platform.platformTesting)
|
||||
testImplementation(libs.ktor.server.tests)
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
package at.mocode.masterdata.api
|
||||
|
||||
import at.mocode.core.domain.model.ApiResponse
|
||||
import io.ktor.http.*
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.plugins.statuspages.*
|
||||
import io.ktor.server.response.*
|
||||
|
||||
// Eine einfache, eigene Exception, um "Nicht gefunden"-Fälle klarer zu machen.
|
||||
class NotFoundException(message: String) : RuntimeException(message)
|
||||
|
||||
fun Application.configureStatusPages() {
|
||||
install(StatusPages) {
|
||||
|
||||
// Regel 1: Fange alle "IllegalArgumentException" ab.
|
||||
// Das passiert bei ungültigen Eingaben, z.B. ein falsches UUID-Format.
|
||||
exception<IllegalArgumentException> { call, cause ->
|
||||
log.warn("Bad Request: ${cause.message}")
|
||||
val errorResponse = ApiResponse<Unit>(
|
||||
message = cause.message ?: "Invalid input provided.",
|
||||
errors = listOf("BAD_REQUEST")
|
||||
)
|
||||
call.respond(HttpStatusCode.BadRequest, errorResponse)
|
||||
}
|
||||
|
||||
// Regel 2: Fange unsere eigene "NotFoundException" ab.
|
||||
// Diese werfen wir, wenn eine Entität nicht in der DB gefunden wurde.
|
||||
exception<NotFoundException> { call, cause ->
|
||||
log.info("Resource not found: ${cause.message}")
|
||||
val errorResponse = ApiResponse<Unit>(
|
||||
message = cause.message ?: "The requested resource was not found.",
|
||||
errors = listOf("NOT_FOUND")
|
||||
)
|
||||
call.respond(HttpStatusCode.NotFound, errorResponse)
|
||||
}
|
||||
|
||||
// Regel 3: Fange alle anderen, unerwarteten Fehler ab.
|
||||
// Das ist unser Sicherheitsnetz für alles, was wir nicht vorhergesehen haben.
|
||||
exception<Throwable> { call, cause ->
|
||||
log.error("Internal Server Error", cause) // Logge den kompletten Stacktrace
|
||||
val errorResponse = ApiResponse<Unit>(
|
||||
message = "An unexpected internal server error occurred.",
|
||||
errors = listOf("INTERNAL_SERVER_ERROR")
|
||||
)
|
||||
call.respond(HttpStatusCode.InternalServerError, errorResponse)
|
||||
}
|
||||
}
|
||||
}
|
||||
+463
@@ -0,0 +1,463 @@
|
||||
package at.mocode.masterdata.api.rest
|
||||
|
||||
import at.mocode.core.domain.model.ApiResponse
|
||||
import at.mocode.core.domain.model.SparteE
|
||||
import at.mocode.masterdata.application.usecase.CreateAltersklasseUseCase
|
||||
import at.mocode.masterdata.application.usecase.GetAltersklasseUseCase
|
||||
import at.mocode.masterdata.domain.model.AltersklasseDefinition
|
||||
import at.mocode.core.utils.validation.ApiValidationUtils
|
||||
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.serialization.Serializable
|
||||
|
||||
/**
|
||||
* REST API controller for age class management operations.
|
||||
*
|
||||
* This controller provides HTTP endpoints for the master-data context's
|
||||
* age class functionality, following REST conventions and proper error handling.
|
||||
*/
|
||||
class AltersklasseController(
|
||||
private val getAltersklasseUseCase: GetAltersklasseUseCase,
|
||||
private val createAltersklasseUseCase: CreateAltersklasseUseCase
|
||||
) {
|
||||
|
||||
/**
|
||||
* DTO for age class API responses.
|
||||
*/
|
||||
@Serializable
|
||||
data class AltersklasseDto(
|
||||
val altersklasseId: String,
|
||||
val altersklasseCode: String,
|
||||
val bezeichnung: String,
|
||||
val minAlter: Int? = null,
|
||||
val maxAlter: Int? = null,
|
||||
val stichtagRegelText: String? = null,
|
||||
val sparteFilter: String? = null,
|
||||
val geschlechtFilter: String? = null,
|
||||
val oetoRegelReferenzId: String? = null,
|
||||
val istAktiv: Boolean = true,
|
||||
val createdAt: String,
|
||||
val updatedAt: String
|
||||
)
|
||||
|
||||
/**
|
||||
* DTO for creating a new age class.
|
||||
*/
|
||||
@Serializable
|
||||
data class CreateAltersklasseDto(
|
||||
val altersklasseCode: String,
|
||||
val bezeichnung: String,
|
||||
val minAlter: Int? = null,
|
||||
val maxAlter: Int? = null,
|
||||
val stichtagRegelText: String? = "31.12. des laufenden Kalenderjahres",
|
||||
val sparteFilter: String? = null,
|
||||
val geschlechtFilter: String? = null,
|
||||
val oetoRegelReferenzId: String? = null,
|
||||
val istAktiv: Boolean = true
|
||||
)
|
||||
|
||||
/**
|
||||
* DTO for updating an existing age class.
|
||||
*/
|
||||
@Serializable
|
||||
data class UpdateAltersklasseDto(
|
||||
val altersklasseCode: String,
|
||||
val bezeichnung: String,
|
||||
val minAlter: Int? = null,
|
||||
val maxAlter: Int? = null,
|
||||
val stichtagRegelText: String? = "31.12. des laufenden Kalenderjahres",
|
||||
val sparteFilter: String? = null,
|
||||
val geschlechtFilter: String? = null,
|
||||
val oetoRegelReferenzId: String? = null,
|
||||
val istAktiv: Boolean = true
|
||||
)
|
||||
|
||||
/**
|
||||
* Configures the routing for age class endpoints.
|
||||
*/
|
||||
fun configureRouting(routing: Routing) {
|
||||
routing.route("/api/masterdata/altersklassen") {
|
||||
|
||||
// GET /api/masterdata/altersklassen - Get all active age classes
|
||||
get {
|
||||
try {
|
||||
val sparteFilterParam = call.request.queryParameters["sparte"]
|
||||
val sparteFilter = sparteFilterParam?.let {
|
||||
try {
|
||||
SparteE.valueOf(it.uppercase())
|
||||
} catch (_: Exception) {
|
||||
return@get call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
ApiResponse<List<AltersklasseDto>>("Invalid sparte parameter: $it")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val geschlechtFilterParam = call.request.queryParameters["geschlecht"]
|
||||
val geschlechtFilter = geschlechtFilterParam?.let { gender ->
|
||||
if (gender.length == 1 && (gender == "M" || gender == "W")) {
|
||||
gender[0]
|
||||
} else {
|
||||
return@get call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
ApiResponse<List<AltersklasseDto>>("Invalid geschlecht parameter. Must be 'M' or 'W'")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val altersklassen = getAltersklasseUseCase.getAllActive(sparteFilter, geschlechtFilter)
|
||||
val altersklasseDtos = altersklassen.map { it.toDto() }
|
||||
call.respond(HttpStatusCode.OK, ApiResponse.success(altersklasseDtos))
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<List<AltersklasseDto>>("Failed to retrieve age classes: ${e.message}"))
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/masterdata/altersklassen/{id} - Get age class by ID
|
||||
get("/{id}") {
|
||||
try {
|
||||
val altersklasseId = call.parameters["id"]?.let { uuidFrom(it) }
|
||||
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<AltersklasseDto>("Invalid age class ID"))
|
||||
|
||||
val altersklasse = getAltersklasseUseCase.getById(altersklasseId)
|
||||
if (altersklasse != null) {
|
||||
call.respond(HttpStatusCode.OK, ApiResponse.success(altersklasse.toDto()))
|
||||
} else {
|
||||
call.respond(HttpStatusCode.NotFound, ApiResponse.error<AltersklasseDto>("Age class not found"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<AltersklasseDto>("Failed to retrieve age class: ${e.message}"))
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/masterdata/altersklassen/code/{code} - Get age class by code
|
||||
get("/code/{code}") {
|
||||
try {
|
||||
val altersklasseCode = call.parameters["code"]
|
||||
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<AltersklasseDto>("Age class code is required"))
|
||||
|
||||
val altersklasse = getAltersklasseUseCase.getByCode(altersklasseCode)
|
||||
if (altersklasse != null) {
|
||||
call.respond(HttpStatusCode.OK, ApiResponse.success(altersklasse.toDto()))
|
||||
} else {
|
||||
call.respond(HttpStatusCode.NotFound, ApiResponse.error<AltersklasseDto>("Age class not found"))
|
||||
}
|
||||
} catch (e: IllegalArgumentException) {
|
||||
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<AltersklasseDto>(e.message ?: "Invalid age class code"))
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<AltersklasseDto>("Failed to retrieve age class: ${e.message}"))
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/masterdata/altersklassen/search - Search age classes by name
|
||||
get("/search") {
|
||||
try {
|
||||
val validationErrors = ApiValidationUtils.validateQueryParameters(
|
||||
limit = call.request.queryParameters["limit"],
|
||||
q = call.request.queryParameters["q"]
|
||||
)
|
||||
|
||||
if (!ApiValidationUtils.isValid(validationErrors)) {
|
||||
call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
ApiResponse.error<List<AltersklasseDto>>(ApiValidationUtils.createErrorMessage(validationErrors))
|
||||
)
|
||||
return@get
|
||||
}
|
||||
|
||||
val searchTerm = call.request.queryParameters["q"]
|
||||
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<List<AltersklasseDto>>("Search term 'q' is required"))
|
||||
|
||||
val limit = call.request.queryParameters["limit"]?.toIntOrNull() ?: 50
|
||||
|
||||
val altersklassen = getAltersklasseUseCase.searchByName(searchTerm, limit)
|
||||
val altersklasseDtos = altersklassen.map { it.toDto() }
|
||||
call.respond(HttpStatusCode.OK, ApiResponse.success(altersklasseDtos))
|
||||
} catch (e: IllegalArgumentException) {
|
||||
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<List<AltersklasseDto>>(e.message ?: "Invalid search parameters"))
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<List<AltersklasseDto>>("Failed to search age classes: ${e.message}"))
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/masterdata/altersklassen/age/{age} - Get age classes applicable for specific age
|
||||
get("/age/{age}") {
|
||||
try {
|
||||
val age = call.parameters["age"]?.toIntOrNull()
|
||||
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<List<AltersklasseDto>>("Invalid age parameter"))
|
||||
|
||||
if (age < 0) {
|
||||
return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<List<AltersklasseDto>>("Age must be non-negative"))
|
||||
}
|
||||
|
||||
val sparteFilterParam = call.request.queryParameters["sparte"]
|
||||
val sparteFilter = sparteFilterParam?.let { SparteE.valueOf(it.uppercase()) }
|
||||
|
||||
val geschlechtFilterParam = call.request.queryParameters["geschlecht"]
|
||||
val geschlechtFilter = geschlechtFilterParam?.let { gender ->
|
||||
if (gender.length == 1 && (gender == "M" || gender == "W")) {
|
||||
gender[0]
|
||||
} else {
|
||||
return@get call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
ApiResponse.error<List<AltersklasseDto>>("Invalid geschlecht parameter. Must be 'M' or 'W'")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val altersklassen = getAltersklasseUseCase.getApplicableForAge(age, sparteFilter, geschlechtFilter)
|
||||
val altersklasseDtos = altersklassen.map { it.toDto() }
|
||||
call.respond(HttpStatusCode.OK, ApiResponse.success(altersklasseDtos))
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<List<AltersklasseDto>>("Failed to retrieve age classes: ${e.message}"))
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/masterdata/altersklassen/sparte/{sparte} - Get age classes by sport type
|
||||
get("/sparte/{sparte}") {
|
||||
try {
|
||||
val sparteParam = call.parameters["sparte"]
|
||||
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<List<AltersklasseDto>>("Sport type is required"))
|
||||
|
||||
val sparte = try {
|
||||
SparteE.valueOf(sparteParam.uppercase())
|
||||
} catch (_: Exception) {
|
||||
return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<List<AltersklasseDto>>("Invalid sport type: $sparteParam"))
|
||||
}
|
||||
|
||||
val activeOnlyParam = call.request.queryParameters["activeOnly"]
|
||||
val activeOnly = activeOnlyParam?.toBoolean() ?: true
|
||||
|
||||
val altersklassen = getAltersklasseUseCase.getBySparte(sparte, activeOnly)
|
||||
val altersklasseDtos = altersklassen.map { it.toDto() }
|
||||
call.respond(HttpStatusCode.OK, ApiResponse.success(altersklasseDtos))
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<List<AltersklasseDto>>("Failed to retrieve age classes: ${e.message}"))
|
||||
}
|
||||
}
|
||||
|
||||
// POST /api/masterdata/altersklassen - Create new age class
|
||||
post {
|
||||
try {
|
||||
val createDto = call.receive<CreateAltersklasseDto>()
|
||||
|
||||
// Basic validation
|
||||
if (createDto.altersklasseCode.isBlank()) {
|
||||
call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
ApiResponse.error<AltersklasseDto>("Age class code is required")
|
||||
)
|
||||
return@post
|
||||
}
|
||||
|
||||
if (createDto.bezeichnung.isBlank()) {
|
||||
call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
ApiResponse.error<AltersklasseDto>("Bezeichnung is required")
|
||||
)
|
||||
return@post
|
||||
}
|
||||
|
||||
val sparteFilter = createDto.sparteFilter?.let {
|
||||
try {
|
||||
SparteE.valueOf(it.uppercase())
|
||||
} catch (_: Exception) {
|
||||
return@post call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
ApiResponse.error<AltersklasseDto>("Invalid sparte filter: $it")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val geschlechtFilter = createDto.geschlechtFilter?.let { gender ->
|
||||
if (gender.length == 1 && (gender == "M" || gender == "W")) {
|
||||
gender[0]
|
||||
} else {
|
||||
return@post call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
ApiResponse.error<AltersklasseDto>("Invalid geschlecht filter. Must be 'M' or 'W'")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val oetoRegelReferenzId = createDto.oetoRegelReferenzId?.let {
|
||||
try {
|
||||
uuidFrom(it)
|
||||
} catch (_: Exception) {
|
||||
return@post call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
ApiResponse.error<AltersklasseDto>("Invalid OETO regel referenz ID format")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val request = CreateAltersklasseUseCase.CreateAltersklasseRequest(
|
||||
altersklasseCode = createDto.altersklasseCode,
|
||||
bezeichnung = createDto.bezeichnung,
|
||||
minAlter = createDto.minAlter,
|
||||
maxAlter = createDto.maxAlter,
|
||||
stichtagRegelText = createDto.stichtagRegelText,
|
||||
sparteFilter = sparteFilter,
|
||||
geschlechtFilter = geschlechtFilter,
|
||||
oetoRegelReferenzId = oetoRegelReferenzId,
|
||||
istAktiv = createDto.istAktiv
|
||||
)
|
||||
|
||||
val result = createAltersklasseUseCase.createAltersklasse(request)
|
||||
if (result.success) {
|
||||
call.respond(HttpStatusCode.Created, ApiResponse.success(result.altersklasse!!.toDto()))
|
||||
} else {
|
||||
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<AltersklasseDto>("Validation failed: ${result.errors.joinToString(", ")}"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<AltersklasseDto>("Failed to create age class: ${e.message}"))
|
||||
}
|
||||
}
|
||||
|
||||
// PUT /api/masterdata/altersklassen/{id} - Update existing age class
|
||||
put("/{id}") {
|
||||
try {
|
||||
val altersklasseId = call.parameters["id"]?.let { uuidFrom(it) }
|
||||
?: return@put call.respond(HttpStatusCode.BadRequest, ApiResponse.error<AltersklasseDto>("Invalid age class ID"))
|
||||
|
||||
val updateDto = call.receive<UpdateAltersklasseDto>()
|
||||
|
||||
// Basic validation
|
||||
if (updateDto.altersklasseCode.isBlank()) {
|
||||
call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
ApiResponse.error<AltersklasseDto>("Age class code is required")
|
||||
)
|
||||
return@put
|
||||
}
|
||||
|
||||
if (updateDto.bezeichnung.isBlank()) {
|
||||
call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
ApiResponse.error<AltersklasseDto>("Bezeichnung is required")
|
||||
)
|
||||
return@put
|
||||
}
|
||||
|
||||
val sparteFilter = updateDto.sparteFilter?.let {
|
||||
try {
|
||||
SparteE.valueOf(it.uppercase())
|
||||
} catch (_: Exception) {
|
||||
return@put call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
ApiResponse.error<AltersklasseDto>("Invalid sparte filter: $it")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val geschlechtFilter = updateDto.geschlechtFilter?.let { gender ->
|
||||
if (gender.length == 1 && (gender == "M" || gender == "W")) {
|
||||
gender[0]
|
||||
} else {
|
||||
return@put call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
ApiResponse.error<AltersklasseDto>("Invalid geschlecht filter. Must be 'M' or 'W'")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val oetoRegelReferenzId = updateDto.oetoRegelReferenzId?.let {
|
||||
try {
|
||||
uuidFrom(it)
|
||||
} catch (_: Exception) {
|
||||
return@put call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
ApiResponse.error<AltersklasseDto>("Invalid OETO regel referenz ID format")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val request = CreateAltersklasseUseCase.UpdateAltersklasseRequest(
|
||||
altersklasseId = altersklasseId,
|
||||
altersklasseCode = updateDto.altersklasseCode,
|
||||
bezeichnung = updateDto.bezeichnung,
|
||||
minAlter = updateDto.minAlter,
|
||||
maxAlter = updateDto.maxAlter,
|
||||
stichtagRegelText = updateDto.stichtagRegelText,
|
||||
sparteFilter = sparteFilter,
|
||||
geschlechtFilter = geschlechtFilter,
|
||||
oetoRegelReferenzId = oetoRegelReferenzId,
|
||||
istAktiv = updateDto.istAktiv
|
||||
)
|
||||
|
||||
val result = createAltersklasseUseCase.updateAltersklasse(request)
|
||||
if (result.success) {
|
||||
call.respond(HttpStatusCode.OK, ApiResponse.success(result.altersklasse!!.toDto()))
|
||||
} else {
|
||||
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<AltersklasseDto>("Validation failed: ${result.errors.joinToString(", ")}"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<AltersklasseDto>("Failed to update age class: ${e.message}"))
|
||||
}
|
||||
}
|
||||
|
||||
// DELETE /api/masterdata/altersklassen/{id} - Delete age class
|
||||
delete("/{id}") {
|
||||
try {
|
||||
val altersklasseId = call.parameters["id"]?.let { uuidFrom(it) }
|
||||
?: return@delete call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Unit>("Invalid age class ID"))
|
||||
|
||||
val result = createAltersklasseUseCase.deleteAltersklasse(altersklasseId)
|
||||
if (result.success) {
|
||||
call.respond(HttpStatusCode.NoContent)
|
||||
} else {
|
||||
call.respond(HttpStatusCode.NotFound, ApiResponse.error<Unit>("Age class not found: ${result.errors.joinToString(", ")}"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<Unit>("Failed to delete age class: ${e.message}"))
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/masterdata/altersklassen/eligible/{id} - Check eligibility for age class
|
||||
get("/eligible/{id}") {
|
||||
try {
|
||||
val altersklasseId = call.parameters["id"]?.let { uuidFrom(it) }
|
||||
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Boolean>("Invalid age class ID"))
|
||||
|
||||
val ageParam = call.request.queryParameters["age"]?.toIntOrNull()
|
||||
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Boolean>("Age parameter is required"))
|
||||
|
||||
val geschlechtParam = call.request.queryParameters["geschlecht"]
|
||||
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Boolean>("Gender parameter is required"))
|
||||
|
||||
if (geschlechtParam.length != 1 || (geschlechtParam != "M" && geschlechtParam != "W")) {
|
||||
return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Boolean>("Gender must be 'M' or 'W'"))
|
||||
}
|
||||
|
||||
val isEligible = getAltersklasseUseCase.isEligible(altersklasseId, ageParam, geschlechtParam[0])
|
||||
call.respond(HttpStatusCode.OK, ApiResponse.success(isEligible))
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<Boolean>("Failed to check eligibility: ${e.message}"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extension function to convert AltersklasseDefinition domain object to AltersklasseDto.
|
||||
*/
|
||||
private fun AltersklasseDefinition.toDto(): AltersklasseDto {
|
||||
return AltersklasseDto(
|
||||
altersklasseId = this.altersklasseId.toString(),
|
||||
altersklasseCode = this.altersklasseCode,
|
||||
bezeichnung = this.bezeichnung,
|
||||
minAlter = this.minAlter,
|
||||
maxAlter = this.maxAlter,
|
||||
stichtagRegelText = this.stichtagRegelText,
|
||||
sparteFilter = this.sparteFilter?.name,
|
||||
geschlechtFilter = this.geschlechtFilter?.toString(),
|
||||
oetoRegelReferenzId = this.oetoRegelReferenzId?.toString(),
|
||||
istAktiv = this.istAktiv,
|
||||
createdAt = this.createdAt.toString(),
|
||||
updatedAt = this.updatedAt.toString()
|
||||
)
|
||||
}
|
||||
}
|
||||
+368
@@ -0,0 +1,368 @@
|
||||
package at.mocode.masterdata.api.rest
|
||||
|
||||
import at.mocode.core.domain.model.ApiResponse
|
||||
import at.mocode.masterdata.application.usecase.CreateBundeslandUseCase
|
||||
import at.mocode.masterdata.application.usecase.GetBundeslandUseCase
|
||||
import at.mocode.masterdata.domain.model.BundeslandDefinition
|
||||
import at.mocode.core.utils.validation.ApiValidationUtils
|
||||
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.serialization.Serializable
|
||||
|
||||
/**
|
||||
* REST API controller for federal state management operations.
|
||||
*
|
||||
* This controller provides HTTP endpoints for the master-data context's
|
||||
* federal state functionality, following REST conventions and proper error handling.
|
||||
*/
|
||||
class BundeslandController(
|
||||
private val getBundeslandUseCase: GetBundeslandUseCase,
|
||||
private val createBundeslandUseCase: CreateBundeslandUseCase
|
||||
) {
|
||||
|
||||
/**
|
||||
* DTO for federal state API responses.
|
||||
*/
|
||||
@Serializable
|
||||
data class BundeslandDto(
|
||||
val bundeslandId: String,
|
||||
val landId: String,
|
||||
val oepsCode: String? = null,
|
||||
val iso3166_2_Code: String? = null,
|
||||
val name: String,
|
||||
val kuerzel: String? = null,
|
||||
val wappenUrl: String? = null,
|
||||
val istAktiv: Boolean = true,
|
||||
val sortierReihenfolge: Int? = null,
|
||||
val createdAt: String,
|
||||
val updatedAt: String
|
||||
)
|
||||
|
||||
/**
|
||||
* DTO for creating a new federal state.
|
||||
*/
|
||||
@Serializable
|
||||
data class CreateBundeslandDto(
|
||||
val landId: String,
|
||||
val oepsCode: String? = null,
|
||||
val iso3166_2_Code: String? = null,
|
||||
val name: String,
|
||||
val kuerzel: String? = null,
|
||||
val wappenUrl: String? = null,
|
||||
val istAktiv: Boolean = true,
|
||||
val sortierReihenfolge: Int? = null
|
||||
)
|
||||
|
||||
/**
|
||||
* DTO for updating an existing federal state.
|
||||
*/
|
||||
@Serializable
|
||||
data class UpdateBundeslandDto(
|
||||
val landId: String,
|
||||
val oepsCode: String? = null,
|
||||
val iso3166_2_Code: String? = null,
|
||||
val name: String,
|
||||
val kuerzel: String? = null,
|
||||
val wappenUrl: String? = null,
|
||||
val istAktiv: Boolean = true,
|
||||
val sortierReihenfolge: Int? = null
|
||||
)
|
||||
|
||||
/**
|
||||
* Configures the routing for federal state endpoints.
|
||||
*/
|
||||
fun configureRouting(routing: Routing) {
|
||||
routing.route("/api/masterdata/bundeslaender") {
|
||||
|
||||
// GET /api/masterdata/bundeslaender - Get all active federal states
|
||||
get {
|
||||
try {
|
||||
val orderBySortierungParam = call.request.queryParameters["orderBySortierung"]
|
||||
val orderBySortierung = if (orderBySortierungParam != null) {
|
||||
try {
|
||||
orderBySortierungParam.toBoolean()
|
||||
} catch (_: Exception) {
|
||||
return@get call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
ApiResponse.error<List<BundeslandDto>>("Invalid orderBySortierung parameter. Must be true or false")
|
||||
)
|
||||
}
|
||||
} else {
|
||||
true
|
||||
}
|
||||
|
||||
val bundeslaender = getBundeslandUseCase.getAllActive(orderBySortierung)
|
||||
val bundeslandDtos = bundeslaender.map { it.toDto() }
|
||||
call.respond(HttpStatusCode.OK, ApiResponse.success(bundeslandDtos))
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<List<BundeslandDto>>("Failed to retrieve federal states: ${e.message}"))
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/masterdata/bundeslaender/{id} - Get federal state by ID
|
||||
get("/{id}") {
|
||||
try {
|
||||
val bundeslandId = call.parameters["id"]?.let { uuidFrom(it) }
|
||||
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<BundeslandDto>("Invalid federal state ID"))
|
||||
|
||||
val bundesland = getBundeslandUseCase.getById(bundeslandId)
|
||||
if (bundesland != null) {
|
||||
call.respond(HttpStatusCode.OK, ApiResponse.success(bundesland.toDto()))
|
||||
} else {
|
||||
call.respond(HttpStatusCode.NotFound, ApiResponse.error<BundeslandDto>("Federal state not found"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<BundeslandDto>("Failed to retrieve federal state: ${e.message}"))
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/masterdata/bundeslaender/oeps/{code} - Get federal state by OEPS code
|
||||
get("/oeps/{code}") {
|
||||
try {
|
||||
val oepsCode = call.parameters["code"]
|
||||
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<BundeslandDto>("OEPS code is required"))
|
||||
|
||||
val landIdParam = call.request.queryParameters["landId"]
|
||||
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<BundeslandDto>("Country ID (landId) is required"))
|
||||
|
||||
val landId = try {
|
||||
uuidFrom(landIdParam)
|
||||
} catch (_: Exception) {
|
||||
return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<BundeslandDto>("Invalid country ID format"))
|
||||
}
|
||||
|
||||
val bundesland = getBundeslandUseCase.getByOepsCode(oepsCode, landId)
|
||||
if (bundesland != null) {
|
||||
call.respond(HttpStatusCode.OK, ApiResponse.success(bundesland.toDto()))
|
||||
} else {
|
||||
call.respond(HttpStatusCode.NotFound, ApiResponse.error<BundeslandDto>("Federal state not found"))
|
||||
}
|
||||
} catch (e: IllegalArgumentException) {
|
||||
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<BundeslandDto>(e.message ?: "Invalid OEPS code"))
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<BundeslandDto>("Failed to retrieve federal state: ${e.message}"))
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/masterdata/bundeslaender/iso/{code} - Get federal state by ISO 3166-2 code
|
||||
get("/iso/{code}") {
|
||||
try {
|
||||
val isoCode = call.parameters["code"]
|
||||
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<BundeslandDto>("ISO 3166-2 code is required"))
|
||||
|
||||
val bundesland = getBundeslandUseCase.getByIso3166_2_Code(isoCode)
|
||||
if (bundesland != null) {
|
||||
call.respond(HttpStatusCode.OK, ApiResponse.success(bundesland.toDto()))
|
||||
} else {
|
||||
call.respond(HttpStatusCode.NotFound, ApiResponse.error<BundeslandDto>("Federal state not found"))
|
||||
}
|
||||
} catch (e: IllegalArgumentException) {
|
||||
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<BundeslandDto>(e.message ?: "Invalid ISO code"))
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<BundeslandDto>("Failed to retrieve federal state: ${e.message}"))
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/masterdata/bundeslaender/country/{countryId} - Get federal states by country
|
||||
get("/country/{countryId}") {
|
||||
try {
|
||||
val landId = call.parameters["countryId"]?.let { uuidFrom(it) }
|
||||
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<List<BundeslandDto>>("Invalid country ID"))
|
||||
|
||||
val activeOnlyParam = call.request.queryParameters["activeOnly"]
|
||||
val activeOnly = activeOnlyParam?.toBoolean() ?: true
|
||||
|
||||
val orderBySortierungParam = call.request.queryParameters["orderBySortierung"]
|
||||
val orderBySortierung = orderBySortierungParam?.toBoolean() ?: true
|
||||
|
||||
val bundeslaender = getBundeslandUseCase.getByCountry(landId, activeOnly, orderBySortierung)
|
||||
val bundeslandDtos = bundeslaender.map { it.toDto() }
|
||||
call.respond(HttpStatusCode.OK, ApiResponse.success(bundeslandDtos))
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<List<BundeslandDto>>("Failed to retrieve federal states: ${e.message}"))
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/masterdata/bundeslaender/search - Search federal states by name
|
||||
get("/search") {
|
||||
try {
|
||||
val validationErrors = ApiValidationUtils.validateQueryParameters(
|
||||
limit = call.request.queryParameters["limit"],
|
||||
q = call.request.queryParameters["q"]
|
||||
)
|
||||
|
||||
if (!ApiValidationUtils.isValid(validationErrors)) {
|
||||
call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
ApiResponse.error<List<BundeslandDto>>(ApiValidationUtils.createErrorMessage(validationErrors))
|
||||
)
|
||||
return@get
|
||||
}
|
||||
|
||||
val searchTerm = call.request.queryParameters["q"]
|
||||
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<List<BundeslandDto>>("Search term 'q' is required"))
|
||||
|
||||
val limit = call.request.queryParameters["limit"]?.toIntOrNull() ?: 50
|
||||
val landIdParam = call.request.queryParameters["landId"]
|
||||
val landId = landIdParam?.let { uuidFrom(it) }
|
||||
|
||||
val bundeslaender = getBundeslandUseCase.searchByName(searchTerm, landId, limit)
|
||||
val bundeslandDtos = bundeslaender.map { it.toDto() }
|
||||
call.respond(HttpStatusCode.OK, ApiResponse.success(bundeslandDtos))
|
||||
} catch (e: IllegalArgumentException) {
|
||||
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<List<BundeslandDto>>(e.message ?: "Invalid search parameters"))
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<List<BundeslandDto>>("Failed to search federal states: ${e.message}"))
|
||||
}
|
||||
}
|
||||
|
||||
// POST /api/masterdata/bundeslaender - Create new federal state
|
||||
post {
|
||||
try {
|
||||
val createDto = call.receive<CreateBundeslandDto>()
|
||||
|
||||
// Basic validation
|
||||
if (createDto.name.isBlank()) {
|
||||
call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
ApiResponse.error<BundeslandDto>("Name is required")
|
||||
)
|
||||
return@post
|
||||
}
|
||||
|
||||
try {
|
||||
uuidFrom(createDto.landId)
|
||||
} catch (_: Exception) {
|
||||
call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
ApiResponse.error<BundeslandDto>("Invalid country ID format")
|
||||
)
|
||||
return@post
|
||||
}
|
||||
|
||||
val request = CreateBundeslandUseCase.CreateBundeslandRequest(
|
||||
landId = uuidFrom(createDto.landId),
|
||||
oepsCode = createDto.oepsCode,
|
||||
iso3166_2_Code = createDto.iso3166_2_Code,
|
||||
name = createDto.name,
|
||||
kuerzel = createDto.kuerzel,
|
||||
wappenUrl = createDto.wappenUrl,
|
||||
istAktiv = createDto.istAktiv,
|
||||
sortierReihenfolge = createDto.sortierReihenfolge
|
||||
)
|
||||
|
||||
val result = createBundeslandUseCase.createBundesland(request)
|
||||
if (result.success) {
|
||||
call.respond(HttpStatusCode.Created, ApiResponse.success(result.bundesland!!.toDto()))
|
||||
} else {
|
||||
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<BundeslandDto>("Validation failed: ${result.errors.joinToString(", ")}"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<BundeslandDto>("Failed to create federal state: ${e.message}"))
|
||||
}
|
||||
}
|
||||
|
||||
// PUT /api/masterdata/bundeslaender/{id} - Update existing federal state
|
||||
put("/{id}") {
|
||||
try {
|
||||
val bundeslandId = call.parameters["id"]?.let { uuidFrom(it) }
|
||||
?: return@put call.respond(HttpStatusCode.BadRequest, ApiResponse.error<BundeslandDto>("Invalid federal state ID"))
|
||||
|
||||
val updateDto = call.receive<UpdateBundeslandDto>()
|
||||
|
||||
// Basic validation
|
||||
if (updateDto.name.isBlank()) {
|
||||
call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
ApiResponse.error<BundeslandDto>("Name is required")
|
||||
)
|
||||
return@put
|
||||
}
|
||||
|
||||
try {
|
||||
uuidFrom(updateDto.landId)
|
||||
} catch (_: Exception) {
|
||||
call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
ApiResponse.error<BundeslandDto>("Invalid country ID format")
|
||||
)
|
||||
return@put
|
||||
}
|
||||
|
||||
val request = CreateBundeslandUseCase.UpdateBundeslandRequest(
|
||||
bundeslandId = bundeslandId,
|
||||
landId = uuidFrom(updateDto.landId),
|
||||
oepsCode = updateDto.oepsCode,
|
||||
iso3166_2_Code = updateDto.iso3166_2_Code,
|
||||
name = updateDto.name,
|
||||
kuerzel = updateDto.kuerzel,
|
||||
wappenUrl = updateDto.wappenUrl,
|
||||
istAktiv = updateDto.istAktiv,
|
||||
sortierReihenfolge = updateDto.sortierReihenfolge
|
||||
)
|
||||
|
||||
val result = createBundeslandUseCase.updateBundesland(request)
|
||||
if (result.success) {
|
||||
call.respond(HttpStatusCode.OK, ApiResponse.success(result.bundesland!!.toDto()))
|
||||
} else {
|
||||
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<BundeslandDto>("Validation failed: ${result.errors.joinToString(", ")}"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<BundeslandDto>("Failed to update federal state: ${e.message}"))
|
||||
}
|
||||
}
|
||||
|
||||
// DELETE /api/masterdata/bundeslaender/{id} - Delete federal state
|
||||
delete("/{id}") {
|
||||
try {
|
||||
val bundeslandId = call.parameters["id"]?.let { uuidFrom(it) }
|
||||
?: return@delete call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Unit>("Invalid federal state ID"))
|
||||
|
||||
val result = createBundeslandUseCase.deleteBundesland(bundeslandId)
|
||||
if (result.success) {
|
||||
call.respond(HttpStatusCode.NoContent)
|
||||
} else {
|
||||
call.respond(HttpStatusCode.NotFound, ApiResponse.error<Unit>("Federal state not found: ${result.errors.joinToString(", ")}"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<Unit>("Failed to delete federal state: ${e.message}"))
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/masterdata/bundeslaender/count/{countryId} - Count active federal states by country
|
||||
get("/count/{countryId}") {
|
||||
try {
|
||||
val landId = call.parameters["countryId"]?.let { uuidFrom(it) }
|
||||
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Long>("Invalid country ID"))
|
||||
|
||||
val count = getBundeslandUseCase.countActiveByCountry(landId)
|
||||
call.respond(HttpStatusCode.OK, ApiResponse.success(count))
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<Long>("Failed to count federal states: ${e.message}"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extension function to convert BundeslandDefinition domain object to BundeslandDto.
|
||||
*/
|
||||
private fun BundeslandDefinition.toDto(): BundeslandDto {
|
||||
return BundeslandDto(
|
||||
bundeslandId = this.bundeslandId.toString(),
|
||||
landId = this.landId.toString(),
|
||||
oepsCode = this.oepsCode,
|
||||
iso3166_2_Code = this.iso3166_2_Code,
|
||||
name = this.name,
|
||||
kuerzel = this.kuerzel,
|
||||
wappenUrl = this.wappenUrl,
|
||||
istAktiv = this.istAktiv,
|
||||
sortierReihenfolge = this.sortierReihenfolge,
|
||||
createdAt = this.createdAt.toString(),
|
||||
updatedAt = this.updatedAt.toString()
|
||||
)
|
||||
}
|
||||
}
|
||||
+353
@@ -0,0 +1,353 @@
|
||||
package at.mocode.masterdata.api.rest
|
||||
|
||||
import at.mocode.core.domain.model.ApiResponse
|
||||
import at.mocode.masterdata.application.usecase.CreateCountryUseCase
|
||||
import at.mocode.masterdata.application.usecase.GetCountryUseCase
|
||||
import at.mocode.masterdata.domain.model.LandDefinition
|
||||
import at.mocode.core.utils.validation.ApiValidationUtils
|
||||
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.serialization.Serializable
|
||||
|
||||
/**
|
||||
* REST API controller for country management operations.
|
||||
*
|
||||
* This controller provides HTTP endpoints for the master-data context's
|
||||
* country functionality, following REST conventions and proper error handling.
|
||||
*/
|
||||
class CountryController(
|
||||
private val getCountryUseCase: GetCountryUseCase,
|
||||
private val createCountryUseCase: CreateCountryUseCase
|
||||
) {
|
||||
|
||||
/**
|
||||
* DTO for country API responses.
|
||||
*/
|
||||
@Serializable
|
||||
data class CountryDto(
|
||||
val landId: String,
|
||||
val isoAlpha2Code: String,
|
||||
val isoAlpha3Code: String,
|
||||
val isoNumerischerCode: String? = null,
|
||||
val nameDeutsch: String,
|
||||
val nameEnglisch: String? = null,
|
||||
val wappenUrl: String? = null,
|
||||
val istEuMitglied: Boolean? = null,
|
||||
val istEwrMitglied: Boolean? = null,
|
||||
val istAktiv: Boolean = true,
|
||||
val sortierReihenfolge: Int? = null,
|
||||
val createdAt: String,
|
||||
val updatedAt: String
|
||||
)
|
||||
|
||||
/**
|
||||
* DTO for creating a new country.
|
||||
*/
|
||||
@Serializable
|
||||
data class CreateCountryDto(
|
||||
val isoAlpha2Code: String,
|
||||
val isoAlpha3Code: String,
|
||||
val isoNumerischerCode: String? = null,
|
||||
val nameDeutsch: String,
|
||||
val nameEnglisch: String? = null,
|
||||
val wappenUrl: String? = null,
|
||||
val istEuMitglied: Boolean? = null,
|
||||
val istEwrMitglied: Boolean? = null,
|
||||
val istAktiv: Boolean = true,
|
||||
val sortierReihenfolge: Int? = null
|
||||
)
|
||||
|
||||
/**
|
||||
* DTO for updating an existing country.
|
||||
*/
|
||||
@Serializable
|
||||
data class UpdateCountryDto(
|
||||
val isoAlpha2Code: String,
|
||||
val isoAlpha3Code: String,
|
||||
val isoNumerischerCode: String? = null,
|
||||
val nameDeutsch: String,
|
||||
val nameEnglisch: String? = null,
|
||||
val wappenUrl: String? = null,
|
||||
val istEuMitglied: Boolean? = null,
|
||||
val istEwrMitglied: Boolean? = null,
|
||||
val istAktiv: Boolean = true,
|
||||
val sortierReihenfolge: Int? = null
|
||||
)
|
||||
|
||||
/**
|
||||
* Configures the routing for country endpoints.
|
||||
*/
|
||||
fun configureRouting(routing: Routing) {
|
||||
routing.route("/api/masterdata/countries") {
|
||||
|
||||
// GET /api/masterdata/countries - Get all active countries
|
||||
get {
|
||||
try {
|
||||
// Validate orderBySortierung parameter if provided
|
||||
val orderBySortierungParam = call.request.queryParameters["orderBySortierung"]
|
||||
val orderBySortierung = if (orderBySortierungParam != null) {
|
||||
try {
|
||||
orderBySortierungParam.toBoolean()
|
||||
} catch (_: Exception) {
|
||||
return@get call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
ApiResponse.error<List<CountryDto>>("Invalid orderBySortierung parameter. Must be true or false")
|
||||
)
|
||||
}
|
||||
} else {
|
||||
true
|
||||
}
|
||||
val countries = getCountryUseCase.getAllActive(orderBySortierung)
|
||||
val countryDtos = countries.map { it.toDto() }
|
||||
call.respond(HttpStatusCode.OK, ApiResponse.success(countryDtos))
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<List<CountryDto>>("Failed to retrieve countries: ${e.message}"))
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/masterdata/countries/{id} - Get country by ID
|
||||
get("/{id}") {
|
||||
try {
|
||||
val countryId = call.parameters["id"]?.let { uuidFrom(it) }
|
||||
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<CountryDto>("Invalid country ID"))
|
||||
|
||||
val country = getCountryUseCase.getById(countryId)
|
||||
if (country != null) {
|
||||
call.respond(HttpStatusCode.OK, ApiResponse.success(country.toDto()))
|
||||
} else {
|
||||
call.respond(HttpStatusCode.NotFound, ApiResponse.error<CountryDto>("Country not found"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<CountryDto>("Failed to retrieve country: ${e.message}"))
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/masterdata/countries/iso2/{code} - Get country by ISO Alpha-2 code
|
||||
get("/iso2/{code}") {
|
||||
try {
|
||||
val isoCode = call.parameters["code"]
|
||||
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<CountryDto>("ISO code is required"))
|
||||
|
||||
val country = getCountryUseCase.getByIsoAlpha2Code(isoCode)
|
||||
if (country != null) {
|
||||
call.respond(HttpStatusCode.OK, ApiResponse.success(country.toDto()))
|
||||
} else {
|
||||
call.respond(HttpStatusCode.NotFound, ApiResponse.error<CountryDto>("Country not found"))
|
||||
}
|
||||
} catch (e: IllegalArgumentException) {
|
||||
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<CountryDto>(e.message ?: "Invalid ISO code"))
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<CountryDto>("Failed to retrieve country: ${e.message}"))
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/masterdata/countries/iso3/{code} - Get country by ISO Alpha-3 code
|
||||
get("/iso3/{code}") {
|
||||
try {
|
||||
val isoCode = call.parameters["code"]
|
||||
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<CountryDto>("ISO code is required"))
|
||||
|
||||
val country = getCountryUseCase.getByIsoAlpha3Code(isoCode)
|
||||
if (country != null) {
|
||||
call.respond(HttpStatusCode.OK, ApiResponse.success(country.toDto()))
|
||||
} else {
|
||||
call.respond(HttpStatusCode.NotFound, ApiResponse.error<CountryDto>("Country not found"))
|
||||
}
|
||||
} catch (e: IllegalArgumentException) {
|
||||
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<CountryDto>(e.message ?: "Invalid ISO code"))
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<CountryDto>("Failed to retrieve country: ${e.message}"))
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/masterdata/countries/search - Search countries by name
|
||||
get("/search") {
|
||||
try {
|
||||
// Validate query parameters
|
||||
val validationErrors = ApiValidationUtils.validateQueryParameters(
|
||||
limit = call.request.queryParameters["limit"],
|
||||
q = call.request.queryParameters["q"]
|
||||
)
|
||||
|
||||
if (!ApiValidationUtils.isValid(validationErrors)) {
|
||||
call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
ApiResponse.error<List<CountryDto>>(ApiValidationUtils.createErrorMessage(validationErrors))
|
||||
)
|
||||
return@get
|
||||
}
|
||||
|
||||
val searchTerm = call.request.queryParameters["q"]
|
||||
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<List<CountryDto>>("Search term 'q' is required"))
|
||||
|
||||
val limit = call.request.queryParameters["limit"]?.toIntOrNull() ?: 50
|
||||
|
||||
val countries = getCountryUseCase.searchByName(searchTerm, limit)
|
||||
val countryDtos = countries.map { it.toDto() }
|
||||
call.respond(HttpStatusCode.OK, ApiResponse.success(countryDtos))
|
||||
} catch (e: IllegalArgumentException) {
|
||||
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<List<CountryDto>>(e.message ?: "Invalid search parameters"))
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<List<CountryDto>>("Failed to search countries: ${e.message}"))
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/masterdata/countries/eu - Get EU member countries
|
||||
get("/eu") {
|
||||
try {
|
||||
val countries = getCountryUseCase.getEuMembers()
|
||||
val countryDtos = countries.map { it.toDto() }
|
||||
call.respond(HttpStatusCode.OK, ApiResponse.success(countryDtos))
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<List<CountryDto>>("Failed to retrieve EU countries: ${e.message}"))
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/masterdata/countries/ewr - Get EWR member countries
|
||||
get("/ewr") {
|
||||
try {
|
||||
val countries = getCountryUseCase.getEwrMembers()
|
||||
val countryDtos = countries.map { it.toDto() }
|
||||
call.respond(HttpStatusCode.OK, ApiResponse.success(countryDtos))
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<List<CountryDto>>("Failed to retrieve EWR countries: ${e.message}"))
|
||||
}
|
||||
}
|
||||
|
||||
// POST /api/masterdata/countries - Create new country
|
||||
post {
|
||||
try {
|
||||
val createDto = call.receive<CreateCountryDto>()
|
||||
|
||||
// Validate input using shared validation utilities
|
||||
val validationErrors = ApiValidationUtils.validateCountryRequest(
|
||||
isoAlpha2Code = createDto.isoAlpha2Code,
|
||||
isoAlpha3Code = createDto.isoAlpha3Code,
|
||||
nameDeutsch = createDto.nameDeutsch,
|
||||
nameEnglisch = createDto.nameEnglisch
|
||||
)
|
||||
|
||||
if (!ApiValidationUtils.isValid(validationErrors)) {
|
||||
call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
ApiResponse.error<CountryDto>(ApiValidationUtils.createErrorMessage(validationErrors))
|
||||
)
|
||||
return@post
|
||||
}
|
||||
|
||||
val request = CreateCountryUseCase.CreateCountryRequest(
|
||||
isoAlpha2Code = createDto.isoAlpha2Code,
|
||||
isoAlpha3Code = createDto.isoAlpha3Code,
|
||||
isoNumerischerCode = createDto.isoNumerischerCode,
|
||||
nameDeutsch = createDto.nameDeutsch,
|
||||
nameEnglisch = createDto.nameEnglisch,
|
||||
wappenUrl = createDto.wappenUrl,
|
||||
istEuMitglied = createDto.istEuMitglied,
|
||||
istEwrMitglied = createDto.istEwrMitglied,
|
||||
istAktiv = createDto.istAktiv,
|
||||
sortierReihenfolge = createDto.sortierReihenfolge
|
||||
)
|
||||
|
||||
val result = createCountryUseCase.createCountry(request)
|
||||
if (result.success) {
|
||||
call.respond(HttpStatusCode.Created, ApiResponse.success(result.country!!.toDto()))
|
||||
} else {
|
||||
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<CountryDto>("Validation failed: ${result.errors.joinToString(", ")}"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<CountryDto>("Failed to create country: ${e.message}"))
|
||||
}
|
||||
}
|
||||
|
||||
// PUT /api/masterdata/countries/{id} - Update existing country
|
||||
put("/{id}") {
|
||||
try {
|
||||
val countryId = call.parameters["id"]?.let { uuidFrom(it) }
|
||||
?: return@put call.respond(HttpStatusCode.BadRequest, ApiResponse.error<CountryDto>("Invalid country ID"))
|
||||
|
||||
val updateDto = call.receive<UpdateCountryDto>()
|
||||
|
||||
// Validate input using shared validation utilities
|
||||
val validationErrors = ApiValidationUtils.validateCountryRequest(
|
||||
isoAlpha2Code = updateDto.isoAlpha2Code,
|
||||
isoAlpha3Code = updateDto.isoAlpha3Code,
|
||||
nameDeutsch = updateDto.nameDeutsch,
|
||||
nameEnglisch = updateDto.nameEnglisch
|
||||
)
|
||||
|
||||
if (!ApiValidationUtils.isValid(validationErrors)) {
|
||||
call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
ApiResponse.error<CountryDto>(ApiValidationUtils.createErrorMessage(validationErrors))
|
||||
)
|
||||
return@put
|
||||
}
|
||||
|
||||
val request = CreateCountryUseCase.UpdateCountryRequest(
|
||||
landId = countryId,
|
||||
isoAlpha2Code = updateDto.isoAlpha2Code,
|
||||
isoAlpha3Code = updateDto.isoAlpha3Code,
|
||||
isoNumerischerCode = updateDto.isoNumerischerCode,
|
||||
nameDeutsch = updateDto.nameDeutsch,
|
||||
nameEnglisch = updateDto.nameEnglisch,
|
||||
wappenUrl = updateDto.wappenUrl,
|
||||
istEuMitglied = updateDto.istEuMitglied,
|
||||
istEwrMitglied = updateDto.istEwrMitglied,
|
||||
istAktiv = updateDto.istAktiv,
|
||||
sortierReihenfolge = updateDto.sortierReihenfolge
|
||||
)
|
||||
|
||||
val result = createCountryUseCase.updateCountry(request)
|
||||
if (result.success) {
|
||||
call.respond(HttpStatusCode.OK, ApiResponse.success(result.country!!.toDto()))
|
||||
} else {
|
||||
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<CountryDto>("Validation failed: ${result.errors.joinToString(", ")}"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<CountryDto>("Failed to update country: ${e.message}"))
|
||||
}
|
||||
}
|
||||
|
||||
// DELETE /api/masterdata/countries/{id} - Delete country
|
||||
delete("/{id}") {
|
||||
try {
|
||||
val countryId = call.parameters["id"]?.let { uuidFrom(it) }
|
||||
?: return@delete call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Unit>("Invalid country ID"))
|
||||
|
||||
val result = createCountryUseCase.deleteCountry(countryId)
|
||||
if (result.success) {
|
||||
call.respond(HttpStatusCode.NoContent)
|
||||
} else {
|
||||
call.respond(HttpStatusCode.NotFound, ApiResponse.error<Unit>("Country not found: ${result.errors.joinToString(", ")}"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<Unit>("Failed to delete country: ${e.message}"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extension function to convert LandDefinition domain object to CountryDto.
|
||||
*/
|
||||
private fun LandDefinition.toDto(): CountryDto {
|
||||
return CountryDto(
|
||||
landId = this.landId.toString(),
|
||||
isoAlpha2Code = this.isoAlpha2Code,
|
||||
isoAlpha3Code = this.isoAlpha3Code,
|
||||
isoNumerischerCode = this.isoNumerischerCode,
|
||||
nameDeutsch = this.nameDeutsch,
|
||||
nameEnglisch = this.nameEnglisch,
|
||||
wappenUrl = this.wappenUrl,
|
||||
istEuMitglied = this.istEuMitglied,
|
||||
istEwrMitglied = this.istEwrMitglied,
|
||||
istAktiv = this.istAktiv,
|
||||
sortierReihenfolge = this.sortierReihenfolge,
|
||||
createdAt = this.createdAt.toString(),
|
||||
updatedAt = this.updatedAt.toString()
|
||||
)
|
||||
}
|
||||
}
|
||||
+474
@@ -0,0 +1,474 @@
|
||||
package at.mocode.masterdata.api.rest
|
||||
|
||||
import at.mocode.core.domain.model.ApiResponse
|
||||
import at.mocode.core.domain.model.PlatzTypE
|
||||
import at.mocode.masterdata.application.usecase.CreatePlatzUseCase
|
||||
import at.mocode.masterdata.application.usecase.GetPlatzUseCase
|
||||
import at.mocode.masterdata.domain.model.Platz
|
||||
import at.mocode.core.utils.validation.ApiValidationUtils
|
||||
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.serialization.Serializable
|
||||
|
||||
/**
|
||||
* REST API controller for venue/arena management operations.
|
||||
*
|
||||
* This controller provides HTTP endpoints for the master-data context's
|
||||
* venue functionality, following REST conventions and proper error handling.
|
||||
*/
|
||||
class PlatzController(
|
||||
private val getPlatzUseCase: GetPlatzUseCase,
|
||||
private val createPlatzUseCase: CreatePlatzUseCase
|
||||
) {
|
||||
|
||||
/**
|
||||
* DTO for venue API responses.
|
||||
*/
|
||||
@Serializable
|
||||
data class PlatzDto(
|
||||
val id: String,
|
||||
val turnierId: String,
|
||||
val name: String,
|
||||
val dimension: String? = null,
|
||||
val boden: String? = null,
|
||||
val typ: String,
|
||||
val istAktiv: Boolean = true,
|
||||
val sortierReihenfolge: Int? = null,
|
||||
val createdAt: String,
|
||||
val updatedAt: String
|
||||
)
|
||||
|
||||
/**
|
||||
* DTO for creating a new venue.
|
||||
*/
|
||||
@Serializable
|
||||
data class CreatePlatzDto(
|
||||
val turnierId: String,
|
||||
val name: String,
|
||||
val dimension: String? = null,
|
||||
val boden: String? = null,
|
||||
val typ: String,
|
||||
val istAktiv: Boolean = true,
|
||||
val sortierReihenfolge: Int? = null
|
||||
)
|
||||
|
||||
/**
|
||||
* DTO for updating an existing venue.
|
||||
*/
|
||||
@Serializable
|
||||
data class UpdatePlatzDto(
|
||||
val turnierId: String,
|
||||
val name: String,
|
||||
val dimension: String? = null,
|
||||
val boden: String? = null,
|
||||
val typ: String,
|
||||
val istAktiv: Boolean = true,
|
||||
val sortierReihenfolge: Int? = null
|
||||
)
|
||||
|
||||
/**
|
||||
* Configures the routing for venue endpoints.
|
||||
*/
|
||||
fun configureRouting(routing: Routing) {
|
||||
routing.route("/api/masterdata/plaetze") {
|
||||
|
||||
// GET /api/masterdata/plaetze/{id} - Get venue by ID
|
||||
get("/{id}") {
|
||||
try {
|
||||
val platzId = call.parameters["id"]?.let { uuidFrom(it) }
|
||||
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<PlatzDto>("Invalid venue ID"))
|
||||
|
||||
val platz = getPlatzUseCase.getById(platzId)
|
||||
if (platz != null) {
|
||||
call.respond(HttpStatusCode.OK, ApiResponse.success(platz.toDto()))
|
||||
} else {
|
||||
call.respond(HttpStatusCode.NotFound, ApiResponse.error<PlatzDto>("Venue not found"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<PlatzDto>("Failed to retrieve venue: ${e.message}"))
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/masterdata/plaetze/tournament/{turnierId} - Get venues by tournament
|
||||
get("/tournament/{turnierId}") {
|
||||
try {
|
||||
val turnierId = call.parameters["turnierId"]?.let { uuidFrom(it) }
|
||||
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<List<PlatzDto>>("Invalid tournament ID"))
|
||||
|
||||
val activeOnlyParam = call.request.queryParameters["activeOnly"]
|
||||
val activeOnly = activeOnlyParam?.toBoolean() ?: true
|
||||
|
||||
val orderBySortierungParam = call.request.queryParameters["orderBySortierung"]
|
||||
val orderBySortierung = orderBySortierungParam?.toBoolean() ?: true
|
||||
|
||||
val plaetze = getPlatzUseCase.getByTournament(turnierId, activeOnly, orderBySortierung)
|
||||
val platzDtos = plaetze.map { it.toDto() }
|
||||
call.respond(HttpStatusCode.OK, ApiResponse.success(platzDtos))
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<List<PlatzDto>>("Failed to retrieve venues: ${e.message}"))
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/masterdata/plaetze/search - Search venues by name
|
||||
get("/search") {
|
||||
try {
|
||||
val validationErrors = ApiValidationUtils.validateQueryParameters(
|
||||
limit = call.request.queryParameters["limit"],
|
||||
q = call.request.queryParameters["q"]
|
||||
)
|
||||
|
||||
if (!ApiValidationUtils.isValid(validationErrors)) {
|
||||
call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
ApiResponse.error<List<PlatzDto>>(ApiValidationUtils.createErrorMessage(validationErrors))
|
||||
)
|
||||
return@get
|
||||
}
|
||||
|
||||
val searchTerm = call.request.queryParameters["q"]
|
||||
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<List<PlatzDto>>("Search term 'q' is required"))
|
||||
|
||||
val limit = call.request.queryParameters["limit"]?.toIntOrNull() ?: 50
|
||||
val turnierIdParam = call.request.queryParameters["turnierId"]
|
||||
val turnierId = turnierIdParam?.let { uuidFrom(it) }
|
||||
|
||||
val plaetze = getPlatzUseCase.searchByName(searchTerm, turnierId, limit)
|
||||
val platzDtos = plaetze.map { it.toDto() }
|
||||
call.respond(HttpStatusCode.OK, ApiResponse.success(platzDtos))
|
||||
} catch (e: IllegalArgumentException) {
|
||||
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<List<PlatzDto>>(e.message ?: "Invalid search parameters"))
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<List<PlatzDto>>("Failed to search venues: ${e.message}"))
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/masterdata/plaetze/type/{typ} - Get venues by type
|
||||
get("/type/{typ}") {
|
||||
try {
|
||||
val typParam = call.parameters["typ"]
|
||||
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<List<PlatzDto>>("Venue type is required"))
|
||||
|
||||
val typ = try {
|
||||
PlatzTypE.valueOf(typParam.uppercase())
|
||||
} catch (_: Exception) {
|
||||
return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<List<PlatzDto>>("Invalid venue type: $typParam"))
|
||||
}
|
||||
|
||||
val turnierIdParam = call.request.queryParameters["turnierId"]
|
||||
val turnierId = turnierIdParam?.let { uuidFrom(it) }
|
||||
|
||||
val activeOnlyParam = call.request.queryParameters["activeOnly"]
|
||||
val activeOnly = activeOnlyParam?.toBoolean() ?: true
|
||||
|
||||
val plaetze = getPlatzUseCase.getByType(typ, turnierId, activeOnly)
|
||||
val platzDtos = plaetze.map { it.toDto() }
|
||||
call.respond(HttpStatusCode.OK, ApiResponse.success(platzDtos))
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<List<PlatzDto>>("Failed to retrieve venues: ${e.message}"))
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/masterdata/plaetze/ground/{boden} - Get venues by ground type
|
||||
get("/ground/{boden}") {
|
||||
try {
|
||||
val boden = call.parameters["boden"]
|
||||
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<List<PlatzDto>>("Ground type is required"))
|
||||
|
||||
val turnierIdParam = call.request.queryParameters["turnierId"]
|
||||
val turnierId = turnierIdParam?.let { uuidFrom(it) }
|
||||
|
||||
val activeOnlyParam = call.request.queryParameters["activeOnly"]
|
||||
val activeOnly = activeOnlyParam?.toBoolean() ?: true
|
||||
|
||||
val plaetze = getPlatzUseCase.getByGroundType(boden, turnierId, activeOnly)
|
||||
val platzDtos = plaetze.map { it.toDto() }
|
||||
call.respond(HttpStatusCode.OK, ApiResponse.success(platzDtos))
|
||||
} catch (e: IllegalArgumentException) {
|
||||
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<List<PlatzDto>>(e.message ?: "Invalid ground type"))
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<List<PlatzDto>>("Failed to retrieve venues: ${e.message}"))
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/masterdata/plaetze/dimension/{dimension} - Get venues by dimensions
|
||||
get("/dimension/{dimension}") {
|
||||
try {
|
||||
val dimension = call.parameters["dimension"]
|
||||
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<List<PlatzDto>>("Dimension is required"))
|
||||
|
||||
val turnierIdParam = call.request.queryParameters["turnierId"]
|
||||
val turnierId = turnierIdParam?.let { uuidFrom(it) }
|
||||
|
||||
val activeOnlyParam = call.request.queryParameters["activeOnly"]
|
||||
val activeOnly = activeOnlyParam?.toBoolean() ?: true
|
||||
|
||||
val plaetze = getPlatzUseCase.getByDimensions(dimension, turnierId, activeOnly)
|
||||
val platzDtos = plaetze.map { it.toDto() }
|
||||
call.respond(HttpStatusCode.OK, ApiResponse.success(platzDtos))
|
||||
} catch (e: IllegalArgumentException) {
|
||||
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<List<PlatzDto>>(e.message ?: "Invalid dimension"))
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<List<PlatzDto>>("Failed to retrieve venues: ${e.message}"))
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/masterdata/plaetze/suitable - Get venues suitable for discipline
|
||||
get("/suitable") {
|
||||
try {
|
||||
val typParam = call.request.queryParameters["typ"]
|
||||
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<List<PlatzDto>>("Required venue type parameter is missing"))
|
||||
|
||||
val requiredType = try {
|
||||
PlatzTypE.valueOf(typParam.uppercase())
|
||||
} catch (_: Exception) {
|
||||
return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<List<PlatzDto>>("Invalid venue type: $typParam"))
|
||||
}
|
||||
|
||||
val requiredDimensions = call.request.queryParameters["dimension"]
|
||||
val turnierIdParam = call.request.queryParameters["turnierId"]
|
||||
val turnierId = turnierIdParam?.let { uuidFrom(it) }
|
||||
|
||||
val plaetze = getPlatzUseCase.getSuitableForDiscipline(requiredType, requiredDimensions, turnierId)
|
||||
val platzDtos = plaetze.map { it.toDto() }
|
||||
call.respond(HttpStatusCode.OK, ApiResponse.success(platzDtos))
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<List<PlatzDto>>("Failed to retrieve suitable venues: ${e.message}"))
|
||||
}
|
||||
}
|
||||
|
||||
// POST /api/masterdata/plaetze - Create new venue
|
||||
post {
|
||||
try {
|
||||
val createDto = call.receive<CreatePlatzDto>()
|
||||
|
||||
// Basic validation
|
||||
if (createDto.name.isBlank()) {
|
||||
call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
ApiResponse.error<PlatzDto>("Name is required")
|
||||
)
|
||||
return@post
|
||||
}
|
||||
|
||||
val turnierId = try {
|
||||
uuidFrom(createDto.turnierId)
|
||||
} catch (_: Exception) {
|
||||
return@post call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
ApiResponse.error<PlatzDto>("Invalid tournament ID format")
|
||||
)
|
||||
}
|
||||
|
||||
val typ = try {
|
||||
PlatzTypE.valueOf(createDto.typ.uppercase())
|
||||
} catch (_: Exception) {
|
||||
return@post call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
ApiResponse.error<PlatzDto>("Invalid venue type: ${createDto.typ}")
|
||||
)
|
||||
}
|
||||
|
||||
val request = CreatePlatzUseCase.CreatePlatzRequest(
|
||||
turnierId = turnierId,
|
||||
name = createDto.name,
|
||||
dimension = createDto.dimension,
|
||||
boden = createDto.boden,
|
||||
typ = typ,
|
||||
istAktiv = createDto.istAktiv,
|
||||
sortierReihenfolge = createDto.sortierReihenfolge
|
||||
)
|
||||
|
||||
val result = createPlatzUseCase.createPlatz(request)
|
||||
if (result.success) {
|
||||
call.respond(HttpStatusCode.Created, ApiResponse.success(result.platz!!.toDto()))
|
||||
} else {
|
||||
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<PlatzDto>("Validation failed: ${result.errors.joinToString(", ")}"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<PlatzDto>("Failed to create venue: ${e.message}"))
|
||||
}
|
||||
}
|
||||
|
||||
// PUT /api/masterdata/plaetze/{id} - Update existing venue
|
||||
put("/{id}") {
|
||||
try {
|
||||
val platzId = call.parameters["id"]?.let { uuidFrom(it) }
|
||||
?: return@put call.respond(HttpStatusCode.BadRequest, ApiResponse.error<PlatzDto>("Invalid venue ID"))
|
||||
|
||||
val updateDto = call.receive<UpdatePlatzDto>()
|
||||
|
||||
// Basic validation
|
||||
if (updateDto.name.isBlank()) {
|
||||
call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
ApiResponse.error<PlatzDto>("Name is required")
|
||||
)
|
||||
return@put
|
||||
}
|
||||
|
||||
val turnierId = try {
|
||||
uuidFrom(updateDto.turnierId)
|
||||
} catch (_: Exception) {
|
||||
return@put call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
ApiResponse.error<PlatzDto>("Invalid tournament ID format")
|
||||
)
|
||||
}
|
||||
|
||||
val typ = try {
|
||||
PlatzTypE.valueOf(updateDto.typ.uppercase())
|
||||
} catch (_: Exception) {
|
||||
return@put call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
ApiResponse.error<PlatzDto>("Invalid venue type: ${updateDto.typ}")
|
||||
)
|
||||
}
|
||||
|
||||
val request = CreatePlatzUseCase.UpdatePlatzRequest(
|
||||
platzId = platzId,
|
||||
turnierId = turnierId,
|
||||
name = updateDto.name,
|
||||
dimension = updateDto.dimension,
|
||||
boden = updateDto.boden,
|
||||
typ = typ,
|
||||
istAktiv = updateDto.istAktiv,
|
||||
sortierReihenfolge = updateDto.sortierReihenfolge
|
||||
)
|
||||
|
||||
val result = createPlatzUseCase.updatePlatz(request)
|
||||
if (result.success) {
|
||||
call.respond(HttpStatusCode.OK, ApiResponse.success(result.platz!!.toDto()))
|
||||
} else {
|
||||
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<PlatzDto>("Validation failed: ${result.errors.joinToString(", ")}"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<PlatzDto>("Failed to update venue: ${e.message}"))
|
||||
}
|
||||
}
|
||||
|
||||
// DELETE /api/masterdata/plaetze/{id} - Delete venue
|
||||
delete("/{id}") {
|
||||
try {
|
||||
val platzId = call.parameters["id"]?.let { uuidFrom(it) }
|
||||
?: return@delete call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Unit>("Invalid venue ID"))
|
||||
|
||||
val result = createPlatzUseCase.deletePlatz(platzId)
|
||||
if (result.success) {
|
||||
call.respond(HttpStatusCode.NoContent)
|
||||
} else {
|
||||
call.respond(HttpStatusCode.NotFound, ApiResponse.error<Unit>("Venue not found: ${result.errors.joinToString(", ")}"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<Unit>("Failed to delete venue: ${e.message}"))
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/masterdata/plaetze/count/tournament/{turnierId} - Count venues by tournament
|
||||
get("/count/tournament/{turnierId}") {
|
||||
try {
|
||||
val turnierId = call.parameters["turnierId"]?.let { uuidFrom(it) }
|
||||
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Long>("Invalid tournament ID"))
|
||||
|
||||
val count = getPlatzUseCase.countActiveByTournament(turnierId)
|
||||
call.respond(HttpStatusCode.OK, ApiResponse.success(count))
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<Long>("Failed to count venues: ${e.message}"))
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/masterdata/plaetze/count/type/{typ}/tournament/{turnierId} - Count venues by type and tournament
|
||||
get("/count/type/{typ}/tournament/{turnierId}") {
|
||||
try {
|
||||
val typParam = call.parameters["typ"]
|
||||
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Long>("Venue type is required"))
|
||||
|
||||
val typ = try {
|
||||
PlatzTypE.valueOf(typParam.uppercase())
|
||||
} catch (_: Exception) {
|
||||
return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Long>("Invalid venue type: $typParam"))
|
||||
}
|
||||
|
||||
val turnierId = call.parameters["turnierId"]?.let { uuidFrom(it) }
|
||||
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Long>("Invalid tournament ID"))
|
||||
|
||||
val activeOnlyParam = call.request.queryParameters["activeOnly"]
|
||||
val activeOnly = activeOnlyParam?.toBoolean() ?: true
|
||||
|
||||
val count = getPlatzUseCase.countByTypeAndTournament(typ, turnierId, activeOnly)
|
||||
call.respond(HttpStatusCode.OK, ApiResponse.success(count))
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<Long>("Failed to count venues: ${e.message}"))
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/masterdata/plaetze/grouped/tournament/{turnierId} - Get venues grouped by type
|
||||
get("/grouped/tournament/{turnierId}") {
|
||||
try {
|
||||
val turnierId = call.parameters["turnierId"]?.let { uuidFrom(it) }
|
||||
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Map<String, List<PlatzDto>>>("Invalid tournament ID"))
|
||||
|
||||
val activeOnlyParam = call.request.queryParameters["activeOnly"]
|
||||
val activeOnly = activeOnlyParam?.toBoolean() ?: true
|
||||
|
||||
val groupedVenues = getPlatzUseCase.getGroupedByTypeForTournament(turnierId, activeOnly)
|
||||
val groupedDtos = groupedVenues.mapKeys { it.key.name }.mapValues { entry ->
|
||||
entry.value.map { it.toDto() }
|
||||
}
|
||||
call.respond(HttpStatusCode.OK, ApiResponse.success(groupedDtos))
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<Map<String, List<PlatzDto>>>("Failed to retrieve grouped venues: ${e.message}"))
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/masterdata/plaetze/validate/{id} - Validate venue suitability
|
||||
get("/validate/{id}") {
|
||||
try {
|
||||
val platzId = call.parameters["id"]?.let { uuidFrom(it) }
|
||||
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Map<String, Any>>("Invalid venue ID"))
|
||||
|
||||
val requiredTypeParam = call.request.queryParameters["requiredType"]
|
||||
val requiredType = requiredTypeParam?.let {
|
||||
try {
|
||||
PlatzTypE.valueOf(it.uppercase())
|
||||
} catch (_: Exception) {
|
||||
return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Map<String, Any>>("Invalid required type: $it"))
|
||||
}
|
||||
}
|
||||
|
||||
val requiredDimensions = call.request.queryParameters["requiredDimensions"]
|
||||
val requiredGroundType = call.request.queryParameters["requiredGroundType"]
|
||||
|
||||
val (isValid, reasons) = getPlatzUseCase.validateVenueSuitability(platzId, requiredType, requiredDimensions, requiredGroundType)
|
||||
val response = mapOf(
|
||||
"isValid" to isValid,
|
||||
"reasons" to reasons
|
||||
)
|
||||
call.respond(HttpStatusCode.OK, ApiResponse.success(response))
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<Map<String, Any>>("Failed to validate venue: ${e.message}"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extension function to convert Platz domain object to PlatzDto.
|
||||
*/
|
||||
private fun Platz.toDto(): PlatzDto {
|
||||
return PlatzDto(
|
||||
id = this.id.toString(),
|
||||
turnierId = this.turnierId.toString(),
|
||||
name = this.name,
|
||||
dimension = this.dimension,
|
||||
boden = this.boden,
|
||||
typ = this.typ.name,
|
||||
istAktiv = this.istAktiv,
|
||||
sortierReihenfolge = this.sortierReihenfolge,
|
||||
createdAt = this.createdAt.toString(),
|
||||
updatedAt = this.updatedAt.toString()
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
plugins {
|
||||
kotlin("jvm")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.masterdata.masterdataDomain)
|
||||
implementation(projects.core.coreDomain)
|
||||
implementation(projects.core.coreUtils)
|
||||
testImplementation(projects.platform.platformTesting)
|
||||
}
|
||||
+390
@@ -0,0 +1,390 @@
|
||||
package at.mocode.masterdata.application.usecase
|
||||
|
||||
import at.mocode.core.domain.model.SparteE
|
||||
import at.mocode.masterdata.domain.model.AltersklasseDefinition
|
||||
import at.mocode.masterdata.domain.repository.AltersklasseRepository
|
||||
import at.mocode.core.domain.model.ValidationResult
|
||||
import at.mocode.core.domain.model.ValidationError
|
||||
import com.benasher44.uuid.Uuid
|
||||
import kotlinx.datetime.Clock
|
||||
|
||||
/**
|
||||
* Use case for creating and updating age class information.
|
||||
*
|
||||
* This use case encapsulates the business logic for age class management
|
||||
* including validation, duplicate checking, and persistence.
|
||||
*/
|
||||
class CreateAltersklasseUseCase(
|
||||
private val altersklasseRepository: AltersklasseRepository
|
||||
) {
|
||||
|
||||
/**
|
||||
* Request data for creating a new age class.
|
||||
*/
|
||||
data class CreateAltersklasseRequest(
|
||||
val altersklasseCode: String,
|
||||
val bezeichnung: String,
|
||||
val minAlter: Int? = null,
|
||||
val maxAlter: Int? = null,
|
||||
val stichtagRegelText: String? = "31.12. des laufenden Kalenderjahres",
|
||||
val sparteFilter: SparteE? = null,
|
||||
val geschlechtFilter: Char? = null,
|
||||
val oetoRegelReferenzId: Uuid? = null,
|
||||
val istAktiv: Boolean = true
|
||||
)
|
||||
|
||||
/**
|
||||
* Request data for updating an existing age class.
|
||||
*/
|
||||
data class UpdateAltersklasseRequest(
|
||||
val altersklasseId: Uuid,
|
||||
val altersklasseCode: String,
|
||||
val bezeichnung: String,
|
||||
val minAlter: Int? = null,
|
||||
val maxAlter: Int? = null,
|
||||
val stichtagRegelText: String? = "31.12. des laufenden Kalenderjahres",
|
||||
val sparteFilter: SparteE? = null,
|
||||
val geschlechtFilter: Char? = null,
|
||||
val oetoRegelReferenzId: Uuid? = null,
|
||||
val istAktiv: Boolean = true
|
||||
)
|
||||
|
||||
/**
|
||||
* Response data for age class creation.
|
||||
*/
|
||||
data class CreateAltersklasseResponse(
|
||||
val altersklasse: AltersklasseDefinition?,
|
||||
val success: Boolean,
|
||||
val errors: List<String> = emptyList()
|
||||
)
|
||||
|
||||
/**
|
||||
* Response data for age class update.
|
||||
*/
|
||||
data class UpdateAltersklasseResponse(
|
||||
val altersklasse: AltersklasseDefinition?,
|
||||
val success: Boolean,
|
||||
val errors: List<String> = emptyList()
|
||||
)
|
||||
|
||||
/**
|
||||
* Response data for age class deletion.
|
||||
*/
|
||||
data class DeleteAltersklasseResponse(
|
||||
val success: Boolean,
|
||||
val errors: List<String> = emptyList()
|
||||
)
|
||||
|
||||
/**
|
||||
* Creates a new age class after validation.
|
||||
*
|
||||
* @param request The age class creation request
|
||||
* @return CreateAltersklasseResponse with the created age class or validation errors
|
||||
*/
|
||||
suspend fun createAltersklasse(request: CreateAltersklasseRequest): CreateAltersklasseResponse {
|
||||
// Validate the request
|
||||
val validationResult = validateCreateRequest(request)
|
||||
if (!validationResult.isValid()) {
|
||||
val errors = (validationResult as ValidationResult.Invalid).errors.map { it.message }
|
||||
return CreateAltersklasseResponse(
|
||||
altersklasse = null,
|
||||
success = false,
|
||||
errors = errors
|
||||
)
|
||||
}
|
||||
|
||||
// Check for duplicates
|
||||
val duplicateCheck = checkForDuplicates(request.altersklasseCode)
|
||||
if (!duplicateCheck.isValid()) {
|
||||
val errors = (duplicateCheck as ValidationResult.Invalid).errors.map { it.message }
|
||||
return CreateAltersklasseResponse(
|
||||
altersklasse = null,
|
||||
success = false,
|
||||
errors = errors
|
||||
)
|
||||
}
|
||||
|
||||
// Create the domain object
|
||||
val now = Clock.System.now()
|
||||
val altersklasse = AltersklasseDefinition(
|
||||
altersklasseCode = request.altersklasseCode.trim().uppercase(),
|
||||
bezeichnung = request.bezeichnung.trim(),
|
||||
minAlter = request.minAlter,
|
||||
maxAlter = request.maxAlter,
|
||||
stichtagRegelText = request.stichtagRegelText?.trim(),
|
||||
sparteFilter = request.sparteFilter,
|
||||
geschlechtFilter = request.geschlechtFilter,
|
||||
oetoRegelReferenzId = request.oetoRegelReferenzId,
|
||||
istAktiv = request.istAktiv,
|
||||
createdAt = now,
|
||||
updatedAt = now
|
||||
)
|
||||
|
||||
// Save to repository
|
||||
val savedAltersklasse = altersklasseRepository.save(altersklasse)
|
||||
return CreateAltersklasseResponse(
|
||||
altersklasse = savedAltersklasse,
|
||||
success = true
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an existing age class after validation.
|
||||
*
|
||||
* @param request The age class update request
|
||||
* @return UpdateAltersklasseResponse containing the updated age class or validation errors
|
||||
*/
|
||||
suspend fun updateAltersklasse(request: UpdateAltersklasseRequest): UpdateAltersklasseResponse {
|
||||
// Check if age class exists
|
||||
val existingAltersklasse = altersklasseRepository.findById(request.altersklasseId)
|
||||
if (existingAltersklasse == null) {
|
||||
return UpdateAltersklasseResponse(
|
||||
altersklasse = null,
|
||||
success = false,
|
||||
errors = listOf("Age class with ID ${request.altersklasseId} not found")
|
||||
)
|
||||
}
|
||||
|
||||
// Validate the request
|
||||
val validationResult = validateUpdateRequest(request)
|
||||
if (!validationResult.isValid()) {
|
||||
val errors = (validationResult as ValidationResult.Invalid).errors.map { it.message }
|
||||
return UpdateAltersklasseResponse(
|
||||
altersklasse = null,
|
||||
success = false,
|
||||
errors = errors
|
||||
)
|
||||
}
|
||||
|
||||
// Check for duplicates (excluding current age class)
|
||||
val duplicateCheck = checkForDuplicatesExcluding(request.altersklasseCode, request.altersklasseId)
|
||||
if (!duplicateCheck.isValid()) {
|
||||
val errors = (duplicateCheck as ValidationResult.Invalid).errors.map { it.message }
|
||||
return UpdateAltersklasseResponse(
|
||||
altersklasse = null,
|
||||
success = false,
|
||||
errors = errors
|
||||
)
|
||||
}
|
||||
|
||||
// Update the domain object
|
||||
val updatedAltersklasse = existingAltersklasse.copy(
|
||||
altersklasseCode = request.altersklasseCode.trim().uppercase(),
|
||||
bezeichnung = request.bezeichnung.trim(),
|
||||
minAlter = request.minAlter,
|
||||
maxAlter = request.maxAlter,
|
||||
stichtagRegelText = request.stichtagRegelText?.trim(),
|
||||
sparteFilter = request.sparteFilter,
|
||||
geschlechtFilter = request.geschlechtFilter,
|
||||
oetoRegelReferenzId = request.oetoRegelReferenzId,
|
||||
istAktiv = request.istAktiv,
|
||||
updatedAt = Clock.System.now()
|
||||
)
|
||||
|
||||
// Save to repository
|
||||
val savedAltersklasse = altersklasseRepository.save(updatedAltersklasse)
|
||||
return UpdateAltersklasseResponse(
|
||||
altersklasse = savedAltersklasse,
|
||||
success = true
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes an age class by ID.
|
||||
*
|
||||
* @param altersklasseId The unique identifier of the age class to delete
|
||||
* @return DeleteAltersklasseResponse indicating success or failure
|
||||
*/
|
||||
suspend fun deleteAltersklasse(altersklasseId: Uuid): DeleteAltersklasseResponse {
|
||||
val deleted = altersklasseRepository.delete(altersklasseId)
|
||||
return if (deleted) {
|
||||
DeleteAltersklasseResponse(success = true)
|
||||
} else {
|
||||
DeleteAltersklasseResponse(
|
||||
success = false,
|
||||
errors = listOf("Age class with ID $altersklasseId not found or could not be deleted")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a create age class request.
|
||||
*/
|
||||
private fun validateCreateRequest(request: CreateAltersklasseRequest): ValidationResult {
|
||||
val errors = mutableListOf<ValidationError>()
|
||||
|
||||
// Age class code validation
|
||||
if (request.altersklasseCode.isBlank()) {
|
||||
errors.add(ValidationError("altersklasseCode", "Age class code is required", "REQUIRED")) // "REQUIRED"
|
||||
} else if (request.altersklasseCode.length > 50) {
|
||||
errors.add(ValidationError("altersklasseCode", "Age class code must not exceed 50 characters", "MAX_LENGTH"))
|
||||
} else if (!request.altersklasseCode.matches(Regex("^[A-Z0-9_]+$"))) {
|
||||
errors.add(ValidationError("altersklasseCode", "Age class code must contain only uppercase letters, numbers, and underscores", "INVALID_FORMAT"))
|
||||
}
|
||||
|
||||
// Bezeichnung validation
|
||||
if (request.bezeichnung.isBlank()) {
|
||||
errors.add(ValidationError("bezeichnung", "Bezeichnung is required", "REQUIRED"))
|
||||
} else if (request.bezeichnung.length > 200) {
|
||||
errors.add(ValidationError("bezeichnung", "Bezeichnung must not exceed 200 characters", "MAX_LENGTH"))
|
||||
}
|
||||
|
||||
// Age range validation
|
||||
request.minAlter?.let { min ->
|
||||
if (min < 0) {
|
||||
errors.add(ValidationError("minAlter", "Minimum age must be non-negative", "INVALID_VALUE"))
|
||||
}
|
||||
}
|
||||
|
||||
request.maxAlter?.let { max ->
|
||||
if (max < 0) {
|
||||
errors.add(ValidationError("maxAlter", "Maximum age must be non-negative", "INVALID_VALUE"))
|
||||
}
|
||||
request.minAlter?.let { min ->
|
||||
if (max < min) {
|
||||
errors.add(ValidationError("maxAlter", "Maximum age must be greater than or equal to minimum age", "INVALID_RANGE"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Stichtag regel text validation
|
||||
request.stichtagRegelText?.let { text ->
|
||||
if (text.length > 500) {
|
||||
errors.add(ValidationError("stichtagRegelText", "Stichtag regel text must not exceed 500 characters", "MAX_LENGTH"))
|
||||
}
|
||||
}
|
||||
|
||||
// Gender filter validation
|
||||
request.geschlechtFilter?.let { gender ->
|
||||
if (gender != 'M' && gender != 'W') {
|
||||
errors.add(ValidationError("geschlechtFilter", "Gender filter must be 'M' or 'W'", "INVALID_VALUE"))
|
||||
}
|
||||
}
|
||||
|
||||
return if (errors.isEmpty()) {
|
||||
ValidationResult.Valid
|
||||
} else {
|
||||
ValidationResult.Invalid(errors)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates an update age class request.
|
||||
*/
|
||||
private fun validateUpdateRequest(request: UpdateAltersklasseRequest): ValidationResult {
|
||||
// Use the same validation logic as create request
|
||||
val createRequest = CreateAltersklasseRequest(
|
||||
altersklasseCode = request.altersklasseCode,
|
||||
bezeichnung = request.bezeichnung,
|
||||
minAlter = request.minAlter,
|
||||
maxAlter = request.maxAlter,
|
||||
stichtagRegelText = request.stichtagRegelText,
|
||||
sparteFilter = request.sparteFilter,
|
||||
geschlechtFilter = request.geschlechtFilter,
|
||||
oetoRegelReferenzId = request.oetoRegelReferenzId,
|
||||
istAktiv = request.istAktiv
|
||||
)
|
||||
return validateCreateRequest(createRequest)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks for duplicate age class codes.
|
||||
*/
|
||||
private suspend fun checkForDuplicates(altersklasseCode: String): ValidationResult {
|
||||
val errors = mutableListOf<ValidationError>()
|
||||
|
||||
if (altersklasseRepository.existsByCode(altersklasseCode.trim().uppercase())) {
|
||||
errors.add(ValidationError("altersklasseCode", "Age class with code '${altersklasseCode.uppercase()}' already exists", "DUPLICATE"))
|
||||
}
|
||||
|
||||
return if (errors.isEmpty()) {
|
||||
ValidationResult.Valid
|
||||
} else {
|
||||
ValidationResult.Invalid(errors)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks for duplicate age class codes excluding a specific age class ID.
|
||||
*/
|
||||
private suspend fun checkForDuplicatesExcluding(altersklasseCode: String, excludeId: Uuid): ValidationResult {
|
||||
val errors = mutableListOf<ValidationError>()
|
||||
|
||||
// Check code
|
||||
val existing = altersklasseRepository.findByCode(altersklasseCode.trim().uppercase())
|
||||
if (existing != null && existing.altersklasseId != excludeId) {
|
||||
errors.add(ValidationError("altersklasseCode", "Age class with code '${altersklasseCode.uppercase()}' already exists", "DUPLICATE"))
|
||||
}
|
||||
|
||||
return if (errors.isEmpty()) {
|
||||
ValidationResult.Valid
|
||||
} else {
|
||||
ValidationResult.Invalid(errors)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates age eligibility for a specific age class and participant.
|
||||
* This is a business logic method that can be used by other parts of the application.
|
||||
*
|
||||
* @param altersklasseId The age class ID
|
||||
* @param participantAge The participant's age
|
||||
* @param participantGender The participant's gender ('M', 'W')
|
||||
* @param participantSparte The participant's sport type
|
||||
* @return ValidationResult indicating eligibility or reasons for ineligibility
|
||||
*/
|
||||
suspend fun validateEligibility(
|
||||
altersklasseId: Uuid,
|
||||
participantAge: Int,
|
||||
participantGender: Char,
|
||||
participantSparte: SparteE
|
||||
): ValidationResult {
|
||||
val errors = mutableListOf<ValidationError>()
|
||||
|
||||
// Get the age class
|
||||
val altersklasse = altersklasseRepository.findById(altersklasseId)
|
||||
if (altersklasse == null) {
|
||||
errors.add(ValidationError("altersklasseId", "Age class not found", "NOT_FOUND"))
|
||||
return ValidationResult.Invalid(errors)
|
||||
}
|
||||
|
||||
// Check if age class is active
|
||||
if (!altersklasse.istAktiv) {
|
||||
errors.add(ValidationError("altersklasse", "Age class is not active", "INACTIVE"))
|
||||
}
|
||||
|
||||
// Check age eligibility
|
||||
altersklasse.minAlter?.let { min ->
|
||||
if (participantAge < min) {
|
||||
errors.add(ValidationError("age", "Participant is too young for this age class (minimum age: $min)", "AGE_TOO_LOW"))
|
||||
}
|
||||
}
|
||||
|
||||
altersklasse.maxAlter?.let { max ->
|
||||
if (participantAge > max) {
|
||||
errors.add(ValidationError("age", "Participant is too old for this age class (maximum age: $max)", "AGE_TOO_HIGH"))
|
||||
}
|
||||
}
|
||||
|
||||
// Check gender eligibility
|
||||
altersklasse.geschlechtFilter?.let { requiredGender ->
|
||||
if (participantGender != requiredGender) {
|
||||
val genderName = if (requiredGender == 'M') "male" else "female"
|
||||
errors.add(ValidationError("gender", "This age class is only for $genderName participants", "GENDER_MISMATCH"))
|
||||
}
|
||||
}
|
||||
|
||||
// Check sport eligibility
|
||||
altersklasse.sparteFilter?.let { requiredSparte ->
|
||||
if (participantSparte != requiredSparte) {
|
||||
errors.add(ValidationError("sparte", "This age class is only for ${requiredSparte.name} sport", "SPORT_MISMATCH"))
|
||||
}
|
||||
}
|
||||
|
||||
return if (errors.isEmpty()) {
|
||||
ValidationResult.Valid
|
||||
} else {
|
||||
ValidationResult.Invalid(errors)
|
||||
}
|
||||
}
|
||||
}
|
||||
+338
@@ -0,0 +1,338 @@
|
||||
package at.mocode.masterdata.application.usecase
|
||||
|
||||
import at.mocode.masterdata.domain.model.BundeslandDefinition
|
||||
import at.mocode.masterdata.domain.repository.BundeslandRepository
|
||||
import at.mocode.core.domain.model.ValidationResult
|
||||
import at.mocode.core.domain.model.ValidationError
|
||||
import com.benasher44.uuid.Uuid
|
||||
import kotlinx.datetime.Clock
|
||||
|
||||
/**
|
||||
* Use case for creating and updating federal state information.
|
||||
*
|
||||
* This use case encapsulates the business logic for federal state management
|
||||
* including validation, duplicate checking, and persistence.
|
||||
*/
|
||||
class CreateBundeslandUseCase(
|
||||
private val bundeslandRepository: BundeslandRepository
|
||||
) {
|
||||
|
||||
/**
|
||||
* Request data for creating a new federal state.
|
||||
*/
|
||||
data class CreateBundeslandRequest(
|
||||
val landId: Uuid,
|
||||
val oepsCode: String? = null,
|
||||
val iso3166_2_Code: String? = null,
|
||||
val name: String,
|
||||
val kuerzel: String? = null,
|
||||
val wappenUrl: String? = null,
|
||||
val istAktiv: Boolean = true,
|
||||
val sortierReihenfolge: Int? = null
|
||||
)
|
||||
|
||||
/**
|
||||
* Request data for updating an existing federal state.
|
||||
*/
|
||||
data class UpdateBundeslandRequest(
|
||||
val bundeslandId: Uuid,
|
||||
val landId: Uuid,
|
||||
val oepsCode: String? = null,
|
||||
val iso3166_2_Code: String? = null,
|
||||
val name: String,
|
||||
val kuerzel: String? = null,
|
||||
val wappenUrl: String? = null,
|
||||
val istAktiv: Boolean = true,
|
||||
val sortierReihenfolge: Int? = null
|
||||
)
|
||||
|
||||
/**
|
||||
* Response data for federal state creation.
|
||||
*/
|
||||
data class CreateBundeslandResponse(
|
||||
val bundesland: BundeslandDefinition?,
|
||||
val success: Boolean,
|
||||
val errors: List<String> = emptyList()
|
||||
)
|
||||
|
||||
/**
|
||||
* Response data for federal state update.
|
||||
*/
|
||||
data class UpdateBundeslandResponse(
|
||||
val bundesland: BundeslandDefinition?,
|
||||
val success: Boolean,
|
||||
val errors: List<String> = emptyList()
|
||||
)
|
||||
|
||||
/**
|
||||
* Response data for federal state deletion.
|
||||
*/
|
||||
data class DeleteBundeslandResponse(
|
||||
val success: Boolean,
|
||||
val errors: List<String> = emptyList()
|
||||
)
|
||||
|
||||
/**
|
||||
* Creates a new federal state after validation.
|
||||
*
|
||||
* @param request The federal state creation request
|
||||
* @return CreateBundeslandResponse with the created federal state or validation errors
|
||||
*/
|
||||
suspend fun createBundesland(request: CreateBundeslandRequest): CreateBundeslandResponse {
|
||||
// Validate the request
|
||||
val validationResult = validateCreateRequest(request)
|
||||
if (!validationResult.isValid()) {
|
||||
val errors = (validationResult as ValidationResult.Invalid).errors.map { it.message }
|
||||
return CreateBundeslandResponse(
|
||||
bundesland = null,
|
||||
success = false,
|
||||
errors = errors
|
||||
)
|
||||
}
|
||||
|
||||
// Check for duplicates
|
||||
val duplicateCheck = checkForDuplicates(request.oepsCode, request.iso3166_2_Code, request.landId)
|
||||
if (!duplicateCheck.isValid()) {
|
||||
val errors = (duplicateCheck as ValidationResult.Invalid).errors.map { it.message }
|
||||
return CreateBundeslandResponse(
|
||||
bundesland = null,
|
||||
success = false,
|
||||
errors = errors
|
||||
)
|
||||
}
|
||||
|
||||
// Create the domain object
|
||||
val now = Clock.System.now()
|
||||
val bundesland = BundeslandDefinition(
|
||||
landId = request.landId,
|
||||
oepsCode = request.oepsCode?.trim(),
|
||||
iso3166_2_Code = request.iso3166_2_Code?.trim()?.uppercase(),
|
||||
name = request.name.trim(),
|
||||
kuerzel = request.kuerzel?.trim(),
|
||||
wappenUrl = request.wappenUrl?.trim(),
|
||||
istAktiv = request.istAktiv,
|
||||
sortierReihenfolge = request.sortierReihenfolge,
|
||||
createdAt = now,
|
||||
updatedAt = now
|
||||
)
|
||||
|
||||
// Save to repository
|
||||
val savedBundesland = bundeslandRepository.save(bundesland)
|
||||
return CreateBundeslandResponse(
|
||||
bundesland = savedBundesland,
|
||||
success = true
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an existing federal state after validation.
|
||||
*
|
||||
* @param request The federal state update request
|
||||
* @return UpdateBundeslandResponse containing the updated federal state or validation errors
|
||||
*/
|
||||
suspend fun updateBundesland(request: UpdateBundeslandRequest): UpdateBundeslandResponse {
|
||||
// Check if federal state exists
|
||||
val existingBundesland = bundeslandRepository.findById(request.bundeslandId)
|
||||
if (existingBundesland == null) {
|
||||
return UpdateBundeslandResponse(
|
||||
bundesland = null,
|
||||
success = false,
|
||||
errors = listOf("Federal state with ID ${request.bundeslandId} not found")
|
||||
)
|
||||
}
|
||||
|
||||
// Validate the request
|
||||
val validationResult = validateUpdateRequest(request)
|
||||
if (!validationResult.isValid()) {
|
||||
val errors = (validationResult as ValidationResult.Invalid).errors.map { it.message }
|
||||
return UpdateBundeslandResponse(
|
||||
bundesland = null,
|
||||
success = false,
|
||||
errors = errors
|
||||
)
|
||||
}
|
||||
|
||||
// Check for duplicates (excluding current federal state)
|
||||
val duplicateCheck = checkForDuplicatesExcluding(
|
||||
request.oepsCode,
|
||||
request.iso3166_2_Code,
|
||||
request.landId,
|
||||
request.bundeslandId
|
||||
)
|
||||
if (!duplicateCheck.isValid()) {
|
||||
val errors = (duplicateCheck as ValidationResult.Invalid).errors.map { it.message }
|
||||
return UpdateBundeslandResponse(
|
||||
bundesland = null,
|
||||
success = false,
|
||||
errors = errors
|
||||
)
|
||||
}
|
||||
|
||||
// Update the domain object
|
||||
val updatedBundesland = existingBundesland.copy(
|
||||
landId = request.landId,
|
||||
oepsCode = request.oepsCode?.trim(),
|
||||
iso3166_2_Code = request.iso3166_2_Code?.trim()?.uppercase(),
|
||||
name = request.name.trim(),
|
||||
kuerzel = request.kuerzel?.trim(),
|
||||
wappenUrl = request.wappenUrl?.trim(),
|
||||
istAktiv = request.istAktiv,
|
||||
sortierReihenfolge = request.sortierReihenfolge,
|
||||
updatedAt = Clock.System.now()
|
||||
)
|
||||
|
||||
// Save to repository
|
||||
val savedBundesland = bundeslandRepository.save(updatedBundesland)
|
||||
return UpdateBundeslandResponse(
|
||||
bundesland = savedBundesland,
|
||||
success = true
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a federal state by ID.
|
||||
*
|
||||
* @param bundeslandId The unique identifier of the federal state to delete
|
||||
* @return DeleteBundeslandResponse indicating success or failure
|
||||
*/
|
||||
suspend fun deleteBundesland(bundeslandId: Uuid): DeleteBundeslandResponse {
|
||||
val deleted = bundeslandRepository.delete(bundeslandId)
|
||||
return if (deleted) {
|
||||
DeleteBundeslandResponse(success = true)
|
||||
} else {
|
||||
DeleteBundeslandResponse(
|
||||
success = false,
|
||||
errors = listOf("Federal state with ID $bundeslandId not found or could not be deleted")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a create federal state request.
|
||||
*/
|
||||
private fun validateCreateRequest(request: CreateBundeslandRequest): ValidationResult {
|
||||
val errors = mutableListOf<ValidationError>()
|
||||
|
||||
// Name validation
|
||||
if (request.name.isBlank()) {
|
||||
errors.add(ValidationError("name", "Name is required", "REQUIRED"))
|
||||
} else if (request.name.length > 100) {
|
||||
errors.add(ValidationError("name", "Name must not exceed 100 characters", "MAX_LENGTH"))
|
||||
}
|
||||
|
||||
// OEPS code validation
|
||||
request.oepsCode?.let { code ->
|
||||
if (code.isBlank()) {
|
||||
errors.add(ValidationError("oepsCode", "OEPS code cannot be empty if provided", "INVALID_FORMAT"))
|
||||
} else if (code.length > 10) {
|
||||
errors.add(ValidationError("oepsCode", "OEPS code must not exceed 10 characters", "MAX_LENGTH"))
|
||||
}
|
||||
}
|
||||
|
||||
// ISO 3166-2 code validation
|
||||
request.iso3166_2_Code?.let { code ->
|
||||
if (code.isBlank()) {
|
||||
errors.add(ValidationError("iso3166_2_Code", "ISO 3166-2 code cannot be empty if provided", "INVALID_FORMAT"))
|
||||
} else if (code.length > 10) {
|
||||
errors.add(ValidationError("iso3166_2_Code", "ISO 3166-2 code must not exceed 10 characters", "MAX_LENGTH"))
|
||||
}
|
||||
}
|
||||
|
||||
// Kuerzel validation
|
||||
request.kuerzel?.let { kuerzel ->
|
||||
if (kuerzel.length > 10) {
|
||||
errors.add(ValidationError("kuerzel", "Kuerzel must not exceed 10 characters", "MAX_LENGTH"))
|
||||
}
|
||||
}
|
||||
|
||||
// Sorting order validation
|
||||
request.sortierReihenfolge?.let { order ->
|
||||
if (order < 0) {
|
||||
errors.add(ValidationError("sortierReihenfolge", "Sorting order must be non-negative", "INVALID_VALUE"))
|
||||
}
|
||||
}
|
||||
|
||||
return if (errors.isEmpty()) {
|
||||
ValidationResult.Valid
|
||||
} else {
|
||||
ValidationResult.Invalid(errors)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates an update federal state request.
|
||||
*/
|
||||
private fun validateUpdateRequest(request: UpdateBundeslandRequest): ValidationResult {
|
||||
// Use the same validation logic as create request
|
||||
val createRequest = CreateBundeslandRequest(
|
||||
landId = request.landId,
|
||||
oepsCode = request.oepsCode,
|
||||
iso3166_2_Code = request.iso3166_2_Code,
|
||||
name = request.name,
|
||||
kuerzel = request.kuerzel,
|
||||
wappenUrl = request.wappenUrl,
|
||||
istAktiv = request.istAktiv,
|
||||
sortierReihenfolge = request.sortierReihenfolge
|
||||
)
|
||||
return validateCreateRequest(createRequest)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks for duplicate codes.
|
||||
*/
|
||||
private suspend fun checkForDuplicates(oepsCode: String?, iso3166_2_Code: String?, landId: Uuid): ValidationResult {
|
||||
val errors = mutableListOf<ValidationError>()
|
||||
|
||||
oepsCode?.let { code ->
|
||||
if (bundeslandRepository.existsByOepsCode(code.trim(), landId)) {
|
||||
errors.add(ValidationError("oepsCode", "Federal state with OEPS code '$code' already exists for this country", "DUPLICATE"))
|
||||
}
|
||||
}
|
||||
|
||||
iso3166_2_Code?.let { code ->
|
||||
if (bundeslandRepository.existsByIso3166_2_Code(code.trim().uppercase())) {
|
||||
errors.add(ValidationError("iso3166_2_Code", "Federal state with ISO 3166-2 code '${code.uppercase()}' already exists", "DUPLICATE"))
|
||||
}
|
||||
}
|
||||
|
||||
return if (errors.isEmpty()) {
|
||||
ValidationResult.Valid
|
||||
} else {
|
||||
ValidationResult.Invalid(errors)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks for duplicate codes excluding a specific federal state ID.
|
||||
*/
|
||||
private suspend fun checkForDuplicatesExcluding(
|
||||
oepsCode: String?,
|
||||
iso3166_2_Code: String?,
|
||||
landId: Uuid,
|
||||
excludeId: Uuid
|
||||
): ValidationResult {
|
||||
val errors = mutableListOf<ValidationError>()
|
||||
|
||||
// Check OEPS code
|
||||
oepsCode?.let { code ->
|
||||
val existing = bundeslandRepository.findByOepsCode(code.trim(), landId)
|
||||
if (existing != null && existing.bundeslandId != excludeId) {
|
||||
errors.add(ValidationError("oepsCode", "Federal state with OEPS code '$code' already exists for this country", "DUPLICATE"))
|
||||
}
|
||||
}
|
||||
|
||||
// Check ISO 3166-2 code
|
||||
iso3166_2_Code?.let { code ->
|
||||
val existing = bundeslandRepository.findByIso3166_2_Code(code.trim().uppercase())
|
||||
if (existing != null && existing.bundeslandId != excludeId) {
|
||||
errors.add(ValidationError("iso3166_2_Code", "Federal state with ISO 3166-2 code '${code.uppercase()}' already exists", "DUPLICATE"))
|
||||
}
|
||||
}
|
||||
|
||||
return if (errors.isEmpty()) {
|
||||
ValidationResult.Valid
|
||||
} else {
|
||||
ValidationResult.Invalid(errors)
|
||||
}
|
||||
}
|
||||
}
|
||||
+338
@@ -0,0 +1,338 @@
|
||||
package at.mocode.masterdata.application.usecase
|
||||
|
||||
import at.mocode.masterdata.domain.model.LandDefinition
|
||||
import at.mocode.masterdata.domain.repository.LandRepository
|
||||
import at.mocode.core.domain.model.ValidationResult
|
||||
import at.mocode.core.domain.model.ValidationError
|
||||
import com.benasher44.uuid.Uuid
|
||||
import kotlinx.datetime.Clock
|
||||
|
||||
/**
|
||||
* Use case for creating and updating country information.
|
||||
*
|
||||
* This use case encapsulates the business logic for country management
|
||||
* including validation, duplicate checking, and persistence.
|
||||
*/
|
||||
class CreateCountryUseCase(
|
||||
private val landRepository: LandRepository
|
||||
) {
|
||||
|
||||
/**
|
||||
* Request data for creating a new country.
|
||||
*/
|
||||
data class CreateCountryRequest(
|
||||
val isoAlpha2Code: String,
|
||||
val isoAlpha3Code: String,
|
||||
val isoNumerischerCode: String? = null,
|
||||
val nameDeutsch: String,
|
||||
val nameEnglisch: String? = null,
|
||||
val wappenUrl: String? = null,
|
||||
val istEuMitglied: Boolean? = null,
|
||||
val istEwrMitglied: Boolean? = null,
|
||||
val istAktiv: Boolean = true,
|
||||
val sortierReihenfolge: Int? = null
|
||||
)
|
||||
|
||||
/**
|
||||
* Request data for updating an existing country.
|
||||
*/
|
||||
data class UpdateCountryRequest(
|
||||
val landId: Uuid,
|
||||
val isoAlpha2Code: String,
|
||||
val isoAlpha3Code: String,
|
||||
val isoNumerischerCode: String? = null,
|
||||
val nameDeutsch: String,
|
||||
val nameEnglisch: String? = null,
|
||||
val wappenUrl: String? = null,
|
||||
val istEuMitglied: Boolean? = null,
|
||||
val istEwrMitglied: Boolean? = null,
|
||||
val istAktiv: Boolean = true,
|
||||
val sortierReihenfolge: Int? = null
|
||||
)
|
||||
|
||||
/**
|
||||
* Response data for country creation.
|
||||
*/
|
||||
data class CreateCountryResponse(
|
||||
val country: LandDefinition?,
|
||||
val success: Boolean,
|
||||
val errors: List<String> = emptyList()
|
||||
)
|
||||
|
||||
/**
|
||||
* Response data for country update.
|
||||
*/
|
||||
data class UpdateCountryResponse(
|
||||
val country: LandDefinition?,
|
||||
val success: Boolean,
|
||||
val errors: List<String> = emptyList()
|
||||
)
|
||||
|
||||
/**
|
||||
* Response data for country deletion.
|
||||
*/
|
||||
data class DeleteCountryResponse(
|
||||
val success: Boolean,
|
||||
val errors: List<String> = emptyList()
|
||||
)
|
||||
|
||||
/**
|
||||
* Creates a new country after validation.
|
||||
*
|
||||
* @param request The country creation request
|
||||
* @return CreateCountryResponse with the created country or validation errors
|
||||
*/
|
||||
suspend fun createCountry(request: CreateCountryRequest): CreateCountryResponse {
|
||||
// Validate the request
|
||||
val validationResult = validateCreateRequest(request)
|
||||
if (!validationResult.isValid()) {
|
||||
val errors = (validationResult as ValidationResult.Invalid).errors.map { it.message }
|
||||
return CreateCountryResponse(
|
||||
country = null,
|
||||
success = false,
|
||||
errors = errors
|
||||
)
|
||||
}
|
||||
|
||||
// Check for duplicates
|
||||
val duplicateCheck = checkForDuplicates(request.isoAlpha2Code, request.isoAlpha3Code)
|
||||
if (!duplicateCheck.isValid()) {
|
||||
val errors = (duplicateCheck as ValidationResult.Invalid).errors.map { it.message }
|
||||
return CreateCountryResponse(
|
||||
country = null,
|
||||
success = false,
|
||||
errors = errors
|
||||
)
|
||||
}
|
||||
|
||||
// Create the domain object
|
||||
val now = Clock.System.now()
|
||||
val country = LandDefinition(
|
||||
isoAlpha2Code = request.isoAlpha2Code.uppercase(),
|
||||
isoAlpha3Code = request.isoAlpha3Code.uppercase(),
|
||||
isoNumerischerCode = request.isoNumerischerCode,
|
||||
nameDeutsch = request.nameDeutsch.trim(),
|
||||
nameEnglisch = request.nameEnglisch?.trim(),
|
||||
wappenUrl = request.wappenUrl?.trim(),
|
||||
istEuMitglied = request.istEuMitglied,
|
||||
istEwrMitglied = request.istEwrMitglied,
|
||||
istAktiv = request.istAktiv,
|
||||
sortierReihenfolge = request.sortierReihenfolge,
|
||||
createdAt = now,
|
||||
updatedAt = now
|
||||
)
|
||||
|
||||
// Save to repository
|
||||
val savedCountry = landRepository.save(country)
|
||||
return CreateCountryResponse(
|
||||
country = savedCountry,
|
||||
success = true
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an existing country after validation.
|
||||
*
|
||||
* @param request The country update request
|
||||
* @return ValidationResult containing the updated country or validation errors
|
||||
*/
|
||||
suspend fun updateCountry(request: UpdateCountryRequest): UpdateCountryResponse {
|
||||
// Check if country exists
|
||||
val existingCountry = landRepository.findById(request.landId)
|
||||
if (existingCountry == null) {
|
||||
return UpdateCountryResponse(
|
||||
country = null,
|
||||
success = false,
|
||||
errors = listOf("Country with ID ${request.landId} not found")
|
||||
)
|
||||
}
|
||||
|
||||
// Validate the request
|
||||
val validationResult = validateUpdateRequest(request)
|
||||
if (!validationResult.isValid()) {
|
||||
val errors = (validationResult as ValidationResult.Invalid).errors.map { it.message }
|
||||
return UpdateCountryResponse(
|
||||
country = null,
|
||||
success = false,
|
||||
errors = errors
|
||||
)
|
||||
}
|
||||
|
||||
// Check for duplicates (excluding current country)
|
||||
val duplicateCheck = checkForDuplicatesExcluding(
|
||||
request.isoAlpha2Code,
|
||||
request.isoAlpha3Code,
|
||||
request.landId
|
||||
)
|
||||
if (!duplicateCheck.isValid()) {
|
||||
val errors = (duplicateCheck as ValidationResult.Invalid).errors.map { it.message }
|
||||
return UpdateCountryResponse(
|
||||
country = null,
|
||||
success = false,
|
||||
errors = errors
|
||||
)
|
||||
}
|
||||
|
||||
// Update the domain object
|
||||
val updatedCountry = existingCountry.copy(
|
||||
isoAlpha2Code = request.isoAlpha2Code.uppercase(),
|
||||
isoAlpha3Code = request.isoAlpha3Code.uppercase(),
|
||||
isoNumerischerCode = request.isoNumerischerCode,
|
||||
nameDeutsch = request.nameDeutsch.trim(),
|
||||
nameEnglisch = request.nameEnglisch?.trim(),
|
||||
wappenUrl = request.wappenUrl?.trim(),
|
||||
istEuMitglied = request.istEuMitglied,
|
||||
istEwrMitglied = request.istEwrMitglied,
|
||||
istAktiv = request.istAktiv,
|
||||
sortierReihenfolge = request.sortierReihenfolge,
|
||||
updatedAt = Clock.System.now()
|
||||
)
|
||||
|
||||
// Save to repository
|
||||
val savedCountry = landRepository.save(updatedCountry)
|
||||
return UpdateCountryResponse(
|
||||
country = savedCountry,
|
||||
success = true
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a country by ID.
|
||||
*
|
||||
* @param countryId The unique identifier of the country to delete
|
||||
* @return DeleteCountryResponse indicating success or failure
|
||||
*/
|
||||
suspend fun deleteCountry(countryId: Uuid): DeleteCountryResponse {
|
||||
val deleted = landRepository.delete(countryId)
|
||||
return if (deleted) {
|
||||
DeleteCountryResponse(success = true)
|
||||
} else {
|
||||
DeleteCountryResponse(
|
||||
success = false,
|
||||
errors = listOf("Country with ID $countryId not found or could not be deleted")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a create country request.
|
||||
*/
|
||||
private fun validateCreateRequest(request: CreateCountryRequest): ValidationResult {
|
||||
val errors = mutableListOf<ValidationError>()
|
||||
|
||||
// ISO Alpha-2 Code validation
|
||||
if (request.isoAlpha2Code.isBlank()) {
|
||||
errors.add(ValidationError("isoAlpha2Code", "ISO Alpha-2 code is required", "REQUIRED"))
|
||||
} else if (request.isoAlpha2Code.length != 2) {
|
||||
errors.add(ValidationError("isoAlpha2Code", "ISO Alpha-2 code must be exactly 2 characters", "INVALID_LENGTH"))
|
||||
} else if (!request.isoAlpha2Code.all { it.isLetter() }) {
|
||||
errors.add(ValidationError("isoAlpha2Code", "ISO Alpha-2 code must contain only letters", "INVALID_FORMAT"))
|
||||
}
|
||||
|
||||
// ISO Alpha-3 Code validation
|
||||
if (request.isoAlpha3Code.isBlank()) {
|
||||
errors.add(ValidationError("isoAlpha3Code", "ISO Alpha-3 code is required", "REQUIRED"))
|
||||
} else if (request.isoAlpha3Code.length != 3) {
|
||||
errors.add(ValidationError("isoAlpha3Code", "ISO Alpha-3 code must be exactly 3 characters", "INVALID_LENGTH"))
|
||||
} else if (!request.isoAlpha3Code.all { it.isLetter() }) {
|
||||
errors.add(ValidationError("isoAlpha3Code", "ISO Alpha-3 code must contain only letters", "INVALID_FORMAT"))
|
||||
}
|
||||
|
||||
// German name validation
|
||||
if (request.nameDeutsch.isBlank()) {
|
||||
errors.add(ValidationError("nameDeutsch", "German name is required", "REQUIRED"))
|
||||
} else if (request.nameDeutsch.length > 100) {
|
||||
errors.add(ValidationError("nameDeutsch", "German name must not exceed 100 characters", "MAX_LENGTH"))
|
||||
}
|
||||
|
||||
// English name validation
|
||||
request.nameEnglisch?.let { name ->
|
||||
if (name.length > 100) {
|
||||
errors.add(ValidationError("nameEnglisch", "English name must not exceed 100 characters", "MAX_LENGTH"))
|
||||
}
|
||||
}
|
||||
|
||||
// Sorting order validation
|
||||
request.sortierReihenfolge?.let { order ->
|
||||
if (order < 0) {
|
||||
errors.add(ValidationError("sortierReihenfolge", "Sorting order must be non-negative", "INVALID_VALUE"))
|
||||
}
|
||||
}
|
||||
|
||||
return if (errors.isEmpty()) {
|
||||
ValidationResult.Valid
|
||||
} else {
|
||||
ValidationResult.Invalid(errors)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates an update country request.
|
||||
*/
|
||||
private fun validateUpdateRequest(request: UpdateCountryRequest): ValidationResult {
|
||||
// Use the same validation logic as create request
|
||||
val createRequest = CreateCountryRequest(
|
||||
isoAlpha2Code = request.isoAlpha2Code,
|
||||
isoAlpha3Code = request.isoAlpha3Code,
|
||||
isoNumerischerCode = request.isoNumerischerCode,
|
||||
nameDeutsch = request.nameDeutsch,
|
||||
nameEnglisch = request.nameEnglisch,
|
||||
wappenUrl = request.wappenUrl,
|
||||
istEuMitglied = request.istEuMitglied,
|
||||
istEwrMitglied = request.istEwrMitglied,
|
||||
istAktiv = request.istAktiv,
|
||||
sortierReihenfolge = request.sortierReihenfolge
|
||||
)
|
||||
return validateCreateRequest(createRequest)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks for duplicate ISO codes.
|
||||
*/
|
||||
private suspend fun checkForDuplicates(isoAlpha2Code: String, isoAlpha3Code: String): ValidationResult {
|
||||
val errors = mutableListOf<ValidationError>()
|
||||
|
||||
if (landRepository.existsByIsoAlpha2Code(isoAlpha2Code.uppercase())) {
|
||||
errors.add(ValidationError("isoAlpha2Code", "Country with ISO Alpha-2 code '${isoAlpha2Code.uppercase()}' already exists", "DUPLICATE"))
|
||||
}
|
||||
|
||||
if (landRepository.existsByIsoAlpha3Code(isoAlpha3Code.uppercase())) {
|
||||
errors.add(ValidationError("isoAlpha3Code", "Country with ISO Alpha-3 code '${isoAlpha3Code.uppercase()}' already exists", "DUPLICATE"))
|
||||
}
|
||||
|
||||
return if (errors.isEmpty()) {
|
||||
ValidationResult.Valid
|
||||
} else {
|
||||
ValidationResult.Invalid(errors)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks for duplicate ISO codes excluding a specific country ID.
|
||||
*/
|
||||
private suspend fun checkForDuplicatesExcluding(
|
||||
isoAlpha2Code: String,
|
||||
isoAlpha3Code: String,
|
||||
excludeId: Uuid
|
||||
): ValidationResult {
|
||||
val errors = mutableListOf<ValidationError>()
|
||||
|
||||
// Check Alpha-2 code
|
||||
val existingAlpha2 = landRepository.findByIsoAlpha2Code(isoAlpha2Code.uppercase())
|
||||
if (existingAlpha2 != null && existingAlpha2.landId != excludeId) {
|
||||
errors.add(ValidationError("isoAlpha2Code", "Country with ISO Alpha-2 code '${isoAlpha2Code.uppercase()}' already exists", "DUPLICATE"))
|
||||
}
|
||||
|
||||
// Check Alpha-3 code
|
||||
val existingAlpha3 = landRepository.findByIsoAlpha3Code(isoAlpha3Code.uppercase())
|
||||
if (existingAlpha3 != null && existingAlpha3.landId != excludeId) {
|
||||
errors.add(ValidationError("isoAlpha3Code", "Country with ISO Alpha-3 code '${isoAlpha3Code.uppercase()}' already exists", "DUPLICATE"))
|
||||
}
|
||||
|
||||
return if (errors.isEmpty()) {
|
||||
ValidationResult.Valid
|
||||
} else {
|
||||
ValidationResult.Invalid(errors)
|
||||
}
|
||||
}
|
||||
}
|
||||
+455
@@ -0,0 +1,455 @@
|
||||
package at.mocode.masterdata.application.usecase
|
||||
|
||||
import at.mocode.core.domain.model.PlatzTypE
|
||||
import at.mocode.masterdata.domain.model.Platz
|
||||
import at.mocode.masterdata.domain.repository.PlatzRepository
|
||||
import at.mocode.core.domain.model.ValidationResult
|
||||
import at.mocode.core.domain.model.ValidationError
|
||||
import com.benasher44.uuid.Uuid
|
||||
import kotlinx.datetime.Clock
|
||||
|
||||
/**
|
||||
* Use case for creating and updating venue/arena information.
|
||||
*
|
||||
* This use case encapsulates the business logic for venue management
|
||||
* including validation, duplicate checking, and persistence.
|
||||
*/
|
||||
class CreatePlatzUseCase(
|
||||
private val platzRepository: PlatzRepository
|
||||
) {
|
||||
|
||||
/**
|
||||
* Request data for creating a new venue.
|
||||
*/
|
||||
data class CreatePlatzRequest(
|
||||
val turnierId: Uuid,
|
||||
val name: String,
|
||||
val dimension: String? = null,
|
||||
val boden: String? = null,
|
||||
val typ: PlatzTypE,
|
||||
val istAktiv: Boolean = true,
|
||||
val sortierReihenfolge: Int? = null
|
||||
)
|
||||
|
||||
/**
|
||||
* Request data for updating an existing venue.
|
||||
*/
|
||||
data class UpdatePlatzRequest(
|
||||
val platzId: Uuid,
|
||||
val turnierId: Uuid,
|
||||
val name: String,
|
||||
val dimension: String? = null,
|
||||
val boden: String? = null,
|
||||
val typ: PlatzTypE,
|
||||
val istAktiv: Boolean = true,
|
||||
val sortierReihenfolge: Int? = null
|
||||
)
|
||||
|
||||
/**
|
||||
* Response data for venue creation.
|
||||
*/
|
||||
data class CreatePlatzResponse(
|
||||
val platz: Platz?,
|
||||
val success: Boolean,
|
||||
val errors: List<String> = emptyList()
|
||||
)
|
||||
|
||||
/**
|
||||
* Response data for venue update.
|
||||
*/
|
||||
data class UpdatePlatzResponse(
|
||||
val platz: Platz?,
|
||||
val success: Boolean,
|
||||
val errors: List<String> = emptyList()
|
||||
)
|
||||
|
||||
/**
|
||||
* Response data for venue deletion.
|
||||
*/
|
||||
data class DeletePlatzResponse(
|
||||
val success: Boolean,
|
||||
val errors: List<String> = emptyList()
|
||||
)
|
||||
|
||||
/**
|
||||
* Creates a new venue after validation.
|
||||
*
|
||||
* @param request The venue creation request
|
||||
* @return CreatePlatzResponse with the created venue or validation errors
|
||||
*/
|
||||
suspend fun createPlatz(request: CreatePlatzRequest): CreatePlatzResponse {
|
||||
// Validate the request
|
||||
val validationResult = validateCreateRequest(request)
|
||||
if (!validationResult.isValid()) {
|
||||
val errors = (validationResult as ValidationResult.Invalid).errors.map { it.message }
|
||||
return CreatePlatzResponse(
|
||||
platz = null,
|
||||
success = false,
|
||||
errors = errors
|
||||
)
|
||||
}
|
||||
|
||||
// Check for duplicates
|
||||
val duplicateCheck = checkForDuplicates(request.name, request.turnierId)
|
||||
if (!duplicateCheck.isValid()) {
|
||||
val errors = (duplicateCheck as ValidationResult.Invalid).errors.map { it.message }
|
||||
return CreatePlatzResponse(
|
||||
platz = null,
|
||||
success = false,
|
||||
errors = errors
|
||||
)
|
||||
}
|
||||
|
||||
// Create the domain object
|
||||
val now = Clock.System.now()
|
||||
val platz = Platz(
|
||||
turnierId = request.turnierId,
|
||||
name = request.name.trim(),
|
||||
dimension = request.dimension?.trim(),
|
||||
boden = request.boden?.trim(),
|
||||
typ = request.typ,
|
||||
istAktiv = request.istAktiv,
|
||||
sortierReihenfolge = request.sortierReihenfolge,
|
||||
createdAt = now,
|
||||
updatedAt = now
|
||||
)
|
||||
|
||||
// Save to repository
|
||||
val savedPlatz = platzRepository.save(platz)
|
||||
return CreatePlatzResponse(
|
||||
platz = savedPlatz,
|
||||
success = true
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an existing venue after validation.
|
||||
*
|
||||
* @param request The venue update request
|
||||
* @return UpdatePlatzResponse containing the updated venue or validation errors
|
||||
*/
|
||||
suspend fun updatePlatz(request: UpdatePlatzRequest): UpdatePlatzResponse {
|
||||
// Check if venue exists
|
||||
val existingPlatz = platzRepository.findById(request.platzId)
|
||||
if (existingPlatz == null) {
|
||||
return UpdatePlatzResponse(
|
||||
platz = null,
|
||||
success = false,
|
||||
errors = listOf("Venue with ID ${request.platzId} not found")
|
||||
)
|
||||
}
|
||||
|
||||
// Validate the request
|
||||
val validationResult = validateUpdateRequest(request)
|
||||
if (!validationResult.isValid()) {
|
||||
val errors = (validationResult as ValidationResult.Invalid).errors.map { it.message }
|
||||
return UpdatePlatzResponse(
|
||||
platz = null,
|
||||
success = false,
|
||||
errors = errors
|
||||
)
|
||||
}
|
||||
|
||||
// Check for duplicates (excluding current venue)
|
||||
val duplicateCheck = checkForDuplicatesExcluding(request.name, request.turnierId, request.platzId)
|
||||
if (!duplicateCheck.isValid()) {
|
||||
val errors = (duplicateCheck as ValidationResult.Invalid).errors.map { it.message }
|
||||
return UpdatePlatzResponse(
|
||||
platz = null,
|
||||
success = false,
|
||||
errors = errors
|
||||
)
|
||||
}
|
||||
|
||||
// Update the domain object
|
||||
val updatedPlatz = existingPlatz.copy(
|
||||
turnierId = request.turnierId,
|
||||
name = request.name.trim(),
|
||||
dimension = request.dimension?.trim(),
|
||||
boden = request.boden?.trim(),
|
||||
typ = request.typ,
|
||||
istAktiv = request.istAktiv,
|
||||
sortierReihenfolge = request.sortierReihenfolge,
|
||||
updatedAt = Clock.System.now()
|
||||
)
|
||||
|
||||
// Save to repository
|
||||
val savedPlatz = platzRepository.save(updatedPlatz)
|
||||
return UpdatePlatzResponse(
|
||||
platz = savedPlatz,
|
||||
success = true
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a venue by ID.
|
||||
*
|
||||
* @param platzId The unique identifier of the venue to delete
|
||||
* @return DeletePlatzResponse indicating success or failure
|
||||
*/
|
||||
suspend fun deletePlatz(platzId: Uuid): DeletePlatzResponse {
|
||||
val deleted = platzRepository.delete(platzId)
|
||||
return if (deleted) {
|
||||
DeletePlatzResponse(success = true)
|
||||
} else {
|
||||
DeletePlatzResponse(
|
||||
success = false,
|
||||
errors = listOf("Venue with ID $platzId not found or could not be deleted")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a create venue request.
|
||||
*/
|
||||
private fun validateCreateRequest(request: CreatePlatzRequest): ValidationResult {
|
||||
val errors = mutableListOf<ValidationError>()
|
||||
|
||||
// Name validation
|
||||
if (request.name.isBlank()) {
|
||||
errors.add(ValidationError("name", "Name is required", "REQUIRED"))
|
||||
} else if (request.name.length > 200) {
|
||||
errors.add(ValidationError("name", "Name must not exceed 200 characters", "MAX_LENGTH"))
|
||||
}
|
||||
|
||||
// Dimension validation
|
||||
request.dimension?.let { dimension ->
|
||||
if (dimension.isBlank()) {
|
||||
errors.add(ValidationError("dimension", "Dimension cannot be empty if provided", "INVALID_FORMAT"))
|
||||
} else if (dimension.length > 50) {
|
||||
errors.add(ValidationError("dimension", "Dimension must not exceed 50 characters", "MAX_LENGTH"))
|
||||
} else if (!dimension.matches(Regex("^\\d+x\\d+m?$"))) {
|
||||
errors.add(ValidationError("dimension", "Dimension must be in format like '20x60m' or '20x40'", "INVALID_FORMAT"))
|
||||
}
|
||||
}
|
||||
|
||||
// Ground type validation
|
||||
request.boden?.let { boden ->
|
||||
if (boden.isBlank()) {
|
||||
errors.add(ValidationError("boden", "Ground type cannot be empty if provided", "INVALID_FORMAT"))
|
||||
} else if (boden.length > 100) {
|
||||
errors.add(ValidationError("boden", "Ground type must not exceed 100 characters", "MAX_LENGTH"))
|
||||
}
|
||||
}
|
||||
|
||||
// Sorting order validation
|
||||
request.sortierReihenfolge?.let { order ->
|
||||
if (order < 0) {
|
||||
errors.add(ValidationError("sortierReihenfolge", "Sorting order must be non-negative", "INVALID_VALUE"))
|
||||
}
|
||||
}
|
||||
|
||||
return if (errors.isEmpty()) {
|
||||
ValidationResult.Valid
|
||||
} else {
|
||||
ValidationResult.Invalid(errors)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates an update venue request.
|
||||
*/
|
||||
private fun validateUpdateRequest(request: UpdatePlatzRequest): ValidationResult {
|
||||
// Use the same validation logic as create request
|
||||
val createRequest = CreatePlatzRequest(
|
||||
turnierId = request.turnierId,
|
||||
name = request.name,
|
||||
dimension = request.dimension,
|
||||
boden = request.boden,
|
||||
typ = request.typ,
|
||||
istAktiv = request.istAktiv,
|
||||
sortierReihenfolge = request.sortierReihenfolge
|
||||
)
|
||||
return validateCreateRequest(createRequest)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks for duplicate venue names within a tournament.
|
||||
*/
|
||||
private suspend fun checkForDuplicates(name: String, turnierId: Uuid): ValidationResult {
|
||||
val errors = mutableListOf<ValidationError>()
|
||||
|
||||
if (platzRepository.existsByNameAndTournament(name.trim(), turnierId)) {
|
||||
errors.add(ValidationError("name", "Venue with name '$name' already exists for this tournament", "DUPLICATE"))
|
||||
}
|
||||
|
||||
return if (errors.isEmpty()) {
|
||||
ValidationResult.Valid
|
||||
} else {
|
||||
ValidationResult.Invalid(errors)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks for duplicate venue names excluding a specific venue ID.
|
||||
*/
|
||||
private suspend fun checkForDuplicatesExcluding(name: String, turnierId: Uuid, excludeId: Uuid): ValidationResult {
|
||||
val errors = mutableListOf<ValidationError>()
|
||||
|
||||
// Get all venues with the same name and tournament
|
||||
val existingVenues = platzRepository.findByName(name.trim(), turnierId, 10)
|
||||
val duplicateExists = existingVenues.any { it.id != excludeId }
|
||||
|
||||
if (duplicateExists) {
|
||||
errors.add(ValidationError("name", "Venue with name '$name' already exists for this tournament", "DUPLICATE"))
|
||||
}
|
||||
|
||||
return if (errors.isEmpty()) {
|
||||
ValidationResult.Valid
|
||||
} else {
|
||||
ValidationResult.Invalid(errors)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates venue configuration for specific discipline requirements.
|
||||
* This is a business logic method that can be used by other parts of the application.
|
||||
*
|
||||
* @param platzId The venue ID
|
||||
* @param requiredType The required venue type for the discipline
|
||||
* @param requiredDimensions Optional required dimensions
|
||||
* @param requiredGroundType Optional required ground type
|
||||
* @return ValidationResult indicating suitability or reasons for unsuitability
|
||||
*/
|
||||
suspend fun validateVenueForDiscipline(
|
||||
platzId: Uuid,
|
||||
requiredType: PlatzTypE,
|
||||
requiredDimensions: String? = null,
|
||||
requiredGroundType: String? = null
|
||||
): ValidationResult {
|
||||
val errors = mutableListOf<ValidationError>()
|
||||
|
||||
// Get the venue
|
||||
val platz = platzRepository.findById(platzId)
|
||||
if (platz == null) {
|
||||
errors.add(ValidationError("platzId", "Venue not found", "NOT_FOUND"))
|
||||
return ValidationResult.Invalid(errors)
|
||||
}
|
||||
|
||||
// Check if venue is active
|
||||
if (!platz.istAktiv) {
|
||||
errors.add(ValidationError("platz", "Venue is not active", "INACTIVE"))
|
||||
}
|
||||
|
||||
// Check venue type
|
||||
if (platz.typ != requiredType) {
|
||||
errors.add(ValidationError("typ", "Venue type ${platz.typ} does not match required type $requiredType", "TYPE_MISMATCH"))
|
||||
}
|
||||
|
||||
// Check dimensions if required
|
||||
requiredDimensions?.let { required ->
|
||||
if (platz.dimension != required.trim()) {
|
||||
errors.add(ValidationError("dimension", "Venue dimensions '${platz.dimension}' do not match required dimensions '$required'", "DIMENSION_MISMATCH"))
|
||||
}
|
||||
}
|
||||
|
||||
// Check ground type if required
|
||||
requiredGroundType?.let { required ->
|
||||
if (platz.boden != required.trim()) {
|
||||
errors.add(ValidationError("boden", "Venue ground type '${platz.boden}' does not match required ground type '$required'", "GROUND_TYPE_MISMATCH"))
|
||||
}
|
||||
}
|
||||
|
||||
return if (errors.isEmpty()) {
|
||||
ValidationResult.Valid
|
||||
} else {
|
||||
ValidationResult.Invalid(errors)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates multiple venues for a tournament in batch.
|
||||
* This is a convenience method for setting up tournament venues efficiently.
|
||||
*
|
||||
* @param turnierId The tournament ID
|
||||
* @param venueRequests List of venue creation requests
|
||||
* @return List of creation responses for each venue
|
||||
*/
|
||||
suspend fun createMultipleVenues(turnierId: Uuid, venueRequests: List<CreatePlatzRequest>): List<CreatePlatzResponse> {
|
||||
val responses = mutableListOf<CreatePlatzResponse>()
|
||||
|
||||
for (request in venueRequests) {
|
||||
// Ensure all requests are for the same tournament
|
||||
val adjustedRequest = request.copy(turnierId = turnierId)
|
||||
val response = createPlatz(adjustedRequest)
|
||||
responses.add(response)
|
||||
}
|
||||
|
||||
return responses
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates venue capacity and setup for tournament requirements.
|
||||
* This method performs comprehensive checks for tournament venue setup.
|
||||
*
|
||||
* @param turnierId The tournament ID
|
||||
* @param requiredVenueTypes Map of venue type to minimum count required
|
||||
* @return ValidationResult indicating if the tournament has adequate venue setup
|
||||
*/
|
||||
suspend fun validateTournamentVenueSetup(
|
||||
turnierId: Uuid,
|
||||
requiredVenueTypes: Map<PlatzTypE, Int>
|
||||
): ValidationResult {
|
||||
val errors = mutableListOf<ValidationError>()
|
||||
|
||||
// Get all active venues for the tournament
|
||||
val venues = platzRepository.findByTournament(turnierId, activeOnly = true, orderBySortierung = false)
|
||||
val venuesByType = venues.groupBy { it.typ }
|
||||
|
||||
// Check if each required venue type has sufficient count
|
||||
for ((requiredType, requiredCount) in requiredVenueTypes) {
|
||||
val availableCount = venuesByType[requiredType]?.size ?: 0
|
||||
|
||||
if (availableCount < requiredCount) {
|
||||
errors.add(ValidationError(
|
||||
"venues",
|
||||
"Tournament requires $requiredCount venues of type $requiredType but only has $availableCount",
|
||||
"INSUFFICIENT_VENUES"
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
// Check if tournament has any venues at all
|
||||
if (venues.isEmpty()) {
|
||||
errors.add(ValidationError("venues", "Tournament has no active venues configured", "NO_VENUES"))
|
||||
}
|
||||
|
||||
return if (errors.isEmpty()) {
|
||||
ValidationResult.Valid
|
||||
} else {
|
||||
ValidationResult.Invalid(errors)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Optimizes venue sorting order for a tournament.
|
||||
* This method automatically assigns sorting orders based on venue type and name.
|
||||
*
|
||||
* @param turnierId The tournament ID
|
||||
* @return Number of venues updated
|
||||
*/
|
||||
suspend fun optimizeVenueSorting(turnierId: Uuid): Int {
|
||||
val venues = platzRepository.findByTournament(turnierId, activeOnly = false, orderBySortierung = false)
|
||||
|
||||
// Sort venues by type first, then by name
|
||||
val sortedVenues = venues.sortedWith(compareBy<Platz> { it.typ.ordinal }.thenBy { it.name })
|
||||
|
||||
var updatedCount = 0
|
||||
|
||||
// Assign new sorting orders
|
||||
sortedVenues.forEachIndexed { index, venue ->
|
||||
val newSortOrder = (index + 1) * 10 // Leave gaps for future insertions
|
||||
|
||||
if (venue.sortierReihenfolge != newSortOrder) {
|
||||
val updatedVenue = venue.copy(
|
||||
sortierReihenfolge = newSortOrder,
|
||||
updatedAt = Clock.System.now()
|
||||
)
|
||||
platzRepository.save(updatedVenue)
|
||||
updatedCount++
|
||||
}
|
||||
}
|
||||
|
||||
return updatedCount
|
||||
}
|
||||
}
|
||||
+185
@@ -0,0 +1,185 @@
|
||||
package at.mocode.masterdata.application.usecase
|
||||
|
||||
import at.mocode.core.domain.model.SparteE
|
||||
import at.mocode.masterdata.domain.model.AltersklasseDefinition
|
||||
import at.mocode.masterdata.domain.repository.AltersklasseRepository
|
||||
import com.benasher44.uuid.Uuid
|
||||
|
||||
/**
|
||||
* Use case for retrieving age class information.
|
||||
*
|
||||
* This use case encapsulates the business logic for fetching age class data
|
||||
* and provides a clean interface for the application layer.
|
||||
*/
|
||||
class GetAltersklasseUseCase(
|
||||
private val altersklasseRepository: AltersklasseRepository
|
||||
) {
|
||||
|
||||
/**
|
||||
* Retrieves an age class by its unique ID.
|
||||
*
|
||||
* @param altersklasseId The unique identifier of the age class
|
||||
* @return The age class if found, null otherwise
|
||||
*/
|
||||
suspend fun getById(altersklasseId: Uuid): AltersklasseDefinition? {
|
||||
return altersklasseRepository.findById(altersklasseId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves an age class by its code.
|
||||
*
|
||||
* @param altersklasseCode The age class code (e.g., "JGD_U16", "JUN_U18")
|
||||
* @return The age class if found, null otherwise
|
||||
*/
|
||||
suspend fun getByCode(altersklasseCode: String): AltersklasseDefinition? {
|
||||
require(altersklasseCode.isNotBlank()) { "Age class code cannot be blank" }
|
||||
return altersklasseRepository.findByCode(altersklasseCode.trim().uppercase())
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches for age classes by name (partial match).
|
||||
*
|
||||
* @param searchTerm The search term to match against age class names
|
||||
* @param limit Maximum number of results to return (default: 50)
|
||||
* @return List of matching age classes
|
||||
*/
|
||||
suspend fun searchByName(searchTerm: String, limit: Int = 50): List<AltersklasseDefinition> {
|
||||
require(searchTerm.isNotBlank()) { "Search term cannot be blank" }
|
||||
require(limit > 0) { "Limit must be positive" }
|
||||
return altersklasseRepository.findByName(searchTerm.trim(), limit)
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all active age classes.
|
||||
*
|
||||
* @param sparteFilter Optional filter by sport type
|
||||
* @param geschlechtFilter Optional filter by gender ('M', 'W')
|
||||
* @return List of active age classes
|
||||
*/
|
||||
suspend fun getAllActive(sparteFilter: SparteE? = null, geschlechtFilter: Char? = null): List<AltersklasseDefinition> {
|
||||
geschlechtFilter?.let { gender ->
|
||||
require(gender == 'M' || gender == 'W') { "Gender filter must be 'M' or 'W'" }
|
||||
}
|
||||
return altersklasseRepository.findAllActive(sparteFilter, geschlechtFilter)
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds age classes applicable for a specific age.
|
||||
*
|
||||
* @param age The age to check
|
||||
* @param sparteFilter Optional filter by sport type
|
||||
* @param geschlechtFilter Optional filter by gender ('M', 'W')
|
||||
* @return List of applicable age classes
|
||||
*/
|
||||
suspend fun getApplicableForAge(age: Int, sparteFilter: SparteE? = null, geschlechtFilter: Char? = null): List<AltersklasseDefinition> {
|
||||
require(age >= 0) { "Age must be non-negative" }
|
||||
geschlechtFilter?.let { gender ->
|
||||
require(gender == 'M' || gender == 'W') { "Gender filter must be 'M' or 'W'" }
|
||||
}
|
||||
return altersklasseRepository.findApplicableForAge(age, sparteFilter, geschlechtFilter)
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves age classes by sport type.
|
||||
*
|
||||
* @param sparte The sport type
|
||||
* @param activeOnly Whether to return only active age classes (default: true)
|
||||
* @return List of age classes for the sport type
|
||||
*/
|
||||
suspend fun getBySparte(sparte: SparteE, activeOnly: Boolean = true): List<AltersklasseDefinition> {
|
||||
return altersklasseRepository.findBySparte(sparte, activeOnly)
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves age classes by gender filter.
|
||||
*
|
||||
* @param geschlecht The gender ('M', 'W')
|
||||
* @param activeOnly Whether to return only active age classes (default: true)
|
||||
* @return List of age classes for the gender
|
||||
*/
|
||||
suspend fun getByGeschlecht(geschlecht: Char, activeOnly: Boolean = true): List<AltersklasseDefinition> {
|
||||
require(geschlecht == 'M' || geschlecht == 'W') { "Gender must be 'M' or 'W'" }
|
||||
return altersklasseRepository.findByGeschlecht(geschlecht, activeOnly)
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves age classes by age range.
|
||||
*
|
||||
* @param minAge Minimum age (inclusive)
|
||||
* @param maxAge Maximum age (inclusive)
|
||||
* @param activeOnly Whether to return only active age classes (default: true)
|
||||
* @return List of age classes within the age range
|
||||
*/
|
||||
suspend fun getByAgeRange(minAge: Int?, maxAge: Int?, activeOnly: Boolean = true): List<AltersklasseDefinition> {
|
||||
minAge?.let { min ->
|
||||
require(min >= 0) { "Minimum age must be non-negative" }
|
||||
}
|
||||
maxAge?.let { max ->
|
||||
require(max >= 0) { "Maximum age must be non-negative" }
|
||||
minAge?.let { min ->
|
||||
require(max >= min) { "Maximum age must be greater than or equal to minimum age" }
|
||||
}
|
||||
}
|
||||
return altersklasseRepository.findByAgeRange(minAge, maxAge, activeOnly)
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves age classes by OETO rule reference.
|
||||
*
|
||||
* @param oetoRegelReferenzId The OETO rule reference ID
|
||||
* @return List of age classes linked to the rule
|
||||
*/
|
||||
suspend fun getByOetoRegelReferenz(oetoRegelReferenzId: Uuid): List<AltersklasseDefinition> {
|
||||
return altersklasseRepository.findByOetoRegelReferenz(oetoRegelReferenzId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if an age class with the given code exists.
|
||||
*
|
||||
* @param altersklasseCode The age class code to check
|
||||
* @return true if an age class with this code exists, false otherwise
|
||||
*/
|
||||
suspend fun existsByCode(altersklasseCode: String): Boolean {
|
||||
require(altersklasseCode.isNotBlank()) { "Age class code cannot be blank" }
|
||||
return altersklasseRepository.existsByCode(altersklasseCode.trim().uppercase())
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts the total number of active age classes.
|
||||
*
|
||||
* @param sparteFilter Optional filter by sport type
|
||||
* @return The total count of active age classes
|
||||
*/
|
||||
suspend fun countActive(sparteFilter: SparteE? = null): Long {
|
||||
return altersklasseRepository.countActive(sparteFilter)
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates if a person with given age and gender can participate in an age class.
|
||||
*
|
||||
* @param altersklasseId The age class ID
|
||||
* @param age The person's age
|
||||
* @param geschlecht The person's gender ('M', 'W')
|
||||
* @return true if the person can participate, false otherwise
|
||||
*/
|
||||
suspend fun isEligible(altersklasseId: Uuid, age: Int, geschlecht: Char): Boolean {
|
||||
require(age >= 0) { "Age must be non-negative" }
|
||||
require(geschlecht == 'M' || geschlecht == 'W') { "Gender must be 'M' or 'W'" }
|
||||
return altersklasseRepository.isEligible(altersklasseId, age, geschlecht)
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves age classes suitable for a participant based on age, gender, and sport.
|
||||
* This is a convenience method that combines multiple filters.
|
||||
*
|
||||
* @param age The participant's age
|
||||
* @param geschlecht The participant's gender ('M', 'W')
|
||||
* @param sparte The sport type
|
||||
* @return List of suitable age classes
|
||||
*/
|
||||
suspend fun getSuitableForParticipant(age: Int, geschlecht: Char, sparte: SparteE): List<AltersklasseDefinition> {
|
||||
require(age >= 0) { "Age must be non-negative" }
|
||||
require(geschlecht == 'M' || geschlecht == 'W') { "Gender must be 'M' or 'W'" }
|
||||
return altersklasseRepository.findApplicableForAge(age, sparte, geschlecht)
|
||||
}
|
||||
}
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
package at.mocode.masterdata.application.usecase
|
||||
|
||||
import at.mocode.masterdata.domain.model.BundeslandDefinition
|
||||
import at.mocode.masterdata.domain.repository.BundeslandRepository
|
||||
import com.benasher44.uuid.Uuid
|
||||
|
||||
/**
|
||||
* Use case for retrieving federal state information.
|
||||
*
|
||||
* This use case encapsulates the business logic for fetching federal state data
|
||||
* and provides a clean interface for the application layer.
|
||||
*/
|
||||
class GetBundeslandUseCase(
|
||||
private val bundeslandRepository: BundeslandRepository
|
||||
) {
|
||||
|
||||
/**
|
||||
* Retrieves a federal state by its unique ID.
|
||||
*
|
||||
* @param bundeslandId The unique identifier of the federal state
|
||||
* @return The federal state if found, null otherwise
|
||||
*/
|
||||
suspend fun getById(bundeslandId: Uuid): BundeslandDefinition? {
|
||||
return bundeslandRepository.findById(bundeslandId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a federal state by its OEPS code for a specific country.
|
||||
*
|
||||
* @param oepsCode The OEPS code (e.g., "01", "02")
|
||||
* @param landId The country ID
|
||||
* @return The federal state if found, null otherwise
|
||||
*/
|
||||
suspend fun getByOepsCode(oepsCode: String, landId: Uuid): BundeslandDefinition? {
|
||||
require(oepsCode.isNotBlank()) { "OEPS code cannot be blank" }
|
||||
return bundeslandRepository.findByOepsCode(oepsCode.trim(), landId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a federal state by its ISO 3166-2 code.
|
||||
*
|
||||
* @param iso3166_2_Code The ISO 3166-2 code (e.g., "AT-1", "DE-BY")
|
||||
* @return The federal state if found, null otherwise
|
||||
*/
|
||||
suspend fun getByIso3166_2_Code(iso3166_2_Code: String): BundeslandDefinition? {
|
||||
require(iso3166_2_Code.isNotBlank()) { "ISO 3166-2 code cannot be blank" }
|
||||
return bundeslandRepository.findByIso3166_2_Code(iso3166_2_Code.trim().uppercase())
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all federal states for a specific country.
|
||||
*
|
||||
* @param landId The country ID
|
||||
* @param activeOnly Whether to return only active federal states (default: true)
|
||||
* @param orderBySortierung Whether to order by sortierReihenfolge field (default: true)
|
||||
* @return List of federal states for the country
|
||||
*/
|
||||
suspend fun getByCountry(landId: Uuid, activeOnly: Boolean = true, orderBySortierung: Boolean = true): List<BundeslandDefinition> {
|
||||
return bundeslandRepository.findByCountry(landId, activeOnly, orderBySortierung)
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches for federal states by name (partial match).
|
||||
*
|
||||
* @param searchTerm The search term to match against federal state names
|
||||
* @param landId Optional country ID to limit search
|
||||
* @param limit Maximum number of results to return (default: 50)
|
||||
* @return List of matching federal states
|
||||
*/
|
||||
suspend fun searchByName(searchTerm: String, landId: Uuid? = null, limit: Int = 50): List<BundeslandDefinition> {
|
||||
require(searchTerm.isNotBlank()) { "Search term cannot be blank" }
|
||||
require(limit > 0) { "Limit must be positive" }
|
||||
return bundeslandRepository.findByName(searchTerm.trim(), landId, limit)
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all active federal states.
|
||||
*
|
||||
* @param orderBySortierung Whether to order by sortierReihenfolge field (default: true)
|
||||
* @return List of active federal states
|
||||
*/
|
||||
suspend fun getAllActive(orderBySortierung: Boolean = true): List<BundeslandDefinition> {
|
||||
return bundeslandRepository.findAllActive(orderBySortierung)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a federal state with the given OEPS code exists for a country.
|
||||
*
|
||||
* @param oepsCode The OEPS code to check
|
||||
* @param landId The country ID
|
||||
* @return true if a federal state with this code exists, false otherwise
|
||||
*/
|
||||
suspend fun existsByOepsCode(oepsCode: String, landId: Uuid): Boolean {
|
||||
require(oepsCode.isNotBlank()) { "OEPS code cannot be blank" }
|
||||
return bundeslandRepository.existsByOepsCode(oepsCode.trim(), landId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a federal state with the given ISO 3166-2 code exists.
|
||||
*
|
||||
* @param iso3166_2_Code The ISO 3166-2 code to check
|
||||
* @return true if a federal state with this code exists, false otherwise
|
||||
*/
|
||||
suspend fun existsByIso3166_2_Code(iso3166_2_Code: String): Boolean {
|
||||
require(iso3166_2_Code.isNotBlank()) { "ISO 3166-2 code cannot be blank" }
|
||||
return bundeslandRepository.existsByIso3166_2_Code(iso3166_2_Code.trim().uppercase())
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts the total number of active federal states for a country.
|
||||
*
|
||||
* @param landId The country ID
|
||||
* @return The total count of active federal states
|
||||
*/
|
||||
suspend fun countActiveByCountry(landId: Uuid): Long {
|
||||
return bundeslandRepository.countActiveByCountry(landId)
|
||||
}
|
||||
}
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
package at.mocode.masterdata.application.usecase
|
||||
|
||||
import at.mocode.masterdata.domain.model.LandDefinition
|
||||
import at.mocode.masterdata.domain.repository.LandRepository
|
||||
import com.benasher44.uuid.Uuid
|
||||
|
||||
/**
|
||||
* Use case for retrieving country information.
|
||||
*
|
||||
* This use case encapsulates the business logic for fetching country data
|
||||
* and provides a clean interface for the application layer.
|
||||
*/
|
||||
class GetCountryUseCase(
|
||||
private val landRepository: LandRepository
|
||||
) {
|
||||
|
||||
/**
|
||||
* Retrieves a country by its unique ID.
|
||||
*
|
||||
* @param countryId The unique identifier of the country
|
||||
* @return The country if found, null otherwise
|
||||
*/
|
||||
suspend fun getById(countryId: Uuid): LandDefinition? {
|
||||
return landRepository.findById(countryId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a country by its ISO Alpha-2 code.
|
||||
*
|
||||
* @param isoCode The 2-letter ISO code (e.g., "AT", "DE")
|
||||
* @return The country if found, null otherwise
|
||||
*/
|
||||
suspend fun getByIsoAlpha2Code(isoCode: String): LandDefinition? {
|
||||
require(isoCode.length == 2) { "ISO Alpha-2 code must be exactly 2 characters" }
|
||||
return landRepository.findByIsoAlpha2Code(isoCode.uppercase())
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a country by its ISO Alpha-3 code.
|
||||
*
|
||||
* @param isoCode The 3-letter ISO code (e.g., "AUT", "DEU")
|
||||
* @return The country if found, null otherwise
|
||||
*/
|
||||
suspend fun getByIsoAlpha3Code(isoCode: String): LandDefinition? {
|
||||
require(isoCode.length == 3) { "ISO Alpha-3 code must be exactly 3 characters" }
|
||||
return landRepository.findByIsoAlpha3Code(isoCode.uppercase())
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches for countries by name (partial match).
|
||||
*
|
||||
* @param searchTerm The search term to match against country names
|
||||
* @param limit Maximum number of results to return (default: 50)
|
||||
* @return List of matching countries
|
||||
*/
|
||||
suspend fun searchByName(searchTerm: String, limit: Int = 50): List<LandDefinition> {
|
||||
require(searchTerm.isNotBlank()) { "Search term cannot be blank" }
|
||||
require(limit > 0) { "Limit must be positive" }
|
||||
return landRepository.findByName(searchTerm.trim(), limit)
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all active countries.
|
||||
*
|
||||
* @param orderBySortierung Whether to order by sortierReihenfolge field (default: true)
|
||||
* @return List of active countries
|
||||
*/
|
||||
suspend fun getAllActive(orderBySortierung: Boolean = true): List<LandDefinition> {
|
||||
return landRepository.findAllActive(orderBySortierung)
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all EU member countries.
|
||||
*
|
||||
* @return List of EU member countries
|
||||
*/
|
||||
suspend fun getEuMembers(): List<LandDefinition> {
|
||||
return landRepository.findEuMembers()
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all EWR (European Economic Area) member countries.
|
||||
*
|
||||
* @return List of EWR member countries
|
||||
*/
|
||||
suspend fun getEwrMembers(): List<LandDefinition> {
|
||||
return landRepository.findEwrMembers()
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a country with the given ISO Alpha-2 code exists.
|
||||
*
|
||||
* @param isoCode The ISO Alpha-2 code to check
|
||||
* @return true if a country with this code exists, false otherwise
|
||||
*/
|
||||
suspend fun existsByIsoAlpha2Code(isoCode: String): Boolean {
|
||||
require(isoCode.length == 2) { "ISO Alpha-2 code must be exactly 2 characters" }
|
||||
return landRepository.existsByIsoAlpha2Code(isoCode.uppercase())
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a country with the given ISO Alpha-3 code exists.
|
||||
*
|
||||
* @param isoCode The ISO Alpha-3 code to check
|
||||
* @return true if a country with this code exists, false otherwise
|
||||
*/
|
||||
suspend fun existsByIsoAlpha3Code(isoCode: String): Boolean {
|
||||
require(isoCode.length == 3) { "ISO Alpha-3 code must be exactly 3 characters" }
|
||||
return landRepository.existsByIsoAlpha3Code(isoCode.uppercase())
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts the total number of active countries.
|
||||
*
|
||||
* @return The total count of active countries
|
||||
*/
|
||||
suspend fun countActive(): Long {
|
||||
return landRepository.countActive()
|
||||
}
|
||||
}
|
||||
+275
@@ -0,0 +1,275 @@
|
||||
package at.mocode.masterdata.application.usecase
|
||||
|
||||
import at.mocode.core.domain.model.PlatzTypE
|
||||
import at.mocode.masterdata.domain.model.Platz
|
||||
import at.mocode.masterdata.domain.repository.PlatzRepository
|
||||
import com.benasher44.uuid.Uuid
|
||||
|
||||
/**
|
||||
* Use case for retrieving venue/arena information.
|
||||
*
|
||||
* This use case encapsulates the business logic for fetching venue data
|
||||
* and provides a clean interface for the application layer.
|
||||
*/
|
||||
class GetPlatzUseCase(
|
||||
private val platzRepository: PlatzRepository
|
||||
) {
|
||||
|
||||
/**
|
||||
* Retrieves a venue by its unique ID.
|
||||
*
|
||||
* @param platzId The unique identifier of the venue
|
||||
* @return The venue if found, null otherwise
|
||||
*/
|
||||
suspend fun getById(platzId: Uuid): Platz? {
|
||||
return platzRepository.findById(platzId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all venues for a specific tournament.
|
||||
*
|
||||
* @param turnierId The tournament ID
|
||||
* @param activeOnly Whether to return only active venues (default: true)
|
||||
* @param orderBySortierung Whether to order by sortierReihenfolge field (default: true)
|
||||
* @return List of venues for the tournament
|
||||
*/
|
||||
suspend fun getByTournament(turnierId: Uuid, activeOnly: Boolean = true, orderBySortierung: Boolean = true): List<Platz> {
|
||||
return platzRepository.findByTournament(turnierId, activeOnly, orderBySortierung)
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches for venues by name (partial match).
|
||||
*
|
||||
* @param searchTerm The search term to match against venue names
|
||||
* @param turnierId Optional tournament ID to limit search
|
||||
* @param limit Maximum number of results to return (default: 50)
|
||||
* @return List of matching venues
|
||||
*/
|
||||
suspend fun searchByName(searchTerm: String, turnierId: Uuid? = null, limit: Int = 50): List<Platz> {
|
||||
require(searchTerm.isNotBlank()) { "Search term cannot be blank" }
|
||||
require(limit > 0) { "Limit must be positive" }
|
||||
return platzRepository.findByName(searchTerm.trim(), turnierId, limit)
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves venues by type.
|
||||
*
|
||||
* @param typ The venue type
|
||||
* @param turnierId Optional tournament ID to limit search
|
||||
* @param activeOnly Whether to return only active venues (default: true)
|
||||
* @return List of venues of the specified type
|
||||
*/
|
||||
suspend fun getByType(typ: PlatzTypE, turnierId: Uuid? = null, activeOnly: Boolean = true): List<Platz> {
|
||||
return platzRepository.findByType(typ, turnierId, activeOnly)
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves venues by ground type.
|
||||
*
|
||||
* @param boden The ground type (e.g., "Sand", "Gras", "Kunststoff")
|
||||
* @param turnierId Optional tournament ID to limit search
|
||||
* @param activeOnly Whether to return only active venues (default: true)
|
||||
* @return List of venues with the specified ground type
|
||||
*/
|
||||
suspend fun getByGroundType(boden: String, turnierId: Uuid? = null, activeOnly: Boolean = true): List<Platz> {
|
||||
require(boden.isNotBlank()) { "Ground type cannot be blank" }
|
||||
return platzRepository.findByGroundType(boden.trim(), turnierId, activeOnly)
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves venues by dimensions.
|
||||
*
|
||||
* @param dimension The venue dimensions (e.g., "20x60m", "20x40m")
|
||||
* @param turnierId Optional tournament ID to limit search
|
||||
* @param activeOnly Whether to return only active venues (default: true)
|
||||
* @return List of venues with the specified dimensions
|
||||
*/
|
||||
suspend fun getByDimensions(dimension: String, turnierId: Uuid? = null, activeOnly: Boolean = true): List<Platz> {
|
||||
require(dimension.isNotBlank()) { "Dimension cannot be blank" }
|
||||
return platzRepository.findByDimensions(dimension.trim(), turnierId, activeOnly)
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all active venues.
|
||||
*
|
||||
* @param orderBySortierung Whether to order by sortierReihenfolge field (default: true)
|
||||
* @return List of active venues
|
||||
*/
|
||||
suspend fun getAllActive(orderBySortierung: Boolean = true): List<Platz> {
|
||||
return platzRepository.findAllActive(orderBySortierung)
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds venues suitable for a specific discipline based on type and dimensions.
|
||||
*
|
||||
* @param requiredType The required venue type
|
||||
* @param requiredDimensions Optional required dimensions
|
||||
* @param turnierId Optional tournament ID to limit search
|
||||
* @return List of suitable venues
|
||||
*/
|
||||
suspend fun getSuitableForDiscipline(
|
||||
requiredType: PlatzTypE,
|
||||
requiredDimensions: String? = null,
|
||||
turnierId: Uuid? = null
|
||||
): List<Platz> {
|
||||
requiredDimensions?.let { dimensions ->
|
||||
require(dimensions.isNotBlank()) { "Required dimensions cannot be blank if provided" }
|
||||
}
|
||||
return platzRepository.findSuitableForDiscipline(requiredType, requiredDimensions?.trim(), turnierId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a venue with the given name exists for a tournament.
|
||||
*
|
||||
* @param name The venue name to check
|
||||
* @param turnierId The tournament ID
|
||||
* @return true if a venue with this name exists, false otherwise
|
||||
*/
|
||||
suspend fun existsByNameAndTournament(name: String, turnierId: Uuid): Boolean {
|
||||
require(name.isNotBlank()) { "Venue name cannot be blank" }
|
||||
return platzRepository.existsByNameAndTournament(name.trim(), turnierId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts the total number of active venues for a tournament.
|
||||
*
|
||||
* @param turnierId The tournament ID
|
||||
* @return The total count of active venues
|
||||
*/
|
||||
suspend fun countActiveByTournament(turnierId: Uuid): Long {
|
||||
return platzRepository.countActiveByTournament(turnierId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts venues by type for a tournament.
|
||||
*
|
||||
* @param typ The venue type
|
||||
* @param turnierId The tournament ID
|
||||
* @param activeOnly Whether to count only active venues (default: true)
|
||||
* @return The count of venues of the specified type
|
||||
*/
|
||||
suspend fun countByTypeAndTournament(typ: PlatzTypE, turnierId: Uuid, activeOnly: Boolean = true): Long {
|
||||
return platzRepository.countByTypeAndTournament(typ, turnierId, activeOnly)
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds available venues for a specific time slot.
|
||||
* This method can be extended when venue scheduling functionality is added.
|
||||
*
|
||||
* @param turnierId The tournament ID
|
||||
* @param startTime The start time (placeholder for future scheduling feature)
|
||||
* @param endTime The end time (placeholder for future scheduling feature)
|
||||
* @return List of available venues (currently returns all active venues)
|
||||
*/
|
||||
suspend fun getAvailableForTimeSlot(turnierId: Uuid, startTime: String? = null, endTime: String? = null): List<Platz> {
|
||||
return platzRepository.findAvailableForTimeSlot(turnierId, startTime, endTime)
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves venues grouped by type for a tournament.
|
||||
* This is a convenience method that provides venues organized by their type.
|
||||
*
|
||||
* @param turnierId The tournament ID
|
||||
* @param activeOnly Whether to include only active venues (default: true)
|
||||
* @return Map of venue type to list of venues
|
||||
*/
|
||||
suspend fun getGroupedByTypeForTournament(turnierId: Uuid, activeOnly: Boolean = true): Map<PlatzTypE, List<Platz>> {
|
||||
val venues = platzRepository.findByTournament(turnierId, activeOnly, true)
|
||||
return venues.groupBy { it.typ }
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves venues with specific characteristics for discipline matching.
|
||||
* This method combines multiple filters to find venues suitable for specific disciplines.
|
||||
*
|
||||
* @param turnierId The tournament ID
|
||||
* @param requiredType The required venue type
|
||||
* @param preferredDimensions Preferred dimensions (optional)
|
||||
* @param preferredGroundType Preferred ground type (optional)
|
||||
* @param activeOnly Whether to include only active venues (default: true)
|
||||
* @return List of venues matching the criteria, sorted by preference
|
||||
*/
|
||||
suspend fun getForDisciplineRequirements(
|
||||
turnierId: Uuid,
|
||||
requiredType: PlatzTypE,
|
||||
preferredDimensions: String? = null,
|
||||
preferredGroundType: String? = null,
|
||||
activeOnly: Boolean = true
|
||||
): List<Platz> {
|
||||
// Start with venues of the required type
|
||||
val typeMatches = platzRepository.findByType(requiredType, turnierId, activeOnly)
|
||||
|
||||
// If no specific preferences, return all type matches
|
||||
if (preferredDimensions == null && preferredGroundType == null) {
|
||||
return typeMatches
|
||||
}
|
||||
|
||||
// Filter and sort by preferences
|
||||
val exactMatches = mutableListOf<Platz>()
|
||||
val partialMatches = mutableListOf<Platz>()
|
||||
val otherMatches = mutableListOf<Platz>()
|
||||
|
||||
for (venue in typeMatches) {
|
||||
val dimensionMatch = preferredDimensions == null || venue.dimension == preferredDimensions.trim()
|
||||
val groundMatch = preferredGroundType == null || venue.boden == preferredGroundType.trim()
|
||||
|
||||
when {
|
||||
dimensionMatch && groundMatch -> exactMatches.add(venue)
|
||||
dimensionMatch || groundMatch -> partialMatches.add(venue)
|
||||
else -> otherMatches.add(venue)
|
||||
}
|
||||
}
|
||||
|
||||
// Return sorted by preference: exact matches first, then partial, then others
|
||||
return exactMatches + partialMatches + otherMatches
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates venue availability and suitability for a specific use case.
|
||||
* This method performs comprehensive checks for venue usage.
|
||||
*
|
||||
* @param platzId The venue ID
|
||||
* @param requiredType Optional required venue type
|
||||
* @param requiredDimensions Optional required dimensions
|
||||
* @param requiredGroundType Optional required ground type
|
||||
* @return Pair of (isValid, reasons) where reasons contains any validation issues
|
||||
*/
|
||||
suspend fun validateVenueSuitability(
|
||||
platzId: Uuid,
|
||||
requiredType: PlatzTypE? = null,
|
||||
requiredDimensions: String? = null,
|
||||
requiredGroundType: String? = null
|
||||
): Pair<Boolean, List<String>> {
|
||||
val venue = platzRepository.findById(platzId)
|
||||
val issues = mutableListOf<String>()
|
||||
|
||||
if (venue == null) {
|
||||
issues.add("Venue not found")
|
||||
return Pair(false, issues)
|
||||
}
|
||||
|
||||
if (!venue.istAktiv) {
|
||||
issues.add("Venue is not active")
|
||||
}
|
||||
|
||||
requiredType?.let { type ->
|
||||
if (venue.typ != type) {
|
||||
issues.add("Venue type ${venue.typ} does not match required type $type")
|
||||
}
|
||||
}
|
||||
|
||||
requiredDimensions?.let { dimensions ->
|
||||
if (venue.dimension != dimensions.trim()) {
|
||||
issues.add("Venue dimensions '${venue.dimension}' do not match required dimensions '$dimensions'")
|
||||
}
|
||||
}
|
||||
|
||||
requiredGroundType?.let { groundType ->
|
||||
if (venue.boden != groundType.trim()) {
|
||||
issues.add("Venue ground type '${venue.boden}' does not match required ground type '$groundType'")
|
||||
}
|
||||
}
|
||||
|
||||
return Pair(issues.isEmpty(), issues)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
plugins {
|
||||
alias(libs.plugins.kotlin.multiplatform)
|
||||
alias(libs.plugins.kotlin.serialization)
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvm()
|
||||
js(IR) {
|
||||
browser()
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
val commonMain by getting {
|
||||
dependencies {
|
||||
// KORREKTUR: Diese zwei Zeilen hinzufügen
|
||||
implementation(projects.core.coreDomain)
|
||||
implementation(projects.core.coreUtils)
|
||||
}
|
||||
}
|
||||
val commonTest by getting {
|
||||
dependencies {
|
||||
implementation(kotlin("test"))
|
||||
implementation(projects.platform.platformTesting)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
package at.mocode.masterdata.domain.model
|
||||
|
||||
import at.mocode.core.domain.model.SparteE // Optional, falls Altersklassen stark spartenspezifisch sind
|
||||
import at.mocode.core.domain.serialization.KotlinInstantSerializer
|
||||
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.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Definiert eine spezifische Altersklasse für Teilnehmer (Reiter, Fahrer, Voltigierer)
|
||||
* oder ggf. auch für Pferde, basierend auf den Vorgaben der ÖTO oder anderer Regelwerke.
|
||||
*
|
||||
* Beispiele: "Jugend U16", "Junioren U18", "Junge Reiter U21", "Allgemeine Klasse",
|
||||
* "Pony Jugend U14", "Senioren Ü40".
|
||||
* Diese Definitionen dienen zur Überprüfung von Teilnahmeberechtigungen in Bewerben und Abteilungen.
|
||||
*
|
||||
* @property altersklasseId Eindeutiger interner Identifikator für diese Altersklassendefinition (UUID).
|
||||
* @property altersklasseCode Ein eindeutiges Kürzel oder Code für die Altersklasse
|
||||
* (z.B. "JGD_U16", "JUN_U18", "YR_U21", "AK", "PONY_U14"). Dient als fachlicher Schlüssel.
|
||||
* @property bezeichnung Die offizielle oder allgemein verständliche Bezeichnung der Altersklasse.
|
||||
* @property minAlter Das Mindestalter (Jahre, inklusive) für diese Altersklasse. `null`, wenn es keine Untergrenze gibt.
|
||||
* @property maxAlter Das Höchstalter (Jahre, inklusive) für diese Altersklasse. `null`, wenn es keine Obergrenze gibt.
|
||||
* @property stichtagRegelText Eine Beschreibung der Regel für den Stichtag zur Altersberechnung
|
||||
* (z.B. "31.12. des laufenden Kalenderjahres", "Geburtstag im laufenden Jahr").
|
||||
* @property sparteFilter Optionale Angabe, ob diese Altersklassendefinition nur für eine spezifische Sparte gilt.
|
||||
* @property geschlechtFilter Optionaler Filter für das Geschlecht ('M', 'W'), falls die Altersklasse geschlechtsspezifisch ist.
|
||||
* `null` bedeutet für alle Geschlechter gültig.
|
||||
* @property oetoRegelReferenzId Optionale Verknüpfung zu einer spezifischen Regel in der `OETORegelReferenz`-Tabelle,
|
||||
* die diese Altersklasse definiert.
|
||||
* @property istAktiv Gibt an, ob diese Altersklassendefinition aktuell im System verwendet werden kann.
|
||||
* @property createdAt Zeitstempel der Erstellung dieses Datensatzes.
|
||||
* @property updatedAt Zeitstempel der letzten Aktualisierung dieses Datensatzes.
|
||||
*/
|
||||
@Serializable
|
||||
data class AltersklasseDefinition(
|
||||
@Serializable(with = UuidSerializer::class)
|
||||
val altersklasseId: Uuid = uuid4(), // Interner Primärschlüssel
|
||||
|
||||
var altersklasseCode: String, // Fachlicher PK, z.B. "JGD_U16"
|
||||
var bezeichnung: String,
|
||||
var minAlter: Int? = null,
|
||||
var maxAlter: Int? = null,
|
||||
var stichtagRegelText: String? = "31.12. des laufenden Kalenderjahres", // Typischer Default
|
||||
var sparteFilter: SparteE? = null, // Ist diese Definition spartenspezifisch?
|
||||
var geschlechtFilter: Char? = null, // 'M', 'W', oder null für beide
|
||||
|
||||
@Serializable(with = UuidSerializer::class)
|
||||
var oetoRegelReferenzId: Uuid? = null, // FK zu OETORegelReferenz.oetoRegelReferenzId
|
||||
|
||||
var istAktiv: Boolean = true,
|
||||
|
||||
@Serializable(with = KotlinInstantSerializer::class)
|
||||
val createdAt: Instant = Clock.System.now(),
|
||||
@Serializable(with = KotlinInstantSerializer::class)
|
||||
var updatedAt: Instant = Clock.System.now()
|
||||
)
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
package at.mocode.masterdata.domain.model
|
||||
|
||||
import at.mocode.core.domain.serialization.KotlinInstantSerializer
|
||||
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.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Definiert ein Bundesland oder eine vergleichbare subnationale Verwaltungseinheit.
|
||||
*
|
||||
* Diese Entität ist primär für die österreichischen Bundesländer mit ihren OEPS-spezifischen
|
||||
* Codes gedacht, kann aber auch für Bundesländer/Regionen anderer Nationen erweitert werden.
|
||||
*
|
||||
* @property bundeslandId Eindeutiger interner Identifikator für dieses Bundesland (UUID).
|
||||
* @property landId Fremdschlüssel zur `LandDefinition`, dem dieses Bundesland angehört.
|
||||
* @property oepsCode Der 2-stellige numerische OEPS-Code für österreichische Bundesländer
|
||||
* (z.B. "01" für Wien, "02" für Niederösterreich). Sollte eindeutig sein für Land "Österreich".
|
||||
* @property iso3166_2_Code Optionaler offizieller ISO 3166-2 Code für das Bundesland
|
||||
* (z.B. "AT-1" für Burgenland, "DE-BY" für Bayern).
|
||||
* @property name Der offizielle Name des Bundeslandes.
|
||||
* @property kuerzel Ein gängiges Kürzel für das Bundesland (z.B. "NÖ", "W", "STMK").
|
||||
* @property wappenUrl Optionaler URL-Pfad zu einer Bilddatei des Bundeslandwappens.
|
||||
* @property istAktiv Gibt an, ob dieses Bundesland aktuell im System ausgewählt/verwendet werden kann.
|
||||
* @property sortierReihenfolge Optionale Zahl zur Steuerung der Sortierreihenfolge in Auswahllisten.
|
||||
* @property createdAt Zeitstempel der Erstellung dieses Datensatzes.
|
||||
* @property updatedAt Zeitstempel der letzten Aktualisierung dieses Datensatzes.
|
||||
*/
|
||||
@Serializable
|
||||
data class BundeslandDefinition(
|
||||
@Serializable(with = UuidSerializer::class)
|
||||
val bundeslandId: Uuid = uuid4(),
|
||||
|
||||
@Serializable(with = UuidSerializer::class)
|
||||
var landId: Uuid, // FK zu LandDefinition.landId
|
||||
|
||||
var oepsCode: String?, // z.B. "01", "02", ... für Österreich; eindeutig pro landId = Österreich
|
||||
var iso3166_2_Code: String?, // z.B. "AT-1", "DE-BY"; Eindeutig global oder pro Land?
|
||||
var name: String, // z.B. "Niederösterreich", "Bayern"
|
||||
var kuerzel: String? = null, // z.B. "NÖ", "BY"
|
||||
var wappenUrl: String? = null,
|
||||
var istAktiv: Boolean = true,
|
||||
var sortierReihenfolge: Int? = null,
|
||||
|
||||
@Serializable(with = KotlinInstantSerializer::class)
|
||||
val createdAt: Instant = Clock.System.now(),
|
||||
@Serializable(with = KotlinInstantSerializer::class)
|
||||
var updatedAt: Instant = Clock.System.now()
|
||||
)
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
package at.mocode.masterdata.domain.model
|
||||
|
||||
import at.mocode.core.domain.serialization.KotlinInstantSerializer
|
||||
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.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Definiert ein Land/eine Nation mit seinen offiziellen Codes und Bezeichnungen.
|
||||
*
|
||||
* Diese Entität dient als zentrale Referenz für Länder, die im System für
|
||||
* Nationalitäten von Personen, Vereinen oder für internationale Turniere relevant sind.
|
||||
*
|
||||
* @property landId Eindeutiger interner Identifikator für dieses Land (UUID).
|
||||
* @property isoAlpha2Code Der 2-stellige ISO 3166-1 Alpha-2 Code des Landes (z.B. "AT", "DE"). Sollte eindeutig sein.
|
||||
* @property isoAlpha3Code Der 3-stellige ISO 3166-1 Alpha-3 Code des Landes (z.B. "AUT", "DEU"). Sollte eindeutig sein.
|
||||
* @property isoNumerischerCode Optionaler 3-stelliger numerischer ISO 3166-1 Code des Landes (z.B. "040" für Österreich).
|
||||
* @property nameDeutsch Der offizielle deutsche Name des Landes.
|
||||
* @property nameEnglisch Der offizielle englische Name des Landes.
|
||||
* @property wappenUrl Optionaler URL-Pfad zu einer Bilddatei des Länderwappens oder der Flagge.
|
||||
* @property istEuMitglied Gibt an, ob das Land Mitglied der Europäischen Union ist.
|
||||
* @property istEwrMitglied Gibt an, ob das Land Mitglied des Europäischen Wirtschaftsraums ist.
|
||||
* @property istAktiv Gibt an, ob dieses Land aktuell im System ausgewählt/verwendet werden kann.
|
||||
* @property sortierReihenfolge Optionale Zahl zur Steuerung der Sortierreihenfolge in Auswahllisten.
|
||||
* @property createdAt Zeitstempel der Erstellung dieses Datensatzes.
|
||||
* @property updatedAt Zeitstempel der letzten Aktualisierung dieses Datensatzes.
|
||||
*/
|
||||
@Serializable
|
||||
data class LandDefinition(
|
||||
@Serializable(with = UuidSerializer::class)
|
||||
val landId: Uuid = uuid4(),
|
||||
|
||||
var isoAlpha2Code: String, // z.B. "AT" → Fachlicher PK oder Unique Constraint
|
||||
var isoAlpha3Code: String, // z.B. "AUT" -> Unique Constraint
|
||||
var isoNumerischerCode: String? = null, // z.B. "040"
|
||||
var nameDeutsch: String, // z.B. "Österreich"
|
||||
var nameEnglisch: String? = null, // z.B. "Austria"
|
||||
var wappenUrl: String? = null,
|
||||
var istEuMitglied: Boolean? = null,
|
||||
var istEwrMitglied: Boolean? = null, // Europäischer Wirtschaftsraum
|
||||
var istAktiv: Boolean = true,
|
||||
var sortierReihenfolge: Int? = null,
|
||||
|
||||
@Serializable(with = KotlinInstantSerializer::class)
|
||||
val createdAt: Instant = Clock.System.now(),
|
||||
@Serializable(with = KotlinInstantSerializer::class)
|
||||
var updatedAt: Instant = Clock.System.now()
|
||||
)
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
package at.mocode.masterdata.domain.model
|
||||
|
||||
import at.mocode.core.domain.model.PlatzTypE
|
||||
import at.mocode.core.domain.serialization.KotlinInstantSerializer
|
||||
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.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Definiert einen Turnierplatz oder eine Wettkampfstätte.
|
||||
*
|
||||
* Diese Entität repräsentiert die verschiedenen Plätze und Arenen, die bei Turnieren
|
||||
* für verschiedene Disziplinen verwendet werden können.
|
||||
*
|
||||
* @property id Eindeutiger interner Identifikator für diesen Platz (UUID).
|
||||
* @property turnierId Fremdschlüssel zum Turnier, zu dem dieser Platz gehört.
|
||||
* @property name Der Name oder die Bezeichnung des Platzes (z.B. "Hauptplatz", "Dressurplatz A").
|
||||
* @property dimension Die Abmessungen des Platzes (z.B. "20x60m", "20x40m").
|
||||
* @property boden Die Art des Bodenbelags (z.B. "Sand", "Gras", "Kunststoff").
|
||||
* @property typ Der Typ des Platzes (siehe PlatzTypE enum).
|
||||
* @property istAktiv Gibt an, ob dieser Platz aktuell verwendet werden kann.
|
||||
* @property sortierReihenfolge Optionale Zahl zur Steuerung der Sortierreihenfolge.
|
||||
* @property createdAt Zeitstempel der Erstellung dieses Datensatzes.
|
||||
* @property updatedAt Zeitstempel der letzten Aktualisierung dieses Datensatzes.
|
||||
*/
|
||||
@Serializable
|
||||
data class Platz(
|
||||
@Serializable(with = UuidSerializer::class)
|
||||
val id: Uuid = uuid4(),
|
||||
|
||||
@Serializable(with = UuidSerializer::class)
|
||||
var turnierId: Uuid,
|
||||
|
||||
var name: String,
|
||||
var dimension: String? = null,
|
||||
var boden: String? = null,
|
||||
var typ: PlatzTypE,
|
||||
var istAktiv: Boolean = true,
|
||||
var sortierReihenfolge: Int? = null,
|
||||
|
||||
@Serializable(with = KotlinInstantSerializer::class)
|
||||
val createdAt: Instant = Clock.System.now(),
|
||||
@Serializable(with = KotlinInstantSerializer::class)
|
||||
var updatedAt: Instant = Clock.System.now()
|
||||
)
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
package at.mocode.masterdata.domain.repository
|
||||
|
||||
import at.mocode.core.domain.model.SparteE
|
||||
import at.mocode.masterdata.domain.model.AltersklasseDefinition
|
||||
import com.benasher44.uuid.Uuid
|
||||
|
||||
/**
|
||||
* Repository interface for AltersklasseDefinition (Age Class) domain operations.
|
||||
*
|
||||
* This interface defines the contract for age class data access operations
|
||||
* without depending on specific implementation details (database, etc.).
|
||||
* Following the hexagonal architecture pattern, this interface belongs
|
||||
* to the domain layer and will be implemented in the infrastructure layer.
|
||||
*/
|
||||
interface AltersklasseRepository {
|
||||
|
||||
/**
|
||||
* Finds an age class by its unique ID.
|
||||
*
|
||||
* @param id The unique identifier of the age class
|
||||
* @return The age class if found, null otherwise
|
||||
*/
|
||||
suspend fun findById(id: Uuid): AltersklasseDefinition?
|
||||
|
||||
/**
|
||||
* Finds an age class by its code.
|
||||
*
|
||||
* @param altersklasseCode The age class code (e.g., "JGD_U16", "JUN_U18")
|
||||
* @return The age class if found, null otherwise
|
||||
*/
|
||||
suspend fun findByCode(altersklasseCode: String): AltersklasseDefinition?
|
||||
|
||||
/**
|
||||
* Finds age classes by name (partial match).
|
||||
*
|
||||
* @param searchTerm The search term to match against age class names
|
||||
* @param limit Maximum number of results to return
|
||||
* @return List of matching age classes
|
||||
*/
|
||||
suspend fun findByName(searchTerm: String, limit: Int = 50): List<AltersklasseDefinition>
|
||||
|
||||
/**
|
||||
* Finds all active age classes.
|
||||
*
|
||||
* @param sparteFilter Optional filter by sport type
|
||||
* @param geschlechtFilter Optional filter by gender ('M', 'W')
|
||||
* @return List of active age classes
|
||||
*/
|
||||
suspend fun findAllActive(sparteFilter: SparteE? = null, geschlechtFilter: Char? = null): List<AltersklasseDefinition>
|
||||
|
||||
/**
|
||||
* Finds age classes applicable for a specific age.
|
||||
*
|
||||
* @param age The age to check
|
||||
* @param sparteFilter Optional filter by sport type
|
||||
* @param geschlechtFilter Optional filter by gender ('M', 'W')
|
||||
* @return List of applicable age classes
|
||||
*/
|
||||
suspend fun findApplicableForAge(age: Int, sparteFilter: SparteE? = null, geschlechtFilter: Char? = null): List<AltersklasseDefinition>
|
||||
|
||||
/**
|
||||
* Finds age classes by sport type.
|
||||
*
|
||||
* @param sparte The sport type
|
||||
* @param activeOnly Whether to return only active age classes
|
||||
* @return List of age classes for the sport type
|
||||
*/
|
||||
suspend fun findBySparte(sparte: SparteE, activeOnly: Boolean = true): List<AltersklasseDefinition>
|
||||
|
||||
/**
|
||||
* Finds age classes by gender filter.
|
||||
*
|
||||
* @param geschlecht The gender ('M', 'W')
|
||||
* @param activeOnly Whether to return only active age classes
|
||||
* @return List of age classes for the gender
|
||||
*/
|
||||
suspend fun findByGeschlecht(geschlecht: Char, activeOnly: Boolean = true): List<AltersklasseDefinition>
|
||||
|
||||
/**
|
||||
* Finds age classes by age range.
|
||||
*
|
||||
* @param minAge Minimum age (inclusive)
|
||||
* @param maxAge Maximum age (inclusive)
|
||||
* @param activeOnly Whether to return only active age classes
|
||||
* @return List of age classes within the age range
|
||||
*/
|
||||
suspend fun findByAgeRange(minAge: Int?, maxAge: Int?, activeOnly: Boolean = true): List<AltersklasseDefinition>
|
||||
|
||||
/**
|
||||
* Finds age classes by OETO rule reference.
|
||||
*
|
||||
* @param oetoRegelReferenzId The OETO rule reference ID
|
||||
* @return List of age classes linked to the rule
|
||||
*/
|
||||
suspend fun findByOetoRegelReferenz(oetoRegelReferenzId: Uuid): List<AltersklasseDefinition>
|
||||
|
||||
/**
|
||||
* Saves an age class (create or update).
|
||||
*
|
||||
* @param altersklasse The age class to save
|
||||
* @return The saved age class with updated timestamps
|
||||
*/
|
||||
suspend fun save(altersklasse: AltersklasseDefinition): AltersklasseDefinition
|
||||
|
||||
/**
|
||||
* Deletes an age class by ID.
|
||||
*
|
||||
* @param id The unique identifier of the age class to delete
|
||||
* @return true if the age class was deleted, false if not found
|
||||
*/
|
||||
suspend fun delete(id: Uuid): Boolean
|
||||
|
||||
/**
|
||||
* Checks if an age class with the given code exists.
|
||||
*
|
||||
* @param altersklasseCode The age class code to check
|
||||
* @return true if an age class with this code exists, false otherwise
|
||||
*/
|
||||
suspend fun existsByCode(altersklasseCode: String): Boolean
|
||||
|
||||
/**
|
||||
* Counts the total number of active age classes.
|
||||
*
|
||||
* @param sparteFilter Optional filter by sport type
|
||||
* @return The total count of active age classes
|
||||
*/
|
||||
suspend fun countActive(sparteFilter: SparteE? = null): Long
|
||||
|
||||
/**
|
||||
* Validates if a person with given age and gender can participate in an age class.
|
||||
*
|
||||
* @param altersklasseId The age class ID
|
||||
* @param age The person's age
|
||||
* @param geschlecht The person's gender ('M', 'W')
|
||||
* @return true if the person can participate, false otherwise
|
||||
*/
|
||||
suspend fun isEligible(altersklasseId: Uuid, age: Int, geschlecht: Char): Boolean
|
||||
}
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
package at.mocode.masterdata.domain.repository
|
||||
|
||||
import at.mocode.masterdata.domain.model.BundeslandDefinition
|
||||
import com.benasher44.uuid.Uuid
|
||||
|
||||
/**
|
||||
* Repository interface for BundeslandDefinition (Federal State) domain operations.
|
||||
*
|
||||
* This interface defines the contract for federal state data access operations
|
||||
* without depending on specific implementation details (database, etc.).
|
||||
* Following the hexagonal architecture pattern, this interface belongs
|
||||
* to the domain layer and will be implemented in the infrastructure layer.
|
||||
*/
|
||||
interface BundeslandRepository {
|
||||
|
||||
/**
|
||||
* Finds a federal state by its unique ID.
|
||||
*
|
||||
* @param id The unique identifier of the federal state
|
||||
* @return The federal state if found, null otherwise
|
||||
*/
|
||||
suspend fun findById(id: Uuid): BundeslandDefinition?
|
||||
|
||||
/**
|
||||
* Finds a federal state by its OEPS code.
|
||||
*
|
||||
* @param oepsCode The OEPS code (e.g., "01", "02")
|
||||
* @param landId The country ID to search within
|
||||
* @return The federal state if found, null otherwise
|
||||
*/
|
||||
suspend fun findByOepsCode(oepsCode: String, landId: Uuid): BundeslandDefinition?
|
||||
|
||||
/**
|
||||
* Finds a federal state by its ISO 3166-2 code.
|
||||
*
|
||||
* @param iso3166_2_Code The ISO 3166-2 code (e.g., "AT-1", "DE-BY")
|
||||
* @return The federal state if found, null otherwise
|
||||
*/
|
||||
suspend fun findByIso3166_2_Code(iso3166_2_Code: String): BundeslandDefinition?
|
||||
|
||||
/**
|
||||
* Finds all federal states for a specific country.
|
||||
*
|
||||
* @param landId The country ID
|
||||
* @param activeOnly Whether to return only active federal states
|
||||
* @param orderBySortierung Whether to order by sortierReihenfolge field
|
||||
* @return List of federal states for the country
|
||||
*/
|
||||
suspend fun findByCountry(landId: Uuid, activeOnly: Boolean = true, orderBySortierung: Boolean = true): List<BundeslandDefinition>
|
||||
|
||||
/**
|
||||
* Finds federal states by name (partial match).
|
||||
*
|
||||
* @param searchTerm The search term to match against federal state names
|
||||
* @param landId Optional country ID to limit search
|
||||
* @param limit Maximum number of results to return
|
||||
* @return List of matching federal states
|
||||
*/
|
||||
suspend fun findByName(searchTerm: String, landId: Uuid? = null, limit: Int = 50): List<BundeslandDefinition>
|
||||
|
||||
/**
|
||||
* Finds all active federal states.
|
||||
*
|
||||
* @param orderBySortierung Whether to order by sortierReihenfolge field
|
||||
* @return List of active federal states
|
||||
*/
|
||||
suspend fun findAllActive(orderBySortierung: Boolean = true): List<BundeslandDefinition>
|
||||
|
||||
/**
|
||||
* Saves a federal state (create or update).
|
||||
*
|
||||
* @param bundesland The federal state to save
|
||||
* @return The saved federal state with updated timestamps
|
||||
*/
|
||||
suspend fun save(bundesland: BundeslandDefinition): BundeslandDefinition
|
||||
|
||||
/**
|
||||
* Deletes a federal state by ID.
|
||||
*
|
||||
* @param id The unique identifier of the federal state to delete
|
||||
* @return true if the federal state was deleted, false if not found
|
||||
*/
|
||||
suspend fun delete(id: Uuid): Boolean
|
||||
|
||||
/**
|
||||
* Checks if a federal state with the given OEPS code exists for a country.
|
||||
*
|
||||
* @param oepsCode The OEPS code to check
|
||||
* @param landId The country ID
|
||||
* @return true if a federal state with this code exists, false otherwise
|
||||
*/
|
||||
suspend fun existsByOepsCode(oepsCode: String, landId: Uuid): Boolean
|
||||
|
||||
/**
|
||||
* Checks if a federal state with the given ISO 3166-2 code exists.
|
||||
*
|
||||
* @param iso3166_2_Code The ISO 3166-2 code to check
|
||||
* @return true if a federal state with this code exists, false otherwise
|
||||
*/
|
||||
suspend fun existsByIso3166_2_Code(iso3166_2_Code: String): Boolean
|
||||
|
||||
/**
|
||||
* Counts the total number of active federal states for a country.
|
||||
*
|
||||
* @param landId The country ID
|
||||
* @return The total count of active federal states
|
||||
*/
|
||||
suspend fun countActiveByCountry(landId: Uuid): Long
|
||||
}
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
package at.mocode.masterdata.domain.repository
|
||||
|
||||
import at.mocode.masterdata.domain.model.LandDefinition
|
||||
import com.benasher44.uuid.Uuid
|
||||
|
||||
/**
|
||||
* Repository interface for LandDefinition (Country) domain operations.
|
||||
*
|
||||
* This interface defines the contract for country data access operations
|
||||
* without depending on specific implementation details (database, etc.).
|
||||
* Following the hexagonal architecture pattern, this interface belongs
|
||||
* to the domain layer and will be implemented in the infrastructure layer.
|
||||
*/
|
||||
interface LandRepository {
|
||||
|
||||
/**
|
||||
* Finds a country by its unique ID.
|
||||
*
|
||||
* @param id The unique identifier of the country
|
||||
* @return The country if found, null otherwise
|
||||
*/
|
||||
suspend fun findById(id: Uuid): LandDefinition?
|
||||
|
||||
/**
|
||||
* Finds a country by its ISO Alpha-2 code.
|
||||
*
|
||||
* @param isoAlpha2Code The 2-letter ISO code (e.g., "AT", "DE")
|
||||
* @return The country if found, null otherwise
|
||||
*/
|
||||
suspend fun findByIsoAlpha2Code(isoAlpha2Code: String): LandDefinition?
|
||||
|
||||
/**
|
||||
* Finds a country by its ISO Alpha-3 code.
|
||||
*
|
||||
* @param isoAlpha3Code The 3-letter ISO code (e.g., "AUT", "DEU")
|
||||
* @return The country if found, null otherwise
|
||||
*/
|
||||
suspend fun findByIsoAlpha3Code(isoAlpha3Code: String): LandDefinition?
|
||||
|
||||
/**
|
||||
* Finds countries by name (partial match on German or English name).
|
||||
*
|
||||
* @param searchTerm The search term to match against country names
|
||||
* @param limit Maximum number of results to return
|
||||
* @return List of matching countries
|
||||
*/
|
||||
suspend fun findByName(searchTerm: String, limit: Int = 50): List<LandDefinition>
|
||||
|
||||
/**
|
||||
* Finds all active countries.
|
||||
*
|
||||
* @param orderBySortierung Whether to order by sortierReihenfolge field
|
||||
* @return List of active countries
|
||||
*/
|
||||
suspend fun findAllActive(orderBySortierung: Boolean = true): List<LandDefinition>
|
||||
|
||||
/**
|
||||
* Finds all EU member countries.
|
||||
*
|
||||
* @return List of EU member countries
|
||||
*/
|
||||
suspend fun findEuMembers(): List<LandDefinition>
|
||||
|
||||
/**
|
||||
* Finds all EWR (European Economic Area) member countries.
|
||||
*
|
||||
* @return List of EWR member countries
|
||||
*/
|
||||
suspend fun findEwrMembers(): List<LandDefinition>
|
||||
|
||||
/**
|
||||
* Saves a country (create or update).
|
||||
*
|
||||
* @param land The country to save
|
||||
* @return The saved country with updated timestamps
|
||||
*/
|
||||
suspend fun save(land: LandDefinition): LandDefinition
|
||||
|
||||
/**
|
||||
* Deletes a country by ID.
|
||||
*
|
||||
* @param id The unique identifier of the country to delete
|
||||
* @return true if the country was deleted, false if not found
|
||||
*/
|
||||
suspend fun delete(id: Uuid): Boolean
|
||||
|
||||
/**
|
||||
* Checks if a country with the given ISO Alpha-2 code exists.
|
||||
*
|
||||
* @param isoAlpha2Code The ISO Alpha-2 code to check
|
||||
* @return true if a country with this code exists, false otherwise
|
||||
*/
|
||||
suspend fun existsByIsoAlpha2Code(isoAlpha2Code: String): Boolean
|
||||
|
||||
/**
|
||||
* Checks if a country with the given ISO Alpha-3 code exists.
|
||||
*
|
||||
* @param isoAlpha3Code The ISO Alpha-3 code to check
|
||||
* @return true if a country with this code exists, false otherwise
|
||||
*/
|
||||
suspend fun existsByIsoAlpha3Code(isoAlpha3Code: String): Boolean
|
||||
|
||||
/**
|
||||
* Counts the total number of active countries.
|
||||
*
|
||||
* @return The total count of active countries
|
||||
*/
|
||||
suspend fun countActive(): Long
|
||||
}
|
||||
+150
@@ -0,0 +1,150 @@
|
||||
package at.mocode.masterdata.domain.repository
|
||||
|
||||
import at.mocode.core.domain.model.PlatzTypE
|
||||
import at.mocode.masterdata.domain.model.Platz
|
||||
import com.benasher44.uuid.Uuid
|
||||
|
||||
/**
|
||||
* Repository interface for Platz (Venue/Arena) domain operations.
|
||||
*
|
||||
* This interface defines the contract for venue/arena data access operations
|
||||
* without depending on specific implementation details (database, etc.).
|
||||
* Following the hexagonal architecture pattern, this interface belongs
|
||||
* to the domain layer and will be implemented in the infrastructure layer.
|
||||
*/
|
||||
interface PlatzRepository {
|
||||
|
||||
/**
|
||||
* Finds a venue by its unique ID.
|
||||
*
|
||||
* @param id The unique identifier of the venue
|
||||
* @return The venue if found, null otherwise
|
||||
*/
|
||||
suspend fun findById(id: Uuid): Platz?
|
||||
|
||||
/**
|
||||
* Finds all venues for a specific tournament.
|
||||
*
|
||||
* @param turnierId The tournament ID
|
||||
* @param activeOnly Whether to return only active venues
|
||||
* @param orderBySortierung Whether to order by sortierReihenfolge field
|
||||
* @return List of venues for the tournament
|
||||
*/
|
||||
suspend fun findByTournament(turnierId: Uuid, activeOnly: Boolean = true, orderBySortierung: Boolean = true): List<Platz>
|
||||
|
||||
/**
|
||||
* Finds venues by name (partial match).
|
||||
*
|
||||
* @param searchTerm The search term to match against venue names
|
||||
* @param turnierId Optional tournament ID to limit search
|
||||
* @param limit Maximum number of results to return
|
||||
* @return List of matching venues
|
||||
*/
|
||||
suspend fun findByName(searchTerm: String, turnierId: Uuid? = null, limit: Int = 50): List<Platz>
|
||||
|
||||
/**
|
||||
* Finds venues by type.
|
||||
*
|
||||
* @param typ The venue type
|
||||
* @param turnierId Optional tournament ID to limit search
|
||||
* @param activeOnly Whether to return only active venues
|
||||
* @return List of venues of the specified type
|
||||
*/
|
||||
suspend fun findByType(typ: PlatzTypE, turnierId: Uuid? = null, activeOnly: Boolean = true): List<Platz>
|
||||
|
||||
/**
|
||||
* Finds venues by ground type.
|
||||
*
|
||||
* @param boden The ground type (e.g., "Sand", "Gras", "Kunststoff")
|
||||
* @param turnierId Optional tournament ID to limit search
|
||||
* @param activeOnly Whether to return only active venues
|
||||
* @return List of venues with the specified ground type
|
||||
*/
|
||||
suspend fun findByGroundType(boden: String, turnierId: Uuid? = null, activeOnly: Boolean = true): List<Platz>
|
||||
|
||||
/**
|
||||
* Finds venues by dimensions.
|
||||
*
|
||||
* @param dimension The venue dimensions (e.g., "20x60m", "20x40m")
|
||||
* @param turnierId Optional tournament ID to limit search
|
||||
* @param activeOnly Whether to return only active venues
|
||||
* @return List of venues with the specified dimensions
|
||||
*/
|
||||
suspend fun findByDimensions(dimension: String, turnierId: Uuid? = null, activeOnly: Boolean = true): List<Platz>
|
||||
|
||||
/**
|
||||
* Finds all active venues.
|
||||
*
|
||||
* @param orderBySortierung Whether to order by sortierReihenfolge field
|
||||
* @return List of active venues
|
||||
*/
|
||||
suspend fun findAllActive(orderBySortierung: Boolean = true): List<Platz>
|
||||
|
||||
/**
|
||||
* Finds venues suitable for a specific discipline based on type and dimensions.
|
||||
*
|
||||
* @param requiredType The required venue type
|
||||
* @param requiredDimensions Optional required dimensions
|
||||
* @param turnierId Optional tournament ID to limit search
|
||||
* @return List of suitable venues
|
||||
*/
|
||||
suspend fun findSuitableForDiscipline(
|
||||
requiredType: PlatzTypE,
|
||||
requiredDimensions: String? = null,
|
||||
turnierId: Uuid? = null
|
||||
): List<Platz>
|
||||
|
||||
/**
|
||||
* Saves a venue (create or update).
|
||||
*
|
||||
* @param platz The venue to save
|
||||
* @return The saved venue with updated timestamps
|
||||
*/
|
||||
suspend fun save(platz: Platz): Platz
|
||||
|
||||
/**
|
||||
* Deletes a venue by ID.
|
||||
*
|
||||
* @param id The unique identifier of the venue to delete
|
||||
* @return true if the venue was deleted, false if not found
|
||||
*/
|
||||
suspend fun delete(id: Uuid): Boolean
|
||||
|
||||
/**
|
||||
* Checks if a venue with the given name exists for a tournament.
|
||||
*
|
||||
* @param name The venue name to check
|
||||
* @param turnierId The tournament ID
|
||||
* @return true if a venue with this name exists, false otherwise
|
||||
*/
|
||||
suspend fun existsByNameAndTournament(name: String, turnierId: Uuid): Boolean
|
||||
|
||||
/**
|
||||
* Counts the total number of active venues for a tournament.
|
||||
*
|
||||
* @param turnierId The tournament ID
|
||||
* @return The total count of active venues
|
||||
*/
|
||||
suspend fun countActiveByTournament(turnierId: Uuid): Long
|
||||
|
||||
/**
|
||||
* Counts venues by type for a tournament.
|
||||
*
|
||||
* @param typ The venue type
|
||||
* @param turnierId The tournament ID
|
||||
* @param activeOnly Whether to count only active venues
|
||||
* @return The count of venues of the specified type
|
||||
*/
|
||||
suspend fun countByTypeAndTournament(typ: PlatzTypE, turnierId: Uuid, activeOnly: Boolean = true): Long
|
||||
|
||||
/**
|
||||
* Finds available venues for a specific time slot (if scheduling is implemented).
|
||||
* This method can be extended when venue scheduling functionality is added.
|
||||
*
|
||||
* @param turnierId The tournament ID
|
||||
* @param startTime The start time (placeholder for future scheduling feature)
|
||||
* @param endTime The end time (placeholder for future scheduling feature)
|
||||
* @return List of available venues (currently returns all active venues)
|
||||
*/
|
||||
suspend fun findAvailableForTimeSlot(turnierId: Uuid, startTime: String? = null, endTime: String? = null): List<Platz>
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
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)
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.platform.platformDependencies)
|
||||
|
||||
implementation(projects.masterdata.masterdataDomain)
|
||||
implementation(projects.masterdata.masterdataApplication)
|
||||
implementation(projects.core.coreDomain)
|
||||
implementation(projects.core.coreUtils)
|
||||
implementation(projects.infrastructure.cache.cacheApi)
|
||||
implementation(projects.infrastructure.eventStore.eventStoreApi)
|
||||
implementation(projects.infrastructure.messaging.messagingClient)
|
||||
|
||||
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
|
||||
implementation("org.postgresql:postgresql")
|
||||
|
||||
testImplementation(projects.platform.platformTesting)
|
||||
}
|
||||
+239
@@ -0,0 +1,239 @@
|
||||
package at.mocode.masterdata.infrastructure.persistence
|
||||
|
||||
import at.mocode.core.domain.model.SparteE
|
||||
import at.mocode.masterdata.domain.model.AltersklasseDefinition
|
||||
import at.mocode.masterdata.domain.repository.AltersklasseRepository
|
||||
import at.mocode.core.utils.database.DatabaseFactory
|
||||
import com.benasher44.uuid.Uuid
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.toInstant
|
||||
import kotlinx.datetime.toLocalDateTime
|
||||
import org.jetbrains.exposed.sql.*
|
||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
||||
|
||||
/**
|
||||
* Implementierung des AltersklasseRepository für die Datenbankzugriffe.
|
||||
*
|
||||
* Diese Implementierung verwendet Exposed SQL für den Datenbankzugriff
|
||||
* und mappt zwischen der AltersklasseDefinition Domain-Entität und der AltersklasseTable.
|
||||
*/
|
||||
class AltersklasseRepositoryImpl : AltersklasseRepository {
|
||||
|
||||
/**
|
||||
* Konvertiert eine Datenbankzeile in ein Domain-Objekt.
|
||||
*/
|
||||
private fun rowToAltersklasseDefinition(row: ResultRow): AltersklasseDefinition {
|
||||
return AltersklasseDefinition(
|
||||
altersklasseId = row[AltersklasseTable.id],
|
||||
altersklasseCode = row[AltersklasseTable.altersklasseCode],
|
||||
bezeichnung = row[AltersklasseTable.bezeichnung],
|
||||
minAlter = row[AltersklasseTable.minAlter],
|
||||
maxAlter = row[AltersklasseTable.maxAlter],
|
||||
stichtagRegelText = row[AltersklasseTable.stichtagRegelText],
|
||||
sparteFilter = row[AltersklasseTable.sparteFilter]?.let { SparteE.valueOf(it) },
|
||||
geschlechtFilter = row[AltersklasseTable.geschlechtFilter],
|
||||
oetoRegelReferenzId = row[AltersklasseTable.oetoRegelReferenzId],
|
||||
istAktiv = row[AltersklasseTable.istAktiv],
|
||||
createdAt = row[AltersklasseTable.createdAt].toInstant(TimeZone.UTC),
|
||||
updatedAt = row[AltersklasseTable.updatedAt].toInstant(TimeZone.UTC)
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun findById(id: Uuid): AltersklasseDefinition? = DatabaseFactory.dbQuery {
|
||||
AltersklasseTable.selectAll().where { AltersklasseTable.id eq id }
|
||||
.map(::rowToAltersklasseDefinition)
|
||||
.singleOrNull()
|
||||
}
|
||||
|
||||
override suspend fun findByCode(altersklasseCode: String): AltersklasseDefinition? = DatabaseFactory.dbQuery {
|
||||
AltersklasseTable.selectAll().where { AltersklasseTable.altersklasseCode eq altersklasseCode }
|
||||
.map(::rowToAltersklasseDefinition)
|
||||
.singleOrNull()
|
||||
}
|
||||
|
||||
override suspend fun findByName(searchTerm: String, limit: Int): List<AltersklasseDefinition> = DatabaseFactory.dbQuery {
|
||||
val pattern = "%$searchTerm%"
|
||||
AltersklasseTable.selectAll().where { AltersklasseTable.bezeichnung like pattern }
|
||||
.limit(limit)
|
||||
.map(::rowToAltersklasseDefinition)
|
||||
}
|
||||
|
||||
override suspend fun findAllActive(sparteFilter: SparteE?, geschlechtFilter: Char?): List<AltersklasseDefinition> = DatabaseFactory.dbQuery {
|
||||
val query = AltersklasseTable.selectAll().where { AltersklasseTable.istAktiv eq true }
|
||||
|
||||
sparteFilter?.let { sparte ->
|
||||
query.andWhere {
|
||||
(AltersklasseTable.sparteFilter eq sparte.name) or (AltersklasseTable.sparteFilter.isNull())
|
||||
}
|
||||
}
|
||||
|
||||
geschlechtFilter?.let { geschlecht ->
|
||||
query.andWhere {
|
||||
(AltersklasseTable.geschlechtFilter eq geschlecht) or (AltersklasseTable.geschlechtFilter.isNull())
|
||||
}
|
||||
}
|
||||
|
||||
query.orderBy(AltersklasseTable.bezeichnung to SortOrder.ASC)
|
||||
.map(::rowToAltersklasseDefinition)
|
||||
}
|
||||
|
||||
override suspend fun findApplicableForAge(age: Int, sparteFilter: SparteE?, geschlechtFilter: Char?): List<AltersklasseDefinition> = DatabaseFactory.dbQuery {
|
||||
val query = AltersklasseTable.selectAll().where { AltersklasseTable.istAktiv eq true }
|
||||
|
||||
// Age range filter
|
||||
query.andWhere {
|
||||
(AltersklasseTable.minAlter.isNull() or (AltersklasseTable.minAlter lessEq age)) and
|
||||
(AltersklasseTable.maxAlter.isNull() or (AltersklasseTable.maxAlter greaterEq age))
|
||||
}
|
||||
|
||||
sparteFilter?.let { sparte ->
|
||||
query.andWhere {
|
||||
(AltersklasseTable.sparteFilter eq sparte.name) or (AltersklasseTable.sparteFilter.isNull())
|
||||
}
|
||||
}
|
||||
|
||||
geschlechtFilter?.let { geschlecht ->
|
||||
query.andWhere {
|
||||
(AltersklasseTable.geschlechtFilter eq geschlecht) or (AltersklasseTable.geschlechtFilter.isNull())
|
||||
}
|
||||
}
|
||||
|
||||
query.orderBy(AltersklasseTable.bezeichnung to SortOrder.ASC)
|
||||
.map(::rowToAltersklasseDefinition)
|
||||
}
|
||||
|
||||
override suspend fun findBySparte(sparte: SparteE, activeOnly: Boolean): List<AltersklasseDefinition> = DatabaseFactory.dbQuery {
|
||||
val query = AltersklasseTable.selectAll().where {
|
||||
(AltersklasseTable.sparteFilter eq sparte.name) or (AltersklasseTable.sparteFilter.isNull())
|
||||
}
|
||||
|
||||
if (activeOnly) {
|
||||
query.andWhere { AltersklasseTable.istAktiv eq true }
|
||||
}
|
||||
|
||||
query.orderBy(AltersklasseTable.bezeichnung to SortOrder.ASC)
|
||||
.map(::rowToAltersklasseDefinition)
|
||||
}
|
||||
|
||||
override suspend fun findByGeschlecht(geschlecht: Char, activeOnly: Boolean): List<AltersklasseDefinition> = DatabaseFactory.dbQuery {
|
||||
val query = AltersklasseTable.selectAll().where {
|
||||
(AltersklasseTable.geschlechtFilter eq geschlecht) or (AltersklasseTable.geschlechtFilter.isNull())
|
||||
}
|
||||
|
||||
if (activeOnly) {
|
||||
query.andWhere { AltersklasseTable.istAktiv eq true }
|
||||
}
|
||||
|
||||
query.orderBy(AltersklasseTable.bezeichnung to SortOrder.ASC)
|
||||
.map(::rowToAltersklasseDefinition)
|
||||
}
|
||||
|
||||
override suspend fun findByAgeRange(minAge: Int?, maxAge: Int?, activeOnly: Boolean): List<AltersklasseDefinition> = DatabaseFactory.dbQuery {
|
||||
val query = AltersklasseTable.selectAll()
|
||||
|
||||
minAge?.let { min ->
|
||||
query.andWhere {
|
||||
(AltersklasseTable.maxAlter.isNull()) or (AltersklasseTable.maxAlter greaterEq min)
|
||||
}
|
||||
}
|
||||
|
||||
maxAge?.let { max ->
|
||||
query.andWhere {
|
||||
(AltersklasseTable.minAlter.isNull()) or (AltersklasseTable.minAlter lessEq max)
|
||||
}
|
||||
}
|
||||
|
||||
if (activeOnly) {
|
||||
query.andWhere { AltersklasseTable.istAktiv eq true }
|
||||
}
|
||||
|
||||
query.orderBy(AltersklasseTable.bezeichnung to SortOrder.ASC)
|
||||
.map(::rowToAltersklasseDefinition)
|
||||
}
|
||||
|
||||
override suspend fun findByOetoRegelReferenz(oetoRegelReferenzId: Uuid): List<AltersklasseDefinition> = DatabaseFactory.dbQuery {
|
||||
AltersklasseTable.selectAll().where { AltersklasseTable.oetoRegelReferenzId eq oetoRegelReferenzId }
|
||||
.orderBy(AltersklasseTable.bezeichnung to SortOrder.ASC)
|
||||
.map(::rowToAltersklasseDefinition)
|
||||
}
|
||||
|
||||
override suspend fun save(altersklasse: AltersklasseDefinition): AltersklasseDefinition = DatabaseFactory.dbQuery {
|
||||
val now = Clock.System.now()
|
||||
val existingAltersklasse = AltersklasseTable.selectAll().where { AltersklasseTable.id eq altersklasse.altersklasseId }.singleOrNull()
|
||||
|
||||
if (existingAltersklasse == null) {
|
||||
// Insert a new age class
|
||||
AltersklasseTable.insert { stmt ->
|
||||
stmt[id] = altersklasse.altersklasseId
|
||||
stmt[altersklasseCode] = altersklasse.altersklasseCode
|
||||
stmt[bezeichnung] = altersklasse.bezeichnung
|
||||
stmt[minAlter] = altersklasse.minAlter
|
||||
stmt[maxAlter] = altersklasse.maxAlter
|
||||
stmt[stichtagRegelText] = altersklasse.stichtagRegelText
|
||||
stmt[sparteFilter] = altersklasse.sparteFilter?.name
|
||||
stmt[geschlechtFilter] = altersklasse.geschlechtFilter
|
||||
stmt[oetoRegelReferenzId] = altersklasse.oetoRegelReferenzId
|
||||
stmt[istAktiv] = altersklasse.istAktiv
|
||||
stmt[createdAt] = altersklasse.createdAt.toLocalDateTime(TimeZone.UTC)
|
||||
stmt[updatedAt] = now.toLocalDateTime(TimeZone.UTC)
|
||||
}
|
||||
} else {
|
||||
// Update existing age class
|
||||
AltersklasseTable.update({ AltersklasseTable.id eq altersklasse.altersklasseId }) { stmt ->
|
||||
stmt[altersklasseCode] = altersklasse.altersklasseCode
|
||||
stmt[bezeichnung] = altersklasse.bezeichnung
|
||||
stmt[minAlter] = altersklasse.minAlter
|
||||
stmt[maxAlter] = altersklasse.maxAlter
|
||||
stmt[stichtagRegelText] = altersklasse.stichtagRegelText
|
||||
stmt[sparteFilter] = altersklasse.sparteFilter?.name
|
||||
stmt[geschlechtFilter] = altersklasse.geschlechtFilter
|
||||
stmt[oetoRegelReferenzId] = altersklasse.oetoRegelReferenzId
|
||||
stmt[istAktiv] = altersklasse.istAktiv
|
||||
stmt[updatedAt] = now.toLocalDateTime(TimeZone.UTC)
|
||||
}
|
||||
}
|
||||
|
||||
altersklasse.copy(updatedAt = now)
|
||||
}
|
||||
|
||||
override suspend fun delete(id: Uuid): Boolean = DatabaseFactory.dbQuery {
|
||||
AltersklasseTable.deleteWhere { AltersklasseTable.id eq id } > 0
|
||||
}
|
||||
|
||||
override suspend fun existsByCode(altersklasseCode: String): Boolean = DatabaseFactory.dbQuery {
|
||||
AltersklasseTable.selectAll().where { AltersklasseTable.altersklasseCode eq altersklasseCode }
|
||||
.count() > 0
|
||||
}
|
||||
|
||||
override suspend fun countActive(sparteFilter: SparteE?): Long = DatabaseFactory.dbQuery {
|
||||
val query = AltersklasseTable.selectAll().where { AltersklasseTable.istAktiv eq true }
|
||||
|
||||
sparteFilter?.let { sparte ->
|
||||
query.andWhere {
|
||||
(AltersklasseTable.sparteFilter eq sparte.name) or (AltersklasseTable.sparteFilter.isNull())
|
||||
}
|
||||
}
|
||||
|
||||
query.count()
|
||||
}
|
||||
|
||||
override suspend fun isEligible(altersklasseId: Uuid, age: Int, geschlecht: Char): Boolean = DatabaseFactory.dbQuery {
|
||||
val altersklasse = AltersklasseTable.selectAll().where {
|
||||
(AltersklasseTable.id eq altersklasseId) and (AltersklasseTable.istAktiv eq true)
|
||||
}.singleOrNull()
|
||||
|
||||
if (altersklasse == null) return@dbQuery false
|
||||
|
||||
// Check age eligibility
|
||||
val minAlter = altersklasse[AltersklasseTable.minAlter]
|
||||
val maxAlter = altersklasse[AltersklasseTable.maxAlter]
|
||||
val ageEligible = (minAlter == null || age >= minAlter) && (maxAlter == null || age <= maxAlter)
|
||||
|
||||
// Check gender eligibility
|
||||
val geschlechtFilter = altersklasse[AltersklasseTable.geschlechtFilter]
|
||||
val genderEligible = geschlechtFilter == null || geschlechtFilter == geschlecht
|
||||
|
||||
ageEligible && genderEligible
|
||||
}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
package at.mocode.masterdata.infrastructure.persistence
|
||||
|
||||
import org.jetbrains.exposed.sql.Table
|
||||
import org.jetbrains.exposed.sql.kotlin.datetime.datetime
|
||||
import org.jetbrains.exposed.sql.kotlin.datetime.CurrentDateTime
|
||||
|
||||
/**
|
||||
* Exposed-Tabellendefinition für die Altersklasse-Entität (Altersklassendefinitionen).
|
||||
*
|
||||
* Diese Tabelle speichert alle Informationen zu Altersklassen für Teilnehmer
|
||||
* entsprechend der AltersklasseDefinition Domain-Entität.
|
||||
*/
|
||||
object AltersklasseTable : Table("altersklasse") {
|
||||
val id = uuid("id").autoGenerate()
|
||||
val altersklasseCode = varchar("altersklasse_code", 50).uniqueIndex()
|
||||
val bezeichnung = varchar("bezeichnung", 200)
|
||||
val minAlter = integer("min_alter").nullable()
|
||||
val maxAlter = integer("max_alter").nullable()
|
||||
val stichtagRegelText = varchar("stichtag_regel_text", 500).nullable()
|
||||
val sparteFilter = varchar("sparte_filter", 50).nullable() // Enum as string
|
||||
val geschlechtFilter = char("geschlecht_filter").nullable()
|
||||
val oetoRegelReferenzId = uuid("oeto_regel_referenz_id").nullable()
|
||||
val istAktiv = bool("ist_aktiv").default(true)
|
||||
val createdAt = datetime("created_at").defaultExpression(CurrentDateTime)
|
||||
val updatedAt = datetime("updated_at").defaultExpression(CurrentDateTime)
|
||||
|
||||
override val primaryKey = PrimaryKey(id)
|
||||
|
||||
init {
|
||||
// Index for performance on common queries
|
||||
index(customIndexName = "idx_altersklasse_aktiv", columns = arrayOf(istAktiv))
|
||||
index(customIndexName = "idx_altersklasse_sparte", columns = arrayOf(sparteFilter))
|
||||
index(customIndexName = "idx_altersklasse_geschlecht", columns = arrayOf(geschlechtFilter))
|
||||
index(customIndexName = "idx_altersklasse_alter", columns = arrayOf(minAlter, maxAlter))
|
||||
}
|
||||
}
|
||||
+157
@@ -0,0 +1,157 @@
|
||||
package at.mocode.masterdata.infrastructure.persistence
|
||||
|
||||
import at.mocode.masterdata.domain.model.BundeslandDefinition
|
||||
import at.mocode.masterdata.domain.repository.BundeslandRepository
|
||||
import at.mocode.core.utils.database.DatabaseFactory
|
||||
import com.benasher44.uuid.Uuid
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.toInstant
|
||||
import kotlinx.datetime.toLocalDateTime
|
||||
import org.jetbrains.exposed.sql.*
|
||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
||||
|
||||
/**
|
||||
* Implementierung des BundeslandRepository für die Datenbankzugriffe.
|
||||
*
|
||||
* Diese Implementierung verwendet Exposed SQL für den Datenbankzugriff
|
||||
* und mappt zwischen der BundeslandDefinition Domain-Entität und der BundeslandTable.
|
||||
*/
|
||||
class BundeslandRepositoryImpl : BundeslandRepository {
|
||||
|
||||
/**
|
||||
* Konvertiert eine Datenbankzeile in ein Domain-Objekt.
|
||||
*/
|
||||
private fun rowToBundeslandDefinition(row: ResultRow): BundeslandDefinition {
|
||||
return BundeslandDefinition(
|
||||
bundeslandId = row[BundeslandTable.id],
|
||||
landId = row[BundeslandTable.landId],
|
||||
oepsCode = row[BundeslandTable.oepsCode],
|
||||
iso3166_2_Code = row[BundeslandTable.iso3166_2_Code],
|
||||
name = row[BundeslandTable.name],
|
||||
kuerzel = row[BundeslandTable.kuerzel],
|
||||
wappenUrl = row[BundeslandTable.wappenUrl],
|
||||
istAktiv = row[BundeslandTable.istAktiv],
|
||||
sortierReihenfolge = row[BundeslandTable.sortierReihenfolge],
|
||||
createdAt = row[BundeslandTable.createdAt].toInstant(TimeZone.UTC),
|
||||
updatedAt = row[BundeslandTable.updatedAt].toInstant(TimeZone.UTC)
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun findById(id: Uuid): BundeslandDefinition? = DatabaseFactory.dbQuery {
|
||||
BundeslandTable.selectAll().where { BundeslandTable.id eq id }
|
||||
.map(::rowToBundeslandDefinition)
|
||||
.singleOrNull()
|
||||
}
|
||||
|
||||
override suspend fun findByOepsCode(oepsCode: String, landId: Uuid): BundeslandDefinition? = DatabaseFactory.dbQuery {
|
||||
BundeslandTable.selectAll().where {
|
||||
(BundeslandTable.oepsCode eq oepsCode) and (BundeslandTable.landId eq landId)
|
||||
}
|
||||
.map(::rowToBundeslandDefinition)
|
||||
.singleOrNull()
|
||||
}
|
||||
|
||||
override suspend fun findByIso3166_2_Code(iso3166_2_Code: String): BundeslandDefinition? = DatabaseFactory.dbQuery {
|
||||
BundeslandTable.selectAll().where { BundeslandTable.iso3166_2_Code eq iso3166_2_Code }
|
||||
.map(::rowToBundeslandDefinition)
|
||||
.singleOrNull()
|
||||
}
|
||||
|
||||
override suspend fun findByCountry(landId: Uuid, activeOnly: Boolean, orderBySortierung: Boolean): List<BundeslandDefinition> = DatabaseFactory.dbQuery {
|
||||
val query = BundeslandTable.selectAll().where { BundeslandTable.landId eq landId }
|
||||
|
||||
if (activeOnly) {
|
||||
query.andWhere { BundeslandTable.istAktiv eq true }
|
||||
}
|
||||
|
||||
if (orderBySortierung) {
|
||||
query.orderBy(BundeslandTable.sortierReihenfolge to SortOrder.ASC, BundeslandTable.name to SortOrder.ASC)
|
||||
} else {
|
||||
query.orderBy(BundeslandTable.name to SortOrder.ASC)
|
||||
}
|
||||
|
||||
query.map(::rowToBundeslandDefinition)
|
||||
}
|
||||
|
||||
override suspend fun findByName(searchTerm: String, landId: Uuid?, limit: Int): List<BundeslandDefinition> = DatabaseFactory.dbQuery {
|
||||
val pattern = "%$searchTerm%"
|
||||
val query = BundeslandTable.selectAll().where { BundeslandTable.name like pattern }
|
||||
|
||||
landId?.let {
|
||||
query.andWhere { BundeslandTable.landId eq it }
|
||||
}
|
||||
|
||||
query.limit(limit).map(::rowToBundeslandDefinition)
|
||||
}
|
||||
|
||||
override suspend fun findAllActive(orderBySortierung: Boolean): List<BundeslandDefinition> = DatabaseFactory.dbQuery {
|
||||
val query = BundeslandTable.selectAll().where { BundeslandTable.istAktiv eq true }
|
||||
|
||||
if (orderBySortierung) {
|
||||
query.orderBy(BundeslandTable.sortierReihenfolge to SortOrder.ASC, BundeslandTable.name to SortOrder.ASC)
|
||||
} else {
|
||||
query.orderBy(BundeslandTable.name to SortOrder.ASC)
|
||||
}
|
||||
|
||||
query.map(::rowToBundeslandDefinition)
|
||||
}
|
||||
|
||||
override suspend fun save(bundesland: BundeslandDefinition): BundeslandDefinition = DatabaseFactory.dbQuery {
|
||||
val now = Clock.System.now()
|
||||
val existingBundesland = BundeslandTable.selectAll().where { BundeslandTable.id eq bundesland.bundeslandId }.singleOrNull()
|
||||
|
||||
if (existingBundesland == null) {
|
||||
// Insert a new federal state
|
||||
BundeslandTable.insert { stmt ->
|
||||
stmt[id] = bundesland.bundeslandId
|
||||
stmt[landId] = bundesland.landId
|
||||
stmt[oepsCode] = bundesland.oepsCode
|
||||
stmt[iso3166_2_Code] = bundesland.iso3166_2_Code
|
||||
stmt[name] = bundesland.name
|
||||
stmt[kuerzel] = bundesland.kuerzel
|
||||
stmt[wappenUrl] = bundesland.wappenUrl
|
||||
stmt[istAktiv] = bundesland.istAktiv
|
||||
stmt[sortierReihenfolge] = bundesland.sortierReihenfolge
|
||||
stmt[createdAt] = bundesland.createdAt.toLocalDateTime(TimeZone.UTC)
|
||||
stmt[updatedAt] = now.toLocalDateTime(TimeZone.UTC)
|
||||
}
|
||||
} else {
|
||||
// Update existing federal state
|
||||
BundeslandTable.update({ BundeslandTable.id eq bundesland.bundeslandId }) { stmt ->
|
||||
stmt[landId] = bundesland.landId
|
||||
stmt[oepsCode] = bundesland.oepsCode
|
||||
stmt[iso3166_2_Code] = bundesland.iso3166_2_Code
|
||||
stmt[name] = bundesland.name
|
||||
stmt[kuerzel] = bundesland.kuerzel
|
||||
stmt[wappenUrl] = bundesland.wappenUrl
|
||||
stmt[istAktiv] = bundesland.istAktiv
|
||||
stmt[sortierReihenfolge] = bundesland.sortierReihenfolge
|
||||
stmt[updatedAt] = now.toLocalDateTime(TimeZone.UTC)
|
||||
}
|
||||
}
|
||||
|
||||
bundesland.copy(updatedAt = now)
|
||||
}
|
||||
|
||||
override suspend fun delete(id: Uuid): Boolean = DatabaseFactory.dbQuery {
|
||||
BundeslandTable.deleteWhere { BundeslandTable.id eq id } > 0
|
||||
}
|
||||
|
||||
override suspend fun existsByOepsCode(oepsCode: String, landId: Uuid): Boolean = DatabaseFactory.dbQuery {
|
||||
BundeslandTable.selectAll().where {
|
||||
(BundeslandTable.oepsCode eq oepsCode) and (BundeslandTable.landId eq landId)
|
||||
}.count() > 0
|
||||
}
|
||||
|
||||
override suspend fun existsByIso3166_2_Code(iso3166_2_Code: String): Boolean = DatabaseFactory.dbQuery {
|
||||
BundeslandTable.selectAll().where { BundeslandTable.iso3166_2_Code eq iso3166_2_Code }
|
||||
.count() > 0
|
||||
}
|
||||
|
||||
override suspend fun countActiveByCountry(landId: Uuid): Long = DatabaseFactory.dbQuery {
|
||||
BundeslandTable.selectAll().where {
|
||||
(BundeslandTable.landId eq landId) and (BundeslandTable.istAktiv eq true)
|
||||
}.count()
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
package at.mocode.masterdata.infrastructure.persistence
|
||||
|
||||
import org.jetbrains.exposed.sql.Table
|
||||
import org.jetbrains.exposed.sql.kotlin.datetime.datetime
|
||||
import org.jetbrains.exposed.sql.kotlin.datetime.CurrentDateTime
|
||||
|
||||
/**
|
||||
* Exposed-Tabellendefinition für die Bundesland-Entität (Bundesländer/Regionen).
|
||||
*
|
||||
* Diese Tabelle speichert alle Informationen zu Bundesländern und subnationalen
|
||||
* Verwaltungseinheiten entsprechend der BundeslandDefinition Domain-Entität.
|
||||
*/
|
||||
object BundeslandTable : Table("bundesland") {
|
||||
val id = uuid("id").autoGenerate()
|
||||
val landId = uuid("land_id").references(LandTable.id)
|
||||
val oepsCode = varchar("oeps_code", 10).nullable()
|
||||
val iso3166_2_Code = varchar("iso_3166_2_code", 10).nullable()
|
||||
val name = varchar("name", 100)
|
||||
val kuerzel = varchar("kuerzel", 10).nullable()
|
||||
val wappenUrl = varchar("wappen_url", 500).nullable()
|
||||
val istAktiv = bool("ist_aktiv").default(true)
|
||||
val sortierReihenfolge = integer("sortier_reihenfolge").nullable()
|
||||
val createdAt = datetime("created_at").defaultExpression(CurrentDateTime)
|
||||
val updatedAt = datetime("updated_at").defaultExpression(CurrentDateTime)
|
||||
|
||||
override val primaryKey = PrimaryKey(id)
|
||||
|
||||
init {
|
||||
// Unique constraint for OEPS code per country
|
||||
uniqueIndex("uk_bundesland_oeps_land", oepsCode, landId)
|
||||
// Unique constraint for ISO 3166-2 code globally
|
||||
uniqueIndex("uk_bundesland_iso3166_2", iso3166_2_Code)
|
||||
}
|
||||
}
|
||||
+153
@@ -0,0 +1,153 @@
|
||||
package at.mocode.masterdata.infrastructure.persistence
|
||||
|
||||
import at.mocode.core.utils.database.DatabaseFactory
|
||||
import at.mocode.masterdata.domain.model.LandDefinition
|
||||
import at.mocode.masterdata.domain.repository.LandRepository
|
||||
import com.benasher44.uuid.Uuid
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.toInstant
|
||||
import kotlinx.datetime.toLocalDateTime
|
||||
import org.jetbrains.exposed.sql.*
|
||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
||||
|
||||
/**
|
||||
* Implementierung des LandRepository für die Datenbankzugriffe.
|
||||
*
|
||||
* Diese Implementierung verwendet Exposed SQL für den Datenbankzugriff
|
||||
* und mappt zwischen der LandDefinition Domain-Entität und der LandTable.
|
||||
*/
|
||||
class LandRepositoryImpl : LandRepository {
|
||||
|
||||
/**
|
||||
* Konvertiert eine Datenbankzeile in ein Domain-Objekt.
|
||||
*/
|
||||
private fun rowToLandDefinition(row: ResultRow): LandDefinition {
|
||||
return LandDefinition(
|
||||
landId = row[LandTable.id],
|
||||
isoAlpha2Code = row[LandTable.isoAlpha2Code],
|
||||
isoAlpha3Code = row[LandTable.isoAlpha3Code],
|
||||
isoNumerischerCode = row[LandTable.isoNumerischerCode],
|
||||
nameDeutsch = row[LandTable.nameDeutsch],
|
||||
nameEnglisch = row[LandTable.nameEnglisch],
|
||||
wappenUrl = row[LandTable.wappenUrl],
|
||||
istEuMitglied = row[LandTable.istEuMitglied],
|
||||
istEwrMitglied = row[LandTable.istEwrMitglied],
|
||||
istAktiv = row[LandTable.istAktiv],
|
||||
sortierReihenfolge = row[LandTable.sortierReihenfolge],
|
||||
createdAt = row[LandTable.createdAt].toInstant(TimeZone.UTC),
|
||||
updatedAt = row[LandTable.updatedAt].toInstant(TimeZone.UTC)
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun findById(id: Uuid): LandDefinition? = DatabaseFactory.dbQuery {
|
||||
LandTable.selectAll().where { LandTable.id eq id }
|
||||
.map(::rowToLandDefinition)
|
||||
.singleOrNull()
|
||||
}
|
||||
|
||||
override suspend fun findByIsoAlpha2Code(isoAlpha2Code: String): LandDefinition? = DatabaseFactory.dbQuery {
|
||||
LandTable.selectAll().where { LandTable.isoAlpha2Code eq isoAlpha2Code }
|
||||
.map(::rowToLandDefinition)
|
||||
.singleOrNull()
|
||||
}
|
||||
|
||||
override suspend fun findByIsoAlpha3Code(isoAlpha3Code: String): LandDefinition? = DatabaseFactory.dbQuery {
|
||||
LandTable.selectAll().where { LandTable.isoAlpha3Code eq isoAlpha3Code }
|
||||
.map(::rowToLandDefinition)
|
||||
.singleOrNull()
|
||||
}
|
||||
|
||||
override suspend fun findByName(searchTerm: String, limit: Int): List<LandDefinition> = DatabaseFactory.dbQuery {
|
||||
val pattern = "%$searchTerm%"
|
||||
LandTable.selectAll().where {
|
||||
(LandTable.nameDeutsch like pattern) or
|
||||
(LandTable.nameEnglisch like pattern)
|
||||
}
|
||||
.limit(limit)
|
||||
.map(::rowToLandDefinition)
|
||||
}
|
||||
|
||||
override suspend fun findAllActive(orderBySortierung: Boolean): List<LandDefinition> = DatabaseFactory.dbQuery {
|
||||
val query = LandTable.selectAll().where { LandTable.istAktiv eq true }
|
||||
|
||||
if (orderBySortierung) {
|
||||
query.orderBy(LandTable.sortierReihenfolge to SortOrder.ASC, LandTable.nameDeutsch to SortOrder.ASC)
|
||||
} else {
|
||||
query.orderBy(LandTable.nameDeutsch to SortOrder.ASC)
|
||||
}
|
||||
|
||||
query.map(::rowToLandDefinition)
|
||||
}
|
||||
|
||||
override suspend fun findEuMembers(): List<LandDefinition> = DatabaseFactory.dbQuery {
|
||||
LandTable.selectAll().where { (LandTable.istEuMitglied eq true) and (LandTable.istAktiv eq true) }
|
||||
.orderBy(LandTable.sortierReihenfolge to SortOrder.ASC, LandTable.nameDeutsch to SortOrder.ASC)
|
||||
.map(::rowToLandDefinition)
|
||||
}
|
||||
|
||||
override suspend fun findEwrMembers(): List<LandDefinition> = DatabaseFactory.dbQuery {
|
||||
LandTable.selectAll().where { (LandTable.istEwrMitglied eq true) and (LandTable.istAktiv eq true) }
|
||||
.orderBy(LandTable.sortierReihenfolge to SortOrder.ASC, LandTable.nameDeutsch to SortOrder.ASC)
|
||||
.map(::rowToLandDefinition)
|
||||
}
|
||||
|
||||
override suspend fun save(land: LandDefinition): LandDefinition = DatabaseFactory.dbQuery {
|
||||
val now = Clock.System.now()
|
||||
val existingLand = LandTable.selectAll().where { LandTable.id eq land.landId }.singleOrNull()
|
||||
|
||||
if (existingLand == null) {
|
||||
// Insert a new country
|
||||
LandTable.insert { stmt ->
|
||||
stmt[id] = land.landId
|
||||
stmt[isoAlpha2Code] = land.isoAlpha2Code
|
||||
stmt[isoAlpha3Code] = land.isoAlpha3Code
|
||||
stmt[isoNumerischerCode] = land.isoNumerischerCode
|
||||
stmt[nameDeutsch] = land.nameDeutsch
|
||||
stmt[nameEnglisch] = land.nameEnglisch
|
||||
stmt[wappenUrl] = land.wappenUrl
|
||||
stmt[istEuMitglied] = land.istEuMitglied
|
||||
stmt[istEwrMitglied] = land.istEwrMitglied
|
||||
stmt[istAktiv] = land.istAktiv
|
||||
stmt[sortierReihenfolge] = land.sortierReihenfolge
|
||||
stmt[createdAt] = land.createdAt.toLocalDateTime(TimeZone.UTC)
|
||||
stmt[updatedAt] = now.toLocalDateTime(TimeZone.UTC)
|
||||
}
|
||||
} else {
|
||||
// Update existing country
|
||||
LandTable.update({ LandTable.id eq land.landId }) { stmt ->
|
||||
stmt[isoAlpha2Code] = land.isoAlpha2Code
|
||||
stmt[isoAlpha3Code] = land.isoAlpha3Code
|
||||
stmt[isoNumerischerCode] = land.isoNumerischerCode
|
||||
stmt[nameDeutsch] = land.nameDeutsch
|
||||
stmt[nameEnglisch] = land.nameEnglisch
|
||||
stmt[wappenUrl] = land.wappenUrl
|
||||
stmt[istEuMitglied] = land.istEuMitglied
|
||||
stmt[istEwrMitglied] = land.istEwrMitglied
|
||||
stmt[istAktiv] = land.istAktiv
|
||||
stmt[sortierReihenfolge] = land.sortierReihenfolge
|
||||
stmt[updatedAt] = now.toLocalDateTime(TimeZone.UTC)
|
||||
}
|
||||
}
|
||||
|
||||
land.copy(updatedAt = now)
|
||||
}
|
||||
|
||||
override suspend fun delete(id: Uuid): Boolean = DatabaseFactory.dbQuery {
|
||||
LandTable.deleteWhere { LandTable.id eq id } > 0
|
||||
}
|
||||
|
||||
override suspend fun existsByIsoAlpha2Code(isoAlpha2Code: String): Boolean = DatabaseFactory.dbQuery {
|
||||
LandTable.selectAll().where { LandTable.isoAlpha2Code eq isoAlpha2Code }
|
||||
.count() > 0
|
||||
}
|
||||
|
||||
override suspend fun existsByIsoAlpha3Code(isoAlpha3Code: String): Boolean = DatabaseFactory.dbQuery {
|
||||
LandTable.selectAll().where { LandTable.isoAlpha3Code eq isoAlpha3Code }
|
||||
.count() > 0
|
||||
}
|
||||
|
||||
override suspend fun countActive(): Long = DatabaseFactory.dbQuery {
|
||||
LandTable.selectAll().where { LandTable.istAktiv eq true }.count()
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
package at.mocode.masterdata.infrastructure.persistence
|
||||
|
||||
import org.jetbrains.exposed.sql.Table
|
||||
import org.jetbrains.exposed.sql.kotlin.datetime.datetime
|
||||
import org.jetbrains.exposed.sql.kotlin.datetime.CurrentDateTime
|
||||
|
||||
/**
|
||||
* Exposed-Tabellendefinition für die Land-Entität (Länderstammdaten).
|
||||
*
|
||||
* Diese Tabelle speichert alle Informationen zu Ländern/Nationen entsprechend
|
||||
* der LandDefinition Domain-Entität.
|
||||
*/
|
||||
object LandTable : Table("land") {
|
||||
val id = uuid("id").autoGenerate()
|
||||
val isoAlpha2Code = varchar("iso_alpha2_code", 2).uniqueIndex()
|
||||
val isoAlpha3Code = varchar("iso_alpha3_code", 3).uniqueIndex()
|
||||
val isoNumerischerCode = varchar("iso_numerischer_code", 3).nullable()
|
||||
val nameDeutsch = varchar("name_deutsch", 100)
|
||||
val nameEnglisch = varchar("name_englisch", 100).nullable()
|
||||
val wappenUrl = varchar("wappen_url", 500).nullable()
|
||||
val istEuMitglied = bool("ist_eu_mitglied").nullable()
|
||||
val istEwrMitglied = bool("ist_ewr_mitglied").nullable()
|
||||
val istAktiv = bool("ist_aktiv").default(true)
|
||||
val sortierReihenfolge = integer("sortier_reihenfolge").nullable()
|
||||
val createdAt = datetime("created_at").defaultExpression(CurrentDateTime)
|
||||
val updatedAt = datetime("updated_at").defaultExpression(CurrentDateTime)
|
||||
|
||||
override val primaryKey = PrimaryKey(id)
|
||||
}
|
||||
+230
@@ -0,0 +1,230 @@
|
||||
package at.mocode.masterdata.infrastructure.persistence
|
||||
|
||||
import at.mocode.core.domain.model.PlatzTypE
|
||||
import at.mocode.masterdata.domain.model.Platz
|
||||
import at.mocode.masterdata.domain.repository.PlatzRepository
|
||||
import at.mocode.core.utils.database.DatabaseFactory
|
||||
import com.benasher44.uuid.Uuid
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.toInstant
|
||||
import kotlinx.datetime.toLocalDateTime
|
||||
import org.jetbrains.exposed.sql.*
|
||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
||||
|
||||
/**
|
||||
* Implementierung des PlatzRepository für die Datenbankzugriffe.
|
||||
*
|
||||
* Diese Implementierung verwendet Exposed SQL für den Datenbankzugriff
|
||||
* und mappt zwischen der Platz Domain-Entität und der PlatzTable.
|
||||
*/
|
||||
class PlatzRepositoryImpl : PlatzRepository {
|
||||
|
||||
/**
|
||||
* Konvertiert eine Datenbankzeile in ein Domain-Objekt.
|
||||
*/
|
||||
private fun rowToPlatz(row: ResultRow): Platz {
|
||||
return Platz(
|
||||
id = row[PlatzTable.id],
|
||||
turnierId = row[PlatzTable.turnierId],
|
||||
name = row[PlatzTable.name],
|
||||
dimension = row[PlatzTable.dimension],
|
||||
boden = row[PlatzTable.boden],
|
||||
typ = PlatzTypE.valueOf(row[PlatzTable.typ]),
|
||||
istAktiv = row[PlatzTable.istAktiv],
|
||||
sortierReihenfolge = row[PlatzTable.sortierReihenfolge],
|
||||
createdAt = row[PlatzTable.createdAt].toInstant(TimeZone.UTC),
|
||||
updatedAt = row[PlatzTable.updatedAt].toInstant(TimeZone.UTC)
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun findById(id: Uuid): Platz? = DatabaseFactory.dbQuery {
|
||||
PlatzTable.selectAll().where { PlatzTable.id eq id }
|
||||
.map(::rowToPlatz)
|
||||
.singleOrNull()
|
||||
}
|
||||
|
||||
override suspend fun findByTournament(turnierId: Uuid, activeOnly: Boolean, orderBySortierung: Boolean): List<Platz> = DatabaseFactory.dbQuery {
|
||||
val query = PlatzTable.selectAll().where { PlatzTable.turnierId eq turnierId }
|
||||
|
||||
if (activeOnly) {
|
||||
query.andWhere { PlatzTable.istAktiv eq true }
|
||||
}
|
||||
|
||||
if (orderBySortierung) {
|
||||
query.orderBy(PlatzTable.sortierReihenfolge to SortOrder.ASC, PlatzTable.name to SortOrder.ASC)
|
||||
} else {
|
||||
query.orderBy(PlatzTable.name to SortOrder.ASC)
|
||||
}
|
||||
|
||||
query.map(::rowToPlatz)
|
||||
}
|
||||
|
||||
override suspend fun findByName(searchTerm: String, turnierId: Uuid?, limit: Int): List<Platz> = DatabaseFactory.dbQuery {
|
||||
val pattern = "%$searchTerm%"
|
||||
val query = PlatzTable.selectAll().where { PlatzTable.name like pattern }
|
||||
|
||||
turnierId?.let {
|
||||
query.andWhere { PlatzTable.turnierId eq it }
|
||||
}
|
||||
|
||||
query.limit(limit)
|
||||
.orderBy(PlatzTable.name to SortOrder.ASC)
|
||||
.map(::rowToPlatz)
|
||||
}
|
||||
|
||||
override suspend fun findByType(typ: PlatzTypE, turnierId: Uuid?, activeOnly: Boolean): List<Platz> = DatabaseFactory.dbQuery {
|
||||
val query = PlatzTable.selectAll().where { PlatzTable.typ eq typ.name }
|
||||
|
||||
turnierId?.let {
|
||||
query.andWhere { PlatzTable.turnierId eq it }
|
||||
}
|
||||
|
||||
if (activeOnly) {
|
||||
query.andWhere { PlatzTable.istAktiv eq true }
|
||||
}
|
||||
|
||||
query.orderBy(PlatzTable.name to SortOrder.ASC)
|
||||
.map(::rowToPlatz)
|
||||
}
|
||||
|
||||
override suspend fun findByGroundType(boden: String, turnierId: Uuid?, activeOnly: Boolean): List<Platz> = DatabaseFactory.dbQuery {
|
||||
val query = PlatzTable.selectAll().where { PlatzTable.boden eq boden }
|
||||
|
||||
turnierId?.let {
|
||||
query.andWhere { PlatzTable.turnierId eq it }
|
||||
}
|
||||
|
||||
if (activeOnly) {
|
||||
query.andWhere { PlatzTable.istAktiv eq true }
|
||||
}
|
||||
|
||||
query.orderBy(PlatzTable.name to SortOrder.ASC)
|
||||
.map(::rowToPlatz)
|
||||
}
|
||||
|
||||
override suspend fun findByDimensions(dimension: String, turnierId: Uuid?, activeOnly: Boolean): List<Platz> = DatabaseFactory.dbQuery {
|
||||
val query = PlatzTable.selectAll().where { PlatzTable.dimension eq dimension }
|
||||
|
||||
turnierId?.let {
|
||||
query.andWhere { PlatzTable.turnierId eq it }
|
||||
}
|
||||
|
||||
if (activeOnly) {
|
||||
query.andWhere { PlatzTable.istAktiv eq true }
|
||||
}
|
||||
|
||||
query.orderBy(PlatzTable.name to SortOrder.ASC)
|
||||
.map(::rowToPlatz)
|
||||
}
|
||||
|
||||
override suspend fun findAllActive(orderBySortierung: Boolean): List<Platz> = DatabaseFactory.dbQuery {
|
||||
val query = PlatzTable.selectAll().where { PlatzTable.istAktiv eq true }
|
||||
|
||||
if (orderBySortierung) {
|
||||
query.orderBy(PlatzTable.sortierReihenfolge to SortOrder.ASC, PlatzTable.name to SortOrder.ASC)
|
||||
} else {
|
||||
query.orderBy(PlatzTable.name to SortOrder.ASC)
|
||||
}
|
||||
|
||||
query.map(::rowToPlatz)
|
||||
}
|
||||
|
||||
override suspend fun findSuitableForDiscipline(
|
||||
requiredType: PlatzTypE,
|
||||
requiredDimensions: String?,
|
||||
turnierId: Uuid?
|
||||
): List<Platz> = DatabaseFactory.dbQuery {
|
||||
val query = PlatzTable.selectAll().where {
|
||||
(PlatzTable.typ eq requiredType.name) and (PlatzTable.istAktiv eq true)
|
||||
}
|
||||
|
||||
requiredDimensions?.let { dimensions ->
|
||||
query.andWhere { PlatzTable.dimension eq dimensions }
|
||||
}
|
||||
|
||||
turnierId?.let {
|
||||
query.andWhere { PlatzTable.turnierId eq it }
|
||||
}
|
||||
|
||||
query.orderBy(PlatzTable.sortierReihenfolge to SortOrder.ASC, PlatzTable.name to SortOrder.ASC)
|
||||
.map(::rowToPlatz)
|
||||
}
|
||||
|
||||
override suspend fun save(platz: Platz): Platz = DatabaseFactory.dbQuery {
|
||||
val now = Clock.System.now()
|
||||
val existingPlatz = PlatzTable.selectAll().where { PlatzTable.id eq platz.id }.singleOrNull()
|
||||
|
||||
if (existingPlatz == null) {
|
||||
// Insert a new venue
|
||||
PlatzTable.insert { stmt ->
|
||||
stmt[id] = platz.id
|
||||
stmt[turnierId] = platz.turnierId
|
||||
stmt[name] = platz.name
|
||||
stmt[dimension] = platz.dimension
|
||||
stmt[boden] = platz.boden
|
||||
stmt[typ] = platz.typ.name
|
||||
stmt[istAktiv] = platz.istAktiv
|
||||
stmt[sortierReihenfolge] = platz.sortierReihenfolge
|
||||
stmt[createdAt] = platz.createdAt.toLocalDateTime(TimeZone.UTC)
|
||||
stmt[updatedAt] = now.toLocalDateTime(TimeZone.UTC)
|
||||
}
|
||||
} else {
|
||||
// Update existing venue
|
||||
PlatzTable.update({ PlatzTable.id eq platz.id }) { stmt ->
|
||||
stmt[turnierId] = platz.turnierId
|
||||
stmt[name] = platz.name
|
||||
stmt[dimension] = platz.dimension
|
||||
stmt[boden] = platz.boden
|
||||
stmt[typ] = platz.typ.name
|
||||
stmt[istAktiv] = platz.istAktiv
|
||||
stmt[sortierReihenfolge] = platz.sortierReihenfolge
|
||||
stmt[updatedAt] = now.toLocalDateTime(TimeZone.UTC)
|
||||
}
|
||||
}
|
||||
|
||||
platz.copy(updatedAt = now)
|
||||
}
|
||||
|
||||
override suspend fun delete(id: Uuid): Boolean = DatabaseFactory.dbQuery {
|
||||
PlatzTable.deleteWhere { PlatzTable.id eq id } > 0
|
||||
}
|
||||
|
||||
override suspend fun existsByNameAndTournament(name: String, turnierId: Uuid): Boolean = DatabaseFactory.dbQuery {
|
||||
PlatzTable.selectAll().where {
|
||||
(PlatzTable.name eq name) and (PlatzTable.turnierId eq turnierId)
|
||||
}.count() > 0
|
||||
}
|
||||
|
||||
override suspend fun countActiveByTournament(turnierId: Uuid): Long = DatabaseFactory.dbQuery {
|
||||
PlatzTable.selectAll().where {
|
||||
(PlatzTable.turnierId eq turnierId) and (PlatzTable.istAktiv eq true)
|
||||
}.count()
|
||||
}
|
||||
|
||||
override suspend fun countByTypeAndTournament(typ: PlatzTypE, turnierId: Uuid, activeOnly: Boolean): Long = DatabaseFactory.dbQuery {
|
||||
val query = PlatzTable.selectAll().where {
|
||||
(PlatzTable.typ eq typ.name) and (PlatzTable.turnierId eq turnierId)
|
||||
}
|
||||
|
||||
if (activeOnly) {
|
||||
query.andWhere { PlatzTable.istAktiv eq true }
|
||||
}
|
||||
|
||||
query.count()
|
||||
}
|
||||
|
||||
override suspend fun findAvailableForTimeSlot(turnierId: Uuid, startTime: String?, endTime: String?): List<Platz> = DatabaseFactory.dbQuery {
|
||||
// For now, this returns all active venues for the tournament
|
||||
// This can be extended when venue scheduling functionality is implemented
|
||||
val query = PlatzTable.selectAll().where {
|
||||
(PlatzTable.turnierId eq turnierId) and (PlatzTable.istAktiv eq true)
|
||||
}
|
||||
|
||||
// TODO: Add time slot availability logic when scheduling is implemented
|
||||
// This would involve joining with a scheduling/booking table to check availability
|
||||
|
||||
query.orderBy(PlatzTable.sortierReihenfolge to SortOrder.ASC, PlatzTable.name to SortOrder.ASC)
|
||||
.map(::rowToPlatz)
|
||||
}
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
package at.mocode.masterdata.infrastructure.persistence
|
||||
|
||||
import org.jetbrains.exposed.sql.Table
|
||||
import org.jetbrains.exposed.sql.kotlin.datetime.datetime
|
||||
import org.jetbrains.exposed.sql.kotlin.datetime.CurrentDateTime
|
||||
|
||||
/**
|
||||
* Exposed-Tabellendefinition für die Platz-Entität (Turnierplätze/Wettkampfstätten).
|
||||
*
|
||||
* Diese Tabelle speichert alle Informationen zu Plätzen und Arenen
|
||||
* entsprechend der Platz Domain-Entität.
|
||||
*/
|
||||
object PlatzTable : Table("platz") {
|
||||
val id = uuid("id").autoGenerate()
|
||||
val turnierId = uuid("turnier_id") // Foreign key to tournament (not enforced here as tournament might be in different module)
|
||||
val name = varchar("name", 200)
|
||||
val dimension = varchar("dimension", 50).nullable()
|
||||
val boden = varchar("boden", 100).nullable()
|
||||
val typ = varchar("typ", 50) // Enum as string
|
||||
val istAktiv = bool("ist_aktiv").default(true)
|
||||
val sortierReihenfolge = integer("sortier_reihenfolge").nullable()
|
||||
val createdAt = datetime("created_at").defaultExpression(CurrentDateTime)
|
||||
val updatedAt = datetime("updated_at").defaultExpression(CurrentDateTime)
|
||||
|
||||
override val primaryKey = PrimaryKey(id)
|
||||
|
||||
init {
|
||||
// Index for performance on common queries
|
||||
index(customIndexName = "idx_platz_turnier", columns = arrayOf(turnierId))
|
||||
index(customIndexName = "idx_platz_aktiv", columns = arrayOf(istAktiv))
|
||||
index(customIndexName = "idx_platz_typ", columns = arrayOf(typ))
|
||||
index(customIndexName = "idx_platz_turnier_aktiv", columns = arrayOf(turnierId, istAktiv))
|
||||
|
||||
// Unique constraint for name per tournament
|
||||
uniqueIndex("uk_platz_name_turnier", name, turnierId)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
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.masterdata.service.MasterdataServiceApplicationKt")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// Interne Module
|
||||
implementation(projects.platform.platformDependencies)
|
||||
implementation(projects.core.coreUtils)
|
||||
implementation(projects.masterdata.masterdataDomain)
|
||||
implementation(projects.masterdata.masterdataApplication)
|
||||
implementation(projects.masterdata.masterdataInfrastructure)
|
||||
implementation(projects.masterdata.masterdataApi)
|
||||
|
||||
// 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)
|
||||
//implementation(libs.springdoc.openapi.starter.webmvc.ui)
|
||||
|
||||
// Datenbank-Abhängigkeiten
|
||||
implementation(libs.exposed.core)
|
||||
implementation(libs.exposed.dao)
|
||||
implementation(libs.exposed.jdbc)
|
||||
implementation(libs.exposed.kotlin.datetime)
|
||||
implementation(libs.hikari.cp)
|
||||
runtimeOnly(libs.postgresql.driver)
|
||||
testRuntimeOnly(libs.h2.driver)
|
||||
|
||||
// Testing
|
||||
testImplementation(projects.platform.platformTesting)
|
||||
testImplementation(libs.spring.boot.starter.test)
|
||||
testImplementation(libs.logback.classic) // SLF4J provider for tests
|
||||
}
|
||||
|
||||
tasks.test {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
package at.mocode.masterdata.service
|
||||
|
||||
import at.mocode.core.utils.config.AppConfig
|
||||
import at.mocode.core.utils.database.DatabaseFactory
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication
|
||||
import org.springframework.boot.runApplication
|
||||
|
||||
/**
|
||||
* Main application class for the Masterdata Service.
|
||||
*
|
||||
* This service provides APIs for managing master data such as countries, regions, and other reference data.
|
||||
*/
|
||||
@SpringBootApplication
|
||||
class MasterdataServiceApplication
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
// 1. Lade die Konfiguration explizit, genau einmal beim Start.
|
||||
val appConfig = AppConfig.load()
|
||||
println("Konfiguration für Umgebung '${appConfig.environment}' geladen.")
|
||||
|
||||
// 2. Initialisiere die Datenbank mit der geladenen Konfiguration.
|
||||
// Flyway-Migrationen werden hier automatisch ausgeführt.
|
||||
DatabaseFactory.init(appConfig.database)
|
||||
println("Datenbank initialisiert und migriert.")
|
||||
|
||||
// 3. Starte die Spring Boot / Ktor Anwendung.
|
||||
// Der appConfig-Wert kann hier an die Anwendung übergeben werden,
|
||||
// um ihn später per Dependency Injection zu nutzen.
|
||||
runApplication<MasterdataServiceApplication>(*args)
|
||||
}
|
||||
+154
@@ -0,0 +1,154 @@
|
||||
package at.mocode.masterdata.service.config
|
||||
|
||||
import at.mocode.masterdata.application.usecase.*
|
||||
import at.mocode.masterdata.domain.repository.*
|
||||
import at.mocode.masterdata.infrastructure.persistence.*
|
||||
import at.mocode.masterdata.api.rest.*
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.context.annotation.Profile
|
||||
|
||||
/**
|
||||
* Spring Boot configuration for the Masterdata Service.
|
||||
*
|
||||
* This configuration class sets up all the necessary beans for dependency injection
|
||||
* following the clean architecture pattern with proper separation of concerns.
|
||||
*/
|
||||
@Configuration
|
||||
class MasterdataConfiguration {
|
||||
|
||||
// Repository Implementations
|
||||
@Bean
|
||||
fun landRepository(): LandRepository {
|
||||
return LandRepositoryImpl()
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun bundeslandRepository(): BundeslandRepository {
|
||||
return BundeslandRepositoryImpl()
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun altersklasseRepository(): AltersklasseRepository {
|
||||
return AltersklasseRepositoryImpl()
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun platzRepository(): PlatzRepository {
|
||||
return PlatzRepositoryImpl()
|
||||
}
|
||||
|
||||
// Use Cases - Country/Land
|
||||
@Bean
|
||||
fun getCountryUseCase(landRepository: LandRepository): GetCountryUseCase {
|
||||
return GetCountryUseCase(landRepository)
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun createCountryUseCase(landRepository: LandRepository): CreateCountryUseCase {
|
||||
return CreateCountryUseCase(landRepository)
|
||||
}
|
||||
|
||||
// Use Cases - Federal State/Bundesland
|
||||
@Bean
|
||||
fun getBundeslandUseCase(bundeslandRepository: BundeslandRepository): GetBundeslandUseCase {
|
||||
return GetBundeslandUseCase(bundeslandRepository)
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun createBundeslandUseCase(bundeslandRepository: BundeslandRepository): CreateBundeslandUseCase {
|
||||
return CreateBundeslandUseCase(bundeslandRepository)
|
||||
}
|
||||
|
||||
// Use Cases - Age Class/Altersklasse
|
||||
@Bean
|
||||
fun getAltersklasseUseCase(altersklasseRepository: AltersklasseRepository): GetAltersklasseUseCase {
|
||||
return GetAltersklasseUseCase(altersklasseRepository)
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun createAltersklasseUseCase(altersklasseRepository: AltersklasseRepository): CreateAltersklasseUseCase {
|
||||
return CreateAltersklasseUseCase(altersklasseRepository)
|
||||
}
|
||||
|
||||
// Use Cases - Venue/Platz
|
||||
@Bean
|
||||
fun getPlatzUseCase(platzRepository: PlatzRepository): GetPlatzUseCase {
|
||||
return GetPlatzUseCase(platzRepository)
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun createPlatzUseCase(platzRepository: PlatzRepository): CreatePlatzUseCase {
|
||||
return CreatePlatzUseCase(platzRepository)
|
||||
}
|
||||
|
||||
// API Controllers
|
||||
@Bean
|
||||
fun countryController(
|
||||
getCountryUseCase: GetCountryUseCase,
|
||||
createCountryUseCase: CreateCountryUseCase
|
||||
): CountryController {
|
||||
return CountryController(getCountryUseCase, createCountryUseCase)
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun bundeslandController(
|
||||
getBundeslandUseCase: GetBundeslandUseCase,
|
||||
createBundeslandUseCase: CreateBundeslandUseCase
|
||||
): BundeslandController {
|
||||
return BundeslandController(getBundeslandUseCase, createBundeslandUseCase)
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun altersklasseController(
|
||||
getAltersklasseUseCase: GetAltersklasseUseCase,
|
||||
createAltersklasseUseCase: CreateAltersklasseUseCase
|
||||
): AltersklasseController {
|
||||
return AltersklasseController(getAltersklasseUseCase, createAltersklasseUseCase)
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun platzController(
|
||||
getPlatzUseCase: GetPlatzUseCase,
|
||||
createPlatzUseCase: CreatePlatzUseCase
|
||||
): PlatzController {
|
||||
return PlatzController(getPlatzUseCase, createPlatzUseCase)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Database configuration for different environments.
|
||||
*/
|
||||
@Configuration
|
||||
class DatabaseConfiguration {
|
||||
|
||||
/**
|
||||
* Development database configuration.
|
||||
*/
|
||||
@Configuration
|
||||
@Profile("dev", "development")
|
||||
class DevelopmentDatabaseConfig {
|
||||
// Development-specific database configuration
|
||||
// This would typically include H2 or local PostgreSQL setup
|
||||
}
|
||||
|
||||
/**
|
||||
* Production database configuration.
|
||||
*/
|
||||
@Configuration
|
||||
@Profile("prod", "production")
|
||||
class ProductionDatabaseConfig {
|
||||
// Production-specific database configuration
|
||||
// This would include production PostgreSQL setup with connection pooling
|
||||
}
|
||||
|
||||
/**
|
||||
* Test database configuration.
|
||||
*/
|
||||
@Configuration
|
||||
@Profile("test")
|
||||
class TestDatabaseConfig {
|
||||
// Test-specific database configuration
|
||||
// This would typically include in-memory H2 database
|
||||
}
|
||||
}
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
package at.mocode.masterdata.service.config
|
||||
|
||||
import at.mocode.core.utils.database.DatabaseConfig
|
||||
import at.mocode.core.utils.database.DatabaseFactory
|
||||
import at.mocode.masterdata.infrastructure.persistence.LandTable
|
||||
import at.mocode.masterdata.infrastructure.persistence.BundeslandTable
|
||||
import at.mocode.masterdata.infrastructure.persistence.AltersklasseTable
|
||||
import at.mocode.masterdata.infrastructure.persistence.PlatzTable
|
||||
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 Masterdata Service.
|
||||
*
|
||||
* This configuration ensures that Database.connect() is called properly
|
||||
* before any Exposed operations are performed.
|
||||
*/
|
||||
@Configuration
|
||||
@Profile("!test")
|
||||
class MasterdataDatabaseConfiguration {
|
||||
|
||||
private val log = LoggerFactory.getLogger(MasterdataDatabaseConfiguration::class.java)
|
||||
|
||||
@PostConstruct
|
||||
fun initializeDatabase() {
|
||||
log.info("Initializing database schema for Masterdata Service...")
|
||||
|
||||
try {
|
||||
// Database connection is already initialized by the gateway
|
||||
// Only initialize the schema for this service
|
||||
transaction {
|
||||
SchemaUtils.createMissingTablesAndColumns(
|
||||
LandTable,
|
||||
BundeslandTable,
|
||||
AltersklasseTable,
|
||||
PlatzTable
|
||||
)
|
||||
log.info("Masterdata 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 Masterdata 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 MasterdataTestDatabaseConfiguration {
|
||||
|
||||
private val log = LoggerFactory.getLogger(MasterdataTestDatabaseConfiguration::class.java)
|
||||
|
||||
@PostConstruct
|
||||
fun initializeTestDatabase() {
|
||||
log.info("Initializing test database connection for Masterdata Service...")
|
||||
|
||||
try {
|
||||
// Use H2 in-memory database for tests
|
||||
val testConfig = DatabaseConfig(
|
||||
jdbcUrl = "jdbc:h2:mem:masterdata_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(
|
||||
LandTable,
|
||||
BundeslandTable,
|
||||
AltersklasseTable,
|
||||
PlatzTable
|
||||
)
|
||||
log.info("Test masterdata 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 Masterdata Service...")
|
||||
try {
|
||||
DatabaseFactory.close()
|
||||
log.info("Test database connection closed successfully")
|
||||
} catch (e: Exception) {
|
||||
log.error("Error closing test database connection", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
-- Migration V001: Create Land (Country) table
|
||||
-- This migration creates the base table for country master data
|
||||
|
||||
CREATE TABLE IF NOT EXISTS land (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
iso_alpha2_code VARCHAR(2) NOT NULL,
|
||||
iso_alpha3_code VARCHAR(3) NOT NULL,
|
||||
iso_numerischer_code VARCHAR(3),
|
||||
name_deutsch VARCHAR(100) NOT NULL,
|
||||
name_englisch VARCHAR(100),
|
||||
wappen_url VARCHAR(500),
|
||||
ist_eu_mitglied BOOLEAN,
|
||||
ist_ewr_mitglied BOOLEAN,
|
||||
ist_aktiv BOOLEAN NOT NULL DEFAULT true,
|
||||
sortier_reihenfolge INTEGER,
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Create unique indexes for ISO codes
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uk_land_iso_alpha2 ON land(iso_alpha2_code);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uk_land_iso_alpha3 ON land(iso_alpha3_code);
|
||||
|
||||
-- Create indexes for performance
|
||||
CREATE INDEX IF NOT EXISTS idx_land_aktiv ON land(ist_aktiv);
|
||||
CREATE INDEX IF NOT EXISTS idx_land_sortierung ON land(sortier_reihenfolge);
|
||||
CREATE INDEX IF NOT EXISTS idx_land_eu_mitglied ON land(ist_eu_mitglied);
|
||||
CREATE INDEX IF NOT EXISTS idx_land_ewr_mitglied ON land(ist_ewr_mitglied);
|
||||
|
||||
-- Create index for name searches
|
||||
CREATE INDEX IF NOT EXISTS idx_land_name_deutsch ON land(name_deutsch);
|
||||
CREATE INDEX IF NOT EXISTS idx_land_name_englisch ON land(name_englisch);
|
||||
|
||||
-- Add comments for documentation
|
||||
COMMENT ON TABLE land IS 'Master data table for countries/nations with ISO codes and EU/EWR membership information';
|
||||
COMMENT ON COLUMN land.id IS 'Unique internal identifier (UUID)';
|
||||
COMMENT ON COLUMN land.iso_alpha2_code IS '2-letter ISO 3166-1 Alpha-2 code (e.g., AT, DE)';
|
||||
COMMENT ON COLUMN land.iso_alpha3_code IS '3-letter ISO 3166-1 Alpha-3 code (e.g., AUT, DEU)';
|
||||
COMMENT ON COLUMN land.iso_numerischer_code IS '3-digit ISO 3166-1 numeric code (e.g., 040 for Austria)';
|
||||
COMMENT ON COLUMN land.name_deutsch IS 'Official German name of the country';
|
||||
COMMENT ON COLUMN land.name_englisch IS 'Official English name of the country';
|
||||
COMMENT ON COLUMN land.wappen_url IS 'Optional URL path to country coat of arms or flag image';
|
||||
COMMENT ON COLUMN land.ist_eu_mitglied IS 'Indicates if the country is a member of the European Union';
|
||||
COMMENT ON COLUMN land.ist_ewr_mitglied IS 'Indicates if the country is a member of the European Economic Area';
|
||||
COMMENT ON COLUMN land.ist_aktiv IS 'Indicates if this country is currently active/selectable in the system';
|
||||
COMMENT ON COLUMN land.sortier_reihenfolge IS 'Optional number for controlling sort order in selection lists';
|
||||
COMMENT ON COLUMN land.created_at IS 'Timestamp when this record was created';
|
||||
COMMENT ON COLUMN land.updated_at IS 'Timestamp when this record was last updated';
|
||||
|
||||
-- Insert some initial data for common countries
|
||||
INSERT INTO land (iso_alpha2_code, iso_alpha3_code, iso_numerischer_code, name_deutsch, name_englisch, ist_eu_mitglied, ist_ewr_mitglied, sortier_reihenfolge) VALUES
|
||||
('AT', 'AUT', '040', 'Österreich', 'Austria', true, true, 1),
|
||||
('DE', 'DEU', '276', 'Deutschland', 'Germany', true, true, 2),
|
||||
('CH', 'CHE', '756', 'Schweiz', 'Switzerland', false, false, 3),
|
||||
('IT', 'ITA', '380', 'Italien', 'Italy', true, true, 4),
|
||||
('FR', 'FRA', '250', 'Frankreich', 'France', true, true, 5),
|
||||
('CZ', 'CZE', '203', 'Tschechien', 'Czech Republic', true, true, 6),
|
||||
('SK', 'SVK', '703', 'Slowakei', 'Slovakia', true, true, 7),
|
||||
('SI', 'SVN', '705', 'Slowenien', 'Slovenia', true, true, 8),
|
||||
('HU', 'HUN', '348', 'Ungarn', 'Hungary', true, true, 9),
|
||||
('PL', 'POL', '616', 'Polen', 'Poland', true, true, 10)
|
||||
ON CONFLICT (iso_alpha2_code) DO NOTHING;
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
-- Migration V002: Create Bundesland (Federal State) table
|
||||
-- This migration creates the table for federal states/regions with OEPS and ISO codes
|
||||
|
||||
CREATE TABLE IF NOT EXISTS bundesland (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
land_id UUID NOT NULL REFERENCES land(id) ON DELETE CASCADE,
|
||||
oeps_code VARCHAR(10),
|
||||
iso_3166_2_code VARCHAR(10),
|
||||
name VARCHAR(100) NOT NULL,
|
||||
kuerzel VARCHAR(10),
|
||||
wappen_url VARCHAR(500),
|
||||
ist_aktiv BOOLEAN NOT NULL DEFAULT true,
|
||||
sortier_reihenfolge INTEGER,
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Create unique constraints
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uk_bundesland_oeps_land ON bundesland(oeps_code, land_id) WHERE oeps_code IS NOT NULL;
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uk_bundesland_iso3166_2 ON bundesland(iso_3166_2_code) WHERE iso_3166_2_code IS NOT NULL;
|
||||
|
||||
-- Create indexes for performance
|
||||
CREATE INDEX IF NOT EXISTS idx_bundesland_land_id ON bundesland(land_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_bundesland_aktiv ON bundesland(ist_aktiv);
|
||||
CREATE INDEX IF NOT EXISTS idx_bundesland_sortierung ON bundesland(sortier_reihenfolge);
|
||||
CREATE INDEX IF NOT EXISTS idx_bundesland_name ON bundesland(name);
|
||||
CREATE INDEX IF NOT EXISTS idx_bundesland_land_aktiv ON bundesland(land_id, ist_aktiv);
|
||||
|
||||
-- Add comments for documentation
|
||||
COMMENT ON TABLE bundesland IS 'Master data table for federal states/regions with OEPS and ISO 3166-2 codes';
|
||||
COMMENT ON COLUMN bundesland.id IS 'Unique internal identifier (UUID)';
|
||||
COMMENT ON COLUMN bundesland.land_id IS 'Foreign key reference to the country this federal state belongs to';
|
||||
COMMENT ON COLUMN bundesland.oeps_code IS '2-digit OEPS code for Austrian federal states (e.g., 01 for Vienna, 02 for Lower Austria)';
|
||||
COMMENT ON COLUMN bundesland.iso_3166_2_code IS 'Official ISO 3166-2 code for the federal state (e.g., AT-1 for Burgenland, DE-BY for Bavaria)';
|
||||
COMMENT ON COLUMN bundesland.name IS 'Official name of the federal state';
|
||||
COMMENT ON COLUMN bundesland.kuerzel IS 'Common abbreviation for the federal state (e.g., NÖ, W, STMK)';
|
||||
COMMENT ON COLUMN bundesland.wappen_url IS 'Optional URL path to federal state coat of arms image';
|
||||
COMMENT ON COLUMN bundesland.ist_aktiv IS 'Indicates if this federal state is currently active/selectable in the system';
|
||||
COMMENT ON COLUMN bundesland.sortier_reihenfolge IS 'Optional number for controlling sort order in selection lists';
|
||||
COMMENT ON COLUMN bundesland.created_at IS 'Timestamp when this record was created';
|
||||
COMMENT ON COLUMN bundesland.updated_at IS 'Timestamp when this record was last updated';
|
||||
|
||||
-- Insert Austrian federal states (Bundesländer)
|
||||
-- First, get the Austria country ID
|
||||
DO $$
|
||||
DECLARE
|
||||
austria_id UUID;
|
||||
BEGIN
|
||||
SELECT id INTO austria_id FROM land WHERE iso_alpha2_code = 'AT';
|
||||
|
||||
IF austria_id IS NOT NULL THEN
|
||||
INSERT INTO bundesland (land_id, oeps_code, iso_3166_2_code, name, kuerzel, sortier_reihenfolge) VALUES
|
||||
(austria_id, '01', 'AT-1', 'Burgenland', 'BGLD', 1),
|
||||
(austria_id, '02', 'AT-2', 'Kärnten', 'KTN', 2),
|
||||
(austria_id, '03', 'AT-3', 'Niederösterreich', 'NÖ', 3),
|
||||
(austria_id, '04', 'AT-4', 'Oberösterreich', 'OÖ', 4),
|
||||
(austria_id, '05', 'AT-5', 'Salzburg', 'SBG', 5),
|
||||
(austria_id, '06', 'AT-6', 'Steiermark', 'STMK', 6),
|
||||
(austria_id, '07', 'AT-7', 'Tirol', 'T', 7),
|
||||
(austria_id, '08', 'AT-8', 'Vorarlberg', 'VBG', 8),
|
||||
(austria_id, '09', 'AT-9', 'Wien', 'W', 9)
|
||||
ON CONFLICT DO NOTHING;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- Insert German federal states (Bundesländer)
|
||||
DO $$
|
||||
DECLARE
|
||||
germany_id UUID;
|
||||
BEGIN
|
||||
SELECT id INTO germany_id FROM land WHERE iso_alpha2_code = 'DE';
|
||||
|
||||
IF germany_id IS NOT NULL THEN
|
||||
INSERT INTO bundesland (land_id, iso_3166_2_code, name, kuerzel, sortier_reihenfolge) VALUES
|
||||
(germany_id, 'DE-BW', 'Baden-Württemberg', 'BW', 1),
|
||||
(germany_id, 'DE-BY', 'Bayern', 'BY', 2),
|
||||
(germany_id, 'DE-BE', 'Berlin', 'BE', 3),
|
||||
(germany_id, 'DE-BB', 'Brandenburg', 'BB', 4),
|
||||
(germany_id, 'DE-HB', 'Bremen', 'HB', 5),
|
||||
(germany_id, 'DE-HH', 'Hamburg', 'HH', 6),
|
||||
(germany_id, 'DE-HE', 'Hessen', 'HE', 7),
|
||||
(germany_id, 'DE-MV', 'Mecklenburg-Vorpommern', 'MV', 8),
|
||||
(germany_id, 'DE-NI', 'Niedersachsen', 'NI', 9),
|
||||
(germany_id, 'DE-NW', 'Nordrhein-Westfalen', 'NW', 10),
|
||||
(germany_id, 'DE-RP', 'Rheinland-Pfalz', 'RP', 11),
|
||||
(germany_id, 'DE-SL', 'Saarland', 'SL', 12),
|
||||
(germany_id, 'DE-SN', 'Sachsen', 'SN', 13),
|
||||
(germany_id, 'DE-ST', 'Sachsen-Anhalt', 'ST', 14),
|
||||
(germany_id, 'DE-SH', 'Schleswig-Holstein', 'SH', 15),
|
||||
(germany_id, 'DE-TH', 'Thüringen', 'TH', 16)
|
||||
ON CONFLICT DO NOTHING;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- Insert Swiss cantons
|
||||
DO $$
|
||||
DECLARE
|
||||
switzerland_id UUID;
|
||||
BEGIN
|
||||
SELECT id INTO switzerland_id FROM land WHERE iso_alpha2_code = 'CH';
|
||||
|
||||
IF switzerland_id IS NOT NULL THEN
|
||||
INSERT INTO bundesland (land_id, iso_3166_2_code, name, kuerzel, sortier_reihenfolge) VALUES
|
||||
(switzerland_id, 'CH-AG', 'Aargau', 'AG', 1),
|
||||
(switzerland_id, 'CH-AI', 'Appenzell Innerrhoden', 'AI', 2),
|
||||
(switzerland_id, 'CH-AR', 'Appenzell Ausserrhoden', 'AR', 3),
|
||||
(switzerland_id, 'CH-BE', 'Bern', 'BE', 4),
|
||||
(switzerland_id, 'CH-BL', 'Basel-Landschaft', 'BL', 5),
|
||||
(switzerland_id, 'CH-BS', 'Basel-Stadt', 'BS', 6),
|
||||
(switzerland_id, 'CH-FR', 'Freiburg', 'FR', 7),
|
||||
(switzerland_id, 'CH-GE', 'Genf', 'GE', 8),
|
||||
(switzerland_id, 'CH-GL', 'Glarus', 'GL', 9),
|
||||
(switzerland_id, 'CH-GR', 'Graubünden', 'GR', 10),
|
||||
(switzerland_id, 'CH-JU', 'Jura', 'JU', 11),
|
||||
(switzerland_id, 'CH-LU', 'Luzern', 'LU', 12),
|
||||
(switzerland_id, 'CH-NE', 'Neuenburg', 'NE', 13),
|
||||
(switzerland_id, 'CH-NW', 'Nidwalden', 'NW', 14),
|
||||
(switzerland_id, 'CH-OW', 'Obwalden', 'OW', 15),
|
||||
(switzerland_id, 'CH-SG', 'St. Gallen', 'SG', 16),
|
||||
(switzerland_id, 'CH-SH', 'Schaffhausen', 'SH', 17),
|
||||
(switzerland_id, 'CH-SO', 'Solothurn', 'SO', 18),
|
||||
(switzerland_id, 'CH-SZ', 'Schwyz', 'SZ', 19),
|
||||
(switzerland_id, 'CH-TG', 'Thurgau', 'TG', 20),
|
||||
(switzerland_id, 'CH-TI', 'Tessin', 'TI', 21),
|
||||
(switzerland_id, 'CH-UR', 'Uri', 'UR', 22),
|
||||
(switzerland_id, 'CH-VD', 'Waadt', 'VD', 23),
|
||||
(switzerland_id, 'CH-VS', 'Wallis', 'VS', 24),
|
||||
(switzerland_id, 'CH-ZG', 'Zug', 'ZG', 25),
|
||||
(switzerland_id, 'CH-ZH', 'Zürich', 'ZH', 26)
|
||||
ON CONFLICT DO NOTHING;
|
||||
END IF;
|
||||
END $$;
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
-- Migration V003: Create Altersklasse (Age Class) table
|
||||
-- This migration creates the table for age class definitions with sport and gender filters
|
||||
|
||||
CREATE TABLE IF NOT EXISTS altersklasse (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
altersklasse_code VARCHAR(50) NOT NULL UNIQUE,
|
||||
bezeichnung VARCHAR(200) NOT NULL,
|
||||
min_alter INTEGER,
|
||||
max_alter INTEGER,
|
||||
stichtag_regel_text VARCHAR(500),
|
||||
sparte_filter VARCHAR(50),
|
||||
geschlecht_filter CHAR(1),
|
||||
oeto_regel_referenz_id UUID,
|
||||
ist_aktiv BOOLEAN NOT NULL DEFAULT true,
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
-- Constraints
|
||||
CONSTRAINT chk_altersklasse_geschlecht CHECK (geschlecht_filter IN ('M', 'W') OR geschlecht_filter IS NULL),
|
||||
CONSTRAINT chk_altersklasse_alter_range CHECK (min_alter IS NULL OR max_alter IS NULL OR min_alter <= max_alter),
|
||||
CONSTRAINT chk_altersklasse_min_alter CHECK (min_alter IS NULL OR min_alter >= 0),
|
||||
CONSTRAINT chk_altersklasse_max_alter CHECK (max_alter IS NULL OR max_alter >= 0)
|
||||
);
|
||||
|
||||
-- Create indexes for performance
|
||||
CREATE INDEX IF NOT EXISTS idx_altersklasse_aktiv ON altersklasse(ist_aktiv);
|
||||
CREATE INDEX IF NOT EXISTS idx_altersklasse_sparte ON altersklasse(sparte_filter);
|
||||
CREATE INDEX IF NOT EXISTS idx_altersklasse_geschlecht ON altersklasse(geschlecht_filter);
|
||||
CREATE INDEX IF NOT EXISTS idx_altersklasse_alter ON altersklasse(min_alter, max_alter);
|
||||
CREATE INDEX IF NOT EXISTS idx_altersklasse_bezeichnung ON altersklasse(bezeichnung);
|
||||
CREATE INDEX IF NOT EXISTS idx_altersklasse_code ON altersklasse(altersklasse_code);
|
||||
|
||||
-- Add comments for documentation
|
||||
COMMENT ON TABLE altersklasse IS 'Master data table for age class definitions with eligibility rules for participants';
|
||||
COMMENT ON COLUMN altersklasse.id IS 'Unique internal identifier (UUID)';
|
||||
COMMENT ON COLUMN altersklasse.altersklasse_code IS 'Unique code for the age class (e.g., JGD_U16, JUN_U18, YR_U21, AK)';
|
||||
COMMENT ON COLUMN altersklasse.bezeichnung IS 'Official or commonly understood designation of the age class';
|
||||
COMMENT ON COLUMN altersklasse.min_alter IS 'Minimum age (years, inclusive) for this age class. NULL if no lower limit';
|
||||
COMMENT ON COLUMN altersklasse.max_alter IS 'Maximum age (years, inclusive) for this age class. NULL if no upper limit';
|
||||
COMMENT ON COLUMN altersklasse.stichtag_regel_text IS 'Description of the rule for the reference date for age calculation';
|
||||
COMMENT ON COLUMN altersklasse.sparte_filter IS 'Optional specification if this age class definition only applies to a specific sport';
|
||||
COMMENT ON COLUMN altersklasse.geschlecht_filter IS 'Optional filter for gender (M, W) if the age class is gender-specific. NULL means valid for all genders';
|
||||
COMMENT ON COLUMN altersklasse.oeto_regel_referenz_id IS 'Optional link to a specific rule in the OETO rule reference table';
|
||||
COMMENT ON COLUMN altersklasse.ist_aktiv IS 'Indicates if this age class definition can currently be used in the system';
|
||||
COMMENT ON COLUMN altersklasse.created_at IS 'Timestamp when this record was created';
|
||||
COMMENT ON COLUMN altersklasse.updated_at IS 'Timestamp when this record was last updated';
|
||||
|
||||
-- Insert common age class definitions for equestrian sports
|
||||
INSERT INTO altersklasse (altersklasse_code, bezeichnung, min_alter, max_alter, stichtag_regel_text, sparte_filter, geschlecht_filter) VALUES
|
||||
-- General age classes (all sports)
|
||||
('PONY_U10', 'Pony Führzügel U10', NULL, 9, '31.12. des laufenden Kalenderjahres', NULL, NULL),
|
||||
('PONY_U12', 'Pony U12', NULL, 11, '31.12. des laufenden Kalenderjahres', NULL, NULL),
|
||||
('PONY_U14', 'Pony U14', NULL, 13, '31.12. des laufenden Kalenderjahres', NULL, NULL),
|
||||
('PONY_U16', 'Pony U16', NULL, 15, '31.12. des laufenden Kalenderjahres', NULL, NULL),
|
||||
('JGD_U16', 'Jugend U16', NULL, 15, '31.12. des laufenden Kalenderjahres', NULL, NULL),
|
||||
('JGD_U18', 'Jugend U18', NULL, 17, '31.12. des laufenden Kalenderjahres', NULL, NULL),
|
||||
('JUN_U21', 'Junioren U21', NULL, 20, '31.12. des laufenden Kalenderjahres', NULL, NULL),
|
||||
('YR_U25', 'Junge Reiter U25', NULL, 24, '31.12. des laufenden Kalenderjahres', NULL, NULL),
|
||||
('AK', 'Allgemeine Klasse', 18, NULL, '31.12. des laufenden Kalenderjahres', NULL, NULL),
|
||||
('SEN_40', 'Senioren Ü40', 40, NULL, '31.12. des laufenden Kalenderjahres', NULL, NULL),
|
||||
('SEN_50', 'Senioren Ü50', 50, NULL, '31.12. des laufenden Kalenderjahres', NULL, NULL),
|
||||
('SEN_60', 'Senioren Ü60', 60, NULL, '31.12. des laufenden Kalenderjahres', NULL, NULL),
|
||||
|
||||
-- Dressage-specific age classes
|
||||
('DR_PONY_U12', 'Dressur Pony U12', NULL, 11, '31.12. des laufenden Kalenderjahres', 'DRESSUR', NULL),
|
||||
('DR_PONY_U14', 'Dressur Pony U14', NULL, 13, '31.12. des laufenden Kalenderjahres', 'DRESSUR', NULL),
|
||||
('DR_PONY_U16', 'Dressur Pony U16', NULL, 15, '31.12. des laufenden Kalenderjahres', 'DRESSUR', NULL),
|
||||
('DR_JGD_U18', 'Dressur Jugend U18', NULL, 17, '31.12. des laufenden Kalenderjahres', 'DRESSUR', NULL),
|
||||
('DR_JUN_U21', 'Dressur Junioren U21', NULL, 20, '31.12. des laufenden Kalenderjahres', 'DRESSUR', NULL),
|
||||
('DR_YR_U25', 'Dressur Junge Reiter U25', NULL, 24, '31.12. des laufenden Kalenderjahres', 'DRESSUR', NULL),
|
||||
|
||||
-- Jumping-specific age classes
|
||||
('SP_PONY_U12', 'Springen Pony U12', NULL, 11, '31.12. des laufenden Kalenderjahres', 'SPRINGEN', NULL),
|
||||
('SP_PONY_U14', 'Springen Pony U14', NULL, 13, '31.12. des laufenden Kalenderjahres', 'SPRINGEN', NULL),
|
||||
('SP_PONY_U16', 'Springen Pony U16', NULL, 15, '31.12. des laufenden Kalenderjahres', 'SPRINGEN', NULL),
|
||||
('SP_JGD_U18', 'Springen Jugend U18', NULL, 17, '31.12. des laufenden Kalenderjahres', 'SPRINGEN', NULL),
|
||||
('SP_JUN_U21', 'Springen Junioren U21', NULL, 20, '31.12. des laufenden Kalenderjahres', 'SPRINGEN', NULL),
|
||||
('SP_YR_U25', 'Springen Junge Reiter U25', NULL, 24, '31.12. des laufenden Kalenderjahres', 'SPRINGEN', NULL),
|
||||
|
||||
-- Eventing-specific age classes
|
||||
('VK_PONY_U14', 'Vielseitigkeit Pony U14', NULL, 13, '31.12. des laufenden Kalenderjahres', 'VIELSEITIGKEIT', NULL),
|
||||
('VK_PONY_U16', 'Vielseitigkeit Pony U16', NULL, 15, '31.12. des laufenden Kalenderjahres', 'VIELSEITIGKEIT', NULL),
|
||||
('VK_JGD_U18', 'Vielseitigkeit Jugend U18', NULL, 17, '31.12. des laufenden Kalenderjahres', 'VIELSEITIGKEIT', NULL),
|
||||
('VK_JUN_U21', 'Vielseitigkeit Junioren U21', NULL, 20, '31.12. des laufenden Kalenderjahres', 'VIELSEITIGKEIT', NULL),
|
||||
('VK_YR_U25', 'Vielseitigkeit Junge Reiter U25', NULL, 24, '31.12. des laufenden Kalenderjahres', 'VIELSEITIGKEIT', NULL),
|
||||
|
||||
-- Driving-specific age classes
|
||||
('FA_PONY_U16', 'Fahren Pony U16', NULL, 15, '31.12. des laufenden Kalenderjahres', 'FAHREN', NULL),
|
||||
('FA_JGD_U18', 'Fahren Jugend U18', NULL, 17, '31.12. des laufenden Kalenderjahres', 'FAHREN', NULL),
|
||||
('FA_JUN_U21', 'Fahren Junioren U21', NULL, 20, '31.12. des laufenden Kalenderjahres', 'FAHREN', NULL),
|
||||
('FA_YR_U25', 'Fahren Junge Reiter U25', NULL, 24, '31.12. des laufenden Kalenderjahres', 'FAHREN', NULL),
|
||||
|
||||
-- Vaulting-specific age classes
|
||||
('VT_U10', 'Voltigieren U10', NULL, 9, '31.12. des laufenden Kalenderjahres', 'VOLTIGIEREN', NULL),
|
||||
('VT_U12', 'Voltigieren U12', NULL, 11, '31.12. des laufenden Kalenderjahres', 'VOLTIGIEREN', NULL),
|
||||
('VT_U14', 'Voltigieren U14', NULL, 13, '31.12. des laufenden Kalenderjahres', 'VOLTIGIEREN', NULL),
|
||||
('VT_U16', 'Voltigieren U16', NULL, 15, '31.12. des laufenden Kalenderjahres', 'VOLTIGIEREN', NULL),
|
||||
('VT_U18', 'Voltigieren U18', NULL, 17, '31.12. des laufenden Kalenderjahres', 'VOLTIGIEREN', NULL),
|
||||
('VT_JUN_U21', 'Voltigieren Junioren U21', NULL, 20, '31.12. des laufenden Kalenderjahres', 'VOLTIGIEREN', NULL),
|
||||
|
||||
-- Gender-specific examples (if needed)
|
||||
('DR_DAMEN', 'Dressur Damen', 18, NULL, '31.12. des laufenden Kalenderjahres', 'DRESSUR', 'W'),
|
||||
('DR_HERREN', 'Dressur Herren', 18, NULL, '31.12. des laufenden Kalenderjahres', 'DRESSUR', 'M')
|
||||
|
||||
ON CONFLICT (altersklasse_code) DO NOTHING;
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
-- Migration V004: Create Platz (Venue/Arena) table
|
||||
-- This migration creates the table for tournament venues and arenas
|
||||
|
||||
CREATE TABLE IF NOT EXISTS platz (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
turnier_id UUID NOT NULL, -- Foreign key to tournament (not enforced as tournament might be in different module)
|
||||
name VARCHAR(200) NOT NULL,
|
||||
dimension VARCHAR(50),
|
||||
boden VARCHAR(100),
|
||||
typ VARCHAR(50) NOT NULL,
|
||||
ist_aktiv BOOLEAN NOT NULL DEFAULT true,
|
||||
sortier_reihenfolge INTEGER,
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
-- Constraints
|
||||
CONSTRAINT chk_platz_sortier_reihenfolge CHECK (sortier_reihenfolge IS NULL OR sortier_reihenfolge >= 0)
|
||||
);
|
||||
|
||||
-- Create unique constraint for name per tournament
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uk_platz_name_turnier ON platz(name, turnier_id);
|
||||
|
||||
-- Create indexes for performance
|
||||
CREATE INDEX IF NOT EXISTS idx_platz_turnier ON platz(turnier_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_platz_aktiv ON platz(ist_aktiv);
|
||||
CREATE INDEX IF NOT EXISTS idx_platz_typ ON platz(typ);
|
||||
CREATE INDEX IF NOT EXISTS idx_platz_turnier_aktiv ON platz(turnier_id, ist_aktiv);
|
||||
CREATE INDEX IF NOT EXISTS idx_platz_name ON platz(name);
|
||||
CREATE INDEX IF NOT EXISTS idx_platz_dimension ON platz(dimension);
|
||||
CREATE INDEX IF NOT EXISTS idx_platz_boden ON platz(boden);
|
||||
CREATE INDEX IF NOT EXISTS idx_platz_sortierung ON platz(sortier_reihenfolge);
|
||||
|
||||
-- Add comments for documentation
|
||||
COMMENT ON TABLE platz IS 'Master data table for tournament venues and arenas with type and dimension specifications';
|
||||
COMMENT ON COLUMN platz.id IS 'Unique internal identifier (UUID)';
|
||||
COMMENT ON COLUMN platz.turnier_id IS 'Foreign key reference to the tournament this venue belongs to';
|
||||
COMMENT ON COLUMN platz.name IS 'Name or designation of the venue (e.g., "Hauptplatz", "Dressurplatz A")';
|
||||
COMMENT ON COLUMN platz.dimension IS 'Dimensions of the venue (e.g., "20x60m", "20x40m")';
|
||||
COMMENT ON COLUMN platz.boden IS 'Type of ground surface (e.g., "Sand", "Gras", "Kunststoff")';
|
||||
COMMENT ON COLUMN platz.typ IS 'Type of venue (see PlatzTypE enum)';
|
||||
COMMENT ON COLUMN platz.ist_aktiv IS 'Indicates if this venue can currently be used';
|
||||
COMMENT ON COLUMN platz.sortier_reihenfolge IS 'Optional number for controlling sort order';
|
||||
COMMENT ON COLUMN platz.created_at IS 'Timestamp when this record was created';
|
||||
COMMENT ON COLUMN platz.updated_at IS 'Timestamp when this record was last updated';
|
||||
|
||||
-- Insert some example venue types and common configurations
|
||||
-- Note: These are examples and would typically be created per tournament
|
||||
-- Using a dummy tournament ID for demonstration purposes
|
||||
|
||||
-- Create a function to generate example venues for a tournament
|
||||
CREATE OR REPLACE FUNCTION create_example_venues(tournament_id UUID) RETURNS VOID AS $$
|
||||
BEGIN
|
||||
INSERT INTO platz (turnier_id, name, dimension, boden, typ, sortier_reihenfolge) VALUES
|
||||
-- Dressage arenas
|
||||
(tournament_id, 'Dressurplatz A', '20x60m', 'Sand', 'DRESSURPLATZ', 10),
|
||||
(tournament_id, 'Dressurplatz B', '20x40m', 'Sand', 'DRESSURPLATZ', 20),
|
||||
(tournament_id, 'Abreiteplatz Dressur', '20x40m', 'Sand', 'ABREITEPLATZ', 30),
|
||||
|
||||
-- Jumping arenas
|
||||
(tournament_id, 'Springplatz Hauptring', '40x80m', 'Sand', 'SPRINGPLATZ', 40),
|
||||
(tournament_id, 'Springplatz Ring 2', '35x70m', 'Sand', 'SPRINGPLATZ', 50),
|
||||
(tournament_id, 'Abreiteplatz Springen', '30x60m', 'Sand', 'ABREITEPLATZ', 60),
|
||||
|
||||
-- Cross-country and eventing
|
||||
(tournament_id, 'Geländestrecke', 'variabel', 'Gras', 'GELAENDESTRECKE', 70),
|
||||
(tournament_id, 'Vielseitigkeitsplatz', '25x65m', 'Sand', 'VIELSEITIGKEITSPLATZ', 80),
|
||||
|
||||
-- Driving arenas
|
||||
(tournament_id, 'Fahrplatz', '40x100m', 'Sand', 'FAHRPLATZ', 90),
|
||||
(tournament_id, 'Hindernisfahren', '40x80m', 'Sand', 'FAHRPLATZ', 100),
|
||||
|
||||
-- Vaulting
|
||||
(tournament_id, 'Voltigierplatz', '20m Durchmesser', 'Sand', 'VOLTIGIERPLATZ', 110),
|
||||
|
||||
-- Training and warm-up areas
|
||||
(tournament_id, 'Führanlage', '20m Durchmesser', 'Sand', 'FUEHRANLAGE', 120),
|
||||
(tournament_id, 'Longierplatz', '20m Durchmesser', 'Sand', 'LONGIERPLATZ', 130),
|
||||
(tournament_id, 'Trainingsplatz 1', '20x40m', 'Sand', 'TRAININGSPLATZ', 140),
|
||||
(tournament_id, 'Trainingsplatz 2', '20x40m', 'Gras', 'TRAININGSPLATZ', 150),
|
||||
|
||||
-- Indoor arenas
|
||||
(tournament_id, 'Reithalle A', '20x60m', 'Sand', 'REITHALLE', 160),
|
||||
(tournament_id, 'Reithalle B', '20x40m', 'Sand', 'REITHALLE', 170),
|
||||
|
||||
-- Outdoor areas
|
||||
(tournament_id, 'Außenplatz 1', '25x50m', 'Gras', 'AUSSENPLATZ', 180),
|
||||
(tournament_id, 'Außenplatz 2', '20x40m', 'Sand', 'AUSSENPLATZ', 190),
|
||||
|
||||
-- Special purpose areas
|
||||
(tournament_id, 'Siegerehrungsplatz', '15x25m', 'Gras', 'SONDERPLATZ', 200),
|
||||
(tournament_id, 'Vorführplatz', '20x30m', 'Sand', 'SONDERPLATZ', 210)
|
||||
|
||||
ON CONFLICT (name, turnier_id) DO NOTHING;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Add some venue type validation comments
|
||||
COMMENT ON FUNCTION create_example_venues(UUID) IS 'Helper function to create example venues for a tournament. Call with tournament UUID.';
|
||||
|
||||
-- Create a view for venue statistics
|
||||
CREATE OR REPLACE VIEW platz_statistics AS
|
||||
SELECT
|
||||
typ,
|
||||
COUNT(*) as total_count,
|
||||
COUNT(CASE WHEN ist_aktiv THEN 1 END) as active_count,
|
||||
COUNT(CASE WHEN NOT ist_aktiv THEN 1 END) as inactive_count,
|
||||
COUNT(DISTINCT turnier_id) as tournament_count,
|
||||
COUNT(DISTINCT dimension) as dimension_variants,
|
||||
COUNT(DISTINCT boden) as ground_type_variants
|
||||
FROM platz
|
||||
GROUP BY typ
|
||||
ORDER BY typ;
|
||||
|
||||
COMMENT ON VIEW platz_statistics IS 'Statistical overview of venues by type, showing counts and variants';
|
||||
|
||||
-- Create a view for tournament venue overview
|
||||
CREATE OR REPLACE VIEW tournament_venue_overview AS
|
||||
SELECT
|
||||
turnier_id,
|
||||
COUNT(*) as total_venues,
|
||||
COUNT(CASE WHEN ist_aktiv THEN 1 END) as active_venues,
|
||||
COUNT(DISTINCT typ) as venue_types,
|
||||
COUNT(DISTINCT dimension) as dimension_variants,
|
||||
COUNT(DISTINCT boden) as ground_types,
|
||||
STRING_AGG(DISTINCT typ, ', ' ORDER BY typ) as available_types
|
||||
FROM platz
|
||||
GROUP BY turnier_id
|
||||
ORDER BY turnier_id;
|
||||
|
||||
COMMENT ON VIEW tournament_venue_overview IS 'Overview of venues per tournament with summary statistics';
|
||||
|
||||
-- Example of how to use the function (commented out as it requires actual tournament IDs)
|
||||
-- SELECT create_example_venues('550e8400-e29b-41d4-a716-446655440000'::UUID);
|
||||
|
||||
-- Add some helpful indexes for the views
|
||||
CREATE INDEX IF NOT EXISTS idx_platz_typ_aktiv ON platz(typ, ist_aktiv);
|
||||
CREATE INDEX IF NOT EXISTS idx_platz_turnier_typ ON platz(turnier_id, typ);
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
-- File: V1__Create_Initial_Tables.sql
|
||||
|
||||
-- Tabelle zur Verwaltung der Vereine (Mandanten)
|
||||
CREATE TABLE IF NOT EXISTS dom_verein (
|
||||
verein_id UUID PRIMARY KEY,
|
||||
oeps_vereins_nr VARCHAR(4) UNIQUE,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
kuerzel VARCHAR(20),
|
||||
bundesland_code VARCHAR(2),
|
||||
daten_quelle VARCHAR(50) NOT NULL,
|
||||
ist_aktiv BOOLEAN NOT NULL DEFAULT true,
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Tabelle zur Verwaltung der Personen (Sportler, Funktionäre)
|
||||
CREATE TABLE IF NOT EXISTS dom_person (
|
||||
person_id UUID PRIMARY KEY,
|
||||
oeps_satz_nr VARCHAR(6) UNIQUE,
|
||||
nachname VARCHAR(100) NOT NULL,
|
||||
vorname VARCHAR(100) NOT NULL,
|
||||
geburtsdatum DATE,
|
||||
geschlecht VARCHAR(10),
|
||||
nationalitaet_code VARCHAR(3),
|
||||
stamm_verein_id UUID REFERENCES dom_verein(verein_id),
|
||||
ist_gesperrt BOOLEAN NOT NULL DEFAULT false,
|
||||
daten_quelle VARCHAR(50) NOT NULL,
|
||||
ist_aktiv BOOLEAN NOT NULL DEFAULT true,
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Weitere Tabellen können hier hinzugefügt werden...
|
||||
@@ -0,0 +1,10 @@
|
||||
<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