feat(tests): add QA test suites for onboarding and departmental logic validation
- **Onboarding (B-2):** Extracted `OnboardingValidator` and added `OnboardingValidatorTest` for edge-case validations (17 new unit tests: field guards, double-click prevention, cancel/reset behavior, `rememberSaveable` regression fix). - **Departmental Logic (B-3):** Extended `AbteilungsRegelServiceTest` with 14 new tests covering CSN-C-NEU splitting logic (≤95 cm: license-free/licensed, ≥100 cm: R1/R2+), Caprilli regressions, and organizational/separate award scenarios. - Updated `AbteilungsRegelService.kt` to implement CSN-C-NEU logic and added `ORGANISATORISCH` + `SEPARATE_SIEGEREHRUNG` enums for new rules. - Updated Changelog and QA roadmap with completed tasks. Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
This commit is contained in:
+37
-1
@@ -5,6 +5,7 @@ package at.mocode.entries.domain.service
|
||||
import at.mocode.core.domain.model.AbteilungsTeilungsTypE
|
||||
import at.mocode.core.domain.model.LizenzKlasseE
|
||||
import at.mocode.core.domain.model.PruefungsTypE
|
||||
import at.mocode.core.domain.model.SparteE
|
||||
import at.mocode.entries.domain.model.DomAbteilung
|
||||
import at.mocode.entries.domain.model.DomBewerb
|
||||
import at.mocode.masterdata.domain.model.DomReiter
|
||||
@@ -59,16 +60,51 @@ class AbteilungsRegelService {
|
||||
|
||||
AbteilungsTeilungsTypE.STRUKTURELL -> {
|
||||
// Bei strukturellen Teilungen (z.B. Caprilli oder CSN-C-NEU)
|
||||
// Hier müsste eine detailliertere Prüfung der bewerb.pruefungsTyp erfolgen.
|
||||
if (bewerb.pruefungsTyp == PruefungsTypE.CAPRILLI) {
|
||||
val istLizenzfrei = reiter.lizenzKlasse == LizenzKlasseE.LIZENZFREI
|
||||
if (istLizenzfrei) abteilungen.find { it.abteilungsNummer == 1 }
|
||||
else abteilungen.find { it.abteilungsNummer == 2 }
|
||||
} else if (bewerb.sparte == SparteE.SPRINGEN && bewerb.hoeheCm != null) {
|
||||
// CSN-C-NEU Pflicht-Teilung gemäß ÖTO § 231:
|
||||
// ≤ 95 cm: Abt. 1 = „ohne Lizenz" (LIZENZFREI), Abt. 2 = „mit Lizenz" (R1+)
|
||||
// ≥ 100 cm: nur R1+ erlaubt → alle in Abt. 1 (R1/R2+)
|
||||
val hoehe = bewerb.hoeheCm!!
|
||||
if (hoehe <= 95) {
|
||||
val istLizenzfrei = reiter.lizenzKlasse == LizenzKlasseE.LIZENZFREI
|
||||
if (istLizenzfrei) {
|
||||
abteilungen.find { it.bezeichnung?.contains("ohne", ignoreCase = true) == true }
|
||||
?: abteilungen.minByOrNull { it.abteilungsNummer }
|
||||
} else {
|
||||
abteilungen.find { it.bezeichnung?.contains("mit", ignoreCase = true) == true }
|
||||
?: abteilungen.maxByOrNull { it.abteilungsNummer }
|
||||
}
|
||||
} else {
|
||||
// ≥ 100 cm: Pflicht-Teilung R1 / R2+
|
||||
val istR1 = reiter.lizenzKlasse == LizenzKlasseE.R1
|
||||
if (istR1) {
|
||||
abteilungen.find { it.bezeichnung?.contains("R1", ignoreCase = false) == true }
|
||||
?: abteilungen.minByOrNull { it.abteilungsNummer }
|
||||
} else {
|
||||
abteilungen.find { abt ->
|
||||
val bez = abt.bezeichnung?.lowercase() ?: ""
|
||||
bez.contains("r2") || bez.contains("r2+")
|
||||
} ?: abteilungen.maxByOrNull { it.abteilungsNummer }
|
||||
}
|
||||
}
|
||||
} else {
|
||||
abteilungen.firstOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
// Organisatorische Teilung: Reiter werden nach Startplatznummer auf Abteilungen verteilt,
|
||||
// die Rangliste wird am Ende zusammengeführt (§ 39 Abs. 3).
|
||||
// Zuweisung erfolgt durch den Caller (z.B. nach Startnummer-Modulo) – hier Fallback auf erste Abt.
|
||||
AbteilungsTeilungsTypE.ORGANISATORISCH -> abteilungen.minByOrNull { it.abteilungsNummer }
|
||||
|
||||
// Separate Siegerehrung: Jede Abteilung hat eine eigene Platzierung, keine Zusammenführung.
|
||||
// Zuweisung erfolgt durch den Caller – hier Fallback auf erste Abt.
|
||||
AbteilungsTeilungsTypE.SEPARATE_SIEGEREHRUNG -> abteilungen.minByOrNull { it.abteilungsNummer }
|
||||
|
||||
else -> abteilungen.firstOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
+152
-2
@@ -61,9 +61,158 @@ class AbteilungsRegelServiceTest {
|
||||
assertTrue(warnings[0].contains("WARN_BEWERB_PFLICHT_TEILUNG_ERFORDERLICH"))
|
||||
}
|
||||
|
||||
// ─── B-3: CSN-C-NEU ≤ 95 cm ────────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
fun `B3 CSN-C-NEU 95cm LIZENZFREI wird Abteilung ohne Lizenz zugewiesen`() {
|
||||
val bewerb = createBewerb(teilungsTyp = AbteilungsTeilungsTypE.STRUKTURELL, hoeheCm = 95)
|
||||
val abtOhne = createAbteilung(bewerb.bewerbId, 1, "ohne Lizenz")
|
||||
val abtMit = createAbteilung(bewerb.bewerbId, 2, "mit Lizenz")
|
||||
val reiter = createReiter(LizenzKlasseE.LIZENZFREI)
|
||||
|
||||
val result = service.bestimmeAbteilung(bewerb, listOf(abtOhne, abtMit), reiter)
|
||||
|
||||
assertEquals(abtOhne.abteilungId, result?.abteilungId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `B3 CSN-C-NEU 95cm R1-Reiter wird Abteilung mit Lizenz zugewiesen`() {
|
||||
val bewerb = createBewerb(teilungsTyp = AbteilungsTeilungsTypE.STRUKTURELL, hoeheCm = 95)
|
||||
val abtOhne = createAbteilung(bewerb.bewerbId, 1, "ohne Lizenz")
|
||||
val abtMit = createAbteilung(bewerb.bewerbId, 2, "mit Lizenz")
|
||||
val reiter = createReiter(LizenzKlasseE.R1)
|
||||
|
||||
val result = service.bestimmeAbteilung(bewerb, listOf(abtOhne, abtMit), reiter)
|
||||
|
||||
assertEquals(abtMit.abteilungId, result?.abteilungId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `B3 CSN-C-NEU Grenzfall 90cm LIZENZFREI bleibt in ohne-Lizenz-Abteilung`() {
|
||||
val bewerb = createBewerb(teilungsTyp = AbteilungsTeilungsTypE.STRUKTURELL, hoeheCm = 90)
|
||||
val abtOhne = createAbteilung(bewerb.bewerbId, 1, "ohne Lizenz")
|
||||
val abtMit = createAbteilung(bewerb.bewerbId, 2, "mit Lizenz")
|
||||
|
||||
val result = service.bestimmeAbteilung(bewerb, listOf(abtOhne, abtMit), createReiter(LizenzKlasseE.LIZENZFREI))
|
||||
|
||||
assertEquals(abtOhne.abteilungId, result?.abteilungId)
|
||||
}
|
||||
|
||||
// ─── B-3: CSN-C-NEU ≥ 100 cm ───────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
fun `B3 CSN-C-NEU 100cm R1-Reiter wird R1-Abteilung zugewiesen`() {
|
||||
val bewerb = createBewerb(teilungsTyp = AbteilungsTeilungsTypE.STRUKTURELL, hoeheCm = 100)
|
||||
val abtR1 = createAbteilung(bewerb.bewerbId, 1, "R1")
|
||||
val abtR2 = createAbteilung(bewerb.bewerbId, 2, "R2+")
|
||||
val reiter = createReiter(LizenzKlasseE.R1)
|
||||
|
||||
val result = service.bestimmeAbteilung(bewerb, listOf(abtR1, abtR2), reiter)
|
||||
|
||||
assertEquals(abtR1.abteilungId, result?.abteilungId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `B3 CSN-C-NEU 100cm R2-Reiter wird R2plus-Abteilung zugewiesen`() {
|
||||
val bewerb = createBewerb(teilungsTyp = AbteilungsTeilungsTypE.STRUKTURELL, hoeheCm = 100)
|
||||
val abtR1 = createAbteilung(bewerb.bewerbId, 1, "R1")
|
||||
val abtR2 = createAbteilung(bewerb.bewerbId, 2, "R2+")
|
||||
val reiter = createReiter(LizenzKlasseE.R2)
|
||||
|
||||
val result = service.bestimmeAbteilung(bewerb, listOf(abtR1, abtR2), reiter)
|
||||
|
||||
assertEquals(abtR2.abteilungId, result?.abteilungId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `B3 CSN-C-NEU Grenzfall 110cm R1-Reiter bleibt in R1-Abteilung`() {
|
||||
val bewerb = createBewerb(teilungsTyp = AbteilungsTeilungsTypE.STRUKTURELL, hoeheCm = 110)
|
||||
val abtR1 = createAbteilung(bewerb.bewerbId, 1, "R1")
|
||||
val abtR2 = createAbteilung(bewerb.bewerbId, 2, "R2+")
|
||||
|
||||
val result = service.bestimmeAbteilung(bewerb, listOf(abtR1, abtR2), createReiter(LizenzKlasseE.R1))
|
||||
|
||||
assertEquals(abtR1.abteilungId, result?.abteilungId)
|
||||
}
|
||||
|
||||
// ─── B-3: ORGANISATORISCH ───────────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
fun `B3 ORGANISATORISCH liefert immer die erste Abteilung als Fallback`() {
|
||||
val bewerb = createBewerb(teilungsTyp = AbteilungsTeilungsTypE.ORGANISATORISCH)
|
||||
val abt1 = createAbteilung(bewerb.bewerbId, 1, "Abteilung 1")
|
||||
val abt2 = createAbteilung(bewerb.bewerbId, 2, "Abteilung 2")
|
||||
|
||||
val result = service.bestimmeAbteilung(bewerb, listOf(abt1, abt2), createReiter())
|
||||
|
||||
assertEquals(abt1.abteilungId, result?.abteilungId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `B3 ORGANISATORISCH Gesamtrangliste - validateStrukturelleVollstaendigkeit gibt keine Warnung`() {
|
||||
val bewerb = createBewerb(teilungsTyp = AbteilungsTeilungsTypE.ORGANISATORISCH)
|
||||
val abt1 = createAbteilung(bewerb.bewerbId, 1, starterAnzahl = 20)
|
||||
val abt2 = createAbteilung(bewerb.bewerbId, 2, starterAnzahl = 20)
|
||||
|
||||
val warnings = service.validateStrukturelleVollstaendigkeit(bewerb, listOf(abt1, abt2))
|
||||
|
||||
assertTrue(warnings.isEmpty())
|
||||
}
|
||||
|
||||
// ─── B-3: SEPARATE_SIEGEREHRUNG ─────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
fun `B3 SEPARATE_SIEGEREHRUNG liefert immer die erste Abteilung als Fallback`() {
|
||||
val bewerb = createBewerb(teilungsTyp = AbteilungsTeilungsTypE.SEPARATE_SIEGEREHRUNG)
|
||||
val abt1 = createAbteilung(bewerb.bewerbId, 1, "Klasse A")
|
||||
val abt2 = createAbteilung(bewerb.bewerbId, 2, "Klasse B")
|
||||
|
||||
val result = service.bestimmeAbteilung(bewerb, listOf(abt1, abt2), createReiter())
|
||||
|
||||
assertEquals(abt1.abteilungId, result?.abteilungId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `B3 SEPARATE_SIEGEREHRUNG - validateStrukturelleVollstaendigkeit gibt keine Warnung bei 2 Abteilungen`() {
|
||||
val bewerb = createBewerb(teilungsTyp = AbteilungsTeilungsTypE.SEPARATE_SIEGEREHRUNG)
|
||||
val abt1 = createAbteilung(bewerb.bewerbId, 1, starterAnzahl = 15)
|
||||
val abt2 = createAbteilung(bewerb.bewerbId, 2, starterAnzahl = 15)
|
||||
|
||||
val warnings = service.validateStrukturelleVollstaendigkeit(bewerb, listOf(abt1, abt2))
|
||||
|
||||
assertTrue(warnings.isEmpty())
|
||||
}
|
||||
|
||||
// ─── B-3: Caprilli (Regressions-Absicherung) ────────────────────────────────
|
||||
|
||||
@Test
|
||||
fun `B3 Caprilli LIZENZFREI wird Abteilung 1 zugewiesen`() {
|
||||
val bewerb = createBewerb(pruefungsTyp = PruefungsTypE.CAPRILLI, teilungsTyp = AbteilungsTeilungsTypE.STRUKTURELL)
|
||||
val abt1 = createAbteilung(bewerb.bewerbId, 1, "ohne Lizenz")
|
||||
val abt2 = createAbteilung(bewerb.bewerbId, 2, "mit Lizenz")
|
||||
|
||||
val result = service.bestimmeAbteilung(bewerb, listOf(abt1, abt2), createReiter(LizenzKlasseE.LIZENZFREI))
|
||||
|
||||
assertEquals(abt1.abteilungId, result?.abteilungId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `B3 Caprilli R1-Reiter wird Abteilung 2 zugewiesen`() {
|
||||
val bewerb = createBewerb(pruefungsTyp = PruefungsTypE.CAPRILLI, teilungsTyp = AbteilungsTeilungsTypE.STRUKTURELL)
|
||||
val abt1 = createAbteilung(bewerb.bewerbId, 1, "ohne Lizenz")
|
||||
val abt2 = createAbteilung(bewerb.bewerbId, 2, "mit Lizenz")
|
||||
|
||||
val result = service.bestimmeAbteilung(bewerb, listOf(abt1, abt2), createReiter(LizenzKlasseE.R1))
|
||||
|
||||
assertEquals(abt2.abteilungId, result?.abteilungId)
|
||||
}
|
||||
|
||||
// ─── Hilfsmethoden ──────────────────────────────────────────────────────────
|
||||
|
||||
private fun createBewerb(
|
||||
pruefungsTyp: PruefungsTypE = PruefungsTypE.SPRINGEN_UEBRIG,
|
||||
teilungsTyp: AbteilungsTeilungsTypE = AbteilungsTeilungsTypE.KEINE
|
||||
teilungsTyp: AbteilungsTeilungsTypE = AbteilungsTeilungsTypE.KEINE,
|
||||
hoeheCm: Int? = null
|
||||
) = DomBewerb(
|
||||
turnierId = Uuid.random(),
|
||||
bewerbNummer = 1,
|
||||
@@ -71,7 +220,8 @@ class AbteilungsRegelServiceTest {
|
||||
sparte = SparteE.SPRINGEN,
|
||||
turnierkategorie = TurnierkategorieE.B,
|
||||
pruefungsTyp = pruefungsTyp,
|
||||
teilungsTyp = teilungsTyp
|
||||
teilungsTyp = teilungsTyp,
|
||||
hoeheCm = hoeheCm
|
||||
)
|
||||
|
||||
private fun createAbteilung(
|
||||
|
||||
Reference in New Issue
Block a user