Standardize and refactor master data infrastructure: rename tables for plural consistency, remove unused entity tables, improve ZNS import mappings with enriched license properties, introduce Altersklasse domain model, activate Consul service discovery, and update application configuration accordingly.

This commit is contained in:
2026-04-06 19:48:15 +02:00
parent bc13a58a14
commit abaaeddaaf
53 changed files with 1575 additions and 333 deletions
@@ -0,0 +1,22 @@
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
package at.mocode.masterdata.domain.model
import kotlinx.serialization.Serializable
import at.mocode.core.domain.serialization.UuidSerializer
import kotlin.uuid.Uuid
/**
* Domain-Modell für eine Bewerbs-Klasse (z.B. E, A, L, M, S).
*/
@Serializable
data class BewerbsKlasse(
@Serializable(with = UuidSerializer::class)
val bewerbsklasseId: Uuid = Uuid.random(),
val sparte: String,
val code: String,
val bezeichnung: String,
val maxHoehe: Int? = null,
val aufgabenNiveau: String? = null,
val istAktiv: Boolean = true
)
@@ -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 Bewerbsklasse gemäß ÖTO.
*/
@Serializable
data class BewerbsklasseDefinition(
@Serializable(with = UuidSerializer::class)
val bewerbsklasseId: 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
)
@@ -7,12 +7,12 @@ import at.mocode.core.domain.serialization.UuidSerializer
import kotlin.uuid.Uuid
/**
* Domain-Modell für Bundesland.
* Domain-Modell für Bundesland (Mehrzahl 'Bundeslaender').
*/
@Serializable
data class Bundesland(
@Serializable(with = UuidSerializer::class)
val id: Uuid,
val bundeslandId: Uuid,
val bundeslandNr: Int,
val bezeichnung: String,
val wappenUrl: String? = null
@@ -0,0 +1,19 @@
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
package at.mocode.masterdata.domain.model
import kotlinx.serialization.Serializable
import at.mocode.core.domain.serialization.UuidSerializer
import kotlin.uuid.Uuid
/**
* Domain-Modell für eine Fahr-Lizenz (z.B. F1, F2).
*/
@Serializable
data class FahrLizenz(
@Serializable(with = UuidSerializer::class)
val lizenzId: Uuid = Uuid.random(),
val code: String,
val bezeichnung: String,
val istAktiv: Boolean = true
)
@@ -0,0 +1,19 @@
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
package at.mocode.masterdata.domain.model
import kotlinx.serialization.Serializable
import at.mocode.core.domain.serialization.UuidSerializer
import at.mocode.core.domain.serialization.LocalDateSerializer
import kotlinx.datetime.LocalDate
import kotlin.uuid.Uuid
@Serializable
data class ReitLizenz(
@Serializable(with = UuidSerializer::class)
val lizenzId: Uuid = Uuid.random(),
val code: String,
val bezeichnung: String,
val sparte: String? = null,
val istAktiv: Boolean = true
)
@@ -14,6 +14,16 @@ import kotlin.time.Clock
import kotlin.time.Instant
import kotlin.uuid.Uuid
@Serializable
data class ReiterLizenz(
@Serializable(with = UuidSerializer::class)
val lizenzId: Uuid = Uuid.random(),
val lizenzTyp: String, // STARTKARTE, REITERLIZENZ, FAHRLIZENZ
val kuerzel: String,
@Serializable(with = LocalDateSerializer::class)
val gueltigBis: LocalDate? = null
)
/**
* Domain model representing a rider (Reiter) in the actor-context.
*
@@ -88,6 +98,15 @@ data class Reiter(
@Serializable(with = UuidSerializer::class)
var nationId: Uuid? = null,
@Serializable(with = UuidSerializer::class)
var reitLizenzId: Uuid? = null,
@Serializable(with = UuidSerializer::class)
var fahrLizenzId: Uuid? = null,
@Serializable(with = UuidSerializer::class)
var startkarteId: Uuid? = null,
// Alphanumerisch (4) Keine Lizenz: BLANK
var reiterLizenz: String? = null,
@@ -98,10 +117,10 @@ data class Reiter(
var fahrLizenz: String? = null,
// Alphanumerisch (2) WERTE: Standard: BLANK, JG=JUGENDLICHER, JR=JUNIOR, 25=U25
var altersklasseJgJrU25: String? = null,
var altersklasseJgJrU25: at.mocode.core.domain.model.ReiterAltersKlasseE? = null,
// Alphanumerisch (1) WERTE: Standard: BLANK Y=JUNGER-REITER
var altersklasseY: String? = null,
var altersklasseY: at.mocode.core.domain.model.ReiterAltersKlasseE? = null,
// Numerisch (8) FORMAT: 00000000
var mitgliedsNummer: Int? = null,
@@ -145,7 +164,12 @@ data class Reiter(
@Serializable(with = InstantSerializer::class)
val createdAt: Instant = Clock.System.now(),
@Serializable(with = InstantSerializer::class)
var updatedAt: Instant = Clock.System.now()
var updatedAt: Instant = Clock.System.now(),
/**
* List of specialized licenses for this rider.
*/
var lizenzen: List<ReiterLizenz> = emptyList()
) {
/**
* Returns the display name of the rider.
@@ -183,7 +207,7 @@ data class Reiter(
/**
* Validates the 8-digit membership number.
* Format: [B][VVV][MMMM]
* B: Bundesland (1 digit)
* B: Bundesland (1 digit, 1-9)
* VVV: Verein (3 digits)
* MMMM: Member (4 digits)
*/
@@ -192,12 +216,20 @@ data class Reiter(
if (nrStr.length != 8) return false
val b = nrStr.substring(0, 1).toInt()
if (b < 1 || b > 9) return false // Valid Bundesland prefix is 1-9
// Validation against bundeslandNummer if available
if (bundeslandNummer != null && b != (bundeslandNummer!! % 10)) {
// ZNS bundeslandNummer is 01-09, while membership first digit is 1-9
// This might need refinement depending on how "00" (Unbekannt) is handled in membership numbers.
// ZNS bundeslandNummer is 01-09, while membership first digit is 1-9
if (bundeslandNummer != null && b != bundeslandNummer) {
return false
}
// Verein part (2nd-4th digit)
// ZNS Verein ID usually contains state + 3 digits.
// Here we just check if it's not all zeros as a basic sanity check
val vvv = nrStr.substring(1, 4).toInt()
if (vvv == 0) return false
return true
}
@@ -0,0 +1,19 @@
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
package at.mocode.masterdata.domain.model
import kotlinx.serialization.Serializable
import at.mocode.core.domain.serialization.UuidSerializer
import kotlin.uuid.Uuid
/**
* Domain-Modell für eine Startkarte.
*/
@Serializable
data class Startkarte(
@Serializable(with = UuidSerializer::class)
val startkarteId: Uuid = Uuid.random(),
val code: String,
val bezeichnung: String,
val istAktiv: Boolean = true
)
@@ -0,0 +1,20 @@
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
package at.mocode.masterdata.domain.model
import kotlinx.serialization.Serializable
import at.mocode.core.domain.serialization.UuidSerializer
import kotlin.uuid.Uuid
/**
* Domain-Modell für eine Turnier-Kategorie (z.B. CSN-C, CDN-A, CSN-C Neu).
*/
@Serializable
data class TurnierKategorie(
@Serializable(with = UuidSerializer::class)
val kategorieId: Uuid = Uuid.random(),
val code: String,
val bezeichnung: String,
val sparte: String? = null,
val istAktiv: Boolean = true
)
@@ -0,0 +1,22 @@
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
package at.mocode.masterdata.domain.model
import kotlinx.serialization.Serializable
import at.mocode.core.domain.serialization.UuidSerializer
import kotlin.uuid.Uuid
/**
* Domain-Modell für eine Turnier-Klasse.
*/
@Serializable
data class TurnierKlasse(
@Serializable(with = UuidSerializer::class)
val turnierklasseId: Uuid = Uuid.random(),
val sparte: String,
val code: String,
val bezeichnung: String,
val maxHoehe: Int? = null,
val aufgabenNiveau: String? = null,
val istAktiv: Boolean = true
)
@@ -0,0 +1,19 @@
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
package at.mocode.masterdata.domain.model
import kotlinx.serialization.Serializable
import at.mocode.core.domain.serialization.UuidSerializer
import kotlin.uuid.Uuid
/**
* Domain-Modell für eine Turnier-Sparte.
*/
@Serializable
data class TurnierSparte(
@Serializable(with = UuidSerializer::class)
val sparteId: Uuid = Uuid.random(),
val code: String,
val bezeichnung: String,
val istAktiv: Boolean = true
)
@@ -0,0 +1,12 @@
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
package at.mocode.masterdata.domain.repository
import at.mocode.masterdata.domain.model.AltersklasseDefinition
/**
* Repository für Altersklassen-Stammdaten.
*/
interface AltersklassenRepository {
suspend fun findByCode(code: String): AltersklasseDefinition?
}
@@ -0,0 +1,16 @@
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
package at.mocode.masterdata.domain.repository
import at.mocode.masterdata.domain.model.FahrLizenz
import at.mocode.masterdata.domain.model.ReitLizenz
import at.mocode.masterdata.domain.model.Startkarte
/**
* Repository für alle Lizenz-Stammdaten (Reit, Fahr, Startkarten).
*/
interface MasterdataLicenseRepository {
suspend fun findReitLizenzByCode(code: String): at.mocode.masterdata.domain.model.ReitLizenz?
suspend fun findFahrLizenzByCode(code: String): at.mocode.masterdata.domain.model.FahrLizenz?
suspend fun findStartkarteByCode(code: String): at.mocode.masterdata.domain.model.Startkarte?
}
@@ -9,6 +9,7 @@ import at.mocode.masterdata.domain.model.*
*/
interface RegulationRepository {
suspend fun findAllTurnierklassen(): List<TurnierklasseDefinition>
suspend fun findAllBewerbsklassen(): List<BewerbsklasseDefinition>
suspend fun findAllLicenseMatrixEntries(): List<LicenseMatrixEntry>
suspend fun findAllRichtverfahren(): List<RichtverfahrenDefinition>
suspend fun findAllGebuehren(): List<GebuehrDefinition>
@@ -7,10 +7,10 @@ import org.jetbrains.exposed.v1.datetime.CurrentTimestamp
import org.jetbrains.exposed.v1.datetime.timestamp
/**
* Exposed-Tabellendefinition für die Altersklasse-Entität (Altersklassendefinition).
* Exposed-Tabellendefinition für die Altersklassen-Entität (Altersklassendefinition).
*
* Diese Tabelle speichert alle Informationen zu Altersklassen für Teilnehmer
* entsprechend der AltersklasseDefinition Domain-Entität.
* entsprechend der AltersklassenDefinition Domain-Entität.
*/
object AltersklasseTable : Table("altersklasse") {
val id = uuid("id")
@@ -29,7 +29,7 @@ object AltersklasseTable : Table("altersklasse") {
override val primaryKey = PrimaryKey(id)
init {
// Index for performance on common queries
// Index für Performance (an Migration angepasst)
index(customIndexName = "idx_altersklasse_aktiv", columns = arrayOf(istAktiv))
index(customIndexName = "idx_altersklasse_sparte", columns = arrayOf(sparteFilter))
index(customIndexName = "idx_altersklasse_geschlecht", columns = arrayOf(geschlechtFilter))
@@ -7,12 +7,12 @@ import org.jetbrains.exposed.v1.datetime.CurrentTimestamp
import org.jetbrains.exposed.v1.datetime.timestamp
/**
* Exposed-Tabellendefinition für die Bundesland-Entität.
* Exposed-Tabellendefinition für die Bundesländer-Entität.
*/
object BundeslandTable : Table("bundesland") {
val id = uuid("id")
object BundeslaenderTable : Table("bundeslaender") {
val id = uuid("bundesland_id")
val landId = uuid("land_id")
val bundeslandNr = integer("bundesland_nr").nullable()
val bundeslandNr = integer("bundesland_nr").uniqueIndex().nullable()
val oepsCode = varchar("oeps_code", 10).nullable()
val iso3166_2_Code = varchar("iso_3166_2_code", 10).nullable()
val name = varchar("name", 100)
@@ -26,10 +26,10 @@ object BundeslandTable : Table("bundesland") {
override val primaryKey = PrimaryKey(id)
init {
uniqueIndex("idx_bundesland_oeps", oepsCode, landId)
uniqueIndex("idx_bundesland_iso", iso3166_2_Code)
uniqueIndex("idx_bundeslaender_oeps", oepsCode, landId)
uniqueIndex("idx_bundeslaender_iso", iso3166_2_Code)
// Natürlicher Schlüssel gem. Architektur: (land_id + kuerzel) eindeutig
// Hinweis: kuerzel kann NULL sein der Unique-Index greift dann nur für nicht-null Werte pro Land.
uniqueIndex("ux_bundesland_land_kuerzel", landId, kuerzel)
uniqueIndex("ux_bundeslaender_land_kuerzel", landId, kuerzel)
}
}
@@ -11,46 +11,47 @@ import kotlin.uuid.Uuid
/**
* Implementierung des BundeslandRepository für die Datenbankzugriffe.
* Verwendet die Tabelle 'bundeslaender'.
*/
class BundeslandRepositoryImpl : BundeslandRepository {
private fun rowToBundeslandDefinition(row: ResultRow): BundeslandDefinition {
return BundeslandDefinition(
bundeslandId = row[BundeslandTable.id],
landId = row[BundeslandTable.landId],
bundeslandNr = row[BundeslandTable.bundeslandNr],
oepsCode = row[BundeslandTable.oepsCode],
iso3166_2_Code = row[BundeslandTable.iso3166_2_Code],
name = row[BundeslandTable.name],
kuerzel = row[BundeslandTable.kuerzel],
wappenUrl = row[BundeslandTable.wappenUrl],
istAktiv = row[BundeslandTable.istAktiv],
sortierReihenfolge = row[BundeslandTable.sortierReihenfolge],
createdAt = row[BundeslandTable.createdAt],
updatedAt = row[BundeslandTable.updatedAt]
bundeslandId = row[BundeslaenderTable.id],
landId = row[BundeslaenderTable.landId],
bundeslandNr = row[BundeslaenderTable.bundeslandNr],
oepsCode = row[BundeslaenderTable.oepsCode],
iso3166_2_Code = row[BundeslaenderTable.iso3166_2_Code],
name = row[BundeslaenderTable.name],
kuerzel = row[BundeslaenderTable.kuerzel],
wappenUrl = row[BundeslaenderTable.wappenUrl],
istAktiv = row[BundeslaenderTable.istAktiv],
sortierReihenfolge = row[BundeslaenderTable.sortierReihenfolge],
createdAt = row[BundeslaenderTable.createdAt],
updatedAt = row[BundeslaenderTable.updatedAt]
)
}
override suspend fun findByNr(nr: Int): BundeslandDefinition? = DatabaseFactory.dbQuery {
BundeslandTable.selectAll().where { BundeslandTable.bundeslandNr eq nr }
BundeslaenderTable.selectAll().where { BundeslaenderTable.bundeslandNr eq nr }
.map(::rowToBundeslandDefinition)
.singleOrNull()
}
override suspend fun findById(id: Uuid): BundeslandDefinition? = DatabaseFactory.dbQuery {
BundeslandTable.selectAll().where { BundeslandTable.id eq id }
BundeslaenderTable.selectAll().where { BundeslaenderTable.id eq id }
.map(::rowToBundeslandDefinition)
.singleOrNull()
}
override suspend fun findByOepsCode(oepsCode: String, landId: Uuid): BundeslandDefinition? = DatabaseFactory.dbQuery {
BundeslandTable.selectAll().where { (BundeslandTable.oepsCode eq oepsCode) and (BundeslandTable.landId eq landId) }
BundeslaenderTable.selectAll().where { (BundeslaenderTable.oepsCode eq oepsCode) and (BundeslaenderTable.landId eq landId) }
.map(::rowToBundeslandDefinition)
.singleOrNull()
}
override suspend fun findByIso3166_2_Code(iso3166_2_Code: String): BundeslandDefinition? = DatabaseFactory.dbQuery {
BundeslandTable.selectAll().where { BundeslandTable.iso3166_2_Code eq iso3166_2_Code }
BundeslaenderTable.selectAll().where { BundeslaenderTable.iso3166_2_Code eq iso3166_2_Code }
.map(::rowToBundeslandDefinition)
.singleOrNull()
}
@@ -60,14 +61,14 @@ class BundeslandRepositoryImpl : BundeslandRepository {
activeOnly: Boolean,
orderBySortierung: Boolean
): List<BundeslandDefinition> = DatabaseFactory.dbQuery {
val query = BundeslandTable.selectAll().where { BundeslandTable.landId eq landId }
val query = BundeslaenderTable.selectAll().where { BundeslaenderTable.landId eq landId }
if (activeOnly) {
query.andWhere { BundeslandTable.istAktiv eq true }
query.andWhere { BundeslaenderTable.istAktiv eq true }
}
if (orderBySortierung) {
query.orderBy(BundeslandTable.sortierReihenfolge to SortOrder.ASC, BundeslandTable.name to SortOrder.ASC)
query.orderBy(BundeslaenderTable.sortierReihenfolge to SortOrder.ASC, BundeslaenderTable.name to SortOrder.ASC)
} else {
query.orderBy(BundeslandTable.name to SortOrder.ASC)
query.orderBy(BundeslaenderTable.name to SortOrder.ASC)
}
query.map(::rowToBundeslandDefinition)
}
@@ -75,26 +76,27 @@ class BundeslandRepositoryImpl : BundeslandRepository {
override suspend fun findByName(searchTerm: String, landId: Uuid?, limit: Int): List<BundeslandDefinition> =
DatabaseFactory.dbQuery {
val pattern = "%$searchTerm%"
val query = BundeslandTable.selectAll().where { BundeslandTable.name like pattern }
landId?.let { query.andWhere { BundeslandTable.landId eq it } }
val query = BundeslaenderTable.selectAll().where { BundeslaenderTable.name like pattern }
landId?.let { query.andWhere { BundeslaenderTable.landId eq it } }
query.limit(limit).map(::rowToBundeslandDefinition)
}
override suspend fun findAllActive(orderBySortierung: Boolean): List<BundeslandDefinition> = DatabaseFactory.dbQuery {
val query = BundeslandTable.selectAll().where { BundeslandTable.istAktiv eq true }
val query = BundeslaenderTable.selectAll().where { BundeslaenderTable.istAktiv eq true }
if (orderBySortierung) {
query.orderBy(BundeslandTable.sortierReihenfolge to SortOrder.ASC, BundeslandTable.name to SortOrder.ASC)
query.orderBy(BundeslaenderTable.sortierReihenfolge to SortOrder.ASC, BundeslaenderTable.name to SortOrder.ASC)
} else {
query.orderBy(BundeslandTable.name to SortOrder.ASC)
query.orderBy(BundeslaenderTable.name to SortOrder.ASC)
}
query.map(::rowToBundeslandDefinition)
}
override suspend fun save(bundesland: BundeslandDefinition): BundeslandDefinition = DatabaseFactory.dbQuery {
val exists = BundeslandTable.selectAll().where { BundeslandTable.id eq bundesland.bundeslandId }.any()
val exists = BundeslaenderTable.selectAll().where { BundeslaenderTable.id eq bundesland.bundeslandId }.any()
if (exists) {
BundeslandTable.update({ BundeslandTable.id eq bundesland.bundeslandId }) {
BundeslaenderTable.update({ BundeslaenderTable.id eq bundesland.bundeslandId }) {
it[landId] = bundesland.landId
it[bundeslandNr] = bundesland.bundeslandNr
it[oepsCode] = bundesland.oepsCode
it[iso3166_2_Code] = bundesland.iso3166_2_Code
it[name] = bundesland.name
@@ -106,9 +108,10 @@ class BundeslandRepositoryImpl : BundeslandRepository {
}
bundesland
} else {
BundeslandTable.insert {
BundeslaenderTable.insert {
it[id] = bundesland.bundeslandId
it[landId] = bundesland.landId
it[bundeslandNr] = bundesland.bundeslandNr
it[oepsCode] = bundesland.oepsCode
it[iso3166_2_Code] = bundesland.iso3166_2_Code
it[name] = bundesland.name
@@ -124,31 +127,31 @@ class BundeslandRepositoryImpl : BundeslandRepository {
}
override suspend fun delete(id: Uuid): Boolean = DatabaseFactory.dbQuery {
BundeslandTable.deleteWhere { BundeslandTable.id eq id } > 0
BundeslaenderTable.deleteWhere { BundeslaenderTable.id eq id } > 0
}
override suspend fun existsByOepsCode(oepsCode: String, landId: Uuid): Boolean = DatabaseFactory.dbQuery {
BundeslandTable.selectAll().where { (BundeslandTable.oepsCode eq oepsCode) and (BundeslandTable.landId eq landId) }
BundeslaenderTable.selectAll().where { (BundeslaenderTable.oepsCode eq oepsCode) and (BundeslaenderTable.landId eq landId) }
.any()
}
override suspend fun existsByIso3166_2_Code(iso3166_2_Code: String): Boolean = DatabaseFactory.dbQuery {
BundeslandTable.selectAll().where { BundeslandTable.iso3166_2_Code eq iso3166_2_Code }.any()
BundeslaenderTable.selectAll().where { BundeslaenderTable.iso3166_2_Code eq iso3166_2_Code }.any()
}
override suspend fun countActiveByCountry(landId: Uuid): Long = DatabaseFactory.dbQuery {
BundeslandTable.selectAll().where { (BundeslandTable.landId eq landId) and (BundeslandTable.istAktiv eq true) }
BundeslaenderTable.selectAll().where { (BundeslaenderTable.landId eq landId) and (BundeslaenderTable.istAktiv eq true) }
.count()
}
override suspend fun upsertByLandIdAndKuerzel(bundesland: BundeslandDefinition): BundeslandDefinition = DatabaseFactory.dbQuery {
// 1) Update anhand des natürlichen Schlüssels (landId + kuerzel)
val updated = if (bundesland.kuerzel == null) {
// Ohne Kuerzel ist der natürliche Schlüssel nicht definiert → versuche Update via (landId + name) als Fallback nicht, bleib bei None
0
} else {
BundeslandTable.update({ (BundeslandTable.landId eq bundesland.landId) and (BundeslandTable.kuerzel eq bundesland.kuerzel) }) {
BundeslaenderTable.update({ (BundeslaenderTable.landId eq bundesland.landId) and (BundeslaenderTable.kuerzel eq bundesland.kuerzel) }) {
it[landId] = bundesland.landId
it[bundeslandNr] = bundesland.bundeslandNr
it[oepsCode] = bundesland.oepsCode
it[iso3166_2_Code] = bundesland.iso3166_2_Code
it[name] = bundesland.name
@@ -161,16 +164,17 @@ class BundeslandRepositoryImpl : BundeslandRepository {
}
if (updated > 0) {
BundeslandTable.selectAll()
.where { (BundeslandTable.landId eq bundesland.landId) and (BundeslandTable.kuerzel eq bundesland.kuerzel) }
BundeslaenderTable.selectAll()
.where { (BundeslaenderTable.landId eq bundesland.landId) and (BundeslaenderTable.kuerzel eq bundesland.kuerzel) }
.map(::rowToBundeslandDefinition)
.single()
} else {
// 2) Insert versuchen
try {
BundeslandTable.insert {
BundeslaenderTable.insert {
it[id] = bundesland.bundeslandId
it[landId] = bundesland.landId
it[bundeslandNr] = bundesland.bundeslandNr
it[oepsCode] = bundesland.oepsCode
it[iso3166_2_Code] = bundesland.iso3166_2_Code
it[name] = bundesland.name
@@ -184,8 +188,9 @@ class BundeslandRepositoryImpl : BundeslandRepository {
} catch (_: Exception) {
// Race-Condition → erneut Update
if (bundesland.kuerzel != null) {
BundeslandTable.update({ (BundeslandTable.landId eq bundesland.landId) and (BundeslandTable.kuerzel eq bundesland.kuerzel) }) {
BundeslaenderTable.update({ (BundeslaenderTable.landId eq bundesland.landId) and (BundeslaenderTable.kuerzel eq bundesland.kuerzel) }) {
it[landId] = bundesland.landId
it[bundeslandNr] = bundesland.bundeslandNr
it[oepsCode] = bundesland.oepsCode
it[iso3166_2_Code] = bundesland.iso3166_2_Code
it[name] = bundesland.name
@@ -197,14 +202,14 @@ class BundeslandRepositoryImpl : BundeslandRepository {
}
}
}
// Rückgabe des aktuellen Datensatzes: Falls Kuerzel null greift auf ID zurück
if (bundesland.kuerzel != null) {
BundeslandTable.selectAll()
.where { (BundeslandTable.landId eq bundesland.landId) and (BundeslandTable.kuerzel eq bundesland.kuerzel) }
BundeslaenderTable.selectAll()
.where { (BundeslaenderTable.landId eq bundesland.landId) and (BundeslaenderTable.kuerzel eq bundesland.kuerzel) }
.map(::rowToBundeslandDefinition)
.single()
} else {
BundeslandTable.selectAll().where { BundeslandTable.id eq bundesland.bundeslandId }
BundeslaenderTable.selectAll().where { BundeslaenderTable.id eq bundesland.bundeslandId }
.map(::rowToBundeslandDefinition)
.single()
}
@@ -19,10 +19,15 @@ import kotlin.time.Instant as KxInstant
class ExposedRegulationRepository : RegulationRepository {
override suspend fun findAllTurnierklassen(): List<TurnierklasseDefinition> = DatabaseFactory.dbQuery {
TurnierklasseTable.selectAll()
TurnierKlassenTable.selectAll()
.map { it.toTurnierklasseDefinition() }
}
override suspend fun findAllBewerbsklassen(): List<BewerbsklasseDefinition> = DatabaseFactory.dbQuery {
TurnierKlassenTable.selectAll()
.map { it.toBewerbsklasseDefinition() }
}
override suspend fun findAllLicenseMatrixEntries(): List<LicenseMatrixEntry> = DatabaseFactory.dbQuery {
LicenseTable.selectAll()
.map { it.toLicenseMatrixEntry() }
@@ -55,18 +60,32 @@ class ExposedRegulationRepository : RegulationRepository {
}
}
private fun ResultRow.toBewerbsklasseDefinition() = BewerbsklasseDefinition(
bewerbsklasseId = this[TurnierKlassenTable.id],
sparte = SparteE.valueOf(this[TurnierKlassenTable.sparte]),
code = this[TurnierKlassenTable.code],
bezeichnung = this[TurnierKlassenTable.bezeichnung],
maxHoehe = this[TurnierKlassenTable.maxHoehe],
aufgabenNiveau = this[TurnierKlassenTable.aufgabenNiveau],
validFrom = this[TurnierKlassenTable.validFrom].toKtInstant(),
validTo = this[TurnierKlassenTable.validTo]?.toOptionalKtInstant(),
istAktiv = this[TurnierKlassenTable.istAktiv],
createdAt = this[TurnierKlassenTable.createdAt].toKtInstant(),
updatedAt = this[TurnierKlassenTable.updatedAt].toKtInstant()
)
private fun ResultRow.toTurnierklasseDefinition() = TurnierklasseDefinition(
turnierklasseId = this[TurnierklasseTable.id],
sparte = SparteE.valueOf(this[TurnierklasseTable.sparte]),
code = this[TurnierklasseTable.code],
bezeichnung = this[TurnierklasseTable.bezeichnung],
maxHoehe = this[TurnierklasseTable.maxHoehe],
aufgabenNiveau = this[TurnierklasseTable.aufgabenNiveau],
validFrom = this[TurnierklasseTable.validFrom].toKtInstant(),
validTo = this[TurnierklasseTable.validTo]?.toOptionalKtInstant(),
istAktiv = this[TurnierklasseTable.istAktiv],
createdAt = this[TurnierklasseTable.createdAt].toKtInstant(),
updatedAt = this[TurnierklasseTable.updatedAt].toKtInstant()
turnierklasseId = this[TurnierKlassenTable.id],
sparte = SparteE.valueOf(this[TurnierKlassenTable.sparte]),
code = this[TurnierKlassenTable.code],
bezeichnung = this[TurnierKlassenTable.bezeichnung],
maxHoehe = this[TurnierKlassenTable.maxHoehe],
aufgabenNiveau = this[TurnierKlassenTable.aufgabenNiveau],
validFrom = this[TurnierKlassenTable.validFrom].toKtInstant(),
validTo = this[TurnierKlassenTable.validTo]?.toOptionalKtInstant(),
istAktiv = this[TurnierKlassenTable.istAktiv],
createdAt = this[TurnierKlassenTable.createdAt].toKtInstant(),
updatedAt = this[TurnierKlassenTable.updatedAt].toKtInstant()
)
private fun ResultRow.toLicenseMatrixEntry() = LicenseMatrixEntry(
@@ -1,27 +0,0 @@
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
package at.mocode.masterdata.infrastructure.persistence
import at.mocode.masterdata.infrastructure.persistence.reiter.ReiterTable
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)
}
}
@@ -0,0 +1,22 @@
@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 Turnier-Kategorien (z.B. CSN-B, CDN-A).
*/
object TurnierKategorienTable : Table("turnier_kategorien") {
val id = uuid("kategorie_id")
val code = varchar("code", 20).uniqueIndex()
val bezeichnung = varchar("bezeichnung", 100)
val sparte = varchar("sparte", 20).nullable()
val istAktiv = bool("ist_aktiv").default(true)
val createdAt = timestamp("created_at").defaultExpression(CurrentTimestamp)
val updatedAt = timestamp("updated_at").defaultExpression(CurrentTimestamp)
override val primaryKey = PrimaryKey(id)
}
@@ -7,11 +7,11 @@ import org.jetbrains.exposed.v1.datetime.CurrentTimestamp
import org.jetbrains.exposed.v1.datetime.timestamp
/**
* Exposed-Tabellendefinition für Turnierklassen (Springen/Dressur).
* Exposed-Tabellendefinition für Bewerbsklassen (Springen/Dressur).
* Basierend auf ÖTO 2026.
*/
object TurnierklasseTable : Table("turnierklasse") {
val id = uuid("turnierklasse_id")
object TurnierKlassenTable : Table("bewerbs_klassen") {
val id = uuid("bewerbsklasse_id")
val sparte = varchar("sparte", 20) // DRESSUR, SPRINGEN
val code = varchar("code", 10) // E, A, L, LM, M, S
val bezeichnung = varchar("bezeichnung", 100)
@@ -29,6 +29,6 @@ object TurnierklasseTable : Table("turnierklasse") {
override val primaryKey = PrimaryKey(id)
init {
index("idx_turnierklasse_sparte_code", false, sparte, code)
index("idx_bewerbs_klassen_sparte_code", false, sparte, code)
}
}
@@ -0,0 +1,21 @@
@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 Turnier-Sparten (z.B. Dressur, Springen, Fahren).
*/
object TurnierSpartenTable : Table("turnier_sparten") {
val id = uuid("sparte_id")
val code = varchar("code", 10).uniqueIndex() // z.B. D, S, V, F, R, C
val bezeichnung = varchar("bezeichnung", 100)
val istAktiv = bool("ist_aktiv").default(true)
val createdAt = timestamp("created_at").defaultExpression(CurrentTimestamp)
val updatedAt = timestamp("updated_at").defaultExpression(CurrentTimestamp)
override val primaryKey = PrimaryKey(id)
}
@@ -5,10 +5,7 @@ import at.mocode.core.domain.model.DatenQuelleE
import at.mocode.core.utils.database.DatabaseFactory
import at.mocode.masterdata.domain.model.Funktionaer
import at.mocode.masterdata.domain.repository.FunktionaerRepository
import org.jetbrains.exposed.v1.core.ResultRow
import org.jetbrains.exposed.v1.core.and
import org.jetbrains.exposed.v1.core.eq
import org.jetbrains.exposed.v1.core.inList
import org.jetbrains.exposed.v1.core.*
import org.jetbrains.exposed.v1.jdbc.deleteWhere
import org.jetbrains.exposed.v1.jdbc.insert
import org.jetbrains.exposed.v1.jdbc.selectAll
@@ -20,7 +17,7 @@ import kotlin.uuid.Uuid
* Exposed-basierte Implementierung des Funktionaer-Repositorys.
*
* Verwaltet die Persistenz von Funktionären und deren Qualifikationen.
* Die Qualifikationen werden beim Speichern gegen die [QualifikationMasterTable]
* Die Qualifikationen werden beim Speichern gegen die [FunktionaersQualifikationenTable]
* aufgelöst, um Datenintegrität bezüglich offizieller ÖTO-Kürzel sicherzustellen.
*/
class FunktionaerExposedRepository : FunktionaerRepository {
@@ -44,9 +41,9 @@ class FunktionaerExposedRepository : FunktionaerRepository {
}
override suspend fun findById(id: Uuid): Funktionaer? = DatabaseFactory.dbQuery {
val qualifikationen = (FunktionaerQualifikationTable innerJoin QualifikationMasterTable)
val qualifikationen = (FunktionaerQualifikationTable innerJoin FunktionaersQualifikationenTable)
.selectAll().where { FunktionaerQualifikationTable.funktionaerId eq id }
.map { it[QualifikationMasterTable.code] }
.map { it[FunktionaersQualifikationenTable.code] }
FunktionaerTable.selectAll().where { FunktionaerTable.id eq id }
.map { rowToDomFunktionaer(it, qualifikationen) }
@@ -58,9 +55,9 @@ class FunktionaerExposedRepository : FunktionaerRepository {
.where { (FunktionaerTable.satzId eq satzID) and (FunktionaerTable.satzNummer eq satzNummer) }
.singleOrNull() ?: return@dbQuery null
val qualifikationen = (FunktionaerQualifikationTable innerJoin QualifikationMasterTable)
val qualifikationen = (FunktionaerQualifikationTable innerJoin FunktionaersQualifikationenTable)
.selectAll().where { FunktionaerQualifikationTable.funktionaerId eq row[FunktionaerTable.id] }
.map { it[QualifikationMasterTable.code] }
.map { it[FunktionaersQualifikationenTable.code] }
rowToDomFunktionaer(row, qualifikationen)
}
@@ -71,9 +68,9 @@ class FunktionaerExposedRepository : FunktionaerRepository {
.toList()
val ids = funktionaere.map { it[FunktionaerTable.id] }
val qualisMap = (FunktionaerQualifikationTable innerJoin QualifikationMasterTable)
val qualisMap = (FunktionaerQualifikationTable innerJoin FunktionaersQualifikationenTable)
.selectAll().where { FunktionaerQualifikationTable.funktionaerId inList ids }
.groupBy({ it[FunktionaerQualifikationTable.funktionaerId] }) { it[QualifikationMasterTable.code] }
.groupBy({ it[FunktionaerQualifikationTable.funktionaerId] }) { it[FunktionaersQualifikationenTable.code] }
funktionaere.map { row ->
rowToDomFunktionaer(row, qualisMap[row[FunktionaerTable.id]] ?: emptyList())
@@ -118,9 +115,9 @@ class FunktionaerExposedRepository : FunktionaerRepository {
val typ = if (funktionaer.istRichter()) "RICHTER" else "PARCOURSBAUER"
funktionaer.qualifikationen.forEach { code ->
val masterId = QualifikationMasterTable
.selectAll().where { (QualifikationMasterTable.code eq code) and (QualifikationMasterTable.typ eq typ) }
.map { it[QualifikationMasterTable.id] }
val masterId = FunktionaersQualifikationenTable
.selectAll().where { (FunktionaersQualifikationenTable.code eq code) and (FunktionaersQualifikationenTable.typ eq typ) }
.map { it[FunktionaersQualifikationenTable.id] }
.singleOrNull()
if (masterId != null) {
@@ -48,10 +48,10 @@ object FunktionaerTable : Table("funktionaer") {
}
/**
* Exposed-Tabellendefinition für die Qualifikation-Master-Daten.
* Exposed-Tabellendefinition für die Qualifikation-Master-Daten (Funktionäre).
* Enthält offizielle ÖTO/FEI Kürzel (z.B. "D", "S", "P1").
*/
object QualifikationMasterTable : Table("qualifikation_master") {
object FunktionaersQualifikationenTable : Table("funktionaers_qualifikationen") {
val id = uuid("qualifikation_id")
/** Offizielles Kürzel (z.B. "SPF" für Springpferde) */
@@ -66,7 +66,7 @@ object QualifikationMasterTable : Table("qualifikation_master") {
override val primaryKey = PrimaryKey(id)
init {
index("idx_qualifikation_code_typ", isUnique = true, code, typ)
index("idx_funktionaers_qualifikationen_code_typ", isUnique = true, code, typ)
}
}
@@ -75,7 +75,7 @@ object QualifikationMasterTable : Table("qualifikation_master") {
*/
object FunktionaerQualifikationTable : Table("funktionaer_qualifikation") {
val funktionaerId = uuid("funktionaer_id").references(FunktionaerTable.id)
val qualifikationId = uuid("qualifikation_id").references(QualifikationMasterTable.id)
val qualifikationId = uuid("qualifikation_id").references(FunktionaersQualifikationenTable.id)
override val primaryKey = PrimaryKey(funktionaerId, qualifikationId)
}
@@ -0,0 +1,37 @@
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
package at.mocode.masterdata.infrastructure.persistence.reiter
import at.mocode.core.domain.model.SparteE
import at.mocode.core.utils.database.DatabaseFactory
import at.mocode.masterdata.domain.model.AltersklasseDefinition
import at.mocode.masterdata.domain.repository.AltersklassenRepository
import at.mocode.masterdata.infrastructure.persistence.AltersklasseTable
import org.jetbrains.exposed.v1.core.eq
import org.jetbrains.exposed.v1.jdbc.selectAll
/**
* Exposed-basierte Implementierung des AltersklassenRepository.
*/
class AltersklassenExposedRepository : AltersklassenRepository {
override suspend fun findByCode(code: String): AltersklasseDefinition? = DatabaseFactory.dbQuery {
AltersklasseTable.selectAll().where { AltersklasseTable.altersklasseCode eq code }
.map {
AltersklasseDefinition(
altersklasseId = it[AltersklasseTable.id],
altersklasseCode = it[AltersklasseTable.altersklasseCode],
bezeichnung = it[AltersklasseTable.bezeichnung],
minAlter = it[AltersklasseTable.minAlter],
maxAlter = it[AltersklasseTable.maxAlter],
stichtagRegelText = it[AltersklasseTable.stichtagRegelText],
sparteFilter = it[AltersklasseTable.sparteFilter]?.let { s -> SparteE.valueOf(s) },
geschlechtFilter = it[AltersklasseTable.geschlechtFilter],
oetoRegelReferenzId = it[AltersklasseTable.oetoRegelReferenzId],
istAktiv = it[AltersklasseTable.istAktiv],
createdAt = it[AltersklasseTable.createdAt],
updatedAt = it[AltersklasseTable.updatedAt]
)
}.singleOrNull()
}
}
@@ -5,7 +5,7 @@ package at.mocode.masterdata.infrastructure.persistence.reiter
import at.mocode.core.utils.database.DatabaseFactory
import at.mocode.masterdata.domain.model.BundeslandDefinition
import at.mocode.masterdata.domain.repository.BundeslandRepository
import at.mocode.masterdata.infrastructure.persistence.BundeslandTable
import at.mocode.masterdata.infrastructure.persistence.BundeslaenderTable
import org.jetbrains.exposed.v1.core.ResultRow
import org.jetbrains.exposed.v1.core.eq
import org.jetbrains.exposed.v1.jdbc.selectAll
@@ -17,22 +17,22 @@ import kotlin.uuid.Uuid
*/
class BundeslandExposedRepository : BundeslandRepository {
private fun rowToDom(row: ResultRow) = BundeslandDefinition(
bundeslandId = row[BundeslandTable.id],
landId = row[BundeslandTable.landId],
bundeslandNr = row[BundeslandTable.bundeslandNr],
oepsCode = row[BundeslandTable.oepsCode],
iso3166_2_Code = row[BundeslandTable.iso3166_2_Code],
name = row[BundeslandTable.name],
kuerzel = row[BundeslandTable.kuerzel],
wappenUrl = row[BundeslandTable.wappenUrl],
istAktiv = row[BundeslandTable.istAktiv],
sortierReihenfolge = row[BundeslandTable.sortierReihenfolge],
createdAt = row[BundeslandTable.createdAt],
updatedAt = row[BundeslandTable.updatedAt]
bundeslandId = row[BundeslaenderTable.id],
landId = row[BundeslaenderTable.landId],
bundeslandNr = row[BundeslaenderTable.bundeslandNr],
oepsCode = row[BundeslaenderTable.oepsCode],
iso3166_2_Code = row[BundeslaenderTable.iso3166_2_Code],
name = row[BundeslaenderTable.name],
kuerzel = row[BundeslaenderTable.kuerzel],
wappenUrl = row[BundeslaenderTable.wappenUrl],
istAktiv = row[BundeslaenderTable.istAktiv],
sortierReihenfolge = row[BundeslaenderTable.sortierReihenfolge],
createdAt = row[BundeslaenderTable.createdAt],
updatedAt = row[BundeslaenderTable.updatedAt]
)
override suspend fun findByNr(nr: Int): BundeslandDefinition? = DatabaseFactory.dbQuery {
BundeslandTable.selectAll().where { BundeslandTable.bundeslandNr eq nr }
BundeslaenderTable.selectAll().where { BundeslaenderTable.bundeslandNr eq nr }
.map(::rowToDom)
.singleOrNull()
}
@@ -0,0 +1,54 @@
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
package at.mocode.masterdata.infrastructure.persistence.reiter
import at.mocode.core.utils.database.DatabaseFactory
import at.mocode.masterdata.domain.model.FahrLizenz
import at.mocode.masterdata.domain.model.ReitLizenz
import at.mocode.masterdata.domain.model.Startkarte
import at.mocode.masterdata.domain.repository.MasterdataLicenseRepository
import org.jetbrains.exposed.v1.core.eq
import org.jetbrains.exposed.v1.jdbc.selectAll
/**
* Exposed-basierte Implementierung des MasterdataLicenseRepository.
*/
class MasterdataLicenseExposedRepository : MasterdataLicenseRepository {
override suspend fun findReitLizenzByCode(code: String): ReitLizenz? = DatabaseFactory.dbQuery {
ReitLizenzenTable.selectAll().where { ReitLizenzenTable.code eq code }
.map {
ReitLizenz(
lizenzId = it[ReitLizenzenTable.id],
code = it[ReitLizenzenTable.code],
bezeichnung = it[ReitLizenzenTable.bezeichnung],
sparte = it[ReitLizenzenTable.sparte],
istAktiv = it[ReitLizenzenTable.istAktiv]
)
}.singleOrNull()
}
override suspend fun findFahrLizenzByCode(code: String): FahrLizenz? = DatabaseFactory.dbQuery {
FahrLizenzenTable.selectAll().where { FahrLizenzenTable.code eq code }
.map {
FahrLizenz(
lizenzId = it[FahrLizenzenTable.id],
code = it[FahrLizenzenTable.code],
bezeichnung = it[FahrLizenzenTable.bezeichnung],
istAktiv = it[FahrLizenzenTable.istAktiv]
)
}.singleOrNull()
}
override suspend fun findStartkarteByCode(code: String): Startkarte? = DatabaseFactory.dbQuery {
StartkartenTable.selectAll().where { StartkartenTable.code eq code }
.map {
Startkarte(
startkarteId = it[StartkartenTable.id],
code = it[StartkartenTable.code],
bezeichnung = it[StartkartenTable.bezeichnung],
istAktiv = it[StartkartenTable.istAktiv]
)
}.singleOrNull()
}
}
@@ -3,15 +3,20 @@
package at.mocode.masterdata.infrastructure.persistence.reiter
import at.mocode.core.domain.model.DatenQuelleE
import at.mocode.core.domain.model.ReiterAltersKlasseE
import at.mocode.core.domain.model.ReiterLizenzKlasseE
import at.mocode.core.utils.database.DatabaseFactory
import at.mocode.masterdata.domain.model.Reiter
import at.mocode.masterdata.domain.model.ReiterLizenz
import at.mocode.masterdata.domain.repository.ReiterRepository
import org.jetbrains.exposed.v1.core.ResultRow
import org.jetbrains.exposed.v1.core.and
import org.jetbrains.exposed.v1.core.eq
import org.jetbrains.exposed.v1.core.lowerCase
import org.jetbrains.exposed.v1.jdbc.*
import org.jetbrains.exposed.v1.jdbc.deleteWhere
import org.jetbrains.exposed.v1.jdbc.insert
import org.jetbrains.exposed.v1.jdbc.selectAll
import org.jetbrains.exposed.v1.jdbc.update
import kotlin.uuid.ExperimentalUuidApi
import kotlin.uuid.Uuid
@@ -33,11 +38,14 @@ class ReiterExposedRepository : ReiterRepository {
vereinId = row[ReiterTable.vereinId],
bundeslandId = row[ReiterTable.bundeslandId],
nationId = row[ReiterTable.nationId],
reitLizenzId = row[ReiterTable.reitLizenzId],
fahrLizenzId = row[ReiterTable.fahrLizenzId],
startkarteId = row[ReiterTable.startkarteId],
reiterLizenz = row[ReiterTable.reiterLizenz],
startkarte = row[ReiterTable.startkarte],
fahrLizenz = row[ReiterTable.fahrLizenz],
altersklasseJgJrU25 = row[ReiterTable.altersklasseJgJrU25],
altersklasseY = row[ReiterTable.altersklasseY],
altersklasseJgJrU25 = row[ReiterTable.altersklasseJgJrU25]?.let { ReiterAltersKlasseE.valueOf(it) },
altersklasseY = row[ReiterTable.altersklasseY]?.let { ReiterAltersKlasseE.valueOf(it) },
mitgliedsNummer = row[ReiterTable.mitgliedsNummer],
telefonNummer = row[ReiterTable.telefonNummer],
kader = row[ReiterTable.kader],
@@ -52,10 +60,23 @@ class ReiterExposedRepository : ReiterRepository {
bemerkungen = row[ReiterTable.bemerkungen],
datenQuelle = DatenQuelleE.valueOf(row[ReiterTable.datenQuelle]),
createdAt = row[ReiterTable.createdAt],
updatedAt = row[ReiterTable.updatedAt]
updatedAt = row[ReiterTable.updatedAt],
lizenzen = loadLizenzen(row[ReiterTable.id])
)
}
private fun loadLizenzen(reiterId: Uuid): List<ReiterLizenz> {
return ReiterLizenzenZuordnungTable.selectAll().where { ReiterLizenzenZuordnungTable.reiterId eq reiterId }
.map {
at.mocode.masterdata.domain.model.ReiterLizenz(
lizenzId = it[ReiterLizenzenZuordnungTable.id],
lizenzTyp = it[ReiterLizenzenZuordnungTable.lizenzTyp],
kuerzel = it[ReiterLizenzenZuordnungTable.kuerzel],
gueltigBis = it[ReiterLizenzenZuordnungTable.gueltigBis]
)
}
}
override suspend fun findById(id: Uuid): Reiter? = DatabaseFactory.dbQuery {
ReiterTable.selectAll().where { ReiterTable.id eq id }
.map { rowToDomReiter(it) }
@@ -72,7 +93,7 @@ class ReiterExposedRepository : ReiterRepository {
ReiterTable.selectAll()
.where {
(ReiterTable.vorname.lowerCase() eq vorname.lowercase()) and
(ReiterTable.nachname.lowerCase() eq nachname.lowercase())
(ReiterTable.nachname.lowerCase() eq nachname.lowercase())
}
.map { row -> rowToDomReiter(row) }
}
@@ -97,11 +118,14 @@ class ReiterExposedRepository : ReiterRepository {
it[vereinId] = reiter.vereinId
it[bundeslandId] = reiter.bundeslandId
it[nationId] = reiter.nationId
it[reitLizenzId] = reiter.reitLizenzId
it[fahrLizenzId] = reiter.fahrLizenzId
it[startkarteId] = reiter.startkarteId
it[reiterLizenz] = reiter.reiterLizenz
it[startkarte] = reiter.startkarte
it[fahrLizenz] = reiter.fahrLizenz
it[altersklasseJgJrU25] = reiter.altersklasseJgJrU25
it[altersklasseY] = reiter.altersklasseY
it[altersklasseJgJrU25] = reiter.altersklasseJgJrU25?.name
it[altersklasseY] = reiter.altersklasseY?.name
it[mitgliedsNummer] = reiter.mitgliedsNummer
it[telefonNummer] = reiter.telefonNummer
it[kader] = reiter.kader
@@ -117,6 +141,7 @@ class ReiterExposedRepository : ReiterRepository {
it[datenQuelle] = reiter.datenQuelle.name
it[updatedAt] = reiter.updatedAt
}
saveLizenzen(reiter)
} else {
ReiterTable.insert {
it[id] = reiter.reiterId
@@ -130,11 +155,14 @@ class ReiterExposedRepository : ReiterRepository {
it[vereinId] = reiter.vereinId
it[bundeslandId] = reiter.bundeslandId
it[nationId] = reiter.nationId
it[reitLizenzId] = reiter.reitLizenzId
it[fahrLizenzId] = reiter.fahrLizenzId
it[startkarteId] = reiter.startkarteId
it[reiterLizenz] = reiter.reiterLizenz
it[startkarte] = reiter.startkarte
it[fahrLizenz] = reiter.fahrLizenz
it[altersklasseJgJrU25] = reiter.altersklasseJgJrU25
it[altersklasseY] = reiter.altersklasseY
it[altersklasseJgJrU25] = reiter.altersklasseJgJrU25?.name
it[altersklasseY] = reiter.altersklasseY?.name
it[mitgliedsNummer] = reiter.mitgliedsNummer
it[telefonNummer] = reiter.telefonNummer
it[kader] = reiter.kader
@@ -151,11 +179,25 @@ class ReiterExposedRepository : ReiterRepository {
it[createdAt] = reiter.createdAt
it[updatedAt] = reiter.updatedAt
}
saveLizenzen(reiter)
}
reiter
}
private fun saveLizenzen(reiter: Reiter) {
ReiterLizenzenZuordnungTable.deleteWhere { reiterId eq reiter.reiterId }
reiter.lizenzen.forEach { lizenz ->
ReiterLizenzenZuordnungTable.insert {
it[id] = lizenz.lizenzId
it[reiterId] = reiter.reiterId
it[lizenzTyp] = lizenz.lizenzTyp
it[kuerzel] = lizenz.kuerzel
it[gueltigBis] = lizenz.gueltigBis
}
}
}
override suspend fun delete(id: Uuid): Boolean = DatabaseFactory.dbQuery {
ReiterTable.deleteWhere { ReiterTable.id eq id } > 0
}
@@ -2,6 +2,7 @@
package at.mocode.masterdata.infrastructure.persistence.reiter
import at.mocode.masterdata.infrastructure.persistence.BundeslaenderTable
import org.jetbrains.exposed.v1.core.Table
import org.jetbrains.exposed.v1.datetime.CurrentTimestamp
import org.jetbrains.exposed.v1.datetime.date
@@ -27,6 +28,9 @@ object ReiterTable : Table("reiter") {
val vereinId = uuid("verein_id").nullable()
val bundeslandId = uuid("bundesland_id").nullable()
val nationId = uuid("nation_id").nullable()
val reitLizenzId = uuid("reit_lizenz_id").nullable()
val fahrLizenzId = uuid("fahr_lizenz_id").nullable()
val startkarteId = uuid("startkarte_id").nullable()
val reiterLizenz = varchar("reiter_lizenz", 20).nullable()
val startkarte = varchar("startkarte", 20).nullable()
@@ -65,24 +69,10 @@ object ReiterTable : Table("reiter") {
}
/**
* Exposed-Tabellendefinition für die Bundesland-Mastertabelle.
* Exposed-Tabellendefinition für die Reiter-Lizenzen-Zuordnung (historisch/mehrfach).
*/
object BundeslandTable : Table("bundesland") {
val id = uuid("bundesland_id")
val bundeslandNr = integer("bundesland_nr").uniqueIndex()
val bezeichnung = varchar("bezeichnung", 100)
val wappenUrl = varchar("wappen_url", 255).nullable()
val createdAt = timestamp("created_at").defaultExpression(CurrentTimestamp)
val updatedAt = timestamp("updated_at").defaultExpression(CurrentTimestamp)
override val primaryKey = PrimaryKey(id)
}
/**
* Exposed-Tabellendefinition für die Reiter-Lizenzen.
*/
object ReiterLizenzTable : Table("reiter_lizenz") {
val id = uuid("lizenz_id")
object ReiterLizenzenZuordnungTable : Table("reiter_lizenzen_zuordnung") {
val id = uuid("lizenz_zuordnung_id")
val reiterId = uuid("reiter_id")
val lizenzTyp = varchar("lizenz_typ", 50) // STARTKARTE, REITERLIZENZ, FAHRLIZENZ
val kuerzel = varchar("kuerzel", 20)
@@ -92,3 +82,46 @@ object ReiterLizenzTable : Table("reiter_lizenz") {
override val primaryKey = PrimaryKey(id)
}
/**
* Exposed-Tabellendefinition für Reit-Lizenzen (Stammdaten).
*/
object ReitLizenzenTable : Table("reit_lizenzen") {
val id = uuid("lizenz_id")
val code = varchar("code", 20).uniqueIndex()
val bezeichnung = varchar("bezeichnung", 100)
val sparte = varchar("sparte", 20).nullable()
val istAktiv = bool("ist_aktiv").default(true)
val createdAt = timestamp("created_at").defaultExpression(CurrentTimestamp)
val updatedAt = timestamp("updated_at").defaultExpression(CurrentTimestamp)
override val primaryKey = PrimaryKey(id)
}
/**
* Exposed-Tabellendefinition für Fahr-Lizenzen (Stammdaten).
*/
object FahrLizenzenTable : Table("fahr_lizenzen") {
val id = uuid("lizenz_id")
val code = varchar("code", 20).uniqueIndex()
val bezeichnung = varchar("bezeichnung", 100)
val istAktiv = bool("ist_aktiv").default(true)
val createdAt = timestamp("created_at").defaultExpression(CurrentTimestamp)
val updatedAt = timestamp("updated_at").defaultExpression(CurrentTimestamp)
override val primaryKey = PrimaryKey(id)
}
/**
* Exposed-Tabellendefinition für Startkarten (Stammdaten).
*/
object StartkartenTable : Table("startkarten") {
val id = uuid("startkarte_id")
val code = varchar("code", 20).uniqueIndex()
val bezeichnung = varchar("bezeichnung", 100)
val istAktiv = bool("ist_aktiv").default(true)
val createdAt = timestamp("created_at").defaultExpression(CurrentTimestamp)
val updatedAt = timestamp("updated_at").defaultExpression(CurrentTimestamp)
override val primaryKey = PrimaryKey(id)
}
@@ -28,7 +28,7 @@ class RegulationSeedVerificationTest {
Database.connect("jdbc:h2:mem:regulationseed;DB_CLOSE_DELAY=-1", driver = "org.h2.Driver")
transaction {
SchemaUtils.create(
TurnierklasseTable,
TurnierKlassenTable,
LicenseTable,
RichtverfahrenTable,
GebuehrTable,
@@ -31,6 +31,7 @@ dependencies {
implementation(libs.spring.boot.starter.web)
implementation(libs.spring.boot.starter.validation)
implementation(libs.spring.boot.starter.actuator)
implementation(libs.spring.cloud.starter.consul.discovery)
implementation(libs.jackson.module.kotlin)
//implementation(libs.springdoc.openapi.starter.webmvc.ui)
@@ -0,0 +1,57 @@
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
package at.mocode.masterdata.service.config
import at.mocode.masterdata.infrastructure.persistence.AltersklasseTable
import jakarta.annotation.PostConstruct
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
import org.jetbrains.exposed.v1.jdbc.insert
import org.jetbrains.exposed.v1.jdbc.selectAll
import org.jetbrains.exposed.v1.core.*
import org.slf4j.LoggerFactory
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.DependsOn
import org.springframework.context.annotation.Profile
import kotlin.uuid.Uuid
/**
* Seeder für Altersklassen gemäß LIZENZ01.DAT (JG, JR, 25, Y).
*/
@Configuration
@Profile("!test")
@DependsOn("masterdataDatabaseConfiguration")
class AltersklassenSeeder {
private val log = LoggerFactory.getLogger(AltersklassenSeeder::class.java)
@PostConstruct
fun seed() {
log.info("Starte Seeding der Altersklassen...")
transaction {
val klassen = listOf(
Triple("JG", "JUGENDLICHER", "Altersklasse Jugend"),
Triple("JR", "JUNIOR", "Altersklasse Junior"),
Triple("25", "U25", "Altersklasse U25"),
Triple("Y", "JUNGER-REITER", "Altersklasse Junger Reiter")
)
klassen.forEach { (code, bez, desc) ->
upsertAltersklasse(code, bez)
}
}
}
private fun upsertAltersklasse(code: String, bezeichnung: String) {
val exists = AltersklasseTable.selectAll()
.where { AltersklasseTable.altersklasseCode eq code }
.any()
if (!exists) {
AltersklasseTable.insert {
it[id] = Uuid.random()
it[altersklasseCode] = code
it[AltersklasseTable.bezeichnung] = bezeichnung
it[istAktiv] = true
}
log.debug("Altersklasse '{}' angelegt.", code)
}
}
}
@@ -0,0 +1,80 @@
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
package at.mocode.masterdata.service.config
import at.mocode.masterdata.infrastructure.persistence.TurnierKlassenTable
import jakarta.annotation.PostConstruct
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
import org.jetbrains.exposed.v1.jdbc.insert
import org.jetbrains.exposed.v1.jdbc.selectAll
import org.jetbrains.exposed.v1.core.*
import org.slf4j.LoggerFactory
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.DependsOn
import org.springframework.context.annotation.Profile
import kotlin.uuid.Uuid
/**
* Seeder für Bewerbsklassen (ehemals Turnierklassen) gemäß ÖTO.
*/
@Configuration
@Profile("!test")
@DependsOn("masterdataDatabaseConfiguration")
class BewerbsKlassenSeeder {
private val log = LoggerFactory.getLogger(BewerbsKlassenSeeder::class.java)
@PostConstruct
fun seed() {
log.info("Starte Seeding der Bewerbsklassen...")
transaction {
seedSpringenKlassen()
seedDressurKlassen()
}
log.info("Seeding der Bewerbsklassen abgeschlossen.")
}
private fun seedSpringenKlassen() {
val klassen = listOf(
Triple("E", "Einsteiger", 80),
Triple("A", "Anfänger", 105),
Triple("L", "Leicht", 115),
Triple("LM", "Leicht-Mittel", 125),
Triple("M", "Mittelschwer", 135),
Triple("S", "Schwer", 150)
)
klassen.forEach { (code, bez, hoehe) ->
upsertKlasse("SPRINGEN", code, bez, hoehe, null)
}
}
private fun seedDressurKlassen() {
val klassen = listOf(
Triple("E", "Einsteiger", "Aufgaben E"),
Triple("A", "Anfänger", "Aufgaben A"),
Triple("L", "Leicht", "Aufgaben L"),
Triple("LM", "Leicht-Mittel", "Aufgaben LM"),
Triple("M", "Mittelschwer", "Aufgaben M"),
Triple("S", "Schwer", "Aufgaben S")
)
klassen.forEach { (code, bez, niveau) ->
upsertKlasse("DRESSUR", code, bez, null, niveau)
}
}
private fun upsertKlasse(sparte: String, code: String, bezeichnung: String, hoehe: Int?, niveau: String?) {
val exists = TurnierKlassenTable.selectAll()
.where { (TurnierKlassenTable.sparte eq sparte) and (TurnierKlassenTable.code eq code) }
.any()
if (!exists) {
TurnierKlassenTable.insert {
it[id] = Uuid.random()
it[TurnierKlassenTable.sparte] = sparte
it[TurnierKlassenTable.code] = code
it[TurnierKlassenTable.bezeichnung] = bezeichnung
it[TurnierKlassenTable.maxHoehe] = hoehe
it[TurnierKlassenTable.aufgabenNiveau] = niveau
}
log.debug("Bewerbsklasse '{}' ({}) angelegt.", code, sparte)
}
}
}
@@ -0,0 +1,79 @@
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
package at.mocode.masterdata.service.config
import at.mocode.masterdata.infrastructure.persistence.funktionaer.FunktionaersQualifikationenTable
import jakarta.annotation.PostConstruct
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
import org.jetbrains.exposed.v1.jdbc.insert
import org.jetbrains.exposed.v1.jdbc.selectAll
import org.jetbrains.exposed.v1.core.*
import org.slf4j.LoggerFactory
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.DependsOn
import org.springframework.context.annotation.Profile
import kotlin.uuid.Uuid
/**
* Seeder für Funktionärs-Qualifikationen.
* Befüllt die FunktionaersQualifikationenTable mit Standard-Werten.
*/
@Configuration
@Profile("!test")
@DependsOn("masterdataDatabaseConfiguration")
class FunktionaersQualifikationenSeeder {
private val log = LoggerFactory.getLogger(FunktionaersQualifikationenSeeder::class.java)
@PostConstruct
fun seed() {
log.info("Starte Seeding der Funktionärs-Qualifikationen...")
transaction {
seedRichterQualifikationen()
seedParcoursbauerQualifikationen()
}
log.info("Seeding der Funktionärs-Qualifikationen abgeschlossen.")
}
private fun seedRichterQualifikationen() {
val qualis = listOf(
"D" to "Dressur",
"S" to "Springen",
"DPF" to "Dressurpferde",
"SPF" to "Springpferde",
"G" to "Gelände",
"STW" to "Steward",
"DM" to "Dressur Master",
"SM" to "Springen Master"
)
qualis.forEach { (code, bezeichnung) ->
upsertQualifikation(code, bezeichnung, "RICHTER")
}
}
private fun seedParcoursbauerQualifikationen() {
val qualis = listOf(
"P1" to "Einsteiger",
"P2" to "Fortgeschritten",
"P3" to "National",
"P4" to "Grand Prix"
)
qualis.forEach { (code, bezeichnung) ->
upsertQualifikation(code, bezeichnung, "PARCOURSBAUER")
}
}
private fun upsertQualifikation(code: String, bezeichnung: String, typ: String) {
val exists = FunktionaersQualifikationenTable.selectAll()
.where { (FunktionaersQualifikationenTable.code eq code) and (FunktionaersQualifikationenTable.typ eq typ) }
.any()
if (!exists) {
FunktionaersQualifikationenTable.insert {
it[id] = Uuid.random()
it[FunktionaersQualifikationenTable.code] = code
it[FunktionaersQualifikationenTable.bezeichnung] = bezeichnung
it[FunktionaersQualifikationenTable.typ] = typ
}
log.debug("Qualifikation '{}' ({}) angelegt.", code, typ)
}
}
}
@@ -1,14 +1,11 @@
package at.mocode.masterdata.service.config
import at.mocode.masterdata.infrastructure.persistence.*
import at.mocode.masterdata.infrastructure.persistence.funktionaer.FunktionaerQualifikationTable
import at.mocode.masterdata.infrastructure.persistence.funktionaer.FunktionaerTable
import at.mocode.masterdata.infrastructure.persistence.funktionaer.QualifikationMasterTable
import at.mocode.masterdata.infrastructure.persistence.funktionaer.FunktionaersQualifikationenTable
import at.mocode.masterdata.infrastructure.persistence.pferd.HorseTable
import at.mocode.masterdata.infrastructure.persistence.reiter.ReiterTable
import at.mocode.masterdata.infrastructure.persistence.reiter.ReiterLizenzTable
import at.mocode.masterdata.infrastructure.persistence.reiter.BundeslandTable
import at.mocode.masterdata.infrastructure.persistence.reiter.*
import at.mocode.masterdata.infrastructure.persistence.verein.VereinTable
import jakarta.annotation.PostConstruct
import jakarta.annotation.PreDestroy
@@ -46,22 +43,26 @@ class MasterdataDatabaseConfiguration(
transaction {
SchemaUtils.create(
LandTable,
BundeslandTable,
BundeslaenderTable,
AltersklasseTable,
PlatzTable,
ReiterTable,
HorseTable,
VereinTable,
FunktionaerTable,
QualifikationMasterTable,
FunktionaersQualifikationenTable,
FunktionaerQualifikationTable,
ReiterLizenzTable,
TurnierklasseTable,
ReitLizenzenTable,
FahrLizenzenTable,
StartkartenTable,
ReiterLizenzenZuordnungTable,
TurnierKlassenTable,
TurnierSpartenTable,
TurnierKategorienTable,
LicenseTable,
RichtverfahrenTable,
GebuehrTable,
RegulationConfigTable,
ReiterSparteTable
RegulationConfigTable
)
log.info("Masterdata database schema initialized successfully")
}
@@ -95,22 +96,26 @@ class MasterdataTestDatabaseConfiguration {
transaction {
SchemaUtils.create(
LandTable,
BundeslandTable,
BundeslaenderTable,
AltersklasseTable,
PlatzTable,
ReiterTable,
HorseTable,
VereinTable,
FunktionaerTable,
QualifikationMasterTable,
FunktionaersQualifikationenTable,
FunktionaerQualifikationTable,
ReiterLizenzTable,
TurnierklasseTable,
ReitLizenzenTable,
FahrLizenzenTable,
StartkartenTable,
ReiterLizenzenZuordnungTable,
TurnierKlassenTable,
TurnierSpartenTable,
TurnierKategorienTable,
LicenseTable,
RichtverfahrenTable,
GebuehrTable,
RegulationConfigTable,
ReiterSparteTable
RegulationConfigTable
)
log.info("Test masterdata database schema initialized successfully")
}
@@ -0,0 +1,164 @@
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
package at.mocode.masterdata.service.config
import at.mocode.masterdata.infrastructure.persistence.BundeslaenderTable
import at.mocode.masterdata.infrastructure.persistence.LandTable
import at.mocode.masterdata.infrastructure.persistence.reiter.ReiterTable
import jakarta.annotation.PostConstruct
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
import org.jetbrains.exposed.v1.core.*
import org.jetbrains.exposed.v1.jdbc.*
import org.slf4j.LoggerFactory
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.DependsOn
import org.springframework.context.annotation.Profile
import kotlin.uuid.Uuid
/**
* Seeder für Nationen (Länder) und Bundesländer.
* Stellt sicher, dass die für den ZNS-Import notwendigen Masterdaten vorhanden sind.
*/
@Configuration
@Profile("!test")
@DependsOn("masterdataDatabaseConfiguration")
class MasterdataSeeder {
private val log = LoggerFactory.getLogger(MasterdataSeeder::class.java)
@PostConstruct
fun seed() {
log.info("Starte Seeding der Master-Daten (Länder & Bundesländer)...")
transaction {
seedCountries()
seedAustrianBundeslaender()
fixReiterForeignKeys()
}
log.info("Seeding der Master-Daten abgeschlossen.")
}
private fun fixReiterForeignKeys() {
// Falls Reiter bereits importiert wurden, bevor die Masterdaten da waren,
// versuchen wir hier die Verknüpfungen (FKs) zu heilen.
log.info("Prüfe und korrigiere Reiter-Fremdschlüssel (Heilung)...")
// 1. Bundesland-Links heilen
val states = BundeslaenderTable.selectAll().toList()
states.forEach { row ->
val bId = row[BundeslaenderTable.id]
val bNr = row[BundeslaenderTable.bundeslandNr]
if (bNr != null) {
ReiterTable.update({ (ReiterTable.bundeslandNummer eq bNr) and (ReiterTable.bundeslandId.isNull()) }) {
it[bundeslandId] = bId
}
}
}
// 2. Nation-Links heilen (ISO-Alpha3 Lookup)
val nations = LandTable.selectAll().toList()
nations.forEach { row ->
val nId = row[LandTable.id]
val iso3 = row[LandTable.isoAlpha3Code]
ReiterTable.update({ (ReiterTable.nation eq iso3) and (ReiterTable.nationId.isNull()) }) {
it[nationId] = nId
}
}
// 3. Verein-Links heilen (Lookup über Name)
log.info("Heile Reiter-Vereins-Verknüpfungen via Name...")
val vt = at.mocode.masterdata.infrastructure.persistence.verein.VereinTable
val vereine = vt.selectAll().toList()
vereine.forEach { row ->
val vId = row[vt.id]
val vName = row[vt.vereinName]
ReiterTable.update({ (ReiterTable.vereinsName eq vName) and (ReiterTable.vereinId.isNull()) }) {
it[vereinId] = vId
}
}
}
private fun seedCountries() {
val countries = listOf(
// iso2, iso3, num, nameDe, nameEn, eu, ewr, sort
listOf("AT", "AUT", "040", "Österreich", "Austria", true, true, 1),
listOf("DE", "DEU", "276", "Deutschland", "Germany", true, true, 2),
listOf("CH", "CHE", "756", "Schweiz", "Switzerland", false, false, 3),
listOf("IT", "ITA", "380", "Italien", "Italy", true, true, 4),
listOf("CZ", "CZE", "203", "Tschechien", "Czech Republic", true, true, 6),
listOf("SK", "SVK", "703", "Slowakei", "Slovakia", true, true, 7),
listOf("SI", "SVN", "705", "Slowenien", "Slovenia", true, true, 8),
listOf("HU", "HUN", "348", "Ungarn", "Hungary", true, true, 9),
listOf("LI", "LIE", "438", "Liechtenstein", "Liechtenstein", false, true, 11)
)
countries.forEach { c ->
val iso2 = c[0] as String
val exists = LandTable.selectAll().where { LandTable.isoAlpha2Code eq iso2 }.any()
if (!exists) {
LandTable.insert {
it[id] = Uuid.random()
it[isoAlpha2Code] = iso2
it[isoAlpha3Code] = c[1] as String
it[isoNumerischerCode] = c[2] as String
it[nameDeutsch] = c[3] as String
it[nameEnglisch] = c[4] as String
it[istEuMitglied] = c[5] as Boolean
it[istEwrMitglied] = c[6] as Boolean
it[sortierReihenfolge] = c[7] as Int
}
log.debug("Land '{}' angelegt.", iso2)
}
}
}
private fun seedAustrianBundeslaender() {
val austria = LandTable.selectAll().where { LandTable.isoAlpha2Code eq "AT" }.singleOrNull()
if (austria == null) {
log.error("Österreich (AT) nicht gefunden, überspringe Bundesland-Seeding.")
return
}
val austriaId = austria[LandTable.id]
val states = listOf(
// nr, name, kuerzel, oeps
listOf(1, "Wien", "W", "09"),
listOf(2, "Niederösterreich", "", "03"),
listOf(3, "Burgenland", "BGLD", "01"),
listOf(4, "Steiermark", "STMK", "06"),
listOf(5, "Kärnten", "KTN", "02"),
listOf(6, "Oberösterreich", "", "04"),
listOf(7, "Salzburg", "SBG", "05"),
listOf(8, "Tirol", "T", "07"),
listOf(9, "Vorarlberg", "VBG", "08"),
listOf(0, "Unbekannt", "UNK", "00")
)
states.forEach { s ->
val nr = s[0] as Int
val name = s[1] as String
val kuerzel = s[2] as String
val oeps = s[3] as String
val exists = BundeslaenderTable.selectAll().where {
(BundeslaenderTable.landId eq austriaId) and (BundeslaenderTable.bundeslandNr eq nr)
}.any()
if (!exists) {
BundeslaenderTable.insert {
it[id] = Uuid.random()
it[landId] = austriaId
it[bundeslandNr] = nr
it[BundeslaenderTable.name] = name
it[BundeslaenderTable.kuerzel] = kuerzel
it[oepsCode] = oeps
}
log.debug("Bundesland '{}' (Nr. {}) angelegt.", name, nr)
} else {
// Update falls vorhanden (Harmonisierung)
BundeslaenderTable.update({ (BundeslaenderTable.landId eq austriaId) and (BundeslaenderTable.bundeslandNr eq nr) }) {
it[BundeslaenderTable.name] = name
it[BundeslaenderTable.kuerzel] = kuerzel
it[oepsCode] = oeps
}
}
}
}
}
@@ -1,86 +0,0 @@
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
package at.mocode.masterdata.service.config
import at.mocode.masterdata.infrastructure.persistence.funktionaer.QualifikationMasterTable
import jakarta.annotation.PostConstruct
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
import org.jetbrains.exposed.v1.core.*
import org.jetbrains.exposed.v1.jdbc.*
import org.slf4j.LoggerFactory
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.DependsOn
import org.springframework.context.annotation.Profile
import kotlin.uuid.Uuid
/**
* Seeder für die offiziellen ÖTO/FEI Qualifikations-Kürzel.
* Befüllt die QualifikationMasterTable mit Standard-Werten.
*/
@Configuration
@Profile("!test")
@DependsOn("masterdataDatabaseConfiguration")
class QualifikationMasterSeeder {
private val log = LoggerFactory.getLogger(QualifikationMasterSeeder::class.java)
@PostConstruct
fun seed() {
log.info("Starte Seeding der Qualifikations-Master-Daten (ÖTO/FEI)...")
transaction {
seedRichter()
seedParcoursbauer()
}
log.info("Seeding der Qualifikations-Master-Daten abgeschlossen.")
}
private fun seedRichter() {
val richterQualis = listOf(
"D" to "Dressur",
"S" to "Springen",
"DPF" to "Dressurpferde",
"SPF" to "Springpferde",
"G" to "Gelände",
"STW" to "Steward",
"DM" to "Dressur Master",
"SM" to "Springen Master",
"GA" to "Grundausbildung",
"G3" to "Gruppe 3",
"G2" to "Gruppe 2",
"G1" to "Gruppe 1"
)
richterQualis.forEach { (code, bezeichnung) ->
upsertQuali(code, bezeichnung, "RICHTER")
}
}
private fun seedParcoursbauer() {
val pbQualis = listOf(
"P1" to "Einsteiger",
"P2" to "Fortgeschritten",
"P3" to "National",
"P4" to "Grand Prix",
"SP" to "Springen",
"VS" to "Vielseitigkeit"
)
pbQualis.forEach { (code, bezeichnung) ->
upsertQuali(code, bezeichnung, "PARCOURSBAUER")
}
}
private fun upsertQuali(code: String, bezeichnung: String, typ: String) {
val exists = QualifikationMasterTable.selectAll()
.where { (QualifikationMasterTable.code eq code) and (QualifikationMasterTable.typ eq typ) }
.any()
if (!exists) {
QualifikationMasterTable.insert {
it[id] = Uuid.random()
it[QualifikationMasterTable.code] = code
it[QualifikationMasterTable.bezeichnung] = bezeichnung
it[QualifikationMasterTable.typ] = typ
}
log.debug("QualifikationMaster '{}' ({}) angelegt.", code, typ)
}
}
}
@@ -0,0 +1,88 @@
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
package at.mocode.masterdata.service.config
import at.mocode.masterdata.infrastructure.persistence.reiter.FahrLizenzenTable
import at.mocode.masterdata.infrastructure.persistence.reiter.StartkartenTable
import at.mocode.masterdata.infrastructure.persistence.reiter.ReitLizenzenTable
import jakarta.annotation.PostConstruct
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
import org.jetbrains.exposed.v1.jdbc.insert
import org.jetbrains.exposed.v1.jdbc.selectAll
import org.jetbrains.exposed.v1.core.*
import org.slf4j.LoggerFactory
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.DependsOn
import org.springframework.context.annotation.Profile
import kotlin.uuid.Uuid
/**
* Seeder für Reit-Lizenzen, Fahr-Lizenzen und Startkarten.
*/
@Configuration
@Profile("!test")
@DependsOn("masterdataDatabaseConfiguration")
class ReitLizenzenSeeder {
private val log = LoggerFactory.getLogger(ReitLizenzenSeeder::class.java)
@PostConstruct
fun seed() {
log.info("Starte Seeding der Lizenzen...")
transaction {
seedReitLizenzen()
seedFahrLizenzen()
seedStartkarten()
}
log.info("Seeding der Lizenzen abgeschlossen.")
}
private fun seedReitLizenzen() {
val lizenzen = listOf(
Triple("R1", "Reitlizenz R1", "SPRINGEN"),
Triple("RD1", "Reitlizenz RD1", "DRESSUR"),
Triple("RD2", "Reitlizenz RD2", "DRESSUR"),
Triple("RS2", "Reitlizenz RS2", "SPRINGEN")
)
lizenzen.forEach { (code, bez, sparte) ->
if (ReitLizenzenTable.selectAll().where { ReitLizenzenTable.code eq code }.none()) {
ReitLizenzenTable.insert {
it[id] = Uuid.random()
it[ReitLizenzenTable.code] = code
it[bezeichnung] = bez
it[ReitLizenzenTable.sparte] = sparte
}
}
}
}
private fun seedFahrLizenzen() {
val lizenzen = listOf(
Pair("F1", "Fahrlizenz F1"),
Pair("F2", "Fahrlizenz F2")
)
lizenzen.forEach { (code, bez) ->
if (FahrLizenzenTable.selectAll().where { FahrLizenzenTable.code eq code }.none()) {
FahrLizenzenTable.insert {
it[id] = Uuid.random()
it[FahrLizenzenTable.code] = code
it[bezeichnung] = bez
}
}
}
}
private fun seedStartkarten() {
val karten = listOf(
Pair("S1", "Startkarte S1"),
Pair("S2", "Startkarte S2")
)
karten.forEach { (code, bez) ->
if (StartkartenTable.selectAll().where { StartkartenTable.code eq code }.none()) {
StartkartenTable.insert {
it[id] = Uuid.random()
it[StartkartenTable.code] = code
it[bezeichnung] = bez
}
}
}
}
}
@@ -0,0 +1,51 @@
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
package at.mocode.masterdata.service.config
import at.mocode.masterdata.infrastructure.persistence.TurnierKategorienTable
import jakarta.annotation.PostConstruct
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
import org.jetbrains.exposed.v1.jdbc.insert
import org.jetbrains.exposed.v1.jdbc.selectAll
import org.jetbrains.exposed.v1.core.*
import org.slf4j.LoggerFactory
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.DependsOn
import org.springframework.context.annotation.Profile
import kotlin.uuid.Uuid
/**
* Seeder für Turnier-Kategorien (z.B. CSN-C, CDN-A).
*/
@Configuration
@Profile("!test")
@DependsOn("masterdataDatabaseConfiguration")
class TurnierKategorienSeeder {
private val log = LoggerFactory.getLogger(TurnierKategorienSeeder::class.java)
@PostConstruct
fun seed() {
log.info("Starte Seeding der Turnier-Kategorien...")
transaction {
val kategorien = listOf(
Triple("CSN-C", "Nationales Springturnier Kategorie C", "SPRINGEN"),
Triple("CSN-C Neu", "Nationales Springturnier Kategorie C Neu", "SPRINGEN"),
Triple("CSN-B", "Nationales Springturnier Kategorie B", "SPRINGEN"),
Triple("CSN-A", "Nationales Springturnier Kategorie A", "SPRINGEN"),
Triple("CDN-C", "Nationales Dressurturnier Kategorie C", "DRESSUR"),
Triple("CDN-B", "Nationales Dressurturnier Kategorie B", "DRESSUR"),
Triple("CDN-A", "Nationales Dressurturnier Kategorie A", "DRESSUR")
)
kategorien.forEach { (code, bez, sparte) ->
if (TurnierKategorienTable.selectAll().where { TurnierKategorienTable.code eq code }.none()) {
TurnierKategorienTable.insert {
it[id] = Uuid.random()
it[TurnierKategorienTable.code] = code
it[bezeichnung] = bez
it[TurnierKategorienTable.sparte] = sparte
}
}
}
}
}
}
@@ -0,0 +1,80 @@
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
package at.mocode.masterdata.service.config
import at.mocode.masterdata.infrastructure.persistence.TurnierKlassenTable
import jakarta.annotation.PostConstruct
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
import org.jetbrains.exposed.v1.jdbc.insert
import org.jetbrains.exposed.v1.jdbc.selectAll
import org.jetbrains.exposed.v1.core.*
import org.slf4j.LoggerFactory
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.DependsOn
import org.springframework.context.annotation.Profile
import kotlin.uuid.Uuid
/**
* Seeder für Turnierklassen (z.B. E, A, L, M, S) gemäß ÖTO.
*/
@Configuration
@Profile("!test")
@DependsOn("masterdataDatabaseConfiguration")
class TurnierKlassenSeeder {
private val log = LoggerFactory.getLogger(TurnierKlassenSeeder::class.java)
@PostConstruct
fun seed() {
log.info("Starte Seeding der Turnierklassen...")
transaction {
seedSpringenKlassen()
seedDressurKlassen()
}
log.info("Seeding der Turnierklassen abgeschlossen.")
}
private fun seedSpringenKlassen() {
val klassen = listOf(
Triple("E", "Einsteiger", 80),
Triple("A", "Anfänger", 105),
Triple("L", "Leicht", 115),
Triple("LM", "Leicht-Mittel", 125),
Triple("M", "Mittelschwer", 135),
Triple("S", "Schwer", 150)
)
klassen.forEach { (code, bez, hoehe) ->
upsertKlasse("SPRINGEN", code, bez, hoehe, null)
}
}
private fun seedDressurKlassen() {
val klassen = listOf(
Triple("E", "Einsteiger", "Aufgaben E"),
Triple("A", "Anfänger", "Aufgaben A"),
Triple("L", "Leicht", "Aufgaben L"),
Triple("LM", "Leicht-Mittel", "Aufgaben LM"),
Triple("M", "Mittelschwer", "Aufgaben M"),
Triple("S", "Schwer", "Aufgaben S")
)
klassen.forEach { (code, bez, niveau) ->
upsertKlasse("DRESSUR", code, bez, null, niveau)
}
}
private fun upsertKlasse(sparte: String, code: String, bezeichnung: String, hoehe: Int?, niveau: String?) {
val exists = TurnierKlassenTable.selectAll()
.where { (TurnierKlassenTable.sparte eq sparte) and (TurnierKlassenTable.code eq code) }
.any()
if (!exists) {
TurnierKlassenTable.insert {
it[id] = Uuid.random()
it[TurnierKlassenTable.sparte] = sparte
it[TurnierKlassenTable.code] = code
it[TurnierKlassenTable.bezeichnung] = bezeichnung
it[TurnierKlassenTable.maxHoehe] = hoehe
it[TurnierKlassenTable.aufgabenNiveau] = niveau
}
log.debug("Turnierklasse '{}' ({}) angelegt.", code, sparte)
}
}
}
@@ -0,0 +1,60 @@
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
package at.mocode.masterdata.service.config
import at.mocode.masterdata.infrastructure.persistence.TurnierSpartenTable
import jakarta.annotation.PostConstruct
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
import org.jetbrains.exposed.v1.jdbc.insert
import org.jetbrains.exposed.v1.jdbc.selectAll
import org.jetbrains.exposed.v1.core.*
import org.slf4j.LoggerFactory
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.DependsOn
import org.springframework.context.annotation.Profile
import kotlin.uuid.Uuid
/**
* Seeder für Turnier-Sparten (z.B. Dressur, Springen, Vielseitigkeit) gemäß ÖTO.
*/
@Configuration
@Profile("!test")
@DependsOn("masterdataDatabaseConfiguration")
class TurnierSpartenSeeder {
private val log = LoggerFactory.getLogger(TurnierSpartenSeeder::class.java)
@PostConstruct
fun seed() {
log.info("Starte Seeding der Turnier-Sparten...")
transaction {
val sparten = listOf(
"D" to "Dressur",
"S" to "Springen",
"V" to "Vielseitigkeit",
"F" to "Fahren",
"R" to "Reiten",
"C" to "Voltigieren",
"W" to "Western",
"O" to "Orientierungsreiten"
)
sparten.forEach { (code, bezeichnung) ->
upsertSparte(code, bezeichnung)
}
}
log.info("Seeding der Turnier-Sparten abgeschlossen.")
}
private fun upsertSparte(code: String, bezeichnung: String) {
val exists = TurnierSpartenTable.selectAll()
.where { TurnierSpartenTable.code eq code }
.any()
if (!exists) {
TurnierSpartenTable.insert {
it[id] = Uuid.random()
it[TurnierSpartenTable.code] = code
it[TurnierSpartenTable.bezeichnung] = bezeichnung
}
log.debug("Turnier-Sparte '{}' angelegt.", bezeichnung)
}
}
}
@@ -11,10 +11,24 @@ spring:
flyway:
enabled: true
baseline-on-migrate: true
cloud:
consul:
host: ${SPRING_CLOUD_CONSUL_HOST:localhost}
port: ${SPRING_CLOUD_CONSUL_PORT:8500}
enabled: ${CONSUL_ENABLED:true}
discovery:
enabled: ${CONSUL_ENABLED:true}
register: ${CONSUL_ENABLED:true}
health-check-path: /actuator/health
health-check-interval: 10s
health-check-port: 8086
instance-id: ${spring.application.name}-${server.port}-${random.uuid}
service-name: ${spring.application.name}
port: 8091
server:
port: 8086 # Spring Boot Management Port (Actuator & Tomcat)
address: 127.0.0.1 # Sicherheit: Nur lokal erreichbar
address: 0.0.0.0 # Erreichbar für Consul Health Checks
masterdata:
http:
@@ -0,0 +1,45 @@
-- V013__Cleanup_and_Standardize_Masterdata.sql
-- Datum: 6. April 2026
-- 1. Bundesland -> bundeslaender
ALTER TABLE bundesland RENAME TO bundeslaender;
ALTER TABLE bundeslaender RENAME COLUMN id TO bundesland_id;
ALTER INDEX IF EXISTS pk_bundesland RENAME TO pk_bundeslaender;
ALTER INDEX IF EXISTS idx_bundesland_oeps RENAME TO idx_bundeslaender_oeps;
ALTER INDEX IF EXISTS idx_bundesland_iso RENAME TO idx_bundeslaender_iso;
ALTER INDEX IF EXISTS ux_bundesland_land_kuerzel RENAME TO ux_bundeslaender_land_kuerzel;
ALTER INDEX IF EXISTS bundesland_bundesland_nr_unique RENAME TO bundeslaender_bundesland_nr_unique;
-- 2. qualifikation_master -> funktionaers_qualifikationen
ALTER TABLE qualifikation_master RENAME TO funktionaers_qualifikationen;
-- Die Join-Tabelle funktionaer_qualifikation bleibt als solche bestehen,
-- referenziert aber nun funktionaers_qualifikationen.
-- (Der Name der Join-Tabelle ist bereits fast korrekt, wir lassen sie vorerst so,
-- da sie die Verknüpfung zw. Funktionär und Qualifikation darstellt.)
-- Update: Der User möchte "funktionaers_qualifikationen" als Name für die Qualifikationen.
-- 3. reiter_lizenz -> reit_lizenzen
ALTER TABLE reiter_lizenz RENAME TO reit_lizenzen;
ALTER INDEX IF EXISTS pk_reiter_lizenz RENAME TO pk_reit_lizenzen;
-- 4. reiter_sparte entfernen
DROP TABLE IF EXISTS reiter_sparte;
-- 5. turnierklasse -> turnier_klassen
ALTER TABLE turnierklasse RENAME TO turnier_klassen;
ALTER INDEX IF EXISTS pk_turnierklasse RENAME TO pk_turnier_klassen;
ALTER INDEX IF EXISTS idx_turnierklasse_sparte_code RENAME TO idx_turnier_klassen_sparte_code;
-- 6. turnier_sparten erstellen
CREATE TABLE IF NOT EXISTS turnier_sparten (
sparte_id UUID PRIMARY KEY,
code VARCHAR(10) UNIQUE NOT NULL, -- z.B. D, S, V, F, R, C
bezeichnung VARCHAR(100) NOT NULL, -- z.B. Dressur, Springen, Vielseitigkeit, Fahren, Reiten, Voltigieren
ist_aktiv BOOLEAN DEFAULT true,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 7. Constraints aktualisieren (falls nötig)
-- Da wir nur Tabellen umbenannt haben, bleiben die Foreign Keys in PostgreSQL erhalten
-- und zeigen automatisch auf die neuen Tabellennamen.
@@ -0,0 +1,80 @@
-- V014__Expand_Masterdata_Rider_and_Competitions.sql
-- Datum: 6. April 2026
-- 1. altersklasse -> altersklassen
ALTER TABLE altersklasse RENAME TO altersklassen;
ALTER INDEX IF EXISTS pk_altersklasse RENAME TO pk_altersklassen;
ALTER INDEX IF EXISTS idx_altersklasse_aktiv RENAME TO idx_altersklassen_aktiv;
ALTER INDEX IF EXISTS idx_altersklasse_sparte RENAME TO idx_altersklassen_sparte;
ALTER INDEX IF EXISTS idx_altersklasse_geschlecht RENAME TO idx_altersklassen_geschlecht;
ALTER INDEX IF EXISTS idx_altersklasse_alter RENAME TO idx_altersklassen_alter;
-- 2. turnier_klassen -> bewerbs_klassen
ALTER TABLE turnier_klassen RENAME TO bewerbs_klassen;
ALTER TABLE bewerbs_klassen RENAME COLUMN turnierklasse_id TO bewerbsklasse_id;
ALTER INDEX IF EXISTS pk_turnier_klassen RENAME TO pk_bewerbs_klassen;
ALTER INDEX IF EXISTS idx_turnier_klassen_sparte_code RENAME TO idx_bewerbs_klassen_sparte_code;
-- 3. turnier_kategorien erstellen
CREATE TABLE IF NOT EXISTS turnier_kategorien (
kategorie_id UUID PRIMARY KEY,
code VARCHAR(20) UNIQUE NOT NULL, -- z.B. CSN-C, CDN-A, CSN-C Neu
bezeichnung VARCHAR(100) NOT NULL,
sparte VARCHAR(20), -- SPRINGEN, DRESSUR, etc.
ist_aktiv BOOLEAN DEFAULT true,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 4. reit_lizenzen anpassen: Ein Reiter hat 0..1 Reit-Lizenz (Kürzel wie R1, RD1, etc.)
-- Die bestehende Tabelle reit_lizenzen (Join-Tabelle/Mehrfach) wird hier
-- beibehalten für fachspezifische Detail-Lizenzen, aber wir fügen
-- in der Reiter-Tabelle direkte Spalten für die Haupt-Lizenzen hinzu.
-- Fahr-Lizenzen und Startkarten ebenfalls als 0..1 Spalten beim Reiter.
ALTER TABLE reiter ADD COLUMN IF NOT EXISTS fahr_lizenz_id UUID;
ALTER TABLE reiter ADD COLUMN IF NOT EXISTS startkarte_id UUID;
-- 5. fahr_lizenzen Master-Tabelle
CREATE TABLE IF NOT EXISTS fahr_lizenzen (
lizenz_id UUID PRIMARY KEY,
code VARCHAR(20) UNIQUE NOT NULL, -- z.B. F1, F2
bezeichnung VARCHAR(100) NOT NULL,
ist_aktiv BOOLEAN DEFAULT true,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 6. startkarten Master-Tabelle
CREATE TABLE IF NOT EXISTS startkarten (
startkarte_id UUID PRIMARY KEY,
code VARCHAR(20) UNIQUE NOT NULL, -- z.B. S1, S2
bezeichnung VARCHAR(100) NOT NULL,
ist_aktiv BOOLEAN DEFAULT true,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 7. reit_lizenzen (Master-Tabelle für Kürzel-Definitionen)
-- Wir hatten reit_lizenzen als Join-Tabelle definiert in V013.
-- Wir behalten reit_lizenzen als Join-Tabelle (reiter_id, kuerzel) bei,
-- aber fügen eine Master-Tabelle für die Kürzel selbst hinzu.
-- Umbenennung der in V013 erstellten reit_lizenzen zu reiter_lizenzen_zuordnung
ALTER TABLE reit_lizenzen RENAME TO reiter_lizenzen_zuordnung;
CREATE TABLE IF NOT EXISTS reit_lizenzen (
lizenz_id UUID PRIMARY KEY,
code VARCHAR(20) UNIQUE NOT NULL, -- z.B. R1, RD1, RS2
bezeichnung VARCHAR(100) NOT NULL,
sparte VARCHAR(20), -- SPRINGEN, DRESSUR, VIELSEITIGKEIT
ist_aktiv BOOLEAN DEFAULT true,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 8. Fremdschlüssel für Reiter Tabelle aktualisieren
ALTER TABLE reiter ADD CONSTRAINT fk_reiter_fahr_lizenz FOREIGN KEY (fahr_lizenz_id) REFERENCES fahr_lizenzen(lizenz_id);
ALTER TABLE reiter ADD CONSTRAINT fk_reiter_startkarte FOREIGN KEY (startkarte_id) REFERENCES startkarten(startkarte_id);
-- reit_lizenz_id hinzufügen für 0..1 Beziehung
ALTER TABLE reiter ADD COLUMN IF NOT EXISTS reit_lizenz_id UUID;
ALTER TABLE reiter ADD CONSTRAINT fk_reiter_reit_lizenz FOREIGN KEY (reit_lizenz_id) REFERENCES reit_lizenzen(lizenz_id);