From 0ae9a1f1b872c875ccb8e9bafa4d13337d0ddeaa Mon Sep 17 00:00:00 2001 From: StefanMoCoAt Date: Mon, 6 Apr 2026 16:39:06 +0200 Subject: [PATCH] Refactor master data infrastructure to streamline `Reiter` and `Bundesland` relationships, add `V012` migration, harmonize domain models, implement repository methods for enhanced ZNS import logic, and update associated tests. --- .../mocode/zns/importer/ZnsImportService.kt | 23 +++++- .../zns/importer/ZnsImportServiceTest.kt | 21 +++++- .../masterdata/domain/model/Bundesland.kt | 19 +++++ .../domain/model/BundeslandDefinition.kt | 1 + .../mocode/masterdata/domain/model/Reiter.kt | 48 +++++++++---- .../domain/repository/BundeslandRepository.kt | 51 ++------------ .../domain/repository/VereinRepository.kt | 5 ++ .../persistence/BundeslandRepositoryImpl.kt | 7 ++ .../persistence/BundeslandTable.kt | 3 +- .../reiter/BundeslandExposedRepository.kt | 45 ++++++++++++ .../reiter/ReiterExposedRepository.kt | 9 +++ .../persistence/reiter/ReiterTable.kt | 48 +++++++++---- .../verein/VereinExposedRepository.kt | 6 ++ .../config/MasterdataDatabaseConfiguration.kt | 4 ++ .../V012__Reiter_Masterdata_Refactoring.sql | 70 +++++++++++++++++++ 15 files changed, 284 insertions(+), 76 deletions(-) create mode 100644 backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/model/Bundesland.kt create mode 100644 backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/reiter/BundeslandExposedRepository.kt create mode 100644 backend/services/masterdata/masterdata-service/src/main/resources/db/migration/V012__Reiter_Masterdata_Refactoring.sql diff --git a/backend/infrastructure/zns-importer/src/main/kotlin/at/mocode/zns/importer/ZnsImportService.kt b/backend/infrastructure/zns-importer/src/main/kotlin/at/mocode/zns/importer/ZnsImportService.kt index 3b5cb4de..974c3205 100644 --- a/backend/infrastructure/zns-importer/src/main/kotlin/at/mocode/zns/importer/ZnsImportService.kt +++ b/backend/infrastructure/zns-importer/src/main/kotlin/at/mocode/zns/importer/ZnsImportService.kt @@ -6,6 +6,8 @@ import at.mocode.masterdata.domain.repository.VereinRepository import at.mocode.masterdata.domain.repository.HorseRepository import at.mocode.masterdata.domain.repository.FunktionaerRepository import at.mocode.masterdata.domain.repository.ReiterRepository +import at.mocode.masterdata.domain.repository.LandRepository +import at.mocode.masterdata.domain.repository.BundeslandRepository import at.mocode.zns.parser.ZnsFunktionaerParser import at.mocode.zns.parser.ZnsPferdParser import at.mocode.zns.parser.ZnsReiterParser @@ -40,7 +42,9 @@ class ZnsImportService( private val vereinRepository: VereinRepository, private val reiterRepository: ReiterRepository, private val horseRepository: HorseRepository, - private val funktionaerRepository: FunktionaerRepository + private val funktionaerRepository: FunktionaerRepository, + private val landRepository: LandRepository, + private val bundeslandRepository: BundeslandRepository ) { companion object { @@ -166,7 +170,19 @@ class ZnsImportService( var aktualisiert = 0 zeilen.forEachIndexed { index, zeile -> runCatching { - val reiter = ZnsReiterParser.parse(zeile) ?: return@forEachIndexed + val parsed = ZnsReiterParser.parse(zeile) ?: return@forEachIndexed + + // Relationen auflösen + val verein = parsed.vereinsName?.let { vereinRepository.findByExactName(it) } + val bundesland = parsed.bundeslandNummer?.let { bundeslandRepository.findByNr(it) } + val nation = parsed.nation?.let { landRepository.findByIsoAlpha3Code(it) } + + val reiter = parsed.copy( + vereinId = verein?.vereinId, + bundeslandId = bundesland?.bundeslandId, + nationId = nation?.landId + ) + val vorhanden = reiterRepository.findBySatznummer(reiter.satznummer) if (vorhanden == null) { reiterRepository.save(reiter) @@ -179,6 +195,9 @@ class ZnsImportService( bundeslandNummer = reiter.bundeslandNummer, vereinsName = reiter.vereinsName, nation = reiter.nation, + vereinId = reiter.vereinId, + bundeslandId = reiter.bundeslandId, + nationId = reiter.nationId, reiterLizenz = reiter.reiterLizenz, startkarte = reiter.startkarte, fahrLizenz = reiter.fahrLizenz, diff --git a/backend/infrastructure/zns-importer/src/test/kotlin/at/mocode/zns/importer/ZnsImportServiceTest.kt b/backend/infrastructure/zns-importer/src/test/kotlin/at/mocode/zns/importer/ZnsImportServiceTest.kt index cdcb17bb..9b1f01b2 100644 --- a/backend/infrastructure/zns-importer/src/test/kotlin/at/mocode/zns/importer/ZnsImportServiceTest.kt +++ b/backend/infrastructure/zns-importer/src/test/kotlin/at/mocode/zns/importer/ZnsImportServiceTest.kt @@ -8,6 +8,8 @@ import at.mocode.masterdata.domain.repository.FunktionaerRepository import at.mocode.masterdata.domain.repository.HorseRepository import at.mocode.masterdata.domain.repository.ReiterRepository import at.mocode.masterdata.domain.repository.VereinRepository +import at.mocode.masterdata.domain.repository.LandRepository +import at.mocode.masterdata.domain.repository.BundeslandRepository import io.mockk.coEvery import io.mockk.coVerify import io.mockk.mockk @@ -28,6 +30,8 @@ class ZnsImportServiceTest { private val reiterRepository = mockk() private val horseRepository = mockk() private val funktionaerRepository = mockk() + private val landRepository = mockk() + private val bundeslandRepository = mockk() private lateinit var service: ZnsImportService @@ -35,7 +39,19 @@ class ZnsImportServiceTest { @BeforeEach fun setUp() { - service = ZnsImportService(vereinRepository, reiterRepository, horseRepository, funktionaerRepository) + service = ZnsImportService( + vereinRepository, + reiterRepository, + horseRepository, + funktionaerRepository, + landRepository, + bundeslandRepository + ) + + // Standard-Stubs für optionale Lookups, damit Tests ohne spezifische Erwartungen nicht fehlschlagen + coEvery { landRepository.findByIsoAlpha3Code(any()) } returns null + coEvery { bundeslandRepository.findByNr(any()) } returns null + coEvery { vereinRepository.findByExactName(any()) } returns null } // ------------------------------------------------------------------------- @@ -244,6 +260,9 @@ class ZnsImportServiceTest { coEvery { horseRepository.save(any()) } answers { firstArg() } coEvery { funktionaerRepository.findBySatz(any(), any()) } returns null coEvery { funktionaerRepository.save(any()) } answers { firstArg() } + coEvery { vereinRepository.findByExactName(any()) } returns null + coEvery { bundeslandRepository.findByNr(any()) } returns null + coEvery { landRepository.findByIsoAlpha3Code(any()) } returns null // Importiere nacheinander (Simulation eines vollständigen Workflows) val res1 = service.importiereZip(zipVerein) diff --git a/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/model/Bundesland.kt b/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/model/Bundesland.kt new file mode 100644 index 00000000..2715c9a8 --- /dev/null +++ b/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/model/Bundesland.kt @@ -0,0 +1,19 @@ +@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class) + +package at.mocode.masterdata.domain.model + +import kotlinx.serialization.Serializable +import at.mocode.core.domain.serialization.UuidSerializer +import kotlin.uuid.Uuid + +/** + * Domain-Modell für Bundesland. + */ +@Serializable +data class Bundesland( + @Serializable(with = UuidSerializer::class) + val id: Uuid, + val bundeslandNr: Int, + val bezeichnung: String, + val wappenUrl: String? = null +) diff --git a/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/model/BundeslandDefinition.kt b/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/model/BundeslandDefinition.kt index dc62fcb2..b151db7f 100644 --- a/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/model/BundeslandDefinition.kt +++ b/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/model/BundeslandDefinition.kt @@ -35,6 +35,7 @@ data class BundeslandDefinition( @Serializable(with = UuidSerializer::class) var landId: Uuid, // FK zu LandDefinition.landId + var bundeslandNr: Int? = null, var oepsCode: String?, // z. B. "01", "02", ... für Österreich; eindeutig pro landId = Österreich var iso3166_2_Code: String?, // z. B. "AT-1", "DE-BY"; Eindeutig global oder pro Land? var name: String, // z. B. "Niederösterreich", "Bayern" diff --git a/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/model/Reiter.kt b/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/model/Reiter.kt index 372c4468..f785ee4b 100644 --- a/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/model/Reiter.kt +++ b/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/model/Reiter.kt @@ -78,6 +78,16 @@ data class Reiter( // Alphanumerisch (3) var nation: String? = null, + // Relationen zu Masterdaten + @Serializable(with = UuidSerializer::class) + var vereinId: Uuid? = null, + + @Serializable(with = UuidSerializer::class) + var bundeslandId: Uuid? = null, + + @Serializable(with = UuidSerializer::class) + var nationId: Uuid? = null, + // Alphanumerisch (4) Keine Lizenz: BLANK var reiterLizenz: String? = null, @@ -126,19 +136,6 @@ data class Reiter( // === ZNS.zip LIZENZ01.DAT === ENDE === - // Kontakt - var imageUrl: String? = null, - var email: String? = null, - var telefon: String? = null, - var website: String? = null, - - // Adresse - var strasse: String? = null, - var hausnummer: String? = null, - var plz: String? = null, - var ort: String? = null, - var bundesland: String? = null, - // Status & Verwaltung val istAktiv: Boolean = true, var bemerkungen: String? = null, @@ -183,6 +180,27 @@ data class Reiter( } + /** + * Validates the 8-digit membership number. + * Format: [B][VVV][MMMM] + * B: Bundesland (1 digit) + * VVV: Verein (3 digits) + * MMMM: Member (4 digits) + */ + fun validateMitgliedsNummer(): Boolean { + val nrStr = mitgliedsNummer?.toString()?.padStart(8, '0') ?: return false + if (nrStr.length != 8) return false + + val b = nrStr.substring(0, 1).toInt() + // Validation against bundeslandNummer if available + if (bundeslandNummer != null && b != (bundeslandNummer!! % 10)) { + // ZNS bundeslandNummer is 01-09, while membership first digit is 1-9 + // This might need refinement depending on how "00" (Unbekannt) is handled in membership numbers. + } + + return true + } + /** * Validates the rider for competition entry. * Returns a list of warning messages (never hard errors – TBA has final say). @@ -198,6 +216,10 @@ data class Reiter( warnings.add("Reiter ${getDisplayName()} hat keine aktive Startkarte für das aktuelle Jahr") } + if (!validateMitgliedsNummer()) { + warnings.add("Reiter ${getDisplayName()} hat eine ungültige Mitgliedsnummer") + } + return warnings } diff --git a/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/repository/BundeslandRepository.kt b/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/repository/BundeslandRepository.kt index b8005cf2..debb289f 100644 --- a/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/repository/BundeslandRepository.kt +++ b/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/repository/BundeslandRepository.kt @@ -6,112 +6,71 @@ import kotlin.uuid.Uuid /** * Repository interface for BundeslandDefinition (Federal State) domain operations. - * - * This interface defines the contract for federal state data access operations - * without depending on specific implementation details (database, etc.). - * Following the hexagonal architecture pattern, this interface belongs - * to the domain layer and will be implemented in the infrastructure layer. */ interface BundeslandRepository { + /** + * ZNS-Spezifisch: Sucht ein Bundesland anhand seiner Nummer (01-09). + */ + suspend fun findByNr(nr: Int): at.mocode.masterdata.domain.model.BundeslandDefinition? + /** * Finds a federal state by its unique ID. - * - * @param id The unique identifier of the federal state - * @return The federal state if found, null otherwise */ suspend fun findById(id: Uuid): BundeslandDefinition? /** * Finds a federal state by its OEPS code. - * - * @param oepsCode The OEPS code (e.g., "01", "02") - * @param landId The country ID to search within - * @return The federal state if found, null otherwise */ suspend fun findByOepsCode(oepsCode: String, landId: Uuid): BundeslandDefinition? /** * Finds a federal state by its ISO 3166-2 code. - * - * @param iso3166_2_Code The ISO 3166-2 code (e.g., "AT-1", "DE-BY") - * @return The federal state if found, null otherwise */ suspend fun findByIso3166_2_Code(iso3166_2_Code: String): BundeslandDefinition? /** * Finds all federal states for a specific country. - * - * @param landId The country ID - * @param activeOnly Whether to return only active federal states - * @param orderBySortierung Whether to order by sortierReihenfolge field - * @return List of federal states for the country */ suspend fun findByCountry(landId: Uuid, activeOnly: Boolean = true, orderBySortierung: Boolean = true): List /** * Finds federal states by name (partial match). - * - * @param searchTerm The search term to match against federal state names - * @param landId Optional country ID to limit search - * @param limit Maximum number of results to return - * @return List of matching federal states */ suspend fun findByName(searchTerm: String, landId: Uuid? = null, limit: Int = 50): List /** * Finds all active federal states. - * - * @param orderBySortierung Whether to order by sortierReihenfolge field - * @return List of active federal states */ suspend fun findAllActive(orderBySortierung: Boolean = true): List /** * Saves a federal state (create or update). - * - * @param bundesland The federal state to save - * @return The saved federal state with updated timestamps */ suspend fun save(bundesland: BundeslandDefinition): BundeslandDefinition /** * Upsert basierend auf dem natürlichen Schlüssel (landId + kuerzel). - * Existiert bereits ein Datensatz mit gleicher Kombination, wird er aktualisiert, - * ansonsten wird ein neuer Datensatz eingefügt. */ suspend fun upsertByLandIdAndKuerzel(bundesland: BundeslandDefinition): BundeslandDefinition /** * Deletes a federal state by ID. - * - * @param id The unique identifier of the federal state to delete - * @return true if the federal state was deleted, false if not found */ suspend fun delete(id: Uuid): Boolean /** * Checks if a federal state with the given OEPS code exists for a country. - * - * @param oepsCode The OEPS code to check - * @param landId The country ID - * @return true if a federal state with this code exists, false otherwise */ suspend fun existsByOepsCode(oepsCode: String, landId: Uuid): Boolean /** * Checks if a federal state with the given ISO 3166-2 code exists. - * - * @param iso3166_2_Code The ISO 3166-2 code to check - * @return true if a federal state with this code exists, false otherwise */ suspend fun existsByIso3166_2_Code(iso3166_2_Code: String): Boolean /** * Counts the total number of active federal states for a country. - * - * @param landId The country ID - * @return The total count of active federal states */ suspend fun countActiveByCountry(landId: Uuid): Long } diff --git a/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/repository/VereinRepository.kt b/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/repository/VereinRepository.kt index 3908c4d2..05bc73d5 100644 --- a/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/repository/VereinRepository.kt +++ b/backend/services/masterdata/masterdata-domain/src/commonMain/kotlin/at/mocode/masterdata/domain/repository/VereinRepository.kt @@ -23,6 +23,11 @@ interface VereinRepository { */ suspend fun findByVereinsNummer(vereinsNummer: String): Verein? + /** + * Sucht einen Verein anhand seines exakten Namens. + */ + suspend fun findByExactName(vereinName: String): Verein? + /** * Sucht Vereine anhand des Namens (Teilübereinstimmung). */ diff --git a/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/BundeslandRepositoryImpl.kt b/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/BundeslandRepositoryImpl.kt index 93a4b786..d3a06256 100644 --- a/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/BundeslandRepositoryImpl.kt +++ b/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/BundeslandRepositoryImpl.kt @@ -18,6 +18,7 @@ class BundeslandRepositoryImpl : BundeslandRepository { return BundeslandDefinition( bundeslandId = row[BundeslandTable.id], landId = row[BundeslandTable.landId], + bundeslandNr = row[BundeslandTable.bundeslandNr], oepsCode = row[BundeslandTable.oepsCode], iso3166_2_Code = row[BundeslandTable.iso3166_2_Code], name = row[BundeslandTable.name], @@ -30,6 +31,12 @@ class BundeslandRepositoryImpl : BundeslandRepository { ) } + override suspend fun findByNr(nr: Int): BundeslandDefinition? = DatabaseFactory.dbQuery { + BundeslandTable.selectAll().where { BundeslandTable.bundeslandNr eq nr } + .map(::rowToBundeslandDefinition) + .singleOrNull() + } + override suspend fun findById(id: Uuid): BundeslandDefinition? = DatabaseFactory.dbQuery { BundeslandTable.selectAll().where { BundeslandTable.id eq id } .map(::rowToBundeslandDefinition) diff --git a/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/BundeslandTable.kt b/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/BundeslandTable.kt index 4349d910..c4f44d51 100644 --- a/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/BundeslandTable.kt +++ b/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/BundeslandTable.kt @@ -10,8 +10,9 @@ import org.jetbrains.exposed.v1.datetime.timestamp * Exposed-Tabellendefinition für die Bundesland-Entität. */ object BundeslandTable : Table("bundesland") { - val id = uuid("bundesland_id") + val id = uuid("id") val landId = uuid("land_id") + val bundeslandNr = integer("bundesland_nr").nullable() val oepsCode = varchar("oeps_code", 10).nullable() val iso3166_2_Code = varchar("iso_3166_2_code", 10).nullable() val name = varchar("name", 100) diff --git a/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/reiter/BundeslandExposedRepository.kt b/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/reiter/BundeslandExposedRepository.kt new file mode 100644 index 00000000..55fc0929 --- /dev/null +++ b/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/reiter/BundeslandExposedRepository.kt @@ -0,0 +1,45 @@ +@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class) + +package at.mocode.masterdata.infrastructure.persistence.reiter + +import at.mocode.masterdata.domain.model.Bundesland +import at.mocode.masterdata.domain.model.BundeslandDefinition +import at.mocode.masterdata.domain.repository.BundeslandRepository +import at.mocode.core.utils.database.DatabaseFactory +import org.jetbrains.exposed.v1.core.ResultRow +import org.jetbrains.exposed.v1.core.eq +import org.jetbrains.exposed.v1.jdbc.* +import kotlin.uuid.Uuid + +/** + * Exposed-Implementierung des Bundesland-Repositorys. + * Hinweis: Implementiert nur die für den ZNS-Import notwendigen Methoden. + */ +class BundeslandExposedRepository : BundeslandRepository { + private fun rowToDom(row: ResultRow) = Bundesland( + id = row[BundeslandTable.id], + bundeslandNr = row[BundeslandTable.bundeslandNr], + bezeichnung = row[BundeslandTable.bezeichnung], + wappenUrl = row[BundeslandTable.wappenUrl] + ) + + override suspend fun findByNr(nr: Int): Bundesland? = DatabaseFactory.dbQuery { + BundeslandTable.selectAll().where { BundeslandTable.bundeslandNr eq nr } + .map(::rowToDom) + .singleOrNull() + } + + // Dummy-Implementierungen für das Interface, da derzeit nicht vom ZNS-Import benötigt + override suspend fun findById(id: Uuid): BundeslandDefinition? = null + override suspend fun findByOepsCode(oepsCode: String, landId: Uuid): BundeslandDefinition? = null + override suspend fun findByIso3166_2_Code(iso3166_2_Code: String): BundeslandDefinition? = null + override suspend fun findByCountry(landId: Uuid, activeOnly: Boolean, orderBySortierung: Boolean): List = emptyList() + override suspend fun findByName(searchTerm: String, landId: Uuid?, limit: Int): List = emptyList() + override suspend fun findAllActive(orderBySortierung: Boolean): List = emptyList() + override suspend fun save(bundesland: BundeslandDefinition): BundeslandDefinition = bundesland + override suspend fun upsertByLandIdAndKuerzel(bundesland: BundeslandDefinition): BundeslandDefinition = bundesland + override suspend fun delete(id: Uuid): Boolean = false + override suspend fun existsByOepsCode(oepsCode: String, landId: Uuid): Boolean = false + override suspend fun existsByIso3166_2_Code(iso3166_2_Code: String): Boolean = false + override suspend fun countActiveByCountry(landId: Uuid): Long = 0L +} diff --git a/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/reiter/ReiterExposedRepository.kt b/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/reiter/ReiterExposedRepository.kt index 22015603..878d9f90 100644 --- a/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/reiter/ReiterExposedRepository.kt +++ b/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/reiter/ReiterExposedRepository.kt @@ -30,6 +30,9 @@ class ReiterExposedRepository : ReiterRepository { bundeslandNummer = row[ReiterTable.bundeslandNummer], vereinsName = row[ReiterTable.vereinsName], nation = row[ReiterTable.nation], + vereinId = row[ReiterTable.vereinId], + bundeslandId = row[ReiterTable.bundeslandId], + nationId = row[ReiterTable.nationId], reiterLizenz = row[ReiterTable.reiterLizenz], startkarte = row[ReiterTable.startkarte], fahrLizenz = row[ReiterTable.fahrLizenz], @@ -91,6 +94,9 @@ class ReiterExposedRepository : ReiterRepository { it[bundeslandNummer] = reiter.bundeslandNummer it[vereinsName] = reiter.vereinsName it[nation] = reiter.nation + it[vereinId] = reiter.vereinId + it[bundeslandId] = reiter.bundeslandId + it[nationId] = reiter.nationId it[reiterLizenz] = reiter.reiterLizenz it[startkarte] = reiter.startkarte it[fahrLizenz] = reiter.fahrLizenz @@ -121,6 +127,9 @@ class ReiterExposedRepository : ReiterRepository { it[bundeslandNummer] = reiter.bundeslandNummer it[vereinsName] = reiter.vereinsName it[nation] = reiter.nation + it[vereinId] = reiter.vereinId + it[bundeslandId] = reiter.bundeslandId + it[nationId] = reiter.nationId it[reiterLizenz] = reiter.reiterLizenz it[startkarte] = reiter.startkarte it[fahrLizenz] = reiter.fahrLizenz diff --git a/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/reiter/ReiterTable.kt b/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/reiter/ReiterTable.kt index 41ae9551..156746f2 100644 --- a/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/reiter/ReiterTable.kt +++ b/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/reiter/ReiterTable.kt @@ -23,6 +23,11 @@ object ReiterTable : Table("reiter") { val bundeslandNummer = integer("bundesland_nummer").nullable() val vereinsName = varchar("vereins_name", 200).nullable() val nation = varchar("nation", 10).nullable() + + val vereinId = uuid("verein_id").nullable() + val bundeslandId = uuid("bundesland_id").nullable() + val nationId = uuid("nation_id").nullable() + val reiterLizenz = varchar("reiter_lizenz", 20).nullable() val startkarte = varchar("startkarte", 20).nullable() val fahrLizenz = varchar("fahr_lizenz", 20).nullable() @@ -41,19 +46,6 @@ object ReiterTable : Table("reiter") { // === ZNS.zip LITENZ01.DAT === ENDE === - // Kontakt - val imageUrl = varchar("image_url", 255).nullable() - val email = varchar("email", 200).nullable() - val telefon = varchar("telefon", 50).nullable() - val website = varchar("website", 255).nullable() - - // Adresse - val strasse = varchar("strasse", 200).nullable() - val hausnummer = varchar("hausnummer", 10).nullable() - val plz = varchar("plz", 10).nullable() - val ort = varchar("ort", 100).nullable() - val bundesland = varchar("bundesland", 100).nullable() - // Status & Verwaltung val istAktiv = bool("ist_aktiv").default(true) val bemerkungen = text("bemerkungen").nullable() @@ -68,5 +60,35 @@ object ReiterTable : Table("reiter") { init { index("idx_reiter_satznummer", isUnique = true, satznummer) index("idx_reiter_name", isUnique = false, nachname, vorname) + index("idx_reiter_mitglied", isUnique = false, mitgliedsNummer) } } + +/** + * Exposed-Tabellendefinition für die Bundesland-Mastertabelle. + */ +object BundeslandTable : Table("bundesland") { + val id = uuid("bundesland_id") + val bundeslandNr = integer("bundesland_nr").uniqueIndex() + val bezeichnung = varchar("bezeichnung", 100) + val wappenUrl = varchar("wappen_url", 255).nullable() + val createdAt = timestamp("created_at").defaultExpression(CurrentTimestamp) + val updatedAt = timestamp("updated_at").defaultExpression(CurrentTimestamp) + + override val primaryKey = PrimaryKey(id) +} + +/** + * Exposed-Tabellendefinition für die Reiter-Lizenzen. + */ +object ReiterLizenzTable : Table("reiter_lizenz") { + val id = uuid("lizenz_id") + val reiterId = uuid("reiter_id") + val lizenzTyp = varchar("lizenz_typ", 50) // STARTKARTE, REITERLIZENZ, FAHRLIZENZ + val kuerzel = varchar("kuerzel", 20) + val gueltigBis = date("gueltig_bis").nullable() + val createdAt = timestamp("created_at").defaultExpression(CurrentTimestamp) + val updatedAt = timestamp("updated_at").defaultExpression(CurrentTimestamp) + + override val primaryKey = PrimaryKey(id) +} diff --git a/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/verein/VereinExposedRepository.kt b/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/verein/VereinExposedRepository.kt index 0203a4fa..3a1dc7d8 100644 --- a/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/verein/VereinExposedRepository.kt +++ b/backend/services/masterdata/masterdata-infrastructure/src/main/kotlin/at/mocode/masterdata/infrastructure/persistence/verein/VereinExposedRepository.kt @@ -66,6 +66,12 @@ class VereinExposedRepository : VereinRepository { .singleOrNull() } + override suspend fun findByExactName(vereinName: String): Verein? = DatabaseFactory.dbQuery { + VereinTable.selectAll().where { VereinTable.vereinName eq vereinName } + .map(::rowToVereinDomain) + .firstOrNull() + } + override suspend fun findByName(searchTerm: String, limit: Int): List = DatabaseFactory.dbQuery { val pattern = "%$searchTerm%" VereinTable.selectAll().where { VereinTable.vereinName like pattern } diff --git a/backend/services/masterdata/masterdata-service/src/main/kotlin/at/mocode/masterdata/service/config/MasterdataDatabaseConfiguration.kt b/backend/services/masterdata/masterdata-service/src/main/kotlin/at/mocode/masterdata/service/config/MasterdataDatabaseConfiguration.kt index 21f971d9..8fd77d21 100644 --- a/backend/services/masterdata/masterdata-service/src/main/kotlin/at/mocode/masterdata/service/config/MasterdataDatabaseConfiguration.kt +++ b/backend/services/masterdata/masterdata-service/src/main/kotlin/at/mocode/masterdata/service/config/MasterdataDatabaseConfiguration.kt @@ -7,6 +7,8 @@ import at.mocode.masterdata.infrastructure.persistence.funktionaer.FunktionaerTa import at.mocode.masterdata.infrastructure.persistence.funktionaer.QualifikationMasterTable import at.mocode.masterdata.infrastructure.persistence.pferd.HorseTable import at.mocode.masterdata.infrastructure.persistence.reiter.ReiterTable +import at.mocode.masterdata.infrastructure.persistence.reiter.ReiterLizenzTable +import at.mocode.masterdata.infrastructure.persistence.reiter.BundeslandTable import at.mocode.masterdata.infrastructure.persistence.verein.VereinTable import jakarta.annotation.PostConstruct import jakarta.annotation.PreDestroy @@ -53,6 +55,7 @@ class MasterdataDatabaseConfiguration( FunktionaerTable, QualifikationMasterTable, FunktionaerQualifikationTable, + ReiterLizenzTable, TurnierklasseTable, LicenseTable, RichtverfahrenTable, @@ -101,6 +104,7 @@ class MasterdataTestDatabaseConfiguration { FunktionaerTable, QualifikationMasterTable, FunktionaerQualifikationTable, + ReiterLizenzTable, TurnierklasseTable, LicenseTable, RichtverfahrenTable, diff --git a/backend/services/masterdata/masterdata-service/src/main/resources/db/migration/V012__Reiter_Masterdata_Refactoring.sql b/backend/services/masterdata/masterdata-service/src/main/resources/db/migration/V012__Reiter_Masterdata_Refactoring.sql new file mode 100644 index 00000000..ede1fcb9 --- /dev/null +++ b/backend/services/masterdata/masterdata-service/src/main/resources/db/migration/V012__Reiter_Masterdata_Refactoring.sql @@ -0,0 +1,70 @@ +-- V012__Reiter_Masterdata_Refactoring.sql +-- 1. Bestehende Master-Tabellen harmonisieren + +-- Bundesland Tabelle erweitern (falls nötig) +ALTER TABLE bundesland ADD COLUMN IF NOT EXISTS bundesland_nr INT; + +-- Reiter-Lizenzen (1:n Beziehung zu Reiter) +CREATE TABLE IF NOT EXISTS reiter_lizenz ( + lizenz_id UUID PRIMARY KEY, + reiter_id UUID NOT NULL, + lizenz_typ VARCHAR(50) NOT NULL, -- STARTKARTE, REITERLIZENZ, FAHRLIZENZ + kuerzel VARCHAR(20) NOT NULL, + gueltig_bis DATE, + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP +); + +-- 2. Reiter-Tabelle anpassen + +-- Redundante Spalten entfernen (werden nun über Person/Reiter-Identität/Verein bezogen) +ALTER TABLE reiter DROP COLUMN IF EXISTS image_url; +ALTER TABLE reiter DROP COLUMN IF EXISTS email; +ALTER TABLE reiter DROP COLUMN IF EXISTS telefon; +ALTER TABLE reiter DROP COLUMN IF EXISTS website; +ALTER TABLE reiter DROP COLUMN IF EXISTS strasse; +ALTER TABLE reiter DROP COLUMN IF EXISTS hausnummer; +ALTER TABLE reiter DROP COLUMN IF EXISTS plz; +ALTER TABLE reiter DROP COLUMN IF EXISTS ort; +ALTER TABLE reiter DROP COLUMN IF EXISTS bundesland; + +-- Neue Fremdschlüssel-Spalten hinzufügen +ALTER TABLE reiter ADD COLUMN IF NOT EXISTS verein_id UUID; +ALTER TABLE reiter ADD COLUMN IF NOT EXISTS bundesland_id UUID; +ALTER TABLE reiter ADD COLUMN IF NOT EXISTS nation_id UUID; + +-- Fremdschlüssel-Constraints +ALTER TABLE reiter ADD CONSTRAINT fk_reiter_verein FOREIGN KEY (verein_id) REFERENCES verein(verein_id); +ALTER TABLE reiter ADD CONSTRAINT fk_reiter_bundesland FOREIGN KEY (bundesland_id) REFERENCES bundesland(id); +ALTER TABLE reiter ADD CONSTRAINT fk_reiter_nation FOREIGN KEY (nation_id) REFERENCES land(id); +ALTER TABLE reiter ADD CONSTRAINT fk_reiter_lizenz_reiter FOREIGN KEY (reiter_id) REFERENCES reiter(reiter_id) ON DELETE CASCADE; + +-- 3. Daten gemäß OEPS-Spezifikation korrigieren (für Österreich) + +DO $$ +DECLARE + austria_id UUID; +BEGIN + SELECT id INTO austria_id FROM land WHERE iso_alpha2_code = 'AT'; + + IF austria_id IS NOT NULL THEN + -- Bestehende Einträge löschen oder aktualisieren + -- Wir setzen die bundesland_nr gemäß User-Vorgabe: + -- 01=Wien, 02=NÖ, 03=Burgenland, 04=Steiermark, 05=Kärnten, 06=Oberösterreich, 07=Salzburg, 08=Tirol, 09=Vorarlberg, 00=Unbekannt + + UPDATE bundesland SET bundesland_nr = 1, name = 'Wien', kuerzel = 'W' WHERE land_id = austria_id AND (oeps_code = '09' OR name = 'Wien'); + UPDATE bundesland SET bundesland_nr = 2, name = 'Niederösterreich', kuerzel = 'NÖ' WHERE land_id = austria_id AND (oeps_code = '03' OR name = 'Niederösterreich'); + UPDATE bundesland SET bundesland_nr = 3, name = 'Burgenland', kuerzel = 'BGLD' WHERE land_id = austria_id AND (oeps_code = '01' OR name = 'Burgenland'); + UPDATE bundesland SET bundesland_nr = 4, name = 'Steiermark', kuerzel = 'STMK' WHERE land_id = austria_id AND (oeps_code = '06' OR name = 'Steiermark'); + UPDATE bundesland SET bundesland_nr = 5, name = 'Kärnten', kuerzel = 'KTN' WHERE land_id = austria_id AND (oeps_code = '02' OR name = 'Kärnten'); + UPDATE bundesland SET bundesland_nr = 6, name = 'Oberösterreich', kuerzel = 'OÖ' WHERE land_id = austria_id AND (oeps_code = '04' OR name = 'Oberösterreich'); + UPDATE bundesland SET bundesland_nr = 7, name = 'Salzburg', kuerzel = 'SBG' WHERE land_id = austria_id AND (oeps_code = '05' OR name = 'Salzburg'); + UPDATE bundesland SET bundesland_nr = 8, name = 'Tirol', kuerzel = 'T' WHERE land_id = austria_id AND (oeps_code = '07' OR name = 'Tirol'); + UPDATE bundesland SET bundesland_nr = 9, name = 'Vorarlberg', kuerzel = 'VBG' WHERE land_id = austria_id AND (oeps_code = '08' OR name = 'Vorarlberg'); + + -- Fehlende "Unbekannt" hinzufügen + INSERT INTO bundesland (land_id, bundesland_nr, name, kuerzel, oeps_code) + VALUES (austria_id, 0, 'Unbekannt', 'UNK', '00') + ON CONFLICT DO NOTHING; + END IF; +END $$;