feat: integrate new desktop shell and extend backend & ADRs

- Added `meldestelle-desktop` module using JVM/Compose Desktop, registered in `settings.gradle.kts`.
- Integrated new screens and desktop navigation into core: `Veranstaltungen`, `TurnierDetail`, etc.
- Expanded backend with `ExposedFunktionaerRepository` in `officials-infrastructure`.
- Completed ADRs for bounded context mapping (`ADR-0014`) and context map (`ADR-0015`).
- Updated and extended project documentation with session logs and architecture decisions.

Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
This commit is contained in:
2026-03-24 18:22:15 +01:00
parent c624df8744
commit 354bd49de6
75 changed files with 7616 additions and 48 deletions
@@ -0,0 +1,24 @@
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.kotlinSerialization)
}
kotlin {
jvm()
sourceSets {
commonMain {
kotlin.srcDir("src/main/kotlin")
dependencies {
implementation(projects.core.coreDomain)
implementation(projects.core.coreUtils)
implementation(libs.kotlinx.datetime)
implementation(libs.kotlinx.serialization.json)
}
}
commonTest {
kotlin.srcDir("src/test/kotlin")
dependencies {
implementation(projects.platform.platformTesting)
}
}
}
}
@@ -0,0 +1,110 @@
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
package at.mocode.entries.domain.model
import at.mocode.core.domain.model.AbteilungsTeilungsTypE
import at.mocode.core.domain.serialization.KotlinInstantSerializer
import at.mocode.core.domain.serialization.UuidSerializer
import kotlinx.serialization.Serializable
import kotlin.time.Clock
import kotlin.time.Instant
import kotlin.uuid.Uuid
/**
* Domain-Modell für eine Abteilung im registration-context.
*
* Eine Abteilung ist die kleinste startbare Einheit innerhalb eines [DomBewerb]s.
* Ein Bewerb kann in mehrere Abteilungen aufgeteilt sein (z.B. Abt. 1: ohne Lizenz,
* Abt. 2: mit Lizenz R1). Die Aufteilung erfolgt gemäß ÖTO § 39 und den
* spartenspezifischen Bestimmungen.
*
* @property abteilungId Eindeutige interne ID (UUID).
* @property bewerbId Referenz auf den übergeordneten Bewerb (UUID).
* @property abteilungsNummer Laufende Nummer der Abteilung innerhalb des Bewerbs (1, 2, 3, ...).
* @property bezeichnung Optionale Bezeichnung der Abteilung (z.B. „ohne Lizenz", „R1", „4-jährige").
* @property teilungsTyp Kriterium, nach dem diese Abteilung abgegrenzt ist (Lizenz, Pferdealter, ...).
* @property teilnehmerkreisBeschreibung Freitext-Beschreibung des Teilnehmerkreises (z.B. „Reiter ohne Lizenz").
* @property starterAnzahl Aktuelle Anzahl der zugeordneten Starter (Nennungen).
* @property maxStarter Maximale Starter in dieser Abteilung (0 = kein Limit).
* @property startzeit Geplante Startzeit als ISO-String (z.B. „09:00").
* @property bemerkungen Interne Notizen.
* @property createdAt Erstellungszeitpunkt.
* @property updatedAt Letzter Änderungszeitpunkt.
*/
@Serializable
data class DomAbteilung(
@Serializable(with = UuidSerializer::class)
val abteilungId: Uuid = Uuid.random(),
// Zuordnung
@Serializable(with = UuidSerializer::class)
val bewerbId: Uuid,
// Identifikation
var abteilungsNummer: Int,
var bezeichnung: String? = null,
// Teilungs-Kriterium
var teilungsTyp: AbteilungsTeilungsTypE = AbteilungsTeilungsTypE.KEINE,
var teilnehmerkreisBeschreibung: String? = null,
// Starter
var starterAnzahl: Int = 0,
var maxStarter: Int = 0,
// Zeitplanung
var startzeit: String? = null,
// Verwaltung
var bemerkungen: String? = null,
// Audit
@Serializable(with = KotlinInstantSerializer::class)
val createdAt: Instant = Clock.System.now(),
@Serializable(with = KotlinInstantSerializer::class)
var updatedAt: Instant = Clock.System.now()
) {
/**
* Gibt den Anzeigenamen zurück (z.B. „Abt. 1 ohne Lizenz").
*/
fun getDisplayName(): String =
bezeichnung?.let { "Abt. $abteilungsNummer $it" } ?: "Abt. $abteilungsNummer"
/**
* Prüft, ob die Abteilung noch freie Startplätze hat.
* Gibt true zurück, wenn kein Limit gesetzt ist (maxStarter == 0).
*/
fun hatFreiePlaetze(): Boolean =
maxStarter == 0 || starterAnzahl < maxStarter
/**
* Validiert die Abteilung auf Überschreitung des maximalen Starter-Limits (§ 39 Abs. 2).
* Gibt Warnungen zurück (kein harter Fehler Override-Event möglich, ADR-0016).
*/
fun validateStarterLimit(): List<String> {
val warnings = mutableListOf<String>()
// Maximale Abteilungsgröße nach Teilung: > 80 Starter → erneute Teilung verpflichtend (§ 39 Abs. 2)
if (starterAnzahl > 80) {
warnings.add(
"WARN_ABTEILUNG_PFLICHT_TEILUNG_UEBERSCHRITTEN: ${getDisplayName()}, " +
"Starter: $starterAnzahl > 80. Erneute Teilung verpflichtend (§ 39 Abs. 2). " +
"Override möglich (TBA-Entscheidung)."
)
}
if (maxStarter > 0 && starterAnzahl > maxStarter) {
warnings.add(
"WARN_ABTEILUNG_MAX_STARTER_UEBERSCHRITTEN: ${getDisplayName()}, " +
"Starter: $starterAnzahl > Limit $maxStarter. Override möglich (TBA-Entscheidung)."
)
}
return warnings
}
/**
* Erstellt eine Kopie mit aktualisiertem Zeitstempel.
*/
fun withUpdatedTimestamp(): DomAbteilung = this.copy(updatedAt = Clock.System.now())
}
@@ -0,0 +1,150 @@
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
package at.mocode.entries.domain.model
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.core.domain.serialization.KotlinInstantSerializer
import at.mocode.core.domain.serialization.UuidSerializer
import kotlinx.serialization.Serializable
import kotlin.time.Clock
import kotlin.time.Instant
import kotlin.uuid.Uuid
/**
* Domain-Modell für einen Bewerb im registration-context.
*
* Ein Bewerb ist eine einzelne Prüfung innerhalb eines Turniers (z.B. „Stilspringen 90 cm").
* Er kann in mehrere [DomAbteilung]en aufgeteilt sein. Die Abteilungs-Warn-Logik basiert
* auf den ÖTO-Schwellenwerten (§ 39 A-Teil + spartenspezifische Bestimmungen).
*
* Aggregate Root des `registration-context` für den Bewerbs-Workflow.
*
* @property bewerbId Eindeutige interne ID (UUID).
* @property turnierId Referenz auf das übergeordnete Turnier (UUID).
* @property bewerbNummer Laufende Nummer des Bewerbs innerhalb des Turniers (z.B. 1, 2, 3).
* @property bezeichnung Offizielle Bezeichnung des Bewerbs (z.B. „Stilspringen 90 cm").
* @property sparte Sportliche Sparte (Springen, Dressur, Vielseitigkeit, ...).
* @property turnierkategorie Turnierkategorie (A*, A, B*, B, C, ...).
* @property pruefungsTyp Typ der Prüfung bestimmt den Abteilungs-Schwellenwert (§ 39).
* @property hoeheCm Höhe in cm (relevant für Springen und Vielseitigkeit).
* @property teilungsTyp Kriterium, nach dem der Bewerb in Abteilungen aufgeteilt wird.
* @property maxStarterProAbteilung Maximale Starter pro Abteilung (0 = kein Limit gesetzt).
* @property istMeisterschaft Ob es sich um einen Meisterschaftsbewerb handelt (Ausnahme von § 39 Abs. 4).
* @property istNachnennungErlaubt Ob Nachnennungen für diesen Bewerb zugelassen sind.
* @property bemerkungen Interne Notizen.
* @property createdAt Erstellungszeitpunkt.
* @property updatedAt Letzter Änderungszeitpunkt.
*/
@Serializable
data class DomBewerb(
@Serializable(with = UuidSerializer::class)
val bewerbId: Uuid = Uuid.random(),
// Zuordnung
@Serializable(with = UuidSerializer::class)
val turnierId: Uuid,
// Identifikation
var bewerbNummer: Int,
var bezeichnung: String,
// Fachliche Klassifikation
var sparte: SparteE,
var turnierkategorie: TurnierkategorieE,
var pruefungsTyp: PruefungsTypE,
var hoeheCm: Int? = null,
// Abteilungs-Konfiguration
var teilungsTyp: AbteilungsTeilungsTypE = AbteilungsTeilungsTypE.KEINE,
var maxStarterProAbteilung: Int = 0,
// Flags
var istMeisterschaft: Boolean = false,
var istNachnennungErlaubt: Boolean = true,
// Verwaltung
var bemerkungen: String? = null,
// Audit
@Serializable(with = KotlinInstantSerializer::class)
val createdAt: Instant = Clock.System.now(),
@Serializable(with = KotlinInstantSerializer::class)
var updatedAt: Instant = Clock.System.now()
) {
/**
* Gibt den Anzeigenamen mit Nummer zurück (z.B. „1 Stilspringen 90 cm").
*/
fun getDisplayName(): String = "$bewerbNummer $bezeichnung"
/**
* Liefert den Pflicht-Teilungs-Schwellenwert gemäß ÖTO § 39 für diesen Prüfungstyp.
* Gibt null zurück, wenn keine Pflicht-Teilung aufgrund der Starterzahl gilt
* (strukturelle Teilungen werden separat über [pruefungsTyp] und [teilungsTyp] abgebildet).
*
* Meisterschaftsbewerbe sind von der Pflicht-Teilung ausgenommen (§ 39 Abs. 4).
*/
fun getPflichtTeilungsSchwellenwert(): Int? {
if (istMeisterschaft) return null
return when (pruefungsTyp) {
PruefungsTypE.STIL_SPRINGEN,
PruefungsTypE.SPRINGPFERDE,
PruefungsTypE.DRESSURPFERDE -> 30
PruefungsTypE.VIELSEITIGKEIT -> 40
PruefungsTypE.SPRINGEN_UEBRIG -> 80
else -> null // Kann-Teilung oder strukturell kein Starter-Schwellenwert
}
}
/**
* Gibt den Kann-Teilungs-Schwellenwert zurück (nur für Dressur: > 30 Starter, § 39 Abs. 2).
* Gibt null zurück, wenn keine Kann-Teilung gilt.
*/
fun getKannTeilungsSchwellenwert(): Int? {
if (istMeisterschaft) return null
return when (pruefungsTyp) {
PruefungsTypE.DRESSUR -> 30
else -> null
}
}
/**
* Validiert den Bewerb auf Abteilungs-Schwellenwerte anhand der aktuellen Starterzahl.
* Gibt Warnungen zurück (kein harter Fehler Override-Event möglich, ADR-0016).
*
* @param aktuelleStarterAnzahl Aktuelle Anzahl der Nennungen für diesen Bewerb.
*/
fun validateAbteilungsSchwellenwerte(aktuelleStarterAnzahl: Int): List<String> {
val warnings = mutableListOf<String>()
val pflichtSchwellenwert = getPflichtTeilungsSchwellenwert()
if (pflichtSchwellenwert != null && aktuelleStarterAnzahl > pflichtSchwellenwert) {
warnings.add(
"WARN_ABTEILUNG_PFLICHT_TEILUNG_UEBERSCHRITTEN: Bewerb ${getDisplayName()}, " +
"Prüfungstyp $pruefungsTyp, Starter: $aktuelleStarterAnzahl > Schwellenwert $pflichtSchwellenwert. " +
"Empfehlung: Teilung nach ${teilungsTyp.name}. Override möglich (TBA-Entscheidung)."
)
}
val kannSchwellenwert = getKannTeilungsSchwellenwert()
if (kannSchwellenwert != null && aktuelleStarterAnzahl > kannSchwellenwert &&
teilungsTyp == AbteilungsTeilungsTypE.KEINE
) {
warnings.add(
"WARN_ABTEILUNG_KANN_TEILUNG_EMPFOHLEN: Bewerb ${getDisplayName()}, " +
"Prüfungstyp $pruefungsTyp, Starter: $aktuelleStarterAnzahl > $kannSchwellenwert. " +
"Kann-Teilung empfohlen (§ 39 Abs. 2)."
)
}
return warnings
}
/**
* Erstellt eine Kopie mit aktualisiertem Zeitstempel.
*/
fun withUpdatedTimestamp(): DomBewerb = this.copy(updatedAt = Clock.System.now())
}
@@ -0,0 +1,99 @@
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
package at.mocode.entries.domain.model
import at.mocode.core.domain.model.NennungsStatusE
import at.mocode.core.domain.model.StartwunschE
import at.mocode.core.domain.serialization.KotlinInstantSerializer
import at.mocode.core.domain.serialization.UuidSerializer
import kotlinx.serialization.Serializable
import kotlin.time.Clock
import kotlin.time.Instant
import kotlin.uuid.Uuid
/**
* Domain model representing a competition entry (Nennung) in the registration-context.
*
* A Nennung is the binding registration of a Reiter-Pferd-Paar (rider-horse pair)
* to a specific Abteilung (the smallest unit for entries and results per ÖTO).
*
* Key rules (ÖTO / registration-context):
* - The Abteilung (not the Bewerb) is the smallest unit for entries.
* - A Nennungs-Transfer is NOT a cancellation + re-entry, but a transfer operation.
* Already paid Nenngeld is kept as credit (Guthaben) on the payer's account.
* - After Nennschluss, a Nachnenngebühr applies (can be waived by the organizer).
* - The system issues WARNINGS only never hard errors. The TBA has the final say.
*
* @property nennungId Unique internal identifier (UUID).
* @property abteilungId Reference to the Abteilung (smallest entry unit).
* @property bewerbId Reference to the parent Bewerb (for display/reporting).
* @property turnierId Reference to the Turnier.
* @property reiterId Reference to the DomReiter (actor-context).
* @property pferdId Reference to the DomPferd (actor-context).
* @property zahlerId Reference to the payer (may differ from rider, e.g. club pays).
* @property status Current status of this entry.
* @property startwunsch Rider's preferred starting position (vorne/hinten).
* @property istNachnennung Whether this entry was submitted after Nennschluss.
* @property nachnenngebuehrErlassen Whether the organizer waived the late entry fee.
* @property bemerkungen Optional notes from the Meldestelle.
* @property createdAt Timestamp when this entry was created.
* @property updatedAt Timestamp when this entry was last updated.
*/
@Serializable
data class DomNennung(
@Serializable(with = UuidSerializer::class)
val nennungId: Uuid = Uuid.random(),
// Competition References
@Serializable(with = UuidSerializer::class)
val abteilungId: Uuid,
@Serializable(with = UuidSerializer::class)
val bewerbId: Uuid,
@Serializable(with = UuidSerializer::class)
val turnierId: Uuid,
// Actor References (actor-context)
@Serializable(with = UuidSerializer::class)
val reiterId: Uuid,
@Serializable(with = UuidSerializer::class)
val pferdId: Uuid,
// Billing Reference (billing-context)
@Serializable(with = UuidSerializer::class)
val zahlerId: Uuid? = null,
// Entry Details
val status: NennungsStatusE = NennungsStatusE.EINGEGANGEN,
val startwunsch: StartwunschE = StartwunschE.KEIN_WUNSCH,
// Late Entry (Nachnennung)
val istNachnennung: Boolean = false,
val nachnenngebuehrErlassen: Boolean = false,
// Notes
val bemerkungen: String? = null,
// Audit Fields
@Serializable(with = KotlinInstantSerializer::class)
val createdAt: Instant = Clock.System.now(),
@Serializable(with = KotlinInstantSerializer::class)
var updatedAt: Instant = Clock.System.now()
) {
/**
* Checks if this entry is still active (not withdrawn or cancelled).
*/
fun isAktiv(): Boolean = status !in listOf(
NennungsStatusE.ZURUECKGEZOGEN,
NennungsStatusE.NICHT_ANGETRETEN
)
/**
* Checks if a late entry fee (Nachnenngebühr) is due.
*/
fun isNachnenngebuehrFaellig(): Boolean = istNachnennung && !nachnenngebuehrErlassen
/**
* Creates a copy of this entry with an updated timestamp.
*/
fun withUpdatedTimestamp(): DomNennung = this.copy(updatedAt = Clock.System.now())
}
@@ -0,0 +1,97 @@
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
package at.mocode.entries.domain.model
import at.mocode.core.domain.serialization.KotlinInstantSerializer
import at.mocode.core.domain.serialization.UuidSerializer
import kotlinx.serialization.Serializable
import kotlin.time.Clock
import kotlin.time.Instant
import kotlin.uuid.Uuid
/**
* Domain model representing a transfer operation on an existing entry (Nennungs-Transfer).
*
* A Nennungs-Transfer is the correct way to change the rider and/or horse on an existing
* Nennung. It is explicitly NOT a cancellation + re-entry. Already paid Nenngeld is
* retained as credit (Guthaben) on the payer's account in the billing-context.
*
* This model captures the full audit trail of the transfer: who was replaced by whom,
* who authorized it, and whether a Nachnenngebühr applies.
*
* Key rules (ÖTO / registration-context):
* - Transfer = atomic operation on the existing Nennung (status → TRANSFERIERT).
* - A new Nennung is created for the new Reiter-Pferd-Paar.
* - The original Nennung is closed with status TRANSFERIERT (not deleted).
* - Paid Nenngeld is credited to the payer's account, not refunded directly.
* - Every transfer is stored as an explicit event (Override-Event if after Nennschluss).
*
* @property transferId Unique internal identifier (UUID).
* @property ursprungsNennungId The original Nennung being transferred.
* @property neueNennungId The newly created Nennung after the transfer.
* @property alterReiterId Rider ID before the transfer (null if only horse changed).
* @property neuerReiterId Rider ID after the transfer (null if only horse changed).
* @property altesPferdId Horse ID before the transfer (null if only rider changed).
* @property neuesPferdId Horse ID after the transfer (null if only rider changed).
* @property istNachNennschluss Whether the transfer occurred after Nennschluss.
* @property nachnenngebuehrErlassen Whether the organizer waived the late transfer fee.
* @property autorisiertVon UUID of the Meldestelle user who authorized the transfer.
* @property grund Reason for the transfer (optional, for audit trail).
* @property createdAt Timestamp when this transfer was recorded.
*/
@Serializable
data class DomNennungsTransfer(
@Serializable(with = UuidSerializer::class)
val transferId: Uuid = Uuid.random(),
// Linked Entries
@Serializable(with = UuidSerializer::class)
val ursprungsNennungId: Uuid,
@Serializable(with = UuidSerializer::class)
val neueNennungId: Uuid,
// What changed Rider
@Serializable(with = UuidSerializer::class)
val alterReiterId: Uuid? = null,
@Serializable(with = UuidSerializer::class)
val neuerReiterId: Uuid? = null,
// What changed Horse
@Serializable(with = UuidSerializer::class)
val altesPferdId: Uuid? = null,
@Serializable(with = UuidSerializer::class)
val neuesPferdId: Uuid? = null,
// Timing & Fees
val istNachNennschluss: Boolean = false,
val nachnenngebuehrErlassen: Boolean = false,
// Authorization (Override-Event)
@Serializable(with = UuidSerializer::class)
val autorisiertVon: Uuid,
val grund: String? = null,
// Audit Fields
@Serializable(with = KotlinInstantSerializer::class)
val createdAt: Instant = Clock.System.now()
) {
/**
* Returns true if the rider was changed in this transfer.
*/
fun isReiterGetauscht(): Boolean = alterReiterId != null && neuerReiterId != null
/**
* Returns true if the horse was changed in this transfer.
*/
fun isPferdGetauscht(): Boolean = altesPferdId != null && neuesPferdId != null
/**
* Returns true if a Nachnenngebühr is due for this transfer.
*/
fun isNachnenngebuehrFaellig(): Boolean = istNachNennschluss && !nachnenngebuehrErlassen
/**
* Validates that the transfer changes at least one of rider or horse.
*/
fun isValid(): Boolean = isReiterGetauscht() || isPferdGetauscht()
}
@@ -0,0 +1,156 @@
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
package at.mocode.entries.domain.model
import at.mocode.core.domain.model.StartlistenStatusE
import at.mocode.core.domain.serialization.KotlinInstantSerializer
import at.mocode.core.domain.serialization.UuidSerializer
import kotlinx.serialization.Serializable
import kotlin.time.Clock
import kotlin.time.Instant
import kotlin.uuid.Uuid
/**
* Domain-Modell für eine Startliste im registration-context.
*
* Eine Startliste gehört zu einer [DomAbteilung] und enthält die geordnete Liste
* der Starter (Nennungen) mit ihren Startnummern. Sie durchläuft einen definierten
* Workflow: NICHT_ERSTELLT → ENTWURF → VEROEFFENTLICHT → GESPERRT → ARCHIVIERT.
*
* @property startlisteId Eindeutige interne ID (UUID).
* @property abteilungId Referenz auf die zugehörige Abteilung (UUID).
* @property bewerbId Referenz auf den übergeordneten Bewerb (UUID) Denormalisierung für schnellen Zugriff.
* @property turnierId Referenz auf das übergeordnete Turnier (UUID) Denormalisierung für schnellen Zugriff.
* @property status Aktueller Status der Startliste im Workflow.
* @property eintraege Geordnete Liste der Startlisteneinträge (Startnummer → Nennung).
* @property veroeffentlichtAt Zeitpunkt der Veröffentlichung (null = noch nicht veröffentlicht).
* @property gesperrtAt Zeitpunkt der Sperrung (null = noch nicht gesperrt).
* @property bemerkungen Interne Notizen.
* @property createdAt Erstellungszeitpunkt.
* @property updatedAt Letzter Änderungszeitpunkt.
*/
@Serializable
data class DomStartliste(
@Serializable(with = UuidSerializer::class)
val startlisteId: Uuid = Uuid.random(),
// Zuordnung
@Serializable(with = UuidSerializer::class)
val abteilungId: Uuid,
@Serializable(with = UuidSerializer::class)
val bewerbId: Uuid,
@Serializable(with = UuidSerializer::class)
val turnierId: Uuid,
// Status
var status: StartlistenStatusE = StartlistenStatusE.NICHT_ERSTELLT,
// Einträge
var eintraege: List<StartlistenEintrag> = emptyList(),
// Zeitstempel Workflow
@Serializable(with = KotlinInstantSerializer::class)
var veroeffentlichtAt: Instant? = null,
@Serializable(with = KotlinInstantSerializer::class)
var gesperrtAt: Instant? = null,
// Verwaltung
var bemerkungen: String? = null,
// Audit
@Serializable(with = KotlinInstantSerializer::class)
val createdAt: Instant = Clock.System.now(),
@Serializable(with = KotlinInstantSerializer::class)
var updatedAt: Instant = Clock.System.now()
) {
/**
* Gibt die Anzahl der Starter in dieser Startliste zurück.
*/
fun getStarterAnzahl(): Int = eintraege.size
/**
* Gibt den Eintrag für eine bestimmte Startnummer zurück, oder null.
*/
fun getEintragByStartnummer(startnummer: Int): StartlistenEintrag? =
eintraege.find { it.startnummer == startnummer }
/**
* Prüft, ob die Startliste bearbeitbar ist (nur im Status ENTWURF).
*/
fun istBearbeitbar(): Boolean = status == StartlistenStatusE.ENTWURF
/**
* Prüft, ob die Startliste veröffentlicht oder gesperrt ist (für Reiter sichtbar).
*/
fun istSichtbar(): Boolean =
status == StartlistenStatusE.VEROEFFENTLICHT || status == StartlistenStatusE.GESPERRT
/**
* Führt den Workflow-Übergang ENTWURF → VEROEFFENTLICHT durch.
* Gibt Warnungen zurück, wenn der Übergang nicht möglich ist.
*/
fun veroeffentlichen(): List<String> {
val warnings = mutableListOf<String>()
if (status != StartlistenStatusE.ENTWURF) {
warnings.add(
"WARN_STARTLISTE_UNGÜLTIGER_STATUS_ÜBERGANG: Startliste kann nur aus Status " +
"ENTWURF veröffentlicht werden. Aktueller Status: $status."
)
return warnings
}
if (eintraege.isEmpty()) {
warnings.add("WARN_STARTLISTE_LEER: Startliste enthält keine Einträge.")
}
return warnings
}
/**
* Führt den Workflow-Übergang VEROEFFENTLICHT → GESPERRT durch.
* Gibt Warnungen zurück, wenn der Übergang nicht möglich ist.
*/
fun sperren(): List<String> {
val warnings = mutableListOf<String>()
if (status != StartlistenStatusE.VEROEFFENTLICHT) {
warnings.add(
"WARN_STARTLISTE_UNGÜLTIGER_STATUS_ÜBERGANG: Startliste kann nur aus Status " +
"VEROEFFENTLICHT gesperrt werden. Aktueller Status: $status."
)
}
return warnings
}
/**
* Erstellt eine Kopie mit aktualisiertem Zeitstempel.
*/
fun withUpdatedTimestamp(): DomStartliste = this.copy(updatedAt = Clock.System.now())
}
/**
* Ein einzelner Eintrag in einer Startliste.
*
* Verbindet eine Startnummer mit einer Nennung ([DomNennung]).
*
* @property startnummer Die zugewiesene Startnummer (Kopfnummer gemäß Ubiquitous Language).
* @property nennungId Referenz auf die zugehörige Nennung (UUID).
* @property reiterName Denormalisierter Reitername für schnelle Anzeige.
* @property pferdeName Denormalisierter Pferdename für schnelle Anzeige.
* @property startwunsch Optionaler Startwunsch (VORNE, HINTEN, KEIN_WUNSCH).
* @property istGestrichen Ob der Starter gestrichen wurde (Abmeldung nach Startlistenerstellung).
*/
@Serializable
data class StartlistenEintrag(
var startnummer: Int,
@Serializable(with = UuidSerializer::class)
val nennungId: Uuid,
// Denormalisierte Felder für schnelle Anzeige
var reiterName: String,
var pferdeName: String,
// Startwunsch
var startwunsch: String? = null,
// Status
var istGestrichen: Boolean = false
)
@@ -0,0 +1,88 @@
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
package at.mocode.entries.domain.repository
import at.mocode.core.domain.model.NennungsStatusE
import at.mocode.entries.domain.model.DomNennung
import kotlin.uuid.Uuid
/**
* Repository-Interface für DomNennung (Nennung) Domain-Operationen.
*
* Definiert den Vertrag für Datenzugriffs-Operationen ohne Abhängigkeit
* von konkreten Implementierungsdetails (Datenbank, etc.).
*/
interface NennungRepository {
/**
* Sucht eine Nennung anhand ihrer eindeutigen ID.
*/
suspend fun findById(id: Uuid): DomNennung?
/**
* Sucht alle Nennungen für einen bestimmten Bewerb.
*/
suspend fun findByBewerbId(bewerbId: Uuid): List<DomNennung>
/**
* Sucht alle Nennungen für eine bestimmte Abteilung.
*/
suspend fun findByAbteilungId(abteilungId: Uuid): List<DomNennung>
/**
* Sucht alle Nennungen für ein bestimmtes Turnier.
*/
suspend fun findByTurnierId(turnierId: Uuid): List<DomNennung>
/**
* Sucht alle Nennungen eines bestimmten Reiters.
*/
suspend fun findByReiterId(reiterId: Uuid): List<DomNennung>
/**
* Sucht alle Nennungen für ein bestimmtes Pferd.
*/
suspend fun findByPferdId(pferdId: Uuid): List<DomNennung>
/**
* Sucht alle Nennungen eines Reiters für ein bestimmtes Turnier.
*/
suspend fun findByReiterIdAndTurnierId(reiterId: Uuid, turnierId: Uuid): List<DomNennung>
/**
* Sucht alle Nennungen mit einem bestimmten Status.
*/
suspend fun findByStatus(status: NennungsStatusE): List<DomNennung>
/**
* Sucht alle Nachnennungen für einen Bewerb.
*/
suspend fun findNachnennungenByBewerbId(bewerbId: Uuid): List<DomNennung>
/**
* Speichert eine Nennung (Insert oder Update).
*/
suspend fun save(nennung: DomNennung): DomNennung
/**
* Löscht eine Nennung anhand ihrer ID.
*
* @return true wenn gelöscht, false wenn nicht gefunden
*/
suspend fun delete(id: Uuid): Boolean
/**
* Zählt alle Nennungen für einen Bewerb.
*/
suspend fun countByBewerbId(bewerbId: Uuid): Long
/**
* Zählt alle Nennungen für eine Abteilung.
*/
suspend fun countByAbteilungId(abteilungId: Uuid): Long
/**
* Zählt alle Nennungen für ein Turnier mit einem bestimmten Status.
*/
suspend fun countByTurnierIdAndStatus(turnierId: Uuid, status: NennungsStatusE): Long
}
@@ -0,0 +1,15 @@
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
package at.mocode.entries.domain.repository
import at.mocode.entries.domain.model.DomNennungsTransfer
import kotlin.uuid.Uuid
/**
* Repository-Interface für DomNennungsTransfer Domain-Operationen.
*/
interface NennungsTransferRepository {
suspend fun findById(id: Uuid): DomNennungsTransfer?
suspend fun findByUrsprungsNennungId(nennungId: Uuid): List<DomNennungsTransfer>
suspend fun save(transfer: DomNennungsTransfer): DomNennungsTransfer
}