Compare commits

...

2 Commits

Author SHA1 Message Date
c53daa926a feat(horses-service): remove legacy configuration and repository classes
All checks were successful
Build and Publish Docker Images / build-and-push (., backend/infrastructure/gateway/Dockerfile, api-gateway, api-gateway) (push) Successful in 7m12s
Build and Publish Docker Images / build-and-push (., backend/services/ping/Dockerfile, ping-service, ping-service) (push) Successful in 6m42s
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Successful in 6m28s
Build and Publish Docker Images / build-and-push (., config/docker/keycloak/Dockerfile, keycloak, keycloak) (push) Successful in 1m51s
- Deleted outdated `ApplicationConfiguration` and `HorseRepositoryImpl` classes.
- Migrated to streamlined modular Gradle configuration in `build.gradle.kts`.
- Updated domain models and dependencies to use multiplatform and serialization plugins.
- Added modular setup for `horses` namespace in `settings.gradle.kts`.
- Reorganized database configuration with minimal JDBC bindings for development.

Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
2026-03-23 13:47:04 +01:00
7b35831d8c feat(docs): add detailed reference for CSN and CDN competitions under ÖTO rules
- Introduced comprehensive documentation on parameters for Springen (CSN) and Dressur (CDN).
- Covered class definitions, scoring systems, and special competition formats for both disciplines.
- Detailed guideline examples to assist organizers with tournament preparation.

Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
2026-03-23 12:40:29 +01:00
45 changed files with 75909 additions and 530 deletions

View File

@ -0,0 +1,26 @@
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)
}
}
}
}

View File

@ -0,0 +1,38 @@
@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()
)

View File

@ -0,0 +1,23 @@
plugins {
alias(libs.plugins.kotlinJvm)
alias(libs.plugins.kotlinSpring)
alias(libs.plugins.kotlinSerialization)
}
dependencies {
implementation(projects.platform.platformDependencies)
implementation(projects.clubs.clubsDomain)
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)
}

View File

@ -0,0 +1,14 @@
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")
}

View File

@ -0,0 +1,38 @@
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.clubs.clubsDomain)
implementation(projects.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.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()
}

View File

@ -0,0 +1,18 @@
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)
}

View File

@ -0,0 +1,31 @@
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.SchemaUtils
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
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 {
SchemaUtils.createMissingTablesAndColumns(ZnsClubTable)
log.info("Datenbank-Schema erfolgreich initialisiert")
}
}
}

View File

@ -0,0 +1,80 @@
package at.mocode.clubs.service.dev
import at.mocode.clubs.infrastructure.persistence.ZnsClubTable
import org.jetbrains.exposed.v1.jdbc.SchemaUtils
import org.jetbrains.exposed.v1.jdbc.batchInsert
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
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 {
SchemaUtils.createMissingTablesAndColumns(ZnsClubTable)
}
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)
}
}

View File

@ -1,5 +1,5 @@
plugins {
kotlin("jvm")
alias(libs.plugins.kotlinJvm)
}
dependencies {

View File

@ -1,9 +1,26 @@
plugins {
kotlin("jvm")
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.kotlinSerialization)
}
dependencies {
implementation(projects.core.coreDomain)
implementation(projects.core.coreUtils)
testImplementation(projects.platform.platformTesting)
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)
}
}
}
}

View File

@ -1,16 +1,16 @@
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
package at.mocode.horses.domain.model
import at.mocode.core.domain.model.PferdeGeschlechtE
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 kotlin.uuid.Uuid
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
import kotlinx.datetime.LocalDate
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.
@ -129,7 +129,7 @@ data class DomPferd(
*/
fun getAge(): Int? {
return geburtsdatum?.let { birthDate ->
val today = kotlinx.datetime.Clock.System.todayIn(kotlinx.datetime.TimeZone.currentSystemDefault())
val today = kotlin.time.Clock.System.todayIn(kotlinx.datetime.TimeZone.currentSystemDefault())
var age = today.year - birthDate.year
// Check if birthday has occurred this year
@ -167,6 +167,6 @@ data class DomPferd(
* Creates a copy of this horse with updated timestamp.
*/
fun withUpdatedTimestamp(): DomPferd {
return this.copy(updatedAt = Clock.System.now())
return this.copy(updatedAt = kotlin.time.Clock.System.now())
}
}

View File

@ -1,26 +1,26 @@
plugins {
// KORREKTUR: Alle Plugins werden jetzt konsistent über den Version Catalog geladen.
alias(libs.plugins.kotlin.jvm)
alias(libs.plugins.kotlin.spring)
// Das JPA-Plugin wird jetzt ebenfalls zentral verwaltet.
alias(libs.plugins.kotlin.jpa)
alias(libs.plugins.kotlinJvm)
alias(libs.plugins.kotlinSpring)
alias(libs.plugins.kotlinSerialization)
}
dependencies {
// Interne Module
implementation(projects.platform.platformDependencies)
implementation(projects.horses.horsesDomain)
implementation(projects.horses.horsesApplication)
// horses-common: ON HOLD
// implementation(projects.horses.horsesCommon)
implementation(projects.core.coreDomain)
implementation(projects.core.coreUtils)
implementation(projects.infrastructure.cache.cacheApi)
implementation(projects.infrastructure.eventStore.eventStoreApi)
implementation(projects.infrastructure.messaging.messagingClient)
// KORREKTUR: Alle externen Abhängigkeiten werden jetzt über den Version Catalog bezogen.
// Spring
implementation(libs.spring.boot.starter.web)
// Spring Data JPA
implementation(libs.spring.boot.starter.data.jpa)
// 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)

View File

@ -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 kotlinx.datetime.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
}
}

View File

@ -1,11 +1,11 @@
package at.mocode.horses.infrastructure.persistence
import at.mocode.core.domain.model.PferdeGeschlechtE
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.kotlin.datetime.date
import org.jetbrains.exposed.v1.core.kotlin.datetime.timestamp
import org.jetbrains.exposed.v1.core.javaUUID
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.
@ -13,7 +13,7 @@ import org.jetbrains.exposed.v1.core.javaUUID
* This table stores all horse information including identification,
* ownership, breeding data, and administrative information.
*/
object HorseTable : UUIDTable("horses") {
object HorseTable : UUIDTable("zns_horses") {
// Basic Information
val pferdeName = varchar("pferde_name", 255)
val geschlecht = enumerationByName<PferdeGeschlechtE>("geschlecht", 20)

View File

@ -1,12 +1,10 @@
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.kotlinJvm)
alias(libs.plugins.kotlinSpring)
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.service.HorsesServiceApplicationKt")
}
@ -17,16 +15,9 @@ dependencies {
implementation(projects.core.coreDomain)
implementation(projects.core.coreUtils)
implementation(projects.horses.horsesDomain)
implementation(projects.horses.horsesApplication)
// horses-common: ON HOLD veraltete API-Referenzen
// implementation(projects.horses.horsesCommon)
implementation(projects.horses.horsesInfrastructure)
implementation(projects.horses.horsesApi)
// Infrastruktur-Clients
implementation(projects.infrastructure.cache.redisCache)
implementation(projects.infrastructure.messaging.messagingClient)
implementation(projects.infrastructure.monitoring.monitoringClient)
// KORREKTUR: Alle externen Abhängigkeiten werden jetzt über den Version Catalog bezogen.
// Spring Boot Starters
implementation(libs.spring.boot.starter.web)
@ -42,11 +33,10 @@ dependencies {
runtimeOnly(libs.postgresql.driver)
testRuntimeOnly(libs.h2.driver)
// Testing
testImplementation(projects.platform.platformTesting)
testImplementation(libs.spring.boot.starter.test)
testImplementation(libs.logback.classic) // SLF4J provider for tests
testImplementation(libs.logback.classic)
}
tasks.test {

View File

@ -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)
}
}

View File

@ -1,106 +1,37 @@
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 org.springframework.boot.context.properties.ConfigurationProperties
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.SchemaUtils
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Value
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.sql.SchemaUtils
import org.jetbrains.exposed.sql.transactions.transaction
/**
* Database configuration for the Horses Service.
*
* This configuration ensures that Database.connect() is called properly
* before any Exposed operations are performed.
* Minimale Datenbank-Konfiguration für den Horses-Service (Dev-Profil).
* Verbindet sich direkt via JDBC und legt die Tabellen an.
*/
@Configuration
@Profile("!test")
class HorsesDatabaseConfiguration {
@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("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)
log.info("Initialisiere Datenbank-Schema für Horses-Service...")
Database.connect(jdbcUrl, user = username, password = password)
transaction {
SchemaUtils.createMissingTablesAndColumns(HorseTable, ZnsPersonTable, ZnsClubTable)
log.info("Datenbank-Schema erfolgreich initialisiert")
}
}
}

View File

@ -0,0 +1,26 @@
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)
}
}
}
}

