chore: remove deprecated horses, clubs, officials, and persons services
- Deleted obsolete modules related to horses, clubs, officials, and persons services, including their configurations, build files, and database provisioning scripts. - Cleaned up associated references in the project structure (e.g., `settings.gradle.kts`). - Removed unused database tables and Spring beans related to these domains. Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
This commit is contained in:
@@ -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
|
||||
|
||||
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 kotlinx.serialization.Serializable
|
||||
import kotlin.time.Clock
|
||||
@@ -59,9 +59,9 @@ data class DomAbteilung(
|
||||
var bemerkungen: String? = null,
|
||||
|
||||
// Audit
|
||||
@Serializable(with = KotlinInstantSerializer::class)
|
||||
@Serializable(with = InstantSerializer::class)
|
||||
val createdAt: Instant = Clock.System.now(),
|
||||
@Serializable(with = KotlinInstantSerializer::class)
|
||||
@Serializable(with = InstantSerializer::class)
|
||||
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.SparteE
|
||||
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 kotlinx.serialization.Serializable
|
||||
import kotlin.time.Clock
|
||||
@@ -69,9 +69,9 @@ data class DomBewerb(
|
||||
var bemerkungen: String? = null,
|
||||
|
||||
// Audit
|
||||
@Serializable(with = KotlinInstantSerializer::class)
|
||||
@Serializable(with = InstantSerializer::class)
|
||||
val createdAt: Instant = Clock.System.now(),
|
||||
@Serializable(with = KotlinInstantSerializer::class)
|
||||
@Serializable(with = InstantSerializer::class)
|
||||
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.StartwunschE
|
||||
import at.mocode.core.domain.serialization.KotlinInstantSerializer
|
||||
import at.mocode.core.domain.serialization.InstantSerializer
|
||||
import at.mocode.core.domain.serialization.UuidSerializer
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlin.time.Clock
|
||||
@@ -74,9 +74,9 @@ data class DomNennung(
|
||||
val bemerkungen: String? = null,
|
||||
|
||||
// Audit Fields
|
||||
@Serializable(with = KotlinInstantSerializer::class)
|
||||
@Serializable(with = InstantSerializer::class)
|
||||
val createdAt: Instant = Clock.System.now(),
|
||||
@Serializable(with = KotlinInstantSerializer::class)
|
||||
@Serializable(with = InstantSerializer::class)
|
||||
var updatedAt: Instant = Clock.System.now()
|
||||
) {
|
||||
/**
|
||||
|
||||
+2
-2
@@ -2,7 +2,7 @@
|
||||
|
||||
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 kotlinx.serialization.Serializable
|
||||
import kotlin.time.Clock
|
||||
@@ -72,7 +72,7 @@ data class DomNennungsTransfer(
|
||||
val grund: String? = null,
|
||||
|
||||
// Audit Fields
|
||||
@Serializable(with = KotlinInstantSerializer::class)
|
||||
@Serializable(with = InstantSerializer::class)
|
||||
val createdAt: Instant = Clock.System.now()
|
||||
) {
|
||||
/**
|
||||
|
||||
+5
-5
@@ -3,7 +3,7 @@
|
||||
package at.mocode.entries.domain.model
|
||||
|
||||
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 kotlinx.serialization.Serializable
|
||||
import kotlin.time.Clock
|
||||
@@ -49,18 +49,18 @@ data class DomStartliste(
|
||||
var eintraege: List<StartlistenEintrag> = emptyList(),
|
||||
|
||||
// Zeitstempel Workflow
|
||||
@Serializable(with = KotlinInstantSerializer::class)
|
||||
@Serializable(with = InstantSerializer::class)
|
||||
var veroeffentlichtAt: Instant? = null,
|
||||
@Serializable(with = KotlinInstantSerializer::class)
|
||||
@Serializable(with = InstantSerializer::class)
|
||||
var gesperrtAt: Instant? = null,
|
||||
|
||||
// Verwaltung
|
||||
var bemerkungen: String? = null,
|
||||
|
||||
// Audit
|
||||
@Serializable(with = KotlinInstantSerializer::class)
|
||||
@Serializable(with = InstantSerializer::class)
|
||||
val createdAt: Instant = Clock.System.now(),
|
||||
@Serializable(with = KotlinInstantSerializer::class)
|
||||
@Serializable(with = InstantSerializer::class)
|
||||
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.SparteE
|
||||
import at.mocode.core.domain.serialization.KotlinInstantSerializer
|
||||
import at.mocode.core.domain.serialization.KotlinxInstantSerializer
|
||||
import at.mocode.core.domain.serialization.UuidSerializer
|
||||
import kotlinx.datetime.LocalDate
|
||||
import kotlinx.serialization.Serializable
|
||||
@@ -88,9 +88,9 @@ data class DomAusschreibung(
|
||||
var genehmigungsNummer: String? = null,
|
||||
|
||||
// Audit
|
||||
@Serializable(with = KotlinInstantSerializer::class)
|
||||
@Serializable(with = KotlinxInstantSerializer::class)
|
||||
val createdAt: Instant = Clock.System.now(),
|
||||
@Serializable(with = KotlinInstantSerializer::class)
|
||||
@Serializable(with = KotlinxInstantSerializer::class)
|
||||
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.TurnierkategorieE
|
||||
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 kotlinx.datetime.LocalDate
|
||||
import kotlinx.serialization.Serializable
|
||||
@@ -67,9 +67,9 @@ data class DomTurnier(
|
||||
var bemerkungen: String? = null,
|
||||
|
||||
// Audit
|
||||
@Serializable(with = KotlinInstantSerializer::class)
|
||||
@Serializable(with = KotlinxInstantSerializer::class)
|
||||
val createdAt: Instant = Clock.System.now(),
|
||||
@Serializable(with = KotlinInstantSerializer::class)
|
||||
@Serializable(with = KotlinxInstantSerializer::class)
|
||||
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.VeranstaltungsStatusE
|
||||
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 kotlinx.datetime.LocalDate
|
||||
import kotlinx.serialization.Serializable
|
||||
@@ -71,9 +71,9 @@ data class DomVeranstaltung(
|
||||
var bemerkungen: String? = null,
|
||||
|
||||
// Audit
|
||||
@Serializable(with = KotlinInstantSerializer::class)
|
||||
@Serializable(with = KotlinxInstantSerializer::class)
|
||||
val createdAt: Instant = Clock.System.now(),
|
||||
@Serializable(with = KotlinInstantSerializer::class)
|
||||
@Serializable(with = KotlinxInstantSerializer::class)
|
||||
var updatedAt: Instant = Clock.System.now()
|
||||
) {
|
||||
/**
|
||||
|
||||
+3
-3
@@ -2,7 +2,7 @@
|
||||
package at.mocode.events.domain.model
|
||||
|
||||
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.UuidSerializer
|
||||
import kotlin.uuid.Uuid
|
||||
@@ -63,9 +63,9 @@ data class Veranstaltung(
|
||||
var anmeldeschluss: LocalDate? = null,
|
||||
|
||||
// Audit Fields
|
||||
@Serializable(with = KotlinInstantSerializer::class)
|
||||
@Serializable(with = KotlinxInstantSerializer::class)
|
||||
val createdAt: Instant = Clock.System.now(),
|
||||
@Serializable(with = KotlinInstantSerializer::class)
|
||||
@Serializable(with = KotlinxInstantSerializer::class)
|
||||
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 {
|
||||
alias(libs.plugins.kotlinJvm)
|
||||
alias(libs.plugins.kotlinSpring)
|
||||
alias(libs.plugins.kotlinSerialization)
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.platform.platformDependencies)
|
||||
implementation(projects.backend.services.clubs.clubsDomain)
|
||||
implementation(projects.backend.services.identity.identityDomain)
|
||||
implementation(projects.core.coreDomain)
|
||||
implementation(projects.core.coreUtils)
|
||||
|
||||
implementation(libs.spring.boot.starter.web)
|
||||
|
||||
implementation(libs.exposed.core)
|
||||
implementation(libs.exposed.dao)
|
||||
implementation(libs.exposed.jdbc)
|
||||
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,28 @@
|
||||
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(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"])
|
||||
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 {
|
||||
// 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)
|
||||
alias(libs.plugins.ktor)
|
||||
application
|
||||
alias(libs.plugins.spring.dependencyManagement)
|
||||
alias(libs.plugins.kotlinJvm)
|
||||
alias(libs.plugins.kotlinSerialization)
|
||||
id("application")
|
||||
}
|
||||
|
||||
application {
|
||||
@@ -16,8 +12,8 @@ dependencies {
|
||||
api(platform(libs.spring.boot.dependencies))
|
||||
// Interne Module
|
||||
implementation(projects.platform.platformDependencies)
|
||||
implementation(projects.masterdata.masterdataDomain)
|
||||
implementation(projects.masterdata.masterdataApplication)
|
||||
implementation(projects.backend.services.masterdata.masterdataDomain)
|
||||
implementation(projects.backend.services.masterdata.masterdataCommon)
|
||||
implementation(projects.core.coreDomain)
|
||||
implementation(projects.core.coreUtils)
|
||||
|
||||
|
||||
+16
-12
@@ -1,10 +1,14 @@
|
||||
package at.mocode.masterdata.api
|
||||
|
||||
import at.mocode.core.domain.model.ApiResponse
|
||||
import at.mocode.core.domain.model.ErrorCode
|
||||
import io.ktor.http.*
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.plugins.statuspages.*
|
||||
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.
|
||||
class NotFoundException(message: String) : RuntimeException(message)
|
||||
@@ -15,10 +19,10 @@ fun Application.configureStatusPages() {
|
||||
// Regel 1: Fange alle "IllegalArgumentException" ab.
|
||||
// Das passiert bei ungültigen Eingaben, z.B. ein falsches UUID-Format.
|
||||
exception<IllegalArgumentException> { call, cause ->
|
||||
log.warn("Bad Request: ${cause.message}")
|
||||
val errorResponse = ApiResponse<Unit>(
|
||||
message = cause.message ?: "Invalid input provided.",
|
||||
errors = listOf("BAD_REQUEST")
|
||||
logger.warn("Bad Request: ${cause.message}")
|
||||
val errorResponse = ApiResponse.error<Unit>(
|
||||
code = ErrorCode("BAD_REQUEST"),
|
||||
message = cause.message ?: "Invalid input provided."
|
||||
)
|
||||
call.respond(HttpStatusCode.BadRequest, errorResponse)
|
||||
}
|
||||
@@ -26,10 +30,10 @@ fun Application.configureStatusPages() {
|
||||
// Regel 2: Fange unsere eigene "NotFoundException" ab.
|
||||
// Diese werfen wir, wenn eine Entität nicht in der DB gefunden wurde.
|
||||
exception<NotFoundException> { call, cause ->
|
||||
log.info("Resource not found: ${cause.message}")
|
||||
val errorResponse = ApiResponse<Unit>(
|
||||
message = cause.message ?: "The requested resource was not found.",
|
||||
errors = listOf("NOT_FOUND")
|
||||
logger.info("Resource not found: ${cause.message}")
|
||||
val errorResponse = ApiResponse.error<Unit>(
|
||||
code = ErrorCode("NOT_FOUND"),
|
||||
message = cause.message ?: "The requested resource was not found."
|
||||
)
|
||||
call.respond(HttpStatusCode.NotFound, errorResponse)
|
||||
}
|
||||
@@ -37,10 +41,10 @@ fun Application.configureStatusPages() {
|
||||
// Regel 3: Fange alle anderen, unerwarteten Fehler ab.
|
||||
// Das ist unser Sicherheitsnetz für alles, was wir nicht vorhergesehen haben.
|
||||
exception<Throwable> { call, cause ->
|
||||
log.error("Internal Server Error", cause) // Logge den kompletten Stacktrace
|
||||
val errorResponse = ApiResponse<Unit>(
|
||||
message = "An unexpected internal server error occurred.",
|
||||
errors = listOf("INTERNAL_SERVER_ERROR")
|
||||
logger.error("Internal Server Error", cause) // Logge den kompletten Stacktrace
|
||||
val errorResponse = ApiResponse.error<Unit>(
|
||||
code = ErrorCode("INTERNAL_SERVER_ERROR"),
|
||||
message = "An unexpected internal server error occurred."
|
||||
)
|
||||
call.respond(HttpStatusCode.InternalServerError, errorResponse)
|
||||
}
|
||||
|
||||
+206
-435
@@ -1,4 +1,5 @@
|
||||
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
||||
|
||||
package at.mocode.masterdata.api.rest
|
||||
|
||||
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.GetAltersklasseUseCase
|
||||
import at.mocode.masterdata.domain.model.AltersklasseDefinition
|
||||
import at.mocode.core.utils.validation.ApiValidationUtils
|
||||
import kotlin.uuid.Uuid
|
||||
import io.ktor.http.*
|
||||
import io.ktor.server.request.*
|
||||
@@ -16,449 +16,220 @@ import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* 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(
|
||||
private val getAltersklasseUseCase: GetAltersklasseUseCase,
|
||||
private val createAltersklasseUseCase: CreateAltersklasseUseCase
|
||||
private val getAltersklasseUseCase: GetAltersklasseUseCase,
|
||||
private val createAltersklasseUseCase: CreateAltersklasseUseCase
|
||||
) {
|
||||
|
||||
/**
|
||||
* DTO for age class API responses.
|
||||
*/
|
||||
@Serializable
|
||||
data class AltersklasseDto(
|
||||
val altersklasseId: String,
|
||||
val altersklasseCode: String,
|
||||
val bezeichnung: String,
|
||||
val minAlter: Int? = null,
|
||||
val maxAlter: Int? = null,
|
||||
val stichtagRegelText: String? = null,
|
||||
val sparteFilter: String? = null,
|
||||
val geschlechtFilter: String? = null,
|
||||
val oetoRegelReferenzId: String? = null,
|
||||
val istAktiv: Boolean = true,
|
||||
val createdAt: String,
|
||||
val updatedAt: String
|
||||
)
|
||||
@Serializable
|
||||
data class AltersklasseDto(
|
||||
val altersklasseId: String,
|
||||
val altersklasseCode: String,
|
||||
val bezeichnung: String,
|
||||
val minAlter: Int? = null,
|
||||
val maxAlter: Int? = null,
|
||||
val stichtagRegelText: String? = null,
|
||||
val sparteFilter: String? = null,
|
||||
val geschlechtFilter: String? = null,
|
||||
val oetoRegelReferenzId: String? = null,
|
||||
val istAktiv: Boolean = true,
|
||||
val createdAt: String,
|
||||
val updatedAt: String
|
||||
)
|
||||
|
||||
/**
|
||||
* DTO for creating a new age class.
|
||||
*/
|
||||
@Serializable
|
||||
data class CreateAltersklasseDto(
|
||||
val altersklasseCode: String,
|
||||
val bezeichnung: String,
|
||||
val minAlter: Int? = null,
|
||||
val maxAlter: Int? = null,
|
||||
val stichtagRegelText: String? = "31.12. des laufenden Kalenderjahres",
|
||||
val sparteFilter: String? = null,
|
||||
val geschlechtFilter: String? = null,
|
||||
val oetoRegelReferenzId: String? = null,
|
||||
val istAktiv: Boolean = true
|
||||
)
|
||||
@Serializable
|
||||
data class CreateAltersklasseDto(
|
||||
val altersklasseCode: String,
|
||||
val bezeichnung: String,
|
||||
val minAlter: Int? = null,
|
||||
val maxAlter: Int? = null,
|
||||
val stichtagRegelText: String? = "31.12. des laufenden Kalenderjahres",
|
||||
val sparteFilter: String? = null,
|
||||
val geschlechtFilter: String? = null,
|
||||
val oetoRegelReferenzId: String? = null,
|
||||
val istAktiv: Boolean = true
|
||||
)
|
||||
|
||||
/**
|
||||
* DTO for updating an existing age class.
|
||||
*/
|
||||
@Serializable
|
||||
data class UpdateAltersklasseDto(
|
||||
val altersklasseCode: String,
|
||||
val bezeichnung: String,
|
||||
val minAlter: Int? = null,
|
||||
val maxAlter: Int? = null,
|
||||
val stichtagRegelText: String? = "31.12. des laufenden Kalenderjahres",
|
||||
val sparteFilter: String? = null,
|
||||
val geschlechtFilter: String? = null,
|
||||
val oetoRegelReferenzId: String? = null,
|
||||
val istAktiv: Boolean = true
|
||||
)
|
||||
@Serializable
|
||||
data class UpdateAltersklasseDto(
|
||||
val altersklasseCode: String,
|
||||
val bezeichnung: String,
|
||||
val minAlter: Int? = null,
|
||||
val maxAlter: Int? = null,
|
||||
val stichtagRegelText: String? = null,
|
||||
val sparteFilter: String? = null,
|
||||
val geschlechtFilter: String? = null,
|
||||
val oetoRegelReferenzId: String? = null,
|
||||
val istAktiv: Boolean = true
|
||||
)
|
||||
|
||||
/**
|
||||
* Configures the routing for age class endpoints.
|
||||
*/
|
||||
fun configureRouting(routing: Routing) {
|
||||
routing.route("/api/masterdata/altersklassen") {
|
||||
|
||||
// GET /api/masterdata/altersklassen - Get all active age classes
|
||||
get {
|
||||
try {
|
||||
val sparteFilterParam = call.request.queryParameters["sparte"]
|
||||
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) {
|
||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<List<AltersklasseDto>>("Failed to retrieve age classes: ${e.message}"))
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/masterdata/altersklassen/{id} - Get age class by ID
|
||||
get("/{id}") {
|
||||
try {
|
||||
val altersklasseId = call.parameters["id"]?.let { 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) {
|
||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<AltersklasseDto>("Failed to retrieve age class: ${e.message}"))
|
||||
}
|
||||
}
|
||||
|
||||
// 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,
|
||||
ApiResponse.error<List<AltersklasseDto>>(ApiValidationUtils.createErrorMessage(validationErrors))
|
||||
)
|
||||
return@get
|
||||
}
|
||||
|
||||
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 {
|
||||
try {
|
||||
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(
|
||||
altersklasseCode = createDto.altersklasseCode,
|
||||
bezeichnung = createDto.bezeichnung,
|
||||
minAlter = createDto.minAlter,
|
||||
maxAlter = createDto.maxAlter,
|
||||
stichtagRegelText = createDto.stichtagRegelText,
|
||||
sparteFilter = sparteFilter,
|
||||
geschlechtFilter = geschlechtFilter,
|
||||
oetoRegelReferenzId = oetoRegelReferenzId,
|
||||
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) {
|
||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<AltersklasseDto>("Failed to create age class: ${e.message}"))
|
||||
}
|
||||
}
|
||||
|
||||
// PUT /api/masterdata/altersklassen/{id} - Update existing age class
|
||||
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 {
|
||||
Uuid.parse(it)
|
||||
} catch (_: Exception) {
|
||||
return@put call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
ApiResponse.error<AltersklasseDto>("Invalid OETO regel referenz ID format")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val request = CreateAltersklasseUseCase.UpdateAltersklasseRequest(
|
||||
altersklasseId = altersklasseId,
|
||||
altersklasseCode = updateDto.altersklasseCode,
|
||||
bezeichnung = updateDto.bezeichnung,
|
||||
minAlter = updateDto.minAlter,
|
||||
maxAlter = updateDto.maxAlter,
|
||||
stichtagRegelText = updateDto.stichtagRegelText,
|
||||
sparteFilter = sparteFilter,
|
||||
geschlechtFilter = geschlechtFilter,
|
||||
oetoRegelReferenzId = oetoRegelReferenzId,
|
||||
istAktiv = updateDto.istAktiv
|
||||
)
|
||||
|
||||
val result = createAltersklasseUseCase.updateAltersklasse(request)
|
||||
if (result.success) {
|
||||
call.respond(HttpStatusCode.OK, ApiResponse.success(result.altersklasse!!.toDto()))
|
||||
} else {
|
||||
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<AltersklasseDto>("Validation failed: ${result.errors.joinToString(", ")}"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<AltersklasseDto>("Failed to update age class: ${e.message}"))
|
||||
}
|
||||
}
|
||||
|
||||
// DELETE /api/masterdata/altersklassen/{id} - Delete age class
|
||||
delete("/{id}") {
|
||||
try {
|
||||
val altersklasseId = call.parameters["id"]?.let { Uuid.parse(it) }
|
||||
?: return@delete call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Unit>("Invalid age class ID"))
|
||||
|
||||
val result = createAltersklasseUseCase.deleteAltersklasse(altersklasseId)
|
||||
if (result.success) {
|
||||
call.respond(HttpStatusCode.NoContent)
|
||||
} else {
|
||||
call.respond(HttpStatusCode.NotFound, ApiResponse.error<Unit>("Age class not found: ${result.errors.joinToString(", ")}"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
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}"))
|
||||
}
|
||||
}
|
||||
fun Route.registerRoutes() {
|
||||
route("/altersklassen") {
|
||||
get {
|
||||
val sparte = call.request.queryParameters["sparte"]?.let {
|
||||
try {
|
||||
SparteE.valueOf(it)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
val geschlecht = call.request.queryParameters["geschlecht"]?.getOrNull(0)
|
||||
|
||||
/**
|
||||
* 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()
|
||||
val response = getAltersklasseUseCase.getAllActive(sparte, geschlecht)
|
||||
val dtos = response.altersklassen.map { it.toDto() }
|
||||
call.respond(ApiResponse.success(dtos))
|
||||
}
|
||||
|
||||
get("/{id}") {
|
||||
val idStr = call.parameters["id"]
|
||||
val id = idStr?.let {
|
||||
try {
|
||||
Uuid.parse(it)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
?: return@get call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
ApiResponse.error<Unit>("INVALID_ID", "Missing or invalid ID")
|
||||
)
|
||||
|
||||
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"))
|
||||
}
|
||||
|
||||
post {
|
||||
val dto = call.receive<CreateAltersklasseDto>()
|
||||
val request = CreateAltersklasseUseCase.CreateAltersklasseRequest(
|
||||
altersklasseCode = dto.altersklasseCode,
|
||||
bezeichnung = dto.bezeichnung,
|
||||
minAlter = dto.minAlter,
|
||||
maxAlter = dto.maxAlter,
|
||||
stichtagRegelText = dto.stichtagRegelText,
|
||||
sparteFilter = dto.sparteFilter?.let {
|
||||
try {
|
||||
SparteE.valueOf(it)
|
||||
} 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 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,
|
||||
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(
|
||||
altersklasseId = id,
|
||||
altersklasseCode = dto.altersklasseCode,
|
||||
bezeichnung = dto.bezeichnung,
|
||||
minAlter = dto.minAlter,
|
||||
maxAlter = dto.maxAlter,
|
||||
stichtagRegelText = dto.stichtagRegelText,
|
||||
sparteFilter = dto.sparteFilter?.let {
|
||||
try {
|
||||
SparteE.valueOf(it)
|
||||
} 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 response = createAltersklasseUseCase.updateAltersklasse(request)
|
||||
val altersklasse = response.altersklasse
|
||||
if (response.success && altersklasse != null) {
|
||||
call.respond(ApiResponse.success(altersklasse.toDto()))
|
||||
} else {
|
||||
call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
ApiResponse.error<Unit>("UPDATE_FAILED", response.errors.joinToString())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
delete("/{id}") {
|
||||
val idStr = call.parameters["id"]
|
||||
val id = idStr?.let {
|
||||
try {
|
||||
Uuid.parse(it)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
?: return@delete call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
ApiResponse.error<Unit>("INVALID_ID", "Missing or invalid ID")
|
||||
)
|
||||
|
||||
val response = createAltersklasseUseCase.deleteAltersklasse(id)
|
||||
if (response.success) {
|
||||
call.respond(ApiResponse.success(Unit))
|
||||
} else {
|
||||
call.respond(
|
||||
HttpStatusCode.NotFound,
|
||||
ApiResponse.error<Unit>("DELETE_FAILED", response.errors.joinToString())
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
)
|
||||
}
|
||||
|
||||
+76
-302
@@ -5,7 +5,6 @@ import at.mocode.core.domain.model.ApiResponse
|
||||
import at.mocode.masterdata.application.usecase.CreateBundeslandUseCase
|
||||
import at.mocode.masterdata.application.usecase.GetBundeslandUseCase
|
||||
import at.mocode.masterdata.domain.model.BundeslandDefinition
|
||||
import at.mocode.core.utils.validation.ApiValidationUtils
|
||||
import kotlin.uuid.Uuid
|
||||
import io.ktor.http.*
|
||||
import io.ktor.server.request.*
|
||||
@@ -14,19 +13,13 @@ import io.ktor.server.routing.*
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* REST API controller for federal state management operations.
|
||||
*
|
||||
* This controller provides HTTP endpoints for the master-data context's
|
||||
* federal state functionality, following REST conventions and proper error handling.
|
||||
* REST API controller for federal state (Bundesland) management.
|
||||
*/
|
||||
class BundeslandController(
|
||||
private val getBundeslandUseCase: GetBundeslandUseCase,
|
||||
private val createBundeslandUseCase: CreateBundeslandUseCase
|
||||
) {
|
||||
|
||||
/**
|
||||
* DTO for federal state API responses.
|
||||
*/
|
||||
@Serializable
|
||||
data class BundeslandDto(
|
||||
val bundeslandId: String,
|
||||
@@ -42,9 +35,6 @@ class BundeslandController(
|
||||
val updatedAt: String
|
||||
)
|
||||
|
||||
/**
|
||||
* DTO for creating a new federal state.
|
||||
*/
|
||||
@Serializable
|
||||
data class CreateBundeslandDto(
|
||||
val landId: String,
|
||||
@@ -57,313 +47,97 @@ class BundeslandController(
|
||||
val sortierReihenfolge: Int? = null
|
||||
)
|
||||
|
||||
/**
|
||||
* DTO for updating an existing federal state.
|
||||
*/
|
||||
@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
|
||||
fun Route.registerRoutes() {
|
||||
route("/bundeslaender") {
|
||||
get {
|
||||
val landId = call.request.queryParameters["landId"]?.let {
|
||||
try {
|
||||
val orderBySortierungParam = call.request.queryParameters["orderBySortierung"]
|
||||
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))
|
||||
Uuid.parse(it)
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<List<BundeslandDto>>("Failed to retrieve federal states: ${e.message}"))
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
val response = if (landId != null) {
|
||||
getBundeslandUseCase.getByCountry(landId)
|
||||
} else {
|
||||
getBundeslandUseCase.getAllActive()
|
||||
}
|
||||
|
||||
val dtos = response.bundeslaender.map { it.toDto() }
|
||||
call.respond(ApiResponse.success(dtos))
|
||||
}
|
||||
|
||||
// GET /api/masterdata/bundeslaender/{id} - Get federal state by ID
|
||||
get("/{id}") {
|
||||
val idStr = call.parameters["id"]
|
||||
val id = idStr?.let {
|
||||
try {
|
||||
val bundeslandId = call.parameters["id"]?.let { 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"))
|
||||
}
|
||||
Uuid.parse(it)
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<BundeslandDto>("Failed to retrieve federal state: ${e.message}"))
|
||||
null
|
||||
}
|
||||
}
|
||||
?: return@get call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
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")
|
||||
)
|
||||
}
|
||||
|
||||
// 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,
|
||||
ApiResponse.error<List<BundeslandDto>>(ApiValidationUtils.createErrorMessage(validationErrors))
|
||||
)
|
||||
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 {
|
||||
try {
|
||||
val createDto = call.receive<CreateBundeslandDto>()
|
||||
val dto = call.receive<CreateBundeslandDto>()
|
||||
val landId = try {
|
||||
Uuid.parse(dto.landId)
|
||||
} catch (e: Exception) {
|
||||
return@post call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
ApiResponse.error<Unit>("INVALID_LAND_ID", "Invalid landId format")
|
||||
)
|
||||
}
|
||||
|
||||
// Basic validation
|
||||
if (createDto.name.isBlank()) {
|
||||
call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
ApiResponse.error<BundeslandDto>("Name is required")
|
||||
)
|
||||
return@post
|
||||
}
|
||||
val request = CreateBundeslandUseCase.CreateBundeslandRequest(
|
||||
landId = landId,
|
||||
oepsCode = dto.oepsCode,
|
||||
iso3166_2_Code = dto.iso3166_2_Code,
|
||||
name = dto.name,
|
||||
kuerzel = dto.kuerzel,
|
||||
wappenUrl = dto.wappenUrl,
|
||||
istAktiv = dto.istAktiv,
|
||||
sortierReihenfolge = dto.sortierReihenfolge
|
||||
)
|
||||
|
||||
try {
|
||||
uuidFrom(createDto.landId)
|
||||
} catch (_: Exception) {
|
||||
call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
ApiResponse.error<BundeslandDto>("Invalid country ID format")
|
||||
)
|
||||
return@post
|
||||
}
|
||||
|
||||
val request = CreateBundeslandUseCase.CreateBundeslandRequest(
|
||||
landId = uuidFrom(createDto.landId),
|
||||
oepsCode = createDto.oepsCode,
|
||||
iso3166_2_Code = createDto.iso3166_2_Code,
|
||||
name = createDto.name,
|
||||
kuerzel = createDto.kuerzel,
|
||||
wappenUrl = createDto.wappenUrl,
|
||||
istAktiv = createDto.istAktiv,
|
||||
sortierReihenfolge = createDto.sortierReihenfolge
|
||||
)
|
||||
|
||||
val result = createBundeslandUseCase.createBundesland(request)
|
||||
if (result.success) {
|
||||
call.respond(HttpStatusCode.Created, 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 create federal state: ${e.message}"))
|
||||
}
|
||||
val response = createBundeslandUseCase.createBundesland(request)
|
||||
val bundesland = response.bundesland
|
||||
if (response.success && bundesland != null) {
|
||||
call.respond(HttpStatusCode.Created, ApiResponse.success(bundesland.toDto()))
|
||||
} else {
|
||||
call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
ApiResponse.error<Unit>("CREATION_FAILED", response.errors.joinToString())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 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(
|
||||
HttpStatusCode.BadRequest,
|
||||
ApiResponse.error<BundeslandDto>("Name is required")
|
||||
)
|
||||
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()
|
||||
)
|
||||
}
|
||||
|
||||
+57
-285
@@ -5,7 +5,6 @@ import at.mocode.core.domain.model.ApiResponse
|
||||
import at.mocode.masterdata.application.usecase.CreateCountryUseCase
|
||||
import at.mocode.masterdata.application.usecase.GetCountryUseCase
|
||||
import at.mocode.masterdata.domain.model.LandDefinition
|
||||
import at.mocode.core.utils.validation.ApiValidationUtils
|
||||
import kotlin.uuid.Uuid
|
||||
import io.ktor.http.*
|
||||
import io.ktor.server.request.*
|
||||
@@ -14,19 +13,13 @@ import io.ktor.server.routing.*
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* REST API controller for country management operations.
|
||||
*
|
||||
* This controller provides HTTP endpoints for the master-data context's
|
||||
* country functionality, following REST conventions and proper error handling.
|
||||
* REST API controller for country (Land) management.
|
||||
*/
|
||||
class CountryController(
|
||||
private val getCountryUseCase: GetCountryUseCase,
|
||||
private val createCountryUseCase: CreateCountryUseCase
|
||||
) {
|
||||
|
||||
/**
|
||||
* DTO for country API responses.
|
||||
*/
|
||||
@Serializable
|
||||
data class CountryDto(
|
||||
val landId: String,
|
||||
@@ -44,9 +37,6 @@ class CountryController(
|
||||
val updatedAt: String
|
||||
)
|
||||
|
||||
/**
|
||||
* DTO for creating a new country.
|
||||
*/
|
||||
@Serializable
|
||||
data class CreateCountryDto(
|
||||
val isoAlpha2Code: String,
|
||||
@@ -61,294 +51,76 @@ class CountryController(
|
||||
val sortierReihenfolge: Int? = null
|
||||
)
|
||||
|
||||
/**
|
||||
* DTO for updating an existing country.
|
||||
*/
|
||||
@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
|
||||
fun Route.registerRoutes() {
|
||||
route("/countries") {
|
||||
get {
|
||||
try {
|
||||
// Validate orderBySortierung parameter if provided
|
||||
val orderBySortierungParam = call.request.queryParameters["orderBySortierung"]
|
||||
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}"))
|
||||
}
|
||||
val response = getCountryUseCase.getAllActive()
|
||||
val dtos = response.countries.map { it.toDto() }
|
||||
call.respond(ApiResponse.success(dtos))
|
||||
}
|
||||
|
||||
// GET /api/masterdata/countries/{id} - Get country by ID
|
||||
get("/{id}") {
|
||||
val idStr = call.parameters["id"]
|
||||
val id = idStr?.let {
|
||||
try {
|
||||
val countryId = call.parameters["id"]?.let { 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"))
|
||||
}
|
||||
Uuid.parse(it)
|
||||
} catch (e: Exception) {
|
||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<CountryDto>("Failed to retrieve country: ${e.message}"))
|
||||
null
|
||||
}
|
||||
}
|
||||
?: return@get call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
ApiResponse.error<Unit>("INVALID_ID", "Missing or invalid ID")
|
||||
)
|
||||
|
||||
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"))
|
||||
}
|
||||
|
||||
// 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,
|
||||
ApiResponse.error<List<CountryDto>>(ApiValidationUtils.createErrorMessage(validationErrors))
|
||||
)
|
||||
return@get
|
||||
}
|
||||
|
||||
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 {
|
||||
try {
|
||||
val createDto = call.receive<CreateCountryDto>()
|
||||
val dto = call.receive<CreateCountryDto>()
|
||||
val request = CreateCountryUseCase.CreateCountryRequest(
|
||||
isoAlpha2Code = dto.isoAlpha2Code,
|
||||
isoAlpha3Code = dto.isoAlpha3Code,
|
||||
isoNumerischerCode = dto.isoNumerischerCode,
|
||||
nameDeutsch = dto.nameDeutsch,
|
||||
nameEnglisch = dto.nameEnglisch,
|
||||
wappenUrl = dto.wappenUrl,
|
||||
istEuMitglied = dto.istEuMitglied,
|
||||
istEwrMitglied = dto.istEwrMitglied,
|
||||
istAktiv = dto.istAktiv,
|
||||
sortierReihenfolge = dto.sortierReihenfolge
|
||||
)
|
||||
|
||||
// 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(
|
||||
isoAlpha2Code = createDto.isoAlpha2Code,
|
||||
isoAlpha3Code = createDto.isoAlpha3Code,
|
||||
isoNumerischerCode = createDto.isoNumerischerCode,
|
||||
nameDeutsch = createDto.nameDeutsch,
|
||||
nameEnglisch = createDto.nameEnglisch,
|
||||
wappenUrl = createDto.wappenUrl,
|
||||
istEuMitglied = createDto.istEuMitglied,
|
||||
istEwrMitglied = createDto.istEwrMitglied,
|
||||
istAktiv = createDto.istAktiv,
|
||||
sortierReihenfolge = createDto.sortierReihenfolge
|
||||
)
|
||||
|
||||
val result = createCountryUseCase.createCountry(request)
|
||||
if (result.success) {
|
||||
call.respond(HttpStatusCode.Created, 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 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(
|
||||
HttpStatusCode.BadRequest,
|
||||
ApiResponse.error<CountryDto>(ApiValidationUtils.createErrorMessage(validationErrors))
|
||||
)
|
||||
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}"))
|
||||
val response = createCountryUseCase.createCountry(request)
|
||||
val country = response.country
|
||||
if (response.success && country != null) {
|
||||
call.respond(HttpStatusCode.Created, ApiResponse.success(country.toDto()))
|
||||
} else {
|
||||
call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
ApiResponse.error<Unit>("CREATION_FAILED", response.errors.joinToString())
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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()
|
||||
)
|
||||
}
|
||||
|
||||
+90
-425
@@ -6,7 +6,6 @@ import at.mocode.core.domain.model.PlatzTypE
|
||||
import at.mocode.masterdata.application.usecase.CreatePlatzUseCase
|
||||
import at.mocode.masterdata.application.usecase.GetPlatzUseCase
|
||||
import at.mocode.masterdata.domain.model.Platz
|
||||
import at.mocode.core.utils.validation.ApiValidationUtils
|
||||
import kotlin.uuid.Uuid
|
||||
import io.ktor.http.*
|
||||
import io.ktor.server.request.*
|
||||
@@ -15,22 +14,16 @@ import io.ktor.server.routing.*
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* REST API controller for venue/arena management operations.
|
||||
*
|
||||
* This controller provides HTTP endpoints for the master-data context's
|
||||
* venue functionality, following REST conventions and proper error handling.
|
||||
* REST API controller for venue/arena (Platz) management.
|
||||
*/
|
||||
class PlatzController(
|
||||
private val getPlatzUseCase: GetPlatzUseCase,
|
||||
private val createPlatzUseCase: CreatePlatzUseCase
|
||||
) {
|
||||
|
||||
/**
|
||||
* DTO for venue API responses.
|
||||
*/
|
||||
@Serializable
|
||||
data class PlatzDto(
|
||||
val id: String,
|
||||
val platzId: String,
|
||||
val turnierId: String,
|
||||
val name: String,
|
||||
val dimension: String? = null,
|
||||
@@ -42,9 +35,6 @@ class PlatzController(
|
||||
val updatedAt: String
|
||||
)
|
||||
|
||||
/**
|
||||
* DTO for creating a new venue.
|
||||
*/
|
||||
@Serializable
|
||||
data class CreatePlatzDto(
|
||||
val turnierId: String,
|
||||
@@ -56,420 +46,95 @@ class PlatzController(
|
||||
val sortierReihenfolge: Int? = null
|
||||
)
|
||||
|
||||
/**
|
||||
* DTO for updating an existing venue.
|
||||
*/
|
||||
@Serializable
|
||||
data class UpdatePlatzDto(
|
||||
val turnierId: String,
|
||||
val name: String,
|
||||
val dimension: String? = null,
|
||||
val boden: String? = null,
|
||||
val typ: String,
|
||||
val istAktiv: Boolean = true,
|
||||
val sortierReihenfolge: Int? = null
|
||||
)
|
||||
|
||||
/**
|
||||
* Configures the routing for venue endpoints.
|
||||
*/
|
||||
fun configureRouting(routing: Routing) {
|
||||
routing.route("/api/masterdata/plaetze") {
|
||||
|
||||
// GET /api/masterdata/plaetze/{id} - Get venue by ID
|
||||
get("/{id}") {
|
||||
try {
|
||||
val platzId = call.parameters["id"]?.let { 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) {
|
||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<PlatzDto>("Failed to retrieve venue: ${e.message}"))
|
||||
}
|
||||
}
|
||||
|
||||
// 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,
|
||||
ApiResponse.error<List<PlatzDto>>(ApiValidationUtils.createErrorMessage(validationErrors))
|
||||
)
|
||||
return@get
|
||||
}
|
||||
|
||||
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 {
|
||||
try {
|
||||
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 {
|
||||
uuidFrom(createDto.turnierId)
|
||||
} catch (_: Exception) {
|
||||
return@post call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
ApiResponse.error<PlatzDto>("Invalid tournament ID 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(
|
||||
turnierId = turnierId,
|
||||
name = createDto.name,
|
||||
dimension = createDto.dimension,
|
||||
boden = createDto.boden,
|
||||
typ = typ,
|
||||
istAktiv = createDto.istAktiv,
|
||||
sortierReihenfolge = createDto.sortierReihenfolge
|
||||
)
|
||||
|
||||
val result = createPlatzUseCase.createPlatz(request)
|
||||
if (result.success) {
|
||||
call.respond(HttpStatusCode.Created, 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 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(
|
||||
HttpStatusCode.BadRequest,
|
||||
ApiResponse.error<PlatzDto>("Name is required")
|
||||
)
|
||||
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}"))
|
||||
}
|
||||
}
|
||||
fun Route.registerRoutes() {
|
||||
route("/plaetze") {
|
||||
get {
|
||||
val turnierId = call.request.queryParameters["turnierId"]?.let {
|
||||
try {
|
||||
Uuid.parse(it)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
?: return@get call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
ApiResponse.error<Unit>("MISSING_TURNIER_ID", "Query parameter turnierId is required")
|
||||
)
|
||||
|
||||
/**
|
||||
* 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()
|
||||
)
|
||||
val response = getPlatzUseCase.getByTournament(turnierId)
|
||||
val dtos = response.plaetze.map { it.toDto() }
|
||||
call.respond(ApiResponse.success(dtos))
|
||||
}
|
||||
|
||||
get("/{id}") {
|
||||
val idStr = call.parameters["id"]
|
||||
val id = idStr?.let {
|
||||
try {
|
||||
Uuid.parse(it)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
?: return@get call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
ApiResponse.error<Unit>("INVALID_ID", "Missing or invalid ID")
|
||||
)
|
||||
|
||||
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"))
|
||||
}
|
||||
|
||||
post {
|
||||
val dto = call.receive<CreatePlatzDto>()
|
||||
val turnierId = try {
|
||||
Uuid.parse(dto.turnierId)
|
||||
} catch (e: Exception) {
|
||||
return@post call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
ApiResponse.error<Unit>("INVALID_TURNIER_ID", "Invalid turnierId format")
|
||||
)
|
||||
}
|
||||
|
||||
val request = CreatePlatzUseCase.CreatePlatzRequest(
|
||||
turnierId = turnierId,
|
||||
name = dto.name,
|
||||
dimension = dto.dimension,
|
||||
boden = dto.boden,
|
||||
typ = try {
|
||||
PlatzTypE.valueOf(dto.typ)
|
||||
} catch (e: Exception) {
|
||||
PlatzTypE.SONSTIGE
|
||||
},
|
||||
istAktiv = dto.istAktiv,
|
||||
sortierReihenfolge = dto.sortierReihenfolge
|
||||
)
|
||||
|
||||
val response = createPlatzUseCase.createPlatz(request)
|
||||
val platz = response.platz
|
||||
if (response.success && platz != null) {
|
||||
call.respond(HttpStatusCode.Created, ApiResponse.success(platz.toDto()))
|
||||
} else {
|
||||
call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
ApiResponse.error<Unit>("CREATION_FAILED", response.errors.joinToString())
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
implementation(projects.masterdata.masterdataDomain)
|
||||
implementation(projects.backend.services.masterdata.masterdataDomain)
|
||||
implementation(projects.core.coreDomain)
|
||||
implementation(projects.core.coreUtils)
|
||||
implementation(libs.kotlinx.datetime)
|
||||
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.ValidationError
|
||||
import kotlin.uuid.Uuid
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlin.time.Clock
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
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.ValidationError
|
||||
import kotlin.uuid.Uuid
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlin.time.Clock
|
||||
|
||||
/**
|
||||
* 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.ValidationError
|
||||
import kotlin.uuid.Uuid
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlin.time.Clock
|
||||
|
||||
/**
|
||||
* 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.ValidationError
|
||||
import kotlin.uuid.Uuid
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlin.time.Clock
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @return The age class if found, null otherwise
|
||||
*/
|
||||
suspend fun getById(altersklasseId: Uuid): AltersklasseDefinition? {
|
||||
return altersklasseRepository.findById(altersklasseId)
|
||||
suspend fun getById(altersklasseId: Uuid): GetAltersklasseResponse {
|
||||
val altersklasse = altersklasseRepository.findById(altersklasseId)
|
||||
return GetAltersklasseResponse(altersklasse = altersklasse)
|
||||
}
|
||||
|
||||
data class GetAltersklasseResponse(val altersklasse: AltersklasseDefinition?)
|
||||
|
||||
/**
|
||||
* Retrieves an age class by its code.
|
||||
*
|
||||
@@ -57,13 +60,16 @@ class GetAltersklasseUseCase(
|
||||
* @param geschlechtFilter Optional filter by gender ('M', 'W')
|
||||
* @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 ->
|
||||
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.
|
||||
*
|
||||
|
||||
+17
-20
@@ -21,10 +21,13 @@ class GetBundeslandUseCase(
|
||||
* @param bundeslandId The unique identifier of the federal state
|
||||
* @return The federal state if found, null otherwise
|
||||
*/
|
||||
suspend fun getById(bundeslandId: Uuid): BundeslandDefinition? {
|
||||
return bundeslandRepository.findById(bundeslandId)
|
||||
suspend fun getById(bundeslandId: Uuid): GetBundeslandResponse {
|
||||
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.
|
||||
*
|
||||
@@ -56,22 +59,13 @@ class GetBundeslandUseCase(
|
||||
* @param orderBySortierung Whether to order by sortierReihenfolge field (default: true)
|
||||
* @return List of federal states for the country
|
||||
*/
|
||||
suspend fun getByCountry(landId: Uuid, activeOnly: Boolean = true, orderBySortierung: Boolean = true): List<BundeslandDefinition> {
|
||||
return bundeslandRepository.findByCountry(landId, activeOnly, orderBySortierung)
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches for federal states by name (partial match).
|
||||
*
|
||||
* @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)
|
||||
suspend fun getByCountry(
|
||||
landId: Uuid,
|
||||
activeOnly: Boolean = true,
|
||||
orderBySortierung: Boolean = true
|
||||
): GetBundeslaenderResponse {
|
||||
val bundeslaender = bundeslandRepository.findByCountry(landId, activeOnly, orderBySortierung)
|
||||
return GetBundeslaenderResponse(bundeslaender = bundeslaender)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -80,10 +74,13 @@ class GetBundeslandUseCase(
|
||||
* @param orderBySortierung Whether to order by sortierReihenfolge field (default: true)
|
||||
* @return List of active federal states
|
||||
*/
|
||||
suspend fun getAllActive(orderBySortierung: Boolean = true): List<BundeslandDefinition> {
|
||||
return bundeslandRepository.findAllActive(orderBySortierung)
|
||||
suspend fun getAllActive(orderBySortierung: Boolean = true): GetBundeslaenderResponse {
|
||||
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.
|
||||
*
|
||||
|
||||
+10
-4
@@ -21,10 +21,13 @@ class GetCountryUseCase(
|
||||
* @param countryId The unique identifier of the country
|
||||
* @return The country if found, null otherwise
|
||||
*/
|
||||
suspend fun getById(countryId: Uuid): LandDefinition? {
|
||||
return landRepository.findById(countryId)
|
||||
suspend fun getById(countryId: Uuid): GetCountryResponse {
|
||||
val country = landRepository.findById(countryId)
|
||||
return GetCountryResponse(country = country)
|
||||
}
|
||||
|
||||
data class GetCountryResponse(val country: LandDefinition?)
|
||||
|
||||
/**
|
||||
* 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)
|
||||
* @return List of active countries
|
||||
*/
|
||||
suspend fun getAllActive(orderBySortierung: Boolean = true): List<LandDefinition> {
|
||||
return landRepository.findAllActive(orderBySortierung)
|
||||
suspend fun getAllActive(orderBySortierung: Boolean = true): GetCountriesResponse {
|
||||
val countries = landRepository.findAllActive(orderBySortierung)
|
||||
return GetCountriesResponse(countries = countries)
|
||||
}
|
||||
|
||||
data class GetCountriesResponse(val countries: List<LandDefinition>)
|
||||
|
||||
/**
|
||||
* Retrieves all EU member countries.
|
||||
*
|
||||
|
||||
+14
-4
@@ -22,10 +22,13 @@ class GetPlatzUseCase(
|
||||
* @param platzId The unique identifier of the venue
|
||||
* @return The venue if found, null otherwise
|
||||
*/
|
||||
suspend fun getById(platzId: Uuid): Platz? {
|
||||
return platzRepository.findById(platzId)
|
||||
suspend fun getById(platzId: Uuid): GetPlatzResponse {
|
||||
val platz = platzRepository.findById(platzId)
|
||||
return GetPlatzResponse(platz = platz)
|
||||
}
|
||||
|
||||
data class GetPlatzResponse(val platz: Platz?)
|
||||
|
||||
/**
|
||||
* Retrieves all venues for a specific tournament.
|
||||
*
|
||||
@@ -34,10 +37,17 @@ class GetPlatzUseCase(
|
||||
* @param orderBySortierung Whether to order by sortierReihenfolge field (default: true)
|
||||
* @return List of venues for the tournament
|
||||
*/
|
||||
suspend fun getByTournament(turnierId: Uuid, activeOnly: Boolean = true, orderBySortierung: Boolean = true): List<Platz> {
|
||||
return platzRepository.findByTournament(turnierId, activeOnly, orderBySortierung)
|
||||
suspend fun getByTournament(
|
||||
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).
|
||||
*
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
plugins {
|
||||
alias(libs.plugins.kotlin.multiplatform)
|
||||
alias(libs.plugins.kotlin.serialization)
|
||||
alias(libs.plugins.kotlinMultiplatform)
|
||||
alias(libs.plugins.kotlinSerialization)
|
||||
}
|
||||
|
||||
kotlin {
|
||||
@@ -20,6 +20,10 @@ kotlin {
|
||||
val commonTest by getting {
|
||||
dependencies {
|
||||
implementation(kotlin("test"))
|
||||
}
|
||||
}
|
||||
val jvmTest by getting {
|
||||
dependencies {
|
||||
implementation(projects.platform.platformTesting)
|
||||
}
|
||||
}
|
||||
|
||||
+8
-9
@@ -1,13 +1,12 @@
|
||||
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
||||
package at.mocode.masterdata.domain.model
|
||||
|
||||
import at.mocode.core.domain.model.SparteE // Optional, falls Altersklassen stark spartenspezifisch sind
|
||||
import at.mocode.core.domain.serialization.KotlinInstantSerializer
|
||||
import at.mocode.core.domain.model.SparteE
|
||||
import at.mocode.core.domain.serialization.InstantSerializer
|
||||
import at.mocode.core.domain.serialization.UuidSerializer
|
||||
import kotlin.uuid.Uuid
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlinx.datetime.Instant
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlin.time.Instant
|
||||
import kotlin.uuid.Uuid
|
||||
|
||||
/**
|
||||
* Definiert eine spezifische Altersklasse für Teilnehmer (Reiter, Fahrer, Voltigierer)
|
||||
@@ -52,8 +51,8 @@ data class AltersklasseDefinition(
|
||||
|
||||
var istAktiv: Boolean = true,
|
||||
|
||||
@Serializable(with = KotlinInstantSerializer::class)
|
||||
val createdAt: Instant = Clock.System.now(),
|
||||
@Serializable(with = KotlinInstantSerializer::class)
|
||||
var updatedAt: Instant = Clock.System.now()
|
||||
@Serializable(with = InstantSerializer::class)
|
||||
val createdAt: Instant,
|
||||
@Serializable(with = InstantSerializer::class)
|
||||
var updatedAt: Instant
|
||||
)
|
||||
+7
-8
@@ -1,12 +1,11 @@
|
||||
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
||||
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 kotlin.uuid.Uuid
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlinx.datetime.Instant
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlin.time.Instant
|
||||
import kotlin.uuid.Uuid
|
||||
|
||||
/**
|
||||
* Definiert ein Bundesland oder eine vergleichbare subnationale Verwaltungseinheit.
|
||||
@@ -44,8 +43,8 @@ data class BundeslandDefinition(
|
||||
var istAktiv: Boolean = true,
|
||||
var sortierReihenfolge: Int? = null,
|
||||
|
||||
@Serializable(with = KotlinInstantSerializer::class)
|
||||
val createdAt: Instant = Clock.System.now(),
|
||||
@Serializable(with = KotlinInstantSerializer::class)
|
||||
var updatedAt: Instant = Clock.System.now()
|
||||
@Serializable(with = InstantSerializer::class)
|
||||
val createdAt: Instant,
|
||||
@Serializable(with = InstantSerializer::class)
|
||||
var updatedAt: Instant
|
||||
)
|
||||
+4
-4
@@ -1,12 +1,12 @@
|
||||
@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.FunktionaerRolleE
|
||||
import at.mocode.core.domain.model.RichterQualifikationE
|
||||
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 kotlinx.datetime.LocalDate
|
||||
import kotlinx.serialization.Serializable
|
||||
@@ -70,9 +70,9 @@ data class DomFunktionaer(
|
||||
var datenQuelle: DatenQuelleE = DatenQuelleE.IMPORT_ZNS,
|
||||
|
||||
// Audit
|
||||
@Serializable(with = KotlinInstantSerializer::class)
|
||||
@Serializable(with = InstantSerializer::class)
|
||||
val createdAt: Instant = Clock.System.now(),
|
||||
@Serializable(with = KotlinInstantSerializer::class)
|
||||
@Serializable(with = InstantSerializer::class)
|
||||
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)
|
||||
|
||||
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.LizenzKlasseE
|
||||
import at.mocode.core.domain.model.SparteE
|
||||
import at.mocode.core.domain.serialization.KotlinInstantSerializer
|
||||
import at.mocode.core.domain.serialization.KotlinLocalDateSerializer
|
||||
import at.mocode.core.domain.serialization.InstantSerializer
|
||||
import at.mocode.core.domain.serialization.LocalDateSerializer
|
||||
import at.mocode.core.domain.serialization.UuidSerializer
|
||||
import kotlinx.datetime.LocalDate
|
||||
import kotlinx.serialization.Serializable
|
||||
@@ -74,7 +74,7 @@ data class DomReiter(
|
||||
// Personal Data (denormalized from DomPerson for performance)
|
||||
val nachname: String,
|
||||
val vorname: String,
|
||||
@Serializable(with = KotlinLocalDateSerializer::class)
|
||||
@Serializable(with = LocalDateSerializer::class)
|
||||
val geburtsdatum: LocalDate? = null,
|
||||
|
||||
// Club Affiliation
|
||||
@@ -87,9 +87,9 @@ data class DomReiter(
|
||||
val datenQuelle: DatenQuelleE = DatenQuelleE.IMPORT_ZNS,
|
||||
|
||||
// Audit Fields
|
||||
@Serializable(with = KotlinInstantSerializer::class)
|
||||
@Serializable(with = InstantSerializer::class)
|
||||
val createdAt: Instant = Clock.System.now(),
|
||||
@Serializable(with = KotlinInstantSerializer::class)
|
||||
@Serializable(with = InstantSerializer::class)
|
||||
var updatedAt: Instant = Clock.System.now()
|
||||
) {
|
||||
/**
|
||||
+4
-4
@@ -1,9 +1,9 @@
|
||||
@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.serialization.KotlinInstantSerializer
|
||||
import at.mocode.core.domain.serialization.InstantSerializer
|
||||
import at.mocode.core.domain.serialization.UuidSerializer
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlin.time.Clock
|
||||
@@ -71,9 +71,9 @@ data class DomVerein(
|
||||
var datenQuelle: DatenQuelleE = DatenQuelleE.IMPORT_ZNS,
|
||||
|
||||
// Audit
|
||||
@Serializable(with = KotlinInstantSerializer::class)
|
||||
@Serializable(with = InstantSerializer::class)
|
||||
val createdAt: Instant = Clock.System.now(),
|
||||
@Serializable(with = KotlinInstantSerializer::class)
|
||||
@Serializable(with = InstantSerializer::class)
|
||||
var updatedAt: Instant = Clock.System.now()
|
||||
) {
|
||||
/**
|
||||
+7
-8
@@ -1,12 +1,11 @@
|
||||
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
||||
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 kotlin.uuid.Uuid
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlinx.datetime.Instant
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlin.time.Instant
|
||||
import kotlin.uuid.Uuid
|
||||
|
||||
/**
|
||||
* Definiert ein Land/eine Nation mit seinen offiziellen Codes und Bezeichnungen.
|
||||
@@ -44,8 +43,8 @@ data class LandDefinition(
|
||||
var istAktiv: Boolean = true,
|
||||
var sortierReihenfolge: Int? = null,
|
||||
|
||||
@Serializable(with = KotlinInstantSerializer::class)
|
||||
val createdAt: Instant = Clock.System.now(),
|
||||
@Serializable(with = KotlinInstantSerializer::class)
|
||||
var updatedAt: Instant = Clock.System.now()
|
||||
@Serializable(with = InstantSerializer::class)
|
||||
val createdAt: Instant,
|
||||
@Serializable(with = InstantSerializer::class)
|
||||
var updatedAt: Instant
|
||||
)
|
||||
+7
-8
@@ -2,12 +2,11 @@
|
||||
package at.mocode.masterdata.domain.model
|
||||
|
||||
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 kotlin.uuid.Uuid
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlinx.datetime.Instant
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlin.time.Instant
|
||||
import kotlin.uuid.Uuid
|
||||
|
||||
/**
|
||||
* Definiert einen Turnierplatz oder eine Wettkampfstätte.
|
||||
@@ -41,8 +40,8 @@ data class Platz(
|
||||
var istAktiv: Boolean = true,
|
||||
var sortierReihenfolge: Int? = null,
|
||||
|
||||
@Serializable(with = KotlinInstantSerializer::class)
|
||||
val createdAt: Instant = Clock.System.now(),
|
||||
@Serializable(with = KotlinInstantSerializer::class)
|
||||
var updatedAt: Instant = Clock.System.now()
|
||||
@Serializable(with = InstantSerializer::class)
|
||||
val createdAt: Instant,
|
||||
@Serializable(with = InstantSerializer::class)
|
||||
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)
|
||||
|
||||
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.RichterQualifikationE
|
||||
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
|
||||
|
||||
/**
|
||||
+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)
|
||||
|
||||
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.SparteE
|
||||
import at.mocode.persons.domain.model.DomReiter
|
||||
import at.mocode.masterdata.domain.model.DomReiter
|
||||
import kotlin.uuid.Uuid
|
||||
|
||||
/**
|
||||
+3
-3
@@ -1,15 +1,15 @@
|
||||
@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
|
||||
|
||||
/**
|
||||
* Repository-Interface für DomVerein (Verein) Domain-Operationen.
|
||||
*
|
||||
* Definiert den Vertrag für Datenzugriffs-Operationen ohne Abhängigkeit
|
||||
* von konkreten Implementierungsdetails (Datenbank, etc.).
|
||||
* von konkreten Implementierungsdetails (Datenbank etc.).
|
||||
*/
|
||||
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 {
|
||||
alias(libs.plugins.kotlin.jvm)
|
||||
alias(libs.plugins.kotlin.spring)
|
||||
|
||||
// KORREKTUR: Dieses Plugin ist entscheidend. Es schaltet den `springBoot`-Block
|
||||
// und alle Spring-Boot-spezifischen Gradle-Tasks frei.
|
||||
alias(libs.plugins.spring.boot)
|
||||
|
||||
// Dependency Management für konsistente Spring-Versionen
|
||||
kotlin("jvm")
|
||||
alias(libs.plugins.spring.boot) apply false
|
||||
alias(libs.plugins.spring.dependencyManagement)
|
||||
alias(libs.plugins.kotlinSpring)
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.platform.platformDependencies)
|
||||
|
||||
implementation(projects.masterdata.masterdataDomain)
|
||||
implementation(projects.masterdata.masterdataApplication)
|
||||
implementation(projects.backend.services.masterdata.masterdataDomain)
|
||||
implementation(projects.core.coreDomain)
|
||||
implementation(projects.core.coreUtils)
|
||||
implementation(projects.infrastructure.cache.cacheApi)
|
||||
implementation(projects.infrastructure.eventStore.eventStoreApi)
|
||||
implementation(projects.infrastructure.messaging.messagingClient)
|
||||
implementation(projects.backend.infrastructure.cache.cacheApi)
|
||||
implementation(projects.backend.infrastructure.eventStore.eventStoreApi)
|
||||
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")
|
||||
|
||||
testImplementation(projects.platform.platformTesting)
|
||||
|
||||
+173
-193
@@ -1,240 +1,220 @@
|
||||
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
||||
|
||||
package at.mocode.masterdata.infrastructure.persistence
|
||||
|
||||
import at.mocode.core.domain.model.SparteE
|
||||
import at.mocode.core.utils.database.DatabaseFactory
|
||||
import at.mocode.masterdata.domain.model.AltersklasseDefinition
|
||||
import at.mocode.masterdata.domain.repository.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 kotlinx.datetime.Clock
|
||||
import kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.toInstant
|
||||
import kotlinx.datetime.toLocalDateTime
|
||||
import org.jetbrains.exposed.sql.*
|
||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
||||
|
||||
/**
|
||||
* Implementierung des AltersklasseRepository für die Datenbankzugriffe.
|
||||
*
|
||||
* Diese Implementierung verwendet Exposed SQL für den Datenbankzugriff
|
||||
* und mappt zwischen der AltersklasseDefinition Domain-Entität und der AltersklasseTable.
|
||||
*/
|
||||
class AltersklasseRepositoryImpl : AltersklasseRepository {
|
||||
|
||||
/**
|
||||
* Konvertiert eine Datenbankzeile in ein Domain-Objekt.
|
||||
*/
|
||||
private fun rowToAltersklasseDefinition(row: ResultRow): AltersklasseDefinition {
|
||||
return AltersklasseDefinition(
|
||||
altersklasseId = row[AltersklasseTable.id],
|
||||
altersklasseCode = row[AltersklasseTable.altersklasseCode],
|
||||
bezeichnung = row[AltersklasseTable.bezeichnung],
|
||||
minAlter = row[AltersklasseTable.minAlter],
|
||||
maxAlter = row[AltersklasseTable.maxAlter],
|
||||
stichtagRegelText = row[AltersklasseTable.stichtagRegelText],
|
||||
sparteFilter = row[AltersklasseTable.sparteFilter]?.let { SparteE.valueOf(it) },
|
||||
geschlechtFilter = row[AltersklasseTable.geschlechtFilter],
|
||||
oetoRegelReferenzId = row[AltersklasseTable.oetoRegelReferenzId],
|
||||
istAktiv = row[AltersklasseTable.istAktiv],
|
||||
createdAt = row[AltersklasseTable.createdAt].toInstant(TimeZone.UTC),
|
||||
updatedAt = row[AltersklasseTable.updatedAt].toInstant(TimeZone.UTC)
|
||||
)
|
||||
private fun rowToAltersklasseDefinition(row: ResultRow): AltersklasseDefinition {
|
||||
return AltersklasseDefinition(
|
||||
altersklasseId = row[AltersklasseTable.id],
|
||||
altersklasseCode = row[AltersklasseTable.altersklasseCode],
|
||||
bezeichnung = row[AltersklasseTable.bezeichnung],
|
||||
minAlter = row[AltersklasseTable.minAlter],
|
||||
maxAlter = row[AltersklasseTable.maxAlter],
|
||||
stichtagRegelText = row[AltersklasseTable.stichtagRegelText],
|
||||
sparteFilter = row[AltersklasseTable.sparteFilter]?.let { SparteE.valueOf(it) },
|
||||
geschlechtFilter = row[AltersklasseTable.geschlechtFilter],
|
||||
oetoRegelReferenzId = row[AltersklasseTable.oetoRegelReferenzId],
|
||||
istAktiv = row[AltersklasseTable.istAktiv],
|
||||
createdAt = row[AltersklasseTable.createdAt],
|
||||
updatedAt = row[AltersklasseTable.updatedAt]
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun findById(id: Uuid): AltersklasseDefinition? = DatabaseFactory.dbQuery {
|
||||
AltersklasseTable.selectAll().where { AltersklasseTable.id eq id }
|
||||
.map(::rowToAltersklasseDefinition)
|
||||
.singleOrNull()
|
||||
}
|
||||
|
||||
override suspend fun findByCode(altersklasseCode: String): AltersklasseDefinition? = DatabaseFactory.dbQuery {
|
||||
AltersklasseTable.selectAll().where { AltersklasseTable.altersklasseCode eq altersklasseCode }
|
||||
.map(::rowToAltersklasseDefinition)
|
||||
.singleOrNull()
|
||||
}
|
||||
|
||||
override suspend fun findByName(searchTerm: String, limit: Int): List<AltersklasseDefinition> =
|
||||
DatabaseFactory.dbQuery {
|
||||
val pattern = "%$searchTerm%"
|
||||
AltersklasseTable.selectAll().where { AltersklasseTable.bezeichnung like pattern }
|
||||
.limit(limit)
|
||||
.map(::rowToAltersklasseDefinition)
|
||||
}
|
||||
|
||||
override suspend fun findById(id: Uuid): AltersklasseDefinition? = DatabaseFactory.dbQuery {
|
||||
AltersklasseTable.selectAll().where { AltersklasseTable.id eq id }
|
||||
.map(::rowToAltersklasseDefinition)
|
||||
.singleOrNull()
|
||||
}
|
||||
override suspend fun findAllActive(sparteFilter: SparteE?, geschlechtFilter: Char?): List<AltersklasseDefinition> =
|
||||
DatabaseFactory.dbQuery {
|
||||
val query = AltersklasseTable.selectAll().where { AltersklasseTable.istAktiv eq true }
|
||||
|
||||
override suspend fun findByCode(altersklasseCode: String): AltersklasseDefinition? = DatabaseFactory.dbQuery {
|
||||
AltersklasseTable.selectAll().where { AltersklasseTable.altersklasseCode eq altersklasseCode }
|
||||
.map(::rowToAltersklasseDefinition)
|
||||
.singleOrNull()
|
||||
}
|
||||
|
||||
override suspend fun findByName(searchTerm: String, limit: Int): List<AltersklasseDefinition> = DatabaseFactory.dbQuery {
|
||||
val pattern = "%$searchTerm%"
|
||||
AltersklasseTable.selectAll().where { AltersklasseTable.bezeichnung like pattern }
|
||||
.limit(limit)
|
||||
.map(::rowToAltersklasseDefinition)
|
||||
}
|
||||
|
||||
override suspend fun findAllActive(sparteFilter: SparteE?, geschlechtFilter: Char?): List<AltersklasseDefinition> = DatabaseFactory.dbQuery {
|
||||
val query = AltersklasseTable.selectAll().where { AltersklasseTable.istAktiv eq true }
|
||||
|
||||
sparteFilter?.let { sparte ->
|
||||
query.andWhere {
|
||||
(AltersklasseTable.sparteFilter eq sparte.name) or (AltersklasseTable.sparteFilter.isNull())
|
||||
}
|
||||
}
|
||||
|
||||
geschlechtFilter?.let { geschlecht ->
|
||||
query.andWhere {
|
||||
(AltersklasseTable.geschlechtFilter eq geschlecht) or (AltersklasseTable.geschlechtFilter.isNull())
|
||||
}
|
||||
}
|
||||
|
||||
query.orderBy(AltersklasseTable.bezeichnung to SortOrder.ASC)
|
||||
.map(::rowToAltersklasseDefinition)
|
||||
}
|
||||
|
||||
override suspend fun findApplicableForAge(age: Int, sparteFilter: SparteE?, geschlechtFilter: Char?): List<AltersklasseDefinition> = DatabaseFactory.dbQuery {
|
||||
val query = AltersklasseTable.selectAll().where { AltersklasseTable.istAktiv eq true }
|
||||
|
||||
// Age range filter
|
||||
sparteFilter?.let { sparte ->
|
||||
query.andWhere {
|
||||
(AltersklasseTable.minAlter.isNull() or (AltersklasseTable.minAlter lessEq age)) and
|
||||
(AltersklasseTable.maxAlter.isNull() or (AltersklasseTable.maxAlter greaterEq age))
|
||||
(AltersklasseTable.sparteFilter eq sparte.name) or (AltersklasseTable.sparteFilter.isNull())
|
||||
}
|
||||
}
|
||||
|
||||
sparteFilter?.let { sparte ->
|
||||
query.andWhere {
|
||||
(AltersklasseTable.sparteFilter eq sparte.name) or (AltersklasseTable.sparteFilter.isNull())
|
||||
}
|
||||
geschlechtFilter?.let { geschlecht ->
|
||||
query.andWhere {
|
||||
(AltersklasseTable.geschlechtFilter eq geschlecht) or (AltersklasseTable.geschlechtFilter.isNull())
|
||||
}
|
||||
}
|
||||
|
||||
geschlechtFilter?.let { geschlecht ->
|
||||
query.andWhere {
|
||||
(AltersklasseTable.geschlechtFilter eq geschlecht) or (AltersklasseTable.geschlechtFilter.isNull())
|
||||
}
|
||||
}
|
||||
|
||||
query.orderBy(AltersklasseTable.bezeichnung to SortOrder.ASC)
|
||||
.map(::rowToAltersklasseDefinition)
|
||||
query.orderBy(AltersklasseTable.bezeichnung to SortOrder.ASC)
|
||||
.map(::rowToAltersklasseDefinition)
|
||||
}
|
||||
|
||||
override suspend fun findBySparte(sparte: SparteE, activeOnly: Boolean): List<AltersklasseDefinition> = DatabaseFactory.dbQuery {
|
||||
val query = AltersklasseTable.selectAll().where {
|
||||
(AltersklasseTable.sparteFilter eq sparte.name) or (AltersklasseTable.sparteFilter.isNull())
|
||||
}
|
||||
override suspend fun findApplicableForAge(
|
||||
age: Int,
|
||||
sparteFilter: SparteE?,
|
||||
geschlechtFilter: Char?
|
||||
): List<AltersklasseDefinition> = DatabaseFactory.dbQuery {
|
||||
val query = AltersklasseTable.selectAll().where { AltersklasseTable.istAktiv eq true }
|
||||
|
||||
if (activeOnly) {
|
||||
query.andWhere { AltersklasseTable.istAktiv eq true }
|
||||
}
|
||||
|
||||
query.orderBy(AltersklasseTable.bezeichnung to SortOrder.ASC)
|
||||
.map(::rowToAltersklasseDefinition)
|
||||
query.andWhere {
|
||||
(AltersklasseTable.minAlter.isNull() or (AltersklasseTable.minAlter lessEq age)) and
|
||||
(AltersklasseTable.maxAlter.isNull() or (AltersklasseTable.maxAlter greaterEq age))
|
||||
}
|
||||
|
||||
override suspend fun findByGeschlecht(geschlecht: Char, activeOnly: Boolean): List<AltersklasseDefinition> = DatabaseFactory.dbQuery {
|
||||
val query = AltersklasseTable.selectAll().where {
|
||||
(AltersklasseTable.geschlechtFilter eq geschlecht) or (AltersklasseTable.geschlechtFilter.isNull())
|
||||
}
|
||||
|
||||
if (activeOnly) {
|
||||
query.andWhere { AltersklasseTable.istAktiv eq true }
|
||||
}
|
||||
|
||||
query.orderBy(AltersklasseTable.bezeichnung to SortOrder.ASC)
|
||||
.map(::rowToAltersklasseDefinition)
|
||||
sparteFilter?.let { sparte ->
|
||||
query.andWhere {
|
||||
(AltersklasseTable.sparteFilter eq sparte.name) or (AltersklasseTable.sparteFilter.isNull())
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun findByAgeRange(minAge: Int?, maxAge: Int?, activeOnly: Boolean): List<AltersklasseDefinition> = DatabaseFactory.dbQuery {
|
||||
val query = AltersklasseTable.selectAll()
|
||||
|
||||
minAge?.let { min ->
|
||||
query.andWhere {
|
||||
(AltersklasseTable.maxAlter.isNull()) or (AltersklasseTable.maxAlter greaterEq min)
|
||||
}
|
||||
}
|
||||
|
||||
maxAge?.let { max ->
|
||||
query.andWhere {
|
||||
(AltersklasseTable.minAlter.isNull()) or (AltersklasseTable.minAlter lessEq max)
|
||||
}
|
||||
}
|
||||
|
||||
if (activeOnly) {
|
||||
query.andWhere { AltersklasseTable.istAktiv eq true }
|
||||
}
|
||||
|
||||
query.orderBy(AltersklasseTable.bezeichnung to SortOrder.ASC)
|
||||
.map(::rowToAltersklasseDefinition)
|
||||
geschlechtFilter?.let { geschlecht ->
|
||||
query.andWhere {
|
||||
(AltersklasseTable.geschlechtFilter eq geschlecht) or (AltersklasseTable.geschlechtFilter.isNull())
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun findByOetoRegelReferenz(oetoRegelReferenzId: Uuid): List<AltersklasseDefinition> = DatabaseFactory.dbQuery {
|
||||
AltersklasseTable.selectAll().where { AltersklasseTable.oetoRegelReferenzId eq oetoRegelReferenzId }
|
||||
.orderBy(AltersklasseTable.bezeichnung to SortOrder.ASC)
|
||||
.map(::rowToAltersklasseDefinition)
|
||||
query.orderBy(AltersklasseTable.bezeichnung to SortOrder.ASC)
|
||||
.map(::rowToAltersklasseDefinition)
|
||||
}
|
||||
|
||||
override suspend fun findBySparte(sparte: SparteE, activeOnly: Boolean): List<AltersklasseDefinition> =
|
||||
DatabaseFactory.dbQuery {
|
||||
val query = AltersklasseTable.selectAll().where {
|
||||
(AltersklasseTable.sparteFilter eq sparte.name) or (AltersklasseTable.sparteFilter.isNull())
|
||||
}
|
||||
|
||||
if (activeOnly) {
|
||||
query.andWhere { AltersklasseTable.istAktiv eq true }
|
||||
}
|
||||
|
||||
query.orderBy(AltersklasseTable.bezeichnung to SortOrder.ASC)
|
||||
.map(::rowToAltersklasseDefinition)
|
||||
}
|
||||
|
||||
override suspend fun save(altersklasse: AltersklasseDefinition): AltersklasseDefinition = DatabaseFactory.dbQuery {
|
||||
val now = Clock.System.now()
|
||||
val existingAltersklasse = AltersklasseTable.selectAll().where { AltersklasseTable.id eq altersklasse.altersklasseId }.singleOrNull()
|
||||
override suspend fun findByGeschlecht(geschlecht: Char, activeOnly: Boolean): List<AltersklasseDefinition> =
|
||||
DatabaseFactory.dbQuery {
|
||||
val query = AltersklasseTable.selectAll().where {
|
||||
(AltersklasseTable.geschlechtFilter eq geschlecht) or (AltersklasseTable.geschlechtFilter.isNull())
|
||||
}
|
||||
|
||||
if (existingAltersklasse == null) {
|
||||
// Insert a new age class
|
||||
AltersklasseTable.insert { stmt ->
|
||||
stmt[id] = altersklasse.altersklasseId
|
||||
stmt[altersklasseCode] = altersklasse.altersklasseCode
|
||||
stmt[bezeichnung] = altersklasse.bezeichnung
|
||||
stmt[minAlter] = altersklasse.minAlter
|
||||
stmt[maxAlter] = altersklasse.maxAlter
|
||||
stmt[stichtagRegelText] = altersklasse.stichtagRegelText
|
||||
stmt[sparteFilter] = altersklasse.sparteFilter?.name
|
||||
stmt[geschlechtFilter] = altersklasse.geschlechtFilter
|
||||
stmt[oetoRegelReferenzId] = altersklasse.oetoRegelReferenzId
|
||||
stmt[istAktiv] = altersklasse.istAktiv
|
||||
stmt[createdAt] = altersklasse.createdAt.toLocalDateTime(TimeZone.UTC)
|
||||
stmt[updatedAt] = now.toLocalDateTime(TimeZone.UTC)
|
||||
}
|
||||
} else {
|
||||
// Update existing age class
|
||||
AltersklasseTable.update({ AltersklasseTable.id eq altersklasse.altersklasseId }) { stmt ->
|
||||
stmt[altersklasseCode] = altersklasse.altersklasseCode
|
||||
stmt[bezeichnung] = altersklasse.bezeichnung
|
||||
stmt[minAlter] = altersklasse.minAlter
|
||||
stmt[maxAlter] = altersklasse.maxAlter
|
||||
stmt[stichtagRegelText] = altersklasse.stichtagRegelText
|
||||
stmt[sparteFilter] = altersklasse.sparteFilter?.name
|
||||
stmt[geschlechtFilter] = altersklasse.geschlechtFilter
|
||||
stmt[oetoRegelReferenzId] = altersklasse.oetoRegelReferenzId
|
||||
stmt[istAktiv] = altersklasse.istAktiv
|
||||
stmt[updatedAt] = now.toLocalDateTime(TimeZone.UTC)
|
||||
}
|
||||
}
|
||||
if (activeOnly) {
|
||||
query.andWhere { AltersklasseTable.istAktiv eq true }
|
||||
}
|
||||
|
||||
altersklasse.copy(updatedAt = now)
|
||||
query.orderBy(AltersklasseTable.bezeichnung to SortOrder.ASC)
|
||||
.map(::rowToAltersklasseDefinition)
|
||||
}
|
||||
|
||||
override suspend fun delete(id: Uuid): Boolean = DatabaseFactory.dbQuery {
|
||||
AltersklasseTable.deleteWhere { AltersklasseTable.id eq id } > 0
|
||||
override suspend fun findByAgeRange(minAge: Int?, maxAge: Int?, activeOnly: Boolean): List<AltersklasseDefinition> =
|
||||
DatabaseFactory.dbQuery {
|
||||
val query = AltersklasseTable.selectAll()
|
||||
|
||||
if (minAge != null) {
|
||||
query.andWhere { AltersklasseTable.minAlter greaterEq minAge }
|
||||
}
|
||||
if (maxAge != null) {
|
||||
query.andWhere { AltersklasseTable.maxAlter lessEq maxAge }
|
||||
}
|
||||
if (activeOnly) {
|
||||
query.andWhere { AltersklasseTable.istAktiv eq true }
|
||||
}
|
||||
|
||||
query.orderBy(AltersklasseTable.bezeichnung to SortOrder.ASC)
|
||||
.map(::rowToAltersklasseDefinition)
|
||||
}
|
||||
|
||||
override suspend fun existsByCode(altersklasseCode: String): Boolean = DatabaseFactory.dbQuery {
|
||||
AltersklasseTable.selectAll().where { AltersklasseTable.altersklasseCode eq altersklasseCode }
|
||||
.count() > 0
|
||||
override suspend fun findByOetoRegelReferenz(oetoRegelReferenzId: Uuid): List<AltersklasseDefinition> =
|
||||
DatabaseFactory.dbQuery {
|
||||
AltersklasseTable.selectAll().where { AltersklasseTable.oetoRegelReferenzId eq oetoRegelReferenzId }
|
||||
.map(::rowToAltersklasseDefinition)
|
||||
}
|
||||
|
||||
override suspend fun countActive(sparteFilter: SparteE?): Long = DatabaseFactory.dbQuery {
|
||||
val query = AltersklasseTable.selectAll().where { AltersklasseTable.istAktiv eq true }
|
||||
override suspend fun save(altersklasse: AltersklasseDefinition): AltersklasseDefinition = DatabaseFactory.dbQuery {
|
||||
val exists = AltersklasseTable.selectAll().where { AltersklasseTable.id eq altersklasse.altersklasseId }.any()
|
||||
|
||||
sparteFilter?.let { sparte ->
|
||||
query.andWhere {
|
||||
(AltersklasseTable.sparteFilter eq sparte.name) or (AltersklasseTable.sparteFilter.isNull())
|
||||
}
|
||||
}
|
||||
|
||||
query.count()
|
||||
if (exists) {
|
||||
AltersklasseTable.update({ AltersklasseTable.id eq altersklasse.altersklasseId }) {
|
||||
it[altersklasseCode] = altersklasse.altersklasseCode
|
||||
it[bezeichnung] = altersklasse.bezeichnung
|
||||
it[minAlter] = altersklasse.minAlter
|
||||
it[maxAlter] = altersklasse.maxAlter
|
||||
it[stichtagRegelText] = altersklasse.stichtagRegelText
|
||||
it[sparteFilter] = altersklasse.sparteFilter?.name
|
||||
it[geschlechtFilter] = altersklasse.geschlechtFilter
|
||||
it[oetoRegelReferenzId] = altersklasse.oetoRegelReferenzId
|
||||
it[istAktiv] = altersklasse.istAktiv
|
||||
it[updatedAt] = altersklasse.updatedAt
|
||||
}
|
||||
altersklasse
|
||||
} else {
|
||||
AltersklasseTable.insert {
|
||||
it[id] = altersklasse.altersklasseId
|
||||
it[altersklasseCode] = altersklasse.altersklasseCode
|
||||
it[bezeichnung] = altersklasse.bezeichnung
|
||||
it[minAlter] = altersklasse.minAlter
|
||||
it[maxAlter] = altersklasse.maxAlter
|
||||
it[stichtagRegelText] = altersklasse.stichtagRegelText
|
||||
it[sparteFilter] = altersklasse.sparteFilter?.name
|
||||
it[geschlechtFilter] = altersklasse.geschlechtFilter
|
||||
it[oetoRegelReferenzId] = altersklasse.oetoRegelReferenzId
|
||||
it[istAktiv] = altersklasse.istAktiv
|
||||
it[createdAt] = altersklasse.createdAt
|
||||
it[updatedAt] = altersklasse.updatedAt
|
||||
}
|
||||
altersklasse
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun isEligible(altersklasseId: Uuid, age: Int, geschlecht: Char): Boolean = DatabaseFactory.dbQuery {
|
||||
val altersklasse = AltersklasseTable.selectAll().where {
|
||||
(AltersklasseTable.id eq altersklasseId) and (AltersklasseTable.istAktiv eq true)
|
||||
}.singleOrNull()
|
||||
override suspend fun delete(id: Uuid): Boolean = DatabaseFactory.dbQuery {
|
||||
AltersklasseTable.deleteWhere { AltersklasseTable.id eq id } > 0
|
||||
}
|
||||
|
||||
if (altersklasse == null) return@dbQuery false
|
||||
override suspend fun existsByCode(altersklasseCode: String): Boolean = DatabaseFactory.dbQuery {
|
||||
AltersklasseTable.selectAll().where { AltersklasseTable.altersklasseCode eq altersklasseCode }.any()
|
||||
}
|
||||
|
||||
// Check age eligibility
|
||||
val minAlter = altersklasse[AltersklasseTable.minAlter]
|
||||
val maxAlter = altersklasse[AltersklasseTable.maxAlter]
|
||||
val ageEligible = (minAlter == null || age >= minAlter) && (maxAlter == null || age <= maxAlter)
|
||||
|
||||
// Check gender eligibility
|
||||
val geschlechtFilter = altersklasse[AltersklasseTable.geschlechtFilter]
|
||||
val genderEligible = geschlechtFilter == null || geschlechtFilter == geschlecht
|
||||
|
||||
ageEligible && genderEligible
|
||||
override suspend fun countActive(sparteFilter: SparteE?): Long = DatabaseFactory.dbQuery {
|
||||
val query = AltersklasseTable.selectAll().where { AltersklasseTable.istAktiv eq true }
|
||||
sparteFilter?.let { sparte ->
|
||||
query.andWhere { (AltersklasseTable.sparteFilter eq sparte.name) or (AltersklasseTable.sparteFilter.isNull()) }
|
||||
}
|
||||
query.count()
|
||||
}
|
||||
|
||||
override suspend fun isEligible(altersklasseId: Uuid, age: Int, geschlecht: Char): Boolean = DatabaseFactory.dbQuery {
|
||||
AltersklasseTable.selectAll().where { AltersklasseTable.id eq altersklasseId }
|
||||
.map {
|
||||
val min = it[AltersklasseTable.minAlter]
|
||||
val max = it[AltersklasseTable.maxAlter]
|
||||
val g = it[AltersklasseTable.geschlechtFilter]
|
||||
|
||||
val ageOk = (min == null || age >= min) && (max == null || age <= max)
|
||||
val geschlechtOk = (g == null || g == geschlecht)
|
||||
|
||||
ageOk && geschlechtOk
|
||||
}.singleOrNull() ?: false
|
||||
}
|
||||
}
|
||||
|
||||
+25
-24
@@ -1,37 +1,38 @@
|
||||
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
||||
|
||||
package at.mocode.masterdata.infrastructure.persistence
|
||||
|
||||
import org.jetbrains.exposed.v1.core.Table
|
||||
import org.jetbrains.exposed.v1.core.kotlin.datetime.datetime
|
||||
import org.jetbrains.exposed.v1.core.kotlin.datetime.CurrentDateTime
|
||||
import org.jetbrains.exposed.v1.core.javaUUID
|
||||
import org.jetbrains.exposed.v1.datetime.CurrentTimestamp
|
||||
import org.jetbrains.exposed.v1.datetime.timestamp
|
||||
|
||||
/**
|
||||
* 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
|
||||
* entsprechend der AltersklasseDefinition Domain-Entität.
|
||||
*/
|
||||
object AltersklasseTable : Table("altersklasse") {
|
||||
val id = javaUUID("id").autoGenerate()
|
||||
val altersklasseCode = varchar("altersklasse_code", 50).uniqueIndex()
|
||||
val bezeichnung = varchar("bezeichnung", 200)
|
||||
val minAlter = integer("min_alter").nullable()
|
||||
val maxAlter = integer("max_alter").nullable()
|
||||
val stichtagRegelText = varchar("stichtag_regel_text", 500).nullable()
|
||||
val sparteFilter = varchar("sparte_filter", 50).nullable() // Enum as string
|
||||
val geschlechtFilter = char("geschlecht_filter").nullable()
|
||||
val oetoRegelReferenzId = javaUUID("oeto_regel_referenz_id").nullable()
|
||||
val istAktiv = bool("ist_aktiv").default(true)
|
||||
val createdAt = datetime("created_at").defaultExpression(CurrentDateTime)
|
||||
val updatedAt = datetime("updated_at").defaultExpression(CurrentDateTime)
|
||||
val id = uuid("id")
|
||||
val altersklasseCode = varchar("altersklasse_code", 50).uniqueIndex()
|
||||
val bezeichnung = varchar("bezeichnung", 200)
|
||||
val minAlter = integer("min_alter").nullable()
|
||||
val maxAlter = integer("max_alter").nullable()
|
||||
val stichtagRegelText = varchar("stichtag_regel_text", 500).nullable()
|
||||
val sparteFilter = varchar("sparte_filter", 50).nullable() // Enum as string
|
||||
val geschlechtFilter = char("geschlecht_filter").nullable()
|
||||
val oetoRegelReferenzId = uuid("oeto_regel_referenz_id").nullable()
|
||||
val istAktiv = bool("ist_aktiv").default(true)
|
||||
val createdAt = timestamp("created_at").defaultExpression(CurrentTimestamp)
|
||||
val updatedAt = timestamp("updated_at").defaultExpression(CurrentTimestamp)
|
||||
|
||||
override val primaryKey = PrimaryKey(id)
|
||||
override val primaryKey = PrimaryKey(id)
|
||||
|
||||
init {
|
||||
// Index for performance on common queries
|
||||
index(customIndexName = "idx_altersklasse_aktiv", columns = arrayOf(istAktiv))
|
||||
index(customIndexName = "idx_altersklasse_sparte", columns = arrayOf(sparteFilter))
|
||||
index(customIndexName = "idx_altersklasse_geschlecht", columns = arrayOf(geschlechtFilter))
|
||||
index(customIndexName = "idx_altersklasse_alter", columns = arrayOf(minAlter, maxAlter))
|
||||
}
|
||||
init {
|
||||
// Index for performance on common queries
|
||||
index(customIndexName = "idx_altersklasse_aktiv", columns = arrayOf(istAktiv))
|
||||
index(customIndexName = "idx_altersklasse_sparte", columns = arrayOf(sparteFilter))
|
||||
index(customIndexName = "idx_altersklasse_geschlecht", columns = arrayOf(geschlechtFilter))
|
||||
index(customIndexName = "idx_altersklasse_alter", columns = arrayOf(minAlter, maxAlter))
|
||||
}
|
||||
}
|
||||
|
||||
+114
-136
@@ -1,158 +1,136 @@
|
||||
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
||||
|
||||
package at.mocode.masterdata.infrastructure.persistence
|
||||
|
||||
import at.mocode.core.utils.database.DatabaseFactory
|
||||
import at.mocode.masterdata.domain.model.BundeslandDefinition
|
||||
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 kotlinx.datetime.Clock
|
||||
import kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.toInstant
|
||||
import kotlinx.datetime.toLocalDateTime
|
||||
import org.jetbrains.exposed.sql.*
|
||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
||||
|
||||
/**
|
||||
* Implementierung des BundeslandRepository für die Datenbankzugriffe.
|
||||
*
|
||||
* Diese Implementierung verwendet Exposed SQL für den Datenbankzugriff
|
||||
* und mappt zwischen der BundeslandDefinition Domain-Entität und der BundeslandTable.
|
||||
*/
|
||||
class BundeslandRepositoryImpl : BundeslandRepository {
|
||||
|
||||
/**
|
||||
* Konvertiert eine Datenbankzeile in ein Domain-Objekt.
|
||||
*/
|
||||
private fun rowToBundeslandDefinition(row: ResultRow): BundeslandDefinition {
|
||||
return BundeslandDefinition(
|
||||
bundeslandId = row[BundeslandTable.id],
|
||||
landId = row[BundeslandTable.landId],
|
||||
oepsCode = row[BundeslandTable.oepsCode],
|
||||
iso3166_2_Code = row[BundeslandTable.iso3166_2_Code],
|
||||
name = row[BundeslandTable.name],
|
||||
kuerzel = row[BundeslandTable.kuerzel],
|
||||
wappenUrl = row[BundeslandTable.wappenUrl],
|
||||
istAktiv = row[BundeslandTable.istAktiv],
|
||||
sortierReihenfolge = row[BundeslandTable.sortierReihenfolge],
|
||||
createdAt = row[BundeslandTable.createdAt].toInstant(TimeZone.UTC),
|
||||
updatedAt = row[BundeslandTable.updatedAt].toInstant(TimeZone.UTC)
|
||||
)
|
||||
private fun rowToBundeslandDefinition(row: ResultRow): BundeslandDefinition {
|
||||
return BundeslandDefinition(
|
||||
bundeslandId = row[BundeslandTable.id],
|
||||
landId = row[BundeslandTable.landId],
|
||||
oepsCode = row[BundeslandTable.oepsCode],
|
||||
iso3166_2_Code = row[BundeslandTable.iso3166_2_Code],
|
||||
name = row[BundeslandTable.name],
|
||||
kuerzel = row[BundeslandTable.kuerzel],
|
||||
wappenUrl = row[BundeslandTable.wappenUrl],
|
||||
istAktiv = row[BundeslandTable.istAktiv],
|
||||
sortierReihenfolge = row[BundeslandTable.sortierReihenfolge],
|
||||
createdAt = row[BundeslandTable.createdAt],
|
||||
updatedAt = row[BundeslandTable.updatedAt]
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun findById(id: Uuid): BundeslandDefinition? = DatabaseFactory.dbQuery {
|
||||
BundeslandTable.selectAll().where { BundeslandTable.id eq id }
|
||||
.map(::rowToBundeslandDefinition)
|
||||
.singleOrNull()
|
||||
}
|
||||
|
||||
override suspend fun findByOepsCode(oepsCode: String, landId: Uuid): BundeslandDefinition? = DatabaseFactory.dbQuery {
|
||||
BundeslandTable.selectAll().where { (BundeslandTable.oepsCode eq oepsCode) and (BundeslandTable.landId eq landId) }
|
||||
.map(::rowToBundeslandDefinition)
|
||||
.singleOrNull()
|
||||
}
|
||||
|
||||
override suspend fun findByIso3166_2_Code(iso3166_2_Code: String): BundeslandDefinition? = DatabaseFactory.dbQuery {
|
||||
BundeslandTable.selectAll().where { BundeslandTable.iso3166_2_Code eq iso3166_2_Code }
|
||||
.map(::rowToBundeslandDefinition)
|
||||
.singleOrNull()
|
||||
}
|
||||
|
||||
override suspend fun findByCountry(
|
||||
landId: Uuid,
|
||||
activeOnly: Boolean,
|
||||
orderBySortierung: Boolean
|
||||
): List<BundeslandDefinition> = DatabaseFactory.dbQuery {
|
||||
val query = BundeslandTable.selectAll().where { BundeslandTable.landId eq landId }
|
||||
if (activeOnly) {
|
||||
query.andWhere { BundeslandTable.istAktiv eq true }
|
||||
}
|
||||
if (orderBySortierung) {
|
||||
query.orderBy(BundeslandTable.sortierReihenfolge to SortOrder.ASC, BundeslandTable.name to SortOrder.ASC)
|
||||
} else {
|
||||
query.orderBy(BundeslandTable.name to SortOrder.ASC)
|
||||
}
|
||||
query.map(::rowToBundeslandDefinition)
|
||||
}
|
||||
|
||||
override suspend fun findByName(searchTerm: String, landId: Uuid?, limit: Int): List<BundeslandDefinition> =
|
||||
DatabaseFactory.dbQuery {
|
||||
val pattern = "%$searchTerm%"
|
||||
val query = BundeslandTable.selectAll().where { BundeslandTable.name like pattern }
|
||||
landId?.let { query.andWhere { BundeslandTable.landId eq it } }
|
||||
query.limit(limit).map(::rowToBundeslandDefinition)
|
||||
}
|
||||
|
||||
override suspend fun findById(id: Uuid): BundeslandDefinition? = DatabaseFactory.dbQuery {
|
||||
BundeslandTable.selectAll().where { BundeslandTable.id eq id }
|
||||
.map(::rowToBundeslandDefinition)
|
||||
.singleOrNull()
|
||||
override suspend fun findAllActive(orderBySortierung: Boolean): List<BundeslandDefinition> = DatabaseFactory.dbQuery {
|
||||
val query = BundeslandTable.selectAll().where { BundeslandTable.istAktiv eq true }
|
||||
if (orderBySortierung) {
|
||||
query.orderBy(BundeslandTable.sortierReihenfolge to SortOrder.ASC, BundeslandTable.name to SortOrder.ASC)
|
||||
} else {
|
||||
query.orderBy(BundeslandTable.name to SortOrder.ASC)
|
||||
}
|
||||
query.map(::rowToBundeslandDefinition)
|
||||
}
|
||||
|
||||
override suspend fun findByOepsCode(oepsCode: String, landId: Uuid): BundeslandDefinition? = DatabaseFactory.dbQuery {
|
||||
BundeslandTable.selectAll().where {
|
||||
(BundeslandTable.oepsCode eq oepsCode) and (BundeslandTable.landId eq landId)
|
||||
}
|
||||
.map(::rowToBundeslandDefinition)
|
||||
.singleOrNull()
|
||||
override suspend fun save(bundesland: BundeslandDefinition): BundeslandDefinition = DatabaseFactory.dbQuery {
|
||||
val exists = BundeslandTable.selectAll().where { BundeslandTable.id eq bundesland.bundeslandId }.any()
|
||||
if (exists) {
|
||||
BundeslandTable.update({ BundeslandTable.id eq bundesland.bundeslandId }) {
|
||||
it[landId] = bundesland.landId
|
||||
it[oepsCode] = bundesland.oepsCode
|
||||
it[iso3166_2_Code] = bundesland.iso3166_2_Code
|
||||
it[name] = bundesland.name
|
||||
it[kuerzel] = bundesland.kuerzel
|
||||
it[wappenUrl] = bundesland.wappenUrl
|
||||
it[istAktiv] = bundesland.istAktiv
|
||||
it[sortierReihenfolge] = bundesland.sortierReihenfolge
|
||||
it[updatedAt] = bundesland.updatedAt
|
||||
}
|
||||
bundesland
|
||||
} else {
|
||||
BundeslandTable.insert {
|
||||
it[id] = bundesland.bundeslandId
|
||||
it[landId] = bundesland.landId
|
||||
it[oepsCode] = bundesland.oepsCode
|
||||
it[iso3166_2_Code] = bundesland.iso3166_2_Code
|
||||
it[name] = bundesland.name
|
||||
it[kuerzel] = bundesland.kuerzel
|
||||
it[wappenUrl] = bundesland.wappenUrl
|
||||
it[istAktiv] = bundesland.istAktiv
|
||||
it[sortierReihenfolge] = bundesland.sortierReihenfolge
|
||||
it[createdAt] = bundesland.createdAt
|
||||
it[updatedAt] = bundesland.updatedAt
|
||||
}
|
||||
bundesland
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun findByIso3166_2_Code(iso3166_2_Code: String): BundeslandDefinition? = DatabaseFactory.dbQuery {
|
||||
BundeslandTable.selectAll().where { BundeslandTable.iso3166_2_Code eq iso3166_2_Code }
|
||||
.map(::rowToBundeslandDefinition)
|
||||
.singleOrNull()
|
||||
}
|
||||
override suspend fun delete(id: Uuid): Boolean = DatabaseFactory.dbQuery {
|
||||
BundeslandTable.deleteWhere { BundeslandTable.id eq id } > 0
|
||||
}
|
||||
|
||||
override suspend fun findByCountry(landId: Uuid, activeOnly: Boolean, orderBySortierung: Boolean): List<BundeslandDefinition> = DatabaseFactory.dbQuery {
|
||||
val query = BundeslandTable.selectAll().where { BundeslandTable.landId eq landId }
|
||||
override suspend fun existsByOepsCode(oepsCode: String, landId: Uuid): Boolean = DatabaseFactory.dbQuery {
|
||||
BundeslandTable.selectAll().where { (BundeslandTable.oepsCode eq oepsCode) and (BundeslandTable.landId eq landId) }
|
||||
.any()
|
||||
}
|
||||
|
||||
if (activeOnly) {
|
||||
query.andWhere { BundeslandTable.istAktiv eq true }
|
||||
}
|
||||
override suspend fun existsByIso3166_2_Code(iso3166_2_Code: String): Boolean = DatabaseFactory.dbQuery {
|
||||
BundeslandTable.selectAll().where { BundeslandTable.iso3166_2_Code eq iso3166_2_Code }.any()
|
||||
}
|
||||
|
||||
if (orderBySortierung) {
|
||||
query.orderBy(BundeslandTable.sortierReihenfolge to SortOrder.ASC, BundeslandTable.name to SortOrder.ASC)
|
||||
} else {
|
||||
query.orderBy(BundeslandTable.name to SortOrder.ASC)
|
||||
}
|
||||
|
||||
query.map(::rowToBundeslandDefinition)
|
||||
}
|
||||
|
||||
override suspend fun findByName(searchTerm: String, landId: Uuid?, limit: Int): List<BundeslandDefinition> = DatabaseFactory.dbQuery {
|
||||
val pattern = "%$searchTerm%"
|
||||
val query = BundeslandTable.selectAll().where { BundeslandTable.name like pattern }
|
||||
|
||||
landId?.let {
|
||||
query.andWhere { BundeslandTable.landId eq it }
|
||||
}
|
||||
|
||||
query.limit(limit).map(::rowToBundeslandDefinition)
|
||||
}
|
||||
|
||||
override suspend fun findAllActive(orderBySortierung: Boolean): List<BundeslandDefinition> = DatabaseFactory.dbQuery {
|
||||
val query = BundeslandTable.selectAll().where { BundeslandTable.istAktiv eq true }
|
||||
|
||||
if (orderBySortierung) {
|
||||
query.orderBy(BundeslandTable.sortierReihenfolge to SortOrder.ASC, BundeslandTable.name to SortOrder.ASC)
|
||||
} else {
|
||||
query.orderBy(BundeslandTable.name to SortOrder.ASC)
|
||||
}
|
||||
|
||||
query.map(::rowToBundeslandDefinition)
|
||||
}
|
||||
|
||||
override suspend fun save(bundesland: BundeslandDefinition): BundeslandDefinition = DatabaseFactory.dbQuery {
|
||||
val now = Clock.System.now()
|
||||
val existingBundesland = BundeslandTable.selectAll().where { BundeslandTable.id eq bundesland.bundeslandId }.singleOrNull()
|
||||
|
||||
if (existingBundesland == null) {
|
||||
// Insert a new federal state
|
||||
BundeslandTable.insert { stmt ->
|
||||
stmt[id] = bundesland.bundeslandId
|
||||
stmt[landId] = bundesland.landId
|
||||
stmt[oepsCode] = bundesland.oepsCode
|
||||
stmt[iso3166_2_Code] = bundesland.iso3166_2_Code
|
||||
stmt[name] = bundesland.name
|
||||
stmt[kuerzel] = bundesland.kuerzel
|
||||
stmt[wappenUrl] = bundesland.wappenUrl
|
||||
stmt[istAktiv] = bundesland.istAktiv
|
||||
stmt[sortierReihenfolge] = bundesland.sortierReihenfolge
|
||||
stmt[createdAt] = bundesland.createdAt.toLocalDateTime(TimeZone.UTC)
|
||||
stmt[updatedAt] = now.toLocalDateTime(TimeZone.UTC)
|
||||
}
|
||||
} else {
|
||||
// Update existing federal state
|
||||
BundeslandTable.update({ BundeslandTable.id eq bundesland.bundeslandId }) { stmt ->
|
||||
stmt[landId] = bundesland.landId
|
||||
stmt[oepsCode] = bundesland.oepsCode
|
||||
stmt[iso3166_2_Code] = bundesland.iso3166_2_Code
|
||||
stmt[name] = bundesland.name
|
||||
stmt[kuerzel] = bundesland.kuerzel
|
||||
stmt[wappenUrl] = bundesland.wappenUrl
|
||||
stmt[istAktiv] = bundesland.istAktiv
|
||||
stmt[sortierReihenfolge] = bundesland.sortierReihenfolge
|
||||
stmt[updatedAt] = now.toLocalDateTime(TimeZone.UTC)
|
||||
}
|
||||
}
|
||||
|
||||
bundesland.copy(updatedAt = now)
|
||||
}
|
||||
|
||||
override suspend fun delete(id: Uuid): Boolean = DatabaseFactory.dbQuery {
|
||||
BundeslandTable.deleteWhere { BundeslandTable.id eq id } > 0
|
||||
}
|
||||
|
||||
override suspend fun existsByOepsCode(oepsCode: String, landId: Uuid): Boolean = DatabaseFactory.dbQuery {
|
||||
BundeslandTable.selectAll().where {
|
||||
(BundeslandTable.oepsCode eq oepsCode) and (BundeslandTable.landId eq landId)
|
||||
}.count() > 0
|
||||
}
|
||||
|
||||
override suspend fun existsByIso3166_2_Code(iso3166_2_Code: String): Boolean = DatabaseFactory.dbQuery {
|
||||
BundeslandTable.selectAll().where { BundeslandTable.iso3166_2_Code eq iso3166_2_Code }
|
||||
.count() > 0
|
||||
}
|
||||
|
||||
override suspend fun countActiveByCountry(landId: Uuid): Long = DatabaseFactory.dbQuery {
|
||||
BundeslandTable.selectAll().where {
|
||||
(BundeslandTable.landId eq landId) and (BundeslandTable.istAktiv eq true)
|
||||
}.count()
|
||||
}
|
||||
override suspend fun countActiveByCountry(landId: Uuid): Long = DatabaseFactory.dbQuery {
|
||||
BundeslandTable.selectAll().where { (BundeslandTable.landId eq landId) and (BundeslandTable.istAktiv eq true) }
|
||||
.count()
|
||||
}
|
||||
}
|
||||
|
||||
+21
-25
@@ -1,35 +1,31 @@
|
||||
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
||||
|
||||
package at.mocode.masterdata.infrastructure.persistence
|
||||
|
||||
import org.jetbrains.exposed.v1.core.Table
|
||||
import org.jetbrains.exposed.v1.core.kotlin.datetime.datetime
|
||||
import org.jetbrains.exposed.v1.core.kotlin.datetime.CurrentDateTime
|
||||
import org.jetbrains.exposed.v1.core.javaUUID
|
||||
import org.jetbrains.exposed.v1.datetime.CurrentTimestamp
|
||||
import org.jetbrains.exposed.v1.datetime.timestamp
|
||||
|
||||
/**
|
||||
* Exposed-Tabellendefinition für die Bundesland-Entität (Bundesländer/Regionen).
|
||||
*
|
||||
* Diese Tabelle speichert alle Informationen zu Bundesländern und subnationalen
|
||||
* Verwaltungseinheiten entsprechend der BundeslandDefinition Domain-Entität.
|
||||
* Exposed-Tabellendefinition für die Bundesland-Entität.
|
||||
*/
|
||||
object BundeslandTable : Table("bundesland") {
|
||||
val id = javaUUID("id").autoGenerate()
|
||||
val landId = javaUUID("land_id").references(LandTable.id)
|
||||
val oepsCode = varchar("oeps_code", 10).nullable()
|
||||
val iso3166_2_Code = varchar("iso_3166_2_code", 10).nullable()
|
||||
val name = varchar("name", 100)
|
||||
val kuerzel = varchar("kuerzel", 10).nullable()
|
||||
val wappenUrl = varchar("wappen_url", 500).nullable()
|
||||
val istAktiv = bool("ist_aktiv").default(true)
|
||||
val sortierReihenfolge = integer("sortier_reihenfolge").nullable()
|
||||
val createdAt = datetime("created_at").defaultExpression(CurrentDateTime)
|
||||
val updatedAt = datetime("updated_at").defaultExpression(CurrentDateTime)
|
||||
val id = uuid("bundesland_id")
|
||||
val landId = uuid("land_id")
|
||||
val oepsCode = varchar("oeps_code", 10).nullable()
|
||||
val iso3166_2_Code = varchar("iso_3166_2_code", 10).nullable()
|
||||
val name = varchar("name", 100)
|
||||
val kuerzel = varchar("kuerzel", 10).nullable()
|
||||
val wappenUrl = varchar("wappen_url", 255).nullable()
|
||||
val istAktiv = bool("ist_aktiv").default(true)
|
||||
val sortierReihenfolge = integer("sortier_reihenfolge").nullable()
|
||||
val createdAt = timestamp("created_at").defaultExpression(CurrentTimestamp)
|
||||
val updatedAt = timestamp("updated_at").defaultExpression(CurrentTimestamp)
|
||||
|
||||
override val primaryKey = PrimaryKey(id)
|
||||
override val primaryKey = PrimaryKey(id)
|
||||
|
||||
init {
|
||||
// Unique constraint for OEPS code per country
|
||||
uniqueIndex("uk_bundesland_oeps_land", oepsCode, landId)
|
||||
// Unique constraint for ISO 3166-2 code globally
|
||||
uniqueIndex("uk_bundesland_iso3166_2", iso3166_2_Code)
|
||||
}
|
||||
init {
|
||||
uniqueIndex("idx_bundesland_oeps", oepsCode, landId)
|
||||
uniqueIndex("idx_bundesland_iso", 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)
|
||||
}
|
||||
+286
@@ -0,0 +1,286 @@
|
||||
@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.PferdeGeschlechtE
|
||||
import at.mocode.core.utils.database.DatabaseFactory
|
||||
import at.mocode.masterdata.domain.model.DomPferd
|
||||
import at.mocode.masterdata.domain.repository.HorseRepository
|
||||
import org.jetbrains.exposed.v1.core.*
|
||||
import org.jetbrains.exposed.v1.jdbc.*
|
||||
import kotlin.uuid.Uuid
|
||||
|
||||
/**
|
||||
* Exposed-basierte Implementierung des Horse-Repositorys.
|
||||
*/
|
||||
class HorseRepositoryImpl : HorseRepository {
|
||||
|
||||
private fun rowToDomPferd(row: ResultRow): DomPferd {
|
||||
return DomPferd(
|
||||
pferdId = row[HorseTable.id],
|
||||
pferdeName = row[HorseTable.pferdeName],
|
||||
geschlecht = PferdeGeschlechtE.valueOf(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 = DatenQuelleE.valueOf(row[HorseTable.datenQuelle]),
|
||||
createdAt = row[HorseTable.createdAt],
|
||||
updatedAt = row[HorseTable.updatedAt]
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun findById(id: Uuid): DomPferd? = DatabaseFactory.dbQuery {
|
||||
HorseTable.selectAll().where { HorseTable.id eq id }
|
||||
.map(::rowToDomPferd)
|
||||
.singleOrNull()
|
||||
}
|
||||
|
||||
override suspend fun findByLebensnummer(lebensnummer: String): DomPferd? = DatabaseFactory.dbQuery {
|
||||
HorseTable.selectAll().where { HorseTable.lebensnummer eq lebensnummer }
|
||||
.map(::rowToDomPferd)
|
||||
.singleOrNull()
|
||||
}
|
||||
|
||||
override suspend fun findByChipNummer(chipNummer: String): DomPferd? = DatabaseFactory.dbQuery {
|
||||
HorseTable.selectAll().where { HorseTable.chipNummer eq chipNummer }
|
||||
.map(::rowToDomPferd)
|
||||
.singleOrNull()
|
||||
}
|
||||
|
||||
override suspend fun findByPassNummer(passNummer: String): DomPferd? = DatabaseFactory.dbQuery {
|
||||
HorseTable.selectAll().where { HorseTable.passNummer eq passNummer }
|
||||
.map(::rowToDomPferd)
|
||||
.singleOrNull()
|
||||
}
|
||||
|
||||
override suspend fun findByOepsNummer(oepsNummer: String): DomPferd? = DatabaseFactory.dbQuery {
|
||||
HorseTable.selectAll().where { HorseTable.oepsNummer eq oepsNummer }
|
||||
.map(::rowToDomPferd)
|
||||
.singleOrNull()
|
||||
}
|
||||
|
||||
override suspend fun findByFeiNummer(feiNummer: String): DomPferd? = DatabaseFactory.dbQuery {
|
||||
HorseTable.selectAll().where { HorseTable.feiNummer eq feiNummer }
|
||||
.map(::rowToDomPferd)
|
||||
.singleOrNull()
|
||||
}
|
||||
|
||||
override suspend fun findByName(searchTerm: String, limit: Int): List<DomPferd> = DatabaseFactory.dbQuery {
|
||||
val pattern = "%$searchTerm%"
|
||||
HorseTable.selectAll().where { HorseTable.pferdeName like pattern }
|
||||
.limit(limit)
|
||||
.map(::rowToDomPferd)
|
||||
}
|
||||
|
||||
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 }
|
||||
}
|
||||
query.map(::rowToDomPferd)
|
||||
}
|
||||
|
||||
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 }
|
||||
}
|
||||
query.map(::rowToDomPferd)
|
||||
}
|
||||
|
||||
override suspend fun findByGeschlecht(
|
||||
geschlecht: PferdeGeschlechtE,
|
||||
activeOnly: Boolean,
|
||||
limit: Int
|
||||
): List<DomPferd> = DatabaseFactory.dbQuery {
|
||||
val query = HorseTable.selectAll().where { HorseTable.geschlecht eq geschlecht.name }
|
||||
if (activeOnly) {
|
||||
query.andWhere { HorseTable.istAktiv eq true }
|
||||
}
|
||||
query.limit(limit).map(::rowToDomPferd)
|
||||
}
|
||||
|
||||
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 }
|
||||
}
|
||||
query.limit(limit).map(::rowToDomPferd)
|
||||
}
|
||||
|
||||
override suspend fun findByBirthYear(birthYear: Int, activeOnly: Boolean): List<DomPferd> = DatabaseFactory.dbQuery {
|
||||
// In Exposed v1 gibt es kein directes year() für date Spalten ohne extra Extension.
|
||||
// Wir suchen im Datumsbereich.
|
||||
val startDate = kotlinx.datetime.LocalDate(birthYear, 1, 1)
|
||||
val endDate = kotlinx.datetime.LocalDate(birthYear, 12, 31)
|
||||
val query = HorseTable.selectAll()
|
||||
.where { (HorseTable.geburtsdatum greaterEq startDate) and (HorseTable.geburtsdatum lessEq endDate) }
|
||||
if (activeOnly) {
|
||||
query.andWhere { HorseTable.istAktiv eq true }
|
||||
}
|
||||
query.map(::rowToDomPferd)
|
||||
}
|
||||
|
||||
override suspend fun findByBirthYearRange(fromYear: Int, toYear: Int, activeOnly: Boolean): List<DomPferd> =
|
||||
DatabaseFactory.dbQuery {
|
||||
val startDate = kotlinx.datetime.LocalDate(fromYear, 1, 1)
|
||||
val endDate = kotlinx.datetime.LocalDate(toYear, 12, 31)
|
||||
val query = HorseTable.selectAll()
|
||||
.where { (HorseTable.geburtsdatum greaterEq startDate) and (HorseTable.geburtsdatum lessEq endDate) }
|
||||
if (activeOnly) {
|
||||
query.andWhere { HorseTable.istAktiv eq true }
|
||||
}
|
||||
query.map(::rowToDomPferd)
|
||||
}
|
||||
|
||||
override suspend fun findAllActive(limit: Int): List<DomPferd> = DatabaseFactory.dbQuery {
|
||||
HorseTable.selectAll().where { HorseTable.istAktiv eq true }
|
||||
.limit(limit)
|
||||
.map(::rowToDomPferd)
|
||||
}
|
||||
|
||||
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 }
|
||||
}
|
||||
query.map(::rowToDomPferd)
|
||||
}
|
||||
|
||||
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 }
|
||||
}
|
||||
query.map(::rowToDomPferd)
|
||||
}
|
||||
|
||||
override suspend fun save(horse: DomPferd): DomPferd = DatabaseFactory.dbQuery {
|
||||
val exists = HorseTable.selectAll().where { HorseTable.id eq horse.pferdId }.any()
|
||||
if (exists) {
|
||||
HorseTable.update({ HorseTable.id eq horse.pferdId }) {
|
||||
it[pferdeName] = horse.pferdeName
|
||||
it[geschlecht] = horse.geschlecht.name
|
||||
it[geburtsdatum] = horse.geburtsdatum
|
||||
it[rasse] = horse.rasse
|
||||
it[farbe] = horse.farbe
|
||||
it[besitzerId] = horse.besitzerId
|
||||
it[verantwortlichePersonId] = horse.verantwortlichePersonId
|
||||
it[zuechterName] = horse.zuechterName
|
||||
it[zuchtbuchNummer] = horse.zuchtbuchNummer
|
||||
it[lebensnummer] = horse.lebensnummer
|
||||
it[chipNummer] = horse.chipNummer
|
||||
it[passNummer] = horse.passNummer
|
||||
it[oepsNummer] = horse.oepsNummer
|
||||
it[feiNummer] = horse.feiNummer
|
||||
it[vaterName] = horse.vaterName
|
||||
it[mutterName] = horse.mutterName
|
||||
it[mutterVaterName] = horse.mutterVaterName
|
||||
it[stockmass] = horse.stockmass
|
||||
it[istAktiv] = horse.istAktiv
|
||||
it[bemerkungen] = horse.bemerkungen
|
||||
it[datenQuelle] = horse.datenQuelle.name
|
||||
it[updatedAt] = horse.updatedAt
|
||||
}
|
||||
horse
|
||||
} else {
|
||||
HorseTable.insert {
|
||||
it[id] = horse.pferdId
|
||||
it[pferdeName] = horse.pferdeName
|
||||
it[geschlecht] = horse.geschlecht.name
|
||||
it[geburtsdatum] = horse.geburtsdatum
|
||||
it[rasse] = horse.rasse
|
||||
it[farbe] = horse.farbe
|
||||
it[besitzerId] = horse.besitzerId
|
||||
it[verantwortlichePersonId] = horse.verantwortlichePersonId
|
||||
it[zuechterName] = horse.zuechterName
|
||||
it[zuchtbuchNummer] = horse.zuchtbuchNummer
|
||||
it[lebensnummer] = horse.lebensnummer
|
||||
it[chipNummer] = horse.chipNummer
|
||||
it[passNummer] = horse.passNummer
|
||||
it[oepsNummer] = horse.oepsNummer
|
||||
it[feiNummer] = horse.feiNummer
|
||||
it[vaterName] = horse.vaterName
|
||||
it[mutterName] = horse.mutterName
|
||||
it[mutterVaterName] = horse.mutterVaterName
|
||||
it[stockmass] = horse.stockmass
|
||||
it[istAktiv] = horse.istAktiv
|
||||
it[bemerkungen] = horse.bemerkungen
|
||||
it[datenQuelle] = horse.datenQuelle.name
|
||||
it[createdAt] = horse.createdAt
|
||||
it[updatedAt] = horse.updatedAt
|
||||
}
|
||||
horse
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun delete(id: Uuid): Boolean = DatabaseFactory.dbQuery {
|
||||
HorseTable.deleteWhere { HorseTable.id eq id } > 0
|
||||
}
|
||||
|
||||
override suspend fun existsByLebensnummer(lebensnummer: String): Boolean = DatabaseFactory.dbQuery {
|
||||
HorseTable.selectAll().where { HorseTable.lebensnummer eq lebensnummer }.any()
|
||||
}
|
||||
|
||||
override suspend fun existsByChipNummer(chipNummer: String): Boolean = DatabaseFactory.dbQuery {
|
||||
HorseTable.selectAll().where { HorseTable.chipNummer eq chipNummer }.any()
|
||||
}
|
||||
|
||||
override suspend fun existsByPassNummer(passNummer: String): Boolean = DatabaseFactory.dbQuery {
|
||||
HorseTable.selectAll().where { HorseTable.passNummer eq passNummer }.any()
|
||||
}
|
||||
|
||||
override suspend fun existsByOepsNummer(oepsNummer: String): Boolean = DatabaseFactory.dbQuery {
|
||||
HorseTable.selectAll().where { HorseTable.oepsNummer eq oepsNummer }.any()
|
||||
}
|
||||
|
||||
override suspend fun existsByFeiNummer(feiNummer: String): Boolean = DatabaseFactory.dbQuery {
|
||||
HorseTable.selectAll().where { HorseTable.feiNummer eq feiNummer }.any()
|
||||
}
|
||||
|
||||
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 }
|
||||
}
|
||||
query.count()
|
||||
}
|
||||
|
||||
override suspend fun countOepsRegistered(activeOnly: Boolean): Long = DatabaseFactory.dbQuery {
|
||||
val query = HorseTable.selectAll().where { HorseTable.oepsNummer.isNotNull() }
|
||||
if (activeOnly) {
|
||||
query.andWhere { HorseTable.istAktiv eq true }
|
||||
}
|
||||
query.count()
|
||||
}
|
||||
|
||||
override suspend fun countFeiRegistered(activeOnly: Boolean): Long = DatabaseFactory.dbQuery {
|
||||
val query = HorseTable.selectAll().where { HorseTable.feiNummer.isNotNull() }
|
||||
if (activeOnly) {
|
||||
query.andWhere { HorseTable.istAktiv eq true }
|
||||
}
|
||||
query.count()
|
||||
}
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
@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 Pferd-Entität.
|
||||
*/
|
||||
object HorseTable : Table("horse") {
|
||||
val id = uuid("horse_id")
|
||||
val pferdeName = varchar("pferde_name", 200)
|
||||
val geschlecht = varchar("geschlecht", 20)
|
||||
val geburtsdatum = date("geburtsdatum").nullable()
|
||||
val rasse = varchar("rasse", 100).nullable()
|
||||
val farbe = varchar("farbe", 100).nullable()
|
||||
val besitzerId = uuid("besitzer_id").nullable()
|
||||
val verantwortlichePersonId = uuid("verantwortliche_person_id").nullable()
|
||||
val zuechterName = varchar("zuechter_name", 200).nullable()
|
||||
val zuchtbuchNummer = varchar("zuchtbuch_nummer", 50).nullable()
|
||||
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()
|
||||
val vaterName = varchar("vater_name", 200).nullable()
|
||||
val mutterName = varchar("mutter_name", 200).nullable()
|
||||
val mutterVaterName = varchar("mutter_vater_name", 200).nullable()
|
||||
val stockmass = integer("stockmass").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)
|
||||
|
||||
init {
|
||||
index("idx_horse_lebensnummer", isUnique = false, lebensnummer)
|
||||
index("idx_horse_name", isUnique = false, pferdeName)
|
||||
}
|
||||
}
|
||||
+58
-71
@@ -1,28 +1,25 @@
|
||||
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
||||
package at.mocode.masterdata.infrastructure.persistence
|
||||
|
||||
import at.mocode.core.utils.database.DatabaseFactory
|
||||
import at.mocode.masterdata.domain.model.LandDefinition
|
||||
import at.mocode.masterdata.domain.repository.LandRepository
|
||||
import at.mocode.core.utils.database.DatabaseFactory
|
||||
import org.jetbrains.exposed.v1.core.ResultRow
|
||||
import org.jetbrains.exposed.v1.core.SortOrder
|
||||
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 kotlin.uuid.Uuid
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.toInstant
|
||||
import kotlinx.datetime.toLocalDateTime
|
||||
import org.jetbrains.exposed.sql.*
|
||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
||||
|
||||
/**
|
||||
* Implementierung des LandRepository für die Datenbankzugriffe.
|
||||
*
|
||||
* Diese Implementierung verwendet Exposed SQL für den Datenbankzugriff
|
||||
* und mappt zwischen der LandDefinition Domain-Entität und der LandTable.
|
||||
*/
|
||||
class LandRepositoryImpl : LandRepository {
|
||||
|
||||
/**
|
||||
* Konvertiert eine Datenbankzeile in ein Domain-Objekt.
|
||||
*/
|
||||
private fun rowToLandDefinition(row: ResultRow): LandDefinition {
|
||||
return LandDefinition(
|
||||
landId = row[LandTable.id],
|
||||
@@ -36,8 +33,8 @@ class LandRepositoryImpl : LandRepository {
|
||||
istEwrMitglied = row[LandTable.istEwrMitglied],
|
||||
istAktiv = row[LandTable.istAktiv],
|
||||
sortierReihenfolge = row[LandTable.sortierReihenfolge],
|
||||
createdAt = row[LandTable.createdAt].toInstant(TimeZone.UTC),
|
||||
updatedAt = row[LandTable.updatedAt].toInstant(TimeZone.UTC)
|
||||
createdAt = row[LandTable.createdAt],
|
||||
updatedAt = row[LandTable.updatedAt]
|
||||
)
|
||||
}
|
||||
|
||||
@@ -48,90 +45,82 @@ class LandRepositoryImpl : LandRepository {
|
||||
}
|
||||
|
||||
override suspend fun findByIsoAlpha2Code(isoAlpha2Code: String): LandDefinition? = DatabaseFactory.dbQuery {
|
||||
LandTable.selectAll().where { LandTable.isoAlpha2Code eq isoAlpha2Code }
|
||||
LandTable.selectAll().where { LandTable.isoAlpha2Code eq isoAlpha2Code.uppercase() }
|
||||
.map(::rowToLandDefinition)
|
||||
.singleOrNull()
|
||||
}
|
||||
|
||||
override suspend fun findByIsoAlpha3Code(isoAlpha3Code: String): LandDefinition? = DatabaseFactory.dbQuery {
|
||||
LandTable.selectAll().where { LandTable.isoAlpha3Code eq isoAlpha3Code }
|
||||
LandTable.selectAll().where { LandTable.isoAlpha3Code eq isoAlpha3Code.uppercase() }
|
||||
.map(::rowToLandDefinition)
|
||||
.singleOrNull()
|
||||
}
|
||||
|
||||
override suspend fun findByName(searchTerm: String, limit: Int): List<LandDefinition> = DatabaseFactory.dbQuery {
|
||||
val pattern = "%$searchTerm%"
|
||||
LandTable.selectAll().where {
|
||||
(LandTable.nameDeutsch like pattern) or
|
||||
(LandTable.nameEnglisch like pattern)
|
||||
}
|
||||
.limit(limit)
|
||||
.map(::rowToLandDefinition)
|
||||
}
|
||||
|
||||
override suspend fun findAllActive(orderBySortierung: Boolean): List<LandDefinition> = DatabaseFactory.dbQuery {
|
||||
val query = LandTable.selectAll().where { LandTable.istAktiv eq true }
|
||||
|
||||
if (orderBySortierung) {
|
||||
query.orderBy(LandTable.sortierReihenfolge to SortOrder.ASC, LandTable.nameDeutsch to SortOrder.ASC)
|
||||
} else {
|
||||
query.orderBy(LandTable.nameDeutsch to SortOrder.ASC)
|
||||
}
|
||||
|
||||
query.map(::rowToLandDefinition)
|
||||
}
|
||||
|
||||
override suspend fun findByName(searchTerm: String, limit: Int): List<LandDefinition> = DatabaseFactory.dbQuery {
|
||||
val pattern = "%$searchTerm%"
|
||||
LandTable.selectAll()
|
||||
.where { (LandTable.nameDeutsch like pattern) or (LandTable.nameEnglisch like pattern) }
|
||||
.limit(limit)
|
||||
.map(::rowToLandDefinition)
|
||||
}
|
||||
|
||||
override suspend fun findEuMembers(): List<LandDefinition> = DatabaseFactory.dbQuery {
|
||||
LandTable.selectAll().where { (LandTable.istEuMitglied eq true) and (LandTable.istAktiv eq true) }
|
||||
.orderBy(LandTable.sortierReihenfolge to SortOrder.ASC, LandTable.nameDeutsch to SortOrder.ASC)
|
||||
LandTable.selectAll().where { LandTable.istEuMitglied eq true }
|
||||
.orderBy(LandTable.nameDeutsch to SortOrder.ASC)
|
||||
.map(::rowToLandDefinition)
|
||||
}
|
||||
|
||||
override suspend fun findEwrMembers(): List<LandDefinition> = DatabaseFactory.dbQuery {
|
||||
LandTable.selectAll().where { (LandTable.istEwrMitglied eq true) and (LandTable.istAktiv eq true) }
|
||||
.orderBy(LandTable.sortierReihenfolge to SortOrder.ASC, LandTable.nameDeutsch to SortOrder.ASC)
|
||||
LandTable.selectAll().where { LandTable.istEwrMitglied eq true }
|
||||
.orderBy(LandTable.nameDeutsch to SortOrder.ASC)
|
||||
.map(::rowToLandDefinition)
|
||||
}
|
||||
|
||||
override suspend fun save(land: LandDefinition): LandDefinition = DatabaseFactory.dbQuery {
|
||||
val now = Clock.System.now()
|
||||
val existingLand = LandTable.selectAll().where { LandTable.id eq land.landId }.singleOrNull()
|
||||
|
||||
if (existingLand == null) {
|
||||
// Insert a new country
|
||||
LandTable.insert { stmt ->
|
||||
stmt[id] = land.landId
|
||||
stmt[isoAlpha2Code] = land.isoAlpha2Code
|
||||
stmt[isoAlpha3Code] = land.isoAlpha3Code
|
||||
stmt[isoNumerischerCode] = land.isoNumerischerCode
|
||||
stmt[nameDeutsch] = land.nameDeutsch
|
||||
stmt[nameEnglisch] = land.nameEnglisch
|
||||
stmt[wappenUrl] = land.wappenUrl
|
||||
stmt[istEuMitglied] = land.istEuMitglied
|
||||
stmt[istEwrMitglied] = land.istEwrMitglied
|
||||
stmt[istAktiv] = land.istAktiv
|
||||
stmt[sortierReihenfolge] = land.sortierReihenfolge
|
||||
stmt[createdAt] = land.createdAt.toLocalDateTime(TimeZone.UTC)
|
||||
stmt[updatedAt] = now.toLocalDateTime(TimeZone.UTC)
|
||||
val exists = LandTable.selectAll().where { LandTable.id eq land.landId }.any()
|
||||
if (exists) {
|
||||
LandTable.update({ LandTable.id eq land.landId }) {
|
||||
it[isoAlpha2Code] = land.isoAlpha2Code.uppercase()
|
||||
it[isoAlpha3Code] = land.isoAlpha3Code.uppercase()
|
||||
it[isoNumerischerCode] = land.isoNumerischerCode
|
||||
it[nameDeutsch] = land.nameDeutsch
|
||||
it[nameEnglisch] = land.nameEnglisch
|
||||
it[wappenUrl] = land.wappenUrl
|
||||
it[istEuMitglied] = land.istEuMitglied
|
||||
it[istEwrMitglied] = land.istEwrMitglied
|
||||
it[istAktiv] = land.istAktiv
|
||||
it[sortierReihenfolge] = land.sortierReihenfolge
|
||||
it[updatedAt] = land.updatedAt
|
||||
}
|
||||
land
|
||||
} else {
|
||||
// Update existing country
|
||||
LandTable.update({ LandTable.id eq land.landId }) { stmt ->
|
||||
stmt[isoAlpha2Code] = land.isoAlpha2Code
|
||||
stmt[isoAlpha3Code] = land.isoAlpha3Code
|
||||
stmt[isoNumerischerCode] = land.isoNumerischerCode
|
||||
stmt[nameDeutsch] = land.nameDeutsch
|
||||
stmt[nameEnglisch] = land.nameEnglisch
|
||||
stmt[wappenUrl] = land.wappenUrl
|
||||
stmt[istEuMitglied] = land.istEuMitglied
|
||||
stmt[istEwrMitglied] = land.istEwrMitglied
|
||||
stmt[istAktiv] = land.istAktiv
|
||||
stmt[sortierReihenfolge] = land.sortierReihenfolge
|
||||
stmt[updatedAt] = now.toLocalDateTime(TimeZone.UTC)
|
||||
LandTable.insert {
|
||||
it[id] = land.landId
|
||||
it[isoAlpha2Code] = land.isoAlpha2Code.uppercase()
|
||||
it[isoAlpha3Code] = land.isoAlpha3Code.uppercase()
|
||||
it[isoNumerischerCode] = land.isoNumerischerCode
|
||||
it[nameDeutsch] = land.nameDeutsch
|
||||
it[nameEnglisch] = land.nameEnglisch
|
||||
it[wappenUrl] = land.wappenUrl
|
||||
it[istEuMitglied] = land.istEuMitglied
|
||||
it[istEwrMitglied] = land.istEwrMitglied
|
||||
it[istAktiv] = land.istAktiv
|
||||
it[sortierReihenfolge] = land.sortierReihenfolge
|
||||
it[createdAt] = land.createdAt
|
||||
it[updatedAt] = land.updatedAt
|
||||
}
|
||||
land
|
||||
}
|
||||
|
||||
land.copy(updatedAt = now)
|
||||
}
|
||||
|
||||
override suspend fun delete(id: Uuid): Boolean = DatabaseFactory.dbQuery {
|
||||
@@ -139,13 +128,11 @@ class LandRepositoryImpl : LandRepository {
|
||||
}
|
||||
|
||||
override suspend fun existsByIsoAlpha2Code(isoAlpha2Code: String): Boolean = DatabaseFactory.dbQuery {
|
||||
LandTable.selectAll().where { LandTable.isoAlpha2Code eq isoAlpha2Code }
|
||||
.count() > 0
|
||||
LandTable.selectAll().where { LandTable.isoAlpha2Code eq isoAlpha2Code.uppercase() }.any()
|
||||
}
|
||||
|
||||
override suspend fun existsByIsoAlpha3Code(isoAlpha3Code: String): Boolean = DatabaseFactory.dbQuery {
|
||||
LandTable.selectAll().where { LandTable.isoAlpha3Code eq isoAlpha3Code }
|
||||
.count() > 0
|
||||
LandTable.selectAll().where { LandTable.isoAlpha3Code eq isoAlpha3Code.uppercase() }.any()
|
||||
}
|
||||
|
||||
override suspend fun countActive(): Long = DatabaseFactory.dbQuery {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user