From d8c9d11adb32e74febfffa7725aedc3e0d2a8037 Mon Sep 17 00:00:00 2001 From: Stefan Mogeritsch Date: Mon, 30 Mar 2026 14:47:11 +0200 Subject: [PATCH] feat(masterdata): introduce Reiter-Sparte persistence, services, and validations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added `ReiterSparteTable` to manage rider-discipline associations. - Introduced services and tests for `LicenseMatrix`, `Altersklasse`, and `AbteilungsRegel` with domain logic and validations for ÖTO compliance. - Enhanced `ExposedReiterRepository` to save and query `Reiter` disciplines efficiently. - Implemented database migration script `V007__Cleanup_Initial_Tables_and_Add_Sparte.sql`. - Updated `MasterdataDatabaseConfiguration` to include `ReiterSparteTable` in the schema initialization. - Expanded test coverage with new cases for eligibility checks, age group determinations, and splitting regulations. Signed-off-by: Stefan Mogeritsch --- backend/services/masterdata/docs/ROADMAP.md | 6 +- .../domain/model/GebuehrDefinition.kt | 33 +++++ .../domain/model/LicenseMatrixEntry.kt | 34 +++++ .../domain/model/RegulationConfig.kt | 32 +++++ .../domain/model/RichtverfahrenDefinition.kt | 34 +++++ .../domain/model/TurnierklasseDefinition.kt | 35 +++++ .../domain/service/AbteilungsRegelService.kt | 53 ++++++++ .../service/AbteilungsRegelServiceImpl.kt | 73 ++++++++++ .../domain/service/LicenseMatrixService.kt | 44 ++++++ .../service/LicenseMatrixServiceImpl.kt | 48 +++++++ .../service/AbteilungsRegelServiceTest.kt | 110 +++++++++++++++ .../domain/service/AltersklasseRechnerTest.kt | 113 ++++++++++++++++ .../service/LicenseMatrixServiceTest.kt | 128 ++++++++++++++++++ .../persistence/ExposedReiterRepository.kt | 102 +++++++++----- .../persistence/ReiterSparteTable.kt | 26 ++++ .../config/MasterdataDatabaseConfiguration.kt | 6 +- ..._Cleanup_Initial_Tables_and_Add_Sparte.sql | 35 +++++ 17 files changed, 871 insertions(+), 41 deletions(-) create mode 100644 backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/model/GebuehrDefinition.kt create mode 100644 backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/model/LicenseMatrixEntry.kt create mode 100644 backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/model/RegulationConfig.kt create mode 100644 backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/model/RichtverfahrenDefinition.kt create mode 100644 backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/model/TurnierklasseDefinition.kt create mode 100644 backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/service/AbteilungsRegelService.kt create mode 100644 backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/service/AbteilungsRegelServiceImpl.kt create mode 100644 backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/service/LicenseMatrixService.kt create mode 100644 backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/service/LicenseMatrixServiceImpl.kt create mode 100644 backend/services/masterdata/masterdata-domain/src/commonTest/kotlin/at/mocode/masterdata/domain/service/AbteilungsRegelServiceTest.kt create mode 100644 backend/services/masterdata/masterdata-domain/src/commonTest/kotlin/at/mocode/masterdata/domain/service/AltersklasseRechnerTest.kt create mode 100644 backend/services/masterdata/masterdata-domain/src/commonTest/kotlin/at/mocode/masterdata/domain/service/LicenseMatrixServiceTest.kt create mode 100644 backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/ReiterSparteTable.kt create mode 100644 backend/services/masterdata/masterdata-service/src/main/resources/db/migration/V007__Cleanup_Initial_Tables_and_Add_Sparte.sql diff --git a/backend/services/masterdata/docs/ROADMAP.md b/backend/services/masterdata/docs/ROADMAP.md index 7a2c8fa7..d8a3c765 100644 --- a/backend/services/masterdata/docs/ROADMAP.md +++ b/backend/services/masterdata/docs/ROADMAP.md @@ -132,9 +132,9 @@ ## Nächste konkrete Schritte (2‑Wochen Sprint‑Plan) 1. [x] ADRs für Importer‑Einbettung, Rule‑Versionierung, API-Schichten abschließen (🏗️) -2. Exposed-Tabellen vervollständigen und in `SchemaUtils.create`/Migrationen registrieren (👷) -3. UseCases: Altersklasse, Lizenz‑Matrix, Abteilungs‑Regeln inkl. Unit‑Tests (👷🧐) -4. ZNS‑Importer an Repositories anbinden, Idempotenz-Checks ergänzen, Mini‑ZNS Testlauf (👷🧐) +2. [x] Exposed-Tabellen vervollständigen und in `SchemaUtils.create`/Migrationen registrieren (👷) +3. [x] UseCases: Altersklasse, Lizenz‑Matrix, Abteilungs‑Regeln inkl. Unit‑Tests (👷🧐) +4. [ ] ZNS‑Importer an Repositories anbinden, Idempotenz-Checks ergänzen, Mini‑ZNS Testlauf (👷🧐) * 5. API v1 Endpunkte + OpenAPI, Contract‑Tests (👷🧐) 6. Observability-Grundlagen (Metriken + Dashboards) (🐧) 7. Curator: Docs aktualisieren, Runbooks und Changelogs pflegen (🧹) diff --git a/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/model/GebuehrDefinition.kt b/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/model/GebuehrDefinition.kt new file mode 100644 index 00000000..e76e3592 --- /dev/null +++ b/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/model/GebuehrDefinition.kt @@ -0,0 +1,33 @@ +@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class) + +package at.mocode.masterdata.domain.model + +import at.mocode.core.domain.serialization.InstantSerializer +import at.mocode.core.domain.serialization.UuidSerializer +import kotlinx.serialization.Serializable +import kotlin.time.Instant +import kotlin.uuid.Uuid + +/** + * Domänenmodell für Gebühren gemäß ÖTO oder Veranstaltervorgabe. + */ +@Serializable +data class GebuehrDefinition( + @Serializable(with = UuidSerializer::class) + val gebuehrId: Uuid = Uuid.random(), + val bezeichnung: String, + val typ: String, // NENNUNG, STARTGELD, BOX, STALLGELD, SONSTIGES + val betrag: Double, + val waehrung: String = "EUR", + + @Serializable(with = InstantSerializer::class) + val validFrom: Instant, + @Serializable(with = InstantSerializer::class) + val validTo: Instant? = null, + + val istAktiv: Boolean = true, + @Serializable(with = InstantSerializer::class) + val createdAt: Instant, + @Serializable(with = InstantSerializer::class) + val updatedAt: Instant +) diff --git a/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/model/LicenseMatrixEntry.kt b/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/model/LicenseMatrixEntry.kt new file mode 100644 index 00000000..b186b434 --- /dev/null +++ b/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/model/LicenseMatrixEntry.kt @@ -0,0 +1,34 @@ +@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class) + +package at.mocode.masterdata.domain.model + +import at.mocode.core.domain.model.LizenzKlasseE +import at.mocode.core.domain.model.SparteE +import at.mocode.core.domain.serialization.InstantSerializer +import at.mocode.core.domain.serialization.UuidSerializer +import kotlinx.serialization.Serializable +import kotlin.time.Instant +import kotlin.uuid.Uuid + +/** + * Domänenmodell für die Lizenz-Matrix (Reiter-Lizenz vs. maximal erlaubte Turnierklasse). + */ +@Serializable +data class LicenseMatrixEntry( + @Serializable(with = UuidSerializer::class) + val licenseId: Uuid = Uuid.random(), + val sparte: SparteE, + val lizenzKlasse: LizenzKlasseE, + val maxTurnierklasseCode: String, // E, A, L, LM, M, S + + @Serializable(with = InstantSerializer::class) + val validFrom: Instant, + @Serializable(with = InstantSerializer::class) + val validTo: Instant? = null, + + val istAktiv: Boolean = true, + @Serializable(with = InstantSerializer::class) + val createdAt: Instant, + @Serializable(with = InstantSerializer::class) + val updatedAt: Instant +) diff --git a/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/model/RegulationConfig.kt b/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/model/RegulationConfig.kt new file mode 100644 index 00000000..315e171f --- /dev/null +++ b/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/model/RegulationConfig.kt @@ -0,0 +1,32 @@ +@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class) + +package at.mocode.masterdata.domain.model + +import at.mocode.core.domain.serialization.InstantSerializer +import at.mocode.core.domain.serialization.UuidSerializer +import kotlinx.serialization.Serializable +import kotlin.time.Instant +import kotlin.uuid.Uuid + +/** + * Domänenmodell für allgemeine Regelkonfigurationen. + */ +@Serializable +data class RegulationConfig( + @Serializable(with = UuidSerializer::class) + val configId: Uuid = Uuid.random(), + val key: String, + val value: String, + val beschreibung: String? = null, + + @Serializable(with = InstantSerializer::class) + val validFrom: Instant, + @Serializable(with = InstantSerializer::class) + val validTo: Instant? = null, + + val istAktiv: Boolean = true, + @Serializable(with = InstantSerializer::class) + val createdAt: Instant, + @Serializable(with = InstantSerializer::class) + val updatedAt: Instant +) diff --git a/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/model/RichtverfahrenDefinition.kt b/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/model/RichtverfahrenDefinition.kt new file mode 100644 index 00000000..48f4dcf0 --- /dev/null +++ b/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/model/RichtverfahrenDefinition.kt @@ -0,0 +1,34 @@ +@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class) + +package at.mocode.masterdata.domain.model + +import at.mocode.core.domain.model.SparteE +import at.mocode.core.domain.serialization.InstantSerializer +import at.mocode.core.domain.serialization.UuidSerializer +import kotlinx.serialization.Serializable +import kotlin.time.Instant +import kotlin.uuid.Uuid + +/** + * Domänenmodell für Richtverfahren gemäß ÖTO. + */ +@Serializable +data class RichtverfahrenDefinition( + @Serializable(with = UuidSerializer::class) + val richtverfahrenId: Uuid = Uuid.random(), + val sparte: SparteE, + val code: String, // A1, A2, AM5, RV_A, RV_B + val bezeichnung: String, + val beschreibung: String? = null, + + @Serializable(with = InstantSerializer::class) + val validFrom: Instant, + @Serializable(with = InstantSerializer::class) + val validTo: Instant? = null, + + val istAktiv: Boolean = true, + @Serializable(with = InstantSerializer::class) + val createdAt: Instant, + @Serializable(with = InstantSerializer::class) + val updatedAt: Instant +) diff --git a/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/model/TurnierklasseDefinition.kt b/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/model/TurnierklasseDefinition.kt new file mode 100644 index 00000000..aff7e55f --- /dev/null +++ b/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/model/TurnierklasseDefinition.kt @@ -0,0 +1,35 @@ +@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class) + +package at.mocode.masterdata.domain.model + +import at.mocode.core.domain.model.SparteE +import at.mocode.core.domain.serialization.InstantSerializer +import at.mocode.core.domain.serialization.UuidSerializer +import kotlinx.serialization.Serializable +import kotlin.time.Instant +import kotlin.uuid.Uuid + +/** + * Domänenmodell für eine Turnierklasse gemäß ÖTO. + */ +@Serializable +data class TurnierklasseDefinition( + @Serializable(with = UuidSerializer::class) + val turnierklasseId: Uuid = Uuid.random(), + val sparte: SparteE, + val code: String, // E, A, L, LM, M, S + val bezeichnung: String, + val maxHoehe: Int? = null, // in cm (Springen) + val aufgabenNiveau: String? = null, // (Dressur) + + @Serializable(with = InstantSerializer::class) + val validFrom: Instant, + @Serializable(with = InstantSerializer::class) + val validTo: Instant? = null, + + val istAktiv: Boolean = true, + @Serializable(with = InstantSerializer::class) + val createdAt: Instant, + @Serializable(with = InstantSerializer::class) + val updatedAt: Instant +) diff --git a/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/service/AbteilungsRegelService.kt b/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/service/AbteilungsRegelService.kt new file mode 100644 index 00000000..4c9194b7 --- /dev/null +++ b/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/service/AbteilungsRegelService.kt @@ -0,0 +1,53 @@ +package at.mocode.masterdata.domain.service + +import at.mocode.masterdata.domain.model.DomPferd +import at.mocode.masterdata.domain.model.DomReiter + +/** + * Service zur Prüfung von Abteilungs-Regeln gemäß ÖTO § 39. + */ +interface AbteilungsRegelService { + + /** + * Prüft, ob eine strukturelle Teilung (unabhängig von der Starterzahl) erforderlich ist. + * Gemäß § 39 A-Teil: + * - Klassen A & L: Trennung nach R1/RD1 (Abt. 1) und höher (Abt. 2+). + * - CSN-C-NEU (bis 95cm): Abt. 1 (lizenzfrei), Abt. 2 (R1), Abt. 3 (R2+). + * + * @param reiter Der Reiter. + * @param pferd Das Pferd. + * @param turnierklasseCode Der Code der Turnierklasse (E, A, L, ...). + * @param sparte Die Sparte (DRESSUR, SPRINGEN). + * @param istCNeu Ob es sich um ein C-NEU Turnier handelt. + * @param hoehe Bei Springen: Die Hindernishöhe in cm. + * @return Die Abteilungsnummer (1, 2, 3), in die der Teilnehmer fällt. + */ + fun ermittleAbteilungStrukturell( + reiter: DomReiter, + pferd: DomPferd, + turnierklasseCode: String, + sparte: at.mocode.core.domain.model.SparteE, + istCNeu: Boolean = false, + hoehe: Int? = null + ): Int + + /** + * Prüft, ob eine kapazitive Teilung (aufgrund der Starterzahl) erforderlich ist. + * Gemäß § 39 A-Teil: + * - Standard-Springen: > 80 Starter. + * - Stil- & Springpferdeprüfungen: > 30 Starter. + * - Dressur: > 30 Starter (Empfehlung/Warnung). + * + * @param starterAnzahl Aktuelle Anzahl der Nennungen/Starter. + * @param turnierklasseCode Der Code der Turnierklasse. + * @param sparte Die Sparte. + * @param istStilOderJungpferdePruefung Ob es sich um eine Stil- oder Jungpferdeprüfung handelt. + * @return true, wenn eine Teilung MUSS oder SOLLTE (Warnung). + */ + fun istTeilungErforderlich( + starterAnzahl: Int, + turnierklasseCode: String, + sparte: at.mocode.core.domain.model.SparteE, + istStilOderJungpferdePruefung: Boolean = false + ): Boolean +} diff --git a/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/service/AbteilungsRegelServiceImpl.kt b/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/service/AbteilungsRegelServiceImpl.kt new file mode 100644 index 00000000..1ec1d891 --- /dev/null +++ b/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/service/AbteilungsRegelServiceImpl.kt @@ -0,0 +1,73 @@ +package at.mocode.masterdata.domain.service + +import at.mocode.core.domain.model.LizenzKlasseE +import at.mocode.core.domain.model.SparteE +import at.mocode.masterdata.domain.model.DomPferd +import at.mocode.masterdata.domain.model.DomReiter + +/** + * Standard-Implementierung des [AbteilungsRegelService] gemäß ÖTO § 39. + */ +class AbteilungsRegelServiceImpl : AbteilungsRegelService { + + override fun ermittleAbteilungStrukturell( + reiter: DomReiter, + pferd: DomPferd, + turnierklasseCode: String, + sparte: SparteE, + istCNeu: Boolean, + hoehe: Int? + ): Int { + // Gemäß § 39 A-Teil / 3.1 Strukturelle Teilung + + // Fall 1: CSN-C-NEU (Spezialregeln) + if (istCNeu && sparte == SparteE.SPRINGEN) { + if (hoehe != null && hoehe <= 95) { + return when (reiter.lizenzKlasse) { + LizenzKlasseE.LIZENZFREI -> 1 + LizenzKlasseE.R1, LizenzKlasseE.RD1 -> 2 + else -> 3 // R2+ + } + } else if (hoehe != null && hoehe >= 100) { + return when (reiter.lizenzKlasse) { + LizenzKlasseE.R1, LizenzKlasseE.RD1 -> 1 + else -> 2 // R2+ + } + } + } + + // Fall 2: Klassen A & L (Standardregelung § 39 Abs. 1) + if (turnierklasseCode == "A" || turnierklasseCode == "L") { + return when (reiter.lizenzKlasse) { + LizenzKlasseE.R1, LizenzKlasseE.RD1 -> 1 // Abt. 1: R1 + else -> 2 // Abt. 2+: R2 und höher + } + } + + // Default: Keine strukturelle Teilung (Abt. 1) + return 1 + } + + override fun istTeilungErforderlich( + starterAnzahl: Int, + turnierklasseCode: String, + sparte: SparteE, + istStilOderJungpferdePruefung: Boolean + ): Boolean { + // Gemäß § 39 A-Teil / 3.2 Kapazitive Teilung + + if (sparte == SparteE.SPRINGEN) { + return if (istStilOderJungpferdePruefung) { + starterAnzahl > 30 // MUSS + } else { + starterAnzahl > 80 // MUSS + } + } + + if (sparte == SparteE.DRESSUR) { + return starterAnzahl > 30 // KANN (System gibt Warnung) + } + + return false + } +} diff --git a/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/service/LicenseMatrixService.kt b/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/service/LicenseMatrixService.kt new file mode 100644 index 00000000..51585f94 --- /dev/null +++ b/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/service/LicenseMatrixService.kt @@ -0,0 +1,44 @@ +package at.mocode.masterdata.domain.service + +import at.mocode.core.domain.model.SparteE +import at.mocode.masterdata.domain.model.DomReiter +import at.mocode.masterdata.domain.model.LicenseMatrixEntry +import at.mocode.masterdata.domain.model.TurnierklasseDefinition + +/** + * Service zur Prüfung der Teilnahmeberechtigung basierend auf der Lizenz-Matrix. + */ +interface LicenseMatrixService { + + /** + * Prüft, ob ein Reiter mit seiner aktuellen Lizenz in einer bestimmten Turnierklasse startberechtigt ist. + * + * @param reiter Der Reiter, dessen Berechtigung geprüft werden soll. + * @param turnierklasse Die Turnierklasse (E, A, L, LM, M, S), in der gestartet werden soll. + * @param sparte Die Sparte des Bewerbs. + * @param matrix Die aktuelle Lizenz-Matrix (Regulation-as-Data). + * @param alleKlassen Alle verfügbaren Turnierklassen-Definitionen zur Code-Validierung. + * @return true, wenn der Reiter startberechtigt ist, sonst false. + */ + fun isEligible( + reiter: DomReiter, + turnierklasse: TurnierklasseDefinition, + sparte: SparteE, + matrix: List, + alleKlassen: List + ): Boolean + + /** + * Ermittelt die maximal erlaubte Turnierklasse für einen Reiter in einer Sparte. + * + * @param reiter Der Reiter. + * @param sparte Die Sparte. + * @param matrix Die aktuelle Lizenz-Matrix. + * @return Der Code der maximal erlaubten Turnierklasse oder null, wenn keine Regel gefunden wurde. + */ + fun getMaxTurnierklasse( + reiter: DomReiter, + sparte: SparteE, + matrix: List + ): String? +} diff --git a/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/service/LicenseMatrixServiceImpl.kt b/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/service/LicenseMatrixServiceImpl.kt new file mode 100644 index 00000000..66783d59 --- /dev/null +++ b/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/service/LicenseMatrixServiceImpl.kt @@ -0,0 +1,48 @@ +package at.mocode.masterdata.domain.service + +import at.mocode.core.domain.model.SparteE +import at.mocode.masterdata.domain.model.DomReiter +import at.mocode.masterdata.domain.model.LicenseMatrixEntry +import at.mocode.masterdata.domain.model.TurnierklasseDefinition + +/** + * Standard-Implementierung des [LicenseMatrixService] gemäß ÖTO. + */ +class LicenseMatrixServiceImpl : LicenseMatrixService { + + private val classHierarchy = listOf("E", "A", "L", "LM", "M", "S") + + override fun isEligible( + reiter: DomReiter, + turnierklasse: TurnierklasseDefinition, + sparte: SparteE, + matrix: List, + alleKlassen: List + ): Boolean { + // 1. Basis-Check: Hat der Reiter überhaupt eine Lizenz für diese Sparte? + if (!reiter.hasLizenzForSparte(sparte)) return false + + // 2. Max Turnierklasse aus Matrix ermitteln + val maxClassCode = getMaxTurnierklasse(reiter, sparte, matrix) ?: return false + + // 3. Hierarchie-Check (maxClassCode vs. turnierklasse.code) + val maxIndex = classHierarchy.indexOf(maxClassCode) + val targetIndex = classHierarchy.indexOf(turnierklasse.code) + + if (maxIndex == -1 || targetIndex == -1) return false + + return targetIndex <= maxIndex + } + + override fun getMaxTurnierklasse( + reiter: DomReiter, + sparte: SparteE, + matrix: List + ): String? { + // Suche passenden Eintrag in der Matrix für (Sparte, Lizenzklasse) + val entry = matrix.find { it.sparte == sparte && it.lizenzKlasse == reiter.lizenzKlasse } + ?: matrix.find { it.sparte == SparteE.DRESSUR && sparte == SparteE.DRESSUR && it.lizenzKlasse == reiter.lizenzKlasse } // Fallback/Spezial + + return entry?.maxTurnierklasseCode + } +} diff --git a/backend/services/masterdata/masterdata-domain/src/commonTest/kotlin/at/mocode/masterdata/domain/service/AbteilungsRegelServiceTest.kt b/backend/services/masterdata/masterdata-domain/src/commonTest/kotlin/at/mocode/masterdata/domain/service/AbteilungsRegelServiceTest.kt new file mode 100644 index 00000000..c5207e89 --- /dev/null +++ b/backend/services/masterdata/masterdata-domain/src/commonTest/kotlin/at/mocode/masterdata/domain/service/AbteilungsRegelServiceTest.kt @@ -0,0 +1,110 @@ +@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class) + +package at.mocode.masterdata.domain.service + +import at.mocode.core.domain.model.LizenzKlasseE +import at.mocode.core.domain.model.PferdeGeschlechtE +import at.mocode.core.domain.model.SparteE +import at.mocode.masterdata.domain.model.DomPferd +import at.mocode.masterdata.domain.model.DomReiter +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue +import kotlin.uuid.Uuid + +class AbteilungsRegelServiceTest { + + private val service = AbteilungsRegelServiceImpl() + + private val standardPferd = DomPferd(pferdeName = "Testpferd", geschlecht = PferdeGeschlechtE.WALLACH) + private val dummyPersonId = Uuid.random() + + @Test + fun `ermittleAbteilungStrukturell teilt Klassen A und L nach R1`() { + val r1Reiter = DomReiter( + personId = dummyPersonId, + satznummer = "1", + nachname = "R1", + vorname = "R1", + lizenzKlasse = LizenzKlasseE.R1 + ) + val r2Reiter = DomReiter( + personId = dummyPersonId, + satznummer = "2", + nachname = "R2", + vorname = "R2", + lizenzKlasse = LizenzKlasseE.R2 + ) + + assertEquals(1, service.ermittleAbteilungStrukturell(r1Reiter, standardPferd, "A", SparteE.SPRINGEN)) + assertEquals(2, service.ermittleAbteilungStrukturell(r2Reiter, standardPferd, "A", SparteE.SPRINGEN)) + + assertEquals(1, service.ermittleAbteilungStrukturell(r1Reiter, standardPferd, "L", SparteE.SPRINGEN)) + assertEquals(2, service.ermittleAbteilungStrukturell(r2Reiter, standardPferd, "L", SparteE.SPRINGEN)) + } + + @Test + fun `ermittleAbteilungStrukturell berücksichtigt C-NEU Regeln`() { + val lfReiter = DomReiter( + personId = dummyPersonId, + satznummer = "0", + nachname = "LF", + vorname = "LF", + lizenzKlasse = LizenzKlasseE.LIZENZFREI + ) + val r1Reiter = DomReiter( + personId = dummyPersonId, + satznummer = "1", + nachname = "R1", + vorname = "R1", + lizenzKlasse = LizenzKlasseE.R1 + ) + val r2Reiter = DomReiter( + personId = dummyPersonId, + satznummer = "2", + nachname = "R2", + vorname = "R2", + lizenzKlasse = LizenzKlasseE.R2 + ) + + // Bis 95cm + assertEquals( + 1, + service.ermittleAbteilungStrukturell(lfReiter, standardPferd, "E", SparteE.SPRINGEN, istCNeu = true, hoehe = 90) + ) + assertEquals( + 2, + service.ermittleAbteilungStrukturell(r1Reiter, standardPferd, "E", SparteE.SPRINGEN, istCNeu = true, hoehe = 90) + ) + assertEquals( + 3, + service.ermittleAbteilungStrukturell(r2Reiter, standardPferd, "E", SparteE.SPRINGEN, istCNeu = true, hoehe = 90) + ) + + // Ab 100cm + assertEquals( + 1, + service.ermittleAbteilungStrukturell(r1Reiter, standardPferd, "A", SparteE.SPRINGEN, istCNeu = true, hoehe = 100) + ) + assertEquals( + 2, + service.ermittleAbteilungStrukturell(r2Reiter, standardPferd, "A", SparteE.SPRINGEN, istCNeu = true, hoehe = 100) + ) + } + + @Test + fun `istTeilungErforderlich prüft Starterzahlen`() { + // Springen Standard + assertFalse(service.istTeilungErforderlich(80, "A", SparteE.SPRINGEN)) + assertTrue(service.istTeilungErforderlich(81, "A", SparteE.SPRINGEN)) + + // Springen Stil / Jungpferde + assertFalse(service.istTeilungErforderlich(30, "A", SparteE.SPRINGEN, istStilOderJungpferdePruefung = true)) + assertTrue(service.istTeilungErforderlich(31, "A", SparteE.SPRINGEN, istStilOderJungpferdePruefung = true)) + + // Dressur + assertFalse(service.istTeilungErforderlich(30, "A", SparteE.DRESSUR)) + assertTrue(service.istTeilungErforderlich(31, "A", SparteE.DRESSUR)) + } +} diff --git a/backend/services/masterdata/masterdata-domain/src/commonTest/kotlin/at/mocode/masterdata/domain/service/AltersklasseRechnerTest.kt b/backend/services/masterdata/masterdata-domain/src/commonTest/kotlin/at/mocode/masterdata/domain/service/AltersklasseRechnerTest.kt new file mode 100644 index 00000000..e914dab0 --- /dev/null +++ b/backend/services/masterdata/masterdata-domain/src/commonTest/kotlin/at/mocode/masterdata/domain/service/AltersklasseRechnerTest.kt @@ -0,0 +1,113 @@ +@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class) + +package at.mocode.masterdata.domain.service + +import at.mocode.core.domain.model.SparteE +import at.mocode.masterdata.domain.model.AltersklasseDefinition +import at.mocode.masterdata.domain.model.DomReiter +import kotlinx.datetime.LocalDate +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue +import kotlin.time.Clock +import kotlin.uuid.Uuid + +class AltersklasseRechnerTest { + + private val rechner = AltersklasseRechnerImpl() + + @Test + fun `berechneOetoAlter berechnet korrektes Alter am 31_12_`() { + val geb = LocalDate(2010, 5, 15) + assertEquals(16, rechner.berechneOetoAlter(geb, 2026)) + + val gebSilvester = LocalDate(2010, 12, 31) + assertEquals(16, rechner.berechneOetoAlter(gebSilvester, 2026)) + + val gebNeujahr = LocalDate(2011, 1, 1) + assertEquals(15, rechner.berechneOetoAlter(gebNeujahr, 2026)) + } + + @Test + fun `ermittleAltersklassen findet passende Definitionen`() { + val reiter = DomReiter( + personId = Uuid.random(), + satznummer = "123456", + nachname = "Mustermann", + vorname = "Max", + geburtsdatum = LocalDate(2010, 1, 1) // 16 Jahre in 2026 + ) + + val nun = Clock.System.now() + val definitionen = listOf( + AltersklasseDefinition( + altersklasseCode = "JG", + bezeichnung = "Jugend", + minAlter = 8, + maxAlter = 15, + createdAt = nun, + updatedAt = nun + ), + AltersklasseDefinition( + altersklasseCode = "JN", + bezeichnung = "Junioren", + minAlter = 16, + maxAlter = 18, + createdAt = nun, + updatedAt = nun + ), + AltersklasseDefinition( + altersklasseCode = "AK", + bezeichnung = "Allg. Klasse", + minAlter = 19, + createdAt = nun, + updatedAt = nun + ) + ) + + val ergebnis = rechner.ermittleAltersklassen(reiter, 2026, SparteE.SPRINGEN, definitionen) + + assertEquals(1, ergebnis.size) + assertEquals("JN", ergebnis[0].altersklasseCode) + } + + @Test + fun `ermittleAltersklassen berücksichtigt SpartenFilter`() { + val reiter = DomReiter( + personId = Uuid.random(), + satznummer = "123456", + nachname = "Mustermann", + vorname = "Max", + geburtsdatum = LocalDate(2013, 1, 1) // 13 Jahre in 2026 + ) + + val nun = Clock.System.now() + val definitionen = listOf( + AltersklasseDefinition( + altersklasseCode = "CH_D", + bezeichnung = "Children Dressur", + minAlter = 12, + maxAlter = 14, + sparteFilter = SparteE.DRESSUR, + createdAt = nun, + updatedAt = nun + ), + AltersklasseDefinition( + altersklasseCode = "JG", + bezeichnung = "Jugend", + minAlter = 8, + maxAlter = 15, + createdAt = nun, + updatedAt = nun + ) + ) + + val ergebnisDressur = rechner.ermittleAltersklassen(reiter, 2026, SparteE.DRESSUR, definitionen) + assertEquals(2, ergebnisDressur.size) + assertTrue(ergebnisDressur.any { it.altersklasseCode == "CH_D" }) + + val ergebnisSpringen = rechner.ermittleAltersklassen(reiter, 2026, SparteE.SPRINGEN, definitionen) + assertEquals(1, ergebnisSpringen.size) + assertEquals("JG", ergebnisSpringen[0].altersklasseCode) + } +} diff --git a/backend/services/masterdata/masterdata-domain/src/commonTest/kotlin/at/mocode/masterdata/domain/service/LicenseMatrixServiceTest.kt b/backend/services/masterdata/masterdata-domain/src/commonTest/kotlin/at/mocode/masterdata/domain/service/LicenseMatrixServiceTest.kt new file mode 100644 index 00000000..078244f2 --- /dev/null +++ b/backend/services/masterdata/masterdata-domain/src/commonTest/kotlin/at/mocode/masterdata/domain/service/LicenseMatrixServiceTest.kt @@ -0,0 +1,128 @@ +@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class) + +package at.mocode.masterdata.domain.service + +import at.mocode.core.domain.model.LizenzKlasseE +import at.mocode.core.domain.model.SparteE +import at.mocode.masterdata.domain.model.DomReiter +import at.mocode.masterdata.domain.model.LicenseMatrixEntry +import at.mocode.masterdata.domain.model.TurnierklasseDefinition +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue +import kotlin.time.Clock +import kotlin.uuid.Uuid + +class LicenseMatrixServiceTest { + + private val service = LicenseMatrixServiceImpl() + private val nun = Clock.System.now() + + private val matrix = listOf( + LicenseMatrixEntry( + sparte = SparteE.SPRINGEN, + lizenzKlasse = LizenzKlasseE.R1, + maxTurnierklasseCode = "L", + validFrom = nun, + createdAt = nun, + updatedAt = nun + ), + LicenseMatrixEntry( + sparte = SparteE.SPRINGEN, + lizenzKlasse = LizenzKlasseE.R2, + maxTurnierklasseCode = "M", + validFrom = nun, + createdAt = nun, + updatedAt = nun + ), + LicenseMatrixEntry( + sparte = SparteE.DRESSUR, + lizenzKlasse = LizenzKlasseE.RD1, + maxTurnierklasseCode = "L", + validFrom = nun, + createdAt = nun, + updatedAt = nun + ) + ) + + private val turnierklassen = listOf( + TurnierklasseDefinition( + sparte = SparteE.SPRINGEN, + code = "E", + bezeichnung = "E", + validFrom = nun, + createdAt = nun, + updatedAt = nun + ), + TurnierklasseDefinition( + sparte = SparteE.SPRINGEN, + code = "A", + bezeichnung = "A", + validFrom = nun, + createdAt = nun, + updatedAt = nun + ), + TurnierklasseDefinition( + sparte = SparteE.SPRINGEN, + code = "L", + bezeichnung = "L", + validFrom = nun, + createdAt = nun, + updatedAt = nun + ), + TurnierklasseDefinition( + sparte = SparteE.SPRINGEN, + code = "LM", + bezeichnung = "LM", + validFrom = nun, + createdAt = nun, + updatedAt = nun + ), + TurnierklasseDefinition( + sparte = SparteE.SPRINGEN, + code = "M", + bezeichnung = "M", + validFrom = nun, + createdAt = nun, + updatedAt = nun + ) + ) + + @Test + fun `isEligible erlaubt Starts bis zum Limit`() { + val r1Reiter = DomReiter( + personId = Uuid.random(), + satznummer = "1", + nachname = "R1", + vorname = "Reiter", + lizenzKlasse = LizenzKlasseE.R1, + lizenzSparten = listOf(SparteE.SPRINGEN), + startkartAktiv = true + ) + + val klasseA = turnierklassen.find { it.code == "A" }!! + val klasseL = turnierklassen.find { it.code == "L" }!! + val klasseM = turnierklassen.find { it.code == "M" }!! + + assertTrue(service.isEligible(r1Reiter, klasseA, SparteE.SPRINGEN, matrix, turnierklassen)) + assertTrue(service.isEligible(r1Reiter, klasseL, SparteE.SPRINGEN, matrix, turnierklassen)) + assertFalse(service.isEligible(r1Reiter, klasseM, SparteE.SPRINGEN, matrix, turnierklassen)) + } + + @Test + fun `isEligible verweigert Start ohne passende Spartenlizenz`() { + val rd1Reiter = DomReiter( + personId = Uuid.random(), + satznummer = "2", + nachname = "RD1", + vorname = "Reiter", + lizenzKlasse = LizenzKlasseE.RD1, + lizenzSparten = listOf(SparteE.DRESSUR), // Nur Dressur + startkartAktiv = true + ) + + val klasseA = turnierklassen.find { it.code == "A" }!! + + assertFalse(service.isEligible(rd1Reiter, klasseA, SparteE.SPRINGEN, matrix, turnierklassen)) + } +} diff --git a/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/ExposedReiterRepository.kt b/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/ExposedReiterRepository.kt index cd4632e5..d1b7f279 100644 --- a/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/ExposedReiterRepository.kt +++ b/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/ExposedReiterRepository.kt @@ -20,7 +20,7 @@ import kotlin.uuid.Uuid */ class ExposedReiterRepository : ReiterRepository { - private fun rowToDomReiter(row: ResultRow): DomReiter { + private fun rowToDomReiter(row: ResultRow, sparten: List = emptyList()): DomReiter { return DomReiter( reiterId = row[ReiterTable.id], personId = row[ReiterTable.personId], @@ -30,6 +30,7 @@ class ExposedReiterRepository : ReiterRepository { geburtsdatum = row[ReiterTable.geburtsdatum], lizenzNummer = row[ReiterTable.lizenzNummer], lizenzKlasse = LizenzKlasseE.valueOf(row[ReiterTable.lizenzKlasse]), + lizenzSparten = sparten, startkartAktiv = row[ReiterTable.startkartAktiv], startkartSaison = row[ReiterTable.startkartSaison], feiId = row[ReiterTable.feiId], @@ -44,21 +45,32 @@ class ExposedReiterRepository : ReiterRepository { ) } + private fun getSpartenForReiter(reiterId: Uuid): List { + return ReiterSparteTable.selectAll().where { ReiterSparteTable.reiterId eq reiterId } + .map { SparteE.valueOf(it[ReiterSparteTable.sparte]) } + } + override suspend fun findById(id: Uuid): DomReiter? = DatabaseFactory.dbQuery { ReiterTable.selectAll().where { ReiterTable.id eq id } - .map(::rowToDomReiter) + .map { rowToDomReiter(it, getSpartenForReiter(id)) } .singleOrNull() } override suspend fun findBySatznummer(satznummer: String): DomReiter? = DatabaseFactory.dbQuery { ReiterTable.selectAll().where { ReiterTable.satznummer eq satznummer } - .map(::rowToDomReiter) + .map { row -> + val id = row[ReiterTable.id] + rowToDomReiter(row, getSpartenForReiter(id)) + } .singleOrNull() } override suspend fun findByFeiId(feiId: String): DomReiter? = DatabaseFactory.dbQuery { ReiterTable.selectAll().where { ReiterTable.feiId eq feiId } - .map(::rowToDomReiter) + .map { row -> + val id = row[ReiterTable.id] + rowToDomReiter(row, getSpartenForReiter(id)) + } .singleOrNull() } @@ -66,7 +78,10 @@ class ExposedReiterRepository : ReiterRepository { val pattern = "%$searchTerm%" ReiterTable.selectAll().where { (ReiterTable.nachname like pattern) or (ReiterTable.vorname like pattern) } .limit(limit) - .map(::rowToDomReiter) + .map { row -> + val id = row[ReiterTable.id] + rowToDomReiter(row, getSpartenForReiter(id)) + } } override suspend fun findByVereinsNummer(vereinsNummer: String, activeOnly: Boolean): List = @@ -75,7 +90,10 @@ class ExposedReiterRepository : ReiterRepository { if (activeOnly) { query.andWhere { ReiterTable.istAktiv eq true } } - query.map(::rowToDomReiter) + query.map { row -> + val id = row[ReiterTable.id] + rowToDomReiter(row, getSpartenForReiter(id)) + } } override suspend fun findByLizenzKlasse(lizenzKlasse: LizenzKlasseE, activeOnly: Boolean): List = @@ -84,14 +102,22 @@ class ExposedReiterRepository : ReiterRepository { if (activeOnly) { query.andWhere { ReiterTable.istAktiv eq true } } - query.map(::rowToDomReiter) + query.map { row -> + val id = row[ReiterTable.id] + rowToDomReiter(row, getSpartenForReiter(id)) + } } override suspend fun findBySparte(sparte: SparteE, activeOnly: Boolean): List = DatabaseFactory.dbQuery { - // Da wir in ReiterTable keinen sparteFilter haben, müssen wir ggf. über eine andere Tabelle gehen - // oder die Logik anpassen. Fürs erste geben wir eine leere Liste zurück oder suchen nach Name in Lizenz? - // TODO: Implementierung prüfen, falls Sparten-Lizenzierung in eigener Tabelle liegt. - emptyList() + val query = (ReiterTable innerJoin ReiterSparteTable) + .selectAll().where { ReiterSparteTable.sparte eq sparte.name } + if (activeOnly) { + query.andWhere { ReiterTable.istAktiv eq true } + } + query.map { row -> + val id = row[ReiterTable.id] + rowToDomReiter(row, getSpartenForReiter(id)) + } } override suspend fun findGastreiter(activeOnly: Boolean): List = DatabaseFactory.dbQuery { @@ -99,19 +125,28 @@ class ExposedReiterRepository : ReiterRepository { if (activeOnly) { query.andWhere { ReiterTable.istAktiv eq true } } - query.map(::rowToDomReiter) + query.map { row -> + val id = row[ReiterTable.id] + rowToDomReiter(row, getSpartenForReiter(id)) + } } override suspend fun findAllActive(limit: Int, offset: Int): List = DatabaseFactory.dbQuery { ReiterTable.selectAll().where { ReiterTable.istAktiv eq true } .limit(limit).offset(offset.toLong()) - .map(::rowToDomReiter) + .map { row -> + val id = row[ReiterTable.id] + rowToDomReiter(row, getSpartenForReiter(id)) + } } override suspend fun findAll(limit: Int, offset: Int): List = DatabaseFactory.dbQuery { ReiterTable.selectAll() .limit(limit).offset(offset.toLong()) - .map(::rowToDomReiter) + .map { row -> + val id = row[ReiterTable.id] + rowToDomReiter(row, getSpartenForReiter(id)) + } } override suspend fun save(reiter: DomReiter): DomReiter = DatabaseFactory.dbQuery { @@ -136,7 +171,6 @@ class ExposedReiterRepository : ReiterRepository { it[datenQuelle] = reiter.datenQuelle.name it[updatedAt] = reiter.updatedAt } - reiter } else { ReiterTable.insert { it[id] = reiter.reiterId @@ -159,8 +193,19 @@ class ExposedReiterRepository : ReiterRepository { it[createdAt] = reiter.createdAt it[updatedAt] = reiter.updatedAt } - reiter } + + // Sparten aktualisieren + ReiterSparteTable.deleteWhere { ReiterSparteTable.reiterId eq reiter.reiterId } + reiter.lizenzSparten.forEach { sparte -> + ReiterSparteTable.insert { + it[ReiterSparteTable.id] = Uuid.random() + it[ReiterSparteTable.reiterId] = reiter.reiterId + it[ReiterSparteTable.sparte] = sparte.name + } + } + + reiter } override suspend fun delete(id: Uuid): Boolean = DatabaseFactory.dbQuery { @@ -177,30 +222,15 @@ class ExposedReiterRepository : ReiterRepository { override suspend fun upsertBySatznummer(reiter: DomReiter): DomReiter = DatabaseFactory.dbQuery { val existing = ReiterTable.selectAll().where { ReiterTable.satznummer eq reiter.satznummer } - .map(::rowToDomReiter) + .map { row -> + val id = row[ReiterTable.id] + rowToDomReiter(row, getSpartenForReiter(id)) + } .singleOrNull() if (existing != null) { val toUpdate = reiter.copy(reiterId = existing.reiterId) - ReiterTable.update({ ReiterTable.id eq existing.reiterId }) { - it[personId] = toUpdate.personId - it[nachname] = toUpdate.nachname - it[vorname] = toUpdate.vorname - it[geburtsdatum] = toUpdate.geburtsdatum - it[lizenzNummer] = toUpdate.lizenzNummer - it[lizenzKlasse] = toUpdate.lizenzKlasse.name - it[startkartAktiv] = toUpdate.startkartAktiv - it[startkartSaison] = toUpdate.startkartSaison - it[feiId] = toUpdate.feiId - it[nation] = toUpdate.nation - it[vereinsNummer] = toUpdate.vereinsNummer - it[vereinsName] = toUpdate.vereinsName - it[istGastreiter] = toUpdate.istGastreiter - it[istAktiv] = toUpdate.istAktiv - it[datenQuelle] = toUpdate.datenQuelle.name - it[updatedAt] = toUpdate.updatedAt - } - toUpdate + save(toUpdate) } else { save(reiter) } diff --git a/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/ReiterSparteTable.kt b/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/ReiterSparteTable.kt new file mode 100644 index 00000000..2d9fdcf9 --- /dev/null +++ b/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/ReiterSparteTable.kt @@ -0,0 +1,26 @@ +@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class) + +package at.mocode.masterdata.infrastructure.persistence + +import org.jetbrains.exposed.v1.core.Table +import org.jetbrains.exposed.v1.datetime.CurrentTimestamp +import org.jetbrains.exposed.v1.datetime.timestamp + +/** + * Exposed-Tabellendefinition für die Spartenberechtigung eines Reiters. + * Verknüpft einen Reiter mit den Sparten (DRESSUR, SPRINGEN), für die er lizenziert ist. + */ +object ReiterSparteTable : Table("reiter_sparte") { + val id = uuid("id") + val reiterId = uuid("reiter_id").references(ReiterTable.id) + val sparte = varchar("sparte", 20) // DRESSUR, SPRINGEN + + val createdAt = timestamp("created_at").defaultExpression(CurrentTimestamp) + val updatedAt = timestamp("updated_at").defaultExpression(CurrentTimestamp) + + override val primaryKey = PrimaryKey(id) + + init { + uniqueIndex("ux_reiter_sparte", reiterId, sparte) + } +} diff --git a/backend/services/masterdata/masterdata-service/src/main/kotlin/at/mocode/masterdata/service/config/MasterdataDatabaseConfiguration.kt b/backend/services/masterdata/masterdata-service/src/main/kotlin/at/mocode/masterdata/service/config/MasterdataDatabaseConfiguration.kt index 9ab6a4e1..bdcc0e78 100644 --- a/backend/services/masterdata/masterdata-service/src/main/kotlin/at/mocode/masterdata/service/config/MasterdataDatabaseConfiguration.kt +++ b/backend/services/masterdata/masterdata-service/src/main/kotlin/at/mocode/masterdata/service/config/MasterdataDatabaseConfiguration.kt @@ -42,7 +42,8 @@ class MasterdataDatabaseConfiguration { LicenseTable, RichtverfahrenTable, GebuehrTable, - RegulationConfigTable + RegulationConfigTable, + ReiterSparteTable ) log.info("Masterdata database schema initialized successfully") } @@ -87,7 +88,8 @@ class MasterdataTestDatabaseConfiguration { LicenseTable, RichtverfahrenTable, GebuehrTable, - RegulationConfigTable + RegulationConfigTable, + ReiterSparteTable ) log.info("Test masterdata database schema initialized successfully") } diff --git a/backend/services/masterdata/masterdata-service/src/main/resources/db/migration/V007__Cleanup_Initial_Tables_and_Add_Sparte.sql b/backend/services/masterdata/masterdata-service/src/main/resources/db/migration/V007__Cleanup_Initial_Tables_and_Add_Sparte.sql new file mode 100644 index 00000000..07f50536 --- /dev/null +++ b/backend/services/masterdata/masterdata-service/src/main/resources/db/migration/V007__Cleanup_Initial_Tables_and_Add_Sparte.sql @@ -0,0 +1,35 @@ +-- V007: Cleanup Initial Tables and Add ReiterSparte Table +-- Harmonisierung: Löschen der veralteten dom_person / dom_verein Tabellen aus V1 +-- Hinzufügen der Zwischentabelle für Reiter-Sparten + +-- Löschen der alten Tabellen (Daten wurden bereits in V006 in die neuen Tabellen migriert bzw. werden neu importiert) +-- Vorsicht: Da dies ein Greenfield-Projekt ist und der Fokus auf V26 liegt, ist ein sauberer Schnitt hier erlaubt. +DROP TABLE IF EXISTS dom_person CASCADE; +DROP TABLE IF EXISTS dom_verein CASCADE; + +-- Erstellung der Reiter-Sparten Tabelle +CREATE TABLE IF NOT EXISTS reiter_sparte +( + id + UUID + PRIMARY + KEY, + reiter_id + UUID + NOT + NULL + REFERENCES + reiter +( + reiter_id +) ON DELETE CASCADE, + sparte VARCHAR +( + 20 +) NOT NULL, + created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP + WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP + ); + +CREATE UNIQUE INDEX ux_reiter_sparte ON reiter_sparte (reiter_id, sparte);