View File

@ -0,0 +1,40 @@
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
package at.mocode.officials.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 an official (Richter) in the registry system.
*
* @property officialId Unique internal identifier (UUID).
* @property richterNummer ÖPS official number (from ZNS RICHT01.dat).
* @property name Full name of the official.
* @property qualifikation Qualification/class code (e.g. "GA", "G3").
* @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 DomOfficial(
@Serializable(with = UuidSerializer::class)
val officialId: Uuid = Uuid.random(),
val richterNummer: String,
val name: String,
val qualifikation: String? = null,
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()
)

View File

@ -0,0 +1,23 @@
plugins {
alias(libs.plugins.kotlinJvm)
alias(libs.plugins.kotlinSpring)
alias(libs.plugins.kotlinSerialization)
}
dependencies {
implementation(projects.platform.platformDependencies)
implementation(projects.officials.officialsDomain)
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)
}

View File

@ -0,0 +1,15 @@
package at.mocode.officials.infrastructure.persistence
import org.jetbrains.exposed.v1.core.dao.id.java.UUIDTable
import org.jetbrains.exposed.v1.datetime.timestamp
/**
* Tabelle für importierte Richter aus dem ZNS-Datenbestand (RICHT01.dat).
* Wird ausschließlich als Dev-Seed-Daten verwendet.
*/
object ZnsOfficialTable : UUIDTable("zns_officials") {
val richterNummer = varchar("richter_nummer", 6).uniqueIndex()
val name = varchar("name", 80)
val qualifikation = varchar("qualifikation", 5).nullable()
val createdAt = timestamp("created_at")
}

View File

@ -0,0 +1,38 @@
plugins {
alias(libs.plugins.kotlinJvm)
alias(libs.plugins.kotlinSpring)
alias(libs.plugins.spring.boot)
alias(libs.plugins.spring.dependencyManagement)
}
springBoot {
mainClass.set("at.mocode.officials.service.OfficialsServiceApplicationKt")
}
dependencies {
implementation(projects.platform.platformDependencies)
implementation(projects.core.coreDomain)
implementation(projects.core.coreUtils)
implementation(projects.officials.officialsDomain)
implementation(projects.officials.officialsInfrastructure)
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.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()
}

View File

@ -0,0 +1,18 @@
package at.mocode.officials.service
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.context.annotation.ComponentScan
@SpringBootApplication
@ComponentScan(
basePackages = [
"at.mocode.officials.service",
"at.mocode.officials.infrastructure"
]
)
class OfficialsServiceApplication
fun main(args: Array<String>) {
runApplication<OfficialsServiceApplication>(*args)
}

View File

@ -0,0 +1,31 @@
package at.mocode.officials.service.config
import at.mocode.officials.infrastructure.persistence.ZnsOfficialTable
import jakarta.annotation.PostConstruct
import org.jetbrains.exposed.v1.jdbc.Database
import org.jetbrains.exposed.v1.jdbc.SchemaUtils
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
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 OfficialsDatabaseConfiguration(
@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(OfficialsDatabaseConfiguration::class.java)
@PostConstruct
fun initializeDatabase() {
log.info("Initialisiere Datenbank-Schema für Officials-Service...")
Database.connect(jdbcUrl, user = username, password = password)
transaction {
SchemaUtils.createMissingTablesAndColumns(ZnsOfficialTable)
log.info("Datenbank-Schema erfolgreich initialisiert")
}
}
}

View File

@ -0,0 +1,90 @@
package at.mocode.officials.service.dev
import at.mocode.officials.infrastructure.persistence.ZnsOfficialTable
import org.jetbrains.exposed.v1.jdbc.SchemaUtils
import org.jetbrains.exposed.v1.jdbc.batchInsert
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
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 RICHT01.dat (ZNS) zns_officials.
* Aktivierung: Umgebungsvariable ZNS_DATA_DIR setzen + Profil "dev".
*/
@Component
@Profile("dev")
class ZnsOfficialSeeder(
@Value("\${zns.data.dir:#{null}}") private val znsDataDir: String?
) : CommandLineRunner {
private val log = LoggerFactory.getLogger(ZnsOfficialSeeder::class.java)
override fun run(vararg args: String) {
if (znsDataDir == null) {
log.info("ZNS_DATA_DIR nicht gesetzt ZnsOfficialSeeder 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-Richter-Import aus: {}", dir.absolutePath)
transaction {
SchemaUtils.createMissingTablesAndColumns(ZnsOfficialTable)
}
seedOfficials(dir)
log.info("ZNS-Richter-Import abgeschlossen.")
}
// -------------------------------------------------------------------------
// RICHT01.dat → zns_officials
// Format (~109 Zeichen):
// [0] 'Y' (Kennzeichen)
// [1-7] RichterNr (6 Zeichen)
// [7-87] Name (80 Zeichen)
// [87-89] Qualifikation (z.B. "GA", "G3")
// -------------------------------------------------------------------------
private fun seedOfficials(dir: File) {
val file = File(dir, "RICHT01.dat")
if (!file.exists()) {
log.warn("RICHT01.dat nicht gefunden Richter werden übersprungen.")
return
}
data class OfficialRow(
val richterNr: String,
val name: String,
val qualifikation: String?
)
val rows = file.readLines(Charsets.ISO_8859_1)
.filter { it.length >= 7 && it.startsWith("Y") }
.mapNotNull { line ->
val richterNr = line.substring(1, 7).trim()
if (richterNr.isBlank()) return@mapNotNull null
val name = line.safeSubstring(7, 87).trim()
val qualifikation = line.safeSubstring(87, 92).trim().takeIf { it.isNotBlank() }
OfficialRow(richterNr, name, qualifikation)
}
val now = Clock.System.now()
transaction {
ZnsOfficialTable.batchInsert(rows, ignore = true) { row ->
this[ZnsOfficialTable.richterNummer] = row.richterNr
this[ZnsOfficialTable.name] = row.name
this[ZnsOfficialTable.qualifikation] = row.qualifikation
this[ZnsOfficialTable.createdAt] = now
}
}
log.info("Richter importiert: {} Datensätze", rows.size)
}
private fun String.safeSubstring(start: Int, end: Int): String =
if (this.length >= end) this.substring(start, end) else if (this.length > start) this.substring(start) else ""
}

View File

@ -0,0 +1,26 @@
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)
}
}
}
}

View File

