Refactor license matrix and tokenizer logic: rename LicenseTable to LizenzTable, replace LicenseMatrixService with LizenzMatrixService, enhance tokenizer with normalized and fallback token handling, improve ZNS import for license extraction, and update related documentation.
Desktop CI — Headless Tests & Build / Compose Desktop — Tests (headless) & Build (push) Waiting to run
Build and Publish Docker Images / build-and-push (., backend/infrastructure/gateway/Dockerfile, api-gateway, api-gateway) (push) Waiting to run
Build and Publish Docker Images / build-and-push (., backend/services/ping/Dockerfile, ping-service, ping-service) (push) Waiting to run
Build and Publish Docker Images / build-and-push (., config/docker/caddy/web-app/Dockerfile, web-app, web-app) (push) Waiting to run
Build and Publish Docker Images / build-and-push (., config/docker/keycloak/Dockerfile, keycloak, keycloak) (push) Waiting to run

This commit is contained in:
2026-04-06 23:51:56 +02:00
parent b7fa2d26a9
commit 7bf89c58d3
25 changed files with 612 additions and 138 deletions
@@ -192,13 +192,25 @@ data class Reiter(
/**
* Checks if the rider holds a license for a specific discipline.
* Simple logic for now: Any non-blank license field counts.
* Strikte Logik: Eine Dressur-Lizenz (RD1..RD3) gilt nur für DRESSUR,
* eine Reit-/Spring-Lizenz (R1..R4) nur für SPRINGEN. FAHREN nutzt die separate Fahr-Lizenz.
*/
fun hasLizenzForSparte(sparte: at.mocode.core.domain.model.SparteE): Boolean {
val lk = lizenzKlasse
return when (sparte) {
at.mocode.core.domain.model.SparteE.DRESSUR -> !reiterLizenz.isNullOrBlank()
at.mocode.core.domain.model.SparteE.SPRINGEN -> !reiterLizenz.isNullOrBlank()
at.mocode.core.domain.model.SparteE.DRESSUR ->
lk == ReiterLizenzKlasseE.RD1 ||
lk == ReiterLizenzKlasseE.RD2 ||
lk == ReiterLizenzKlasseE.RD3
at.mocode.core.domain.model.SparteE.SPRINGEN ->
lk == ReiterLizenzKlasseE.R1 ||
lk == ReiterLizenzKlasseE.R2 ||
lk == ReiterLizenzKlasseE.R3 ||
lk == ReiterLizenzKlasseE.R4
at.mocode.core.domain.model.SparteE.FAHREN -> !fahrLizenz.isNullOrBlank()
else -> hasLizenz()
}
}
@@ -7,7 +7,7 @@ import kotlinx.serialization.Serializable
import kotlin.uuid.Uuid
@Serializable
data class ReitLizenz(
data class Reiterlizenz(
@Serializable(with = UuidSerializer::class)
val lizenzId: Uuid = Uuid.random(),
val code: String,
@@ -3,14 +3,14 @@
package at.mocode.masterdata.domain.repository
import at.mocode.masterdata.domain.model.FahrLizenz
import at.mocode.masterdata.domain.model.ReitLizenz
import at.mocode.masterdata.domain.model.Reiterlizenz
import at.mocode.masterdata.domain.model.Startkarte
/**
* Repository für alle Lizenz-Stammdaten (Reit, Fahr, Startkarten).
*/
interface MasterdataLicenseRepository {
suspend fun findReitLizenzByCode(code: String): ReitLizenz?
interface LizenzRepository {
suspend fun findReitLizenzByCode(code: String): Reiterlizenz?
suspend fun findFahrLizenzByCode(code: String): FahrLizenz?
suspend fun findStartkarteByCode(code: String): Startkarte?
}
@@ -1,57 +0,0 @@
package at.mocode.masterdata.domain.service
import at.mocode.core.domain.model.ReiterLizenzKlasseE
import at.mocode.core.domain.model.SparteE
import at.mocode.masterdata.domain.model.Reiter
import at.mocode.masterdata.domain.model.LicenseMatrixEntry
import at.mocode.masterdata.domain.model.TurnierklasseDefinition
/**
* Standard-Implementierung des [LicenseMatrixService] gemäß ÖTO.
*/
class LicenseMatrixServiceImpl : LicenseMatrixService {
private val classHierarchy = listOf("E", "A", "L", "LM", "M", "S")
override fun isEligible(
reiter: Reiter,
turnierklasse: TurnierklasseDefinition,
sparte: SparteE,
matrix: List<LicenseMatrixEntry>,
alleKlassen: List<TurnierklasseDefinition>
): Boolean {
// 1. Basis-Check: Hat der Reiter überhaupt eine Lizenz für diese Sparte?
if (!reiter.hasLizenzForSparte(sparte)) return false
// 2. Max Turnierklasse aus Matrix ermitteln
val maxClassCode = getMaxTurnierklasse(reiter, sparte, matrix) ?: return false
// 3. Hierarchie-Check (maxClassCode vs. turnierklasse.code)
val maxIndex = classHierarchy.indexOf(maxClassCode)
val targetIndex = classHierarchy.indexOf(turnierklasse.code)
if (maxIndex == -1 || targetIndex == -1) return false
return targetIndex <= maxIndex
}
override fun getMaxTurnierklasse(
reiter: Reiter,
sparte: SparteE,
matrix: List<LicenseMatrixEntry>
): String? {
// Suche passenden Eintrag in der Matrix für (Sparte, Lizenzklasse)
val entry = matrix.find { it.sparte == sparte && it.lizenzKlasse == reiter.lizenzKlasse }
?: matrix.find { it.sparte == SparteE.DRESSUR && sparte == SparteE.DRESSUR && it.lizenzKlasse == reiter.lizenzKlasse } // Fallback/Spezial
?: if (reiter.lizenzKlasse == ReiterLizenzKlasseE.R1 ||
reiter.lizenzKlasse == ReiterLizenzKlasseE.R2 ||
reiter.lizenzKlasse == ReiterLizenzKlasseE.R3 ||
reiter.lizenzKlasse == ReiterLizenzKlasseE.R4) {
// Fallback für Dressur, wenn man eine Springlizenz hat (R1 gilt oft auch als RD1 etc. in manchen Kontexten,
// aber hier schauen wir primär ob die Matrix einen generischen Eintrag hat)
matrix.find { it.sparte == sparte && it.lizenzKlasse == ReiterLizenzKlasseE.LIZENZFREI }
} else null
return entry?.maxTurnierklasseCode
}
}
@@ -8,7 +8,7 @@ import at.mocode.masterdata.domain.model.TurnierklasseDefinition
/**
* Service zur Prüfung der Teilnahmeberechtigung basierend auf der Lizenz-Matrix.
*/
interface LicenseMatrixService {
interface LizenzMatrixService {
/**
* Prüft, ob ein Reiter mit seiner aktuellen Lizenz in einer bestimmten Turnierklasse startberechtigt ist.
@@ -0,0 +1,72 @@
package at.mocode.masterdata.domain.service
import at.mocode.core.domain.model.ReiterLizenzKlasseE
import at.mocode.core.domain.model.SparteE
import at.mocode.masterdata.domain.model.Reiter
import at.mocode.masterdata.domain.model.LicenseMatrixEntry
import at.mocode.masterdata.domain.model.TurnierklasseDefinition
/**
* Standard-Implementierung des [LizenzMatrixService] gemäß ÖTO.
*/
class LizenzMatrixServiceImpl : LizenzMatrixService {
private val classHierarchy = listOf("E", "A", "L", "LM", "M", "S")
override fun isEligible(
reiter: Reiter,
turnierklasse: TurnierklasseDefinition,
sparte: SparteE,
matrix: List<LicenseMatrixEntry>,
alleKlassen: List<TurnierklasseDefinition>
): Boolean {
// 1. Basis-Check: Hat der Reiter überhaupt eine Lizenz für diese Sparte?
if (!reiter.hasLizenzForSparte(sparte)) return false
// 2. Max Turnierklasse aus Matrix ermitteln
val maxClassCode = getMaxTurnierklasse(reiter, sparte, matrix) ?: return false
// 3. Hierarchie-Check (maxClassCode vs. turnierklasse.code)
val maxIndex = classHierarchy.indexOf(maxClassCode)
val targetIndex = classHierarchy.indexOf(turnierklasse.code)
if (maxIndex == -1 || targetIndex == -1) return false
return targetIndex <= maxIndex
}
override fun getMaxTurnierklasse(
reiter: Reiter,
sparte: SparteE,
matrix: List<LicenseMatrixEntry>
): String? {
// 1) Direkter Treffer in Matrix für (Sparte, Lizenzklasse)
val direct = matrix.find { it.sparte == sparte && it.lizenzKlasse == reiter.lizenzKlasse }
if (direct != null) return direct.maxTurnierklasseCode
// 2) Cross-Discipline Mapping (R<->RD) gemäß ÖTO-Äquivalenzen
// Hinweis: RD4 ist im aktuellen Enum nicht modelliert. R4 wird für Zwecke der
// Dressur-Kappung wie RD3 behandelt (max. S), bis RD4 eingeführt wird.
val mappedLizenz = when (sparte) {
SparteE.DRESSUR -> when (reiter.lizenzKlasse) {
ReiterLizenzKlasseE.R1 -> ReiterLizenzKlasseE.RD1
ReiterLizenzKlasseE.R2 -> ReiterLizenzKlasseE.RD2
ReiterLizenzKlasseE.R3, ReiterLizenzKlasseE.R4 -> ReiterLizenzKlasseE.RD3 // RD4 derzeit nicht modelliert
else -> reiter.lizenzKlasse
}
SparteE.SPRINGEN -> when (reiter.lizenzKlasse) {
ReiterLizenzKlasseE.RD1 -> ReiterLizenzKlasseE.R1
ReiterLizenzKlasseE.RD2 -> ReiterLizenzKlasseE.R2
ReiterLizenzKlasseE.RD3 -> ReiterLizenzKlasseE.R3
else -> reiter.lizenzKlasse
}
else -> reiter.lizenzKlasse
}
val cross = matrix.find { it.sparte == sparte && it.lizenzKlasse == mappedLizenz }
if (cross != null) return cross.maxTurnierklasseCode
// 3) Letzter Fallback: LIZENZFREI in gewünschter Sparte
return matrix.find { it.sparte == sparte && it.lizenzKlasse == ReiterLizenzKlasseE.LIZENZFREI }
?.maxTurnierklasseCode
}
}
@@ -13,9 +13,9 @@ import kotlin.test.assertTrue
import kotlin.time.Clock
import kotlin.uuid.Uuid
class LicenseMatrixServiceTest {
class LiznezMatrixServiceTest {
private val service = LicenseMatrixServiceImpl()
private val service = LizenzMatrixServiceImpl()
private val nun = Clock.System.now()
private val matrix = listOf(