Merge pull request #18

* MP-19 Refactoring: Einführung der "Registry" & "Masterdata" Trennung …

* MP-19 Refactoring: Frontend Tabula Rasa

* MP-19 Refactoring: Frontend Tabula Rasa

* refactoring:

* MP-20 fix(docker/clients): include `:domains` module in web/desktop b…

* MP-20 fix(web-app build): resolve JS compile error and add dev/prod b…

* MP-20 fix(web-app): remove vendor.js reference and harden JS bootstra…

* MP-20 fixing: clients

* MP-20 fixing: clients
This commit is contained in:
StefanMo
2025-11-30 14:13:12 +01:00
committed by GitHub
parent 596a05b69c
commit 9ea2b74a81
254 changed files with 5485 additions and 15971 deletions
@@ -0,0 +1,28 @@
plugins {
alias(libs.plugins.kotlin.jvm)
alias(libs.plugins.kotlin.spring)
// KORREKTUR: Dieses Plugin ist entscheidend. Es schaltet den `springBoot`-Block
// und alle Spring-Boot-spezifischen Gradle-Tasks frei.
alias(libs.plugins.spring.boot)
// Dependency Management für konsistente Spring-Versionen
alias(libs.plugins.spring.dependencyManagement)
}
dependencies {
implementation(projects.platform.platformDependencies)
implementation(projects.masterdata.masterdataDomain)
implementation(projects.masterdata.masterdataApplication)
implementation(projects.core.coreDomain)
implementation(projects.core.coreUtils)
implementation(projects.infrastructure.cache.cacheApi)
implementation(projects.infrastructure.eventStore.eventStoreApi)
implementation(projects.infrastructure.messaging.messagingClient)
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("org.postgresql:postgresql")
testImplementation(projects.platform.platformTesting)
}
@@ -0,0 +1,240 @@
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
package at.mocode.masterdata.infrastructure.persistence
import at.mocode.core.domain.model.SparteE
import at.mocode.masterdata.domain.model.AltersklasseDefinition
import at.mocode.masterdata.domain.repository.AltersklasseRepository
import at.mocode.core.utils.database.DatabaseFactory
import kotlin.uuid.Uuid
import kotlinx.datetime.Clock
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toInstant
import kotlinx.datetime.toLocalDateTime
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
/**
* Implementierung des AltersklasseRepository für die Datenbankzugriffe.
*
* Diese Implementierung verwendet Exposed SQL für den Datenbankzugriff
* und mappt zwischen der AltersklasseDefinition Domain-Entität und der AltersklasseTable.
*/
class AltersklasseRepositoryImpl : AltersklasseRepository {
/**
* Konvertiert eine Datenbankzeile in ein Domain-Objekt.
*/
private fun rowToAltersklasseDefinition(row: ResultRow): AltersklasseDefinition {
return AltersklasseDefinition(
altersklasseId = row[AltersklasseTable.id],
altersklasseCode = row[AltersklasseTable.altersklasseCode],
bezeichnung = row[AltersklasseTable.bezeichnung],
minAlter = row[AltersklasseTable.minAlter],
maxAlter = row[AltersklasseTable.maxAlter],
stichtagRegelText = row[AltersklasseTable.stichtagRegelText],
sparteFilter = row[AltersklasseTable.sparteFilter]?.let { SparteE.valueOf(it) },
geschlechtFilter = row[AltersklasseTable.geschlechtFilter],
oetoRegelReferenzId = row[AltersklasseTable.oetoRegelReferenzId],
istAktiv = row[AltersklasseTable.istAktiv],
createdAt = row[AltersklasseTable.createdAt].toInstant(TimeZone.UTC),
updatedAt = row[AltersklasseTable.updatedAt].toInstant(TimeZone.UTC)
)
}
override suspend fun findById(id: Uuid): AltersklasseDefinition? = DatabaseFactory.dbQuery {
AltersklasseTable.selectAll().where { AltersklasseTable.id eq id }
.map(::rowToAltersklasseDefinition)
.singleOrNull()
}
override suspend fun findByCode(altersklasseCode: String): AltersklasseDefinition? = DatabaseFactory.dbQuery {
AltersklasseTable.selectAll().where { AltersklasseTable.altersklasseCode eq altersklasseCode }
.map(::rowToAltersklasseDefinition)
.singleOrNull()
}
override suspend fun findByName(searchTerm: String, limit: Int): List<AltersklasseDefinition> = DatabaseFactory.dbQuery {
val pattern = "%$searchTerm%"
AltersklasseTable.selectAll().where { AltersklasseTable.bezeichnung like pattern }
.limit(limit)
.map(::rowToAltersklasseDefinition)
}
override suspend fun findAllActive(sparteFilter: SparteE?, geschlechtFilter: Char?): List<AltersklasseDefinition> = DatabaseFactory.dbQuery {
val query = AltersklasseTable.selectAll().where { AltersklasseTable.istAktiv eq true }
sparteFilter?.let { sparte ->
query.andWhere {
(AltersklasseTable.sparteFilter eq sparte.name) or (AltersklasseTable.sparteFilter.isNull())
}
}
geschlechtFilter?.let { geschlecht ->
query.andWhere {
(AltersklasseTable.geschlechtFilter eq geschlecht) or (AltersklasseTable.geschlechtFilter.isNull())
}
}
query.orderBy(AltersklasseTable.bezeichnung to SortOrder.ASC)
.map(::rowToAltersklasseDefinition)
}
override suspend fun findApplicableForAge(age: Int, sparteFilter: SparteE?, geschlechtFilter: Char?): List<AltersklasseDefinition> = DatabaseFactory.dbQuery {
val query = AltersklasseTable.selectAll().where { AltersklasseTable.istAktiv eq true }
// Age range filter
query.andWhere {
(AltersklasseTable.minAlter.isNull() or (AltersklasseTable.minAlter lessEq age)) and
(AltersklasseTable.maxAlter.isNull() or (AltersklasseTable.maxAlter greaterEq age))
}
sparteFilter?.let { sparte ->
query.andWhere {
(AltersklasseTable.sparteFilter eq sparte.name) or (AltersklasseTable.sparteFilter.isNull())
}
}
geschlechtFilter?.let { geschlecht ->
query.andWhere {
(AltersklasseTable.geschlechtFilter eq geschlecht) or (AltersklasseTable.geschlechtFilter.isNull())
}
}
query.orderBy(AltersklasseTable.bezeichnung to SortOrder.ASC)
.map(::rowToAltersklasseDefinition)
}
override suspend fun findBySparte(sparte: SparteE, activeOnly: Boolean): List<AltersklasseDefinition> = DatabaseFactory.dbQuery {
val query = AltersklasseTable.selectAll().where {
(AltersklasseTable.sparteFilter eq sparte.name) or (AltersklasseTable.sparteFilter.isNull())
}
if (activeOnly) {
query.andWhere { AltersklasseTable.istAktiv eq true }
}
query.orderBy(AltersklasseTable.bezeichnung to SortOrder.ASC)
.map(::rowToAltersklasseDefinition)
}
override suspend fun findByGeschlecht(geschlecht: Char, activeOnly: Boolean): List<AltersklasseDefinition> = DatabaseFactory.dbQuery {
val query = AltersklasseTable.selectAll().where {
(AltersklasseTable.geschlechtFilter eq geschlecht) or (AltersklasseTable.geschlechtFilter.isNull())
}
if (activeOnly) {
query.andWhere { AltersklasseTable.istAktiv eq true }
}
query.orderBy(AltersklasseTable.bezeichnung to SortOrder.ASC)
.map(::rowToAltersklasseDefinition)
}
override suspend fun findByAgeRange(minAge: Int?, maxAge: Int?, activeOnly: Boolean): List<AltersklasseDefinition> = DatabaseFactory.dbQuery {
val query = AltersklasseTable.selectAll()
minAge?.let { min ->
query.andWhere {
(AltersklasseTable.maxAlter.isNull()) or (AltersklasseTable.maxAlter greaterEq min)
}
}
maxAge?.let { max ->
query.andWhere {
(AltersklasseTable.minAlter.isNull()) or (AltersklasseTable.minAlter lessEq max)
}
}
if (activeOnly) {
query.andWhere { AltersklasseTable.istAktiv eq true }
}
query.orderBy(AltersklasseTable.bezeichnung to SortOrder.ASC)
.map(::rowToAltersklasseDefinition)
}
override suspend fun findByOetoRegelReferenz(oetoRegelReferenzId: Uuid): List<AltersklasseDefinition> = DatabaseFactory.dbQuery {
AltersklasseTable.selectAll().where { AltersklasseTable.oetoRegelReferenzId eq oetoRegelReferenzId }
.orderBy(AltersklasseTable.bezeichnung to SortOrder.ASC)
.map(::rowToAltersklasseDefinition)
}
override suspend fun save(altersklasse: AltersklasseDefinition): AltersklasseDefinition = DatabaseFactory.dbQuery {
val now = Clock.System.now()
val existingAltersklasse = AltersklasseTable.selectAll().where { AltersklasseTable.id eq altersklasse.altersklasseId }.singleOrNull()
if (existingAltersklasse == null) {
// Insert a new age class
AltersklasseTable.insert { stmt ->
stmt[id] = altersklasse.altersklasseId
stmt[altersklasseCode] = altersklasse.altersklasseCode
stmt[bezeichnung] = altersklasse.bezeichnung
stmt[minAlter] = altersklasse.minAlter
stmt[maxAlter] = altersklasse.maxAlter
stmt[stichtagRegelText] = altersklasse.stichtagRegelText
stmt[sparteFilter] = altersklasse.sparteFilter?.name
stmt[geschlechtFilter] = altersklasse.geschlechtFilter
stmt[oetoRegelReferenzId] = altersklasse.oetoRegelReferenzId
stmt[istAktiv] = altersklasse.istAktiv
stmt[createdAt] = altersklasse.createdAt.toLocalDateTime(TimeZone.UTC)
stmt[updatedAt] = now.toLocalDateTime(TimeZone.UTC)
}
} else {
// Update existing age class
AltersklasseTable.update({ AltersklasseTable.id eq altersklasse.altersklasseId }) { stmt ->
stmt[altersklasseCode] = altersklasse.altersklasseCode
stmt[bezeichnung] = altersklasse.bezeichnung
stmt[minAlter] = altersklasse.minAlter
stmt[maxAlter] = altersklasse.maxAlter
stmt[stichtagRegelText] = altersklasse.stichtagRegelText
stmt[sparteFilter] = altersklasse.sparteFilter?.name
stmt[geschlechtFilter] = altersklasse.geschlechtFilter
stmt[oetoRegelReferenzId] = altersklasse.oetoRegelReferenzId
stmt[istAktiv] = altersklasse.istAktiv
stmt[updatedAt] = now.toLocalDateTime(TimeZone.UTC)
}
}
altersklasse.copy(updatedAt = now)
}
override suspend fun delete(id: Uuid): Boolean = DatabaseFactory.dbQuery {
AltersklasseTable.deleteWhere { AltersklasseTable.id eq id } > 0
}
override suspend fun existsByCode(altersklasseCode: String): Boolean = DatabaseFactory.dbQuery {
AltersklasseTable.selectAll().where { AltersklasseTable.altersklasseCode eq altersklasseCode }
.count() > 0
}
override suspend fun countActive(sparteFilter: SparteE?): Long = DatabaseFactory.dbQuery {
val query = AltersklasseTable.selectAll().where { AltersklasseTable.istAktiv eq true }
sparteFilter?.let { sparte ->
query.andWhere {
(AltersklasseTable.sparteFilter eq sparte.name) or (AltersklasseTable.sparteFilter.isNull())
}
}
query.count()
}
override suspend fun isEligible(altersklasseId: Uuid, age: Int, geschlecht: Char): Boolean = DatabaseFactory.dbQuery {
val altersklasse = AltersklasseTable.selectAll().where {
(AltersklasseTable.id eq altersklasseId) and (AltersklasseTable.istAktiv eq true)
}.singleOrNull()
if (altersklasse == null) return@dbQuery false
// Check age eligibility
val minAlter = altersklasse[AltersklasseTable.minAlter]
val maxAlter = altersklasse[AltersklasseTable.maxAlter]
val ageEligible = (minAlter == null || age >= minAlter) && (maxAlter == null || age <= maxAlter)
// Check gender eligibility
val geschlechtFilter = altersklasse[AltersklasseTable.geschlechtFilter]
val genderEligible = geschlechtFilter == null || geschlechtFilter == geschlecht
ageEligible && genderEligible
}
}
@@ -0,0 +1,36 @@
package at.mocode.masterdata.infrastructure.persistence
import org.jetbrains.exposed.sql.Table
import org.jetbrains.exposed.sql.kotlin.datetime.datetime
import org.jetbrains.exposed.sql.kotlin.datetime.CurrentDateTime
/**
* Exposed-Tabellendefinition für die Altersklasse-Entität (Altersklassendefinitionen).
*
* Diese Tabelle speichert alle Informationen zu Altersklassen für Teilnehmer
* entsprechend der AltersklasseDefinition Domain-Entität.
*/
object AltersklasseTable : Table("altersklasse") {
val id = uuid("id").autoGenerate()
val altersklasseCode = varchar("altersklasse_code", 50).uniqueIndex()
val bezeichnung = varchar("bezeichnung", 200)
val minAlter = integer("min_alter").nullable()
val maxAlter = integer("max_alter").nullable()
val stichtagRegelText = varchar("stichtag_regel_text", 500).nullable()
val sparteFilter = varchar("sparte_filter", 50).nullable() // Enum as string
val geschlechtFilter = char("geschlecht_filter").nullable()
val oetoRegelReferenzId = uuid("oeto_regel_referenz_id").nullable()
val istAktiv = bool("ist_aktiv").default(true)
val createdAt = datetime("created_at").defaultExpression(CurrentDateTime)
val updatedAt = datetime("updated_at").defaultExpression(CurrentDateTime)
override val primaryKey = PrimaryKey(id)
init {
// Index for performance on common queries
index(customIndexName = "idx_altersklasse_aktiv", columns = arrayOf(istAktiv))
index(customIndexName = "idx_altersklasse_sparte", columns = arrayOf(sparteFilter))
index(customIndexName = "idx_altersklasse_geschlecht", columns = arrayOf(geschlechtFilter))
index(customIndexName = "idx_altersklasse_alter", columns = arrayOf(minAlter, maxAlter))
}
}
@@ -0,0 +1,158 @@
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
package at.mocode.masterdata.infrastructure.persistence
import at.mocode.masterdata.domain.model.BundeslandDefinition
import at.mocode.masterdata.domain.repository.BundeslandRepository
import at.mocode.core.utils.database.DatabaseFactory
import kotlin.uuid.Uuid
import kotlinx.datetime.Clock
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toInstant
import kotlinx.datetime.toLocalDateTime
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
/**
* Implementierung des BundeslandRepository für die Datenbankzugriffe.
*
* Diese Implementierung verwendet Exposed SQL für den Datenbankzugriff
* und mappt zwischen der BundeslandDefinition Domain-Entität und der BundeslandTable.
*/
class BundeslandRepositoryImpl : BundeslandRepository {
/**
* Konvertiert eine Datenbankzeile in ein Domain-Objekt.
*/
private fun rowToBundeslandDefinition(row: ResultRow): BundeslandDefinition {
return BundeslandDefinition(
bundeslandId = row[BundeslandTable.id],
landId = row[BundeslandTable.landId],
oepsCode = row[BundeslandTable.oepsCode],
iso3166_2_Code = row[BundeslandTable.iso3166_2_Code],
name = row[BundeslandTable.name],
kuerzel = row[BundeslandTable.kuerzel],
wappenUrl = row[BundeslandTable.wappenUrl],
istAktiv = row[BundeslandTable.istAktiv],
sortierReihenfolge = row[BundeslandTable.sortierReihenfolge],
createdAt = row[BundeslandTable.createdAt].toInstant(TimeZone.UTC),
updatedAt = row[BundeslandTable.updatedAt].toInstant(TimeZone.UTC)
)
}
override suspend fun findById(id: Uuid): BundeslandDefinition? = DatabaseFactory.dbQuery {
BundeslandTable.selectAll().where { BundeslandTable.id eq id }
.map(::rowToBundeslandDefinition)
.singleOrNull()
}
override suspend fun findByOepsCode(oepsCode: String, landId: Uuid): BundeslandDefinition? = DatabaseFactory.dbQuery {
BundeslandTable.selectAll().where {
(BundeslandTable.oepsCode eq oepsCode) and (BundeslandTable.landId eq landId)
}
.map(::rowToBundeslandDefinition)
.singleOrNull()
}
override suspend fun findByIso3166_2_Code(iso3166_2_Code: String): BundeslandDefinition? = DatabaseFactory.dbQuery {
BundeslandTable.selectAll().where { BundeslandTable.iso3166_2_Code eq iso3166_2_Code }
.map(::rowToBundeslandDefinition)
.singleOrNull()
}
override suspend fun findByCountry(landId: Uuid, activeOnly: Boolean, orderBySortierung: Boolean): List<BundeslandDefinition> = DatabaseFactory.dbQuery {
val query = BundeslandTable.selectAll().where { BundeslandTable.landId eq landId }
if (activeOnly) {
query.andWhere { BundeslandTable.istAktiv eq true }
}
if (orderBySortierung) {
query.orderBy(BundeslandTable.sortierReihenfolge to SortOrder.ASC, BundeslandTable.name to SortOrder.ASC)
} else {
query.orderBy(BundeslandTable.name to SortOrder.ASC)
}
query.map(::rowToBundeslandDefinition)
}
override suspend fun findByName(searchTerm: String, landId: Uuid?, limit: Int): List<BundeslandDefinition> = DatabaseFactory.dbQuery {
val pattern = "%$searchTerm%"
val query = BundeslandTable.selectAll().where { BundeslandTable.name like pattern }
landId?.let {
query.andWhere { BundeslandTable.landId eq it }
}
query.limit(limit).map(::rowToBundeslandDefinition)
}
override suspend fun findAllActive(orderBySortierung: Boolean): List<BundeslandDefinition> = DatabaseFactory.dbQuery {
val query = BundeslandTable.selectAll().where { BundeslandTable.istAktiv eq true }
if (orderBySortierung) {
query.orderBy(BundeslandTable.sortierReihenfolge to SortOrder.ASC, BundeslandTable.name to SortOrder.ASC)
} else {
query.orderBy(BundeslandTable.name to SortOrder.ASC)
}
query.map(::rowToBundeslandDefinition)
}
override suspend fun save(bundesland: BundeslandDefinition): BundeslandDefinition = DatabaseFactory.dbQuery {
val now = Clock.System.now()
val existingBundesland = BundeslandTable.selectAll().where { BundeslandTable.id eq bundesland.bundeslandId }.singleOrNull()
if (existingBundesland == null) {
// Insert a new federal state
BundeslandTable.insert { stmt ->
stmt[id] = bundesland.bundeslandId
stmt[landId] = bundesland.landId
stmt[oepsCode] = bundesland.oepsCode
stmt[iso3166_2_Code] = bundesland.iso3166_2_Code
stmt[name] = bundesland.name
stmt[kuerzel] = bundesland.kuerzel
stmt[wappenUrl] = bundesland.wappenUrl
stmt[istAktiv] = bundesland.istAktiv
stmt[sortierReihenfolge] = bundesland.sortierReihenfolge
stmt[createdAt] = bundesland.createdAt.toLocalDateTime(TimeZone.UTC)
stmt[updatedAt] = now.toLocalDateTime(TimeZone.UTC)
}
} else {
// Update existing federal state
BundeslandTable.update({ BundeslandTable.id eq bundesland.bundeslandId }) { stmt ->
stmt[landId] = bundesland.landId
stmt[oepsCode] = bundesland.oepsCode
stmt[iso3166_2_Code] = bundesland.iso3166_2_Code
stmt[name] = bundesland.name
stmt[kuerzel] = bundesland.kuerzel
stmt[wappenUrl] = bundesland.wappenUrl
stmt[istAktiv] = bundesland.istAktiv
stmt[sortierReihenfolge] = bundesland.sortierReihenfolge
stmt[updatedAt] = now.toLocalDateTime(TimeZone.UTC)
}
}
bundesland.copy(updatedAt = now)
}
override suspend fun delete(id: Uuid): Boolean = DatabaseFactory.dbQuery {
BundeslandTable.deleteWhere { BundeslandTable.id eq id } > 0
}
override suspend fun existsByOepsCode(oepsCode: String, landId: Uuid): Boolean = DatabaseFactory.dbQuery {
BundeslandTable.selectAll().where {
(BundeslandTable.oepsCode eq oepsCode) and (BundeslandTable.landId eq landId)
}.count() > 0
}
override suspend fun existsByIso3166_2_Code(iso3166_2_Code: String): Boolean = DatabaseFactory.dbQuery {
BundeslandTable.selectAll().where { BundeslandTable.iso3166_2_Code eq iso3166_2_Code }
.count() > 0
}
override suspend fun countActiveByCountry(landId: Uuid): Long = DatabaseFactory.dbQuery {
BundeslandTable.selectAll().where {
(BundeslandTable.landId eq landId) and (BundeslandTable.istAktiv eq true)
}.count()
}
}
@@ -0,0 +1,34 @@
package at.mocode.masterdata.infrastructure.persistence
import org.jetbrains.exposed.sql.Table
import org.jetbrains.exposed.sql.kotlin.datetime.datetime
import org.jetbrains.exposed.sql.kotlin.datetime.CurrentDateTime
/**
* Exposed-Tabellendefinition für die Bundesland-Entität (Bundesländer/Regionen).
*
* Diese Tabelle speichert alle Informationen zu Bundesländern und subnationalen
* Verwaltungseinheiten entsprechend der BundeslandDefinition Domain-Entität.
*/
object BundeslandTable : Table("bundesland") {
val id = uuid("id").autoGenerate()
val landId = uuid("land_id").references(LandTable.id)
val oepsCode = varchar("oeps_code", 10).nullable()
val iso3166_2_Code = varchar("iso_3166_2_code", 10).nullable()
val name = varchar("name", 100)
val kuerzel = varchar("kuerzel", 10).nullable()
val wappenUrl = varchar("wappen_url", 500).nullable()
val istAktiv = bool("ist_aktiv").default(true)
val sortierReihenfolge = integer("sortier_reihenfolge").nullable()
val createdAt = datetime("created_at").defaultExpression(CurrentDateTime)
val updatedAt = datetime("updated_at").defaultExpression(CurrentDateTime)
override val primaryKey = PrimaryKey(id)
init {
// Unique constraint for OEPS code per country
uniqueIndex("uk_bundesland_oeps_land", oepsCode, landId)
// Unique constraint for ISO 3166-2 code globally
uniqueIndex("uk_bundesland_iso3166_2", iso3166_2_Code)
}
}
@@ -0,0 +1,154 @@
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
package at.mocode.masterdata.infrastructure.persistence
import at.mocode.core.utils.database.DatabaseFactory
import at.mocode.masterdata.domain.model.LandDefinition
import at.mocode.masterdata.domain.repository.LandRepository
import kotlin.uuid.Uuid
import kotlinx.datetime.Clock
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toInstant
import kotlinx.datetime.toLocalDateTime
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
/**
* Implementierung des LandRepository für die Datenbankzugriffe.
*
* Diese Implementierung verwendet Exposed SQL für den Datenbankzugriff
* und mappt zwischen der LandDefinition Domain-Entität und der LandTable.
*/
class LandRepositoryImpl : LandRepository {
/**
* Konvertiert eine Datenbankzeile in ein Domain-Objekt.
*/
private fun rowToLandDefinition(row: ResultRow): LandDefinition {
return LandDefinition(
landId = row[LandTable.id],
isoAlpha2Code = row[LandTable.isoAlpha2Code],
isoAlpha3Code = row[LandTable.isoAlpha3Code],
isoNumerischerCode = row[LandTable.isoNumerischerCode],
nameDeutsch = row[LandTable.nameDeutsch],
nameEnglisch = row[LandTable.nameEnglisch],
wappenUrl = row[LandTable.wappenUrl],
istEuMitglied = row[LandTable.istEuMitglied],
istEwrMitglied = row[LandTable.istEwrMitglied],
istAktiv = row[LandTable.istAktiv],
sortierReihenfolge = row[LandTable.sortierReihenfolge],
createdAt = row[LandTable.createdAt].toInstant(TimeZone.UTC),
updatedAt = row[LandTable.updatedAt].toInstant(TimeZone.UTC)
)
}
override suspend fun findById(id: Uuid): LandDefinition? = DatabaseFactory.dbQuery {
LandTable.selectAll().where { LandTable.id eq id }
.map(::rowToLandDefinition)
.singleOrNull()
}
override suspend fun findByIsoAlpha2Code(isoAlpha2Code: String): LandDefinition? = DatabaseFactory.dbQuery {
LandTable.selectAll().where { LandTable.isoAlpha2Code eq isoAlpha2Code }
.map(::rowToLandDefinition)
.singleOrNull()
}
override suspend fun findByIsoAlpha3Code(isoAlpha3Code: String): LandDefinition? = DatabaseFactory.dbQuery {
LandTable.selectAll().where { LandTable.isoAlpha3Code eq isoAlpha3Code }
.map(::rowToLandDefinition)
.singleOrNull()
}
override suspend fun findByName(searchTerm: String, limit: Int): List<LandDefinition> = DatabaseFactory.dbQuery {
val pattern = "%$searchTerm%"
LandTable.selectAll().where {
(LandTable.nameDeutsch like pattern) or
(LandTable.nameEnglisch like pattern)
}
.limit(limit)
.map(::rowToLandDefinition)
}
override suspend fun findAllActive(orderBySortierung: Boolean): List<LandDefinition> = DatabaseFactory.dbQuery {
val query = LandTable.selectAll().where { LandTable.istAktiv eq true }
if (orderBySortierung) {
query.orderBy(LandTable.sortierReihenfolge to SortOrder.ASC, LandTable.nameDeutsch to SortOrder.ASC)
} else {
query.orderBy(LandTable.nameDeutsch to SortOrder.ASC)
}
query.map(::rowToLandDefinition)
}
override suspend fun findEuMembers(): List<LandDefinition> = DatabaseFactory.dbQuery {
LandTable.selectAll().where { (LandTable.istEuMitglied eq true) and (LandTable.istAktiv eq true) }
.orderBy(LandTable.sortierReihenfolge to SortOrder.ASC, LandTable.nameDeutsch to SortOrder.ASC)
.map(::rowToLandDefinition)
}
override suspend fun findEwrMembers(): List<LandDefinition> = DatabaseFactory.dbQuery {
LandTable.selectAll().where { (LandTable.istEwrMitglied eq true) and (LandTable.istAktiv eq true) }
.orderBy(LandTable.sortierReihenfolge to SortOrder.ASC, LandTable.nameDeutsch to SortOrder.ASC)
.map(::rowToLandDefinition)
}
override suspend fun save(land: LandDefinition): LandDefinition = DatabaseFactory.dbQuery {
val now = Clock.System.now()
val existingLand = LandTable.selectAll().where { LandTable.id eq land.landId }.singleOrNull()
if (existingLand == null) {
// Insert a new country
LandTable.insert { stmt ->
stmt[id] = land.landId
stmt[isoAlpha2Code] = land.isoAlpha2Code
stmt[isoAlpha3Code] = land.isoAlpha3Code
stmt[isoNumerischerCode] = land.isoNumerischerCode
stmt[nameDeutsch] = land.nameDeutsch
stmt[nameEnglisch] = land.nameEnglisch
stmt[wappenUrl] = land.wappenUrl
stmt[istEuMitglied] = land.istEuMitglied
stmt[istEwrMitglied] = land.istEwrMitglied
stmt[istAktiv] = land.istAktiv
stmt[sortierReihenfolge] = land.sortierReihenfolge
stmt[createdAt] = land.createdAt.toLocalDateTime(TimeZone.UTC)
stmt[updatedAt] = now.toLocalDateTime(TimeZone.UTC)
}
} else {
// Update existing country
LandTable.update({ LandTable.id eq land.landId }) { stmt ->
stmt[isoAlpha2Code] = land.isoAlpha2Code
stmt[isoAlpha3Code] = land.isoAlpha3Code
stmt[isoNumerischerCode] = land.isoNumerischerCode
stmt[nameDeutsch] = land.nameDeutsch
stmt[nameEnglisch] = land.nameEnglisch
stmt[wappenUrl] = land.wappenUrl
stmt[istEuMitglied] = land.istEuMitglied
stmt[istEwrMitglied] = land.istEwrMitglied
stmt[istAktiv] = land.istAktiv
stmt[sortierReihenfolge] = land.sortierReihenfolge
stmt[updatedAt] = now.toLocalDateTime(TimeZone.UTC)
}
}
land.copy(updatedAt = now)
}
override suspend fun delete(id: Uuid): Boolean = DatabaseFactory.dbQuery {
LandTable.deleteWhere { LandTable.id eq id } > 0
}
override suspend fun existsByIsoAlpha2Code(isoAlpha2Code: String): Boolean = DatabaseFactory.dbQuery {
LandTable.selectAll().where { LandTable.isoAlpha2Code eq isoAlpha2Code }
.count() > 0
}
override suspend fun existsByIsoAlpha3Code(isoAlpha3Code: String): Boolean = DatabaseFactory.dbQuery {
LandTable.selectAll().where { LandTable.isoAlpha3Code eq isoAlpha3Code }
.count() > 0
}
override suspend fun countActive(): Long = DatabaseFactory.dbQuery {
LandTable.selectAll().where { LandTable.istAktiv eq true }.count()
}
}
@@ -0,0 +1,29 @@
package at.mocode.masterdata.infrastructure.persistence
import org.jetbrains.exposed.sql.Table
import org.jetbrains.exposed.sql.kotlin.datetime.datetime
import org.jetbrains.exposed.sql.kotlin.datetime.CurrentDateTime
/**
* Exposed-Tabellendefinition für die Land-Entität (Länderstammdaten).
*
* Diese Tabelle speichert alle Informationen zu Ländern/Nationen entsprechend
* der LandDefinition Domain-Entität.
*/
object LandTable : Table("land") {
val id = uuid("id").autoGenerate()
val isoAlpha2Code = varchar("iso_alpha2_code", 2).uniqueIndex()
val isoAlpha3Code = varchar("iso_alpha3_code", 3).uniqueIndex()
val isoNumerischerCode = varchar("iso_numerischer_code", 3).nullable()
val nameDeutsch = varchar("name_deutsch", 100)
val nameEnglisch = varchar("name_englisch", 100).nullable()
val wappenUrl = varchar("wappen_url", 500).nullable()
val istEuMitglied = bool("ist_eu_mitglied").nullable()
val istEwrMitglied = bool("ist_ewr_mitglied").nullable()
val istAktiv = bool("ist_aktiv").default(true)
val sortierReihenfolge = integer("sortier_reihenfolge").nullable()
val createdAt = datetime("created_at").defaultExpression(CurrentDateTime)
val updatedAt = datetime("updated_at").defaultExpression(CurrentDateTime)
override val primaryKey = PrimaryKey(id)
}
@@ -0,0 +1,231 @@
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
package at.mocode.masterdata.infrastructure.persistence
import at.mocode.core.domain.model.PlatzTypE
import at.mocode.masterdata.domain.model.Platz
import at.mocode.masterdata.domain.repository.PlatzRepository
import at.mocode.core.utils.database.DatabaseFactory
import kotlin.uuid.Uuid
import kotlinx.datetime.Clock
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toInstant
import kotlinx.datetime.toLocalDateTime
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
/**
* Implementierung des PlatzRepository für die Datenbankzugriffe.
*
* Diese Implementierung verwendet Exposed SQL für den Datenbankzugriff
* und mappt zwischen der Platz Domain-Entität und der PlatzTable.
*/
class PlatzRepositoryImpl : PlatzRepository {
/**
* Konvertiert eine Datenbankzeile in ein Domain-Objekt.
*/
private fun rowToPlatz(row: ResultRow): Platz {
return Platz(
id = row[PlatzTable.id],
turnierId = row[PlatzTable.turnierId],
name = row[PlatzTable.name],
dimension = row[PlatzTable.dimension],
boden = row[PlatzTable.boden],
typ = PlatzTypE.valueOf(row[PlatzTable.typ]),
istAktiv = row[PlatzTable.istAktiv],
sortierReihenfolge = row[PlatzTable.sortierReihenfolge],
createdAt = row[PlatzTable.createdAt].toInstant(TimeZone.UTC),
updatedAt = row[PlatzTable.updatedAt].toInstant(TimeZone.UTC)
)
}
override suspend fun findById(id: Uuid): Platz? = DatabaseFactory.dbQuery {
PlatzTable.selectAll().where { PlatzTable.id eq id }
.map(::rowToPlatz)
.singleOrNull()
}
override suspend fun findByTournament(turnierId: Uuid, activeOnly: Boolean, orderBySortierung: Boolean): List<Platz> = DatabaseFactory.dbQuery {
val query = PlatzTable.selectAll().where { PlatzTable.turnierId eq turnierId }
if (activeOnly) {
query.andWhere { PlatzTable.istAktiv eq true }
}
if (orderBySortierung) {
query.orderBy(PlatzTable.sortierReihenfolge to SortOrder.ASC, PlatzTable.name to SortOrder.ASC)
} else {
query.orderBy(PlatzTable.name to SortOrder.ASC)
}
query.map(::rowToPlatz)
}
override suspend fun findByName(searchTerm: String, turnierId: Uuid?, limit: Int): List<Platz> = DatabaseFactory.dbQuery {
val pattern = "%$searchTerm%"
val query = PlatzTable.selectAll().where { PlatzTable.name like pattern }
turnierId?.let {
query.andWhere { PlatzTable.turnierId eq it }
}
query.limit(limit)
.orderBy(PlatzTable.name to SortOrder.ASC)
.map(::rowToPlatz)
}
override suspend fun findByType(typ: PlatzTypE, turnierId: Uuid?, activeOnly: Boolean): List<Platz> = DatabaseFactory.dbQuery {
val query = PlatzTable.selectAll().where { PlatzTable.typ eq typ.name }
turnierId?.let {
query.andWhere { PlatzTable.turnierId eq it }
}
if (activeOnly) {
query.andWhere { PlatzTable.istAktiv eq true }
}
query.orderBy(PlatzTable.name to SortOrder.ASC)
.map(::rowToPlatz)
}
override suspend fun findByGroundType(boden: String, turnierId: Uuid?, activeOnly: Boolean): List<Platz> = DatabaseFactory.dbQuery {
val query = PlatzTable.selectAll().where { PlatzTable.boden eq boden }
turnierId?.let {
query.andWhere { PlatzTable.turnierId eq it }
}
if (activeOnly) {
query.andWhere { PlatzTable.istAktiv eq true }
}
query.orderBy(PlatzTable.name to SortOrder.ASC)
.map(::rowToPlatz)
}
override suspend fun findByDimensions(dimension: String, turnierId: Uuid?, activeOnly: Boolean): List<Platz> = DatabaseFactory.dbQuery {
val query = PlatzTable.selectAll().where { PlatzTable.dimension eq dimension }
turnierId?.let {
query.andWhere { PlatzTable.turnierId eq it }
}
if (activeOnly) {
query.andWhere { PlatzTable.istAktiv eq true }
}
query.orderBy(PlatzTable.name to SortOrder.ASC)
.map(::rowToPlatz)
}
override suspend fun findAllActive(orderBySortierung: Boolean): List<Platz> = DatabaseFactory.dbQuery {
val query = PlatzTable.selectAll().where { PlatzTable.istAktiv eq true }
if (orderBySortierung) {
query.orderBy(PlatzTable.sortierReihenfolge to SortOrder.ASC, PlatzTable.name to SortOrder.ASC)
} else {
query.orderBy(PlatzTable.name to SortOrder.ASC)
}
query.map(::rowToPlatz)
}
override suspend fun findSuitableForDiscipline(
requiredType: PlatzTypE,
requiredDimensions: String?,
turnierId: Uuid?
): List<Platz> = DatabaseFactory.dbQuery {
val query = PlatzTable.selectAll().where {
(PlatzTable.typ eq requiredType.name) and (PlatzTable.istAktiv eq true)
}
requiredDimensions?.let { dimensions ->
query.andWhere { PlatzTable.dimension eq dimensions }
}
turnierId?.let {
query.andWhere { PlatzTable.turnierId eq it }
}
query.orderBy(PlatzTable.sortierReihenfolge to SortOrder.ASC, PlatzTable.name to SortOrder.ASC)
.map(::rowToPlatz)
}
override suspend fun save(platz: Platz): Platz = DatabaseFactory.dbQuery {
val now = Clock.System.now()
val existingPlatz = PlatzTable.selectAll().where { PlatzTable.id eq platz.id }.singleOrNull()
if (existingPlatz == null) {
// Insert a new venue
PlatzTable.insert { stmt ->
stmt[id] = platz.id
stmt[turnierId] = platz.turnierId
stmt[name] = platz.name
stmt[dimension] = platz.dimension
stmt[boden] = platz.boden
stmt[typ] = platz.typ.name
stmt[istAktiv] = platz.istAktiv
stmt[sortierReihenfolge] = platz.sortierReihenfolge
stmt[createdAt] = platz.createdAt.toLocalDateTime(TimeZone.UTC)
stmt[updatedAt] = now.toLocalDateTime(TimeZone.UTC)
}
} else {
// Update existing venue
PlatzTable.update({ PlatzTable.id eq platz.id }) { stmt ->
stmt[turnierId] = platz.turnierId
stmt[name] = platz.name
stmt[dimension] = platz.dimension
stmt[boden] = platz.boden
stmt[typ] = platz.typ.name
stmt[istAktiv] = platz.istAktiv
stmt[sortierReihenfolge] = platz.sortierReihenfolge
stmt[updatedAt] = now.toLocalDateTime(TimeZone.UTC)
}
}
platz.copy(updatedAt = now)
}
override suspend fun delete(id: Uuid): Boolean = DatabaseFactory.dbQuery {
PlatzTable.deleteWhere { PlatzTable.id eq id } > 0
}
override suspend fun existsByNameAndTournament(name: String, turnierId: Uuid): Boolean = DatabaseFactory.dbQuery {
PlatzTable.selectAll().where {
(PlatzTable.name eq name) and (PlatzTable.turnierId eq turnierId)
}.count() > 0
}
override suspend fun countActiveByTournament(turnierId: Uuid): Long = DatabaseFactory.dbQuery {
PlatzTable.selectAll().where {
(PlatzTable.turnierId eq turnierId) and (PlatzTable.istAktiv eq true)
}.count()
}
override suspend fun countByTypeAndTournament(typ: PlatzTypE, turnierId: Uuid, activeOnly: Boolean): Long = DatabaseFactory.dbQuery {
val query = PlatzTable.selectAll().where {
(PlatzTable.typ eq typ.name) and (PlatzTable.turnierId eq turnierId)
}
if (activeOnly) {
query.andWhere { PlatzTable.istAktiv eq true }
}
query.count()
}
override suspend fun findAvailableForTimeSlot(turnierId: Uuid, startTime: String?, endTime: String?): List<Platz> = DatabaseFactory.dbQuery {
// For now, this returns all active venues for the tournament
// This can be extended when venue scheduling functionality is implemented
val query = PlatzTable.selectAll().where {
(PlatzTable.turnierId eq turnierId) and (PlatzTable.istAktiv eq true)
}
// TODO: Add time slot availability logic when scheduling is implemented
// This would involve joining with a scheduling/booking table to check availability
query.orderBy(PlatzTable.sortierReihenfolge to SortOrder.ASC, PlatzTable.name to SortOrder.ASC)
.map(::rowToPlatz)
}
}
@@ -0,0 +1,37 @@
package at.mocode.masterdata.infrastructure.persistence
import org.jetbrains.exposed.sql.Table
import org.jetbrains.exposed.sql.kotlin.datetime.datetime
import org.jetbrains.exposed.sql.kotlin.datetime.CurrentDateTime
/**
* Exposed-Tabellendefinition für die Platz-Entität (Turnierplätze/Wettkampfstätten).
*
* Diese Tabelle speichert alle Informationen zu Plätzen und Arenen
* entsprechend der Platz Domain-Entität.
*/
object PlatzTable : Table("platz") {
val id = uuid("id").autoGenerate()
val turnierId = uuid("turnier_id") // Foreign key to tournament (not enforced here as tournament might be in different module)
val name = varchar("name", 200)
val dimension = varchar("dimension", 50).nullable()
val boden = varchar("boden", 100).nullable()
val typ = varchar("typ", 50) // Enum as string
val istAktiv = bool("ist_aktiv").default(true)
val sortierReihenfolge = integer("sortier_reihenfolge").nullable()
val createdAt = datetime("created_at").defaultExpression(CurrentDateTime)
val updatedAt = datetime("updated_at").defaultExpression(CurrentDateTime)
override val primaryKey = PrimaryKey(id)
init {
// Index for performance on common queries
index(customIndexName = "idx_platz_turnier", columns = arrayOf(turnierId))
index(customIndexName = "idx_platz_aktiv", columns = arrayOf(istAktiv))
index(customIndexName = "idx_platz_typ", columns = arrayOf(typ))
index(customIndexName = "idx_platz_turnier_aktiv", columns = arrayOf(turnierId, istAktiv))
// Unique constraint for name per tournament
uniqueIndex("uk_platz_name_turnier", name, turnierId)
}
}