@ -0,0 +1,58 @@
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
package at.mocode.persons.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.datetime.LocalDate
import kotlinx.serialization.Serializable
import kotlin.time.Clock
import kotlin.time.Instant
import kotlin.uuid.Uuid
/**
* Domain model representing a person (rider/member) in the registry system.
*
* @property personId Unique internal identifier (UUID).
* @property lizenzNummer ÖPS license number (from ZNS LIZENZ01.dat).
* @property nachname Last name.
* @property vorname First name.
* @property geschlecht Gender code (M/W).
* @property geburtsdatum Date of birth.
* @property nation Nation code (e.g. AUT).
* @property lizenzKlasse License class (e.g. R1, R2, RD2).
* @property mitgliedsNummer Membership number.
* @property vereinsNummer Club number.
* @property vereinsName 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 DomPerson(
@Serializable(with = UuidSerializer::class)
val personId: Uuid = Uuid.random(),
val lizenzNummer: String,
val nachname: String,
val vorname: String,
val geschlecht: String? = null,
val geburtsdatum: LocalDate? = null,
val nation: String? = null,
val lizenzKlasse: String? = null,
val mitgliedsNummer: String? = null,
val vereinsNummer: String? = null,
val vereinsName: String? = null,
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()
) {
fun getDisplayName(): String = "$vorname $nachname"
}

View File

@ -0,0 +1,23 @@
plugins {
alias(libs.plugins.kotlinJvm)
alias(libs.plugins.kotlinSpring)
alias(libs.plugins.kotlinSerialization)
}
dependencies {
implementation(projects.platform.platformDependencies)
implementation(projects.persons.personsDomain)
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)
}

View File

@ -0,0 +1,23 @@
package at.mocode.persons.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")
}

View File

@ -0,0 +1,38 @@
plugins {
alias(libs.plugins.kotlinJvm)
alias(libs.plugins.kotlinSpring)
alias(libs.plugins.spring.boot)
alias(libs.plugins.spring.dependencyManagement)
}
springBoot {
mainClass.set("at.mocode.persons.service.PersonsServiceApplicationKt")
}
dependencies {
implementation(projects.platform.platformDependencies)
implementation(projects.core.coreDomain)
implementation(projects.core.coreUtils)
implementation(projects.persons.personsDomain)
implementation(projects.persons.personsInfrastructure)
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.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()
}

View File

@ -0,0 +1,18 @@
package at.mocode.persons.service
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.context.annotation.ComponentScan
@SpringBootApplication
@ComponentScan(
basePackages = [
"at.mocode.persons.service",
"at.mocode.persons.infrastructure"
]
)
class PersonsServiceApplication
fun main(args: Array<String>) {
runApplication<PersonsServiceApplication>(*args)
}

View File

@ -0,0 +1,31 @@
package at.mocode.persons.service.config
import at.mocode.persons.infrastructure.persistence.ZnsPersonTable
import jakarta.annotation.PostConstruct
import org.jetbrains.exposed.v1.jdbc.Database
import org.jetbrains.exposed.v1.jdbc.SchemaUtils
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
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 PersonsDatabaseConfiguration(
@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(PersonsDatabaseConfiguration::class.java)
@PostConstruct
fun initializeDatabase() {
log.info("Initialisiere Datenbank-Schema für Persons-Service...")
Database.connect(jdbcUrl, user = username, password = password)
transaction {
SchemaUtils.createMissingTablesAndColumns(ZnsPersonTable)
log.info("Datenbank-Schema erfolgreich initialisiert")
}
}
}

View File

@ -0,0 +1,136 @@
package at.mocode.persons.service.dev
import at.mocode.persons.infrastructure.persistence.ZnsPersonTable
import org.jetbrains.exposed.v1.jdbc.SchemaUtils
import org.jetbrains.exposed.v1.jdbc.batchInsert
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
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 LIZENZ01.dat (ZNS) zns_persons.
* Aktivierung: Umgebungsvariable ZNS_DATA_DIR setzen + Profil "dev".
*/
@Component
@Profile("dev")
class ZnsPersonSeeder(
@Value("\${zns.data.dir:#{null}}") private val znsDataDir: String?
) : CommandLineRunner {
private val log = LoggerFactory.getLogger(ZnsPersonSeeder::class.java)
override fun run(vararg args: String) {
if (znsDataDir == null) {
log.info("ZNS_DATA_DIR nicht gesetzt ZnsPersonSeeder 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-Personen-Import aus: {}", dir.absolutePath)
transaction {
SchemaUtils.createMissingTablesAndColumns(ZnsPersonTable)
}
seedPersons(dir)
log.info("ZNS-Personen-Import abgeschlossen.")
}
// -------------------------------------------------------------------------
// LIZENZ01.dat → zns_persons
// Format (220 Zeichen):
// [0-5] LizenzNr
// [6-55] Nachname
// [56-80] Vorname
// [81-83] VereinsNr
// [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)
}
private fun String.safeSubstring(start: Int, end: Int): String =
if (this.length >= end) this.substring(start, end) else if (this.length > start) this.substring(start) else ""
private fun parseDate(s: String): java.time.LocalDate? {
if (s.length < 8 || s.all { it == '0' }) 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
}
}
}

View File

@ -65,6 +65,17 @@ enum class VerifikationsStatusE {
KORREKTUR_ERFORDERLICH
}
/**
* Geschlecht eines Pferdes gemäß ÖTO/ZNS-Klassifikation.
*/
@Serializable
enum class PferdeGeschlechtE {
HENGST,
STUTE,
WALLACH,
UNBEKANNT
}
/**
* Processing states for workflows and tasks.
*/

View File

