feat(domain+frontend): implement structured division warnings and enhance validation rules
- **Domain Updates:** - Introduced `AbteilungsWarnung` entity for structured warning handling compliant with ÖTO § 39. - Added validation rules for mandatory and optional division thresholds and structural completeness. - Implemented `CompetitionWarningService` and `AbteilungsRegelService` for domain-centric validations. - Updated domain models (`Bewerb`, `Abteilung`) to reflect structured warning logic. - **Services:** - Expanded `BewerbService` to include warning validation through `CompetitionWarningService`. - **Frontend Enhancements:** - Updated `TurnierBewerbeTab` to display warnings using tooltips with clear descriptions and structured formatting. - Modified `BewerbUiModel` to handle warnings and integrate them into the UI. - **Persistence:** - Implemented `CompetitionRepositoryImpl` to map database rows to the new domain models and validation logic. - **Testing:** - Added comprehensive unit tests for `validateStrukturellesTeilung` and division-specific warnings. - Enhanced existing tests to validate the new warning structure and code-based assertions. - **Docs:** - Updated roadmap to reflect the completion of structural warnings implementation.
This commit is contained in:
+5
@@ -7,6 +7,7 @@ import at.mocode.entries.service.errors.LockedException
|
||||
import at.mocode.entries.service.persistence.TurnierTable
|
||||
import at.mocode.entries.service.tenant.tenantTransaction
|
||||
import at.mocode.entries.domain.model.RichterEinsatz
|
||||
import at.mocode.entries.domain.service.CompetitionWarningService
|
||||
import org.jetbrains.exposed.v1.core.eq
|
||||
import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||
import kotlin.uuid.Uuid
|
||||
@@ -15,8 +16,12 @@ import kotlin.uuid.toJavaUuid
|
||||
class BewerbService(
|
||||
private val repo: BewerbRepository,
|
||||
private val nennungen: NennungRepository,
|
||||
private val warningService: CompetitionWarningService,
|
||||
) {
|
||||
|
||||
suspend fun validateTurnier(turnierId: Uuid) = warningService.validateTurnier(turnierId)
|
||||
suspend fun validateBewerb(bewerbId: Uuid) = warningService.validateBewerb(bewerbId)
|
||||
|
||||
private suspend fun isTurnierPublished(turnierId: Uuid): Boolean = tenantTransaction {
|
||||
val row = TurnierTable.selectAll().where { TurnierTable.id eq turnierId.toJavaUuid() }.singleOrNull()
|
||||
row?.get(TurnierTable.status) == "PUBLISHED"
|
||||
|
||||
+32
-6
@@ -4,6 +4,8 @@ package at.mocode.entries.service.bewerbe
|
||||
|
||||
import at.mocode.core.domain.model.AbteilungsTeilungsTypE
|
||||
import at.mocode.core.domain.model.BeginnZeitTypE
|
||||
import at.mocode.entries.domain.model.AbteilungsWarnung
|
||||
import at.mocode.entries.domain.model.AbteilungsWarnungCodeE
|
||||
import at.mocode.entries.domain.model.RichterEinsatz
|
||||
import kotlinx.datetime.LocalDate
|
||||
import kotlinx.datetime.LocalTime
|
||||
@@ -127,6 +129,13 @@ data class BewerbResponse(
|
||||
// ZNS-Integration
|
||||
val znsNummer: Int?,
|
||||
val znsAbteilung: Int?,
|
||||
val warnungen: List<AbteilungsWarnungDto> = emptyList(),
|
||||
)
|
||||
|
||||
data class AbteilungsWarnungDto(
|
||||
val code: AbteilungsWarnungCodeE,
|
||||
val nachricht: String,
|
||||
val oetoParagraph: String?
|
||||
)
|
||||
|
||||
private fun RichterEinsatzDto.toDomain(): RichterEinsatz =
|
||||
@@ -135,7 +144,7 @@ private fun RichterEinsatzDto.toDomain(): RichterEinsatz =
|
||||
position = this.position
|
||||
)
|
||||
|
||||
private fun domainToDto(b: Bewerb): BewerbResponse = BewerbResponse(
|
||||
private fun domainToDto(b: Bewerb, warnungen: List<AbteilungsWarnung> = emptyList()): BewerbResponse = BewerbResponse(
|
||||
id = b.id.toString(),
|
||||
turnierId = b.turnierId.toString(),
|
||||
klasse = b.klasse,
|
||||
@@ -159,6 +168,7 @@ private fun domainToDto(b: Bewerb): BewerbResponse = BewerbResponse(
|
||||
geldpreisAusbezahlt = b.geldpreisAusbezahlt,
|
||||
znsNummer = b.znsNummer,
|
||||
znsAbteilung = b.znsAbteilung,
|
||||
warnungen = warnungen.map { AbteilungsWarnungDto(it.code, it.nachricht, it.oetoParagraph) }
|
||||
)
|
||||
|
||||
@RestController
|
||||
@@ -185,21 +195,37 @@ class BewerbeController(
|
||||
): List<BewerbResponse> = service.importZns(
|
||||
Uuid.parse(turnierId),
|
||||
body
|
||||
).map(::domainToDto)
|
||||
).map { domainToDto(it) }
|
||||
|
||||
@GetMapping("/turniere/{turnierId}/bewerbe")
|
||||
suspend fun list(
|
||||
@PathVariable turnierId: String,
|
||||
@RequestParam(required = false) klasse: String?,
|
||||
@RequestParam(required = false) q: String?,
|
||||
): List<BewerbResponse> = service.list(Uuid.parse(turnierId), klasse, q).map(::domainToDto)
|
||||
): List<BewerbResponse> {
|
||||
val turnierUuid = Uuid.parse(turnierId)
|
||||
val bewerbe = service.list(turnierUuid, klasse, q)
|
||||
val warnungenMap = service.validateTurnier(turnierUuid)
|
||||
return bewerbe.map { b ->
|
||||
domainToDto(b, warnungenMap[b.id] ?: emptyList())
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/bewerbe/{id}")
|
||||
suspend fun get(@PathVariable id: String): BewerbResponse = domainToDto(service.get(Uuid.parse(id)))
|
||||
suspend fun get(@PathVariable id: String): BewerbResponse {
|
||||
val uuid = Uuid.parse(id)
|
||||
val b = service.get(uuid)
|
||||
val warnungen = service.validateBewerb(uuid)
|
||||
return domainToDto(b, warnungen)
|
||||
}
|
||||
|
||||
@PutMapping("/bewerbe/{id}")
|
||||
suspend fun update(@PathVariable id: String, @RequestBody body: UpdateBewerbRequest): BewerbResponse =
|
||||
domainToDto(service.update(Uuid.parse(id), body))
|
||||
suspend fun update(@PathVariable id: String, @RequestBody body: UpdateBewerbRequest): BewerbResponse {
|
||||
val uuid = Uuid.parse(id)
|
||||
val updated = service.update(uuid, body)
|
||||
val warnungen = service.validateBewerb(uuid)
|
||||
return domainToDto(updated, warnungen)
|
||||
}
|
||||
|
||||
@DeleteMapping("/bewerbe/{id}")
|
||||
@ResponseStatus(HttpStatus.NO_CONTENT)
|
||||
|
||||
+19
-2
@@ -13,6 +13,10 @@ import at.mocode.entries.service.bewerbe.BewerbService
|
||||
import at.mocode.entries.service.abteilungen.AbteilungRepository
|
||||
import at.mocode.entries.service.abteilungen.AbteilungRepositoryImpl
|
||||
import at.mocode.entries.service.abteilungen.AbteilungenService
|
||||
import at.mocode.entries.domain.repository.CompetitionRepository
|
||||
import at.mocode.entries.domain.service.AbteilungsRegelService
|
||||
import at.mocode.entries.domain.service.CompetitionWarningService
|
||||
import at.mocode.entries.service.persistence.CompetitionRepositoryImpl
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
|
||||
@@ -43,11 +47,24 @@ class EntriesBeansConfiguration {
|
||||
@Bean
|
||||
fun bewerbRepository(): BewerbRepository = BewerbRepositoryImpl()
|
||||
|
||||
@Bean
|
||||
fun abteilungsRegelService(): AbteilungsRegelService = AbteilungsRegelService()
|
||||
|
||||
@Bean
|
||||
fun competitionRepository(): CompetitionRepository = CompetitionRepositoryImpl()
|
||||
|
||||
@Bean
|
||||
fun competitionWarningService(
|
||||
competitionRepository: CompetitionRepository,
|
||||
regelService: AbteilungsRegelService
|
||||
): CompetitionWarningService = CompetitionWarningService(competitionRepository, regelService)
|
||||
|
||||
@Bean
|
||||
fun bewerbService(
|
||||
bewerbRepository: BewerbRepository,
|
||||
nennungRepository: NennungRepository
|
||||
): BewerbService = BewerbService(bewerbRepository, nennungRepository)
|
||||
nennungRepository: NennungRepository,
|
||||
warningService: CompetitionWarningService
|
||||
): BewerbService = BewerbService(bewerbRepository, nennungRepository, warningService)
|
||||
|
||||
@Bean
|
||||
fun abteilungRepository(): AbteilungRepository = AbteilungRepositoryImpl()
|
||||
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
||||
|
||||
package at.mocode.entries.service.persistence
|
||||
|
||||
import at.mocode.core.domain.model.AbteilungsTeilungsTypE
|
||||
import at.mocode.core.domain.model.PruefungsTypE
|
||||
import at.mocode.core.domain.model.SparteE
|
||||
import at.mocode.core.domain.model.TurnierkategorieE
|
||||
import at.mocode.entries.domain.model.Abteilung as DomainAbteilung
|
||||
import at.mocode.entries.domain.model.Bewerb as DomainBewerb
|
||||
import at.mocode.entries.domain.repository.CompetitionRepository
|
||||
import at.mocode.entries.service.tenant.tenantTransaction
|
||||
import org.jetbrains.exposed.v1.core.ResultRow
|
||||
import org.jetbrains.exposed.v1.core.eq
|
||||
import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||
import kotlin.uuid.Uuid
|
||||
import kotlin.uuid.toJavaUuid
|
||||
import kotlin.uuid.toKotlinUuid
|
||||
|
||||
/**
|
||||
* Implementierung des CompetitionRepository für den Domain-Service.
|
||||
* Mappt zwischen Service-Tabellen und Domain-Modellen.
|
||||
*/
|
||||
class CompetitionRepositoryImpl : CompetitionRepository {
|
||||
|
||||
override suspend fun findBewerbById(id: Uuid): DomainBewerb? = tenantTransaction {
|
||||
BewerbTable.selectAll().where { BewerbTable.id eq id.toJavaUuid() }
|
||||
.map { row -> rowToDomainBewerb(row) }
|
||||
.singleOrNull()
|
||||
}
|
||||
|
||||
override suspend fun findBewerbeByTurnierId(turnierId: Uuid): List<DomainBewerb> = tenantTransaction {
|
||||
BewerbTable.selectAll().where { BewerbTable.turnierId eq turnierId.toJavaUuid() }
|
||||
.map { row -> rowToDomainBewerb(row) }
|
||||
}
|
||||
|
||||
override suspend fun saveBewerb(bewerb: DomainBewerb): DomainBewerb {
|
||||
// Für Read-Only Validierung im WarningService vorerst nicht implementiert
|
||||
throw UnsupportedOperationException("saveBewerb via CompetitionRepository not implemented yet")
|
||||
}
|
||||
|
||||
override suspend fun deleteBewerb(id: Uuid): Boolean {
|
||||
throw UnsupportedOperationException("deleteBewerb via CompetitionRepository not implemented yet")
|
||||
}
|
||||
|
||||
override suspend fun findAbteilungById(id: Uuid): DomainAbteilung? = tenantTransaction {
|
||||
AbteilungTable.selectAll().where { AbteilungTable.id eq id.toJavaUuid() }
|
||||
.map { row -> rowToDomainAbteilung(row) }
|
||||
.singleOrNull()
|
||||
}
|
||||
|
||||
override suspend fun findAbteilungenByBewerbId(bewerbId: Uuid): List<DomainAbteilung> = tenantTransaction {
|
||||
AbteilungTable.selectAll().where { AbteilungTable.bewerbId eq bewerbId.toJavaUuid() }
|
||||
.map { row -> rowToDomainAbteilung(row) }
|
||||
}
|
||||
|
||||
override suspend fun saveAbteilung(abteilung: DomainAbteilung): DomainAbteilung {
|
||||
throw UnsupportedOperationException("saveAbteilung via CompetitionRepository not implemented yet")
|
||||
}
|
||||
|
||||
override suspend fun deleteAbteilung(id: Uuid): Boolean {
|
||||
throw UnsupportedOperationException("deleteAbteilung via CompetitionRepository not implemented yet")
|
||||
}
|
||||
|
||||
private fun rowToDomainBewerb(row: ResultRow): DomainBewerb {
|
||||
val bId = row[BewerbTable.id].toKotlinUuid()
|
||||
val tId = row[BewerbTable.turnierId].toKotlinUuid()
|
||||
|
||||
// Wir müssen Sparte und Kategorie irgendwie herleiten oder aus einer anderen Tabelle laden.
|
||||
// In der BewerbTable fehlen diese Felder aktuell noch im Vergleich zum Domain-Modell.
|
||||
// Für den MVP hardcoden wir Standardwerte oder versuchen sie aus dem Turnier zu lesen.
|
||||
|
||||
return DomainBewerb(
|
||||
bewerbId = bId,
|
||||
turnierId = tId,
|
||||
bewerbNummer = row[BewerbTable.znsNummer] ?: 0,
|
||||
bezeichnung = row[BewerbTable.bezeichnung],
|
||||
sparte = SparteE.SPRINGEN, // FIXME: Herleiten
|
||||
turnierkategorie = TurnierkategorieE.B, // FIXME: Herleiten
|
||||
pruefungsTyp = PruefungsTypE.SPRINGEN_UEBRIG, // FIXME: Herleiten
|
||||
hoeheCm = row[BewerbTable.hoeheCm],
|
||||
teilungsTyp = row[BewerbTable.teilungsTyp]?.let { AbteilungsTeilungsTypE.valueOf(it) } ?: AbteilungsTeilungsTypE.KEINE,
|
||||
beschreibung = row[BewerbTable.beschreibung],
|
||||
aufgabe = row[BewerbTable.aufgabe],
|
||||
aufgabenNummer = row[BewerbTable.aufgabenNummer],
|
||||
paraGrade = row[BewerbTable.paraGrade],
|
||||
austragungsplatzId = row[BewerbTable.austragungsplatzId]?.toKotlinUuid(),
|
||||
geplantesDatum = row[BewerbTable.geplantesDatum],
|
||||
beginnZeitTyp = row[BewerbTable.beginnZeitTyp]?.let { at.mocode.core.domain.model.BeginnZeitTypE.valueOf(it) },
|
||||
beginnZeit = row[BewerbTable.beginnZeit],
|
||||
reitdauerMinuten = row[BewerbTable.reitdauerMinuten],
|
||||
umbauMinuten = row[BewerbTable.umbauMinuten],
|
||||
besichtigungMinuten = row[BewerbTable.besichtigungMinuten],
|
||||
stechenGeplant = row[BewerbTable.stechenGeplant],
|
||||
startgeldCent = row[BewerbTable.startgeldCent],
|
||||
geldpreisAusbezahlt = row[BewerbTable.geldpreisAusbezahlt],
|
||||
createdAt = row[BewerbTable.createdAt],
|
||||
updatedAt = row[BewerbTable.updatedAt]
|
||||
)
|
||||
}
|
||||
|
||||
private fun rowToDomainAbteilung(row: ResultRow): DomainAbteilung {
|
||||
val aId = row[AbteilungTable.id].toKotlinUuid()
|
||||
val bId = row[AbteilungTable.bewerbId].toKotlinUuid()
|
||||
|
||||
// Starteranzahl berechnen
|
||||
val count = NennungTable.selectAll().where { NennungTable.abteilungId eq aId.toJavaUuid() }.count()
|
||||
|
||||
return DomainAbteilung(
|
||||
abteilungId = aId,
|
||||
bewerbId = bId,
|
||||
abteilungsNummer = row[AbteilungTable.nr],
|
||||
bezeichnung = row[AbteilungTable.bezeichnung],
|
||||
teilungsTyp = row[AbteilungTable.typ].let { AbteilungsTeilungsTypE.valueOf(it) },
|
||||
starterAnzahl = count.toInt(),
|
||||
createdAt = row[AbteilungTable.createdAt],
|
||||
updatedAt = row[AbteilungTable.updatedAt]
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user