Compare commits
4 Commits
3da6643724
...
6ef1fd4ca6
| Author | SHA1 | Date | |
|---|---|---|---|
| 6ef1fd4ca6 | |||
| e30304937f | |||
| c806660685 | |||
| 2cb3f0b125 |
@@ -7,10 +7,7 @@ dependencies {
|
|||||||
implementation(projects.core.coreUtils)
|
implementation(projects.core.coreUtils)
|
||||||
implementation(projects.core.coreDomain)
|
implementation(projects.core.coreDomain)
|
||||||
implementation(projects.core.znsParser)
|
implementation(projects.core.znsParser)
|
||||||
implementation(projects.backend.services.clubs.clubsDomain)
|
implementation(projects.backend.services.masterdata.masterdataDomain)
|
||||||
implementation(projects.backend.services.persons.personsDomain)
|
|
||||||
implementation(projects.backend.services.horses.horsesDomain)
|
|
||||||
implementation(projects.backend.services.officials.officialsDomain)
|
|
||||||
|
|
||||||
testImplementation(projects.platform.platformTesting)
|
testImplementation(projects.platform.platformTesting)
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-4
@@ -2,10 +2,10 @@
|
|||||||
|
|
||||||
package at.mocode.zns.importer
|
package at.mocode.zns.importer
|
||||||
|
|
||||||
import at.mocode.clubs.domain.repository.VereinRepository
|
import at.mocode.masterdata.domain.repository.VereinRepository
|
||||||
import at.mocode.horses.domain.repository.HorseRepository
|
import at.mocode.masterdata.domain.repository.HorseRepository
|
||||||
import at.mocode.officials.domain.repository.FunktionaerRepository
|
import at.mocode.masterdata.domain.repository.FunktionaerRepository
|
||||||
import at.mocode.persons.domain.repository.ReiterRepository
|
import at.mocode.masterdata.domain.repository.ReiterRepository
|
||||||
import at.mocode.zns.parser.ZnsLegacyParsers
|
import at.mocode.zns.parser.ZnsLegacyParsers
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.nio.charset.Charset
|
import java.nio.charset.Charset
|
||||||
|
|||||||
+17
-17
@@ -1,13 +1,13 @@
|
|||||||
package at.mocode.zns.importer
|
package at.mocode.zns.importer
|
||||||
|
|
||||||
import at.mocode.clubs.domain.model.DomVerein
|
import at.mocode.masterdata.domain.model.DomFunktionaer
|
||||||
import at.mocode.clubs.domain.repository.VereinRepository
|
import at.mocode.masterdata.domain.model.DomPferd
|
||||||
import at.mocode.horses.domain.model.DomPferd
|
import at.mocode.masterdata.domain.model.DomReiter
|
||||||
import at.mocode.horses.domain.repository.HorseRepository
|
import at.mocode.masterdata.domain.model.DomVerein
|
||||||
import at.mocode.officials.domain.model.DomFunktionaer
|
import at.mocode.masterdata.domain.repository.FunktionaerRepository
|
||||||
import at.mocode.officials.domain.repository.FunktionaerRepository
|
import at.mocode.masterdata.domain.repository.HorseRepository
|
||||||
import at.mocode.persons.domain.model.DomReiter
|
import at.mocode.masterdata.domain.repository.ReiterRepository
|
||||||
import at.mocode.persons.domain.repository.ReiterRepository
|
import at.mocode.masterdata.domain.repository.VereinRepository
|
||||||
import io.mockk.coEvery
|
import io.mockk.coEvery
|
||||||
import io.mockk.coVerify
|
import io.mockk.coVerify
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
@@ -110,7 +110,7 @@ class ZnsImportServiceTest {
|
|||||||
val zip = buildZip("VEREIN01.DAT" to vereinZeile())
|
val zip = buildZip("VEREIN01.DAT" to vereinZeile())
|
||||||
|
|
||||||
coEvery { vereinRepository.findByVereinsNummer(any()) } returns null
|
coEvery { vereinRepository.findByVereinsNummer(any()) } returns null
|
||||||
coEvery { vereinRepository.save(any()) } answers { firstArg() }
|
coEvery { vereinRepository.save(any()) } answers { firstArg<DomVerein>() }
|
||||||
|
|
||||||
val result = service.importiereZip(zip)
|
val result = service.importiereZip(zip)
|
||||||
|
|
||||||
@@ -126,7 +126,7 @@ class ZnsImportServiceTest {
|
|||||||
val vorhanden = DomVerein(vereinsNummer = "0001", name = "Alter Name")
|
val vorhanden = DomVerein(vereinsNummer = "0001", name = "Alter Name")
|
||||||
|
|
||||||
coEvery { vereinRepository.findByVereinsNummer("0001") } returns vorhanden
|
coEvery { vereinRepository.findByVereinsNummer("0001") } returns vorhanden
|
||||||
coEvery { vereinRepository.save(any()) } answers { firstArg() }
|
coEvery { vereinRepository.save(any()) } answers { firstArg<DomVerein>() }
|
||||||
|
|
||||||
val result = service.importiereZip(zip)
|
val result = service.importiereZip(zip)
|
||||||
|
|
||||||
@@ -140,7 +140,7 @@ class ZnsImportServiceTest {
|
|||||||
val zip = buildZip("LIZENZ01.DAT" to lizenzZeile())
|
val zip = buildZip("LIZENZ01.DAT" to lizenzZeile())
|
||||||
|
|
||||||
coEvery { reiterRepository.findBySatznummer(any()) } returns null
|
coEvery { reiterRepository.findBySatznummer(any()) } returns null
|
||||||
coEvery { reiterRepository.save(any()) } answers { firstArg() }
|
coEvery { reiterRepository.save(any()) } answers { firstArg<DomReiter>() }
|
||||||
|
|
||||||
val result = service.importiereZip(zip)
|
val result = service.importiereZip(zip)
|
||||||
|
|
||||||
@@ -155,7 +155,7 @@ class ZnsImportServiceTest {
|
|||||||
val zip = buildZip("PFERDE01.DAT" to pferdeZeile())
|
val zip = buildZip("PFERDE01.DAT" to pferdeZeile())
|
||||||
|
|
||||||
coEvery { horseRepository.findByLebensnummer(any()) } returns null
|
coEvery { horseRepository.findByLebensnummer(any()) } returns null
|
||||||
coEvery { horseRepository.save(any()) } answers { firstArg() }
|
coEvery { horseRepository.save(any()) } answers { firstArg<DomPferd>() }
|
||||||
|
|
||||||
val result = service.importiereZip(zip)
|
val result = service.importiereZip(zip)
|
||||||
|
|
||||||
@@ -170,7 +170,7 @@ class ZnsImportServiceTest {
|
|||||||
val zip = buildZip("RICHT01.DAT" to richterZeile())
|
val zip = buildZip("RICHT01.DAT" to richterZeile())
|
||||||
|
|
||||||
coEvery { funktionaerRepository.findByRichterNummer(any()) } returns null
|
coEvery { funktionaerRepository.findByRichterNummer(any()) } returns null
|
||||||
coEvery { funktionaerRepository.save(any()) } answers { firstArg() }
|
coEvery { funktionaerRepository.save(any()) } answers { firstArg<DomFunktionaer>() }
|
||||||
|
|
||||||
val result = service.importiereZip(zip)
|
val result = service.importiereZip(zip)
|
||||||
|
|
||||||
@@ -190,13 +190,13 @@ class ZnsImportServiceTest {
|
|||||||
)
|
)
|
||||||
|
|
||||||
coEvery { vereinRepository.findByVereinsNummer(any()) } returns null
|
coEvery { vereinRepository.findByVereinsNummer(any()) } returns null
|
||||||
coEvery { vereinRepository.save(any()) } answers { firstArg() }
|
coEvery { vereinRepository.save(any()) } answers { firstArg<DomVerein>() }
|
||||||
coEvery { reiterRepository.findBySatznummer(any()) } returns null
|
coEvery { reiterRepository.findBySatznummer(any()) } returns null
|
||||||
coEvery { reiterRepository.save(any()) } answers { firstArg() }
|
coEvery { reiterRepository.save(any()) } answers { firstArg<DomReiter>() }
|
||||||
coEvery { horseRepository.findByLebensnummer(any()) } returns null
|
coEvery { horseRepository.findByLebensnummer(any()) } returns null
|
||||||
coEvery { horseRepository.save(any()) } answers { firstArg() }
|
coEvery { horseRepository.save(any()) } answers { firstArg<DomPferd>() }
|
||||||
coEvery { funktionaerRepository.findByRichterNummer(any()) } returns null
|
coEvery { funktionaerRepository.findByRichterNummer(any()) } returns null
|
||||||
coEvery { funktionaerRepository.save(any()) } answers { firstArg() }
|
coEvery { funktionaerRepository.save(any()) } answers { firstArg<DomFunktionaer>() }
|
||||||
|
|
||||||
val result = service.importiereZip(zip)
|
val result = service.importiereZip(zip)
|
||||||
|
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
plugins {
|
|
||||||
alias(libs.plugins.kotlinMultiplatform)
|
|
||||||
alias(libs.plugins.kotlinSerialization)
|
|
||||||
}
|
|
||||||
|
|
||||||
kotlin {
|
|
||||||
jvm()
|
|
||||||
|
|
||||||
sourceSets {
|
|
||||||
commonMain {
|
|
||||||
kotlin.srcDir("src/main/kotlin")
|
|
||||||
dependencies {
|
|
||||||
implementation(projects.core.coreDomain)
|
|
||||||
implementation(projects.core.coreUtils)
|
|
||||||
implementation(libs.kotlinx.datetime)
|
|
||||||
implementation(libs.kotlinx.serialization.json)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
commonTest {
|
|
||||||
kotlin.srcDir("src/test/kotlin")
|
|
||||||
dependencies {
|
|
||||||
implementation(projects.platform.platformTesting)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-38
@@ -1,38 +0,0 @@
|
|||||||
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
|
||||||
|
|
||||||
package at.mocode.clubs.domain.model
|
|
||||||
|
|
||||||
import at.mocode.core.domain.model.DatenQuelleE
|
|
||||||
import at.mocode.core.domain.serialization.KotlinInstantSerializer
|
|
||||||
import at.mocode.core.domain.serialization.UuidSerializer
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import kotlin.time.Clock
|
|
||||||
import kotlin.time.Instant
|
|
||||||
import kotlin.uuid.Uuid
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Domain model representing a club (Verein) in the registry system.
|
|
||||||
*
|
|
||||||
* @property clubId Unique internal identifier (UUID).
|
|
||||||
* @property vereinsNummer ÖPS club number (from ZNS VEREIN01.dat).
|
|
||||||
* @property name Club name.
|
|
||||||
* @property datenQuelle Source of the data.
|
|
||||||
* @property createdAt Timestamp when this record was created.
|
|
||||||
* @property updatedAt Timestamp when this record was last updated.
|
|
||||||
*/
|
|
||||||
@Serializable
|
|
||||||
data class DomClub(
|
|
||||||
@Serializable(with = UuidSerializer::class)
|
|
||||||
val clubId: Uuid = Uuid.random(),
|
|
||||||
|
|
||||||
val vereinsNummer: String,
|
|
||||||
val name: String,
|
|
||||||
|
|
||||||
val istAktiv: Boolean = true,
|
|
||||||
val datenQuelle: DatenQuelleE = DatenQuelleE.IMPORT_ZNS,
|
|
||||||
|
|
||||||
@Serializable(with = KotlinInstantSerializer::class)
|
|
||||||
val createdAt: Instant = Clock.System.now(),
|
|
||||||
@Serializable(with = KotlinInstantSerializer::class)
|
|
||||||
var updatedAt: Instant = Clock.System.now()
|
|
||||||
)
|
|
||||||
-141
@@ -1,141 +0,0 @@
|
|||||||
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
|
||||||
|
|
||||||
package at.mocode.clubs.infrastructure.persistence
|
|
||||||
|
|
||||||
import at.mocode.clubs.domain.model.DomVerein
|
|
||||||
import at.mocode.clubs.domain.repository.VereinRepository
|
|
||||||
import at.mocode.core.domain.model.DatenQuelleE
|
|
||||||
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.like
|
|
||||||
import org.jetbrains.exposed.v1.core.statements.UpdateBuilder
|
|
||||||
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.transactions.transaction
|
|
||||||
import org.jetbrains.exposed.v1.jdbc.update
|
|
||||||
import kotlin.time.Clock
|
|
||||||
import kotlin.uuid.Uuid
|
|
||||||
import kotlin.uuid.toJavaUuid
|
|
||||||
import kotlin.uuid.toKotlinUuid
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Exposed-basierte Implementierung des VereinRepository.
|
|
||||||
*/
|
|
||||||
class ExposedVereinRepository : VereinRepository {
|
|
||||||
|
|
||||||
override suspend fun findById(id: Uuid): DomVerein? = transaction {
|
|
||||||
VereinTable.selectAll().where { VereinTable.id eq id.toJavaUuid() }
|
|
||||||
.map { rowToVerein(it) }
|
|
||||||
.singleOrNull()
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun findByVereinsNummer(vereinsNummer: String): DomVerein? = transaction {
|
|
||||||
VereinTable.selectAll().where { VereinTable.vereinsNummer eq vereinsNummer }
|
|
||||||
.map { rowToVerein(it) }
|
|
||||||
.singleOrNull()
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun findByName(searchTerm: String, limit: Int): List<DomVerein> = transaction {
|
|
||||||
VereinTable.selectAll().where { VereinTable.name like "%$searchTerm%" }
|
|
||||||
.limit(limit).map { rowToVerein(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun findByBundesland(bundesland: String, activeOnly: Boolean): List<DomVerein> = transaction {
|
|
||||||
VereinTable.selectAll().where {
|
|
||||||
(VereinTable.bundesland eq bundesland).let {
|
|
||||||
if (activeOnly) it and (VereinTable.istAktiv eq true) else it
|
|
||||||
}
|
|
||||||
}.map { rowToVerein(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun findVeranstalter(activeOnly: Boolean): List<DomVerein> = transaction {
|
|
||||||
VereinTable.selectAll().where {
|
|
||||||
(VereinTable.istVeranstalter eq true).let {
|
|
||||||
if (activeOnly) it and (VereinTable.istAktiv eq true) else it
|
|
||||||
}
|
|
||||||
}.map { rowToVerein(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun findAllActive(limit: Int, offset: Int): List<DomVerein> = transaction {
|
|
||||||
VereinTable.selectAll().where { VereinTable.istAktiv eq true }
|
|
||||||
.limit(limit).offset(offset.toLong())
|
|
||||||
.map { rowToVerein(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun findAll(limit: Int, offset: Int): List<DomVerein> = transaction {
|
|
||||||
VereinTable.selectAll()
|
|
||||||
.limit(limit).offset(offset.toLong())
|
|
||||||
.map { rowToVerein(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun save(verein: DomVerein): DomVerein = transaction {
|
|
||||||
val now = Clock.System.now()
|
|
||||||
val updated = verein.copy(updatedAt = now)
|
|
||||||
val javaId = verein.vereinId.toJavaUuid()
|
|
||||||
val existing = VereinTable.selectAll().where { VereinTable.id eq javaId }.singleOrNull()
|
|
||||||
if (existing != null) {
|
|
||||||
VereinTable.update({ VereinTable.id eq javaId }) { vereinToStatement(it, updated) }
|
|
||||||
} else {
|
|
||||||
VereinTable.insert {
|
|
||||||
it[id] = javaId
|
|
||||||
vereinToStatement(it, updated)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
updated
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun delete(id: Uuid): Boolean = transaction {
|
|
||||||
VereinTable.deleteWhere { VereinTable.id eq id.toJavaUuid() } > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun countActive(): Long = transaction {
|
|
||||||
VereinTable.selectAll().where { VereinTable.istAktiv eq true }.count()
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun existsByVereinsNummer(vereinsNummer: String): Boolean = transaction {
|
|
||||||
VereinTable.selectAll().where { VereinTable.vereinsNummer eq vereinsNummer }.count() > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun rowToVerein(row: ResultRow): DomVerein = DomVerein(
|
|
||||||
vereinId = row[VereinTable.id].toKotlinUuid(),
|
|
||||||
vereinsNummer = row[VereinTable.vereinsNummer],
|
|
||||||
name = row[VereinTable.name],
|
|
||||||
kurzname = row[VereinTable.kurzname],
|
|
||||||
bundesland = row[VereinTable.bundesland],
|
|
||||||
ort = row[VereinTable.ort],
|
|
||||||
plz = row[VereinTable.plz],
|
|
||||||
strasse = row[VereinTable.strasse],
|
|
||||||
email = row[VereinTable.email],
|
|
||||||
telefon = row[VereinTable.telefon],
|
|
||||||
website = row[VereinTable.webseite],
|
|
||||||
oepsRegionNummer = row[VereinTable.oepsRegionsNummer],
|
|
||||||
istVeranstalter = row[VereinTable.istVeranstalter],
|
|
||||||
istAktiv = row[VereinTable.istAktiv],
|
|
||||||
bemerkungen = row[VereinTable.bemerkungen],
|
|
||||||
datenQuelle = runCatching { DatenQuelleE.valueOf(row[VereinTable.datenQuelle]) }.getOrDefault(DatenQuelleE.IMPORT_ZNS),
|
|
||||||
createdAt = row[VereinTable.createdAt],
|
|
||||||
updatedAt = row[VereinTable.updatedAt]
|
|
||||||
)
|
|
||||||
|
|
||||||
private fun vereinToStatement(stmt: UpdateBuilder<*>, v: DomVerein) {
|
|
||||||
stmt[VereinTable.vereinsNummer] = v.vereinsNummer
|
|
||||||
stmt[VereinTable.name] = v.name
|
|
||||||
stmt[VereinTable.kurzname] = v.kurzname
|
|
||||||
stmt[VereinTable.bundesland] = v.bundesland
|
|
||||||
stmt[VereinTable.ort] = v.ort
|
|
||||||
stmt[VereinTable.plz] = v.plz
|
|
||||||
stmt[VereinTable.strasse] = v.strasse
|
|
||||||
stmt[VereinTable.email] = v.email
|
|
||||||
stmt[VereinTable.telefon] = v.telefon
|
|
||||||
stmt[VereinTable.webseite] = v.website
|
|
||||||
stmt[VereinTable.oepsRegionsNummer] = v.oepsRegionNummer
|
|
||||||
stmt[VereinTable.istVeranstalter] = v.istVeranstalter
|
|
||||||
stmt[VereinTable.istAktiv] = v.istAktiv
|
|
||||||
stmt[VereinTable.bemerkungen] = v.bemerkungen
|
|
||||||
stmt[VereinTable.datenQuelle] = v.datenQuelle.name
|
|
||||||
stmt[VereinTable.createdAt] = v.createdAt
|
|
||||||
stmt[VereinTable.updatedAt] = v.updatedAt
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-56
@@ -1,56 +0,0 @@
|
|||||||
package at.mocode.clubs.infrastructure.persistence
|
|
||||||
|
|
||||||
import org.jetbrains.exposed.v1.core.Table
|
|
||||||
import org.jetbrains.exposed.v1.core.java.javaUUID
|
|
||||||
import org.jetbrains.exposed.v1.datetime.timestamp
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Exposed-Tabellendefinition für Vereine (DomVerein).
|
|
||||||
*
|
|
||||||
* Speichert alle Vereins-Daten inkl. Adresse, Kontakt und OEPS-Regionsnummer.
|
|
||||||
*/
|
|
||||||
object VereinTable : Table("vereine") {
|
|
||||||
|
|
||||||
val id = javaUUID("id").autoGenerate()
|
|
||||||
override val primaryKey = PrimaryKey(id)
|
|
||||||
|
|
||||||
// Identifikation
|
|
||||||
val vereinsNummer = varchar("vereins_nummer", 20).uniqueIndex()
|
|
||||||
|
|
||||||
// Vereinsdaten
|
|
||||||
val name = varchar("name", 200)
|
|
||||||
val kurzname = varchar("kurzname", 50).nullable()
|
|
||||||
|
|
||||||
// Adresse
|
|
||||||
val strasse = varchar("strasse", 200).nullable()
|
|
||||||
val plz = varchar("plz", 10).nullable()
|
|
||||||
val ort = varchar("ort", 100).nullable()
|
|
||||||
val bundesland = varchar("bundesland", 50).nullable()
|
|
||||||
val land = varchar("land", 50).nullable().default("AT")
|
|
||||||
|
|
||||||
// Kontakt
|
|
||||||
val email = varchar("email", 255).nullable()
|
|
||||||
val telefon = varchar("telefon", 50).nullable()
|
|
||||||
val webseite = varchar("webseite", 255).nullable()
|
|
||||||
|
|
||||||
// OEPS-Daten
|
|
||||||
val oepsRegionsNummer = varchar("oeps_regions_nummer", 10).nullable()
|
|
||||||
|
|
||||||
// Status & Verwaltung
|
|
||||||
val istAktiv = bool("ist_aktiv").default(true)
|
|
||||||
val istVeranstalter = bool("ist_veranstalter").default(false)
|
|
||||||
val bemerkungen = text("bemerkungen").nullable()
|
|
||||||
val datenQuelle = varchar("daten_quelle", 50)
|
|
||||||
|
|
||||||
// Audit-Felder
|
|
||||||
val createdAt = timestamp("created_at")
|
|
||||||
val updatedAt = timestamp("updated_at")
|
|
||||||
|
|
||||||
init {
|
|
||||||
index(false, name)
|
|
||||||
index(false, bundesland)
|
|
||||||
index(false, istAktiv)
|
|
||||||
index(false, istVeranstalter)
|
|
||||||
index(false, oepsRegionsNummer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-14
@@ -1,14 +0,0 @@
|
|||||||
package at.mocode.clubs.infrastructure.persistence
|
|
||||||
|
|
||||||
import org.jetbrains.exposed.v1.core.dao.id.java.UUIDTable
|
|
||||||
import org.jetbrains.exposed.v1.datetime.timestamp
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tabelle für importierte Vereine aus dem ZNS-Datenbestand (VEREIN01.dat).
|
|
||||||
* Wird ausschließlich als Dev-Seed-Daten verwendet.
|
|
||||||
*/
|
|
||||||
object ZnsClubTable : UUIDTable("zns_clubs") {
|
|
||||||
val vereinsNummer = varchar("vereins_nummer", 4).uniqueIndex()
|
|
||||||
val name = varchar("name", 50)
|
|
||||||
val createdAt = timestamp("created_at")
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
plugins {
|
|
||||||
alias(libs.plugins.kotlinJvm)
|
|
||||||
alias(libs.plugins.kotlinSpring)
|
|
||||||
alias(libs.plugins.spring.boot)
|
|
||||||
alias(libs.plugins.spring.dependencyManagement)
|
|
||||||
}
|
|
||||||
|
|
||||||
springBoot {
|
|
||||||
mainClass.set("at.mocode.clubs.service.ClubsServiceApplicationKt")
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
implementation(projects.platform.platformDependencies)
|
|
||||||
implementation(projects.core.coreDomain)
|
|
||||||
implementation(projects.core.coreUtils)
|
|
||||||
implementation(projects.backend.services.clubs.clubsDomain)
|
|
||||||
implementation(projects.backend.services.clubs.clubsInfrastructure)
|
|
||||||
|
|
||||||
implementation(libs.spring.boot.starter.web)
|
|
||||||
implementation(libs.spring.boot.starter.validation)
|
|
||||||
implementation(libs.spring.boot.starter.actuator)
|
|
||||||
|
|
||||||
implementation(libs.exposed.core)
|
|
||||||
implementation(libs.exposed.dao)
|
|
||||||
implementation(libs.exposed.jdbc)
|
|
||||||
implementation(libs.exposed.migration.jdbc)
|
|
||||||
implementation(libs.exposed.kotlin.datetime)
|
|
||||||
implementation(libs.hikari.cp)
|
|
||||||
runtimeOnly(libs.postgresql.driver)
|
|
||||||
testRuntimeOnly(libs.h2.driver)
|
|
||||||
|
|
||||||
testImplementation(projects.platform.platformTesting)
|
|
||||||
testImplementation(libs.spring.boot.starter.test)
|
|
||||||
testImplementation(libs.logback.classic)
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.test {
|
|
||||||
useJUnitPlatform()
|
|
||||||
}
|
|
||||||
-18
@@ -1,18 +0,0 @@
|
|||||||
package at.mocode.clubs.service
|
|
||||||
|
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication
|
|
||||||
import org.springframework.boot.runApplication
|
|
||||||
import org.springframework.context.annotation.ComponentScan
|
|
||||||
|
|
||||||
@SpringBootApplication
|
|
||||||
@ComponentScan(
|
|
||||||
basePackages = [
|
|
||||||
"at.mocode.clubs.service",
|
|
||||||
"at.mocode.clubs.infrastructure"
|
|
||||||
]
|
|
||||||
)
|
|
||||||
class ClubsServiceApplication
|
|
||||||
|
|
||||||
fun main(args: Array<String>) {
|
|
||||||
runApplication<ClubsServiceApplication>(*args)
|
|
||||||
}
|
|
||||||
-32
@@ -1,32 +0,0 @@
|
|||||||
package at.mocode.clubs.service.config
|
|
||||||
|
|
||||||
import at.mocode.clubs.infrastructure.persistence.ZnsClubTable
|
|
||||||
import jakarta.annotation.PostConstruct
|
|
||||||
import org.jetbrains.exposed.v1.jdbc.Database
|
|
||||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
|
||||||
import org.jetbrains.exposed.v1.migration.jdbc.MigrationUtils
|
|
||||||
import org.slf4j.LoggerFactory
|
|
||||||
import org.springframework.beans.factory.annotation.Value
|
|
||||||
import org.springframework.context.annotation.Configuration
|
|
||||||
import org.springframework.context.annotation.Profile
|
|
||||||
|
|
||||||
@Configuration
|
|
||||||
@Profile("dev")
|
|
||||||
class ClubsDatabaseConfiguration(
|
|
||||||
@Value("\${spring.datasource.url}") private val jdbcUrl: String,
|
|
||||||
@Value("\${spring.datasource.username}") private val username: String,
|
|
||||||
@Value("\${spring.datasource.password}") private val password: String
|
|
||||||
) {
|
|
||||||
private val log = LoggerFactory.getLogger(ClubsDatabaseConfiguration::class.java)
|
|
||||||
|
|
||||||
@PostConstruct
|
|
||||||
fun initializeDatabase() {
|
|
||||||
log.info("Initialisiere Datenbank-Schema für Clubs-Service...")
|
|
||||||
Database.connect(jdbcUrl, user = username, password = password)
|
|
||||||
transaction {
|
|
||||||
val statements = MigrationUtils.statementsRequiredForDatabaseMigration(ZnsClubTable)
|
|
||||||
statements.forEach { exec(it) }
|
|
||||||
log.info("Datenbank-Schema erfolgreich initialisiert")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-81
@@ -1,81 +0,0 @@
|
|||||||
package at.mocode.clubs.service.dev
|
|
||||||
|
|
||||||
import at.mocode.clubs.infrastructure.persistence.ZnsClubTable
|
|
||||||
import org.jetbrains.exposed.v1.jdbc.batchInsert
|
|
||||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
|
||||||
import org.jetbrains.exposed.v1.migration.jdbc.MigrationUtils
|
|
||||||
import org.slf4j.LoggerFactory
|
|
||||||
import org.springframework.beans.factory.annotation.Value
|
|
||||||
import org.springframework.boot.CommandLineRunner
|
|
||||||
import org.springframework.context.annotation.Profile
|
|
||||||
import org.springframework.stereotype.Component
|
|
||||||
import java.io.File
|
|
||||||
import kotlin.time.Clock
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dev-only Seeder: Importiert VEREIN01.dat (ZNS) → zns_clubs.
|
|
||||||
* Aktivierung: Umgebungsvariable ZNS_DATA_DIR setzen + Profil "dev".
|
|
||||||
*/
|
|
||||||
@Component
|
|
||||||
@Profile("dev")
|
|
||||||
class ZnsClubSeeder(
|
|
||||||
@Value("\${zns.data.dir:#{null}}") private val znsDataDir: String?
|
|
||||||
) : CommandLineRunner {
|
|
||||||
|
|
||||||
private val log = LoggerFactory.getLogger(ZnsClubSeeder::class.java)
|
|
||||||
|
|
||||||
override fun run(vararg args: String) {
|
|
||||||
if (znsDataDir == null) {
|
|
||||||
log.info("ZNS_DATA_DIR nicht gesetzt – ZnsClubSeeder wird übersprungen.")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
val dir = File(znsDataDir)
|
|
||||||
if (!dir.exists() || !dir.isDirectory) {
|
|
||||||
log.warn("ZNS_DATA_DIR '{}' existiert nicht oder ist kein Verzeichnis.", znsDataDir)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
log.info("Starte ZNS-Vereine-Import aus: {}", dir.absolutePath)
|
|
||||||
transaction {
|
|
||||||
val statements = MigrationUtils.statementsRequiredForDatabaseMigration(ZnsClubTable)
|
|
||||||
statements.forEach { exec(it) }
|
|
||||||
}
|
|
||||||
seedClubs(dir)
|
|
||||||
log.info("ZNS-Vereine-Import abgeschlossen.")
|
|
||||||
}
|
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
// VEREIN01.dat → zns_clubs
|
|
||||||
// Format:
|
|
||||||
// [0-3] VereinsNr (4 Zeichen)
|
|
||||||
// [4-53] Name (50 Zeichen)
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
private fun seedClubs(dir: File) {
|
|
||||||
val file = File(dir, "VEREIN01.dat")
|
|
||||||
if (!file.exists()) {
|
|
||||||
log.warn("VEREIN01.dat nicht gefunden – Vereine werden übersprungen.")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
data class ClubRow(val nr: String, val name: String)
|
|
||||||
|
|
||||||
val rows = file.readLines(Charsets.ISO_8859_1)
|
|
||||||
.filter { it.length >= 4 }
|
|
||||||
.map { line ->
|
|
||||||
ClubRow(
|
|
||||||
nr = line.substring(0, 4).trim(),
|
|
||||||
name = line.substring(4).trim().take(50)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.filter { it.nr.isNotBlank() }
|
|
||||||
|
|
||||||
val now = Clock.System.now()
|
|
||||||
transaction {
|
|
||||||
ZnsClubTable.batchInsert(rows, ignore = true) { row ->
|
|
||||||
this[ZnsClubTable.vereinsNummer] = row.nr
|
|
||||||
this[ZnsClubTable.name] = row.name
|
|
||||||
this[ZnsClubTable.createdAt] = now
|
|
||||||
}
|
|
||||||
}
|
|
||||||
log.info("Vereine importiert: {} Datensätze", rows.size)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-95
@@ -1,95 +0,0 @@
|
|||||||
-- Migration V001: Create Vereine (Clubs) table
|
|
||||||
-- Speichert alle Vereins-Daten inkl. Adresse, Kontakt und OEPS-Regionsnummer.
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS vereine
|
|
||||||
(
|
|
||||||
id
|
|
||||||
UUID
|
|
||||||
PRIMARY
|
|
||||||
KEY
|
|
||||||
DEFAULT
|
|
||||||
gen_random_uuid
|
|
||||||
(
|
|
||||||
),
|
|
||||||
vereins_nummer VARCHAR
|
|
||||||
(
|
|
||||||
20
|
|
||||||
) NOT NULL,
|
|
||||||
name VARCHAR
|
|
||||||
(
|
|
||||||
200
|
|
||||||
) NOT NULL,
|
|
||||||
kurzname VARCHAR
|
|
||||||
(
|
|
||||||
50
|
|
||||||
),
|
|
||||||
strasse VARCHAR
|
|
||||||
(
|
|
||||||
200
|
|
||||||
),
|
|
||||||
plz VARCHAR
|
|
||||||
(
|
|
||||||
10
|
|
||||||
),
|
|
||||||
ort VARCHAR
|
|
||||||
(
|
|
||||||
100
|
|
||||||
),
|
|
||||||
bundesland VARCHAR
|
|
||||||
(
|
|
||||||
50
|
|
||||||
),
|
|
||||||
land VARCHAR
|
|
||||||
(
|
|
||||||
50
|
|
||||||
) DEFAULT 'AT',
|
|
||||||
email VARCHAR
|
|
||||||
(
|
|
||||||
255
|
|
||||||
),
|
|
||||||
telefon VARCHAR
|
|
||||||
(
|
|
||||||
50
|
|
||||||
),
|
|
||||||
webseite VARCHAR
|
|
||||||
(
|
|
||||||
255
|
|
||||||
),
|
|
||||||
oeps_regions_nummer VARCHAR
|
|
||||||
(
|
|
||||||
10
|
|
||||||
),
|
|
||||||
ist_aktiv BOOLEAN NOT NULL DEFAULT true,
|
|
||||||
ist_veranstalter BOOLEAN NOT NULL DEFAULT false,
|
|
||||||
bemerkungen TEXT,
|
|
||||||
daten_quelle VARCHAR
|
|
||||||
(
|
|
||||||
50
|
|
||||||
) NOT NULL DEFAULT 'MANUELL',
|
|
||||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Unique index für Vereinsnummer
|
|
||||||
CREATE UNIQUE INDEX IF NOT EXISTS uk_vereine_vereins_nummer ON vereine(vereins_nummer);
|
|
||||||
|
|
||||||
-- Performance-Indizes
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_vereine_name ON vereine(name);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_vereine_bundesland ON vereine(bundesland);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_vereine_ist_aktiv ON vereine(ist_aktiv);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_vereine_ist_veranstalter ON vereine(ist_veranstalter);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_vereine_oeps_region ON vereine(oeps_regions_nummer);
|
|
||||||
|
|
||||||
-- Dokumentation
|
|
||||||
COMMENT
|
|
||||||
ON TABLE vereine IS 'Reitsportvereine gemäß OEPS-Vereinsregister';
|
|
||||||
COMMENT
|
|
||||||
ON COLUMN vereine.id IS 'Eindeutige interne ID (UUID)';
|
|
||||||
COMMENT
|
|
||||||
ON COLUMN vereine.vereins_nummer IS 'Offizielle OEPS-Vereinsnummer (eindeutig)';
|
|
||||||
COMMENT
|
|
||||||
ON COLUMN vereine.oeps_regions_nummer IS 'OEPS-Regionsnummer des Landesverbands';
|
|
||||||
COMMENT
|
|
||||||
ON COLUMN vereine.ist_veranstalter IS 'Gibt an ob der Verein als Veranstalter von Turnieren zugelassen ist';
|
|
||||||
COMMENT
|
|
||||||
ON COLUMN vereine.daten_quelle IS 'Datenherkunft: MANUELL, IMPORT_ZNS, IMPORT_FEI';
|
|
||||||
+3
-3
@@ -3,7 +3,7 @@
|
|||||||
package at.mocode.entries.domain.model
|
package at.mocode.entries.domain.model
|
||||||
|
|
||||||
import at.mocode.core.domain.model.AbteilungsTeilungsTypE
|
import at.mocode.core.domain.model.AbteilungsTeilungsTypE
|
||||||
import at.mocode.core.domain.serialization.KotlinInstantSerializer
|
import at.mocode.core.domain.serialization.InstantSerializer
|
||||||
import at.mocode.core.domain.serialization.UuidSerializer
|
import at.mocode.core.domain.serialization.UuidSerializer
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlin.time.Clock
|
import kotlin.time.Clock
|
||||||
@@ -59,9 +59,9 @@ data class DomAbteilung(
|
|||||||
var bemerkungen: String? = null,
|
var bemerkungen: String? = null,
|
||||||
|
|
||||||
// Audit
|
// Audit
|
||||||
@Serializable(with = KotlinInstantSerializer::class)
|
@Serializable(with = InstantSerializer::class)
|
||||||
val createdAt: Instant = Clock.System.now(),
|
val createdAt: Instant = Clock.System.now(),
|
||||||
@Serializable(with = KotlinInstantSerializer::class)
|
@Serializable(with = InstantSerializer::class)
|
||||||
var updatedAt: Instant = Clock.System.now()
|
var updatedAt: Instant = Clock.System.now()
|
||||||
) {
|
) {
|
||||||
/**
|
/**
|
||||||
|
|||||||
+3
-3
@@ -6,7 +6,7 @@ import at.mocode.core.domain.model.AbteilungsTeilungsTypE
|
|||||||
import at.mocode.core.domain.model.PruefungsTypE
|
import at.mocode.core.domain.model.PruefungsTypE
|
||||||
import at.mocode.core.domain.model.SparteE
|
import at.mocode.core.domain.model.SparteE
|
||||||
import at.mocode.core.domain.model.TurnierkategorieE
|
import at.mocode.core.domain.model.TurnierkategorieE
|
||||||
import at.mocode.core.domain.serialization.KotlinInstantSerializer
|
import at.mocode.core.domain.serialization.InstantSerializer
|
||||||
import at.mocode.core.domain.serialization.UuidSerializer
|
import at.mocode.core.domain.serialization.UuidSerializer
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlin.time.Clock
|
import kotlin.time.Clock
|
||||||
@@ -69,9 +69,9 @@ data class DomBewerb(
|
|||||||
var bemerkungen: String? = null,
|
var bemerkungen: String? = null,
|
||||||
|
|
||||||
// Audit
|
// Audit
|
||||||
@Serializable(with = KotlinInstantSerializer::class)
|
@Serializable(with = InstantSerializer::class)
|
||||||
val createdAt: Instant = Clock.System.now(),
|
val createdAt: Instant = Clock.System.now(),
|
||||||
@Serializable(with = KotlinInstantSerializer::class)
|
@Serializable(with = InstantSerializer::class)
|
||||||
var updatedAt: Instant = Clock.System.now()
|
var updatedAt: Instant = Clock.System.now()
|
||||||
) {
|
) {
|
||||||
/**
|
/**
|
||||||
|
|||||||
+3
-3
@@ -4,7 +4,7 @@ package at.mocode.entries.domain.model
|
|||||||
|
|
||||||
import at.mocode.core.domain.model.NennungsStatusE
|
import at.mocode.core.domain.model.NennungsStatusE
|
||||||
import at.mocode.core.domain.model.StartwunschE
|
import at.mocode.core.domain.model.StartwunschE
|
||||||
import at.mocode.core.domain.serialization.KotlinInstantSerializer
|
import at.mocode.core.domain.serialization.InstantSerializer
|
||||||
import at.mocode.core.domain.serialization.UuidSerializer
|
import at.mocode.core.domain.serialization.UuidSerializer
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlin.time.Clock
|
import kotlin.time.Clock
|
||||||
@@ -74,9 +74,9 @@ data class DomNennung(
|
|||||||
val bemerkungen: String? = null,
|
val bemerkungen: String? = null,
|
||||||
|
|
||||||
// Audit Fields
|
// Audit Fields
|
||||||
@Serializable(with = KotlinInstantSerializer::class)
|
@Serializable(with = InstantSerializer::class)
|
||||||
val createdAt: Instant = Clock.System.now(),
|
val createdAt: Instant = Clock.System.now(),
|
||||||
@Serializable(with = KotlinInstantSerializer::class)
|
@Serializable(with = InstantSerializer::class)
|
||||||
var updatedAt: Instant = Clock.System.now()
|
var updatedAt: Instant = Clock.System.now()
|
||||||
) {
|
) {
|
||||||
/**
|
/**
|
||||||
|
|||||||
+2
-2
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
package at.mocode.entries.domain.model
|
package at.mocode.entries.domain.model
|
||||||
|
|
||||||
import at.mocode.core.domain.serialization.KotlinInstantSerializer
|
import at.mocode.core.domain.serialization.InstantSerializer
|
||||||
import at.mocode.core.domain.serialization.UuidSerializer
|
import at.mocode.core.domain.serialization.UuidSerializer
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlin.time.Clock
|
import kotlin.time.Clock
|
||||||
@@ -72,7 +72,7 @@ data class DomNennungsTransfer(
|
|||||||
val grund: String? = null,
|
val grund: String? = null,
|
||||||
|
|
||||||
// Audit Fields
|
// Audit Fields
|
||||||
@Serializable(with = KotlinInstantSerializer::class)
|
@Serializable(with = InstantSerializer::class)
|
||||||
val createdAt: Instant = Clock.System.now()
|
val createdAt: Instant = Clock.System.now()
|
||||||
) {
|
) {
|
||||||
/**
|
/**
|
||||||
|
|||||||
+5
-5
@@ -3,7 +3,7 @@
|
|||||||
package at.mocode.entries.domain.model
|
package at.mocode.entries.domain.model
|
||||||
|
|
||||||
import at.mocode.core.domain.model.StartlistenStatusE
|
import at.mocode.core.domain.model.StartlistenStatusE
|
||||||
import at.mocode.core.domain.serialization.KotlinInstantSerializer
|
import at.mocode.core.domain.serialization.InstantSerializer
|
||||||
import at.mocode.core.domain.serialization.UuidSerializer
|
import at.mocode.core.domain.serialization.UuidSerializer
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlin.time.Clock
|
import kotlin.time.Clock
|
||||||
@@ -49,18 +49,18 @@ data class DomStartliste(
|
|||||||
var eintraege: List<StartlistenEintrag> = emptyList(),
|
var eintraege: List<StartlistenEintrag> = emptyList(),
|
||||||
|
|
||||||
// Zeitstempel Workflow
|
// Zeitstempel Workflow
|
||||||
@Serializable(with = KotlinInstantSerializer::class)
|
@Serializable(with = InstantSerializer::class)
|
||||||
var veroeffentlichtAt: Instant? = null,
|
var veroeffentlichtAt: Instant? = null,
|
||||||
@Serializable(with = KotlinInstantSerializer::class)
|
@Serializable(with = InstantSerializer::class)
|
||||||
var gesperrtAt: Instant? = null,
|
var gesperrtAt: Instant? = null,
|
||||||
|
|
||||||
// Verwaltung
|
// Verwaltung
|
||||||
var bemerkungen: String? = null,
|
var bemerkungen: String? = null,
|
||||||
|
|
||||||
// Audit
|
// Audit
|
||||||
@Serializable(with = KotlinInstantSerializer::class)
|
@Serializable(with = InstantSerializer::class)
|
||||||
val createdAt: Instant = Clock.System.now(),
|
val createdAt: Instant = Clock.System.now(),
|
||||||
@Serializable(with = KotlinInstantSerializer::class)
|
@Serializable(with = InstantSerializer::class)
|
||||||
var updatedAt: Instant = Clock.System.now()
|
var updatedAt: Instant = Clock.System.now()
|
||||||
) {
|
) {
|
||||||
/**
|
/**
|
||||||
|
|||||||
+3
-3
@@ -4,7 +4,7 @@ package at.mocode.events.domain.model
|
|||||||
|
|
||||||
import at.mocode.core.domain.model.AusschreibungsStatusE
|
import at.mocode.core.domain.model.AusschreibungsStatusE
|
||||||
import at.mocode.core.domain.model.SparteE
|
import at.mocode.core.domain.model.SparteE
|
||||||
import at.mocode.core.domain.serialization.KotlinInstantSerializer
|
import at.mocode.core.domain.serialization.KotlinxInstantSerializer
|
||||||
import at.mocode.core.domain.serialization.UuidSerializer
|
import at.mocode.core.domain.serialization.UuidSerializer
|
||||||
import kotlinx.datetime.LocalDate
|
import kotlinx.datetime.LocalDate
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
@@ -88,9 +88,9 @@ data class DomAusschreibung(
|
|||||||
var genehmigungsNummer: String? = null,
|
var genehmigungsNummer: String? = null,
|
||||||
|
|
||||||
// Audit
|
// Audit
|
||||||
@Serializable(with = KotlinInstantSerializer::class)
|
@Serializable(with = KotlinxInstantSerializer::class)
|
||||||
val createdAt: Instant = Clock.System.now(),
|
val createdAt: Instant = Clock.System.now(),
|
||||||
@Serializable(with = KotlinInstantSerializer::class)
|
@Serializable(with = KotlinxInstantSerializer::class)
|
||||||
var updatedAt: Instant = Clock.System.now()
|
var updatedAt: Instant = Clock.System.now()
|
||||||
) {
|
) {
|
||||||
/**
|
/**
|
||||||
|
|||||||
+3
-3
@@ -5,7 +5,7 @@ package at.mocode.events.domain.model
|
|||||||
import at.mocode.core.domain.model.SparteE
|
import at.mocode.core.domain.model.SparteE
|
||||||
import at.mocode.core.domain.model.TurnierkategorieE
|
import at.mocode.core.domain.model.TurnierkategorieE
|
||||||
import at.mocode.core.domain.model.TurnierStatusE
|
import at.mocode.core.domain.model.TurnierStatusE
|
||||||
import at.mocode.core.domain.serialization.KotlinInstantSerializer
|
import at.mocode.core.domain.serialization.KotlinxInstantSerializer
|
||||||
import at.mocode.core.domain.serialization.UuidSerializer
|
import at.mocode.core.domain.serialization.UuidSerializer
|
||||||
import kotlinx.datetime.LocalDate
|
import kotlinx.datetime.LocalDate
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
@@ -67,9 +67,9 @@ data class DomTurnier(
|
|||||||
var bemerkungen: String? = null,
|
var bemerkungen: String? = null,
|
||||||
|
|
||||||
// Audit
|
// Audit
|
||||||
@Serializable(with = KotlinInstantSerializer::class)
|
@Serializable(with = KotlinxInstantSerializer::class)
|
||||||
val createdAt: Instant = Clock.System.now(),
|
val createdAt: Instant = Clock.System.now(),
|
||||||
@Serializable(with = KotlinInstantSerializer::class)
|
@Serializable(with = KotlinxInstantSerializer::class)
|
||||||
var updatedAt: Instant = Clock.System.now()
|
var updatedAt: Instant = Clock.System.now()
|
||||||
) {
|
) {
|
||||||
/**
|
/**
|
||||||
|
|||||||
+3
-3
@@ -5,7 +5,7 @@ package at.mocode.events.domain.model
|
|||||||
import at.mocode.core.domain.model.SparteE
|
import at.mocode.core.domain.model.SparteE
|
||||||
import at.mocode.core.domain.model.VeranstaltungsStatusE
|
import at.mocode.core.domain.model.VeranstaltungsStatusE
|
||||||
import at.mocode.core.domain.model.VeranstaltungsTypE
|
import at.mocode.core.domain.model.VeranstaltungsTypE
|
||||||
import at.mocode.core.domain.serialization.KotlinInstantSerializer
|
import at.mocode.core.domain.serialization.KotlinxInstantSerializer
|
||||||
import at.mocode.core.domain.serialization.UuidSerializer
|
import at.mocode.core.domain.serialization.UuidSerializer
|
||||||
import kotlinx.datetime.LocalDate
|
import kotlinx.datetime.LocalDate
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
@@ -71,9 +71,9 @@ data class DomVeranstaltung(
|
|||||||
var bemerkungen: String? = null,
|
var bemerkungen: String? = null,
|
||||||
|
|
||||||
// Audit
|
// Audit
|
||||||
@Serializable(with = KotlinInstantSerializer::class)
|
@Serializable(with = KotlinxInstantSerializer::class)
|
||||||
val createdAt: Instant = Clock.System.now(),
|
val createdAt: Instant = Clock.System.now(),
|
||||||
@Serializable(with = KotlinInstantSerializer::class)
|
@Serializable(with = KotlinxInstantSerializer::class)
|
||||||
var updatedAt: Instant = Clock.System.now()
|
var updatedAt: Instant = Clock.System.now()
|
||||||
) {
|
) {
|
||||||
/**
|
/**
|
||||||
|
|||||||
+3
-3
@@ -2,7 +2,7 @@
|
|||||||
package at.mocode.events.domain.model
|
package at.mocode.events.domain.model
|
||||||
|
|
||||||
import at.mocode.core.domain.model.SparteE
|
import at.mocode.core.domain.model.SparteE
|
||||||
import at.mocode.core.domain.serialization.KotlinInstantSerializer
|
import at.mocode.core.domain.serialization.KotlinxInstantSerializer
|
||||||
import at.mocode.core.domain.serialization.KotlinLocalDateSerializer
|
import at.mocode.core.domain.serialization.KotlinLocalDateSerializer
|
||||||
import at.mocode.core.domain.serialization.UuidSerializer
|
import at.mocode.core.domain.serialization.UuidSerializer
|
||||||
import kotlin.uuid.Uuid
|
import kotlin.uuid.Uuid
|
||||||
@@ -63,9 +63,9 @@ data class Veranstaltung(
|
|||||||
var anmeldeschluss: LocalDate? = null,
|
var anmeldeschluss: LocalDate? = null,
|
||||||
|
|
||||||
// Audit Fields
|
// Audit Fields
|
||||||
@Serializable(with = KotlinInstantSerializer::class)
|
@Serializable(with = KotlinxInstantSerializer::class)
|
||||||
val createdAt: Instant = Clock.System.now(),
|
val createdAt: Instant = Clock.System.now(),
|
||||||
@Serializable(with = KotlinInstantSerializer::class)
|
@Serializable(with = KotlinxInstantSerializer::class)
|
||||||
var updatedAt: Instant = Clock.System.now()
|
var updatedAt: Instant = Clock.System.now()
|
||||||
) {
|
) {
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,167 +0,0 @@
|
|||||||
# syntax=docker/dockerfile:1.7
|
|
||||||
|
|
||||||
# ===================================================================
|
|
||||||
# Dockerfile for Horses Service
|
|
||||||
# Based on Spring Boot Service Template with Horses-specific configuration
|
|
||||||
# ===================================================================
|
|
||||||
|
|
||||||
# === CENTRALIZED BUILD ARGUMENTS ===
|
|
||||||
# Values sourced from docker/versions.toml and docker/build-args/
|
|
||||||
# Global arguments (docker/build-args/global.env)
|
|
||||||
ARG GRADLE_VERSION
|
|
||||||
ARG JAVA_VERSION
|
|
||||||
ARG BUILD_DATE
|
|
||||||
ARG VERSION
|
|
||||||
|
|
||||||
# Service-specific arguments (docker/build-args/services.env)
|
|
||||||
# Note: Keine Runtime-Profile/Ports als Build-ARGs
|
|
||||||
ARG SERVICE_PATH=horses/horses-service
|
|
||||||
ARG SERVICE_NAME=horses-service
|
|
||||||
|
|
||||||
# ===================================================================
|
|
||||||
# Build Stage
|
|
||||||
# ===================================================================
|
|
||||||
FROM gradle:${GRADLE_VERSION}-jdk${JAVA_VERSION}-alpine AS builder
|
|
||||||
|
|
||||||
# Re-declare build arguments for diesem Stage (nur Build-Zeit)
|
|
||||||
ARG SERVICE_PATH=horses/horses-service
|
|
||||||
ARG SERVICE_NAME=horses-service
|
|
||||||
|
|
||||||
LABEL stage=builder
|
|
||||||
LABEL maintainer="Meldestelle Development Team"
|
|
||||||
|
|
||||||
WORKDIR /workspace
|
|
||||||
|
|
||||||
# Gradle optimizations
|
|
||||||
ENV GRADLE_OPTS="-Dorg.gradle.caching=true \
|
|
||||||
-Dorg.gradle.daemon=false \
|
|
||||||
-Dorg.gradle.parallel=true \
|
|
||||||
-Dorg.gradle.configureondemand=true \
|
|
||||||
-Xmx2g"
|
|
||||||
|
|
||||||
# Copy build files in optimal order for caching
|
|
||||||
COPY gradlew gradlew.bat gradle.properties settings.gradle.kts ./
|
|
||||||
COPY gradle/ gradle/
|
|
||||||
|
|
||||||
# Make gradlew executable (required on Linux/Unix systems)
|
|
||||||
RUN chmod +x gradlew
|
|
||||||
|
|
||||||
COPY platform/ platform/
|
|
||||||
COPY core/ core/
|
|
||||||
COPY build.gradle.kts ./
|
|
||||||
|
|
||||||
# Copy horses service modules in dependency order
|
|
||||||
COPY horses/horses-domain/ horses/horses-domain/
|
|
||||||
COPY horses/horses-api/ horses/horses-api/
|
|
||||||
COPY horses/horses-application/ horses/horses-application/
|
|
||||||
COPY horses/horses-infrastructure/ horses/horses-infrastructure/
|
|
||||||
COPY horses/horses-service/ horses/horses-service/
|
|
||||||
|
|
||||||
# Build horses service (ohne Runtime-Profile bei Build)
|
|
||||||
RUN echo "Building Horses Service..." && \
|
|
||||||
./gradlew :horses:horses-service:dependencies --no-daemon --info && \
|
|
||||||
./gradlew :horses:horses-service:bootJar --no-daemon --info
|
|
||||||
|
|
||||||
# Extract JAR layers for optimized Docker layer caching
|
|
||||||
WORKDIR /builder
|
|
||||||
RUN cp /workspace/horses/horses-service/build/libs/*.jar app.jar && \
|
|
||||||
java -Djarmode=layertools -jar app.jar extract
|
|
||||||
|
|
||||||
# ===================================================================
|
|
||||||
# Runtime Stage
|
|
||||||
# ===================================================================
|
|
||||||
FROM eclipse-temurin:${JAVA_VERSION}-jre-alpine AS runtime
|
|
||||||
|
|
||||||
# Metadata
|
|
||||||
LABEL service="horses-service" \
|
|
||||||
version="1.0.0" \
|
|
||||||
description="Horses Management Service for Austrian Equestrian Federation" \
|
|
||||||
maintainer="Meldestelle Development Team" \
|
|
||||||
java.version="${JAVA_VERSION}"
|
|
||||||
|
|
||||||
# Build arguments
|
|
||||||
ARG APP_USER=horsesuser
|
|
||||||
ARG APP_GROUP=horsesgroup
|
|
||||||
ARG APP_UID=1005
|
|
||||||
ARG APP_GID=1005
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# System setup
|
|
||||||
RUN apk update && \
|
|
||||||
apk upgrade && \
|
|
||||||
apk add --no-cache curl jq tzdata && \
|
|
||||||
rm -rf /var/cache/apk/*
|
|
||||||
|
|
||||||
# Non-root user creation
|
|
||||||
RUN addgroup -g ${APP_GID} -S ${APP_GROUP} && \
|
|
||||||
adduser -u ${APP_UID} -S ${APP_USER} -G ${APP_GROUP} -h /app -s /bin/sh
|
|
||||||
|
|
||||||
# Directory setup
|
|
||||||
RUN mkdir -p /app/logs /app/tmp && \
|
|
||||||
chown -R ${APP_USER}:${APP_GROUP} /app
|
|
||||||
|
|
||||||
# Re-declare build arguments for runtime stage
|
|
||||||
ARG SERVICE_PATH=horses/horses-service
|
|
||||||
ARG SERVICE_NAME=horses-service
|
|
||||||
|
|
||||||
# Copy Spring Boot layers in optimal order for Docker layer caching
|
|
||||||
COPY --from=builder --chown=${APP_USER}:${APP_GROUP} /builder/dependencies/ ./
|
|
||||||
COPY --from=builder --chown=${APP_USER}:${APP_GROUP} /builder/spring-boot-loader/ ./
|
|
||||||
COPY --from=builder --chown=${APP_USER}:${APP_GROUP} /builder/snapshot-dependencies/ ./
|
|
||||||
COPY --from=builder --chown=${APP_USER}:${APP_GROUP} /builder/application/ ./
|
|
||||||
|
|
||||||
USER ${APP_USER}
|
|
||||||
|
|
||||||
# Expose application port and debug port
|
|
||||||
EXPOSE 8084 5005
|
|
||||||
|
|
||||||
# Health check
|
|
||||||
HEALTHCHECK --interval=15s --timeout=3s --start-period=40s --retries=3 \
|
|
||||||
CMD curl -fsS --max-time 2 http://localhost:8084/actuator/health/readiness || exit 1
|
|
||||||
|
|
||||||
# JVM configuration optimized for horses service
|
|
||||||
ENV JAVA_OPTS="-XX:MaxRAMPercentage=80.0 \
|
|
||||||
-XX:+UseG1GC \
|
|
||||||
-XX:+UseStringDeduplication \
|
|
||||||
-XX:+UseContainerSupport \
|
|
||||||
-XX:G1HeapRegionSize=16m \
|
|
||||||
-XX:+OptimizeStringConcat \
|
|
||||||
-XX:+UseCompressedOops \
|
|
||||||
-Djava.security.egd=file:/dev/./urandom \
|
|
||||||
-Djava.awt.headless=true \
|
|
||||||
-Dfile.encoding=UTF-8 \
|
|
||||||
-Duser.timezone=Europe/Vienna \
|
|
||||||
-Dmanagement.endpoints.web.exposure.include=health,info,metrics,prometheus"
|
|
||||||
|
|
||||||
# Spring Boot configuration (Profile nur zur Laufzeit via Compose/Env)
|
|
||||||
ENV SPRING_OUTPUT_ANSI_ENABLED=ALWAYS \
|
|
||||||
SERVER_PORT=8084 \
|
|
||||||
LOGGING_LEVEL_ROOT=INFO \
|
|
||||||
LOGGING_LEVEL_AT_MOCODE_HORSES=DEBUG
|
|
||||||
|
|
||||||
# Startup command with debug support
|
|
||||||
ENTRYPOINT ["sh", "-c", "\
|
|
||||||
echo 'Starting Horses Service on port 8084...'; \
|
|
||||||
if [ \"${DEBUG:-false}\" = \"true\" ]; then \
|
|
||||||
echo 'Debug mode enabled on port 5005'; \
|
|
||||||
exec java $JAVA_OPTS -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 org.springframework.boot.loader.launch.JarLauncher; \
|
|
||||||
else \
|
|
||||||
exec java $JAVA_OPTS org.springframework.boot.loader.launch.JarLauncher; \
|
|
||||||
fi"]
|
|
||||||
|
|
||||||
# ===================================================================
|
|
||||||
# Documentation
|
|
||||||
# ===================================================================
|
|
||||||
# Build commands:
|
|
||||||
# docker build -t meldestelle/horses-service:latest -f dockerfiles/services/horses-service/Dockerfile .
|
|
||||||
# docker run -p 8085:8085 --name horses-service meldestelle/horses-service:latest
|
|
||||||
#
|
|
||||||
# Key features:
|
|
||||||
# - Multi-stage build with JAR layer extraction for optimal caching
|
|
||||||
# - Non-root user execution for security (UID/GID 1005)
|
|
||||||
# - Optimized JVM settings for containers
|
|
||||||
# - Comprehensive health checks with horses-specific endpoint
|
|
||||||
# - Debug support on port 5005
|
|
||||||
# - Vienna timezone configuration for Austrian operations
|
|
||||||
# ===================================================================
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
plugins {
|
|
||||||
// KORREKTUR: Alle Plugins werden jetzt konsistent über den Version Catalog geladen.
|
|
||||||
alias(libs.plugins.kotlin.jvm)
|
|
||||||
alias(libs.plugins.kotlin.spring)
|
|
||||||
alias(libs.plugins.kotlin.serialization)
|
|
||||||
|
|
||||||
// Das Ktor-Plugin wird hier nicht benötigt, da Ktor als Bibliothek in Spring Boot läuft.
|
|
||||||
// Das 'application'-Plugin wird vom Spring Boot Plugin bereitgestellt.
|
|
||||||
alias(libs.plugins.spring.boot)
|
|
||||||
alias(libs.plugins.spring.dependencyManagement)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Der springBoot-Block konfiguriert die Anwendung, wenn sie als JAR-Datei ausgeführt wird.
|
|
||||||
springBoot {
|
|
||||||
mainClass.set("at.mocode.horses.api.ApplicationKt")
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
// Interne Module
|
|
||||||
implementation(projects.platform.platformDependencies)
|
|
||||||
implementation(projects.backend.services.horses.horsesDomain)
|
|
||||||
implementation(projects.backend.services.horses.horsesApplication)
|
|
||||||
implementation(projects.core.coreDomain)
|
|
||||||
implementation(projects.core.coreUtils)
|
|
||||||
|
|
||||||
// KORREKTUR: Alle externen Abhängigkeiten werden jetzt über den Version Catalog bezogen.
|
|
||||||
|
|
||||||
// Spring dependencies
|
|
||||||
implementation(libs.spring.web)
|
|
||||||
|
|
||||||
// Ktor Server (als embedded Server in Spring)
|
|
||||||
implementation(libs.ktor.server.core)
|
|
||||||
implementation(libs.ktor.server.netty)
|
|
||||||
implementation(libs.ktor.server.contentNegotiation)
|
|
||||||
implementation(libs.ktor.server.serialization.kotlinx.json)
|
|
||||||
implementation(libs.ktor.server.statusPages)
|
|
||||||
implementation(libs.ktor.server.auth)
|
|
||||||
implementation(libs.ktor.server.authJwt)
|
|
||||||
|
|
||||||
// Testing
|
|
||||||
testImplementation(projects.platform.platformTesting)
|
|
||||||
testImplementation(libs.ktor.server.tests)
|
|
||||||
testImplementation(libs.spring.boot.starter.test)
|
|
||||||
}
|
|
||||||
-438
@@ -1,438 +0,0 @@
|
|||||||
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
|
||||||
package at.mocode.horses.api.rest
|
|
||||||
|
|
||||||
import at.mocode.core.domain.model.ApiResponse
|
|
||||||
import at.mocode.core.domain.model.PferdeGeschlechtE
|
|
||||||
import at.mocode.horses.application.usecase.CreateHorseUseCase
|
|
||||||
import at.mocode.horses.application.usecase.DeleteHorseUseCase
|
|
||||||
import at.mocode.horses.application.usecase.GetHorseUseCase
|
|
||||||
import at.mocode.horses.application.usecase.UpdateHorseUseCase
|
|
||||||
import at.mocode.horses.domain.repository.HorseRepository
|
|
||||||
import at.mocode.core.utils.validation.ApiValidationUtils
|
|
||||||
import kotlin.uuid.Uuid
|
|
||||||
import io.ktor.http.*
|
|
||||||
import io.ktor.server.request.*
|
|
||||||
import io.ktor.server.response.*
|
|
||||||
import io.ktor.server.routing.*
|
|
||||||
import kotlinx.serialization.Contextual
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
|
|
||||||
/**
|
|
||||||
* REST API controller for horse registry operations.
|
|
||||||
*
|
|
||||||
* This controller provides HTTP endpoints for all horse-related operations
|
|
||||||
* following REST conventions and proper HTTP status codes.
|
|
||||||
*/
|
|
||||||
class HorseController(
|
|
||||||
private val horseRepository: HorseRepository
|
|
||||||
) {
|
|
||||||
|
|
||||||
private val getHorseUseCase = GetHorseUseCase(horseRepository)
|
|
||||||
private val createHorseUseCase = CreateHorseUseCase(horseRepository)
|
|
||||||
private val updateHorseUseCase = UpdateHorseUseCase(horseRepository)
|
|
||||||
private val deleteHorseUseCase = DeleteHorseUseCase(horseRepository)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Configures the horse-related routes.
|
|
||||||
*/
|
|
||||||
fun configureRoutes(routing: Routing) {
|
|
||||||
routing.route("/api/horses") {
|
|
||||||
|
|
||||||
// GET /api/horses - Get all horses with optional filtering
|
|
||||||
get {
|
|
||||||
try {
|
|
||||||
// Validate query parameters
|
|
||||||
val validationErrors = ApiValidationUtils.validateQueryParameters(
|
|
||||||
limit = call.request.queryParameters["limit"],
|
|
||||||
search = call.request.queryParameters["search"]
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!ApiValidationUtils.isValid(validationErrors)) {
|
|
||||||
call.respond(
|
|
||||||
HttpStatusCode.BadRequest,
|
|
||||||
ApiResponse.error<Any>(ApiValidationUtils.createErrorMessage(validationErrors))
|
|
||||||
)
|
|
||||||
return@get
|
|
||||||
}
|
|
||||||
|
|
||||||
val activeOnly = call.request.queryParameters["activeOnly"]?.toBoolean() ?: true
|
|
||||||
val limit = call.request.queryParameters["limit"]?.toInt() ?: 100
|
|
||||||
val ownerId = call.request.queryParameters["ownerId"]?.let {
|
|
||||||
ApiValidationUtils.validateUuidString(it) ?: return@get call.respond(
|
|
||||||
HttpStatusCode.BadRequest,
|
|
||||||
ApiResponse.error<Any>("Invalid ownerId format")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
val geschlecht = call.request.queryParameters["geschlecht"]?.let {
|
|
||||||
try {
|
|
||||||
PferdeGeschlechtE.valueOf(it)
|
|
||||||
} catch (_: IllegalArgumentException) {
|
|
||||||
return@get call.respond(
|
|
||||||
HttpStatusCode.BadRequest,
|
|
||||||
ApiResponse.error<Any>("Invalid geschlecht value. Valid values: ${PferdeGeschlechtE.entries.joinToString(", ")}")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val rasse = call.request.queryParameters["rasse"]
|
|
||||||
val searchTerm = call.request.queryParameters["search"]
|
|
||||||
|
|
||||||
val horses = when {
|
|
||||||
searchTerm != null -> getHorseUseCase.searchByName(searchTerm, limit)
|
|
||||||
ownerId != null -> getHorseUseCase.getByOwnerId(ownerId, activeOnly)
|
|
||||||
geschlecht != null -> getHorseUseCase.getByGeschlecht(geschlecht, activeOnly, limit)
|
|
||||||
rasse != null -> getHorseUseCase.getByRasse(rasse, activeOnly, limit)
|
|
||||||
else -> getHorseUseCase.getAllActive(limit)
|
|
||||||
}
|
|
||||||
|
|
||||||
call.respond(HttpStatusCode.OK, ApiResponse.success(horses))
|
|
||||||
} catch (e: Exception) {
|
|
||||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<Any>("Failed to retrieve horses: ${e.message}"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GET /api/horses/{id} - Get horse by ID
|
|
||||||
get("/{id}") {
|
|
||||||
try {
|
|
||||||
val horseId = Uuid.parse(call.parameters["id"]!!)
|
|
||||||
val horse = getHorseUseCase.getById(horseId)
|
|
||||||
|
|
||||||
if (horse != null) {
|
|
||||||
call.respond(HttpStatusCode.OK, ApiResponse.success(horse))
|
|
||||||
} else {
|
|
||||||
call.respond(HttpStatusCode.NotFound, ApiResponse.error<Any>("Horse not found"))
|
|
||||||
}
|
|
||||||
} catch (_: IllegalArgumentException) {
|
|
||||||
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Any>("Invalid horse ID format"))
|
|
||||||
} catch (e: Exception) {
|
|
||||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<Any>("Failed to retrieve horse: ${e.message}"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GET /api/horses/search/lebensnummer/{nummer} - Find by life number
|
|
||||||
get("/search/lebensnummer/{nummer}") {
|
|
||||||
try {
|
|
||||||
val lebensnummer = call.parameters["nummer"]!!
|
|
||||||
val horse = getHorseUseCase.getByLebensnummer(lebensnummer)
|
|
||||||
|
|
||||||
if (horse != null) {
|
|
||||||
call.respond(HttpStatusCode.OK, ApiResponse.success(horse))
|
|
||||||
} else {
|
|
||||||
call.respond(HttpStatusCode.NotFound, ApiResponse.error<Any>("Horse with life number '$lebensnummer' not found"))
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<Any>("Failed to search horse: ${e.message}"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GET /api/horses/search/chip/{nummer} - Find by chip number
|
|
||||||
get("/search/chip/{nummer}") {
|
|
||||||
try {
|
|
||||||
val chipNummer = call.parameters["nummer"]!!
|
|
||||||
val horse = getHorseUseCase.getByChipNummer(chipNummer)
|
|
||||||
|
|
||||||
if (horse != null) {
|
|
||||||
call.respond(HttpStatusCode.OK, ApiResponse.success(horse))
|
|
||||||
} else {
|
|
||||||
call.respond(HttpStatusCode.NotFound, ApiResponse.error<Any>("Horse with chip number '$chipNummer' not found"))
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<Any>("Failed to search horse: ${e.message}"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GET /api/horses/search/passport/{nummer} - Find by passport number
|
|
||||||
get("/search/passport/{nummer}") {
|
|
||||||
try {
|
|
||||||
val passNummer = call.parameters["nummer"]!!
|
|
||||||
val horse = getHorseUseCase.getByPassNummer(passNummer)
|
|
||||||
|
|
||||||
if (horse != null) {
|
|
||||||
call.respond(HttpStatusCode.OK, ApiResponse.success(horse))
|
|
||||||
} else {
|
|
||||||
call.respond(HttpStatusCode.NotFound, ApiResponse.error<Any>("Horse with passport number '$passNummer' not found"))
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<Any>("Failed to search horse: ${e.message}"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GET /api/horses/search/oeps/{nummer} - Find by OEPS number
|
|
||||||
get("/search/oeps/{nummer}") {
|
|
||||||
try {
|
|
||||||
val oepsNummer = call.parameters["nummer"]!!
|
|
||||||
val horse = getHorseUseCase.getByOepsNummer(oepsNummer)
|
|
||||||
|
|
||||||
if (horse != null) {
|
|
||||||
call.respond(HttpStatusCode.OK, ApiResponse.success(horse))
|
|
||||||
} else {
|
|
||||||
call.respond(HttpStatusCode.NotFound, ApiResponse.error<Any>("Horse with OEPS number '$oepsNummer' not found"))
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<Any>("Failed to search horse: ${e.message}"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GET /api/horses/search/fei/{nummer} - Find by FEI number
|
|
||||||
get("/search/fei/{nummer}") {
|
|
||||||
try {
|
|
||||||
val feiNummer = call.parameters["nummer"]!!
|
|
||||||
val horse = getHorseUseCase.getByFeiNummer(feiNummer)
|
|
||||||
|
|
||||||
if (horse != null) {
|
|
||||||
call.respond(HttpStatusCode.OK, ApiResponse.success(horse))
|
|
||||||
} else {
|
|
||||||
call.respond(HttpStatusCode.NotFound, ApiResponse.error<Any>("Horse with FEI number '$feiNummer' not found"))
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<Any>("Failed to search horse: ${e.message}"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GET /api/horses/oeps-registered - Get OEPS registered horses
|
|
||||||
get("/oeps-registered") {
|
|
||||||
try {
|
|
||||||
val activeOnly = call.request.queryParameters["activeOnly"]?.toBoolean() ?: true
|
|
||||||
val horses = getHorseUseCase.getOepsRegistered(activeOnly)
|
|
||||||
call.respond(HttpStatusCode.OK, ApiResponse.success(horses))
|
|
||||||
} catch (e: Exception) {
|
|
||||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<Any>("Failed to retrieve OEPS horses: ${e.message}"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GET /api/horses/fei-registered - Get FEI registered horses
|
|
||||||
get("/fei-registered") {
|
|
||||||
try {
|
|
||||||
val activeOnly = call.request.queryParameters["activeOnly"]?.toBoolean() ?: true
|
|
||||||
val horses = getHorseUseCase.getFeiRegistered(activeOnly)
|
|
||||||
call.respond(HttpStatusCode.OK, ApiResponse.success(horses))
|
|
||||||
} catch (e: Exception) {
|
|
||||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<Any>("Failed to retrieve FEI horses: ${e.message}"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GET /api/horses/stats - Get horse statistics
|
|
||||||
get("/stats") {
|
|
||||||
try {
|
|
||||||
val activeCount = getHorseUseCase.countActive()
|
|
||||||
val oepsCount = getHorseUseCase.countOepsRegistered(true)
|
|
||||||
val feiCount = getHorseUseCase.countFeiRegistered(true)
|
|
||||||
|
|
||||||
val stats = HorseStats(
|
|
||||||
totalActive = activeCount,
|
|
||||||
oepsRegistered = oepsCount,
|
|
||||||
feiRegistered = feiCount
|
|
||||||
)
|
|
||||||
|
|
||||||
call.respond(HttpStatusCode.OK, ApiResponse.success(stats))
|
|
||||||
} catch (e: Exception) {
|
|
||||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<Any>("Failed to retrieve statistics: ${e.message}"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// POST /api/horses - Create new horse
|
|
||||||
post {
|
|
||||||
try {
|
|
||||||
val createRequest = call.receive<CreateHorseUseCase.CreateHorseRequest>()
|
|
||||||
|
|
||||||
// Validate input using shared validation utilities
|
|
||||||
val validationErrors = ApiValidationUtils.validateHorseRequest(
|
|
||||||
pferdeName = createRequest.pferdeName,
|
|
||||||
lebensnummer = createRequest.lebensnummer,
|
|
||||||
chipNummer = createRequest.chipNummer,
|
|
||||||
oepsNummer = createRequest.oepsNummer,
|
|
||||||
feiNummer = createRequest.feiNummer
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!ApiValidationUtils.isValid(validationErrors)) {
|
|
||||||
call.respond(
|
|
||||||
HttpStatusCode.BadRequest,
|
|
||||||
ApiResponse.error<Any>(ApiValidationUtils.createErrorMessage(validationErrors))
|
|
||||||
)
|
|
||||||
return@post
|
|
||||||
}
|
|
||||||
|
|
||||||
val response = createHorseUseCase.execute(createRequest)
|
|
||||||
|
|
||||||
if (response.success) {
|
|
||||||
call.respond(HttpStatusCode.Created, ApiResponse.success(response.data!!))
|
|
||||||
} else {
|
|
||||||
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Any>("Validation failed"))
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<Any>("Failed to create horse: ${e.message}"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// PUT /api/horses/{id} - Update horse
|
|
||||||
put("/{id}") {
|
|
||||||
try {
|
|
||||||
val horseId = Uuid.parse(call.parameters["id"]!!)
|
|
||||||
val updateData = call.receive<UpdateHorseRequest>()
|
|
||||||
|
|
||||||
// Validate input using shared validation utilities
|
|
||||||
val validationErrors = ApiValidationUtils.validateHorseRequest(
|
|
||||||
pferdeName = updateData.pferdeName,
|
|
||||||
lebensnummer = updateData.lebensnummer,
|
|
||||||
chipNummer = updateData.chipNummer,
|
|
||||||
oepsNummer = updateData.oepsNummer,
|
|
||||||
feiNummer = updateData.feiNummer
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!ApiValidationUtils.isValid(validationErrors)) {
|
|
||||||
call.respond(
|
|
||||||
HttpStatusCode.BadRequest,
|
|
||||||
ApiResponse.error<Any>(ApiValidationUtils.createErrorMessage(validationErrors))
|
|
||||||
)
|
|
||||||
return@put
|
|
||||||
}
|
|
||||||
|
|
||||||
val updateRequest = UpdateHorseUseCase.UpdateHorseRequest(
|
|
||||||
pferdId = horseId,
|
|
||||||
pferdeName = updateData.pferdeName,
|
|
||||||
geschlecht = updateData.geschlecht,
|
|
||||||
geburtsdatum = updateData.geburtsdatum,
|
|
||||||
rasse = updateData.rasse,
|
|
||||||
farbe = updateData.farbe,
|
|
||||||
besitzerId = updateData.besitzerId,
|
|
||||||
verantwortlichePersonId = updateData.verantwortlichePersonId,
|
|
||||||
zuechterName = updateData.zuechterName,
|
|
||||||
zuchtbuchNummer = updateData.zuchtbuchNummer,
|
|
||||||
lebensnummer = updateData.lebensnummer,
|
|
||||||
chipNummer = updateData.chipNummer,
|
|
||||||
passNummer = updateData.passNummer,
|
|
||||||
oepsNummer = updateData.oepsNummer,
|
|
||||||
feiNummer = updateData.feiNummer,
|
|
||||||
vaterName = updateData.vaterName,
|
|
||||||
mutterName = updateData.mutterName,
|
|
||||||
mutterVaterName = updateData.mutterVaterName,
|
|
||||||
stockmass = updateData.stockmass,
|
|
||||||
istAktiv = updateData.istAktiv,
|
|
||||||
bemerkungen = updateData.bemerkungen,
|
|
||||||
datenQuelle = updateData.datenQuelle
|
|
||||||
)
|
|
||||||
|
|
||||||
val response = updateHorseUseCase.execute(updateRequest)
|
|
||||||
|
|
||||||
if (response.success && response.horse != null) {
|
|
||||||
call.respond(HttpStatusCode.OK, ApiResponse.success(response.horse))
|
|
||||||
} else {
|
|
||||||
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Any>("Update failed: ${response.errors.joinToString(", ")}"))
|
|
||||||
}
|
|
||||||
} catch (_: IllegalArgumentException) {
|
|
||||||
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Any>("Invalid horse ID format"))
|
|
||||||
} catch (e: Exception) {
|
|
||||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<Any>("Failed to update horse: ${e.message}"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DELETE /api/horses/{id} - Delete horse
|
|
||||||
delete("/{id}") {
|
|
||||||
try {
|
|
||||||
val horseId = Uuid.parse(call.parameters["id"]!!)
|
|
||||||
val forceDelete = call.request.queryParameters["force"]?.toBoolean() ?: false
|
|
||||||
|
|
||||||
val deleteRequest = DeleteHorseUseCase.DeleteHorseRequest(horseId, forceDelete)
|
|
||||||
val response = deleteHorseUseCase.execute(deleteRequest)
|
|
||||||
|
|
||||||
if (response.success) {
|
|
||||||
val message = if (response.warnings.isNotEmpty()) {
|
|
||||||
"Horse deleted successfully. Warnings: ${response.warnings.joinToString(", ")}"
|
|
||||||
} else {
|
|
||||||
"Horse deleted successfully"
|
|
||||||
}
|
|
||||||
call.respond(HttpStatusCode.OK, ApiResponse.success(message))
|
|
||||||
} else {
|
|
||||||
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Any>("Delete failed: ${response.errors.joinToString(", ")}"))
|
|
||||||
}
|
|
||||||
} catch (_: IllegalArgumentException) {
|
|
||||||
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Any>("Invalid horse ID format"))
|
|
||||||
} catch (e: Exception) {
|
|
||||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<Any>("Failed to delete horse: ${e.message}"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// POST /api/horses/{id}/soft-delete - Soft delete horse (mark as inactive)
|
|
||||||
post("/{id}/soft-delete") {
|
|
||||||
try {
|
|
||||||
val horseId = Uuid.parse(call.parameters["id"]!!)
|
|
||||||
val response = deleteHorseUseCase.softDelete(horseId)
|
|
||||||
|
|
||||||
if (response.success) {
|
|
||||||
val message = if (response.warnings.isNotEmpty()) {
|
|
||||||
"Horse marked as inactive. Warnings: ${response.warnings.joinToString(", ")}"
|
|
||||||
} else {
|
|
||||||
"Horse marked as inactive"
|
|
||||||
}
|
|
||||||
call.respond(HttpStatusCode.OK, ApiResponse.success(message))
|
|
||||||
} else {
|
|
||||||
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Any>("Soft delete failed: ${response.errors.joinToString(", ")}"))
|
|
||||||
}
|
|
||||||
} catch (_: IllegalArgumentException) {
|
|
||||||
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Any>("Invalid horse ID format"))
|
|
||||||
} catch (e: Exception) {
|
|
||||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<Any>("Failed to soft delete horse: ${e.message}"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// POST /api/horses/batch-delete - Batch delete multiple horses
|
|
||||||
post("/batch-delete") {
|
|
||||||
try {
|
|
||||||
val batchRequest = call.receive<BatchDeleteRequest>()
|
|
||||||
val response = deleteHorseUseCase.batchDelete(batchRequest.horseIds, batchRequest.forceDelete)
|
|
||||||
|
|
||||||
val statusCode = if (response.overallSuccess) HttpStatusCode.OK else HttpStatusCode.PartialContent
|
|
||||||
call.respond(statusCode, ApiResponse.success(response))
|
|
||||||
} catch (e: Exception) {
|
|
||||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<Any>("Failed to batch delete horses: ${e.message}"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* DTO for updating horse data via API.
|
|
||||||
*/
|
|
||||||
@Serializable
|
|
||||||
data class UpdateHorseRequest(
|
|
||||||
val pferdeName: String,
|
|
||||||
val geschlecht: PferdeGeschlechtE,
|
|
||||||
val geburtsdatum: kotlinx.datetime.LocalDate? = null,
|
|
||||||
val rasse: String? = null,
|
|
||||||
val farbe: String? = null,
|
|
||||||
@Contextual val besitzerId: Uuid? = null,
|
|
||||||
@Contextual val verantwortlichePersonId: Uuid? = null,
|
|
||||||
val zuechterName: String? = null,
|
|
||||||
val zuchtbuchNummer: String? = null,
|
|
||||||
val lebensnummer: String? = null,
|
|
||||||
val chipNummer: String? = null,
|
|
||||||
val passNummer: String? = null,
|
|
||||||
val oepsNummer: String? = null,
|
|
||||||
val feiNummer: String? = null,
|
|
||||||
val vaterName: String? = null,
|
|
||||||
val mutterName: String? = null,
|
|
||||||
val mutterVaterName: String? = null,
|
|
||||||
val stockmass: Int? = null,
|
|
||||||
val istAktiv: Boolean = true,
|
|
||||||
val bemerkungen: String? = null,
|
|
||||||
val datenQuelle: at.mocode.core.domain.model.DatenQuelleE = at.mocode.core.domain.model.DatenQuelleE.MANUELL
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* DTO for batch delete request.
|
|
||||||
*/
|
|
||||||
@Serializable
|
|
||||||
data class BatchDeleteRequest(
|
|
||||||
val horseIds: List<@Contextual Uuid>,
|
|
||||||
val forceDelete: Boolean = false
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* DTO for horse statistics.
|
|
||||||
*/
|
|
||||||
@Serializable
|
|
||||||
data class HorseStats(
|
|
||||||
val totalActive: Long,
|
|
||||||
val oepsRegistered: Long,
|
|
||||||
val feiRegistered: Long
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
plugins {
|
|
||||||
alias(libs.plugins.kotlinJvm)
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
implementation(projects.backend.services.horses.horsesDomain)
|
|
||||||
implementation(projects.core.coreDomain)
|
|
||||||
implementation(projects.core.coreUtils)
|
|
||||||
testImplementation(projects.platform.platformTesting)
|
|
||||||
}
|
|
||||||
-207
@@ -1,207 +0,0 @@
|
|||||||
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
|
||||||
package at.mocode.horses.application.usecase
|
|
||||||
|
|
||||||
import at.mocode.horses.domain.model.DomPferd
|
|
||||||
import at.mocode.horses.domain.repository.HorseRepository
|
|
||||||
import at.mocode.core.domain.model.PferdeGeschlechtE
|
|
||||||
import at.mocode.core.domain.model.DatenQuelleE
|
|
||||||
import at.mocode.core.domain.model.ApiResponse
|
|
||||||
import at.mocode.core.domain.model.ErrorDto
|
|
||||||
import at.mocode.core.domain.model.ValidationResult
|
|
||||||
import at.mocode.core.domain.model.ValidationError
|
|
||||||
import kotlin.uuid.Uuid
|
|
||||||
import kotlinx.datetime.LocalDate
|
|
||||||
import kotlinx.datetime.todayIn
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Use case for creating a new horse in the registry.
|
|
||||||
*
|
|
||||||
* This use case handles the business logic for horse registration including
|
|
||||||
* validation, uniqueness checks, and persistence.
|
|
||||||
*/
|
|
||||||
class CreateHorseUseCase(
|
|
||||||
private val horseRepository: HorseRepository
|
|
||||||
) {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Request data for creating a new horse.
|
|
||||||
*/
|
|
||||||
data class CreateHorseRequest(
|
|
||||||
val pferdeName: String,
|
|
||||||
val geschlecht: PferdeGeschlechtE,
|
|
||||||
val geburtsdatum: LocalDate? = null,
|
|
||||||
val rasse: String? = null,
|
|
||||||
val farbe: String? = null,
|
|
||||||
val besitzerId: Uuid? = null,
|
|
||||||
val verantwortlichePersonId: Uuid? = null,
|
|
||||||
val zuechterName: String? = null,
|
|
||||||
val zuchtbuchNummer: String? = null,
|
|
||||||
val lebensnummer: String? = null,
|
|
||||||
val chipNummer: String? = null,
|
|
||||||
val passNummer: String? = null,
|
|
||||||
val oepsNummer: String? = null,
|
|
||||||
val feiNummer: String? = null,
|
|
||||||
val vaterName: String? = null,
|
|
||||||
val mutterName: String? = null,
|
|
||||||
val mutterVaterName: String? = null,
|
|
||||||
val stockmass: Int? = null,
|
|
||||||
val bemerkungen: String? = null,
|
|
||||||
val datenQuelle: DatenQuelleE = DatenQuelleE.MANUELL
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Executes the horse creation use case.
|
|
||||||
*
|
|
||||||
* @param request The horse creation request data
|
|
||||||
* @return ApiResponse with the created horse or validation errors
|
|
||||||
*/
|
|
||||||
suspend fun execute(request: CreateHorseRequest): ApiResponse<DomPferd> {
|
|
||||||
// Create domain object
|
|
||||||
val horse = DomPferd(
|
|
||||||
pferdeName = request.pferdeName,
|
|
||||||
geschlecht = request.geschlecht,
|
|
||||||
geburtsdatum = request.geburtsdatum,
|
|
||||||
rasse = request.rasse,
|
|
||||||
farbe = request.farbe,
|
|
||||||
besitzerId = request.besitzerId,
|
|
||||||
verantwortlichePersonId = request.verantwortlichePersonId,
|
|
||||||
zuechterName = request.zuechterName,
|
|
||||||
zuchtbuchNummer = request.zuchtbuchNummer,
|
|
||||||
lebensnummer = request.lebensnummer,
|
|
||||||
chipNummer = request.chipNummer,
|
|
||||||
passNummer = request.passNummer,
|
|
||||||
oepsNummer = request.oepsNummer,
|
|
||||||
feiNummer = request.feiNummer,
|
|
||||||
vaterName = request.vaterName,
|
|
||||||
mutterName = request.mutterName,
|
|
||||||
mutterVaterName = request.mutterVaterName,
|
|
||||||
stockmass = request.stockmass,
|
|
||||||
bemerkungen = request.bemerkungen,
|
|
||||||
datenQuelle = request.datenQuelle
|
|
||||||
)
|
|
||||||
|
|
||||||
// Validate the horse
|
|
||||||
val validationResult = validateHorse(horse)
|
|
||||||
if (!validationResult.isValid()) {
|
|
||||||
val errors = (validationResult as ValidationResult.Invalid).errors
|
|
||||||
return ApiResponse(
|
|
||||||
success = false,
|
|
||||||
data = null,
|
|
||||||
error = ErrorDto(
|
|
||||||
code = "VALIDATION_ERROR",
|
|
||||||
message = "Horse validation failed",
|
|
||||||
details = errors.associate { it.field to it.message }
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for uniqueness constraints
|
|
||||||
val uniquenessResult = checkUniquenessConstraints(horse)
|
|
||||||
if (!uniquenessResult.isValid()) {
|
|
||||||
val errors = (uniquenessResult as ValidationResult.Invalid).errors
|
|
||||||
return ApiResponse(
|
|
||||||
success = false,
|
|
||||||
data = null,
|
|
||||||
error = ErrorDto(
|
|
||||||
code = "UNIQUENESS_ERROR",
|
|
||||||
message = "Horse uniqueness validation failed",
|
|
||||||
details = errors.associate { it.field to it.message }
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save the horse
|
|
||||||
val savedHorse = horseRepository.save(horse)
|
|
||||||
|
|
||||||
return ApiResponse(
|
|
||||||
success = true,
|
|
||||||
data = savedHorse,
|
|
||||||
message = "Horse created successfully"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates the horse data according to business rules.
|
|
||||||
*/
|
|
||||||
private fun validateHorse(horse: DomPferd): ValidationResult {
|
|
||||||
val errors = mutableListOf<ValidationError>()
|
|
||||||
|
|
||||||
// Use domain validation
|
|
||||||
val domainErrors = horse.validateForRegistration()
|
|
||||||
domainErrors.forEach { errorMessage ->
|
|
||||||
errors.add(ValidationError("horse", errorMessage, "DOMAIN_VALIDATION"))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Additional business validations
|
|
||||||
horse.stockmass?.let { height ->
|
|
||||||
if (height < 50 || height > 220) {
|
|
||||||
errors.add(ValidationError("stockmass", "Horse height must be between 50 and 220 cm", "INVALID_RANGE"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
horse.geburtsdatum?.let { birthDate ->
|
|
||||||
val currentYear = kotlinx.datetime.Clock.System.todayIn(kotlinx.datetime.TimeZone.currentSystemDefault()).year
|
|
||||||
if (birthDate.year > currentYear) {
|
|
||||||
errors.add(ValidationError("geburtsdatum", "Birth date cannot be in the future", "FUTURE_DATE"))
|
|
||||||
}
|
|
||||||
if (birthDate.year < (currentYear - 50)) {
|
|
||||||
errors.add(ValidationError("geburtsdatum", "Birth date cannot be more than 50 years ago", "TOO_OLD"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return if (errors.isEmpty()) {
|
|
||||||
ValidationResult.Valid
|
|
||||||
} else {
|
|
||||||
ValidationResult.Invalid(errors)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks uniqueness constraints for identification numbers.
|
|
||||||
*/
|
|
||||||
private suspend fun checkUniquenessConstraints(horse: DomPferd): ValidationResult {
|
|
||||||
val errors = mutableListOf<ValidationError>()
|
|
||||||
|
|
||||||
// Check lebensnummer uniqueness
|
|
||||||
horse.lebensnummer?.let { lebensnummer ->
|
|
||||||
if (lebensnummer.isNotBlank() && horseRepository.existsByLebensnummer(lebensnummer)) {
|
|
||||||
errors.add(ValidationError("lebensnummer", "A horse with this life number already exists", "DUPLICATE"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check chip number uniqueness
|
|
||||||
horse.chipNummer?.let { chipNummer ->
|
|
||||||
if (chipNummer.isNotBlank() && horseRepository.existsByChipNummer(chipNummer)) {
|
|
||||||
errors.add(ValidationError("chipNummer", "A horse with this chip number already exists", "DUPLICATE"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check passport number uniqueness
|
|
||||||
horse.passNummer?.let { passNummer ->
|
|
||||||
if (passNummer.isNotBlank() && horseRepository.existsByPassNummer(passNummer)) {
|
|
||||||
errors.add(ValidationError("passNummer", "A horse with this passport number already exists", "DUPLICATE"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check OEPS number uniqueness
|
|
||||||
horse.oepsNummer?.let { oepsNummer ->
|
|
||||||
if (oepsNummer.isNotBlank() && horseRepository.existsByOepsNummer(oepsNummer)) {
|
|
||||||
errors.add(ValidationError("oepsNummer", "A horse with this OEPS number already exists", "DUPLICATE"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check FEI number uniqueness
|
|
||||||
horse.feiNummer?.let { feiNummer ->
|
|
||||||
if (feiNummer.isNotBlank() && horseRepository.existsByFeiNummer(feiNummer)) {
|
|
||||||
errors.add(ValidationError("feiNummer", "A horse with this FEI number already exists", "DUPLICATE"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return if (errors.isEmpty()) {
|
|
||||||
ValidationResult.Valid
|
|
||||||
} else {
|
|
||||||
ValidationResult.Invalid(errors)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-215
@@ -1,215 +0,0 @@
|
|||||||
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
|
||||||
package at.mocode.horses.application.usecase
|
|
||||||
|
|
||||||
import at.mocode.horses.domain.repository.HorseRepository
|
|
||||||
import kotlin.uuid.Uuid
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Use case for deleting a horse from the registry.
|
|
||||||
*
|
|
||||||
* This use case handles the business logic for horse deletion including
|
|
||||||
* existence checks and business rule validation.
|
|
||||||
*/
|
|
||||||
class DeleteHorseUseCase(
|
|
||||||
private val horseRepository: HorseRepository
|
|
||||||
) {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Request data for deleting a horse.
|
|
||||||
*/
|
|
||||||
data class DeleteHorseRequest(
|
|
||||||
val pferdId: Uuid,
|
|
||||||
val forceDelete: Boolean = false
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Response data for horse deletion.
|
|
||||||
*/
|
|
||||||
data class DeleteHorseResponse(
|
|
||||||
val success: Boolean,
|
|
||||||
val errors: List<String> = emptyList(),
|
|
||||||
val warnings: List<String> = emptyList()
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Executes the horse deletion use case.
|
|
||||||
*
|
|
||||||
* @param request The horse deletion request data
|
|
||||||
* @return DeleteHorseResponse indicating success or failure with errors
|
|
||||||
*/
|
|
||||||
suspend fun execute(request: DeleteHorseRequest): DeleteHorseResponse {
|
|
||||||
// Check if horse exists
|
|
||||||
val existingHorse = horseRepository.findById(request.pferdId)
|
|
||||||
?: return DeleteHorseResponse(
|
|
||||||
success = false,
|
|
||||||
errors = listOf("Horse not found")
|
|
||||||
)
|
|
||||||
|
|
||||||
// Check business rules for deletion
|
|
||||||
val businessRuleErrors = checkBusinessRules(request, existingHorse.pferdeName)
|
|
||||||
if (businessRuleErrors.isNotEmpty() && !request.forceDelete) {
|
|
||||||
return DeleteHorseResponse(
|
|
||||||
success = false,
|
|
||||||
errors = businessRuleErrors
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate warnings for important information
|
|
||||||
val warnings = generateWarnings(existingHorse.pferdeName, existingHorse.oepsNummer, existingHorse.feiNummer)
|
|
||||||
|
|
||||||
// Perform the deletion
|
|
||||||
val deleted = horseRepository.delete(request.pferdId)
|
|
||||||
|
|
||||||
return if (deleted) {
|
|
||||||
DeleteHorseResponse(
|
|
||||||
success = true,
|
|
||||||
warnings = warnings
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
DeleteHorseResponse(
|
|
||||||
success = false,
|
|
||||||
errors = listOf("Failed to delete horse from database")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Soft delete alternative - marks horse as inactive instead of deleting.
|
|
||||||
*/
|
|
||||||
suspend fun softDelete(pferdId: Uuid): DeleteHorseResponse {
|
|
||||||
val existingHorse = horseRepository.findById(pferdId)
|
|
||||||
?: return DeleteHorseResponse(
|
|
||||||
success = false,
|
|
||||||
errors = listOf("Horse not found")
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!existingHorse.istAktiv) {
|
|
||||||
return DeleteHorseResponse(
|
|
||||||
success = false,
|
|
||||||
errors = listOf("Horse is already inactive")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mark as inactive
|
|
||||||
val inactiveHorse = existingHorse.copy(istAktiv = false).withUpdatedTimestamp()
|
|
||||||
horseRepository.save(inactiveHorse)
|
|
||||||
|
|
||||||
return DeleteHorseResponse(
|
|
||||||
success = true,
|
|
||||||
warnings = listOf("Horse marked as inactive instead of deleted")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks business rules that might prevent deletion.
|
|
||||||
*/
|
|
||||||
private suspend fun checkBusinessRules(request: DeleteHorseRequest, horseName: String): List<String> {
|
|
||||||
val errors = mutableListOf<String>()
|
|
||||||
|
|
||||||
// In a real system, you would check for:
|
|
||||||
// - Active competitions/entries
|
|
||||||
// - Historical records that should be preserved
|
|
||||||
// - Breeding records
|
|
||||||
// - License dependencies
|
|
||||||
|
|
||||||
// For now, we'll implement basic checks
|
|
||||||
|
|
||||||
// Example: Check if horse has OEPS or FEI registration
|
|
||||||
val horse = horseRepository.findById(request.pferdId)
|
|
||||||
if (horse != null) {
|
|
||||||
if (horse.isOepsRegistered() && !request.forceDelete) {
|
|
||||||
errors.add("Cannot delete OEPS registered horse without force delete flag")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (horse.isFeiRegistered() && !request.forceDelete) {
|
|
||||||
errors.add("Cannot delete FEI registered horse without force delete flag")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if horse has breeding information (might be important for pedigree)
|
|
||||||
if ((horse.vaterName != null || horse.mutterName != null) && !request.forceDelete) {
|
|
||||||
errors.add("Horse has pedigree information that might be referenced by other horses")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generates warnings about the deletion.
|
|
||||||
*/
|
|
||||||
private fun generateWarnings(horseName: String, oepsNummer: String?, feiNummer: String?): List<String> {
|
|
||||||
val warnings = mutableListOf<String>()
|
|
||||||
|
|
||||||
warnings.add("Horse '$horseName' will be permanently deleted")
|
|
||||||
|
|
||||||
if (!oepsNummer.isNullOrBlank()) {
|
|
||||||
warnings.add("OEPS registration number '$oepsNummer' will be lost")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!feiNummer.isNullOrBlank()) {
|
|
||||||
warnings.add("FEI registration number '$feiNummer' will be lost")
|
|
||||||
}
|
|
||||||
|
|
||||||
warnings.add("This action cannot be undone")
|
|
||||||
|
|
||||||
return warnings
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Batch delete multiple horses.
|
|
||||||
*/
|
|
||||||
suspend fun batchDelete(horseIds: List<Uuid>, forceDelete: Boolean = false): BatchDeleteResponse {
|
|
||||||
val results = mutableListOf<DeleteResult>()
|
|
||||||
var successCount = 0
|
|
||||||
var errorCount = 0
|
|
||||||
|
|
||||||
for (horseId in horseIds) {
|
|
||||||
val request = DeleteHorseRequest(horseId, forceDelete)
|
|
||||||
val response = execute(request)
|
|
||||||
|
|
||||||
results.add(
|
|
||||||
DeleteResult(
|
|
||||||
horseId = horseId,
|
|
||||||
success = response.success,
|
|
||||||
errors = response.errors,
|
|
||||||
warnings = response.warnings
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
if (response.success) {
|
|
||||||
successCount++
|
|
||||||
} else {
|
|
||||||
errorCount++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return BatchDeleteResponse(
|
|
||||||
results = results,
|
|
||||||
successCount = successCount,
|
|
||||||
errorCount = errorCount,
|
|
||||||
totalCount = horseIds.size
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Result for individual horse deletion in batch operation.
|
|
||||||
*/
|
|
||||||
data class DeleteResult(
|
|
||||||
val horseId: Uuid,
|
|
||||||
val success: Boolean,
|
|
||||||
val errors: List<String> = emptyList(),
|
|
||||||
val warnings: List<String> = emptyList()
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Response for batch delete operation.
|
|
||||||
*/
|
|
||||||
data class BatchDeleteResponse(
|
|
||||||
val results: List<DeleteResult>,
|
|
||||||
val successCount: Int,
|
|
||||||
val errorCount: Int,
|
|
||||||
val totalCount: Int
|
|
||||||
) {
|
|
||||||
val overallSuccess: Boolean = errorCount == 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-304
@@ -1,304 +0,0 @@
|
|||||||
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
|
||||||
package at.mocode.horses.application.usecase
|
|
||||||
|
|
||||||
import at.mocode.horses.domain.model.DomPferd
|
|
||||||
import at.mocode.horses.domain.repository.HorseRepository
|
|
||||||
import at.mocode.core.domain.model.PferdeGeschlechtE
|
|
||||||
import kotlin.uuid.Uuid
|
|
||||||
import kotlinx.datetime.todayIn
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Use case for retrieving horse information.
|
|
||||||
*
|
|
||||||
* This use case encapsulates the business logic for fetching horse data
|
|
||||||
* and provides a clean interface for the application layer.
|
|
||||||
*/
|
|
||||||
class GetHorseUseCase(
|
|
||||||
private val horseRepository: HorseRepository
|
|
||||||
) {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves a horse by its unique ID.
|
|
||||||
*
|
|
||||||
* @param horseId The unique identifier of the horse
|
|
||||||
* @return The horse if found, null otherwise
|
|
||||||
*/
|
|
||||||
suspend fun getById(horseId: Uuid): DomPferd? {
|
|
||||||
return horseRepository.findById(horseId)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves a horse by its life number.
|
|
||||||
*
|
|
||||||
* @param lebensnummer The life number to search for
|
|
||||||
* @return The horse if found, null otherwise
|
|
||||||
*/
|
|
||||||
suspend fun getByLebensnummer(lebensnummer: String): DomPferd? {
|
|
||||||
require(lebensnummer.isNotBlank()) { "Life number cannot be blank" }
|
|
||||||
return horseRepository.findByLebensnummer(lebensnummer.trim())
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves a horse by its chip number.
|
|
||||||
*
|
|
||||||
* @param chipNummer The chip number to search for
|
|
||||||
* @return The horse if found, null otherwise
|
|
||||||
*/
|
|
||||||
suspend fun getByChipNummer(chipNummer: String): DomPferd? {
|
|
||||||
require(chipNummer.isNotBlank()) { "Chip number cannot be blank" }
|
|
||||||
return horseRepository.findByChipNummer(chipNummer.trim())
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves a horse by its passport number.
|
|
||||||
*
|
|
||||||
* @param passNummer The passport number to search for
|
|
||||||
* @return The horse if found, null otherwise
|
|
||||||
*/
|
|
||||||
suspend fun getByPassNummer(passNummer: String): DomPferd? {
|
|
||||||
require(passNummer.isNotBlank()) { "Passport number cannot be blank" }
|
|
||||||
return horseRepository.findByPassNummer(passNummer.trim())
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves a horse by its OEPS number.
|
|
||||||
*
|
|
||||||
* @param oepsNummer The OEPS number to search for
|
|
||||||
* @return The horse if found, null otherwise
|
|
||||||
*/
|
|
||||||
suspend fun getByOepsNummer(oepsNummer: String): DomPferd? {
|
|
||||||
require(oepsNummer.isNotBlank()) { "OEPS number cannot be blank" }
|
|
||||||
return horseRepository.findByOepsNummer(oepsNummer.trim())
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves a horse by its FEI number.
|
|
||||||
*
|
|
||||||
* @param feiNummer The FEI number to search for
|
|
||||||
* @return The horse if found, null otherwise
|
|
||||||
*/
|
|
||||||
suspend fun getByFeiNummer(feiNummer: String): DomPferd? {
|
|
||||||
require(feiNummer.isNotBlank()) { "FEI number cannot be blank" }
|
|
||||||
return horseRepository.findByFeiNummer(feiNummer.trim())
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Searches for horses by name (partial match).
|
|
||||||
*
|
|
||||||
* @param searchTerm The search term to match against horse names
|
|
||||||
* @param limit Maximum number of results to return (default: 50)
|
|
||||||
* @return List of matching horses
|
|
||||||
*/
|
|
||||||
suspend fun searchByName(searchTerm: String, limit: Int = 50): List<DomPferd> {
|
|
||||||
require(searchTerm.isNotBlank()) { "Search term cannot be blank" }
|
|
||||||
require(limit > 0) { "Limit must be positive" }
|
|
||||||
return horseRepository.findByName(searchTerm.trim(), limit)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves all horses owned by a specific person.
|
|
||||||
*
|
|
||||||
* @param ownerId The ID of the owner
|
|
||||||
* @param activeOnly Whether to return only active horses (default: true)
|
|
||||||
* @return List of horses owned by the person
|
|
||||||
*/
|
|
||||||
suspend fun getByOwnerId(ownerId: Uuid, activeOnly: Boolean = true): List<DomPferd> {
|
|
||||||
return horseRepository.findByOwnerId(ownerId, activeOnly)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves all horses for which a person is responsible.
|
|
||||||
*
|
|
||||||
* @param responsiblePersonId The ID of the responsible person
|
|
||||||
* @param activeOnly Whether to return only active horses (default: true)
|
|
||||||
* @return List of horses for which the person is responsible
|
|
||||||
*/
|
|
||||||
suspend fun getByResponsiblePersonId(responsiblePersonId: Uuid, activeOnly: Boolean = true): List<DomPferd> {
|
|
||||||
return horseRepository.findByResponsiblePersonId(responsiblePersonId, activeOnly)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves horses by gender.
|
|
||||||
*
|
|
||||||
* @param geschlecht The gender to filter by
|
|
||||||
* @param activeOnly Whether to return only active horses (default: true)
|
|
||||||
* @param limit Maximum number of results to return (default: 100)
|
|
||||||
* @return List of horses with the specified gender
|
|
||||||
*/
|
|
||||||
suspend fun getByGeschlecht(geschlecht: PferdeGeschlechtE, activeOnly: Boolean = true, limit: Int = 100): List<DomPferd> {
|
|
||||||
require(limit > 0) { "Limit must be positive" }
|
|
||||||
return horseRepository.findByGeschlecht(geschlecht, activeOnly, limit)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves horses by breed.
|
|
||||||
*
|
|
||||||
* @param rasse The breed to filter by
|
|
||||||
* @param activeOnly Whether to return only active horses (default: true)
|
|
||||||
* @param limit Maximum number of results to return (default: 100)
|
|
||||||
* @return List of horses of the specified breed
|
|
||||||
*/
|
|
||||||
suspend fun getByRasse(rasse: String, activeOnly: Boolean = true, limit: Int = 100): List<DomPferd> {
|
|
||||||
require(rasse.isNotBlank()) { "Breed cannot be blank" }
|
|
||||||
require(limit > 0) { "Limit must be positive" }
|
|
||||||
return horseRepository.findByRasse(rasse.trim(), activeOnly, limit)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves horses by birth year.
|
|
||||||
*
|
|
||||||
* @param birthYear The birth year to filter by
|
|
||||||
* @param activeOnly Whether to return only active horses (default: true)
|
|
||||||
* @return List of horses born in the specified year
|
|
||||||
*/
|
|
||||||
suspend fun getByBirthYear(birthYear: Int, activeOnly: Boolean = true): List<DomPferd> {
|
|
||||||
require(birthYear > 1900) { "Birth year must be after 1900" }
|
|
||||||
require(birthYear <= kotlinx.datetime.Clock.System.todayIn(kotlinx.datetime.TimeZone.currentSystemDefault()).year) {
|
|
||||||
"Birth year cannot be in the future"
|
|
||||||
}
|
|
||||||
return horseRepository.findByBirthYear(birthYear, activeOnly)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves horses by birth year range.
|
|
||||||
*
|
|
||||||
* @param fromYear The start year (inclusive)
|
|
||||||
* @param toYear The end year (inclusive)
|
|
||||||
* @param activeOnly Whether to return only active horses (default: true)
|
|
||||||
* @return List of horses born within the specified year range
|
|
||||||
*/
|
|
||||||
suspend fun getByBirthYearRange(fromYear: Int, toYear: Int, activeOnly: Boolean = true): List<DomPferd> {
|
|
||||||
require(fromYear > 1900) { "From year must be after 1900" }
|
|
||||||
require(toYear >= fromYear) { "To year must be greater than or equal to from year" }
|
|
||||||
val currentYear = kotlinx.datetime.Clock.System.todayIn(kotlinx.datetime.TimeZone.currentSystemDefault()).year
|
|
||||||
require(toYear <= currentYear) { "To year cannot be in the future" }
|
|
||||||
|
|
||||||
return horseRepository.findByBirthYearRange(fromYear, toYear, activeOnly)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves all active horses.
|
|
||||||
*
|
|
||||||
* @param limit Maximum number of results to return (default: 1000)
|
|
||||||
* @return List of active horses
|
|
||||||
*/
|
|
||||||
suspend fun getAllActive(limit: Int = 1000): List<DomPferd> {
|
|
||||||
require(limit > 0) { "Limit must be positive" }
|
|
||||||
return horseRepository.findAllActive(limit)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves horses with OEPS registration.
|
|
||||||
*
|
|
||||||
* @param activeOnly Whether to return only active horses (default: true)
|
|
||||||
* @return List of OEPS registered horses
|
|
||||||
*/
|
|
||||||
suspend fun getOepsRegistered(activeOnly: Boolean = true): List<DomPferd> {
|
|
||||||
return horseRepository.findOepsRegistered(activeOnly)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves horses with FEI registration.
|
|
||||||
*
|
|
||||||
* @param activeOnly Whether to return only active horses (default: true)
|
|
||||||
* @return List of FEI registered horses
|
|
||||||
*/
|
|
||||||
suspend fun getFeiRegistered(activeOnly: Boolean = true): List<DomPferd> {
|
|
||||||
return horseRepository.findFeiRegistered(activeOnly)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if a horse with the given life number exists.
|
|
||||||
*
|
|
||||||
* @param lebensnummer The life number to check
|
|
||||||
* @return true if a horse with this life number exists, false otherwise
|
|
||||||
*/
|
|
||||||
suspend fun existsByLebensnummer(lebensnummer: String): Boolean {
|
|
||||||
require(lebensnummer.isNotBlank()) { "Life number cannot be blank" }
|
|
||||||
return horseRepository.existsByLebensnummer(lebensnummer.trim())
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if a horse with the given chip number exists.
|
|
||||||
*
|
|
||||||
* @param chipNummer The chip number to check
|
|
||||||
* @return true if a horse with this chip number exists, false otherwise
|
|
||||||
*/
|
|
||||||
suspend fun existsByChipNummer(chipNummer: String): Boolean {
|
|
||||||
require(chipNummer.isNotBlank()) { "Chip number cannot be blank" }
|
|
||||||
return horseRepository.existsByChipNummer(chipNummer.trim())
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if a horse with the given passport number exists.
|
|
||||||
*
|
|
||||||
* @param passNummer The passport number to check
|
|
||||||
* @return true if a horse with this passport number exists, false otherwise
|
|
||||||
*/
|
|
||||||
suspend fun existsByPassNummer(passNummer: String): Boolean {
|
|
||||||
require(passNummer.isNotBlank()) { "Passport number cannot be blank" }
|
|
||||||
return horseRepository.existsByPassNummer(passNummer.trim())
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if a horse with the given OEPS number exists.
|
|
||||||
*
|
|
||||||
* @param oepsNummer The OEPS number to check
|
|
||||||
* @return true if a horse with this OEPS number exists, false otherwise
|
|
||||||
*/
|
|
||||||
suspend fun existsByOepsNummer(oepsNummer: String): Boolean {
|
|
||||||
require(oepsNummer.isNotBlank()) { "OEPS number cannot be blank" }
|
|
||||||
return horseRepository.existsByOepsNummer(oepsNummer.trim())
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if a horse with the given FEI number exists.
|
|
||||||
*
|
|
||||||
* @param feiNummer The FEI number to check
|
|
||||||
* @return true if a horse with this FEI number exists, false otherwise
|
|
||||||
*/
|
|
||||||
suspend fun existsByFeiNummer(feiNummer: String): Boolean {
|
|
||||||
require(feiNummer.isNotBlank()) { "FEI number cannot be blank" }
|
|
||||||
return horseRepository.existsByFeiNummer(feiNummer.trim())
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Counts the total number of active horses.
|
|
||||||
*
|
|
||||||
* @return The total count of active horses
|
|
||||||
*/
|
|
||||||
suspend fun countActive(): Long {
|
|
||||||
return horseRepository.countActive()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Counts horses by owner.
|
|
||||||
*
|
|
||||||
* @param ownerId The ID of the owner
|
|
||||||
* @param activeOnly Whether to count only active horses (default: true)
|
|
||||||
* @return The count of horses owned by the person
|
|
||||||
*/
|
|
||||||
suspend fun countByOwnerId(ownerId: Uuid, activeOnly: Boolean = true): Long {
|
|
||||||
return horseRepository.countByOwnerId(ownerId, activeOnly)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Counts horses with OEPS registration.
|
|
||||||
*
|
|
||||||
* @param activeOnly Whether to count only active horses (default: true)
|
|
||||||
* @return The count of OEPS registered horses
|
|
||||||
*/
|
|
||||||
suspend fun countOepsRegistered(activeOnly: Boolean = true): Long {
|
|
||||||
return horseRepository.countOepsRegistered(activeOnly)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Counts horses with FEI registration.
|
|
||||||
*
|
|
||||||
* @param activeOnly Whether to count only active horses (default: true)
|
|
||||||
* @return The count of FEI registered horses
|
|
||||||
*/
|
|
||||||
suspend fun countFeiRegistered(activeOnly: Boolean = true): Long {
|
|
||||||
return horseRepository.countFeiRegistered(activeOnly)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-256
@@ -1,256 +0,0 @@
|
|||||||
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
|
||||||
package at.mocode.horses.application.usecase
|
|
||||||
|
|
||||||
import at.mocode.horses.domain.model.DomPferd
|
|
||||||
import at.mocode.horses.domain.repository.HorseRepository
|
|
||||||
import at.mocode.core.domain.model.PferdeGeschlechtE
|
|
||||||
import at.mocode.core.domain.model.DatenQuelleE
|
|
||||||
import at.mocode.core.domain.model.ApiResponse
|
|
||||||
import at.mocode.core.domain.model.ErrorDto
|
|
||||||
import at.mocode.core.domain.model.ValidationResult
|
|
||||||
import at.mocode.core.domain.model.ValidationError
|
|
||||||
import at.mocode.core.utils.database.DatabaseFactory
|
|
||||||
import kotlin.uuid.Uuid
|
|
||||||
import kotlinx.datetime.LocalDate
|
|
||||||
import kotlinx.datetime.todayIn
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Transactional version of CreateHorseUseCase that ensures all database operations
|
|
||||||
* run within a single transaction to maintain data consistency.
|
|
||||||
*
|
|
||||||
* This use case handles the business logic for horse registration including
|
|
||||||
* validation, uniqueness checks, and persistence - all within a single transaction.
|
|
||||||
*/
|
|
||||||
class TransactionalCreateHorseUseCase(
|
|
||||||
private val horseRepository: HorseRepository
|
|
||||||
) {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Request data for creating a new horse.
|
|
||||||
*/
|
|
||||||
data class CreateHorseRequest(
|
|
||||||
val pferdeName: String,
|
|
||||||
val geschlecht: PferdeGeschlechtE,
|
|
||||||
val geburtsdatum: LocalDate? = null,
|
|
||||||
val rasse: String? = null,
|
|
||||||
val farbe: String? = null,
|
|
||||||
val besitzerId: Uuid? = null,
|
|
||||||
val verantwortlichePersonId: Uuid? = null,
|
|
||||||
val zuechterName: String? = null,
|
|
||||||
val zuchtbuchNummer: String? = null,
|
|
||||||
val lebensnummer: String? = null,
|
|
||||||
val chipNummer: String? = null,
|
|
||||||
val passNummer: String? = null,
|
|
||||||
val oepsNummer: String? = null,
|
|
||||||
val feiNummer: String? = null,
|
|
||||||
val vaterName: String? = null,
|
|
||||||
val mutterName: String? = null,
|
|
||||||
val mutterVaterName: String? = null,
|
|
||||||
val stockmass: Int? = null,
|
|
||||||
val bemerkungen: String? = null,
|
|
||||||
val datenQuelle: DatenQuelleE = DatenQuelleE.MANUELL
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Executes the horse creation use case within a single transaction.
|
|
||||||
*
|
|
||||||
* @param request The horse creation request data
|
|
||||||
* @return ApiResponse with the created horse or validation errors
|
|
||||||
*/
|
|
||||||
suspend fun execute(request: CreateHorseRequest): ApiResponse<DomPferd> {
|
|
||||||
println("[DEBUG_LOG] TransactionalCreateHorseUseCase.execute() called for horse: ${request.pferdeName}")
|
|
||||||
|
|
||||||
// Wrap the entire use case logic in a single transaction
|
|
||||||
return DatabaseFactory.dbQuery {
|
|
||||||
println("[DEBUG_LOG] Inside transaction for horse: ${request.pferdeName}")
|
|
||||||
// Create domain object
|
|
||||||
val horse = DomPferd(
|
|
||||||
pferdeName = request.pferdeName,
|
|
||||||
geschlecht = request.geschlecht,
|
|
||||||
geburtsdatum = request.geburtsdatum,
|
|
||||||
rasse = request.rasse,
|
|
||||||
farbe = request.farbe,
|
|
||||||
besitzerId = request.besitzerId,
|
|
||||||
verantwortlichePersonId = request.verantwortlichePersonId,
|
|
||||||
zuechterName = request.zuechterName,
|
|
||||||
zuchtbuchNummer = request.zuchtbuchNummer,
|
|
||||||
lebensnummer = request.lebensnummer,
|
|
||||||
chipNummer = request.chipNummer,
|
|
||||||
passNummer = request.passNummer,
|
|
||||||
oepsNummer = request.oepsNummer,
|
|
||||||
feiNummer = request.feiNummer,
|
|
||||||
vaterName = request.vaterName,
|
|
||||||
mutterName = request.mutterName,
|
|
||||||
mutterVaterName = request.mutterVaterName,
|
|
||||||
stockmass = request.stockmass,
|
|
||||||
bemerkungen = request.bemerkungen,
|
|
||||||
datenQuelle = request.datenQuelle
|
|
||||||
)
|
|
||||||
|
|
||||||
// Validate the horse
|
|
||||||
println("[DEBUG_LOG] Starting validation for horse: ${horse.pferdeName}")
|
|
||||||
val validationResult = validateHorse(horse)
|
|
||||||
if (!validationResult.isValid()) {
|
|
||||||
val errors = (validationResult as ValidationResult.Invalid).errors
|
|
||||||
println("[DEBUG_LOG] Validation failed for horse: ${horse.pferdeName}, errors: ${errors.map { "${it.field}: ${it.message}" }}")
|
|
||||||
return@dbQuery ApiResponse(
|
|
||||||
success = false,
|
|
||||||
data = null,
|
|
||||||
error = ErrorDto(
|
|
||||||
code = "VALIDATION_ERROR",
|
|
||||||
message = "Horse validation failed",
|
|
||||||
details = errors.associate { it.field to it.message }
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
println("[DEBUG_LOG] Validation passed for horse: ${horse.pferdeName}")
|
|
||||||
|
|
||||||
// Check for uniqueness constraints - all within the same transaction
|
|
||||||
println("[DEBUG_LOG] Starting uniqueness check for horse: ${horse.pferdeName}")
|
|
||||||
val uniquenessResult = checkUniquenessConstraints(horse)
|
|
||||||
if (!uniquenessResult.isValid()) {
|
|
||||||
val errors = (uniquenessResult as ValidationResult.Invalid).errors
|
|
||||||
println("[DEBUG_LOG] Uniqueness check failed for horse: ${horse.pferdeName}, errors: ${errors.map { "${it.field}: ${it.message}" }}")
|
|
||||||
return@dbQuery ApiResponse(
|
|
||||||
success = false,
|
|
||||||
data = null,
|
|
||||||
error = ErrorDto(
|
|
||||||
code = "UNIQUENESS_ERROR",
|
|
||||||
message = "Horse uniqueness validation failed",
|
|
||||||
details = errors.associate { it.field to it.message }
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
println("[DEBUG_LOG] Uniqueness check passed for horse: ${horse.pferdeName}")
|
|
||||||
|
|
||||||
// Save the horse - still within the same transaction
|
|
||||||
println("[DEBUG_LOG] Saving horse: ${horse.pferdeName}")
|
|
||||||
try {
|
|
||||||
val savedHorse = horseRepository.save(horse)
|
|
||||||
println("[DEBUG_LOG] Horse saved successfully: ${savedHorse.pferdeName} with ID: ${savedHorse.pferdId}")
|
|
||||||
|
|
||||||
ApiResponse(
|
|
||||||
success = true,
|
|
||||||
data = savedHorse,
|
|
||||||
message = "Horse created successfully"
|
|
||||||
)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
println("[DEBUG_LOG] Database constraint violation for horse: ${horse.pferdeName}, error: ${e.message}")
|
|
||||||
|
|
||||||
// Handle database constraint violations (duplicate keys)
|
|
||||||
if (e.message?.contains("unique", ignoreCase = true) == true ||
|
|
||||||
e.message?.contains("duplicate", ignoreCase = true) == true) {
|
|
||||||
|
|
||||||
// Determine which field caused the constraint violation
|
|
||||||
val constraintField = when {
|
|
||||||
e.message?.contains("lebensnummer", ignoreCase = true) == true -> "lebensnummer"
|
|
||||||
e.message?.contains("chip_nummer", ignoreCase = true) == true -> "chipNummer"
|
|
||||||
e.message?.contains("pass_nummer", ignoreCase = true) == true -> "passNummer"
|
|
||||||
e.message?.contains("oeps_nummer", ignoreCase = true) == true -> "oepsNummer"
|
|
||||||
e.message?.contains("fei_nummer", ignoreCase = true) == true -> "feiNummer"
|
|
||||||
else -> "identification"
|
|
||||||
}
|
|
||||||
|
|
||||||
ApiResponse(
|
|
||||||
success = false,
|
|
||||||
data = null,
|
|
||||||
error = ErrorDto(
|
|
||||||
code = "UNIQUENESS_ERROR",
|
|
||||||
message = "Horse uniqueness validation failed due to database constraint",
|
|
||||||
details = mapOf(constraintField to "A horse with this ${constraintField} already exists")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
// Re-throw other exceptions
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates the horse data according to business rules.
|
|
||||||
*/
|
|
||||||
private fun validateHorse(horse: DomPferd): ValidationResult {
|
|
||||||
val errors = mutableListOf<ValidationError>()
|
|
||||||
|
|
||||||
// Use domain validation
|
|
||||||
val domainErrors = horse.validateForRegistration()
|
|
||||||
domainErrors.forEach { errorMessage ->
|
|
||||||
errors.add(ValidationError("horse", errorMessage, "DOMAIN_VALIDATION"))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Additional business validations
|
|
||||||
horse.stockmass?.let { height ->
|
|
||||||
if (height < 50 || height > 220) {
|
|
||||||
errors.add(ValidationError("stockmass", "Horse height must be between 50 and 220 cm", "INVALID_RANGE"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
horse.geburtsdatum?.let { birthDate ->
|
|
||||||
val currentYear = kotlinx.datetime.Clock.System.todayIn(kotlinx.datetime.TimeZone.currentSystemDefault()).year
|
|
||||||
if (birthDate.year > currentYear) {
|
|
||||||
errors.add(ValidationError("geburtsdatum", "Birth date cannot be in the future", "FUTURE_DATE"))
|
|
||||||
}
|
|
||||||
if (birthDate.year < (currentYear - 50)) {
|
|
||||||
errors.add(ValidationError("geburtsdatum", "Birth date cannot be more than 50 years ago", "TOO_OLD"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return if (errors.isEmpty()) {
|
|
||||||
ValidationResult.Valid
|
|
||||||
} else {
|
|
||||||
ValidationResult.Invalid(errors)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks uniqueness constraints for identification numbers.
|
|
||||||
* Note: This method is called within a transaction, so all repository calls
|
|
||||||
* will use the same transaction context.
|
|
||||||
*/
|
|
||||||
private suspend fun checkUniquenessConstraints(horse: DomPferd): ValidationResult {
|
|
||||||
val errors = mutableListOf<ValidationError>()
|
|
||||||
|
|
||||||
// Check lebensnummer uniqueness
|
|
||||||
horse.lebensnummer?.let { lebensnummer ->
|
|
||||||
if (lebensnummer.isNotBlank() && horseRepository.existsByLebensnummer(lebensnummer)) {
|
|
||||||
errors.add(ValidationError("lebensnummer", "A horse with this life number already exists", "DUPLICATE"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check chip number uniqueness
|
|
||||||
horse.chipNummer?.let { chipNummer ->
|
|
||||||
if (chipNummer.isNotBlank() && horseRepository.existsByChipNummer(chipNummer)) {
|
|
||||||
errors.add(ValidationError("chipNummer", "A horse with this chip number already exists", "DUPLICATE"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check passport number uniqueness
|
|
||||||
horse.passNummer?.let { passNummer ->
|
|
||||||
if (passNummer.isNotBlank() && horseRepository.existsByPassNummer(passNummer)) {
|
|
||||||
errors.add(ValidationError("passNummer", "A horse with this passport number already exists", "DUPLICATE"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check OEPS number uniqueness
|
|
||||||
horse.oepsNummer?.let { oepsNummer ->
|
|
||||||
if (oepsNummer.isNotBlank() && horseRepository.existsByOepsNummer(oepsNummer)) {
|
|
||||||
errors.add(ValidationError("oepsNummer", "A horse with this OEPS number already exists", "DUPLICATE"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check FEI number uniqueness
|
|
||||||
horse.feiNummer?.let { feiNummer ->
|
|
||||||
if (feiNummer.isNotBlank() && horseRepository.existsByFeiNummer(feiNummer)) {
|
|
||||||
errors.add(ValidationError("feiNummer", "A horse with this FEI number already exists", "DUPLICATE"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return if (errors.isEmpty()) {
|
|
||||||
ValidationResult.Valid
|
|
||||||
} else {
|
|
||||||
ValidationResult.Invalid(errors)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-213
@@ -1,213 +0,0 @@
|
|||||||
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
|
||||||
package at.mocode.horses.application.usecase
|
|
||||||
|
|
||||||
import at.mocode.horses.domain.model.DomPferd
|
|
||||||
import at.mocode.horses.domain.repository.HorseRepository
|
|
||||||
import at.mocode.core.domain.model.PferdeGeschlechtE
|
|
||||||
import at.mocode.core.domain.model.DatenQuelleE
|
|
||||||
import kotlin.uuid.Uuid
|
|
||||||
import kotlinx.datetime.LocalDate
|
|
||||||
import kotlinx.datetime.todayIn
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Use case for updating an existing horse in the registry.
|
|
||||||
*
|
|
||||||
* This use case handles the business logic for horse updates including
|
|
||||||
* validation, uniqueness checks, and persistence.
|
|
||||||
*/
|
|
||||||
class UpdateHorseUseCase(
|
|
||||||
private val horseRepository: HorseRepository
|
|
||||||
) {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Request data for updating a horse.
|
|
||||||
*/
|
|
||||||
data class UpdateHorseRequest(
|
|
||||||
val pferdId: Uuid,
|
|
||||||
val pferdeName: String,
|
|
||||||
val geschlecht: PferdeGeschlechtE,
|
|
||||||
val geburtsdatum: LocalDate? = null,
|
|
||||||
val rasse: String? = null,
|
|
||||||
val farbe: String? = null,
|
|
||||||
val besitzerId: Uuid? = null,
|
|
||||||
val verantwortlichePersonId: Uuid? = null,
|
|
||||||
val zuechterName: String? = null,
|
|
||||||
val zuchtbuchNummer: String? = null,
|
|
||||||
val lebensnummer: String? = null,
|
|
||||||
val chipNummer: String? = null,
|
|
||||||
val passNummer: String? = null,
|
|
||||||
val oepsNummer: String? = null,
|
|
||||||
val feiNummer: String? = null,
|
|
||||||
val vaterName: String? = null,
|
|
||||||
val mutterName: String? = null,
|
|
||||||
val mutterVaterName: String? = null,
|
|
||||||
val stockmass: Int? = null,
|
|
||||||
val istAktiv: Boolean = true,
|
|
||||||
val bemerkungen: String? = null,
|
|
||||||
val datenQuelle: DatenQuelleE = DatenQuelleE.MANUELL
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Response data for horse update.
|
|
||||||
*/
|
|
||||||
data class UpdateHorseResponse(
|
|
||||||
val horse: DomPferd?,
|
|
||||||
val success: Boolean,
|
|
||||||
val errors: List<String> = emptyList()
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Executes the horse update use case.
|
|
||||||
*
|
|
||||||
* @param request The horse update request data
|
|
||||||
* @return UpdateHorseResponse with the updated horse or validation errors
|
|
||||||
*/
|
|
||||||
suspend fun execute(request: UpdateHorseRequest): UpdateHorseResponse {
|
|
||||||
// Check if horse exists
|
|
||||||
val existingHorse = horseRepository.findById(request.pferdId)
|
|
||||||
?: return UpdateHorseResponse(
|
|
||||||
horse = null,
|
|
||||||
success = false,
|
|
||||||
errors = listOf("Horse not found")
|
|
||||||
)
|
|
||||||
|
|
||||||
// Create updated domain object
|
|
||||||
val updatedHorse = existingHorse.copy(
|
|
||||||
pferdeName = request.pferdeName,
|
|
||||||
geschlecht = request.geschlecht,
|
|
||||||
geburtsdatum = request.geburtsdatum,
|
|
||||||
rasse = request.rasse,
|
|
||||||
farbe = request.farbe,
|
|
||||||
besitzerId = request.besitzerId,
|
|
||||||
verantwortlichePersonId = request.verantwortlichePersonId,
|
|
||||||
zuechterName = request.zuechterName,
|
|
||||||
zuchtbuchNummer = request.zuchtbuchNummer,
|
|
||||||
lebensnummer = request.lebensnummer,
|
|
||||||
chipNummer = request.chipNummer,
|
|
||||||
passNummer = request.passNummer,
|
|
||||||
oepsNummer = request.oepsNummer,
|
|
||||||
feiNummer = request.feiNummer,
|
|
||||||
vaterName = request.vaterName,
|
|
||||||
mutterName = request.mutterName,
|
|
||||||
mutterVaterName = request.mutterVaterName,
|
|
||||||
stockmass = request.stockmass,
|
|
||||||
istAktiv = request.istAktiv,
|
|
||||||
bemerkungen = request.bemerkungen,
|
|
||||||
datenQuelle = request.datenQuelle
|
|
||||||
)
|
|
||||||
|
|
||||||
// Validate the updated horse
|
|
||||||
val validationErrors = validateHorse(updatedHorse)
|
|
||||||
if (validationErrors.isNotEmpty()) {
|
|
||||||
return UpdateHorseResponse(
|
|
||||||
horse = updatedHorse,
|
|
||||||
success = false,
|
|
||||||
errors = validationErrors
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for uniqueness constraints (excluding current horse)
|
|
||||||
val uniquenessErrors = checkUniquenessConstraints(updatedHorse, existingHorse)
|
|
||||||
if (uniquenessErrors.isNotEmpty()) {
|
|
||||||
return UpdateHorseResponse(
|
|
||||||
horse = updatedHorse,
|
|
||||||
success = false,
|
|
||||||
errors = uniquenessErrors
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save the updated horse
|
|
||||||
val savedHorse = horseRepository.save(updatedHorse)
|
|
||||||
|
|
||||||
return UpdateHorseResponse(
|
|
||||||
horse = savedHorse,
|
|
||||||
success = true
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates the horse data according to business rules.
|
|
||||||
*/
|
|
||||||
private fun validateHorse(horse: DomPferd): List<String> {
|
|
||||||
val errors = mutableListOf<String>()
|
|
||||||
|
|
||||||
// Basic validation
|
|
||||||
if (horse.pferdeName.isBlank()) {
|
|
||||||
errors.add("Horse name is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Height validation
|
|
||||||
horse.stockmass?.let { height ->
|
|
||||||
if (height < 50 || height > 220) {
|
|
||||||
errors.add("Horse height must be between 50 and 220 cm")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Birth date validation
|
|
||||||
horse.geburtsdatum?.let { birthDate ->
|
|
||||||
val currentYear = kotlinx.datetime.Clock.System.todayIn(kotlinx.datetime.TimeZone.currentSystemDefault()).year
|
|
||||||
if (birthDate.year > currentYear) {
|
|
||||||
errors.add("Birth date cannot be in the future")
|
|
||||||
}
|
|
||||||
if (birthDate.year < (currentYear - 50)) {
|
|
||||||
errors.add("Birth date cannot be more than 50 years ago")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks uniqueness constraints for identification numbers, excluding the current horse.
|
|
||||||
*/
|
|
||||||
private suspend fun checkUniquenessConstraints(updatedHorse: DomPferd, existingHorse: DomPferd): List<String> {
|
|
||||||
val errors = mutableListOf<String>()
|
|
||||||
|
|
||||||
// Check lebensnummer uniqueness (if changed)
|
|
||||||
updatedHorse.lebensnummer?.let { lebensnummer ->
|
|
||||||
if (lebensnummer.isNotBlank() &&
|
|
||||||
lebensnummer != existingHorse.lebensnummer &&
|
|
||||||
horseRepository.existsByLebensnummer(lebensnummer)) {
|
|
||||||
errors.add("A horse with this life number already exists")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check chip number uniqueness (if changed)
|
|
||||||
updatedHorse.chipNummer?.let { chipNummer ->
|
|
||||||
if (chipNummer.isNotBlank() &&
|
|
||||||
chipNummer != existingHorse.chipNummer &&
|
|
||||||
horseRepository.existsByChipNummer(chipNummer)) {
|
|
||||||
errors.add("A horse with this chip number already exists")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check passport number uniqueness (if changed)
|
|
||||||
updatedHorse.passNummer?.let { passNummer ->
|
|
||||||
if (passNummer.isNotBlank() &&
|
|
||||||
passNummer != existingHorse.passNummer &&
|
|
||||||
horseRepository.existsByPassNummer(passNummer)) {
|
|
||||||
errors.add("A horse with this passport number already exists")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check OEPS number uniqueness (if changed)
|
|
||||||
updatedHorse.oepsNummer?.let { oepsNummer ->
|
|
||||||
if (oepsNummer.isNotBlank() &&
|
|
||||||
oepsNummer != existingHorse.oepsNummer &&
|
|
||||||
horseRepository.existsByOepsNummer(oepsNummer)) {
|
|
||||||
errors.add("A horse with this OEPS number already exists")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check FEI number uniqueness (if changed)
|
|
||||||
updatedHorse.feiNummer?.let { feiNummer ->
|
|
||||||
if (feiNummer.isNotBlank() &&
|
|
||||||
feiNummer != existingHorse.feiNummer &&
|
|
||||||
horseRepository.existsByFeiNummer(feiNummer)) {
|
|
||||||
errors.add("A horse with this FEI number already exists")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
plugins {
|
|
||||||
alias(libs.plugins.kotlinMultiplatform)
|
|
||||||
alias(libs.plugins.kotlinSerialization)
|
|
||||||
}
|
|
||||||
|
|
||||||
kotlin {
|
|
||||||
jvm()
|
|
||||||
|
|
||||||
sourceSets {
|
|
||||||
commonMain {
|
|
||||||
kotlin.srcDir("src/main/kotlin")
|
|
||||||
dependencies {
|
|
||||||
implementation(projects.core.coreDomain)
|
|
||||||
implementation(projects.core.coreUtils)
|
|
||||||
implementation(libs.kotlinx.datetime)
|
|
||||||
implementation(libs.kotlinx.serialization.json)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
commonTest {
|
|
||||||
kotlin.srcDir("src/test/kotlin")
|
|
||||||
dependencies {
|
|
||||||
implementation(projects.platform.platformTesting)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-174
@@ -1,174 +0,0 @@
|
|||||||
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
|
||||||
package at.mocode.horses.domain.model
|
|
||||||
|
|
||||||
import at.mocode.core.domain.model.DatenQuelleE
|
|
||||||
import at.mocode.core.domain.model.PferdeGeschlechtE
|
|
||||||
import at.mocode.core.domain.serialization.KotlinInstantSerializer
|
|
||||||
import at.mocode.core.domain.serialization.UuidSerializer
|
|
||||||
import kotlinx.datetime.LocalDate
|
|
||||||
import kotlinx.datetime.number
|
|
||||||
import kotlinx.datetime.todayIn
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import kotlin.time.Clock
|
|
||||||
import kotlin.time.Instant
|
|
||||||
import kotlin.uuid.Uuid
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Domain model representing a horse in the registry system.
|
|
||||||
*
|
|
||||||
* This entity contains all essential information about a horse including
|
|
||||||
* identification, ownership, breeding information, and administrative data.
|
|
||||||
* It serves as the core aggregate root for the horse-registry bounded context.
|
|
||||||
*
|
|
||||||
* @property pferdId Unique internal identifier for this horse (UUID).
|
|
||||||
* @property pferdeName Name of the horse.
|
|
||||||
* @property geschlecht Gender of the horse (Hengst, Stute, Wallach).
|
|
||||||
* @property geburtsdatum Birthdate of the horse.
|
|
||||||
* @property rasse Breed of the horse.
|
|
||||||
* @property farbe Color/coat of the horse.
|
|
||||||
* @property besitzerId ID of the current owner (Person from member-management context).
|
|
||||||
* @property verantwortlichePersonId ID of the responsible person (trainer, rider, etc.).
|
|
||||||
* @property zuechterName Name of the breeder.
|
|
||||||
* @property zuchtbuchNummer Studbook number if registered.
|
|
||||||
* @property lebensnummer Life number (unique identification number).
|
|
||||||
* @property chipNummer Microchip number for identification.
|
|
||||||
* @property passNummer Passport number.
|
|
||||||
* @property oepsNummer OEPS (Austrian Equestrian Federation) number.
|
|
||||||
* @property feiNummer FEI (International Equestrian Federation) number.
|
|
||||||
* @property vaterName Name of the sire (father).
|
|
||||||
* @property mutterName Name of the dam (mother).
|
|
||||||
* @property mutterVaterName Name of the maternal grandsire.
|
|
||||||
* @property stockmass Height of the horse in cm.
|
|
||||||
* @property istAktiv Whether the horse is currently active in the system.
|
|
||||||
* @property bemerkungen Additional notes or comments.
|
|
||||||
* @property datenQuelle Source of the data (manual entry, import, etc.).
|
|
||||||
* @property createdAt Timestamp when this record was created.
|
|
||||||
* @property updatedAt Timestamp when this record was last updated.
|
|
||||||
*/
|
|
||||||
@Serializable
|
|
||||||
data class DomPferd(
|
|
||||||
@Serializable(with = UuidSerializer::class)
|
|
||||||
val pferdId: Uuid = Uuid.random(),
|
|
||||||
|
|
||||||
// Basic Information
|
|
||||||
var pferdeName: String,
|
|
||||||
var geschlecht: PferdeGeschlechtE,
|
|
||||||
var geburtsdatum: LocalDate? = null,
|
|
||||||
var rasse: String? = null,
|
|
||||||
var farbe: String? = null,
|
|
||||||
|
|
||||||
// Ownership and Responsibility
|
|
||||||
@Serializable(with = UuidSerializer::class)
|
|
||||||
var besitzerId: Uuid? = null,
|
|
||||||
@Serializable(with = UuidSerializer::class)
|
|
||||||
var verantwortlichePersonId: Uuid? = null,
|
|
||||||
|
|
||||||
// Breeding Information
|
|
||||||
var zuechterName: String? = null,
|
|
||||||
var zuchtbuchNummer: String? = null,
|
|
||||||
|
|
||||||
// Identification Numbers
|
|
||||||
var lebensnummer: String? = null,
|
|
||||||
var chipNummer: String? = null,
|
|
||||||
var passNummer: String? = null,
|
|
||||||
var oepsNummer: String? = null,
|
|
||||||
var feiNummer: String? = null,
|
|
||||||
|
|
||||||
// Pedigree Information
|
|
||||||
var vaterName: String? = null,
|
|
||||||
var mutterName: String? = null,
|
|
||||||
var mutterVaterName: String? = null,
|
|
||||||
|
|
||||||
// Physical Characteristics
|
|
||||||
var stockmass: Int? = null, // Height in cm
|
|
||||||
|
|
||||||
// Status and Administrative
|
|
||||||
var istAktiv: Boolean = true,
|
|
||||||
var bemerkungen: String? = null,
|
|
||||||
var datenQuelle: DatenQuelleE = DatenQuelleE.MANUELL,
|
|
||||||
|
|
||||||
// Audit Fields
|
|
||||||
@Serializable(with = KotlinInstantSerializer::class)
|
|
||||||
val createdAt: Instant = Clock.System.now(),
|
|
||||||
@Serializable(with = KotlinInstantSerializer::class)
|
|
||||||
var updatedAt: Instant = Clock.System.now()
|
|
||||||
) {
|
|
||||||
/**
|
|
||||||
* Returns the display name for the horse, combining name and birth year if available.
|
|
||||||
*/
|
|
||||||
fun getDisplayName(): String {
|
|
||||||
return geburtsdatum?.let { birthDate ->
|
|
||||||
"$pferdeName (${birthDate.year})"
|
|
||||||
} ?: pferdeName
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the horse has complete identification information.
|
|
||||||
*/
|
|
||||||
fun hasCompleteIdentification(): Boolean {
|
|
||||||
return !lebensnummer.isNullOrBlank() ||
|
|
||||||
!chipNummer.isNullOrBlank() ||
|
|
||||||
!passNummer.isNullOrBlank()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the horse is registered with OEPS.
|
|
||||||
*/
|
|
||||||
fun isOepsRegistered(): Boolean {
|
|
||||||
return !oepsNummer.isNullOrBlank()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the horse is registered with FEI.
|
|
||||||
*/
|
|
||||||
fun isFeiRegistered(): Boolean {
|
|
||||||
return !feiNummer.isNullOrBlank()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the age of the horse in years, or null if birth date is unknown.
|
|
||||||
*/
|
|
||||||
fun getAge(): Int? {
|
|
||||||
return geburtsdatum?.let { birthDate ->
|
|
||||||
val today = Clock.System.todayIn(kotlinx.datetime.TimeZone.currentSystemDefault())
|
|
||||||
var age = today.year - birthDate.year
|
|
||||||
|
|
||||||
// Check if a birthday has occurred this year
|
|
||||||
if (today.month.number < birthDate.month.number ||
|
|
||||||
(today.month.number == birthDate.month.number && today.day < birthDate.day)
|
|
||||||
) {
|
|
||||||
age--
|
|
||||||
}
|
|
||||||
|
|
||||||
age
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates that required fields are present for horse registration.
|
|
||||||
*/
|
|
||||||
fun validateForRegistration(): List<String> {
|
|
||||||
val errors = mutableListOf<String>()
|
|
||||||
|
|
||||||
if (pferdeName.isBlank()) {
|
|
||||||
errors.add("Horse name is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!hasCompleteIdentification()) {
|
|
||||||
errors.add("At least one identification number (life number, chip number, or passport number) is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (besitzerId == null) {
|
|
||||||
errors.add("Owner is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a copy of this horse with an updated timestamp.
|
|
||||||
*/
|
|
||||||
fun withUpdatedTimestamp(): DomPferd {
|
|
||||||
return this.copy(updatedAt = Clock.System.now())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-243
@@ -1,243 +0,0 @@
|
|||||||
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
|
||||||
package at.mocode.horses.domain.repository
|
|
||||||
|
|
||||||
import at.mocode.horses.domain.model.DomPferd
|
|
||||||
import at.mocode.core.domain.model.PferdeGeschlechtE
|
|
||||||
import kotlin.uuid.Uuid
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Repository interface for DomPferd (Horse) domain operations.
|
|
||||||
*
|
|
||||||
* This interface defines the contract for horse data access operations
|
|
||||||
* without depending on specific implementation details (database, etc.).
|
|
||||||
* Following the hexagonal architecture pattern, this interface belongs
|
|
||||||
* to the domain layer and will be implemented in the infrastructure layer.
|
|
||||||
*/
|
|
||||||
interface HorseRepository {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds a horse by its unique ID.
|
|
||||||
*
|
|
||||||
* @param id The unique identifier of the horse
|
|
||||||
* @return The horse if found, null otherwise
|
|
||||||
*/
|
|
||||||
suspend fun findById(id: Uuid): DomPferd?
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds a horse by its life number (Lebensnummer).
|
|
||||||
*
|
|
||||||
* @param lebensnummer The life number to search for
|
|
||||||
* @return The horse if found, null otherwise
|
|
||||||
*/
|
|
||||||
suspend fun findByLebensnummer(lebensnummer: String): DomPferd?
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds a horse by its chip number.
|
|
||||||
*
|
|
||||||
* @param chipNummer The chip number to search for
|
|
||||||
* @return The horse if found, null otherwise
|
|
||||||
*/
|
|
||||||
suspend fun findByChipNummer(chipNummer: String): DomPferd?
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds a horse by its passport number.
|
|
||||||
*
|
|
||||||
* @param passNummer The passport number to search for
|
|
||||||
* @return The horse if found, null otherwise
|
|
||||||
*/
|
|
||||||
suspend fun findByPassNummer(passNummer: String): DomPferd?
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds a horse by its OEPS number.
|
|
||||||
*
|
|
||||||
* @param oepsNummer The OEPS number to search for
|
|
||||||
* @return The horse if found, null otherwise
|
|
||||||
*/
|
|
||||||
suspend fun findByOepsNummer(oepsNummer: String): DomPferd?
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds a horse by its FEI number.
|
|
||||||
*
|
|
||||||
* @param feiNummer The FEI number to search for
|
|
||||||
* @return The horse if found, null otherwise
|
|
||||||
*/
|
|
||||||
suspend fun findByFeiNummer(feiNummer: String): DomPferd?
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds horses by name (partial match).
|
|
||||||
*
|
|
||||||
* @param searchTerm The search term to match against horse names
|
|
||||||
* @param limit Maximum number of results to return
|
|
||||||
* @return List of matching horses
|
|
||||||
*/
|
|
||||||
suspend fun findByName(searchTerm: String, limit: Int = 50): List<DomPferd>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds all horses owned by a specific person.
|
|
||||||
*
|
|
||||||
* @param ownerId The ID of the owner (from member-management context)
|
|
||||||
* @param activeOnly Whether to return only active horses
|
|
||||||
* @return List of horses owned by the person
|
|
||||||
*/
|
|
||||||
suspend fun findByOwnerId(ownerId: Uuid, activeOnly: Boolean = true): List<DomPferd>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds all horses for which a person is responsible.
|
|
||||||
*
|
|
||||||
* @param responsiblePersonId The ID of the responsible person
|
|
||||||
* @param activeOnly Whether to return only active horses
|
|
||||||
* @return List of horses for which the person is responsible
|
|
||||||
*/
|
|
||||||
suspend fun findByResponsiblePersonId(responsiblePersonId: Uuid, activeOnly: Boolean = true): List<DomPferd>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds horses by gender.
|
|
||||||
*
|
|
||||||
* @param geschlecht The gender to filter by
|
|
||||||
* @param activeOnly Whether to return only active horses
|
|
||||||
* @param limit Maximum number of results to return
|
|
||||||
* @return List of horses with the specified gender
|
|
||||||
*/
|
|
||||||
suspend fun findByGeschlecht(geschlecht: PferdeGeschlechtE, activeOnly: Boolean = true, limit: Int = 100): List<DomPferd>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds horses by breed.
|
|
||||||
*
|
|
||||||
* @param rasse The breed to filter by
|
|
||||||
* @param activeOnly Whether to return only active horses
|
|
||||||
* @param limit Maximum number of results to return
|
|
||||||
* @return List of horses of the specified breed
|
|
||||||
*/
|
|
||||||
suspend fun findByRasse(rasse: String, activeOnly: Boolean = true, limit: Int = 100): List<DomPferd>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds horses by birth year.
|
|
||||||
*
|
|
||||||
* @param birthYear The birth year to filter by
|
|
||||||
* @param activeOnly Whether to return only active horses
|
|
||||||
* @return List of horses born in the specified year
|
|
||||||
*/
|
|
||||||
suspend fun findByBirthYear(birthYear: Int, activeOnly: Boolean = true): List<DomPferd>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds horses by birth year range.
|
|
||||||
*
|
|
||||||
* @param fromYear The start year (inclusive)
|
|
||||||
* @param toYear The end year (inclusive)
|
|
||||||
* @param activeOnly Whether to return only active horses
|
|
||||||
* @return List of horses born within the specified year range
|
|
||||||
*/
|
|
||||||
suspend fun findByBirthYearRange(fromYear: Int, toYear: Int, activeOnly: Boolean = true): List<DomPferd>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds all active horses.
|
|
||||||
*
|
|
||||||
* @param limit Maximum number of results to return
|
|
||||||
* @return List of active horses
|
|
||||||
*/
|
|
||||||
suspend fun findAllActive(limit: Int = 1000): List<DomPferd>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds horses with OEPS registration.
|
|
||||||
*
|
|
||||||
* @param activeOnly Whether to return only active horses
|
|
||||||
* @return List of OEPS registered horses
|
|
||||||
*/
|
|
||||||
suspend fun findOepsRegistered(activeOnly: Boolean = true): List<DomPferd>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds horses with FEI registration.
|
|
||||||
*
|
|
||||||
* @param activeOnly Whether to return only active horses
|
|
||||||
* @return List of FEI registered horses
|
|
||||||
*/
|
|
||||||
suspend fun findFeiRegistered(activeOnly: Boolean = true): List<DomPferd>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Saves a horse (create or update).
|
|
||||||
*
|
|
||||||
* @param horse The horse to save
|
|
||||||
* @return The saved horse with updated timestamps
|
|
||||||
*/
|
|
||||||
suspend fun save(horse: DomPferd): DomPferd
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deletes a horse by ID.
|
|
||||||
*
|
|
||||||
* @param id The unique identifier of the horse to delete
|
|
||||||
* @return true if the horse was deleted, false if not found
|
|
||||||
*/
|
|
||||||
suspend fun delete(id: Uuid): Boolean
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if a horse with the given life number exists.
|
|
||||||
*
|
|
||||||
* @param lebensnummer The life number to check
|
|
||||||
* @return true if a horse with this life number exists, false otherwise
|
|
||||||
*/
|
|
||||||
suspend fun existsByLebensnummer(lebensnummer: String): Boolean
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if a horse with the given chip number exists.
|
|
||||||
*
|
|
||||||
* @param chipNummer The chip number to check
|
|
||||||
* @return true if a horse with this chip number exists, false otherwise
|
|
||||||
*/
|
|
||||||
suspend fun existsByChipNummer(chipNummer: String): Boolean
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if a horse with the given passport number exists.
|
|
||||||
*
|
|
||||||
* @param passNummer The passport number to check
|
|
||||||
* @return true if a horse with this passport number exists, false otherwise
|
|
||||||
*/
|
|
||||||
suspend fun existsByPassNummer(passNummer: String): Boolean
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if a horse with the given OEPS number exists.
|
|
||||||
*
|
|
||||||
* @param oepsNummer The OEPS number to check
|
|
||||||
* @return true if a horse with this OEPS number exists, false otherwise
|
|
||||||
*/
|
|
||||||
suspend fun existsByOepsNummer(oepsNummer: String): Boolean
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if a horse with the given FEI number exists.
|
|
||||||
*
|
|
||||||
* @param feiNummer The FEI number to check
|
|
||||||
* @return true if a horse with this FEI number exists, false otherwise
|
|
||||||
*/
|
|
||||||
suspend fun existsByFeiNummer(feiNummer: String): Boolean
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Counts the total number of active horses.
|
|
||||||
*
|
|
||||||
* @return The total count of active horses
|
|
||||||
*/
|
|
||||||
suspend fun countActive(): Long
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Counts horses by owner.
|
|
||||||
*
|
|
||||||
* @param ownerId The ID of the owner
|
|
||||||
* @param activeOnly Whether to count only active horses
|
|
||||||
* @return The count of horses owned by the person
|
|
||||||
*/
|
|
||||||
suspend fun countByOwnerId(ownerId: Uuid, activeOnly: Boolean = true): Long
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Counts horses with OEPS registration.
|
|
||||||
*
|
|
||||||
* @param activeOnly Whether to count only active horses
|
|
||||||
* @return The count of OEPS registered horses
|
|
||||||
*/
|
|
||||||
suspend fun countOepsRegistered(activeOnly: Boolean = true): Long
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Counts horses with FEI registration.
|
|
||||||
*
|
|
||||||
* @param activeOnly Whether to count only active horses
|
|
||||||
* @return The count of FEI registered horses
|
|
||||||
*/
|
|
||||||
suspend fun countFeiRegistered(activeOnly: Boolean = true): Long
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
plugins {
|
|
||||||
alias(libs.plugins.kotlinJvm)
|
|
||||||
alias(libs.plugins.kotlinSpring)
|
|
||||||
alias(libs.plugins.kotlinSerialization)
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
// Interne Module
|
|
||||||
implementation(projects.platform.platformDependencies)
|
|
||||||
implementation(projects.backend.services.horses.horsesDomain)
|
|
||||||
// horses-common: ON HOLD
|
|
||||||
// implementation(projects.backend.services.horses.horsesCommon)
|
|
||||||
implementation(projects.core.coreDomain)
|
|
||||||
implementation(projects.core.coreUtils)
|
|
||||||
|
|
||||||
// Spring
|
|
||||||
implementation(libs.spring.boot.starter.web)
|
|
||||||
|
|
||||||
// Datenbank via Exposed
|
|
||||||
implementation(libs.exposed.core)
|
|
||||||
implementation(libs.exposed.dao)
|
|
||||||
implementation(libs.exposed.jdbc)
|
|
||||||
implementation(libs.exposed.kotlin.datetime)
|
|
||||||
|
|
||||||
// Datenbank-Treiber
|
|
||||||
runtimeOnly(libs.postgresql.driver)
|
|
||||||
|
|
||||||
// Testing
|
|
||||||
testImplementation(projects.platform.platformTesting)
|
|
||||||
}
|
|
||||||
-266
@@ -1,266 +0,0 @@
|
|||||||
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
|
||||||
|
|
||||||
package at.mocode.horses.infrastructure.persistence
|
|
||||||
|
|
||||||
import at.mocode.core.domain.model.PferdeGeschlechtE
|
|
||||||
import at.mocode.horses.domain.model.DomPferd
|
|
||||||
import at.mocode.horses.domain.repository.HorseRepository
|
|
||||||
import org.jetbrains.exposed.v1.core.*
|
|
||||||
import org.jetbrains.exposed.v1.core.statements.UpdateBuilder
|
|
||||||
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.transactions.transaction
|
|
||||||
import org.jetbrains.exposed.v1.jdbc.update
|
|
||||||
import kotlin.time.Clock
|
|
||||||
import kotlin.uuid.Uuid
|
|
||||||
import kotlin.uuid.toJavaUuid
|
|
||||||
import kotlin.uuid.toKotlinUuid
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Exposed-basierte Implementierung des HorseRepository (v1-API).
|
|
||||||
*/
|
|
||||||
class HorseRepositoryImpl : HorseRepository {
|
|
||||||
|
|
||||||
override suspend fun findById(id: Uuid): DomPferd? = transaction {
|
|
||||||
HorseTable.selectAll().where { HorseTable.id eq id.toJavaUuid() }
|
|
||||||
.map { rowToDomPferd(it) }
|
|
||||||
.singleOrNull()
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun findByLebensnummer(lebensnummer: String): DomPferd? = transaction {
|
|
||||||
HorseTable.selectAll().where { HorseTable.lebensnummer eq lebensnummer }
|
|
||||||
.map { rowToDomPferd(it) }
|
|
||||||
.singleOrNull()
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun findByChipNummer(chipNummer: String): DomPferd? = transaction {
|
|
||||||
HorseTable.selectAll().where { HorseTable.chipNummer eq chipNummer }
|
|
||||||
.map { rowToDomPferd(it) }
|
|
||||||
.singleOrNull()
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun findByPassNummer(passNummer: String): DomPferd? = transaction {
|
|
||||||
HorseTable.selectAll().where { HorseTable.passNummer eq passNummer }
|
|
||||||
.map { rowToDomPferd(it) }
|
|
||||||
.singleOrNull()
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun findByOepsNummer(oepsNummer: String): DomPferd? = transaction {
|
|
||||||
HorseTable.selectAll().where { HorseTable.oepsNummer eq oepsNummer }
|
|
||||||
.map { rowToDomPferd(it) }
|
|
||||||
.singleOrNull()
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun findByFeiNummer(feiNummer: String): DomPferd? = transaction {
|
|
||||||
HorseTable.selectAll().where { HorseTable.feiNummer eq feiNummer }
|
|
||||||
.map { rowToDomPferd(it) }
|
|
||||||
.singleOrNull()
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun findByName(searchTerm: String, limit: Int): List<DomPferd> = transaction {
|
|
||||||
val pattern = "%$searchTerm%"
|
|
||||||
HorseTable.selectAll().where { HorseTable.pferdeName like pattern }
|
|
||||||
.limit(limit)
|
|
||||||
.map { rowToDomPferd(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun findByOwnerId(ownerId: Uuid, activeOnly: Boolean): List<DomPferd> = transaction {
|
|
||||||
HorseTable.selectAll().where {
|
|
||||||
(HorseTable.besitzerId eq ownerId.toJavaUuid()).let {
|
|
||||||
if (activeOnly) it and (HorseTable.istAktiv eq true) else it
|
|
||||||
}
|
|
||||||
}.map { rowToDomPferd(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun findByResponsiblePersonId(responsiblePersonId: Uuid, activeOnly: Boolean): List<DomPferd> =
|
|
||||||
transaction {
|
|
||||||
HorseTable.selectAll().where {
|
|
||||||
(HorseTable.verantwortlichePersonId eq responsiblePersonId.toJavaUuid()).let {
|
|
||||||
if (activeOnly) it and (HorseTable.istAktiv eq true) else it
|
|
||||||
}
|
|
||||||
}.map { rowToDomPferd(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun findByGeschlecht(
|
|
||||||
geschlecht: PferdeGeschlechtE,
|
|
||||||
activeOnly: Boolean,
|
|
||||||
limit: Int
|
|
||||||
): List<DomPferd> = transaction {
|
|
||||||
HorseTable.selectAll().where {
|
|
||||||
(HorseTable.geschlecht eq geschlecht).let {
|
|
||||||
if (activeOnly) it and (HorseTable.istAktiv eq true) else it
|
|
||||||
}
|
|
||||||
}.limit(limit).map { rowToDomPferd(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun findByRasse(rasse: String, activeOnly: Boolean, limit: Int): List<DomPferd> = transaction {
|
|
||||||
HorseTable.selectAll().where {
|
|
||||||
(HorseTable.rasse eq rasse).let {
|
|
||||||
if (activeOnly) it and (HorseTable.istAktiv eq true) else it
|
|
||||||
}
|
|
||||||
}.limit(limit).map { rowToDomPferd(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun findByBirthYear(birthYear: Int, activeOnly: Boolean): List<DomPferd> = transaction {
|
|
||||||
HorseTable.selectAll()
|
|
||||||
.map { rowToDomPferd(it) }
|
|
||||||
.filter { it.geburtsdatum?.year == birthYear && (!activeOnly || it.istAktiv) }
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun findByBirthYearRange(fromYear: Int, toYear: Int, activeOnly: Boolean): List<DomPferd> =
|
|
||||||
transaction {
|
|
||||||
HorseTable.selectAll()
|
|
||||||
.map { rowToDomPferd(it) }
|
|
||||||
.filter {
|
|
||||||
val year = it.geburtsdatum?.year
|
|
||||||
year != null && year in fromYear..toYear && (!activeOnly || it.istAktiv)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun findAllActive(limit: Int): List<DomPferd> = transaction {
|
|
||||||
HorseTable.selectAll().where { HorseTable.istAktiv eq true }
|
|
||||||
.limit(limit)
|
|
||||||
.map { rowToDomPferd(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun findOepsRegistered(activeOnly: Boolean): List<DomPferd> = transaction {
|
|
||||||
HorseTable.selectAll().where {
|
|
||||||
HorseTable.oepsNummer.isNotNull().let {
|
|
||||||
if (activeOnly) it and (HorseTable.istAktiv eq true) else it
|
|
||||||
}
|
|
||||||
}.map { rowToDomPferd(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun findFeiRegistered(activeOnly: Boolean): List<DomPferd> = transaction {
|
|
||||||
HorseTable.selectAll().where {
|
|
||||||
HorseTable.feiNummer.isNotNull().let {
|
|
||||||
if (activeOnly) it and (HorseTable.istAktiv eq true) else it
|
|
||||||
}
|
|
||||||
}.map { rowToDomPferd(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun save(horse: DomPferd): DomPferd = transaction {
|
|
||||||
val existing = HorseTable.selectAll()
|
|
||||||
.where { HorseTable.id eq horse.pferdId.toJavaUuid() }
|
|
||||||
.singleOrNull()
|
|
||||||
if (existing == null) {
|
|
||||||
HorseTable.insert { applyToStatement(it, horse) }
|
|
||||||
} else {
|
|
||||||
HorseTable.update({ HorseTable.id eq horse.pferdId.toJavaUuid() }) {
|
|
||||||
applyToStatement(it, horse.copy(updatedAt = Clock.System.now()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
horse
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun delete(id: Uuid): Boolean = transaction {
|
|
||||||
HorseTable.deleteWhere { HorseTable.id eq id.toJavaUuid() } > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun existsByLebensnummer(lebensnummer: String): Boolean = transaction {
|
|
||||||
HorseTable.selectAll().where { HorseTable.lebensnummer eq lebensnummer }.count() > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun existsByChipNummer(chipNummer: String): Boolean = transaction {
|
|
||||||
HorseTable.selectAll().where { HorseTable.chipNummer eq chipNummer }.count() > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun existsByPassNummer(passNummer: String): Boolean = transaction {
|
|
||||||
HorseTable.selectAll().where { HorseTable.passNummer eq passNummer }.count() > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun existsByOepsNummer(oepsNummer: String): Boolean = transaction {
|
|
||||||
HorseTable.selectAll().where { HorseTable.oepsNummer eq oepsNummer }.count() > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun existsByFeiNummer(feiNummer: String): Boolean = transaction {
|
|
||||||
HorseTable.selectAll().where { HorseTable.feiNummer eq feiNummer }.count() > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun countActive(): Long = transaction {
|
|
||||||
HorseTable.selectAll().where { HorseTable.istAktiv eq true }.count()
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun countByOwnerId(ownerId: Uuid, activeOnly: Boolean): Long = transaction {
|
|
||||||
HorseTable.selectAll().where {
|
|
||||||
(HorseTable.besitzerId eq ownerId.toJavaUuid()).let {
|
|
||||||
if (activeOnly) it and (HorseTable.istAktiv eq true) else it
|
|
||||||
}
|
|
||||||
}.count()
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun countOepsRegistered(activeOnly: Boolean): Long = transaction {
|
|
||||||
HorseTable.selectAll().where {
|
|
||||||
HorseTable.oepsNummer.isNotNull().let {
|
|
||||||
if (activeOnly) it and (HorseTable.istAktiv eq true) else it
|
|
||||||
}
|
|
||||||
}.count()
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun countFeiRegistered(activeOnly: Boolean): Long = transaction {
|
|
||||||
HorseTable.selectAll().where {
|
|
||||||
HorseTable.feiNummer.isNotNull().let {
|
|
||||||
if (activeOnly) it and (HorseTable.istAktiv eq true) else it
|
|
||||||
}
|
|
||||||
}.count()
|
|
||||||
}
|
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
// Hilfsmethoden
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
|
|
||||||
private fun rowToDomPferd(row: ResultRow): DomPferd = DomPferd(
|
|
||||||
pferdId = row[HorseTable.id].value.toKotlinUuid(),
|
|
||||||
pferdeName = row[HorseTable.pferdeName],
|
|
||||||
geschlecht = row[HorseTable.geschlecht],
|
|
||||||
geburtsdatum = row[HorseTable.geburtsdatum],
|
|
||||||
rasse = row[HorseTable.rasse],
|
|
||||||
farbe = row[HorseTable.farbe],
|
|
||||||
besitzerId = row[HorseTable.besitzerId]?.toKotlinUuid(),
|
|
||||||
verantwortlichePersonId = row[HorseTable.verantwortlichePersonId]?.toKotlinUuid(),
|
|
||||||
zuechterName = row[HorseTable.zuechterName],
|
|
||||||
zuchtbuchNummer = row[HorseTable.zuchtbuchNummer],
|
|
||||||
lebensnummer = row[HorseTable.lebensnummer],
|
|
||||||
chipNummer = row[HorseTable.chipNummer],
|
|
||||||
passNummer = row[HorseTable.passNummer],
|
|
||||||
oepsNummer = row[HorseTable.oepsNummer],
|
|
||||||
feiNummer = row[HorseTable.feiNummer],
|
|
||||||
vaterName = row[HorseTable.vaterName],
|
|
||||||
mutterName = row[HorseTable.mutterName],
|
|
||||||
mutterVaterName = row[HorseTable.mutterVaterName],
|
|
||||||
stockmass = row[HorseTable.stockmass],
|
|
||||||
istAktiv = row[HorseTable.istAktiv],
|
|
||||||
bemerkungen = row[HorseTable.bemerkungen],
|
|
||||||
datenQuelle = row[HorseTable.datenQuelle],
|
|
||||||
createdAt = row[HorseTable.createdAt],
|
|
||||||
updatedAt = row[HorseTable.updatedAt]
|
|
||||||
)
|
|
||||||
|
|
||||||
private fun applyToStatement(statement: UpdateBuilder<*>, horse: DomPferd) {
|
|
||||||
statement[HorseTable.id] = horse.pferdId.toJavaUuid()
|
|
||||||
statement[HorseTable.pferdeName] = horse.pferdeName
|
|
||||||
statement[HorseTable.geschlecht] = horse.geschlecht
|
|
||||||
statement[HorseTable.geburtsdatum] = horse.geburtsdatum
|
|
||||||
statement[HorseTable.rasse] = horse.rasse
|
|
||||||
statement[HorseTable.farbe] = horse.farbe
|
|
||||||
statement[HorseTable.besitzerId] = horse.besitzerId?.toJavaUuid()
|
|
||||||
statement[HorseTable.verantwortlichePersonId] = horse.verantwortlichePersonId?.toJavaUuid()
|
|
||||||
statement[HorseTable.zuechterName] = horse.zuechterName
|
|
||||||
statement[HorseTable.zuchtbuchNummer] = horse.zuchtbuchNummer
|
|
||||||
statement[HorseTable.lebensnummer] = horse.lebensnummer
|
|
||||||
statement[HorseTable.chipNummer] = horse.chipNummer
|
|
||||||
statement[HorseTable.passNummer] = horse.passNummer
|
|
||||||
statement[HorseTable.oepsNummer] = horse.oepsNummer
|
|
||||||
statement[HorseTable.feiNummer] = horse.feiNummer
|
|
||||||
statement[HorseTable.vaterName] = horse.vaterName
|
|
||||||
statement[HorseTable.mutterName] = horse.mutterName
|
|
||||||
statement[HorseTable.mutterVaterName] = horse.mutterVaterName
|
|
||||||
statement[HorseTable.stockmass] = horse.stockmass
|
|
||||||
statement[HorseTable.istAktiv] = horse.istAktiv
|
|
||||||
statement[HorseTable.bemerkungen] = horse.bemerkungen
|
|
||||||
statement[HorseTable.datenQuelle] = horse.datenQuelle
|
|
||||||
statement[HorseTable.createdAt] = horse.createdAt
|
|
||||||
statement[HorseTable.updatedAt] = horse.updatedAt
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-336
@@ -1,336 +0,0 @@
|
|||||||
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
|
||||||
package at.mocode.horses.infrastructure.persistence
|
|
||||||
|
|
||||||
import at.mocode.core.domain.model.PferdeGeschlechtE
|
|
||||||
import at.mocode.horses.domain.model.DomPferd
|
|
||||||
import at.mocode.horses.domain.repository.HorseRepository
|
|
||||||
import at.mocode.core.utils.database.DatabaseFactory
|
|
||||||
import kotlin.uuid.Uuid
|
|
||||||
import kotlin.time.Clock
|
|
||||||
import org.jetbrains.exposed.sql.*
|
|
||||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
|
||||||
import org.jetbrains.exposed.sql.statements.UpdateBuilder
|
|
||||||
import org.springframework.stereotype.Repository
|
|
||||||
|
|
||||||
/**
|
|
||||||
* PostgreSQL implementation of the HorseRepository using Exposed ORM.
|
|
||||||
*
|
|
||||||
* This implementation provides database operations for horse entities,
|
|
||||||
* mapping between the domain model (DomPferd) and the database table (HorseTable).
|
|
||||||
*/
|
|
||||||
@Repository
|
|
||||||
class HorseRepositoryImpl : HorseRepository {
|
|
||||||
|
|
||||||
override suspend fun findById(id: Uuid): DomPferd? = DatabaseFactory.dbQuery {
|
|
||||||
HorseTable.selectAll().where { HorseTable.id eq id }
|
|
||||||
.map { rowToDomPferd(it) }
|
|
||||||
.singleOrNull()
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun findByLebensnummer(lebensnummer: String): DomPferd? = DatabaseFactory.dbQuery {
|
|
||||||
HorseTable.selectAll().where { HorseTable.lebensnummer eq lebensnummer }
|
|
||||||
.map { rowToDomPferd(it) }
|
|
||||||
.singleOrNull()
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun findByChipNummer(chipNummer: String): DomPferd? = DatabaseFactory.dbQuery {
|
|
||||||
HorseTable.selectAll().where { HorseTable.chipNummer eq chipNummer }
|
|
||||||
.map { rowToDomPferd(it) }
|
|
||||||
.singleOrNull()
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun findByPassNummer(passNummer: String): DomPferd? = DatabaseFactory.dbQuery {
|
|
||||||
HorseTable.selectAll().where { HorseTable.passNummer eq passNummer }
|
|
||||||
.map { rowToDomPferd(it) }
|
|
||||||
.singleOrNull()
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun findByOepsNummer(oepsNummer: String): DomPferd? = DatabaseFactory.dbQuery {
|
|
||||||
HorseTable.selectAll().where { HorseTable.oepsNummer eq oepsNummer }
|
|
||||||
.map { rowToDomPferd(it) }
|
|
||||||
.singleOrNull()
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun findByFeiNummer(feiNummer: String): DomPferd? = DatabaseFactory.dbQuery {
|
|
||||||
HorseTable.selectAll().where { HorseTable.feiNummer eq feiNummer }
|
|
||||||
.map { rowToDomPferd(it) }
|
|
||||||
.singleOrNull()
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun findByName(searchTerm: String, limit: Int): List<DomPferd> = DatabaseFactory.dbQuery {
|
|
||||||
HorseTable.selectAll().where { HorseTable.pferdeName like "%$searchTerm%" }
|
|
||||||
.orderBy(HorseTable.pferdeName to SortOrder.ASC)
|
|
||||||
.limit(limit)
|
|
||||||
.map { rowToDomPferd(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun findByOwnerId(ownerId: Uuid, activeOnly: Boolean): List<DomPferd> = DatabaseFactory.dbQuery {
|
|
||||||
val query = HorseTable.selectAll().where { HorseTable.besitzerId eq ownerId }
|
|
||||||
|
|
||||||
if (activeOnly) {
|
|
||||||
query.andWhere { HorseTable.istAktiv eq true }
|
|
||||||
} else {
|
|
||||||
query
|
|
||||||
}.orderBy(HorseTable.pferdeName to SortOrder.ASC)
|
|
||||||
.map { rowToDomPferd(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun findByResponsiblePersonId(responsiblePersonId: Uuid, activeOnly: Boolean): List<DomPferd> = DatabaseFactory.dbQuery {
|
|
||||||
val query = HorseTable.selectAll().where { HorseTable.verantwortlichePersonId eq responsiblePersonId }
|
|
||||||
|
|
||||||
if (activeOnly) {
|
|
||||||
query.andWhere { HorseTable.istAktiv eq true }
|
|
||||||
} else {
|
|
||||||
query
|
|
||||||
}.orderBy(HorseTable.pferdeName to SortOrder.ASC)
|
|
||||||
.map { rowToDomPferd(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun findByGeschlecht(geschlecht: PferdeGeschlechtE, activeOnly: Boolean, limit: Int): List<DomPferd> = DatabaseFactory.dbQuery {
|
|
||||||
val query = HorseTable.selectAll().where { HorseTable.geschlecht eq geschlecht }
|
|
||||||
|
|
||||||
if (activeOnly) {
|
|
||||||
query.andWhere { HorseTable.istAktiv eq true }
|
|
||||||
} else {
|
|
||||||
query
|
|
||||||
}.orderBy(HorseTable.pferdeName to SortOrder.ASC)
|
|
||||||
.limit(limit)
|
|
||||||
.map { rowToDomPferd(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun findByRasse(rasse: String, activeOnly: Boolean, limit: Int): List<DomPferd> = DatabaseFactory.dbQuery {
|
|
||||||
val query = HorseTable.selectAll().where { HorseTable.rasse eq rasse }
|
|
||||||
|
|
||||||
if (activeOnly) {
|
|
||||||
query.andWhere { HorseTable.istAktiv eq true }
|
|
||||||
} else {
|
|
||||||
query
|
|
||||||
}.orderBy(HorseTable.pferdeName to SortOrder.ASC)
|
|
||||||
.limit(limit)
|
|
||||||
.map { rowToDomPferd(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun findByBirthYear(birthYear: Int, activeOnly: Boolean): List<DomPferd> = DatabaseFactory.dbQuery {
|
|
||||||
val query = HorseTable.selectAll().where {
|
|
||||||
HorseTable.geburtsdatum.isNotNull() and
|
|
||||||
(CustomFunction(
|
|
||||||
"EXTRACT",
|
|
||||||
IntegerColumnType(),
|
|
||||||
stringLiteral("YEAR FROM "),
|
|
||||||
HorseTable.geburtsdatum
|
|
||||||
) eq birthYear)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (activeOnly) {
|
|
||||||
query.andWhere { HorseTable.istAktiv eq true }
|
|
||||||
} else {
|
|
||||||
query
|
|
||||||
}.orderBy(HorseTable.pferdeName to SortOrder.ASC)
|
|
||||||
.map { rowToDomPferd(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun findByBirthYearRange(fromYear: Int, toYear: Int, activeOnly: Boolean): List<DomPferd> = DatabaseFactory.dbQuery {
|
|
||||||
val query = HorseTable.selectAll().where {
|
|
||||||
HorseTable.geburtsdatum.isNotNull() and
|
|
||||||
(CustomFunction(
|
|
||||||
"EXTRACT",
|
|
||||||
IntegerColumnType(),
|
|
||||||
stringLiteral("YEAR FROM "),
|
|
||||||
HorseTable.geburtsdatum
|
|
||||||
) greaterEq fromYear) and
|
|
||||||
(CustomFunction(
|
|
||||||
"EXTRACT",
|
|
||||||
IntegerColumnType(),
|
|
||||||
stringLiteral("YEAR FROM "),
|
|
||||||
HorseTable.geburtsdatum
|
|
||||||
) lessEq toYear)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (activeOnly) {
|
|
||||||
query.andWhere { HorseTable.istAktiv eq true }
|
|
||||||
} else {
|
|
||||||
query
|
|
||||||
}.orderBy(HorseTable.geburtsdatum, SortOrder.DESC)
|
|
||||||
.map { rowToDomPferd(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun findAllActive(limit: Int): List<DomPferd> = DatabaseFactory.dbQuery {
|
|
||||||
HorseTable.selectAll().where { HorseTable.istAktiv eq true }
|
|
||||||
.orderBy(HorseTable.pferdeName to SortOrder.ASC)
|
|
||||||
.limit(limit)
|
|
||||||
.map { rowToDomPferd(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun findOepsRegistered(activeOnly: Boolean): List<DomPferd> = DatabaseFactory.dbQuery {
|
|
||||||
val query = HorseTable.selectAll().where { HorseTable.oepsNummer.isNotNull() }
|
|
||||||
|
|
||||||
if (activeOnly) {
|
|
||||||
query.andWhere { HorseTable.istAktiv eq true }
|
|
||||||
} else {
|
|
||||||
query
|
|
||||||
}.orderBy(HorseTable.pferdeName to SortOrder.ASC)
|
|
||||||
.map { rowToDomPferd(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun findFeiRegistered(activeOnly: Boolean): List<DomPferd> = DatabaseFactory.dbQuery {
|
|
||||||
val query = HorseTable.selectAll().where { HorseTable.feiNummer.isNotNull() }
|
|
||||||
|
|
||||||
if (activeOnly) {
|
|
||||||
query.andWhere { HorseTable.istAktiv eq true }
|
|
||||||
} else {
|
|
||||||
query
|
|
||||||
}.orderBy(HorseTable.pferdeName to SortOrder.ASC)
|
|
||||||
.map { rowToDomPferd(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun save(horse: DomPferd): DomPferd = DatabaseFactory.dbQuery {
|
|
||||||
val now = Clock.System.now()
|
|
||||||
val existingHorse = findById(horse.pferdId)
|
|
||||||
|
|
||||||
if (existingHorse != null) {
|
|
||||||
// Update existing horse
|
|
||||||
val updatedHorse = horse.copy(updatedAt = now)
|
|
||||||
HorseTable.update({ HorseTable.id eq horse.pferdId }) {
|
|
||||||
domPferdToStatement(it, updatedHorse)
|
|
||||||
}
|
|
||||||
updatedHorse
|
|
||||||
} else {
|
|
||||||
// Insert a new horse
|
|
||||||
HorseTable.insert {
|
|
||||||
it[id] = horse.pferdId
|
|
||||||
domPferdToStatement(it, horse.copy(updatedAt = now))
|
|
||||||
}
|
|
||||||
horse.copy(updatedAt = now)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun delete(id: Uuid): Boolean = DatabaseFactory.dbQuery {
|
|
||||||
val deletedRows = HorseTable.deleteWhere { HorseTable.id eq id }
|
|
||||||
deletedRows > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun existsByLebensnummer(lebensnummer: String): Boolean = DatabaseFactory.dbQuery {
|
|
||||||
HorseTable.selectAll().where { HorseTable.lebensnummer eq lebensnummer }
|
|
||||||
.count() > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun existsByChipNummer(chipNummer: String): Boolean = DatabaseFactory.dbQuery {
|
|
||||||
HorseTable.selectAll().where { HorseTable.chipNummer eq chipNummer }
|
|
||||||
.count() > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun existsByPassNummer(passNummer: String): Boolean = DatabaseFactory.dbQuery {
|
|
||||||
HorseTable.selectAll().where { HorseTable.passNummer eq passNummer }
|
|
||||||
.count() > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun existsByOepsNummer(oepsNummer: String): Boolean = DatabaseFactory.dbQuery {
|
|
||||||
HorseTable.selectAll().where { HorseTable.oepsNummer eq oepsNummer }
|
|
||||||
.count() > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun existsByFeiNummer(feiNummer: String): Boolean = DatabaseFactory.dbQuery {
|
|
||||||
HorseTable.selectAll().where { HorseTable.feiNummer eq feiNummer }
|
|
||||||
.count() > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun countActive(): Long = DatabaseFactory.dbQuery {
|
|
||||||
HorseTable.selectAll().where { HorseTable.istAktiv eq true }
|
|
||||||
.count()
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun countByOwnerId(ownerId: Uuid, activeOnly: Boolean): Long = DatabaseFactory.dbQuery {
|
|
||||||
val query = HorseTable.selectAll().where { HorseTable.besitzerId eq ownerId }
|
|
||||||
|
|
||||||
if (activeOnly) {
|
|
||||||
query.andWhere { HorseTable.istAktiv eq true }
|
|
||||||
} else {
|
|
||||||
query
|
|
||||||
}.count()
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun countOepsRegistered(activeOnly: Boolean): Long = DatabaseFactory.dbQuery {
|
|
||||||
val query = HorseTable.selectAll().where {
|
|
||||||
HorseTable.oepsNummer.isNotNull() and (HorseTable.oepsNummer neq "")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (activeOnly) {
|
|
||||||
query.andWhere { HorseTable.istAktiv eq true }
|
|
||||||
} else {
|
|
||||||
query
|
|
||||||
}.count()
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun countFeiRegistered(activeOnly: Boolean): Long = DatabaseFactory.dbQuery {
|
|
||||||
val query = HorseTable.selectAll().where {
|
|
||||||
HorseTable.feiNummer.isNotNull() and (HorseTable.feiNummer neq "")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (activeOnly) {
|
|
||||||
query.andWhere { HorseTable.istAktiv eq true }
|
|
||||||
} else {
|
|
||||||
query
|
|
||||||
}.count()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Maps a database row to a DomPferd domain object.
|
|
||||||
*/
|
|
||||||
private fun rowToDomPferd(row: ResultRow): DomPferd {
|
|
||||||
return DomPferd(
|
|
||||||
pferdId = row[HorseTable.id].value,
|
|
||||||
pferdeName = row[HorseTable.pferdeName],
|
|
||||||
geschlecht = row[HorseTable.geschlecht],
|
|
||||||
geburtsdatum = row[HorseTable.geburtsdatum],
|
|
||||||
rasse = row[HorseTable.rasse],
|
|
||||||
farbe = row[HorseTable.farbe],
|
|
||||||
besitzerId = row[HorseTable.besitzerId],
|
|
||||||
verantwortlichePersonId = row[HorseTable.verantwortlichePersonId],
|
|
||||||
zuechterName = row[HorseTable.zuechterName],
|
|
||||||
zuchtbuchNummer = row[HorseTable.zuchtbuchNummer],
|
|
||||||
lebensnummer = row[HorseTable.lebensnummer],
|
|
||||||
chipNummer = row[HorseTable.chipNummer],
|
|
||||||
passNummer = row[HorseTable.passNummer],
|
|
||||||
oepsNummer = row[HorseTable.oepsNummer],
|
|
||||||
feiNummer = row[HorseTable.feiNummer],
|
|
||||||
vaterName = row[HorseTable.vaterName],
|
|
||||||
mutterName = row[HorseTable.mutterName],
|
|
||||||
mutterVaterName = row[HorseTable.mutterVaterName],
|
|
||||||
stockmass = row[HorseTable.stockmass],
|
|
||||||
istAktiv = row[HorseTable.istAktiv],
|
|
||||||
bemerkungen = row[HorseTable.bemerkungen],
|
|
||||||
datenQuelle = row[HorseTable.datenQuelle],
|
|
||||||
createdAt = row[HorseTable.createdAt],
|
|
||||||
updatedAt = row[HorseTable.updatedAt]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Maps a DomPferd domain object to database statement values.
|
|
||||||
*/
|
|
||||||
private fun domPferdToStatement(statement: UpdateBuilder<*>, horse: DomPferd) {
|
|
||||||
statement[HorseTable.pferdeName] = horse.pferdeName
|
|
||||||
statement[HorseTable.geschlecht] = horse.geschlecht
|
|
||||||
statement[HorseTable.geburtsdatum] = horse.geburtsdatum
|
|
||||||
statement[HorseTable.rasse] = horse.rasse
|
|
||||||
statement[HorseTable.farbe] = horse.farbe
|
|
||||||
statement[HorseTable.besitzerId] = horse.besitzerId
|
|
||||||
statement[HorseTable.verantwortlichePersonId] = horse.verantwortlichePersonId
|
|
||||||
statement[HorseTable.zuechterName] = horse.zuechterName
|
|
||||||
statement[HorseTable.zuchtbuchNummer] = horse.zuchtbuchNummer
|
|
||||||
statement[HorseTable.lebensnummer] = horse.lebensnummer
|
|
||||||
statement[HorseTable.chipNummer] = horse.chipNummer
|
|
||||||
statement[HorseTable.passNummer] = horse.passNummer
|
|
||||||
statement[HorseTable.oepsNummer] = horse.oepsNummer
|
|
||||||
statement[HorseTable.feiNummer] = horse.feiNummer
|
|
||||||
statement[HorseTable.vaterName] = horse.vaterName
|
|
||||||
statement[HorseTable.mutterName] = horse.mutterName
|
|
||||||
statement[HorseTable.mutterVaterName] = horse.mutterVaterName
|
|
||||||
statement[HorseTable.stockmass] = horse.stockmass
|
|
||||||
statement[HorseTable.istAktiv] = horse.istAktiv
|
|
||||||
statement[HorseTable.bemerkungen] = horse.bemerkungen
|
|
||||||
statement[HorseTable.datenQuelle] = horse.datenQuelle
|
|
||||||
statement[HorseTable.createdAt] = horse.createdAt
|
|
||||||
statement[HorseTable.updatedAt] = horse.updatedAt
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-70
@@ -1,70 +0,0 @@
|
|||||||
package at.mocode.horses.infrastructure.persistence
|
|
||||||
|
|
||||||
import at.mocode.core.domain.model.DatenQuelleE
|
|
||||||
import at.mocode.core.domain.model.PferdeGeschlechtE
|
|
||||||
import org.jetbrains.exposed.v1.core.dao.id.java.UUIDTable
|
|
||||||
import org.jetbrains.exposed.v1.core.java.javaUUID
|
|
||||||
import org.jetbrains.exposed.v1.datetime.date
|
|
||||||
import org.jetbrains.exposed.v1.datetime.timestamp
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Database table definition for horses in the horse-registry context.
|
|
||||||
*
|
|
||||||
* This table stores all horse information including identification,
|
|
||||||
* ownership, breeding data, and administrative information.
|
|
||||||
*/
|
|
||||||
object HorseTable : UUIDTable("zns_horses") {
|
|
||||||
// Basic Information
|
|
||||||
val pferdeName = varchar("pferde_name", 255)
|
|
||||||
val geschlecht = enumerationByName<PferdeGeschlechtE>("geschlecht", 20)
|
|
||||||
val geburtsdatum = date("geburtsdatum").nullable()
|
|
||||||
val rasse = varchar("rasse", 100).nullable()
|
|
||||||
val farbe = varchar("farbe", 100).nullable()
|
|
||||||
|
|
||||||
// Ownership and Responsibility
|
|
||||||
val besitzerId = javaUUID("besitzer_id").nullable()
|
|
||||||
val verantwortlichePersonId = javaUUID("verantwortliche_person_id").nullable()
|
|
||||||
|
|
||||||
// Breeding Information
|
|
||||||
val zuechterName = varchar("zuechter_name", 255).nullable()
|
|
||||||
val zuchtbuchNummer = varchar("zuchtbuch_nummer", 100).nullable()
|
|
||||||
|
|
||||||
// Identification Numbers
|
|
||||||
val lebensnummer = varchar("lebensnummer", 50).nullable()
|
|
||||||
val chipNummer = varchar("chip_nummer", 50).nullable()
|
|
||||||
val passNummer = varchar("pass_nummer", 50).nullable()
|
|
||||||
val oepsNummer = varchar("oeps_nummer", 50).nullable()
|
|
||||||
val feiNummer = varchar("fei_nummer", 50).nullable()
|
|
||||||
|
|
||||||
// Pedigree Information
|
|
||||||
val vaterName = varchar("vater_name", 255).nullable()
|
|
||||||
val mutterName = varchar("mutter_name", 255).nullable()
|
|
||||||
val mutterVaterName = varchar("mutter_vater_name", 255).nullable()
|
|
||||||
|
|
||||||
// Physical Characteristics
|
|
||||||
val stockmass = integer("stockmass").nullable()
|
|
||||||
|
|
||||||
// Status and Administrative
|
|
||||||
val istAktiv = bool("ist_aktiv").default(true)
|
|
||||||
val bemerkungen = text("bemerkungen").nullable()
|
|
||||||
val datenQuelle = enumerationByName<DatenQuelleE>("daten_quelle", 20).default(DatenQuelleE.MANUELL)
|
|
||||||
|
|
||||||
// Audit Fields
|
|
||||||
val createdAt = timestamp("created_at")
|
|
||||||
val updatedAt = timestamp("updated_at")
|
|
||||||
|
|
||||||
init {
|
|
||||||
// Indexes for performance
|
|
||||||
index(false, pferdeName)
|
|
||||||
index(false, besitzerId)
|
|
||||||
index(false, istAktiv)
|
|
||||||
|
|
||||||
// Unique constraints for identification numbers
|
|
||||||
// These ensure database-level uniqueness even under concurrent access
|
|
||||||
uniqueIndex(lebensnummer)
|
|
||||||
uniqueIndex(chipNummer)
|
|
||||||
uniqueIndex(passNummer)
|
|
||||||
uniqueIndex(oepsNummer)
|
|
||||||
uniqueIndex(feiNummer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-14
@@ -1,14 +0,0 @@
|
|||||||
package at.mocode.horses.infrastructure.persistence
|
|
||||||
|
|
||||||
import org.jetbrains.exposed.v1.core.dao.id.java.UUIDTable
|
|
||||||
import org.jetbrains.exposed.v1.datetime.timestamp
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tabelle für importierte Vereine aus dem ZNS-Datenbestand (VEREIN01.dat).
|
|
||||||
* Wird ausschließlich als Dev-Seed-Daten verwendet.
|
|
||||||
*/
|
|
||||||
object ZnsClubTable : UUIDTable("zns_clubs") {
|
|
||||||
val vereinsNummer = varchar("vereins_nummer", 4).uniqueIndex()
|
|
||||||
val name = varchar("name", 50)
|
|
||||||
val createdAt = timestamp("created_at")
|
|
||||||
}
|
|
||||||
-23
@@ -1,23 +0,0 @@
|
|||||||
package at.mocode.horses.infrastructure.persistence
|
|
||||||
|
|
||||||
import org.jetbrains.exposed.v1.core.dao.id.java.UUIDTable
|
|
||||||
import org.jetbrains.exposed.v1.datetime.date
|
|
||||||
import org.jetbrains.exposed.v1.datetime.timestamp
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tabelle für importierte Personen/Reiter aus dem ZNS-Datenbestand (LIZENZ01.dat).
|
|
||||||
* Wird ausschließlich als Dev-Seed-Daten verwendet.
|
|
||||||
*/
|
|
||||||
object ZnsPersonTable : UUIDTable("zns_persons") {
|
|
||||||
val lizenzNummer = varchar("lizenz_nummer", 10).uniqueIndex()
|
|
||||||
val nachname = varchar("nachname", 50)
|
|
||||||
val vorname = varchar("vorname", 25)
|
|
||||||
val vereinsNummer = varchar("vereins_nummer", 4).nullable()
|
|
||||||
val vereinsName = varchar("vereins_name", 50).nullable()
|
|
||||||
val nation = varchar("nation", 3).nullable()
|
|
||||||
val lizenzKlasse = varchar("lizenz_klasse", 6).nullable()
|
|
||||||
val mitgliedsNummer = varchar("mitglieds_nummer", 13).nullable()
|
|
||||||
val geschlecht = varchar("geschlecht", 1).nullable()
|
|
||||||
val geburtsdatum = date("geburtsdatum").nullable()
|
|
||||||
val createdAt = timestamp("created_at")
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
plugins {
|
|
||||||
alias(libs.plugins.kotlinJvm)
|
|
||||||
alias(libs.plugins.kotlinSpring)
|
|
||||||
alias(libs.plugins.spring.boot)
|
|
||||||
alias(libs.plugins.spring.dependencyManagement)
|
|
||||||
}
|
|
||||||
|
|
||||||
springBoot {
|
|
||||||
mainClass.set("at.mocode.horses.service.HorsesServiceApplicationKt")
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
// Interne Module
|
|
||||||
implementation(projects.platform.platformDependencies)
|
|
||||||
implementation(projects.core.coreDomain)
|
|
||||||
implementation(projects.core.coreUtils)
|
|
||||||
implementation(projects.backend.services.horses.horsesDomain)
|
|
||||||
// horses-common: ON HOLD – veraltete API-Referenzen
|
|
||||||
// implementation(projects.backend.services.horses.horsesCommon)
|
|
||||||
implementation(projects.backend.services.horses.horsesInfrastructure)
|
|
||||||
|
|
||||||
// Spring Boot Starters
|
|
||||||
implementation(libs.spring.boot.starter.web)
|
|
||||||
implementation(libs.spring.boot.starter.validation)
|
|
||||||
implementation(libs.spring.boot.starter.actuator)
|
|
||||||
|
|
||||||
// Datenbank-Abhängigkeiten
|
|
||||||
implementation(libs.exposed.core)
|
|
||||||
implementation(libs.exposed.dao)
|
|
||||||
implementation(libs.exposed.jdbc)
|
|
||||||
implementation(libs.exposed.migration.jdbc)
|
|
||||||
implementation(libs.exposed.kotlin.datetime)
|
|
||||||
implementation(libs.hikari.cp)
|
|
||||||
testImplementation(project(":backend:infrastructure:messaging:messaging-client"))
|
|
||||||
runtimeOnly(libs.postgresql.driver)
|
|
||||||
testRuntimeOnly(libs.h2.driver)
|
|
||||||
|
|
||||||
// Testing
|
|
||||||
testImplementation(projects.platform.platformTesting)
|
|
||||||
testImplementation(libs.spring.boot.starter.test)
|
|
||||||
testImplementation(libs.logback.classic)
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.test {
|
|
||||||
useJUnitPlatform()
|
|
||||||
}
|
|
||||||
-26
@@ -1,26 +0,0 @@
|
|||||||
package at.mocode.horses.service
|
|
||||||
|
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication
|
|
||||||
import org.springframework.boot.runApplication
|
|
||||||
import org.springframework.context.annotation.ComponentScan
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Main application class for the Horses Service.
|
|
||||||
*
|
|
||||||
* This service provides APIs for managing horses and their data.
|
|
||||||
*/
|
|
||||||
@SpringBootApplication
|
|
||||||
@ComponentScan(basePackages = [
|
|
||||||
"at.mocode.horses.service",
|
|
||||||
"at.mocode.horses.api",
|
|
||||||
"at.mocode.horses.infrastructure",
|
|
||||||
"at.mocode.infrastructure.messaging"
|
|
||||||
])
|
|
||||||
class HorsesServiceApplication
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Main entry point for the Horses Service application.
|
|
||||||
*/
|
|
||||||
fun main(args: Array<String>) {
|
|
||||||
runApplication<HorsesServiceApplication>(*args)
|
|
||||||
}
|
|
||||||
-60
@@ -1,60 +0,0 @@
|
|||||||
package at.mocode.horses.service.config
|
|
||||||
|
|
||||||
import at.mocode.horses.application.usecase.CreateHorseUseCase
|
|
||||||
import at.mocode.horses.application.usecase.TransactionalCreateHorseUseCase
|
|
||||||
import at.mocode.horses.application.usecase.UpdateHorseUseCase
|
|
||||||
import at.mocode.horses.application.usecase.DeleteHorseUseCase
|
|
||||||
import at.mocode.horses.application.usecase.GetHorseUseCase
|
|
||||||
import at.mocode.horses.domain.repository.HorseRepository
|
|
||||||
import org.springframework.context.annotation.Bean
|
|
||||||
import org.springframework.context.annotation.Configuration
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Application configuration for the Horses Service.
|
|
||||||
*
|
|
||||||
* This configuration wires the use cases as Spring beans.
|
|
||||||
*/
|
|
||||||
@Configuration
|
|
||||||
class ApplicationConfiguration {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates the CreateHorseUseCase as a Spring bean.
|
|
||||||
*/
|
|
||||||
@Bean
|
|
||||||
fun createHorseUseCase(horseRepository: HorseRepository): CreateHorseUseCase {
|
|
||||||
return CreateHorseUseCase(horseRepository)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates the TransactionalCreateHorseUseCase as a Spring bean.
|
|
||||||
* This version ensures all database operations run within a single transaction.
|
|
||||||
*/
|
|
||||||
@Bean
|
|
||||||
fun transactionalCreateHorseUseCase(horseRepository: HorseRepository): TransactionalCreateHorseUseCase {
|
|
||||||
return TransactionalCreateHorseUseCase(horseRepository)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates the UpdateHorseUseCase as a Spring bean.
|
|
||||||
*/
|
|
||||||
@Bean
|
|
||||||
fun updateHorseUseCase(horseRepository: HorseRepository): UpdateHorseUseCase {
|
|
||||||
return UpdateHorseUseCase(horseRepository)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates the DeleteHorseUseCase as a Spring bean.
|
|
||||||
*/
|
|
||||||
@Bean
|
|
||||||
fun deleteHorseUseCase(horseRepository: HorseRepository): DeleteHorseUseCase {
|
|
||||||
return DeleteHorseUseCase(horseRepository)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates the GetHorseUseCase as a Spring bean.
|
|
||||||
*/
|
|
||||||
@Bean
|
|
||||||
fun getHorseUseCase(horseRepository: HorseRepository): GetHorseUseCase {
|
|
||||||
return GetHorseUseCase(horseRepository)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-38
@@ -1,38 +0,0 @@
|
|||||||
package at.mocode.horses.service.config
|
|
||||||
|
|
||||||
import at.mocode.horses.infrastructure.persistence.HorseTable
|
|
||||||
import at.mocode.horses.infrastructure.persistence.ZnsClubTable
|
|
||||||
import at.mocode.horses.infrastructure.persistence.ZnsPersonTable
|
|
||||||
import jakarta.annotation.PostConstruct
|
|
||||||
import org.jetbrains.exposed.v1.jdbc.Database
|
|
||||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
|
||||||
import org.jetbrains.exposed.v1.migration.jdbc.MigrationUtils
|
|
||||||
import org.slf4j.LoggerFactory
|
|
||||||
import org.springframework.beans.factory.annotation.Value
|
|
||||||
import org.springframework.context.annotation.Configuration
|
|
||||||
import org.springframework.context.annotation.Profile
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Minimale Datenbank-Konfiguration für den Horses-Service (Dev-Profil).
|
|
||||||
* Verbindet sich direkt via JDBC und legt die Tabellen an.
|
|
||||||
*/
|
|
||||||
@Configuration
|
|
||||||
@Profile("dev")
|
|
||||||
class HorsesDatabaseConfiguration(
|
|
||||||
@Value("\${spring.datasource.url}") private val jdbcUrl: String,
|
|
||||||
@Value("\${spring.datasource.username}") private val username: String,
|
|
||||||
@Value("\${spring.datasource.password}") private val password: String
|
|
||||||
) {
|
|
||||||
private val log = LoggerFactory.getLogger(HorsesDatabaseConfiguration::class.java)
|
|
||||||
|
|
||||||
@PostConstruct
|
|
||||||
fun initializeDatabase() {
|
|
||||||
log.info("Initialisiere Datenbank-Schema für Horses-Service...")
|
|
||||||
Database.connect(jdbcUrl, user = username, password = password)
|
|
||||||
transaction {
|
|
||||||
val statements = MigrationUtils.statementsRequiredForDatabaseMigration(HorseTable, ZnsPersonTable, ZnsClubTable)
|
|
||||||
statements.forEach { exec(it) }
|
|
||||||
log.info("Datenbank-Schema erfolgreich initialisiert")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-108
@@ -1,108 +0,0 @@
|
|||||||
package at.mocode.horses.service.config
|
|
||||||
|
|
||||||
import at.mocode.core.utils.database.DatabaseConfig
|
|
||||||
import at.mocode.core.utils.database.DatabaseFactory
|
|
||||||
import at.mocode.horses.infrastructure.persistence.HorseTable
|
|
||||||
import at.mocode.horses.infrastructure.persistence.ZnsPersonTable
|
|
||||||
import at.mocode.horses.infrastructure.persistence.ZnsClubTable
|
|
||||||
import org.springframework.boot.context.properties.ConfigurationProperties
|
|
||||||
import org.springframework.context.annotation.Configuration
|
|
||||||
import org.springframework.context.annotation.Profile
|
|
||||||
import org.springframework.stereotype.Component
|
|
||||||
import jakarta.annotation.PostConstruct
|
|
||||||
import jakarta.annotation.PreDestroy
|
|
||||||
import org.slf4j.LoggerFactory
|
|
||||||
import org.jetbrains.exposed.v1.core.SchemaUtils
|
|
||||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Database configuration for the Horses Service.
|
|
||||||
*
|
|
||||||
* This configuration ensures that Database.connect() is called properly
|
|
||||||
* before any Exposed operations are performed.
|
|
||||||
*/
|
|
||||||
@Configuration
|
|
||||||
@Profile("!test")
|
|
||||||
class HorsesDatabaseConfiguration {
|
|
||||||
|
|
||||||
private val log = LoggerFactory.getLogger(HorsesDatabaseConfiguration::class.java)
|
|
||||||
|
|
||||||
@PostConstruct
|
|
||||||
fun initializeDatabase() {
|
|
||||||
log.info("Initializing database schema for Horses Service...")
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Database connection is already initialized by the gateway
|
|
||||||
// Only initialize the schema for this service
|
|
||||||
transaction {
|
|
||||||
SchemaUtils.createMissingTablesAndColumns(HorseTable)
|
|
||||||
log.info("Horse database schema initialized successfully")
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
log.error("Failed to initialize database schema", e)
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@PreDestroy
|
|
||||||
fun closeDatabase() {
|
|
||||||
log.info("Closing database connection for Horses Service...")
|
|
||||||
try {
|
|
||||||
DatabaseFactory.close()
|
|
||||||
log.info("Database connection closed successfully")
|
|
||||||
} catch (e: Exception) {
|
|
||||||
log.error("Error closing database connection", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test-specific database configuration.
|
|
||||||
*/
|
|
||||||
@Configuration
|
|
||||||
@Profile("test")
|
|
||||||
class HorsesTestDatabaseConfiguration {
|
|
||||||
|
|
||||||
private val log = LoggerFactory.getLogger(HorsesTestDatabaseConfiguration::class.java)
|
|
||||||
|
|
||||||
@PostConstruct
|
|
||||||
fun initializeTestDatabase() {
|
|
||||||
log.info("Initializing test database connection for Horses Service...")
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Use H2 in-memory database for tests
|
|
||||||
val testConfig = DatabaseConfig(
|
|
||||||
jdbcUrl = "jdbc:h2:mem:horses_test;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE",
|
|
||||||
username = "sa",
|
|
||||||
password = "",
|
|
||||||
driverClassName = "org.h2.Driver",
|
|
||||||
maxPoolSize = 5,
|
|
||||||
minPoolSize = 1,
|
|
||||||
autoMigrate = true
|
|
||||||
)
|
|
||||||
|
|
||||||
DatabaseFactory.init(testConfig)
|
|
||||||
log.info("Test database connection initialized successfully")
|
|
||||||
|
|
||||||
// Initialize database schema for tests
|
|
||||||
transaction {
|
|
||||||
SchemaUtils.createMissingTablesAndColumns(HorseTable)
|
|
||||||
log.info("Test horse database schema initialized successfully")
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
log.error("Failed to initialize test database connection", e)
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@PreDestroy
|
|
||||||
fun closeTestDatabase() {
|
|
||||||
log.info("Closing test database connection for Horses Service...")
|
|
||||||
try {
|
|
||||||
DatabaseFactory.close()
|
|
||||||
log.info("Test database connection closed successfully")
|
|
||||||
} catch (e: Exception) {
|
|
||||||
log.error("Error closing test database connection", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-277
@@ -1,277 +0,0 @@
|
|||||||
package at.mocode.horses.service.dev
|
|
||||||
|
|
||||||
import at.mocode.core.domain.model.DatenQuelleE
|
|
||||||
import at.mocode.core.domain.model.PferdeGeschlechtE
|
|
||||||
import at.mocode.horses.infrastructure.persistence.HorseTable
|
|
||||||
import at.mocode.horses.infrastructure.persistence.ZnsClubTable
|
|
||||||
import at.mocode.horses.infrastructure.persistence.ZnsPersonTable
|
|
||||||
import org.jetbrains.exposed.v1.jdbc.batchInsert
|
|
||||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
|
||||||
import org.jetbrains.exposed.v1.migration.jdbc.MigrationUtils
|
|
||||||
import org.slf4j.LoggerFactory
|
|
||||||
import org.springframework.beans.factory.annotation.Value
|
|
||||||
import org.springframework.boot.CommandLineRunner
|
|
||||||
import org.springframework.context.annotation.Profile
|
|
||||||
import org.springframework.stereotype.Component
|
|
||||||
import java.io.File
|
|
||||||
import kotlin.time.Clock
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dev-only Seeder: Importiert ZNS-Rohdaten (Fixbreiten-Flat-Files) in die lokale Entwicklungs-DB.
|
|
||||||
*
|
|
||||||
* Aktivierung: Spring-Profil "dev" + Umgebungsvariable ZNS_DATA_DIR (Pfad zum ZNS-Verzeichnis).
|
|
||||||
* Beispiel: ZNS_DATA_DIR=/pfad/zu/ZNS ./gradlew :horses-service:bootRun --args='--spring.profiles.active=dev'
|
|
||||||
*
|
|
||||||
* ACHTUNG: Kein Produktions-Feature – nur für lokale Entwicklung gedacht.
|
|
||||||
*/
|
|
||||||
@Component
|
|
||||||
@Profile("dev")
|
|
||||||
class ZnsDataSeeder(
|
|
||||||
@Value("\${zns.data.dir:#{null}}") private val znsDataDir: String?
|
|
||||||
) : CommandLineRunner {
|
|
||||||
|
|
||||||
private val log = LoggerFactory.getLogger(ZnsDataSeeder::class.java)
|
|
||||||
|
|
||||||
override fun run(vararg args: String) {
|
|
||||||
if (znsDataDir == null) {
|
|
||||||
log.warn("ZNS_DATA_DIR nicht gesetzt – ZnsDataSeeder wird übersprungen.")
|
|
||||||
log.warn("Setze die Umgebungsvariable ZNS_DATA_DIR=/pfad/zu/ZNS um den Seeder zu aktivieren.")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val dir = File(znsDataDir)
|
|
||||||
if (!dir.exists() || !dir.isDirectory) {
|
|
||||||
log.error("ZNS_DATA_DIR '{}' existiert nicht oder ist kein Verzeichnis.", znsDataDir)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.info("=== ZnsDataSeeder gestartet – Quelle: {} ===", znsDataDir)
|
|
||||||
|
|
||||||
transaction {
|
|
||||||
MigrationUtils.statementsRequiredForDatabaseMigration(ZnsClubTable, ZnsPersonTable, HorseTable)
|
|
||||||
.forEach { exec(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
seedClubs(dir)
|
|
||||||
seedPersons(dir)
|
|
||||||
seedHorses(dir)
|
|
||||||
|
|
||||||
log.info("=== ZnsDataSeeder abgeschlossen ===")
|
|
||||||
}
|
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
// VEREIN01.dat → zns_clubs
|
|
||||||
// Format: [0-3] VereinsNr, [4-53] Name (54 Zeichen pro Zeile)
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
private fun seedClubs(dir: File) {
|
|
||||||
val file = File(dir, "VEREIN01.dat")
|
|
||||||
if (!file.exists()) {
|
|
||||||
log.warn("VEREIN01.dat nicht gefunden – Vereine werden übersprungen."); return
|
|
||||||
}
|
|
||||||
|
|
||||||
data class ClubRow(val nr: String, val name: String)
|
|
||||||
|
|
||||||
val rows = file.readLines(Charsets.ISO_8859_1)
|
|
||||||
.filter { it.length >= 4 }
|
|
||||||
.map { line ->
|
|
||||||
ClubRow(
|
|
||||||
nr = line.substring(0, 4).trim(),
|
|
||||||
name = line.substring(4).trim().take(50)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.filter { it.nr.isNotBlank() }
|
|
||||||
|
|
||||||
val now = Clock.System.now()
|
|
||||||
transaction {
|
|
||||||
ZnsClubTable.batchInsert(rows, ignore = true) { row ->
|
|
||||||
this[ZnsClubTable.vereinsNummer] = row.nr
|
|
||||||
this[ZnsClubTable.name] = row.name
|
|
||||||
this[ZnsClubTable.createdAt] = now
|
|
||||||
}
|
|
||||||
}
|
|
||||||
log.info("Vereine importiert: {} Datensätze", rows.size)
|
|
||||||
}
|
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
// LIZENZ01.dat → zns_persons
|
|
||||||
// Format (220 Zeichen):
|
|
||||||
// [0-5] LizenzNr
|
|
||||||
// [6-55] Nachname
|
|
||||||
// [56-80] Vorname
|
|
||||||
// [81-83] VereinsNr (2-stellig, rechtsbündig)
|
|
||||||
// [84-133] VereinsName
|
|
||||||
// [134-136] Nation
|
|
||||||
// [137-143] LizenzKlasse (z.B. "R1", "R2", "RD2")
|
|
||||||
// [144-156] MitgliedsNr
|
|
||||||
// [157-165] Telefon (nicht gespeichert)
|
|
||||||
// [166-169] Jahr
|
|
||||||
// [170] Geschlecht (M/W)
|
|
||||||
// [171-178] Geburtsdatum (YYYYMMDD)
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
private fun seedPersons(dir: File) {
|
|
||||||
val file = File(dir, "LIZENZ01.dat")
|
|
||||||
if (!file.exists()) {
|
|
||||||
log.warn("LIZENZ01.dat nicht gefunden – Personen werden übersprungen."); return
|
|
||||||
}
|
|
||||||
|
|
||||||
data class PersonRow(
|
|
||||||
val lizenzNr: String,
|
|
||||||
val nachname: String,
|
|
||||||
val vorname: String,
|
|
||||||
val vereinsNr: String?,
|
|
||||||
val vereinsName: String?,
|
|
||||||
val nation: String?,
|
|
||||||
val lizenzKlasse: String?,
|
|
||||||
val mitgliedsNr: String?,
|
|
||||||
val geschlecht: String?,
|
|
||||||
val geburtsdatum: java.time.LocalDate?
|
|
||||||
)
|
|
||||||
|
|
||||||
val rows = file.readLines(Charsets.ISO_8859_1)
|
|
||||||
.filter { it.length >= 6 }
|
|
||||||
.mapNotNull { line ->
|
|
||||||
val lizenzNr = line.substring(0, 6).trim()
|
|
||||||
if (lizenzNr.isBlank()) return@mapNotNull null
|
|
||||||
|
|
||||||
val nachname = line.safeSubstring(6, 56).trim()
|
|
||||||
val vorname = line.safeSubstring(56, 81).trim()
|
|
||||||
val vereinsNr = line.safeSubstring(81, 84).trim().takeIf { it.isNotBlank() }
|
|
||||||
val vereinsName = line.safeSubstring(84, 134).trim().takeIf { it.isNotBlank() }
|
|
||||||
val nation = line.safeSubstring(134, 137).trim().takeIf { it.isNotBlank() }
|
|
||||||
val lizenzKlasse = line.safeSubstring(137, 144).trim().takeIf { it.isNotBlank() }
|
|
||||||
val mitgliedsNr = line.safeSubstring(144, 157).trim().takeIf { it.isNotBlank() }
|
|
||||||
val geschlecht = line.safeSubstring(170, 171).trim().takeIf { it.isNotBlank() }
|
|
||||||
val gebDatStr = line.safeSubstring(171, 179).trim()
|
|
||||||
val gebDat = parseDate(gebDatStr)
|
|
||||||
|
|
||||||
PersonRow(
|
|
||||||
lizenzNr, nachname, vorname, vereinsNr, vereinsName,
|
|
||||||
nation, lizenzKlasse, mitgliedsNr, geschlecht, gebDat
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val now = Clock.System.now()
|
|
||||||
transaction {
|
|
||||||
ZnsPersonTable.batchInsert(rows, ignore = true) { row ->
|
|
||||||
this[ZnsPersonTable.lizenzNummer] = row.lizenzNr
|
|
||||||
this[ZnsPersonTable.nachname] = row.nachname
|
|
||||||
this[ZnsPersonTable.vorname] = row.vorname
|
|
||||||
this[ZnsPersonTable.vereinsNummer] = row.vereinsNr
|
|
||||||
this[ZnsPersonTable.vereinsName] = row.vereinsName
|
|
||||||
this[ZnsPersonTable.nation] = row.nation
|
|
||||||
this[ZnsPersonTable.lizenzKlasse] = row.lizenzKlasse
|
|
||||||
this[ZnsPersonTable.mitgliedsNummer] = row.mitgliedsNr
|
|
||||||
this[ZnsPersonTable.geschlecht] = row.geschlecht
|
|
||||||
this[ZnsPersonTable.geburtsdatum] = row.geburtsdatum?.let {
|
|
||||||
kotlinx.datetime.LocalDate(it.year, it.monthValue, it.dayOfMonth)
|
|
||||||
}
|
|
||||||
this[ZnsPersonTable.createdAt] = now
|
|
||||||
}
|
|
||||||
}
|
|
||||||
log.info("Personen importiert: {} Datensätze", rows.size)
|
|
||||||
}
|
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
// PFERDE01.dat → horses
|
|
||||||
// Format (211 Zeichen):
|
|
||||||
// [0-3] OepsNr (4 Zeichen)
|
|
||||||
// [4-33] Name (30 Zeichen)
|
|
||||||
// [34-42] ZnsNr / interne Nummer (9 Zeichen)
|
|
||||||
// [43] Geschlecht (S=Stute, W=Wallach, H=Hengst)
|
|
||||||
// [44-47] Geburtsjahr (4 Zeichen)
|
|
||||||
// [48-62] Farbe (15 Zeichen)
|
|
||||||
// [63-77] Rasse (15 Zeichen)
|
|
||||||
// [78-81] VereinsNr (4 Zeichen)
|
|
||||||
// [82-85] Jahr (4 Zeichen)
|
|
||||||
// [86-135] BesitzerName (50 Zeichen)
|
|
||||||
// [136-185] VaterName (50 Zeichen)
|
|
||||||
// [186-210] ChipNr (25 Zeichen)
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
private fun seedHorses(dir: File) {
|
|
||||||
val file = File(dir, "PFERDE01.dat")
|
|
||||||
if (!file.exists()) {
|
|
||||||
log.warn("PFERDE01.dat nicht gefunden – Pferde werden übersprungen."); return
|
|
||||||
}
|
|
||||||
|
|
||||||
data class HorseRow(
|
|
||||||
val oepsNr: String,
|
|
||||||
val name: String,
|
|
||||||
val geschlecht: PferdeGeschlechtE,
|
|
||||||
val geburtsjahr: Int?,
|
|
||||||
val farbe: String?,
|
|
||||||
val rasse: String?,
|
|
||||||
val besitzerName: String?,
|
|
||||||
val vaterName: String?,
|
|
||||||
val chipNr: String?
|
|
||||||
)
|
|
||||||
|
|
||||||
val rows = file.readLines(Charsets.ISO_8859_1)
|
|
||||||
.filter { it.length >= 5 }
|
|
||||||
.mapNotNull { line ->
|
|
||||||
val oepsNr = line.safeSubstring(0, 4).trim()
|
|
||||||
val name = line.safeSubstring(4, 34).trim()
|
|
||||||
if (name.isBlank()) return@mapNotNull null
|
|
||||||
|
|
||||||
val geschlechtChar = line.safeSubstring(43, 44).trim()
|
|
||||||
val geschlecht = when (geschlechtChar.uppercase()) {
|
|
||||||
"S" -> PferdeGeschlechtE.STUTE
|
|
||||||
"W" -> PferdeGeschlechtE.WALLACH
|
|
||||||
"H" -> PferdeGeschlechtE.HENGST
|
|
||||||
else -> PferdeGeschlechtE.UNBEKANNT
|
|
||||||
}
|
|
||||||
val gebjahr = line.safeSubstring(44, 48).trim().toIntOrNull()
|
|
||||||
val farbe = line.safeSubstring(48, 63).trim().takeIf { it.isNotBlank() }
|
|
||||||
val rasse = line.safeSubstring(63, 78).trim().takeIf { it.isNotBlank() }
|
|
||||||
val besitzer = line.safeSubstring(86, 136).trim().takeIf { it.isNotBlank() }
|
|
||||||
val vater = line.safeSubstring(136, 186).trim().takeIf { it.isNotBlank() }
|
|
||||||
val chip = line.safeSubstring(186, 211).trim().takeIf { it.isNotBlank() }
|
|
||||||
|
|
||||||
HorseRow(oepsNr, name, geschlecht, gebjahr, farbe, rasse, besitzer, vater, chip)
|
|
||||||
}
|
|
||||||
|
|
||||||
val now = Clock.System.now()
|
|
||||||
transaction {
|
|
||||||
HorseTable.batchInsert(rows, ignore = true) { row ->
|
|
||||||
this[HorseTable.pferdeName] = row.name
|
|
||||||
this[HorseTable.geschlecht] = row.geschlecht
|
|
||||||
this[HorseTable.geburtsdatum] = row.geburtsjahr?.let {
|
|
||||||
kotlinx.datetime.LocalDate(it, 1, 1)
|
|
||||||
}
|
|
||||||
this[HorseTable.farbe] = row.farbe
|
|
||||||
this[HorseTable.rasse] = row.rasse
|
|
||||||
this[HorseTable.oepsNummer] = row.oepsNr.takeIf { it.isNotBlank() }
|
|
||||||
this[HorseTable.chipNummer] = row.chipNr
|
|
||||||
this[HorseTable.vaterName] = row.vaterName
|
|
||||||
this[HorseTable.zuechterName] = row.besitzerName
|
|
||||||
this[HorseTable.datenQuelle] = DatenQuelleE.IMPORT_ZNS
|
|
||||||
this[HorseTable.istAktiv] = true
|
|
||||||
this[HorseTable.createdAt] = now
|
|
||||||
this[HorseTable.updatedAt] = now
|
|
||||||
}
|
|
||||||
}
|
|
||||||
log.info("Pferde importiert: {} Datensätze", rows.size)
|
|
||||||
}
|
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
// Hilfsfunktionen
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
|
|
||||||
private fun String.safeSubstring(start: Int, end: Int): String {
|
|
||||||
if (start >= this.length) return ""
|
|
||||||
return this.substring(start, minOf(end, this.length))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseDate(s: String): java.time.LocalDate? {
|
|
||||||
if (s.length < 8) return null
|
|
||||||
return try {
|
|
||||||
java.time.LocalDate.of(
|
|
||||||
s.substring(0, 4).toInt(),
|
|
||||||
s.substring(4, 6).toInt(),
|
|
||||||
s.substring(6, 8).toInt()
|
|
||||||
)
|
|
||||||
} catch (_: Exception) {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
plugins {
|
||||||
|
alias(libs.plugins.kotlinJvm)
|
||||||
|
alias(libs.plugins.kotlinSerialization)
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(projects.platform.platformDependencies)
|
||||||
|
implementation(projects.core.coreDomain)
|
||||||
|
|
||||||
|
testImplementation(projects.platform.platformTesting)
|
||||||
|
testImplementation(libs.mockk)
|
||||||
|
testImplementation(libs.kotlin.test)
|
||||||
|
}
|
||||||
+41
@@ -0,0 +1,41 @@
|
|||||||
|
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
||||||
|
|
||||||
|
package at.mocode.identity.domain.model
|
||||||
|
|
||||||
|
import at.mocode.core.domain.serialization.InstantSerializer
|
||||||
|
import at.mocode.core.domain.serialization.UuidSerializer
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlin.time.Clock
|
||||||
|
import kotlin.time.Instant
|
||||||
|
import kotlin.uuid.Uuid
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Domain model representing an extended profile of a user.
|
||||||
|
* This links a Keycloak User ID with an official ZNS Satznummer.
|
||||||
|
*
|
||||||
|
* @property profileId Unique internal identifier.
|
||||||
|
* @property userId The Keycloak User ID (UUID string).
|
||||||
|
* @property satznummer The official ZNS Satznummer (link to master-data-context).
|
||||||
|
* @property logoUrl Optional URL to a logo or profile picture.
|
||||||
|
* @property bio Optional short biography or description.
|
||||||
|
* @property contactEmail Optional contact email (might differ from account email).
|
||||||
|
* @property createdAt Timestamp of creation.
|
||||||
|
* @property updatedAt Timestamp of the last update.
|
||||||
|
*/
|
||||||
|
@Serializable
|
||||||
|
data class DomProfil(
|
||||||
|
@Serializable(with = UuidSerializer::class)
|
||||||
|
val profileId: Uuid = Uuid.random(),
|
||||||
|
|
||||||
|
val userId: String,
|
||||||
|
val satznummer: String,
|
||||||
|
|
||||||
|
val logoUrl: String? = null,
|
||||||
|
val bio: String? = null,
|
||||||
|
val contactEmail: String? = null,
|
||||||
|
|
||||||
|
@Serializable(with = InstantSerializer::class)
|
||||||
|
val createdAt: Instant = Clock.System.now(),
|
||||||
|
@Serializable(with = InstantSerializer::class)
|
||||||
|
var updatedAt: Instant = Clock.System.now()
|
||||||
|
)
|
||||||
+14
@@ -0,0 +1,14 @@
|
|||||||
|
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
||||||
|
|
||||||
|
package at.mocode.identity.domain.repository
|
||||||
|
|
||||||
|
import at.mocode.identity.domain.model.DomProfil
|
||||||
|
import kotlin.uuid.Uuid
|
||||||
|
|
||||||
|
interface ProfileRepository {
|
||||||
|
suspend fun findById(id: Uuid): DomProfil?
|
||||||
|
suspend fun findByUserId(userId: String): DomProfil?
|
||||||
|
suspend fun findBySatznummer(satznummer: String): List<DomProfil>
|
||||||
|
suspend fun save(profil: DomProfil): DomProfil
|
||||||
|
suspend fun delete(id: Uuid): Boolean
|
||||||
|
}
|
||||||
+44
@@ -0,0 +1,44 @@
|
|||||||
|
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
||||||
|
|
||||||
|
package at.mocode.identity.domain.service
|
||||||
|
|
||||||
|
import at.mocode.identity.domain.model.DomProfil
|
||||||
|
import at.mocode.identity.domain.repository.ProfileRepository
|
||||||
|
import kotlin.uuid.Uuid
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Domain service for managing user profiles and ZNS links.
|
||||||
|
*/
|
||||||
|
class ProfileService(
|
||||||
|
private val profileRepository: ProfileRepository
|
||||||
|
) {
|
||||||
|
suspend fun getProfileByUserId(userId: String): DomProfil? {
|
||||||
|
return profileRepository.findByUserId(userId)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun linkUserToZns(userId: String, satznummer: String): DomProfil {
|
||||||
|
val existing = profileRepository.findByUserId(userId)
|
||||||
|
val profil = if (existing != null) {
|
||||||
|
existing.copy(satznummer = satznummer)
|
||||||
|
} else {
|
||||||
|
DomProfil(userId = userId, satznummer = satznummer)
|
||||||
|
}
|
||||||
|
return profileRepository.save(profil)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun updateProfile(userId: String, logoUrl: String?, bio: String?, contactEmail: String?): DomProfil {
|
||||||
|
val profil = profileRepository.findByUserId(userId)
|
||||||
|
?: throw IllegalStateException("Profile for user $userId not found. Link to ZNS first.")
|
||||||
|
|
||||||
|
val updated = profil.copy(
|
||||||
|
logoUrl = logoUrl ?: profil.logoUrl,
|
||||||
|
bio = bio ?: profil.bio,
|
||||||
|
contactEmail = contactEmail ?: profil.contactEmail
|
||||||
|
)
|
||||||
|
return profileRepository.save(updated)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun deleteProfile(profileId: Uuid): Boolean {
|
||||||
|
return profileRepository.delete(profileId)
|
||||||
|
}
|
||||||
|
}
|
||||||
+51
@@ -0,0 +1,51 @@
|
|||||||
|
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
||||||
|
|
||||||
|
package at.mocode.identity.domain.service
|
||||||
|
|
||||||
|
import at.mocode.identity.domain.model.DomProfil
|
||||||
|
import at.mocode.identity.domain.repository.ProfileRepository
|
||||||
|
import io.mockk.coEvery
|
||||||
|
import io.mockk.coVerify
|
||||||
|
import io.mockk.mockk
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertNotNull
|
||||||
|
|
||||||
|
class ProfileServiceTest {
|
||||||
|
|
||||||
|
private val profileRepository = mockk<ProfileRepository>()
|
||||||
|
private val profileService = ProfileService(profileRepository)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `linkUserToZns should create new profile if none exists`() = runBlocking {
|
||||||
|
val userId = "user-123"
|
||||||
|
val satznummer = "0000123456"
|
||||||
|
|
||||||
|
coEvery { profileRepository.findByUserId(userId) } returns null
|
||||||
|
coEvery { profileRepository.save(any()) } answers { it.invocation.args[0] as DomProfil }
|
||||||
|
|
||||||
|
val result = profileService.linkUserToZns(userId, satznummer)
|
||||||
|
|
||||||
|
assertNotNull(result)
|
||||||
|
assertEquals(userId, result.userId)
|
||||||
|
assertEquals(satznummer, result.satznummer)
|
||||||
|
coVerify { profileRepository.save(match { it.userId == userId && it.satznummer == satznummer }) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `linkUserToZns should update existing profile`() = runBlocking {
|
||||||
|
val userId = "user-123"
|
||||||
|
val oldSatz = "old-123"
|
||||||
|
val newSatz = "new-456"
|
||||||
|
val existing = DomProfil(userId = userId, satznummer = oldSatz)
|
||||||
|
|
||||||
|
coEvery { profileRepository.findByUserId(userId) } returns existing
|
||||||
|
coEvery { profileRepository.save(any()) } answers { it.invocation.args[0] as DomProfil }
|
||||||
|
|
||||||
|
val result = profileService.linkUserToZns(userId, newSatz)
|
||||||
|
|
||||||
|
assertEquals(newSatz, result.satznummer)
|
||||||
|
coVerify { profileRepository.save(match { it.userId == userId && it.satznummer == newSatz }) }
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-9
@@ -1,23 +1,15 @@
|
|||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.kotlinJvm)
|
alias(libs.plugins.kotlinJvm)
|
||||||
alias(libs.plugins.kotlinSpring)
|
alias(libs.plugins.kotlinSpring)
|
||||||
alias(libs.plugins.kotlinSerialization)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(projects.platform.platformDependencies)
|
implementation(projects.platform.platformDependencies)
|
||||||
implementation(projects.backend.services.clubs.clubsDomain)
|
implementation(projects.backend.services.identity.identityDomain)
|
||||||
implementation(projects.core.coreDomain)
|
implementation(projects.core.coreDomain)
|
||||||
implementation(projects.core.coreUtils)
|
implementation(projects.core.coreUtils)
|
||||||
|
|
||||||
implementation(libs.spring.boot.starter.web)
|
|
||||||
|
|
||||||
implementation(libs.exposed.core)
|
implementation(libs.exposed.core)
|
||||||
implementation(libs.exposed.dao)
|
implementation(libs.exposed.dao)
|
||||||
implementation(libs.exposed.jdbc)
|
implementation(libs.exposed.jdbc)
|
||||||
implementation(libs.exposed.kotlin.datetime)
|
implementation(libs.exposed.kotlin.datetime)
|
||||||
|
|
||||||
runtimeOnly(libs.postgresql.driver)
|
|
||||||
|
|
||||||
testImplementation(projects.platform.platformTesting)
|
|
||||||
}
|
}
|
||||||
+80
@@ -0,0 +1,80 @@
|
|||||||
|
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
||||||
|
|
||||||
|
package at.mocode.identity.infrastructure.persistence
|
||||||
|
|
||||||
|
import at.mocode.identity.domain.model.DomProfil
|
||||||
|
import at.mocode.identity.domain.repository.ProfileRepository
|
||||||
|
import org.jetbrains.exposed.v1.core.ResultRow
|
||||||
|
import org.jetbrains.exposed.v1.core.eq
|
||||||
|
import org.jetbrains.exposed.v1.core.statements.UpdateBuilder
|
||||||
|
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.transactions.transaction
|
||||||
|
import org.jetbrains.exposed.v1.jdbc.update
|
||||||
|
import kotlin.time.Clock
|
||||||
|
import kotlin.uuid.Uuid
|
||||||
|
import kotlin.uuid.toJavaUuid
|
||||||
|
import kotlin.uuid.toKotlinUuid
|
||||||
|
|
||||||
|
class ExposedProfileRepository : ProfileRepository {
|
||||||
|
|
||||||
|
override suspend fun findById(id: Uuid): DomProfil? = transaction {
|
||||||
|
ProfileTable.selectAll().where { ProfileTable.id eq id.toJavaUuid() }
|
||||||
|
.map { rowToProfile(it) }
|
||||||
|
.singleOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun findByUserId(userId: String): DomProfil? = transaction {
|
||||||
|
ProfileTable.selectAll().where { ProfileTable.userId eq userId }
|
||||||
|
.map { rowToProfile(it) }
|
||||||
|
.singleOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun findBySatznummer(satznummer: String): List<DomProfil> = transaction {
|
||||||
|
ProfileTable.selectAll().where { ProfileTable.satznummer eq satznummer }
|
||||||
|
.map { rowToProfile(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun save(profil: DomProfil): DomProfil = transaction {
|
||||||
|
val now = Clock.System.now()
|
||||||
|
val updated = profil.copy(updatedAt = now)
|
||||||
|
val javaId = profil.profileId.toJavaUuid()
|
||||||
|
|
||||||
|
val existing = ProfileTable.selectAll().where { ProfileTable.id eq javaId }.singleOrNull()
|
||||||
|
if (existing != null) {
|
||||||
|
ProfileTable.update({ ProfileTable.id eq javaId }) { profileToStatement(it, updated) }
|
||||||
|
} else {
|
||||||
|
ProfileTable.insert {
|
||||||
|
it[id] = javaId
|
||||||
|
profileToStatement(it, updated)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updated
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun delete(id: Uuid): Boolean = transaction {
|
||||||
|
ProfileTable.deleteWhere { ProfileTable.id eq id.toJavaUuid() } > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun rowToProfile(row: ResultRow): DomProfil = DomProfil(
|
||||||
|
profileId = row[ProfileTable.id].toKotlinUuid(),
|
||||||
|
userId = row[ProfileTable.userId],
|
||||||
|
satznummer = row[ProfileTable.satznummer],
|
||||||
|
logoUrl = row[ProfileTable.logoUrl],
|
||||||
|
bio = row[ProfileTable.bio],
|
||||||
|
contactEmail = row[ProfileTable.contactEmail],
|
||||||
|
createdAt = row[ProfileTable.createdAt],
|
||||||
|
updatedAt = row[ProfileTable.updatedAt]
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun profileToStatement(stmt: UpdateBuilder<*>, p: DomProfil) {
|
||||||
|
stmt[ProfileTable.userId] = p.userId
|
||||||
|
stmt[ProfileTable.satznummer] = p.satznummer
|
||||||
|
stmt[ProfileTable.logoUrl] = p.logoUrl
|
||||||
|
stmt[ProfileTable.bio] = p.bio
|
||||||
|
stmt[ProfileTable.contactEmail] = p.contactEmail
|
||||||
|
stmt[ProfileTable.createdAt] = p.createdAt
|
||||||
|
stmt[ProfileTable.updatedAt] = p.updatedAt
|
||||||
|
}
|
||||||
|
}
|
||||||
+29
@@ -0,0 +1,29 @@
|
|||||||
|
package at.mocode.identity.infrastructure.persistence
|
||||||
|
|
||||||
|
import org.jetbrains.exposed.v1.core.Table
|
||||||
|
import org.jetbrains.exposed.v1.core.java.javaUUID
|
||||||
|
import org.jetbrains.exposed.v1.datetime.timestamp
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exposed Table definition for user profiles.
|
||||||
|
* Links Keycloak user IDs to official ZNS satznummer.
|
||||||
|
*/
|
||||||
|
object ProfileTable : Table("identity_profiles") {
|
||||||
|
val id = javaUUID("id").autoGenerate()
|
||||||
|
override val primaryKey = PrimaryKey(id)
|
||||||
|
|
||||||
|
val userId = varchar("user_id", 100)
|
||||||
|
val satznummer = varchar("satznummer", 20)
|
||||||
|
|
||||||
|
val logoUrl = text("logo_url").nullable()
|
||||||
|
val bio = text("bio").nullable()
|
||||||
|
val contactEmail = varchar("contact_email", 200).nullable()
|
||||||
|
|
||||||
|
val createdAt = timestamp("created_at")
|
||||||
|
val updatedAt = timestamp("updated_at")
|
||||||
|
|
||||||
|
init {
|
||||||
|
index(true, userId)
|
||||||
|
index(false, satznummer)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
plugins {
|
||||||
|
alias(libs.plugins.kotlinJvm)
|
||||||
|
alias(libs.plugins.kotlinSpring)
|
||||||
|
alias(libs.plugins.spring.boot)
|
||||||
|
alias(libs.plugins.spring.dependencyManagement)
|
||||||
|
}
|
||||||
|
|
||||||
|
springBoot {
|
||||||
|
mainClass.set("at.mocode.identity.service.IdentityServiceApplicationKt")
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(projects.platform.platformDependencies)
|
||||||
|
implementation(projects.core.coreDomain)
|
||||||
|
implementation(projects.core.coreUtils)
|
||||||
|
implementation(projects.backend.services.identity.identityDomain)
|
||||||
|
implementation(projects.backend.services.identity.identityInfrastructure)
|
||||||
|
implementation(projects.backend.infrastructure.security)
|
||||||
|
implementation(projects.backend.infrastructure.persistence)
|
||||||
|
|
||||||
|
implementation(libs.spring.boot.starter.web)
|
||||||
|
implementation(libs.spring.boot.starter.validation)
|
||||||
|
implementation(libs.spring.boot.starter.security)
|
||||||
|
implementation(libs.spring.boot.starter.oauth2.resource.server)
|
||||||
|
|
||||||
|
implementation(libs.exposed.core)
|
||||||
|
implementation(libs.exposed.jdbc)
|
||||||
|
implementation(libs.hikari.cp)
|
||||||
|
runtimeOnly(libs.postgresql.driver)
|
||||||
|
}
|
||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
package at.mocode.identity.service
|
||||||
|
|
||||||
|
import org.springframework.boot.autoconfigure.SpringBootApplication
|
||||||
|
import org.springframework.boot.runApplication
|
||||||
|
|
||||||
|
@SpringBootApplication(scanBasePackages = ["at.mocode.identity", "at.mocode.infrastructure.security", "at.mocode.backend.infrastructure.persistence"])
|
||||||
|
class IdentityServiceApplication
|
||||||
|
|
||||||
|
fun main(args: Array<String>) {
|
||||||
|
runApplication<IdentityServiceApplication>(*args)
|
||||||
|
}
|
||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
package at.mocode.identity.service.config
|
||||||
|
|
||||||
|
import at.mocode.identity.domain.repository.ProfileRepository
|
||||||
|
import at.mocode.identity.domain.service.ProfileService
|
||||||
|
import at.mocode.identity.infrastructure.persistence.ExposedProfileRepository
|
||||||
|
import org.springframework.context.annotation.Bean
|
||||||
|
import org.springframework.context.annotation.Configuration
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
class IdentityConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
fun profileRepository(): ProfileRepository = ExposedProfileRepository()
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
fun profileService(profileRepository: ProfileRepository): ProfileService =
|
||||||
|
ProfileService(profileRepository)
|
||||||
|
}
|
||||||
+46
@@ -0,0 +1,46 @@
|
|||||||
|
package at.mocode.identity.service.web
|
||||||
|
|
||||||
|
import at.mocode.identity.domain.model.DomProfil
|
||||||
|
import at.mocode.identity.domain.service.ProfileService
|
||||||
|
import org.springframework.security.core.annotation.AuthenticationPrincipal
|
||||||
|
import org.springframework.security.oauth2.jwt.Jwt
|
||||||
|
import org.springframework.web.bind.annotation.*
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/profiles")
|
||||||
|
class ProfileController(
|
||||||
|
private val profileService: ProfileService
|
||||||
|
) {
|
||||||
|
|
||||||
|
@GetMapping("/me")
|
||||||
|
suspend fun getMyProfile(@AuthenticationPrincipal jwt: Jwt): DomProfil? {
|
||||||
|
return profileService.getProfileByUserId(jwt.subject)
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/link/{satznummer}")
|
||||||
|
suspend fun linkToZns(
|
||||||
|
@AuthenticationPrincipal jwt: Jwt,
|
||||||
|
@PathVariable satznummer: String
|
||||||
|
): DomProfil {
|
||||||
|
return profileService.linkUserToZns(jwt.subject, satznummer)
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/me")
|
||||||
|
suspend fun updateMyProfile(
|
||||||
|
@AuthenticationPrincipal jwt: Jwt,
|
||||||
|
@RequestBody request: ProfileUpdateRequest
|
||||||
|
): DomProfil {
|
||||||
|
return profileService.updateProfile(
|
||||||
|
userId = jwt.subject,
|
||||||
|
logoUrl = request.logoUrl,
|
||||||
|
bio = request.bio,
|
||||||
|
contactEmail = request.contactEmail
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class ProfileUpdateRequest(
|
||||||
|
val logoUrl: String? = null,
|
||||||
|
val bio: String? = null,
|
||||||
|
val contactEmail: String? = null
|
||||||
|
)
|
||||||
@@ -1,11 +1,7 @@
|
|||||||
plugins {
|
plugins {
|
||||||
// KORREKTUR: Alle Plugins werden jetzt konsistent über den Version Catalog geladen.
|
alias(libs.plugins.kotlinJvm)
|
||||||
alias(libs.plugins.kotlin.jvm)
|
alias(libs.plugins.kotlinSerialization)
|
||||||
alias(libs.plugins.kotlin.spring)
|
id("application")
|
||||||
alias(libs.plugins.kotlin.serialization)
|
|
||||||
alias(libs.plugins.ktor)
|
|
||||||
application
|
|
||||||
alias(libs.plugins.spring.dependencyManagement)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
application {
|
application {
|
||||||
@@ -16,8 +12,8 @@ dependencies {
|
|||||||
api(platform(libs.spring.boot.dependencies))
|
api(platform(libs.spring.boot.dependencies))
|
||||||
// Interne Module
|
// Interne Module
|
||||||
implementation(projects.platform.platformDependencies)
|
implementation(projects.platform.platformDependencies)
|
||||||
implementation(projects.masterdata.masterdataDomain)
|
implementation(projects.backend.services.masterdata.masterdataDomain)
|
||||||
implementation(projects.masterdata.masterdataApplication)
|
implementation(projects.backend.services.masterdata.masterdataCommon)
|
||||||
implementation(projects.core.coreDomain)
|
implementation(projects.core.coreDomain)
|
||||||
implementation(projects.core.coreUtils)
|
implementation(projects.core.coreUtils)
|
||||||
|
|
||||||
|
|||||||
+16
-12
@@ -1,10 +1,14 @@
|
|||||||
package at.mocode.masterdata.api
|
package at.mocode.masterdata.api
|
||||||
|
|
||||||
import at.mocode.core.domain.model.ApiResponse
|
import at.mocode.core.domain.model.ApiResponse
|
||||||
|
import at.mocode.core.domain.model.ErrorCode
|
||||||
import io.ktor.http.*
|
import io.ktor.http.*
|
||||||
import io.ktor.server.application.*
|
import io.ktor.server.application.*
|
||||||
import io.ktor.server.plugins.statuspages.*
|
import io.ktor.server.plugins.statuspages.*
|
||||||
import io.ktor.server.response.*
|
import io.ktor.server.response.*
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
|
||||||
|
private val logger = LoggerFactory.getLogger("at.mocode.masterdata.api.StatusPages")
|
||||||
|
|
||||||
// Eine einfache, eigene Exception, um "Nicht gefunden"-Fälle klarer zu machen.
|
// Eine einfache, eigene Exception, um "Nicht gefunden"-Fälle klarer zu machen.
|
||||||
class NotFoundException(message: String) : RuntimeException(message)
|
class NotFoundException(message: String) : RuntimeException(message)
|
||||||
@@ -15,10 +19,10 @@ fun Application.configureStatusPages() {
|
|||||||
// Regel 1: Fange alle "IllegalArgumentException" ab.
|
// Regel 1: Fange alle "IllegalArgumentException" ab.
|
||||||
// Das passiert bei ungültigen Eingaben, z.B. ein falsches UUID-Format.
|
// Das passiert bei ungültigen Eingaben, z.B. ein falsches UUID-Format.
|
||||||
exception<IllegalArgumentException> { call, cause ->
|
exception<IllegalArgumentException> { call, cause ->
|
||||||
log.warn("Bad Request: ${cause.message}")
|
logger.warn("Bad Request: ${cause.message}")
|
||||||
val errorResponse = ApiResponse<Unit>(
|
val errorResponse = ApiResponse.error<Unit>(
|
||||||
message = cause.message ?: "Invalid input provided.",
|
code = ErrorCode("BAD_REQUEST"),
|
||||||
errors = listOf("BAD_REQUEST")
|
message = cause.message ?: "Invalid input provided."
|
||||||
)
|
)
|
||||||
call.respond(HttpStatusCode.BadRequest, errorResponse)
|
call.respond(HttpStatusCode.BadRequest, errorResponse)
|
||||||
}
|
}
|
||||||
@@ -26,10 +30,10 @@ fun Application.configureStatusPages() {
|
|||||||
// Regel 2: Fange unsere eigene "NotFoundException" ab.
|
// Regel 2: Fange unsere eigene "NotFoundException" ab.
|
||||||
// Diese werfen wir, wenn eine Entität nicht in der DB gefunden wurde.
|
// Diese werfen wir, wenn eine Entität nicht in der DB gefunden wurde.
|
||||||
exception<NotFoundException> { call, cause ->
|
exception<NotFoundException> { call, cause ->
|
||||||
log.info("Resource not found: ${cause.message}")
|
logger.info("Resource not found: ${cause.message}")
|
||||||
val errorResponse = ApiResponse<Unit>(
|
val errorResponse = ApiResponse.error<Unit>(
|
||||||
message = cause.message ?: "The requested resource was not found.",
|
code = ErrorCode("NOT_FOUND"),
|
||||||
errors = listOf("NOT_FOUND")
|
message = cause.message ?: "The requested resource was not found."
|
||||||
)
|
)
|
||||||
call.respond(HttpStatusCode.NotFound, errorResponse)
|
call.respond(HttpStatusCode.NotFound, errorResponse)
|
||||||
}
|
}
|
||||||
@@ -37,10 +41,10 @@ fun Application.configureStatusPages() {
|
|||||||
// Regel 3: Fange alle anderen, unerwarteten Fehler ab.
|
// Regel 3: Fange alle anderen, unerwarteten Fehler ab.
|
||||||
// Das ist unser Sicherheitsnetz für alles, was wir nicht vorhergesehen haben.
|
// Das ist unser Sicherheitsnetz für alles, was wir nicht vorhergesehen haben.
|
||||||
exception<Throwable> { call, cause ->
|
exception<Throwable> { call, cause ->
|
||||||
log.error("Internal Server Error", cause) // Logge den kompletten Stacktrace
|
logger.error("Internal Server Error", cause) // Logge den kompletten Stacktrace
|
||||||
val errorResponse = ApiResponse<Unit>(
|
val errorResponse = ApiResponse.error<Unit>(
|
||||||
message = "An unexpected internal server error occurred.",
|
code = ErrorCode("INTERNAL_SERVER_ERROR"),
|
||||||
errors = listOf("INTERNAL_SERVER_ERROR")
|
message = "An unexpected internal server error occurred."
|
||||||
)
|
)
|
||||||
call.respond(HttpStatusCode.InternalServerError, errorResponse)
|
call.respond(HttpStatusCode.InternalServerError, errorResponse)
|
||||||
}
|
}
|
||||||
|
|||||||
+131
-360
@@ -1,4 +1,5 @@
|
|||||||
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
||||||
|
|
||||||
package at.mocode.masterdata.api.rest
|
package at.mocode.masterdata.api.rest
|
||||||
|
|
||||||
import at.mocode.core.domain.model.ApiResponse
|
import at.mocode.core.domain.model.ApiResponse
|
||||||
@@ -6,7 +7,6 @@ import at.mocode.core.domain.model.SparteE
|
|||||||
import at.mocode.masterdata.application.usecase.CreateAltersklasseUseCase
|
import at.mocode.masterdata.application.usecase.CreateAltersklasseUseCase
|
||||||
import at.mocode.masterdata.application.usecase.GetAltersklasseUseCase
|
import at.mocode.masterdata.application.usecase.GetAltersklasseUseCase
|
||||||
import at.mocode.masterdata.domain.model.AltersklasseDefinition
|
import at.mocode.masterdata.domain.model.AltersklasseDefinition
|
||||||
import at.mocode.core.utils.validation.ApiValidationUtils
|
|
||||||
import kotlin.uuid.Uuid
|
import kotlin.uuid.Uuid
|
||||||
import io.ktor.http.*
|
import io.ktor.http.*
|
||||||
import io.ktor.server.request.*
|
import io.ktor.server.request.*
|
||||||
@@ -16,18 +16,12 @@ import kotlinx.serialization.Serializable
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* REST API controller for age class management operations.
|
* REST API controller for age class management operations.
|
||||||
*
|
|
||||||
* This controller provides HTTP endpoints for the master-data context's
|
|
||||||
* age class functionality, following REST conventions and proper error handling.
|
|
||||||
*/
|
*/
|
||||||
class AltersklasseController(
|
class AltersklasseController(
|
||||||
private val getAltersklasseUseCase: GetAltersklasseUseCase,
|
private val getAltersklasseUseCase: GetAltersklasseUseCase,
|
||||||
private val createAltersklasseUseCase: CreateAltersklasseUseCase
|
private val createAltersklasseUseCase: CreateAltersklasseUseCase
|
||||||
) {
|
) {
|
||||||
|
|
||||||
/**
|
|
||||||
* DTO for age class API responses.
|
|
||||||
*/
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class AltersklasseDto(
|
data class AltersklasseDto(
|
||||||
val altersklasseId: String,
|
val altersklasseId: String,
|
||||||
@@ -44,9 +38,6 @@ class AltersklasseController(
|
|||||||
val updatedAt: String
|
val updatedAt: String
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
|
||||||
* DTO for creating a new age class.
|
|
||||||
*/
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class CreateAltersklasseDto(
|
data class CreateAltersklasseDto(
|
||||||
val altersklasseCode: String,
|
val altersklasseCode: String,
|
||||||
@@ -60,405 +51,185 @@ class AltersklasseController(
|
|||||||
val istAktiv: Boolean = true
|
val istAktiv: Boolean = true
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
|
||||||
* DTO for updating an existing age class.
|
|
||||||
*/
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class UpdateAltersklasseDto(
|
data class UpdateAltersklasseDto(
|
||||||
val altersklasseCode: String,
|
val altersklasseCode: String,
|
||||||
val bezeichnung: String,
|
val bezeichnung: String,
|
||||||
val minAlter: Int? = null,
|
val minAlter: Int? = null,
|
||||||
val maxAlter: Int? = null,
|
val maxAlter: Int? = null,
|
||||||
val stichtagRegelText: String? = "31.12. des laufenden Kalenderjahres",
|
val stichtagRegelText: String? = null,
|
||||||
val sparteFilter: String? = null,
|
val sparteFilter: String? = null,
|
||||||
val geschlechtFilter: String? = null,
|
val geschlechtFilter: String? = null,
|
||||||
val oetoRegelReferenzId: String? = null,
|
val oetoRegelReferenzId: String? = null,
|
||||||
val istAktiv: Boolean = true
|
val istAktiv: Boolean = true
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
fun Route.registerRoutes() {
|
||||||
* Configures the routing for age class endpoints.
|
route("/altersklassen") {
|
||||||
*/
|
|
||||||
fun configureRouting(routing: Routing) {
|
|
||||||
routing.route("/api/masterdata/altersklassen") {
|
|
||||||
|
|
||||||
// GET /api/masterdata/altersklassen - Get all active age classes
|
|
||||||
get {
|
get {
|
||||||
|
val sparte = call.request.queryParameters["sparte"]?.let {
|
||||||
try {
|
try {
|
||||||
val sparteFilterParam = call.request.queryParameters["sparte"]
|
SparteE.valueOf(it)
|
||||||
val sparteFilter = sparteFilterParam?.let {
|
|
||||||
try {
|
|
||||||
SparteE.valueOf(it.uppercase())
|
|
||||||
} catch (_: Exception) {
|
|
||||||
return@get call.respond(
|
|
||||||
HttpStatusCode.BadRequest,
|
|
||||||
ApiResponse<List<AltersklasseDto>>("Invalid sparte parameter: $it")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val geschlechtFilterParam = call.request.queryParameters["geschlecht"]
|
|
||||||
val geschlechtFilter = geschlechtFilterParam?.let { gender ->
|
|
||||||
if (gender.length == 1 && (gender == "M" || gender == "W")) {
|
|
||||||
gender[0]
|
|
||||||
} else {
|
|
||||||
return@get call.respond(
|
|
||||||
HttpStatusCode.BadRequest,
|
|
||||||
ApiResponse<List<AltersklasseDto>>("Invalid geschlecht parameter. Must be 'M' or 'W'")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val altersklassen = getAltersklasseUseCase.getAllActive(sparteFilter, geschlechtFilter)
|
|
||||||
val altersklasseDtos = altersklassen.map { it.toDto() }
|
|
||||||
call.respond(HttpStatusCode.OK, ApiResponse.success(altersklasseDtos))
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<List<AltersklasseDto>>("Failed to retrieve age classes: ${e.message}"))
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
val geschlecht = call.request.queryParameters["geschlecht"]?.getOrNull(0)
|
||||||
|
|
||||||
|
val response = getAltersklasseUseCase.getAllActive(sparte, geschlecht)
|
||||||
|
val dtos = response.altersklassen.map { it.toDto() }
|
||||||
|
call.respond(ApiResponse.success(dtos))
|
||||||
|
}
|
||||||
|
|
||||||
// GET /api/masterdata/altersklassen/{id} - Get age class by ID
|
|
||||||
get("/{id}") {
|
get("/{id}") {
|
||||||
|
val idStr = call.parameters["id"]
|
||||||
|
val id = idStr?.let {
|
||||||
try {
|
try {
|
||||||
val altersklasseId = call.parameters["id"]?.let { Uuid.parse(it) }
|
Uuid.parse(it)
|
||||||
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<AltersklasseDto>("Invalid age class ID"))
|
|
||||||
|
|
||||||
val altersklasse = getAltersklasseUseCase.getById(altersklasseId)
|
|
||||||
if (altersklasse != null) {
|
|
||||||
call.respond(HttpStatusCode.OK, ApiResponse.success(altersklasse.toDto()))
|
|
||||||
} else {
|
|
||||||
call.respond(HttpStatusCode.NotFound, ApiResponse.error<AltersklasseDto>("Age class not found"))
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<AltersklasseDto>("Failed to retrieve age class: ${e.message}"))
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
?: return@get call.respond(
|
||||||
// GET /api/masterdata/altersklassen/code/{code} - Get age class by code
|
|
||||||
get("/code/{code}") {
|
|
||||||
try {
|
|
||||||
val altersklasseCode = call.parameters["code"]
|
|
||||||
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<AltersklasseDto>("Age class code is required"))
|
|
||||||
|
|
||||||
val altersklasse = getAltersklasseUseCase.getByCode(altersklasseCode)
|
|
||||||
if (altersklasse != null) {
|
|
||||||
call.respond(HttpStatusCode.OK, ApiResponse.success(altersklasse.toDto()))
|
|
||||||
} else {
|
|
||||||
call.respond(HttpStatusCode.NotFound, ApiResponse.error<AltersklasseDto>("Age class not found"))
|
|
||||||
}
|
|
||||||
} catch (e: IllegalArgumentException) {
|
|
||||||
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<AltersklasseDto>(e.message ?: "Invalid age class code"))
|
|
||||||
} catch (e: Exception) {
|
|
||||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<AltersklasseDto>("Failed to retrieve age class: ${e.message}"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GET /api/masterdata/altersklassen/search - Search age classes by name
|
|
||||||
get("/search") {
|
|
||||||
try {
|
|
||||||
val validationErrors = ApiValidationUtils.validateQueryParameters(
|
|
||||||
limit = call.request.queryParameters["limit"],
|
|
||||||
q = call.request.queryParameters["q"]
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!ApiValidationUtils.isValid(validationErrors)) {
|
|
||||||
call.respond(
|
|
||||||
HttpStatusCode.BadRequest,
|
HttpStatusCode.BadRequest,
|
||||||
ApiResponse.error<List<AltersklasseDto>>(ApiValidationUtils.createErrorMessage(validationErrors))
|
ApiResponse.error<Unit>("INVALID_ID", "Missing or invalid ID")
|
||||||
)
|
)
|
||||||
return@get
|
|
||||||
|
val response = getAltersklasseUseCase.getById(id)
|
||||||
|
response.altersklasse?.let {
|
||||||
|
call.respond(ApiResponse.success(it.toDto()))
|
||||||
|
} ?: call.respond(HttpStatusCode.NotFound, ApiResponse.error<Unit>("NOT_FOUND", "Age class not found"))
|
||||||
}
|
}
|
||||||
|
|
||||||
val searchTerm = call.request.queryParameters["q"]
|
|
||||||
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<List<AltersklasseDto>>("Search term 'q' is required"))
|
|
||||||
|
|
||||||
val limit = call.request.queryParameters["limit"]?.toIntOrNull() ?: 50
|
|
||||||
|
|
||||||
val altersklassen = getAltersklasseUseCase.searchByName(searchTerm, limit)
|
|
||||||
val altersklasseDtos = altersklassen.map { it.toDto() }
|
|
||||||
call.respond(HttpStatusCode.OK, ApiResponse.success(altersklasseDtos))
|
|
||||||
} catch (e: IllegalArgumentException) {
|
|
||||||
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<List<AltersklasseDto>>(e.message ?: "Invalid search parameters"))
|
|
||||||
} catch (e: Exception) {
|
|
||||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<List<AltersklasseDto>>("Failed to search age classes: ${e.message}"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GET /api/masterdata/altersklassen/age/{age} - Get age classes applicable for specific age
|
|
||||||
get("/age/{age}") {
|
|
||||||
try {
|
|
||||||
val age = call.parameters["age"]?.toIntOrNull()
|
|
||||||
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<List<AltersklasseDto>>("Invalid age parameter"))
|
|
||||||
|
|
||||||
if (age < 0) {
|
|
||||||
return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<List<AltersklasseDto>>("Age must be non-negative"))
|
|
||||||
}
|
|
||||||
|
|
||||||
val sparteFilterParam = call.request.queryParameters["sparte"]
|
|
||||||
val sparteFilter = sparteFilterParam?.let { SparteE.valueOf(it.uppercase()) }
|
|
||||||
|
|
||||||
val geschlechtFilterParam = call.request.queryParameters["geschlecht"]
|
|
||||||
val geschlechtFilter = geschlechtFilterParam?.let { gender ->
|
|
||||||
if (gender.length == 1 && (gender == "M" || gender == "W")) {
|
|
||||||
gender[0]
|
|
||||||
} else {
|
|
||||||
return@get call.respond(
|
|
||||||
HttpStatusCode.BadRequest,
|
|
||||||
ApiResponse.error<List<AltersklasseDto>>("Invalid geschlecht parameter. Must be 'M' or 'W'")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val altersklassen = getAltersklasseUseCase.getApplicableForAge(age, sparteFilter, geschlechtFilter)
|
|
||||||
val altersklasseDtos = altersklassen.map { it.toDto() }
|
|
||||||
call.respond(HttpStatusCode.OK, ApiResponse.success(altersklasseDtos))
|
|
||||||
} catch (e: Exception) {
|
|
||||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<List<AltersklasseDto>>("Failed to retrieve age classes: ${e.message}"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GET /api/masterdata/altersklassen/sparte/{sparte} - Get age classes by sport type
|
|
||||||
get("/sparte/{sparte}") {
|
|
||||||
try {
|
|
||||||
val sparteParam = call.parameters["sparte"]
|
|
||||||
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<List<AltersklasseDto>>("Sport type is required"))
|
|
||||||
|
|
||||||
val sparte = try {
|
|
||||||
SparteE.valueOf(sparteParam.uppercase())
|
|
||||||
} catch (_: Exception) {
|
|
||||||
return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<List<AltersklasseDto>>("Invalid sport type: $sparteParam"))
|
|
||||||
}
|
|
||||||
|
|
||||||
val activeOnlyParam = call.request.queryParameters["activeOnly"]
|
|
||||||
val activeOnly = activeOnlyParam?.toBoolean() ?: true
|
|
||||||
|
|
||||||
val altersklassen = getAltersklasseUseCase.getBySparte(sparte, activeOnly)
|
|
||||||
val altersklasseDtos = altersklassen.map { it.toDto() }
|
|
||||||
call.respond(HttpStatusCode.OK, ApiResponse.success(altersklasseDtos))
|
|
||||||
} catch (e: Exception) {
|
|
||||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<List<AltersklasseDto>>("Failed to retrieve age classes: ${e.message}"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// POST /api/masterdata/altersklassen - Create new age class
|
|
||||||
post {
|
post {
|
||||||
try {
|
val dto = call.receive<CreateAltersklasseDto>()
|
||||||
val createDto = call.receive<CreateAltersklasseDto>()
|
|
||||||
|
|
||||||
// Basic validation
|
|
||||||
if (createDto.altersklasseCode.isBlank()) {
|
|
||||||
call.respond(
|
|
||||||
HttpStatusCode.BadRequest,
|
|
||||||
ApiResponse.error<AltersklasseDto>("Age class code is required")
|
|
||||||
)
|
|
||||||
return@post
|
|
||||||
}
|
|
||||||
|
|
||||||
if (createDto.bezeichnung.isBlank()) {
|
|
||||||
call.respond(
|
|
||||||
HttpStatusCode.BadRequest,
|
|
||||||
ApiResponse.error<AltersklasseDto>("Bezeichnung is required")
|
|
||||||
)
|
|
||||||
return@post
|
|
||||||
}
|
|
||||||
|
|
||||||
val sparteFilter = createDto.sparteFilter?.let {
|
|
||||||
try {
|
|
||||||
SparteE.valueOf(it.uppercase())
|
|
||||||
} catch (_: Exception) {
|
|
||||||
return@post call.respond(
|
|
||||||
HttpStatusCode.BadRequest,
|
|
||||||
ApiResponse.error<AltersklasseDto>("Invalid sparte filter: $it")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val geschlechtFilter = createDto.geschlechtFilter?.let { gender ->
|
|
||||||
if (gender.length == 1 && (gender == "M" || gender == "W")) {
|
|
||||||
gender[0]
|
|
||||||
} else {
|
|
||||||
return@post call.respond(
|
|
||||||
HttpStatusCode.BadRequest,
|
|
||||||
ApiResponse.error<AltersklasseDto>("Invalid geschlecht filter. Must be 'M' or 'W'")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val oetoRegelReferenzId = createDto.oetoRegelReferenzId?.let {
|
|
||||||
try {
|
|
||||||
Uuid.parse(it)
|
|
||||||
} catch (_: Exception) {
|
|
||||||
return@post call.respond(
|
|
||||||
HttpStatusCode.BadRequest,
|
|
||||||
ApiResponse.error<AltersklasseDto>("Invalid OETO regel referenz ID format")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val request = CreateAltersklasseUseCase.CreateAltersklasseRequest(
|
val request = CreateAltersklasseUseCase.CreateAltersklasseRequest(
|
||||||
altersklasseCode = createDto.altersklasseCode,
|
altersklasseCode = dto.altersklasseCode,
|
||||||
bezeichnung = createDto.bezeichnung,
|
bezeichnung = dto.bezeichnung,
|
||||||
minAlter = createDto.minAlter,
|
minAlter = dto.minAlter,
|
||||||
maxAlter = createDto.maxAlter,
|
maxAlter = dto.maxAlter,
|
||||||
stichtagRegelText = createDto.stichtagRegelText,
|
stichtagRegelText = dto.stichtagRegelText,
|
||||||
sparteFilter = sparteFilter,
|
sparteFilter = dto.sparteFilter?.let {
|
||||||
geschlechtFilter = geschlechtFilter,
|
try {
|
||||||
oetoRegelReferenzId = oetoRegelReferenzId,
|
SparteE.valueOf(it)
|
||||||
istAktiv = createDto.istAktiv
|
|
||||||
)
|
|
||||||
|
|
||||||
val result = createAltersklasseUseCase.createAltersklasse(request)
|
|
||||||
if (result.success) {
|
|
||||||
call.respond(HttpStatusCode.Created, ApiResponse.success(result.altersklasse!!.toDto()))
|
|
||||||
} else {
|
|
||||||
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<AltersklasseDto>("Validation failed: ${result.errors.joinToString(", ")}"))
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<AltersklasseDto>("Failed to create age class: ${e.message}"))
|
null
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
geschlechtFilter = dto.geschlechtFilter?.getOrNull(0),
|
||||||
// PUT /api/masterdata/altersklassen/{id} - Update existing age class
|
oetoRegelReferenzId = dto.oetoRegelReferenzId?.let {
|
||||||
put("/{id}") {
|
|
||||||
try {
|
|
||||||
val altersklasseId = call.parameters["id"]?.let { Uuid.parse(it) }
|
|
||||||
?: return@put call.respond(HttpStatusCode.BadRequest, ApiResponse.error<AltersklasseDto>("Invalid age class ID"))
|
|
||||||
|
|
||||||
val updateDto = call.receive<UpdateAltersklasseDto>()
|
|
||||||
|
|
||||||
// Basic validation
|
|
||||||
if (updateDto.altersklasseCode.isBlank()) {
|
|
||||||
call.respond(
|
|
||||||
HttpStatusCode.BadRequest,
|
|
||||||
ApiResponse.error<AltersklasseDto>("Age class code is required")
|
|
||||||
)
|
|
||||||
return@put
|
|
||||||
}
|
|
||||||
|
|
||||||
if (updateDto.bezeichnung.isBlank()) {
|
|
||||||
call.respond(
|
|
||||||
HttpStatusCode.BadRequest,
|
|
||||||
ApiResponse.error<AltersklasseDto>("Bezeichnung is required")
|
|
||||||
)
|
|
||||||
return@put
|
|
||||||
}
|
|
||||||
|
|
||||||
val sparteFilter = updateDto.sparteFilter?.let {
|
|
||||||
try {
|
|
||||||
SparteE.valueOf(it.uppercase())
|
|
||||||
} catch (_: Exception) {
|
|
||||||
return@put call.respond(
|
|
||||||
HttpStatusCode.BadRequest,
|
|
||||||
ApiResponse.error<AltersklasseDto>("Invalid sparte filter: $it")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val geschlechtFilter = updateDto.geschlechtFilter?.let { gender ->
|
|
||||||
if (gender.length == 1 && (gender == "M" || gender == "W")) {
|
|
||||||
gender[0]
|
|
||||||
} else {
|
|
||||||
return@put call.respond(
|
|
||||||
HttpStatusCode.BadRequest,
|
|
||||||
ApiResponse.error<AltersklasseDto>("Invalid geschlecht filter. Must be 'M' or 'W'")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val oetoRegelReferenzId = updateDto.oetoRegelReferenzId?.let {
|
|
||||||
try {
|
try {
|
||||||
Uuid.parse(it)
|
Uuid.parse(it)
|
||||||
} catch (_: Exception) {
|
} catch (e: Exception) {
|
||||||
return@put call.respond(
|
null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
istAktiv = dto.istAktiv
|
||||||
|
)
|
||||||
|
|
||||||
|
val response = createAltersklasseUseCase.createAltersklasse(request)
|
||||||
|
val altersklasse = response.altersklasse
|
||||||
|
if (response.success && altersklasse != null) {
|
||||||
|
call.respond(HttpStatusCode.Created, ApiResponse.success(altersklasse.toDto()))
|
||||||
|
} else {
|
||||||
|
call.respond(
|
||||||
HttpStatusCode.BadRequest,
|
HttpStatusCode.BadRequest,
|
||||||
ApiResponse.error<AltersklasseDto>("Invalid OETO regel referenz ID format")
|
ApiResponse.error<Unit>("CREATION_FAILED", response.errors.joinToString())
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
put("/{id}") {
|
||||||
|
val idStr = call.parameters["id"]
|
||||||
|
val id = idStr?.let {
|
||||||
|
try {
|
||||||
|
Uuid.parse(it)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?: return@put call.respond(
|
||||||
|
HttpStatusCode.BadRequest,
|
||||||
|
ApiResponse.error<Unit>("INVALID_ID", "Missing or invalid ID")
|
||||||
|
)
|
||||||
|
|
||||||
|
val dto = call.receive<UpdateAltersklasseDto>()
|
||||||
val request = CreateAltersklasseUseCase.UpdateAltersklasseRequest(
|
val request = CreateAltersklasseUseCase.UpdateAltersklasseRequest(
|
||||||
altersklasseId = altersklasseId,
|
altersklasseId = id,
|
||||||
altersklasseCode = updateDto.altersklasseCode,
|
altersklasseCode = dto.altersklasseCode,
|
||||||
bezeichnung = updateDto.bezeichnung,
|
bezeichnung = dto.bezeichnung,
|
||||||
minAlter = updateDto.minAlter,
|
minAlter = dto.minAlter,
|
||||||
maxAlter = updateDto.maxAlter,
|
maxAlter = dto.maxAlter,
|
||||||
stichtagRegelText = updateDto.stichtagRegelText,
|
stichtagRegelText = dto.stichtagRegelText,
|
||||||
sparteFilter = sparteFilter,
|
sparteFilter = dto.sparteFilter?.let {
|
||||||
geschlechtFilter = geschlechtFilter,
|
try {
|
||||||
oetoRegelReferenzId = oetoRegelReferenzId,
|
SparteE.valueOf(it)
|
||||||
istAktiv = updateDto.istAktiv
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
geschlechtFilter = dto.geschlechtFilter?.getOrNull(0),
|
||||||
|
oetoRegelReferenzId = dto.oetoRegelReferenzId?.let {
|
||||||
|
try {
|
||||||
|
Uuid.parse(it)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
istAktiv = dto.istAktiv
|
||||||
)
|
)
|
||||||
|
|
||||||
val result = createAltersklasseUseCase.updateAltersklasse(request)
|
val response = createAltersklasseUseCase.updateAltersklasse(request)
|
||||||
if (result.success) {
|
val altersklasse = response.altersklasse
|
||||||
call.respond(HttpStatusCode.OK, ApiResponse.success(result.altersklasse!!.toDto()))
|
if (response.success && altersklasse != null) {
|
||||||
|
call.respond(ApiResponse.success(altersklasse.toDto()))
|
||||||
} else {
|
} else {
|
||||||
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<AltersklasseDto>("Validation failed: ${result.errors.joinToString(", ")}"))
|
call.respond(
|
||||||
}
|
HttpStatusCode.BadRequest,
|
||||||
} catch (e: Exception) {
|
ApiResponse.error<Unit>("UPDATE_FAILED", response.errors.joinToString())
|
||||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<AltersklasseDto>("Failed to update age class: ${e.message}"))
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// DELETE /api/masterdata/altersklassen/{id} - Delete age class
|
|
||||||
delete("/{id}") {
|
delete("/{id}") {
|
||||||
|
val idStr = call.parameters["id"]
|
||||||
|
val id = idStr?.let {
|
||||||
try {
|
try {
|
||||||
val altersklasseId = call.parameters["id"]?.let { Uuid.parse(it) }
|
Uuid.parse(it)
|
||||||
?: return@delete call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Unit>("Invalid age class ID"))
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?: return@delete call.respond(
|
||||||
|
HttpStatusCode.BadRequest,
|
||||||
|
ApiResponse.error<Unit>("INVALID_ID", "Missing or invalid ID")
|
||||||
|
)
|
||||||
|
|
||||||
val result = createAltersklasseUseCase.deleteAltersklasse(altersklasseId)
|
val response = createAltersklasseUseCase.deleteAltersklasse(id)
|
||||||
if (result.success) {
|
if (response.success) {
|
||||||
call.respond(HttpStatusCode.NoContent)
|
call.respond(ApiResponse.success(Unit))
|
||||||
} else {
|
} else {
|
||||||
call.respond(HttpStatusCode.NotFound, ApiResponse.error<Unit>("Age class not found: ${result.errors.joinToString(", ")}"))
|
call.respond(
|
||||||
}
|
HttpStatusCode.NotFound,
|
||||||
} catch (e: Exception) {
|
ApiResponse.error<Unit>("DELETE_FAILED", response.errors.joinToString())
|
||||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<Unit>("Failed to delete age class: ${e.message}"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GET /api/masterdata/altersklassen/eligible/{id} - Check eligibility for age class
|
|
||||||
get("/eligible/{id}") {
|
|
||||||
try {
|
|
||||||
val altersklasseId = call.parameters["id"]?.let { Uuid.parse(it) }
|
|
||||||
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Boolean>("Invalid age class ID"))
|
|
||||||
|
|
||||||
val ageParam = call.request.queryParameters["age"]?.toIntOrNull()
|
|
||||||
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Boolean>("Age parameter is required"))
|
|
||||||
|
|
||||||
val geschlechtParam = call.request.queryParameters["geschlecht"]
|
|
||||||
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Boolean>("Gender parameter is required"))
|
|
||||||
|
|
||||||
if (geschlechtParam.length != 1 || (geschlechtParam != "M" && geschlechtParam != "W")) {
|
|
||||||
return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Boolean>("Gender must be 'M' or 'W'"))
|
|
||||||
}
|
|
||||||
|
|
||||||
val isEligible = getAltersklasseUseCase.isEligible(altersklasseId, ageParam, geschlechtParam[0])
|
|
||||||
call.respond(HttpStatusCode.OK, ApiResponse.success(isEligible))
|
|
||||||
} catch (e: Exception) {
|
|
||||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<Boolean>("Failed to check eligibility: ${e.message}"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extension function to convert AltersklasseDefinition domain object to AltersklasseDto.
|
|
||||||
*/
|
|
||||||
private fun AltersklasseDefinition.toDto(): AltersklasseDto {
|
|
||||||
return AltersklasseDto(
|
|
||||||
altersklasseId = this.altersklasseId.toString(),
|
|
||||||
altersklasseCode = this.altersklasseCode,
|
|
||||||
bezeichnung = this.bezeichnung,
|
|
||||||
minAlter = this.minAlter,
|
|
||||||
maxAlter = this.maxAlter,
|
|
||||||
stichtagRegelText = this.stichtagRegelText,
|
|
||||||
sparteFilter = this.sparteFilter?.name,
|
|
||||||
geschlechtFilter = this.geschlechtFilter?.toString(),
|
|
||||||
oetoRegelReferenzId = this.oetoRegelReferenzId?.toString(),
|
|
||||||
istAktiv = this.istAktiv,
|
|
||||||
createdAt = this.createdAt.toString(),
|
|
||||||
updatedAt = this.updatedAt.toString()
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun AltersklasseDefinition.toDto() = AltersklasseDto(
|
||||||
|
altersklasseId = altersklasseId.toString(),
|
||||||
|
altersklasseCode = altersklasseCode,
|
||||||
|
bezeichnung = bezeichnung,
|
||||||
|
minAlter = minAlter,
|
||||||
|
maxAlter = maxAlter,
|
||||||
|
stichtagRegelText = stichtagRegelText,
|
||||||
|
sparteFilter = sparteFilter?.name,
|
||||||
|
geschlechtFilter = geschlechtFilter?.toString(),
|
||||||
|
oetoRegelReferenzId = oetoRegelReferenzId?.toString(),
|
||||||
|
istAktiv = istAktiv,
|
||||||
|
createdAt = createdAt.toString(),
|
||||||
|
updatedAt = updatedAt.toString()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
+66
-292
@@ -5,7 +5,6 @@ import at.mocode.core.domain.model.ApiResponse
|
|||||||
import at.mocode.masterdata.application.usecase.CreateBundeslandUseCase
|
import at.mocode.masterdata.application.usecase.CreateBundeslandUseCase
|
||||||
import at.mocode.masterdata.application.usecase.GetBundeslandUseCase
|
import at.mocode.masterdata.application.usecase.GetBundeslandUseCase
|
||||||
import at.mocode.masterdata.domain.model.BundeslandDefinition
|
import at.mocode.masterdata.domain.model.BundeslandDefinition
|
||||||
import at.mocode.core.utils.validation.ApiValidationUtils
|
|
||||||
import kotlin.uuid.Uuid
|
import kotlin.uuid.Uuid
|
||||||
import io.ktor.http.*
|
import io.ktor.http.*
|
||||||
import io.ktor.server.request.*
|
import io.ktor.server.request.*
|
||||||
@@ -14,19 +13,13 @@ import io.ktor.server.routing.*
|
|||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* REST API controller for federal state management operations.
|
* REST API controller for federal state (Bundesland) management.
|
||||||
*
|
|
||||||
* This controller provides HTTP endpoints for the master-data context's
|
|
||||||
* federal state functionality, following REST conventions and proper error handling.
|
|
||||||
*/
|
*/
|
||||||
class BundeslandController(
|
class BundeslandController(
|
||||||
private val getBundeslandUseCase: GetBundeslandUseCase,
|
private val getBundeslandUseCase: GetBundeslandUseCase,
|
||||||
private val createBundeslandUseCase: CreateBundeslandUseCase
|
private val createBundeslandUseCase: CreateBundeslandUseCase
|
||||||
) {
|
) {
|
||||||
|
|
||||||
/**
|
|
||||||
* DTO for federal state API responses.
|
|
||||||
*/
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class BundeslandDto(
|
data class BundeslandDto(
|
||||||
val bundeslandId: String,
|
val bundeslandId: String,
|
||||||
@@ -42,9 +35,6 @@ class BundeslandController(
|
|||||||
val updatedAt: String
|
val updatedAt: String
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
|
||||||
* DTO for creating a new federal state.
|
|
||||||
*/
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class CreateBundeslandDto(
|
data class CreateBundeslandDto(
|
||||||
val landId: String,
|
val landId: String,
|
||||||
@@ -57,313 +47,97 @@ class BundeslandController(
|
|||||||
val sortierReihenfolge: Int? = null
|
val sortierReihenfolge: Int? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
fun Route.registerRoutes() {
|
||||||
* DTO for updating an existing federal state.
|
route("/bundeslaender") {
|
||||||
*/
|
|
||||||
@Serializable
|
|
||||||
data class UpdateBundeslandDto(
|
|
||||||
val landId: String,
|
|
||||||
val oepsCode: String? = null,
|
|
||||||
val iso3166_2_Code: String? = null,
|
|
||||||
val name: String,
|
|
||||||
val kuerzel: String? = null,
|
|
||||||
val wappenUrl: String? = null,
|
|
||||||
val istAktiv: Boolean = true,
|
|
||||||
val sortierReihenfolge: Int? = null
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Configures the routing for federal state endpoints.
|
|
||||||
*/
|
|
||||||
fun configureRouting(routing: Routing) {
|
|
||||||
routing.route("/api/masterdata/bundeslaender") {
|
|
||||||
|
|
||||||
// GET /api/masterdata/bundeslaender - Get all active federal states
|
|
||||||
get {
|
get {
|
||||||
|
val landId = call.request.queryParameters["landId"]?.let {
|
||||||
try {
|
try {
|
||||||
val orderBySortierungParam = call.request.queryParameters["orderBySortierung"]
|
Uuid.parse(it)
|
||||||
val orderBySortierung = if (orderBySortierungParam != null) {
|
|
||||||
try {
|
|
||||||
orderBySortierungParam.toBoolean()
|
|
||||||
} catch (_: Exception) {
|
|
||||||
return@get call.respond(
|
|
||||||
HttpStatusCode.BadRequest,
|
|
||||||
ApiResponse.error<List<BundeslandDto>>("Invalid orderBySortierung parameter. Must be true or false")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
val bundeslaender = getBundeslandUseCase.getAllActive(orderBySortierung)
|
|
||||||
val bundeslandDtos = bundeslaender.map { it.toDto() }
|
|
||||||
call.respond(HttpStatusCode.OK, ApiResponse.success(bundeslandDtos))
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<List<BundeslandDto>>("Failed to retrieve federal states: ${e.message}"))
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET /api/masterdata/bundeslaender/{id} - Get federal state by ID
|
val response = if (landId != null) {
|
||||||
|
getBundeslandUseCase.getByCountry(landId)
|
||||||
|
} else {
|
||||||
|
getBundeslandUseCase.getAllActive()
|
||||||
|
}
|
||||||
|
|
||||||
|
val dtos = response.bundeslaender.map { it.toDto() }
|
||||||
|
call.respond(ApiResponse.success(dtos))
|
||||||
|
}
|
||||||
|
|
||||||
get("/{id}") {
|
get("/{id}") {
|
||||||
|
val idStr = call.parameters["id"]
|
||||||
|
val id = idStr?.let {
|
||||||
try {
|
try {
|
||||||
val bundeslandId = call.parameters["id"]?.let { Uuid.parse(it) }
|
Uuid.parse(it)
|
||||||
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<BundeslandDto>("Invalid federal state ID"))
|
|
||||||
|
|
||||||
val bundesland = getBundeslandUseCase.getById(bundeslandId)
|
|
||||||
if (bundesland != null) {
|
|
||||||
call.respond(HttpStatusCode.OK, ApiResponse.success(bundesland.toDto()))
|
|
||||||
} else {
|
|
||||||
call.respond(HttpStatusCode.NotFound, ApiResponse.error<BundeslandDto>("Federal state not found"))
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<BundeslandDto>("Failed to retrieve federal state: ${e.message}"))
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
?: return@get call.respond(
|
||||||
// GET /api/masterdata/bundeslaender/oeps/{code} - Get federal state by OEPS code
|
|
||||||
get("/oeps/{code}") {
|
|
||||||
try {
|
|
||||||
val oepsCode = call.parameters["code"]
|
|
||||||
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<BundeslandDto>("OEPS code is required"))
|
|
||||||
|
|
||||||
val landIdParam = call.request.queryParameters["landId"]
|
|
||||||
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<BundeslandDto>("Country ID (landId) is required"))
|
|
||||||
|
|
||||||
val landId = try {
|
|
||||||
Uuid.parse(landIdParam)
|
|
||||||
} catch (_: Exception) {
|
|
||||||
return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<BundeslandDto>("Invalid country ID format"))
|
|
||||||
}
|
|
||||||
|
|
||||||
val bundesland = getBundeslandUseCase.getByOepsCode(oepsCode, landId)
|
|
||||||
if (bundesland != null) {
|
|
||||||
call.respond(HttpStatusCode.OK, ApiResponse.success(bundesland.toDto()))
|
|
||||||
} else {
|
|
||||||
call.respond(HttpStatusCode.NotFound, ApiResponse.error<BundeslandDto>("Federal state not found"))
|
|
||||||
}
|
|
||||||
} catch (e: IllegalArgumentException) {
|
|
||||||
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<BundeslandDto>(e.message ?: "Invalid OEPS code"))
|
|
||||||
} catch (e: Exception) {
|
|
||||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<BundeslandDto>("Failed to retrieve federal state: ${e.message}"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GET /api/masterdata/bundeslaender/iso/{code} - Get federal state by ISO 3166-2 code
|
|
||||||
get("/iso/{code}") {
|
|
||||||
try {
|
|
||||||
val isoCode = call.parameters["code"]
|
|
||||||
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<BundeslandDto>("ISO 3166-2 code is required"))
|
|
||||||
|
|
||||||
val bundesland = getBundeslandUseCase.getByIso3166_2_Code(isoCode)
|
|
||||||
if (bundesland != null) {
|
|
||||||
call.respond(HttpStatusCode.OK, ApiResponse.success(bundesland.toDto()))
|
|
||||||
} else {
|
|
||||||
call.respond(HttpStatusCode.NotFound, ApiResponse.error<BundeslandDto>("Federal state not found"))
|
|
||||||
}
|
|
||||||
} catch (e: IllegalArgumentException) {
|
|
||||||
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<BundeslandDto>(e.message ?: "Invalid ISO code"))
|
|
||||||
} catch (e: Exception) {
|
|
||||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<BundeslandDto>("Failed to retrieve federal state: ${e.message}"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GET /api/masterdata/bundeslaender/country/{countryId} - Get federal states by country
|
|
||||||
get("/country/{countryId}") {
|
|
||||||
try {
|
|
||||||
val landId = call.parameters["countryId"]?.let { Uuid.parse(it) }
|
|
||||||
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<List<BundeslandDto>>("Invalid country ID"))
|
|
||||||
|
|
||||||
val activeOnlyParam = call.request.queryParameters["activeOnly"]
|
|
||||||
val activeOnly = activeOnlyParam?.toBoolean() ?: true
|
|
||||||
|
|
||||||
val orderBySortierungParam = call.request.queryParameters["orderBySortierung"]
|
|
||||||
val orderBySortierung = orderBySortierungParam?.toBoolean() ?: true
|
|
||||||
|
|
||||||
val bundeslaender = getBundeslandUseCase.getByCountry(landId, activeOnly, orderBySortierung)
|
|
||||||
val bundeslandDtos = bundeslaender.map { it.toDto() }
|
|
||||||
call.respond(HttpStatusCode.OK, ApiResponse.success(bundeslandDtos))
|
|
||||||
} catch (e: Exception) {
|
|
||||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<List<BundeslandDto>>("Failed to retrieve federal states: ${e.message}"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GET /api/masterdata/bundeslaender/search - Search federal states by name
|
|
||||||
get("/search") {
|
|
||||||
try {
|
|
||||||
val validationErrors = ApiValidationUtils.validateQueryParameters(
|
|
||||||
limit = call.request.queryParameters["limit"],
|
|
||||||
q = call.request.queryParameters["q"]
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!ApiValidationUtils.isValid(validationErrors)) {
|
|
||||||
call.respond(
|
|
||||||
HttpStatusCode.BadRequest,
|
HttpStatusCode.BadRequest,
|
||||||
ApiResponse.error<List<BundeslandDto>>(ApiValidationUtils.createErrorMessage(validationErrors))
|
ApiResponse.error<Unit>("INVALID_ID", "Missing or invalid ID")
|
||||||
|
)
|
||||||
|
|
||||||
|
val response = getBundeslandUseCase.getById(id)
|
||||||
|
response.bundesland?.let {
|
||||||
|
call.respond(ApiResponse.success(it.toDto()))
|
||||||
|
} ?: call.respond(
|
||||||
|
HttpStatusCode.NotFound,
|
||||||
|
ApiResponse.error<Unit>("NOT_FOUND", "Federal state not found")
|
||||||
)
|
)
|
||||||
return@get
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val searchTerm = call.request.queryParameters["q"]
|
|
||||||
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<List<BundeslandDto>>("Search term 'q' is required"))
|
|
||||||
|
|
||||||
val limit = call.request.queryParameters["limit"]?.toIntOrNull() ?: 50
|
|
||||||
val landIdParam = call.request.queryParameters["landId"]
|
|
||||||
val landId = landIdParam?.let { Uuid.parse(it) }
|
|
||||||
|
|
||||||
val bundeslaender = getBundeslandUseCase.searchByName(searchTerm, landId, limit)
|
|
||||||
val bundeslandDtos = bundeslaender.map { it.toDto() }
|
|
||||||
call.respond(HttpStatusCode.OK, ApiResponse.success(bundeslandDtos))
|
|
||||||
} catch (e: IllegalArgumentException) {
|
|
||||||
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<List<BundeslandDto>>(e.message ?: "Invalid search parameters"))
|
|
||||||
} catch (e: Exception) {
|
|
||||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<List<BundeslandDto>>("Failed to search federal states: ${e.message}"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// POST /api/masterdata/bundeslaender - Create new federal state
|
|
||||||
post {
|
post {
|
||||||
try {
|
val dto = call.receive<CreateBundeslandDto>()
|
||||||
val createDto = call.receive<CreateBundeslandDto>()
|
val landId = try {
|
||||||
|
Uuid.parse(dto.landId)
|
||||||
// Basic validation
|
} catch (e: Exception) {
|
||||||
if (createDto.name.isBlank()) {
|
return@post call.respond(
|
||||||
call.respond(
|
|
||||||
HttpStatusCode.BadRequest,
|
HttpStatusCode.BadRequest,
|
||||||
ApiResponse.error<BundeslandDto>("Name is required")
|
ApiResponse.error<Unit>("INVALID_LAND_ID", "Invalid landId format")
|
||||||
)
|
)
|
||||||
return@post
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
uuidFrom(createDto.landId)
|
|
||||||
} catch (_: Exception) {
|
|
||||||
call.respond(
|
|
||||||
HttpStatusCode.BadRequest,
|
|
||||||
ApiResponse.error<BundeslandDto>("Invalid country ID format")
|
|
||||||
)
|
|
||||||
return@post
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val request = CreateBundeslandUseCase.CreateBundeslandRequest(
|
val request = CreateBundeslandUseCase.CreateBundeslandRequest(
|
||||||
landId = uuidFrom(createDto.landId),
|
landId = landId,
|
||||||
oepsCode = createDto.oepsCode,
|
oepsCode = dto.oepsCode,
|
||||||
iso3166_2_Code = createDto.iso3166_2_Code,
|
iso3166_2_Code = dto.iso3166_2_Code,
|
||||||
name = createDto.name,
|
name = dto.name,
|
||||||
kuerzel = createDto.kuerzel,
|
kuerzel = dto.kuerzel,
|
||||||
wappenUrl = createDto.wappenUrl,
|
wappenUrl = dto.wappenUrl,
|
||||||
istAktiv = createDto.istAktiv,
|
istAktiv = dto.istAktiv,
|
||||||
sortierReihenfolge = createDto.sortierReihenfolge
|
sortierReihenfolge = dto.sortierReihenfolge
|
||||||
)
|
)
|
||||||
|
|
||||||
val result = createBundeslandUseCase.createBundesland(request)
|
val response = createBundeslandUseCase.createBundesland(request)
|
||||||
if (result.success) {
|
val bundesland = response.bundesland
|
||||||
call.respond(HttpStatusCode.Created, ApiResponse.success(result.bundesland!!.toDto()))
|
if (response.success && bundesland != null) {
|
||||||
|
call.respond(HttpStatusCode.Created, ApiResponse.success(bundesland.toDto()))
|
||||||
} else {
|
} else {
|
||||||
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<BundeslandDto>("Validation failed: ${result.errors.joinToString(", ")}"))
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<BundeslandDto>("Failed to create federal state: ${e.message}"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// PUT /api/masterdata/bundeslaender/{id} - Update existing federal state
|
|
||||||
put("/{id}") {
|
|
||||||
try {
|
|
||||||
val bundeslandId = call.parameters["id"]?.let { uuidFrom(it) }
|
|
||||||
?: return@put call.respond(HttpStatusCode.BadRequest, ApiResponse.error<BundeslandDto>("Invalid federal state ID"))
|
|
||||||
|
|
||||||
val updateDto = call.receive<UpdateBundeslandDto>()
|
|
||||||
|
|
||||||
// Basic validation
|
|
||||||
if (updateDto.name.isBlank()) {
|
|
||||||
call.respond(
|
call.respond(
|
||||||
HttpStatusCode.BadRequest,
|
HttpStatusCode.BadRequest,
|
||||||
ApiResponse.error<BundeslandDto>("Name is required")
|
ApiResponse.error<Unit>("CREATION_FAILED", response.errors.joinToString())
|
||||||
)
|
|
||||||
return@put
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
uuidFrom(updateDto.landId)
|
|
||||||
} catch (_: Exception) {
|
|
||||||
call.respond(
|
|
||||||
HttpStatusCode.BadRequest,
|
|
||||||
ApiResponse.error<BundeslandDto>("Invalid country ID format")
|
|
||||||
)
|
|
||||||
return@put
|
|
||||||
}
|
|
||||||
|
|
||||||
val request = CreateBundeslandUseCase.UpdateBundeslandRequest(
|
|
||||||
bundeslandId = bundeslandId,
|
|
||||||
landId = uuidFrom(updateDto.landId),
|
|
||||||
oepsCode = updateDto.oepsCode,
|
|
||||||
iso3166_2_Code = updateDto.iso3166_2_Code,
|
|
||||||
name = updateDto.name,
|
|
||||||
kuerzel = updateDto.kuerzel,
|
|
||||||
wappenUrl = updateDto.wappenUrl,
|
|
||||||
istAktiv = updateDto.istAktiv,
|
|
||||||
sortierReihenfolge = updateDto.sortierReihenfolge
|
|
||||||
)
|
|
||||||
|
|
||||||
val result = createBundeslandUseCase.updateBundesland(request)
|
|
||||||
if (result.success) {
|
|
||||||
call.respond(HttpStatusCode.OK, ApiResponse.success(result.bundesland!!.toDto()))
|
|
||||||
} else {
|
|
||||||
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<BundeslandDto>("Validation failed: ${result.errors.joinToString(", ")}"))
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<BundeslandDto>("Failed to update federal state: ${e.message}"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DELETE /api/masterdata/bundeslaender/{id} - Delete federal state
|
|
||||||
delete("/{id}") {
|
|
||||||
try {
|
|
||||||
val bundeslandId = call.parameters["id"]?.let { uuidFrom(it) }
|
|
||||||
?: return@delete call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Unit>("Invalid federal state ID"))
|
|
||||||
|
|
||||||
val result = createBundeslandUseCase.deleteBundesland(bundeslandId)
|
|
||||||
if (result.success) {
|
|
||||||
call.respond(HttpStatusCode.NoContent)
|
|
||||||
} else {
|
|
||||||
call.respond(HttpStatusCode.NotFound, ApiResponse.error<Unit>("Federal state not found: ${result.errors.joinToString(", ")}"))
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<Unit>("Failed to delete federal state: ${e.message}"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GET /api/masterdata/bundeslaender/count/{countryId} - Count active federal states by country
|
|
||||||
get("/count/{countryId}") {
|
|
||||||
try {
|
|
||||||
val landId = call.parameters["countryId"]?.let { uuidFrom(it) }
|
|
||||||
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Long>("Invalid country ID"))
|
|
||||||
|
|
||||||
val count = getBundeslandUseCase.countActiveByCountry(landId)
|
|
||||||
call.respond(HttpStatusCode.OK, ApiResponse.success(count))
|
|
||||||
} catch (e: Exception) {
|
|
||||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<Long>("Failed to count federal states: ${e.message}"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extension function to convert BundeslandDefinition domain object to BundeslandDto.
|
|
||||||
*/
|
|
||||||
private fun BundeslandDefinition.toDto(): BundeslandDto {
|
|
||||||
return BundeslandDto(
|
|
||||||
bundeslandId = this.bundeslandId.toString(),
|
|
||||||
landId = this.landId.toString(),
|
|
||||||
oepsCode = this.oepsCode,
|
|
||||||
iso3166_2_Code = this.iso3166_2_Code,
|
|
||||||
name = this.name,
|
|
||||||
kuerzel = this.kuerzel,
|
|
||||||
wappenUrl = this.wappenUrl,
|
|
||||||
istAktiv = this.istAktiv,
|
|
||||||
sortierReihenfolge = this.sortierReihenfolge,
|
|
||||||
createdAt = this.createdAt.toString(),
|
|
||||||
updatedAt = this.updatedAt.toString()
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun BundeslandDefinition.toDto() = BundeslandDto(
|
||||||
|
bundeslandId = bundeslandId.toString(),
|
||||||
|
landId = landId.toString(),
|
||||||
|
oepsCode = oepsCode,
|
||||||
|
iso3166_2_Code = iso3166_2_Code,
|
||||||
|
name = name,
|
||||||
|
kuerzel = kuerzel,
|
||||||
|
wappenUrl = wappenUrl,
|
||||||
|
istAktiv = istAktiv,
|
||||||
|
sortierReihenfolge = sortierReihenfolge,
|
||||||
|
createdAt = createdAt.toString(),
|
||||||
|
updatedAt = updatedAt.toString()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
+52
-280
@@ -5,7 +5,6 @@ import at.mocode.core.domain.model.ApiResponse
|
|||||||
import at.mocode.masterdata.application.usecase.CreateCountryUseCase
|
import at.mocode.masterdata.application.usecase.CreateCountryUseCase
|
||||||
import at.mocode.masterdata.application.usecase.GetCountryUseCase
|
import at.mocode.masterdata.application.usecase.GetCountryUseCase
|
||||||
import at.mocode.masterdata.domain.model.LandDefinition
|
import at.mocode.masterdata.domain.model.LandDefinition
|
||||||
import at.mocode.core.utils.validation.ApiValidationUtils
|
|
||||||
import kotlin.uuid.Uuid
|
import kotlin.uuid.Uuid
|
||||||
import io.ktor.http.*
|
import io.ktor.http.*
|
||||||
import io.ktor.server.request.*
|
import io.ktor.server.request.*
|
||||||
@@ -14,19 +13,13 @@ import io.ktor.server.routing.*
|
|||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* REST API controller for country management operations.
|
* REST API controller for country (Land) management.
|
||||||
*
|
|
||||||
* This controller provides HTTP endpoints for the master-data context's
|
|
||||||
* country functionality, following REST conventions and proper error handling.
|
|
||||||
*/
|
*/
|
||||||
class CountryController(
|
class CountryController(
|
||||||
private val getCountryUseCase: GetCountryUseCase,
|
private val getCountryUseCase: GetCountryUseCase,
|
||||||
private val createCountryUseCase: CreateCountryUseCase
|
private val createCountryUseCase: CreateCountryUseCase
|
||||||
) {
|
) {
|
||||||
|
|
||||||
/**
|
|
||||||
* DTO for country API responses.
|
|
||||||
*/
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class CountryDto(
|
data class CountryDto(
|
||||||
val landId: String,
|
val landId: String,
|
||||||
@@ -44,9 +37,6 @@ class CountryController(
|
|||||||
val updatedAt: String
|
val updatedAt: String
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
|
||||||
* DTO for creating a new country.
|
|
||||||
*/
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class CreateCountryDto(
|
data class CreateCountryDto(
|
||||||
val isoAlpha2Code: String,
|
val isoAlpha2Code: String,
|
||||||
@@ -61,294 +51,76 @@ class CountryController(
|
|||||||
val sortierReihenfolge: Int? = null
|
val sortierReihenfolge: Int? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
fun Route.registerRoutes() {
|
||||||
* DTO for updating an existing country.
|
route("/countries") {
|
||||||
*/
|
|
||||||
@Serializable
|
|
||||||
data class UpdateCountryDto(
|
|
||||||
val isoAlpha2Code: String,
|
|
||||||
val isoAlpha3Code: String,
|
|
||||||
val isoNumerischerCode: String? = null,
|
|
||||||
val nameDeutsch: String,
|
|
||||||
val nameEnglisch: String? = null,
|
|
||||||
val wappenUrl: String? = null,
|
|
||||||
val istEuMitglied: Boolean? = null,
|
|
||||||
val istEwrMitglied: Boolean? = null,
|
|
||||||
val istAktiv: Boolean = true,
|
|
||||||
val sortierReihenfolge: Int? = null
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Configures the routing for country endpoints.
|
|
||||||
*/
|
|
||||||
fun configureRouting(routing: Routing) {
|
|
||||||
routing.route("/api/masterdata/countries") {
|
|
||||||
|
|
||||||
// GET /api/masterdata/countries - Get all active countries
|
|
||||||
get {
|
get {
|
||||||
try {
|
val response = getCountryUseCase.getAllActive()
|
||||||
// Validate orderBySortierung parameter if provided
|
val dtos = response.countries.map { it.toDto() }
|
||||||
val orderBySortierungParam = call.request.queryParameters["orderBySortierung"]
|
call.respond(ApiResponse.success(dtos))
|
||||||
val orderBySortierung = if (orderBySortierungParam != null) {
|
|
||||||
try {
|
|
||||||
orderBySortierungParam.toBoolean()
|
|
||||||
} catch (_: Exception) {
|
|
||||||
return@get call.respond(
|
|
||||||
HttpStatusCode.BadRequest,
|
|
||||||
ApiResponse.error<List<CountryDto>>("Invalid orderBySortierung parameter. Must be true or false")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
val countries = getCountryUseCase.getAllActive(orderBySortierung)
|
|
||||||
val countryDtos = countries.map { it.toDto() }
|
|
||||||
call.respond(HttpStatusCode.OK, ApiResponse.success(countryDtos))
|
|
||||||
} catch (e: Exception) {
|
|
||||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<List<CountryDto>>("Failed to retrieve countries: ${e.message}"))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET /api/masterdata/countries/{id} - Get country by ID
|
|
||||||
get("/{id}") {
|
get("/{id}") {
|
||||||
|
val idStr = call.parameters["id"]
|
||||||
|
val id = idStr?.let {
|
||||||
try {
|
try {
|
||||||
val countryId = call.parameters["id"]?.let { Uuid.parse(it) }
|
Uuid.parse(it)
|
||||||
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<CountryDto>("Invalid country ID"))
|
|
||||||
|
|
||||||
val country = getCountryUseCase.getById(countryId)
|
|
||||||
if (country != null) {
|
|
||||||
call.respond(HttpStatusCode.OK, ApiResponse.success(country.toDto()))
|
|
||||||
} else {
|
|
||||||
call.respond(HttpStatusCode.NotFound, ApiResponse.error<CountryDto>("Country not found"))
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<CountryDto>("Failed to retrieve country: ${e.message}"))
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
?: return@get call.respond(
|
||||||
// GET /api/masterdata/countries/iso2/{code} - Get country by ISO Alpha-2 code
|
|
||||||
get("/iso2/{code}") {
|
|
||||||
try {
|
|
||||||
val isoCode = call.parameters["code"]
|
|
||||||
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<CountryDto>("ISO code is required"))
|
|
||||||
|
|
||||||
val country = getCountryUseCase.getByIsoAlpha2Code(isoCode)
|
|
||||||
if (country != null) {
|
|
||||||
call.respond(HttpStatusCode.OK, ApiResponse.success(country.toDto()))
|
|
||||||
} else {
|
|
||||||
call.respond(HttpStatusCode.NotFound, ApiResponse.error<CountryDto>("Country not found"))
|
|
||||||
}
|
|
||||||
} catch (e: IllegalArgumentException) {
|
|
||||||
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<CountryDto>(e.message ?: "Invalid ISO code"))
|
|
||||||
} catch (e: Exception) {
|
|
||||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<CountryDto>("Failed to retrieve country: ${e.message}"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GET /api/masterdata/countries/iso3/{code} - Get country by ISO Alpha-3 code
|
|
||||||
get("/iso3/{code}") {
|
|
||||||
try {
|
|
||||||
val isoCode = call.parameters["code"]
|
|
||||||
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<CountryDto>("ISO code is required"))
|
|
||||||
|
|
||||||
val country = getCountryUseCase.getByIsoAlpha3Code(isoCode)
|
|
||||||
if (country != null) {
|
|
||||||
call.respond(HttpStatusCode.OK, ApiResponse.success(country.toDto()))
|
|
||||||
} else {
|
|
||||||
call.respond(HttpStatusCode.NotFound, ApiResponse.error<CountryDto>("Country not found"))
|
|
||||||
}
|
|
||||||
} catch (e: IllegalArgumentException) {
|
|
||||||
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<CountryDto>(e.message ?: "Invalid ISO code"))
|
|
||||||
} catch (e: Exception) {
|
|
||||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<CountryDto>("Failed to retrieve country: ${e.message}"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GET /api/masterdata/countries/search - Search countries by name
|
|
||||||
get("/search") {
|
|
||||||
try {
|
|
||||||
// Validate query parameters
|
|
||||||
val validationErrors = ApiValidationUtils.validateQueryParameters(
|
|
||||||
limit = call.request.queryParameters["limit"],
|
|
||||||
q = call.request.queryParameters["q"]
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!ApiValidationUtils.isValid(validationErrors)) {
|
|
||||||
call.respond(
|
|
||||||
HttpStatusCode.BadRequest,
|
HttpStatusCode.BadRequest,
|
||||||
ApiResponse.error<List<CountryDto>>(ApiValidationUtils.createErrorMessage(validationErrors))
|
ApiResponse.error<Unit>("INVALID_ID", "Missing or invalid ID")
|
||||||
)
|
)
|
||||||
return@get
|
|
||||||
|
val response = getCountryUseCase.getById(id)
|
||||||
|
response.country?.let {
|
||||||
|
call.respond(ApiResponse.success(it.toDto()))
|
||||||
|
} ?: call.respond(HttpStatusCode.NotFound, ApiResponse.error<Unit>("NOT_FOUND", "Country not found"))
|
||||||
}
|
}
|
||||||
|
|
||||||
val searchTerm = call.request.queryParameters["q"]
|
|
||||||
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<List<CountryDto>>("Search term 'q' is required"))
|
|
||||||
|
|
||||||
val limit = call.request.queryParameters["limit"]?.toIntOrNull() ?: 50
|
|
||||||
|
|
||||||
val countries = getCountryUseCase.searchByName(searchTerm, limit)
|
|
||||||
val countryDtos = countries.map { it.toDto() }
|
|
||||||
call.respond(HttpStatusCode.OK, ApiResponse.success(countryDtos))
|
|
||||||
} catch (e: IllegalArgumentException) {
|
|
||||||
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<List<CountryDto>>(e.message ?: "Invalid search parameters"))
|
|
||||||
} catch (e: Exception) {
|
|
||||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<List<CountryDto>>("Failed to search countries: ${e.message}"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GET /api/masterdata/countries/eu - Get EU member countries
|
|
||||||
get("/eu") {
|
|
||||||
try {
|
|
||||||
val countries = getCountryUseCase.getEuMembers()
|
|
||||||
val countryDtos = countries.map { it.toDto() }
|
|
||||||
call.respond(HttpStatusCode.OK, ApiResponse.success(countryDtos))
|
|
||||||
} catch (e: Exception) {
|
|
||||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<List<CountryDto>>("Failed to retrieve EU countries: ${e.message}"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GET /api/masterdata/countries/ewr - Get EWR member countries
|
|
||||||
get("/ewr") {
|
|
||||||
try {
|
|
||||||
val countries = getCountryUseCase.getEwrMembers()
|
|
||||||
val countryDtos = countries.map { it.toDto() }
|
|
||||||
call.respond(HttpStatusCode.OK, ApiResponse.success(countryDtos))
|
|
||||||
} catch (e: Exception) {
|
|
||||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<List<CountryDto>>("Failed to retrieve EWR countries: ${e.message}"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// POST /api/masterdata/countries - Create new country
|
|
||||||
post {
|
post {
|
||||||
try {
|
val dto = call.receive<CreateCountryDto>()
|
||||||
val createDto = call.receive<CreateCountryDto>()
|
|
||||||
|
|
||||||
// Validate input using shared validation utilities
|
|
||||||
val validationErrors = ApiValidationUtils.validateCountryRequest(
|
|
||||||
isoAlpha2Code = createDto.isoAlpha2Code,
|
|
||||||
isoAlpha3Code = createDto.isoAlpha3Code,
|
|
||||||
nameDeutsch = createDto.nameDeutsch,
|
|
||||||
nameEnglisch = createDto.nameEnglisch
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!ApiValidationUtils.isValid(validationErrors)) {
|
|
||||||
call.respond(
|
|
||||||
HttpStatusCode.BadRequest,
|
|
||||||
ApiResponse.error<CountryDto>(ApiValidationUtils.createErrorMessage(validationErrors))
|
|
||||||
)
|
|
||||||
return@post
|
|
||||||
}
|
|
||||||
|
|
||||||
val request = CreateCountryUseCase.CreateCountryRequest(
|
val request = CreateCountryUseCase.CreateCountryRequest(
|
||||||
isoAlpha2Code = createDto.isoAlpha2Code,
|
isoAlpha2Code = dto.isoAlpha2Code,
|
||||||
isoAlpha3Code = createDto.isoAlpha3Code,
|
isoAlpha3Code = dto.isoAlpha3Code,
|
||||||
isoNumerischerCode = createDto.isoNumerischerCode,
|
isoNumerischerCode = dto.isoNumerischerCode,
|
||||||
nameDeutsch = createDto.nameDeutsch,
|
nameDeutsch = dto.nameDeutsch,
|
||||||
nameEnglisch = createDto.nameEnglisch,
|
nameEnglisch = dto.nameEnglisch,
|
||||||
wappenUrl = createDto.wappenUrl,
|
wappenUrl = dto.wappenUrl,
|
||||||
istEuMitglied = createDto.istEuMitglied,
|
istEuMitglied = dto.istEuMitglied,
|
||||||
istEwrMitglied = createDto.istEwrMitglied,
|
istEwrMitglied = dto.istEwrMitglied,
|
||||||
istAktiv = createDto.istAktiv,
|
istAktiv = dto.istAktiv,
|
||||||
sortierReihenfolge = createDto.sortierReihenfolge
|
sortierReihenfolge = dto.sortierReihenfolge
|
||||||
)
|
)
|
||||||
|
|
||||||
val result = createCountryUseCase.createCountry(request)
|
val response = createCountryUseCase.createCountry(request)
|
||||||
if (result.success) {
|
val country = response.country
|
||||||
call.respond(HttpStatusCode.Created, ApiResponse.success(result.country!!.toDto()))
|
if (response.success && country != null) {
|
||||||
|
call.respond(HttpStatusCode.Created, ApiResponse.success(country.toDto()))
|
||||||
} else {
|
} else {
|
||||||
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<CountryDto>("Validation failed: ${result.errors.joinToString(", ")}"))
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<CountryDto>("Failed to create country: ${e.message}"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// PUT /api/masterdata/countries/{id} - Update existing country
|
|
||||||
put("/{id}") {
|
|
||||||
try {
|
|
||||||
val countryId = call.parameters["id"]?.let { uuidFrom(it) }
|
|
||||||
?: return@put call.respond(HttpStatusCode.BadRequest, ApiResponse.error<CountryDto>("Invalid country ID"))
|
|
||||||
|
|
||||||
val updateDto = call.receive<UpdateCountryDto>()
|
|
||||||
|
|
||||||
// Validate input using shared validation utilities
|
|
||||||
val validationErrors = ApiValidationUtils.validateCountryRequest(
|
|
||||||
isoAlpha2Code = updateDto.isoAlpha2Code,
|
|
||||||
isoAlpha3Code = updateDto.isoAlpha3Code,
|
|
||||||
nameDeutsch = updateDto.nameDeutsch,
|
|
||||||
nameEnglisch = updateDto.nameEnglisch
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!ApiValidationUtils.isValid(validationErrors)) {
|
|
||||||
call.respond(
|
call.respond(
|
||||||
HttpStatusCode.BadRequest,
|
HttpStatusCode.BadRequest,
|
||||||
ApiResponse.error<CountryDto>(ApiValidationUtils.createErrorMessage(validationErrors))
|
ApiResponse.error<Unit>("CREATION_FAILED", response.errors.joinToString())
|
||||||
)
|
|
||||||
return@put
|
|
||||||
}
|
|
||||||
|
|
||||||
val request = CreateCountryUseCase.UpdateCountryRequest(
|
|
||||||
landId = countryId,
|
|
||||||
isoAlpha2Code = updateDto.isoAlpha2Code,
|
|
||||||
isoAlpha3Code = updateDto.isoAlpha3Code,
|
|
||||||
isoNumerischerCode = updateDto.isoNumerischerCode,
|
|
||||||
nameDeutsch = updateDto.nameDeutsch,
|
|
||||||
nameEnglisch = updateDto.nameEnglisch,
|
|
||||||
wappenUrl = updateDto.wappenUrl,
|
|
||||||
istEuMitglied = updateDto.istEuMitglied,
|
|
||||||
istEwrMitglied = updateDto.istEwrMitglied,
|
|
||||||
istAktiv = updateDto.istAktiv,
|
|
||||||
sortierReihenfolge = updateDto.sortierReihenfolge
|
|
||||||
)
|
|
||||||
|
|
||||||
val result = createCountryUseCase.updateCountry(request)
|
|
||||||
if (result.success) {
|
|
||||||
call.respond(HttpStatusCode.OK, ApiResponse.success(result.country!!.toDto()))
|
|
||||||
} else {
|
|
||||||
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<CountryDto>("Validation failed: ${result.errors.joinToString(", ")}"))
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<CountryDto>("Failed to update country: ${e.message}"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DELETE /api/masterdata/countries/{id} - Delete country
|
|
||||||
delete("/{id}") {
|
|
||||||
try {
|
|
||||||
val countryId = call.parameters["id"]?.let { uuidFrom(it) }
|
|
||||||
?: return@delete call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Unit>("Invalid country ID"))
|
|
||||||
|
|
||||||
val result = createCountryUseCase.deleteCountry(countryId)
|
|
||||||
if (result.success) {
|
|
||||||
call.respond(HttpStatusCode.NoContent)
|
|
||||||
} else {
|
|
||||||
call.respond(HttpStatusCode.NotFound, ApiResponse.error<Unit>("Country not found: ${result.errors.joinToString(", ")}"))
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<Unit>("Failed to delete country: ${e.message}"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extension function to convert LandDefinition domain object to CountryDto.
|
|
||||||
*/
|
|
||||||
private fun LandDefinition.toDto(): CountryDto {
|
|
||||||
return CountryDto(
|
|
||||||
landId = this.landId.toString(),
|
|
||||||
isoAlpha2Code = this.isoAlpha2Code,
|
|
||||||
isoAlpha3Code = this.isoAlpha3Code,
|
|
||||||
isoNumerischerCode = this.isoNumerischerCode,
|
|
||||||
nameDeutsch = this.nameDeutsch,
|
|
||||||
nameEnglisch = this.nameEnglisch,
|
|
||||||
wappenUrl = this.wappenUrl,
|
|
||||||
istEuMitglied = this.istEuMitglied,
|
|
||||||
istEwrMitglied = this.istEwrMitglied,
|
|
||||||
istAktiv = this.istAktiv,
|
|
||||||
sortierReihenfolge = this.sortierReihenfolge,
|
|
||||||
createdAt = this.createdAt.toString(),
|
|
||||||
updatedAt = this.updatedAt.toString()
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun LandDefinition.toDto() = CountryDto(
|
||||||
|
landId = landId.toString(),
|
||||||
|
isoAlpha2Code = isoAlpha2Code,
|
||||||
|
isoAlpha3Code = isoAlpha3Code,
|
||||||
|
isoNumerischerCode = isoNumerischerCode,
|
||||||
|
nameDeutsch = nameDeutsch,
|
||||||
|
nameEnglisch = nameEnglisch,
|
||||||
|
wappenUrl = wappenUrl,
|
||||||
|
istEuMitglied = istEuMitglied,
|
||||||
|
istEwrMitglied = istEwrMitglied,
|
||||||
|
istAktiv = istAktiv,
|
||||||
|
sortierReihenfolge = sortierReihenfolge,
|
||||||
|
createdAt = createdAt.toString(),
|
||||||
|
updatedAt = updatedAt.toString()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
+65
-400
@@ -6,7 +6,6 @@ import at.mocode.core.domain.model.PlatzTypE
|
|||||||
import at.mocode.masterdata.application.usecase.CreatePlatzUseCase
|
import at.mocode.masterdata.application.usecase.CreatePlatzUseCase
|
||||||
import at.mocode.masterdata.application.usecase.GetPlatzUseCase
|
import at.mocode.masterdata.application.usecase.GetPlatzUseCase
|
||||||
import at.mocode.masterdata.domain.model.Platz
|
import at.mocode.masterdata.domain.model.Platz
|
||||||
import at.mocode.core.utils.validation.ApiValidationUtils
|
|
||||||
import kotlin.uuid.Uuid
|
import kotlin.uuid.Uuid
|
||||||
import io.ktor.http.*
|
import io.ktor.http.*
|
||||||
import io.ktor.server.request.*
|
import io.ktor.server.request.*
|
||||||
@@ -15,22 +14,16 @@ import io.ktor.server.routing.*
|
|||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* REST API controller for venue/arena management operations.
|
* REST API controller for venue/arena (Platz) management.
|
||||||
*
|
|
||||||
* This controller provides HTTP endpoints for the master-data context's
|
|
||||||
* venue functionality, following REST conventions and proper error handling.
|
|
||||||
*/
|
*/
|
||||||
class PlatzController(
|
class PlatzController(
|
||||||
private val getPlatzUseCase: GetPlatzUseCase,
|
private val getPlatzUseCase: GetPlatzUseCase,
|
||||||
private val createPlatzUseCase: CreatePlatzUseCase
|
private val createPlatzUseCase: CreatePlatzUseCase
|
||||||
) {
|
) {
|
||||||
|
|
||||||
/**
|
|
||||||
* DTO for venue API responses.
|
|
||||||
*/
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class PlatzDto(
|
data class PlatzDto(
|
||||||
val id: String,
|
val platzId: String,
|
||||||
val turnierId: String,
|
val turnierId: String,
|
||||||
val name: String,
|
val name: String,
|
||||||
val dimension: String? = null,
|
val dimension: String? = null,
|
||||||
@@ -42,9 +35,6 @@ class PlatzController(
|
|||||||
val updatedAt: String
|
val updatedAt: String
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
|
||||||
* DTO for creating a new venue.
|
|
||||||
*/
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class CreatePlatzDto(
|
data class CreatePlatzDto(
|
||||||
val turnierId: String,
|
val turnierId: String,
|
||||||
@@ -56,420 +46,95 @@ class PlatzController(
|
|||||||
val sortierReihenfolge: Int? = null
|
val sortierReihenfolge: Int? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
fun Route.registerRoutes() {
|
||||||
* DTO for updating an existing venue.
|
route("/plaetze") {
|
||||||
*/
|
get {
|
||||||
@Serializable
|
val turnierId = call.request.queryParameters["turnierId"]?.let {
|
||||||
data class UpdatePlatzDto(
|
try {
|
||||||
val turnierId: String,
|
Uuid.parse(it)
|
||||||
val name: String,
|
} catch (e: Exception) {
|
||||||
val dimension: String? = null,
|
null
|
||||||
val boden: String? = null,
|
}
|
||||||
val typ: String,
|
}
|
||||||
val istAktiv: Boolean = true,
|
?: return@get call.respond(
|
||||||
val sortierReihenfolge: Int? = null
|
HttpStatusCode.BadRequest,
|
||||||
|
ApiResponse.error<Unit>("MISSING_TURNIER_ID", "Query parameter turnierId is required")
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
val response = getPlatzUseCase.getByTournament(turnierId)
|
||||||
* Configures the routing for venue endpoints.
|
val dtos = response.plaetze.map { it.toDto() }
|
||||||
*/
|
call.respond(ApiResponse.success(dtos))
|
||||||
fun configureRouting(routing: Routing) {
|
}
|
||||||
routing.route("/api/masterdata/plaetze") {
|
|
||||||
|
|
||||||
// GET /api/masterdata/plaetze/{id} - Get venue by ID
|
|
||||||
get("/{id}") {
|
get("/{id}") {
|
||||||
|
val idStr = call.parameters["id"]
|
||||||
|
val id = idStr?.let {
|
||||||
try {
|
try {
|
||||||
val platzId = call.parameters["id"]?.let { Uuid.parse(it) }
|
Uuid.parse(it)
|
||||||
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<PlatzDto>("Invalid venue ID"))
|
|
||||||
|
|
||||||
val platz = getPlatzUseCase.getById(platzId)
|
|
||||||
if (platz != null) {
|
|
||||||
call.respond(HttpStatusCode.OK, ApiResponse.success(platz.toDto()))
|
|
||||||
} else {
|
|
||||||
call.respond(HttpStatusCode.NotFound, ApiResponse.error<PlatzDto>("Venue not found"))
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<PlatzDto>("Failed to retrieve venue: ${e.message}"))
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
?: return@get call.respond(
|
||||||
// GET /api/masterdata/plaetze/tournament/{turnierId} - Get venues by tournament
|
|
||||||
get("/tournament/{turnierId}") {
|
|
||||||
try {
|
|
||||||
val turnierId = call.parameters["turnierId"]?.let { Uuid.parse(it) }
|
|
||||||
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<List<PlatzDto>>("Invalid tournament ID"))
|
|
||||||
|
|
||||||
val activeOnlyParam = call.request.queryParameters["activeOnly"]
|
|
||||||
val activeOnly = activeOnlyParam?.toBoolean() ?: true
|
|
||||||
|
|
||||||
val orderBySortierungParam = call.request.queryParameters["orderBySortierung"]
|
|
||||||
val orderBySortierung = orderBySortierungParam?.toBoolean() ?: true
|
|
||||||
|
|
||||||
val plaetze = getPlatzUseCase.getByTournament(turnierId, activeOnly, orderBySortierung)
|
|
||||||
val platzDtos = plaetze.map { it.toDto() }
|
|
||||||
call.respond(HttpStatusCode.OK, ApiResponse.success(platzDtos))
|
|
||||||
} catch (e: Exception) {
|
|
||||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<List<PlatzDto>>("Failed to retrieve venues: ${e.message}"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GET /api/masterdata/plaetze/search - Search venues by name
|
|
||||||
get("/search") {
|
|
||||||
try {
|
|
||||||
val validationErrors = ApiValidationUtils.validateQueryParameters(
|
|
||||||
limit = call.request.queryParameters["limit"],
|
|
||||||
q = call.request.queryParameters["q"]
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!ApiValidationUtils.isValid(validationErrors)) {
|
|
||||||
call.respond(
|
|
||||||
HttpStatusCode.BadRequest,
|
HttpStatusCode.BadRequest,
|
||||||
ApiResponse.error<List<PlatzDto>>(ApiValidationUtils.createErrorMessage(validationErrors))
|
ApiResponse.error<Unit>("INVALID_ID", "Missing or invalid ID")
|
||||||
)
|
)
|
||||||
return@get
|
|
||||||
|
val response = getPlatzUseCase.getById(id)
|
||||||
|
response.platz?.let {
|
||||||
|
call.respond(ApiResponse.success(it.toDto()))
|
||||||
|
} ?: call.respond(HttpStatusCode.NotFound, ApiResponse.error<Unit>("NOT_FOUND", "Venue not found"))
|
||||||
}
|
}
|
||||||
|
|
||||||
val searchTerm = call.request.queryParameters["q"]
|
|
||||||
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<List<PlatzDto>>("Search term 'q' is required"))
|
|
||||||
|
|
||||||
val limit = call.request.queryParameters["limit"]?.toIntOrNull() ?: 50
|
|
||||||
val turnierIdParam = call.request.queryParameters["turnierId"]
|
|
||||||
val turnierId = turnierIdParam?.let { uuidFrom(it) }
|
|
||||||
|
|
||||||
val plaetze = getPlatzUseCase.searchByName(searchTerm, turnierId, limit)
|
|
||||||
val platzDtos = plaetze.map { it.toDto() }
|
|
||||||
call.respond(HttpStatusCode.OK, ApiResponse.success(platzDtos))
|
|
||||||
} catch (e: IllegalArgumentException) {
|
|
||||||
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<List<PlatzDto>>(e.message ?: "Invalid search parameters"))
|
|
||||||
} catch (e: Exception) {
|
|
||||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<List<PlatzDto>>("Failed to search venues: ${e.message}"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GET /api/masterdata/plaetze/type/{typ} - Get venues by type
|
|
||||||
get("/type/{typ}") {
|
|
||||||
try {
|
|
||||||
val typParam = call.parameters["typ"]
|
|
||||||
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<List<PlatzDto>>("Venue type is required"))
|
|
||||||
|
|
||||||
val typ = try {
|
|
||||||
PlatzTypE.valueOf(typParam.uppercase())
|
|
||||||
} catch (_: Exception) {
|
|
||||||
return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<List<PlatzDto>>("Invalid venue type: $typParam"))
|
|
||||||
}
|
|
||||||
|
|
||||||
val turnierIdParam = call.request.queryParameters["turnierId"]
|
|
||||||
val turnierId = turnierIdParam?.let { uuidFrom(it) }
|
|
||||||
|
|
||||||
val activeOnlyParam = call.request.queryParameters["activeOnly"]
|
|
||||||
val activeOnly = activeOnlyParam?.toBoolean() ?: true
|
|
||||||
|
|
||||||
val plaetze = getPlatzUseCase.getByType(typ, turnierId, activeOnly)
|
|
||||||
val platzDtos = plaetze.map { it.toDto() }
|
|
||||||
call.respond(HttpStatusCode.OK, ApiResponse.success(platzDtos))
|
|
||||||
} catch (e: Exception) {
|
|
||||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<List<PlatzDto>>("Failed to retrieve venues: ${e.message}"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GET /api/masterdata/plaetze/ground/{boden} - Get venues by ground type
|
|
||||||
get("/ground/{boden}") {
|
|
||||||
try {
|
|
||||||
val boden = call.parameters["boden"]
|
|
||||||
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<List<PlatzDto>>("Ground type is required"))
|
|
||||||
|
|
||||||
val turnierIdParam = call.request.queryParameters["turnierId"]
|
|
||||||
val turnierId = turnierIdParam?.let { uuidFrom(it) }
|
|
||||||
|
|
||||||
val activeOnlyParam = call.request.queryParameters["activeOnly"]
|
|
||||||
val activeOnly = activeOnlyParam?.toBoolean() ?: true
|
|
||||||
|
|
||||||
val plaetze = getPlatzUseCase.getByGroundType(boden, turnierId, activeOnly)
|
|
||||||
val platzDtos = plaetze.map { it.toDto() }
|
|
||||||
call.respond(HttpStatusCode.OK, ApiResponse.success(platzDtos))
|
|
||||||
} catch (e: IllegalArgumentException) {
|
|
||||||
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<List<PlatzDto>>(e.message ?: "Invalid ground type"))
|
|
||||||
} catch (e: Exception) {
|
|
||||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<List<PlatzDto>>("Failed to retrieve venues: ${e.message}"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GET /api/masterdata/plaetze/dimension/{dimension} - Get venues by dimensions
|
|
||||||
get("/dimension/{dimension}") {
|
|
||||||
try {
|
|
||||||
val dimension = call.parameters["dimension"]
|
|
||||||
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<List<PlatzDto>>("Dimension is required"))
|
|
||||||
|
|
||||||
val turnierIdParam = call.request.queryParameters["turnierId"]
|
|
||||||
val turnierId = turnierIdParam?.let { uuidFrom(it) }
|
|
||||||
|
|
||||||
val activeOnlyParam = call.request.queryParameters["activeOnly"]
|
|
||||||
val activeOnly = activeOnlyParam?.toBoolean() ?: true
|
|
||||||
|
|
||||||
val plaetze = getPlatzUseCase.getByDimensions(dimension, turnierId, activeOnly)
|
|
||||||
val platzDtos = plaetze.map { it.toDto() }
|
|
||||||
call.respond(HttpStatusCode.OK, ApiResponse.success(platzDtos))
|
|
||||||
} catch (e: IllegalArgumentException) {
|
|
||||||
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<List<PlatzDto>>(e.message ?: "Invalid dimension"))
|
|
||||||
} catch (e: Exception) {
|
|
||||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<List<PlatzDto>>("Failed to retrieve venues: ${e.message}"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GET /api/masterdata/plaetze/suitable - Get venues suitable for discipline
|
|
||||||
get("/suitable") {
|
|
||||||
try {
|
|
||||||
val typParam = call.request.queryParameters["typ"]
|
|
||||||
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<List<PlatzDto>>("Required venue type parameter is missing"))
|
|
||||||
|
|
||||||
val requiredType = try {
|
|
||||||
PlatzTypE.valueOf(typParam.uppercase())
|
|
||||||
} catch (_: Exception) {
|
|
||||||
return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<List<PlatzDto>>("Invalid venue type: $typParam"))
|
|
||||||
}
|
|
||||||
|
|
||||||
val requiredDimensions = call.request.queryParameters["dimension"]
|
|
||||||
val turnierIdParam = call.request.queryParameters["turnierId"]
|
|
||||||
val turnierId = turnierIdParam?.let { uuidFrom(it) }
|
|
||||||
|
|
||||||
val plaetze = getPlatzUseCase.getSuitableForDiscipline(requiredType, requiredDimensions, turnierId)
|
|
||||||
val platzDtos = plaetze.map { it.toDto() }
|
|
||||||
call.respond(HttpStatusCode.OK, ApiResponse.success(platzDtos))
|
|
||||||
} catch (e: Exception) {
|
|
||||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<List<PlatzDto>>("Failed to retrieve suitable venues: ${e.message}"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// POST /api/masterdata/plaetze - Create new venue
|
|
||||||
post {
|
post {
|
||||||
try {
|
val dto = call.receive<CreatePlatzDto>()
|
||||||
val createDto = call.receive<CreatePlatzDto>()
|
|
||||||
|
|
||||||
// Basic validation
|
|
||||||
if (createDto.name.isBlank()) {
|
|
||||||
call.respond(
|
|
||||||
HttpStatusCode.BadRequest,
|
|
||||||
ApiResponse.error<PlatzDto>("Name is required")
|
|
||||||
)
|
|
||||||
return@post
|
|
||||||
}
|
|
||||||
|
|
||||||
val turnierId = try {
|
val turnierId = try {
|
||||||
uuidFrom(createDto.turnierId)
|
Uuid.parse(dto.turnierId)
|
||||||
} catch (_: Exception) {
|
} catch (e: Exception) {
|
||||||
return@post call.respond(
|
return@post call.respond(
|
||||||
HttpStatusCode.BadRequest,
|
HttpStatusCode.BadRequest,
|
||||||
ApiResponse.error<PlatzDto>("Invalid tournament ID format")
|
ApiResponse.error<Unit>("INVALID_TURNIER_ID", "Invalid turnierId format")
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val typ = try {
|
|
||||||
PlatzTypE.valueOf(createDto.typ.uppercase())
|
|
||||||
} catch (_: Exception) {
|
|
||||||
return@post call.respond(
|
|
||||||
HttpStatusCode.BadRequest,
|
|
||||||
ApiResponse.error<PlatzDto>("Invalid venue type: ${createDto.typ}")
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val request = CreatePlatzUseCase.CreatePlatzRequest(
|
val request = CreatePlatzUseCase.CreatePlatzRequest(
|
||||||
turnierId = turnierId,
|
turnierId = turnierId,
|
||||||
name = createDto.name,
|
name = dto.name,
|
||||||
dimension = createDto.dimension,
|
dimension = dto.dimension,
|
||||||
boden = createDto.boden,
|
boden = dto.boden,
|
||||||
typ = typ,
|
typ = try {
|
||||||
istAktiv = createDto.istAktiv,
|
PlatzTypE.valueOf(dto.typ)
|
||||||
sortierReihenfolge = createDto.sortierReihenfolge
|
} catch (e: Exception) {
|
||||||
|
PlatzTypE.SONSTIGE
|
||||||
|
},
|
||||||
|
istAktiv = dto.istAktiv,
|
||||||
|
sortierReihenfolge = dto.sortierReihenfolge
|
||||||
)
|
)
|
||||||
|
|
||||||
val result = createPlatzUseCase.createPlatz(request)
|
val response = createPlatzUseCase.createPlatz(request)
|
||||||
if (result.success) {
|
val platz = response.platz
|
||||||
call.respond(HttpStatusCode.Created, ApiResponse.success(result.platz!!.toDto()))
|
if (response.success && platz != null) {
|
||||||
|
call.respond(HttpStatusCode.Created, ApiResponse.success(platz.toDto()))
|
||||||
} else {
|
} else {
|
||||||
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<PlatzDto>("Validation failed: ${result.errors.joinToString(", ")}"))
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<PlatzDto>("Failed to create venue: ${e.message}"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// PUT /api/masterdata/plaetze/{id} - Update existing venue
|
|
||||||
put("/{id}") {
|
|
||||||
try {
|
|
||||||
val platzId = call.parameters["id"]?.let { uuidFrom(it) }
|
|
||||||
?: return@put call.respond(HttpStatusCode.BadRequest, ApiResponse.error<PlatzDto>("Invalid venue ID"))
|
|
||||||
|
|
||||||
val updateDto = call.receive<UpdatePlatzDto>()
|
|
||||||
|
|
||||||
// Basic validation
|
|
||||||
if (updateDto.name.isBlank()) {
|
|
||||||
call.respond(
|
call.respond(
|
||||||
HttpStatusCode.BadRequest,
|
HttpStatusCode.BadRequest,
|
||||||
ApiResponse.error<PlatzDto>("Name is required")
|
ApiResponse.error<Unit>("CREATION_FAILED", response.errors.joinToString())
|
||||||
)
|
|
||||||
return@put
|
|
||||||
}
|
|
||||||
|
|
||||||
val turnierId = try {
|
|
||||||
uuidFrom(updateDto.turnierId)
|
|
||||||
} catch (_: Exception) {
|
|
||||||
return@put call.respond(
|
|
||||||
HttpStatusCode.BadRequest,
|
|
||||||
ApiResponse.error<PlatzDto>("Invalid tournament ID format")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val typ = try {
|
|
||||||
PlatzTypE.valueOf(updateDto.typ.uppercase())
|
|
||||||
} catch (_: Exception) {
|
|
||||||
return@put call.respond(
|
|
||||||
HttpStatusCode.BadRequest,
|
|
||||||
ApiResponse.error<PlatzDto>("Invalid venue type: ${updateDto.typ}")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val request = CreatePlatzUseCase.UpdatePlatzRequest(
|
|
||||||
platzId = platzId,
|
|
||||||
turnierId = turnierId,
|
|
||||||
name = updateDto.name,
|
|
||||||
dimension = updateDto.dimension,
|
|
||||||
boden = updateDto.boden,
|
|
||||||
typ = typ,
|
|
||||||
istAktiv = updateDto.istAktiv,
|
|
||||||
sortierReihenfolge = updateDto.sortierReihenfolge
|
|
||||||
)
|
|
||||||
|
|
||||||
val result = createPlatzUseCase.updatePlatz(request)
|
|
||||||
if (result.success) {
|
|
||||||
call.respond(HttpStatusCode.OK, ApiResponse.success(result.platz!!.toDto()))
|
|
||||||
} else {
|
|
||||||
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<PlatzDto>("Validation failed: ${result.errors.joinToString(", ")}"))
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<PlatzDto>("Failed to update venue: ${e.message}"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DELETE /api/masterdata/plaetze/{id} - Delete venue
|
|
||||||
delete("/{id}") {
|
|
||||||
try {
|
|
||||||
val platzId = call.parameters["id"]?.let { uuidFrom(it) }
|
|
||||||
?: return@delete call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Unit>("Invalid venue ID"))
|
|
||||||
|
|
||||||
val result = createPlatzUseCase.deletePlatz(platzId)
|
|
||||||
if (result.success) {
|
|
||||||
call.respond(HttpStatusCode.NoContent)
|
|
||||||
} else {
|
|
||||||
call.respond(HttpStatusCode.NotFound, ApiResponse.error<Unit>("Venue not found: ${result.errors.joinToString(", ")}"))
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<Unit>("Failed to delete venue: ${e.message}"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GET /api/masterdata/plaetze/count/tournament/{turnierId} - Count venues by tournament
|
|
||||||
get("/count/tournament/{turnierId}") {
|
|
||||||
try {
|
|
||||||
val turnierId = call.parameters["turnierId"]?.let { uuidFrom(it) }
|
|
||||||
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Long>("Invalid tournament ID"))
|
|
||||||
|
|
||||||
val count = getPlatzUseCase.countActiveByTournament(turnierId)
|
|
||||||
call.respond(HttpStatusCode.OK, ApiResponse.success(count))
|
|
||||||
} catch (e: Exception) {
|
|
||||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<Long>("Failed to count venues: ${e.message}"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GET /api/masterdata/plaetze/count/type/{typ}/tournament/{turnierId} - Count venues by type and tournament
|
|
||||||
get("/count/type/{typ}/tournament/{turnierId}") {
|
|
||||||
try {
|
|
||||||
val typParam = call.parameters["typ"]
|
|
||||||
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Long>("Venue type is required"))
|
|
||||||
|
|
||||||
val typ = try {
|
|
||||||
PlatzTypE.valueOf(typParam.uppercase())
|
|
||||||
} catch (_: Exception) {
|
|
||||||
return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Long>("Invalid venue type: $typParam"))
|
|
||||||
}
|
|
||||||
|
|
||||||
val turnierId = call.parameters["turnierId"]?.let { uuidFrom(it) }
|
|
||||||
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Long>("Invalid tournament ID"))
|
|
||||||
|
|
||||||
val activeOnlyParam = call.request.queryParameters["activeOnly"]
|
|
||||||
val activeOnly = activeOnlyParam?.toBoolean() ?: true
|
|
||||||
|
|
||||||
val count = getPlatzUseCase.countByTypeAndTournament(typ, turnierId, activeOnly)
|
|
||||||
call.respond(HttpStatusCode.OK, ApiResponse.success(count))
|
|
||||||
} catch (e: Exception) {
|
|
||||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<Long>("Failed to count venues: ${e.message}"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GET /api/masterdata/plaetze/grouped/tournament/{turnierId} - Get venues grouped by type
|
|
||||||
get("/grouped/tournament/{turnierId}") {
|
|
||||||
try {
|
|
||||||
val turnierId = call.parameters["turnierId"]?.let { uuidFrom(it) }
|
|
||||||
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Map<String, List<PlatzDto>>>("Invalid tournament ID"))
|
|
||||||
|
|
||||||
val activeOnlyParam = call.request.queryParameters["activeOnly"]
|
|
||||||
val activeOnly = activeOnlyParam?.toBoolean() ?: true
|
|
||||||
|
|
||||||
val groupedVenues = getPlatzUseCase.getGroupedByTypeForTournament(turnierId, activeOnly)
|
|
||||||
val groupedDtos = groupedVenues.mapKeys { it.key.name }.mapValues { entry ->
|
|
||||||
entry.value.map { it.toDto() }
|
|
||||||
}
|
|
||||||
call.respond(HttpStatusCode.OK, ApiResponse.success(groupedDtos))
|
|
||||||
} catch (e: Exception) {
|
|
||||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<Map<String, List<PlatzDto>>>("Failed to retrieve grouped venues: ${e.message}"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GET /api/masterdata/plaetze/validate/{id} - Validate venue suitability
|
|
||||||
get("/validate/{id}") {
|
|
||||||
try {
|
|
||||||
val platzId = call.parameters["id"]?.let { uuidFrom(it) }
|
|
||||||
?: return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Map<String, Any>>("Invalid venue ID"))
|
|
||||||
|
|
||||||
val requiredTypeParam = call.request.queryParameters["requiredType"]
|
|
||||||
val requiredType = requiredTypeParam?.let {
|
|
||||||
try {
|
|
||||||
PlatzTypE.valueOf(it.uppercase())
|
|
||||||
} catch (_: Exception) {
|
|
||||||
return@get call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Map<String, Any>>("Invalid required type: $it"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val requiredDimensions = call.request.queryParameters["requiredDimensions"]
|
|
||||||
val requiredGroundType = call.request.queryParameters["requiredGroundType"]
|
|
||||||
|
|
||||||
val (isValid, reasons) = getPlatzUseCase.validateVenueSuitability(platzId, requiredType, requiredDimensions, requiredGroundType)
|
|
||||||
val response = mapOf(
|
|
||||||
"isValid" to isValid,
|
|
||||||
"reasons" to reasons
|
|
||||||
)
|
|
||||||
call.respond(HttpStatusCode.OK, ApiResponse.success(response))
|
|
||||||
} catch (e: Exception) {
|
|
||||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<Map<String, Any>>("Failed to validate venue: ${e.message}"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extension function to convert Platz domain object to PlatzDto.
|
|
||||||
*/
|
|
||||||
private fun Platz.toDto(): PlatzDto {
|
|
||||||
return PlatzDto(
|
|
||||||
id = this.id.toString(),
|
|
||||||
turnierId = this.turnierId.toString(),
|
|
||||||
name = this.name,
|
|
||||||
dimension = this.dimension,
|
|
||||||
boden = this.boden,
|
|
||||||
typ = this.typ.name,
|
|
||||||
istAktiv = this.istAktiv,
|
|
||||||
sortierReihenfolge = this.sortierReihenfolge,
|
|
||||||
createdAt = this.createdAt.toString(),
|
|
||||||
updatedAt = this.updatedAt.toString()
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Platz.toDto() = PlatzDto(
|
||||||
|
platzId = id.toString(),
|
||||||
|
turnierId = turnierId.toString(),
|
||||||
|
name = name,
|
||||||
|
dimension = dimension,
|
||||||
|
boden = boden,
|
||||||
|
typ = typ.name,
|
||||||
|
istAktiv = istAktiv,
|
||||||
|
sortierReihenfolge = sortierReihenfolge,
|
||||||
|
createdAt = createdAt.toString(),
|
||||||
|
updatedAt = updatedAt.toString()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,8 +3,9 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(projects.masterdata.masterdataDomain)
|
implementation(projects.backend.services.masterdata.masterdataDomain)
|
||||||
implementation(projects.core.coreDomain)
|
implementation(projects.core.coreDomain)
|
||||||
implementation(projects.core.coreUtils)
|
implementation(projects.core.coreUtils)
|
||||||
|
implementation(libs.kotlinx.datetime)
|
||||||
testImplementation(projects.platform.platformTesting)
|
testImplementation(projects.platform.platformTesting)
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -7,7 +7,7 @@ import at.mocode.masterdata.domain.repository.AltersklasseRepository
|
|||||||
import at.mocode.core.domain.model.ValidationResult
|
import at.mocode.core.domain.model.ValidationResult
|
||||||
import at.mocode.core.domain.model.ValidationError
|
import at.mocode.core.domain.model.ValidationError
|
||||||
import kotlin.uuid.Uuid
|
import kotlin.uuid.Uuid
|
||||||
import kotlinx.datetime.Clock
|
import kotlin.time.Clock
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Use case for creating and updating age class information.
|
* Use case for creating and updating age class information.
|
||||||
@@ -209,7 +209,7 @@ class CreateAltersklasseUseCase(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validates a create age class request.
|
* Validates a creation age class request.
|
||||||
*/
|
*/
|
||||||
private fun validateCreateRequest(request: CreateAltersklasseRequest): ValidationResult {
|
private fun validateCreateRequest(request: CreateAltersklasseRequest): ValidationResult {
|
||||||
val errors = mutableListOf<ValidationError>()
|
val errors = mutableListOf<ValidationError>()
|
||||||
|
|||||||
+1
-1
@@ -6,7 +6,7 @@ import at.mocode.masterdata.domain.repository.BundeslandRepository
|
|||||||
import at.mocode.core.domain.model.ValidationResult
|
import at.mocode.core.domain.model.ValidationResult
|
||||||
import at.mocode.core.domain.model.ValidationError
|
import at.mocode.core.domain.model.ValidationError
|
||||||
import kotlin.uuid.Uuid
|
import kotlin.uuid.Uuid
|
||||||
import kotlinx.datetime.Clock
|
import kotlin.time.Clock
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Use case for creating and updating federal state information.
|
* Use case for creating and updating federal state information.
|
||||||
|
|||||||
+1
-1
@@ -6,7 +6,7 @@ import at.mocode.masterdata.domain.repository.LandRepository
|
|||||||
import at.mocode.core.domain.model.ValidationResult
|
import at.mocode.core.domain.model.ValidationResult
|
||||||
import at.mocode.core.domain.model.ValidationError
|
import at.mocode.core.domain.model.ValidationError
|
||||||
import kotlin.uuid.Uuid
|
import kotlin.uuid.Uuid
|
||||||
import kotlinx.datetime.Clock
|
import kotlin.time.Clock
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Use case for creating and updating country information.
|
* Use case for creating and updating country information.
|
||||||
|
|||||||
+1
-1
@@ -7,7 +7,7 @@ import at.mocode.masterdata.domain.repository.PlatzRepository
|
|||||||
import at.mocode.core.domain.model.ValidationResult
|
import at.mocode.core.domain.model.ValidationResult
|
||||||
import at.mocode.core.domain.model.ValidationError
|
import at.mocode.core.domain.model.ValidationError
|
||||||
import kotlin.uuid.Uuid
|
import kotlin.uuid.Uuid
|
||||||
import kotlinx.datetime.Clock
|
import kotlin.time.Clock
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Use case for creating and updating venue/arena information.
|
* Use case for creating and updating venue/arena information.
|
||||||
|
|||||||
+10
-4
@@ -22,10 +22,13 @@ class GetAltersklasseUseCase(
|
|||||||
* @param altersklasseId The unique identifier of the age class
|
* @param altersklasseId The unique identifier of the age class
|
||||||
* @return The age class if found, null otherwise
|
* @return The age class if found, null otherwise
|
||||||
*/
|
*/
|
||||||
suspend fun getById(altersklasseId: Uuid): AltersklasseDefinition? {
|
suspend fun getById(altersklasseId: Uuid): GetAltersklasseResponse {
|
||||||
return altersklasseRepository.findById(altersklasseId)
|
val altersklasse = altersklasseRepository.findById(altersklasseId)
|
||||||
|
return GetAltersklasseResponse(altersklasse = altersklasse)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class GetAltersklasseResponse(val altersklasse: AltersklasseDefinition?)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves an age class by its code.
|
* Retrieves an age class by its code.
|
||||||
*
|
*
|
||||||
@@ -57,13 +60,16 @@ class GetAltersklasseUseCase(
|
|||||||
* @param geschlechtFilter Optional filter by gender ('M', 'W')
|
* @param geschlechtFilter Optional filter by gender ('M', 'W')
|
||||||
* @return List of active age classes
|
* @return List of active age classes
|
||||||
*/
|
*/
|
||||||
suspend fun getAllActive(sparteFilter: SparteE? = null, geschlechtFilter: Char? = null): List<AltersklasseDefinition> {
|
suspend fun getAllActive(sparteFilter: SparteE? = null, geschlechtFilter: Char? = null): GetAltersklassenResponse {
|
||||||
geschlechtFilter?.let { gender ->
|
geschlechtFilter?.let { gender ->
|
||||||
require(gender == 'M' || gender == 'W') { "Gender filter must be 'M' or 'W'" }
|
require(gender == 'M' || gender == 'W') { "Gender filter must be 'M' or 'W'" }
|
||||||
}
|
}
|
||||||
return altersklasseRepository.findAllActive(sparteFilter, geschlechtFilter)
|
val altersklassen = altersklasseRepository.findAllActive(sparteFilter, geschlechtFilter)
|
||||||
|
return GetAltersklassenResponse(altersklassen = altersklassen)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class GetAltersklassenResponse(val altersklassen: List<AltersklasseDefinition>)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Finds age classes applicable for a specific age.
|
* Finds age classes applicable for a specific age.
|
||||||
*
|
*
|
||||||
|
|||||||
+17
-20
@@ -21,10 +21,13 @@ class GetBundeslandUseCase(
|
|||||||
* @param bundeslandId The unique identifier of the federal state
|
* @param bundeslandId The unique identifier of the federal state
|
||||||
* @return The federal state if found, null otherwise
|
* @return The federal state if found, null otherwise
|
||||||
*/
|
*/
|
||||||
suspend fun getById(bundeslandId: Uuid): BundeslandDefinition? {
|
suspend fun getById(bundeslandId: Uuid): GetBundeslandResponse {
|
||||||
return bundeslandRepository.findById(bundeslandId)
|
val bundesland = bundeslandRepository.findById(bundeslandId)
|
||||||
|
return GetBundeslandResponse(bundesland = bundesland)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class GetBundeslandResponse(val bundesland: BundeslandDefinition?)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves a federal state by its OEPS code for a specific country.
|
* Retrieves a federal state by its OEPS code for a specific country.
|
||||||
*
|
*
|
||||||
@@ -56,22 +59,13 @@ class GetBundeslandUseCase(
|
|||||||
* @param orderBySortierung Whether to order by sortierReihenfolge field (default: true)
|
* @param orderBySortierung Whether to order by sortierReihenfolge field (default: true)
|
||||||
* @return List of federal states for the country
|
* @return List of federal states for the country
|
||||||
*/
|
*/
|
||||||
suspend fun getByCountry(landId: Uuid, activeOnly: Boolean = true, orderBySortierung: Boolean = true): List<BundeslandDefinition> {
|
suspend fun getByCountry(
|
||||||
return bundeslandRepository.findByCountry(landId, activeOnly, orderBySortierung)
|
landId: Uuid,
|
||||||
}
|
activeOnly: Boolean = true,
|
||||||
|
orderBySortierung: Boolean = true
|
||||||
/**
|
): GetBundeslaenderResponse {
|
||||||
* Searches for federal states by name (partial match).
|
val bundeslaender = bundeslandRepository.findByCountry(landId, activeOnly, orderBySortierung)
|
||||||
*
|
return GetBundeslaenderResponse(bundeslaender = bundeslaender)
|
||||||
* @param searchTerm The search term to match against federal state names
|
|
||||||
* @param landId Optional country ID to limit search
|
|
||||||
* @param limit Maximum number of results to return (default: 50)
|
|
||||||
* @return List of matching federal states
|
|
||||||
*/
|
|
||||||
suspend fun searchByName(searchTerm: String, landId: Uuid? = null, limit: Int = 50): List<BundeslandDefinition> {
|
|
||||||
require(searchTerm.isNotBlank()) { "Search term cannot be blank" }
|
|
||||||
require(limit > 0) { "Limit must be positive" }
|
|
||||||
return bundeslandRepository.findByName(searchTerm.trim(), landId, limit)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -80,10 +74,13 @@ class GetBundeslandUseCase(
|
|||||||
* @param orderBySortierung Whether to order by sortierReihenfolge field (default: true)
|
* @param orderBySortierung Whether to order by sortierReihenfolge field (default: true)
|
||||||
* @return List of active federal states
|
* @return List of active federal states
|
||||||
*/
|
*/
|
||||||
suspend fun getAllActive(orderBySortierung: Boolean = true): List<BundeslandDefinition> {
|
suspend fun getAllActive(orderBySortierung: Boolean = true): GetBundeslaenderResponse {
|
||||||
return bundeslandRepository.findAllActive(orderBySortierung)
|
val bundeslaender = bundeslandRepository.findAllActive(orderBySortierung)
|
||||||
|
return GetBundeslaenderResponse(bundeslaender = bundeslaender)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class GetBundeslaenderResponse(val bundeslaender: List<BundeslandDefinition>)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if a federal state with the given OEPS code exists for a country.
|
* Checks if a federal state with the given OEPS code exists for a country.
|
||||||
*
|
*
|
||||||
|
|||||||
+10
-4
@@ -21,10 +21,13 @@ class GetCountryUseCase(
|
|||||||
* @param countryId The unique identifier of the country
|
* @param countryId The unique identifier of the country
|
||||||
* @return The country if found, null otherwise
|
* @return The country if found, null otherwise
|
||||||
*/
|
*/
|
||||||
suspend fun getById(countryId: Uuid): LandDefinition? {
|
suspend fun getById(countryId: Uuid): GetCountryResponse {
|
||||||
return landRepository.findById(countryId)
|
val country = landRepository.findById(countryId)
|
||||||
|
return GetCountryResponse(country = country)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class GetCountryResponse(val country: LandDefinition?)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves a country by its ISO Alpha-2 code.
|
* Retrieves a country by its ISO Alpha-2 code.
|
||||||
*
|
*
|
||||||
@@ -66,10 +69,13 @@ class GetCountryUseCase(
|
|||||||
* @param orderBySortierung Whether to order by sortierReihenfolge field (default: true)
|
* @param orderBySortierung Whether to order by sortierReihenfolge field (default: true)
|
||||||
* @return List of active countries
|
* @return List of active countries
|
||||||
*/
|
*/
|
||||||
suspend fun getAllActive(orderBySortierung: Boolean = true): List<LandDefinition> {
|
suspend fun getAllActive(orderBySortierung: Boolean = true): GetCountriesResponse {
|
||||||
return landRepository.findAllActive(orderBySortierung)
|
val countries = landRepository.findAllActive(orderBySortierung)
|
||||||
|
return GetCountriesResponse(countries = countries)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class GetCountriesResponse(val countries: List<LandDefinition>)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves all EU member countries.
|
* Retrieves all EU member countries.
|
||||||
*
|
*
|
||||||
|
|||||||
+14
-4
@@ -22,10 +22,13 @@ class GetPlatzUseCase(
|
|||||||
* @param platzId The unique identifier of the venue
|
* @param platzId The unique identifier of the venue
|
||||||
* @return The venue if found, null otherwise
|
* @return The venue if found, null otherwise
|
||||||
*/
|
*/
|
||||||
suspend fun getById(platzId: Uuid): Platz? {
|
suspend fun getById(platzId: Uuid): GetPlatzResponse {
|
||||||
return platzRepository.findById(platzId)
|
val platz = platzRepository.findById(platzId)
|
||||||
|
return GetPlatzResponse(platz = platz)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class GetPlatzResponse(val platz: Platz?)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves all venues for a specific tournament.
|
* Retrieves all venues for a specific tournament.
|
||||||
*
|
*
|
||||||
@@ -34,10 +37,17 @@ class GetPlatzUseCase(
|
|||||||
* @param orderBySortierung Whether to order by sortierReihenfolge field (default: true)
|
* @param orderBySortierung Whether to order by sortierReihenfolge field (default: true)
|
||||||
* @return List of venues for the tournament
|
* @return List of venues for the tournament
|
||||||
*/
|
*/
|
||||||
suspend fun getByTournament(turnierId: Uuid, activeOnly: Boolean = true, orderBySortierung: Boolean = true): List<Platz> {
|
suspend fun getByTournament(
|
||||||
return platzRepository.findByTournament(turnierId, activeOnly, orderBySortierung)
|
turnierId: Uuid,
|
||||||
|
activeOnly: Boolean = true,
|
||||||
|
orderBySortierung: Boolean = true
|
||||||
|
): GetPlaetzeResponse {
|
||||||
|
val plaetze = platzRepository.findByTournament(turnierId, activeOnly, orderBySortierung)
|
||||||
|
return GetPlaetzeResponse(plaetze = plaetze)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class GetPlaetzeResponse(val plaetze: List<Platz>)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Searches for venues by name (partial match).
|
* Searches for venues by name (partial match).
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.kotlin.multiplatform)
|
alias(libs.plugins.kotlinMultiplatform)
|
||||||
alias(libs.plugins.kotlin.serialization)
|
alias(libs.plugins.kotlinSerialization)
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
@@ -20,6 +20,10 @@ kotlin {
|
|||||||
val commonTest by getting {
|
val commonTest by getting {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(kotlin("test"))
|
implementation(kotlin("test"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val jvmTest by getting {
|
||||||
|
dependencies {
|
||||||
implementation(projects.platform.platformTesting)
|
implementation(projects.platform.platformTesting)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-9
@@ -1,13 +1,12 @@
|
|||||||
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
||||||
package at.mocode.masterdata.domain.model
|
package at.mocode.masterdata.domain.model
|
||||||
|
|
||||||
import at.mocode.core.domain.model.SparteE // Optional, falls Altersklassen stark spartenspezifisch sind
|
import at.mocode.core.domain.model.SparteE
|
||||||
import at.mocode.core.domain.serialization.KotlinInstantSerializer
|
import at.mocode.core.domain.serialization.InstantSerializer
|
||||||
import at.mocode.core.domain.serialization.UuidSerializer
|
import at.mocode.core.domain.serialization.UuidSerializer
|
||||||
import kotlin.uuid.Uuid
|
|
||||||
import kotlinx.datetime.Clock
|
|
||||||
import kotlinx.datetime.Instant
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlin.time.Instant
|
||||||
|
import kotlin.uuid.Uuid
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Definiert eine spezifische Altersklasse für Teilnehmer (Reiter, Fahrer, Voltigierer)
|
* Definiert eine spezifische Altersklasse für Teilnehmer (Reiter, Fahrer, Voltigierer)
|
||||||
@@ -52,8 +51,8 @@ data class AltersklasseDefinition(
|
|||||||
|
|
||||||
var istAktiv: Boolean = true,
|
var istAktiv: Boolean = true,
|
||||||
|
|
||||||
@Serializable(with = KotlinInstantSerializer::class)
|
@Serializable(with = InstantSerializer::class)
|
||||||
val createdAt: Instant = Clock.System.now(),
|
val createdAt: Instant,
|
||||||
@Serializable(with = KotlinInstantSerializer::class)
|
@Serializable(with = InstantSerializer::class)
|
||||||
var updatedAt: Instant = Clock.System.now()
|
var updatedAt: Instant
|
||||||
)
|
)
|
||||||
+7
-8
@@ -1,12 +1,11 @@
|
|||||||
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
||||||
package at.mocode.masterdata.domain.model
|
package at.mocode.masterdata.domain.model
|
||||||
|
|
||||||
import at.mocode.core.domain.serialization.KotlinInstantSerializer
|
import at.mocode.core.domain.serialization.InstantSerializer
|
||||||
import at.mocode.core.domain.serialization.UuidSerializer
|
import at.mocode.core.domain.serialization.UuidSerializer
|
||||||
import kotlin.uuid.Uuid
|
|
||||||
import kotlinx.datetime.Clock
|
|
||||||
import kotlinx.datetime.Instant
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlin.time.Instant
|
||||||
|
import kotlin.uuid.Uuid
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Definiert ein Bundesland oder eine vergleichbare subnationale Verwaltungseinheit.
|
* Definiert ein Bundesland oder eine vergleichbare subnationale Verwaltungseinheit.
|
||||||
@@ -44,8 +43,8 @@ data class BundeslandDefinition(
|
|||||||
var istAktiv: Boolean = true,
|
var istAktiv: Boolean = true,
|
||||||
var sortierReihenfolge: Int? = null,
|
var sortierReihenfolge: Int? = null,
|
||||||
|
|
||||||
@Serializable(with = KotlinInstantSerializer::class)
|
@Serializable(with = InstantSerializer::class)
|
||||||
val createdAt: Instant = Clock.System.now(),
|
val createdAt: Instant,
|
||||||
@Serializable(with = KotlinInstantSerializer::class)
|
@Serializable(with = InstantSerializer::class)
|
||||||
var updatedAt: Instant = Clock.System.now()
|
var updatedAt: Instant
|
||||||
)
|
)
|
||||||
+4
-4
@@ -1,12 +1,12 @@
|
|||||||
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
||||||
|
|
||||||
package at.mocode.officials.domain.model
|
package at.mocode.masterdata.domain.model
|
||||||
|
|
||||||
import at.mocode.core.domain.model.DatenQuelleE
|
import at.mocode.core.domain.model.DatenQuelleE
|
||||||
import at.mocode.core.domain.model.FunktionaerRolleE
|
import at.mocode.core.domain.model.FunktionaerRolleE
|
||||||
import at.mocode.core.domain.model.RichterQualifikationE
|
import at.mocode.core.domain.model.RichterQualifikationE
|
||||||
import at.mocode.core.domain.model.SparteE
|
import at.mocode.core.domain.model.SparteE
|
||||||
import at.mocode.core.domain.serialization.KotlinInstantSerializer
|
import at.mocode.core.domain.serialization.InstantSerializer
|
||||||
import at.mocode.core.domain.serialization.UuidSerializer
|
import at.mocode.core.domain.serialization.UuidSerializer
|
||||||
import kotlinx.datetime.LocalDate
|
import kotlinx.datetime.LocalDate
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
@@ -70,9 +70,9 @@ data class DomFunktionaer(
|
|||||||
var datenQuelle: DatenQuelleE = DatenQuelleE.IMPORT_ZNS,
|
var datenQuelle: DatenQuelleE = DatenQuelleE.IMPORT_ZNS,
|
||||||
|
|
||||||
// Audit
|
// Audit
|
||||||
@Serializable(with = KotlinInstantSerializer::class)
|
@Serializable(with = InstantSerializer::class)
|
||||||
val createdAt: Instant = Clock.System.now(),
|
val createdAt: Instant = Clock.System.now(),
|
||||||
@Serializable(with = KotlinInstantSerializer::class)
|
@Serializable(with = InstantSerializer::class)
|
||||||
var updatedAt: Instant = Clock.System.now()
|
var updatedAt: Instant = Clock.System.now()
|
||||||
) {
|
) {
|
||||||
/**
|
/**
|
||||||
+175
@@ -0,0 +1,175 @@
|
|||||||
|
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
||||||
|
|
||||||
|
package at.mocode.masterdata.domain.model
|
||||||
|
|
||||||
|
import at.mocode.core.domain.model.DatenQuelleE
|
||||||
|
import at.mocode.core.domain.model.PferdeGeschlechtE
|
||||||
|
import at.mocode.core.domain.serialization.InstantSerializer
|
||||||
|
import at.mocode.core.domain.serialization.UuidSerializer
|
||||||
|
import kotlinx.datetime.LocalDate
|
||||||
|
import kotlinx.datetime.number
|
||||||
|
import kotlinx.datetime.todayIn
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlin.time.Clock
|
||||||
|
import kotlin.time.Instant
|
||||||
|
import kotlin.uuid.Uuid
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Domain model representing a horse in the registry system.
|
||||||
|
*
|
||||||
|
* This entity contains all essential information about a horse including
|
||||||
|
* identification, ownership, breeding information, and administrative data.
|
||||||
|
* It serves as the core aggregate root for the horse-registry bounded context.
|
||||||
|
*
|
||||||
|
* @property pferdId Unique internal identifier for this horse (UUID).
|
||||||
|
* @property pferdeName Name of the horse.
|
||||||
|
* @property geschlecht Gender of the horse (Hengst, Stute, Wallach).
|
||||||
|
* @property geburtsdatum Birthdate of the horse.
|
||||||
|
* @property rasse Breed of the horse.
|
||||||
|
* @property farbe Color/coat of the horse.
|
||||||
|
* @property besitzerId ID of the current owner (Person from member-management context).
|
||||||
|
* @property verantwortlichePersonId ID of the responsible person (trainer, rider, etc.).
|
||||||
|
* @property zuechterName Name of the breeder.
|
||||||
|
* @property zuchtbuchNummer Studbook number if registered.
|
||||||
|
* @property lebensnummer Life number (unique identification number).
|
||||||
|
* @property chipNummer Microchip number for identification.
|
||||||
|
* @property passNummer Passport number.
|
||||||
|
* @property oepsNummer OEPS (Austrian Equestrian Federation) number.
|
||||||
|
* @property feiNummer FEI (International Equestrian Federation) number.
|
||||||
|
* @property vaterName Name of the sire (father).
|
||||||
|
* @property mutterName Name of the dam (mother).
|
||||||
|
* @property mutterVaterName Name of the maternal grandsire.
|
||||||
|
* @property stockmass Height of the horse in cm.
|
||||||
|
* @property istAktiv Whether the horse is currently active in the system.
|
||||||
|
* @property bemerkungen Additional notes or comments.
|
||||||
|
* @property datenQuelle Source of the data (manual entry, import, etc.).
|
||||||
|
* @property createdAt Timestamp when this record was created.
|
||||||
|
* @property updatedAt Timestamp when this record was last updated.
|
||||||
|
*/
|
||||||
|
@Serializable
|
||||||
|
data class DomPferd(
|
||||||
|
@Serializable(with = UuidSerializer::class)
|
||||||
|
val pferdId: Uuid = Uuid.random(),
|
||||||
|
|
||||||
|
// Basic Information
|
||||||
|
var pferdeName: String,
|
||||||
|
var geschlecht: PferdeGeschlechtE,
|
||||||
|
var geburtsdatum: LocalDate? = null,
|
||||||
|
var rasse: String? = null,
|
||||||
|
var farbe: String? = null,
|
||||||
|
|
||||||
|
// Ownership and Responsibility
|
||||||
|
@Serializable(with = UuidSerializer::class)
|
||||||
|
var besitzerId: Uuid? = null,
|
||||||
|
@Serializable(with = UuidSerializer::class)
|
||||||
|
var verantwortlichePersonId: Uuid? = null,
|
||||||
|
|
||||||
|
// Breeding Information
|
||||||
|
var zuechterName: String? = null,
|
||||||
|
var zuchtbuchNummer: String? = null,
|
||||||
|
|
||||||
|
// Identification Numbers
|
||||||
|
var lebensnummer: String? = null,
|
||||||
|
var chipNummer: String? = null,
|
||||||
|
var passNummer: String? = null,
|
||||||
|
var oepsNummer: String? = null,
|
||||||
|
var feiNummer: String? = null,
|
||||||
|
|
||||||
|
// Pedigree Information
|
||||||
|
var vaterName: String? = null,
|
||||||
|
var mutterName: String? = null,
|
||||||
|
var mutterVaterName: String? = null,
|
||||||
|
|
||||||
|
// Physical Characteristics
|
||||||
|
var stockmass: Int? = null, // Height in cm
|
||||||
|
|
||||||
|
// Status and Administrative
|
||||||
|
var istAktiv: Boolean = true,
|
||||||
|
var bemerkungen: String? = null,
|
||||||
|
var datenQuelle: DatenQuelleE = DatenQuelleE.MANUELL,
|
||||||
|
|
||||||
|
// Audit Fields
|
||||||
|
@Serializable(with = InstantSerializer::class)
|
||||||
|
val createdAt: Instant = Clock.System.now(),
|
||||||
|
@Serializable(with = InstantSerializer::class)
|
||||||
|
var updatedAt: Instant = Clock.System.now()
|
||||||
|
) {
|
||||||
|
/**
|
||||||
|
* Returns the display name for the horse, combining name and birth year if available.
|
||||||
|
*/
|
||||||
|
fun getDisplayName(): String {
|
||||||
|
return geburtsdatum?.let { birthDate ->
|
||||||
|
"$pferdeName (${birthDate.year})"
|
||||||
|
} ?: pferdeName
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the horse has complete identification information.
|
||||||
|
*/
|
||||||
|
fun hasCompleteIdentification(): Boolean {
|
||||||
|
return !lebensnummer.isNullOrBlank() ||
|
||||||
|
!chipNummer.isNullOrBlank() ||
|
||||||
|
!passNummer.isNullOrBlank()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the horse is registered with OEPS.
|
||||||
|
*/
|
||||||
|
fun isOepsRegistered(): Boolean {
|
||||||
|
return !oepsNummer.isNullOrBlank()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the horse is registered with FEI.
|
||||||
|
*/
|
||||||
|
fun isFeiRegistered(): Boolean {
|
||||||
|
return !feiNummer.isNullOrBlank()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the age of the horse in years, or null if birth date is unknown.
|
||||||
|
*/
|
||||||
|
fun getAge(): Int? {
|
||||||
|
return geburtsdatum?.let { birthDate ->
|
||||||
|
val today = Clock.System.todayIn(kotlinx.datetime.TimeZone.currentSystemDefault())
|
||||||
|
var age = today.year - birthDate.year
|
||||||
|
|
||||||
|
// Check if a birthday has occurred this year
|
||||||
|
if (today.month.number < birthDate.month.number ||
|
||||||
|
(today.month.number == birthDate.month.number && today.day < birthDate.day)
|
||||||
|
) {
|
||||||
|
age--
|
||||||
|
}
|
||||||
|
|
||||||
|
age
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates that required fields are present for horse registration.
|
||||||
|
*/
|
||||||
|
fun validateForRegistration(): List<String> {
|
||||||
|
val errors = mutableListOf<String>()
|
||||||
|
|
||||||
|
if (pferdeName.isBlank()) {
|
||||||
|
errors.add("Horse name is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasCompleteIdentification()) {
|
||||||
|
errors.add("At least one identification number (life number, chip number, or passport number) is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (besitzerId == null) {
|
||||||
|
errors.add("Owner is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a copy of this horse with an updated timestamp.
|
||||||
|
*/
|
||||||
|
fun withUpdatedTimestamp(): DomPferd {
|
||||||
|
return this.copy(updatedAt = Clock.System.now())
|
||||||
|
}
|
||||||
|
}
|
||||||
+6
-6
@@ -1,12 +1,12 @@
|
|||||||
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
||||||
|
|
||||||
package at.mocode.persons.domain.model
|
package at.mocode.masterdata.domain.model
|
||||||
|
|
||||||
import at.mocode.core.domain.model.DatenQuelleE
|
import at.mocode.core.domain.model.DatenQuelleE
|
||||||
import at.mocode.core.domain.model.LizenzKlasseE
|
import at.mocode.core.domain.model.LizenzKlasseE
|
||||||
import at.mocode.core.domain.model.SparteE
|
import at.mocode.core.domain.model.SparteE
|
||||||
import at.mocode.core.domain.serialization.KotlinInstantSerializer
|
import at.mocode.core.domain.serialization.InstantSerializer
|
||||||
import at.mocode.core.domain.serialization.KotlinLocalDateSerializer
|
import at.mocode.core.domain.serialization.LocalDateSerializer
|
||||||
import at.mocode.core.domain.serialization.UuidSerializer
|
import at.mocode.core.domain.serialization.UuidSerializer
|
||||||
import kotlinx.datetime.LocalDate
|
import kotlinx.datetime.LocalDate
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
@@ -74,7 +74,7 @@ data class DomReiter(
|
|||||||
// Personal Data (denormalized from DomPerson for performance)
|
// Personal Data (denormalized from DomPerson for performance)
|
||||||
val nachname: String,
|
val nachname: String,
|
||||||
val vorname: String,
|
val vorname: String,
|
||||||
@Serializable(with = KotlinLocalDateSerializer::class)
|
@Serializable(with = LocalDateSerializer::class)
|
||||||
val geburtsdatum: LocalDate? = null,
|
val geburtsdatum: LocalDate? = null,
|
||||||
|
|
||||||
// Club Affiliation
|
// Club Affiliation
|
||||||
@@ -87,9 +87,9 @@ data class DomReiter(
|
|||||||
val datenQuelle: DatenQuelleE = DatenQuelleE.IMPORT_ZNS,
|
val datenQuelle: DatenQuelleE = DatenQuelleE.IMPORT_ZNS,
|
||||||
|
|
||||||
// Audit Fields
|
// Audit Fields
|
||||||
@Serializable(with = KotlinInstantSerializer::class)
|
@Serializable(with = InstantSerializer::class)
|
||||||
val createdAt: Instant = Clock.System.now(),
|
val createdAt: Instant = Clock.System.now(),
|
||||||
@Serializable(with = KotlinInstantSerializer::class)
|
@Serializable(with = InstantSerializer::class)
|
||||||
var updatedAt: Instant = Clock.System.now()
|
var updatedAt: Instant = Clock.System.now()
|
||||||
) {
|
) {
|
||||||
/**
|
/**
|
||||||
+4
-4
@@ -1,9 +1,9 @@
|
|||||||
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
||||||
|
|
||||||
package at.mocode.clubs.domain.model
|
package at.mocode.masterdata.domain.model
|
||||||
|
|
||||||
import at.mocode.core.domain.model.DatenQuelleE
|
import at.mocode.core.domain.model.DatenQuelleE
|
||||||
import at.mocode.core.domain.serialization.KotlinInstantSerializer
|
import at.mocode.core.domain.serialization.InstantSerializer
|
||||||
import at.mocode.core.domain.serialization.UuidSerializer
|
import at.mocode.core.domain.serialization.UuidSerializer
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlin.time.Clock
|
import kotlin.time.Clock
|
||||||
@@ -71,9 +71,9 @@ data class DomVerein(
|
|||||||
var datenQuelle: DatenQuelleE = DatenQuelleE.IMPORT_ZNS,
|
var datenQuelle: DatenQuelleE = DatenQuelleE.IMPORT_ZNS,
|
||||||
|
|
||||||
// Audit
|
// Audit
|
||||||
@Serializable(with = KotlinInstantSerializer::class)
|
@Serializable(with = InstantSerializer::class)
|
||||||
val createdAt: Instant = Clock.System.now(),
|
val createdAt: Instant = Clock.System.now(),
|
||||||
@Serializable(with = KotlinInstantSerializer::class)
|
@Serializable(with = InstantSerializer::class)
|
||||||
var updatedAt: Instant = Clock.System.now()
|
var updatedAt: Instant = Clock.System.now()
|
||||||
) {
|
) {
|
||||||
/**
|
/**
|
||||||
+7
-8
@@ -1,12 +1,11 @@
|
|||||||
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
||||||
package at.mocode.masterdata.domain.model
|
package at.mocode.masterdata.domain.model
|
||||||
|
|
||||||
import at.mocode.core.domain.serialization.KotlinInstantSerializer
|
import at.mocode.core.domain.serialization.InstantSerializer
|
||||||
import at.mocode.core.domain.serialization.UuidSerializer
|
import at.mocode.core.domain.serialization.UuidSerializer
|
||||||
import kotlin.uuid.Uuid
|
|
||||||
import kotlinx.datetime.Clock
|
|
||||||
import kotlinx.datetime.Instant
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlin.time.Instant
|
||||||
|
import kotlin.uuid.Uuid
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Definiert ein Land/eine Nation mit seinen offiziellen Codes und Bezeichnungen.
|
* Definiert ein Land/eine Nation mit seinen offiziellen Codes und Bezeichnungen.
|
||||||
@@ -44,8 +43,8 @@ data class LandDefinition(
|
|||||||
var istAktiv: Boolean = true,
|
var istAktiv: Boolean = true,
|
||||||
var sortierReihenfolge: Int? = null,
|
var sortierReihenfolge: Int? = null,
|
||||||
|
|
||||||
@Serializable(with = KotlinInstantSerializer::class)
|
@Serializable(with = InstantSerializer::class)
|
||||||
val createdAt: Instant = Clock.System.now(),
|
val createdAt: Instant,
|
||||||
@Serializable(with = KotlinInstantSerializer::class)
|
@Serializable(with = InstantSerializer::class)
|
||||||
var updatedAt: Instant = Clock.System.now()
|
var updatedAt: Instant
|
||||||
)
|
)
|
||||||
+7
-8
@@ -2,12 +2,11 @@
|
|||||||
package at.mocode.masterdata.domain.model
|
package at.mocode.masterdata.domain.model
|
||||||
|
|
||||||
import at.mocode.core.domain.model.PlatzTypE
|
import at.mocode.core.domain.model.PlatzTypE
|
||||||
import at.mocode.core.domain.serialization.KotlinInstantSerializer
|
import at.mocode.core.domain.serialization.InstantSerializer
|
||||||
import at.mocode.core.domain.serialization.UuidSerializer
|
import at.mocode.core.domain.serialization.UuidSerializer
|
||||||
import kotlin.uuid.Uuid
|
|
||||||
import kotlinx.datetime.Clock
|
|
||||||
import kotlinx.datetime.Instant
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlin.time.Instant
|
||||||
|
import kotlin.uuid.Uuid
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Definiert einen Turnierplatz oder eine Wettkampfstätte.
|
* Definiert einen Turnierplatz oder eine Wettkampfstätte.
|
||||||
@@ -41,8 +40,8 @@ data class Platz(
|
|||||||
var istAktiv: Boolean = true,
|
var istAktiv: Boolean = true,
|
||||||
var sortierReihenfolge: Int? = null,
|
var sortierReihenfolge: Int? = null,
|
||||||
|
|
||||||
@Serializable(with = KotlinInstantSerializer::class)
|
@Serializable(with = InstantSerializer::class)
|
||||||
val createdAt: Instant = Clock.System.now(),
|
val createdAt: Instant,
|
||||||
@Serializable(with = KotlinInstantSerializer::class)
|
@Serializable(with = InstantSerializer::class)
|
||||||
var updatedAt: Instant = Clock.System.now()
|
var updatedAt: Instant
|
||||||
)
|
)
|
||||||
+144
@@ -0,0 +1,144 @@
|
|||||||
|
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
||||||
|
|
||||||
|
package at.mocode.masterdata.domain.repository
|
||||||
|
|
||||||
|
import at.mocode.core.domain.model.SparteE
|
||||||
|
import at.mocode.masterdata.domain.model.AltersklasseDefinition
|
||||||
|
import kotlin.uuid.Uuid
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Repository interface for AltersklasseDefinition (Age Class) domain operations.
|
||||||
|
*
|
||||||
|
* This interface defines the contract for age class data access operations
|
||||||
|
* without depending on specific implementation details (database, etc.).
|
||||||
|
* Following the hexagonal architecture pattern, this interface belongs
|
||||||
|
* to the domain layer and will be implemented in the infrastructure layer.
|
||||||
|
*/
|
||||||
|
interface AltersklasseRepository {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds an age class by its unique ID.
|
||||||
|
*
|
||||||
|
* @param id The unique identifier of the age class
|
||||||
|
* @return The age class if found, null otherwise
|
||||||
|
*/
|
||||||
|
suspend fun findById(id: Uuid): AltersklasseDefinition?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds an age class by its code.
|
||||||
|
*
|
||||||
|
* @param altersklasseCode The age class code (e.g., "JGD_U16", "JUN_U18")
|
||||||
|
* @return The age class if found, null otherwise
|
||||||
|
*/
|
||||||
|
suspend fun findByCode(altersklasseCode: String): AltersklasseDefinition?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds age classes by name (partial match).
|
||||||
|
*
|
||||||
|
* @param searchTerm The search term to match against age class names
|
||||||
|
* @param limit Maximum number of results to return
|
||||||
|
* @return List of matching age classes
|
||||||
|
*/
|
||||||
|
suspend fun findByName(searchTerm: String, limit: Int = 50): List<AltersklasseDefinition>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds all active age classes.
|
||||||
|
*
|
||||||
|
* @param sparteFilter Optional filter by sport type
|
||||||
|
* @param geschlechtFilter Optional filter by gender ('M', 'W')
|
||||||
|
* @return List of active age classes
|
||||||
|
*/
|
||||||
|
suspend fun findAllActive(sparteFilter: SparteE? = null, geschlechtFilter: Char? = null): List<AltersklasseDefinition>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds age classes applicable for a specific age.
|
||||||
|
*
|
||||||
|
* @param age The age to check
|
||||||
|
* @param sparteFilter Optional filter by sport type
|
||||||
|
* @param geschlechtFilter Optional filter by gender ('M', 'W')
|
||||||
|
* @return List of applicable age classes
|
||||||
|
*/
|
||||||
|
suspend fun findApplicableForAge(
|
||||||
|
age: Int,
|
||||||
|
sparteFilter: SparteE? = null,
|
||||||
|
geschlechtFilter: Char? = null
|
||||||
|
): List<AltersklasseDefinition>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds age classes by sport type.
|
||||||
|
*
|
||||||
|
* @param sparte The sport type
|
||||||
|
* @param activeOnly Whether to return only active age classes
|
||||||
|
* @return List of age classes for the sport type
|
||||||
|
*/
|
||||||
|
suspend fun findBySparte(sparte: SparteE, activeOnly: Boolean = true): List<AltersklasseDefinition>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds age classes by gender filter.
|
||||||
|
*
|
||||||
|
* @param geschlecht The gender ('M', 'W')
|
||||||
|
* @param activeOnly Whether to return only active age classes
|
||||||
|
* @return List of age classes for the gender
|
||||||
|
*/
|
||||||
|
suspend fun findByGeschlecht(geschlecht: Char, activeOnly: Boolean = true): List<AltersklasseDefinition>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds age classes by age range.
|
||||||
|
*
|
||||||
|
* @param minAge Minimum age (inclusive)
|
||||||
|
* @param maxAge Maximum age (inclusive)
|
||||||
|
* @param activeOnly Whether to return only active age classes
|
||||||
|
* @return List of age classes within the age range
|
||||||
|
*/
|
||||||
|
suspend fun findByAgeRange(minAge: Int?, maxAge: Int?, activeOnly: Boolean = true): List<AltersklasseDefinition>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds age classes by OETO rule reference.
|
||||||
|
*
|
||||||
|
* @param oetoRegelReferenzId The OETO rule reference ID
|
||||||
|
* @return List of age classes linked to the rule
|
||||||
|
*/
|
||||||
|
suspend fun findByOetoRegelReferenz(oetoRegelReferenzId: Uuid): List<AltersklasseDefinition>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves an age class (create or update).
|
||||||
|
*
|
||||||
|
* @param altersklasse The age class to save
|
||||||
|
* @return The saved age class with updated timestamps
|
||||||
|
*/
|
||||||
|
suspend fun save(altersklasse: AltersklasseDefinition): AltersklasseDefinition
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes an age class by ID.
|
||||||
|
*
|
||||||
|
* @param id The unique identifier of the age class to delete
|
||||||
|
* @return true if the age class was deleted, false if not found
|
||||||
|
*/
|
||||||
|
suspend fun delete(id: Uuid): Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if an age class with the given code exists.
|
||||||
|
*
|
||||||
|
* @param altersklasseCode The age class code to check
|
||||||
|
* @return true if an age class with this code exists, false otherwise
|
||||||
|
*/
|
||||||
|
suspend fun existsByCode(altersklasseCode: String): Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Counts the total number of active age classes.
|
||||||
|
*
|
||||||
|
* @param sparteFilter Optional filter by sport type
|
||||||
|
* @return The total count of active age classes
|
||||||
|
*/
|
||||||
|
suspend fun countActive(sparteFilter: SparteE? = null): Long
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates if a person with given age and gender can participate in an age class.
|
||||||
|
*
|
||||||
|
* @param altersklasseId The age class ID
|
||||||
|
* @param age The person's age
|
||||||
|
* @param geschlecht The person's gender ('M', 'W')
|
||||||
|
* @return true if the person can participate, false otherwise
|
||||||
|
*/
|
||||||
|
suspend fun isEligible(altersklasseId: Uuid, age: Int, geschlecht: Char): Boolean
|
||||||
|
}
|
||||||
+2
-2
@@ -1,11 +1,11 @@
|
|||||||
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
||||||
|
|
||||||
package at.mocode.officials.domain.repository
|
package at.mocode.masterdata.domain.repository
|
||||||
|
|
||||||
import at.mocode.core.domain.model.FunktionaerRolleE
|
import at.mocode.core.domain.model.FunktionaerRolleE
|
||||||
import at.mocode.core.domain.model.RichterQualifikationE
|
import at.mocode.core.domain.model.RichterQualifikationE
|
||||||
import at.mocode.core.domain.model.SparteE
|
import at.mocode.core.domain.model.SparteE
|
||||||
import at.mocode.officials.domain.model.DomFunktionaer
|
import at.mocode.masterdata.domain.model.DomFunktionaer
|
||||||
import kotlin.uuid.Uuid
|
import kotlin.uuid.Uuid
|
||||||
|
|
||||||
/**
|
/**
|
||||||
+248
@@ -0,0 +1,248 @@
|
|||||||
|
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
||||||
|
|
||||||
|
package at.mocode.masterdata.domain.repository
|
||||||
|
|
||||||
|
import at.mocode.masterdata.domain.model.DomPferd
|
||||||
|
import at.mocode.core.domain.model.PferdeGeschlechtE
|
||||||
|
import kotlin.uuid.Uuid
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Repository interface for DomPferd (Horse) domain operations.
|
||||||
|
*
|
||||||
|
* This interface defines the contract for horse data access operations
|
||||||
|
* without depending on specific implementation details (database, etc.).
|
||||||
|
* Following the hexagonal architecture pattern, this interface belongs
|
||||||
|
* to the domain layer and will be implemented in the infrastructure layer.
|
||||||
|
*/
|
||||||
|
interface HorseRepository {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds a horse by its unique ID.
|
||||||
|
*
|
||||||
|
* @param id The unique identifier of the horse
|
||||||
|
* @return The horse if found, null otherwise
|
||||||
|
*/
|
||||||
|
suspend fun findById(id: Uuid): DomPferd?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds a horse by its life number (Lebensnummer).
|
||||||
|
*
|
||||||
|
* @param lebensnummer The life number to search for
|
||||||
|
* @return The horse if found, null otherwise
|
||||||
|
*/
|
||||||
|
suspend fun findByLebensnummer(lebensnummer: String): DomPferd?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds a horse by its chip number.
|
||||||
|
*
|
||||||
|
* @param chipNummer The chip number to search for
|
||||||
|
* @return The horse if found, null otherwise
|
||||||
|
*/
|
||||||
|
suspend fun findByChipNummer(chipNummer: String): DomPferd?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds a horse by its passport number.
|
||||||
|
*
|
||||||
|
* @param passNummer The passport number to search for
|
||||||
|
* @return The horse if found, null otherwise
|
||||||
|
*/
|
||||||
|
suspend fun findByPassNummer(passNummer: String): DomPferd?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds a horse by its OEPS number.
|
||||||
|
*
|
||||||
|
* @param oepsNummer The OEPS number to search for
|
||||||
|
* @return The horse if found, null otherwise
|
||||||
|
*/
|
||||||
|
suspend fun findByOepsNummer(oepsNummer: String): DomPferd?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds a horse by its FEI number.
|
||||||
|
*
|
||||||
|
* @param feiNummer The FEI number to search for
|
||||||
|
* @return The horse if found, null otherwise
|
||||||
|
*/
|
||||||
|
suspend fun findByFeiNummer(feiNummer: String): DomPferd?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds horses by name (partial match).
|
||||||
|
*
|
||||||
|
* @param searchTerm The search term to match against horse names
|
||||||
|
* @param limit Maximum number of results to return
|
||||||
|
* @return List of matching horses
|
||||||
|
*/
|
||||||
|
suspend fun findByName(searchTerm: String, limit: Int = 50): List<DomPferd>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds all horses owned by a specific person.
|
||||||
|
*
|
||||||
|
* @param ownerId The ID of the owner (from member-management context)
|
||||||
|
* @param activeOnly Whether to return only active horses
|
||||||
|
* @return List of horses owned by the person
|
||||||
|
*/
|
||||||
|
suspend fun findByOwnerId(ownerId: Uuid, activeOnly: Boolean = true): List<DomPferd>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds all horses for which a person is responsible.
|
||||||
|
*
|
||||||
|
* @param responsiblePersonId The ID of the responsible person
|
||||||
|
* @param activeOnly Whether to return only active horses
|
||||||
|
* @return List of horses for which the person is responsible
|
||||||
|
*/
|
||||||
|
suspend fun findByResponsiblePersonId(responsiblePersonId: Uuid, activeOnly: Boolean = true): List<DomPferd>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds horses by gender.
|
||||||
|
*
|
||||||
|
* @param geschlecht The gender to filter by
|
||||||
|
* @param activeOnly Whether to return only active horses
|
||||||
|
* @param limit Maximum number of results to return
|
||||||
|
* @return List of horses with the specified gender
|
||||||
|
*/
|
||||||
|
suspend fun findByGeschlecht(
|
||||||
|
geschlecht: PferdeGeschlechtE,
|
||||||
|
activeOnly: Boolean = true,
|
||||||
|
limit: Int = 100
|
||||||
|
): List<DomPferd>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds horses by breed.
|
||||||
|
*
|
||||||
|
* @param rasse The breed to filter by
|
||||||
|
* @param activeOnly Whether to return only active horses
|
||||||
|
* @param limit Maximum number of results to return
|
||||||
|
* @return List of horses of the specified breed
|
||||||
|
*/
|
||||||
|
suspend fun findByRasse(rasse: String, activeOnly: Boolean = true, limit: Int = 100): List<DomPferd>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds horses by birth year.
|
||||||
|
*
|
||||||
|
* @param birthYear The birth year to filter by
|
||||||
|
* @param activeOnly Whether to return only active horses
|
||||||
|
* @return List of horses born in the specified year
|
||||||
|
*/
|
||||||
|
suspend fun findByBirthYear(birthYear: Int, activeOnly: Boolean = true): List<DomPferd>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds horses by birth year range.
|
||||||
|
*
|
||||||
|
* @param fromYear The start year (inclusive)
|
||||||
|
* @param toYear The end year (inclusive)
|
||||||
|
* @param activeOnly Whether to return only active horses
|
||||||
|
* @return List of horses born within the specified year range
|
||||||
|
*/
|
||||||
|
suspend fun findByBirthYearRange(fromYear: Int, toYear: Int, activeOnly: Boolean = true): List<DomPferd>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds all active horses.
|
||||||
|
*
|
||||||
|
* @param limit Maximum number of results to return
|
||||||
|
* @return List of active horses
|
||||||
|
*/
|
||||||
|
suspend fun findAllActive(limit: Int = 1000): List<DomPferd>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds horses with OEPS registration.
|
||||||
|
*
|
||||||
|
* @param activeOnly Whether to return only active horses
|
||||||
|
* @return List of OEPS registered horses
|
||||||
|
*/
|
||||||
|
suspend fun findOepsRegistered(activeOnly: Boolean = true): List<DomPferd>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds horses with FEI registration.
|
||||||
|
*
|
||||||
|
* @param activeOnly Whether to return only active horses
|
||||||
|
* @return List of FEI registered horses
|
||||||
|
*/
|
||||||
|
suspend fun findFeiRegistered(activeOnly: Boolean = true): List<DomPferd>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves a horse (create or update).
|
||||||
|
*
|
||||||
|
* @param horse The horse to save
|
||||||
|
* @return The saved horse with updated timestamps
|
||||||
|
*/
|
||||||
|
suspend fun save(horse: DomPferd): DomPferd
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes a horse by ID.
|
||||||
|
*
|
||||||
|
* @param id The unique identifier of the horse to delete
|
||||||
|
* @return true if the horse was deleted, false if not found
|
||||||
|
*/
|
||||||
|
suspend fun delete(id: Uuid): Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a horse with the given life number exists.
|
||||||
|
*
|
||||||
|
* @param lebensnummer The life number to check
|
||||||
|
* @return true if a horse with this life number exists, false otherwise
|
||||||
|
*/
|
||||||
|
suspend fun existsByLebensnummer(lebensnummer: String): Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a horse with the given chip number exists.
|
||||||
|
*
|
||||||
|
* @param chipNummer The chip number to check
|
||||||
|
* @return true if a horse with this chip number exists, false otherwise
|
||||||
|
*/
|
||||||
|
suspend fun existsByChipNummer(chipNummer: String): Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a horse with the given passport number exists.
|
||||||
|
*
|
||||||
|
* @param passNummer The passport number to check
|
||||||
|
* @return true if a horse with this passport number exists, false otherwise
|
||||||
|
*/
|
||||||
|
suspend fun existsByPassNummer(passNummer: String): Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a horse with the given OEPS number exists.
|
||||||
|
*
|
||||||
|
* @param oepsNummer The OEPS number to check
|
||||||
|
* @return true if a horse with this OEPS number exists, false otherwise
|
||||||
|
*/
|
||||||
|
suspend fun existsByOepsNummer(oepsNummer: String): Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a horse with the given FEI number exists.
|
||||||
|
*
|
||||||
|
* @param feiNummer The FEI number to check
|
||||||
|
* @return true if a horse with this FEI number exists, false otherwise
|
||||||
|
*/
|
||||||
|
suspend fun existsByFeiNummer(feiNummer: String): Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Counts the total number of active horses.
|
||||||
|
*
|
||||||
|
* @return The total count of active horses
|
||||||
|
*/
|
||||||
|
suspend fun countActive(): Long
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Counts horses by owner.
|
||||||
|
*
|
||||||
|
* @param ownerId The ID of the owner
|
||||||
|
* @param activeOnly Whether to count only active horses
|
||||||
|
* @return The count of horses owned by the person
|
||||||
|
*/
|
||||||
|
suspend fun countByOwnerId(ownerId: Uuid, activeOnly: Boolean = true): Long
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Counts horses with OEPS registration.
|
||||||
|
*
|
||||||
|
* @param activeOnly Whether to count only active horses
|
||||||
|
* @return The count of OEPS registered horses
|
||||||
|
*/
|
||||||
|
suspend fun countOepsRegistered(activeOnly: Boolean = true): Long
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Counts horses with FEI registration.
|
||||||
|
*
|
||||||
|
* @param activeOnly Whether to count only active horses
|
||||||
|
* @return The count of FEI registered horses
|
||||||
|
*/
|
||||||
|
suspend fun countFeiRegistered(activeOnly: Boolean = true): Long
|
||||||
|
}
|
||||||
+2
-2
@@ -1,10 +1,10 @@
|
|||||||
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
||||||
|
|
||||||
package at.mocode.persons.domain.repository
|
package at.mocode.masterdata.domain.repository
|
||||||
|
|
||||||
import at.mocode.core.domain.model.LizenzKlasseE
|
import at.mocode.core.domain.model.LizenzKlasseE
|
||||||
import at.mocode.core.domain.model.SparteE
|
import at.mocode.core.domain.model.SparteE
|
||||||
import at.mocode.persons.domain.model.DomReiter
|
import at.mocode.masterdata.domain.model.DomReiter
|
||||||
import kotlin.uuid.Uuid
|
import kotlin.uuid.Uuid
|
||||||
|
|
||||||
/**
|
/**
|
||||||
+3
-3
@@ -1,15 +1,15 @@
|
|||||||
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
||||||
|
|
||||||
package at.mocode.clubs.domain.repository
|
package at.mocode.masterdata.domain.repository
|
||||||
|
|
||||||
import at.mocode.clubs.domain.model.DomVerein
|
import at.mocode.masterdata.domain.model.DomVerein
|
||||||
import kotlin.uuid.Uuid
|
import kotlin.uuid.Uuid
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Repository-Interface für DomVerein (Verein) Domain-Operationen.
|
* Repository-Interface für DomVerein (Verein) Domain-Operationen.
|
||||||
*
|
*
|
||||||
* Definiert den Vertrag für Datenzugriffs-Operationen ohne Abhängigkeit
|
* Definiert den Vertrag für Datenzugriffs-Operationen ohne Abhängigkeit
|
||||||
* von konkreten Implementierungsdetails (Datenbank, etc.).
|
* von konkreten Implementierungsdetails (Datenbank etc.).
|
||||||
*/
|
*/
|
||||||
interface VereinRepository {
|
interface VereinRepository {
|
||||||
|
|
||||||
-139
@@ -1,139 +0,0 @@
|
|||||||
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
|
||||||
package at.mocode.masterdata.domain.repository
|
|
||||||
|
|
||||||
import at.mocode.core.domain.model.SparteE
|
|
||||||
import at.mocode.masterdata.domain.model.AltersklasseDefinition
|
|
||||||
import kotlin.uuid.Uuid
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Repository interface for AltersklasseDefinition (Age Class) domain operations.
|
|
||||||
*
|
|
||||||
* This interface defines the contract for age class data access operations
|
|
||||||
* without depending on specific implementation details (database, etc.).
|
|
||||||
* Following the hexagonal architecture pattern, this interface belongs
|
|
||||||
* to the domain layer and will be implemented in the infrastructure layer.
|
|
||||||
*/
|
|
||||||
interface AltersklasseRepository {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds an age class by its unique ID.
|
|
||||||
*
|
|
||||||
* @param id The unique identifier of the age class
|
|
||||||
* @return The age class if found, null otherwise
|
|
||||||
*/
|
|
||||||
suspend fun findById(id: Uuid): AltersklasseDefinition?
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds an age class by its code.
|
|
||||||
*
|
|
||||||
* @param altersklasseCode The age class code (e.g., "JGD_U16", "JUN_U18")
|
|
||||||
* @return The age class if found, null otherwise
|
|
||||||
*/
|
|
||||||
suspend fun findByCode(altersklasseCode: String): AltersklasseDefinition?
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds age classes by name (partial match).
|
|
||||||
*
|
|
||||||
* @param searchTerm The search term to match against age class names
|
|
||||||
* @param limit Maximum number of results to return
|
|
||||||
* @return List of matching age classes
|
|
||||||
*/
|
|
||||||
suspend fun findByName(searchTerm: String, limit: Int = 50): List<AltersklasseDefinition>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds all active age classes.
|
|
||||||
*
|
|
||||||
* @param sparteFilter Optional filter by sport type
|
|
||||||
* @param geschlechtFilter Optional filter by gender ('M', 'W')
|
|
||||||
* @return List of active age classes
|
|
||||||
*/
|
|
||||||
suspend fun findAllActive(sparteFilter: SparteE? = null, geschlechtFilter: Char? = null): List<AltersklasseDefinition>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds age classes applicable for a specific age.
|
|
||||||
*
|
|
||||||
* @param age The age to check
|
|
||||||
* @param sparteFilter Optional filter by sport type
|
|
||||||
* @param geschlechtFilter Optional filter by gender ('M', 'W')
|
|
||||||
* @return List of applicable age classes
|
|
||||||
*/
|
|
||||||
suspend fun findApplicableForAge(age: Int, sparteFilter: SparteE? = null, geschlechtFilter: Char? = null): List<AltersklasseDefinition>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds age classes by sport type.
|
|
||||||
*
|
|
||||||
* @param sparte The sport type
|
|
||||||
* @param activeOnly Whether to return only active age classes
|
|
||||||
* @return List of age classes for the sport type
|
|
||||||
*/
|
|
||||||
suspend fun findBySparte(sparte: SparteE, activeOnly: Boolean = true): List<AltersklasseDefinition>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds age classes by gender filter.
|
|
||||||
*
|
|
||||||
* @param geschlecht The gender ('M', 'W')
|
|
||||||
* @param activeOnly Whether to return only active age classes
|
|
||||||
* @return List of age classes for the gender
|
|
||||||
*/
|
|
||||||
suspend fun findByGeschlecht(geschlecht: Char, activeOnly: Boolean = true): List<AltersklasseDefinition>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds age classes by age range.
|
|
||||||
*
|
|
||||||
* @param minAge Minimum age (inclusive)
|
|
||||||
* @param maxAge Maximum age (inclusive)
|
|
||||||
* @param activeOnly Whether to return only active age classes
|
|
||||||
* @return List of age classes within the age range
|
|
||||||
*/
|
|
||||||
suspend fun findByAgeRange(minAge: Int?, maxAge: Int?, activeOnly: Boolean = true): List<AltersklasseDefinition>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds age classes by OETO rule reference.
|
|
||||||
*
|
|
||||||
* @param oetoRegelReferenzId The OETO rule reference ID
|
|
||||||
* @return List of age classes linked to the rule
|
|
||||||
*/
|
|
||||||
suspend fun findByOetoRegelReferenz(oetoRegelReferenzId: Uuid): List<AltersklasseDefinition>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Saves an age class (create or update).
|
|
||||||
*
|
|
||||||
* @param altersklasse The age class to save
|
|
||||||
* @return The saved age class with updated timestamps
|
|
||||||
*/
|
|
||||||
suspend fun save(altersklasse: AltersklasseDefinition): AltersklasseDefinition
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deletes an age class by ID.
|
|
||||||
*
|
|
||||||
* @param id The unique identifier of the age class to delete
|
|
||||||
* @return true if the age class was deleted, false if not found
|
|
||||||
*/
|
|
||||||
suspend fun delete(id: Uuid): Boolean
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if an age class with the given code exists.
|
|
||||||
*
|
|
||||||
* @param altersklasseCode The age class code to check
|
|
||||||
* @return true if an age class with this code exists, false otherwise
|
|
||||||
*/
|
|
||||||
suspend fun existsByCode(altersklasseCode: String): Boolean
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Counts the total number of active age classes.
|
|
||||||
*
|
|
||||||
* @param sparteFilter Optional filter by sport type
|
|
||||||
* @return The total count of active age classes
|
|
||||||
*/
|
|
||||||
suspend fun countActive(sparteFilter: SparteE? = null): Long
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates if a person with given age and gender can participate in an age class.
|
|
||||||
*
|
|
||||||
* @param altersklasseId The age class ID
|
|
||||||
* @param age The person's age
|
|
||||||
* @param geschlecht The person's gender ('M', 'W')
|
|
||||||
* @return true if the person can participate, false otherwise
|
|
||||||
*/
|
|
||||||
suspend fun isEligible(altersklasseId: Uuid, age: Int, geschlecht: Char): Boolean
|
|
||||||
}
|
|
||||||
@@ -1,27 +1,26 @@
|
|||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.kotlin.jvm)
|
kotlin("jvm")
|
||||||
alias(libs.plugins.kotlin.spring)
|
alias(libs.plugins.spring.boot) apply false
|
||||||
|
|
||||||
// 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)
|
alias(libs.plugins.spring.dependencyManagement)
|
||||||
|
alias(libs.plugins.kotlinSpring)
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(projects.platform.platformDependencies)
|
implementation(projects.platform.platformDependencies)
|
||||||
|
|
||||||
implementation(projects.masterdata.masterdataDomain)
|
implementation(projects.backend.services.masterdata.masterdataDomain)
|
||||||
implementation(projects.masterdata.masterdataApplication)
|
|
||||||
implementation(projects.core.coreDomain)
|
implementation(projects.core.coreDomain)
|
||||||
implementation(projects.core.coreUtils)
|
implementation(projects.core.coreUtils)
|
||||||
implementation(projects.infrastructure.cache.cacheApi)
|
implementation(projects.backend.infrastructure.cache.cacheApi)
|
||||||
implementation(projects.infrastructure.eventStore.eventStoreApi)
|
implementation(projects.backend.infrastructure.eventStore.eventStoreApi)
|
||||||
implementation(projects.infrastructure.messaging.messagingClient)
|
implementation(projects.backend.infrastructure.messaging.messagingClient)
|
||||||
|
|
||||||
|
// Exposed
|
||||||
|
implementation(libs.exposed.core)
|
||||||
|
implementation(libs.exposed.dao)
|
||||||
|
implementation(libs.exposed.jdbc)
|
||||||
|
implementation(libs.exposed.kotlin.datetime)
|
||||||
|
|
||||||
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
|
|
||||||
implementation("org.postgresql:postgresql")
|
implementation("org.postgresql:postgresql")
|
||||||
|
|
||||||
testImplementation(projects.platform.platformTesting)
|
testImplementation(projects.platform.platformTesting)
|
||||||
|
|||||||
+66
-86
@@ -1,29 +1,20 @@
|
|||||||
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
||||||
|
|
||||||
package at.mocode.masterdata.infrastructure.persistence
|
package at.mocode.masterdata.infrastructure.persistence
|
||||||
|
|
||||||
import at.mocode.core.domain.model.SparteE
|
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.model.AltersklasseDefinition
|
||||||
import at.mocode.masterdata.domain.repository.AltersklasseRepository
|
import at.mocode.masterdata.domain.repository.AltersklasseRepository
|
||||||
import at.mocode.core.utils.database.DatabaseFactory
|
import org.jetbrains.exposed.v1.core.*
|
||||||
|
import org.jetbrains.exposed.v1.jdbc.*
|
||||||
import kotlin.uuid.Uuid
|
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.
|
* 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 {
|
class AltersklasseRepositoryImpl : AltersklasseRepository {
|
||||||
|
|
||||||
/**
|
|
||||||
* Konvertiert eine Datenbankzeile in ein Domain-Objekt.
|
|
||||||
*/
|
|
||||||
private fun rowToAltersklasseDefinition(row: ResultRow): AltersklasseDefinition {
|
private fun rowToAltersklasseDefinition(row: ResultRow): AltersklasseDefinition {
|
||||||
return AltersklasseDefinition(
|
return AltersklasseDefinition(
|
||||||
altersklasseId = row[AltersklasseTable.id],
|
altersklasseId = row[AltersklasseTable.id],
|
||||||
@@ -36,8 +27,8 @@ class AltersklasseRepositoryImpl : AltersklasseRepository {
|
|||||||
geschlechtFilter = row[AltersklasseTable.geschlechtFilter],
|
geschlechtFilter = row[AltersklasseTable.geschlechtFilter],
|
||||||
oetoRegelReferenzId = row[AltersklasseTable.oetoRegelReferenzId],
|
oetoRegelReferenzId = row[AltersklasseTable.oetoRegelReferenzId],
|
||||||
istAktiv = row[AltersklasseTable.istAktiv],
|
istAktiv = row[AltersklasseTable.istAktiv],
|
||||||
createdAt = row[AltersklasseTable.createdAt].toInstant(TimeZone.UTC),
|
createdAt = row[AltersklasseTable.createdAt],
|
||||||
updatedAt = row[AltersklasseTable.updatedAt].toInstant(TimeZone.UTC)
|
updatedAt = row[AltersklasseTable.updatedAt]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,14 +44,16 @@ class AltersklasseRepositoryImpl : AltersklasseRepository {
|
|||||||
.singleOrNull()
|
.singleOrNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun findByName(searchTerm: String, limit: Int): List<AltersklasseDefinition> = DatabaseFactory.dbQuery {
|
override suspend fun findByName(searchTerm: String, limit: Int): List<AltersklasseDefinition> =
|
||||||
|
DatabaseFactory.dbQuery {
|
||||||
val pattern = "%$searchTerm%"
|
val pattern = "%$searchTerm%"
|
||||||
AltersklasseTable.selectAll().where { AltersklasseTable.bezeichnung like pattern }
|
AltersklasseTable.selectAll().where { AltersklasseTable.bezeichnung like pattern }
|
||||||
.limit(limit)
|
.limit(limit)
|
||||||
.map(::rowToAltersklasseDefinition)
|
.map(::rowToAltersklasseDefinition)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun findAllActive(sparteFilter: SparteE?, geschlechtFilter: Char?): List<AltersklasseDefinition> = DatabaseFactory.dbQuery {
|
override suspend fun findAllActive(sparteFilter: SparteE?, geschlechtFilter: Char?): List<AltersklasseDefinition> =
|
||||||
|
DatabaseFactory.dbQuery {
|
||||||
val query = AltersklasseTable.selectAll().where { AltersklasseTable.istAktiv eq true }
|
val query = AltersklasseTable.selectAll().where { AltersklasseTable.istAktiv eq true }
|
||||||
|
|
||||||
sparteFilter?.let { sparte ->
|
sparteFilter?.let { sparte ->
|
||||||
@@ -79,10 +72,13 @@ class AltersklasseRepositoryImpl : AltersklasseRepository {
|
|||||||
.map(::rowToAltersklasseDefinition)
|
.map(::rowToAltersklasseDefinition)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun findApplicableForAge(age: Int, sparteFilter: SparteE?, geschlechtFilter: Char?): List<AltersklasseDefinition> = DatabaseFactory.dbQuery {
|
override suspend fun findApplicableForAge(
|
||||||
|
age: Int,
|
||||||
|
sparteFilter: SparteE?,
|
||||||
|
geschlechtFilter: Char?
|
||||||
|
): List<AltersklasseDefinition> = DatabaseFactory.dbQuery {
|
||||||
val query = AltersklasseTable.selectAll().where { AltersklasseTable.istAktiv eq true }
|
val query = AltersklasseTable.selectAll().where { AltersklasseTable.istAktiv eq true }
|
||||||
|
|
||||||
// Age range filter
|
|
||||||
query.andWhere {
|
query.andWhere {
|
||||||
(AltersklasseTable.minAlter.isNull() or (AltersklasseTable.minAlter lessEq age)) and
|
(AltersklasseTable.minAlter.isNull() or (AltersklasseTable.minAlter lessEq age)) and
|
||||||
(AltersklasseTable.maxAlter.isNull() or (AltersklasseTable.maxAlter greaterEq age))
|
(AltersklasseTable.maxAlter.isNull() or (AltersklasseTable.maxAlter greaterEq age))
|
||||||
@@ -104,7 +100,8 @@ class AltersklasseRepositoryImpl : AltersklasseRepository {
|
|||||||
.map(::rowToAltersklasseDefinition)
|
.map(::rowToAltersklasseDefinition)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun findBySparte(sparte: SparteE, activeOnly: Boolean): List<AltersklasseDefinition> = DatabaseFactory.dbQuery {
|
override suspend fun findBySparte(sparte: SparteE, activeOnly: Boolean): List<AltersklasseDefinition> =
|
||||||
|
DatabaseFactory.dbQuery {
|
||||||
val query = AltersklasseTable.selectAll().where {
|
val query = AltersklasseTable.selectAll().where {
|
||||||
(AltersklasseTable.sparteFilter eq sparte.name) or (AltersklasseTable.sparteFilter.isNull())
|
(AltersklasseTable.sparteFilter eq sparte.name) or (AltersklasseTable.sparteFilter.isNull())
|
||||||
}
|
}
|
||||||
@@ -117,7 +114,8 @@ class AltersklasseRepositoryImpl : AltersklasseRepository {
|
|||||||
.map(::rowToAltersklasseDefinition)
|
.map(::rowToAltersklasseDefinition)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun findByGeschlecht(geschlecht: Char, activeOnly: Boolean): List<AltersklasseDefinition> = DatabaseFactory.dbQuery {
|
override suspend fun findByGeschlecht(geschlecht: Char, activeOnly: Boolean): List<AltersklasseDefinition> =
|
||||||
|
DatabaseFactory.dbQuery {
|
||||||
val query = AltersklasseTable.selectAll().where {
|
val query = AltersklasseTable.selectAll().where {
|
||||||
(AltersklasseTable.geschlechtFilter eq geschlecht) or (AltersklasseTable.geschlechtFilter.isNull())
|
(AltersklasseTable.geschlechtFilter eq geschlecht) or (AltersklasseTable.geschlechtFilter.isNull())
|
||||||
}
|
}
|
||||||
@@ -130,21 +128,16 @@ class AltersklasseRepositoryImpl : AltersklasseRepository {
|
|||||||
.map(::rowToAltersklasseDefinition)
|
.map(::rowToAltersklasseDefinition)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun findByAgeRange(minAge: Int?, maxAge: Int?, activeOnly: Boolean): List<AltersklasseDefinition> = DatabaseFactory.dbQuery {
|
override suspend fun findByAgeRange(minAge: Int?, maxAge: Int?, activeOnly: Boolean): List<AltersklasseDefinition> =
|
||||||
|
DatabaseFactory.dbQuery {
|
||||||
val query = AltersklasseTable.selectAll()
|
val query = AltersklasseTable.selectAll()
|
||||||
|
|
||||||
minAge?.let { min ->
|
if (minAge != null) {
|
||||||
query.andWhere {
|
query.andWhere { AltersklasseTable.minAlter greaterEq minAge }
|
||||||
(AltersklasseTable.maxAlter.isNull()) or (AltersklasseTable.maxAlter greaterEq min)
|
|
||||||
}
|
}
|
||||||
|
if (maxAge != null) {
|
||||||
|
query.andWhere { AltersklasseTable.maxAlter lessEq maxAge }
|
||||||
}
|
}
|
||||||
|
|
||||||
maxAge?.let { max ->
|
|
||||||
query.andWhere {
|
|
||||||
(AltersklasseTable.minAlter.isNull()) or (AltersklasseTable.minAlter lessEq max)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (activeOnly) {
|
if (activeOnly) {
|
||||||
query.andWhere { AltersklasseTable.istAktiv eq true }
|
query.andWhere { AltersklasseTable.istAktiv eq true }
|
||||||
}
|
}
|
||||||
@@ -153,49 +146,46 @@ class AltersklasseRepositoryImpl : AltersklasseRepository {
|
|||||||
.map(::rowToAltersklasseDefinition)
|
.map(::rowToAltersklasseDefinition)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun findByOetoRegelReferenz(oetoRegelReferenzId: Uuid): List<AltersklasseDefinition> = DatabaseFactory.dbQuery {
|
override suspend fun findByOetoRegelReferenz(oetoRegelReferenzId: Uuid): List<AltersklasseDefinition> =
|
||||||
|
DatabaseFactory.dbQuery {
|
||||||
AltersklasseTable.selectAll().where { AltersklasseTable.oetoRegelReferenzId eq oetoRegelReferenzId }
|
AltersklasseTable.selectAll().where { AltersklasseTable.oetoRegelReferenzId eq oetoRegelReferenzId }
|
||||||
.orderBy(AltersklasseTable.bezeichnung to SortOrder.ASC)
|
|
||||||
.map(::rowToAltersklasseDefinition)
|
.map(::rowToAltersklasseDefinition)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun save(altersklasse: AltersklasseDefinition): AltersklasseDefinition = DatabaseFactory.dbQuery {
|
override suspend fun save(altersklasse: AltersklasseDefinition): AltersklasseDefinition = DatabaseFactory.dbQuery {
|
||||||
val now = Clock.System.now()
|
val exists = AltersklasseTable.selectAll().where { AltersklasseTable.id eq altersklasse.altersklasseId }.any()
|
||||||
val existingAltersklasse = AltersklasseTable.selectAll().where { AltersklasseTable.id eq altersklasse.altersklasseId }.singleOrNull()
|
|
||||||
|
|
||||||
if (existingAltersklasse == null) {
|
if (exists) {
|
||||||
// Insert a new age class
|
AltersklasseTable.update({ AltersklasseTable.id eq altersklasse.altersklasseId }) {
|
||||||
AltersklasseTable.insert { stmt ->
|
it[altersklasseCode] = altersklasse.altersklasseCode
|
||||||
stmt[id] = altersklasse.altersklasseId
|
it[bezeichnung] = altersklasse.bezeichnung
|
||||||
stmt[altersklasseCode] = altersklasse.altersklasseCode
|
it[minAlter] = altersklasse.minAlter
|
||||||
stmt[bezeichnung] = altersklasse.bezeichnung
|
it[maxAlter] = altersklasse.maxAlter
|
||||||
stmt[minAlter] = altersklasse.minAlter
|
it[stichtagRegelText] = altersklasse.stichtagRegelText
|
||||||
stmt[maxAlter] = altersklasse.maxAlter
|
it[sparteFilter] = altersklasse.sparteFilter?.name
|
||||||
stmt[stichtagRegelText] = altersklasse.stichtagRegelText
|
it[geschlechtFilter] = altersklasse.geschlechtFilter
|
||||||
stmt[sparteFilter] = altersklasse.sparteFilter?.name
|
it[oetoRegelReferenzId] = altersklasse.oetoRegelReferenzId
|
||||||
stmt[geschlechtFilter] = altersklasse.geschlechtFilter
|
it[istAktiv] = altersklasse.istAktiv
|
||||||
stmt[oetoRegelReferenzId] = altersklasse.oetoRegelReferenzId
|
it[updatedAt] = altersklasse.updatedAt
|
||||||
stmt[istAktiv] = altersklasse.istAktiv
|
|
||||||
stmt[createdAt] = altersklasse.createdAt.toLocalDateTime(TimeZone.UTC)
|
|
||||||
stmt[updatedAt] = now.toLocalDateTime(TimeZone.UTC)
|
|
||||||
}
|
}
|
||||||
|
altersklasse
|
||||||
} else {
|
} else {
|
||||||
// Update existing age class
|
AltersklasseTable.insert {
|
||||||
AltersklasseTable.update({ AltersklasseTable.id eq altersklasse.altersklasseId }) { stmt ->
|
it[id] = altersklasse.altersklasseId
|
||||||
stmt[altersklasseCode] = altersklasse.altersklasseCode
|
it[altersklasseCode] = altersklasse.altersklasseCode
|
||||||
stmt[bezeichnung] = altersklasse.bezeichnung
|
it[bezeichnung] = altersklasse.bezeichnung
|
||||||
stmt[minAlter] = altersklasse.minAlter
|
it[minAlter] = altersklasse.minAlter
|
||||||
stmt[maxAlter] = altersklasse.maxAlter
|
it[maxAlter] = altersklasse.maxAlter
|
||||||
stmt[stichtagRegelText] = altersklasse.stichtagRegelText
|
it[stichtagRegelText] = altersklasse.stichtagRegelText
|
||||||
stmt[sparteFilter] = altersklasse.sparteFilter?.name
|
it[sparteFilter] = altersklasse.sparteFilter?.name
|
||||||
stmt[geschlechtFilter] = altersklasse.geschlechtFilter
|
it[geschlechtFilter] = altersklasse.geschlechtFilter
|
||||||
stmt[oetoRegelReferenzId] = altersklasse.oetoRegelReferenzId
|
it[oetoRegelReferenzId] = altersklasse.oetoRegelReferenzId
|
||||||
stmt[istAktiv] = altersklasse.istAktiv
|
it[istAktiv] = altersklasse.istAktiv
|
||||||
stmt[updatedAt] = now.toLocalDateTime(TimeZone.UTC)
|
it[createdAt] = altersklasse.createdAt
|
||||||
|
it[updatedAt] = altersklasse.updatedAt
|
||||||
}
|
}
|
||||||
|
altersklasse
|
||||||
}
|
}
|
||||||
|
|
||||||
altersklasse.copy(updatedAt = now)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun delete(id: Uuid): Boolean = DatabaseFactory.dbQuery {
|
override suspend fun delete(id: Uuid): Boolean = DatabaseFactory.dbQuery {
|
||||||
@@ -203,38 +193,28 @@ class AltersklasseRepositoryImpl : AltersklasseRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun existsByCode(altersklasseCode: String): Boolean = DatabaseFactory.dbQuery {
|
override suspend fun existsByCode(altersklasseCode: String): Boolean = DatabaseFactory.dbQuery {
|
||||||
AltersklasseTable.selectAll().where { AltersklasseTable.altersklasseCode eq altersklasseCode }
|
AltersklasseTable.selectAll().where { AltersklasseTable.altersklasseCode eq altersklasseCode }.any()
|
||||||
.count() > 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun countActive(sparteFilter: SparteE?): Long = DatabaseFactory.dbQuery {
|
override suspend fun countActive(sparteFilter: SparteE?): Long = DatabaseFactory.dbQuery {
|
||||||
val query = AltersklasseTable.selectAll().where { AltersklasseTable.istAktiv eq true }
|
val query = AltersklasseTable.selectAll().where { AltersklasseTable.istAktiv eq true }
|
||||||
|
|
||||||
sparteFilter?.let { sparte ->
|
sparteFilter?.let { sparte ->
|
||||||
query.andWhere {
|
query.andWhere { (AltersklasseTable.sparteFilter eq sparte.name) or (AltersklasseTable.sparteFilter.isNull()) }
|
||||||
(AltersklasseTable.sparteFilter eq sparte.name) or (AltersklasseTable.sparteFilter.isNull())
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
query.count()
|
query.count()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun isEligible(altersklasseId: Uuid, age: Int, geschlecht: Char): Boolean = DatabaseFactory.dbQuery {
|
override suspend fun isEligible(altersklasseId: Uuid, age: Int, geschlecht: Char): Boolean = DatabaseFactory.dbQuery {
|
||||||
val altersklasse = AltersklasseTable.selectAll().where {
|
AltersklasseTable.selectAll().where { AltersklasseTable.id eq altersklasseId }
|
||||||
(AltersklasseTable.id eq altersklasseId) and (AltersklasseTable.istAktiv eq true)
|
.map {
|
||||||
}.singleOrNull()
|
val min = it[AltersklasseTable.minAlter]
|
||||||
|
val max = it[AltersklasseTable.maxAlter]
|
||||||
|
val g = it[AltersklasseTable.geschlechtFilter]
|
||||||
|
|
||||||
if (altersklasse == null) return@dbQuery false
|
val ageOk = (min == null || age >= min) && (max == null || age <= max)
|
||||||
|
val geschlechtOk = (g == null || g == geschlecht)
|
||||||
|
|
||||||
// Check age eligibility
|
ageOk && geschlechtOk
|
||||||
val minAlter = altersklasse[AltersklasseTable.minAlter]
|
}.singleOrNull() ?: false
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-8
@@ -1,18 +1,19 @@
|
|||||||
|
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
||||||
|
|
||||||
package at.mocode.masterdata.infrastructure.persistence
|
package at.mocode.masterdata.infrastructure.persistence
|
||||||
|
|
||||||
import org.jetbrains.exposed.v1.core.Table
|
import org.jetbrains.exposed.v1.core.Table
|
||||||
import org.jetbrains.exposed.v1.core.kotlin.datetime.datetime
|
import org.jetbrains.exposed.v1.datetime.CurrentTimestamp
|
||||||
import org.jetbrains.exposed.v1.core.kotlin.datetime.CurrentDateTime
|
import org.jetbrains.exposed.v1.datetime.timestamp
|
||||||
import org.jetbrains.exposed.v1.core.javaUUID
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Exposed-Tabellendefinition für die Altersklasse-Entität (Altersklassendefinitionen).
|
* Exposed-Tabellendefinition für die Altersklasse-Entität (Altersklassendefinition).
|
||||||
*
|
*
|
||||||
* Diese Tabelle speichert alle Informationen zu Altersklassen für Teilnehmer
|
* Diese Tabelle speichert alle Informationen zu Altersklassen für Teilnehmer
|
||||||
* entsprechend der AltersklasseDefinition Domain-Entität.
|
* entsprechend der AltersklasseDefinition Domain-Entität.
|
||||||
*/
|
*/
|
||||||
object AltersklasseTable : Table("altersklasse") {
|
object AltersklasseTable : Table("altersklasse") {
|
||||||
val id = javaUUID("id").autoGenerate()
|
val id = uuid("id")
|
||||||
val altersklasseCode = varchar("altersklasse_code", 50).uniqueIndex()
|
val altersklasseCode = varchar("altersklasse_code", 50).uniqueIndex()
|
||||||
val bezeichnung = varchar("bezeichnung", 200)
|
val bezeichnung = varchar("bezeichnung", 200)
|
||||||
val minAlter = integer("min_alter").nullable()
|
val minAlter = integer("min_alter").nullable()
|
||||||
@@ -20,10 +21,10 @@ object AltersklasseTable : Table("altersklasse") {
|
|||||||
val stichtagRegelText = varchar("stichtag_regel_text", 500).nullable()
|
val stichtagRegelText = varchar("stichtag_regel_text", 500).nullable()
|
||||||
val sparteFilter = varchar("sparte_filter", 50).nullable() // Enum as string
|
val sparteFilter = varchar("sparte_filter", 50).nullable() // Enum as string
|
||||||
val geschlechtFilter = char("geschlecht_filter").nullable()
|
val geschlechtFilter = char("geschlecht_filter").nullable()
|
||||||
val oetoRegelReferenzId = javaUUID("oeto_regel_referenz_id").nullable()
|
val oetoRegelReferenzId = uuid("oeto_regel_referenz_id").nullable()
|
||||||
val istAktiv = bool("ist_aktiv").default(true)
|
val istAktiv = bool("ist_aktiv").default(true)
|
||||||
val createdAt = datetime("created_at").defaultExpression(CurrentDateTime)
|
val createdAt = timestamp("created_at").defaultExpression(CurrentTimestamp)
|
||||||
val updatedAt = datetime("updated_at").defaultExpression(CurrentDateTime)
|
val updatedAt = timestamp("updated_at").defaultExpression(CurrentTimestamp)
|
||||||
|
|
||||||
override val primaryKey = PrimaryKey(id)
|
override val primaryKey = PrimaryKey(id)
|
||||||
|
|
||||||
|
|||||||
+46
-68
@@ -1,28 +1,19 @@
|
|||||||
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
||||||
|
|
||||||
package at.mocode.masterdata.infrastructure.persistence
|
package at.mocode.masterdata.infrastructure.persistence
|
||||||
|
|
||||||
|
import at.mocode.core.utils.database.DatabaseFactory
|
||||||
import at.mocode.masterdata.domain.model.BundeslandDefinition
|
import at.mocode.masterdata.domain.model.BundeslandDefinition
|
||||||
import at.mocode.masterdata.domain.repository.BundeslandRepository
|
import at.mocode.masterdata.domain.repository.BundeslandRepository
|
||||||
import at.mocode.core.utils.database.DatabaseFactory
|
import org.jetbrains.exposed.v1.core.*
|
||||||
|
import org.jetbrains.exposed.v1.jdbc.*
|
||||||
import kotlin.uuid.Uuid
|
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.
|
* 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 {
|
class BundeslandRepositoryImpl : BundeslandRepository {
|
||||||
|
|
||||||
/**
|
|
||||||
* Konvertiert eine Datenbankzeile in ein Domain-Objekt.
|
|
||||||
*/
|
|
||||||
private fun rowToBundeslandDefinition(row: ResultRow): BundeslandDefinition {
|
private fun rowToBundeslandDefinition(row: ResultRow): BundeslandDefinition {
|
||||||
return BundeslandDefinition(
|
return BundeslandDefinition(
|
||||||
bundeslandId = row[BundeslandTable.id],
|
bundeslandId = row[BundeslandTable.id],
|
||||||
@@ -34,8 +25,8 @@ class BundeslandRepositoryImpl : BundeslandRepository {
|
|||||||
wappenUrl = row[BundeslandTable.wappenUrl],
|
wappenUrl = row[BundeslandTable.wappenUrl],
|
||||||
istAktiv = row[BundeslandTable.istAktiv],
|
istAktiv = row[BundeslandTable.istAktiv],
|
||||||
sortierReihenfolge = row[BundeslandTable.sortierReihenfolge],
|
sortierReihenfolge = row[BundeslandTable.sortierReihenfolge],
|
||||||
createdAt = row[BundeslandTable.createdAt].toInstant(TimeZone.UTC),
|
createdAt = row[BundeslandTable.createdAt],
|
||||||
updatedAt = row[BundeslandTable.updatedAt].toInstant(TimeZone.UTC)
|
updatedAt = row[BundeslandTable.updatedAt]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,9 +37,7 @@ class BundeslandRepositoryImpl : BundeslandRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun findByOepsCode(oepsCode: String, landId: Uuid): BundeslandDefinition? = DatabaseFactory.dbQuery {
|
override suspend fun findByOepsCode(oepsCode: String, landId: Uuid): BundeslandDefinition? = DatabaseFactory.dbQuery {
|
||||||
BundeslandTable.selectAll().where {
|
BundeslandTable.selectAll().where { (BundeslandTable.oepsCode eq oepsCode) and (BundeslandTable.landId eq landId) }
|
||||||
(BundeslandTable.oepsCode eq oepsCode) and (BundeslandTable.landId eq landId)
|
|
||||||
}
|
|
||||||
.map(::rowToBundeslandDefinition)
|
.map(::rowToBundeslandDefinition)
|
||||||
.singleOrNull()
|
.singleOrNull()
|
||||||
}
|
}
|
||||||
@@ -59,80 +48,72 @@ class BundeslandRepositoryImpl : BundeslandRepository {
|
|||||||
.singleOrNull()
|
.singleOrNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun findByCountry(landId: Uuid, activeOnly: Boolean, orderBySortierung: Boolean): List<BundeslandDefinition> = DatabaseFactory.dbQuery {
|
override suspend fun findByCountry(
|
||||||
|
landId: Uuid,
|
||||||
|
activeOnly: Boolean,
|
||||||
|
orderBySortierung: Boolean
|
||||||
|
): List<BundeslandDefinition> = DatabaseFactory.dbQuery {
|
||||||
val query = BundeslandTable.selectAll().where { BundeslandTable.landId eq landId }
|
val query = BundeslandTable.selectAll().where { BundeslandTable.landId eq landId }
|
||||||
|
|
||||||
if (activeOnly) {
|
if (activeOnly) {
|
||||||
query.andWhere { BundeslandTable.istAktiv eq true }
|
query.andWhere { BundeslandTable.istAktiv eq true }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (orderBySortierung) {
|
if (orderBySortierung) {
|
||||||
query.orderBy(BundeslandTable.sortierReihenfolge to SortOrder.ASC, BundeslandTable.name to SortOrder.ASC)
|
query.orderBy(BundeslandTable.sortierReihenfolge to SortOrder.ASC, BundeslandTable.name to SortOrder.ASC)
|
||||||
} else {
|
} else {
|
||||||
query.orderBy(BundeslandTable.name to SortOrder.ASC)
|
query.orderBy(BundeslandTable.name to SortOrder.ASC)
|
||||||
}
|
}
|
||||||
|
|
||||||
query.map(::rowToBundeslandDefinition)
|
query.map(::rowToBundeslandDefinition)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun findByName(searchTerm: String, landId: Uuid?, limit: Int): List<BundeslandDefinition> = DatabaseFactory.dbQuery {
|
override suspend fun findByName(searchTerm: String, landId: Uuid?, limit: Int): List<BundeslandDefinition> =
|
||||||
|
DatabaseFactory.dbQuery {
|
||||||
val pattern = "%$searchTerm%"
|
val pattern = "%$searchTerm%"
|
||||||
val query = BundeslandTable.selectAll().where { BundeslandTable.name like pattern }
|
val query = BundeslandTable.selectAll().where { BundeslandTable.name like pattern }
|
||||||
|
landId?.let { query.andWhere { BundeslandTable.landId eq it } }
|
||||||
landId?.let {
|
|
||||||
query.andWhere { BundeslandTable.landId eq it }
|
|
||||||
}
|
|
||||||
|
|
||||||
query.limit(limit).map(::rowToBundeslandDefinition)
|
query.limit(limit).map(::rowToBundeslandDefinition)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun findAllActive(orderBySortierung: Boolean): List<BundeslandDefinition> = DatabaseFactory.dbQuery {
|
override suspend fun findAllActive(orderBySortierung: Boolean): List<BundeslandDefinition> = DatabaseFactory.dbQuery {
|
||||||
val query = BundeslandTable.selectAll().where { BundeslandTable.istAktiv eq true }
|
val query = BundeslandTable.selectAll().where { BundeslandTable.istAktiv eq true }
|
||||||
|
|
||||||
if (orderBySortierung) {
|
if (orderBySortierung) {
|
||||||
query.orderBy(BundeslandTable.sortierReihenfolge to SortOrder.ASC, BundeslandTable.name to SortOrder.ASC)
|
query.orderBy(BundeslandTable.sortierReihenfolge to SortOrder.ASC, BundeslandTable.name to SortOrder.ASC)
|
||||||
} else {
|
} else {
|
||||||
query.orderBy(BundeslandTable.name to SortOrder.ASC)
|
query.orderBy(BundeslandTable.name to SortOrder.ASC)
|
||||||
}
|
}
|
||||||
|
|
||||||
query.map(::rowToBundeslandDefinition)
|
query.map(::rowToBundeslandDefinition)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun save(bundesland: BundeslandDefinition): BundeslandDefinition = DatabaseFactory.dbQuery {
|
override suspend fun save(bundesland: BundeslandDefinition): BundeslandDefinition = DatabaseFactory.dbQuery {
|
||||||
val now = Clock.System.now()
|
val exists = BundeslandTable.selectAll().where { BundeslandTable.id eq bundesland.bundeslandId }.any()
|
||||||
val existingBundesland = BundeslandTable.selectAll().where { BundeslandTable.id eq bundesland.bundeslandId }.singleOrNull()
|
if (exists) {
|
||||||
|
BundeslandTable.update({ BundeslandTable.id eq bundesland.bundeslandId }) {
|
||||||
if (existingBundesland == null) {
|
it[landId] = bundesland.landId
|
||||||
// Insert a new federal state
|
it[oepsCode] = bundesland.oepsCode
|
||||||
BundeslandTable.insert { stmt ->
|
it[iso3166_2_Code] = bundesland.iso3166_2_Code
|
||||||
stmt[id] = bundesland.bundeslandId
|
it[name] = bundesland.name
|
||||||
stmt[landId] = bundesland.landId
|
it[kuerzel] = bundesland.kuerzel
|
||||||
stmt[oepsCode] = bundesland.oepsCode
|
it[wappenUrl] = bundesland.wappenUrl
|
||||||
stmt[iso3166_2_Code] = bundesland.iso3166_2_Code
|
it[istAktiv] = bundesland.istAktiv
|
||||||
stmt[name] = bundesland.name
|
it[sortierReihenfolge] = bundesland.sortierReihenfolge
|
||||||
stmt[kuerzel] = bundesland.kuerzel
|
it[updatedAt] = bundesland.updatedAt
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
bundesland
|
||||||
} else {
|
} else {
|
||||||
// Update existing federal state
|
BundeslandTable.insert {
|
||||||
BundeslandTable.update({ BundeslandTable.id eq bundesland.bundeslandId }) { stmt ->
|
it[id] = bundesland.bundeslandId
|
||||||
stmt[landId] = bundesland.landId
|
it[landId] = bundesland.landId
|
||||||
stmt[oepsCode] = bundesland.oepsCode
|
it[oepsCode] = bundesland.oepsCode
|
||||||
stmt[iso3166_2_Code] = bundesland.iso3166_2_Code
|
it[iso3166_2_Code] = bundesland.iso3166_2_Code
|
||||||
stmt[name] = bundesland.name
|
it[name] = bundesland.name
|
||||||
stmt[kuerzel] = bundesland.kuerzel
|
it[kuerzel] = bundesland.kuerzel
|
||||||
stmt[wappenUrl] = bundesland.wappenUrl
|
it[wappenUrl] = bundesland.wappenUrl
|
||||||
stmt[istAktiv] = bundesland.istAktiv
|
it[istAktiv] = bundesland.istAktiv
|
||||||
stmt[sortierReihenfolge] = bundesland.sortierReihenfolge
|
it[sortierReihenfolge] = bundesland.sortierReihenfolge
|
||||||
stmt[updatedAt] = now.toLocalDateTime(TimeZone.UTC)
|
it[createdAt] = bundesland.createdAt
|
||||||
|
it[updatedAt] = bundesland.updatedAt
|
||||||
}
|
}
|
||||||
|
bundesland
|
||||||
}
|
}
|
||||||
|
|
||||||
bundesland.copy(updatedAt = now)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun delete(id: Uuid): Boolean = DatabaseFactory.dbQuery {
|
override suspend fun delete(id: Uuid): Boolean = DatabaseFactory.dbQuery {
|
||||||
@@ -140,19 +121,16 @@ class BundeslandRepositoryImpl : BundeslandRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun existsByOepsCode(oepsCode: String, landId: Uuid): Boolean = DatabaseFactory.dbQuery {
|
override suspend fun existsByOepsCode(oepsCode: String, landId: Uuid): Boolean = DatabaseFactory.dbQuery {
|
||||||
BundeslandTable.selectAll().where {
|
BundeslandTable.selectAll().where { (BundeslandTable.oepsCode eq oepsCode) and (BundeslandTable.landId eq landId) }
|
||||||
(BundeslandTable.oepsCode eq oepsCode) and (BundeslandTable.landId eq landId)
|
.any()
|
||||||
}.count() > 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun existsByIso3166_2_Code(iso3166_2_Code: String): Boolean = DatabaseFactory.dbQuery {
|
override suspend fun existsByIso3166_2_Code(iso3166_2_Code: String): Boolean = DatabaseFactory.dbQuery {
|
||||||
BundeslandTable.selectAll().where { BundeslandTable.iso3166_2_Code eq iso3166_2_Code }
|
BundeslandTable.selectAll().where { BundeslandTable.iso3166_2_Code eq iso3166_2_Code }.any()
|
||||||
.count() > 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun countActiveByCountry(landId: Uuid): Long = DatabaseFactory.dbQuery {
|
override suspend fun countActiveByCountry(landId: Uuid): Long = DatabaseFactory.dbQuery {
|
||||||
BundeslandTable.selectAll().where {
|
BundeslandTable.selectAll().where { (BundeslandTable.landId eq landId) and (BundeslandTable.istAktiv eq true) }
|
||||||
(BundeslandTable.landId eq landId) and (BundeslandTable.istAktiv eq true)
|
.count()
|
||||||
}.count()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-16
@@ -1,35 +1,31 @@
|
|||||||
|
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
||||||
|
|
||||||
package at.mocode.masterdata.infrastructure.persistence
|
package at.mocode.masterdata.infrastructure.persistence
|
||||||
|
|
||||||
import org.jetbrains.exposed.v1.core.Table
|
import org.jetbrains.exposed.v1.core.Table
|
||||||
import org.jetbrains.exposed.v1.core.kotlin.datetime.datetime
|
import org.jetbrains.exposed.v1.datetime.CurrentTimestamp
|
||||||
import org.jetbrains.exposed.v1.core.kotlin.datetime.CurrentDateTime
|
import org.jetbrains.exposed.v1.datetime.timestamp
|
||||||
import org.jetbrains.exposed.v1.core.javaUUID
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Exposed-Tabellendefinition für die Bundesland-Entität (Bundesländer/Regionen).
|
* Exposed-Tabellendefinition für die Bundesland-Entität.
|
||||||
*
|
|
||||||
* Diese Tabelle speichert alle Informationen zu Bundesländern und subnationalen
|
|
||||||
* Verwaltungseinheiten entsprechend der BundeslandDefinition Domain-Entität.
|
|
||||||
*/
|
*/
|
||||||
object BundeslandTable : Table("bundesland") {
|
object BundeslandTable : Table("bundesland") {
|
||||||
val id = javaUUID("id").autoGenerate()
|
val id = uuid("bundesland_id")
|
||||||
val landId = javaUUID("land_id").references(LandTable.id)
|
val landId = uuid("land_id")
|
||||||
val oepsCode = varchar("oeps_code", 10).nullable()
|
val oepsCode = varchar("oeps_code", 10).nullable()
|
||||||
val iso3166_2_Code = varchar("iso_3166_2_code", 10).nullable()
|
val iso3166_2_Code = varchar("iso_3166_2_code", 10).nullable()
|
||||||
val name = varchar("name", 100)
|
val name = varchar("name", 100)
|
||||||
val kuerzel = varchar("kuerzel", 10).nullable()
|
val kuerzel = varchar("kuerzel", 10).nullable()
|
||||||
val wappenUrl = varchar("wappen_url", 500).nullable()
|
val wappenUrl = varchar("wappen_url", 255).nullable()
|
||||||
val istAktiv = bool("ist_aktiv").default(true)
|
val istAktiv = bool("ist_aktiv").default(true)
|
||||||
val sortierReihenfolge = integer("sortier_reihenfolge").nullable()
|
val sortierReihenfolge = integer("sortier_reihenfolge").nullable()
|
||||||
val createdAt = datetime("created_at").defaultExpression(CurrentDateTime)
|
val createdAt = timestamp("created_at").defaultExpression(CurrentTimestamp)
|
||||||
val updatedAt = datetime("updated_at").defaultExpression(CurrentDateTime)
|
val updatedAt = timestamp("updated_at").defaultExpression(CurrentTimestamp)
|
||||||
|
|
||||||
override val primaryKey = PrimaryKey(id)
|
override val primaryKey = PrimaryKey(id)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
// Unique constraint for OEPS code per country
|
uniqueIndex("idx_bundesland_oeps", oepsCode, landId)
|
||||||
uniqueIndex("uk_bundesland_oeps_land", oepsCode, landId)
|
uniqueIndex("idx_bundesland_iso", iso3166_2_Code)
|
||||||
// Unique constraint for ISO 3166-2 code globally
|
|
||||||
uniqueIndex("uk_bundesland_iso3166_2", iso3166_2_Code)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+157
@@ -0,0 +1,157 @@
|
|||||||
|
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
||||||
|
|
||||||
|
package at.mocode.masterdata.infrastructure.persistence
|
||||||
|
|
||||||
|
import at.mocode.core.domain.model.DatenQuelleE
|
||||||
|
import at.mocode.core.domain.model.FunktionaerRolleE
|
||||||
|
import at.mocode.core.domain.model.RichterQualifikationE
|
||||||
|
import at.mocode.core.domain.model.SparteE
|
||||||
|
import at.mocode.core.utils.database.DatabaseFactory
|
||||||
|
import at.mocode.masterdata.domain.model.DomFunktionaer
|
||||||
|
import at.mocode.masterdata.domain.repository.FunktionaerRepository
|
||||||
|
import org.jetbrains.exposed.v1.core.ResultRow
|
||||||
|
import org.jetbrains.exposed.v1.core.eq
|
||||||
|
import org.jetbrains.exposed.v1.core.like
|
||||||
|
import org.jetbrains.exposed.v1.core.or
|
||||||
|
import org.jetbrains.exposed.v1.jdbc.*
|
||||||
|
import kotlin.uuid.Uuid
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exposed-basierte Implementierung des Funktionaer-Repositorys.
|
||||||
|
*/
|
||||||
|
class ExposedFunktionaerRepository : FunktionaerRepository {
|
||||||
|
|
||||||
|
private fun rowToDomFunktionaer(row: ResultRow): DomFunktionaer {
|
||||||
|
return DomFunktionaer(
|
||||||
|
funktionaerId = row[FunktionaerTable.id],
|
||||||
|
richterNummer = row[FunktionaerTable.richterNummer],
|
||||||
|
vorname = row[FunktionaerTable.vorname],
|
||||||
|
nachname = row[FunktionaerTable.nachname],
|
||||||
|
geburtsdatum = row[FunktionaerTable.geburtsdatum],
|
||||||
|
email = row[FunktionaerTable.email],
|
||||||
|
telefon = row[FunktionaerTable.telefon],
|
||||||
|
vereinsNummer = row[FunktionaerTable.vereinsNummer],
|
||||||
|
istAktiv = row[FunktionaerTable.istAktiv],
|
||||||
|
bemerkungen = row[FunktionaerTable.bemerkungen],
|
||||||
|
datenQuelle = DatenQuelleE.valueOf(row[FunktionaerTable.datenQuelle]),
|
||||||
|
createdAt = row[FunktionaerTable.createdAt],
|
||||||
|
updatedAt = row[FunktionaerTable.updatedAt]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun findById(id: Uuid): DomFunktionaer? = DatabaseFactory.dbQuery {
|
||||||
|
FunktionaerTable.selectAll().where { FunktionaerTable.id eq id }
|
||||||
|
.map(::rowToDomFunktionaer)
|
||||||
|
.singleOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun findByRichterNummer(richterNummer: String): DomFunktionaer? = DatabaseFactory.dbQuery {
|
||||||
|
FunktionaerTable.selectAll().where { FunktionaerTable.richterNummer eq richterNummer }
|
||||||
|
.map(::rowToDomFunktionaer)
|
||||||
|
.singleOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun findByName(searchTerm: String, limit: Int): List<DomFunktionaer> = DatabaseFactory.dbQuery {
|
||||||
|
val pattern = "%$searchTerm%"
|
||||||
|
FunktionaerTable.selectAll()
|
||||||
|
.where { (FunktionaerTable.nachname like pattern) or (FunktionaerTable.vorname like pattern) }
|
||||||
|
.limit(limit)
|
||||||
|
.map(::rowToDomFunktionaer)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun findByRolle(rolle: FunktionaerRolleE, activeOnly: Boolean): List<DomFunktionaer> =
|
||||||
|
DatabaseFactory.dbQuery {
|
||||||
|
// Rolle wird aktuell nicht in FunktionaerTable gespeichert.
|
||||||
|
// Falls benötigt, muss die Tabelle erweitert werden.
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun findByRichterQualifikation(
|
||||||
|
qualifikation: RichterQualifikationE,
|
||||||
|
activeOnly: Boolean
|
||||||
|
): List<DomFunktionaer> = DatabaseFactory.dbQuery {
|
||||||
|
// Qualifikationen werden aktuell nicht in FunktionaerTable gespeichert.
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun findBySparte(sparte: SparteE, activeOnly: Boolean): List<DomFunktionaer> =
|
||||||
|
DatabaseFactory.dbQuery {
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun findByVereinsNummer(vereinsNummer: String, activeOnly: Boolean): List<DomFunktionaer> =
|
||||||
|
DatabaseFactory.dbQuery {
|
||||||
|
val query = FunktionaerTable.selectAll().where { FunktionaerTable.vereinsNummer eq vereinsNummer }
|
||||||
|
if (activeOnly) {
|
||||||
|
query.andWhere { FunktionaerTable.istAktiv eq true }
|
||||||
|
}
|
||||||
|
query.map(::rowToDomFunktionaer)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun findAllActive(limit: Int, offset: Int): List<DomFunktionaer> = DatabaseFactory.dbQuery {
|
||||||
|
FunktionaerTable.selectAll().where { FunktionaerTable.istAktiv eq true }
|
||||||
|
.limit(limit).offset(offset.toLong())
|
||||||
|
.map(::rowToDomFunktionaer)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun findAll(limit: Int, offset: Int): List<DomFunktionaer> = DatabaseFactory.dbQuery {
|
||||||
|
FunktionaerTable.selectAll()
|
||||||
|
.limit(limit).offset(offset.toLong())
|
||||||
|
.map(::rowToDomFunktionaer)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun save(funktionaer: DomFunktionaer): DomFunktionaer = DatabaseFactory.dbQuery {
|
||||||
|
val exists = FunktionaerTable.selectAll().where { FunktionaerTable.id eq funktionaer.funktionaerId }.any()
|
||||||
|
if (exists) {
|
||||||
|
FunktionaerTable.update({ FunktionaerTable.id eq funktionaer.funktionaerId }) {
|
||||||
|
it[richterNummer] = funktionaer.richterNummer
|
||||||
|
it[vorname] = funktionaer.vorname
|
||||||
|
it[nachname] = funktionaer.nachname
|
||||||
|
it[geburtsdatum] = funktionaer.geburtsdatum
|
||||||
|
it[email] = funktionaer.email
|
||||||
|
it[telefon] = funktionaer.telefon
|
||||||
|
it[vereinsNummer] = funktionaer.vereinsNummer
|
||||||
|
it[istAktiv] = funktionaer.istAktiv
|
||||||
|
it[bemerkungen] = funktionaer.bemerkungen
|
||||||
|
it[datenQuelle] = funktionaer.datenQuelle.name
|
||||||
|
it[updatedAt] = funktionaer.updatedAt
|
||||||
|
}
|
||||||
|
funktionaer
|
||||||
|
} else {
|
||||||
|
FunktionaerTable.insert {
|
||||||
|
it[id] = funktionaer.funktionaerId
|
||||||
|
it[richterNummer] = funktionaer.richterNummer
|
||||||
|
it[vorname] = funktionaer.vorname
|
||||||
|
it[nachname] = funktionaer.nachname
|
||||||
|
it[geburtsdatum] = funktionaer.geburtsdatum
|
||||||
|
it[email] = funktionaer.email
|
||||||
|
it[telefon] = funktionaer.telefon
|
||||||
|
it[vereinsNummer] = funktionaer.vereinsNummer
|
||||||
|
it[istAktiv] = funktionaer.istAktiv
|
||||||
|
it[bemerkungen] = funktionaer.bemerkungen
|
||||||
|
it[datenQuelle] = funktionaer.datenQuelle.name
|
||||||
|
it[createdAt] = funktionaer.createdAt
|
||||||
|
it[updatedAt] = funktionaer.updatedAt
|
||||||
|
}
|
||||||
|
funktionaer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun delete(id: Uuid): Boolean = DatabaseFactory.dbQuery {
|
||||||
|
FunktionaerTable.deleteWhere { FunktionaerTable.id eq id } > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun countActive(): Long = DatabaseFactory.dbQuery {
|
||||||
|
FunktionaerTable.selectAll().where { FunktionaerTable.istAktiv eq true }.count()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun countByRichterQualifikation(qualifikation: RichterQualifikationE, activeOnly: Boolean): Long =
|
||||||
|
DatabaseFactory.dbQuery {
|
||||||
|
// Aktuell keine Qualifikations-Speicherung
|
||||||
|
0L
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun existsByRichterNummer(richterNummer: String): Boolean = DatabaseFactory.dbQuery {
|
||||||
|
FunktionaerTable.selectAll().where { FunktionaerTable.richterNummer eq richterNummer }.any()
|
||||||
|
}
|
||||||
|
}
|
||||||
+177
@@ -0,0 +1,177 @@
|
|||||||
|
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
||||||
|
|
||||||
|
package at.mocode.masterdata.infrastructure.persistence
|
||||||
|
|
||||||
|
import at.mocode.core.domain.model.DatenQuelleE
|
||||||
|
import at.mocode.core.domain.model.LizenzKlasseE
|
||||||
|
import at.mocode.core.domain.model.SparteE
|
||||||
|
import at.mocode.core.utils.database.DatabaseFactory
|
||||||
|
import at.mocode.masterdata.domain.model.DomReiter
|
||||||
|
import at.mocode.masterdata.domain.repository.ReiterRepository
|
||||||
|
import org.jetbrains.exposed.v1.core.ResultRow
|
||||||
|
import org.jetbrains.exposed.v1.core.eq
|
||||||
|
import org.jetbrains.exposed.v1.core.like
|
||||||
|
import org.jetbrains.exposed.v1.core.or
|
||||||
|
import org.jetbrains.exposed.v1.jdbc.*
|
||||||
|
import kotlin.uuid.Uuid
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exposed-basierte Implementierung des Reiter-Repositorys.
|
||||||
|
*/
|
||||||
|
class ExposedReiterRepository : ReiterRepository {
|
||||||
|
|
||||||
|
private fun rowToDomReiter(row: ResultRow): DomReiter {
|
||||||
|
return DomReiter(
|
||||||
|
reiterId = row[ReiterTable.id],
|
||||||
|
personId = row[ReiterTable.personId],
|
||||||
|
satznummer = row[ReiterTable.satznummer],
|
||||||
|
nachname = row[ReiterTable.nachname],
|
||||||
|
vorname = row[ReiterTable.vorname],
|
||||||
|
geburtsdatum = row[ReiterTable.geburtsdatum],
|
||||||
|
lizenzNummer = row[ReiterTable.lizenzNummer],
|
||||||
|
lizenzKlasse = LizenzKlasseE.valueOf(row[ReiterTable.lizenzKlasse]),
|
||||||
|
startkartAktiv = row[ReiterTable.startkartAktiv],
|
||||||
|
startkartSaison = row[ReiterTable.startkartSaison],
|
||||||
|
feiId = row[ReiterTable.feiId],
|
||||||
|
nation = row[ReiterTable.nation],
|
||||||
|
vereinsNummer = row[ReiterTable.vereinsNummer],
|
||||||
|
vereinsName = row[ReiterTable.vereinsName],
|
||||||
|
istGastreiter = row[ReiterTable.istGastreiter],
|
||||||
|
istAktiv = row[ReiterTable.istAktiv],
|
||||||
|
datenQuelle = DatenQuelleE.valueOf(row[ReiterTable.datenQuelle]),
|
||||||
|
createdAt = row[ReiterTable.createdAt],
|
||||||
|
updatedAt = row[ReiterTable.updatedAt]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun findById(id: Uuid): DomReiter? = DatabaseFactory.dbQuery {
|
||||||
|
ReiterTable.selectAll().where { ReiterTable.id eq id }
|
||||||
|
.map(::rowToDomReiter)
|
||||||
|
.singleOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun findBySatznummer(satznummer: String): DomReiter? = DatabaseFactory.dbQuery {
|
||||||
|
ReiterTable.selectAll().where { ReiterTable.satznummer eq satznummer }
|
||||||
|
.map(::rowToDomReiter)
|
||||||
|
.singleOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun findByFeiId(feiId: String): DomReiter? = DatabaseFactory.dbQuery {
|
||||||
|
ReiterTable.selectAll().where { ReiterTable.feiId eq feiId }
|
||||||
|
.map(::rowToDomReiter)
|
||||||
|
.singleOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun findByName(searchTerm: String, limit: Int): List<DomReiter> = DatabaseFactory.dbQuery {
|
||||||
|
val pattern = "%$searchTerm%"
|
||||||
|
ReiterTable.selectAll().where { (ReiterTable.nachname like pattern) or (ReiterTable.vorname like pattern) }
|
||||||
|
.limit(limit)
|
||||||
|
.map(::rowToDomReiter)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun findByVereinsNummer(vereinsNummer: String, activeOnly: Boolean): List<DomReiter> =
|
||||||
|
DatabaseFactory.dbQuery {
|
||||||
|
val query = ReiterTable.selectAll().where { ReiterTable.vereinsNummer eq vereinsNummer }
|
||||||
|
if (activeOnly) {
|
||||||
|
query.andWhere { ReiterTable.istAktiv eq true }
|
||||||
|
}
|
||||||
|
query.map(::rowToDomReiter)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun findByLizenzKlasse(lizenzKlasse: LizenzKlasseE, activeOnly: Boolean): List<DomReiter> =
|
||||||
|
DatabaseFactory.dbQuery {
|
||||||
|
val query = ReiterTable.selectAll().where { ReiterTable.lizenzKlasse eq lizenzKlasse.name }
|
||||||
|
if (activeOnly) {
|
||||||
|
query.andWhere { ReiterTable.istAktiv eq true }
|
||||||
|
}
|
||||||
|
query.map(::rowToDomReiter)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun findBySparte(sparte: SparteE, activeOnly: Boolean): List<DomReiter> = DatabaseFactory.dbQuery {
|
||||||
|
// Da wir in ReiterTable keinen sparteFilter haben, müssen wir ggf. über eine andere Tabelle gehen
|
||||||
|
// oder die Logik anpassen. Fürs erste geben wir eine leere Liste zurück oder suchen nach Name in Lizenz?
|
||||||
|
// TODO: Implementierung prüfen, falls Sparten-Lizenzierung in eigener Tabelle liegt.
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun findGastreiter(activeOnly: Boolean): List<DomReiter> = DatabaseFactory.dbQuery {
|
||||||
|
val query = ReiterTable.selectAll().where { ReiterTable.istGastreiter eq true }
|
||||||
|
if (activeOnly) {
|
||||||
|
query.andWhere { ReiterTable.istAktiv eq true }
|
||||||
|
}
|
||||||
|
query.map(::rowToDomReiter)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun findAllActive(limit: Int, offset: Int): List<DomReiter> = DatabaseFactory.dbQuery {
|
||||||
|
ReiterTable.selectAll().where { ReiterTable.istAktiv eq true }
|
||||||
|
.limit(limit).offset(offset.toLong())
|
||||||
|
.map(::rowToDomReiter)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun findAll(limit: Int, offset: Int): List<DomReiter> = DatabaseFactory.dbQuery {
|
||||||
|
ReiterTable.selectAll()
|
||||||
|
.limit(limit).offset(offset.toLong())
|
||||||
|
.map(::rowToDomReiter)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun save(reiter: DomReiter): DomReiter = DatabaseFactory.dbQuery {
|
||||||
|
val exists = ReiterTable.selectAll().where { ReiterTable.id eq reiter.reiterId }.any()
|
||||||
|
if (exists) {
|
||||||
|
ReiterTable.update({ ReiterTable.id eq reiter.reiterId }) {
|
||||||
|
it[personId] = reiter.personId
|
||||||
|
it[satznummer] = reiter.satznummer
|
||||||
|
it[nachname] = reiter.nachname
|
||||||
|
it[vorname] = reiter.vorname
|
||||||
|
it[geburtsdatum] = reiter.geburtsdatum
|
||||||
|
it[lizenzNummer] = reiter.lizenzNummer
|
||||||
|
it[lizenzKlasse] = reiter.lizenzKlasse.name
|
||||||
|
it[startkartAktiv] = reiter.startkartAktiv
|
||||||
|
it[startkartSaison] = reiter.startkartSaison
|
||||||
|
it[feiId] = reiter.feiId
|
||||||
|
it[nation] = reiter.nation
|
||||||
|
it[vereinsNummer] = reiter.vereinsNummer
|
||||||
|
it[vereinsName] = reiter.vereinsName
|
||||||
|
it[istGastreiter] = reiter.istGastreiter
|
||||||
|
it[istAktiv] = reiter.istAktiv
|
||||||
|
it[datenQuelle] = reiter.datenQuelle.name
|
||||||
|
it[updatedAt] = reiter.updatedAt
|
||||||
|
}
|
||||||
|
reiter
|
||||||
|
} else {
|
||||||
|
ReiterTable.insert {
|
||||||
|
it[id] = reiter.reiterId
|
||||||
|
it[personId] = reiter.personId
|
||||||
|
it[satznummer] = reiter.satznummer
|
||||||
|
it[nachname] = reiter.nachname
|
||||||
|
it[vorname] = reiter.vorname
|
||||||
|
it[geburtsdatum] = reiter.geburtsdatum
|
||||||
|
it[lizenzNummer] = reiter.lizenzNummer
|
||||||
|
it[lizenzKlasse] = reiter.lizenzKlasse.name
|
||||||
|
it[startkartAktiv] = reiter.startkartAktiv
|
||||||
|
it[startkartSaison] = reiter.startkartSaison
|
||||||
|
it[feiId] = reiter.feiId
|
||||||
|
it[nation] = reiter.nation
|
||||||
|
it[vereinsNummer] = reiter.vereinsNummer
|
||||||
|
it[vereinsName] = reiter.vereinsName
|
||||||
|
it[istGastreiter] = reiter.istGastreiter
|
||||||
|
it[istAktiv] = reiter.istAktiv
|
||||||
|
it[datenQuelle] = reiter.datenQuelle.name
|
||||||
|
it[createdAt] = reiter.createdAt
|
||||||
|
it[updatedAt] = reiter.updatedAt
|
||||||
|
}
|
||||||
|
reiter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun delete(id: Uuid): Boolean = DatabaseFactory.dbQuery {
|
||||||
|
ReiterTable.deleteWhere { ReiterTable.id eq id } > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun countActive(): Long = DatabaseFactory.dbQuery {
|
||||||
|
ReiterTable.selectAll().where { ReiterTable.istAktiv eq true }.count()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun existsBySatznummer(satznummer: String): Boolean = DatabaseFactory.dbQuery {
|
||||||
|
ReiterTable.selectAll().where { ReiterTable.satznummer eq satznummer }.any()
|
||||||
|
}
|
||||||
|
}
|
||||||
+154
@@ -0,0 +1,154 @@
|
|||||||
|
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
||||||
|
|
||||||
|
package at.mocode.masterdata.infrastructure.persistence
|
||||||
|
|
||||||
|
import at.mocode.core.domain.model.DatenQuelleE
|
||||||
|
import at.mocode.core.utils.database.DatabaseFactory
|
||||||
|
import at.mocode.masterdata.domain.model.DomVerein
|
||||||
|
import at.mocode.masterdata.domain.repository.VereinRepository
|
||||||
|
import org.jetbrains.exposed.v1.core.ResultRow
|
||||||
|
import org.jetbrains.exposed.v1.core.or
|
||||||
|
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 org.jetbrains.exposed.v1.core.eq
|
||||||
|
import org.jetbrains.exposed.v1.core.like
|
||||||
|
import org.jetbrains.exposed.v1.jdbc.andWhere
|
||||||
|
import kotlin.uuid.Uuid
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exposed-basierte Implementierung des Verein-Repositorys.
|
||||||
|
*/
|
||||||
|
class ExposedVereinRepository : VereinRepository {
|
||||||
|
|
||||||
|
private fun rowToDomVerein(row: ResultRow): DomVerein {
|
||||||
|
return DomVerein(
|
||||||
|
vereinId = row[VereinTable.id],
|
||||||
|
vereinsNummer = row[VereinTable.vereinsNummer],
|
||||||
|
name = row[VereinTable.name],
|
||||||
|
kurzname = row[VereinTable.kurzname],
|
||||||
|
bundesland = row[VereinTable.bundesland],
|
||||||
|
ort = row[VereinTable.ort],
|
||||||
|
plz = row[VereinTable.plz],
|
||||||
|
strasse = row[VereinTable.strasse],
|
||||||
|
email = row[VereinTable.email],
|
||||||
|
telefon = row[VereinTable.telefon],
|
||||||
|
website = row[VereinTable.website],
|
||||||
|
oepsRegionNummer = row[VereinTable.oepsRegionNummer],
|
||||||
|
istVeranstalter = row[VereinTable.istVeranstalter],
|
||||||
|
istAktiv = row[VereinTable.istAktiv],
|
||||||
|
bemerkungen = row[VereinTable.bemerkungen],
|
||||||
|
datenQuelle = DatenQuelleE.valueOf(row[VereinTable.datenQuelle]),
|
||||||
|
createdAt = row[VereinTable.createdAt],
|
||||||
|
updatedAt = row[VereinTable.updatedAt]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun findById(id: Uuid): DomVerein? = DatabaseFactory.dbQuery {
|
||||||
|
VereinTable.selectAll().where { VereinTable.id eq id }
|
||||||
|
.map(::rowToDomVerein)
|
||||||
|
.singleOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun findByVereinsNummer(vereinsNummer: String): DomVerein? = DatabaseFactory.dbQuery {
|
||||||
|
VereinTable.selectAll().where { VereinTable.vereinsNummer eq vereinsNummer }
|
||||||
|
.map(::rowToDomVerein)
|
||||||
|
.singleOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun findByName(searchTerm: String, limit: Int): List<DomVerein> = DatabaseFactory.dbQuery {
|
||||||
|
val pattern = "%$searchTerm%"
|
||||||
|
VereinTable.selectAll().where { (VereinTable.name like pattern) or (VereinTable.kurzname like pattern) }
|
||||||
|
.limit(limit)
|
||||||
|
.map(::rowToDomVerein)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun findByBundesland(bundesland: String, activeOnly: Boolean): List<DomVerein> =
|
||||||
|
DatabaseFactory.dbQuery {
|
||||||
|
val query = VereinTable.selectAll().where { VereinTable.bundesland eq bundesland }
|
||||||
|
if (activeOnly) {
|
||||||
|
query.andWhere { VereinTable.istAktiv eq true }
|
||||||
|
}
|
||||||
|
query.map(::rowToDomVerein)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun findVeranstalter(activeOnly: Boolean): List<DomVerein> = DatabaseFactory.dbQuery {
|
||||||
|
val query = VereinTable.selectAll().where { VereinTable.istVeranstalter eq true }
|
||||||
|
if (activeOnly) {
|
||||||
|
query.andWhere { VereinTable.istAktiv eq true }
|
||||||
|
}
|
||||||
|
query.map(::rowToDomVerein)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun findAllActive(limit: Int, offset: Int): List<DomVerein> = DatabaseFactory.dbQuery {
|
||||||
|
VereinTable.selectAll().where { VereinTable.istAktiv eq true }
|
||||||
|
.limit(limit).offset(offset.toLong())
|
||||||
|
.map(::rowToDomVerein)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun findAll(limit: Int, offset: Int): List<DomVerein> = DatabaseFactory.dbQuery {
|
||||||
|
VereinTable.selectAll()
|
||||||
|
.limit(limit).offset(offset.toLong())
|
||||||
|
.map(::rowToDomVerein)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun save(verein: DomVerein): DomVerein = DatabaseFactory.dbQuery {
|
||||||
|
val exists = VereinTable.selectAll().where { VereinTable.id eq verein.vereinId }.any()
|
||||||
|
if (exists) {
|
||||||
|
VereinTable.update({ VereinTable.id eq verein.vereinId }) {
|
||||||
|
it[vereinsNummer] = verein.vereinsNummer
|
||||||
|
it[name] = verein.name
|
||||||
|
it[kurzname] = verein.kurzname
|
||||||
|
it[bundesland] = verein.bundesland
|
||||||
|
it[ort] = verein.ort
|
||||||
|
it[plz] = verein.plz
|
||||||
|
it[strasse] = verein.strasse
|
||||||
|
it[email] = verein.email
|
||||||
|
it[telefon] = verein.telefon
|
||||||
|
it[website] = verein.website
|
||||||
|
it[oepsRegionNummer] = verein.oepsRegionNummer
|
||||||
|
it[istVeranstalter] = verein.istVeranstalter
|
||||||
|
it[istAktiv] = verein.istAktiv
|
||||||
|
it[bemerkungen] = verein.bemerkungen
|
||||||
|
it[datenQuelle] = verein.datenQuelle.name
|
||||||
|
it[updatedAt] = verein.updatedAt
|
||||||
|
}
|
||||||
|
verein
|
||||||
|
} else {
|
||||||
|
VereinTable.insert {
|
||||||
|
it[id] = verein.vereinId
|
||||||
|
it[vereinsNummer] = verein.vereinsNummer
|
||||||
|
it[name] = verein.name
|
||||||
|
it[kurzname] = verein.kurzname
|
||||||
|
it[bundesland] = verein.bundesland
|
||||||
|
it[ort] = verein.ort
|
||||||
|
it[plz] = verein.plz
|
||||||
|
it[strasse] = verein.strasse
|
||||||
|
it[email] = verein.email
|
||||||
|
it[telefon] = verein.telefon
|
||||||
|
it[website] = verein.website
|
||||||
|
it[oepsRegionNummer] = verein.oepsRegionNummer
|
||||||
|
it[istVeranstalter] = verein.istVeranstalter
|
||||||
|
it[istAktiv] = verein.istAktiv
|
||||||
|
it[bemerkungen] = verein.bemerkungen
|
||||||
|
it[datenQuelle] = verein.datenQuelle.name
|
||||||
|
it[createdAt] = verein.createdAt
|
||||||
|
it[updatedAt] = verein.updatedAt
|
||||||
|
}
|
||||||
|
verein
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun delete(id: Uuid): Boolean = DatabaseFactory.dbQuery {
|
||||||
|
VereinTable.deleteWhere { VereinTable.id eq id } > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun countActive(): Long = DatabaseFactory.dbQuery {
|
||||||
|
VereinTable.selectAll().where { VereinTable.istAktiv eq true }.count()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun existsByVereinsNummer(vereinsNummer: String): Boolean = DatabaseFactory.dbQuery {
|
||||||
|
VereinTable.selectAll().where { VereinTable.vereinsNummer eq vereinsNummer }.any()
|
||||||
|
}
|
||||||
|
}
|
||||||
+30
@@ -0,0 +1,30 @@
|
|||||||
|
@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.date
|
||||||
|
import org.jetbrains.exposed.v1.datetime.timestamp
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exposed-Tabellendefinition für die Funktionär-Entität.
|
||||||
|
*/
|
||||||
|
object FunktionaerTable : Table("funktionaer") {
|
||||||
|
val id = uuid("funktionaer_id")
|
||||||
|
val richterNummer = varchar("richter_nummer", 10).nullable().uniqueIndex()
|
||||||
|
val vorname = varchar("vorname", 100)
|
||||||
|
val nachname = varchar("nachname", 100)
|
||||||
|
val geburtsdatum = date("geburtsdatum").nullable()
|
||||||
|
val email = varchar("email", 200).nullable()
|
||||||
|
val telefon = varchar("telefon", 50).nullable()
|
||||||
|
val vereinsNummer = varchar("vereins_nummer", 10).nullable()
|
||||||
|
val istAktiv = bool("ist_aktiv").default(true)
|
||||||
|
val bemerkungen = text("bemerkungen").nullable()
|
||||||
|
val datenQuelle = varchar("daten_quelle", 50)
|
||||||
|
val createdAt = timestamp("created_at").defaultExpression(CurrentTimestamp)
|
||||||
|
val updatedAt = timestamp("updated_at").defaultExpression(CurrentTimestamp)
|
||||||
|
|
||||||
|
override val primaryKey = PrimaryKey(id)
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user