@ -0,0 +1,128 @@
# Detaillierte Bewerbs-Parameter: Springen und Dressur
Dieses Dokument beschreibt die genauen Parameter, die in der Turnier-Ausschreibung für die einzelnen Bewerbe der Sparten
Springen (CSN) und Dressur (CDN) definiert werden müssen, basierend auf der aktuellen ÖTO.
---
## 1. Verständnis eines Ausschreibungs-Beispiels
Ein typischer Bewerb im Ausschreibungs-Text sieht oft so aus:
> **6 Stilspringprüfung 80 cm J RV: § 204/4 CSN-C Neu**
> **1. Abt. lizenzfrei 2.Abt. R1 und Reiter mit 5 & 6 jährigen Pferden**
### Aufschlüsselung der Parameter:
* **`6`**: **Bewerbsnummer**. Eine fortlaufende Nummer zur eindeutigen Identifikation des Bewerbs (Prüfung Nr. 6 des
Turniers).
* **`Stilspringprüfung`**: **Art der Prüfung**. Es geht hier nicht rein nach Fehlern und Zeit, sondern der Reiter wird
von Richtern mit einer Wertnote für seinen Sitz, seine Einwirkung und den Rhythmus beurteilt.
* **`80 cm`**: **Klasse / Maximale Hindernishöhe**. In diesem Fall entspricht dies der Einsteigerklasse (E bzw. E0).
* **`J`**: **Startbuchstabe**. Nach diesem Buchstaben wird die Startreihenfolge alphabetisch gelost (oft nach dem Namen
des Pferdes). Ist der Buchstabe "J", startet das Pferd "Jolly Jumper" als erstes, danach "Karino" usw., bis am Ende
das Pferd "Ikarus" startet.
* **`RV: § 204/4`**: **Richtverfahren**. Der direkte Verweis auf den entsprechenden Paragraphen der ÖTO. Hier bedeutet
dies: Stilspringprüfung mit Wertnoten von 0,0 bis 10,0.
* **`CSN-C Neu`**: **Turnierkategorie**. Nationales Springturnier, Kategorie C-Neu. Das ist eine Einsteiger-Kategorie,
bei der auch Reiter ohne Lizenz (nur mit Reiterpass) antreten dürfen. Es gibt hier kein Preisgeld, nur Sachpreise und
Schleifen.
* **`1. Abt. lizenzfrei`**: **Abteilung 1 (Unterteilung)**. Diese Abteilung (erste Siegerehrung/Platzierung) ist
exklusiv für Reiter, die noch keine Turnierlizenz haben.
* **`2. Abt. R1 und Reiter mit 5 & 6 jährigen Pferden`**: **Abteilung 2**. Die zweite Abteilung wertet alle Reiter mit
der Einstiegslizenz (R1) sowie erfahrene Reiter, wenn sie junge Nachwuchspferde reiten. So wird Fairness garantiert.
---
## 2. Sparte Springen (CSN) im Detail
### 2.1 Die Klassen (Höhen für Großpferde)
Die Klassen geben die maximale Höhe der Hindernisse an:
* **Klasse E0 (Einsteiger):** 60 bis 90 cm
* **Klasse A (Leicht):** 105 bis 110 cm
* **Klasse L (Mittelleicht):** 115 bis 120 cm
* **Klasse LM (Leicht-Mittelschwer):** 125 bis 130 cm
* **Klasse M (Mittelschwer):** 135 cm
* **Klasse S (Schwer):** 140 bis 160 cm
*(Hinweis: Für Ponys sind die Höhen reduziert und die Abstände in Kombinationen verkürzt)*
### 2.2 Die Richtverfahren (RV) gemäß § 204 ÖTO
Das Richtverfahren definiert, wie Fehler und Zeiten gewertet werden.
#### Richtverfahren A (Standardspringprüfung)
Es gibt Strafpunkte (Fehler) für Abwürfe (4 Fehlerpunkte) und Verweigerungen/Ungehorsam (4 Fehlerpunkte beim ersten Mal,
beim zweiten Mal ab 115 cm Ausschluss). Zeitüberschreitungen über die "Erlaubte Zeit" geben Zeitfehler (0,25 Punkte pro
Sekunde).
* **A1:** Es gibt keine Zeitwertung. Alle fehlerfreien Ritte (gleiche Punktezahl) sind ex aequo auf Platz 1.
* **A2:** Fehler und Zeit. Bei Punktegleichheit gewinnt die schnellere Umlaufzeit.
* **A3 (Idealzeit):** Eine Idealzeit (meist Erlaubte Zeit minus 10%) wird definiert. Es gewinnt der Reiter, der am
nächsten an der Idealzeit ist (darunter oder darüber). Schützt vor "Bolzen" in Anfängerprüfungen.
* **AM3, AM4, AM5, AM6:** Standardspringen **mit Stechen**. Wer im Grundparcours fehlerfrei bleibt, reitet danach einen
verkürzten Stechparcours auf Zeit.
#### Richtverfahren C (Zeitspringen)
Fehlerpunkte gibt es nicht. Für jeden Abwurf werden stattdessen **Strafsekunden** (meist 4 Sekunden) zur gerittenen Zeit
addiert. Die schnellste Endzeit gewinnt.
### 2.3 Spezial-Richtverfahren / Prüfungsarten
* **Einlaufspringprüfung (§ 218):** Ein Trainingsbewerb (RV A1). Es gibt keine Platzierung. Jeder fehlerfreie Reiter
bekommt eine braune "Clear-Round"-Schleife.
* **Punktespringprüfung (§ 219):** Man sammelt Pluspunkte für fehlerfreie Sprünge. Am Ende steht oft ein schwieriger "
Joker-Sprung", der doppelte Punkte oder bei einem Fehler doppelten Abzug bringt.
* **2-Phasenspringprüfung (§ 220):** Der Parcours ist in zwei Teile geteilt. Wer Phase 1 fehlerfrei schafft, reitet ohne
anzuhalten sofort in Phase 2 (die meist auf Zeit geht). Wer in Phase 1 patzt, wird abgeglockt.
* **Stilspringprüfung:** Bewertung mit Wertnoten von 0 bis 10. Abzüge erfolgen für Ungehorsam (-0,5 oder -1,0) und
Hindernisfehler (-0,5).
---
## 3. Sparte Dressur (CDN) im Detail
### 3.1 Die Klassen und Aufgaben
Die Dressur wird nach vorgegebenen "Aufgaben" geritten (z.B. "Aufgabe A2", "FEI Grand Prix"), die die zu reitenden
Hufschlagfiguren exakt vorgeben.
* **Klasse A:** Grundlagen, Hufschlagfiguren, einfache Galoppwechsel über den Trab.
* **Klasse L:** Beginnende Versammlung, Kurzkehrt, Außengalopp.
* **Klasse LM:** Schulterherein, fliegende Wechsel können vorkommen. (Ab hier wahlweise Trense oder Kandare).
* **Klasse M:** Traversalen, fliegende Galoppwechsel. (Kandarenpflicht).
* **Klasse S:** Pirouetten, Serienwechsel (z.B. Wechsel alle 2 Sprünge), Piaffe, Passage.
### 3.2 Die Richtverfahren (RV) gemäß § 104 ÖTO
Das Richtverfahren definiert die Sitzverteilung und Art der Notengebung der Richter.
#### Richtverfahren A (Gemeinsames Richten)
Typisch für untere bis mittlere Klassen. Die Richtergruppe (die zusammen bei "C" sitzt) einigt sich auf **eine
gemeinsame Wertnote** zwischen 0,0 und 10,0 (z.B. 7,4).
* *Verreiten:* Wird mit einem Abzug von der Gesamtnote bestraft (1. Mal: -0,2 / 2. Mal: -0,4).
#### Richtverfahren B (Getrenntes Richten)
Typisch ab Klasse M und bei Meisterschaften. Mindestens drei Richter sitzen an verschiedenen Stellen (C, H, M) und
werten völlig unabhängig voneinander. Jede einzelne Lektion wird mit einer Note (0 bis 10) bewertet. Am Ende werden die
Punkte addiert und in einen Prozentwert umgerechnet (z.B. 68,542 %).
* *Verreiten:* Führt zu Abzügen bei der Gesamtpunktezahl bei jedem Richter (1. Mal: -2 Pkt. pro Richter / 2. Mal: -4
Pkt. pro Richter).
*Wichtig für alle Dressurprüfungen:* Ein **drittes Verreiten** führt unausweichlich zum Ausschluss (Abglocken) des
Teilnehmers.
### 3.3 Sonderformen
* **Musikkür:** Wird immer nach RV B gerichtet. Hierbei werden zwei getrennte Notensets vergeben: Eine Note für die *
*Technik** (Wurden alle geforderten Lektionen korrekt gezeigt?) und eine Note für die **Künstlerische Ausführung** (
Choreographie, Musikinterpretation, Schwierigkeitsgrad).
* **Dressurreiterprüfung / Dressurpferdeprüfung:** Ähnlich wie im Stilspringen zählt hier primär der Sitz und die
Einwirkung des Reiters (Reiterprüfung) bzw. das Potenzial und die Grundgangarten des jungen Pferdes (Pferdeprüfung).
Wird in der Regel nach RV A (Wertnoten) gerichtet.

View File

