feat(horses-service): remove integration tests and obsolete configurations
- Deleted outdated integration test classes (`HorseServiceIntegrationTest`, `TransactionalContextTest`, and others) and test resources (`logback-test.xml`). - Removed obsolete Gradle dependencies related to these tests and revised project module references. - Simplified `DomPferd` domain model with minor refactorings for serialization and validation. Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
This commit is contained in:
parent
c53daa926a
commit
ceb1ccdab8
|
|
@ -6,7 +6,7 @@ plugins {
|
|||
|
||||
dependencies {
|
||||
implementation(projects.platform.platformDependencies)
|
||||
implementation(projects.clubs.clubsDomain)
|
||||
implementation(projects.backend.services.clubs.clubsDomain)
|
||||
implementation(projects.core.coreDomain)
|
||||
implementation(projects.core.coreUtils)
|
||||
|
||||
|
|
|
|||
|
|
@ -13,8 +13,8 @@ dependencies {
|
|||
implementation(projects.platform.platformDependencies)
|
||||
implementation(projects.core.coreDomain)
|
||||
implementation(projects.core.coreUtils)
|
||||
implementation(projects.clubs.clubsDomain)
|
||||
implementation(projects.clubs.clubsInfrastructure)
|
||||
implementation(projects.backend.services.clubs.clubsDomain)
|
||||
implementation(projects.backend.services.clubs.clubsInfrastructure)
|
||||
|
||||
implementation(libs.spring.boot.starter.web)
|
||||
implementation(libs.spring.boot.starter.validation)
|
||||
|
|
@ -23,6 +23,7 @@ dependencies {
|
|||
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)
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ 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.jetbrains.exposed.v1.migration.jdbc.MigrationUtils
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.beans.factory.annotation.Value
|
||||
import org.springframework.context.annotation.Configuration
|
||||
|
|
@ -24,7 +24,8 @@ class ClubsDatabaseConfiguration(
|
|||
log.info("Initialisiere Datenbank-Schema für Clubs-Service...")
|
||||
Database.connect(jdbcUrl, user = username, password = password)
|
||||
transaction {
|
||||
SchemaUtils.createMissingTablesAndColumns(ZnsClubTable)
|
||||
val statements = MigrationUtils.statementsRequiredForDatabaseMigration(ZnsClubTable)
|
||||
statements.forEach { exec(it) }
|
||||
log.info("Datenbank-Schema erfolgreich initialisiert")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
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.jetbrains.exposed.v1.migration.jdbc.MigrationUtils
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.beans.factory.annotation.Value
|
||||
import org.springframework.boot.CommandLineRunner
|
||||
|
|
@ -36,7 +36,8 @@ class ZnsClubSeeder(
|
|||
}
|
||||
log.info("Starte ZNS-Vereine-Import aus: {}", dir.absolutePath)
|
||||
transaction {
|
||||
SchemaUtils.createMissingTablesAndColumns(ZnsClubTable)
|
||||
val statements = MigrationUtils.statementsRequiredForDatabaseMigration(ZnsClubTable)
|
||||
statements.forEach { exec(it) }
|
||||
}
|
||||
seedClubs(dir)
|
||||
log.info("ZNS-Vereine-Import abgeschlossen.")
|
||||
|
|
|
|||
|
|
@ -18,8 +18,8 @@ springBoot {
|
|||
dependencies {
|
||||
// Interne Module
|
||||
implementation(projects.platform.platformDependencies)
|
||||
implementation(projects.horses.horsesDomain)
|
||||
implementation(projects.horses.horsesApplication)
|
||||
implementation(projects.backend.services.horses.horsesDomain)
|
||||
implementation(projects.backend.services.horses.horsesApplication)
|
||||
implementation(projects.core.coreDomain)
|
||||
implementation(projects.core.coreUtils)
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ plugins {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.horses.horsesDomain)
|
||||
implementation(projects.backend.services.horses.horsesDomain)
|
||||
implementation(projects.core.coreDomain)
|
||||
implementation(projects.core.coreUtils)
|
||||
testImplementation(projects.platform.platformTesting)
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ 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
|
||||
|
|
@ -22,7 +23,7 @@ import kotlin.uuid.Uuid
|
|||
* @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 Birth date of the horse.
|
||||
* @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).
|
||||
|
|
@ -132,9 +133,10 @@ data class DomPferd(
|
|||
val today = kotlin.time.Clock.System.todayIn(kotlinx.datetime.TimeZone.currentSystemDefault())
|
||||
var age = today.year - birthDate.year
|
||||
|
||||
// Check if birthday has occurred this year
|
||||
if (today.monthNumber < birthDate.monthNumber ||
|
||||
(today.monthNumber == birthDate.monthNumber && today.dayOfMonth < birthDate.dayOfMonth)) {
|
||||
// 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--
|
||||
}
|
||||
|
||||
|
|
@ -164,7 +166,7 @@ data class DomPferd(
|
|||
}
|
||||
|
||||
/**
|
||||
* Creates a copy of this horse with updated timestamp.
|
||||
* Creates a copy of this horse with an updated timestamp.
|
||||
*/
|
||||
fun withUpdatedTimestamp(): DomPferd {
|
||||
return this.copy(updatedAt = kotlin.time.Clock.System.now())
|
||||
|
|
|
|||
|
|
@ -7,9 +7,9 @@ plugins {
|
|||
dependencies {
|
||||
// Interne Module
|
||||
implementation(projects.platform.platformDependencies)
|
||||
implementation(projects.horses.horsesDomain)
|
||||
implementation(projects.backend.services.horses.horsesDomain)
|
||||
// horses-common: ON HOLD
|
||||
// implementation(projects.horses.horsesCommon)
|
||||
// implementation(projects.backend.services.horses.horsesCommon)
|
||||
implementation(projects.core.coreDomain)
|
||||
implementation(projects.core.coreUtils)
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,336 @@
|
|||
@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
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
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")
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
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")
|
||||
}
|
||||
|
|
@ -14,10 +14,10 @@ dependencies {
|
|||
implementation(projects.platform.platformDependencies)
|
||||
implementation(projects.core.coreDomain)
|
||||
implementation(projects.core.coreUtils)
|
||||
implementation(projects.horses.horsesDomain)
|
||||
implementation(projects.backend.services.horses.horsesDomain)
|
||||
// horses-common: ON HOLD – veraltete API-Referenzen
|
||||
// implementation(projects.horses.horsesCommon)
|
||||
implementation(projects.horses.horsesInfrastructure)
|
||||
// implementation(projects.backend.services.horses.horsesCommon)
|
||||
implementation(projects.backend.services.horses.horsesInfrastructure)
|
||||
|
||||
// Spring Boot Starters
|
||||
implementation(libs.spring.boot.starter.web)
|
||||
|
|
@ -28,9 +28,11 @@ dependencies {
|
|||
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)
|
||||
testImplementation(project(":backend:infrastructure:messaging:messaging-client"))
|
||||
runtimeOnly(libs.postgresql.driver)
|
||||
testRuntimeOnly(libs.h2.driver)
|
||||
|
||||
// Testing
|
||||
|
|
|
|||
|
|
@ -0,0 +1,60 @@
|
|||
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)
|
||||
}
|
||||
}
|
||||
|
|
@ -5,8 +5,8 @@ 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.jetbrains.exposed.v1.migration.jdbc.MigrationUtils
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.beans.factory.annotation.Value
|
||||
import org.springframework.context.annotation.Configuration
|
||||
|
|
@ -30,7 +30,8 @@ class HorsesDatabaseConfiguration(
|
|||
log.info("Initialisiere Datenbank-Schema für Horses-Service...")
|
||||
Database.connect(jdbcUrl, user = username, password = password)
|
||||
transaction {
|
||||
SchemaUtils.createMissingTablesAndColumns(HorseTable, ZnsPersonTable, ZnsClubTable)
|
||||
val statements = MigrationUtils.statementsRequiredForDatabaseMigration(HorseTable, ZnsPersonTable, ZnsClubTable)
|
||||
statements.forEach { exec(it) }
|
||||
log.info("Datenbank-Schema erfolgreich initialisiert")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,108 @@
|
|||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,277 @@
|
|||
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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,350 +0,0 @@
|
|||
package at.mocode.horses.service.integration
|
||||
|
||||
import at.mocode.horses.domain.model.DomPferd
|
||||
import at.mocode.horses.domain.repository.HorseRepository
|
||||
import at.mocode.infrastructure.messaging.client.EventPublisher
|
||||
import at.mocode.core.domain.model.PferdeGeschlechtE
|
||||
import io.mockk.mockk
|
||||
import kotlinx.datetime.LocalDate
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.boot.test.context.SpringBootTest
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.test.context.ActiveProfiles
|
||||
import org.springframework.test.context.ContextConfiguration
|
||||
import org.springframework.test.context.TestPropertySource
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
/**
|
||||
* Integration tests for the Horses Service.
|
||||
*
|
||||
* These tests verify the complete functionality including:
|
||||
* - Repository operations
|
||||
* - Database persistence
|
||||
* - Domain model validation
|
||||
*/
|
||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
||||
@ActiveProfiles("test")
|
||||
@TestPropertySource(properties = [
|
||||
"spring.datasource.url=jdbc:h2:mem:testdb",
|
||||
"spring.kafka.bootstrap-servers=localhost:9092"
|
||||
])
|
||||
@ContextConfiguration(classes = [HorseServiceIntegrationTest.TestConfig::class])
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
class HorseServiceIntegrationTest {
|
||||
|
||||
@Autowired
|
||||
private lateinit var horseRepository: HorseRepository
|
||||
|
||||
@Configuration
|
||||
class TestConfig {
|
||||
@Bean
|
||||
fun eventPublisher(): EventPublisher = mockk(relaxed = true)
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() = runBlocking {
|
||||
// Clean up database before each test
|
||||
println("[DEBUG_LOG] Setting up horse test - cleaning database")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should create horse successfully`() = runBlocking {
|
||||
println("[DEBUG_LOG] Testing horse creation")
|
||||
|
||||
// Given
|
||||
val horse = DomPferd(
|
||||
pferdeName = "Thunder",
|
||||
geschlecht = PferdeGeschlechtE.WALLACH,
|
||||
geburtsdatum = LocalDate(2020, 5, 15),
|
||||
rasse = "Warmblut",
|
||||
farbe = "Braun",
|
||||
lebensnummer = "AT123456789",
|
||||
chipNummer = "123456789012345",
|
||||
stockmass = 165,
|
||||
istAktiv = true
|
||||
)
|
||||
|
||||
// When
|
||||
val savedHorse = horseRepository.save(horse)
|
||||
|
||||
// Then
|
||||
assertNotNull(savedHorse)
|
||||
assertEquals("Thunder", savedHorse.pferdeName)
|
||||
assertEquals(PferdeGeschlechtE.WALLACH, savedHorse.geschlecht)
|
||||
assertEquals("AT123456789", savedHorse.lebensnummer)
|
||||
assertEquals("123456789012345", savedHorse.chipNummer)
|
||||
assertEquals("Warmblut", savedHorse.rasse)
|
||||
assertTrue(savedHorse.istAktiv)
|
||||
|
||||
println("[DEBUG_LOG] Horse created successfully with ID: ${savedHorse.pferdId}")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should find horse by lebensnummer`() = runBlocking {
|
||||
println("[DEBUG_LOG] Testing find horse by lebensnummer")
|
||||
|
||||
// Given
|
||||
val horse = DomPferd(
|
||||
pferdeName = "Lightning",
|
||||
geschlecht = PferdeGeschlechtE.STUTE,
|
||||
geburtsdatum = LocalDate(2019, 3, 10),
|
||||
rasse = "Vollblut",
|
||||
farbe = "Schimmel",
|
||||
lebensnummer = "AT987654321",
|
||||
chipNummer = "987654321098765",
|
||||
stockmass = 160,
|
||||
istAktiv = true
|
||||
)
|
||||
horseRepository.save(horse)
|
||||
|
||||
// When
|
||||
val foundHorse = horseRepository.findByLebensnummer("AT987654321")
|
||||
|
||||
// Then
|
||||
assertNotNull(foundHorse)
|
||||
assertEquals("Lightning", foundHorse.pferdeName)
|
||||
assertEquals("AT987654321", foundHorse.lebensnummer)
|
||||
assertEquals(PferdeGeschlechtE.STUTE, foundHorse.geschlecht)
|
||||
assertEquals("Vollblut", foundHorse.rasse)
|
||||
|
||||
println("[DEBUG_LOG] Horse found by lebensnummer: ${foundHorse.pferdId}")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should find horse by chip number`() = runBlocking {
|
||||
println("[DEBUG_LOG] Testing find horse by chip number")
|
||||
|
||||
// Given
|
||||
val horse = DomPferd(
|
||||
pferdeName = "Storm",
|
||||
geschlecht = PferdeGeschlechtE.HENGST,
|
||||
geburtsdatum = LocalDate(2021, 8, 20),
|
||||
rasse = "Haflinger",
|
||||
farbe = "Fuchs",
|
||||
lebensnummer = "AT555666777",
|
||||
chipNummer = "555666777888999",
|
||||
stockmass = 150,
|
||||
istAktiv = true
|
||||
)
|
||||
horseRepository.save(horse)
|
||||
|
||||
// When
|
||||
val foundHorse = horseRepository.findByChipNummer("555666777888999")
|
||||
|
||||
// Then
|
||||
assertNotNull(foundHorse)
|
||||
assertEquals("Storm", foundHorse.pferdeName)
|
||||
assertEquals("555666777888999", foundHorse.chipNummer)
|
||||
assertEquals(PferdeGeschlechtE.HENGST, foundHorse.geschlecht)
|
||||
assertEquals("Haflinger", foundHorse.rasse)
|
||||
|
||||
println("[DEBUG_LOG] Horse found by chip number: ${foundHorse.pferdId}")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should find horses by gender`() = runBlocking {
|
||||
println("[DEBUG_LOG] Testing find horses by gender")
|
||||
|
||||
// Given
|
||||
val stallion = DomPferd(
|
||||
pferdeName = "Stallion Horse",
|
||||
geschlecht = PferdeGeschlechtE.HENGST,
|
||||
geburtsdatum = LocalDate(2018, 4, 12),
|
||||
rasse = "Warmblut",
|
||||
farbe = "Braun",
|
||||
lebensnummer = "AT111222333",
|
||||
chipNummer = "111222333444555",
|
||||
stockmass = 170,
|
||||
istAktiv = true
|
||||
)
|
||||
|
||||
val mare = DomPferd(
|
||||
pferdeName = "Mare Horse",
|
||||
geschlecht = PferdeGeschlechtE.STUTE,
|
||||
geburtsdatum = LocalDate(2017, 6, 8),
|
||||
rasse = "Vollblut",
|
||||
farbe = "Rappe",
|
||||
lebensnummer = "AT444555666",
|
||||
chipNummer = "444555666777888",
|
||||
stockmass = 165,
|
||||
istAktiv = true
|
||||
)
|
||||
|
||||
horseRepository.save(stallion)
|
||||
horseRepository.save(mare)
|
||||
|
||||
// When
|
||||
val stallions = horseRepository.findByGeschlecht(PferdeGeschlechtE.HENGST, true, 10)
|
||||
|
||||
// Then
|
||||
assertTrue(stallions.isNotEmpty(), "Should find at least one stallion")
|
||||
assertTrue(stallions.any { it.pferdeName == "Stallion Horse" }, "Should contain the stallion horse")
|
||||
assertTrue(stallions.all { it.geschlecht == PferdeGeschlechtE.HENGST }, "All returned horses should be stallions")
|
||||
|
||||
println("[DEBUG_LOG] Found ${stallions.size} stallions")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should find horses by breed`() = runBlocking {
|
||||
println("[DEBUG_LOG] Testing find horses by breed")
|
||||
|
||||
// Given
|
||||
val warmblutHorse = DomPferd(
|
||||
pferdeName = "Warmblut Horse",
|
||||
geschlecht = PferdeGeschlechtE.WALLACH,
|
||||
geburtsdatum = LocalDate(2019, 9, 15),
|
||||
rasse = "Warmblut",
|
||||
farbe = "Braun",
|
||||
lebensnummer = "AT333444555",
|
||||
chipNummer = "333444555666777",
|
||||
stockmass = 168,
|
||||
istAktiv = true
|
||||
)
|
||||
horseRepository.save(warmblutHorse)
|
||||
|
||||
// When
|
||||
val warmblutHorses = horseRepository.findByRasse("Warmblut", true, 10)
|
||||
|
||||
// Then
|
||||
assertTrue(warmblutHorses.isNotEmpty(), "Should find at least one Warmblut horse")
|
||||
assertTrue(warmblutHorses.any { it.pferdeName == "Warmblut Horse" }, "Should contain the Warmblut horse")
|
||||
assertTrue(warmblutHorses.all { it.rasse == "Warmblut" }, "All returned horses should be Warmblut")
|
||||
|
||||
println("[DEBUG_LOG] Found ${warmblutHorses.size} Warmblut horses")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should find OEPS registered horses`() = runBlocking {
|
||||
println("[DEBUG_LOG] Testing find OEPS registered horses")
|
||||
|
||||
// Given
|
||||
val oepsHorse = DomPferd(
|
||||
pferdeName = "OEPS Horse",
|
||||
geschlecht = PferdeGeschlechtE.WALLACH,
|
||||
geburtsdatum = LocalDate(2018, 7, 22),
|
||||
rasse = "Warmblut",
|
||||
farbe = "Braun",
|
||||
lebensnummer = "AT777888999",
|
||||
chipNummer = "777888999000111",
|
||||
oepsNummer = "OEPS123456",
|
||||
stockmass = 170,
|
||||
istAktiv = true
|
||||
)
|
||||
|
||||
val nonOepsHorse = DomPferd(
|
||||
pferdeName = "Non-OEPS Horse",
|
||||
geschlecht = PferdeGeschlechtE.STUTE,
|
||||
geburtsdatum = LocalDate(2017, 11, 5),
|
||||
rasse = "Vollblut",
|
||||
farbe = "Rappe",
|
||||
lebensnummer = "AT000111222",
|
||||
chipNummer = "000111222333444",
|
||||
stockmass = 165,
|
||||
istAktiv = true
|
||||
)
|
||||
|
||||
horseRepository.save(oepsHorse)
|
||||
horseRepository.save(nonOepsHorse)
|
||||
|
||||
// When
|
||||
val oepsHorses = horseRepository.findOepsRegistered(true)
|
||||
|
||||
// Then
|
||||
assertTrue(oepsHorses.isNotEmpty(), "Should find at least one OEPS registered horse")
|
||||
assertTrue(oepsHorses.any { it.pferdeName == "OEPS Horse" }, "Should contain the OEPS registered horse")
|
||||
assertTrue(oepsHorses.all { !it.oepsNummer.isNullOrBlank() }, "All returned horses should have OEPS numbers")
|
||||
|
||||
println("[DEBUG_LOG] Found ${oepsHorses.size} OEPS registered horses")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should find FEI registered horses`() = runBlocking {
|
||||
println("[DEBUG_LOG] Testing find FEI registered horses")
|
||||
|
||||
// Given
|
||||
val feiHorse = DomPferd(
|
||||
pferdeName = "FEI Horse",
|
||||
geschlecht = PferdeGeschlechtE.HENGST,
|
||||
geburtsdatum = LocalDate(2016, 2, 14),
|
||||
rasse = "Warmblut",
|
||||
farbe = "Schimmel",
|
||||
lebensnummer = "AT999000111",
|
||||
chipNummer = "999000111222333",
|
||||
feiNummer = "FEI789012",
|
||||
stockmass = 175,
|
||||
istAktiv = true
|
||||
)
|
||||
horseRepository.save(feiHorse)
|
||||
|
||||
// When
|
||||
val feiHorses = horseRepository.findFeiRegistered(true)
|
||||
|
||||
// Then
|
||||
assertTrue(feiHorses.isNotEmpty(), "Should find at least one FEI registered horse")
|
||||
assertTrue(feiHorses.any { it.pferdeName == "FEI Horse" }, "Should contain the FEI registered horse")
|
||||
assertTrue(feiHorses.all { !it.feiNummer.isNullOrBlank() }, "All returned horses should have FEI numbers")
|
||||
|
||||
println("[DEBUG_LOG] Found ${feiHorses.size} FEI registered horses")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should validate duplicate lebensnummer`() = runBlocking {
|
||||
println("[DEBUG_LOG] Testing duplicate lebensnummer validation")
|
||||
|
||||
// Given
|
||||
val horse = DomPferd(
|
||||
pferdeName = "First Horse",
|
||||
geschlecht = PferdeGeschlechtE.WALLACH,
|
||||
geburtsdatum = LocalDate(2019, 1, 1),
|
||||
rasse = "Warmblut",
|
||||
farbe = "Braun",
|
||||
lebensnummer = "AT123123123",
|
||||
chipNummer = "123123123456789",
|
||||
stockmass = 165,
|
||||
istAktiv = true
|
||||
)
|
||||
horseRepository.save(horse)
|
||||
|
||||
// When
|
||||
val exists = horseRepository.existsByLebensnummer("AT123123123")
|
||||
|
||||
// Then
|
||||
assertTrue(exists, "Should detect existing lebensnummer")
|
||||
|
||||
println("[DEBUG_LOG] Duplicate lebensnummer validation passed")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should validate duplicate chip number`() = runBlocking {
|
||||
println("[DEBUG_LOG] Testing duplicate chip number validation")
|
||||
|
||||
// Given
|
||||
val horse = DomPferd(
|
||||
pferdeName = "Chip Test Horse",
|
||||
geschlecht = PferdeGeschlechtE.STUTE,
|
||||
geburtsdatum = LocalDate(2020, 12, 25),
|
||||
rasse = "Haflinger",
|
||||
farbe = "Fuchs",
|
||||
lebensnummer = "AT456456456",
|
||||
chipNummer = "456456456789012",
|
||||
stockmass = 148,
|
||||
istAktiv = true
|
||||
)
|
||||
horseRepository.save(horse)
|
||||
|
||||
// When
|
||||
val exists = horseRepository.existsByChipNummer("456456456789012")
|
||||
|
||||
// Then
|
||||
assertTrue(exists, "Should detect existing chip number")
|
||||
|
||||
println("[DEBUG_LOG] Duplicate chip number validation passed")
|
||||
}
|
||||
}
|
||||
|
|
@ -1,171 +0,0 @@
|
|||
package at.mocode.horses.service.integration
|
||||
|
||||
import at.mocode.horses.domain.model.DomPferd
|
||||
import at.mocode.horses.domain.repository.HorseRepository
|
||||
import at.mocode.core.domain.model.PferdeGeschlechtE
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.datetime.LocalDate
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import org.springframework.boot.test.context.SpringBootTest
|
||||
import org.springframework.test.context.ActiveProfiles
|
||||
import org.springframework.test.context.TestPropertySource
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import kotlin.test.assertTrue
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotNull
|
||||
|
||||
/**
|
||||
* Integration tests to demonstrate and verify transaction context issues with coroutines.
|
||||
*
|
||||
* This test class reproduces the race condition that can occur when multiple
|
||||
* coroutines perform database operations without proper transaction boundaries.
|
||||
*/
|
||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
||||
@ActiveProfiles("test")
|
||||
@TestPropertySource(properties = [
|
||||
"spring.datasource.url=jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE",
|
||||
"spring.jpa.hibernate.ddl-auto=create-drop"
|
||||
])
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
class TransactionContextTest {
|
||||
|
||||
@Autowired
|
||||
private lateinit var horseRepository: HorseRepository
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
runBlocking {
|
||||
// Clean up any existing test data
|
||||
// Note: This is a simplified cleanup - in a real scenario you'd have proper cleanup
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should demonstrate race condition without transaction boundaries`(): Unit = runBlocking {
|
||||
println("[DEBUG_LOG] Starting race condition test")
|
||||
|
||||
val lebensnummer = "TEST-RACE-001"
|
||||
val chipNummer = "CHIP-RACE-001"
|
||||
|
||||
// Create two horses with the same identifiers
|
||||
val horse1 = DomPferd(
|
||||
pferdeName = "Race Horse 1",
|
||||
geschlecht = PferdeGeschlechtE.WALLACH,
|
||||
geburtsdatum = LocalDate(2020, 1, 1),
|
||||
lebensnummer = lebensnummer,
|
||||
chipNummer = chipNummer,
|
||||
istAktiv = true
|
||||
)
|
||||
|
||||
val horse2 = DomPferd(
|
||||
pferdeName = "Race Horse 2",
|
||||
geschlecht = PferdeGeschlechtE.STUTE,
|
||||
geburtsdatum = LocalDate(2020, 1, 2),
|
||||
lebensnummer = lebensnummer, // Same lebensnummer - should cause conflict
|
||||
chipNummer = chipNummer, // Same chipNummer - should cause conflict
|
||||
istAktiv = true
|
||||
)
|
||||
|
||||
println("[DEBUG_LOG] Created horses with duplicate identifiers")
|
||||
|
||||
// Simulate the use case logic: check uniqueness then save
|
||||
// This mimics what CreateHorseUseCase.execute() does without transactions
|
||||
suspend fun createHorseWithChecks(horse: DomPferd): Boolean {
|
||||
return try {
|
||||
// Check uniqueness constraints (like in checkUniquenessConstraints)
|
||||
val existsByLebensnummer = horse.lebensnummer?.let {
|
||||
horseRepository.existsByLebensnummer(it)
|
||||
} ?: false
|
||||
|
||||
val existsByChipNummer = horse.chipNummer?.let {
|
||||
horseRepository.existsByChipNummer(it)
|
||||
} ?: false
|
||||
|
||||
println("[DEBUG_LOG] ${horse.pferdeName}: existsByLebensnummer=$existsByLebensnummer, existsByChipNummer=$existsByChipNummer")
|
||||
|
||||
if (existsByLebensnummer || existsByChipNummer) {
|
||||
println("[DEBUG_LOG] ${horse.pferdeName}: Uniqueness check failed")
|
||||
false
|
||||
} else {
|
||||
// Save the horse (like in the use case)
|
||||
horseRepository.save(horse)
|
||||
println("[DEBUG_LOG] ${horse.pferdeName}: Saved successfully")
|
||||
true
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
println("[DEBUG_LOG] ${horse.pferdeName}: Exception during creation: ${e.message}")
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
// Launch two concurrent coroutines to create horses
|
||||
val results = listOf(
|
||||
async {
|
||||
println("[DEBUG_LOG] Starting creation 1")
|
||||
createHorseWithChecks(horse1)
|
||||
},
|
||||
async {
|
||||
println("[DEBUG_LOG] Starting creation 2")
|
||||
createHorseWithChecks(horse2)
|
||||
}
|
||||
).awaitAll()
|
||||
|
||||
println("[DEBUG_LOG] Both operations completed")
|
||||
println("[DEBUG_LOG] Result 1 success: ${results[0]}")
|
||||
println("[DEBUG_LOG] Result 2 success: ${results[1]}")
|
||||
|
||||
// In a properly transactional system, exactly one should succeed
|
||||
val successCount = results.count { it }
|
||||
val failureCount = results.count { !it }
|
||||
|
||||
println("[DEBUG_LOG] Success count: $successCount, Failure count: $failureCount")
|
||||
|
||||
// Check what actually got saved in the database
|
||||
val savedByLebensnummer = horseRepository.findByLebensnummer(lebensnummer)
|
||||
val savedByChipNummer = horseRepository.findByChipNummer(chipNummer)
|
||||
|
||||
println("[DEBUG_LOG] Found by lebensnummer: ${savedByLebensnummer?.pferdeName}")
|
||||
println("[DEBUG_LOG] Found by chipNummer: ${savedByChipNummer?.pferdeName}")
|
||||
|
||||
// This test demonstrates the issue - without transactions, both operations might succeed
|
||||
// due to race conditions, or the behavior might be unpredictable
|
||||
// The fix should ensure exactly one succeeds and one fails with a proper error
|
||||
assertTrue(successCount >= 1, "At least one operation should succeed")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should demonstrate transaction context propagation issue`(): Unit = runBlocking {
|
||||
println("[DEBUG_LOG] Starting transaction context propagation test")
|
||||
|
||||
// This test will show that without @Transactional, each repository call
|
||||
// runs in its own transaction context, which can lead to inconsistencies
|
||||
|
||||
val horse = DomPferd(
|
||||
pferdeName = "Transaction Test Horse",
|
||||
geschlecht = PferdeGeschlechtE.HENGST,
|
||||
lebensnummer = "TRANS-TEST-001",
|
||||
istAktiv = true
|
||||
)
|
||||
|
||||
println("[DEBUG_LOG] Creating horse with repository operations")
|
||||
|
||||
// Simulate multiple repository operations that should be atomic
|
||||
val existsCheck = horseRepository.existsByLebensnummer("TRANS-TEST-001")
|
||||
println("[DEBUG_LOG] Exists check result: $existsCheck")
|
||||
|
||||
if (!existsCheck) {
|
||||
val savedHorse = horseRepository.save(horse)
|
||||
println("[DEBUG_LOG] Horse saved successfully: ${savedHorse.pferdeName}")
|
||||
assertNotNull(savedHorse)
|
||||
assertEquals("Transaction Test Horse", savedHorse.pferdeName)
|
||||
}
|
||||
|
||||
// The issue is that without @Transactional, if an exception occurs after
|
||||
// the uniqueness checks but before/during save, the database state
|
||||
// might be inconsistent
|
||||
val finalCheck = horseRepository.findByLebensnummer("TRANS-TEST-001")
|
||||
assertNotNull(finalCheck, "Horse should be saved in database")
|
||||
}
|
||||
}
|
||||
|
|
@ -1,187 +0,0 @@
|
|||
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
||||
package at.mocode.horses.service.integration
|
||||
|
||||
import at.mocode.horses.application.usecase.TransactionalCreateHorseUseCase
|
||||
import at.mocode.horses.domain.repository.HorseRepository
|
||||
import at.mocode.core.domain.model.PferdeGeschlechtE
|
||||
import kotlin.uuid.Uuid
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.datetime.LocalDate
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import org.springframework.boot.test.context.SpringBootTest
|
||||
import org.springframework.test.context.ActiveProfiles
|
||||
import org.springframework.test.context.TestPropertySource
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import kotlin.test.assertTrue
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotNull
|
||||
|
||||
/**
|
||||
* Integration tests to verify that transaction context issues with coroutines are resolved.
|
||||
*
|
||||
* This test class verifies that the transactional use cases properly handle
|
||||
* concurrent operations and maintain data consistency.
|
||||
*/
|
||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
||||
@ActiveProfiles("test")
|
||||
@TestPropertySource(properties = [
|
||||
"spring.datasource.url=jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE",
|
||||
"spring.jpa.hibernate.ddl-auto=create-drop"
|
||||
])
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
class TransactionalContextTest {
|
||||
|
||||
@Autowired
|
||||
private lateinit var horseRepository: HorseRepository
|
||||
|
||||
@Autowired
|
||||
private lateinit var transactionalCreateHorseUseCase: TransactionalCreateHorseUseCase
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
runBlocking {
|
||||
// Clean up any existing test data
|
||||
// Note: This is a simplified cleanup - in a real scenario you'd have proper cleanup
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should handle race condition properly with transaction boundaries`(): Unit = runBlocking {
|
||||
println("[DEBUG_LOG] Starting transactional race condition test")
|
||||
|
||||
val lebensnummer = "TRANS-RACE-001"
|
||||
val chipNummer = "TRANS-CHIP-001"
|
||||
|
||||
// Create two identical horse creation requests
|
||||
val ownerId = Uuid.random()
|
||||
val request1 = TransactionalCreateHorseUseCase.CreateHorseRequest(
|
||||
pferdeName = "Transactional Race Horse 1",
|
||||
geschlecht = PferdeGeschlechtE.WALLACH,
|
||||
geburtsdatum = LocalDate(2020, 1, 1),
|
||||
lebensnummer = lebensnummer,
|
||||
chipNummer = chipNummer,
|
||||
besitzerId = ownerId
|
||||
)
|
||||
|
||||
val request2 = TransactionalCreateHorseUseCase.CreateHorseRequest(
|
||||
pferdeName = "Transactional Race Horse 2",
|
||||
geschlecht = PferdeGeschlechtE.STUTE,
|
||||
geburtsdatum = LocalDate(2020, 1, 2),
|
||||
lebensnummer = lebensnummer, // Same lebensnummer - should cause conflict
|
||||
chipNummer = chipNummer, // Same chipNummer - should cause conflict
|
||||
besitzerId = ownerId
|
||||
)
|
||||
|
||||
println("[DEBUG_LOG] Created requests with duplicate identifiers")
|
||||
|
||||
// Launch two concurrent coroutines to create horses using transactional use case
|
||||
val results = listOf(
|
||||
async {
|
||||
println("[DEBUG_LOG] Starting transactional creation 1")
|
||||
transactionalCreateHorseUseCase.execute(request1)
|
||||
},
|
||||
async {
|
||||
println("[DEBUG_LOG] Starting transactional creation 2")
|
||||
transactionalCreateHorseUseCase.execute(request2)
|
||||
}
|
||||
).awaitAll()
|
||||
|
||||
println("[DEBUG_LOG] Both transactional operations completed")
|
||||
println("[DEBUG_LOG] Result 1 success: ${results[0].success}")
|
||||
println("[DEBUG_LOG] Result 2 success: ${results[1].success}")
|
||||
|
||||
// With proper transaction boundaries, exactly one should succeed
|
||||
val successCount = results.count { it.success }
|
||||
val failureCount = results.count { !it.success }
|
||||
|
||||
println("[DEBUG_LOG] Success count: $successCount, Failure count: $failureCount")
|
||||
|
||||
// Verify that exactly one operation succeeded and one failed
|
||||
assertEquals(1, successCount, "Exactly one operation should succeed with proper transactions")
|
||||
assertEquals(1, failureCount, "Exactly one operation should fail with proper transactions")
|
||||
|
||||
// Check what actually got saved in the database
|
||||
val savedByLebensnummer = horseRepository.findByLebensnummer(lebensnummer)
|
||||
val savedByChipNummer = horseRepository.findByChipNummer(chipNummer)
|
||||
|
||||
println("[DEBUG_LOG] Found by lebensnummer: ${savedByLebensnummer?.pferdeName}")
|
||||
println("[DEBUG_LOG] Found by chipNummer: ${savedByChipNummer?.pferdeName}")
|
||||
|
||||
// Verify that exactly one horse was saved
|
||||
assertNotNull(savedByLebensnummer, "One horse should be saved with the lebensnummer")
|
||||
assertNotNull(savedByChipNummer, "One horse should be saved with the chipNummer")
|
||||
assertEquals(savedByLebensnummer?.pferdId, savedByChipNummer?.pferdId, "Both queries should return the same horse")
|
||||
|
||||
// Verify that the failed operation returned proper error
|
||||
val failedResult = results.find { !it.success }
|
||||
assertNotNull(failedResult, "There should be one failed result")
|
||||
assertEquals("UNIQUENESS_ERROR", failedResult?.error?.code, "Failed operation should return uniqueness error")
|
||||
|
||||
println("[DEBUG_LOG] Transactional test completed successfully - race condition properly handled")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should maintain transaction consistency on validation failure`(): Unit = runBlocking {
|
||||
println("[DEBUG_LOG] Starting transaction consistency test")
|
||||
|
||||
// Create a request with invalid data that will fail validation
|
||||
val request = TransactionalCreateHorseUseCase.CreateHorseRequest(
|
||||
pferdeName = "", // Empty name should fail validation
|
||||
geschlecht = PferdeGeschlechtE.HENGST,
|
||||
lebensnummer = "VALIDATION-TEST-001",
|
||||
stockmass = 300, // Invalid height should fail validation
|
||||
besitzerId = Uuid.random() // Add owner to pass basic validation
|
||||
)
|
||||
|
||||
println("[DEBUG_LOG] Executing transactional create with invalid data")
|
||||
val result = transactionalCreateHorseUseCase.execute(request)
|
||||
|
||||
println("[DEBUG_LOG] Creation result: success=${result.success}")
|
||||
|
||||
// Verify that the operation failed due to validation
|
||||
assertTrue(!result.success, "Operation should fail due to validation errors")
|
||||
assertEquals("VALIDATION_ERROR", result.error?.code, "Should return validation error")
|
||||
|
||||
// Verify that no horse was saved in the database
|
||||
val savedHorse = horseRepository.findByLebensnummer("VALIDATION-TEST-001")
|
||||
assertTrue(savedHorse == null, "No horse should be saved when validation fails")
|
||||
|
||||
println("[DEBUG_LOG] Transaction consistency test completed - no data saved on validation failure")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should successfully create horse with valid data in transaction`(): Unit = runBlocking {
|
||||
println("[DEBUG_LOG] Starting successful transactional creation test")
|
||||
|
||||
val request = TransactionalCreateHorseUseCase.CreateHorseRequest(
|
||||
pferdeName = "Successful Transaction Horse",
|
||||
geschlecht = PferdeGeschlechtE.STUTE,
|
||||
geburtsdatum = LocalDate(2021, 6, 15),
|
||||
lebensnummer = "SUCCESS-TEST-001",
|
||||
chipNummer = "SUCCESS-CHIP-001",
|
||||
rasse = "Warmblut",
|
||||
stockmass = 165,
|
||||
besitzerId = Uuid.random() // Add required owner
|
||||
)
|
||||
|
||||
println("[DEBUG_LOG] Executing transactional create with valid data")
|
||||
val result = transactionalCreateHorseUseCase.execute(request)
|
||||
|
||||
println("[DEBUG_LOG] Creation result: success=${result.success}")
|
||||
|
||||
// Verify that the operation succeeded
|
||||
assertTrue(result.success, "Operation should succeed with valid data")
|
||||
assertNotNull(result.data, "Result should contain the created horse")
|
||||
assertEquals("Successful Transaction Horse", result.data?.pferdeName, "Horse name should match")
|
||||
|
||||
// Verify that the horse was saved in the database
|
||||
val savedHorse = horseRepository.findByLebensnummer("SUCCESS-TEST-001")
|
||||
assertNotNull(savedHorse, "Horse should be saved in database")
|
||||
assertEquals("Successful Transaction Horse", savedHorse.pferdeName, "Saved horse name should match")
|
||||
assertEquals("SUCCESS-CHIP-001", savedHorse.chipNummer, "Saved horse chip number should match")
|
||||
|
||||
println("[DEBUG_LOG] Successful transactional creation test completed")
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
<configuration>
|
||||
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
<root level="INFO">
|
||||
<appender-ref ref="CONSOLE" />
|
||||
</root>
|
||||
</configuration>
|
||||
|
|
@ -6,7 +6,7 @@ plugins {
|
|||
|
||||
dependencies {
|
||||
implementation(projects.platform.platformDependencies)
|
||||
implementation(projects.officials.officialsDomain)
|
||||
implementation(projects.backend.services.officials.officialsDomain)
|
||||
implementation(projects.core.coreDomain)
|
||||
implementation(projects.core.coreUtils)
|
||||
|
||||
|
|
|
|||
|
|
@ -13,8 +13,8 @@ dependencies {
|
|||
implementation(projects.platform.platformDependencies)
|
||||
implementation(projects.core.coreDomain)
|
||||
implementation(projects.core.coreUtils)
|
||||
implementation(projects.officials.officialsDomain)
|
||||
implementation(projects.officials.officialsInfrastructure)
|
||||
implementation(projects.backend.services.officials.officialsDomain)
|
||||
implementation(projects.backend.services.officials.officialsInfrastructure)
|
||||
|
||||
implementation(libs.spring.boot.starter.web)
|
||||
implementation(libs.spring.boot.starter.validation)
|
||||
|
|
@ -23,6 +23,7 @@ dependencies {
|
|||
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)
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ 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.jetbrains.exposed.v1.migration.jdbc.MigrationUtils
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.beans.factory.annotation.Value
|
||||
import org.springframework.context.annotation.Configuration
|
||||
|
|
@ -24,7 +24,8 @@ class OfficialsDatabaseConfiguration(
|
|||
log.info("Initialisiere Datenbank-Schema für Officials-Service...")
|
||||
Database.connect(jdbcUrl, user = username, password = password)
|
||||
transaction {
|
||||
SchemaUtils.createMissingTablesAndColumns(ZnsOfficialTable)
|
||||
val statements = MigrationUtils.statementsRequiredForDatabaseMigration(ZnsOfficialTable)
|
||||
statements.forEach { exec(it) }
|
||||
log.info("Datenbank-Schema erfolgreich initialisiert")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
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.jetbrains.exposed.v1.migration.jdbc.MigrationUtils
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.beans.factory.annotation.Value
|
||||
import org.springframework.boot.CommandLineRunner
|
||||
|
|
@ -36,7 +36,8 @@ class ZnsOfficialSeeder(
|
|||
}
|
||||
log.info("Starte ZNS-Richter-Import aus: {}", dir.absolutePath)
|
||||
transaction {
|
||||
SchemaUtils.createMissingTablesAndColumns(ZnsOfficialTable)
|
||||
val statements = MigrationUtils.statementsRequiredForDatabaseMigration(ZnsOfficialTable)
|
||||
statements.forEach { exec(it) }
|
||||
}
|
||||
seedOfficials(dir)
|
||||
log.info("ZNS-Richter-Import abgeschlossen.")
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ plugins {
|
|||
|
||||
dependencies {
|
||||
implementation(projects.platform.platformDependencies)
|
||||
implementation(projects.persons.personsDomain)
|
||||
implementation(projects.backend.services.persons.personsDomain)
|
||||
implementation(projects.core.coreDomain)
|
||||
implementation(projects.core.coreUtils)
|
||||
|
||||
|
|
|
|||
|
|
@ -13,8 +13,8 @@ dependencies {
|
|||
implementation(projects.platform.platformDependencies)
|
||||
implementation(projects.core.coreDomain)
|
||||
implementation(projects.core.coreUtils)
|
||||
implementation(projects.persons.personsDomain)
|
||||
implementation(projects.persons.personsInfrastructure)
|
||||
implementation(projects.backend.services.persons.personsDomain)
|
||||
implementation(projects.backend.services.persons.personsInfrastructure)
|
||||
|
||||
implementation(libs.spring.boot.starter.web)
|
||||
implementation(libs.spring.boot.starter.validation)
|
||||
|
|
@ -23,6 +23,7 @@ dependencies {
|
|||
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)
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ 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.jetbrains.exposed.v1.migration.jdbc.MigrationUtils
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.beans.factory.annotation.Value
|
||||
import org.springframework.context.annotation.Configuration
|
||||
|
|
@ -24,7 +24,8 @@ class PersonsDatabaseConfiguration(
|
|||
log.info("Initialisiere Datenbank-Schema für Persons-Service...")
|
||||
Database.connect(jdbcUrl, user = username, password = password)
|
||||
transaction {
|
||||
SchemaUtils.createMissingTablesAndColumns(ZnsPersonTable)
|
||||
val statements = MigrationUtils.statementsRequiredForDatabaseMigration(ZnsPersonTable)
|
||||
statements.forEach { exec(it) }
|
||||
log.info("Datenbank-Schema erfolgreich initialisiert")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
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.jetbrains.exposed.v1.migration.jdbc.MigrationUtils
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.beans.factory.annotation.Value
|
||||
import org.springframework.boot.CommandLineRunner
|
||||
|
|
@ -36,7 +36,8 @@ class ZnsPersonSeeder(
|
|||
}
|
||||
log.info("Starte ZNS-Personen-Import aus: {}", dir.absolutePath)
|
||||
transaction {
|
||||
SchemaUtils.createMissingTablesAndColumns(ZnsPersonTable)
|
||||
val statements = MigrationUtils.statementsRequiredForDatabaseMigration(ZnsPersonTable)
|
||||
statements.forEach { exec(it) }
|
||||
}
|
||||
seedPersons(dir)
|
||||
log.info("ZNS-Personen-Import abgeschlossen.")
|
||||
|
|
|
|||
|
|
@ -218,6 +218,7 @@ exposed-core = { module = "org.jetbrains.exposed:exposed-core", version.ref = "e
|
|||
exposed-dao = { module = "org.jetbrains.exposed:exposed-dao", version.ref = "exposed" }
|
||||
exposed-jdbc = { module = "org.jetbrains.exposed:exposed-jdbc", version.ref = "exposed" }
|
||||
exposed-kotlin-datetime = { module = "org.jetbrains.exposed:exposed-kotlin-datetime", version.ref = "exposed" }
|
||||
exposed-migration-jdbc = { module = "org.jetbrains.exposed:exposed-migration-jdbc", version.ref = "exposed" }
|
||||
exposed-java-time = { module = "org.jetbrains.exposed:exposed-java-time", version.ref = "exposed" }
|
||||
exposed-json = { module = "org.jetbrains.exposed:exposed-json", version.ref = "exposed" }
|
||||
exposed-money = { module = "org.jetbrains.exposed:exposed-money", version.ref = "exposed" }
|
||||
|
|
|
|||
|
|
@ -73,51 +73,29 @@ 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")
|
||||
include(":backend:services:clubs:clubs-domain")
|
||||
include(":backend:services:clubs:clubs-infrastructure")
|
||||
include(":backend:services:clubs:clubs-service")
|
||||
|
||||
// --- HORSES (Pferde-Verwaltung) ---
|
||||
include(":backend:services:horses:horses-domain")
|
||||
// horses-common: ON HOLD – veraltete API-Referenzen
|
||||
// include(":backend:services:horses:horses-common")
|
||||
include(":backend:services:horses:horses-infrastructure")
|
||||
// horses-api: ON HOLD – Ktor-basiert, wird separat aktiviert
|
||||
// include(":backend:services:horses:horses-api")
|
||||
include(":backend:services:horses:horses-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")
|
||||
include(":backend:services:officials:officials-domain")
|
||||
include(":backend:services:officials:officials-infrastructure")
|
||||
include(":backend:services:officials:officials-service")
|
||||
|
||||
// --- PERSONS (Personen/Reiter) ---
|
||||
include(":backend:services:persons:persons-domain")
|
||||
include(":backend:services:persons:persons-infrastructure")
|
||||
include(":backend:services:persons:persons-service")
|
||||
|
||||
// --- PING (Ping Service) ---
|
||||
include(":backend:services:ping:ping-service")
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user