Compare commits
No commits in common. "d7095bef47de0c6be1ce99452558be3ea4e11aac" and "3ce085ea185fe311bcafd2691ff2731b69df48b7" have entirely different histories.
d7095bef47
...
3ce085ea18
|
|
@ -26,13 +26,6 @@ Versionierung folgt [Semantic Versioning](https://semver.org/lang/de/).
|
||||||
|
|
||||||
### Hinzugefügt
|
### Hinzugefügt
|
||||||
|
|
||||||
- **Events-Service Bundle:** Vollständige Stabilisierung der `events` Services (Domain, Infrastructure, API, Service).
|
|
||||||
- **Domain:** Umstellung auf `kotlin.time.Instant` zur Vermeidung von Deprecation-Warnungen (Kotlin 2.1.20+) und Harmonisierung mit dem Rulebook-Expert.
|
|
||||||
- **Infrastructure:** Anpassung an den `org.jetbrains.exposed.v1` Namespace und Implementierung von UUID-Konvertierungen zwischen `kotlin.uuid.Uuid` (Domain) und `java.util.UUID` (DB).
|
|
||||||
- **API:** Refactoring des `VeranstaltungController` zur direkten Repository-Nutzung (Alignment mit `entries` Service).
|
|
||||||
- **Service-Config:** Umstellung auf Flyway-basiertes Tenant-Schema-Management in `EventsDatabaseConfiguration`.
|
|
||||||
- **Build:** Behebung des `shadowJar` Fehlers in `events-infrastructure` durch Entfernen des unnötigen `ktor` Plugins in der Library-Schicht.
|
|
||||||
|
|
||||||
- Masterdata: Automatisches Seeding aller Reiterlizenzen (license_matrix) beim Start des `masterdata-service` via `ReiterlizenzenSeeder` (idempotent; SPRINGEN: LIZENZFREI,R1–R4; DRESSUR: LIZENZFREI,RD1–RD3).
|
- Masterdata: Automatisches Seeding aller Reiterlizenzen (license_matrix) beim Start des `masterdata-service` via `ReiterlizenzenSeeder` (idempotent; SPRINGEN: LIZENZFREI,R1–R4; DRESSUR: LIZENZFREI,RD1–RD3).
|
||||||
|
|
||||||
- **ZNS-Import (LIZENZ01.dat):** Robuster Lizenz-Tokenizer und Normalizer implementiert.
|
- **ZNS-Import (LIZENZ01.dat):** Robuster Lizenz-Tokenizer und Normalizer implementiert.
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ import kotlin.uuid.Uuid
|
||||||
/**
|
/**
|
||||||
* Domain-Modell für eine Abteilung im registration-context.
|
* Domain-Modell für eine Abteilung im registration-context.
|
||||||
*
|
*
|
||||||
* Eine Abteilung ist die kleinste startbare Einheit innerhalb eines [Bewerb]s.
|
* 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,
|
* 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
|
* Abt. 2: mit Lizenz R1). Die Aufteilung erfolgt gemäß ÖTO § 39 und den
|
||||||
* spartenspezifischen Bestimmungen.
|
* spartenspezifischen Bestimmungen.
|
||||||
|
|
@ -32,7 +32,7 @@ import kotlin.uuid.Uuid
|
||||||
* @property updatedAt Letzter Änderungszeitpunkt.
|
* @property updatedAt Letzter Änderungszeitpunkt.
|
||||||
*/
|
*/
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Abteilung(
|
data class DomAbteilung(
|
||||||
@Serializable(with = UuidSerializer::class)
|
@Serializable(with = UuidSerializer::class)
|
||||||
val abteilungId: Uuid = Uuid.random(),
|
val abteilungId: Uuid = Uuid.random(),
|
||||||
|
|
||||||
|
|
@ -106,5 +106,5 @@ data class Abteilung(
|
||||||
/**
|
/**
|
||||||
* Erstellt eine Kopie mit aktualisiertem Zeitstempel.
|
* Erstellt eine Kopie mit aktualisiertem Zeitstempel.
|
||||||
*/
|
*/
|
||||||
fun withUpdatedTimestamp(): Abteilung = this.copy(updatedAt = Clock.System.now())
|
fun withUpdatedTimestamp(): DomAbteilung = this.copy(updatedAt = Clock.System.now())
|
||||||
}
|
}
|
||||||
|
|
@ -3,14 +3,11 @@
|
||||||
package at.mocode.entries.domain.model
|
package at.mocode.entries.domain.model
|
||||||
|
|
||||||
import at.mocode.core.domain.model.AbteilungsTeilungsTypE
|
import at.mocode.core.domain.model.AbteilungsTeilungsTypE
|
||||||
import at.mocode.core.domain.model.BeginnZeitTypE
|
|
||||||
import at.mocode.core.domain.model.PruefungsTypE
|
import at.mocode.core.domain.model.PruefungsTypE
|
||||||
import at.mocode.core.domain.model.SparteE
|
import at.mocode.core.domain.model.SparteE
|
||||||
import at.mocode.core.domain.model.TurnierkategorieE
|
import at.mocode.core.domain.model.TurnierkategorieE
|
||||||
import at.mocode.core.domain.serialization.InstantSerializer
|
import at.mocode.core.domain.serialization.InstantSerializer
|
||||||
import at.mocode.core.domain.serialization.UuidSerializer
|
import at.mocode.core.domain.serialization.UuidSerializer
|
||||||
import kotlinx.datetime.LocalDate
|
|
||||||
import kotlinx.datetime.LocalTime
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlin.time.Clock
|
import kotlin.time.Clock
|
||||||
import kotlin.time.Instant
|
import kotlin.time.Instant
|
||||||
|
|
@ -20,7 +17,7 @@ import kotlin.uuid.Uuid
|
||||||
* Domain-Modell für einen Bewerb im registration-context.
|
* Domain-Modell für einen Bewerb im registration-context.
|
||||||
*
|
*
|
||||||
* Ein Bewerb ist eine einzelne Prüfung innerhalb eines Turniers (z.B. „Stilspringen 90 cm").
|
* Ein Bewerb ist eine einzelne Prüfung innerhalb eines Turniers (z.B. „Stilspringen 90 cm").
|
||||||
* Er kann in mehrere [Abteilung]en aufgeteilt sein. Die Abteilungs-Warn-Logik basiert
|
* Er kann in mehrere [DomAbteilung]en aufgeteilt sein. Die Abteilungs-Warn-Logik basiert
|
||||||
* auf den ÖTO-Schwellenwerten (§ 39 A-Teil + spartenspezifische Bestimmungen).
|
* auf den ÖTO-Schwellenwerten (§ 39 A-Teil + spartenspezifische Bestimmungen).
|
||||||
*
|
*
|
||||||
* Aggregate Root des `registration-context` für den Bewerbs-Workflow.
|
* Aggregate Root des `registration-context` für den Bewerbs-Workflow.
|
||||||
|
|
@ -37,27 +34,12 @@ import kotlin.uuid.Uuid
|
||||||
* @property maxStarterProAbteilung Maximale Starter pro Abteilung (0 = kein Limit gesetzt).
|
* @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 istMeisterschaft Ob es sich um einen Meisterschaftsbewerb handelt (Ausnahme von § 39 Abs. 4).
|
||||||
* @property istNachnennungErlaubt Ob Nachnennungen für diesen Bewerb zugelassen sind.
|
* @property istNachnennungErlaubt Ob Nachnennungen für diesen Bewerb zugelassen sind.
|
||||||
* @property beschreibung Optionale Beschreibung (z.B. "Pony Einsteiger Cup").
|
|
||||||
* @property aufgabe Aufgaben-Bezeichnung gemäß ÖTO (z.B. "R1", "L1").
|
|
||||||
* @property aufgabenNummer Aufgaben-Nummer (z.B. "R1/2024").
|
|
||||||
* @property paraGrade Para-Equestrian Grade (z.B. "Grade I"), falls zutreffend.
|
|
||||||
* @property austragungsplatzId Referenz auf den Austragungsplatz (UUID aus events-context).
|
|
||||||
* @property richterEinsaetze Liste der Richter-Einsätze für diesen Bewerb.
|
|
||||||
* @property geplantesDatum Geplantes Datum des Bewerbs.
|
|
||||||
* @property beginnZeitTyp Typ des Beginnzeit-Eintrags (FIX oder ANSCHLIESSEND).
|
|
||||||
* @property beginnZeit Geplante Beginnzeit (nur bei FIX).
|
|
||||||
* @property reitdauerMinuten Geplante Reitdauer in Minuten.
|
|
||||||
* @property umbauMinuten Geplante Umbauzeit in Minuten.
|
|
||||||
* @property besichtigungMinuten Geplante Besichtigungszeit in Minuten.
|
|
||||||
* @property stechenGeplant Ob ein Stechen geplant ist.
|
|
||||||
* @property startgeldCent Startgeld in Cent (z.B. 1500 = 15,00 €).
|
|
||||||
* @property geldpreisAusbezahlt Ob der Geldpreis bereits ausbezahlt wurde.
|
|
||||||
* @property bemerkungen Interne Notizen.
|
* @property bemerkungen Interne Notizen.
|
||||||
* @property createdAt Erstellungszeitpunkt.
|
* @property createdAt Erstellungszeitpunkt.
|
||||||
* @property updatedAt Letzter Änderungszeitpunkt.
|
* @property updatedAt Letzter Änderungszeitpunkt.
|
||||||
*/
|
*/
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Bewerb(
|
data class DomBewerb(
|
||||||
@Serializable(with = UuidSerializer::class)
|
@Serializable(with = UuidSerializer::class)
|
||||||
val bewerbId: Uuid = Uuid.random(),
|
val bewerbId: Uuid = Uuid.random(),
|
||||||
|
|
||||||
|
|
@ -79,30 +61,6 @@ data class Bewerb(
|
||||||
var teilungsTyp: AbteilungsTeilungsTypE = AbteilungsTeilungsTypE.KEINE,
|
var teilungsTyp: AbteilungsTeilungsTypE = AbteilungsTeilungsTypE.KEINE,
|
||||||
var maxStarterProAbteilung: Int = 0,
|
var maxStarterProAbteilung: Int = 0,
|
||||||
|
|
||||||
// Text & Details
|
|
||||||
var beschreibung: String? = null,
|
|
||||||
var aufgabe: String? = null,
|
|
||||||
var aufgabenNummer: String? = null,
|
|
||||||
var paraGrade: String? = null,
|
|
||||||
|
|
||||||
// Ort & Funktionäre
|
|
||||||
@Serializable(with = UuidSerializer::class)
|
|
||||||
var austragungsplatzId: Uuid? = null,
|
|
||||||
var richterEinsaetze: List<RichterEinsatz> = emptyList(),
|
|
||||||
|
|
||||||
// Zeitplan
|
|
||||||
var geplantesDatum: LocalDate? = null,
|
|
||||||
var beginnZeitTyp: BeginnZeitTypE? = null,
|
|
||||||
var beginnZeit: LocalTime? = null,
|
|
||||||
var reitdauerMinuten: Int? = null,
|
|
||||||
var umbauMinuten: Int? = null,
|
|
||||||
var besichtigungMinuten: Int? = null,
|
|
||||||
var stechenGeplant: Boolean = false,
|
|
||||||
|
|
||||||
// Finanzen
|
|
||||||
var startgeldCent: Long? = null,
|
|
||||||
var geldpreisAusbezahlt: Boolean = false,
|
|
||||||
|
|
||||||
// Flags
|
// Flags
|
||||||
var istMeisterschaft: Boolean = false,
|
var istMeisterschaft: Boolean = false,
|
||||||
var istNachnennungErlaubt: Boolean = true,
|
var istNachnennungErlaubt: Boolean = true,
|
||||||
|
|
@ -188,5 +146,5 @@ data class Bewerb(
|
||||||
/**
|
/**
|
||||||
* Erstellt eine Kopie mit aktualisiertem Zeitstempel.
|
* Erstellt eine Kopie mit aktualisiertem Zeitstempel.
|
||||||
*/
|
*/
|
||||||
fun withUpdatedTimestamp(): Bewerb = this.copy(updatedAt = Clock.System.now())
|
fun withUpdatedTimestamp(): DomBewerb = this.copy(updatedAt = Clock.System.now())
|
||||||
}
|
}
|
||||||
|
|
@ -40,7 +40,7 @@ import kotlin.uuid.Uuid
|
||||||
* @property updatedAt Timestamp when this entry was last updated.
|
* @property updatedAt Timestamp when this entry was last updated.
|
||||||
*/
|
*/
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Nennung(
|
data class DomNennung(
|
||||||
@Serializable(with = UuidSerializer::class)
|
@Serializable(with = UuidSerializer::class)
|
||||||
val nennungId: Uuid = Uuid.random(),
|
val nennungId: Uuid = Uuid.random(),
|
||||||
|
|
||||||
|
|
@ -95,5 +95,5 @@ data class Nennung(
|
||||||
/**
|
/**
|
||||||
* Creates a copy of this entry with an updated timestamp.
|
* Creates a copy of this entry with an updated timestamp.
|
||||||
*/
|
*/
|
||||||
fun withUpdatedTimestamp(): Nennung = this.copy(updatedAt = Clock.System.now())
|
fun withUpdatedTimestamp(): DomNennung = this.copy(updatedAt = Clock.System.now())
|
||||||
}
|
}
|
||||||
|
|
@ -40,7 +40,7 @@ import kotlin.uuid.Uuid
|
||||||
* @property createdAt Timestamp when this transfer was recorded.
|
* @property createdAt Timestamp when this transfer was recorded.
|
||||||
*/
|
*/
|
||||||
@Serializable
|
@Serializable
|
||||||
data class NennungsTransfer(
|
data class DomNennungsTransfer(
|
||||||
@Serializable(with = UuidSerializer::class)
|
@Serializable(with = UuidSerializer::class)
|
||||||
val transferId: Uuid = Uuid.random(),
|
val transferId: Uuid = Uuid.random(),
|
||||||
|
|
||||||
|
|
@ -13,7 +13,7 @@ import kotlin.uuid.Uuid
|
||||||
/**
|
/**
|
||||||
* Domain-Modell für eine Startliste im registration-context.
|
* Domain-Modell für eine Startliste im registration-context.
|
||||||
*
|
*
|
||||||
* Eine Startliste gehört zu einer [Abteilung] und enthält die geordnete Liste
|
* Eine Startliste gehört zu einer [DomAbteilung] und enthält die geordnete Liste
|
||||||
* der Starter (Nennungen) mit ihren Startnummern. Sie durchläuft einen definierten
|
* der Starter (Nennungen) mit ihren Startnummern. Sie durchläuft einen definierten
|
||||||
* Workflow: NICHT_ERSTELLT → ENTWURF → VEROEFFENTLICHT → GESPERRT → ARCHIVIERT.
|
* Workflow: NICHT_ERSTELLT → ENTWURF → VEROEFFENTLICHT → GESPERRT → ARCHIVIERT.
|
||||||
*
|
*
|
||||||
|
|
@ -128,7 +128,7 @@ data class DomStartliste(
|
||||||
/**
|
/**
|
||||||
* Ein einzelner Eintrag in einer Startliste.
|
* Ein einzelner Eintrag in einer Startliste.
|
||||||
*
|
*
|
||||||
* Verbindet eine Startnummer mit einer Nennung ([Nennung]).
|
* Verbindet eine Startnummer mit einer Nennung ([DomNennung]).
|
||||||
*
|
*
|
||||||
* @property startnummer Die zugewiesene Startnummer (Kopfnummer gemäß Ubiquitous Language).
|
* @property startnummer Die zugewiesene Startnummer (Kopfnummer gemäß Ubiquitous Language).
|
||||||
* @property nennungId Referenz auf die zugehörige Nennung (UUID).
|
* @property nennungId Referenz auf die zugehörige Nennung (UUID).
|
||||||
|
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
|
||||||
|
|
||||||
package at.mocode.entries.domain.model
|
|
||||||
|
|
||||||
import at.mocode.core.domain.serialization.UuidSerializer
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import kotlin.uuid.Uuid
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Value Object für den Einsatz eines Richters (Funktionärs) in einem Bewerb.
|
|
||||||
*
|
|
||||||
* Beschreibt, welcher Funktionär in welcher Funktion/Position bei einem Bewerb eingesetzt wird
|
|
||||||
* (z.B. Richter C, Aufsicht, Parcoursbauer).
|
|
||||||
*
|
|
||||||
* @property funktionaerId Referenz auf den Funktionär (UUID aus masterdata-context).
|
|
||||||
* @property position Positions-Bezeichnung gemäß ÖTO (z.B. "C", "M", "B", "Aufsicht").
|
|
||||||
*/
|
|
||||||
@Serializable
|
|
||||||
data class RichterEinsatz(
|
|
||||||
@Serializable(with = UuidSerializer::class)
|
|
||||||
val funktionaerId: Uuid,
|
|
||||||
val position: String
|
|
||||||
)
|
|
||||||
|
|
@ -2,23 +2,23 @@
|
||||||
|
|
||||||
package at.mocode.entries.domain.repository
|
package at.mocode.entries.domain.repository
|
||||||
|
|
||||||
import at.mocode.entries.domain.model.Abteilung
|
import at.mocode.entries.domain.model.DomAbteilung
|
||||||
import at.mocode.entries.domain.model.Bewerb
|
import at.mocode.entries.domain.model.DomBewerb
|
||||||
import kotlin.uuid.Uuid
|
import kotlin.uuid.Uuid
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Repository-Interface für Bewerb und Abteilung Domain-Operationen.
|
* Repository-Interface für DomBewerb und DomAbteilung Domain-Operationen.
|
||||||
*/
|
*/
|
||||||
interface CompetitionRepository {
|
interface CompetitionRepository {
|
||||||
// Bewerbe
|
// Bewerbe
|
||||||
suspend fun findBewerbById(id: Uuid): Bewerb?
|
suspend fun findBewerbById(id: Uuid): DomBewerb?
|
||||||
suspend fun findBewerbeByTurnierId(turnierId: Uuid): List<Bewerb>
|
suspend fun findBewerbeByTurnierId(turnierId: Uuid): List<DomBewerb>
|
||||||
suspend fun saveBewerb(bewerb: Bewerb): Bewerb
|
suspend fun saveBewerb(bewerb: DomBewerb): DomBewerb
|
||||||
suspend fun deleteBewerb(id: Uuid): Boolean
|
suspend fun deleteBewerb(id: Uuid): Boolean
|
||||||
|
|
||||||
// Abteilungen
|
// Abteilungen
|
||||||
suspend fun findAbteilungById(id: Uuid): Abteilung?
|
suspend fun findAbteilungById(id: Uuid): DomAbteilung?
|
||||||
suspend fun findAbteilungenByBewerbId(bewerbId: Uuid): List<Abteilung>
|
suspend fun findAbteilungenByBewerbId(bewerbId: Uuid): List<DomAbteilung>
|
||||||
suspend fun saveAbteilung(abteilung: Abteilung): Abteilung
|
suspend fun saveAbteilung(abteilung: DomAbteilung): DomAbteilung
|
||||||
suspend fun deleteAbteilung(id: Uuid): Boolean
|
suspend fun deleteAbteilung(id: Uuid): Boolean
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,11 @@
|
||||||
package at.mocode.entries.domain.repository
|
package at.mocode.entries.domain.repository
|
||||||
|
|
||||||
import at.mocode.core.domain.model.NennStatusE
|
import at.mocode.core.domain.model.NennStatusE
|
||||||
import at.mocode.entries.domain.model.Nennung
|
import at.mocode.entries.domain.model.DomNennung
|
||||||
import kotlin.uuid.Uuid
|
import kotlin.uuid.Uuid
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Repository-Interface für Nennung (Nennung) Domain-Operationen.
|
* Repository-Interface für DomNennung (Nennung) Domain-Operationen.
|
||||||
*
|
*
|
||||||
* Definiert den Vertrag für Datenzugriffs-Operationen ohne Abhängigkeit
|
* Definiert den Vertrag für Datenzugriffs-Operationen ohne Abhängigkeit
|
||||||
* von konkreten Implementierungsdetails (Datenbank, etc.).
|
* von konkreten Implementierungsdetails (Datenbank, etc.).
|
||||||
|
|
@ -17,52 +17,52 @@ interface NennungRepository {
|
||||||
/**
|
/**
|
||||||
* Sucht eine Nennung anhand ihrer eindeutigen ID.
|
* Sucht eine Nennung anhand ihrer eindeutigen ID.
|
||||||
*/
|
*/
|
||||||
suspend fun findById(id: Uuid): Nennung?
|
suspend fun findById(id: Uuid): DomNennung?
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sucht alle Nennungen für einen bestimmten Bewerb.
|
* Sucht alle Nennungen für einen bestimmten Bewerb.
|
||||||
*/
|
*/
|
||||||
suspend fun findByBewerbId(bewerbId: Uuid): List<Nennung>
|
suspend fun findByBewerbId(bewerbId: Uuid): List<DomNennung>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sucht alle Nennungen für eine bestimmte Abteilung.
|
* Sucht alle Nennungen für eine bestimmte Abteilung.
|
||||||
*/
|
*/
|
||||||
suspend fun findByAbteilungId(abteilungId: Uuid): List<Nennung>
|
suspend fun findByAbteilungId(abteilungId: Uuid): List<DomNennung>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sucht alle Nennungen für ein bestimmtes Turnier.
|
* Sucht alle Nennungen für ein bestimmtes Turnier.
|
||||||
*/
|
*/
|
||||||
suspend fun findByTurnierId(turnierId: Uuid): List<Nennung>
|
suspend fun findByTurnierId(turnierId: Uuid): List<DomNennung>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sucht alle Nennungen eines bestimmten Reiters.
|
* Sucht alle Nennungen eines bestimmten Reiters.
|
||||||
*/
|
*/
|
||||||
suspend fun findByReiterId(reiterId: Uuid): List<Nennung>
|
suspend fun findByReiterId(reiterId: Uuid): List<DomNennung>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sucht alle Nennungen für ein bestimmtes Pferd.
|
* Sucht alle Nennungen für ein bestimmtes Pferd.
|
||||||
*/
|
*/
|
||||||
suspend fun findByPferdId(pferdId: Uuid): List<Nennung>
|
suspend fun findByPferdId(pferdId: Uuid): List<DomNennung>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sucht alle Nennungen eines Reiters für ein bestimmtes Turnier.
|
* Sucht alle Nennungen eines Reiters für ein bestimmtes Turnier.
|
||||||
*/
|
*/
|
||||||
suspend fun findByReiterIdAndTurnierId(reiterId: Uuid, turnierId: Uuid): List<Nennung>
|
suspend fun findByReiterIdAndTurnierId(reiterId: Uuid, turnierId: Uuid): List<DomNennung>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sucht alle Nennungen mit einem bestimmten Status.
|
* Sucht alle Nennungen mit einem bestimmten Status.
|
||||||
*/
|
*/
|
||||||
suspend fun findByStatus(status: NennStatusE): List<Nennung>
|
suspend fun findByStatus(status: NennStatusE): List<DomNennung>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sucht alle Nachnennungen für einen Bewerb.
|
* Sucht alle Nachnennungen für einen Bewerb.
|
||||||
*/
|
*/
|
||||||
suspend fun findNachnennungenByBewerbId(bewerbId: Uuid): List<Nennung>
|
suspend fun findNachnennungenByBewerbId(bewerbId: Uuid): List<DomNennung>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Speichert eine Nennung (Insert oder Update).
|
* Speichert eine Nennung (Insert oder Update).
|
||||||
*/
|
*/
|
||||||
suspend fun save(nennung: Nennung): Nennung
|
suspend fun save(nennung: DomNennung): DomNennung
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Löscht eine Nennung anhand ihrer ID.
|
* Löscht eine Nennung anhand ihrer ID.
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,14 @@
|
||||||
|
|
||||||
package at.mocode.entries.domain.repository
|
package at.mocode.entries.domain.repository
|
||||||
|
|
||||||
import at.mocode.entries.domain.model.NennungsTransfer
|
import at.mocode.entries.domain.model.DomNennungsTransfer
|
||||||
import kotlin.uuid.Uuid
|
import kotlin.uuid.Uuid
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Repository-Interface für NennungsTransfer Domain-Operationen.
|
* Repository-Interface für DomNennungsTransfer Domain-Operationen.
|
||||||
*/
|
*/
|
||||||
interface NennungsTransferRepository {
|
interface NennungsTransferRepository {
|
||||||
suspend fun findById(id: Uuid): NennungsTransfer?
|
suspend fun findById(id: Uuid): DomNennungsTransfer?
|
||||||
suspend fun findByUrsprungsNennungId(nennungId: Uuid): List<NennungsTransfer>
|
suspend fun findByUrsprungsNennungId(nennungId: Uuid): List<DomNennungsTransfer>
|
||||||
suspend fun save(transfer: NennungsTransfer): NennungsTransfer
|
suspend fun save(transfer: DomNennungsTransfer): DomNennungsTransfer
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,8 @@ import at.mocode.core.domain.model.AbteilungsTeilungsTypE
|
||||||
import at.mocode.core.domain.model.ReiterLizenzKlasseE
|
import at.mocode.core.domain.model.ReiterLizenzKlasseE
|
||||||
import at.mocode.core.domain.model.PruefungsTypE
|
import at.mocode.core.domain.model.PruefungsTypE
|
||||||
import at.mocode.core.domain.model.SparteE
|
import at.mocode.core.domain.model.SparteE
|
||||||
import at.mocode.entries.domain.model.Abteilung
|
import at.mocode.entries.domain.model.DomAbteilung
|
||||||
import at.mocode.entries.domain.model.Bewerb
|
import at.mocode.entries.domain.model.DomBewerb
|
||||||
import at.mocode.masterdata.domain.model.Reiter
|
import at.mocode.masterdata.domain.model.Reiter
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -32,13 +32,13 @@ class AbteilungsRegelService {
|
||||||
* @param bewerb Der betroffene Bewerb.
|
* @param bewerb Der betroffene Bewerb.
|
||||||
* @param abteilungen Liste der verfügbaren Abteilungen des Bewerbs.
|
* @param abteilungen Liste der verfügbaren Abteilungen des Bewerbs.
|
||||||
* @param reiter Der Reiter, der genannt werden soll.
|
* @param reiter Der Reiter, der genannt werden soll.
|
||||||
* @return Die passende [Abteilung] oder null, wenn keine Zuordnung eindeutig möglich ist.
|
* @return Die passende [DomAbteilung] oder null, wenn keine Zuordnung eindeutig möglich ist.
|
||||||
*/
|
*/
|
||||||
fun bestimmeAbteilung(
|
fun bestimmeAbteilung(
|
||||||
bewerb: Bewerb,
|
bewerb: DomBewerb,
|
||||||
abteilungen: List<Abteilung>,
|
abteilungen: List<DomAbteilung>,
|
||||||
reiter: Reiter
|
reiter: Reiter
|
||||||
): Abteilung? {
|
): DomAbteilung? {
|
||||||
if (abteilungen.isEmpty()) return null
|
if (abteilungen.isEmpty()) return null
|
||||||
if (abteilungen.size == 1) return abteilungen.first()
|
if (abteilungen.size == 1) return abteilungen.first()
|
||||||
|
|
||||||
|
|
@ -115,8 +115,8 @@ class AbteilungsRegelService {
|
||||||
* Beispiel CSN-C-NEU: Ein Bewerb muss zwingend eine Abteilung für lizenzfreie Reiter haben.
|
* Beispiel CSN-C-NEU: Ein Bewerb muss zwingend eine Abteilung für lizenzfreie Reiter haben.
|
||||||
*/
|
*/
|
||||||
fun validateStrukturelleVollstaendigkeit(
|
fun validateStrukturelleVollstaendigkeit(
|
||||||
bewerb: Bewerb,
|
bewerb: DomBewerb,
|
||||||
abteilungen: List<Abteilung>
|
abteilungen: List<DomAbteilung>
|
||||||
): List<String> {
|
): List<String> {
|
||||||
val warnings = mutableListOf<String>()
|
val warnings = mutableListOf<String>()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,11 +10,11 @@ import kotlin.uuid.ExperimentalUuidApi
|
||||||
import kotlin.uuid.Uuid
|
import kotlin.uuid.Uuid
|
||||||
|
|
||||||
@OptIn(ExperimentalUuidApi::class)
|
@OptIn(ExperimentalUuidApi::class)
|
||||||
class BewerbTest {
|
class DomBewerbTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `getPflichtTeilungsSchwellenwert liefert korrekte Werte fuer alle PruefungsTypen`() {
|
fun `getPflichtTeilungsSchwellenwert liefert korrekte Werte fuer alle PruefungsTypen`() {
|
||||||
val baseBewerb = Bewerb(
|
val baseBewerb = DomBewerb(
|
||||||
turnierId = Uuid.random(),
|
turnierId = Uuid.random(),
|
||||||
bewerbNummer = 1,
|
bewerbNummer = 1,
|
||||||
bezeichnung = "Test",
|
bezeichnung = "Test",
|
||||||
|
|
@ -33,7 +33,7 @@ class BewerbTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `getPflichtTeilungsSchwellenwert liefert null fuer Meisterschaftsbewerbe`() {
|
fun `getPflichtTeilungsSchwellenwert liefert null fuer Meisterschaftsbewerbe`() {
|
||||||
val meisterschaft = Bewerb(
|
val meisterschaft = DomBewerb(
|
||||||
turnierId = Uuid.random(),
|
turnierId = Uuid.random(),
|
||||||
bewerbNummer = 1,
|
bewerbNummer = 1,
|
||||||
bezeichnung = "Meisterschaft",
|
bezeichnung = "Meisterschaft",
|
||||||
|
|
@ -48,7 +48,7 @@ class BewerbTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `validateAbteilungsSchwellenwerte gibt Warnung bei Ueberschreitung des Pflicht-Schwellenwerts`() {
|
fun `validateAbteilungsSchwellenwerte gibt Warnung bei Ueberschreitung des Pflicht-Schwellenwerts`() {
|
||||||
val bewerb = Bewerb(
|
val bewerb = DomBewerb(
|
||||||
turnierId = Uuid.random(),
|
turnierId = Uuid.random(),
|
||||||
bewerbNummer = 1,
|
bewerbNummer = 1,
|
||||||
bezeichnung = "Springprüfung",
|
bezeichnung = "Springprüfung",
|
||||||
|
|
@ -64,7 +64,7 @@ class BewerbTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `validateAbteilungsSchwellenwerte gibt Warnung bei Dressur-Kann-Teilung`() {
|
fun `validateAbteilungsSchwellenwerte gibt Warnung bei Dressur-Kann-Teilung`() {
|
||||||
val bewerb = Bewerb(
|
val bewerb = DomBewerb(
|
||||||
turnierId = Uuid.random(),
|
turnierId = Uuid.random(),
|
||||||
bewerbNummer = 1,
|
bewerbNummer = 1,
|
||||||
bezeichnung = "Dressurprüfung",
|
bezeichnung = "Dressurprüfung",
|
||||||
|
|
@ -3,8 +3,8 @@
|
||||||
package at.mocode.entries.domain.service
|
package at.mocode.entries.domain.service
|
||||||
|
|
||||||
import at.mocode.core.domain.model.*
|
import at.mocode.core.domain.model.*
|
||||||
import at.mocode.entries.domain.model.Abteilung
|
import at.mocode.entries.domain.model.DomAbteilung
|
||||||
import at.mocode.entries.domain.model.Bewerb
|
import at.mocode.entries.domain.model.DomBewerb
|
||||||
import at.mocode.masterdata.domain.model.Reiter
|
import at.mocode.masterdata.domain.model.Reiter
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
|
|
@ -213,7 +213,7 @@ class AbteilungsRegelServiceTest {
|
||||||
pruefungsTyp: PruefungsTypE = PruefungsTypE.SPRINGEN_UEBRIG,
|
pruefungsTyp: PruefungsTypE = PruefungsTypE.SPRINGEN_UEBRIG,
|
||||||
teilungsTyp: AbteilungsTeilungsTypE = AbteilungsTeilungsTypE.KEINE,
|
teilungsTyp: AbteilungsTeilungsTypE = AbteilungsTeilungsTypE.KEINE,
|
||||||
hoeheCm: Int? = null
|
hoeheCm: Int? = null
|
||||||
) = Bewerb(
|
) = DomBewerb(
|
||||||
turnierId = Uuid.random(),
|
turnierId = Uuid.random(),
|
||||||
bewerbNummer = 1,
|
bewerbNummer = 1,
|
||||||
bezeichnung = "Testbewerb",
|
bezeichnung = "Testbewerb",
|
||||||
|
|
@ -229,7 +229,7 @@ class AbteilungsRegelServiceTest {
|
||||||
nummer: Int,
|
nummer: Int,
|
||||||
bezeichnung: String? = null,
|
bezeichnung: String? = null,
|
||||||
starterAnzahl: Int = 0
|
starterAnzahl: Int = 0
|
||||||
) = Abteilung(
|
) = DomAbteilung(
|
||||||
bewerbId = bewerbId,
|
bewerbId = bewerbId,
|
||||||
abteilungsNummer = nummer,
|
abteilungsNummer = nummer,
|
||||||
bezeichnung = bezeichnung,
|
bezeichnung = bezeichnung,
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,6 @@
|
||||||
|
|
||||||
package at.mocode.entries.service.bewerbe
|
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.RichterEinsatz
|
|
||||||
import kotlinx.datetime.LocalDate
|
|
||||||
import kotlinx.datetime.LocalTime
|
|
||||||
import kotlin.uuid.Uuid
|
import kotlin.uuid.Uuid
|
||||||
|
|
||||||
data class Bewerb(
|
data class Bewerb(
|
||||||
|
|
@ -15,27 +10,6 @@ data class Bewerb(
|
||||||
val klasse: String,
|
val klasse: String,
|
||||||
val hoeheCm: Int?,
|
val hoeheCm: Int?,
|
||||||
val bezeichnung: String,
|
val bezeichnung: String,
|
||||||
// Abteilungs-Konfiguration
|
|
||||||
val teilungsTyp: AbteilungsTeilungsTypE? = null,
|
|
||||||
// Text & Details
|
|
||||||
val beschreibung: String? = null,
|
|
||||||
val aufgabe: String? = null,
|
|
||||||
val aufgabenNummer: String? = null,
|
|
||||||
val paraGrade: String? = null,
|
|
||||||
// Ort & Funktionäre
|
|
||||||
val austragungsplatzId: Uuid? = null,
|
|
||||||
val richterEinsaetze: List<RichterEinsatz> = emptyList(),
|
|
||||||
// Zeitplan
|
|
||||||
val geplantesDatum: LocalDate? = null,
|
|
||||||
val beginnZeitTyp: BeginnZeitTypE? = null,
|
|
||||||
val beginnZeit: LocalTime? = null,
|
|
||||||
val reitdauerMinuten: Int? = null,
|
|
||||||
val umbauMinuten: Int? = null,
|
|
||||||
val besichtigungMinuten: Int? = null,
|
|
||||||
val stechenGeplant: Boolean = false,
|
|
||||||
// Finanzen
|
|
||||||
val startgeldCent: Long? = null,
|
|
||||||
val geldpreisAusbezahlt: Boolean = false,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
interface BewerbRepository {
|
interface BewerbRepository {
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,7 @@
|
||||||
|
|
||||||
package at.mocode.entries.service.bewerbe
|
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.RichterEinsatz
|
|
||||||
import at.mocode.entries.service.persistence.AbteilungTable
|
import at.mocode.entries.service.persistence.AbteilungTable
|
||||||
import at.mocode.entries.service.persistence.BewerbRichterEinsatzTable
|
|
||||||
import at.mocode.entries.service.persistence.BewerbTable
|
import at.mocode.entries.service.persistence.BewerbTable
|
||||||
import at.mocode.entries.service.tenant.tenantTransaction
|
import at.mocode.entries.service.tenant.tenantTransaction
|
||||||
import org.jetbrains.exposed.v1.core.ResultRow
|
import org.jetbrains.exposed.v1.core.ResultRow
|
||||||
|
|
@ -22,92 +18,25 @@ import kotlin.uuid.toKotlinUuid
|
||||||
|
|
||||||
class BewerbRepositoryImpl : BewerbRepository {
|
class BewerbRepositoryImpl : BewerbRepository {
|
||||||
|
|
||||||
private fun loadRichterEinsaetze(bewerbId: Uuid): List<RichterEinsatz> =
|
private fun rowToBewerb(row: ResultRow): Bewerb = Bewerb(
|
||||||
BewerbRichterEinsatzTable
|
id = row[BewerbTable.id].toKotlinUuid(),
|
||||||
.selectAll()
|
turnierId = row[BewerbTable.turnierId].toKotlinUuid(),
|
||||||
.where { BewerbRichterEinsatzTable.bewerbId eq bewerbId.toJavaUuid() }
|
klasse = row[BewerbTable.klasse],
|
||||||
.map { row ->
|
hoeheCm = row[BewerbTable.hoeheCm],
|
||||||
RichterEinsatz(
|
bezeichnung = row[BewerbTable.bezeichnung]
|
||||||
funktionaerId = row[BewerbRichterEinsatzTable.funktionaerId].toKotlinUuid(),
|
)
|
||||||
position = row[BewerbRichterEinsatzTable.position]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun persistRichterEinsaetze(bewerbId: Uuid, einsaetze: List<RichterEinsatz>) {
|
|
||||||
BewerbRichterEinsatzTable.deleteWhere { BewerbRichterEinsatzTable.bewerbId eq bewerbId.toJavaUuid() }
|
|
||||||
einsaetze.forEach { re ->
|
|
||||||
BewerbRichterEinsatzTable.insert { s ->
|
|
||||||
s[BewerbRichterEinsatzTable.bewerbId] = bewerbId.toJavaUuid()
|
|
||||||
s[BewerbRichterEinsatzTable.funktionaerId] = re.funktionaerId.toJavaUuid()
|
|
||||||
s[BewerbRichterEinsatzTable.position] = re.position
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun rowToBewerb(row: ResultRow): Bewerb {
|
|
||||||
val id = row[BewerbTable.id].toKotlinUuid()
|
|
||||||
return Bewerb(
|
|
||||||
id = id,
|
|
||||||
turnierId = row[BewerbTable.turnierId].toKotlinUuid(),
|
|
||||||
klasse = row[BewerbTable.klasse],
|
|
||||||
hoeheCm = row[BewerbTable.hoeheCm],
|
|
||||||
bezeichnung = row[BewerbTable.bezeichnung],
|
|
||||||
// Abteilungs-Konfiguration
|
|
||||||
teilungsTyp = row[BewerbTable.teilungsTyp]?.let { AbteilungsTeilungsTypE.valueOf(it) },
|
|
||||||
// Text & Details
|
|
||||||
beschreibung = row[BewerbTable.beschreibung],
|
|
||||||
aufgabe = row[BewerbTable.aufgabe],
|
|
||||||
aufgabenNummer = row[BewerbTable.aufgabenNummer],
|
|
||||||
paraGrade = row[BewerbTable.paraGrade],
|
|
||||||
// Ort & Funktionäre
|
|
||||||
austragungsplatzId = row[BewerbTable.austragungsplatzId]?.toKotlinUuid(),
|
|
||||||
richterEinsaetze = loadRichterEinsaetze(id),
|
|
||||||
// Zeitplan – exposed-kotlin-datetime liefert kotlinx.datetime-Typen direkt
|
|
||||||
geplantesDatum = row[BewerbTable.geplantesDatum],
|
|
||||||
beginnZeitTyp = row[BewerbTable.beginnZeitTyp]?.let { BeginnZeitTypE.valueOf(it) },
|
|
||||||
beginnZeit = row[BewerbTable.beginnZeit],
|
|
||||||
reitdauerMinuten = row[BewerbTable.reitdauerMinuten],
|
|
||||||
umbauMinuten = row[BewerbTable.umbauMinuten],
|
|
||||||
besichtigungMinuten = row[BewerbTable.besichtigungMinuten],
|
|
||||||
stechenGeplant = row[BewerbTable.stechenGeplant],
|
|
||||||
// Finanzen
|
|
||||||
startgeldCent = row[BewerbTable.startgeldCent],
|
|
||||||
geldpreisAusbezahlt = row[BewerbTable.geldpreisAusbezahlt]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun create(b: Bewerb): Bewerb = tenantTransaction {
|
override suspend fun create(b: Bewerb): Bewerb = tenantTransaction {
|
||||||
val now = Clock.System.now()
|
val now = Clock.System.now()
|
||||||
BewerbTable.insert { s ->
|
BewerbTable.insert { s ->
|
||||||
s[BewerbTable.id] = b.id.toJavaUuid()
|
s[BewerbTable.id] = b.id.toJavaUuid()
|
||||||
s[BewerbTable.turnierId] = b.turnierId.toJavaUuid()
|
s[BewerbTable.turnierId] = b.turnierId.toJavaUuid()
|
||||||
s[BewerbTable.klasse] = b.klasse
|
s[BewerbTable.klasse] = b.klasse
|
||||||
s[BewerbTable.hoeheCm] = b.hoeheCm
|
s[BewerbTable.hoeheCm] = b.hoeheCm
|
||||||
s[BewerbTable.bezeichnung] = b.bezeichnung
|
s[BewerbTable.bezeichnung] = b.bezeichnung
|
||||||
// Abteilungs-Konfiguration
|
s[BewerbTable.createdAt] = now
|
||||||
s[BewerbTable.teilungsTyp] = b.teilungsTyp?.name
|
s[BewerbTable.updatedAt] = now
|
||||||
// Text & Details
|
|
||||||
s[BewerbTable.beschreibung] = b.beschreibung
|
|
||||||
s[BewerbTable.aufgabe] = b.aufgabe
|
|
||||||
s[BewerbTable.aufgabenNummer] = b.aufgabenNummer
|
|
||||||
s[BewerbTable.paraGrade] = b.paraGrade
|
|
||||||
// Ort
|
|
||||||
s[BewerbTable.austragungsplatzId] = b.austragungsplatzId?.toJavaUuid()
|
|
||||||
// Zeitplan – kotlinx.datetime-Typen direkt übergeben
|
|
||||||
s[BewerbTable.geplantesDatum] = b.geplantesDatum
|
|
||||||
s[BewerbTable.beginnZeitTyp] = b.beginnZeitTyp?.name
|
|
||||||
s[BewerbTable.beginnZeit] = b.beginnZeit
|
|
||||||
s[BewerbTable.reitdauerMinuten] = b.reitdauerMinuten
|
|
||||||
s[BewerbTable.umbauMinuten] = b.umbauMinuten
|
|
||||||
s[BewerbTable.besichtigungMinuten] = b.besichtigungMinuten
|
|
||||||
s[BewerbTable.stechenGeplant] = b.stechenGeplant
|
|
||||||
// Finanzen
|
|
||||||
s[BewerbTable.startgeldCent] = b.startgeldCent
|
|
||||||
s[BewerbTable.geldpreisAusbezahlt] = b.geldpreisAusbezahlt
|
|
||||||
s[BewerbTable.createdAt] = now
|
|
||||||
s[BewerbTable.updatedAt] = now
|
|
||||||
}
|
}
|
||||||
persistRichterEinsaetze(b.id, b.richterEinsaetze)
|
|
||||||
BewerbTable.selectAll().where { BewerbTable.id eq b.id.toJavaUuid() }.map(::rowToBewerb).single()
|
BewerbTable.selectAll().where { BewerbTable.id eq b.id.toJavaUuid() }.map(::rowToBewerb).single()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -125,32 +54,11 @@ class BewerbRepositoryImpl : BewerbRepository {
|
||||||
override suspend fun update(b: Bewerb): Bewerb = tenantTransaction {
|
override suspend fun update(b: Bewerb): Bewerb = tenantTransaction {
|
||||||
val now = Clock.System.now()
|
val now = Clock.System.now()
|
||||||
BewerbTable.update({ BewerbTable.id eq b.id.toJavaUuid() }) { s ->
|
BewerbTable.update({ BewerbTable.id eq b.id.toJavaUuid() }) { s ->
|
||||||
s[BewerbTable.klasse] = b.klasse
|
s[BewerbTable.klasse] = b.klasse
|
||||||
s[BewerbTable.hoeheCm] = b.hoeheCm
|
s[BewerbTable.hoeheCm] = b.hoeheCm
|
||||||
s[BewerbTable.bezeichnung] = b.bezeichnung
|
s[BewerbTable.bezeichnung] = b.bezeichnung
|
||||||
// Abteilungs-Konfiguration
|
s[BewerbTable.updatedAt] = now
|
||||||
s[BewerbTable.teilungsTyp] = b.teilungsTyp?.name
|
|
||||||
// Text & Details
|
|
||||||
s[BewerbTable.beschreibung] = b.beschreibung
|
|
||||||
s[BewerbTable.aufgabe] = b.aufgabe
|
|
||||||
s[BewerbTable.aufgabenNummer] = b.aufgabenNummer
|
|
||||||
s[BewerbTable.paraGrade] = b.paraGrade
|
|
||||||
// Ort
|
|
||||||
s[BewerbTable.austragungsplatzId] = b.austragungsplatzId?.toJavaUuid()
|
|
||||||
// Zeitplan – kotlinx.datetime-Typen direkt übergeben
|
|
||||||
s[BewerbTable.geplantesDatum] = b.geplantesDatum
|
|
||||||
s[BewerbTable.beginnZeitTyp] = b.beginnZeitTyp?.name
|
|
||||||
s[BewerbTable.beginnZeit] = b.beginnZeit
|
|
||||||
s[BewerbTable.reitdauerMinuten] = b.reitdauerMinuten
|
|
||||||
s[BewerbTable.umbauMinuten] = b.umbauMinuten
|
|
||||||
s[BewerbTable.besichtigungMinuten] = b.besichtigungMinuten
|
|
||||||
s[BewerbTable.stechenGeplant] = b.stechenGeplant
|
|
||||||
// Finanzen
|
|
||||||
s[BewerbTable.startgeldCent] = b.startgeldCent
|
|
||||||
s[BewerbTable.geldpreisAusbezahlt] = b.geldpreisAusbezahlt
|
|
||||||
s[BewerbTable.updatedAt] = now
|
|
||||||
}
|
}
|
||||||
persistRichterEinsaetze(b.id, b.richterEinsaetze)
|
|
||||||
BewerbTable.selectAll().where { BewerbTable.id eq b.id.toJavaUuid() }.map(::rowToBewerb).single()
|
BewerbTable.selectAll().where { BewerbTable.id eq b.id.toJavaUuid() }.map(::rowToBewerb).single()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ import at.mocode.entries.domain.repository.NennungRepository
|
||||||
import at.mocode.entries.service.errors.LockedException
|
import at.mocode.entries.service.errors.LockedException
|
||||||
import at.mocode.entries.service.persistence.TurnierTable
|
import at.mocode.entries.service.persistence.TurnierTable
|
||||||
import at.mocode.entries.service.tenant.tenantTransaction
|
import at.mocode.entries.service.tenant.tenantTransaction
|
||||||
import at.mocode.entries.domain.model.RichterEinsatz
|
|
||||||
import org.jetbrains.exposed.v1.core.eq
|
import org.jetbrains.exposed.v1.core.eq
|
||||||
import org.jetbrains.exposed.v1.jdbc.selectAll
|
import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||||
import kotlin.uuid.Uuid
|
import kotlin.uuid.Uuid
|
||||||
|
|
@ -22,35 +21,14 @@ class BewerbService(
|
||||||
row?.get(TurnierTable.status) == "PUBLISHED"
|
row?.get(TurnierTable.status) == "PUBLISHED"
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun create(turnierId: Uuid, req: CreateBewerbRequest): Bewerb {
|
suspend fun create(turnierId: Uuid, klasse: String, hoeheCm: Int?, bezeichnung: String): Bewerb {
|
||||||
if (isTurnierPublished(turnierId)) throw LockedException("Turnier ist PUBLISHED – Bewerbe können nicht angelegt werden")
|
if (isTurnierPublished(turnierId)) throw LockedException("Turnier ist PUBLISHED – Bewerbe können nicht angelegt werden")
|
||||||
val b = Bewerb(
|
val b = Bewerb(
|
||||||
id = Uuid.random(),
|
id = Uuid.random(),
|
||||||
turnierId = turnierId,
|
turnierId = turnierId,
|
||||||
klasse = req.klasse,
|
klasse = klasse,
|
||||||
hoeheCm = req.hoeheCm,
|
hoeheCm = hoeheCm,
|
||||||
bezeichnung = req.bezeichnung,
|
bezeichnung = bezeichnung
|
||||||
// Abteilungs-Konfiguration
|
|
||||||
teilungsTyp = req.teilungsTyp,
|
|
||||||
// Text & Details
|
|
||||||
beschreibung = req.beschreibung,
|
|
||||||
aufgabe = req.aufgabe,
|
|
||||||
aufgabenNummer = req.aufgabenNummer,
|
|
||||||
paraGrade = req.paraGrade,
|
|
||||||
// Ort & Funktionäre
|
|
||||||
austragungsplatzId = req.austragungsplatzId?.let { Uuid.parse(it) },
|
|
||||||
richterEinsaetze = req.richterEinsaetze.map { RichterEinsatz(Uuid.parse(it.funktionaerId), it.position) },
|
|
||||||
// Zeitplan
|
|
||||||
geplantesDatum = req.geplantesDatum,
|
|
||||||
beginnZeitTyp = req.beginnZeitTyp,
|
|
||||||
beginnZeit = req.beginnZeit,
|
|
||||||
reitdauerMinuten = req.reitdauerMinuten,
|
|
||||||
umbauMinuten = req.umbauMinuten,
|
|
||||||
besichtigungMinuten = req.besichtigungMinuten,
|
|
||||||
stechenGeplant = req.stechenGeplant,
|
|
||||||
// Finanzen
|
|
||||||
startgeldCent = req.startgeldCent,
|
|
||||||
geldpreisAusbezahlt = req.geldpreisAusbezahlt
|
|
||||||
)
|
)
|
||||||
return repo.create(b)
|
return repo.create(b)
|
||||||
}
|
}
|
||||||
|
|
@ -59,36 +37,10 @@ class BewerbService(
|
||||||
|
|
||||||
suspend fun get(id: Uuid): Bewerb = repo.findById(id) ?: throw NoSuchElementException("Bewerb $id nicht gefunden")
|
suspend fun get(id: Uuid): Bewerb = repo.findById(id) ?: throw NoSuchElementException("Bewerb $id nicht gefunden")
|
||||||
|
|
||||||
suspend fun update(id: Uuid, req: UpdateBewerbRequest): Bewerb {
|
suspend fun update(id: Uuid, klasse: String, hoeheCm: Int?, bezeichnung: String): Bewerb {
|
||||||
val current = get(id)
|
val current = get(id)
|
||||||
if (isTurnierPublished(current.turnierId)) throw LockedException("Turnier ist PUBLISHED – Bewerbe können nicht geändert werden")
|
if (isTurnierPublished(current.turnierId)) throw LockedException("Turnier ist PUBLISHED – Bewerbe können nicht geändert werden")
|
||||||
val updated = current.copy(
|
return repo.update(current.copy(klasse = klasse, hoeheCm = hoeheCm, bezeichnung = bezeichnung))
|
||||||
klasse = req.klasse,
|
|
||||||
hoeheCm = req.hoeheCm,
|
|
||||||
bezeichnung = req.bezeichnung,
|
|
||||||
// Abteilungs-Konfiguration
|
|
||||||
teilungsTyp = req.teilungsTyp,
|
|
||||||
// Text & Details
|
|
||||||
beschreibung = req.beschreibung,
|
|
||||||
aufgabe = req.aufgabe,
|
|
||||||
aufgabenNummer = req.aufgabenNummer,
|
|
||||||
paraGrade = req.paraGrade,
|
|
||||||
// Ort & Funktionäre
|
|
||||||
austragungsplatzId = req.austragungsplatzId?.let { Uuid.parse(it) },
|
|
||||||
richterEinsaetze = req.richterEinsaetze.map { RichterEinsatz(Uuid.parse(it.funktionaerId), it.position) },
|
|
||||||
// Zeitplan
|
|
||||||
geplantesDatum = req.geplantesDatum,
|
|
||||||
beginnZeitTyp = req.beginnZeitTyp,
|
|
||||||
beginnZeit = req.beginnZeit,
|
|
||||||
reitdauerMinuten = req.reitdauerMinuten,
|
|
||||||
umbauMinuten = req.umbauMinuten,
|
|
||||||
besichtigungMinuten = req.besichtigungMinuten,
|
|
||||||
stechenGeplant = req.stechenGeplant,
|
|
||||||
// Finanzen
|
|
||||||
startgeldCent = req.startgeldCent,
|
|
||||||
geldpreisAusbezahlt = req.geldpreisAusbezahlt,
|
|
||||||
)
|
|
||||||
return repo.update(updated)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun delete(id: Uuid) {
|
suspend fun delete(id: Uuid) {
|
||||||
|
|
|
||||||
|
|
@ -2,149 +2,20 @@
|
||||||
|
|
||||||
package at.mocode.entries.service.bewerbe
|
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.RichterEinsatz
|
|
||||||
import kotlinx.datetime.LocalDate
|
|
||||||
import kotlinx.datetime.LocalTime
|
|
||||||
import org.springframework.http.HttpStatus
|
import org.springframework.http.HttpStatus
|
||||||
import org.springframework.web.bind.annotation.*
|
import org.springframework.web.bind.annotation.*
|
||||||
import kotlin.uuid.Uuid
|
import kotlin.uuid.Uuid
|
||||||
|
|
||||||
// --- DTOs ---
|
|
||||||
data class RichterEinsatzDto(
|
|
||||||
val funktionaerId: String,
|
|
||||||
val position: String,
|
|
||||||
)
|
|
||||||
|
|
||||||
data class CreateBewerbRequest(
|
data class CreateBewerbRequest(
|
||||||
// Basis
|
|
||||||
val klasse: String,
|
val klasse: String,
|
||||||
val hoeheCm: Int? = null,
|
val hoeheCm: Int? = null,
|
||||||
val bezeichnung: String,
|
val bezeichnung: String,
|
||||||
|
|
||||||
// Abteilungs-Konfiguration
|
|
||||||
val teilungsTyp: AbteilungsTeilungsTypE? = null,
|
|
||||||
|
|
||||||
// Text & Details
|
|
||||||
val beschreibung: String? = null,
|
|
||||||
val aufgabe: String? = null,
|
|
||||||
val aufgabenNummer: String? = null,
|
|
||||||
val paraGrade: String? = null,
|
|
||||||
|
|
||||||
// Ort & Funktionäre
|
|
||||||
val austragungsplatzId: String? = null,
|
|
||||||
val richterEinsaetze: List<RichterEinsatzDto> = emptyList(),
|
|
||||||
|
|
||||||
// Zeitplan
|
|
||||||
val geplantesDatum: LocalDate? = null,
|
|
||||||
val beginnZeitTyp: BeginnZeitTypE? = null,
|
|
||||||
val beginnZeit: LocalTime? = null,
|
|
||||||
val reitdauerMinuten: Int? = null,
|
|
||||||
val umbauMinuten: Int? = null,
|
|
||||||
val besichtigungMinuten: Int? = null,
|
|
||||||
val stechenGeplant: Boolean = false,
|
|
||||||
|
|
||||||
// Finanzen
|
|
||||||
val startgeldCent: Long? = null,
|
|
||||||
val geldpreisAusbezahlt: Boolean = false,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
data class UpdateBewerbRequest(
|
data class UpdateBewerbRequest(
|
||||||
// Basis
|
|
||||||
val klasse: String,
|
val klasse: String,
|
||||||
val hoeheCm: Int? = null,
|
val hoeheCm: Int? = null,
|
||||||
val bezeichnung: String,
|
val bezeichnung: String,
|
||||||
|
|
||||||
// Abteilungs-Konfiguration
|
|
||||||
val teilungsTyp: AbteilungsTeilungsTypE? = null,
|
|
||||||
|
|
||||||
// Text & Details
|
|
||||||
val beschreibung: String? = null,
|
|
||||||
val aufgabe: String? = null,
|
|
||||||
val aufgabenNummer: String? = null,
|
|
||||||
val paraGrade: String? = null,
|
|
||||||
|
|
||||||
// Ort & Funktionäre
|
|
||||||
val austragungsplatzId: String? = null,
|
|
||||||
val richterEinsaetze: List<RichterEinsatzDto> = emptyList(),
|
|
||||||
|
|
||||||
// Zeitplan
|
|
||||||
val geplantesDatum: LocalDate? = null,
|
|
||||||
val beginnZeitTyp: BeginnZeitTypE? = null,
|
|
||||||
val beginnZeit: LocalTime? = null,
|
|
||||||
val reitdauerMinuten: Int? = null,
|
|
||||||
val umbauMinuten: Int? = null,
|
|
||||||
val besichtigungMinuten: Int? = null,
|
|
||||||
val stechenGeplant: Boolean = false,
|
|
||||||
|
|
||||||
// Finanzen
|
|
||||||
val startgeldCent: Long? = null,
|
|
||||||
val geldpreisAusbezahlt: Boolean = false,
|
|
||||||
)
|
|
||||||
|
|
||||||
data class BewerbResponse(
|
|
||||||
val id: String,
|
|
||||||
val turnierId: String,
|
|
||||||
val klasse: String,
|
|
||||||
val hoeheCm: Int?,
|
|
||||||
val bezeichnung: String,
|
|
||||||
|
|
||||||
// Abteilungs-Konfiguration
|
|
||||||
val teilungsTyp: AbteilungsTeilungsTypE?,
|
|
||||||
|
|
||||||
// Text & Details
|
|
||||||
val beschreibung: String?,
|
|
||||||
val aufgabe: String?,
|
|
||||||
val aufgabenNummer: String?,
|
|
||||||
val paraGrade: String?,
|
|
||||||
|
|
||||||
// Ort & Funktionäre
|
|
||||||
val austragungsplatzId: String?,
|
|
||||||
val richterEinsaetze: List<RichterEinsatzDto>,
|
|
||||||
|
|
||||||
// Zeitplan
|
|
||||||
val geplantesDatum: LocalDate?,
|
|
||||||
val beginnZeitTyp: BeginnZeitTypE?,
|
|
||||||
val beginnZeit: LocalTime?,
|
|
||||||
val reitdauerMinuten: Int?,
|
|
||||||
val umbauMinuten: Int?,
|
|
||||||
val besichtigungMinuten: Int?,
|
|
||||||
val stechenGeplant: Boolean,
|
|
||||||
|
|
||||||
// Finanzen
|
|
||||||
val startgeldCent: Long?,
|
|
||||||
val geldpreisAusbezahlt: Boolean,
|
|
||||||
)
|
|
||||||
|
|
||||||
private fun RichterEinsatzDto.toDomain(): RichterEinsatz =
|
|
||||||
RichterEinsatz(
|
|
||||||
funktionaerId = Uuid.parse(this.funktionaerId),
|
|
||||||
position = this.position
|
|
||||||
)
|
|
||||||
|
|
||||||
private fun domainToDto(b: Bewerb): BewerbResponse = BewerbResponse(
|
|
||||||
id = b.id.toString(),
|
|
||||||
turnierId = b.turnierId.toString(),
|
|
||||||
klasse = b.klasse,
|
|
||||||
hoeheCm = b.hoeheCm,
|
|
||||||
bezeichnung = b.bezeichnung,
|
|
||||||
teilungsTyp = b.teilungsTyp,
|
|
||||||
beschreibung = b.beschreibung,
|
|
||||||
aufgabe = b.aufgabe,
|
|
||||||
aufgabenNummer = b.aufgabenNummer,
|
|
||||||
paraGrade = b.paraGrade,
|
|
||||||
austragungsplatzId = b.austragungsplatzId?.toString(),
|
|
||||||
richterEinsaetze = b.richterEinsaetze.map { RichterEinsatzDto(it.funktionaerId.toString(), it.position) },
|
|
||||||
geplantesDatum = b.geplantesDatum,
|
|
||||||
beginnZeitTyp = b.beginnZeitTyp,
|
|
||||||
beginnZeit = b.beginnZeit,
|
|
||||||
reitdauerMinuten = b.reitdauerMinuten,
|
|
||||||
umbauMinuten = b.umbauMinuten,
|
|
||||||
besichtigungMinuten = b.besichtigungMinuten,
|
|
||||||
stechenGeplant = b.stechenGeplant,
|
|
||||||
startgeldCent = b.startgeldCent,
|
|
||||||
geldpreisAusbezahlt = b.geldpreisAusbezahlt,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
|
|
@ -157,26 +28,21 @@ class BewerbeController(
|
||||||
suspend fun create(
|
suspend fun create(
|
||||||
@PathVariable turnierId: String,
|
@PathVariable turnierId: String,
|
||||||
@RequestBody body: CreateBewerbRequest
|
@RequestBody body: CreateBewerbRequest
|
||||||
): BewerbResponse = domainToDto(
|
): Bewerb = service.create(Uuid.parse(turnierId), body.klasse, body.hoeheCm, body.bezeichnung)
|
||||||
service.create(
|
|
||||||
Uuid.parse(turnierId),
|
|
||||||
body
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
@GetMapping("/turniere/{turnierId}/bewerbe")
|
@GetMapping("/turniere/{turnierId}/bewerbe")
|
||||||
suspend fun list(
|
suspend fun list(
|
||||||
@PathVariable turnierId: String,
|
@PathVariable turnierId: String,
|
||||||
@RequestParam(required = false) klasse: String?,
|
@RequestParam(required = false) klasse: String?,
|
||||||
@RequestParam(required = false) q: String?,
|
@RequestParam(required = false) q: String?,
|
||||||
): List<BewerbResponse> = service.list(Uuid.parse(turnierId), klasse, q).map(::domainToDto)
|
): List<Bewerb> = service.list(Uuid.parse(turnierId), klasse, q)
|
||||||
|
|
||||||
@GetMapping("/bewerbe/{id}")
|
@GetMapping("/bewerbe/{id}")
|
||||||
suspend fun get(@PathVariable id: String): BewerbResponse = domainToDto(service.get(Uuid.parse(id)))
|
suspend fun get(@PathVariable id: String): Bewerb = service.get(Uuid.parse(id))
|
||||||
|
|
||||||
@PutMapping("/bewerbe/{id}")
|
@PutMapping("/bewerbe/{id}")
|
||||||
suspend fun update(@PathVariable id: String, @RequestBody body: UpdateBewerbRequest): BewerbResponse =
|
suspend fun update(@PathVariable id: String, @RequestBody body: UpdateBewerbRequest): Bewerb =
|
||||||
domainToDto(service.update(Uuid.parse(id), body))
|
service.update(Uuid.parse(id), body.klasse, body.hoeheCm, body.bezeichnung)
|
||||||
|
|
||||||
@DeleteMapping("/bewerbe/{id}")
|
@DeleteMapping("/bewerbe/{id}")
|
||||||
@ResponseStatus(HttpStatus.NO_CONTENT)
|
@ResponseStatus(HttpStatus.NO_CONTENT)
|
||||||
|
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
package at.mocode.entries.service.persistence
|
|
||||||
|
|
||||||
import org.jetbrains.exposed.v1.core.Table
|
|
||||||
import org.jetbrains.exposed.v1.core.java.javaUUID
|
|
||||||
|
|
||||||
object BewerbRichterEinsatzTable : Table("bewerb_richter_einsaetze") {
|
|
||||||
val id = javaUUID("id").autoGenerate()
|
|
||||||
val bewerbId = javaUUID("bewerb_id").references(BewerbTable.id)
|
|
||||||
val funktionaerId = javaUUID("funktionaer_id")
|
|
||||||
val position = varchar("position", 100)
|
|
||||||
|
|
||||||
override val primaryKey = PrimaryKey(id)
|
|
||||||
|
|
||||||
init {
|
|
||||||
index(false, bewerbId)
|
|
||||||
index(false, funktionaerId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -2,44 +2,16 @@ package at.mocode.entries.service.persistence
|
||||||
|
|
||||||
import org.jetbrains.exposed.v1.core.Table
|
import org.jetbrains.exposed.v1.core.Table
|
||||||
import org.jetbrains.exposed.v1.core.java.javaUUID
|
import org.jetbrains.exposed.v1.core.java.javaUUID
|
||||||
import org.jetbrains.exposed.v1.datetime.date
|
|
||||||
import org.jetbrains.exposed.v1.datetime.time
|
|
||||||
import org.jetbrains.exposed.v1.datetime.timestamp
|
import org.jetbrains.exposed.v1.datetime.timestamp
|
||||||
|
|
||||||
object BewerbTable : Table("bewerbe") {
|
object BewerbTable : Table("bewerbe") {
|
||||||
val id = javaUUID("id").autoGenerate()
|
val id = javaUUID("id").autoGenerate()
|
||||||
val turnierId = javaUUID("turnier_id")
|
val turnierId = javaUUID("turnier_id")
|
||||||
val klasse = varchar("klasse", 50)
|
val klasse = varchar("klasse", 50)
|
||||||
val hoeheCm = integer("hoehe_cm").nullable()
|
val hoeheCm = integer("hoehe_cm").nullable()
|
||||||
val bezeichnung = text("bezeichnung")
|
val bezeichnung = text("bezeichnung")
|
||||||
|
val createdAt = timestamp("created_at")
|
||||||
// Abteilungs-Konfiguration
|
val updatedAt = timestamp("updated_at")
|
||||||
val teilungsTyp = varchar("teilungs_typ", 50).nullable()
|
|
||||||
|
|
||||||
// Text & Details
|
|
||||||
val beschreibung = text("beschreibung").nullable()
|
|
||||||
val aufgabe = varchar("aufgabe", 100).nullable()
|
|
||||||
val aufgabenNummer = varchar("aufgaben_nummer", 50).nullable()
|
|
||||||
val paraGrade = varchar("para_grade", 50).nullable()
|
|
||||||
|
|
||||||
// Ort & Funktionäre
|
|
||||||
val austragungsplatzId = javaUUID("austragungsplatz_id").nullable()
|
|
||||||
|
|
||||||
// Zeitplan
|
|
||||||
val geplantesDatum = date("geplantes_datum").nullable()
|
|
||||||
val beginnZeitTyp = varchar("beginn_zeit_typ", 20).nullable()
|
|
||||||
val beginnZeit = time("beginn_zeit").nullable()
|
|
||||||
val reitdauerMinuten = integer("reitdauer_minuten").nullable()
|
|
||||||
val umbauMinuten = integer("umbau_minuten").nullable()
|
|
||||||
val besichtigungMinuten = integer("besichtigung_minuten").nullable()
|
|
||||||
val stechenGeplant = bool("stechen_geplant").default(false)
|
|
||||||
|
|
||||||
// Finanzen
|
|
||||||
val startgeldCent = long("startgeld_cent").nullable()
|
|
||||||
val geldpreisAusbezahlt = bool("geldpreis_ausbezahlt").default(false)
|
|
||||||
|
|
||||||
val createdAt = timestamp("created_at")
|
|
||||||
val updatedAt = timestamp("updated_at")
|
|
||||||
|
|
||||||
override val primaryKey = PrimaryKey(id)
|
override val primaryKey = PrimaryKey(id)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ package at.mocode.entries.service.persistence
|
||||||
|
|
||||||
import at.mocode.core.domain.model.NennStatusE
|
import at.mocode.core.domain.model.NennStatusE
|
||||||
import at.mocode.core.domain.model.StartwunschE
|
import at.mocode.core.domain.model.StartwunschE
|
||||||
import at.mocode.entries.domain.model.Nennung
|
import at.mocode.entries.domain.model.DomNennung
|
||||||
import at.mocode.entries.domain.repository.NennungRepository
|
import at.mocode.entries.domain.repository.NennungRepository
|
||||||
import org.jetbrains.exposed.v1.core.ResultRow
|
import org.jetbrains.exposed.v1.core.ResultRow
|
||||||
import org.jetbrains.exposed.v1.core.and
|
import org.jetbrains.exposed.v1.core.and
|
||||||
|
|
@ -24,7 +24,7 @@ import kotlin.uuid.toKotlinUuid
|
||||||
*/
|
*/
|
||||||
class NennungRepositoryImpl : NennungRepository {
|
class NennungRepositoryImpl : NennungRepository {
|
||||||
|
|
||||||
private fun rowToNennung(row: ResultRow): Nennung = Nennung(
|
private fun rowToNennung(row: ResultRow): DomNennung = DomNennung(
|
||||||
nennungId = row[NennungTable.id].toKotlinUuid(),
|
nennungId = row[NennungTable.id].toKotlinUuid(),
|
||||||
abteilungId = row[NennungTable.abteilungId].toKotlinUuid(),
|
abteilungId = row[NennungTable.abteilungId].toKotlinUuid(),
|
||||||
bewerbId = row[NennungTable.bewerbId].toKotlinUuid(),
|
bewerbId = row[NennungTable.bewerbId].toKotlinUuid(),
|
||||||
|
|
@ -41,57 +41,57 @@ class NennungRepositoryImpl : NennungRepository {
|
||||||
updatedAt = row[NennungTable.updatedAt]
|
updatedAt = row[NennungTable.updatedAt]
|
||||||
)
|
)
|
||||||
|
|
||||||
override suspend fun findById(id: Uuid): Nennung? = tenantTransaction {
|
override suspend fun findById(id: Uuid): DomNennung? = tenantTransaction {
|
||||||
NennungTable.selectAll().where { NennungTable.id eq id.toJavaUuid() }
|
NennungTable.selectAll().where { NennungTable.id eq id.toJavaUuid() }
|
||||||
.map(::rowToNennung)
|
.map(::rowToNennung)
|
||||||
.singleOrNull()
|
.singleOrNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun findByBewerbId(bewerbId: Uuid): List<Nennung> = tenantTransaction {
|
override suspend fun findByBewerbId(bewerbId: Uuid): List<DomNennung> = tenantTransaction {
|
||||||
NennungTable.selectAll().where { NennungTable.bewerbId eq bewerbId.toJavaUuid() }
|
NennungTable.selectAll().where { NennungTable.bewerbId eq bewerbId.toJavaUuid() }
|
||||||
.map(::rowToNennung)
|
.map(::rowToNennung)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun findByAbteilungId(abteilungId: Uuid): List<Nennung> = tenantTransaction {
|
override suspend fun findByAbteilungId(abteilungId: Uuid): List<DomNennung> = tenantTransaction {
|
||||||
NennungTable.selectAll().where { NennungTable.abteilungId eq abteilungId.toJavaUuid() }
|
NennungTable.selectAll().where { NennungTable.abteilungId eq abteilungId.toJavaUuid() }
|
||||||
.map(::rowToNennung)
|
.map(::rowToNennung)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun findByTurnierId(turnierId: Uuid): List<Nennung> = tenantTransaction {
|
override suspend fun findByTurnierId(turnierId: Uuid): List<DomNennung> = tenantTransaction {
|
||||||
NennungTable.selectAll().where { NennungTable.turnierId eq turnierId.toJavaUuid() }
|
NennungTable.selectAll().where { NennungTable.turnierId eq turnierId.toJavaUuid() }
|
||||||
.map(::rowToNennung)
|
.map(::rowToNennung)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun findByReiterId(reiterId: Uuid): List<Nennung> = tenantTransaction {
|
override suspend fun findByReiterId(reiterId: Uuid): List<DomNennung> = tenantTransaction {
|
||||||
NennungTable.selectAll().where { NennungTable.reiterId eq reiterId.toJavaUuid() }
|
NennungTable.selectAll().where { NennungTable.reiterId eq reiterId.toJavaUuid() }
|
||||||
.map(::rowToNennung)
|
.map(::rowToNennung)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun findByPferdId(pferdId: Uuid): List<Nennung> = tenantTransaction {
|
override suspend fun findByPferdId(pferdId: Uuid): List<DomNennung> = tenantTransaction {
|
||||||
NennungTable.selectAll().where { NennungTable.pferdId eq pferdId.toJavaUuid() }
|
NennungTable.selectAll().where { NennungTable.pferdId eq pferdId.toJavaUuid() }
|
||||||
.map(::rowToNennung)
|
.map(::rowToNennung)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun findByReiterIdAndTurnierId(reiterId: Uuid, turnierId: Uuid): List<Nennung> = tenantTransaction {
|
override suspend fun findByReiterIdAndTurnierId(reiterId: Uuid, turnierId: Uuid): List<DomNennung> = tenantTransaction {
|
||||||
NennungTable.selectAll().where {
|
NennungTable.selectAll().where {
|
||||||
(NennungTable.reiterId eq reiterId.toJavaUuid()) and
|
(NennungTable.reiterId eq reiterId.toJavaUuid()) and
|
||||||
(NennungTable.turnierId eq turnierId.toJavaUuid())
|
(NennungTable.turnierId eq turnierId.toJavaUuid())
|
||||||
}.map(::rowToNennung)
|
}.map(::rowToNennung)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun findByStatus(status: NennStatusE): List<Nennung> = tenantTransaction {
|
override suspend fun findByStatus(status: NennStatusE): List<DomNennung> = tenantTransaction {
|
||||||
NennungTable.selectAll().where { NennungTable.status eq status.name }
|
NennungTable.selectAll().where { NennungTable.status eq status.name }
|
||||||
.map(::rowToNennung)
|
.map(::rowToNennung)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun findNachnennungenByBewerbId(bewerbId: Uuid): List<Nennung> = tenantTransaction {
|
override suspend fun findNachnennungenByBewerbId(bewerbId: Uuid): List<DomNennung> = tenantTransaction {
|
||||||
NennungTable.selectAll().where {
|
NennungTable.selectAll().where {
|
||||||
(NennungTable.bewerbId eq bewerbId.toJavaUuid()) and
|
(NennungTable.bewerbId eq bewerbId.toJavaUuid()) and
|
||||||
(NennungTable.istNachnennung eq true)
|
(NennungTable.istNachnennung eq true)
|
||||||
}.map(::rowToNennung)
|
}.map(::rowToNennung)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun save(nennung: Nennung): Nennung = tenantTransaction {
|
override suspend fun save(nennung: DomNennung): DomNennung = tenantTransaction {
|
||||||
val now = Clock.System.now()
|
val now = Clock.System.now()
|
||||||
val existing = NennungTable.selectAll()
|
val existing = NennungTable.selectAll()
|
||||||
.where { NennungTable.id eq nennung.nennungId.toJavaUuid() }
|
.where { NennungTable.id eq nennung.nennungId.toJavaUuid() }
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
package at.mocode.entries.service.persistence
|
package at.mocode.entries.service.persistence
|
||||||
|
|
||||||
import at.mocode.entries.domain.model.NennungsTransfer
|
import at.mocode.entries.domain.model.DomNennungsTransfer
|
||||||
import at.mocode.entries.domain.repository.NennungsTransferRepository
|
import at.mocode.entries.domain.repository.NennungsTransferRepository
|
||||||
import org.jetbrains.exposed.v1.core.ResultRow
|
import org.jetbrains.exposed.v1.core.ResultRow
|
||||||
import org.jetbrains.exposed.v1.core.eq
|
import org.jetbrains.exposed.v1.core.eq
|
||||||
|
|
@ -19,7 +19,7 @@ import kotlin.uuid.toKotlinUuid
|
||||||
*/
|
*/
|
||||||
class NennungsTransferRepositoryImpl : NennungsTransferRepository {
|
class NennungsTransferRepositoryImpl : NennungsTransferRepository {
|
||||||
|
|
||||||
private fun rowToTransfer(row: ResultRow): NennungsTransfer = NennungsTransfer(
|
private fun rowToTransfer(row: ResultRow): DomNennungsTransfer = DomNennungsTransfer(
|
||||||
transferId = row[NennungsTransferTable.id].toKotlinUuid(),
|
transferId = row[NennungsTransferTable.id].toKotlinUuid(),
|
||||||
ursprungsNennungId = row[NennungsTransferTable.ursprungsNennungId].toKotlinUuid(),
|
ursprungsNennungId = row[NennungsTransferTable.ursprungsNennungId].toKotlinUuid(),
|
||||||
neueNennungId = row[NennungsTransferTable.neueNennungId].toKotlinUuid(),
|
neueNennungId = row[NennungsTransferTable.neueNennungId].toKotlinUuid(),
|
||||||
|
|
@ -34,19 +34,19 @@ class NennungsTransferRepositoryImpl : NennungsTransferRepository {
|
||||||
createdAt = row[NennungsTransferTable.createdAt]
|
createdAt = row[NennungsTransferTable.createdAt]
|
||||||
)
|
)
|
||||||
|
|
||||||
override suspend fun findById(id: Uuid): NennungsTransfer? = tenantTransaction {
|
override suspend fun findById(id: Uuid): DomNennungsTransfer? = tenantTransaction {
|
||||||
NennungsTransferTable.selectAll().where { NennungsTransferTable.id eq id.toJavaUuid() }
|
NennungsTransferTable.selectAll().where { NennungsTransferTable.id eq id.toJavaUuid() }
|
||||||
.map(::rowToTransfer)
|
.map(::rowToTransfer)
|
||||||
.singleOrNull()
|
.singleOrNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun findByUrsprungsNennungId(nennungId: Uuid): List<NennungsTransfer> = tenantTransaction {
|
override suspend fun findByUrsprungsNennungId(nennungId: Uuid): List<DomNennungsTransfer> = tenantTransaction {
|
||||||
NennungsTransferTable.selectAll()
|
NennungsTransferTable.selectAll()
|
||||||
.where { NennungsTransferTable.ursprungsNennungId eq nennungId.toJavaUuid() }
|
.where { NennungsTransferTable.ursprungsNennungId eq nennungId.toJavaUuid() }
|
||||||
.map(::rowToTransfer)
|
.map(::rowToTransfer)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun save(transfer: NennungsTransfer): NennungsTransfer = tenantTransaction {
|
override suspend fun save(transfer: DomNennungsTransfer): DomNennungsTransfer = tenantTransaction {
|
||||||
val now = Clock.System.now()
|
val now = Clock.System.now()
|
||||||
NennungsTransferTable.insert { stmt ->
|
NennungsTransferTable.insert { stmt ->
|
||||||
stmt[id] = transfer.transferId.toJavaUuid()
|
stmt[id] = transfer.transferId.toJavaUuid()
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,8 @@ package at.mocode.entries.service.usecase
|
||||||
|
|
||||||
import at.mocode.core.domain.model.NennStatusE
|
import at.mocode.core.domain.model.NennStatusE
|
||||||
import at.mocode.entries.api.*
|
import at.mocode.entries.api.*
|
||||||
import at.mocode.entries.domain.model.Nennung
|
import at.mocode.entries.domain.model.DomNennung
|
||||||
import at.mocode.entries.domain.model.NennungsTransfer
|
import at.mocode.entries.domain.model.DomNennungsTransfer
|
||||||
import at.mocode.entries.domain.repository.NennungRepository
|
import at.mocode.entries.domain.repository.NennungRepository
|
||||||
import at.mocode.entries.domain.repository.NennungsTransferRepository
|
import at.mocode.entries.domain.repository.NennungsTransferRepository
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
|
|
@ -61,7 +61,7 @@ class NennungUseCases(
|
||||||
request.turnierId, request.bewerbId, request.reiterId
|
request.turnierId, request.bewerbId, request.reiterId
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val nennung = Nennung(
|
val nennung = DomNennung(
|
||||||
abteilungId = request.abteilungId,
|
abteilungId = request.abteilungId,
|
||||||
bewerbId = request.bewerbId,
|
bewerbId = request.bewerbId,
|
||||||
turnierId = request.turnierId,
|
turnierId = request.turnierId,
|
||||||
|
|
@ -135,7 +135,7 @@ class NennungUseCases(
|
||||||
nennungRepository.save(geschlosseneNennung)
|
nennungRepository.save(geschlosseneNennung)
|
||||||
|
|
||||||
// 2. Neue Nennung anlegen
|
// 2. Neue Nennung anlegen
|
||||||
val neueNennung = Nennung(
|
val neueNennung = DomNennung(
|
||||||
abteilungId = ursprung.abteilungId,
|
abteilungId = ursprung.abteilungId,
|
||||||
bewerbId = ursprung.bewerbId,
|
bewerbId = ursprung.bewerbId,
|
||||||
turnierId = ursprung.turnierId,
|
turnierId = ursprung.turnierId,
|
||||||
|
|
@ -150,7 +150,7 @@ class NennungUseCases(
|
||||||
val gespeicherteNeueNennung = nennungRepository.save(neueNennung)
|
val gespeicherteNeueNennung = nennungRepository.save(neueNennung)
|
||||||
|
|
||||||
// 3. Transfer-Record speichern
|
// 3. Transfer-Record speichern
|
||||||
val transfer = NennungsTransfer(
|
val transfer = DomNennungsTransfer(
|
||||||
ursprungsNennungId = ursprung.nennungId,
|
ursprungsNennungId = ursprung.nennungId,
|
||||||
neueNennungId = gespeicherteNeueNennung.nennungId,
|
neueNennungId = gespeicherteNeueNennung.nennungId,
|
||||||
alterReiterId = if (request.neuerReiterId != null) ursprung.reiterId else null,
|
alterReiterId = if (request.neuerReiterId != null) ursprung.reiterId else null,
|
||||||
|
|
@ -175,7 +175,7 @@ class NennungUseCases(
|
||||||
// Mapping Helpers
|
// Mapping Helpers
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
private fun Nennung.toDetailDto() = NennungDetailDto(
|
private fun DomNennung.toDetailDto() = NennungDetailDto(
|
||||||
nennungId = nennungId,
|
nennungId = nennungId,
|
||||||
abteilungId = abteilungId,
|
abteilungId = abteilungId,
|
||||||
bewerbId = bewerbId,
|
bewerbId = bewerbId,
|
||||||
|
|
@ -193,7 +193,7 @@ class NennungUseCases(
|
||||||
updatedAt = updatedAt.toString()
|
updatedAt = updatedAt.toString()
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun Nennung.toSummaryDto() = NennungSummaryDto(
|
private fun DomNennung.toSummaryDto() = NennungSummaryDto(
|
||||||
nennungId = nennungId,
|
nennungId = nennungId,
|
||||||
turnierId = turnierId,
|
turnierId = turnierId,
|
||||||
bewerbId = bewerbId,
|
bewerbId = bewerbId,
|
||||||
|
|
@ -205,7 +205,7 @@ class NennungUseCases(
|
||||||
createdAt = createdAt.toString()
|
createdAt = createdAt.toString()
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun NennungsTransfer.toDto() = NennungsTransferDto(
|
private fun DomNennungsTransfer.toDto() = NennungsTransferDto(
|
||||||
transferId = transferId,
|
transferId = transferId,
|
||||||
ursprungsNennungId = ursprungsNennungId,
|
ursprungsNennungId = ursprungsNennungId,
|
||||||
neueNennungId = neueNennungId,
|
neueNennungId = neueNennungId,
|
||||||
|
|
|
||||||
|
|
@ -1,105 +0,0 @@
|
||||||
-- V5: Bewerb-Details, Zeitplan, Finanzen, Richter-Einsätze, Austragungsplätze, Turnier-Artikel
|
|
||||||
-- Context: Phase 3 – Persistenz für die in Phase 2 erweiterten Domain-Modelle
|
|
||||||
|
|
||||||
-- ─────────────────────────────────────────────────────────────────────────────
|
|
||||||
-- 1. bewerbe: neue Spalten (Text & Details, Ort, Zeitplan, Finanzen)
|
|
||||||
-- ─────────────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
ALTER TABLE bewerbe
|
|
||||||
ADD COLUMN IF NOT EXISTS beschreibung TEXT NULL,
|
|
||||||
ADD COLUMN IF NOT EXISTS aufgabe VARCHAR(100) NULL,
|
|
||||||
ADD COLUMN IF NOT EXISTS aufgaben_nummer VARCHAR(50) NULL,
|
|
||||||
ADD COLUMN IF NOT EXISTS para_grade VARCHAR(50) NULL,
|
|
||||||
ADD COLUMN IF NOT EXISTS austragungsplatz_id UUID NULL,
|
|
||||||
ADD COLUMN IF NOT EXISTS geplantes_datum DATE NULL,
|
|
||||||
ADD COLUMN IF NOT EXISTS beginn_zeit_typ VARCHAR(20) NULL,
|
|
||||||
ADD COLUMN IF NOT EXISTS beginn_zeit TIME NULL,
|
|
||||||
ADD COLUMN IF NOT EXISTS reitdauer_minuten INTEGER NULL,
|
|
||||||
ADD COLUMN IF NOT EXISTS umbau_minuten INTEGER NULL,
|
|
||||||
ADD COLUMN IF NOT EXISTS besichtigung_minuten INTEGER NULL,
|
|
||||||
ADD COLUMN IF NOT EXISTS stechen_geplant BOOLEAN NOT NULL DEFAULT FALSE,
|
|
||||||
ADD COLUMN IF NOT EXISTS startgeld_cent BIGINT NULL,
|
|
||||||
ADD COLUMN IF NOT EXISTS geldpreis_ausbezahlt BOOLEAN NOT NULL DEFAULT FALSE;
|
|
||||||
|
|
||||||
-- Check-Constraint für beginn_zeit_typ (PostgreSQL unterstützt kein IF NOT EXISTS für Constraints)
|
|
||||||
DO $$
|
|
||||||
BEGIN
|
|
||||||
IF NOT EXISTS (
|
|
||||||
SELECT 1 FROM pg_constraint WHERE conname = 'chk_bewerbe_beginn_zeit_typ'
|
|
||||||
) THEN
|
|
||||||
ALTER TABLE bewerbe
|
|
||||||
ADD CONSTRAINT chk_bewerbe_beginn_zeit_typ
|
|
||||||
CHECK (beginn_zeit_typ IN ('FIX', 'ANSCHLIESSEND'));
|
|
||||||
END IF;
|
|
||||||
END $$;
|
|
||||||
|
|
||||||
-- ─────────────────────────────────────────────────────────────────────────────
|
|
||||||
-- 2. abteilungen: CHECK-Constraint um MANUELL erweitern
|
|
||||||
-- ─────────────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
ALTER TABLE abteilungen DROP CONSTRAINT IF EXISTS chk_abteilungen_typ;
|
|
||||||
ALTER TABLE abteilungen
|
|
||||||
ADD CONSTRAINT chk_abteilungen_typ
|
|
||||||
CHECK (typ IN ('SEPARATE_SIEGEREHRUNG', 'ORGANISATORISCH', 'MANUELL'));
|
|
||||||
|
|
||||||
-- ─────────────────────────────────────────────────────────────────────────────
|
|
||||||
-- 3. austragungsplaetze – physische Orte innerhalb einer Veranstaltung
|
|
||||||
-- ─────────────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS austragungsplaetze (
|
|
||||||
id UUID PRIMARY KEY,
|
|
||||||
name VARCHAR(200) NOT NULL,
|
|
||||||
typ VARCHAR(50) NOT NULL,
|
|
||||||
sparte VARCHAR(50) NULL,
|
|
||||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
|
||||||
updated_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
|
||||||
CONSTRAINT chk_austragungsplaetze_typ
|
|
||||||
CHECK (typ IN ('Austragungsplatz', 'Vorbereitungsplatz'))
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_austragungsplaetze_typ ON austragungsplaetze(typ);
|
|
||||||
|
|
||||||
-- Foreign Key von bewerbe → austragungsplaetze (nullable)
|
|
||||||
DO $$
|
|
||||||
BEGIN
|
|
||||||
IF NOT EXISTS (
|
|
||||||
SELECT 1 FROM pg_constraint WHERE conname = 'fk_bewerbe_austragungsplatz'
|
|
||||||
) THEN
|
|
||||||
ALTER TABLE bewerbe
|
|
||||||
ADD CONSTRAINT fk_bewerbe_austragungsplatz
|
|
||||||
FOREIGN KEY (austragungsplatz_id) REFERENCES austragungsplaetze(id) ON DELETE SET NULL;
|
|
||||||
END IF;
|
|
||||||
END $$;
|
|
||||||
|
|
||||||
-- ─────────────────────────────────────────────────────────────────────────────
|
|
||||||
-- 4. bewerb_richter_einsaetze – 1:N Richter-Einsätze pro Bewerb
|
|
||||||
-- ─────────────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS bewerb_richter_einsaetze (
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
bewerb_id UUID NOT NULL REFERENCES bewerbe(id) ON DELETE CASCADE,
|
|
||||||
funktionaer_id UUID NOT NULL,
|
|
||||||
position VARCHAR(100) NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_richter_einsaetze_bewerb_id ON bewerb_richter_einsaetze(bewerb_id);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_richter_einsaetze_funktionaer ON bewerb_richter_einsaetze(funktionaer_id);
|
|
||||||
|
|
||||||
-- ─────────────────────────────────────────────────────────────────────────────
|
|
||||||
-- 5. turnier_artikel – abrechenbare Leistungen/Produkte (Billing Context)
|
|
||||||
-- ─────────────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS turnier_artikel (
|
|
||||||
id UUID PRIMARY KEY,
|
|
||||||
turnier_id UUID NOT NULL REFERENCES turniere(id) ON DELETE CASCADE,
|
|
||||||
name VARCHAR(200) NOT NULL,
|
|
||||||
preis_cent BIGINT NOT NULL DEFAULT 0,
|
|
||||||
typ VARCHAR(20) NOT NULL,
|
|
||||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
|
||||||
updated_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
|
||||||
CONSTRAINT chk_turnier_artikel_typ
|
|
||||||
CHECK (typ IN ('AUTOMATISCH', 'MANUELL'))
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_turnier_artikel_turnier_id ON turnier_artikel(turnier_id);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_turnier_artikel_typ ON turnier_artikel(typ);
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
-- V6: Bewerb – Teilungs-Typ (AbteilungsTeilungsTypE)
|
|
||||||
|
|
||||||
-- Neue optionale Spalte in bewerbe für die Abteilungs-Teilungsregel
|
|
||||||
ALTER TABLE bewerbe
|
|
||||||
ADD COLUMN IF NOT EXISTS teilungs_typ VARCHAR(50) NULL;
|
|
||||||
|
|
||||||
-- Optional: Index für Filter/Reporting
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_bewerbe_teilungstyp ON bewerbe(teilungs_typ);
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
package at.mocode.entries.service.tenant
|
package at.mocode.entries.service.tenant
|
||||||
|
|
||||||
import at.mocode.entries.domain.model.Nennung
|
import at.mocode.entries.domain.model.DomNennung
|
||||||
import at.mocode.entries.domain.repository.NennungRepository
|
import at.mocode.entries.domain.repository.NennungRepository
|
||||||
import at.mocode.entries.service.persistence.NennungTable
|
import at.mocode.entries.service.persistence.NennungTable
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
|
|
@ -120,7 +120,7 @@ class EntriesIsolationIntegrationTest @Autowired constructor(
|
||||||
// Schreibe eine Nennung in Tenant A
|
// Schreibe eine Nennung in Tenant A
|
||||||
TenantContextHolder.set(Tenant(eventId = "event_a", schemaName = "event_a"))
|
TenantContextHolder.set(Tenant(eventId = "event_a", schemaName = "event_a"))
|
||||||
try {
|
try {
|
||||||
val nennungA = Nennung.random(now)
|
val nennungA = DomNennung.random(now)
|
||||||
val loadedA = runBlocking {
|
val loadedA = runBlocking {
|
||||||
nennungRepository.save(nennungA)
|
nennungRepository.save(nennungA)
|
||||||
nennungRepository.findById(nennungA.nennungId)
|
nennungRepository.findById(nennungA.nennungId)
|
||||||
|
|
@ -142,8 +142,8 @@ class EntriesIsolationIntegrationTest @Autowired constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Kleine Test-Helfer ---
|
// --- Kleine Test-Helfer ---
|
||||||
private fun Nennung.Companion.random(now: kotlin.time.Instant): Nennung {
|
private fun DomNennung.Companion.random(now: kotlin.time.Instant): DomNennung {
|
||||||
return Nennung(
|
return DomNennung(
|
||||||
nennungId = Uuid.random(),
|
nennungId = Uuid.random(),
|
||||||
abteilungId = Uuid.random(),
|
abteilungId = Uuid.random(),
|
||||||
bewerbId = Uuid.random(),
|
bewerbId = Uuid.random(),
|
||||||
|
|
|
||||||
|
|
@ -1,32 +1,39 @@
|
||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.kotlinJvm)
|
alias(libs.plugins.kotlin.jvm)
|
||||||
alias(libs.plugins.kotlinSpring)
|
alias(libs.plugins.kotlin.spring)
|
||||||
alias(libs.plugins.kotlinSerialization)
|
alias(libs.plugins.kotlin.serialization)
|
||||||
alias(libs.plugins.ktor)
|
alias(libs.plugins.ktor)
|
||||||
application
|
application
|
||||||
alias(libs.plugins.spring.dependencyManagement)
|
// KORREKTUR 1: Dieses Plugin hinzufügen, um die Spring-BOM zu aktivieren.
|
||||||
|
alias(libs.plugins.spring.dependencyManagement)
|
||||||
}
|
}
|
||||||
|
|
||||||
application {
|
application {
|
||||||
mainClass.set("at.mocode.events.api.ApplicationKt")
|
mainClass.set("at.mocode.events.api.ApplicationKt")
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
api(platform(libs.spring.boot.dependencies))
|
// KORREKTUR 2: Die Spring-Boot-BOM hier explizit als Plattform deklarieren.
|
||||||
implementation(projects.platform.platformDependencies)
|
api(platform(libs.spring.boot.dependencies))
|
||||||
implementation(projects.backend.services.events.eventsDomain)
|
// Bestehende Abhängigkeiten
|
||||||
implementation(projects.core.coreDomain)
|
implementation(projects.platform.platformDependencies)
|
||||||
implementation(projects.core.coreUtils)
|
implementation(projects.events.eventsDomain)
|
||||||
implementation(libs.spring.web)
|
implementation(projects.events.eventsApplication)
|
||||||
implementation(libs.springdoc.openapi.starter.common)
|
implementation(projects.core.coreDomain)
|
||||||
implementation(libs.ktor.server.core)
|
implementation(projects.core.coreUtils)
|
||||||
implementation(libs.ktor.server.netty)
|
// Spring dependencies (jetzt mit korrekter Version aus der BOM)
|
||||||
implementation(libs.ktor.server.contentNegotiation)
|
implementation(libs.spring.web)
|
||||||
implementation(libs.ktor.server.serialization.kotlinx.json)
|
implementation(libs.springdoc.openapi.starter.common)
|
||||||
implementation(libs.ktor.server.statusPages)
|
// Ktor Server
|
||||||
implementation(libs.ktor.server.auth)
|
implementation(libs.ktor.server.core)
|
||||||
implementation(libs.ktor.server.authJwt)
|
implementation(libs.ktor.server.netty)
|
||||||
|
implementation(libs.ktor.server.contentNegotiation)
|
||||||
|
implementation(libs.ktor.server.serialization.kotlinx.json)
|
||||||
|
implementation(libs.ktor.server.statusPages)
|
||||||
|
implementation(libs.ktor.server.auth)
|
||||||
|
implementation(libs.ktor.server.authJwt)
|
||||||
|
|
||||||
testImplementation(projects.platform.platformTesting)
|
testImplementation(projects.platform.platformTesting)
|
||||||
testImplementation(libs.ktor.server.testHost)
|
// Ktor 3.x Test-Host statt veraltetes tests-Artefakt
|
||||||
|
testImplementation(libs.ktor.server.testHost)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,15 @@
|
||||||
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
||||||
package at.mocode.events.api.rest
|
package at.mocode.events.api.rest
|
||||||
|
|
||||||
import at.mocode.events.domain.model.Veranstaltung
|
|
||||||
import at.mocode.core.domain.model.ApiResponse
|
import at.mocode.core.domain.model.ApiResponse
|
||||||
import at.mocode.core.domain.model.SparteE
|
import at.mocode.core.domain.model.SparteE
|
||||||
|
import at.mocode.events.application.usecase.CreateVeranstaltungUseCase
|
||||||
|
import at.mocode.events.application.usecase.DeleteVeranstaltungUseCase
|
||||||
|
import at.mocode.events.application.usecase.GetVeranstaltungUseCase
|
||||||
|
import at.mocode.events.application.usecase.UpdateVeranstaltungUseCase
|
||||||
import at.mocode.events.domain.repository.VeranstaltungRepository
|
import at.mocode.events.domain.repository.VeranstaltungRepository
|
||||||
import at.mocode.core.domain.serialization.UuidSerializer
|
import at.mocode.core.domain.serialization.UuidSerializer
|
||||||
|
import at.mocode.core.utils.validation.ApiValidationUtils
|
||||||
import kotlin.uuid.Uuid
|
import kotlin.uuid.Uuid
|
||||||
import io.ktor.http.*
|
import io.ktor.http.*
|
||||||
import io.ktor.server.request.*
|
import io.ktor.server.request.*
|
||||||
|
|
@ -24,6 +28,11 @@ class VeranstaltungController(
|
||||||
private val veranstaltungRepository: VeranstaltungRepository
|
private val veranstaltungRepository: VeranstaltungRepository
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
private val createVeranstaltungUseCase = CreateVeranstaltungUseCase(veranstaltungRepository)
|
||||||
|
private val getVeranstaltungUseCase = GetVeranstaltungUseCase(veranstaltungRepository)
|
||||||
|
private val updateVeranstaltungUseCase = UpdateVeranstaltungUseCase(veranstaltungRepository)
|
||||||
|
private val deleteVeranstaltungUseCase = DeleteVeranstaltungUseCase(veranstaltungRepository)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configures the event-related routes.
|
* Configures the event-related routes.
|
||||||
*/
|
*/
|
||||||
|
|
@ -33,17 +42,31 @@ class VeranstaltungController(
|
||||||
// GET /api/events - Get all events with optional filtering
|
// GET /api/events - Get all events with optional filtering
|
||||||
get {
|
get {
|
||||||
try {
|
try {
|
||||||
|
// Validate query parameters
|
||||||
|
val validationErrors = ApiValidationUtils.validateQueryParameters(
|
||||||
|
limit = call.request.queryParameters["limit"],
|
||||||
|
offset = call.request.queryParameters["offset"],
|
||||||
|
startDate = call.request.queryParameters["startDate"],
|
||||||
|
endDate = call.request.queryParameters["endDate"],
|
||||||
|
search = call.request.queryParameters["search"]
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!ApiValidationUtils.isValid(validationErrors)) {
|
||||||
|
call.respond(
|
||||||
|
HttpStatusCode.BadRequest,
|
||||||
|
ApiResponse.error<Any>(ApiValidationUtils.createErrorMessage(validationErrors))
|
||||||
|
)
|
||||||
|
return@get
|
||||||
|
}
|
||||||
|
|
||||||
val activeOnly = call.request.queryParameters["activeOnly"]?.toBoolean() ?: true
|
val activeOnly = call.request.queryParameters["activeOnly"]?.toBoolean() ?: true
|
||||||
val limit = call.request.queryParameters["limit"]?.toInt() ?: 100
|
val limit = call.request.queryParameters["limit"]?.toInt() ?: 100
|
||||||
val offset = call.request.queryParameters["offset"]?.toInt() ?: 0
|
val offset = call.request.queryParameters["offset"]?.toInt() ?: 0
|
||||||
|
|
||||||
val organizerId = call.request.queryParameters["organizerId"]?.let {
|
val organizerId = call.request.queryParameters["organizerId"]?.let {
|
||||||
try { Uuid.parse(it) } catch (_: Exception) {
|
ApiValidationUtils.validateUuidString(it) ?: return@get call.respond(
|
||||||
return@get call.respond(
|
HttpStatusCode.BadRequest,
|
||||||
HttpStatusCode.BadRequest,
|
ApiResponse.error<Any>("Invalid organizerId format")
|
||||||
ApiResponse.error<Any>("INVALID_ORGANIZER_ID", "Invalid organizerId format")
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
val searchTerm = call.request.queryParameters["search"]
|
val searchTerm = call.request.queryParameters["search"]
|
||||||
val publicOnly = call.request.queryParameters["publicOnly"]?.toBoolean() ?: false
|
val publicOnly = call.request.queryParameters["publicOnly"]?.toBoolean() ?: false
|
||||||
|
|
@ -61,7 +84,7 @@ class VeranstaltungController(
|
||||||
|
|
||||||
call.respond(HttpStatusCode.OK, ApiResponse.success(events))
|
call.respond(HttpStatusCode.OK, ApiResponse.success(events))
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<Any>("INTERNAL_ERROR", "Failed to retrieve events: ${e.message}"))
|
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<Any>("Failed to retrieve events: ${e.message}"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -69,17 +92,18 @@ class VeranstaltungController(
|
||||||
get("/{id}") {
|
get("/{id}") {
|
||||||
try {
|
try {
|
||||||
val eventId = Uuid.parse(call.parameters["id"]!!)
|
val eventId = Uuid.parse(call.parameters["id"]!!)
|
||||||
val event = veranstaltungRepository.findById(eventId)
|
val request = GetVeranstaltungUseCase.GetVeranstaltungRequest(eventId)
|
||||||
|
val response = getVeranstaltungUseCase.execute(request)
|
||||||
|
|
||||||
if (event != null) {
|
if (response.success && response.data != null) {
|
||||||
call.respond(HttpStatusCode.OK, ApiResponse.success(event))
|
call.respond(HttpStatusCode.OK, ApiResponse.success((response.data as GetVeranstaltungUseCase.GetVeranstaltungResponse).veranstaltung))
|
||||||
} else {
|
} else {
|
||||||
call.respond(HttpStatusCode.NotFound, ApiResponse.error<Any>("NOT_FOUND", "Event not found"))
|
call.respond(HttpStatusCode.NotFound, ApiResponse.error<Any>("Event not found"))
|
||||||
}
|
}
|
||||||
} catch (_: IllegalArgumentException) {
|
} catch (_: IllegalArgumentException) {
|
||||||
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Any>("INVALID_ID", "Invalid event ID format"))
|
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Any>("Invalid event ID format"))
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<Any>("INTERNAL_ERROR", "Failed to retrieve event: ${e.message}"))
|
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<Any>("Failed to retrieve event: ${e.message}"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -96,7 +120,7 @@ class VeranstaltungController(
|
||||||
|
|
||||||
call.respond(HttpStatusCode.OK, ApiResponse.success(stats))
|
call.respond(HttpStatusCode.OK, ApiResponse.success(stats))
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<Any>("INTERNAL_ERROR", "Failed to retrieve event statistics: ${e.message}"))
|
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<Any>("Failed to retrieve event statistics: ${e.message}"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -105,12 +129,26 @@ class VeranstaltungController(
|
||||||
try {
|
try {
|
||||||
val createRequest = call.receive<CreateEventRequest>()
|
val createRequest = call.receive<CreateEventRequest>()
|
||||||
|
|
||||||
val veranstaltung = Veranstaltung(
|
// Validate input using shared validation utilities
|
||||||
|
val validationErrors = ApiValidationUtils.validateEventRequest(
|
||||||
|
name = createRequest.name,
|
||||||
|
ort = createRequest.ort,
|
||||||
|
startDatum = createRequest.startDatum,
|
||||||
|
endDatum = createRequest.endDatum,
|
||||||
|
maxTeilnehmer = createRequest.maxTeilnehmer
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!ApiValidationUtils.isValid(validationErrors)) {
|
||||||
|
call.respond(
|
||||||
|
HttpStatusCode.BadRequest,
|
||||||
|
ApiResponse.error<Any>(ApiValidationUtils.createErrorMessage(validationErrors))
|
||||||
|
)
|
||||||
|
return@post
|
||||||
|
}
|
||||||
|
|
||||||
|
val useCaseRequest = CreateVeranstaltungUseCase.CreateVeranstaltungRequest(
|
||||||
name = createRequest.name,
|
name = createRequest.name,
|
||||||
untertitel = createRequest.untertitel,
|
|
||||||
beschreibung = createRequest.beschreibung,
|
beschreibung = createRequest.beschreibung,
|
||||||
logoUrl = createRequest.logoUrl,
|
|
||||||
sponsoren = createRequest.sponsoren,
|
|
||||||
startDatum = createRequest.startDatum,
|
startDatum = createRequest.startDatum,
|
||||||
endDatum = createRequest.endDatum,
|
endDatum = createRequest.endDatum,
|
||||||
ort = createRequest.ort,
|
ort = createRequest.ort,
|
||||||
|
|
@ -122,16 +160,20 @@ class VeranstaltungController(
|
||||||
anmeldeschluss = createRequest.anmeldeschluss
|
anmeldeschluss = createRequest.anmeldeschluss
|
||||||
)
|
)
|
||||||
|
|
||||||
val errors = veranstaltung.validate()
|
val response = createVeranstaltungUseCase.execute(useCaseRequest)
|
||||||
if (errors.isNotEmpty()) {
|
|
||||||
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Any>("VALIDATION_ERROR", "Validation failed: ${errors.joinToString(", ")}"))
|
|
||||||
return@post
|
|
||||||
}
|
|
||||||
|
|
||||||
val savedEvent = veranstaltungRepository.save(veranstaltung)
|
if (response.success && response.data != null) {
|
||||||
call.respond(HttpStatusCode.Created, ApiResponse.success(savedEvent))
|
call.respond(HttpStatusCode.Created, ApiResponse.success((response.data as CreateVeranstaltungUseCase.CreateVeranstaltungResponse).veranstaltung))
|
||||||
|
} else {
|
||||||
|
val statusCode = when (response.error?.code) {
|
||||||
|
"VALIDATION_ERROR" -> HttpStatusCode.BadRequest
|
||||||
|
"DOMAIN_VALIDATION_ERROR" -> HttpStatusCode.BadRequest
|
||||||
|
else -> HttpStatusCode.InternalServerError
|
||||||
|
}
|
||||||
|
call.respond(statusCode, ApiResponse.error<Any>(response.error?.message ?: "Failed to create event"))
|
||||||
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Any>("INVALID_REQUEST", "Invalid request data: ${e.message}"))
|
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Any>("Invalid request data: ${e.message}"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -141,18 +183,27 @@ class VeranstaltungController(
|
||||||
val eventId = Uuid.parse(call.parameters["id"]!!)
|
val eventId = Uuid.parse(call.parameters["id"]!!)
|
||||||
val updateRequest = call.receive<UpdateEventRequest>()
|
val updateRequest = call.receive<UpdateEventRequest>()
|
||||||
|
|
||||||
val existingEvent = veranstaltungRepository.findById(eventId)
|
// Validate input using shared validation utilities
|
||||||
if (existingEvent == null) {
|
val validationErrors = ApiValidationUtils.validateEventRequest(
|
||||||
call.respond(HttpStatusCode.NotFound, ApiResponse.error<Any>("NOT_FOUND", "Event not found"))
|
name = updateRequest.name,
|
||||||
|
ort = updateRequest.ort,
|
||||||
|
startDatum = updateRequest.startDatum,
|
||||||
|
endDatum = updateRequest.endDatum,
|
||||||
|
maxTeilnehmer = updateRequest.maxTeilnehmer
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!ApiValidationUtils.isValid(validationErrors)) {
|
||||||
|
call.respond(
|
||||||
|
HttpStatusCode.BadRequest,
|
||||||
|
ApiResponse.error<Any>(ApiValidationUtils.createErrorMessage(validationErrors))
|
||||||
|
)
|
||||||
return@put
|
return@put
|
||||||
}
|
}
|
||||||
|
|
||||||
val updatedVeranstaltung = existingEvent.copy(
|
val useCaseRequest = UpdateVeranstaltungUseCase.UpdateVeranstaltungRequest(
|
||||||
|
veranstaltungId = eventId,
|
||||||
name = updateRequest.name,
|
name = updateRequest.name,
|
||||||
untertitel = updateRequest.untertitel,
|
|
||||||
beschreibung = updateRequest.beschreibung,
|
beschreibung = updateRequest.beschreibung,
|
||||||
logoUrl = updateRequest.logoUrl,
|
|
||||||
sponsoren = updateRequest.sponsoren,
|
|
||||||
startDatum = updateRequest.startDatum,
|
startDatum = updateRequest.startDatum,
|
||||||
endDatum = updateRequest.endDatum,
|
endDatum = updateRequest.endDatum,
|
||||||
ort = updateRequest.ort,
|
ort = updateRequest.ort,
|
||||||
|
|
@ -162,46 +213,72 @@ class VeranstaltungController(
|
||||||
istOeffentlich = updateRequest.istOeffentlich,
|
istOeffentlich = updateRequest.istOeffentlich,
|
||||||
maxTeilnehmer = updateRequest.maxTeilnehmer,
|
maxTeilnehmer = updateRequest.maxTeilnehmer,
|
||||||
anmeldeschluss = updateRequest.anmeldeschluss
|
anmeldeschluss = updateRequest.anmeldeschluss
|
||||||
).withUpdatedTimestamp()
|
)
|
||||||
|
|
||||||
val errors = updatedVeranstaltung.validate()
|
val response = updateVeranstaltungUseCase.execute(useCaseRequest)
|
||||||
if (errors.isNotEmpty()) {
|
|
||||||
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Any>("VALIDATION_ERROR", "Validation failed: ${errors.joinToString(", ")}"))
|
if (response.success && response.data != null) {
|
||||||
return@put
|
call.respond(HttpStatusCode.OK, ApiResponse.success((response.data as UpdateVeranstaltungUseCase.UpdateVeranstaltungResponse).veranstaltung))
|
||||||
|
} else {
|
||||||
|
val statusCode = when (response.error?.code) {
|
||||||
|
"NOT_FOUND" -> HttpStatusCode.NotFound
|
||||||
|
"VALIDATION_ERROR" -> HttpStatusCode.BadRequest
|
||||||
|
"DOMAIN_VALIDATION_ERROR" -> HttpStatusCode.BadRequest
|
||||||
|
else -> HttpStatusCode.InternalServerError
|
||||||
|
}
|
||||||
|
call.respond(statusCode, ApiResponse.error<Any>(response.error?.message ?: "Failed to update event"))
|
||||||
}
|
}
|
||||||
|
|
||||||
val savedEvent = veranstaltungRepository.save(updatedVeranstaltung)
|
|
||||||
call.respond(HttpStatusCode.OK, ApiResponse.success(savedEvent))
|
|
||||||
} catch (_: IllegalArgumentException) {
|
} catch (_: IllegalArgumentException) {
|
||||||
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Any>("INVALID_ID", "Invalid event ID format"))
|
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Any>("Invalid event ID format"))
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Any>("INVALID_REQUEST", "Invalid request data: ${e.message}"))
|
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Any>("Invalid request data: ${e.message}"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// DELETE /api/events/{id} - Delete event
|
// DELETE /api/events/{id} - Delete event
|
||||||
delete("/{id}") {
|
delete("/{id}") {
|
||||||
try {
|
try {
|
||||||
val eventIdString = call.parameters["id"] ?: return@delete call.respond(
|
val eventId = ApiValidationUtils.validateUuidString(call.parameters["id"])
|
||||||
HttpStatusCode.BadRequest,
|
?: return@delete call.respond(
|
||||||
ApiResponse.error<Any>("MISSING_ID", "Event ID is required")
|
HttpStatusCode.BadRequest,
|
||||||
|
ApiResponse.error<Any>("Invalid event ID format")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Validate force parameter if provided
|
||||||
|
val forceParam = call.request.queryParameters["force"]
|
||||||
|
val forceDelete = if (forceParam != null) {
|
||||||
|
try {
|
||||||
|
forceParam.toBoolean()
|
||||||
|
} catch (_: Exception) {
|
||||||
|
return@delete call.respond(
|
||||||
|
HttpStatusCode.BadRequest,
|
||||||
|
ApiResponse.error<Any>("Invalid force parameter. Must be true or false")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
val useCaseRequest = DeleteVeranstaltungUseCase.DeleteVeranstaltungRequest(
|
||||||
|
veranstaltungId = eventId,
|
||||||
|
forceDelete = forceDelete
|
||||||
)
|
)
|
||||||
|
|
||||||
val eventId = try { Uuid.parse(eventIdString) } catch (_: Exception) {
|
val response = deleteVeranstaltungUseCase.execute(useCaseRequest)
|
||||||
return@delete call.respond(
|
|
||||||
HttpStatusCode.BadRequest,
|
|
||||||
ApiResponse.error<Any>("INVALID_ID", "Invalid event ID format")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val success = veranstaltungRepository.delete(eventId)
|
if (response.success) {
|
||||||
if (success) {
|
call.respond(HttpStatusCode.OK, ApiResponse.success(response.data))
|
||||||
call.respond(HttpStatusCode.OK, ApiResponse.success("Event deleted successfully"))
|
|
||||||
} else {
|
} else {
|
||||||
call.respond(HttpStatusCode.NotFound, ApiResponse.error<Any>("NOT_FOUND", "Event not found"))
|
val statusCode = when (response.error?.code) {
|
||||||
|
"NOT_FOUND" -> HttpStatusCode.NotFound
|
||||||
|
"CANNOT_DELETE_ACTIVE_EVENT" -> HttpStatusCode.Conflict
|
||||||
|
else -> HttpStatusCode.InternalServerError
|
||||||
|
}
|
||||||
|
call.respond(statusCode, ApiResponse.error<Any>(response.error?.message ?: "Failed to delete event"))
|
||||||
}
|
}
|
||||||
|
} catch (_: IllegalArgumentException) {
|
||||||
|
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Any>("Invalid event ID format"))
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<Any>("INTERNAL_ERROR", "Failed to delete event: ${e.message}"))
|
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<Any>("Failed to delete event: ${e.message}"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -213,10 +290,7 @@ class VeranstaltungController(
|
||||||
@Serializable
|
@Serializable
|
||||||
data class CreateEventRequest(
|
data class CreateEventRequest(
|
||||||
val name: String,
|
val name: String,
|
||||||
val untertitel: String? = null,
|
|
||||||
val beschreibung: String? = null,
|
val beschreibung: String? = null,
|
||||||
val logoUrl: String? = null,
|
|
||||||
val sponsoren: String? = null,
|
|
||||||
val startDatum: LocalDate,
|
val startDatum: LocalDate,
|
||||||
val endDatum: LocalDate,
|
val endDatum: LocalDate,
|
||||||
val ort: String,
|
val ort: String,
|
||||||
|
|
@ -235,10 +309,7 @@ class VeranstaltungController(
|
||||||
@Serializable
|
@Serializable
|
||||||
data class UpdateEventRequest(
|
data class UpdateEventRequest(
|
||||||
val name: String,
|
val name: String,
|
||||||
val untertitel: String? = null,
|
|
||||||
val beschreibung: String? = null,
|
val beschreibung: String? = null,
|
||||||
val logoUrl: String? = null,
|
|
||||||
val sponsoren: String? = null,
|
|
||||||
val startDatum: LocalDate,
|
val startDatum: LocalDate,
|
||||||
val endDatum: LocalDate,
|
val endDatum: LocalDate,
|
||||||
val ort: String,
|
val ort: String,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.kotlinMultiplatform)
|
// KORREKTUR: Von 'kotlin("jvm")' zu Multiplattform wechseln.
|
||||||
alias(libs.plugins.kotlinSerialization)
|
alias(libs.plugins.kotlin.multiplatform)
|
||||||
|
alias(libs.plugins.kotlin.serialization)
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
|
|
@ -24,11 +25,6 @@ kotlin {
|
||||||
val commonTest by getting {
|
val commonTest by getting {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(kotlin("test"))
|
implementation(kotlin("test"))
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val jvmTest by getting {
|
|
||||||
dependencies {
|
|
||||||
implementation(projects.platform.platformTesting)
|
implementation(projects.platform.platformTesting)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,9 @@
|
||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.kotlinJvm)
|
kotlin("jvm")
|
||||||
alias(libs.plugins.kotlinSerialization)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(projects.core.coreDomain)
|
implementation(projects.core.coreDomain)
|
||||||
implementation(projects.core.coreUtils)
|
implementation(projects.core.coreUtils)
|
||||||
implementation(libs.kotlinx.datetime)
|
testImplementation(projects.platform.platformTesting)
|
||||||
testImplementation(projects.platform.platformTesting)
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
|
||||||
|
|
||||||
package at.mocode.events.domain.model
|
|
||||||
|
|
||||||
import at.mocode.core.domain.model.SparteE
|
|
||||||
import at.mocode.core.domain.serialization.UuidSerializer
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import kotlin.uuid.Uuid
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Domain-Modell für einen Austragungsplatz innerhalb einer Veranstaltung.
|
|
||||||
*
|
|
||||||
* Ein Austragungsplatz ist ein physischer Ort (Platz, Arena, Halle), auf dem Bewerbe
|
|
||||||
* stattfinden. Er kann als Austragungsplatz (Wettkampf) oder Vorbereitungsplatz
|
|
||||||
* (Einreiten) klassifiziert sein.
|
|
||||||
*
|
|
||||||
* @property id Eindeutige interne ID (UUID).
|
|
||||||
* @property name Bezeichnung des Platzes (z.B. "Waldarena", "Vorbereitungsplatz Nord").
|
|
||||||
* @property typ Klassifikation: "Austragungsplatz" oder "Vorbereitungsplatz".
|
|
||||||
* @property sparte Optionale Sparten-Zuordnung (z.B. nur für Springen genutzt).
|
|
||||||
*/
|
|
||||||
@Serializable
|
|
||||||
data class Austragungsplatz(
|
|
||||||
@Serializable(with = UuidSerializer::class)
|
|
||||||
val id: Uuid = Uuid.random(),
|
|
||||||
val name: String,
|
|
||||||
val typ: String,
|
|
||||||
val sparte: SparteE? = null
|
|
||||||
)
|
|
||||||
|
|
@ -4,9 +4,8 @@ package at.mocode.events.domain.model
|
||||||
|
|
||||||
import at.mocode.core.domain.model.AusschreibungsStatusE
|
import at.mocode.core.domain.model.AusschreibungsStatusE
|
||||||
import at.mocode.core.domain.model.SparteE
|
import at.mocode.core.domain.model.SparteE
|
||||||
import at.mocode.core.domain.serialization.InstantSerializer
|
import at.mocode.core.domain.serialization.KotlinxInstantSerializer
|
||||||
import at.mocode.core.domain.serialization.UuidSerializer
|
import at.mocode.core.domain.serialization.UuidSerializer
|
||||||
|
|
||||||
import kotlinx.datetime.LocalDate
|
import kotlinx.datetime.LocalDate
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlin.time.Clock
|
import kotlin.time.Clock
|
||||||
|
|
@ -45,7 +44,7 @@ import kotlin.uuid.Uuid
|
||||||
* @property updatedAt Letzter Änderungszeitpunkt.
|
* @property updatedAt Letzter Änderungszeitpunkt.
|
||||||
*/
|
*/
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Ausschreibung(
|
data class DomAusschreibung(
|
||||||
@Serializable(with = UuidSerializer::class)
|
@Serializable(with = UuidSerializer::class)
|
||||||
val ausschreibungsId: Uuid = Uuid.random(),
|
val ausschreibungsId: Uuid = Uuid.random(),
|
||||||
|
|
||||||
|
|
@ -89,9 +88,9 @@ data class Ausschreibung(
|
||||||
var genehmigungsNummer: String? = null,
|
var genehmigungsNummer: String? = null,
|
||||||
|
|
||||||
// Audit
|
// Audit
|
||||||
@Serializable(with = InstantSerializer::class)
|
@Serializable(with = KotlinxInstantSerializer::class)
|
||||||
val createdAt: Instant = Clock.System.now(),
|
val createdAt: Instant = Clock.System.now(),
|
||||||
@Serializable(with = InstantSerializer::class)
|
@Serializable(with = KotlinxInstantSerializer::class)
|
||||||
var updatedAt: Instant = Clock.System.now()
|
var updatedAt: Instant = Clock.System.now()
|
||||||
) {
|
) {
|
||||||
/**
|
/**
|
||||||
|
|
@ -151,5 +150,5 @@ data class Ausschreibung(
|
||||||
/**
|
/**
|
||||||
* Erstellt eine Kopie mit aktualisiertem Zeitstempel.
|
* Erstellt eine Kopie mit aktualisiertem Zeitstempel.
|
||||||
*/
|
*/
|
||||||
fun withUpdatedTimestamp(): Ausschreibung = this.copy(updatedAt = Clock.System.now())
|
fun withUpdatedTimestamp(): DomAusschreibung = this.copy(updatedAt = Clock.System.now())
|
||||||
}
|
}
|
||||||
|
|
@ -8,7 +8,7 @@ import at.mocode.core.domain.model.TurnierkategorieE
|
||||||
import at.mocode.core.domain.model.TurnierStatusE
|
import at.mocode.core.domain.model.TurnierStatusE
|
||||||
import at.mocode.events.domain.validation.TurnierBewerbDescriptor
|
import at.mocode.events.domain.validation.TurnierBewerbDescriptor
|
||||||
import at.mocode.events.domain.validation.TurnierkategoriePolicy
|
import at.mocode.events.domain.validation.TurnierkategoriePolicy
|
||||||
import at.mocode.core.domain.serialization.InstantSerializer
|
import at.mocode.core.domain.serialization.KotlinxInstantSerializer
|
||||||
import at.mocode.core.domain.serialization.UuidSerializer
|
import at.mocode.core.domain.serialization.UuidSerializer
|
||||||
import kotlinx.datetime.LocalDate
|
import kotlinx.datetime.LocalDate
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
@ -39,7 +39,7 @@ import kotlin.uuid.Uuid
|
||||||
* @property updatedAt Letzter Änderungszeitpunkt.
|
* @property updatedAt Letzter Änderungszeitpunkt.
|
||||||
*/
|
*/
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Turnier(
|
data class DomTurnier(
|
||||||
@Serializable(with = UuidSerializer::class)
|
@Serializable(with = UuidSerializer::class)
|
||||||
val turnierId: Uuid = Uuid.random(),
|
val turnierId: Uuid = Uuid.random(),
|
||||||
|
|
||||||
|
|
@ -72,13 +72,13 @@ data class Turnier(
|
||||||
var bemerkungen: String? = null,
|
var bemerkungen: String? = null,
|
||||||
|
|
||||||
// Audit
|
// Audit
|
||||||
@Serializable(with = InstantSerializer::class)
|
@Serializable(with = KotlinxInstantSerializer::class)
|
||||||
val createdAt: Instant = Clock.System.now(),
|
val createdAt: Instant = Clock.System.now(),
|
||||||
@Serializable(with = InstantSerializer::class)
|
@Serializable(with = KotlinxInstantSerializer::class)
|
||||||
var updatedAt: Instant = Clock.System.now()
|
var updatedAt: Instant = Clock.System.now()
|
||||||
) {
|
) {
|
||||||
/**
|
/**
|
||||||
* Prüft, ob das Turnier Pflicht-Funktionäre zugewiesen hat.
|
* Prüft ob das Turnier Pflicht-Funktionäre zugewiesen hat.
|
||||||
* Gibt Warnungen zurück – keine harten Fehler (Warn-Logik statt Exception, ADR-0007).
|
* Gibt Warnungen zurück – keine harten Fehler (Warn-Logik statt Exception, ADR-0007).
|
||||||
*/
|
*/
|
||||||
fun validateFunktionaerBesetzung(): List<String> {
|
fun validateFunktionaerBesetzung(): List<String> {
|
||||||
|
|
@ -115,13 +115,13 @@ data class Turnier(
|
||||||
/**
|
/**
|
||||||
* Erstellt eine Kopie mit aktualisiertem Zeitstempel.
|
* Erstellt eine Kopie mit aktualisiertem Zeitstempel.
|
||||||
*/
|
*/
|
||||||
fun withUpdatedTimestamp(): Turnier = this.copy(updatedAt = Clock.System.now())
|
fun withUpdatedTimestamp(): DomTurnier = this.copy(updatedAt = Clock.System.now())
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validiert die im Turnier geplanten Bewerbe gegen die Limits der Turnierkategorie.
|
* Validiert die im Turnier geplanten Bewerbe gegen die Limits der Turnierkategorie.
|
||||||
*
|
*
|
||||||
* Hinweis: Die konkreten Regeln stammen aus der ÖTO-Spezifikation und werden vom 📜 Rulebook Expert (A-5)
|
* Hinweis: Die konkreten Regeln stammen aus der ÖTO-Spezifikation und werden vom 📜 Rulebook Expert (A-5)
|
||||||
* bereitgestellt. Diese Methode delegiert sich daher an eine Policy-Schnittstelle, um Kopplung zu vermeiden und
|
* bereitgestellt. Diese Methode delegiert daher an eine Policy-Schnittstelle, um Kopplung zu vermeiden und
|
||||||
* die Regeln austauschbar zu halten.
|
* die Regeln austauschbar zu halten.
|
||||||
*
|
*
|
||||||
* Rückgabe: Liste von Meldungen (Fehler/Warnungen) – Formulierung/Schweregrad ist Teil der Policy.
|
* Rückgabe: Liste von Meldungen (Fehler/Warnungen) – Formulierung/Schweregrad ist Teil der Policy.
|
||||||
|
|
@ -0,0 +1,147 @@
|
||||||
|
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
||||||
|
|
||||||
|
package at.mocode.events.domain.model
|
||||||
|
|
||||||
|
import at.mocode.core.domain.model.SparteE
|
||||||
|
import at.mocode.core.domain.model.VeranstaltungsStatusE
|
||||||
|
import at.mocode.core.domain.model.VeranstaltungsTypE
|
||||||
|
import at.mocode.core.domain.serialization.KotlinxInstantSerializer
|
||||||
|
import at.mocode.core.domain.serialization.UuidSerializer
|
||||||
|
import kotlinx.datetime.LocalDate
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlin.time.Clock
|
||||||
|
import kotlin.time.Instant
|
||||||
|
import kotlin.uuid.Uuid
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Aggregate Root für eine pferdesportliche Veranstaltung gemäß ÖTO § 2 Abs. 1.
|
||||||
|
*
|
||||||
|
* Eine Veranstaltung ist der organisatorische Rahmen (z.B. "Frühjahrsturnier Wiener Neustadt").
|
||||||
|
* Sie kann ein oder mehrere Turniere enthalten. Die Unterscheidung Veranstaltung ≠ Turnier
|
||||||
|
* ist eine zentrale ADR-Entscheidung (ADR-0003).
|
||||||
|
*
|
||||||
|
* @property veranstaltungId Eindeutige interne ID (UUID).
|
||||||
|
* @property name Offizieller Name der Veranstaltung.
|
||||||
|
* @property veranstaltungsTyp Typ gemäß ÖTO (z.B. NATIONAL, INTERNATIONAL).
|
||||||
|
* @property sparten Liste der enthaltenen Sparten (Springen, Dressur, etc.).
|
||||||
|
* @property veranstalterVereinId ID des veranstaltenden Vereins (Referenz auf clubs-context).
|
||||||
|
* @property verantwortlicheFunktionaerId ID des verantwortlichen Funktionärs (Referenz auf officials-context).
|
||||||
|
* @property startDatum Erster Veranstaltungstag.
|
||||||
|
* @property endDatum Letzter Veranstaltungstag.
|
||||||
|
* @property ort Veranstaltungsort (Adresse / Reitanlage).
|
||||||
|
* @property nennschluss Nennschluss-Datum gemäß Ausschreibung.
|
||||||
|
* @property status Aktueller Status im Planungs-Workflow.
|
||||||
|
* @property ausschreibungsId Referenz auf die zugehörige Ausschreibung (optional bis Genehmigung).
|
||||||
|
* @property oepsGenehmigungsNummer Offizielle Genehmigungsnummer des Verbands (nach Genehmigung).
|
||||||
|
* @property bemerkungen Interne Bemerkungen.
|
||||||
|
* @property createdAt Erstellungszeitpunkt.
|
||||||
|
* @property updatedAt Letzter Änderungszeitpunkt.
|
||||||
|
*/
|
||||||
|
@Serializable
|
||||||
|
data class DomVeranstaltung(
|
||||||
|
@Serializable(with = UuidSerializer::class)
|
||||||
|
val veranstaltungId: Uuid = Uuid.random(),
|
||||||
|
|
||||||
|
// Basis-Informationen
|
||||||
|
var name: String,
|
||||||
|
var veranstaltungsTyp: VeranstaltungsTypE,
|
||||||
|
var sparten: List<SparteE> = emptyList(),
|
||||||
|
|
||||||
|
// Organisation
|
||||||
|
@Serializable(with = UuidSerializer::class)
|
||||||
|
var veranstalterVereinId: Uuid,
|
||||||
|
@Serializable(with = UuidSerializer::class)
|
||||||
|
var verantwortlicheFunktionaerId: Uuid? = null,
|
||||||
|
|
||||||
|
// Termine
|
||||||
|
var startDatum: LocalDate,
|
||||||
|
var endDatum: LocalDate,
|
||||||
|
var ort: String,
|
||||||
|
var nennschluss: LocalDate? = null,
|
||||||
|
|
||||||
|
// Workflow-Status
|
||||||
|
var status: VeranstaltungsStatusE = VeranstaltungsStatusE.IN_PLANUNG,
|
||||||
|
|
||||||
|
// Verknüpfungen
|
||||||
|
@Serializable(with = UuidSerializer::class)
|
||||||
|
var ausschreibungsId: Uuid? = null,
|
||||||
|
var oepsGenehmigungsNummer: String? = null,
|
||||||
|
|
||||||
|
// Administrativ
|
||||||
|
var bemerkungen: String? = null,
|
||||||
|
|
||||||
|
// Audit
|
||||||
|
@Serializable(with = KotlinxInstantSerializer::class)
|
||||||
|
val createdAt: Instant = Clock.System.now(),
|
||||||
|
@Serializable(with = KotlinxInstantSerializer::class)
|
||||||
|
var updatedAt: Instant = Clock.System.now()
|
||||||
|
) {
|
||||||
|
/**
|
||||||
|
* Gibt die Dauer der Veranstaltung in Tagen zurück.
|
||||||
|
*/
|
||||||
|
fun getDauerInTagen(): Int =
|
||||||
|
(endDatum.toEpochDays() - startDatum.toEpochDays()).toInt() + 1
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prüft ob die Veranstaltung mehrtägig ist.
|
||||||
|
*/
|
||||||
|
fun istMehrtaegig(): Boolean = startDatum != endDatum
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prüft ob Nennungen aktuell möglich sind (Status GENEHMIGT und Nennschluss nicht abgelaufen).
|
||||||
|
* Gibt Warnungen zurück – keine harten Fehler (Warn-Logik statt Exception, ADR-0007).
|
||||||
|
*/
|
||||||
|
fun validateNennungsmoeglichkeit(): List<String> {
|
||||||
|
val warnings = mutableListOf<String>()
|
||||||
|
if (status != VeranstaltungsStatusE.GENEHMIGT) {
|
||||||
|
warnings.add(
|
||||||
|
"Veranstaltung ist nicht im Status GENEHMIGT (aktuell: $status). " +
|
||||||
|
"Nennungen sind erst nach Genehmigung möglich."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (nennschluss == null) {
|
||||||
|
warnings.add("Kein Nennschluss definiert. Bitte Ausschreibung vervollständigen.")
|
||||||
|
}
|
||||||
|
if (veranstalterVereinId == null) {
|
||||||
|
warnings.add("Kein Veranstalter-Verein zugewiesen.")
|
||||||
|
}
|
||||||
|
return warnings
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validiert die Pflichtfelder für die Einreichung beim Verband.
|
||||||
|
* Gibt Warnungen zurück – keine harten Fehler (Warn-Logik statt Exception, ADR-0007).
|
||||||
|
*/
|
||||||
|
fun validateFuerEinreichung(): List<String> {
|
||||||
|
val warnings = mutableListOf<String>()
|
||||||
|
if (name.isBlank()) {
|
||||||
|
warnings.add("Veranstaltungsname ist erforderlich.")
|
||||||
|
}
|
||||||
|
if (ort.isBlank()) {
|
||||||
|
warnings.add("Veranstaltungsort ist erforderlich.")
|
||||||
|
}
|
||||||
|
if (endDatum < startDatum) {
|
||||||
|
warnings.add("Enddatum darf nicht vor dem Startdatum liegen.")
|
||||||
|
}
|
||||||
|
if (sparten.isEmpty()) {
|
||||||
|
warnings.add("Mindestens eine Sparte muss angegeben werden.")
|
||||||
|
}
|
||||||
|
if (nennschluss == null) {
|
||||||
|
warnings.add("Nennschluss ist für die Einreichung erforderlich.")
|
||||||
|
}
|
||||||
|
nennschluss?.let { nl ->
|
||||||
|
if (nl >= startDatum) {
|
||||||
|
warnings.add("Nennschluss muss vor dem Veranstaltungsbeginn liegen (§ 2 ÖTO).")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ausschreibungsId == null) {
|
||||||
|
warnings.add("Keine Ausschreibung verknüpft. Einreichung ohne Ausschreibung nicht möglich.")
|
||||||
|
}
|
||||||
|
return warnings
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Erstellt eine Kopie mit aktualisiertem Zeitstempel.
|
||||||
|
*/
|
||||||
|
fun withUpdatedTimestamp(): DomVeranstaltung = this.copy(updatedAt = Clock.System.now())
|
||||||
|
}
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
|
||||||
|
|
||||||
package at.mocode.events.domain.model
|
|
||||||
|
|
||||||
import at.mocode.core.domain.serialization.UuidSerializer
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import kotlin.uuid.Uuid
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Domain-Modell für einen Turnier-Artikel im Billing-Context.
|
|
||||||
*
|
|
||||||
* Ein TurnierArtikel repräsentiert eine abrechenbare Leistung oder ein Produkt,
|
|
||||||
* das im Rahmen einer Veranstaltung angeboten wird (z.B. Ansage, Heu, Startgeld).
|
|
||||||
* Preise werden in Cent gespeichert (Long), um Gleitkomma-Fehler zu vermeiden.
|
|
||||||
*
|
|
||||||
* @property id Eindeutige interne ID (UUID).
|
|
||||||
* @property name Bezeichnung des Artikels (z.B. "Ansage", "Heu", "Startgeld").
|
|
||||||
* @property preis Preis in Cent (z.B. 1500 = 15,00 €).
|
|
||||||
* @property typ Klassifikation: "AUTOMATISCH" (systemseitig) oder "MANUELL" (manuell erfasst).
|
|
||||||
*/
|
|
||||||
@Serializable
|
|
||||||
data class TurnierArtikel(
|
|
||||||
@Serializable(with = UuidSerializer::class)
|
|
||||||
val id: Uuid = Uuid.random(),
|
|
||||||
val name: String,
|
|
||||||
val preis: Long,
|
|
||||||
val typ: String
|
|
||||||
)
|
|
||||||
|
|
@ -2,15 +2,15 @@
|
||||||
package at.mocode.events.domain.model
|
package at.mocode.events.domain.model
|
||||||
|
|
||||||
import at.mocode.core.domain.model.SparteE
|
import at.mocode.core.domain.model.SparteE
|
||||||
import at.mocode.core.domain.serialization.InstantSerializer
|
import at.mocode.core.domain.serialization.KotlinxInstantSerializer
|
||||||
import at.mocode.core.domain.serialization.LocalDateSerializer
|
import at.mocode.core.domain.serialization.KotlinLocalDateSerializer
|
||||||
import at.mocode.core.domain.serialization.UuidSerializer
|
import at.mocode.core.domain.serialization.UuidSerializer
|
||||||
import kotlin.uuid.Uuid
|
import kotlin.uuid.Uuid
|
||||||
|
import kotlinx.datetime.Clock
|
||||||
|
import kotlinx.datetime.Instant
|
||||||
import kotlinx.datetime.LocalDate
|
import kotlinx.datetime.LocalDate
|
||||||
|
import kotlinx.datetime.TimeZone
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlin.time.Clock
|
|
||||||
import kotlin.time.Instant
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Domain model representing an event/competition in the event management system.
|
* Domain model representing an event/competition in the event management system.
|
||||||
|
|
@ -35,40 +35,40 @@ import kotlin.time.Instant
|
||||||
*/
|
*/
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Veranstaltung(
|
data class Veranstaltung(
|
||||||
@Serializable(with = UuidSerializer::class)
|
@Serializable(with = UuidSerializer::class)
|
||||||
val veranstaltungId: Uuid = Uuid.random(),
|
val veranstaltungId: Uuid = Uuid.random(),
|
||||||
|
|
||||||
// Basic Information
|
// Basic Information
|
||||||
var name: String,
|
var name: String,
|
||||||
var untertitel: String? = null,
|
var untertitel: String? = null,
|
||||||
var beschreibung: String? = null,
|
var beschreibung: String? = null,
|
||||||
var logoUrl: String? = null,
|
var logoUrl: String? = null,
|
||||||
var sponsoren: String? = null, // JSON string or comma-separated for now
|
var sponsoren: String? = null, // JSON string or comma-separated for now
|
||||||
|
|
||||||
// Dates
|
// Dates
|
||||||
@Serializable(with = LocalDateSerializer::class)
|
@Serializable(with = KotlinLocalDateSerializer::class)
|
||||||
var startDatum: LocalDate,
|
var startDatum: LocalDate,
|
||||||
@Serializable(with = LocalDateSerializer::class)
|
@Serializable(with = KotlinLocalDateSerializer::class)
|
||||||
var endDatum: LocalDate,
|
var endDatum: LocalDate,
|
||||||
|
|
||||||
// Location and Organization
|
// Location and Organization
|
||||||
var ort: String,
|
var ort: String,
|
||||||
@Serializable(with = UuidSerializer::class)
|
@Serializable(with = UuidSerializer::class)
|
||||||
var veranstalterVereinId: Uuid,
|
var veranstalterVereinId: Uuid,
|
||||||
|
|
||||||
// Event Details
|
// Event Details
|
||||||
var sparten: List<SparteE> = emptyList(),
|
var sparten: List<SparteE> = emptyList(),
|
||||||
var istAktiv: Boolean = true,
|
var istAktiv: Boolean = true,
|
||||||
var istOeffentlich: Boolean = true,
|
var istOeffentlich: Boolean = true,
|
||||||
var maxTeilnehmer: Int? = null,
|
var maxTeilnehmer: Int? = null,
|
||||||
|
|
||||||
@Serializable(with = LocalDateSerializer::class)
|
@Serializable(with = KotlinLocalDateSerializer::class)
|
||||||
var anmeldeschluss: LocalDate? = null,
|
var anmeldeschluss: LocalDate? = null,
|
||||||
|
|
||||||
// Audit Fields
|
// Audit Fields
|
||||||
@Serializable(with = InstantSerializer::class)
|
@Serializable(with = KotlinxInstantSerializer::class)
|
||||||
val createdAt: Instant = Clock.System.now(),
|
val createdAt: Instant = Clock.System.now(),
|
||||||
@Serializable(with = InstantSerializer::class)
|
@Serializable(with = KotlinxInstantSerializer::class)
|
||||||
var updatedAt: Instant = Clock.System.now()
|
var updatedAt: Instant = Clock.System.now()
|
||||||
) {
|
) {
|
||||||
/**
|
/**
|
||||||
|
|
@ -127,7 +127,7 @@ data class Veranstaltung(
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a copy of this event with an updated timestamp.
|
* Creates a copy of this event with updated timestamp.
|
||||||
*/
|
*/
|
||||||
fun withUpdatedTimestamp(): Veranstaltung {
|
fun withUpdatedTimestamp(): Veranstaltung {
|
||||||
return this.copy(updatedAt = Clock.System.now())
|
return this.copy(updatedAt = Clock.System.now())
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ data class TurnierBewerbDescriptor(
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Policy-Schnittstelle für die Validierung der Turnierkategorie-Limits (ÖTO-Regelwerk).
|
* Policy-Schnittstelle für die Validierung der Turnierkategorie-Limits (ÖTO-Regelwerk).
|
||||||
* Die Implementierung wird durch den 📜 Rulebook Expert (A-5) spezifiziert.
|
* Implementierung wird durch den 📜 Rulebook Expert (A-5) spezifiziert.
|
||||||
*/
|
*/
|
||||||
fun interface TurnierkategoriePolicy {
|
fun interface TurnierkategoriePolicy {
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,12 @@ import at.mocode.core.domain.model.SparteE
|
||||||
import at.mocode.core.domain.model.TurnierkategorieE
|
import at.mocode.core.domain.model.TurnierkategorieE
|
||||||
import at.mocode.events.domain.validation.TurnierBewerbDescriptor
|
import at.mocode.events.domain.validation.TurnierBewerbDescriptor
|
||||||
import at.mocode.events.domain.validation.TurnierkategoriePolicy
|
import at.mocode.events.domain.validation.TurnierkategoriePolicy
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
import kotlin.uuid.Uuid
|
import kotlin.uuid.Uuid
|
||||||
import kotlinx.datetime.LocalDate
|
import kotlinx.datetime.LocalDate
|
||||||
import org.junit.jupiter.api.Assertions.assertEquals
|
|
||||||
import org.junit.jupiter.api.Test
|
|
||||||
import kotlin.uuid.ExperimentalUuidApi
|
|
||||||
|
|
||||||
class TurnierKategorieValidationTest {
|
class DomTurnierKategorieValidationTest {
|
||||||
|
|
||||||
private val fakePolicy = TurnierkategoriePolicy { kategorie, bewerbe ->
|
private val fakePolicy = TurnierkategoriePolicy { kategorie, bewerbe ->
|
||||||
val msgs = mutableListOf<String>()
|
val msgs = mutableListOf<String>()
|
||||||
|
|
@ -30,13 +29,11 @@ class TurnierKategorieValidationTest {
|
||||||
msgs
|
msgs
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalUuidApi::class)
|
|
||||||
@Test
|
@Test
|
||||||
fun `C Turnier verbietet 135cm Springen`() {
|
fun `C Turnier verbietet 135cm Springen`() {
|
||||||
val turnier = Turnier(
|
val turnier = DomTurnier(
|
||||||
veranstaltungId = Uuid.random(),
|
veranstaltungId = Uuid.random(),
|
||||||
name = "CSN-C Samstag",
|
name = "CSN-C Samstag",
|
||||||
turnierNummer = "12345",
|
|
||||||
sparte = SparteE.SPRINGEN,
|
sparte = SparteE.SPRINGEN,
|
||||||
kategorie = TurnierkategorieE.C,
|
kategorie = TurnierkategorieE.C,
|
||||||
datum = LocalDate(2026, 6, 1)
|
datum = LocalDate(2026, 6, 1)
|
||||||
|
|
@ -51,13 +48,11 @@ class TurnierKategorieValidationTest {
|
||||||
assertEquals(1, msgs.size, "Erwartet genau eine Limitverletzung (135cm auf C-Turnier)")
|
assertEquals(1, msgs.size, "Erwartet genau eine Limitverletzung (135cm auf C-Turnier)")
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalUuidApi::class)
|
|
||||||
@Test
|
@Test
|
||||||
fun `C-NEU Turnier verbietet 120cm`() {
|
fun `C-NEU Turnier verbietet 120cm`() {
|
||||||
val turnier = Turnier(
|
val turnier = DomTurnier(
|
||||||
veranstaltungId = Uuid.random(),
|
veranstaltungId = Uuid.random(),
|
||||||
name = "CSN-C-NEU",
|
name = "CSN-C-NEU",
|
||||||
turnierNummer = "12345",
|
|
||||||
sparte = SparteE.SPRINGEN,
|
sparte = SparteE.SPRINGEN,
|
||||||
kategorie = TurnierkategorieE.C_NEU,
|
kategorie = TurnierkategorieE.C_NEU,
|
||||||
datum = LocalDate(2026, 6, 1)
|
datum = LocalDate(2026, 6, 1)
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,8 @@ package at.mocode.events.domain.validation
|
||||||
|
|
||||||
import at.mocode.core.domain.model.SparteE
|
import at.mocode.core.domain.model.SparteE
|
||||||
import at.mocode.core.domain.model.TurnierkategorieE
|
import at.mocode.core.domain.model.TurnierkategorieE
|
||||||
import org.junit.jupiter.api.Assertions.assertEquals
|
import kotlin.test.Test
|
||||||
import org.junit.jupiter.api.Test
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
|
|
||||||
class OeToTurnierkategoriePolicyTest {
|
class OeToTurnierkategoriePolicyTest {
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,24 @@
|
||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.kotlinJvm)
|
alias(libs.plugins.kotlin.jvm)
|
||||||
alias(libs.plugins.kotlinSpring)
|
alias(libs.plugins.kotlin.spring)
|
||||||
alias(libs.plugins.kotlinSerialization)
|
alias(libs.plugins.kotlin.serialization)
|
||||||
|
alias(libs.plugins.ktor)
|
||||||
|
application
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(platform(projects.platform.platformBom))
|
implementation(projects.platform.platformDependencies)
|
||||||
implementation(projects.platform.platformDependencies)
|
|
||||||
implementation(projects.backend.services.events.eventsDomain)
|
|
||||||
implementation(projects.core.coreDomain)
|
|
||||||
implementation(projects.core.coreUtils)
|
|
||||||
implementation(projects.backend.infrastructure.cache.cacheApi)
|
|
||||||
implementation(projects.backend.infrastructure.eventStore.eventStoreApi)
|
|
||||||
implementation(projects.backend.infrastructure.messaging.messagingClient)
|
|
||||||
implementation(projects.backend.infrastructure.persistence)
|
|
||||||
|
|
||||||
implementation(libs.exposed.core)
|
implementation(projects.events.eventsDomain)
|
||||||
implementation(libs.exposed.dao)
|
implementation(projects.events.eventsApplication)
|
||||||
implementation(libs.exposed.jdbc)
|
implementation(projects.core.coreDomain)
|
||||||
implementation(libs.exposed.kotlin.datetime)
|
implementation(projects.core.coreUtils)
|
||||||
implementation(libs.exposed.java.time)
|
implementation(projects.infrastructure.cache.cacheApi)
|
||||||
implementation(libs.exposed.json)
|
implementation(projects.infrastructure.eventStore.eventStoreApi)
|
||||||
|
implementation(projects.infrastructure.messaging.messagingClient)
|
||||||
|
|
||||||
implementation(libs.spring.boot.starter.data.jpa)
|
implementation(libs.spring.boot.starter.data.jpa)
|
||||||
implementation(libs.postgresql.driver)
|
implementation(libs.postgresql.driver)
|
||||||
|
|
||||||
testImplementation(projects.platform.platformTesting)
|
testImplementation(projects.platform.platformTesting)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,19 +4,14 @@ package at.mocode.events.infrastructure.persistence
|
||||||
import at.mocode.core.domain.model.SparteE
|
import at.mocode.core.domain.model.SparteE
|
||||||
import at.mocode.events.domain.model.Veranstaltung
|
import at.mocode.events.domain.model.Veranstaltung
|
||||||
import at.mocode.events.domain.repository.VeranstaltungRepository
|
import at.mocode.events.domain.repository.VeranstaltungRepository
|
||||||
|
import at.mocode.core.utils.database.DatabaseFactory
|
||||||
import kotlin.uuid.Uuid
|
import kotlin.uuid.Uuid
|
||||||
|
import kotlinx.datetime.Clock
|
||||||
import kotlinx.datetime.LocalDate
|
import kotlinx.datetime.LocalDate
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import org.jetbrains.exposed.v1.core.*
|
import org.jetbrains.exposed.sql.*
|
||||||
import org.jetbrains.exposed.v1.jdbc.*
|
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
||||||
import org.jetbrains.exposed.v1.core.statements.UpdateBuilder
|
import org.jetbrains.exposed.sql.statements.UpdateBuilder
|
||||||
import at.mocode.backend.infrastructure.persistence.readTransaction
|
|
||||||
import at.mocode.backend.infrastructure.persistence.writeTransaction
|
|
||||||
import at.mocode.backend.infrastructure.persistence.getOrNull
|
|
||||||
import at.mocode.core.utils.database.DatabaseFactory
|
|
||||||
import java.util.UUID
|
|
||||||
import kotlin.time.Clock
|
|
||||||
import kotlin.time.Instant
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Exposed-based implementation of VeranstaltungRepository.
|
* Exposed-based implementation of VeranstaltungRepository.
|
||||||
|
|
@ -26,11 +21,8 @@ import kotlin.time.Instant
|
||||||
*/
|
*/
|
||||||
class VeranstaltungRepositoryImpl : VeranstaltungRepository {
|
class VeranstaltungRepositoryImpl : VeranstaltungRepository {
|
||||||
|
|
||||||
private fun Uuid.toJavaUuid(): UUID = UUID.fromString(this.toString())
|
|
||||||
private fun UUID.toKotlinUuid(): Uuid = Uuid.parse(this.toString())
|
|
||||||
|
|
||||||
override suspend fun findById(id: Uuid): Veranstaltung? = DatabaseFactory.dbQuery {
|
override suspend fun findById(id: Uuid): Veranstaltung? = DatabaseFactory.dbQuery {
|
||||||
VeranstaltungTable.selectAll().where { VeranstaltungTable.id eq id.toJavaUuid() }
|
VeranstaltungTable.selectAll().where { VeranstaltungTable.id eq id }
|
||||||
.map { rowToVeranstaltung(it) }
|
.map { rowToVeranstaltung(it) }
|
||||||
.singleOrNull()
|
.singleOrNull()
|
||||||
}
|
}
|
||||||
|
|
@ -44,77 +36,78 @@ class VeranstaltungRepositoryImpl : VeranstaltungRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun findByVeranstalterVereinId(vereinId: Uuid, activeOnly: Boolean): List<Veranstaltung> = DatabaseFactory.dbQuery {
|
override suspend fun findByVeranstalterVereinId(vereinId: Uuid, activeOnly: Boolean): List<Veranstaltung> = DatabaseFactory.dbQuery {
|
||||||
var query = VeranstaltungTable.selectAll().where { VeranstaltungTable.veranstalterVereinId eq vereinId.toJavaUuid() }
|
val query = VeranstaltungTable.selectAll().where { VeranstaltungTable.veranstalterVereinId eq vereinId }
|
||||||
|
|
||||||
if (activeOnly) {
|
if (activeOnly) {
|
||||||
query = query.andWhere { VeranstaltungTable.istAktiv eq true }
|
query.andWhere { VeranstaltungTable.istAktiv eq true }
|
||||||
}
|
} else {
|
||||||
query.orderBy(VeranstaltungTable.startDatum, SortOrder.DESC)
|
query
|
||||||
|
}.orderBy(VeranstaltungTable.startDatum, SortOrder.DESC)
|
||||||
.map { rowToVeranstaltung(it) }
|
.map { rowToVeranstaltung(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun findByDateRange(startDate: LocalDate, endDate: LocalDate, activeOnly: Boolean): List<Veranstaltung> = DatabaseFactory.dbQuery {
|
override suspend fun findByDateRange(startDate: LocalDate, endDate: LocalDate, activeOnly: Boolean): List<Veranstaltung> = DatabaseFactory.dbQuery {
|
||||||
var query = VeranstaltungTable.selectAll().where {
|
val query = VeranstaltungTable.selectAll().where {
|
||||||
(VeranstaltungTable.startDatum greaterEq startDate) and
|
(VeranstaltungTable.startDatum greaterEq startDate) and
|
||||||
(VeranstaltungTable.endDatum lessEq endDate)
|
(VeranstaltungTable.endDatum lessEq endDate)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (activeOnly) {
|
if (activeOnly) {
|
||||||
query = query.andWhere { VeranstaltungTable.istAktiv eq true }
|
query.andWhere { VeranstaltungTable.istAktiv eq true }
|
||||||
}
|
} else {
|
||||||
query.orderBy(VeranstaltungTable.startDatum)
|
query
|
||||||
|
}.orderBy(VeranstaltungTable.startDatum)
|
||||||
.map { rowToVeranstaltung(it) }
|
.map { rowToVeranstaltung(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun findByStartDate(date: LocalDate, activeOnly: Boolean): List<Veranstaltung> = DatabaseFactory.dbQuery {
|
override suspend fun findByStartDate(date: LocalDate, activeOnly: Boolean): List<Veranstaltung> = DatabaseFactory.dbQuery {
|
||||||
var query = VeranstaltungTable.selectAll().where { VeranstaltungTable.startDatum eq date }
|
val query = VeranstaltungTable.selectAll().where { VeranstaltungTable.startDatum eq date }
|
||||||
|
|
||||||
if (activeOnly) {
|
if (activeOnly) {
|
||||||
query = query.andWhere { VeranstaltungTable.istAktiv eq true }
|
query.andWhere { VeranstaltungTable.istAktiv eq true }
|
||||||
}
|
} else {
|
||||||
query.orderBy(VeranstaltungTable.name)
|
query
|
||||||
|
}.orderBy(VeranstaltungTable.name)
|
||||||
.map { rowToVeranstaltung(it) }
|
.map { rowToVeranstaltung(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun findAllActive(limit: Int, offset: Int): List<Veranstaltung> = DatabaseFactory.dbQuery {
|
override suspend fun findAllActive(limit: Int, offset: Int): List<Veranstaltung> = DatabaseFactory.dbQuery {
|
||||||
VeranstaltungTable.selectAll().where { VeranstaltungTable.istAktiv eq true }
|
VeranstaltungTable.selectAll().where { VeranstaltungTable.istAktiv eq true }
|
||||||
.orderBy(VeranstaltungTable.startDatum, SortOrder.DESC)
|
.orderBy(VeranstaltungTable.startDatum, SortOrder.DESC)
|
||||||
.limit(limit)
|
.limit(limit, offset.toLong())
|
||||||
.offset(offset.toLong())
|
|
||||||
.map { rowToVeranstaltung(it) }
|
.map { rowToVeranstaltung(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun findPublicEvents(activeOnly: Boolean): List<Veranstaltung> = DatabaseFactory.dbQuery {
|
override suspend fun findPublicEvents(activeOnly: Boolean): List<Veranstaltung> = DatabaseFactory.dbQuery {
|
||||||
var query = VeranstaltungTable.selectAll().where { VeranstaltungTable.istOeffentlich eq true }
|
val query = VeranstaltungTable.selectAll().where { VeranstaltungTable.istOeffentlich eq true }
|
||||||
|
|
||||||
if (activeOnly) {
|
if (activeOnly) {
|
||||||
query = query.andWhere { VeranstaltungTable.istAktiv eq true }
|
query.andWhere { VeranstaltungTable.istAktiv eq true }
|
||||||
}
|
} else {
|
||||||
query.orderBy(VeranstaltungTable.startDatum, SortOrder.DESC)
|
query
|
||||||
|
}.orderBy(VeranstaltungTable.startDatum, SortOrder.DESC)
|
||||||
.map { rowToVeranstaltung(it) }
|
.map { rowToVeranstaltung(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun save(veranstaltung: Veranstaltung): Veranstaltung = DatabaseFactory.dbQuery {
|
override suspend fun save(veranstaltung: Veranstaltung): Veranstaltung = DatabaseFactory.dbQuery {
|
||||||
val now = Clock.System.now()
|
val now = Clock.System.now()
|
||||||
// Ensure now is kotlinx.datetime.Instant as expected by Domain
|
|
||||||
val updatedVeranstaltung = veranstaltung.copy(updatedAt = now)
|
val updatedVeranstaltung = veranstaltung.copy(updatedAt = now)
|
||||||
val vId = veranstaltung.veranstaltungId.toJavaUuid()
|
|
||||||
|
|
||||||
// Check if a record exists
|
// Check if a record exists
|
||||||
val existingRecord = VeranstaltungTable.selectAll()
|
val existingRecord = VeranstaltungTable.selectAll()
|
||||||
.where { VeranstaltungTable.id eq vId }
|
.where { VeranstaltungTable.id eq veranstaltung.veranstaltungId }
|
||||||
.singleOrNull()
|
.singleOrNull()
|
||||||
|
|
||||||
if (existingRecord != null) {
|
if (existingRecord != null) {
|
||||||
// Update existing record
|
// Update existing record
|
||||||
VeranstaltungTable.update({ VeranstaltungTable.id eq vId }) {
|
VeranstaltungTable.update({ VeranstaltungTable.id eq veranstaltung.veranstaltungId }) {
|
||||||
veranstaltungToStatement(it, updatedVeranstaltung)
|
veranstaltungToStatement(it, updatedVeranstaltung)
|
||||||
}
|
}
|
||||||
updatedVeranstaltung
|
updatedVeranstaltung
|
||||||
} else {
|
} else {
|
||||||
// Insert a new record
|
// Insert a new record
|
||||||
VeranstaltungTable.insert {
|
VeranstaltungTable.insert {
|
||||||
it[id] = vId
|
it[id] = veranstaltung.veranstaltungId
|
||||||
veranstaltungToStatement(it, updatedVeranstaltung)
|
veranstaltungToStatement(it, updatedVeranstaltung)
|
||||||
}
|
}
|
||||||
updatedVeranstaltung
|
updatedVeranstaltung
|
||||||
|
|
@ -122,7 +115,7 @@ class VeranstaltungRepositoryImpl : VeranstaltungRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun delete(id: Uuid): Boolean = DatabaseFactory.dbQuery {
|
override suspend fun delete(id: Uuid): Boolean = DatabaseFactory.dbQuery {
|
||||||
val deletedRows = VeranstaltungTable.deleteWhere { VeranstaltungTable.id eq id.toJavaUuid() }
|
val deletedRows = VeranstaltungTable.deleteWhere { VeranstaltungTable.id eq id }
|
||||||
deletedRows > 0
|
deletedRows > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -132,12 +125,13 @@ class VeranstaltungRepositoryImpl : VeranstaltungRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun countByVeranstalterVereinId(vereinId: Uuid, activeOnly: Boolean): Long = DatabaseFactory.dbQuery {
|
override suspend fun countByVeranstalterVereinId(vereinId: Uuid, activeOnly: Boolean): Long = DatabaseFactory.dbQuery {
|
||||||
var query = VeranstaltungTable.selectAll().where { VeranstaltungTable.veranstalterVereinId eq vereinId.toJavaUuid() }
|
val query = VeranstaltungTable.selectAll().where { VeranstaltungTable.veranstalterVereinId eq vereinId }
|
||||||
|
|
||||||
if (activeOnly) {
|
if (activeOnly) {
|
||||||
query = query.andWhere { VeranstaltungTable.istAktiv eq true }
|
query.andWhere { VeranstaltungTable.istAktiv eq true }
|
||||||
}
|
} else {
|
||||||
query.count()
|
query
|
||||||
|
}.count()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -157,7 +151,7 @@ class VeranstaltungRepositoryImpl : VeranstaltungRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
return Veranstaltung(
|
return Veranstaltung(
|
||||||
veranstaltungId = row[VeranstaltungTable.id].toKotlinUuid(),
|
veranstaltungId = row[VeranstaltungTable.id].value,
|
||||||
name = row[VeranstaltungTable.name],
|
name = row[VeranstaltungTable.name],
|
||||||
untertitel = row[VeranstaltungTable.untertitel],
|
untertitel = row[VeranstaltungTable.untertitel],
|
||||||
beschreibung = row[VeranstaltungTable.beschreibung],
|
beschreibung = row[VeranstaltungTable.beschreibung],
|
||||||
|
|
@ -166,7 +160,7 @@ class VeranstaltungRepositoryImpl : VeranstaltungRepository {
|
||||||
startDatum = row[VeranstaltungTable.startDatum],
|
startDatum = row[VeranstaltungTable.startDatum],
|
||||||
endDatum = row[VeranstaltungTable.endDatum],
|
endDatum = row[VeranstaltungTable.endDatum],
|
||||||
ort = row[VeranstaltungTable.ort],
|
ort = row[VeranstaltungTable.ort],
|
||||||
veranstalterVereinId = row[VeranstaltungTable.veranstalterVereinId].toKotlinUuid(),
|
veranstalterVereinId = row[VeranstaltungTable.veranstalterVereinId],
|
||||||
sparten = sparten,
|
sparten = sparten,
|
||||||
istAktiv = row[VeranstaltungTable.istAktiv],
|
istAktiv = row[VeranstaltungTable.istAktiv],
|
||||||
istOeffentlich = row[VeranstaltungTable.istOeffentlich],
|
istOeffentlich = row[VeranstaltungTable.istOeffentlich],
|
||||||
|
|
@ -189,7 +183,7 @@ class VeranstaltungRepositoryImpl : VeranstaltungRepository {
|
||||||
statement[VeranstaltungTable.startDatum] = veranstaltung.startDatum
|
statement[VeranstaltungTable.startDatum] = veranstaltung.startDatum
|
||||||
statement[VeranstaltungTable.endDatum] = veranstaltung.endDatum
|
statement[VeranstaltungTable.endDatum] = veranstaltung.endDatum
|
||||||
statement[VeranstaltungTable.ort] = veranstaltung.ort
|
statement[VeranstaltungTable.ort] = veranstaltung.ort
|
||||||
statement[VeranstaltungTable.veranstalterVereinId] = veranstaltung.veranstalterVereinId.toJavaUuid()
|
statement[VeranstaltungTable.veranstalterVereinId] = veranstaltung.veranstalterVereinId
|
||||||
statement[VeranstaltungTable.sparten] = Json.encodeToString(veranstaltung.sparten)
|
statement[VeranstaltungTable.sparten] = Json.encodeToString(veranstaltung.sparten)
|
||||||
statement[VeranstaltungTable.istAktiv] = veranstaltung.istAktiv
|
statement[VeranstaltungTable.istAktiv] = veranstaltung.istAktiv
|
||||||
statement[VeranstaltungTable.istOeffentlich] = veranstaltung.istOeffentlich
|
statement[VeranstaltungTable.istOeffentlich] = veranstaltung.istOeffentlich
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
package at.mocode.events.infrastructure.persistence
|
package at.mocode.events.infrastructure.persistence
|
||||||
|
|
||||||
import org.jetbrains.exposed.v1.core.Table
|
import at.mocode.core.domain.model.SparteE
|
||||||
import org.jetbrains.exposed.v1.core.java.javaUUID
|
import org.jetbrains.exposed.v1.core.dao.id.java.UUIDTable
|
||||||
import org.jetbrains.exposed.v1.datetime.timestamp
|
import org.jetbrains.exposed.v1.core.kotlin.datetime.date
|
||||||
import org.jetbrains.exposed.v1.datetime.date
|
import org.jetbrains.exposed.v1.core.kotlin.datetime.timestamp
|
||||||
|
import org.jetbrains.exposed.v1.core.javaUUID
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Database table definition for events (Veranstaltung) in the event-management context.
|
* Database table definition for events (Veranstaltung) in the event-management context.
|
||||||
|
|
@ -11,10 +12,7 @@ import org.jetbrains.exposed.v1.datetime.date
|
||||||
* This table stores all event information including dates, location,
|
* This table stores all event information including dates, location,
|
||||||
* organization details, and administrative information.
|
* organization details, and administrative information.
|
||||||
*/
|
*/
|
||||||
object VeranstaltungTable : Table("veranstaltungen") {
|
object VeranstaltungTable : UUIDTable("veranstaltungen") {
|
||||||
|
|
||||||
val id = javaUUID("id").autoGenerate()
|
|
||||||
override val primaryKey = PrimaryKey(id)
|
|
||||||
|
|
||||||
// Basic Information
|
// Basic Information
|
||||||
val name = varchar("name", 255)
|
val name = varchar("name", 255)
|
||||||
|
|
|
||||||
|
|
@ -1,40 +1,50 @@
|
||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.kotlinJvm)
|
alias(libs.plugins.kotlin.jvm)
|
||||||
alias(libs.plugins.kotlinSpring)
|
alias(libs.plugins.kotlin.spring)
|
||||||
alias(libs.plugins.spring.boot)
|
|
||||||
alias(libs.plugins.spring.dependencyManagement)
|
// KORREKTUR: Dieses Plugin ist entscheidend. Es schaltet den `springBoot`-Block
|
||||||
|
// und alle Spring-Boot-spezifischen Gradle-Tasks frei.
|
||||||
|
alias(libs.plugins.spring.boot)
|
||||||
|
|
||||||
|
// Dependency Management für konsistente Spring-Versionen
|
||||||
|
alias(libs.plugins.spring.dependencyManagement)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Dieser Block funktioniert jetzt, weil das `springBoot`-Plugin oben aktiviert ist.
|
||||||
springBoot {
|
springBoot {
|
||||||
mainClass.set("at.mocode.events.service.EventsServiceApplicationKt")
|
mainClass.set("at.mocode.events.service.EventsServiceApplicationKt")
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(platform(projects.platform.platformBom))
|
// Interne Module
|
||||||
implementation(projects.platform.platformDependencies)
|
implementation(projects.platform.platformDependencies)
|
||||||
implementation(projects.core.coreUtils)
|
implementation(projects.core.coreUtils)
|
||||||
implementation(projects.backend.services.events.eventsDomain)
|
implementation(projects.events.eventsDomain)
|
||||||
implementation(projects.backend.services.events.eventsInfrastructure)
|
implementation(projects.events.eventsApplication)
|
||||||
implementation(projects.backend.services.events.eventsApi)
|
implementation(projects.events.eventsInfrastructure)
|
||||||
implementation(projects.backend.infrastructure.persistence)
|
implementation(projects.events.eventsApi)
|
||||||
|
|
||||||
implementation(libs.exposed.core)
|
// Infrastruktur-Clients
|
||||||
implementation(libs.exposed.dao)
|
implementation(projects.infrastructure.cache.redisCache)
|
||||||
implementation(libs.exposed.jdbc)
|
implementation(projects.infrastructure.messaging.messagingClient)
|
||||||
implementation(libs.exposed.kotlin.datetime)
|
implementation(projects.infrastructure.monitoring.monitoringClient)
|
||||||
|
|
||||||
implementation(projects.backend.infrastructure.cache.valkeyCache)
|
// KORREKTUR: Alle externen Abhängigkeiten werden jetzt über den Version Catalog bezogen.
|
||||||
implementation(projects.backend.infrastructure.messaging.messagingClient)
|
|
||||||
implementation(projects.backend.infrastructure.monitoring.monitoringClient)
|
|
||||||
|
|
||||||
implementation(libs.spring.boot.starter.web)
|
// Spring Boot Starters
|
||||||
implementation(libs.spring.boot.starter.validation)
|
implementation(libs.spring.boot.starter.web)
|
||||||
implementation(libs.spring.boot.starter.actuator)
|
implementation(libs.spring.boot.starter.validation)
|
||||||
|
implementation(libs.spring.boot.starter.actuator)
|
||||||
|
|
||||||
runtimeOnly(libs.postgresql.driver)
|
// Datenbank-Treiber
|
||||||
|
runtimeOnly(libs.postgresql.driver)
|
||||||
|
|
||||||
testImplementation(projects.platform.platformTesting)
|
// Testing
|
||||||
testImplementation(libs.spring.boot.starter.test)
|
testImplementation(projects.platform.platformTesting)
|
||||||
testImplementation(libs.logback.classic)
|
testImplementation(libs.spring.boot.starter.test)
|
||||||
testImplementation("com.h2database:h2")
|
testImplementation(libs.logback.classic) // SLF4J provider for tests
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.test {
|
||||||
|
useJUnitPlatform()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,21 @@
|
||||||
package at.mocode.events.service.config
|
package at.mocode.events.service.config
|
||||||
|
|
||||||
|
import at.mocode.core.utils.database.DatabaseConfig
|
||||||
|
import at.mocode.core.utils.database.DatabaseFactory
|
||||||
|
import at.mocode.events.infrastructure.persistence.VeranstaltungTable
|
||||||
import org.springframework.context.annotation.Configuration
|
import org.springframework.context.annotation.Configuration
|
||||||
import org.springframework.context.annotation.Profile
|
import org.springframework.context.annotation.Profile
|
||||||
import jakarta.annotation.PostConstruct
|
import jakarta.annotation.PostConstruct
|
||||||
|
import jakarta.annotation.PreDestroy
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
|
import org.jetbrains.exposed.sql.SchemaUtils
|
||||||
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Datenbank-Konfiguration für den Events Service.
|
* Database configuration for the Events Service.
|
||||||
*
|
*
|
||||||
* Initialisiert das Exposed-Schema für Veranstaltungen und Turniere.
|
* This configuration ensures that Database.connect() is called properly
|
||||||
* Die DB-Verbindung selbst wird durch den zentralen DataSource-Bean initialisiert.
|
* before any Exposed operations are performed.
|
||||||
*/
|
*/
|
||||||
@Configuration
|
@Configuration
|
||||||
@Profile("!test")
|
@Profile("!test")
|
||||||
|
|
@ -19,13 +25,35 @@ class EventsDatabaseConfiguration {
|
||||||
|
|
||||||
@PostConstruct
|
@PostConstruct
|
||||||
fun initializeDatabase() {
|
fun initializeDatabase() {
|
||||||
// Flyway übernimmt ab jetzt die Schema-Erstellung pro Tenant.
|
log.info("Initializing database schema for Events Service...")
|
||||||
log.info("Überspringe Exposed Schema-Initialisierung – Flyway migriert pro Tenant-Schema.")
|
|
||||||
|
try {
|
||||||
|
// Database connection is already initialized by the gateway
|
||||||
|
// Only initialize the schema for this service
|
||||||
|
transaction {
|
||||||
|
SchemaUtils.create(VeranstaltungTable)
|
||||||
|
log.info("Events database schema initialized successfully")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
log.error("Failed to initialize database schema", e)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreDestroy
|
||||||
|
fun closeDatabase() {
|
||||||
|
log.info("Closing database connection for Events Service...")
|
||||||
|
try {
|
||||||
|
DatabaseFactory.close()
|
||||||
|
log.info("Database connection closed successfully")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
log.error("Error closing database connection", e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test-spezifische Datenbank-Konfiguration.
|
* Test-specific database configuration.
|
||||||
*/
|
*/
|
||||||
@Configuration
|
@Configuration
|
||||||
@Profile("test")
|
@Profile("test")
|
||||||
|
|
@ -35,6 +63,42 @@ class EventsTestDatabaseConfiguration {
|
||||||
|
|
||||||
@PostConstruct
|
@PostConstruct
|
||||||
fun initializeTestDatabase() {
|
fun initializeTestDatabase() {
|
||||||
log.info("Initialisiere Test-Datenbank-Schema für den Events Service...")
|
log.info("Initializing test database connection for Events Service...")
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Use H2 in-memory database for tests
|
||||||
|
val testConfig = DatabaseConfig(
|
||||||
|
jdbcUrl = "jdbc:h2:mem:events_test;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE",
|
||||||
|
username = "sa",
|
||||||
|
password = "",
|
||||||
|
driverClassName = "org.h2.Driver",
|
||||||
|
maxPoolSize = 5,
|
||||||
|
minPoolSize = 1,
|
||||||
|
autoMigrate = true
|
||||||
|
)
|
||||||
|
|
||||||
|
DatabaseFactory.init(testConfig)
|
||||||
|
log.info("Test database connection initialized successfully")
|
||||||
|
|
||||||
|
// Initialize database schema for tests
|
||||||
|
transaction {
|
||||||
|
SchemaUtils.create(VeranstaltungTable)
|
||||||
|
log.info("Test events database schema initialized successfully")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
log.error("Failed to initialize test database connection", e)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreDestroy
|
||||||
|
fun closeTestDatabase() {
|
||||||
|
log.info("Closing test database connection for Events Service...")
|
||||||
|
try {
|
||||||
|
DatabaseFactory.close()
|
||||||
|
log.info("Test database connection closed successfully")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
log.error("Error closing test database connection", e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -372,22 +372,7 @@ enum class AbteilungsTeilungsTypE {
|
||||||
ORGANISATORISCH,
|
ORGANISATORISCH,
|
||||||
|
|
||||||
/** Separate Siegerehrung: Abteilungen werden nicht zusammengeführt, jede Abt. hat eigene Platzierung */
|
/** Separate Siegerehrung: Abteilungen werden nicht zusammengeführt, jede Abt. hat eigene Platzierung */
|
||||||
SEPARATE_SIEGEREHRUNG,
|
SEPARATE_SIEGEREHRUNG
|
||||||
|
|
||||||
/** Manuelle Teilung: Abteilungen werden vom Benutzer manuell festgelegt */
|
|
||||||
MANUELL
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Typ des Beginnzeit-Eintrags für einen Bewerb im Zeitplan.
|
|
||||||
*/
|
|
||||||
@Serializable
|
|
||||||
enum class BeginnZeitTypE {
|
|
||||||
/** Fixer Startzeitpunkt (z.B. 09:00 Uhr) */
|
|
||||||
FIX,
|
|
||||||
|
|
||||||
/** Anschließend an den vorherigen Bewerb */
|
|
||||||
ANSCHLIESSEND
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -100,57 +100,16 @@ val myNewProperty by extra("Initialwert")
|
||||||
val myExtraProperty: String by extra
|
val myExtraProperty: String by extra
|
||||||
```
|
```
|
||||||
|
|
||||||
## Best Practices im Projekt
|
### Kotlin DSL Plugin (`kotlin-dsl`)
|
||||||
|
Dieses Plugin ist essenziell für die Entwicklung von Build-Logik in Kotlin (z.B. in `buildSrc` oder für Convention Plugins). Es wendet automatisch das Kotlin-Plugin an und fügt notwendige Abhängigkeiten wie `kotlin-stdlib` und `gradleKotlinDsl()` hinzu.
|
||||||
|
|
||||||
### Plugin-Management
|
|
||||||
Im Projekt wird ein zentraler **Version Catalog** (`gradle/libs.versions.toml`) verwendet. Plugins sollten **immer** über Aliase referenziert werden.
|
|
||||||
|
|
||||||
**Korrekt:**
|
|
||||||
```kotlin
|
```kotlin
|
||||||
|
// buildSrc/build.gradle.kts
|
||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.kotlinJvm)
|
`kotlin-dsl`
|
||||||
alias(libs.plugins.spring.boot)
|
}
|
||||||
}
|
|
||||||
```
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
**Falsch:**
|
|
||||||
```kotlin
|
|
||||||
plugins {
|
|
||||||
kotlin("jvm") // Veraltet/Inkonsistent
|
|
||||||
id("org.springframework.boot") version "3.4.0" // Version gehört in den Catalog
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Projekt-Referenzen (Accessors)
|
|
||||||
Aufgrund der tiefen Modulstruktur (`backend/services/...`) müssen Projekt-Referenzen den vollen Pfad abbilden, um typsicher zu sein.
|
|
||||||
|
|
||||||
**Korrekt:**
|
|
||||||
```kotlin
|
|
||||||
dependencies {
|
|
||||||
implementation(projects.backend.services.events.eventsDomain)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Falsch:**
|
|
||||||
```kotlin
|
|
||||||
dependencies {
|
|
||||||
implementation(projects.events.eventsDomain) // Schlägt fehl, wenn nicht direkt unter Root
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Spring Boot vs. Library
|
|
||||||
* **Services** (ausführbar) nutzen `alias(libs.plugins.spring.boot)` und den `springBoot { mainClass = ... }` Block.
|
|
||||||
* **Libraries/Domains** nutzen nur `alias(libs.plugins.kotlinJvm)` (oder Multiplatform) und definieren keine Main-Class.
|
|
||||||
|
|
||||||
### Kotlin Multiplatform (KMP)
|
|
||||||
Für Module, die sowohl im Backend als auch im Frontend (Compose) genutzt werden (z.B. `api`, `common`), wird das KMP-Plugin verwendet:
|
|
||||||
```kotlin
|
|
||||||
plugins {
|
|
||||||
alias(libs.plugins.kotlinMultiplatform)
|
|
||||||
}
|
|
||||||
|
|
||||||
kotlin {
|
|
||||||
jvm()
|
|
||||||
js(IR) { browser() }
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -1,57 +0,0 @@
|
||||||
---
|
|
||||||
type: ADR
|
|
||||||
status: ACCEPTED
|
|
||||||
date: 2026-04-08
|
|
||||||
deciders: Lead Architect, Backend Developer
|
|
||||||
---
|
|
||||||
|
|
||||||
# ADR-0023: Naming Convention für Domain-Entitäten (kein `Dom`-Präfix)
|
|
||||||
|
|
||||||
## Status
|
|
||||||
|
|
||||||
**ACCEPTED** – Umgesetzt in Session 2026-04-08 (Phase 1 & 2 Domain Clean-Up).
|
|
||||||
|
|
||||||
## Kontext
|
|
||||||
|
|
||||||
In der frühen Entwicklungsphase wurden Domain-Klassen mit dem Präfix `Dom` versehen
|
|
||||||
(z.B. `DomBewerb`, `DomVeranstaltung`, `DomTurnier`), um sie von DTOs und Datenbank-Entitäten
|
|
||||||
zu unterscheiden. Diese Konvention hat sich als unnötig und verwirrend erwiesen, da sie
|
|
||||||
gegen die Prinzipien der **Ubiquitous Language** (DDD) und der **Clean Architecture** verstößt.
|
|
||||||
|
|
||||||
## Entscheidung
|
|
||||||
|
|
||||||
**Domain-Entitäten tragen ab sofort ihren fachlichen Namen ohne Präfix.**
|
|
||||||
|
|
||||||
| Schicht | Namenskonvention | Beispiel |
|
|
||||||
|-----------------|-------------------------|-----------------------|
|
|
||||||
| **Domain** | `<FachlicheName>` | `Bewerb` |
|
|
||||||
| **Datenbank** | `<FachlicheName>Table` | `BewerbTable` |
|
|
||||||
| **Persistence** | `<FachlicheName>Entity` | `BewerbEntity` |
|
|
||||||
| **API/Transfer**| `<FachlicheName>Dto` | `BewerbDto` |
|
|
||||||
|
|
||||||
## Durchgeführte Umbenennungen (Phase 1)
|
|
||||||
|
|
||||||
| Alt (Dom-Präfix) | Neu (Clean Name) | Modul |
|
|
||||||
|-----------------------|---------------------|-----------------|
|
|
||||||
| `DomBewerb` | `Bewerb` | entries-domain |
|
|
||||||
| `DomAbteilung` | `Abteilung` | entries-domain |
|
|
||||||
| `DomNennung` | `Nennung` | entries-domain |
|
|
||||||
| `DomNennungsTransfer` | `NennungsTransfer` | entries-domain |
|
|
||||||
| `DomTurnier` | `Turnier` | events-domain |
|
|
||||||
| `DomAusschreibung` | `Ausschreibung` | events-domain |
|
|
||||||
| `DomVeranstaltung` | `Veranstaltung` | events-domain (bereits vorhanden, Duplikat gelöscht) |
|
|
||||||
|
|
||||||
## Konsequenzen
|
|
||||||
|
|
||||||
- **Positiv:** Ubiquitous Language im Code – Fachexperten und Entwickler sprechen dieselbe Sprache.
|
|
||||||
- **Positiv:** Weniger kognitive Last – kein mentales Mapping zwischen `DomBewerb` und `Bewerb`.
|
|
||||||
- **Positiv:** Entspricht Clean Architecture – die Domain-Schicht ist die "reine" Schicht.
|
|
||||||
- **Negativ:** Einmalige Umbenennung erfordert Anpassung aller Referenzen (wurde systemweit durchgeführt).
|
|
||||||
- **Achtung:** Datenbank-Migrationen (Flyway) und Repository-Mappings werden in Phase 3 separat behandelt.
|
|
||||||
|
|
||||||
## Coding Guideline (verbindlich ab sofort)
|
|
||||||
|
|
||||||
> **Neue Domain-Klassen werden NIEMALS mit `Dom`, `Model`, `Entity` oder ähnlichen
|
|
||||||
> technischen Präfixen/Suffixen versehen. Der fachliche Name ist der Klassenname.**
|
|
||||||
>
|
|
||||||
> Ausnahmen: Datenbank-Klassen (`...Table`, `...Entity`) und Transfer-Objekte (`...Dto`, `...Request`, `...Response`).
|
|
||||||
|
|
@ -53,7 +53,6 @@ Veranstalter (OEPS-Mitgliedsverein)
|
||||||
| Die ÖTO definiert sparten- und klassenabhängige Schwellenwerte, ab wievielen Startern eine Abteilung **verpflichtend** getrennt werden muss. Bei Überschreitung gibt das System eine **WARNUNG** (kein harter Fehler) – der TBA hat das letzte Wort (→ *Override-Event*). Vollständige Schwellenwert-Tabellen: → [`Abteilungs-Trennungs-Schwellenwerte.md`](../02_Reference/OETO_Regelwerk/Abteilungs-Trennungs-Schwellenwerte.md) | ÖTO § 2 Abs. 7, § 39 | |
|
| Die ÖTO definiert sparten- und klassenabhängige Schwellenwerte, ab wievielen Startern eine Abteilung **verpflichtend** getrennt werden muss. Bei Überschreitung gibt das System eine **WARNUNG** (kein harter Fehler) – der TBA hat das letzte Wort (→ *Override-Event*). Vollständige Schwellenwert-Tabellen: → [`Abteilungs-Trennungs-Schwellenwerte.md`](../02_Reference/OETO_Regelwerk/Abteilungs-Trennungs-Schwellenwerte.md) | ÖTO § 2 Abs. 7, § 39 | |
|
||||||
| **Akteur** | Historischer Begriff (siehe → *Stammdaten*). Oberbegriff für alle Personen (Reiter, Richter, Funktionäre, Besitzer) und Organisationen (Vereine), die im System interagieren. | – |
|
| **Akteur** | Historischer Begriff (siehe → *Stammdaten*). Oberbegriff für alle Personen (Reiter, Richter, Funktionäre, Besitzer) und Organisationen (Vereine), die im System interagieren. | – |
|
||||||
| **Ausschreibung** | Das offizielle Dokument, das alle Bedingungen eines Turniers festlegt. Pflichtfelder gemäß ÖTO (A-Satz der ZNS-Schnittstelle). | ÖTO Ausschreibungs-Struktur |
|
| **Ausschreibung** | Das offizielle Dokument, das alle Bedingungen eines Turniers festlegt. Pflichtfelder gemäß ÖTO (A-Satz der ZNS-Schnittstelle). | ÖTO Ausschreibungs-Struktur |
|
||||||
| **Austragungsplatz** | Physischer Ort (Platz, Arena, Halle) innerhalb einer Veranstaltung, auf dem Bewerbe stattfinden. Typ: `"Austragungsplatz"` (Wettkampf) oder `"Vorbereitungsplatz"` (Einreiten). Kann einer Sparte zugeordnet sein. Im Code als Entität **`Austragungsplatz`** (events-domain) abgebildet. | – |
|
|
||||||
|
|
||||||
### B
|
### B
|
||||||
|
|
||||||
|
|
@ -132,8 +131,7 @@ Veranstalter (OEPS-Mitgliedsverein)
|
||||||
### R
|
### R
|
||||||
|
|
||||||
| Begriff | Definition | ÖTO-Referenz |
|
| Begriff | Definition | ÖTO-Referenz |
|
||||||
|--------------------|------------------------------------------------------------------------------------------------------------------------------|--------------|
|
|--------------------|------------------------------------------------------------------------------------------------------------------------------|--------------|
|
||||||
| **RichterEinsatz** | Value Object, das den Einsatz eines Funktionärs (Richter, Aufsicht) in einem Bewerb beschreibt. Felder: `funktionaerId` (Referenz auf Funktionär) und `position` (z.B. `"C"`, `"M"`, `"B"`, `"Aufsicht"`). Im Code als **`RichterEinsatz`** (entries-domain) abgebildet. | ÖTO Funktionärs-Qualifikation |
|
|
||||||
| **Richtverfahren** | Das Bewertungsverfahren eines Bewerbs (z.B. § 204/4 Stilspringen, § 218 Einlauf). Bestimmt, wie Ergebnisse berechnet werden. | ÖTO B-Teil |
|
| **Richtverfahren** | Das Bewertungsverfahren eines Bewerbs (z.B. § 204/4 Stilspringen, § 218 Einlauf). Bestimmt, wie Ergebnisse berechnet werden. | ÖTO B-Teil |
|
||||||
|
|
||||||
### S
|
### S
|
||||||
|
|
@ -155,7 +153,6 @@ Veranstalter (OEPS-Mitgliedsverein)
|
||||||
| Begriff | Definition | ÖTO-Referenz |
|
| Begriff | Definition | ÖTO-Referenz |
|
||||||
|-----------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------|
|
|-----------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------|
|
||||||
| **TBA** | Turnierbeauftragter. Hat bei Regelkonflikten immer das letzte Wort. Jede Überschreibung wird als → *Override-Event* gespeichert. | ÖTO § 24/§ 25 |
|
| **TBA** | Turnierbeauftragter. Hat bei Regelkonflikten immer das letzte Wort. Jede Überschreibung wird als → *Override-Event* gespeichert. | ÖTO § 24/§ 25 |
|
||||||
| **TurnierArtikel** | Abrechenbare Leistung oder Produkt im Rahmen einer Veranstaltung (z.B. Ansage, Heu, Startgeld). Preis in Cent (Long). Typ: `"AUTOMATISCH"` (systemseitig erzeugt) oder `"MANUELL"` (manuell erfasst). Im Code als Entität **`TurnierArtikel`** (events-domain, Billing Context) abgebildet. | Billing Context |
|
|
||||||
| **Tierwohl-Euro** | Gebühr, die **pro Start** anfällt (nicht pro Nennung!). | ÖTO Gebührenordnung |
|
| **Tierwohl-Euro** | Gebühr, die **pro Start** anfällt (nicht pro Nennung!). | ÖTO Gebührenordnung |
|
||||||
| **Turnier** | In unserer Software: Eine pferdesportliche Veranstaltung mit einer offiziellen **Ausschreibung** und einer vom OEPS/LFV vergebenen, eindeutigen **Turniernummer**. Entspricht ÖTO § 2 Abs. 2. Ist eine Spezialisierung von → *Veranstaltung*. | ÖTO § 2 Abs. 2, § 5, § 24 |
|
| **Turnier** | In unserer Software: Eine pferdesportliche Veranstaltung mit einer offiziellen **Ausschreibung** und einer vom OEPS/LFV vergebenen, eindeutigen **Turniernummer**. Entspricht ÖTO § 2 Abs. 2. Ist eine Spezialisierung von → *Veranstaltung*. | ÖTO § 2 Abs. 2, § 5, § 24 |
|
||||||
| **Turniernummer** | Offizielle, vom OEPS vergebene **5-stellige** Kennung eines Turniers (z.B. `26128`). Sie ist eindeutig und Voraussetzung für die offizielle Ausschreibung. | ÖTO § 3, ZNS A-Satz |
|
| **Turniernummer** | Offizielle, vom OEPS vergebene **5-stellige** Kennung eines Turniers (z.B. `26128`). Sie ist eindeutig und Voraussetzung für die offizielle Ausschreibung. | ÖTO § 3, ZNS A-Satz |
|
||||||
|
|
|
||||||
|
|
@ -468,9 +468,7 @@ Denken Sie z.B. an:
|
||||||
• Müssen wir eintragen, an welchem Tag oder auf welchem Platz der Bewerb stattfindet?
|
• Müssen wir eintragen, an welchem Tag oder auf welchem Platz der Bewerb stattfindet?
|
||||||
• Brauchen wir ein Flag für "Qualifikation für Meisterschaft XY"?
|
• Brauchen wir ein Flag für "Qualifikation für Meisterschaft XY"?
|
||||||
|
|
||||||
---
|
--- Ich ---
|
||||||
Ich
|
|
||||||
---
|
|
||||||
|
|
||||||
Bring mich nocht aus dem Konzept in dem du mir schon so viele Sachen vorlegst.
|
Bring mich nocht aus dem Konzept in dem du mir schon so viele Sachen vorlegst.
|
||||||
Darum geht es hier ja, um's "Querdenken"
|
Darum geht es hier ja, um's "Querdenken"
|
||||||
|
|
@ -948,469 +946,3 @@ Dann auch noch hinterfragen, ob es so klever ist, die ganzen Klassen immer mit `
|
||||||
`DomBewerb` ist hierbei nur als vertreter Beispiel für alle Dom... Klassen Bezeichungen.
|
`DomBewerb` ist hierbei nur als vertreter Beispiel für alle Dom... Klassen Bezeichungen.
|
||||||
|
|
||||||
Und dann, hätte ich gerne einen genauen Umsetzungsplan.
|
Und dann, hätte ich gerne einen genauen Umsetzungsplan.
|
||||||
|
|
||||||
---
|
|
||||||
Neuer Chat-Verlauf 8. April 2026
|
|
||||||
---
|
|
||||||
|
|
||||||
---
|
|
||||||
Ich
|
|
||||||
---
|
|
||||||
Datum: 8. April 2026
|
|
||||||
Wir hatten gestern und heute Vormittag einen super Run im Bezug der Planung und Weiterentwicklung.
|
|
||||||
|
|
||||||
Hier unser Chat-verlauf:
|
|
||||||
"docs/temp/Chat-Verlauf-Gemini_2026-04-07.md"
|
|
||||||
|
|
||||||
Können wir fortfahren?
|
|
||||||
|
|
||||||
---
|
|
||||||
Gemini
|
|
||||||
---
|
|
||||||
|
|
||||||
🏗️ [Lead Architect]
|
|
||||||
|
|
||||||
Ja, absolut! Das war ein fantastischer Deep-Dive und wir haben jetzt ein extrem solides, praxisnahes Fundament.
|
|
||||||
Lassen Sie uns direkt an Ihren letzten drei Punkten anknüpfen:
|
|
||||||
1. Visualisierung
|
|
||||||
Ich habe gesehen, dass die Visualisierung bereits als Kernentitaeten_Umbenennung.puml im C4-Verzeichnis abgelegt wurde. Sie zeigt exakt die Beziehungen auf: Wie die Veranstaltung als "Klammer" dient (für Austragungsplätze und Preislisten), wie das Turnier die offiziellen Rahmendaten hält und wie der Bewerb alle sportlichen Details (inkl. Richter und Abteilungen) bündelt.
|
|
||||||
2. Das Dom-Präfix (Namenskonvention)
|
|
||||||
Ihre Intuition ist 100 % richtig. In einer sauberen Clean Architecture ist die Domäne das Zentrum der Anwendung. Die wichtigsten Kern-Klassen sollten die "saubersten" Namen haben.
|
|
||||||
•
|
|
||||||
Richtig: Bewerb, Turnier, Veranstaltung (im Domain-Layer)
|
|
||||||
•
|
|
||||||
Außen: BewerbEntity oder BewerbTable (im Infrastruktur/Datenbank-Layer) und BewerbDto (im API/Web-Layer).
|
|
||||||
Das ständige Dom-Präfix ist ein Relikt und wir werden es als allerersten Schritt im Umsetzungsplan entfernen, um den Code lesbarer und professioneller zu machen.
|
|
||||||
3. Der genaue Umsetzungsplan
|
|
||||||
|
|
||||||
Hier ist der detaillierte Masterplan (Roadmap) für die nächsten Entwicklungs-Sprints. Wir teilen das in logische, gut testbare Pakete (PRs/Stints) auf:
|
|
||||||
|
|
||||||
### 🟢 Phase 1: Clean-Up & Refactoring (Fokus: Domain)
|
|
||||||
|
|
||||||
Agent: 👷 [Backend Developer]
|
|
||||||
1. **Umbenennung:** Alle `Dom...` Klassen im Core- und Events/Entries-Domain-Modul in ihre sauberen Namen umbenennen (z.B. `DomBewerb` -> `Bewerb`, `DomTurnier` -> `Turnier`).
|
|
||||||
2. **Enums erweitern:**
|
|
||||||
- `AbteilungsTeilungsTypE` um den Wert `MANUELL` ergänzen.
|
|
||||||
- Neues Enum `BeginnZeitTypE` (`FIX`, `ANSCHLIESSEND`) anlegen.
|
|
||||||
|
|
||||||
### 🟡 Phase 2: Domain-Erweiterung (Fokus: Plätze, Richter & Bewerbs-Details)
|
|
||||||
|
|
||||||
Agent: 👷 [Backend Developer]
|
|
||||||
1. **Veranstaltungsebene:**
|
|
||||||
- Neues Modell `Austragungsplatz` (ID, Name, Typ, Sparte optional) erstellen.
|
|
||||||
- Neues Modell `TurnierArtikel` (Preisliste für Heu, Ansage etc.) erstellen.
|
|
||||||
2. **Bewerbsebene:**
|
|
||||||
- `Bewerb` um textliche Details erweitern: `beschreibung`, `aufgabe`, `aufgabenNummer`, `paraGrade`.
|
|
||||||
- `Bewerb` um Zeitplan-Felder erweitern: `austragungsplatzId`, `geplantesDatum`, `beginnZeitTyp`, `beginnZeit`, `reitdauerMinuten`, `umbauMinuten`, `besichtigungMinuten`, `stechenGeplant`.
|
|
||||||
- `Bewerb` um finanzielle Felder erweitern: `startgeldCent`, `geldpreisAusbezahlt`.
|
|
||||||
3. **Richter-Einsatz:**
|
|
||||||
- Neues Value-Object `RichterEinsatz` (funktionaerId, position) erstellen und als Liste dem Bewerb hinzufügen.
|
|
||||||
|
|
||||||
### 🟠 Phase 3: Persistenz & Datenbank (Fokus: Infrastruktur)
|
|
||||||
|
|
||||||
Agent: 👷 [Backend Developer]
|
|
||||||
1. **Neue Tabellen:** Flyway-Migrationen für `austragungsplaetze`, `turnier_artikel` und `richter_einsaetze` (Mapping-Tabelle zu Bewerben) schreiben.
|
|
||||||
2. **Bewerb-Tabelle:** Flyway-Migration schreiben, um `bewerbe` um die zahlreichen neuen Spalten (Aufgabe, Zeiten, Geldpreis etc.) zu erweitern.
|
|
||||||
3. **Repositories:** Die `RepositoryImpl`-Klassen anpassen, damit alle neuen Felder und Listen beim Speichern und Laden korrekt gemappt werden.
|
|
||||||
|
|
||||||
### 🔵 Phase 4: API & Frontend-Wizard (Fokus: User Experience)
|
|
||||||
|
|
||||||
Agenten: 👷 [Backend Developer] & 🎨 [Frontend Expert]
|
|
||||||
1. **API-DTOs:** Die Request/Response-Objekte (`CreateBewerbDto`, etc.) um die neuen Felder aktualisieren.
|
|
||||||
2. **Frontend UI:** Den "Neuer Bewerb"-Wizard in Compose Multiplatform bauen (angelehnt an die SuDo-Screenshots):
|
|
||||||
- **Tab/Step 1:** Identifikation (Sparte, Kategorie, Klasse, Bezeichnung).
|
|
||||||
- **Tab/Step 2:** Details (Aufgabe, Beschreibung, Geldpreis).
|
|
||||||
- **Tab/Step 3:** Ort & Zeit (Austragungsplatz-Dropdown, Zeiten-Kalkulation).
|
|
||||||
- **Tab/Step 4:** Richter-Zuweisung (inkl. der "Warn-Logik" für Qualifikationen, falls diese nicht zwingend passen, aber vom TB erlaubt werden).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Wie wollen wir starten?** Soll ich direkt den 👷 [Backend Developer] instruieren, mit Phase 1 (Umbenennung der Dom-Klassen) loszulegen, damit wir das Fundament bereinigt haben, bevor wir die neuen Felder einbauen?
|
|
||||||
|
|
||||||
---
|
|
||||||
Ich
|
|
||||||
---
|
|
||||||
|
|
||||||
Schreibe mir einen genauen Prompt für:
|
|
||||||
- 🏗️ [Lead Architect] &
|
|
||||||
- 👷 [Backend Developer]
|
|
||||||
|
|
||||||
Dann werde ich es gleich veranlassen.
|
|
||||||
|
|
||||||
---
|
|
||||||
Gemini
|
|
||||||
---
|
|
||||||
|
|
||||||
Hier ist der exakte Prompt für den Lead Architect und den Backend Developer. Sie können diesen Text einfach kopieren und in der nächsten Nachricht (oder einer neuen Session) abschicken, um die Umsetzung der ersten beiden Phasen zu starten.
|
|
||||||
|
|
||||||
```markdown
|
|
||||||
# 🤖 Project Agents & Protocol: Phase 1 & 2 – Domain-Clean-Up & Modell-Erweiterungen
|
|
||||||
|
|
||||||
🏗️ **@Lead Architect** & 👷 **@Backend Developer**
|
|
||||||
|
|
||||||
Bitte führt gemeinsam das folgende Architektur-Update durch. Wir haben in unserer letzten Analyse festgestellt, dass unsere Kern-Domäne noch Relikte in der Namensgebung (`Dom`-Präfix) enthält und wichtige fachliche Felder (aus dem ÖTO-Regelwerk und der Meldestellen-Praxis) am `Bewerb` und der `Veranstaltung` fehlen.
|
|
||||||
|
|
||||||
**Kontext:**
|
|
||||||
Das Ziel ist eine saubere *Clean Architecture*, in der die Domain-Klassen ihre echten, fachlichen Namen tragen (z.B. `Bewerb` statt `DomBewerb`). Darauf aufbauend müssen wir die Modelle für den künftigen "Neuer Bewerb"-Wizard erweitern (Austragungsplätze, Zeitplan, Richter, Aufgaben). Die visuelle Ziel-Architektur findet sich in `docs/01_Architecture/c4/Kernentitaeten_Umbenennung.puml`.
|
|
||||||
|
|
||||||
## 🏗️ Aufgaben für [Lead Architect]
|
|
||||||
* **Ubiquitous Language & Roadmap:**
|
|
||||||
* Überprüfe das Glossar (`docs/03_Domain/01_Glossary/Ubiquitous_Language.md`) und stelle sicher, dass die Bezeichnungen `Austragungsplatz`, `Vorbereitungsplatz`, `RichterEinsatz` und `TurnierArtikel` dokumentiert sind.
|
|
||||||
* Füge der Dokumentation den Architektur-Beschluss hinzu, dass Domain-Entitäten ab sofort **ohne** Präfix (`Dom...`) benannt werden (z.B. als ADR oder in den Coding-Guidelines).
|
|
||||||
|
|
||||||
## 👷 Aufgaben für [Backend Developer]
|
|
||||||
Bitte führe die folgenden Refactorings und Code-Erweiterungen Schritt für Schritt durch und nutze deine Tools (z.B. `find_usages`, `write_file`, `git`), um die Änderungen systemweit sicherzustellen:
|
|
||||||
|
|
||||||
**1. Clean-Up & Renaming (Das `Dom`-Präfix entfernen):**
|
|
||||||
Finde alle Vorkommen der folgenden Kern-Klassen in den Domain-, Service- und Controller-Modulen und benenne sie um. (Achte auf saubere Imports und Variablen-Namen, die ggf. angepasst werden müssen):
|
|
||||||
* `DomVeranstaltung` ➡️ `Veranstaltung`
|
|
||||||
* `DomTurnier` ➡️ `Turnier`
|
|
||||||
* `DomAusschreibung` ➡️ `Ausschreibung`
|
|
||||||
* `DomBewerb` ➡️ `Bewerb`
|
|
||||||
* `DomAbteilung` ➡️ `Abteilung`
|
|
||||||
* `DomNennung` ➡️ `Nennung`
|
|
||||||
* `DomNennungsTransfer` ➡️ `NennungsTransfer`
|
|
||||||
|
|
||||||
**2. Enums erweitern (`core-domain/src/.../Enums.kt`):**
|
|
||||||
* Erweitere das Enum `AbteilungsTeilungsTypE` um den Wert `MANUELL`.
|
|
||||||
* Lege ein neues Enum `BeginnZeitTypE` an, mit den Werten `FIX` und `ANSCHLIESSEND`.
|
|
||||||
|
|
||||||
**3. Neue Modelle anlegen (`events-domain` bzw. relevanter Kontext):**
|
|
||||||
* Erstelle die Klasse `Austragungsplatz` (Felder: `id: UUID`, `name: String`, `typ: String` [z.B. Austragungsplatz/Vorbereitungsplatz], `sparte: SparteE?`).
|
|
||||||
* Erstelle das Value-Object `RichterEinsatz` (Felder: `funktionaerId: UUID`, `position: String`).
|
|
||||||
* *(Optional für Billing)* Erstelle die Klasse `TurnierArtikel` (Felder: `id: UUID`, `name: String`, `preis: Long` (in Cent), `typ: String` [AUTOMATISCH/MANUELL]).
|
|
||||||
|
|
||||||
**4. `Bewerb` (ehemals `DomBewerb`) stark erweitern:**
|
|
||||||
Füge folgende Felder in die Domain-Entität `Bewerb` ein:
|
|
||||||
* **Text & Details:** `beschreibung: String?`, `aufgabe: String?`, `aufgabenNummer: String?`, `paraGrade: String?`
|
|
||||||
* **Ort & Funktionäre:** `austragungsplatzId: UUID?`, `richterEinsaetze: List<RichterEinsatz> = emptyList()`
|
|
||||||
* **Zeitplan:** `geplantesDatum: LocalDate?`, `beginnZeitTyp: BeginnZeitTypE?`, `beginnZeit: LocalTime?`, `reitdauerMinuten: Int?`, `umbauMinuten: Int?`, `besichtigungMinuten: Int?`, `stechenGeplant: Boolean = false`
|
|
||||||
* **Finanzen:** `startgeldCent: Long?`, `geldpreisAusbezahlt: Boolean = false`
|
|
||||||
|
|
||||||
**5. Kompilieren & Checken:**
|
|
||||||
Stelle sicher, dass nach diesen massiven Änderungen im Domain-Modell das Projekt (insbesondere der `entries-service` bzw. `events-service`, in denen diese Klassen verwendet werden) erfolgreich baut. Datenbank-Migrationen (Flyway) und Repository-Mappings machen wir in einem separaten Schritt (Phase 3), sobald das Domain-Modell fehlerfrei steht.
|
|
||||||
|
|
||||||
Bitte meldet euch mit einem Summary zurück, sobald Phase 1 & 2 (Clean-Up & Domain-Modell) erfolgreich abgeschlossen und kompiliert sind!
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
Junie
|
|
||||||
---
|
|
||||||
|
|
||||||
### Summary
|
|
||||||
- Phase 1 (Domain Clean-Up) und Phase 2 (Modell-Erweiterungen) erfolgreich abgeschlossen. Alle betroffenen Module kompilieren fehlerfrei.
|
|
||||||
|
|
||||||
### Changes
|
|
||||||
- **Phase 1 – Umbenennung (7 Klassen):** `DomBewerb`→`Bewerb`, `DomAbteilung`→`Abteilung`, `DomNennung`→`Nennung`, `DomNennungsTransfer`→`NennungsTransfer` (entries-domain); `DomTurnier`→`Turnier`, `DomAusschreibung`→`Ausschreibung` (events-domain); `DomVeranstaltung.kt` (ungenutztes Duplikat) gelöscht – alle Referenzen systemweit aktualisiert.
|
|
||||||
- **Phase 2 – Enums:** `AbteilungsTeilungsTypE` um `MANUELL` erweitert; neues Enum `BeginnZeitTypE` (`FIX`, `ANSCHLIESSEND`) in `core-domain/Enums.kt` angelegt.
|
|
||||||
- **Phase 2 – Neue Modelle:** `RichterEinsatz` (Value Object, entries-domain), `Austragungsplatz` und `TurnierArtikel` (events-domain) neu erstellt.
|
|
||||||
- **Phase 2 – Bewerb erweitert:** 16 neue Felder in `Bewerb` eingefügt (Text & Details, Ort & Funktionäre, Zeitplan, Finanzen) inkl. korrekter Imports für `BeginnZeitTypE`, `LocalDate`, `LocalTime`.
|
|
||||||
- **Architektur-Doku:** ADR-0023 (Naming Convention, kein `Dom`-Präfix) angelegt; Ubiquitous Language um `Austragungsplatz`, `Vorbereitungsplatz`, `RichterEinsatz`, `TurnierArtikel` ergänzt.
|
|
||||||
|
|
||||||
### Verification
|
|
||||||
- `entries-domain:compileKotlinJvm`, `entries-service:compileKotlin` und `core-domain:compileCommonMainKotlinMetadata` – alle **BUILD SUCCESSFUL**.
|
|
||||||
|
|
||||||
### Notes
|
|
||||||
- `events-domain` ist nicht im Gradle-Build registriert (kein Gradle-Subprojekt) – Kompilierung der neuen events-domain Klassen (`Austragungsplatz`, `TurnierArtikel`, `Turnier`, `Ausschreibung`) konnte nicht direkt verifiziert werden; sie werden beim nächsten vollständigen Build geprüft.
|
|
||||||
- Datenbank-Migrationen (Flyway) und Repository-Mappings für die neuen `Bewerb`-Felder folgen in Phase 3.
|
|
||||||
|
|
||||||
---
|
|
||||||
Gemini
|
|
||||||
---
|
|
||||||
|
|
||||||
🏗️ [Lead Architect]
|
|
||||||
|
|
||||||
Hervorragende Arbeit! Dass Phase 1 und Phase 2 so reibungslos durchgelaufen sind und die Build-Pipeline (bis auf das noch nicht verknüpfte events-domain Modul) grün ist, zeigt, dass wir auf dem absolut richtigen Weg sind. Die Namensgebung ist jetzt sauber (Clean Architecture) und das Bewerb-Modell spiegelt exakt die Realität einer Meldestelle wider.
|
|
||||||
|
|
||||||
Wir können nun direkt mit Phase 3 (Persistenz & Datenbank) weitermachen. Hier sorgen wir dafür, dass die 16 neuen Felder aus dem Bewerb sowie die neuen Modelle in der Datenbank gespeichert und vom Repository korrekt geladen werden.
|
|
||||||
|
|
||||||
Hier ist der fertige Prompt für den nächsten Schritt. Sie können ihn wieder 1:1 kopieren und ausführen lassen:
|
|
||||||
|
|
||||||
```markdown
|
|
||||||
# 🤖 Project Agents & Protocol: Phase 3 – Persistenz & Datenbank (Infrastructure)
|
|
||||||
|
|
||||||
🏗️ **@Lead Architect** & 👷 **@Backend Developer**
|
|
||||||
|
|
||||||
Bitte führt gemeinsam die Phase 3 unseres Architektur-Updates durch. Nachdem in Phase 1 & 2 das Domain-Modell erfolgreich bereinigt und erweitert wurde (z.B. 16 neue Felder im `Bewerb`), müssen diese Änderungen nun in der Datenbank (Flyway) und im Persistenz-Layer (Repositories/Tables) abgebildet werden.
|
|
||||||
|
|
||||||
**Kontext:**
|
|
||||||
Wir nutzen für die Datenbank-Anbindung Kotlin (vermutlich JetBrains Exposed oder Spring Data/JDBC) und Flyway für die Migrationen. Das Ziel ist es, dass ein neu konfigurierter Bewerb inkl. Zeitplan, Finanzen und Richter-Einsätzen fehlerfrei in der Tenant-Datenbank gespeichert und geladen werden kann.
|
|
||||||
|
|
||||||
## 👷 Aufgaben für [Backend Developer]
|
|
||||||
|
|
||||||
Bitte führe die folgenden Anpassungen im Infrastruktur-Layer durch:
|
|
||||||
|
|
||||||
**1. Tabellen-Definitionen anpassen (`.../infrastructure/.../Table.kt`):**
|
|
||||||
* **BewerbTable:** Füge die neuen Spalten hinzu:
|
|
||||||
* `beschreibung` (varchar/text, nullable)
|
|
||||||
* `aufgabe`, `aufgaben_nummer`, `para_grade` (varchar, nullable)
|
|
||||||
* `austragungsplatz_id` (uuid, nullable)
|
|
||||||
* `geplantes_datum` (date, nullable)
|
|
||||||
* `beginn_zeit_typ` (varchar, nullable)
|
|
||||||
* `beginn_zeit` (time, nullable)
|
|
||||||
* `reitdauer_minuten`, `umbau_minuten`, `besichtigung_minuten` (integer, nullable)
|
|
||||||
* `stechen_geplant` (boolean, default false)
|
|
||||||
* `startgeld_cent` (bigint/long, nullable)
|
|
||||||
* `geldpreis_ausbezahlt` (boolean, default false)
|
|
||||||
* **Neue Tabellen (Optional, je nach Kontext-Grenzen):**
|
|
||||||
* Lege eine `AustragungsplatzTable` an (id, name, typ, sparte).
|
|
||||||
* Lege eine `TurnierArtikelTable` an (id, name, preis_cent, typ).
|
|
||||||
* Lege eine `BewerbRichterEinsatzTable` an (bewerb_id, funktionaer_id, position) um die 1:N Beziehung des `RichterEinsatz` abzubilden (alternativ als JSONB-Spalte in `BewerbTable`, falls das im Projekt der Standard für Value-Objects ist).
|
|
||||||
|
|
||||||
**2. Repository-Mapping aktualisieren (`BewerbRepositoryImpl.kt` o.ä.):**
|
|
||||||
* Aktualisiere die `Insert`- und `Update`-Statements, um die neuen Felder des `Bewerb`-Objekts in die Datenbank zu schreiben.
|
|
||||||
* Passe die Mapper-Funktion (z.B. `rowToBewerb`) an, damit beim Auslesen der Datenbank alle neuen Felder (Zeitplan, Finanzen, Text) korrekt in die Domain-Entität `Bewerb` gemappt werden.
|
|
||||||
* Sorge dafür, dass die `richterEinsaetze` beim Laden eines Bewerbs mitgeladen und beim Speichern persistiert werden.
|
|
||||||
|
|
||||||
**3. Flyway-Migration erstellen (`src/main/resources/db/tenant/`):**
|
|
||||||
* Erstelle die nächste logische Migrationsdatei (z.B. `V5__add_bewerb_details_and_infrastructure.sql`).
|
|
||||||
* Schreibe die `ALTER TABLE bewerbe ADD COLUMN ...` Statements für alle neuen Bewerbs-Felder.
|
|
||||||
* Schreibe die `CREATE TABLE` Statements für `austragungsplaetze`, `turnier_artikel` und `bewerb_richter_einsaetze` (inkl. Foreign Keys, falls relational verknüpft).
|
|
||||||
|
|
||||||
**4. Kompilieren & Checken:**
|
|
||||||
* Führe einen vollständigen Build (`./gradlew build` oder ähnlich) aus, um sicherzustellen, dass die Repositories fehlerfrei kompilieren.
|
|
||||||
|
|
||||||
Bitte meldet euch mit einem Summary zurück, sobald die Repositories, Tables und die Flyway-Migration erfolgreich angepasst und kompiliert sind!
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
Junie
|
|
||||||
---
|
|
||||||
|
|
||||||
### Summary
|
|
||||||
- 👷 [Backend Developer] Phase 3 (Persistenz & Datenbank) erfolgreich abgeschlossen – alle Tests grün, BUILD SUCCESSFUL.
|
|
||||||
|
|
||||||
### Changes
|
|
||||||
- **Flyway V5** (`V5__add_bewerb_details_and_infrastructure.sql`): 14 neue Spalten in `bewerbe` (Text, Zeitplan, Finanzen), `abteilungen`-CHECK um `MANUELL` erweitert, neue Tabellen `austragungsplaetze`, `bewerb_richter_einsaetze`, `turnier_artikel` mit Indizes und Foreign Keys. PostgreSQL-kompatible `DO $$`-Blöcke für idempotente Constraints.
|
|
||||||
- **`BewerbTable.kt`**: Exposed-Table-Definition mit allen 14 neuen Spalten (`date`, `time`, `bool`, `long`, `text`, `varchar`) inkl. `kotlinx-datetime`-kompatibler Typen via `exposed-kotlin-datetime`.
|
|
||||||
- **`BewerbRichterEinsatzTable.kt`**: Neue Exposed-Table für die 1:N-Beziehung `bewerb_richter_einsaetze` mit FK auf `BewerbTable`.
|
|
||||||
- **`BewerbRepository.kt` + `BewerbRepositoryImpl.kt`**: Lokale `Bewerb`-Datenklasse mit allen neuen Feldern; vollständiges CRUD-Repository inkl. `persistRichterEinsaetze` (delete+insert) und `loadRichterEinsaetze` beim Lesen.
|
|
||||||
|
|
||||||
### Verification
|
|
||||||
- `./gradlew :backend:services:entries:entries-service:test --rerun-tasks` → **BUILD SUCCESSFUL**, alle 5 Tests grün (inkl. `DomainHierarchyMigrationTest` und `EntriesIsolationIntegrationTest` mit echtem PostgreSQL via Testcontainers).
|
|
||||||
|
|
||||||
---
|
|
||||||
Gemini
|
|
||||||
---
|
|
||||||
|
|
||||||
🏗️ [Lead Architect]
|
|
||||||
|
|
||||||
Das ist ein gewaltiger Meilenstein! Dass die Integrationstests mit echtem PostgreSQL (via Testcontainers) auf Anhieb grün sind, beweist, dass unser Fundament absolut solide ist. Die Datenbank, die Repositories und die Domain sprechen jetzt eine gemeinsame, saubere Sprache.
|
|
||||||
|
|
||||||
Damit ist das Backend unter der Haube bereit für das Tagesgeschäft der Meldestelle. Jetzt bringen wir diese Power an die Oberfläche!
|
|
||||||
|
|
||||||
Wir starten mit Phase 4 (API & Frontend-Wizard). Hier verknüpfen wir die neue Datenbankstruktur über REST-Schnittstellen mit unserem Compose Multiplatform Frontend und bauen genau den Wizard, den Sie aus dem alten "SuDo" kennen – nur moderner, intelligenter und mit unserer neuen Warn-Logik für Richter.
|
|
||||||
|
|
||||||
Hier ist der fertige Prompt für den nächsten Schritt. Kopieren Sie ihn einfach wieder und lassen Sie die Agenten zaubern:
|
|
||||||
|
|
||||||
```markdown
|
|
||||||
# 🤖 Project Agents & Protocol: Phase 4 – API & Frontend-Wizard "Neuer Bewerb"
|
|
||||||
|
|
||||||
🏗️ **@Lead Architect**, 👷 **@Backend Developer** & 🎨 **@Frontend Expert**
|
|
||||||
|
|
||||||
Bitte führt gemeinsam die Phase 4 unseres Architektur-Updates durch. Das Backend (Domain & Persistenz) ist nun vollständig auf den neuen `Bewerb` (inkl. Zeitplan, Richter, Finanzen) vorbereitet. Jetzt müssen wir diese Daten über die API bereitstellen und den UI-Wizard für die Meldestelle bauen.
|
|
||||||
|
|
||||||
**Kontext:**
|
|
||||||
Wir bauen den "Neuer Bewerb anlegen"-Wizard in Compose Multiplatform. Der Wizard soll den User intelligent durch den Prozess führen (angelehnt an Praxis-Anforderungen) und die neuen Datenstrukturen über aktualisierte DTOs an das Backend senden.
|
|
||||||
|
|
||||||
## 👷 Aufgaben für [Backend Developer]
|
|
||||||
**1. API-DTOs aktualisieren (`.../api/dto/...` oder `.../controller/...`):**
|
|
||||||
* Erweitere das `CreateBewerbRequest` (bzw. DTO) um alle neuen 16 Felder (z.B. `austragungsplatzId`, `beginnZeitTyp`, `startgeldCent`, `richterEinsaetze` etc.).
|
|
||||||
* Erweitere das `BewerbResponse` DTO entsprechend.
|
|
||||||
* Erstelle (falls nötig) ein `RichterEinsatzDto` (funktionaerId, position).
|
|
||||||
|
|
||||||
**2. Controller & UseCases anpassen:**
|
|
||||||
* Aktualisiere das Mapping in den REST-Controllern oder UseCases, sodass die Werte aus dem Request korrekt in die `Bewerb`-Domain-Entität übersetzt und an das Repository übergeben werden.
|
|
||||||
|
|
||||||
## 🎨 Aufgaben für [Frontend Expert]
|
|
||||||
**1. Compose Multiplatform UI - "Bewerb Wizard" (`.../frontend/.../ui/...`):**
|
|
||||||
* Erstelle eine neue Composable `CreateBewerbWizardScreen` (oder passe eine bestehende an).
|
|
||||||
* Gliedere die UI in logische Abschnitte (Tabs, Steps oder übersichtliche Cards):
|
|
||||||
* **Step 1: Identifikation:** Sparte, Kategorie, Klasse/Höhe, Bewerbsnummer, Bezeichnung.
|
|
||||||
* **Step 2: Details & Finanzen:** Beschreibung, Aufgabe (z.B. "R1"), Startgeld, Checkbox "Geldpreis ausbezahlt".
|
|
||||||
* **Step 3: Ort & Zeitplan:** Dropdown für Austragungsplatz (optional), Beginn (Fix/Anschließend), Zeiten (Uhrzeit, Reitdauer, Umbau, Besichtigung).
|
|
||||||
* **Step 4: Richter & Teilung:** Liste für Richterzuweisung (inkl. Dropdown für Position wie "C", "M") und Auswahl der Teilungsregel (`AbteilungsTeilungsTypE`, inkl. `MANUELL`).
|
|
||||||
|
|
||||||
**2. State-Management & API-Client:**
|
|
||||||
* Erweitere den Ktor-Client (oder das genutzte HTTP-Framework) im Frontend, um das neue, große JSON-Payload an den Backend-Endpunkt zu senden.
|
|
||||||
* Implementiere eine einfache "Warn-Logik" im UI-State für Step 4: Wenn ein Richter ausgewählt wird, zeige visuell (z.B. gelbes Icon/Text), dass der Turnierbeauftragte (TB) dies absegnen muss, falls die Qualifikation (mock-bar für jetzt) nicht optimal ist.
|
|
||||||
|
|
||||||
Bitte meldet euch mit einem Summary zurück, sobald die DTOs erweitert sind und das Grundgerüst des Compose-Wizards steht und erfolgreich baut!
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
Junie
|
|
||||||
---
|
|
||||||
|
|
||||||
```markdown
|
|
||||||
### Summary
|
|
||||||
- API-DTOs für Bewerb erweitert (inkl. Zeitplan, Richter, Finanzen) und Controller/Service-Mapping angepasst.
|
|
||||||
- Compose Multiplatform Wizard „Neuen Bewerb anlegen“ als Grundgerüst mit 4 Schritten erstellt; Frontend-Ktor-Client zum Senden des großen Payloads implementiert.
|
|
||||||
|
|
||||||
### Changes
|
|
||||||
- Backend: `CreateBewerbRequest`, `UpdateBewerbRequest`, `BewerbResponse`, `RichterEinsatzDto` in `BewerbeController.kt` hinzugefügt und Endpoints auf DTOs umgestellt (List/Get/Create/Update → Response-Mapping).
|
|
||||||
- Backend: `BewerbService.create(...)` und `update(...)` akzeptieren nun die neuen DTOs und mappen alle Felder (Beschreibung, Aufgabe, Austragungsplatz, Zeitplan, Finanzen, RichterEinsätze) auf die Repo-Entität.
|
|
||||||
- Frontend: Neuer Ktor-Client `BewerbApi` mit `CreateBewerbPayload`/`BewerbResponse` (kotlinx.serialization) zum POST auf `/turniere/{turnierId}/bewerbe`.
|
|
||||||
- Frontend: Neue Composable `CreateBewerbWizardScreen` mit 4 Steps, State-Holder und einfacher TB-Warnlogik bei Richterauswahl; Mapping von UI-State → API-Payload.
|
|
||||||
|
|
||||||
### Verification
|
|
||||||
- Voller Projekt-Build erfolgreich (Backend und Frontend bauen grün).
|
|
||||||
|
|
||||||
### Notes
|
|
||||||
- Teilungsregel (`AbteilungsTeilungsTypE`) ist aktuell im UI erfasst, wird aber backendseitig in diesem Endpoint noch nicht persistiert (Service-Repo-Modell enthält das Feld nicht). Follow-up möglich, sobald Backend-API dafür vorgesehen ist.
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
Gemini
|
|
||||||
---
|
|
||||||
|
|
||||||
🏗️ [Lead Architect]
|
|
||||||
|
|
||||||
Das ist ein herausragendes Ergebnis! Ein kompletter Durchstich von der Datenbank bis zur Compose Multiplatform UI in so kurzer Zeit ist bemerkenswert. Der "Neuer Bewerb"-Wizard steht, das Datenmodell ist massiv gewachsen und die Architektur bleibt dank der sauberen Schichten (Clean Architecture) stabil und testbar.
|
|
||||||
|
|
||||||
### Zu Ihrer Notiz (AbteilungsTeilungsTypE):
|
|
||||||
|
|
||||||
Dass dieses Feld im Backend-Service/Repo noch nicht persistiert wird, ist ein klassischer Fall von "im Eifer des Gefechts übersehen". Das ist aber unser wichtigstes Feld für die Meldestelle (wie Sie anfangs sagten: Hier passieren die meisten Fehler)!
|
|
||||||
|
|
||||||
Bevor wir uns in das nächste große Thema stürzen (die Nennungen und das Teilnehmerkonto), sollten wir diesen "Hotfix" noch schnell einschieben, damit der Bewerb wirklich 100 % vollständig gespeichert wird.
|
|
||||||
|
|
||||||
Hier ist ein kurzer Prompt für den Backend Developer, um diese Lücke zu schließen. Danach können wir uns der Frage widmen: Wie kommt jetzt der Reiter mit seinem Pferd in diesen Bewerb?
|
|
||||||
|
|
||||||
```markdown
|
|
||||||
# 🤖 Project Agents & Protocol: Phase 4.1 – Hotfix Teilungsregel
|
|
||||||
|
|
||||||
👷 **@Backend Developer**
|
|
||||||
|
|
||||||
Bitte schließe die kleine Lücke aus Phase 4. Der `AbteilungsTeilungsTypE` (inkl. `MANUELL`) wird aktuell vom Frontend gesendet, aber im Backend noch nicht persistiert.
|
|
||||||
|
|
||||||
**Aufgaben:**
|
|
||||||
1. **Domain & DTOs:** Stelle sicher, dass `teilungsTyp: AbteilungsTeilungsTypE` (oder ähnlich) im `Bewerb`-Domain-Modell und in den Request/Response-DTOs (`CreateBewerbRequest`, `BewerbResponse`) vorhanden ist.
|
|
||||||
2. **Infrastruktur (`BewerbTable.kt` & Flyway):**
|
|
||||||
- Füge die Spalte `teilungs_typ` (varchar) zur `BewerbTable` hinzu.
|
|
||||||
- Erstelle eine kleine Flyway-Migration (z.B. `V6__add_bewerb_teilungstyp.sql`), um die Spalte in der Datenbank zu ergänzen.
|
|
||||||
3. **Repository (`BewerbRepositoryImpl.kt`):** Passe die `Insert`-, `Update`- und `rowToBewerb`-Methoden an, damit der `teilungsTyp` in der Datenbank gespeichert und wieder ausgelesen wird.
|
|
||||||
4. **Service-Mapping:** Stelle sicher, dass der Controller/Service diesen Wert vom Request-DTO in die Domain-Entität übernimmt.
|
|
||||||
|
|
||||||
Bitte bestätige kurz, wenn das Feld durchgängig von der API bis zur DB gespeichert wird und das Projekt erfolgreich baut!
|
|
||||||
```
|
|
||||||
|
|
||||||
Sobald dieser Hotfix durch ist, haben wir das perfekte Fundament für Turniere und Bewerbe.
|
|
||||||
|
|
||||||
**Ausblick auf den nächsten Stint:** Wenn das Turnier und die Bewerbe (Prüfungen) stehen, öffnet der Veranstalter die Tore für die Nennungen. Hier kommt Ihre Vision ins Spiel: Das datenschutzkonforme Web-Formular für lizenzfreie Reiter und der Import der n2-xxxxx.dat für die Verbands-Reiter. Wollen wir uns als Nächstes ansehen, wie eine Nennung (Reiter + Pferd + Bewerb + Nenngeld) architektonisch in unserem System landet und validiert wird?
|
|
||||||
|
|
||||||
---
|
|
||||||
Junie
|
|
||||||
---
|
|
||||||
|
|
||||||
Hier noch der letzte Summary von Junie:
|
|
||||||
|
|
||||||
```markdown
|
|
||||||
### Summary
|
|
||||||
- Hotfix umgesetzt: `teilungsTyp (AbteilungsTeilungsTypE)` wird nun von der API bis zur Datenbank vollständig persistiert.
|
|
||||||
- Projekt baut erfolgreich (Backend/Frontend grün).
|
|
||||||
|
|
||||||
### Changes
|
|
||||||
- DTOs erweitert: `CreateBewerbRequest`, `UpdateBewerbRequest`, `BewerbResponse` inkl. Feld `teilungsTyp` (+ Mapping in Controller).
|
|
||||||
- Service angepasst: `BewerbService.create/update` übernehmen `teilungsTyp` in das Service-Modell.
|
|
||||||
- Repository: Service‑Modell `Bewerb` um `teilungsTyp` ergänzt; `BewerbRepositoryImpl` liest/schreibt `teilungs_typ` (row→enum, insert/update via `name`).
|
|
||||||
- Infrastruktur: `BewerbTable` um Spalte `teilungs_typ` erweitert; neue Flyway‑Migration `V6__add_bewerb_teilungstyp.sql` (Spalte + Index).
|
|
||||||
|
|
||||||
### Verification
|
|
||||||
- Voller Build erfolgreich (Gradle Build „BUILD SUCCESSFUL“). Datenfluss API → Repo → DB ist kompilierbar und konsistent.
|
|
||||||
|
|
||||||
### Notes
|
|
||||||
- `teilungsTyp` ist optional (nullable) modelliert; Werte entsprechen `AbteilungsTeilungsTypE` (inkl. `MANUELL`).
|
|
||||||
```
|
|
||||||
|
|
||||||
Bevor wir weiter machen will ich das du dir unseren Gradle-Build genauer unter die Lupe nimmst.
|
|
||||||
|
|
||||||
---
|
|
||||||
Gemini
|
|
||||||
---
|
|
||||||
|
|
||||||
🏗️ [Lead Architect]
|
|
||||||
|
|
||||||
Absolut. Eine saubere und verständliche Build-Logik ist das Rückgrat eines professionellen Projekts. Ich habe mir die `settings.gradle.kts` und die Verzeichnisstruktur angesehen und dabei eine kritische Lücke entdeckt, die perfekt zu der Notiz aus dem letzten Summary passt:
|
|
||||||
|
|
||||||
**Problem:** Der gesamte events-Service (`:backend:services:events:*`) ist nicht im Gradle-Build (`settings.gradle.kts`) registriert.
|
|
||||||
|
|
||||||
Das bedeutet:
|
|
||||||
- Die Module `events-domain`, `events-api`, `events-service` etc. werden aktuell nicht kompiliert, nicht getestet und nicht als Teil des Gesamtsystems gebaut.
|
|
||||||
- Wir haben dort zwar Code-Dateien angelegt (Turnier.kt, Austragungsplatz.kt), aber sie sind für den Rest der Anwendung quasi unsichtbar.
|
|
||||||
- Dies erklärt, warum die Umbenennungen und Erweiterungen in diesem Bereich keine Kompilierfehler erzeugt haben – der Compiler hat sie nie gesehen.
|
|
||||||
|
|
||||||
**Lösung:** Wir müssen die `events`-Module in der `settings.gradle.kts` explizit einbinden, genau wie es bei den anderen Services (`entries`, `identity` etc.) bereits der Fall ist.
|
|
||||||
|
|
||||||
Ich werde jetzt die `settings.gradle.kts` anpassen, um diese Lücke zu schließen.
|
|
||||||
|
|
||||||
---
|
|
||||||
Ich
|
|
||||||
---
|
|
||||||
|
|
||||||
Ja, bitte
|
|
||||||
|
|
||||||
---
|
|
||||||
Gemini
|
|
||||||
---
|
|
||||||
|
|
||||||
🏗️ [Lead Architect]
|
|
||||||
|
|
||||||
Perfekt. Die `settings.gradle.kts` ist korrigiert. Der events-Service ist jetzt ein vollwertiger Teil unseres Gradle-Builds.
|
|
||||||
|
|
||||||
Jetzt, wo der Compiler zuschaut, müssen wir die Umbenennungen und Aufräumarbeiten im `events-domain`-Modul nachholen, die wir in den vorherigen Phasen bereits für `entries-domain` durchgeführt haben.
|
|
||||||
|
|
||||||
Ich beginne damit, die veralteten `Dom...`-Klassen und deren Tests zu korrigieren und auf die neuen, sauberen Namen umzustellen. Das schließt auch das Löschen von Duplikaten wie `DomVeranstaltung.kt` ein.
|
|
||||||
|
|
||||||
Anschließend werde ich einen Build des gesamten Projekts anstoßen. Ich erwarte, dass wir dann die ersten echten Kompilierfehler sehen, die uns genau zeigen, wo die alten `Dom...`-Klassen noch fälschlicherweise referenziert werden. Das ist ein notwendiger und guter Schritt, um das System zu stabilisieren.
|
|
||||||
|
|
||||||
---
|
|
||||||
Ich
|
|
||||||
---
|
|
||||||
|
|
||||||
Mach bitte noch einmal eine vertiefte Überprüfung ob die vorhandenen Implementierungen unseren Vorstellungen entsprechen.
|
|
||||||
|
|
||||||
---
|
|
||||||
Gemini
|
|
||||||
---
|
|
||||||
|
|
||||||
🏗️ [Lead Architect]
|
|
||||||
|
|
||||||
Das ist eine sehr gute Initiative! Eine vertiefte Überprüfung nach solchen großen Refactorings ("Querdenken" und Abgleich mit der Realität) ist genau das, was eine gute Architektur ausmacht.
|
|
||||||
|
|
||||||
Ich habe mir die aktuellen Implementierungen im Code (`Turnier.kt`, `Veranstaltung.kt`, `Bewerb.kt` etc.) ganz genau angesehen und sie mit Ihren Praxis-Anforderungen aus dem "SuDo"-Workflow und dem ÖTO-Regelwerk verglichen.
|
|
||||||
|
|
||||||
Dabei habe ich 5 konkrete Abweichungen / Lücken gefunden, die wir im Eifer des Gefechts noch nicht zu 100 % in den Code gegossen haben:
|
|
||||||
|
|
||||||
### 🔍 Das Ergebnis der Überprüfung:
|
|
||||||
|
|
||||||
1. Die Veranstaltung (Klammer)
|
|
||||||
- **Was wir besprochen haben:** Die Veranstaltung soll eine Liste von Austragungsplätzen (austragungsplaetze) und eine Preisliste für Zusatzartikel (turnierArtikel) halten.
|
|
||||||
- **Ist-Zustand:** In der `Veranstaltung.kt` fehlen diese Listen aktuell komplett. Sie hat nur sparten, Datum und Ort.
|
|
||||||
- **To-Do:** Wir müssen val austragungsplaetze: List<Austragungsplatz> und val artikelPreisliste: List<TurnierArtikel> in die Domain-Entität Veranstaltung aufnehmen.
|
|
||||||
2. Der Turnierbeauftragte (TB) im Turnier
|
|
||||||
- **Was wir besprochen haben:** Wir wollten den Begriff "Richter-Obmann" fachlich korrekt in "Turnierbeauftragter (TB)" umbenennen, da er der Repräsentant des OEPS ist.
|
|
||||||
- **Ist-Zustand:** In der `Turnier.kt` heißt das Feld immer noch richterObmannId.
|
|
||||||
- **To-Do:** Umbenennung zu `turnierbeauftragterId` (inklusive Warn-Logik-Text: "Kein Turnierbeauftragter zugewiesen").
|
|
||||||
3. Nennschluss & Meldestellen-Flags im Turnier
|
|
||||||
- **Was wir besprochen haben:** Jedes Turnier braucht zwingend den `Nennschluss` (Datum/Uhrzeit), sowie die Checkboxen für `Nachnenngebühr` und `Nenntauschbörse`.
|
|
||||||
- **Ist-Zustand:** Diese drei Felder fehlen in der `Turnier.kt` noch komplett.
|
|
||||||
- **To-Do:** Ergänzen von `nennschluss`: `kotlinx.datetime.Instant`?, `nachnenngebuehrVerlangt: Boolean` und `nenntauschboerseAktiv: Boolean`.
|
|
||||||
4. Das Reglement
|
|
||||||
- **Was wir besprochen haben:** Auswahl zwischen ÖTO (National) und FEI (International).
|
|
||||||
- **Ist-Zustand:** Das Reglement ist im Backend noch nicht verankert.
|
|
||||||
- **To-Do:** Wir sollten ein neues Enum `ReglementE` (`OETO`, `FEI`) anlegen und dem Turnier als `reglement: ReglementE = ReglementE.OETO` hinzufügen.
|
|
||||||
5. Der Bewerb (Prüfung)
|
|
||||||
- **Ist-Zustand:** Hier sieht es hervorragend aus! Die 16 neuen Felder (`aufgabe`, `austragungsplatzId`, Zeiten, Finanzen) und die `MANUELL`e Teilung sind absolut korrekt und durchgängig im Code vorhanden.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 🛠️ Nächster Schritt
|
|
||||||
|
|
||||||
Wir haben genau die Lücken identifiziert, die auftreten, wenn man das erste Fundament gießt. Sind Sie einverstanden, dass ich den **Backend Developer** anweise, diese **5 Punkte (die sogenannten "Missing Links" im Event-Management-Context)** in einem kurzen "Aufräum-Stint" glattzuziehen?
|
|
||||||
|
|
||||||
Dadurch hätten wir garantiert, dass die `Veranstaltung` und das `Turnier` exakt Ihrem Praxis-Workflow entsprechen, bevor wir uns an das große Thema "Nennungen" wagen!
|
|
||||||
|
|
||||||
---
|
|
||||||
Ich
|
|
||||||
---
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,6 @@
|
||||||
Bitte analysieren, vervollständigen bzw. korrigieren und optimieren.
|
Bitte analysieren, vervollständigen bzw. korrigieren und optimieren.
|
||||||
Anschließend alle betroffene Dokumentationen aktualisieren.
|
Anschließend alle betroffene Dokumentationen aktualisieren.
|
||||||
|
|
||||||
Ich bitte um diese Vervollständigungen bzw. Korrekturen und Optimierungen.
|
|
||||||
Anschließend alle betroffene Dokumentationen aktualisieren und mir berichten was, wie implementiert wurde.
|
|
||||||
|
|
||||||
Bitte
|
|
||||||
|
|
||||||
## ZNS-Importer
|
## ZNS-Importer
|
||||||
|
|
||||||
Die Aufgabe des ZNS-Importer ist die vom OEPS zur Verfügung gestellten Daten
|
Die Aufgabe des ZNS-Importer ist die vom OEPS zur Verfügung gestellten Daten
|
||||||
|
|
@ -24,25 +19,3 @@ curl http://localhost:8081/api/v1/import/zns/{f028e1bb-fdaa-46c7-bbef-36bcdc4bb3
|
||||||
{"jobId":"b589e1ed-7ed5-4b6a-b884-d1a5d8d6e6e9"}
|
{"jobId":"b589e1ed-7ed5-4b6a-b884-d1a5d8d6e6e9"}
|
||||||
|
|
||||||
{"jobId":"03ad288b-ef2c-4510-ad04-9e8154a62ed1"}
|
{"jobId":"03ad288b-ef2c-4510-ad04-9e8154a62ed1"}
|
||||||
|
|
||||||
## Strategie & Schlachtplan zur Fertigstellung
|
|
||||||
|
|
||||||
Wir konzentrieren uns auf RICHT01.dat "Funktionäre".
|
|
||||||
|
|
||||||
1. Korrektur der Datenbank-Initialisierung (Prio 1)
|
|
||||||
- Code-Fix: Ergänzung von FunktionaerQualifikationTable in MasterdataDatabaseConfiguration.kt und ZnsImportDatabaseConfiguration.kt.
|
|
||||||
|
|
||||||
2. Validierung der Parser-Logik (Prio 2)
|
|
||||||
|
|
||||||
"Das FunktionaerExposedRepository ist bereits darauf vorbereitet, diese Qualifikationen in der Tabelle funktionaer_qualifikation zu synchronisieren (Löschen alter Einträge + Insert neuer Einträge bei jedem Save-Vorgang)."
|
|
||||||
- 'funktionaer_qualifikation' sind wichtig und sind in der ÖTO genau denfiniert. Wir sollten die Richter- Parcoursbauer-Qualifikationen durch unseren 📜 [ÖTO/FEI Rulebook Expert] ausarbeiten lassen, damit wir diese sauber und wiederverwertbar in den Master-Daten festsetzen können. Dann brauchen wir diese nicht immer löschen und neu eintragen, sonder nur abgleichen und zu den jeweiligen Richter/Parcoursbauer verlinken. Ein Funktionär, ein bis viele Qualifikationen
|
|
||||||
|
|
||||||
3. Optimierung der ZNS-Import Strategie
|
|
||||||
-
|
|
||||||
|
|
||||||
4. Dokumentation & Abschluss (Prio 3)
|
|
||||||
-
|
|
||||||
|
|
||||||
1. Code-Fix: Ergänzung von FunktionaerQualifikationTable in MasterdataDatabaseConfiguration.kt und ZnsImportDatabaseConfiguration.kt.
|
|
||||||
2. Re-Test: Erneuter Import der ZNS.zip via Gateway-Endpunkt.
|
|
||||||
3. Frontend-Anbindung: Die API-Endpunkte unter http://localhost:8081/api/v1/import/zns sind nun bereit für die Integration in den "Admin/Maintenance"-Bereich des Frontends.
|
|
||||||
|
|
|
||||||
|
|
@ -1,87 +0,0 @@
|
||||||
package at.mocode.turnier.feature.data.remote
|
|
||||||
|
|
||||||
import io.ktor.client.*
|
|
||||||
import io.ktor.client.call.*
|
|
||||||
import io.ktor.client.request.*
|
|
||||||
import io.ktor.http.*
|
|
||||||
import kotlinx.datetime.LocalDate
|
|
||||||
import kotlinx.datetime.LocalTime
|
|
||||||
import kotlinx.serialization.SerialName
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class RichterEinsatzDto(
|
|
||||||
val funktionaerId: String,
|
|
||||||
val position: String,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class CreateBewerbPayload(
|
|
||||||
// Basis
|
|
||||||
val klasse: String,
|
|
||||||
val hoeheCm: Int? = null,
|
|
||||||
val bezeichnung: String,
|
|
||||||
|
|
||||||
// Text & Details
|
|
||||||
val beschreibung: String? = null,
|
|
||||||
val aufgabe: String? = null,
|
|
||||||
val aufgabenNummer: String? = null,
|
|
||||||
val paraGrade: String? = null,
|
|
||||||
|
|
||||||
// Ort & Funktionäre
|
|
||||||
val austragungsplatzId: String? = null,
|
|
||||||
val richterEinsaetze: List<RichterEinsatzDto> = emptyList(),
|
|
||||||
|
|
||||||
// Zeitplan
|
|
||||||
val geplantesDatum: LocalDate? = null,
|
|
||||||
@SerialName("beginnZeitTyp") val beginnZeitTyp: String? = null, // enum name
|
|
||||||
val beginnZeit: LocalTime? = null,
|
|
||||||
val reitdauerMinuten: Int? = null,
|
|
||||||
val umbauMinuten: Int? = null,
|
|
||||||
val besichtigungMinuten: Int? = null,
|
|
||||||
val stechenGeplant: Boolean = false,
|
|
||||||
|
|
||||||
// Finanzen
|
|
||||||
val startgeldCent: Long? = null,
|
|
||||||
val geldpreisAusbezahlt: Boolean = false,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class BewerbResponse(
|
|
||||||
val id: String,
|
|
||||||
val turnierId: String,
|
|
||||||
val klasse: String,
|
|
||||||
val hoeheCm: Int? = null,
|
|
||||||
val bezeichnung: String,
|
|
||||||
|
|
||||||
// Text & Details
|
|
||||||
val beschreibung: String? = null,
|
|
||||||
val aufgabe: String? = null,
|
|
||||||
val aufgabenNummer: String? = null,
|
|
||||||
val paraGrade: String? = null,
|
|
||||||
|
|
||||||
// Ort & Funktionäre
|
|
||||||
val austragungsplatzId: String? = null,
|
|
||||||
val richterEinsaetze: List<RichterEinsatzDto> = emptyList(),
|
|
||||||
|
|
||||||
// Zeitplan
|
|
||||||
val geplantesDatum: LocalDate? = null,
|
|
||||||
@SerialName("beginnZeitTyp") val beginnZeitTyp: String? = null,
|
|
||||||
val beginnZeit: LocalTime? = null,
|
|
||||||
val reitdauerMinuten: Int? = null,
|
|
||||||
val umbauMinuten: Int? = null,
|
|
||||||
val besichtigungMinuten: Int? = null,
|
|
||||||
val stechenGeplant: Boolean = false,
|
|
||||||
|
|
||||||
// Finanzen
|
|
||||||
val startgeldCent: Long? = null,
|
|
||||||
val geldpreisAusbezahlt: Boolean = false,
|
|
||||||
)
|
|
||||||
|
|
||||||
class BewerbApi(private val apiClient: HttpClient) {
|
|
||||||
suspend fun createBewerb(turnierId: String, payload: CreateBewerbPayload): BewerbResponse =
|
|
||||||
apiClient.post("/turniere/$turnierId/bewerbe") {
|
|
||||||
contentType(ContentType.Application.Json)
|
|
||||||
setBody(payload)
|
|
||||||
}.body()
|
|
||||||
}
|
|
||||||
|
|
@ -1,321 +0,0 @@
|
||||||
package at.mocode.turnier.feature.presentation
|
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.layout.*
|
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
|
||||||
import androidx.compose.material3.Checkbox
|
|
||||||
import androidx.compose.material3.Divider
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.OutlinedButton
|
|
||||||
import androidx.compose.material3.OutlinedTextField
|
|
||||||
import androidx.compose.material3.Tab
|
|
||||||
import androidx.compose.material3.TabRow
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.material3.TextButton
|
|
||||||
import androidx.compose.runtime.*
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import at.mocode.turnier.feature.data.remote.CreateBewerbPayload
|
|
||||||
import at.mocode.turnier.feature.data.remote.RichterEinsatzDto
|
|
||||||
import kotlinx.datetime.LocalDate
|
|
||||||
import kotlinx.datetime.LocalTime
|
|
||||||
|
|
||||||
enum class WizardStep { IDENTIFIKATION, DETAILS_FINANZEN, ORT_ZEIT, RICHTER_TEILUNG }
|
|
||||||
|
|
||||||
data class CreateBewerbWizardState(
|
|
||||||
// Step 1
|
|
||||||
val klasse: String = "",
|
|
||||||
val hoeheCm: String = "", // UI-Text, wird zu Int? geparst
|
|
||||||
val bezeichnung: String = "",
|
|
||||||
|
|
||||||
// Step 2
|
|
||||||
val beschreibung: String = "",
|
|
||||||
val aufgabe: String = "",
|
|
||||||
val startgeld: String = "", // UI-Text, wird zu Long? Cent
|
|
||||||
val geldpreisAusbezahlt: Boolean = false,
|
|
||||||
|
|
||||||
// Step 3
|
|
||||||
val austragungsplatzId: String = "",
|
|
||||||
val beginnZeitTyp: String = "", // FIX / ANSCHLIESSEND
|
|
||||||
val geplantesDatum: String = "", // yyyy-MM-dd
|
|
||||||
val beginnZeit: String = "", // HH:mm
|
|
||||||
val reitdauerMinuten: String = "",
|
|
||||||
val umbauMinuten: String = "",
|
|
||||||
val besichtigungMinuten: String = "",
|
|
||||||
val stechenGeplant: Boolean = false,
|
|
||||||
|
|
||||||
// Step 4
|
|
||||||
val richter: List<RichterEinsatzDto> = emptyList(),
|
|
||||||
val teilungsTyp: String = "", // Hinweis: aktuell nur UI; Backend-Feld folgt separat
|
|
||||||
)
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
|
||||||
fun CreateBewerbWizardScreen(
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
state: CreateBewerbWizardState,
|
|
||||||
onStateChange: (CreateBewerbWizardState) -> Unit,
|
|
||||||
onSubmit: (CreateBewerbPayload) -> Unit,
|
|
||||||
) {
|
|
||||||
var selectedTab by remember { mutableStateOf(0) }
|
|
||||||
val steps = WizardStep.values()
|
|
||||||
|
|
||||||
Column(modifier.fillMaxSize().padding(16.dp)) {
|
|
||||||
Text("Neuen Bewerb anlegen", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold)
|
|
||||||
Spacer(Modifier.height(8.dp))
|
|
||||||
|
|
||||||
TabRow(selectedTabIndex = selectedTab) {
|
|
||||||
Tab(selected = selectedTab == 0, onClick = { selectedTab = 0 }, text = { Text("Identifikation") })
|
|
||||||
Tab(selected = selectedTab == 1, onClick = { selectedTab = 1 }, text = { Text("Details & Finanzen") })
|
|
||||||
Tab(selected = selectedTab == 2, onClick = { selectedTab = 2 }, text = { Text("Ort & Zeitplan") })
|
|
||||||
Tab(selected = selectedTab == 3, onClick = { selectedTab = 3 }, text = { Text("Richter & Teilung") })
|
|
||||||
}
|
|
||||||
|
|
||||||
Divider(Modifier.padding(vertical = 8.dp))
|
|
||||||
|
|
||||||
when (steps[selectedTab]) {
|
|
||||||
WizardStep.IDENTIFIKATION -> StepIdentifikation(state, onStateChange)
|
|
||||||
WizardStep.DETAILS_FINANZEN -> StepDetailsFinanzen(state, onStateChange)
|
|
||||||
WizardStep.ORT_ZEIT -> StepOrtZeit(state, onStateChange)
|
|
||||||
WizardStep.RICHTER_TEILUNG -> StepRichterTeilung(state, onStateChange)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(Modifier.height(16.dp))
|
|
||||||
|
|
||||||
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) {
|
|
||||||
TextButton(enabled = selectedTab > 0, onClick = { selectedTab-- }) { Text("Zurück") }
|
|
||||||
Spacer(Modifier.weight(1f))
|
|
||||||
if (selectedTab < steps.lastIndex) {
|
|
||||||
TextButton(onClick = { selectedTab++ }) { Text("Weiter") }
|
|
||||||
} else {
|
|
||||||
OutlinedButton(onClick = {
|
|
||||||
val payload = state.toPayloadOrNull()
|
|
||||||
if (payload != null) onSubmit(payload)
|
|
||||||
}) { Text("Bewerb anlegen") }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun StepIdentifikation(state: CreateBewerbWizardState, onStateChange: (CreateBewerbWizardState) -> Unit) {
|
|
||||||
LazyColumn(Modifier.fillMaxSize(), contentPadding = PaddingValues(4.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
|
||||||
item {
|
|
||||||
OutlinedTextField(
|
|
||||||
value = state.klasse,
|
|
||||||
onValueChange = { onStateChange(state.copy(klasse = it)) },
|
|
||||||
label = { Text("Sparte/Kategorie/Klasse") },
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
item {
|
|
||||||
OutlinedTextField(
|
|
||||||
value = state.hoeheCm,
|
|
||||||
onValueChange = { onStateChange(state.copy(hoeheCm = it.filter { ch -> ch.isDigit() })) },
|
|
||||||
label = { Text("Höhe (cm)") },
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
item {
|
|
||||||
OutlinedTextField(
|
|
||||||
value = state.bezeichnung,
|
|
||||||
onValueChange = { onStateChange(state.copy(bezeichnung = it)) },
|
|
||||||
label = { Text("Bezeichnung") },
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun StepDetailsFinanzen(state: CreateBewerbWizardState, onStateChange: (CreateBewerbWizardState) -> Unit) {
|
|
||||||
LazyColumn(Modifier.fillMaxSize(), contentPadding = PaddingValues(4.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
|
||||||
item {
|
|
||||||
OutlinedTextField(
|
|
||||||
value = state.beschreibung,
|
|
||||||
onValueChange = { onStateChange(state.copy(beschreibung = it)) },
|
|
||||||
label = { Text("Beschreibung (optional)") },
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
item {
|
|
||||||
OutlinedTextField(
|
|
||||||
value = state.aufgabe,
|
|
||||||
onValueChange = { onStateChange(state.copy(aufgabe = it)) },
|
|
||||||
label = { Text("Aufgabe (z.B. R1)") },
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
item {
|
|
||||||
OutlinedTextField(
|
|
||||||
value = state.startgeld,
|
|
||||||
onValueChange = { onStateChange(state.copy(startgeld = it.filter { ch -> ch.isDigit() })) },
|
|
||||||
label = { Text("Startgeld (Cent)") },
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
item {
|
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
|
||||||
Checkbox(checked = state.geldpreisAusbezahlt, onCheckedChange = { onStateChange(state.copy(geldpreisAusbezahlt = it)) })
|
|
||||||
Text("Geldpreis ausbezahlt")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun StepOrtZeit(state: CreateBewerbWizardState, onStateChange: (CreateBewerbWizardState) -> Unit) {
|
|
||||||
LazyColumn(Modifier.fillMaxSize(), contentPadding = PaddingValues(4.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
|
||||||
item {
|
|
||||||
OutlinedTextField(
|
|
||||||
value = state.austragungsplatzId,
|
|
||||||
onValueChange = { onStateChange(state.copy(austragungsplatzId = it)) },
|
|
||||||
label = { Text("Austragungsplatz-ID (optional)") },
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
item {
|
|
||||||
OutlinedTextField(
|
|
||||||
value = state.beginnZeitTyp,
|
|
||||||
onValueChange = { onStateChange(state.copy(beginnZeitTyp = it)) },
|
|
||||||
label = { Text("Beginn (FIX/ANSCHLIESSEND)") },
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
item {
|
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
|
||||||
OutlinedTextField(
|
|
||||||
value = state.geplantesDatum,
|
|
||||||
onValueChange = { onStateChange(state.copy(geplantesDatum = it)) },
|
|
||||||
label = { Text("Datum (yyyy-MM-dd)") },
|
|
||||||
modifier = Modifier.weight(1f)
|
|
||||||
)
|
|
||||||
OutlinedTextField(
|
|
||||||
value = state.beginnZeit,
|
|
||||||
onValueChange = { onStateChange(state.copy(beginnZeit = it)) },
|
|
||||||
label = { Text("Beginn (HH:mm)") },
|
|
||||||
modifier = Modifier.weight(1f)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
item {
|
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
|
||||||
OutlinedTextField(
|
|
||||||
value = state.reitdauerMinuten,
|
|
||||||
onValueChange = { onStateChange(state.copy(reitdauerMinuten = it.filter { ch -> ch.isDigit() })) },
|
|
||||||
label = { Text("Reitdauer (min)") },
|
|
||||||
modifier = Modifier.weight(1f)
|
|
||||||
)
|
|
||||||
OutlinedTextField(
|
|
||||||
value = state.umbauMinuten,
|
|
||||||
onValueChange = { onStateChange(state.copy(umbauMinuten = it.filter { ch -> ch.isDigit() })) },
|
|
||||||
label = { Text("Umbau (min)") },
|
|
||||||
modifier = Modifier.weight(1f)
|
|
||||||
)
|
|
||||||
OutlinedTextField(
|
|
||||||
value = state.besichtigungMinuten,
|
|
||||||
onValueChange = { onStateChange(state.copy(besichtigungMinuten = it.filter { ch -> ch.isDigit() })) },
|
|
||||||
label = { Text("Besichtigung (min)") },
|
|
||||||
modifier = Modifier.weight(1f)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
item {
|
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
|
||||||
Checkbox(checked = state.stechenGeplant, onCheckedChange = { onStateChange(state.copy(stechenGeplant = it)) })
|
|
||||||
Text("Stechen geplant")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun StepRichterTeilung(state: CreateBewerbWizardState, onStateChange: (CreateBewerbWizardState) -> Unit) {
|
|
||||||
Column(Modifier.fillMaxWidth()) {
|
|
||||||
// Warn-Logik (mock): Wenn Richter ausgewählt und Position = "C" ohne weiterer Prüfung -> TB-Hinweis
|
|
||||||
val warnTb = state.richter.isNotEmpty()
|
|
||||||
if (warnTb) {
|
|
||||||
Box(
|
|
||||||
Modifier.fillMaxWidth().background(Color(0xFFFFF8E1)).padding(12.dp)
|
|
||||||
) { Text("Hinweis: Richter-Zuweisung erfordert Freigabe durch TB (Qualifikation prüfen)", color = Color(0xFFFFA000)) }
|
|
||||||
Spacer(Modifier.height(8.dp))
|
|
||||||
}
|
|
||||||
|
|
||||||
OutlinedTextField(
|
|
||||||
value = state.teilungsTyp,
|
|
||||||
onValueChange = { onStateChange(state.copy(teilungsTyp = it)) },
|
|
||||||
label = { Text("Teilungsregel (z.B. MANUELL)") },
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(Modifier.height(8.dp))
|
|
||||||
|
|
||||||
// Minimal-UI für das Hinzufügen eines Richters (freie Eingabe von UUID + Position)
|
|
||||||
var funktionaerId by remember { mutableStateOf("") }
|
|
||||||
var position by remember { mutableStateOf("") }
|
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) {
|
|
||||||
OutlinedTextField(funktionaerId, { funktionaerId = it }, label = { Text("Funktionär-ID") }, modifier = Modifier.weight(1f))
|
|
||||||
OutlinedTextField(position, { position = it }, label = { Text("Position (C/M/…)") }, modifier = Modifier.weight(1f))
|
|
||||||
TextButton(onClick = {
|
|
||||||
if (funktionaerId.isNotBlank() && position.isNotBlank()) {
|
|
||||||
val list = state.richter + RichterEinsatzDto(funktionaerId = funktionaerId.trim(), position = position.trim())
|
|
||||||
onStateChange(state.copy(richter = list))
|
|
||||||
funktionaerId = ""; position = ""
|
|
||||||
}
|
|
||||||
}) { Text("Hinzufügen") }
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(Modifier.height(8.dp))
|
|
||||||
|
|
||||||
state.richter.forEachIndexed { idx, r ->
|
|
||||||
Row(Modifier.fillMaxWidth().padding(vertical = 4.dp), horizontalArrangement = Arrangement.SpaceBetween) {
|
|
||||||
Text("${idx + 1}. ${r.position} – ${r.funktionaerId}")
|
|
||||||
TextButton(onClick = {
|
|
||||||
val list = state.richter.toMutableList().also { it.removeAt(idx) }
|
|
||||||
onStateChange(state.copy(richter = list))
|
|
||||||
}) { Text("Entfernen") }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Mapping UI-State -> API-Payload ---
|
|
||||||
private fun CreateBewerbWizardState.toPayloadOrNull(): CreateBewerbPayload? {
|
|
||||||
if (klasse.isBlank() || bezeichnung.isBlank()) return null
|
|
||||||
|
|
||||||
val hoehe: Int? = hoeheCm.toIntOrNull()
|
|
||||||
val startgeldCent: Long? = startgeld.toLongOrNull()
|
|
||||||
|
|
||||||
val datum: LocalDate? = runCatching { if (geplantesDatum.isBlank()) null else LocalDate.parse(geplantesDatum) }.getOrNull()
|
|
||||||
val zeit: LocalTime? = runCatching { if (beginnZeit.isBlank()) null else LocalTime.parse(beginnZeit) }.getOrNull()
|
|
||||||
val beginnTyp: String? = beginnZeitTyp.ifBlank { null }
|
|
||||||
|
|
||||||
val reitMin = reitdauerMinuten.toIntOrNull()
|
|
||||||
val umbauMin = umbauMinuten.toIntOrNull()
|
|
||||||
val besMin = besichtigungMinuten.toIntOrNull()
|
|
||||||
|
|
||||||
return CreateBewerbPayload(
|
|
||||||
klasse = klasse.trim(),
|
|
||||||
hoeheCm = hoehe,
|
|
||||||
bezeichnung = bezeichnung.trim(),
|
|
||||||
beschreibung = beschreibung.ifBlank { null },
|
|
||||||
aufgabe = aufgabe.ifBlank { null },
|
|
||||||
aufgabenNummer = null,
|
|
||||||
paraGrade = null,
|
|
||||||
austragungsplatzId = austragungsplatzId.ifBlank { null },
|
|
||||||
richterEinsaetze = richter,
|
|
||||||
geplantesDatum = datum,
|
|
||||||
beginnZeitTyp = beginnTyp,
|
|
||||||
beginnZeit = zeit,
|
|
||||||
reitdauerMinuten = reitMin,
|
|
||||||
umbauMinuten = umbauMin,
|
|
||||||
besichtigungMinuten = besMin,
|
|
||||||
stechenGeplant = stechenGeplant,
|
|
||||||
startgeldCent = startgeldCent,
|
|
||||||
geldpreisAusbezahlt = geldpreisAusbezahlt,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -75,13 +75,6 @@ include(":backend:services:entries:entries-api")
|
||||||
include(":backend:services:entries:entries-domain")
|
include(":backend:services:entries:entries-domain")
|
||||||
include(":backend:services:entries:entries-service")
|
include(":backend:services:entries:entries-service")
|
||||||
|
|
||||||
// --- EVENTS (Veranstaltungen & Turniere) ---
|
|
||||||
include(":backend:services:events:events-api")
|
|
||||||
include(":backend:services:events:events-common")
|
|
||||||
include(":backend:services:events:events-domain")
|
|
||||||
include(":backend:services:events:events-infrastructure")
|
|
||||||
include(":backend:services:events:events-service")
|
|
||||||
|
|
||||||
// --- IDENTITY (Benutzerprofile & ZNS-Link) ---
|
// --- IDENTITY (Benutzerprofile & ZNS-Link) ---
|
||||||
include(":backend:services:identity:identity-domain")
|
include(":backend:services:identity:identity-domain")
|
||||||
include(":backend:services:identity:identity-infrastructure")
|
include(":backend:services:identity:identity-infrastructure")
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user