@ -0,0 +1,261 @@
---
type: Log
agent: Curator
date: 2026-03-23
status: COMPLETED
topics:
- ZNS-Importer
- Backend-Services
- Dev-Seeder
- Testdaten
---
# 🧹 Session Log: 23. März 2026 ZNS-Importer & Backend-Services
## Zusammenfassung
Diese Session hatte das Ziel, **echte Testdaten aus dem ZNS-System (Zentrales Nennsystem des OEPS)** für die
Frontend-Entwicklung bereitzustellen. Dazu wurden vier Backend-Services aufgebaut, die die ZNS-Rohdaten
(Fixbreiten-Flat-Files) in eine lokale PostgreSQL-Datenbank importieren.
---
## Kontext: Was ist ZNS?
Das **ZNS (Zentrales Nennsystem)** ist das Stammdaten-System des OEPS (Österreichischer Pferdesport-Verband).
Es liefert Rohdaten als **Fixbreiten-Flat-Files** (`.dat`) ein klassisches Format älterer Verbandssysteme.
Die Rohdaten liegen unter: `docs/OePS/ZNS/`
| Datei | Inhalt | Datensätze |
|----------------------|-----------------------------------|------------|
| `PFERDE01.dat` | Pferde-Stammdaten | ~tausende |
| `LIZENZ01.dat` | Reiter / Personen / Lizenzen | ~tausende |
| `VEREIN01.dat` | Vereine / Clubs | ~hunderte |
| `RICHT01.dat` | Richter / Offizielle | ~hunderte |
| `ISLANDPFERDE01.dat` | Islandpferde (separates Register) | |
| `VOLT01.dat` | Voltigier-Daten | |
---
## Was wurde gebaut?
### Strategische Entscheidung: Dev-Seeder, kein Produktions-Importer
Es wurde bewusst **kein produktiver Import-Service** gebaut, sondern ein **Dev-Seeder** (`@Profile("dev")`).
Das bedeutet:
- Der Seeder läuft **nur im `dev`-Profil** nie in Produktion
- Die Tabellen tragen das Präfix `zns_` klar erkennbar als Import-Rohdaten
- Der Seeder ist **wegwerfbar** der saubere Produktions-Importer folgt in Phase 3
### Vier neue Backend-Services
Alle Services folgen der gleichen Struktur wie der bestehende `horses`-Service:
```
backend/services/
├── horses/ ✅ Pferde (PFERDE01.dat → zns_horses)
├── persons/ ✅ Personen (LIZENZ01.dat → zns_persons)
├── clubs/ ✅ Vereine (VEREIN01.dat → zns_clubs)
└── officials/ ✅ Richter (RICHT01.dat → zns_officials)
```
Jeder Service besteht aus drei Modulen:
| Modul | Inhalt |
|--------------------|-------------------------------------------------------------------|
| `*-domain` | Domain-Modell (`DomPferd`, `DomPerson`, `DomClub`, `DomOfficial`) |
| `*-infrastructure` | Datenbank-Tabelle (`ZnsHorseTable`, `ZnsPersonTable`, etc.) |
| `*-service` | Spring Boot App + `DatabaseConfiguration` + `ZnsXxxSeeder` |
### Datenbank-Tabellen (ZNS-Präfix)
| Service | Tabelle | Quelle |
|---------------------|-----------------|----------------|
| `horses-service` | `zns_horses` | `PFERDE01.dat` |
| `persons-service` | `zns_persons` | `LIZENZ01.dat` |
| `clubs-service` | `zns_clubs` | `VEREIN01.dat` |
| `officials-service` | `zns_officials` | `RICHT01.dat` |
Das `zns_`-Präfix macht sofort klar: **Diese Daten kommen aus dem ZNS-Import** und sind noch nicht das
saubere Domain-Modell.
### Fixbreiten-Parser
Jeder Seeder enthält einen eigenen Fixbreiten-Parser für das jeweilige `.dat`-Format.
Beispiel `LIZENZ01.dat` (220 Zeichen pro Zeile):
```
[0-5] LizenzNr
[6-55] Nachname
[56-80] Vorname
[81-83] VereinsNr
[84-133] VereinsName
[134-136] Nation
[137-143] LizenzKlasse (R1, R2, RD2, ...)
[144-156] MitgliedsNr
[170] Geschlecht (M/W)
[171-178] Geburtsdatum (YYYYMMDD)
```
---
## Betriebsanleitung: So startest du die Services
### Voraussetzungen
1. **PostgreSQL läuft lokal** (via Docker Compose):
```bash
docker compose -f dc-infra.yaml up -d
```
Standard-Verbindung: `jdbc:postgresql://localhost:5432/meldestelle` (User/PW: `meldestelle`)
2. **ZNS-Dateien sind vorhanden** unter `docs/OePS/ZNS/`
```
docs/OePS/ZNS/
├── PFERDE01.dat
├── LIZENZ01.dat
├── VEREIN01.dat
└── RICHT01.dat
```
### Service starten & Daten importieren
Jeden Service **einmalig** mit dem `dev`-Profil starten. Der Seeder läuft automatisch beim Start.
```bash
# Pferde importieren (PFERDE01.dat → zns_horses)
ZNS_DATA_DIR=$(pwd)/docs/OePS/ZNS \
./gradlew :horses:horses-service:bootRun \
--args='--spring.profiles.active=dev'
# Personen/Reiter importieren (LIZENZ01.dat → zns_persons)
ZNS_DATA_DIR=$(pwd)/docs/OePS/ZNS \
./gradlew :persons:persons-service:bootRun \
--args='--spring.profiles.active=dev'
# Vereine importieren (VEREIN01.dat → zns_clubs)
ZNS_DATA_DIR=$(pwd)/docs/OePS/ZNS \
./gradlew :clubs:clubs-service:bootRun \
--args='--spring.profiles.active=dev'
# Richter importieren (RICHT01.dat → zns_officials)
ZNS_DATA_DIR=$(pwd)/docs/OePS/ZNS \
./gradlew :officials:officials-service:bootRun \
--args='--spring.profiles.active=dev'
```
> **Hinweis:** Die Services können nach dem Import wieder gestoppt werden (`Ctrl+C`).
> Die Daten bleiben in der PostgreSQL-Datenbank erhalten.
### Datenbank-Verbindung prüfen (optional)
```bash
# Direkt via psql
psql -h localhost -U meldestelle -d meldestelle -c "SELECT COUNT(*) FROM zns_horses;"
psql -h localhost -U meldestelle -d meldestelle -c "SELECT COUNT(*) FROM zns_persons;"
psql -h localhost -U meldestelle -d meldestelle -c "SELECT COUNT(*) FROM zns_clubs;"
psql -h localhost -U meldestelle -d meldestelle -c "SELECT COUNT(*) FROM zns_officials;"
```
### Datenbank zurücksetzen (neu seeden)
Falls die DB neu aufgesetzt werden muss (z.B. nach Schema-Änderungen):
```bash
# Tabellen droppen (in psql)
psql -h localhost -U meldestelle -d meldestelle -c "
DROP TABLE IF EXISTS zns_horses, zns_persons, zns_clubs, zns_officials;
"
# Dann Services neu starten (siehe oben) Tabellen werden automatisch neu angelegt
```
---
## Für das Frontend: Wie kommen die Daten an?
### Aktueller Stand (Dev-Phase)
Die Daten liegen in der **lokalen PostgreSQL-DB**. Das Frontend kann sie über die jeweiligen
Service-APIs abrufen sobald die REST-Endpoints implementiert sind.
> **Nächster Schritt für das Frontend-Team:**
> Die Services haben noch **keine REST-API** (kein `-api`-Modul aktiv).
> Für schnellen Datenzugriff kann das Frontend direkt die DB abfragen (via Backend-Gateway)
> oder die API-Module werden als nächstes aktiviert.
### Empfohlene Reihenfolge für die nächsten Schritte
| Priorität | Aufgabe | Service |
|-----------|--------------------------------|--------------------------------------|
| 🔴 Hoch | REST-API für Pferde-Abfrage | `horses-api` aktivieren |
| 🔴 Hoch | REST-API für Personen-Abfrage | `persons-api` bauen |
| 🟡 Mittel | REST-API für Vereine | `clubs-api` bauen |
| 🟡 Mittel | REST-API für Richter | `officials-api` bauen |
| 🟢 Später | Produktions-Importer (Phase 3) | `ZnsImportService` mit REST-Endpoint |
---
## Technische Details
### Gradle-Module (settings.gradle.kts)
```kotlin
// Alle vier Services sind registriert:
include(":horses:horses-domain")
include(":horses:horses-infrastructure")
include(":horses:horses-service")
include(":persons:persons-domain")
include(":persons:persons-infrastructure")
include(":persons:persons-service")
include(":clubs:clubs-domain")
include(":clubs:clubs-infrastructure")
include(":clubs:clubs-service")
include(":officials:officials-domain")
include(":officials:officials-infrastructure")
include(":officials:officials-service")
```
### Build-Verifikation
```bash
./gradlew \
:horses:horses-service:compileKotlin \
:horses:horses-infrastructure:compileKotlin \
:horses:horses-domain:compileKotlinJvm \
:persons:persons-domain:compileKotlinJvm \
:persons:persons-infrastructure:compileKotlin \
:persons:persons-service:compileKotlin \
:clubs:clubs-domain:compileKotlinJvm \
:clubs:clubs-infrastructure:compileKotlin \
:clubs:clubs-service:compileKotlin \
:officials:officials-domain:compileKotlinJvm \
:officials:officials-infrastructure:compileKotlin \
:officials:officials-service:compileKotlin
# → BUILD SUCCESSFUL ✅
```
### Bekannte Einschränkungen / ON HOLD
| Modul | Status | Grund |
|----------------------|---------|----------------------------------------|
| `horses-api` | ON HOLD | Ktor-basiert, wird separat aktiviert |
| `horses-common` | ON HOLD | Veraltete API-Referenzen |
| `entries-service` | ON HOLD | Pausiert bis Domain-Workshop (Phase 3) |
| Produktions-Importer | GEPLANT | Phase 3 nach Domain-Workshop |
---
## Erreichte Meilensteine dieser Session
- ✅ `ZnsDataSeeder` für `horses` gebaut (PFERDE01.dat → `zns_horses`)
- ✅ `ZnsPersonSeeder` für `persons` gebaut (LIZENZ01.dat → `zns_persons`)
- ✅ `ZnsClubSeeder` für `clubs` gebaut (VEREIN01.dat → `zns_clubs`)
- ✅ `ZnsOfficialSeeder` für `officials` gebaut (RICHT01.dat → `zns_officials`)
- ✅ Alle Tabellen einheitlich mit `zns_`-Präfix benannt
- ✅ Alle vier Services kompilieren erfolgreich (BUILD SUCCESSFUL)
- ✅ `settings.gradle.kts` vollständig aktualisiert

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

