(fix) Implementiere einen Service-Layer

Erstellung von DTOs für alle Ressourcen
Implement a versioning system
This commit is contained in:
2025-06-30 22:25:35 +02:00
parent 418e692092
commit e2432510af
28 changed files with 4102 additions and 14 deletions
+272
View File
@@ -0,0 +1,272 @@
# API Versioning Implementation
## Übersicht
Dieses Dokument beschreibt die implementierte Versionierungsstrategie für die Meldestelle API. Das System unterstützt sowohl DTO-Versionierung als auch API-Versionierung für eine saubere Evolution der API.
## Architektur
### 1. DTO Versionierung
Alle DTOs implementieren das `VersionedDto` Interface, welches folgende Eigenschaften bereitstellt:
```kotlin
interface VersionedDto {
val schemaVersion: String
val dataVersion: Long?
}
```
#### Beispiel Implementation:
```kotlin
@Serializable
@Since("1.0")
data class ArtikelDto(
@Serializable(with = UuidSerializer::class)
val id: Uuid,
val bezeichnung: String,
// ... andere Felder
override val schemaVersion: String = "1.0",
override val dataVersion: Long? = null
) : VersionedDto
```
### 2. Version Manager
Der `VersionManager` verwaltet API-Versionen und Kompatibilität:
```kotlin
object VersionManager {
const val CURRENT_API_VERSION = "1.0"
val SUPPORTED_VERSIONS = listOf("1.0")
val DEPRECATED_VERSIONS = emptyList<String>()
const val MINIMUM_CLIENT_VERSION = "1.0"
}
```
### 3. API Versioning Plugin
Das Ktor-Plugin `VersioningPlugin` behandelt:
- Version-Header Validierung
- Automatische Version-Header in Responses
- Deprecation Warnings
- Unsupported Version Errors
## Verwendung
### Client-seitige Version Headers
Clients können die API-Version über Header spezifizieren:
```http
GET /api/artikel
API-Version: 1.0
```
oder
```http
GET /api/artikel
X-API-Version: 1.0
```
### Server Response Headers
Der Server antwortet mit Version-Informationen:
```http
HTTP/1.1 200 OK
API-Version: 1.0
X-Supported-Versions: 1.0
```
### Versioned Responses
Verwende die Extension-Funktionen für versionierte Antworten:
```kotlin
// Einzelnes DTO
call.respondVersioned(HttpStatusCode.OK, artikelDto)
// Liste von DTOs
call.respondVersionedList(HttpStatusCode.OK, artikelList)
```
## Migration System
### VersionMigrator Interface
```kotlin
interface VersionMigrator<T : VersionedDto> {
fun migrate(dto: T, fromVersion: String, toVersion: String): T
fun canMigrate(fromVersion: String, toVersion: String): Boolean
}
```
### Beispiel Migrator
```kotlin
class ArtikelDtoMigrator : VersionMigrator<ArtikelDto> {
override fun migrate(dto: ArtikelDto, fromVersion: String, toVersion: String): ArtikelDto {
return when {
fromVersion == "1.0" && toVersion == "1.1" -> migrateFrom1_0To1_1(dto)
else -> throw IllegalArgumentException("Unsupported migration")
}
}
private fun migrateFrom1_0To1_1(dto: ArtikelDto): ArtikelDto {
return dto.copy(
schemaVersion = "1.1",
// Neue Felder mit Standardwerten hinzufügen
)
}
}
```
## Annotations
### @Since(version)
Markiert, seit welcher Version ein DTO oder Feld verfügbar ist.
### @Deprecated(version, message)
Markiert veraltete DTOs oder Felder.
### @Until(version)
Markiert, bis zu welcher Version ein DTO oder Feld verfügbar war.
## Best Practices
### 1. Neue API Version hinzufügen
1. **VersionManager aktualisieren:**
```kotlin
const val CURRENT_API_VERSION = "1.1"
val SUPPORTED_VERSIONS = listOf("1.1", "1.0")
val DEPRECATED_VERSIONS = listOf("1.0")
```
2. **DTOs erweitern:**
```kotlin
@Serializable
@Since("1.1")
data class ArtikelDto(
// Bestehende Felder...
@Since("1.1")
val neuesFeld: String? = null,
override val schemaVersion: String = "1.1"
) : VersionedDto
```
3. **Migrator implementieren:**
```kotlin
class ArtikelDtoMigrator : VersionMigrator<ArtikelDto> {
override fun migrate(dto: ArtikelDto, fromVersion: String, toVersion: String): ArtikelDto {
return when {
fromVersion == "1.0" && toVersion == "1.1" -> migrateFrom1_0To1_1(dto)
// Weitere Migrationen...
}
}
}
```
### 2. Backward Compatibility
- Neue Felder sollten optional sein (nullable oder mit Standardwerten)
- Bestehende Felder nicht entfernen, sondern als @Deprecated markieren
- Migratoren für alle unterstützten Versionsübergänge bereitstellen
### 3. Breaking Changes
Bei Breaking Changes:
1. Neue Major Version erstellen
2. Alte Version als deprecated markieren
3. Migration Path bereitstellen
4. Dokumentation aktualisieren
## Beispiel API Calls
### Erfolgreiche Anfrage
```http
GET /api/artikel
API-Version: 1.0
HTTP/1.1 200 OK
API-Version: 1.0
X-Supported-Versions: 1.0
Content-Type: application/json
```
### Unsupported Version
```http
GET /api/artikel
API-Version: 2.0
HTTP/1.1 400 Bad Request
Content-Type: application/json
```
### Deprecated Version Warning
```http
GET /api/artikel
API-Version: 0.9
HTTP/1.1 200 OK
API-Version: 1.0
X-API-Version-Warning: Version 0.9 is deprecated
```
## Testing
Das Versioning System wird durch `VersioningTest.kt` getestet:
```bash
./gradlew test --tests "at.mocode.VersioningTest"
```
## Implementierte DTOs
Folgende DTOs wurden bereits mit Versionierung ausgestattet:
-`ArtikelDto`, `CreateArtikelDto`, `UpdateArtikelDto`
-`VereinDto`, `CreateVereinDto`, `UpdateVereinDto`
### Noch zu implementieren:
- `AbteilungDto`
- `BewerbDto`
- `DomaeneDto`
- `StammdatenDto`
- `TurnierDto`
- `VeranstaltungDto`
- `CommonDto` (alle Klassen)
- `SpecializedDto`
## Nächste Schritte
1. Alle verbleibenden DTOs mit Versionierung ausstatten
2. API Routes auf DTO-Verwendung umstellen
3. Versioning Plugin in Application.kt aktivieren
4. Client-seitige Version-Header Implementation
5. Monitoring für Version-Usage implementieren
### Noch zu implementieren:
- `AbteilungDto`
- `BewerbDto`
- `DomaeneDto`
- `StammdatenDto`
- `TurnierDto`
- `VeranstaltungDto`
- `CommonDto` (alle Klassen)
- `SpecializedDto`
## Nächste Schritte
1. Alle verbleibenden DTOs mit Versionierung ausstatten
2. API Routes auf DTO-Verwendung umstellen
3. Versioning Plugin in Application.kt aktivieren
4. Client-seitige Version-Header Implementation
5. Monitoring für Version-Usage implementieren
@@ -0,0 +1,106 @@
package at.mocode.plugins
import at.mocode.dto.base.VersionManager
import at.mocode.dto.base.VersionValidationResult
import at.mocode.dto.base.VersionedDto
import at.mocode.dto.base.VersionedResponse
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.plugins.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.util.*
import kotlinx.datetime.Clock
import kotlinx.serialization.json.Json
/**
* Plugin for handling API versioning
*/
val VersioningPlugin = createApplicationPlugin(name = "VersioningPlugin") {
onCall { call ->
// Extract version from headers
val clientVersion = call.request.header("API-Version")
?: call.request.header("X-API-Version")
?: VersionManager.CURRENT_API_VERSION
// Validate version
when (val result = VersionManager.validateClientVersion(clientVersion)) {
is VersionValidationResult.Valid -> {
call.attributes.put(ClientVersionKey, result.version)
}
is VersionValidationResult.DeprecatedVersion -> {
call.attributes.put(ClientVersionKey, result.version)
call.response.header("X-API-Version-Warning", "Version ${result.version} is deprecated")
}
is VersionValidationResult.UnsupportedVersion -> {
call.respond(
HttpStatusCode.BadRequest,
mapOf(
"error" to "Unsupported API version: ${result.version}",
"supportedVersions" to VersionManager.SUPPORTED_VERSIONS,
"currentVersion" to VersionManager.CURRENT_API_VERSION
)
)
return@onCall
}
is VersionValidationResult.MissingVersion -> {
call.attributes.put(ClientVersionKey, VersionManager.CURRENT_API_VERSION)
}
}
// Add version info to response headers
call.response.header("API-Version", VersionManager.CURRENT_API_VERSION)
call.response.header("X-Supported-Versions", VersionManager.SUPPORTED_VERSIONS.joinToString(","))
}
}
/**
* Key for storing client version in call attributes
*/
val ClientVersionKey = AttributeKey<String>("ClientVersion")
/**
* Extension function to get client version from call
*/
fun ApplicationCall.getClientVersion(): String {
return attributes.getOrNull(ClientVersionKey) ?: VersionManager.CURRENT_API_VERSION
}
/**
* Extension function to respond with versioned data
*/
suspend inline fun <reified T : VersionedDto> ApplicationCall.respondVersioned(
status: HttpStatusCode = HttpStatusCode.OK,
data: T
) {
val versionedResponse = VersionedResponse(
data = data,
version = VersionManager.getVersionInfo(),
timestamp = Clock.System.now().toString()
)
respond(status, versionedResponse)
}
/**
* Extension function to respond with versioned list data
*/
suspend inline fun <reified T : VersionedDto> ApplicationCall.respondVersionedList(
status: HttpStatusCode = HttpStatusCode.OK,
data: List<T>
) {
val response = mapOf(
"items" to data,
"count" to data.size,
"version" to VersionManager.getVersionInfo(),
"timestamp" to Clock.System.now().toString()
)
respond(status, response)
}
/**
* Configure versioning for the application
*/
fun Application.configureVersioning() {
install(VersioningPlugin)
}
@@ -1,7 +1,6 @@
package at.mocode.routes
import at.mocode.repositories.PostgresVereinRepository
import at.mocode.repositories.VereinRepository
import at.mocode.services.ServiceLocator
import at.mocode.stammdaten.Verein
import com.benasher44.uuid.uuidFrom
import io.ktor.http.*
@@ -10,13 +9,13 @@ import io.ktor.server.response.*
import io.ktor.server.routing.*
fun Route.vereinRoutes() {
val vereinRepository: VereinRepository = PostgresVereinRepository()
val vereinService = ServiceLocator.vereinService
route("/api/vereine") {
// GET /api/vereine - Get all clubs
get {
try {
val vereine = vereinRepository.findAll()
val vereine = vereinService.getAllVereine()
call.respond(HttpStatusCode.OK, vereine)
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to e.message))
@@ -31,7 +30,7 @@ fun Route.vereinRoutes() {
mapOf("error" to "Missing verein ID")
)
val uuid = uuidFrom(id)
val verein = vereinRepository.findById(uuid)
val verein = vereinService.getVereinById(uuid)
if (verein != null) {
call.respond(HttpStatusCode.OK, verein)
} else {
@@ -51,7 +50,7 @@ fun Route.vereinRoutes() {
HttpStatusCode.BadRequest,
mapOf("error" to "Missing OEPS Vereins number")
)
val verein = vereinRepository.findByOepsVereinsNr(oepsVereinsNr)
val verein = vereinService.getVereinByOepsNr(oepsVereinsNr)
if (verein != null) {
call.respond(HttpStatusCode.OK, verein)
} else {
@@ -69,7 +68,7 @@ fun Route.vereinRoutes() {
HttpStatusCode.BadRequest,
mapOf("error" to "Missing search query parameter 'q'")
)
val vereine = vereinRepository.search(query)
val vereine = vereinService.searchVereine(query)
call.respond(HttpStatusCode.OK, vereine)
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to e.message))
@@ -83,7 +82,7 @@ fun Route.vereinRoutes() {
HttpStatusCode.BadRequest,
mapOf("error" to "Missing bundesland")
)
val vereine = vereinRepository.findByBundesland(bundesland)
val vereine = vereinService.getVereineByBundesland(bundesland)
call.respond(HttpStatusCode.OK, vereine)
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to e.message))
@@ -94,7 +93,7 @@ fun Route.vereinRoutes() {
post {
try {
val verein = call.receive<Verein>()
val createdVerein = vereinRepository.create(verein)
val createdVerein = vereinService.createVerein(verein)
call.respond(HttpStatusCode.Created, createdVerein)
} catch (e: Exception) {
call.respond(HttpStatusCode.BadRequest, mapOf("error" to e.message))
@@ -110,7 +109,7 @@ fun Route.vereinRoutes() {
)
val uuid = uuidFrom(id)
val verein = call.receive<Verein>()
val updatedVerein = vereinRepository.update(uuid, verein)
val updatedVerein = vereinService.updateVerein(uuid, verein)
if (updatedVerein != null) {
call.respond(HttpStatusCode.OK, updatedVerein)
} else {
@@ -131,7 +130,7 @@ fun Route.vereinRoutes() {
mapOf("error" to "Missing verein ID")
)
val uuid = uuidFrom(id)
val deleted = vereinRepository.delete(uuid)
val deleted = vereinService.deleteVerein(uuid)
if (deleted) {
call.respond(HttpStatusCode.NoContent)
} else {
@@ -0,0 +1,192 @@
package at.mocode.services
import at.mocode.model.Abteilung
import at.mocode.repositories.AbteilungRepository
import com.benasher44.uuid.Uuid
/**
* Service layer for Abteilung (Division/Section) business logic.
* Handles business rules, validation, and coordinates with the repository layer.
*/
class AbteilungService(private val abteilungRepository: AbteilungRepository) {
/**
* Retrieve all divisions
*/
suspend fun getAllAbteilungen(): List<Abteilung> {
return abteilungRepository.findAll()
}
/**
* Find a division by its unique identifier
*/
suspend fun getAbteilungById(id: Uuid): Abteilung? {
return abteilungRepository.findById(id)
}
/**
* Find divisions by competition (Bewerb) ID
*/
suspend fun getAbteilungenByBewerbId(bewerbId: Uuid): List<Abteilung> {
return abteilungRepository.findByBewerbId(bewerbId)
}
/**
* Find divisions by active status
*/
suspend fun getAbteilungenByAktiv(istAktiv: Boolean): List<Abteilung> {
return abteilungRepository.findByAktiv(istAktiv)
}
/**
* Get all active divisions
*/
suspend fun getActiveAbteilungen(): List<Abteilung> {
return getAbteilungenByAktiv(true)
}
/**
* Get all inactive divisions
*/
suspend fun getInactiveAbteilungen(): List<Abteilung> {
return getAbteilungenByAktiv(false)
}
/**
* Search for divisions by query string
*/
suspend fun searchAbteilungen(query: String): List<Abteilung> {
if (query.isBlank()) {
throw IllegalArgumentException("Search query cannot be blank")
}
return abteilungRepository.search(query.trim())
}
/**
* Create a new division with business validation
*/
suspend fun createAbteilung(abteilung: Abteilung): Abteilung {
validateAbteilung(abteilung)
return abteilungRepository.create(abteilung)
}
/**
* Update an existing division
*/
suspend fun updateAbteilung(id: Uuid, abteilung: Abteilung): Abteilung? {
validateAbteilung(abteilung)
return abteilungRepository.update(id, abteilung)
}
/**
* Delete a division by ID
*/
suspend fun deleteAbteilung(id: Uuid): Boolean {
return abteilungRepository.delete(id)
}
/**
* Deactivate a division (soft delete)
*/
suspend fun deactivateAbteilung(id: Uuid): Abteilung? {
val abteilung = getAbteilungById(id)
return if (abteilung != null) {
val updatedAbteilung = abteilung.copy(istAktiv = false)
updateAbteilung(id, updatedAbteilung)
} else {
null
}
}
/**
* Activate a division
*/
suspend fun activateAbteilung(id: Uuid): Abteilung? {
val abteilung = getAbteilungById(id)
return if (abteilung != null) {
val updatedAbteilung = abteilung.copy(istAktiv = true)
updateAbteilung(id, updatedAbteilung)
} else {
null
}
}
/**
* Get divisions for a specific competition ordered by sequence
*/
suspend fun getAbteilungenForBewerbOrdered(bewerbId: Uuid): List<Abteilung> {
val abteilungen = getAbteilungenByBewerbId(bewerbId)
// Sort by abteilungsKennzeichen for basic ordering
return abteilungen.sortedBy { it.abteilungsKennzeichen }
}
/**
* Validate division data according to business rules
*/
private fun validateAbteilung(abteilung: Abteilung) {
if (abteilung.abteilungsKennzeichen.isBlank()) {
throw IllegalArgumentException("Division identifier (abteilungsKennzeichen) cannot be blank")
}
if (abteilung.abteilungsKennzeichen.length > 50) {
throw IllegalArgumentException("Division identifier cannot exceed 50 characters")
}
// Validate participant count constraints
if (abteilung.teilungsKriteriumAnzahlMin != null && abteilung.teilungsKriteriumAnzahlMin!! < 0) {
throw IllegalArgumentException("Minimum participant count cannot be negative")
}
if (abteilung.teilungsKriteriumAnzahlMax != null && abteilung.teilungsKriteriumAnzahlMax!! < 0) {
throw IllegalArgumentException("Maximum participant count cannot be negative")
}
if (abteilung.teilungsKriteriumAnzahlMin != null && abteilung.teilungsKriteriumAnzahlMax != null) {
if (abteilung.teilungsKriteriumAnzahlMin!! > abteilung.teilungsKriteriumAnzahlMax!!) {
throw IllegalArgumentException("Minimum participant count cannot be greater than maximum")
}
}
// Validate timing constraints
if (abteilung.dauerProStartGeschaetztSek != null && abteilung.dauerProStartGeschaetztSek!! < 0) {
throw IllegalArgumentException("Estimated duration per start cannot be negative")
}
if (abteilung.umbauzeitNachAbteilungMin != null && abteilung.umbauzeitNachAbteilungMin!! < 0) {
throw IllegalArgumentException("Setup time after division cannot be negative")
}
if (abteilung.besichtigungszeitVorAbteilungMin != null && abteilung.besichtigungszeitVorAbteilungMin!! < 0) {
throw IllegalArgumentException("Inspection time before division cannot be negative")
}
if (abteilung.stechzeitZusaetzlichMin != null && abteilung.stechzeitZusaetzlichMin!! < 0) {
throw IllegalArgumentException("Additional jump-off time cannot be negative")
}
if (abteilung.anzahlStarter < 0) {
throw IllegalArgumentException("Number of starters cannot be negative")
}
// Validate text field lengths
abteilung.bezeichnungIntern?.let { bezeichnung ->
if (bezeichnung.length > 255) {
throw IllegalArgumentException("Internal designation cannot exceed 255 characters")
}
}
abteilung.bezeichnungAufStartliste?.let { bezeichnung ->
if (bezeichnung.length > 255) {
throw IllegalArgumentException("Start list designation cannot exceed 255 characters")
}
}
abteilung.teilungsKriteriumFreiText?.let { freiText ->
if (freiText.length > 500) {
throw IllegalArgumentException("Free text division criterion cannot exceed 500 characters")
}
}
// Additional validation rules can be added here
}
}
@@ -0,0 +1,106 @@
package at.mocode.services
import at.mocode.model.Artikel
import at.mocode.repositories.ArtikelRepository
import com.benasher44.uuid.Uuid
import com.ionspin.kotlin.bignum.decimal.BigDecimal
/**
* Service layer for Artikel (Article) business logic.
* Handles business rules, validation, and coordinates with the repository layer.
*/
class ArtikelService(private val artikelRepository: ArtikelRepository) {
/**
* Retrieve all articles
*/
suspend fun getAllArtikel(): List<Artikel> {
return artikelRepository.findAll()
}
/**
* Find an article by its unique identifier
*/
suspend fun getArtikelById(id: Uuid): Artikel? {
return artikelRepository.findById(id)
}
/**
* Find articles by Verbandsabgabe status
*/
suspend fun getArtikelByVerbandsabgabe(istVerbandsabgabe: Boolean): List<Artikel> {
return artikelRepository.findByVerbandsabgabe(istVerbandsabgabe)
}
/**
* Search for articles by query string
*/
suspend fun searchArtikel(query: String): List<Artikel> {
if (query.isBlank()) {
throw IllegalArgumentException("Search query cannot be blank")
}
return artikelRepository.search(query.trim())
}
/**
* Create a new article with business validation
*/
suspend fun createArtikel(artikel: Artikel): Artikel {
validateArtikel(artikel)
return artikelRepository.create(artikel)
}
/**
* Update an existing article
*/
suspend fun updateArtikel(id: Uuid, artikel: Artikel): Artikel? {
validateArtikel(artikel)
return artikelRepository.update(id, artikel)
}
/**
* Delete an article by ID
*/
suspend fun deleteArtikel(id: Uuid): Boolean {
return artikelRepository.delete(id)
}
/**
* Get all Verbandsabgabe articles (federation fee articles)
*/
suspend fun getVerbandsabgabeArtikel(): List<Artikel> {
return getArtikelByVerbandsabgabe(true)
}
/**
* Get all non-Verbandsabgabe articles
*/
suspend fun getNonVerbandsabgabeArtikel(): List<Artikel> {
return getArtikelByVerbandsabgabe(false)
}
/**
* Validate article data according to business rules
*/
private fun validateArtikel(artikel: Artikel) {
if (artikel.bezeichnung.isBlank()) {
throw IllegalArgumentException("Article bezeichnung cannot be blank")
}
if (artikel.bezeichnung.length > 255) {
throw IllegalArgumentException("Article bezeichnung cannot exceed 255 characters")
}
if (artikel.preis < BigDecimal.ZERO) {
throw IllegalArgumentException("Article price cannot be negative")
}
if (artikel.einheit.isBlank()) {
throw IllegalArgumentException("Article einheit cannot be blank")
}
if (artikel.einheit.length > 50) {
throw IllegalArgumentException("Article einheit cannot exceed 50 characters")
}
}
}
@@ -0,0 +1,254 @@
package at.mocode.services
import at.mocode.model.Bewerb
import at.mocode.repositories.BewerbRepository
import com.benasher44.uuid.Uuid
/**
* Service layer for Bewerb (Competition) business logic.
* Handles business rules, validation, and coordinates with the repository layer.
*/
class BewerbService(private val bewerbRepository: BewerbRepository) {
/**
* Retrieve all competitions
*/
suspend fun getAllBewerbe(): List<Bewerb> {
return bewerbRepository.findAll()
}
/**
* Find a competition by its unique identifier
*/
suspend fun getBewerbById(id: Uuid): Bewerb? {
return bewerbRepository.findById(id)
}
/**
* Find competitions by tournament ID
*/
suspend fun getBewerbeByTurnierId(turnierId: Uuid): List<Bewerb> {
return bewerbRepository.findByTurnierId(turnierId)
}
/**
* Find competitions by sport category (Sparte)
*/
suspend fun getBewerbeBySparte(sparte: String): List<Bewerb> {
if (sparte.isBlank()) {
throw IllegalArgumentException("Sparte cannot be blank")
}
return bewerbRepository.findBySparte(sparte.trim())
}
/**
* Find competitions by class
*/
suspend fun getBewerbeByKlasse(klasse: String): List<Bewerb> {
if (klasse.isBlank()) {
throw IllegalArgumentException("Klasse cannot be blank")
}
return bewerbRepository.findByKlasse(klasse.trim())
}
/**
* Find competitions by start list finalization status
*/
suspend fun getBewerbeByStartlisteFinal(istFinal: Boolean): List<Bewerb> {
return bewerbRepository.findByStartlisteFinal(istFinal)
}
/**
* Find competitions by result list finalization status
*/
suspend fun getBewerbeByErgebnislisteFinal(istFinal: Boolean): List<Bewerb> {
return bewerbRepository.findByErgebnislisteFinal(istFinal)
}
/**
* Get competitions with finalized start lists
*/
suspend fun getBewerbeWithFinalStartliste(): List<Bewerb> {
return getBewerbeByStartlisteFinal(true)
}
/**
* Get competitions with finalized result lists
*/
suspend fun getBewerbeWithFinalErgebnisliste(): List<Bewerb> {
return getBewerbeByErgebnislisteFinal(true)
}
/**
* Search for competitions by query string
*/
suspend fun searchBewerbe(query: String): List<Bewerb> {
if (query.isBlank()) {
throw IllegalArgumentException("Search query cannot be blank")
}
return bewerbRepository.search(query.trim())
}
/**
* Create a new competition with business validation
*/
suspend fun createBewerb(bewerb: Bewerb): Bewerb {
validateBewerb(bewerb)
return bewerbRepository.create(bewerb)
}
/**
* Update an existing competition
*/
suspend fun updateBewerb(id: Uuid, bewerb: Bewerb): Bewerb? {
validateBewerb(bewerb)
return bewerbRepository.update(id, bewerb)
}
/**
* Delete a competition by ID
*/
suspend fun deleteBewerb(id: Uuid): Boolean {
return bewerbRepository.delete(id)
}
/**
* Finalize start list for a competition
*/
suspend fun finalizeStartliste(id: Uuid): Bewerb? {
val bewerb = getBewerbById(id)
return if (bewerb != null) {
val updatedBewerb = bewerb.copy(istStartlisteFinal = true)
updateBewerb(id, updatedBewerb)
} else {
null
}
}
/**
* Finalize result list for a competition
*/
suspend fun finalizeErgebnisliste(id: Uuid): Bewerb? {
val bewerb = getBewerbById(id)
return if (bewerb != null) {
val updatedBewerb = bewerb.copy(istErgebnislisteFinal = true)
updateBewerb(id, updatedBewerb)
} else {
null
}
}
/**
* Reopen start list for a competition
*/
suspend fun reopenStartliste(id: Uuid): Bewerb? {
val bewerb = getBewerbById(id)
return if (bewerb != null) {
val updatedBewerb = bewerb.copy(istStartlisteFinal = false)
updateBewerb(id, updatedBewerb)
} else {
null
}
}
/**
* Reopen result list for a competition
*/
suspend fun reopenErgebnisliste(id: Uuid): Bewerb? {
val bewerb = getBewerbById(id)
return if (bewerb != null) {
val updatedBewerb = bewerb.copy(istErgebnislisteFinal = false)
updateBewerb(id, updatedBewerb)
} else {
null
}
}
/**
* Get competitions for a specific tournament ordered by number
*/
suspend fun getBewerbeForTurnierOrdered(turnierId: Uuid): List<Bewerb> {
val bewerbe = getBewerbeByTurnierId(turnierId)
return bewerbe.sortedBy { it.nummer }
}
/**
* Validate competition data according to business rules
*/
private fun validateBewerb(bewerb: Bewerb) {
if (bewerb.nummer.isBlank()) {
throw IllegalArgumentException("Competition number cannot be blank")
}
if (bewerb.nummer.length > 50) {
throw IllegalArgumentException("Competition number cannot exceed 50 characters")
}
if (bewerb.bezeichnungOffiziell.isBlank()) {
throw IllegalArgumentException("Official designation cannot be blank")
}
if (bewerb.bezeichnungOffiziell.length > 255) {
throw IllegalArgumentException("Official designation cannot exceed 255 characters")
}
// Validate participant constraints
if (bewerb.maxPferdeProReiter != null && bewerb.maxPferdeProReiter!! < 1) {
throw IllegalArgumentException("Maximum horses per rider must be at least 1")
}
if (bewerb.anzahlRichterGeplant != null && bewerb.anzahlRichterGeplant!! < 1) {
throw IllegalArgumentException("Number of planned judges must be at least 1")
}
// Validate timing constraints
if (bewerb.standardDauerProStartGeschaetztSek != null && bewerb.standardDauerProStartGeschaetztSek!! < 0) {
throw IllegalArgumentException("Estimated duration per start cannot be negative")
}
if (bewerb.standardUmbauzeitNachBewerbMin != null && bewerb.standardUmbauzeitNachBewerbMin!! < 0) {
throw IllegalArgumentException("Setup time after competition cannot be negative")
}
if (bewerb.standardBesichtigungszeitVorBewerbMin != null && bewerb.standardBesichtigungszeitVorBewerbMin!! < 0) {
throw IllegalArgumentException("Inspection time before competition cannot be negative")
}
if (bewerb.standardStechzeitZusaetzlichMin != null && bewerb.standardStechzeitZusaetzlichMin!! < 0) {
throw IllegalArgumentException("Additional jump-off time cannot be negative")
}
// Validate text field lengths
bewerb.internerName?.let { name ->
if (name.length > 255) {
throw IllegalArgumentException("Internal name cannot exceed 255 characters")
}
}
bewerb.klasse?.let { klasse ->
if (klasse.length > 100) {
throw IllegalArgumentException("Class cannot exceed 100 characters")
}
}
bewerb.kategorieOetoDesBewerbs?.let { kategorie ->
if (kategorie.length > 100) {
throw IllegalArgumentException("ÖTO category cannot exceed 100 characters")
}
}
bewerb.teilnahmebedingungenText?.let { text ->
if (text.length > 1000) {
throw IllegalArgumentException("Participation conditions text cannot exceed 1000 characters")
}
}
bewerb.notizenIntern?.let { notizen ->
if (notizen.length > 1000) {
throw IllegalArgumentException("Internal notes cannot exceed 1000 characters")
}
}
// Additional validation rules can be added here
}
}
@@ -0,0 +1,132 @@
package at.mocode.services
import at.mocode.model.domaene.DomLizenz
import at.mocode.repositories.DomLizenzRepository
import com.benasher44.uuid.Uuid
/**
* Service layer for DomLizenz (Domain License) business logic.
* Handles business rules, validation, and coordinates with the repository layer.
*/
class DomLizenzService(private val domLizenzRepository: DomLizenzRepository) {
/**
* Retrieve all licenses
*/
suspend fun getAllLizenzen(): List<DomLizenz> {
return domLizenzRepository.findAll()
}
/**
* Find a license by its unique identifier
*/
suspend fun getLizenzById(id: Uuid): DomLizenz? {
return domLizenzRepository.findById(id)
}
/**
* Find licenses by person ID
*/
suspend fun getLizenzenByPersonId(personId: Uuid): List<DomLizenz> {
return domLizenzRepository.findByPersonId(personId)
}
/**
* Find licenses by license type global ID
*/
suspend fun getLizenzenByLizenzTypGlobalId(lizenzTypGlobalId: Uuid): List<DomLizenz> {
return domLizenzRepository.findByLizenzTypGlobalId(lizenzTypGlobalId)
}
/**
* Find active licenses by person ID
*/
suspend fun getActiveLizenzenByPersonId(personId: Uuid): List<DomLizenz> {
return domLizenzRepository.findActiveByPersonId(personId)
}
/**
* Find licenses by validity year
*/
suspend fun getLizenzenByValidityYear(year: Int): List<DomLizenz> {
if (year < 1900 || year > 2100) {
throw IllegalArgumentException("Year must be between 1900 and 2100")
}
return domLizenzRepository.findByValidityYear(year)
}
/**
* Search for licenses by query string
*/
suspend fun searchLizenzen(query: String): List<DomLizenz> {
if (query.isBlank()) {
throw IllegalArgumentException("Search query cannot be blank")
}
return domLizenzRepository.search(query.trim())
}
/**
* Create a new license with business validation
*/
suspend fun createLizenz(domLizenz: DomLizenz): DomLizenz {
validateLizenz(domLizenz)
return domLizenzRepository.create(domLizenz)
}
/**
* Update an existing license
*/
suspend fun updateLizenz(id: Uuid, domLizenz: DomLizenz): DomLizenz? {
validateLizenz(domLizenz)
return domLizenzRepository.update(id, domLizenz)
}
/**
* Delete a license by ID
*/
suspend fun deleteLizenz(id: Uuid): Boolean {
return domLizenzRepository.delete(id)
}
/**
* Check if a person has an active license of a specific type
*/
suspend fun hasActiveLicense(personId: Uuid, lizenzTypGlobalId: Uuid): Boolean {
val activeLicenses = getActiveLizenzenByPersonId(personId)
return activeLicenses.any { it.lizenzTypGlobalId == lizenzTypGlobalId }
}
/**
* Get current year licenses for a person
*/
suspend fun getCurrentYearLizenzenByPersonId(personId: Uuid): List<DomLizenz> {
val currentYear = java.time.LocalDate.now().year
val allPersonLicenses = getLizenzenByPersonId(personId)
return allPersonLicenses.filter { license ->
license.gueltigBisJahr == currentYear || license.ausgestelltAm?.year == currentYear
}
}
/**
* Validate license data according to business rules
*/
private fun validateLizenz(domLizenz: DomLizenz) {
// Validate that gueltigBisJahr is reasonable if provided
domLizenz.gueltigBisJahr?.let { year ->
if (year < 1900 || year > 2100) {
throw IllegalArgumentException("License validity year must be between 1900 and 2100")
}
}
// Validate that ausgestelltAm is not in the future if provided
domLizenz.ausgestelltAm?.let { date ->
val currentYear = java.time.LocalDate.now().year
if (date.year > currentYear) {
throw IllegalArgumentException("License issue date cannot be in the future")
}
}
// Additional validation rules can be added here
// For example, checking if the license type is valid, person exists, etc.
}
}
@@ -0,0 +1,204 @@
package at.mocode.services
import at.mocode.model.domaene.DomPferd
import at.mocode.repositories.DomPferdRepository
import com.benasher44.uuid.Uuid
/**
* Service layer for DomPferd (Domain Horse) business logic.
* Handles business rules, validation, and coordinates with the repository layer.
*/
class DomPferdService(private val domPferdRepository: DomPferdRepository) {
/**
* Retrieve all horses
*/
suspend fun getAllPferde(): List<DomPferd> {
return domPferdRepository.findAll()
}
/**
* Find a horse by its unique identifier
*/
suspend fun getPferdById(id: Uuid): DomPferd? {
return domPferdRepository.findById(id)
}
/**
* Find a horse by its OEPS Satz number
*/
suspend fun getPferdByOepsSatzNr(oepsSatzNr: String): DomPferd? {
if (oepsSatzNr.isBlank()) {
throw IllegalArgumentException("OEPS Satz number cannot be blank")
}
return domPferdRepository.findByOepsSatzNr(oepsSatzNr)
}
/**
* Find horses by name
*/
suspend fun getPferdeByName(name: String): List<DomPferd> {
if (name.isBlank()) {
throw IllegalArgumentException("Horse name cannot be blank")
}
return domPferdRepository.findByName(name.trim())
}
/**
* Find a horse by its life number (Lebensnummer)
*/
suspend fun getPferdByLebensnummer(lebensnummer: String): DomPferd? {
if (lebensnummer.isBlank()) {
throw IllegalArgumentException("Life number cannot be blank")
}
return domPferdRepository.findByLebensnummer(lebensnummer)
}
/**
* Find horses by owner ID
*/
suspend fun getPferdeByBesitzerId(besitzerId: Uuid): List<DomPferd> {
return domPferdRepository.findByBesitzerId(besitzerId)
}
/**
* Find horses by responsible person ID
*/
suspend fun getPferdeByVerantwortlichePersonId(personId: Uuid): List<DomPferd> {
return domPferdRepository.findByVerantwortlichePersonId(personId)
}
/**
* Find horses by home club ID
*/
suspend fun getPferdeByHeimatVereinId(vereinId: Uuid): List<DomPferd> {
return domPferdRepository.findByHeimatVereinId(vereinId)
}
/**
* Find horses by breed
*/
suspend fun getPferdeByRasse(rasse: String): List<DomPferd> {
if (rasse.isBlank()) {
throw IllegalArgumentException("Breed cannot be blank")
}
return domPferdRepository.findByRasse(rasse.trim())
}
/**
* Find horses by birth year
*/
suspend fun getPferdeByGeburtsjahr(geburtsjahr: Int): List<DomPferd> {
if (geburtsjahr < 1900 || geburtsjahr > java.time.LocalDate.now().year) {
throw IllegalArgumentException("Birth year must be between 1900 and current year")
}
return domPferdRepository.findByGeburtsjahr(geburtsjahr)
}
/**
* Find all active horses
*/
suspend fun getActivePferde(): List<DomPferd> {
return domPferdRepository.findActiveHorses()
}
/**
* Search for horses by query string
*/
suspend fun searchPferde(query: String): List<DomPferd> {
if (query.isBlank()) {
throw IllegalArgumentException("Search query cannot be blank")
}
return domPferdRepository.search(query.trim())
}
/**
* Create a new horse with business validation
*/
suspend fun createPferd(domPferd: DomPferd): DomPferd {
validatePferd(domPferd)
// Check if OEPS Satz number already exists
domPferd.oepsSatzNrPferd?.let { oepsNr ->
val existing = domPferdRepository.findByOepsSatzNr(oepsNr)
if (existing != null) {
throw IllegalArgumentException("A horse with OEPS Satz number '$oepsNr' already exists")
}
}
// Check if life number already exists
domPferd.lebensnummer?.let { lebensnummer ->
val existing = domPferdRepository.findByLebensnummer(lebensnummer)
if (existing != null) {
throw IllegalArgumentException("A horse with life number '$lebensnummer' already exists")
}
}
return domPferdRepository.create(domPferd)
}
/**
* Update an existing horse
*/
suspend fun updatePferd(id: Uuid, domPferd: DomPferd): DomPferd? {
validatePferd(domPferd)
// Check if OEPS Satz number conflicts with another horse
domPferd.oepsSatzNrPferd?.let { oepsNr ->
val existing = domPferdRepository.findByOepsSatzNr(oepsNr)
if (existing != null && existing.pferdId != id) {
throw IllegalArgumentException("A horse with OEPS Satz number '$oepsNr' already exists")
}
}
// Check if life number conflicts with another horse
domPferd.lebensnummer?.let { lebensnummer ->
val existing = domPferdRepository.findByLebensnummer(lebensnummer)
if (existing != null && existing.pferdId != id) {
throw IllegalArgumentException("A horse with life number '$lebensnummer' already exists")
}
}
return domPferdRepository.update(id, domPferd)
}
/**
* Delete a horse by ID
*/
suspend fun deletePferd(id: Uuid): Boolean {
return domPferdRepository.delete(id)
}
/**
* Validate horse data according to business rules
*/
private fun validatePferd(domPferd: DomPferd) {
if (domPferd.name.isBlank()) {
throw IllegalArgumentException("Horse name cannot be blank")
}
if (domPferd.name.length > 100) {
throw IllegalArgumentException("Horse name cannot exceed 100 characters")
}
// Validate birth year if provided
domPferd.geburtsjahr?.let { year ->
if (year < 1900 || year > java.time.LocalDate.now().year) {
throw IllegalArgumentException("Birth year must be between 1900 and current year")
}
}
// Additional validation rules can be added here
domPferd.oepsSatzNrPferd?.let { oepsNr ->
if (oepsNr.isBlank()) {
throw IllegalArgumentException("OEPS Satz number cannot be blank if provided")
}
}
domPferd.lebensnummer?.let { lebensnummer ->
if (lebensnummer.isBlank()) {
throw IllegalArgumentException("Life number cannot be blank if provided")
}
}
}
}
@@ -0,0 +1,157 @@
package at.mocode.services
import at.mocode.model.domaene.DomQualifikation
import at.mocode.repositories.DomQualifikationRepository
import com.benasher44.uuid.Uuid
import kotlinx.datetime.LocalDate
/**
* Service layer for DomQualifikation (Domain Qualification) business logic.
* Handles business rules, validation, and coordinates with the repository layer.
*/
class DomQualifikationService(private val domQualifikationRepository: DomQualifikationRepository) {
/**
* Retrieve all qualifications
*/
suspend fun getAllQualifikationen(): List<DomQualifikation> {
return domQualifikationRepository.findAll()
}
/**
* Find a qualification by its unique identifier
*/
suspend fun getQualifikationById(id: Uuid): DomQualifikation? {
return domQualifikationRepository.findById(id)
}
/**
* Find qualifications by person ID
*/
suspend fun getQualifikationenByPersonId(personId: Uuid): List<DomQualifikation> {
return domQualifikationRepository.findByPersonId(personId)
}
/**
* Find qualifications by qualification type ID
*/
suspend fun getQualifikationenByQualTypId(qualTypId: Uuid): List<DomQualifikation> {
return domQualifikationRepository.findByQualTypId(qualTypId)
}
/**
* Find active qualifications by person ID
*/
suspend fun getActiveQualifikationenByPersonId(personId: Uuid): List<DomQualifikation> {
return domQualifikationRepository.findActiveByPersonId(personId)
}
/**
* Find qualifications by validity period
*/
suspend fun getQualifikationenByValidityPeriod(fromDate: LocalDate?, toDate: LocalDate?): List<DomQualifikation> {
// Validate date range if both dates are provided
if (fromDate != null && toDate != null && fromDate > toDate) {
throw IllegalArgumentException("From date must be before or equal to to date")
}
return domQualifikationRepository.findByValidityPeriod(fromDate, toDate)
}
/**
* Search for qualifications by query string
*/
suspend fun searchQualifikationen(query: String): List<DomQualifikation> {
if (query.isBlank()) {
throw IllegalArgumentException("Search query cannot be blank")
}
return domQualifikationRepository.search(query.trim())
}
/**
* Create a new qualification with business validation
*/
suspend fun createQualifikation(domQualifikation: DomQualifikation): DomQualifikation {
validateQualifikation(domQualifikation)
return domQualifikationRepository.create(domQualifikation)
}
/**
* Update an existing qualification
*/
suspend fun updateQualifikation(id: Uuid, domQualifikation: DomQualifikation): DomQualifikation? {
validateQualifikation(domQualifikation)
return domQualifikationRepository.update(id, domQualifikation)
}
/**
* Delete a qualification by ID
*/
suspend fun deleteQualifikation(id: Uuid): Boolean {
return domQualifikationRepository.delete(id)
}
/**
* Check if a person has an active qualification of a specific type
*/
suspend fun hasActiveQualification(personId: Uuid, qualTypId: Uuid): Boolean {
val activeQualifications = getActiveQualifikationenByPersonId(personId)
return activeQualifications.any { it.qualTypId == qualTypId }
}
/**
* Get current valid qualifications for a person
*/
suspend fun getCurrentValidQualifikationenByPersonId(personId: Uuid): List<DomQualifikation> {
val currentJavaDate = java.time.LocalDate.now()
val currentLocalDate = kotlinx.datetime.LocalDate(currentJavaDate.year, currentJavaDate.monthValue, currentJavaDate.dayOfMonth)
val allPersonQualifications = getQualifikationenByPersonId(personId)
return allPersonQualifications.filter { qualification ->
qualification.istAktiv &&
(qualification.gueltigVon == null || qualification.gueltigVon!! <= currentLocalDate) &&
(qualification.gueltigBis == null || qualification.gueltigBis!! >= currentLocalDate)
}
}
/**
* Deactivate a qualification (soft delete)
*/
suspend fun deactivateQualifikation(id: Uuid): DomQualifikation? {
val qualification = getQualifikationById(id)
return if (qualification != null) {
val updatedQualification = qualification.copy(istAktiv = false)
updateQualifikation(id, updatedQualification)
} else {
null
}
}
/**
* Validate qualification data according to business rules
*/
private fun validateQualifikation(domQualifikation: DomQualifikation) {
// Validate validity date range if both dates are provided
if (domQualifikation.gueltigVon != null && domQualifikation.gueltigBis != null) {
if (domQualifikation.gueltigVon!! > domQualifikation.gueltigBis!!) {
throw IllegalArgumentException("Qualification validity start date must be before or equal to end date")
}
}
// Validate that gueltigBis is not in the past for new active qualifications
if (domQualifikation.istAktiv && domQualifikation.gueltigBis != null) {
val currentJavaDate = java.time.LocalDate.now()
val currentLocalDate = kotlinx.datetime.LocalDate(currentJavaDate.year, currentJavaDate.monthValue, currentJavaDate.dayOfMonth)
if (domQualifikation.gueltigBis!! < currentLocalDate) {
throw IllegalArgumentException("Cannot create active qualification with end date in the past")
}
}
// Additional validation rules can be added here
domQualifikation.bemerkung?.let { bemerkung ->
if (bemerkung.length > 1000) {
throw IllegalArgumentException("Qualification remark cannot exceed 1000 characters")
}
}
}
}
@@ -0,0 +1,122 @@
package at.mocode.services
import at.mocode.stammdaten.Person
import at.mocode.repositories.PersonRepository
import com.benasher44.uuid.Uuid
/**
* Service layer for Person business logic.
* Handles business rules, validation, and coordinates with the repository layer.
*/
class PersonService(private val personRepository: PersonRepository) {
/**
* Retrieve all persons
*/
suspend fun getAllPersons(): List<Person> {
return personRepository.findAll()
}
/**
* Find a person by their unique identifier
*/
suspend fun getPersonById(id: Uuid): Person? {
return personRepository.findById(id)
}
/**
* Find a person by their OEPS Satz number
*/
suspend fun getPersonByOepsSatzNr(oepsSatzNr: String): Person? {
if (oepsSatzNr.isBlank()) {
throw IllegalArgumentException("OEPS Satz number cannot be blank")
}
return personRepository.findByOepsSatzNr(oepsSatzNr)
}
/**
* Find persons by Verein (club) ID
*/
suspend fun getPersonsByVereinId(vereinId: Uuid): List<Person> {
return personRepository.findByVereinId(vereinId)
}
/**
* Search for persons by query string
*/
suspend fun searchPersons(query: String): List<Person> {
if (query.isBlank()) {
throw IllegalArgumentException("Search query cannot be blank")
}
return personRepository.search(query.trim())
}
/**
* Create a new person with business validation
*/
suspend fun createPerson(person: Person): Person {
validatePerson(person)
// Check if OEPS Satz number already exists
person.oepsSatzNr?.let { oepsNr ->
val existing = personRepository.findByOepsSatzNr(oepsNr)
if (existing != null) {
throw IllegalArgumentException("A person with OEPS Satz number '$oepsNr' already exists")
}
}
return personRepository.create(person)
}
/**
* Update an existing person
*/
suspend fun updatePerson(id: Uuid, person: Person): Person? {
validatePerson(person)
// Check if OEPS Satz number conflicts with another person
person.oepsSatzNr?.let { oepsNr ->
val existing = personRepository.findByOepsSatzNr(oepsNr)
if (existing != null && existing.id != id) {
throw IllegalArgumentException("A person with OEPS Satz number '$oepsNr' already exists")
}
}
return personRepository.update(id, person)
}
/**
* Delete a person by ID
*/
suspend fun deletePerson(id: Uuid): Boolean {
return personRepository.delete(id)
}
/**
* Validate person data according to business rules
*/
private fun validatePerson(person: Person) {
if (person.vorname.isBlank()) {
throw IllegalArgumentException("Person first name cannot be blank")
}
if (person.nachname.isBlank()) {
throw IllegalArgumentException("Person last name cannot be blank")
}
if (person.vorname.length > 100) {
throw IllegalArgumentException("Person first name cannot exceed 100 characters")
}
if (person.nachname.length > 100) {
throw IllegalArgumentException("Person last name cannot exceed 100 characters")
}
// Additional validation rules can be added here
person.oepsSatzNr?.let { oepsNr ->
if (oepsNr.isBlank()) {
throw IllegalArgumentException("OEPS Satz number cannot be blank if provided")
}
}
}
}
@@ -3,8 +3,8 @@ package at.mocode.services
import at.mocode.repositories.*
/**
* Service locator pattern for managing repository instances.
* This provides a centralized way to access repository implementations
* Service locator pattern for managing repository and service instances.
* This provides a centralized way to access repository and service implementations
* and makes it easier to switch implementations or add caching/decorators.
*/
object ServiceLocator {
@@ -21,10 +21,23 @@ object ServiceLocator {
val turnierRepository: TurnierRepository by lazy { PostgresTurnierRepository() }
val veranstaltungRepository: VeranstaltungRepository by lazy { PostgresVeranstaltungRepository() }
// Service instances - lazy initialization with dependency injection
val artikelService: ArtikelService by lazy { ArtikelService(artikelRepository) }
val vereinService: VereinService by lazy { VereinService(vereinRepository) }
val personService: PersonService by lazy { PersonService(personRepository) }
val domLizenzService: DomLizenzService by lazy { DomLizenzService(domLizenzRepository) }
val domPferdService: DomPferdService by lazy { DomPferdService(domPferdRepository) }
val domQualifikationService: DomQualifikationService by lazy { DomQualifikationService(domQualifikationRepository) }
val abteilungService: AbteilungService by lazy { AbteilungService(abteilungRepository) }
val bewerbService: BewerbService by lazy { BewerbService(bewerbRepository) }
val turnierService: TurnierService by lazy { TurnierService(turnierRepository) }
val veranstaltungService: VeranstaltungService by lazy { VeranstaltungService(veranstaltungRepository) }
/**
* Initialize all repositories - useful for eager loading or validation
* Initialize all repositories and services - useful for eager loading or validation
*/
fun initializeAll() {
// Initialize repositories
artikelRepository
vereinRepository
personRepository
@@ -35,5 +48,17 @@ object ServiceLocator {
bewerbRepository
turnierRepository
veranstaltungRepository
// Initialize services
artikelService
vereinService
personService
domLizenzService
domPferdService
domQualifikationService
abteilungService
bewerbService
turnierService
veranstaltungService
}
}
@@ -0,0 +1,124 @@
package at.mocode.services
import at.mocode.model.Turnier
import at.mocode.repositories.TurnierRepository
import com.benasher44.uuid.Uuid
/**
* Service layer for Turnier (Tournament) business logic.
* Handles business rules, validation, and coordinates with the repository layer.
*/
class TurnierService(private val turnierRepository: TurnierRepository) {
/**
* Retrieve all tournaments
*/
suspend fun getAllTurniere(): List<Turnier> {
return turnierRepository.findAll()
}
/**
* Find a tournament by its unique identifier
*/
suspend fun getTurnierById(id: Uuid): Turnier? {
return turnierRepository.findById(id)
}
/**
* Find tournaments by event (Veranstaltung) ID
*/
suspend fun getTurniereByVeranstaltungId(veranstaltungId: Uuid): List<Turnier> {
return turnierRepository.findByVeranstaltungId(veranstaltungId)
}
/**
* Find a tournament by its OEPS tournament number
*/
suspend fun getTurnierByOepsTurnierNr(oepsTurnierNr: String): Turnier? {
if (oepsTurnierNr.isBlank()) {
throw IllegalArgumentException("OEPS tournament number cannot be blank")
}
return turnierRepository.findByOepsTurnierNr(oepsTurnierNr)
}
/**
* Search for tournaments by query string
*/
suspend fun searchTurniere(query: String): List<Turnier> {
if (query.isBlank()) {
throw IllegalArgumentException("Search query cannot be blank")
}
return turnierRepository.search(query.trim())
}
/**
* Create a new tournament with business validation
*/
suspend fun createTurnier(turnier: Turnier): Turnier {
validateTurnier(turnier)
// Check if OEPS tournament number already exists
turnier.oepsTurnierNr?.let { oepsNr ->
val existing = turnierRepository.findByOepsTurnierNr(oepsNr)
if (existing != null) {
throw IllegalArgumentException("A tournament with OEPS number '$oepsNr' already exists")
}
}
return turnierRepository.create(turnier)
}
/**
* Update an existing tournament
*/
suspend fun updateTurnier(id: Uuid, turnier: Turnier): Turnier? {
validateTurnier(turnier)
// Check if OEPS tournament number conflicts with another tournament
turnier.oepsTurnierNr?.let { oepsNr ->
val existing = turnierRepository.findByOepsTurnierNr(oepsNr)
if (existing != null && existing.id != id) {
throw IllegalArgumentException("A tournament with OEPS number '$oepsNr' already exists")
}
}
return turnierRepository.update(id, turnier)
}
/**
* Delete a tournament by ID
*/
suspend fun deleteTurnier(id: Uuid): Boolean {
return turnierRepository.delete(id)
}
/**
* Get tournaments for a specific event
*/
suspend fun getTurniereForEvent(veranstaltungId: Uuid): List<Turnier> {
return getTurniereByVeranstaltungId(veranstaltungId)
}
/**
* Validate tournament data according to business rules
*/
private fun validateTurnier(turnier: Turnier) {
if (turnier.titel.isBlank()) {
throw IllegalArgumentException("Tournament title cannot be blank")
}
if (turnier.titel.length > 255) {
throw IllegalArgumentException("Tournament title cannot exceed 255 characters")
}
// Validate dates
if (turnier.datumVon > turnier.datumBis) {
throw IllegalArgumentException("Tournament start date must be before or equal to end date")
}
// Additional validation rules can be added here
if (turnier.oepsTurnierNr.isBlank()) {
throw IllegalArgumentException("OEPS tournament number cannot be blank")
}
}
}
@@ -0,0 +1,245 @@
package at.mocode.services
import at.mocode.model.Veranstaltung
import at.mocode.repositories.VeranstaltungRepository
import com.benasher44.uuid.Uuid
/**
* Service layer for Veranstaltung (Event) business logic.
* Handles business rules, validation, and coordinates with the repository layer.
*/
class VeranstaltungService(private val veranstaltungRepository: VeranstaltungRepository) {
/**
* Retrieve all events
*/
suspend fun getAllVeranstaltungen(): List<Veranstaltung> {
return veranstaltungRepository.findAll()
}
/**
* Find an event by its unique identifier
*/
suspend fun getVeranstaltungById(id: Uuid): Veranstaltung? {
return veranstaltungRepository.findById(id)
}
/**
* Find events by name
*/
suspend fun getVeranstaltungenByName(name: String): List<Veranstaltung> {
if (name.isBlank()) {
throw IllegalArgumentException("Event name cannot be blank")
}
return veranstaltungRepository.findByName(name.trim())
}
/**
* Find events by organizer OEPS number
*/
suspend fun getVeranstaltungenByVeranstalterOepsNummer(oepsNummer: String): List<Veranstaltung> {
if (oepsNummer.isBlank()) {
throw IllegalArgumentException("Organizer OEPS number cannot be blank")
}
return veranstaltungRepository.findByVeranstalterOepsNummer(oepsNummer.trim())
}
/**
* Search for events by query string
*/
suspend fun searchVeranstaltungen(query: String): List<Veranstaltung> {
if (query.isBlank()) {
throw IllegalArgumentException("Search query cannot be blank")
}
return veranstaltungRepository.search(query.trim())
}
/**
* Create a new event with business validation
*/
suspend fun createVeranstaltung(veranstaltung: Veranstaltung): Veranstaltung {
validateVeranstaltung(veranstaltung)
return veranstaltungRepository.create(veranstaltung)
}
/**
* Update an existing event
*/
suspend fun updateVeranstaltung(id: Uuid, veranstaltung: Veranstaltung): Veranstaltung? {
validateVeranstaltung(veranstaltung)
return veranstaltungRepository.update(id, veranstaltung)
}
/**
* Delete an event by ID
*/
suspend fun deleteVeranstaltung(id: Uuid): Boolean {
return veranstaltungRepository.delete(id)
}
/**
* Get events happening in a specific year
*/
suspend fun getVeranstaltungenByYear(year: Int): List<Veranstaltung> {
if (year < 1900 || year > 2100) {
throw IllegalArgumentException("Year must be between 1900 and 2100")
}
val allEvents = getAllVeranstaltungen()
return allEvents.filter { event ->
event.datumVon.year == year || event.datumBis.year == year ||
(event.datumVon.year < year && event.datumBis.year > year)
}
}
/**
* Get current events (happening now or in the future)
*/
suspend fun getCurrentAndFutureVeranstaltungen(): List<Veranstaltung> {
val currentJavaDate = java.time.LocalDate.now()
val currentLocalDate = kotlinx.datetime.LocalDate(currentJavaDate.year, currentJavaDate.monthValue, currentJavaDate.dayOfMonth)
val allEvents = getAllVeranstaltungen()
return allEvents.filter { event ->
event.datumBis >= currentLocalDate
}.sortedBy { it.datumVon }
}
/**
* Get past events
*/
suspend fun getPastVeranstaltungen(): List<Veranstaltung> {
val currentJavaDate = java.time.LocalDate.now()
val currentLocalDate = kotlinx.datetime.LocalDate(currentJavaDate.year, currentJavaDate.monthValue, currentJavaDate.dayOfMonth)
val allEvents = getAllVeranstaltungen()
return allEvents.filter { event ->
event.datumBis < currentLocalDate
}.sortedByDescending { it.datumVon }
}
/**
* Get events by organizer name
*/
suspend fun getVeranstaltungenByVeranstalterName(veranstalterName: String): List<Veranstaltung> {
if (veranstalterName.isBlank()) {
throw IllegalArgumentException("Organizer name cannot be blank")
}
val allEvents = getAllVeranstaltungen()
return allEvents.filter { event ->
event.veranstalterName.contains(veranstalterName.trim(), ignoreCase = true)
}
}
/**
* Get events by venue name
*/
suspend fun getVeranstaltungenByVenanstaltungsort(veranstaltungsortName: String): List<Veranstaltung> {
if (veranstaltungsortName.isBlank()) {
throw IllegalArgumentException("Venue name cannot be blank")
}
val allEvents = getAllVeranstaltungen()
return allEvents.filter { event ->
event.veranstaltungsortName.contains(veranstaltungsortName.trim(), ignoreCase = true)
}
}
/**
* Validate event data according to business rules
*/
private fun validateVeranstaltung(veranstaltung: Veranstaltung) {
if (veranstaltung.name.isBlank()) {
throw IllegalArgumentException("Event name cannot be blank")
}
if (veranstaltung.name.length > 255) {
throw IllegalArgumentException("Event name cannot exceed 255 characters")
}
if (veranstaltung.veranstalterName.isBlank()) {
throw IllegalArgumentException("Organizer name cannot be blank")
}
if (veranstaltung.veranstalterName.length > 255) {
throw IllegalArgumentException("Organizer name cannot exceed 255 characters")
}
if (veranstaltung.veranstaltungsortName.isBlank()) {
throw IllegalArgumentException("Venue name cannot be blank")
}
if (veranstaltung.veranstaltungsortName.length > 255) {
throw IllegalArgumentException("Venue name cannot exceed 255 characters")
}
if (veranstaltung.veranstaltungsortAdresse.isBlank()) {
throw IllegalArgumentException("Venue address cannot be blank")
}
if (veranstaltung.veranstaltungsortAdresse.length > 500) {
throw IllegalArgumentException("Venue address cannot exceed 500 characters")
}
// Validate date range
if (veranstaltung.datumVon > veranstaltung.datumBis) {
throw IllegalArgumentException("Event start date must be before or equal to end date")
}
// Validate optional fields
veranstaltung.veranstalterOepsNummer?.let { oepsNr ->
if (oepsNr.isBlank()) {
throw IllegalArgumentException("Organizer OEPS number cannot be blank if provided")
}
}
veranstaltung.kontaktpersonName?.let { name ->
if (name.length > 255) {
throw IllegalArgumentException("Contact person name cannot exceed 255 characters")
}
}
veranstaltung.kontaktTelefon?.let { telefon ->
if (telefon.length > 50) {
throw IllegalArgumentException("Contact phone cannot exceed 50 characters")
}
}
veranstaltung.kontaktEmail?.let { email ->
if (email.length > 255) {
throw IllegalArgumentException("Contact email cannot exceed 255 characters")
}
// Basic email validation
if (!email.contains("@") || !email.contains(".")) {
throw IllegalArgumentException("Contact email must be a valid email address")
}
}
veranstaltung.webseite?.let { webseite ->
if (webseite.length > 500) {
throw IllegalArgumentException("Website URL cannot exceed 500 characters")
}
}
veranstaltung.dsgvoText?.let { text ->
if (text.length > 2000) {
throw IllegalArgumentException("DSGVO text cannot exceed 2000 characters")
}
}
veranstaltung.haftungsText?.let { text ->
if (text.length > 2000) {
throw IllegalArgumentException("Liability text cannot exceed 2000 characters")
}
}
veranstaltung.sonstigeBesondereBestimmungen?.let { text ->
if (text.length > 2000) {
throw IllegalArgumentException("Special provisions text cannot exceed 2000 characters")
}
}
// Additional validation rules can be added here
}
}
@@ -0,0 +1,117 @@
package at.mocode.services
import at.mocode.stammdaten.Verein
import at.mocode.repositories.VereinRepository
import com.benasher44.uuid.Uuid
/**
* Service layer for Verein (Club) business logic.
* Handles business rules, validation, and coordinates with the repository layer.
*/
class VereinService(private val vereinRepository: VereinRepository) {
/**
* Retrieve all clubs
*/
suspend fun getAllVereine(): List<Verein> {
return vereinRepository.findAll()
}
/**
* Find a club by its unique identifier
*/
suspend fun getVereinById(id: Uuid): Verein? {
return vereinRepository.findById(id)
}
/**
* Find a club by its OEPS (Austrian Equestrian Federation) number
*/
suspend fun getVereinByOepsNr(oepsVereinsNr: String): Verein? {
if (oepsVereinsNr.isBlank()) {
throw IllegalArgumentException("OEPS Vereins number cannot be blank")
}
return vereinRepository.findByOepsVereinsNr(oepsVereinsNr)
}
/**
* Search for clubs by query string
*/
suspend fun searchVereine(query: String): List<Verein> {
if (query.isBlank()) {
throw IllegalArgumentException("Search query cannot be blank")
}
return vereinRepository.search(query.trim())
}
/**
* Find clubs by federal state (Bundesland)
*/
suspend fun getVereineByBundesland(bundesland: String): List<Verein> {
if (bundesland.isBlank()) {
throw IllegalArgumentException("Bundesland cannot be blank")
}
return vereinRepository.findByBundesland(bundesland)
}
/**
* Create a new club with business validation
*/
suspend fun createVerein(verein: Verein): Verein {
validateVerein(verein)
// Check if OEPS number already exists
verein.oepsVereinsNr?.let { oepsNr ->
val existing = vereinRepository.findByOepsVereinsNr(oepsNr)
if (existing != null) {
throw IllegalArgumentException("A club with OEPS number '$oepsNr' already exists")
}
}
return vereinRepository.create(verein)
}
/**
* Update an existing club
*/
suspend fun updateVerein(id: Uuid, verein: Verein): Verein? {
validateVerein(verein)
// Check if OEPS number conflicts with another club
verein.oepsVereinsNr?.let { oepsNr ->
val existing = vereinRepository.findByOepsVereinsNr(oepsNr)
if (existing != null && existing.id != id) {
throw IllegalArgumentException("A club with OEPS number '$oepsNr' already exists")
}
}
return vereinRepository.update(id, verein)
}
/**
* Delete a club by ID
*/
suspend fun deleteVerein(id: Uuid): Boolean {
return vereinRepository.delete(id)
}
/**
* Validate club data according to business rules
*/
private fun validateVerein(verein: Verein) {
if (verein.name.isBlank()) {
throw IllegalArgumentException("Club name cannot be blank")
}
if (verein.name.length > 255) {
throw IllegalArgumentException("Club name cannot exceed 255 characters")
}
// Additional validation rules can be added here
verein.oepsVereinsNr?.let { oepsNr ->
if (oepsNr.isBlank()) {
throw IllegalArgumentException("OEPS Vereins number cannot be blank if provided")
}
}
}
}
@@ -0,0 +1,116 @@
package at.mocode
import at.mocode.dto.ArtikelDto
import at.mocode.dto.VereinDto
import at.mocode.dto.base.VersionManager
import at.mocode.dto.base.VersionValidationResult
import at.mocode.dto.migrations.ArtikelDtoMigrator
import com.benasher44.uuid.uuid4
import com.ionspin.kotlin.bignum.decimal.BigDecimal
import kotlinx.datetime.Clock
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertIs
import kotlin.test.assertTrue
class VersioningTest {
@Test
fun testVersionManagerValidation() {
// Test valid version
val validResult = VersionManager.validateClientVersion("1.0")
assertIs<VersionValidationResult.Valid>(validResult)
assertEquals("1.0", validResult.version)
// Test unsupported version
val unsupportedResult = VersionManager.validateClientVersion("2.0")
assertIs<VersionValidationResult.UnsupportedVersion>(unsupportedResult)
assertEquals("2.0", unsupportedResult.version)
// Test missing version
val missingResult = VersionManager.validateClientVersion(null)
assertIs<VersionValidationResult.MissingVersion>(missingResult)
}
@Test
fun testVersionManagerInfo() {
val versionInfo = VersionManager.getVersionInfo()
assertEquals("1.0", versionInfo.apiVersion)
assertTrue(versionInfo.supportedVersions.contains("1.0"))
assertEquals("1.0", versionInfo.minimumClientVersion)
}
@Test
fun testArtikelDtoVersioning() {
val artikel = ArtikelDto(
id = uuid4(),
bezeichnung = "Test Artikel",
preis = BigDecimal.fromInt(100),
einheit = "Stück",
istVerbandsabgabe = false,
createdAt = Clock.System.now(),
updatedAt = Clock.System.now(),
schemaVersion = "1.0",
dataVersion = 1L
)
assertEquals("1.0", artikel.schemaVersion)
assertEquals(1L, artikel.dataVersion)
}
@Test
fun testVereinDtoVersioning() {
val verein = VereinDto(
id = uuid4(),
oepsVereinsNr = "12345",
name = "Test Verein",
kuerzel = "TV",
bundesland = "Wien",
adresse = "Teststraße 1",
plz = "1010",
ort = "Wien",
email = "test@verein.at",
telefon = "+43123456789",
webseite = "www.testverein.at",
istAktiv = true,
createdAt = Clock.System.now(),
updatedAt = Clock.System.now(),
schemaVersion = "1.0",
dataVersion = 1L
)
assertEquals("1.0", verein.schemaVersion)
assertEquals(1L, verein.dataVersion)
}
@Test
fun testArtikelDtoMigrator() {
val migrator = ArtikelDtoMigrator()
// Test migration capability
assertTrue(migrator.canMigrate("1.0", "1.0"))
val artikel = ArtikelDto(
id = uuid4(),
bezeichnung = "Test Artikel",
preis = BigDecimal.fromInt(100),
einheit = "Stück",
istVerbandsabgabe = false,
createdAt = Clock.System.now(),
updatedAt = Clock.System.now(),
schemaVersion = "1.0",
dataVersion = 1L
)
// Test migration (same version should return same object)
val migratedArtikel = migrator.migrate(artikel, "1.0", "1.0")
assertEquals(artikel, migratedArtikel)
}
@Test
fun testVersionSupport() {
assertTrue(VersionManager.isVersionSupported("1.0"))
assertTrue(!VersionManager.isVersionSupported("2.0"))
assertTrue(!VersionManager.isVersionDeprecated("1.0"))
}
}
@@ -0,0 +1,114 @@
package at.mocode.dto
import at.mocode.enums.BeginnzeitTypE
import at.mocode.model.DotierungsAbstufung
import at.mocode.serializers.*
import com.benasher44.uuid.Uuid
import com.ionspin.kotlin.bignum.decimal.BigDecimal
import kotlinx.datetime.Instant
import kotlinx.datetime.LocalDate
import kotlinx.datetime.LocalTime
import kotlinx.serialization.Serializable
@Serializable
data class AbteilungDto(
@Serializable(with = UuidSerializer::class)
val id: Uuid,
@Serializable(with = UuidSerializer::class)
val bewerbId: Uuid,
val abteilungsKennzeichen: String,
val bezeichnungIntern: String?,
val bezeichnungAufStartliste: String?,
val teilungsKriteriumLizenz: String?,
val teilungsKriteriumPferdealter: String?,
val teilungsKriteriumAltersklasseReiter: String?,
val teilungsKriteriumAnzahlMin: Int?,
val teilungsKriteriumAnzahlMax: Int?,
val teilungsKriteriumFreiText: String?,
@Serializable(with = BigDecimalSerializer::class)
val startgeld: BigDecimal?,
val dotierungen: List<DotierungsAbstufung>,
@Serializable(with = UuidSerializer::class)
val platzId: Uuid?,
val datum: LocalDate?,
val beginnzeitTypE: BeginnzeitTypE,
@Serializable(with = KotlinLocalTimeSerializer::class)
val beginnzeitFix: LocalTime?,
@Serializable(with = UuidSerializer::class)
val beginnNachAbteilungId: Uuid?,
val beginnzeitCa: LocalTime?,
val dauerProStartGeschaetztSek: Int?,
val umbauzeitNachAbteilungMin: Int?,
val besichtigungszeitVorAbteilungMin: Int?,
val stechzeitZusaetzlichMin: Int?,
val anzahlStarter: Int,
val istAktiv: Boolean,
@Serializable(with = KotlinInstantSerializer::class)
val createdAt: Instant,
@Serializable(with = KotlinInstantSerializer::class)
val updatedAt: Instant
)
@Serializable
data class CreateAbteilungDto(
@Serializable(with = UuidSerializer::class)
val bewerbId: Uuid,
val abteilungsKennzeichen: String,
val bezeichnungIntern: String? = null,
val bezeichnungAufStartliste: String? = null,
val teilungsKriteriumLizenz: String? = null,
val teilungsKriteriumPferdealter: String? = null,
val teilungsKriteriumAltersklasseReiter: String? = null,
val teilungsKriteriumAnzahlMin: Int? = null,
val teilungsKriteriumAnzahlMax: Int? = null,
val teilungsKriteriumFreiText: String? = null,
@Serializable(with = BigDecimalSerializer::class)
val startgeld: BigDecimal? = null,
val dotierungen: List<DotierungsAbstufung> = emptyList(),
@Serializable(with = UuidSerializer::class)
val platzId: Uuid? = null,
val datum: LocalDate? = null,
val beginnzeitTypE: BeginnzeitTypE = BeginnzeitTypE.ANSCHLIESSEND,
@Serializable(with = KotlinLocalTimeSerializer::class)
val beginnzeitFix: LocalTime? = null,
@Serializable(with = UuidSerializer::class)
val beginnNachAbteilungId: Uuid? = null,
val beginnzeitCa: LocalTime? = null,
val dauerProStartGeschaetztSek: Int? = null,
val umbauzeitNachAbteilungMin: Int? = null,
val besichtigungszeitVorAbteilungMin: Int? = null,
val stechzeitZusaetzlichMin: Int? = null,
val anzahlStarter: Int = 0,
val istAktiv: Boolean = true
)
@Serializable
data class UpdateAbteilungDto(
val abteilungsKennzeichen: String,
val bezeichnungIntern: String? = null,
val bezeichnungAufStartliste: String? = null,
val teilungsKriteriumLizenz: String? = null,
val teilungsKriteriumPferdealter: String? = null,
val teilungsKriteriumAltersklasseReiter: String? = null,
val teilungsKriteriumAnzahlMin: Int? = null,
val teilungsKriteriumAnzahlMax: Int? = null,
val teilungsKriteriumFreiText: String? = null,
@Serializable(with = BigDecimalSerializer::class)
val startgeld: BigDecimal? = null,
val dotierungen: List<DotierungsAbstufung> = emptyList(),
@Serializable(with = UuidSerializer::class)
val platzId: Uuid? = null,
val datum: LocalDate? = null,
val beginnzeitTypE: BeginnzeitTypE = BeginnzeitTypE.ANSCHLIESSEND,
@Serializable(with = KotlinLocalTimeSerializer::class)
val beginnzeitFix: LocalTime? = null,
@Serializable(with = UuidSerializer::class)
val beginnNachAbteilungId: Uuid? = null,
val beginnzeitCa: LocalTime? = null,
val dauerProStartGeschaetztSek: Int? = null,
val umbauzeitNachAbteilungMin: Int? = null,
val besichtigungszeitVorAbteilungMin: Int? = null,
val stechzeitZusaetzlichMin: Int? = null,
val anzahlStarter: Int = 0,
val istAktiv: Boolean = true
)
@@ -0,0 +1,53 @@
package at.mocode.dto
import at.mocode.dto.base.VersionedDto
import at.mocode.dto.base.Since
import at.mocode.serializers.BigDecimalSerializer
import at.mocode.serializers.KotlinInstantSerializer
import at.mocode.serializers.UuidSerializer
import com.benasher44.uuid.Uuid
import com.ionspin.kotlin.bignum.decimal.BigDecimal
import kotlinx.datetime.Instant
import kotlinx.serialization.Serializable
@Serializable
@Since("1.0")
data class ArtikelDto(
@Serializable(with = UuidSerializer::class)
val id: Uuid,
val bezeichnung: String,
@Serializable(with = BigDecimalSerializer::class)
val preis: BigDecimal,
val einheit: String,
val istVerbandsabgabe: Boolean,
@Serializable(with = KotlinInstantSerializer::class)
val createdAt: Instant,
@Serializable(with = KotlinInstantSerializer::class)
val updatedAt: Instant,
override val schemaVersion: String = "1.0",
override val dataVersion: Long? = null
) : VersionedDto
@Serializable
@Since("1.0")
data class CreateArtikelDto(
val bezeichnung: String,
@Serializable(with = BigDecimalSerializer::class)
val preis: BigDecimal,
val einheit: String,
val istVerbandsabgabe: Boolean = false,
override val schemaVersion: String = "1.0",
override val dataVersion: Long? = null
) : VersionedDto
@Serializable
@Since("1.0")
data class UpdateArtikelDto(
val bezeichnung: String,
@Serializable(with = BigDecimalSerializer::class)
val preis: BigDecimal,
val einheit: String,
val istVerbandsabgabe: Boolean = false,
override val schemaVersion: String = "1.0",
override val dataVersion: Long? = null
) : VersionedDto
@@ -0,0 +1,196 @@
package at.mocode.dto
import at.mocode.enums.BeginnzeitTypE
import at.mocode.enums.SparteE
import at.mocode.model.DotierungsAbstufung
import at.mocode.serializers.*
import com.benasher44.uuid.Uuid
import com.ionspin.kotlin.bignum.decimal.BigDecimal
import kotlinx.datetime.Instant
import kotlinx.datetime.LocalDate
import kotlinx.datetime.LocalTime
import kotlinx.serialization.Serializable
@Serializable
data class BewerbDto(
@Serializable(with = UuidSerializer::class)
val id: Uuid,
@Serializable(with = UuidSerializer::class)
val turnierId: Uuid,
val nummer: String,
val bezeichnungOffiziell: String,
val internerName: String?,
val sparteE: SparteE,
val klasse: String?,
val kategorieOetoDesBewerbs: String?,
val teilnahmebedingungenText: String?,
val maxPferdeProReiter: Int?,
val pferdealterAnforderung: String?,
val zusatzTextZeile1: String?,
val zusatzTextZeile2: String?,
val zusatzTextZeile3: String?,
val logoBewerbUrl: String?,
val parcoursskizzeUrl: String?,
val pruefungsArtDetailName: String?,
@Serializable(with = UuidSerializer::class)
val pruefungsaufgabeId: Uuid?,
@Serializable(with = UuidSerializer::class)
val richtverfahrenId: Uuid?,
val anzahlRichterGeplant: Int?,
val paraGradeAnforderung: String?,
val istManuellKalkuliert: Boolean,
val istDotiert: Boolean,
@Serializable(with = BigDecimalSerializer::class)
val startgeldStandard: BigDecimal?,
@Serializable(with = BigDecimalSerializer::class)
val startgeldKaderreiter: BigDecimal?,
val auszahlungsModusGeldpreis: String?,
val hatGeldpreisFuerKaderreiter: Boolean,
@Serializable(with = UuidSerializer::class)
val geldpreisVorlageId: Uuid?,
val dotierungenManuell: List<DotierungsAbstufung>,
@Serializable(with = UuidSerializer::class)
val standardPlatzId: Uuid?,
@Serializable(with = KotlinLocalDateSerializer::class)
val standardDatum: LocalDate?,
val standardBeginnzeitTypE: BeginnzeitTypE,
@Serializable(with = KotlinLocalTimeSerializer::class)
val standardBeginnzeitFix: LocalTime?,
@Serializable(with = UuidSerializer::class)
val standardBeginnNachBewerbId: Uuid?,
@Serializable(with = KotlinLocalTimeSerializer::class)
val standardBeginnzeitCa: LocalTime?,
val standardDauerProStartGeschaetztSek: Int?,
val standardUmbauzeitNachBewerbMin: Int?,
val standardBesichtigungszeitVorBewerbMin: Int?,
val standardStechzeitZusaetzlichMin: Int?,
val oepsBewerbsartCodeZns: String?,
val oepsAltersklasseCodeZns: String?,
val oepsPferderassenCodeZns: String?,
val notizenIntern: String?,
val istStartlisteFinal: Boolean,
val istErgebnislisteFinal: Boolean,
val erfordertAbteilungsAuswahlFuerNennung: Boolean,
@Serializable(with = KotlinInstantSerializer::class)
val createdAt: Instant,
@Serializable(with = KotlinInstantSerializer::class)
val updatedAt: Instant
)
@Serializable
data class CreateBewerbDto(
@Serializable(with = UuidSerializer::class)
val turnierId: Uuid,
val nummer: String,
val bezeichnungOffiziell: String,
val internerName: String? = null,
val sparteE: SparteE,
val klasse: String? = null,
val kategorieOetoDesBewerbs: String? = null,
val teilnahmebedingungenText: String? = null,
val maxPferdeProReiter: Int? = null,
val pferdealterAnforderung: String? = null,
val zusatzTextZeile1: String? = null,
val zusatzTextZeile2: String? = null,
val zusatzTextZeile3: String? = null,
val logoBewerbUrl: String? = null,
val parcoursskizzeUrl: String? = null,
val pruefungsArtDetailName: String? = null,
@Serializable(with = UuidSerializer::class)
val pruefungsaufgabeId: Uuid? = null,
@Serializable(with = UuidSerializer::class)
val richtverfahrenId: Uuid? = null,
val anzahlRichterGeplant: Int? = 1,
val paraGradeAnforderung: String? = null,
val istManuellKalkuliert: Boolean = false,
val istDotiert: Boolean = false,
@Serializable(with = BigDecimalSerializer::class)
val startgeldStandard: BigDecimal? = null,
@Serializable(with = BigDecimalSerializer::class)
val startgeldKaderreiter: BigDecimal? = null,
val auszahlungsModusGeldpreis: String? = null,
val hatGeldpreisFuerKaderreiter: Boolean = false,
@Serializable(with = UuidSerializer::class)
val geldpreisVorlageId: Uuid? = null,
val dotierungenManuell: List<DotierungsAbstufung> = emptyList(),
@Serializable(with = UuidSerializer::class)
val standardPlatzId: Uuid? = null,
@Serializable(with = KotlinLocalDateSerializer::class)
val standardDatum: LocalDate? = null,
val standardBeginnzeitTypE: BeginnzeitTypE = BeginnzeitTypE.ANSCHLIESSEND,
@Serializable(with = KotlinLocalTimeSerializer::class)
val standardBeginnzeitFix: LocalTime? = null,
@Serializable(with = UuidSerializer::class)
val standardBeginnNachBewerbId: Uuid? = null,
@Serializable(with = KotlinLocalTimeSerializer::class)
val standardBeginnzeitCa: LocalTime? = null,
val standardDauerProStartGeschaetztSek: Int? = 120,
val standardUmbauzeitNachBewerbMin: Int? = 10,
val standardBesichtigungszeitVorBewerbMin: Int? = 10,
val standardStechzeitZusaetzlichMin: Int? = 0,
val oepsBewerbsartCodeZns: String? = null,
val oepsAltersklasseCodeZns: String? = null,
val oepsPferderassenCodeZns: String? = null,
val notizenIntern: String? = null,
val istStartlisteFinal: Boolean = false,
val istErgebnislisteFinal: Boolean = false,
val erfordertAbteilungsAuswahlFuerNennung: Boolean = true
)
@Serializable
data class UpdateBewerbDto(
val nummer: String,
val bezeichnungOffiziell: String,
val internerName: String? = null,
val sparteE: SparteE,
val klasse: String? = null,
val kategorieOetoDesBewerbs: String? = null,
val teilnahmebedingungenText: String? = null,
val maxPferdeProReiter: Int? = null,
val pferdealterAnforderung: String? = null,
val zusatzTextZeile1: String? = null,
val zusatzTextZeile2: String? = null,
val zusatzTextZeile3: String? = null,
val logoBewerbUrl: String? = null,
val parcoursskizzeUrl: String? = null,
val pruefungsArtDetailName: String? = null,
@Serializable(with = UuidSerializer::class)
val pruefungsaufgabeId: Uuid? = null,
@Serializable(with = UuidSerializer::class)
val richtverfahrenId: Uuid? = null,
val anzahlRichterGeplant: Int? = 1,
val paraGradeAnforderung: String? = null,
val istManuellKalkuliert: Boolean = false,
val istDotiert: Boolean = false,
@Serializable(with = BigDecimalSerializer::class)
val startgeldStandard: BigDecimal? = null,
@Serializable(with = BigDecimalSerializer::class)
val startgeldKaderreiter: BigDecimal? = null,
val auszahlungsModusGeldpreis: String? = null,
val hatGeldpreisFuerKaderreiter: Boolean = false,
@Serializable(with = UuidSerializer::class)
val geldpreisVorlageId: Uuid? = null,
val dotierungenManuell: List<DotierungsAbstufung> = emptyList(),
@Serializable(with = UuidSerializer::class)
val standardPlatzId: Uuid? = null,
@Serializable(with = KotlinLocalDateSerializer::class)
val standardDatum: LocalDate? = null,
val standardBeginnzeitTypE: BeginnzeitTypE = BeginnzeitTypE.ANSCHLIESSEND,
@Serializable(with = KotlinLocalTimeSerializer::class)
val standardBeginnzeitFix: LocalTime? = null,
@Serializable(with = UuidSerializer::class)
val standardBeginnNachBewerbId: Uuid? = null,
@Serializable(with = KotlinLocalTimeSerializer::class)
val standardBeginnzeitCa: LocalTime? = null,
val standardDauerProStartGeschaetztSek: Int? = 120,
val standardUmbauzeitNachBewerbMin: Int? = 10,
val standardBesichtigungszeitVorBewerbMin: Int? = 10,
val standardStechzeitZusaetzlichMin: Int? = 0,
val oepsBewerbsartCodeZns: String? = null,
val oepsAltersklasseCodeZns: String? = null,
val oepsPferderassenCodeZns: String? = null,
val notizenIntern: String? = null,
val istStartlisteFinal: Boolean = false,
val istErgebnislisteFinal: Boolean = false,
val erfordertAbteilungsAuswahlFuerNennung: Boolean = true
)
@@ -0,0 +1,233 @@
package at.mocode.dto
import at.mocode.serializers.BigDecimalSerializer
import at.mocode.serializers.KotlinInstantSerializer
import at.mocode.serializers.UuidSerializer
import com.benasher44.uuid.Uuid
import com.ionspin.kotlin.bignum.decimal.BigDecimal
import kotlinx.datetime.Instant
import kotlinx.serialization.Serializable
// Pruefungsaufgabe DTOs
@Serializable
data class PruefungsaufgabeDto(
@Serializable(with = UuidSerializer::class)
val id: Uuid,
val bezeichnung: String,
val beschreibung: String?,
val kategorie: String?,
val schwierigkeitsgrad: String?,
val punkteMax: Int?,
val zeitlimitSekunden: Int?,
val istAktiv: Boolean,
val notizen: String?,
@Serializable(with = KotlinInstantSerializer::class)
val createdAt: Instant,
@Serializable(with = KotlinInstantSerializer::class)
val updatedAt: Instant
)
@Serializable
data class CreatePruefungsaufgabeDto(
val bezeichnung: String,
val beschreibung: String? = null,
val kategorie: String? = null,
val schwierigkeitsgrad: String? = null,
val punkteMax: Int? = null,
val zeitlimitSekunden: Int? = null,
val istAktiv: Boolean = true,
val notizen: String? = null
)
@Serializable
data class UpdatePruefungsaufgabeDto(
val bezeichnung: String,
val beschreibung: String? = null,
val kategorie: String? = null,
val schwierigkeitsgrad: String? = null,
val punkteMax: Int? = null,
val zeitlimitSekunden: Int? = null,
val istAktiv: Boolean = true,
val notizen: String? = null
)
// Richtverfahren DTOs
@Serializable
data class RichtverfahrenDto(
@Serializable(with = UuidSerializer::class)
val id: Uuid,
val bezeichnung: String,
val beschreibung: String?,
val kategorie: String?,
val anzahlRichterErforderlich: Int,
val bewertungsSchema: String?,
val istStandardVerfahren: Boolean,
val istAktiv: Boolean,
val regelwerk: String?,
val notizen: String?,
@Serializable(with = KotlinInstantSerializer::class)
val createdAt: Instant,
@Serializable(with = KotlinInstantSerializer::class)
val updatedAt: Instant
)
@Serializable
data class CreateRichtverfahrenDto(
val bezeichnung: String,
val beschreibung: String? = null,
val kategorie: String? = null,
val anzahlRichterErforderlich: Int = 1,
val bewertungsSchema: String? = null,
val istStandardVerfahren: Boolean = false,
val istAktiv: Boolean = true,
val regelwerk: String? = null,
val notizen: String? = null
)
@Serializable
data class UpdateRichtverfahrenDto(
val bezeichnung: String,
val beschreibung: String? = null,
val kategorie: String? = null,
val anzahlRichterErforderlich: Int = 1,
val bewertungsSchema: String? = null,
val istStandardVerfahren: Boolean = false,
val istAktiv: Boolean = true,
val regelwerk: String? = null,
val notizen: String? = null
)
// DotierungsAbstufung DTOs
@Serializable
data class DotierungsAbstufungDto(
val platz: Int,
@Serializable(with = BigDecimalSerializer::class)
val betrag: BigDecimal
)
// MeisterschaftReferenz DTOs
@Serializable
data class MeisterschaftReferenzDto(
@Serializable(with = UuidSerializer::class)
val id: Uuid,
@Serializable(with = UuidSerializer::class)
val meisterschaftId: Uuid,
val bezeichnung: String,
val kategorie: String?
)
@Serializable
data class CreateMeisterschaftReferenzDto(
@Serializable(with = UuidSerializer::class)
val meisterschaftId: Uuid,
val bezeichnung: String,
val kategorie: String? = null
)
@Serializable
data class UpdateMeisterschaftReferenzDto(
@Serializable(with = UuidSerializer::class)
val meisterschaftId: Uuid,
val bezeichnung: String,
val kategorie: String? = null
)
// CupReferenz DTOs
@Serializable
data class CupReferenzDto(
@Serializable(with = UuidSerializer::class)
val id: Uuid,
@Serializable(with = UuidSerializer::class)
val cupId: Uuid,
val bezeichnung: String,
val kategorie: String?
)
@Serializable
data class CreateCupReferenzDto(
@Serializable(with = UuidSerializer::class)
val cupId: Uuid,
val bezeichnung: String,
val kategorie: String? = null
)
@Serializable
data class UpdateCupReferenzDto(
@Serializable(with = UuidSerializer::class)
val cupId: Uuid,
val bezeichnung: String,
val kategorie: String? = null
)
// SonderpruefungReferenz DTOs
@Serializable
data class SonderpruefungReferenzDto(
@Serializable(with = UuidSerializer::class)
val id: Uuid,
@Serializable(with = UuidSerializer::class)
val sonderpruefungId: Uuid,
val bezeichnung: String,
val kategorie: String?
)
@Serializable
data class CreateSonderpruefungReferenzDto(
@Serializable(with = UuidSerializer::class)
val sonderpruefungId: Uuid,
val bezeichnung: String,
val kategorie: String? = null
)
@Serializable
data class UpdateSonderpruefungReferenzDto(
@Serializable(with = UuidSerializer::class)
val sonderpruefungId: Uuid,
val bezeichnung: String,
val kategorie: String? = null
)
// Platz DTOs
@Serializable
data class PlatzDto(
@Serializable(with = UuidSerializer::class)
val id: Uuid,
val bezeichnung: String,
val beschreibung: String?,
val adresse: String?,
val gpsKoordinaten: String?,
val kapazitaet: Int?,
val ausstattung: List<String>,
val istVerfuegbar: Boolean,
val kontaktInfo: String?,
val notizen: String?,
@Serializable(with = KotlinInstantSerializer::class)
val createdAt: Instant,
@Serializable(with = KotlinInstantSerializer::class)
val updatedAt: Instant
)
@Serializable
data class CreatePlatzDto(
val bezeichnung: String,
val beschreibung: String? = null,
val adresse: String? = null,
val gpsKoordinaten: String? = null,
val kapazitaet: Int? = null,
val ausstattung: List<String> = emptyList(),
val istVerfuegbar: Boolean = true,
val kontaktInfo: String? = null,
val notizen: String? = null
)
@Serializable
data class UpdatePlatzDto(
val bezeichnung: String,
val beschreibung: String? = null,
val adresse: String? = null,
val gpsKoordinaten: String? = null,
val kapazitaet: Int? = null,
val ausstattung: List<String> = emptyList(),
val istVerfuegbar: Boolean = true,
val kontaktInfo: String? = null,
val notizen: String? = null
)
@@ -0,0 +1,269 @@
package at.mocode.dto
import at.mocode.serializers.KotlinInstantSerializer
import at.mocode.serializers.KotlinLocalDateSerializer
import at.mocode.serializers.UuidSerializer
import com.benasher44.uuid.Uuid
import kotlinx.datetime.Instant
import kotlinx.datetime.LocalDate
import kotlinx.serialization.Serializable
// DomLizenz DTOs
@Serializable
data class DomLizenzDto(
@Serializable(with = UuidSerializer::class)
val id: Uuid,
val lizenzTyp: String,
val bezeichnung: String,
val beschreibung: String?,
val sparte: String?,
val mindestalter: Int?,
val voraussetzungen: String?,
val gueltigkeitsdauerJahre: Int?,
val istAktiv: Boolean,
@Serializable(with = KotlinInstantSerializer::class)
val createdAt: Instant,
@Serializable(with = KotlinInstantSerializer::class)
val updatedAt: Instant
)
@Serializable
data class CreateDomLizenzDto(
val lizenzTyp: String,
val bezeichnung: String,
val beschreibung: String? = null,
val sparte: String? = null,
val mindestalter: Int? = null,
val voraussetzungen: String? = null,
val gueltigkeitsdauerJahre: Int? = null,
val istAktiv: Boolean = true
)
@Serializable
data class UpdateDomLizenzDto(
val lizenzTyp: String,
val bezeichnung: String,
val beschreibung: String? = null,
val sparte: String? = null,
val mindestalter: Int? = null,
val voraussetzungen: String? = null,
val gueltigkeitsdauerJahre: Int? = null,
val istAktiv: Boolean = true
)
// DomPerson DTOs
@Serializable
data class DomPersonDto(
@Serializable(with = UuidSerializer::class)
val id: Uuid,
val nachname: String,
val vorname: String,
val titel: String?,
@Serializable(with = KotlinLocalDateSerializer::class)
val geburtsdatum: LocalDate?,
val geschlecht: String?,
val nationalitaet: String?,
val email: String?,
val telefon: String?,
val adresse: String?,
val plz: String?,
val ort: String?,
val land: String?,
val feiId: String?,
val istAktiv: Boolean,
@Serializable(with = KotlinInstantSerializer::class)
val createdAt: Instant,
@Serializable(with = KotlinInstantSerializer::class)
val updatedAt: Instant
)
@Serializable
data class CreateDomPersonDto(
val nachname: String,
val vorname: String,
val titel: String? = null,
@Serializable(with = KotlinLocalDateSerializer::class)
val geburtsdatum: LocalDate? = null,
val geschlecht: String? = null,
val nationalitaet: String? = null,
val email: String? = null,
val telefon: String? = null,
val adresse: String? = null,
val plz: String? = null,
val ort: String? = null,
val land: String? = null,
val feiId: String? = null,
val istAktiv: Boolean = true
)
@Serializable
data class UpdateDomPersonDto(
val nachname: String,
val vorname: String,
val titel: String? = null,
@Serializable(with = KotlinLocalDateSerializer::class)
val geburtsdatum: LocalDate? = null,
val geschlecht: String? = null,
val nationalitaet: String? = null,
val email: String? = null,
val telefon: String? = null,
val adresse: String? = null,
val plz: String? = null,
val ort: String? = null,
val land: String? = null,
val feiId: String? = null,
val istAktiv: Boolean = true
)
// DomPferd DTOs
@Serializable
data class DomPferdDto(
@Serializable(with = UuidSerializer::class)
val id: Uuid,
val name: String,
val rasse: String?,
val farbe: String?,
val geschlecht: String?,
@Serializable(with = KotlinLocalDateSerializer::class)
val geburtsdatum: LocalDate?,
val geburtsland: String?,
val vater: String?,
val mutter: String?,
val zuechter: String?,
val eigentuemer: String?,
val feiId: String?,
val lebensnummer: String?,
val chipNummer: String?,
val istAktiv: Boolean,
@Serializable(with = KotlinInstantSerializer::class)
val createdAt: Instant,
@Serializable(with = KotlinInstantSerializer::class)
val updatedAt: Instant
)
@Serializable
data class CreateDomPferdDto(
val name: String,
val rasse: String? = null,
val farbe: String? = null,
val geschlecht: String? = null,
@Serializable(with = KotlinLocalDateSerializer::class)
val geburtsdatum: LocalDate? = null,
val geburtsland: String? = null,
val vater: String? = null,
val mutter: String? = null,
val zuechter: String? = null,
val eigentuemer: String? = null,
val feiId: String? = null,
val lebensnummer: String? = null,
val chipNummer: String? = null,
val istAktiv: Boolean = true
)
@Serializable
data class UpdateDomPferdDto(
val name: String,
val rasse: String? = null,
val farbe: String? = null,
val geschlecht: String? = null,
@Serializable(with = KotlinLocalDateSerializer::class)
val geburtsdatum: LocalDate? = null,
val geburtsland: String? = null,
val vater: String? = null,
val mutter: String? = null,
val zuechter: String? = null,
val eigentuemer: String? = null,
val feiId: String? = null,
val lebensnummer: String? = null,
val chipNummer: String? = null,
val istAktiv: Boolean = true
)
// DomQualifikation DTOs
@Serializable
data class DomQualifikationDto(
@Serializable(with = UuidSerializer::class)
val id: Uuid,
val bezeichnung: String,
val beschreibung: String?,
val kategorie: String?,
val sparte: String?,
val voraussetzungen: String?,
val gueltigkeitsdauerJahre: Int?,
val istAktiv: Boolean,
@Serializable(with = KotlinInstantSerializer::class)
val createdAt: Instant,
@Serializable(with = KotlinInstantSerializer::class)
val updatedAt: Instant
)
@Serializable
data class CreateDomQualifikationDto(
val bezeichnung: String,
val beschreibung: String? = null,
val kategorie: String? = null,
val sparte: String? = null,
val voraussetzungen: String? = null,
val gueltigkeitsdauerJahre: Int? = null,
val istAktiv: Boolean = true
)
@Serializable
data class UpdateDomQualifikationDto(
val bezeichnung: String,
val beschreibung: String? = null,
val kategorie: String? = null,
val sparte: String? = null,
val voraussetzungen: String? = null,
val gueltigkeitsdauerJahre: Int? = null,
val istAktiv: Boolean = true
)
// DomVerein DTOs
@Serializable
data class DomVereinDto(
@Serializable(with = UuidSerializer::class)
val id: Uuid,
val name: String,
val kuerzel: String?,
val adresse: String?,
val plz: String?,
val ort: String?,
val land: String?,
val email: String?,
val telefon: String?,
val webseite: String?,
val istAktiv: Boolean,
@Serializable(with = KotlinInstantSerializer::class)
val createdAt: Instant,
@Serializable(with = KotlinInstantSerializer::class)
val updatedAt: Instant
)
@Serializable
data class CreateDomVereinDto(
val name: String,
val kuerzel: String? = null,
val adresse: String? = null,
val plz: String? = null,
val ort: String? = null,
val land: String? = null,
val email: String? = null,
val telefon: String? = null,
val webseite: String? = null,
val istAktiv: Boolean = true
)
@Serializable
data class UpdateDomVereinDto(
val name: String,
val kuerzel: String? = null,
val adresse: String? = null,
val plz: String? = null,
val ort: String? = null,
val land: String? = null,
val email: String? = null,
val telefon: String? = null,
val webseite: String? = null,
val istAktiv: Boolean = true
)
@@ -0,0 +1,285 @@
package at.mocode.dto
import at.mocode.serializers.KotlinInstantSerializer
import at.mocode.serializers.KotlinLocalDateSerializer
import at.mocode.serializers.KotlinLocalTimeSerializer
import at.mocode.serializers.UuidSerializer
import com.benasher44.uuid.Uuid
import kotlinx.datetime.Instant
import kotlinx.datetime.LocalDate
import kotlinx.datetime.LocalTime
import kotlinx.serialization.Serializable
// Veranstaltung subdirectory DTOs
// Pruefung_OEPS DTOs
@Serializable
data class PruefungOepsDto(
@Serializable(with = UuidSerializer::class)
val id: Uuid,
@Serializable(with = UuidSerializer::class)
val abteilungId: Uuid,
val bezeichnung: String,
val beschreibung: String?,
val kategorie: String?,
val schwierigkeitsgrad: String?,
@Serializable(with = UuidSerializer::class)
val richterIds: List<Uuid>,
@Serializable(with = KotlinLocalDateSerializer::class)
val datum: LocalDate?,
@Serializable(with = KotlinLocalTimeSerializer::class)
val startzeit: LocalTime?,
@Serializable(with = UuidSerializer::class)
val platzId: Uuid?,
@Serializable(with = KotlinInstantSerializer::class)
val createdAt: Instant,
@Serializable(with = KotlinInstantSerializer::class)
val updatedAt: Instant
)
// Pruefung_Abteilung DTOs
@Serializable
data class PruefungAbteilungDto(
@Serializable(with = UuidSerializer::class)
val id: Uuid,
@Serializable(with = UuidSerializer::class)
val pruefungId: Uuid,
@Serializable(with = UuidSerializer::class)
val abteilungId: Uuid,
val reihenfolge: Int,
val istAktiv: Boolean
)
// VeranstaltungsRahmen DTOs
@Serializable
data class VeranstaltungsRahmenDto(
@Serializable(with = UuidSerializer::class)
val id: Uuid,
val bezeichnung: String,
val beschreibung: String?,
val kategorie: String?,
val regelwerk: String?,
@Serializable(with = KotlinLocalDateSerializer::class)
val gueltigVon: LocalDate?,
@Serializable(with = KotlinLocalDateSerializer::class)
val gueltigBis: LocalDate?,
val istStandard: Boolean,
@Serializable(with = UuidSerializer::class)
val veranstalterId: Uuid?,
@Serializable(with = UuidSerializer::class)
val veranstaltungsortId: Uuid?,
val istAktiv: Boolean,
@Serializable(with = KotlinInstantSerializer::class)
val createdAt: Instant,
@Serializable(with = KotlinInstantSerializer::class)
val updatedAt: Instant
)
// Turnier_hat_Platz DTOs
@Serializable
data class TurnierHatPlatzDto(
@Serializable(with = UuidSerializer::class)
val turnierId: Uuid,
@Serializable(with = UuidSerializer::class)
val platzId: Uuid,
val verwendungszweck: String?
)
// OETO Verwaltung DTOs
// AltersklasseDefinition DTOs
@Serializable
data class AltersklasseDefinitionDto(
val code: String,
val bezeichnung: String,
val minAlter: Int?,
val maxAlter: Int?,
val beschreibung: String?,
val istAktiv: Boolean
)
// LizenzTypGlobal DTOs
@Serializable
data class LizenzTypGlobalDto(
val code: String,
val bezeichnung: String,
val beschreibung: String?,
val sparte: String?,
val kategorie: String?,
val istAktiv: Boolean
)
// QualifikationsTyp DTOs
@Serializable
data class QualifikationsTypDto(
val code: String,
val bezeichnung: String,
val beschreibung: String?,
val sparte: String?,
val kategorie: String?,
val istAktiv: Boolean
)
// Sportfachliche_Stammdaten DTOs
@Serializable
data class SportfachlicheStammdatenDto(
@Serializable(with = UuidSerializer::class)
val id: Uuid,
val typ: String,
val code: String,
val bezeichnung: String,
val beschreibung: String?,
val kategorie: String?,
val sortierung: Int?,
val istAktiv: Boolean,
@Serializable(with = KotlinInstantSerializer::class)
val createdAt: Instant,
@Serializable(with = KotlinInstantSerializer::class)
val updatedAt: Instant
)
// OETORegelReferenz DTOs
@Serializable
data class OetoRegelReferenzDto(
@Serializable(with = UuidSerializer::class)
val id: Uuid,
val regelCode: String,
val bezeichnung: String,
val beschreibung: String?,
val kategorie: String?,
val version: String?,
@Serializable(with = KotlinLocalDateSerializer::class)
val gueltigVon: LocalDate?,
@Serializable(with = KotlinLocalDateSerializer::class)
val gueltigBis: LocalDate?,
val istAktiv: Boolean
)
// ZNS Staging DTOs
// Person_ZNS_Staging DTOs
@Serializable
data class PersonZnsStagingDto(
@Serializable(with = UuidSerializer::class)
val id: Uuid,
val znsSatzNr: String,
val nachname: String,
val vorname: String,
val titel: String?,
@Serializable(with = KotlinLocalDateSerializer::class)
val geburtsdatum: LocalDate?,
val geschlecht: String?,
val nationalitaet: String?,
val email: String?,
val telefon: String?,
val adresse: String?,
val plz: String?,
val ort: String?,
val vereinsname: String?,
val vereinsnummer: String?,
val feiId: String?,
val importDatum: Instant,
val istVerarbeitet: Boolean,
val verarbeitungsStatus: String?,
val fehlerMeldung: String?
)
// Pferd_ZNS_Staging DTOs
@Serializable
data class PferdZnsStagingDto(
@Serializable(with = UuidSerializer::class)
val id: Uuid,
val znsPferdNr: String,
val name: String,
val rasse: String?,
val farbe: String?,
val geschlecht: String?,
@Serializable(with = KotlinLocalDateSerializer::class)
val geburtsdatum: LocalDate?,
val geburtsland: String?,
val vater: String?,
val mutter: String?,
val zuechter: String?,
val eigentuemer: String?,
val feiId: String?,
val lebensnummer: String?,
val importDatum: Instant,
val istVerarbeitet: Boolean,
val verarbeitungsStatus: String?,
val fehlerMeldung: String?
)
// Verein_ZNS_Staging DTOs
@Serializable
data class VereinZnsStagingDto(
@Serializable(with = UuidSerializer::class)
val id: Uuid,
val znsVereinsNr: String,
val name: String,
val kuerzel: String?,
val adresse: String?,
val plz: String?,
val ort: String?,
val bundesland: String?,
val email: String?,
val telefon: String?,
val webseite: String?,
val importDatum: Instant,
val istVerarbeitet: Boolean,
val verarbeitungsStatus: String?,
val fehlerMeldung: String?
)
// Cup DTOs
// Meisterschaft_Cup_Serie DTOs
@Serializable
data class MeisterschaftCupSerieDto(
@Serializable(with = UuidSerializer::class)
val id: Uuid,
val bezeichnung: String,
val beschreibung: String?,
val saison: String,
val kategorie: String?,
val sparte: String?,
@Serializable(with = KotlinLocalDateSerializer::class)
val startDatum: LocalDate?,
@Serializable(with = KotlinLocalDateSerializer::class)
val endDatum: LocalDate?,
val istAktiv: Boolean,
@Serializable(with = KotlinInstantSerializer::class)
val createdAt: Instant,
@Serializable(with = KotlinInstantSerializer::class)
val updatedAt: Instant
)
// MCS_Wertungspruefung DTOs
@Serializable
data class McsWertungspruefungDto(
@Serializable(with = UuidSerializer::class)
val id: Uuid,
@Serializable(with = UuidSerializer::class)
val meisterschaftCupSerieId: Uuid,
@Serializable(with = UuidSerializer::class)
val pruefungId: Uuid,
val wertungsfaktor: Double?,
val istPflichtpruefung: Boolean,
val reihenfolge: Int?
)
// Spezifika DTOs
// DressurPruefungSpezifika DTOs
@Serializable
data class DressurPruefungSpezifikaDto(
@Serializable(with = UuidSerializer::class)
val id: Uuid,
@Serializable(with = UuidSerializer::class)
val pruefungId: Uuid,
val aufgabenbezeichnung: String?,
val aufgabenbeschreibung: String?,
val bewertungsschema: String?,
val maxPunkte: Int?,
val zeitlimit: Int?,
val besonderheiten: String?
)
@@ -0,0 +1,260 @@
package at.mocode.dto
import at.mocode.enums.FunktionaerRolle
import at.mocode.enums.GeschlechtE
import at.mocode.stammdaten.LizenzInfo
import at.mocode.serializers.KotlinInstantSerializer
import at.mocode.serializers.KotlinLocalDateSerializer
import at.mocode.serializers.UuidSerializer
import com.benasher44.uuid.Uuid
import kotlinx.datetime.Instant
import kotlinx.datetime.LocalDate
import kotlinx.serialization.Serializable
// Person DTOs
@Serializable
data class PersonDto(
@Serializable(with = UuidSerializer::class)
val id: Uuid,
val oepsSatzNr: String?,
val nachname: String,
val vorname: String,
val titel: String?,
@Serializable(with = KotlinLocalDateSerializer::class)
val geburtsdatum: LocalDate?,
val geschlechtE: GeschlechtE?,
val nationalitaet: String?,
val email: String?,
val telefon: String?,
val adresse: String?,
val plz: String?,
val ort: String?,
@Serializable(with = UuidSerializer::class)
val stammVereinId: Uuid?,
val mitgliedsNummerIntern: String?,
val letzteZahlungJahr: Int?,
val feiId: String?,
val istGesperrt: Boolean,
val sperrGrund: String?,
val rollen: Set<FunktionaerRolle>,
val lizenzen: List<LizenzInfo>,
val qualifikationenRichter: List<String>,
val qualifikationenParcoursbauer: List<String>,
val istAktiv: Boolean,
@Serializable(with = KotlinInstantSerializer::class)
val createdAt: Instant,
@Serializable(with = KotlinInstantSerializer::class)
val updatedAt: Instant
)
@Serializable
data class CreatePersonDto(
val oepsSatzNr: String? = null,
val nachname: String,
val vorname: String,
val titel: String? = null,
@Serializable(with = KotlinLocalDateSerializer::class)
val geburtsdatum: LocalDate? = null,
val geschlechtE: GeschlechtE? = null,
val nationalitaet: String? = null,
val email: String? = null,
val telefon: String? = null,
val adresse: String? = null,
val plz: String? = null,
val ort: String? = null,
@Serializable(with = UuidSerializer::class)
val stammVereinId: Uuid? = null,
val mitgliedsNummerIntern: String? = null,
val letzteZahlungJahr: Int? = null,
val feiId: String? = null,
val istGesperrt: Boolean = false,
val sperrGrund: String? = null,
val rollen: Set<FunktionaerRolle> = emptySet(),
val lizenzen: List<LizenzInfo> = emptyList(),
val qualifikationenRichter: List<String> = emptyList(),
val qualifikationenParcoursbauer: List<String> = emptyList(),
val istAktiv: Boolean = true
)
@Serializable
data class UpdatePersonDto(
val oepsSatzNr: String? = null,
val nachname: String,
val vorname: String,
val titel: String? = null,
@Serializable(with = KotlinLocalDateSerializer::class)
val geburtsdatum: LocalDate? = null,
val geschlechtE: GeschlechtE? = null,
val nationalitaet: String? = null,
val email: String? = null,
val telefon: String? = null,
val adresse: String? = null,
val plz: String? = null,
val ort: String? = null,
@Serializable(with = UuidSerializer::class)
val stammVereinId: Uuid? = null,
val mitgliedsNummerIntern: String? = null,
val letzteZahlungJahr: Int? = null,
val feiId: String? = null,
val istGesperrt: Boolean = false,
val sperrGrund: String? = null,
val rollen: Set<FunktionaerRolle> = emptySet(),
val lizenzen: List<LizenzInfo> = emptyList(),
val qualifikationenRichter: List<String> = emptyList(),
val qualifikationenParcoursbauer: List<String> = emptyList(),
val istAktiv: Boolean = true
)
// Pferd DTOs
@Serializable
data class PferdDto(
@Serializable(with = UuidSerializer::class)
val id: Uuid,
val oepsPferdNr: String?,
val name: String,
val rasse: String?,
val farbe: String?,
val geschlecht: String?,
@Serializable(with = KotlinLocalDateSerializer::class)
val geburtsdatum: LocalDate?,
val geburtsland: String?,
val vater: String?,
val mutter: String?,
val zuechter: String?,
val eigentuemer: String?,
@Serializable(with = UuidSerializer::class)
val heimatVereinId: Uuid?,
val feiId: String?,
val lebensnummer: String?,
val chipNummer: String?,
val istGesperrt: Boolean,
val sperrGrund: String?,
val istAktiv: Boolean,
@Serializable(with = KotlinInstantSerializer::class)
val createdAt: Instant,
@Serializable(with = KotlinInstantSerializer::class)
val updatedAt: Instant
)
@Serializable
data class CreatePferdDto(
val oepsPferdNr: String? = null,
val name: String,
val rasse: String? = null,
val farbe: String? = null,
val geschlecht: String? = null,
@Serializable(with = KotlinLocalDateSerializer::class)
val geburtsdatum: LocalDate? = null,
val geburtsland: String? = null,
val vater: String? = null,
val mutter: String? = null,
val zuechter: String? = null,
val eigentuemer: String? = null,
@Serializable(with = UuidSerializer::class)
val heimatVereinId: Uuid? = null,
val feiId: String? = null,
val lebensnummer: String? = null,
val chipNummer: String? = null,
val istGesperrt: Boolean = false,
val sperrGrund: String? = null,
val istAktiv: Boolean = true
)
@Serializable
data class UpdatePferdDto(
val oepsPferdNr: String? = null,
val name: String,
val rasse: String? = null,
val farbe: String? = null,
val geschlecht: String? = null,
@Serializable(with = KotlinLocalDateSerializer::class)
val geburtsdatum: LocalDate? = null,
val geburtsland: String? = null,
val vater: String? = null,
val mutter: String? = null,
val zuechter: String? = null,
val eigentuemer: String? = null,
@Serializable(with = UuidSerializer::class)
val heimatVereinId: Uuid? = null,
val feiId: String? = null,
val lebensnummer: String? = null,
val chipNummer: String? = null,
val istGesperrt: Boolean = false,
val sperrGrund: String? = null,
val istAktiv: Boolean = true
)
// LizenzInfo DTOs
@Serializable
data class LizenzInfoDto(
val lizenzTyp: String,
val gueltigVon: LocalDate?,
val gueltigBis: LocalDate?,
val istAktiv: Boolean
)
@Serializable
data class CreateLizenzInfoDto(
val lizenzTyp: String,
val gueltigVon: LocalDate? = null,
val gueltigBis: LocalDate? = null,
val istAktiv: Boolean = true
)
@Serializable
data class UpdateLizenzInfoDto(
val lizenzTyp: String,
val gueltigVon: LocalDate? = null,
val gueltigBis: LocalDate? = null,
val istAktiv: Boolean = true
)
// BundeslandDefinition DTOs
@Serializable
data class BundeslandDefinitionDto(
val code: String,
val bezeichnung: String,
val land: String,
val istAktiv: Boolean
)
@Serializable
data class CreateBundeslandDefinitionDto(
val code: String,
val bezeichnung: String,
val land: String,
val istAktiv: Boolean = true
)
@Serializable
data class UpdateBundeslandDefinitionDto(
val code: String,
val bezeichnung: String,
val land: String,
val istAktiv: Boolean = true
)
// LandDefinition DTOs
@Serializable
data class LandDefinitionDto(
val code: String,
val bezeichnung: String,
val istEuMitglied: Boolean,
val istAktiv: Boolean
)
@Serializable
data class CreateLandDefinitionDto(
val code: String,
val bezeichnung: String,
val istEuMitglied: Boolean = false,
val istAktiv: Boolean = true
)
@Serializable
data class UpdateLandDefinitionDto(
val code: String,
val bezeichnung: String,
val istEuMitglied: Boolean = false,
val istAktiv: Boolean = true
)
@@ -0,0 +1,137 @@
package at.mocode.dto
import at.mocode.enums.NennungsArtE
import at.mocode.model.Artikel
import at.mocode.model.MeisterschaftReferenz
import at.mocode.model.Platz
import at.mocode.serializers.*
import com.benasher44.uuid.Uuid
import com.ionspin.kotlin.bignum.decimal.BigDecimal
import kotlinx.datetime.Instant
import kotlinx.datetime.LocalDate
import kotlinx.datetime.LocalDateTime
import kotlinx.serialization.Serializable
@Serializable
data class TurnierDto(
@Serializable(with = UuidSerializer::class)
val id: Uuid,
@Serializable(with = UuidSerializer::class)
val veranstaltungId: Uuid,
val oepsTurnierNr: String,
val titel: String,
val untertitel: String?,
@Serializable(with = KotlinLocalDateSerializer::class)
val datumVon: LocalDate,
@Serializable(with = KotlinLocalDateSerializer::class)
val datumBis: LocalDate,
@Serializable(with = KotlinLocalDateTimeSerializer::class)
val nennungsschluss: LocalDateTime?,
val nennungsArt: List<NennungsArtE>,
val nennungsHinweis: String?,
val eigenesNennsystemUrl: String?,
@Serializable(with = BigDecimalSerializer::class)
val nenngeld: BigDecimal?,
@Serializable(with = BigDecimalSerializer::class)
val startgeldStandard: BigDecimal?,
val austragungsplaetze: List<Platz>,
val vorbereitungsplaetze: List<Platz>,
@Serializable(with = UuidSerializer::class)
val turnierleiterId: Uuid?,
@Serializable(with = UuidSerializer::class)
val turnierbeauftragterId: Uuid?,
val richterIds: List<@Serializable(with = UuidSerializer::class) Uuid>,
val parcoursbauerIds: List<@Serializable(with = UuidSerializer::class) Uuid>,
val parcoursAssistentIds: List<@Serializable(with = UuidSerializer::class) Uuid>,
val tierarztInfos: String?,
val hufschmiedInfo: String?,
@Serializable(with = UuidSerializer::class)
val meldestelleVerantwortlicherId: Uuid?,
val meldestelleTelefon: String?,
val meldestelleOeffnungszeiten: String?,
val ergebnislistenUrl: String?,
val verfuegbareArtikel: List<Artikel>,
val meisterschaftRefs: List<MeisterschaftReferenz>,
@Serializable(with = KotlinInstantSerializer::class)
val createdAt: Instant,
@Serializable(with = KotlinInstantSerializer::class)
val updatedAt: Instant
)
@Serializable
data class CreateTurnierDto(
@Serializable(with = UuidSerializer::class)
val veranstaltungId: Uuid,
val oepsTurnierNr: String,
val titel: String,
val untertitel: String? = null,
@Serializable(with = KotlinLocalDateSerializer::class)
val datumVon: LocalDate,
@Serializable(with = KotlinLocalDateSerializer::class)
val datumBis: LocalDate,
@Serializable(with = KotlinLocalDateTimeSerializer::class)
val nennungsschluss: LocalDateTime? = null,
val nennungsArt: List<NennungsArtE> = emptyList(),
val nennungsHinweis: String? = null,
val eigenesNennsystemUrl: String? = null,
@Serializable(with = BigDecimalSerializer::class)
val nenngeld: BigDecimal? = null,
@Serializable(with = BigDecimalSerializer::class)
val startgeldStandard: BigDecimal? = null,
val austragungsplaetze: List<Platz> = emptyList(),
val vorbereitungsplaetze: List<Platz> = emptyList(),
@Serializable(with = UuidSerializer::class)
val turnierleiterId: Uuid? = null,
@Serializable(with = UuidSerializer::class)
val turnierbeauftragterId: Uuid? = null,
val richterIds: List<@Serializable(with = UuidSerializer::class) Uuid> = emptyList(),
val parcoursbauerIds: List<@Serializable(with = UuidSerializer::class) Uuid> = emptyList(),
val parcoursAssistentIds: List<@Serializable(with = UuidSerializer::class) Uuid> = emptyList(),
val tierarztInfos: String? = null,
val hufschmiedInfo: String? = null,
@Serializable(with = UuidSerializer::class)
val meldestelleVerantwortlicherId: Uuid? = null,
val meldestelleTelefon: String? = null,
val meldestelleOeffnungszeiten: String? = null,
val ergebnislistenUrl: String? = null,
val verfuegbareArtikel: List<Artikel> = emptyList(),
val meisterschaftRefs: List<MeisterschaftReferenz> = emptyList()
)
@Serializable
data class UpdateTurnierDto(
val oepsTurnierNr: String,
val titel: String,
val untertitel: String? = null,
@Serializable(with = KotlinLocalDateSerializer::class)
val datumVon: LocalDate,
@Serializable(with = KotlinLocalDateSerializer::class)
val datumBis: LocalDate,
@Serializable(with = KotlinLocalDateTimeSerializer::class)
val nennungsschluss: LocalDateTime? = null,
val nennungsArt: List<NennungsArtE> = emptyList(),
val nennungsHinweis: String? = null,
val eigenesNennsystemUrl: String? = null,
@Serializable(with = BigDecimalSerializer::class)
val nenngeld: BigDecimal? = null,
@Serializable(with = BigDecimalSerializer::class)
val startgeldStandard: BigDecimal? = null,
val austragungsplaetze: List<Platz> = emptyList(),
val vorbereitungsplaetze: List<Platz> = emptyList(),
@Serializable(with = UuidSerializer::class)
val turnierleiterId: Uuid? = null,
@Serializable(with = UuidSerializer::class)
val turnierbeauftragterId: Uuid? = null,
val richterIds: List<@Serializable(with = UuidSerializer::class) Uuid> = emptyList(),
val parcoursbauerIds: List<@Serializable(with = UuidSerializer::class) Uuid> = emptyList(),
val parcoursAssistentIds: List<@Serializable(with = UuidSerializer::class) Uuid> = emptyList(),
val tierarztInfos: String? = null,
val hufschmiedInfo: String? = null,
@Serializable(with = UuidSerializer::class)
val meldestelleVerantwortlicherId: Uuid? = null,
val meldestelleTelefon: String? = null,
val meldestelleOeffnungszeiten: String? = null,
val ergebnislistenUrl: String? = null,
val verfuegbareArtikel: List<Artikel> = emptyList(),
val meisterschaftRefs: List<MeisterschaftReferenz> = emptyList()
)
@@ -0,0 +1,88 @@
package at.mocode.dto
import at.mocode.enums.VeranstalterTypE
import at.mocode.serializers.KotlinInstantSerializer
import at.mocode.serializers.KotlinLocalDateSerializer
import at.mocode.serializers.UuidSerializer
import com.benasher44.uuid.Uuid
import kotlinx.datetime.Instant
import kotlinx.datetime.LocalDate
import kotlinx.serialization.Serializable
@Serializable
data class VeranstaltungDto(
@Serializable(with = UuidSerializer::class)
val id: Uuid,
val name: String,
@Serializable(with = KotlinLocalDateSerializer::class)
val datumVon: LocalDate,
@Serializable(with = KotlinLocalDateSerializer::class)
val datumBis: LocalDate,
val veranstalterName: String,
val veranstalterOepsNummer: String?,
val veranstalterTypE: VeranstalterTypE,
val veranstaltungsortName: String,
val veranstaltungsortAdresse: String,
val kontaktpersonName: String?,
val kontaktTelefon: String?,
val kontaktEmail: String?,
val webseite: String?,
val logoUrl: String?,
val anfahrtsplanInfo: String?,
val sponsorInfos: List<String>,
val dsgvoText: String?,
val haftungsText: String?,
val sonstigeBesondereBestimmungen: String?,
@Serializable(with = KotlinInstantSerializer::class)
val createdAt: Instant,
@Serializable(with = KotlinInstantSerializer::class)
val updatedAt: Instant
)
@Serializable
data class CreateVeranstaltungDto(
val name: String,
@Serializable(with = KotlinLocalDateSerializer::class)
val datumVon: LocalDate,
@Serializable(with = KotlinLocalDateSerializer::class)
val datumBis: LocalDate,
val veranstalterName: String,
val veranstalterOepsNummer: String? = null,
val veranstalterTypE: VeranstalterTypE = VeranstalterTypE.UNBEKANNT,
val veranstaltungsortName: String,
val veranstaltungsortAdresse: String,
val kontaktpersonName: String? = null,
val kontaktTelefon: String? = null,
val kontaktEmail: String? = null,
val webseite: String? = null,
val logoUrl: String? = null,
val anfahrtsplanInfo: String? = null,
val sponsorInfos: List<String> = emptyList(),
val dsgvoText: String? = null,
val haftungsText: String? = null,
val sonstigeBesondereBestimmungen: String? = null
)
@Serializable
data class UpdateVeranstaltungDto(
val name: String,
@Serializable(with = KotlinLocalDateSerializer::class)
val datumVon: LocalDate,
@Serializable(with = KotlinLocalDateSerializer::class)
val datumBis: LocalDate,
val veranstalterName: String,
val veranstalterOepsNummer: String? = null,
val veranstalterTypE: VeranstalterTypE = VeranstalterTypE.UNBEKANNT,
val veranstaltungsortName: String,
val veranstaltungsortAdresse: String,
val kontaktpersonName: String? = null,
val kontaktTelefon: String? = null,
val kontaktEmail: String? = null,
val webseite: String? = null,
val logoUrl: String? = null,
val anfahrtsplanInfo: String? = null,
val sponsorInfos: List<String> = emptyList(),
val dsgvoText: String? = null,
val haftungsText: String? = null,
val sonstigeBesondereBestimmungen: String? = null
)
@@ -0,0 +1,68 @@
package at.mocode.dto
import at.mocode.dto.base.VersionedDto
import at.mocode.dto.base.Since
import at.mocode.serializers.KotlinInstantSerializer
import at.mocode.serializers.UuidSerializer
import com.benasher44.uuid.Uuid
import kotlinx.datetime.Instant
import kotlinx.serialization.Serializable
@Serializable
@Since("1.0")
data class VereinDto(
@Serializable(with = UuidSerializer::class)
val id: Uuid,
val oepsVereinsNr: String,
val name: String,
val kuerzel: String?,
val bundesland: String?,
val adresse: String?,
val plz: String?,
val ort: String?,
val email: String?,
val telefon: String?,
val webseite: String?,
val istAktiv: Boolean,
@Serializable(with = KotlinInstantSerializer::class)
val createdAt: Instant,
@Serializable(with = KotlinInstantSerializer::class)
val updatedAt: Instant,
override val schemaVersion: String = "1.0",
override val dataVersion: Long? = null
) : VersionedDto
@Serializable
@Since("1.0")
data class CreateVereinDto(
val oepsVereinsNr: String,
val name: String,
val kuerzel: String? = null,
val bundesland: String? = null,
val adresse: String? = null,
val plz: String? = null,
val ort: String? = null,
val email: String? = null,
val telefon: String? = null,
val webseite: String? = null,
val istAktiv: Boolean = true,
override val schemaVersion: String = "1.0",
override val dataVersion: Long? = null
) : VersionedDto
@Serializable
@Since("1.0")
data class UpdateVereinDto(
val name: String,
val kuerzel: String? = null,
val bundesland: String? = null,
val adresse: String? = null,
val plz: String? = null,
val ort: String? = null,
val email: String? = null,
val telefon: String? = null,
val webseite: String? = null,
val istAktiv: Boolean = true,
override val schemaVersion: String = "1.0",
override val dataVersion: Long? = null
) : VersionedDto
@@ -0,0 +1,122 @@
package at.mocode.dto.base
import kotlinx.serialization.Serializable
/**
* Manages API and DTO versioning across the application.
*/
object VersionManager {
// Current API version
const val CURRENT_API_VERSION = "1.0"
// Supported API versions (newest first)
val SUPPORTED_VERSIONS = listOf("1.0")
// Deprecated versions (still supported but discouraged)
val DEPRECATED_VERSIONS = emptyList<String>()
// Minimum client version required
const val MINIMUM_CLIENT_VERSION = "1.0"
/**
* Check if a version is supported
*/
fun isVersionSupported(version: String): Boolean {
return version in SUPPORTED_VERSIONS
}
/**
* Check if a version is deprecated
*/
fun isVersionDeprecated(version: String): Boolean {
return version in DEPRECATED_VERSIONS
}
/**
* Get version compatibility info
*/
fun getVersionInfo(): ApiVersionInfo {
return ApiVersionInfo(
apiVersion = CURRENT_API_VERSION,
supportedVersions = SUPPORTED_VERSIONS,
deprecatedVersions = DEPRECATED_VERSIONS,
minimumClientVersion = MINIMUM_CLIENT_VERSION
)
}
/**
* Validate client version compatibility
*/
fun validateClientVersion(clientVersion: String?): VersionValidationResult {
if (clientVersion == null) {
return VersionValidationResult.MissingVersion
}
if (!isVersionSupported(clientVersion)) {
return VersionValidationResult.UnsupportedVersion(clientVersion)
}
if (isVersionDeprecated(clientVersion)) {
return VersionValidationResult.DeprecatedVersion(clientVersion)
}
return VersionValidationResult.Valid(clientVersion)
}
}
/**
* Result of version validation
*/
sealed class VersionValidationResult {
data class Valid(val version: String) : VersionValidationResult()
data class DeprecatedVersion(val version: String) : VersionValidationResult()
data class UnsupportedVersion(val version: String) : VersionValidationResult()
object MissingVersion : VersionValidationResult()
}
/**
* Version migration interface for handling DTO evolution
*/
interface VersionMigrator<T : VersionedDto> {
/**
* Migrate DTO from one version to another
*/
fun migrate(dto: T, fromVersion: String, toVersion: String): T
/**
* Check if migration is supported between versions
*/
fun canMigrate(fromVersion: String, toVersion: String): Boolean
}
/**
* Registry for version migrators
*/
object MigratorRegistry {
private val migrators = mutableMapOf<String, VersionMigrator<*>>()
fun <T : VersionedDto> register(dtoClass: String, migrator: VersionMigrator<T>) {
migrators[dtoClass] = migrator
}
@Suppress("UNCHECKED_CAST")
fun <T : VersionedDto> getMigrator(dtoClass: String): VersionMigrator<T>? {
return migrators[dtoClass] as? VersionMigrator<T>
}
}
/**
* Version compatibility annotations
*/
@Target(AnnotationTarget.CLASS, AnnotationTarget.PROPERTY)
@Retention(AnnotationRetention.RUNTIME)
annotation class Since(val version: String)
@Target(AnnotationTarget.CLASS, AnnotationTarget.PROPERTY)
@Retention(AnnotationRetention.RUNTIME)
annotation class Deprecated(val version: String, val message: String = "")
@Target(AnnotationTarget.CLASS, AnnotationTarget.PROPERTY)
@Retention(AnnotationRetention.RUNTIME)
annotation class Until(val version: String)
@@ -0,0 +1,52 @@
package at.mocode.dto.base
import kotlinx.serialization.Serializable
/**
* Base interface for all versioned DTOs.
* Provides version information for API compatibility and evolution.
*/
interface VersionedDto {
/**
* The schema version of this DTO.
* Used for API versioning and backward compatibility.
*/
val schemaVersion: String
/**
* Optional data version for optimistic locking.
* Can be used to detect concurrent modifications.
*/
val dataVersion: Long?
get() = null
}
/**
* Base class for versioned DTOs with common versioning fields.
*/
@Serializable
abstract class BaseVersionedDto(
override val schemaVersion: String = "1.0",
override val dataVersion: Long? = null
) : VersionedDto
/**
* Version information for API responses.
*/
@Serializable
data class ApiVersionInfo(
val apiVersion: String,
val supportedVersions: List<String>,
val deprecatedVersions: List<String> = emptyList(),
val minimumClientVersion: String? = null
)
/**
* Wrapper for versioned API responses.
*/
@Serializable
data class VersionedResponse<T>(
val data: T,
val version: ApiVersionInfo,
val timestamp: String
) where T : VersionedDto
@@ -0,0 +1,40 @@
package at.mocode.dto.migrations
import at.mocode.dto.ArtikelDto
import at.mocode.dto.base.VersionMigrator
/**
* Migrator for ArtikelDto versions.
* Handles migration between different versions of ArtikelDto.
*/
class ArtikelDtoMigrator : VersionMigrator<ArtikelDto> {
override fun migrate(dto: ArtikelDto, fromVersion: String, toVersion: String): ArtikelDto {
return when {
fromVersion == "1.0" && toVersion == "1.0" -> dto
// Future migrations would be handled here
// fromVersion == "1.0" && toVersion == "1.1" -> migrateFrom1_0To1_1(dto)
// fromVersion == "1.1" && toVersion == "1.2" -> migrateFrom1_1To1_2(dto)
else -> throw IllegalArgumentException("Unsupported migration from $fromVersion to $toVersion")
}
}
override fun canMigrate(fromVersion: String, toVersion: String): Boolean {
return when {
fromVersion == "1.0" && toVersion == "1.0" -> true
// Future migration paths would be defined here
// fromVersion == "1.0" && toVersion == "1.1" -> true
// fromVersion == "1.1" && toVersion == "1.2" -> true
else -> false
}
}
// Example of future migration method
// private fun migrateFrom1_0To1_1(dto: ArtikelDto): ArtikelDto {
// return dto.copy(
// schemaVersion = "1.1",
// // Add new fields with default values
// // newField = "defaultValue"
// )
// }
}