feat(validation): integrate ÖTO/FEI rule validations and centralized validators

- Added `OetoValidators` with live-validation for OEPS numbers, FEI-IDs, license classes, and horse data to align with ÖTO/FEI 2026 standards.
- Expanded `ReiterProfilViewModel` and `PferdProfilViewModel` to include validation states (`ValidationResult`) for enhanced form feedback and dirty state tracking.
- Standardized mock data in `Stores.kt` and `NennungModels.kt` to comply with updated validation rules.
- Created `OetoValidatorsTest` to ensure validation logic accuracy (30 unit tests, all green).
- Updated `build.gradle.kts` to include `kotlin.test` dependency for JVM testing.

Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
This commit is contained in:
2026-04-03 09:04:08 +02:00
parent 2c8d16b27f
commit 14b458860c
9 changed files with 631 additions and 28 deletions
@@ -88,12 +88,13 @@ object NennungMockData {
Pferd("1005", "Estrella", "Andalusier", "Schimmel", "Bauer Klaus", "Box 2"),
)
// lizenzNr: OEPS-Mitgliedsnummer im Format OEPS-NNNNNNN oder plain 7-8 Ziffern (Validierungsregeln.md §1)
val reiter = listOf(
Reiter("2001", "Hans", "Müller", "RV Neumarkt", "AT-12345", true, 0.0),
Reiter("2002", "Maria", "Huber", "RV Salzburg", "AT-23456", true, -45.0),
Reiter("2003", "Josef", "Gruber", "RV Wien", "AT-34567", false, 0.0),
Reiter("2004", "Anna", "Wagner", "RV Graz", "AT-45678", true, 120.0),
Reiter("2005", "Klaus", "Bauer", "RV Linz", "AT-56789", true, 0.0),
Reiter("2001", "Hans", "Müller", "RV Neumarkt", "OEPS-1234567", true, 0.0),
Reiter("2002", "Maria", "Huber", "RV Salzburg", "OEPS-2345678", true, -45.0),
Reiter("2003", "Josef", "Gruber", "RV Wien", "OEPS-3456789", false, 0.0),
Reiter("2004", "Anna", "Wagner", "RV Graz", "OEPS-4567890", true, 120.0),
Reiter("2005", "Klaus", "Bauer", "RV Linz", "OEPS-5678901", true, 0.0),
)
val verkaufArtikel = listOf(
@@ -1,5 +1,7 @@
package at.mocode.frontend.features.pferde.presentation
import at.mocode.frontend.core.domain.validation.OetoValidators
import at.mocode.frontend.core.domain.validation.ValidationResult
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
@@ -16,9 +18,16 @@ data class PferdProfilState(
val geburtsjahr: String = "",
val farbe: String = "",
val rasse: String = "",
val validHints: List<String> = emptyList(),
// Validierungsergebnisse (Live-Feedback, ÖTO/FEI Regelwerk)
val oepsNummerValidation: ValidationResult = ValidationResult.Ok,
val feiIdValidation: ValidationResult = ValidationResult.Ok,
val dirty: Boolean = false,
)
) {
/** True wenn kein blockierender Fehler vorliegt (Speichern erlaubt). */
val isValid: Boolean
get() = listOf(oepsNummerValidation, feiIdValidation)
.none { it is ValidationResult.Error }
}
sealed interface PferdProfilIntent {
data class Load(val id: String) : PferdProfilIntent
@@ -90,7 +99,13 @@ class PferdProfilViewModel(
}
private inline fun edit(block: (PferdProfilState) -> PferdProfilState) {
reduce { block(it).copy(dirty = true) }
reduce { s ->
val updated = block(s).copy(dirty = true)
updated.copy(
oepsNummerValidation = OetoValidators.validateOepsNummer(updated.oepsNummer),
feiIdValidation = OetoValidators.validateFeiId(updated.feiId),
)
}
}
private inline fun reduce(block: (PferdProfilState) -> PferdProfilState) {
@@ -1,5 +1,7 @@
package at.mocode.frontend.features.reiter.presentation
import at.mocode.frontend.core.domain.validation.OetoValidators
import at.mocode.frontend.core.domain.validation.ValidationResult
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
@@ -16,9 +18,17 @@ data class ReiterProfilState(
val feiId: String = "",
val lizenzKlasse: String = "",
val verein: String = "",
val validHints: List<String> = emptyList(),
// Validierungsergebnisse (Live-Feedback, ÖTO/FEI Regelwerk)
val oepsNummerValidation: ValidationResult = ValidationResult.Ok,
val feiIdValidation: ValidationResult = ValidationResult.Ok,
val lizenzKlasseValidation: ValidationResult = ValidationResult.Ok,
val dirty: Boolean = false,
)
) {
/** True wenn kein blockierender Fehler vorliegt (Speichern erlaubt). */
val isValid: Boolean
get() = listOf(oepsNummerValidation, feiIdValidation, lizenzKlasseValidation)
.none { it is ValidationResult.Error }
}
sealed interface ReiterProfilIntent {
data class Load(val id: String) : ReiterProfilIntent
@@ -90,7 +100,14 @@ class ReiterProfilViewModel(
}
private inline fun edit(block: (ReiterProfilState) -> ReiterProfilState) {
reduce { block(it).copy(dirty = true) }
reduce { s ->
val updated = block(s).copy(dirty = true)
updated.copy(
oepsNummerValidation = OetoValidators.validateOepsNummer(updated.oepsNummer),
feiIdValidation = OetoValidators.validateFeiId(updated.feiId),
lizenzKlasseValidation = OetoValidators.validateLizenzklasse(updated.lizenzKlasse),
)
}
}
private inline fun reduce(block: (ReiterProfilState) -> ReiterProfilState) {