File diff suppressed because it is too large Load Diff

48753
docs/OePS/ZNS/LIZENZ01.dat Normal file

File diff suppressed because it is too large Load Diff

21206
docs/OePS/ZNS/PFERDE01.dat Normal file

File diff suppressed because it is too large Load Diff

382
docs/OePS/ZNS/RICHT01.dat Normal file
View File

@ -0,0 +1,382 @@
Y135894Helmreich Marilena GA
Y001832Paar Karl G3
Y019911Wakonig Manfred GA
Y200085Neunteufel Bernhard GA
Y002112Resch Friedrich G3
Y215034Stumpauer David GA
Y002280Schimanek Peter K. GA
Y002600St”glehner Karl GA
Y002703Trausner Ulfried G1
Y034065Weidinger Karl GA
Y045697Riedl Andreas G3,GGB,IGCK
Y058545Pulsinger Roland G2
Y059962Gaber Franz GA
Y065772Weberhofer Gernot G3,GGB
Y006689Scheiblhofer Ernst GA
Y702876Voglreiter Josef G2
Y803485Auer Magdalena GA
Y079182J„ger Martin GA
Y009624Kuttelwascher Hubert GPC,PL3
Y009386Giuli-Besso Eran P3
Y000098Bauer Anton Martin P1
Y801252Fuchs Hannes P1,PL1
Y802543Mayer Mirjam PA
Y803329Mayer Laura PA
Y081099™ttl Silvia PA
Y806356Schw„rzler Philipp P1
Y072509Berger Hans-Peter PA
Y000744Gschlenk Gregor GPC,P3,PL1
Y069350Peikert Nina PA
Y072094Bamberger-Stoiber Andreas GPC,P3,PL3
Y067760Prilepeck Catharina P1
Y061935Farthofer Isabella P1
Y614933Riedler Verena Victoria PA
Y061226Englbrecht Peter PA
Y603891Birklbauer Gerald PA
Y609325Jahn Bianca P1
Y061003Kurt Yurdaer PA
Y005923Feichtinger Franz PA
Y053720Baischer Andreas P3
Y056907Skiba-Hofinger Tobias PA
Y053343Walter Helga P1
Y052040K”rner Gert PA
Y053206Englbrecht Roland PA
Y051795Bernard Clemens PA
Y004483Lindl Hannes P3
Y420763Roámann Alexandra PA
Y426920Mesaric Izidor PA
Y428602Friedrich Leonhard PA
Y424605Seehofer Sonja P2,PL1
Y041645Schellenbauer Walter P3
Y406513Imrek Lina P2,PL1
Y042250Gr<EFBFBD>nling Josef P1
Y423196Schemeth David PA
Y423775Paier Lisa P1
Y406364Mandl Robert P1
Y000041Aschenbrenner Franz PA
Y035245Steurer Klaus PA
Y401287Thaller Carmen PA
Y401482H<EFBFBD>bler David P3,PL1
Y040279Schiermayr Franz GPC,P3,PL3
Y000034Appe Adi GPC,PL3
Y035473Madl Franz PL3
Y037624Tschaitschmann Mario P1
Y032936Simanek Andreas P3,PL1
Y000033Andresek Thomas PL2
Y003010Ziller Rupert P3
Y031099Wohlmuther Hans Peter PA
Y029642L”ffler Roland PA
Y028034Oberwasserlechner Petra P2
Y027084Krenn Armin P3,PL2
Y002713Tschaitschmann Udo P3
Y027110Sironi Tatjana PA
Y024803Steinbrecher Michael P2
Y024828Wallishauser Marcus PA
Y002385Schorn Peter P2
Y227595Stachl Daniel PA
Y226220Roubal Nicole PA
Y226992Klas Simone P1
Y222808Lazar Philipp PA
Y002211Salusek Andreas Christian P3,PL2
Y021383Domaingo Ulrich P1
Y002129Riedler Helmut P3
Y021771Reitetschl„ger Kurt PL1
Y211374Zak Klemens PA
Y209067H”rler Nicole PA
Y204075Hruschka Barbara P1
Y200171Wippl Gerhard PA
Y200606Sonntag Miriam P1
Y001939Platzer Hans J”rg P3,PL1
Y019467Sommerhuber Dieter P1
Y166085Eidmann Sonja PA
Y001034Bartoschek Ewald PA
Y017141Lemmerer Manfred P3
Y150399Novotny Noah PA
Y001337Kr<EFBFBD>gl Erich P1
Y013938Knoll Christian PA
Y143167Imrek Christopher P1
Y001065Kainz Sascha PA
Y010902Rust Manfred PA
Y011716Zainlinger Karl P3,PL1
Y106622Schranz Christian P3
Y101161Brandst„tter Markus P3,PL2
X010128Zitterbart Rainer PI-A
X100020Gaiotti Elena W
X100101Salomon Rebekka W
X010068Barosch Peter GAR-VO,VO
X100704Vietor Ilja DIR,DPF,SPF,SS
X001029Jarc Andreas W
X001030Jarc Hilde GAR-DI,GAR-WE,IDIR,W,WIR
X001023Meusburger Sabine DPF,DSGP,SPF,SS
X001032Jauck Robert SPF,SS*
X001061Kager Franz DPF,DSGP,GAR-SP,GAR-VS,SPF
X001061Kager Franz SS*,VS
X001196Koffmahn Elisabeth DPF,DSGP
X012045Krippl Rudolf M
X001067Kainz Gerhard F
X011787Haiden Claus IPI
X001112Keiblinger Brigitta DPF,DSGP,SPF,SS,VS,VSILEV1
X011257Kotzab-Wallentin Elisabeth DL,DPF
X113963Lechner Rachel MG-N
X001149Kissmann Claudia DL,DPF,GAR-SP,SPF,SS*
X001330Kroneder Aimee DL,DPF
X132300Lindqvist Anna DIR
X001307Kreupl Rudolf SPF,SS*
X130813H”pfner Andreas PI-A
X128482Sevensma Susan DM
X128897Montgomery Patrick MG-N
X122491Borstnar Nina PI-B
X001227Komarek Reiner DPF,DSGP
X012325Hannak Manuela W
X001255Bachinger Eva Maria DPF,DSGP
X148575De Wolff van Westerrode Eduard DIOR
X148587Hansaghy Peter DIR
X149744Trudenberger Ernst TREC
X014346Schubert Renate DM,DPF,GAR-SP,SPF,SS*
X139551Fore Liselotte DIOR
X139552Mc Mullen Elizabeth DIOR
X139553Hoevenaars Susan DIOR
X014122Rathausky Elfriede DM,SS*
X001416Lechner-Gebhard Jeannette DPF,DSGP
X001428Leitenberger Hans GAR-SP,SPF,SS*
X013938Knoll Christian SILEV1,SPF,SS
X134784Prasser Ulrike DL,DPF,SL-K
X001390Lang Thomas DILEV4,DPF,DSGP
X013372Hess Ingrid DPF,DSGP,SPF,SS
X013605M<EFBFBD>llner Elisabeth VO
X000138Berger Anita SL
X015274Moser Georg F
X015404Thomanek Astrid DPF,DS,SPF,SS,VL
X001541Mandl Victoire DPF,DSGP,M,SL,SPF
X015831Semlitsch Gerhard SL-K
X015865G”dl Anita IDIR
X015139Haberl Gabriela SL,SPF
X014528Klein Monika SILEV3,SPF
X001598Mayr Ernst M
X015995Winter Martina DL,DPF,SS
X015999Schmalhardt B„rbel DL,DPF,SPF,SS
X001724M<EFBFBD>ller Wolf G. DM,DPF,GAR-VS,SPF,SS,VSILEV3
X000017Akinbiyi Akinkunmi DL,SL
X017040Mayer-Rabl Ursula DM,DPF,M
X001720M<EFBFBD>ller Lieselotte SILEV3,SPF,SS
X172283Sanders van Gansewinkel Mariette DIOR
X016178Kribernegg-Heuáerer Anita DL,SPF,SS
X016047Payer Birgit DPF,DS
X016613Hoyos Piet IPI
X016661Auer Josef IPI
X016715Sgustav Alexander IPI
X169292Kjartansd¢ttir Hr”nn PI-A
X017464Brandst„tter Marinda DL
X015669Moáhammer Franz F
X018457Alleithner Margit DIST
X001847Kleindienst-Passweg Susanna DPF,DSGP,M,SPF,SS
X001949Pointl Albert F,FV
X019594Gasperl Regina TREC
X019802Auinger Hans G<>nther PI-A
X188835Christensen Kurt DIOR
X188836McClain Kari DIOR
X019911Wakonig Manfred TREC
X020180Eichinger-Kniely Katrin Maria DM,GAR-VS,SS,VSILEV3
X202084Zauner Andrea W
X202872Dell Cornelia IVLEV3,VO
X002008Prochazka Karin Verena SL,SPF
X201125Dobretsberger Andrea F
X201412Steindl Wolfgang F
X201557Jungwirth Markus DILEV2,DPF,DSGP
X020168Erjawetz Konstantin DM
X020169Erjawetz Hans DM,DPF
X203375Artner Denise VO
X020377Cekoni-Hutter Beate TREC
X203782Zimmermann Florian E. DS
X002003Knasm<EFBFBD>ller-Prinz Ulrike DL,SL
X002061Adelsberger-Streimelweger Renate DL,DPF,SL,SPF
X206206Luschin Nikolaus VO
X002085Regger Harald DPF,DSGP,SPF,SS
X204100Strohmaier Alexandra DM
X204715Lixl Ariane W
X204872Kirchl Karoline W
X205513Oskarsson Trausti IPI
X209350Weber Viktoria PI-A
X021023Schwendimann Claudia TREC
X021040Tentschert Brigitte GAR-TR,TREC
X220050Csandl Stefan IVLEV2,VO
X217076Urbitsch Philipp W
X021765Vormair Anita DM,DPF,SL,SPF
X002129Riedler Helmut SL,SPF
X021593Gampe-Benedict Ingrid TREC
X220161Bejdl Ines GAR-SP,SILEV3,SPF
X224333Huber-Tentschert Erich TREC
X022491Vorraber Franz W
X000234Breza Heinz DPF,DSGP
X023405Hazrati Elfriede DPF,DSGP,SL,SPF
X002273Schiele Barbara DPF,DSGP
X227620Wagenlechner Janine VO
X002385Schorn Peter DM,GAR-SP,SILEV3,SPF,VS
X023862Auinger Monika DM
X213643Chmelik-Wimmer Hannah PI-A
X000240Brosig Renate DL,GAR-SP,SILEV1,SPF,SS*
X002406Schwab Alice DILEV4,DPF,DSGP
X002438Seisenbacher Gerhard DPF,DSGP,SPF,SS
X002558Steinacher Alfred FIRK,GAR-FA
X002567Steiner Christian DPF,DSGP,GAR-VS,SPF,SS,VSILEV3
X024973Schulz Dennis WIRK
X025751Kroneis Sonja W
X002628Stuckel Marita DM,DPF,GAR-SP,SPF,SS*
X025912Galler Marion SL,SPF
X002670Thaler Claudia DPF,DSGP,VL
X026712Grinschgl Harald GAR-DI,IDIR
X002675Max-Theurer Elisabeth DILEV4,DPF
X002681Thomasser Walter DM,DPF,SPF,SS,VL
X002684Thunhart Hubertus F
X027471Hengge Barbara W
X002759Vlach Edith TREC
X027110Sironi Tatjana DL,SPF,SS
X000274Graf Deborah DPF,DS
X002873Werni Walter M
X028745Gaube Hannes GAR-WE,W,WIRK
X028905Kriechbaumer Friedrich TREC
X002901Zoher Petra DM,DPF,SPF,SS,WE-N
X028551Zobl-Wessely Petra SL,SPF
X000286Croy Ferdinand DM,DPF,GAR-VS,M,SPF,SS,VS
X028177Migl Sandra WE-IR,WE-NR
X029315Istinger Thomas SPF,SS
X002946Wolf Heinz-Dieter DM,DPF,SPF,SS
X029531Knotter Doris IVLEV4
X031133Attorf Daniela DM,DPF,SL,SPF
X312319Szedenik Wolfgang F
X031534Pirhofer Rudolf F
X003020Zobl-Wessely Peter DL,SILEV1,SPF,SS*
X030809Kreiner Eva Maria GAR-VO,VO
X029985Ferschl Alexandra DL
X003007Ziemianski Margerita SPF,SS
X032561Holler Esther PI-A
X033190Schilling Peter SL
X031875Galbavy Elisa DL
X000032Andrejs Elisabeth DL,IVLEV3,SL
X037884Wachs Michaela DM,DPF,SPF,SS,VL
X036139Pramendorfer Monika F
X036219Reichl-Rys Katharina DL
X036330Zuschrader Michael TREC
X037441Rappold Verena PI-B
X037445Sager Gerrit PI-A
X034935Petschina Rupert IPI
X035110Schuster Alexandra DM,DPF
X035245Steurer Klaus DPF,DSGP,GAR-SP,SPF,SS*
X033908Brandner Tamara DS,SL
X040322Fock Georg Klaus WIRK
X003996Kronbichler Josef F
X039986Reischauer Christian GAR-PI,IPI
X038289Schmitz Vera DPF,DS,SPF,SS
X038477Stangel-Sapergia Sandra W
X038705Entner Daniela DIST,DM,DPF
X038710Kriechbaumer Margarete TREC
X038714Kriechbaumer Richard III TREC
X038850Standeker Elke DPF,DS,M
X039181Hoyos Thordis PI-C
X039590Krausgruber Sylvia DS
X039648Greisberger Veronika VO-K
X038051Niederbichler Wilfried TREC
X041308Rebel Manfred GAR-VO,VO
X041490Cichini Gert DM,DPF,SL,SPF
X004064Waldbauer-Schall Elisabeth DPF,DSGP
X040513Grafenberger Daniela F-K
X040578Wessely-Trupp Anja Luise DL
X042404Stoiser Alois SL-K
X424346Fries Gabriele DL,GAR-SP,SS*
X424393Sagmeister Valentina W
X423207Kehrer Gottfried TREC
X423228Frank Mario TREC
X406833Burg Ernst TREC
X407534Hoffelner Daniela DM,DPF
X040964Spadinger Frank GAR-VO,IVLEV4
X420063K”berl Iris TREC
X420286Audunsson Valdimar IPI
X043474Vorraber Johanna W
X004362Alleithner Peter DIST
X004396Gruber Waltraud DPF,DSGP,GAR-SP,GAR-VS,M
X004396Gruber Waltraud SPF,SS,VS
X004407Hoyos Johannes GAR-PI,IPI
X425469Egger Ulrike TREC
X042626Quell Manuela TREC
X000433Eisnecker Judith DM,DPF,GAR-VS,SPF,SS,VSILEV3
X042944Morawitz Markus WIRK
X004424Nischelwitzer Helga DL,SPF,SS,VL
X421136Puschitz Gerald W
X044785Ledl Gerhard PI-A
X004483Lindl Hannes GAR-SP,SPF,SS
X046298Sprinzl Gerhard FV
X046370Wallner Karin DM,DPF
X046422Kronfuss Katharina DL
X045434Dorls Solveig PI-A
X004600Steiner Fritz F,GAR-SP,SILEV3,SPF,WE-NR
X005195Wachs Roland DM,DPF,GAR-SP,SPF,SS*
X004762Federczuk Regina GAR-VO,VO
X051859Kermer Christian GAR-VO,MG-N,VO
X050532Haraldsson Iris Maria PI-B
X050997Mantler Daniela GAR-WE,W
X051188Kovac Sophie IPI
X051547M<EFBFBD>ller Petra DM,DPF
X048473Neumeister Walter DM
X049187Kirsteuer Manuela W
X500906Szente-Varga Paul TREC
X050321Rust Wolfgang DL,SL-K
X052570Rischnig Marion DPF,DSGP,SPF,SS
X005369Eisenst„dter Hardy SS
X005800Radl B„rbel FV
X058534Scheifinger Wolfgang GAR-TR,TREC
X055624Piffl Eva DL,SL-K
X054407Fertsak Ulrike PI-A
X005923Feichtinger Franz FIRK,FV
X601798Grafenhofer Ilona DPF,DSGP
X602994Piber Carina PI-B
X055068Stickelberger Josef FIRK,FV,GAR-FA
X061003Kurt Yurdaer SPF,SS
X611405Hochstraáer Christina DM,SL,SPF
X605981Hengster Michaela DL,SL,SPF
X607134Trudenberger Alexandra TREC
X060831Jelinski Susanne GAR-PI,IPI
X608387Kriechbaumer Michael TREC
X612921Pichler Andreas TREC
X614119Auinger Gaby DM
X061422Busam Petra GAR-PI,IPI
X059973Scherz Hubert F
X065600Mandl Veronika VO-K
X062618Csar Wolfgang FIR
X062792Oberhumer Julia DM
X063165Ettl Bernhard F
X063651Schellenbauer Sabrina SL-K
X068608Kubinger Oliver IPI
X068132Kalteis Katharina DL
X006697Schlederer-Mayr Ingeborg PI-A
X000672Gl<EFBFBD>ck-Ragnarsson Claudia PI-A
X071891Tschojer Maddalena TREC
X070029Tschojer Oswald TREC
X007023Baumgarten Herwig TREC
X006944Alber Karin VO
X069497Skerget Ferdinand W
X069080Bieber Heinz FIRK,FV,GAR-FA
X703538Jelinek Yvonne DL,SL-K
X703857Br”tzner Michaela DL
X070905Montgomery Verena MG-N
X071454Trudenberger Susanne TREC
X071528Zrnjevic Patrick PI-A
X007173Choc Petra DPF,DS,SL,SPF
X074590M”schl Sylvia DL,DPF,SL,SPF
X075121Res Michaela TREC
X000741Gr<EFBFBD>ner Wolfgang DPF,DS,SL,SPF
X007778Aigner Harald FV
X007817Holzleitner-Schinko Ines DPF,DSGP
X078316Strilka Julia DM
X008074Seipel Ursula DL,DPF,GAR-SP,M,SILEV3,SPF
X000093Bauer Christian DM,DPF
X084296Matt Susanna VO
X088418Sachslehner Stephanie SL-K
X089040Oschounig Natascha W
X000909Hoffmann Ludwig DPF,DSGP,SPF,SS
X000875Hiesel Gerhard A. GAR-SP,SPF,SS
X087580Gstinig Silvia TREC
X811174Gastl Petra PI-B
X008116Hudec-Semeleder Elisabeth DILEV2,DPF,DSGP
X810139K”ck Daniel DL-K
X800549Thomann Klaus SPF,SS
X009947Katschker Sylvia GAR-WE,WIR

