docs: Migrationsplan für Projekt-Restrukturierung hinzugefügt

- Detaillierter Plan zur Migration von alter zu neuer Modulstruktur
- Umfasst Überführung von shared-kernel zu core-Modulen
- Definiert Migration von Fachdomänen zu bounded contexts:
  * master-data → masterdata-Module
  * member-management → members-Module
  * horse-registry → horses-Module
  * event-management → events-Module
- Beschreibt Verlagerung von api-gateway zu infrastructure/gateway
- Strukturiert nach Domain-driven Design Prinzipien
- Berücksichtigt Clean Architecture Layering (domain, application, infrastructure, api)
This commit is contained in:
stefan
2025-07-25 13:05:42 +02:00
parent a4c7d53aa3
commit 65a0084f91
68 changed files with 13107 additions and 101 deletions
@@ -0,0 +1,239 @@
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 com.benasher44.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,157 @@
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 com.benasher44.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)
}
}
@@ -14,6 +14,9 @@ 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 {
@@ -25,14 +28,16 @@ class LandRepositoryImpl : LandRepository {
landId = row[LandTable.id],
isoAlpha2Code = row[LandTable.isoAlpha2Code],
isoAlpha3Code = row[LandTable.isoAlpha3Code],
nameDeutsch = row[LandTable.nameDe],
nameEnglisch = row[LandTable.nameEn],
isoNumerischerCode = row[LandTable.isoNumerischerCode],
nameDeutsch = row[LandTable.nameDeutsch],
nameEnglisch = row[LandTable.nameEnglisch],
wappenUrl = row[LandTable.wappenUrl],
istEuMitglied = row[LandTable.istEuMitglied],
istEwrMitglied = row[LandTable.istEwrMitglied],
sortierReihenfolge = row[LandTable.sortierReihenfolge],
istAktiv = row[LandTable.istAktiv],
createdAt = row[LandTable.erstelltAm].toInstant(TimeZone.UTC),
updatedAt = row[LandTable.geaendertAm].toInstant(TimeZone.UTC)
sortierReihenfolge = row[LandTable.sortierReihenfolge],
createdAt = row[LandTable.createdAt].toInstant(TimeZone.UTC),
updatedAt = row[LandTable.updatedAt].toInstant(TimeZone.UTC)
)
}
@@ -56,7 +61,10 @@ class LandRepositoryImpl : LandRepository {
override suspend fun findByName(searchTerm: String, limit: Int): List<LandDefinition> = DatabaseFactory.dbQuery {
val pattern = "%$searchTerm%"
LandTable.selectAll().where { (LandTable.nameDe like pattern) or (LandTable.nameEn like pattern) }
LandTable.selectAll().where {
(LandTable.nameDeutsch like pattern) or
(LandTable.nameEnglisch like pattern)
}
.limit(limit)
.map(::rowToLandDefinition)
}
@@ -65,9 +73,9 @@ class LandRepositoryImpl : LandRepository {
val query = LandTable.selectAll().where { LandTable.istAktiv eq true }
if (orderBySortierung) {
query.orderBy(LandTable.sortierReihenfolge to SortOrder.ASC, LandTable.nameDe to SortOrder.ASC)
query.orderBy(LandTable.sortierReihenfolge to SortOrder.ASC, LandTable.nameDeutsch to SortOrder.ASC)
} else {
query.orderBy(LandTable.nameDe to SortOrder.ASC)
query.orderBy(LandTable.nameDeutsch to SortOrder.ASC)
}
query.map(::rowToLandDefinition)
@@ -75,13 +83,13 @@ class LandRepositoryImpl : LandRepository {
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.nameDe to SortOrder.ASC)
.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.nameDe to SortOrder.ASC)
.orderBy(LandTable.sortierReihenfolge to SortOrder.ASC, LandTable.nameDeutsch to SortOrder.ASC)
.map(::rowToLandDefinition)
}
@@ -95,27 +103,31 @@ class LandRepositoryImpl : LandRepository {
stmt[id] = land.landId
stmt[isoAlpha2Code] = land.isoAlpha2Code
stmt[isoAlpha3Code] = land.isoAlpha3Code
stmt[nameDe] = land.nameDeutsch
stmt[nameEn] = land.nameEnglisch ?: ""
stmt[istEuMitglied] = land.istEuMitglied ?: false
stmt[istEwrMitglied] = land.istEwrMitglied ?: false
stmt[sortierReihenfolge] = land.sortierReihenfolge ?: 999
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[erstelltAm] = land.createdAt.toLocalDateTime(TimeZone.UTC)
stmt[geaendertAm] = now.toLocalDateTime(TimeZone.UTC)
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[nameDe] = land.nameDeutsch
stmt[nameEn] = land.nameEnglisch ?: ""
stmt[istEuMitglied] = land.istEuMitglied ?: false
stmt[istEwrMitglied] = land.istEwrMitglied ?: false
stmt[sortierReihenfolge] = land.sortierReihenfolge ?: 999
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[geaendertAm] = now.toLocalDateTime(TimeZone.UTC)
stmt[sortierReihenfolge] = land.sortierReihenfolge
stmt[updatedAt] = now.toLocalDateTime(TimeZone.UTC)
}
}
@@ -6,19 +6,24 @@ 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 nameDe = varchar("name_de", 100)
val nameEn = varchar("name_en", 100)
val istEuMitglied = bool("ist_eu_mitglied").default(false)
val istEwrMitglied = bool("ist_ewr_mitglied").default(false)
val sortierReihenfolge = integer("sortier_reihenfolge").default(999)
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 erstelltAm = datetime("erstellt_am").defaultExpression(CurrentDateTime)
val geaendertAm = datetime("geaendert_am").defaultExpression(CurrentDateTime)
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,230 @@
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 com.benasher44.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)
}
}