From e2432510af1de17b04bfd25f1063c9b35f7cbfb6 Mon Sep 17 00:00:00 2001 From: StefanMoCoAt Date: Mon, 30 Jun 2025 22:25:35 +0200 Subject: [PATCH] =?UTF-8?q?(fix)=20Implementiere=20einen=20Service-Layer?= =?UTF-8?q?=20Erstellung=20von=20DTOs=20f=C3=BCr=20alle=20Ressourcen=20Imp?= =?UTF-8?q?lement=20a=20versioning=20system?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/API_VERSIONING.md | 272 +++++++++++++++++ .../kotlin/at/mocode/plugins/Versioning.kt | 106 +++++++ .../kotlin/at/mocode/routes/VereinRoutes.kt | 21 +- .../at/mocode/services/AbteilungService.kt | 192 ++++++++++++ .../at/mocode/services/ArtikelService.kt | 106 +++++++ .../at/mocode/services/BewerbService.kt | 254 ++++++++++++++++ .../at/mocode/services/DomLizenzService.kt | 132 ++++++++ .../at/mocode/services/DomPferdService.kt | 204 +++++++++++++ .../services/DomQualifikationService.kt | 157 ++++++++++ .../at/mocode/services/PersonService.kt | 122 ++++++++ .../at/mocode/services/ServiceLocator.kt | 31 +- .../at/mocode/services/TurnierService.kt | 124 ++++++++ .../mocode/services/VeranstaltungService.kt | 245 +++++++++++++++ .../at/mocode/services/VereinService.kt | 117 +++++++ .../test/kotlin/at/mocode/VersioningTest.kt | 116 +++++++ .../kotlin/at/mocode/dto/AbteilungDto.kt | 114 +++++++ .../kotlin/at/mocode/dto/ArtikelDto.kt | 53 ++++ .../kotlin/at/mocode/dto/BewerbDto.kt | 196 ++++++++++++ .../kotlin/at/mocode/dto/CommonDto.kt | 233 ++++++++++++++ .../kotlin/at/mocode/dto/DomaeneDto.kt | 269 +++++++++++++++++ .../kotlin/at/mocode/dto/SpecializedDto.kt | 285 ++++++++++++++++++ .../kotlin/at/mocode/dto/StammdatenDto.kt | 260 ++++++++++++++++ .../kotlin/at/mocode/dto/TurnierDto.kt | 137 +++++++++ .../kotlin/at/mocode/dto/VeranstaltungDto.kt | 88 ++++++ .../kotlin/at/mocode/dto/VereinDto.kt | 68 +++++ .../at/mocode/dto/base/VersionManager.kt | 122 ++++++++ .../kotlin/at/mocode/dto/base/VersionedDto.kt | 52 ++++ .../dto/migrations/ArtikelDtoMigrator.kt | 40 +++ 28 files changed, 4102 insertions(+), 14 deletions(-) create mode 100644 docs/API_VERSIONING.md create mode 100644 server/src/main/kotlin/at/mocode/plugins/Versioning.kt create mode 100644 server/src/main/kotlin/at/mocode/services/AbteilungService.kt create mode 100644 server/src/main/kotlin/at/mocode/services/ArtikelService.kt create mode 100644 server/src/main/kotlin/at/mocode/services/BewerbService.kt create mode 100644 server/src/main/kotlin/at/mocode/services/DomLizenzService.kt create mode 100644 server/src/main/kotlin/at/mocode/services/DomPferdService.kt create mode 100644 server/src/main/kotlin/at/mocode/services/DomQualifikationService.kt create mode 100644 server/src/main/kotlin/at/mocode/services/PersonService.kt create mode 100644 server/src/main/kotlin/at/mocode/services/TurnierService.kt create mode 100644 server/src/main/kotlin/at/mocode/services/VeranstaltungService.kt create mode 100644 server/src/main/kotlin/at/mocode/services/VereinService.kt create mode 100644 server/src/test/kotlin/at/mocode/VersioningTest.kt create mode 100644 shared/src/commonMain/kotlin/at/mocode/dto/AbteilungDto.kt create mode 100644 shared/src/commonMain/kotlin/at/mocode/dto/ArtikelDto.kt create mode 100644 shared/src/commonMain/kotlin/at/mocode/dto/BewerbDto.kt create mode 100644 shared/src/commonMain/kotlin/at/mocode/dto/CommonDto.kt create mode 100644 shared/src/commonMain/kotlin/at/mocode/dto/DomaeneDto.kt create mode 100644 shared/src/commonMain/kotlin/at/mocode/dto/SpecializedDto.kt create mode 100644 shared/src/commonMain/kotlin/at/mocode/dto/StammdatenDto.kt create mode 100644 shared/src/commonMain/kotlin/at/mocode/dto/TurnierDto.kt create mode 100644 shared/src/commonMain/kotlin/at/mocode/dto/VeranstaltungDto.kt create mode 100644 shared/src/commonMain/kotlin/at/mocode/dto/VereinDto.kt create mode 100644 shared/src/commonMain/kotlin/at/mocode/dto/base/VersionManager.kt create mode 100644 shared/src/commonMain/kotlin/at/mocode/dto/base/VersionedDto.kt create mode 100644 shared/src/commonMain/kotlin/at/mocode/dto/migrations/ArtikelDtoMigrator.kt diff --git a/docs/API_VERSIONING.md b/docs/API_VERSIONING.md new file mode 100644 index 00000000..587cdc5f --- /dev/null +++ b/docs/API_VERSIONING.md @@ -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() + 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 { + fun migrate(dto: T, fromVersion: String, toVersion: String): T + fun canMigrate(fromVersion: String, toVersion: String): Boolean +} +``` + +### Beispiel Migrator + +```kotlin +class ArtikelDtoMigrator : VersionMigrator { + 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 { + 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 + +{ + "data": { + "id": "...", + "bezeichnung": "Test Artikel", + "schemaVersion": "1.0", + "dataVersion": 1 + }, + "version": { + "apiVersion": "1.0", + "supportedVersions": ["1.0"], + "deprecatedVersions": [] + }, + "timestamp": "2024-01-01T12:00:00Z" +} +``` + +### Unsupported Version +```http +GET /api/artikel +API-Version: 2.0 + +HTTP/1.1 400 Bad Request +Content-Type: application/json + +{ + "error": "Unsupported API version: 2.0", + "supportedVersions": ["1.0"], + "currentVersion": "1.0" +} +``` + +### 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 diff --git a/server/src/main/kotlin/at/mocode/plugins/Versioning.kt b/server/src/main/kotlin/at/mocode/plugins/Versioning.kt new file mode 100644 index 00000000..4773a6a1 --- /dev/null +++ b/server/src/main/kotlin/at/mocode/plugins/Versioning.kt @@ -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("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 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 ApplicationCall.respondVersionedList( + status: HttpStatusCode = HttpStatusCode.OK, + data: List +) { + 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) +} diff --git a/server/src/main/kotlin/at/mocode/routes/VereinRoutes.kt b/server/src/main/kotlin/at/mocode/routes/VereinRoutes.kt index 1bf4aff8..ca19a61e 100644 --- a/server/src/main/kotlin/at/mocode/routes/VereinRoutes.kt +++ b/server/src/main/kotlin/at/mocode/routes/VereinRoutes.kt @@ -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() - 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() - 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 { diff --git a/server/src/main/kotlin/at/mocode/services/AbteilungService.kt b/server/src/main/kotlin/at/mocode/services/AbteilungService.kt new file mode 100644 index 00000000..d48286ed --- /dev/null +++ b/server/src/main/kotlin/at/mocode/services/AbteilungService.kt @@ -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 { + 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 { + return abteilungRepository.findByBewerbId(bewerbId) + } + + /** + * Find divisions by active status + */ + suspend fun getAbteilungenByAktiv(istAktiv: Boolean): List { + return abteilungRepository.findByAktiv(istAktiv) + } + + /** + * Get all active divisions + */ + suspend fun getActiveAbteilungen(): List { + return getAbteilungenByAktiv(true) + } + + /** + * Get all inactive divisions + */ + suspend fun getInactiveAbteilungen(): List { + return getAbteilungenByAktiv(false) + } + + /** + * Search for divisions by query string + */ + suspend fun searchAbteilungen(query: String): List { + 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 { + 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 + } +} diff --git a/server/src/main/kotlin/at/mocode/services/ArtikelService.kt b/server/src/main/kotlin/at/mocode/services/ArtikelService.kt new file mode 100644 index 00000000..73afa1c9 --- /dev/null +++ b/server/src/main/kotlin/at/mocode/services/ArtikelService.kt @@ -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 { + 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 { + return artikelRepository.findByVerbandsabgabe(istVerbandsabgabe) + } + + /** + * Search for articles by query string + */ + suspend fun searchArtikel(query: String): List { + 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 { + return getArtikelByVerbandsabgabe(true) + } + + /** + * Get all non-Verbandsabgabe articles + */ + suspend fun getNonVerbandsabgabeArtikel(): List { + 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") + } + } +} diff --git a/server/src/main/kotlin/at/mocode/services/BewerbService.kt b/server/src/main/kotlin/at/mocode/services/BewerbService.kt new file mode 100644 index 00000000..d1c8afa2 --- /dev/null +++ b/server/src/main/kotlin/at/mocode/services/BewerbService.kt @@ -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 { + 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 { + return bewerbRepository.findByTurnierId(turnierId) + } + + /** + * Find competitions by sport category (Sparte) + */ + suspend fun getBewerbeBySparte(sparte: String): List { + if (sparte.isBlank()) { + throw IllegalArgumentException("Sparte cannot be blank") + } + return bewerbRepository.findBySparte(sparte.trim()) + } + + /** + * Find competitions by class + */ + suspend fun getBewerbeByKlasse(klasse: String): List { + 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 { + return bewerbRepository.findByStartlisteFinal(istFinal) + } + + /** + * Find competitions by result list finalization status + */ + suspend fun getBewerbeByErgebnislisteFinal(istFinal: Boolean): List { + return bewerbRepository.findByErgebnislisteFinal(istFinal) + } + + /** + * Get competitions with finalized start lists + */ + suspend fun getBewerbeWithFinalStartliste(): List { + return getBewerbeByStartlisteFinal(true) + } + + /** + * Get competitions with finalized result lists + */ + suspend fun getBewerbeWithFinalErgebnisliste(): List { + return getBewerbeByErgebnislisteFinal(true) + } + + /** + * Search for competitions by query string + */ + suspend fun searchBewerbe(query: String): List { + 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 { + 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 + } +} diff --git a/server/src/main/kotlin/at/mocode/services/DomLizenzService.kt b/server/src/main/kotlin/at/mocode/services/DomLizenzService.kt new file mode 100644 index 00000000..1955db4a --- /dev/null +++ b/server/src/main/kotlin/at/mocode/services/DomLizenzService.kt @@ -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 { + 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 { + return domLizenzRepository.findByPersonId(personId) + } + + /** + * Find licenses by license type global ID + */ + suspend fun getLizenzenByLizenzTypGlobalId(lizenzTypGlobalId: Uuid): List { + return domLizenzRepository.findByLizenzTypGlobalId(lizenzTypGlobalId) + } + + /** + * Find active licenses by person ID + */ + suspend fun getActiveLizenzenByPersonId(personId: Uuid): List { + return domLizenzRepository.findActiveByPersonId(personId) + } + + /** + * Find licenses by validity year + */ + suspend fun getLizenzenByValidityYear(year: Int): List { + 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 { + 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 { + 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. + } +} diff --git a/server/src/main/kotlin/at/mocode/services/DomPferdService.kt b/server/src/main/kotlin/at/mocode/services/DomPferdService.kt new file mode 100644 index 00000000..ce857dfd --- /dev/null +++ b/server/src/main/kotlin/at/mocode/services/DomPferdService.kt @@ -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 { + 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 { + 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 { + return domPferdRepository.findByBesitzerId(besitzerId) + } + + /** + * Find horses by responsible person ID + */ + suspend fun getPferdeByVerantwortlichePersonId(personId: Uuid): List { + return domPferdRepository.findByVerantwortlichePersonId(personId) + } + + /** + * Find horses by home club ID + */ + suspend fun getPferdeByHeimatVereinId(vereinId: Uuid): List { + return domPferdRepository.findByHeimatVereinId(vereinId) + } + + /** + * Find horses by breed + */ + suspend fun getPferdeByRasse(rasse: String): List { + 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 { + 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 { + return domPferdRepository.findActiveHorses() + } + + /** + * Search for horses by query string + */ + suspend fun searchPferde(query: String): List { + 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") + } + } + } +} diff --git a/server/src/main/kotlin/at/mocode/services/DomQualifikationService.kt b/server/src/main/kotlin/at/mocode/services/DomQualifikationService.kt new file mode 100644 index 00000000..6e568636 --- /dev/null +++ b/server/src/main/kotlin/at/mocode/services/DomQualifikationService.kt @@ -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 { + 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 { + return domQualifikationRepository.findByPersonId(personId) + } + + /** + * Find qualifications by qualification type ID + */ + suspend fun getQualifikationenByQualTypId(qualTypId: Uuid): List { + return domQualifikationRepository.findByQualTypId(qualTypId) + } + + /** + * Find active qualifications by person ID + */ + suspend fun getActiveQualifikationenByPersonId(personId: Uuid): List { + return domQualifikationRepository.findActiveByPersonId(personId) + } + + /** + * Find qualifications by validity period + */ + suspend fun getQualifikationenByValidityPeriod(fromDate: LocalDate?, toDate: LocalDate?): List { + // 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 { + 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 { + 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") + } + } + } +} diff --git a/server/src/main/kotlin/at/mocode/services/PersonService.kt b/server/src/main/kotlin/at/mocode/services/PersonService.kt new file mode 100644 index 00000000..2096a4e0 --- /dev/null +++ b/server/src/main/kotlin/at/mocode/services/PersonService.kt @@ -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 { + 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 { + return personRepository.findByVereinId(vereinId) + } + + /** + * Search for persons by query string + */ + suspend fun searchPersons(query: String): List { + 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") + } + } + } +} diff --git a/server/src/main/kotlin/at/mocode/services/ServiceLocator.kt b/server/src/main/kotlin/at/mocode/services/ServiceLocator.kt index 682d0b20..dab07c51 100644 --- a/server/src/main/kotlin/at/mocode/services/ServiceLocator.kt +++ b/server/src/main/kotlin/at/mocode/services/ServiceLocator.kt @@ -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 } } diff --git a/server/src/main/kotlin/at/mocode/services/TurnierService.kt b/server/src/main/kotlin/at/mocode/services/TurnierService.kt new file mode 100644 index 00000000..a42825a8 --- /dev/null +++ b/server/src/main/kotlin/at/mocode/services/TurnierService.kt @@ -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 { + 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 { + 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 { + 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 { + 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") + } + } +} diff --git a/server/src/main/kotlin/at/mocode/services/VeranstaltungService.kt b/server/src/main/kotlin/at/mocode/services/VeranstaltungService.kt new file mode 100644 index 00000000..08929c02 --- /dev/null +++ b/server/src/main/kotlin/at/mocode/services/VeranstaltungService.kt @@ -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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 + } +} diff --git a/server/src/main/kotlin/at/mocode/services/VereinService.kt b/server/src/main/kotlin/at/mocode/services/VereinService.kt new file mode 100644 index 00000000..154d7bd1 --- /dev/null +++ b/server/src/main/kotlin/at/mocode/services/VereinService.kt @@ -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 { + 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 { + 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 { + 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") + } + } + } +} diff --git a/server/src/test/kotlin/at/mocode/VersioningTest.kt b/server/src/test/kotlin/at/mocode/VersioningTest.kt new file mode 100644 index 00000000..2aab849c --- /dev/null +++ b/server/src/test/kotlin/at/mocode/VersioningTest.kt @@ -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(validResult) + assertEquals("1.0", validResult.version) + + // Test unsupported version + val unsupportedResult = VersionManager.validateClientVersion("2.0") + assertIs(unsupportedResult) + assertEquals("2.0", unsupportedResult.version) + + // Test missing version + val missingResult = VersionManager.validateClientVersion(null) + assertIs(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")) + } +} diff --git a/shared/src/commonMain/kotlin/at/mocode/dto/AbteilungDto.kt b/shared/src/commonMain/kotlin/at/mocode/dto/AbteilungDto.kt new file mode 100644 index 00000000..4f4cbc7a --- /dev/null +++ b/shared/src/commonMain/kotlin/at/mocode/dto/AbteilungDto.kt @@ -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, + @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 = 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 = 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 +) diff --git a/shared/src/commonMain/kotlin/at/mocode/dto/ArtikelDto.kt b/shared/src/commonMain/kotlin/at/mocode/dto/ArtikelDto.kt new file mode 100644 index 00000000..1c8270cb --- /dev/null +++ b/shared/src/commonMain/kotlin/at/mocode/dto/ArtikelDto.kt @@ -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 diff --git a/shared/src/commonMain/kotlin/at/mocode/dto/BewerbDto.kt b/shared/src/commonMain/kotlin/at/mocode/dto/BewerbDto.kt new file mode 100644 index 00000000..750b2b25 --- /dev/null +++ b/shared/src/commonMain/kotlin/at/mocode/dto/BewerbDto.kt @@ -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, + @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 = 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 = 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 +) diff --git a/shared/src/commonMain/kotlin/at/mocode/dto/CommonDto.kt b/shared/src/commonMain/kotlin/at/mocode/dto/CommonDto.kt new file mode 100644 index 00000000..d87f56db --- /dev/null +++ b/shared/src/commonMain/kotlin/at/mocode/dto/CommonDto.kt @@ -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, + 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 = 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 = emptyList(), + val istVerfuegbar: Boolean = true, + val kontaktInfo: String? = null, + val notizen: String? = null +) diff --git a/shared/src/commonMain/kotlin/at/mocode/dto/DomaeneDto.kt b/shared/src/commonMain/kotlin/at/mocode/dto/DomaeneDto.kt new file mode 100644 index 00000000..952232c5 --- /dev/null +++ b/shared/src/commonMain/kotlin/at/mocode/dto/DomaeneDto.kt @@ -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 +) diff --git a/shared/src/commonMain/kotlin/at/mocode/dto/SpecializedDto.kt b/shared/src/commonMain/kotlin/at/mocode/dto/SpecializedDto.kt new file mode 100644 index 00000000..f1fc7d45 --- /dev/null +++ b/shared/src/commonMain/kotlin/at/mocode/dto/SpecializedDto.kt @@ -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, + @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? +) diff --git a/shared/src/commonMain/kotlin/at/mocode/dto/StammdatenDto.kt b/shared/src/commonMain/kotlin/at/mocode/dto/StammdatenDto.kt new file mode 100644 index 00000000..0cf41db9 --- /dev/null +++ b/shared/src/commonMain/kotlin/at/mocode/dto/StammdatenDto.kt @@ -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, + val lizenzen: List, + val qualifikationenRichter: List, + val qualifikationenParcoursbauer: List, + 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 = emptySet(), + val lizenzen: List = emptyList(), + val qualifikationenRichter: List = emptyList(), + val qualifikationenParcoursbauer: List = 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 = emptySet(), + val lizenzen: List = emptyList(), + val qualifikationenRichter: List = emptyList(), + val qualifikationenParcoursbauer: List = 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 +) diff --git a/shared/src/commonMain/kotlin/at/mocode/dto/TurnierDto.kt b/shared/src/commonMain/kotlin/at/mocode/dto/TurnierDto.kt new file mode 100644 index 00000000..b1e24ffa --- /dev/null +++ b/shared/src/commonMain/kotlin/at/mocode/dto/TurnierDto.kt @@ -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, + val nennungsHinweis: String?, + val eigenesNennsystemUrl: String?, + @Serializable(with = BigDecimalSerializer::class) + val nenngeld: BigDecimal?, + @Serializable(with = BigDecimalSerializer::class) + val startgeldStandard: BigDecimal?, + val austragungsplaetze: List, + val vorbereitungsplaetze: List, + @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, + val meisterschaftRefs: List, + @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 = 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 = emptyList(), + val vorbereitungsplaetze: List = 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 = emptyList(), + val meisterschaftRefs: List = 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 = 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 = emptyList(), + val vorbereitungsplaetze: List = 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 = emptyList(), + val meisterschaftRefs: List = emptyList() +) diff --git a/shared/src/commonMain/kotlin/at/mocode/dto/VeranstaltungDto.kt b/shared/src/commonMain/kotlin/at/mocode/dto/VeranstaltungDto.kt new file mode 100644 index 00000000..1ac5b401 --- /dev/null +++ b/shared/src/commonMain/kotlin/at/mocode/dto/VeranstaltungDto.kt @@ -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, + 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 = 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 = emptyList(), + val dsgvoText: String? = null, + val haftungsText: String? = null, + val sonstigeBesondereBestimmungen: String? = null +) diff --git a/shared/src/commonMain/kotlin/at/mocode/dto/VereinDto.kt b/shared/src/commonMain/kotlin/at/mocode/dto/VereinDto.kt new file mode 100644 index 00000000..3456d640 --- /dev/null +++ b/shared/src/commonMain/kotlin/at/mocode/dto/VereinDto.kt @@ -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 diff --git a/shared/src/commonMain/kotlin/at/mocode/dto/base/VersionManager.kt b/shared/src/commonMain/kotlin/at/mocode/dto/base/VersionManager.kt new file mode 100644 index 00000000..aaf34a51 --- /dev/null +++ b/shared/src/commonMain/kotlin/at/mocode/dto/base/VersionManager.kt @@ -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() + + // 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 { + /** + * 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>() + + fun register(dtoClass: String, migrator: VersionMigrator) { + migrators[dtoClass] = migrator + } + + @Suppress("UNCHECKED_CAST") + fun getMigrator(dtoClass: String): VersionMigrator? { + return migrators[dtoClass] as? VersionMigrator + } +} + +/** + * 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) diff --git a/shared/src/commonMain/kotlin/at/mocode/dto/base/VersionedDto.kt b/shared/src/commonMain/kotlin/at/mocode/dto/base/VersionedDto.kt new file mode 100644 index 00000000..86741528 --- /dev/null +++ b/shared/src/commonMain/kotlin/at/mocode/dto/base/VersionedDto.kt @@ -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, + val deprecatedVersions: List = emptyList(), + val minimumClientVersion: String? = null +) + +/** + * Wrapper for versioned API responses. + */ +@Serializable +data class VersionedResponse( + val data: T, + val version: ApiVersionInfo, + val timestamp: String +) where T : VersionedDto diff --git a/shared/src/commonMain/kotlin/at/mocode/dto/migrations/ArtikelDtoMigrator.kt b/shared/src/commonMain/kotlin/at/mocode/dto/migrations/ArtikelDtoMigrator.kt new file mode 100644 index 00000000..ba90e641 --- /dev/null +++ b/shared/src/commonMain/kotlin/at/mocode/dto/migrations/ArtikelDtoMigrator.kt @@ -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 { + + 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" + // ) + // } +}