1427
docs/OePS/ZNS/VEREIN01.dat Normal file

File diff suppressed because it is too large Load Diff

1379
docs/OePS/ZNS/VOLT01.dat Normal file

File diff suppressed because it is too large Load Diff

View File

@ -73,6 +73,52 @@ include(":backend:services:entries:entries-api")
// Code liegt im Branch: feature/entries-service
// include(":backend:services:entries:entries-service")
// --- HORSES (Pferde-Verwaltung) ---
// Namespace ':horses:*' damit projects.horses.* Accessors funktionieren
include(":horses")
project(":horses").projectDir = file("backend/services/horses")
include(":horses:horses-domain")
// horses-common: ON HOLD veraltete API-Referenzen
// include(":horses:horses-common")
include(":horses:horses-infrastructure")
// horses-api: ON HOLD Ktor-basiert, wird separat aktiviert
// include(":horses:horses-api")
include(":horses:horses-service")
project(":horses:horses-domain").projectDir = file("backend/services/horses/horses-domain")
// project(":horses:horses-common").projectDir = file("backend/services/horses/horses-common")
project(":horses:horses-infrastructure").projectDir = file("backend/services/horses/horses-infrastructure")
project(":horses:horses-service").projectDir = file("backend/services/horses/horses-service")
// --- PERSONS (Personen/Reiter) ---
include(":persons")
project(":persons").projectDir = file("backend/services/persons")
include(":persons:persons-domain")
include(":persons:persons-infrastructure")
include(":persons:persons-service")
project(":persons:persons-domain").projectDir = file("backend/services/persons/persons-domain")
project(":persons:persons-infrastructure").projectDir = file("backend/services/persons/persons-infrastructure")
project(":persons:persons-service").projectDir = file("backend/services/persons/persons-service")
// --- CLUBS (Vereine) ---
include(":clubs")
project(":clubs").projectDir = file("backend/services/clubs")
include(":clubs:clubs-domain")
include(":clubs:clubs-infrastructure")
include(":clubs:clubs-service")
project(":clubs:clubs-domain").projectDir = file("backend/services/clubs/clubs-domain")
project(":clubs:clubs-infrastructure").projectDir = file("backend/services/clubs/clubs-infrastructure")
project(":clubs:clubs-service").projectDir = file("backend/services/clubs/clubs-service")
// --- OFFICIALS (Richter) ---
include(":officials")
project(":officials").projectDir = file("backend/services/officials")
include(":officials:officials-domain")
include(":officials:officials-infrastructure")
include(":officials:officials-service")
project(":officials:officials-domain").projectDir = file("backend/services/officials/officials-domain")
project(":officials:officials-infrastructure").projectDir = file("backend/services/officials/officials-infrastructure")
project(":officials:officials-service").projectDir = file("backend/services/officials/officials-service")
// --- PING (Ping Service) ---
include(":backend:services:ping:ping-service")