feat(db+domain): add turniernummer and einschraenkungen fields for tournament scope and constraints
- **Database Changes:** Introduced `turnier_nummer` (mandatory, 5 digits) and `einschraenkungen` (mandatory, ÖTO-specific constraints) columns in `turniere` table. Seeded `turnier_nummer` with `oeps_turniernummer` where applicable. - **Domain Models:** Extended `Turnier` and `DomTurnier` with `turnierNummer` and `einschraenkungen` fields. Added `TeilnehmerKreisE` enum for mapping restriction types. - **Services and Controllers:** Updated repository and service operations to handle the new fields. Controllers reflect the new request models for creation and updates. - **Validation:** Enforced input validation for `turnierNummer` format and `einschraenkungen` values. Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
This commit is contained in:
+2
@@ -11,6 +11,8 @@ object TurnierTable : Table("turniere") {
|
|||||||
val id = javaUUID("id").autoGenerate()
|
val id = javaUUID("id").autoGenerate()
|
||||||
val veranstaltungId = javaUUID("veranstaltung_id")
|
val veranstaltungId = javaUUID("veranstaltung_id")
|
||||||
val oepsTurniernummer = varchar("oeps_turniernummer", 50)
|
val oepsTurniernummer = varchar("oeps_turniernummer", 50)
|
||||||
|
val turnierNummer = varchar("turnier_nummer", 5)
|
||||||
|
val einschraenkungen = text("einschraenkungen")
|
||||||
|
|
||||||
// V3 Felder
|
// V3 Felder
|
||||||
val status = varchar("status", 16).default("DRAFT")
|
val status = varchar("status", 16).default("DRAFT")
|
||||||
|
|||||||
+3
@@ -2,12 +2,15 @@
|
|||||||
|
|
||||||
package at.mocode.entries.service.turniere
|
package at.mocode.entries.service.turniere
|
||||||
|
|
||||||
|
import at.mocode.core.domain.model.TeilnehmerKreisE
|
||||||
import kotlin.uuid.Uuid
|
import kotlin.uuid.Uuid
|
||||||
|
|
||||||
data class Turnier(
|
data class Turnier(
|
||||||
val id: Uuid,
|
val id: Uuid,
|
||||||
val veranstaltungId: Uuid,
|
val veranstaltungId: Uuid,
|
||||||
val oepsTurniernummer: String,
|
val oepsTurniernummer: String,
|
||||||
|
val turnierNummer: String,
|
||||||
|
val einschraenkungen: List<TeilnehmerKreisE>,
|
||||||
val status: String,
|
val status: String,
|
||||||
val publishedAt: String?,
|
val publishedAt: String?,
|
||||||
)
|
)
|
||||||
|
|||||||
+10
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
package at.mocode.entries.service.turniere
|
package at.mocode.entries.service.turniere
|
||||||
|
|
||||||
|
import at.mocode.core.domain.model.TeilnehmerKreisE
|
||||||
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 org.jetbrains.exposed.v1.core.ResultRow
|
import org.jetbrains.exposed.v1.core.ResultRow
|
||||||
@@ -21,6 +22,11 @@ class TurnierRepositoryImpl : TurnierRepository {
|
|||||||
id = row[TurnierTable.id].toKotlinUuid(),
|
id = row[TurnierTable.id].toKotlinUuid(),
|
||||||
veranstaltungId = row[TurnierTable.veranstaltungId].toKotlinUuid(),
|
veranstaltungId = row[TurnierTable.veranstaltungId].toKotlinUuid(),
|
||||||
oepsTurniernummer = row[TurnierTable.oepsTurniernummer],
|
oepsTurniernummer = row[TurnierTable.oepsTurniernummer],
|
||||||
|
turnierNummer = row[TurnierTable.turnierNummer],
|
||||||
|
einschraenkungen = row[TurnierTable.einschraenkungen]
|
||||||
|
.split(',')
|
||||||
|
.mapNotNull { key -> key.trim().takeIf { it.isNotBlank() } }
|
||||||
|
.map(TeilnehmerKreisE::valueOf),
|
||||||
status = row[TurnierTable.status],
|
status = row[TurnierTable.status],
|
||||||
publishedAt = row[TurnierTable.publishedAt]?.toString(),
|
publishedAt = row[TurnierTable.publishedAt]?.toString(),
|
||||||
)
|
)
|
||||||
@@ -31,6 +37,8 @@ class TurnierRepositoryImpl : TurnierRepository {
|
|||||||
stmt[TurnierTable.id] = t.id.toJavaUuid()
|
stmt[TurnierTable.id] = t.id.toJavaUuid()
|
||||||
stmt[TurnierTable.veranstaltungId] = t.veranstaltungId.toJavaUuid()
|
stmt[TurnierTable.veranstaltungId] = t.veranstaltungId.toJavaUuid()
|
||||||
stmt[TurnierTable.oepsTurniernummer] = t.oepsTurniernummer
|
stmt[TurnierTable.oepsTurniernummer] = t.oepsTurniernummer
|
||||||
|
stmt[TurnierTable.turnierNummer] = t.turnierNummer
|
||||||
|
stmt[TurnierTable.einschraenkungen] = t.einschraenkungen.joinToString(",") { it.name }
|
||||||
stmt[TurnierTable.status] = t.status
|
stmt[TurnierTable.status] = t.status
|
||||||
stmt[TurnierTable.publishedAt] = null
|
stmt[TurnierTable.publishedAt] = null
|
||||||
stmt[TurnierTable.createdAt] = now
|
stmt[TurnierTable.createdAt] = now
|
||||||
@@ -63,6 +71,8 @@ class TurnierRepositoryImpl : TurnierRepository {
|
|||||||
val now = Clock.System.now()
|
val now = Clock.System.now()
|
||||||
TurnierTable.update({ TurnierTable.id eq t.id.toJavaUuid() }) { stmt ->
|
TurnierTable.update({ TurnierTable.id eq t.id.toJavaUuid() }) { stmt ->
|
||||||
stmt[TurnierTable.oepsTurniernummer] = t.oepsTurniernummer
|
stmt[TurnierTable.oepsTurniernummer] = t.oepsTurniernummer
|
||||||
|
stmt[TurnierTable.turnierNummer] = t.turnierNummer
|
||||||
|
stmt[TurnierTable.einschraenkungen] = t.einschraenkungen.joinToString(",") { it.name }
|
||||||
stmt[TurnierTable.status] = t.status
|
stmt[TurnierTable.status] = t.status
|
||||||
stmt[TurnierTable.updatedAt] = now
|
stmt[TurnierTable.updatedAt] = now
|
||||||
}
|
}
|
||||||
|
|||||||
+25
-4
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
package at.mocode.entries.service.turniere
|
package at.mocode.entries.service.turniere
|
||||||
|
|
||||||
|
import at.mocode.core.domain.model.TeilnehmerKreisE
|
||||||
import at.mocode.entries.domain.repository.NennungRepository
|
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.errors.ValidationException
|
import at.mocode.entries.service.errors.ValidationException
|
||||||
@@ -12,11 +13,19 @@ class TurnierService(
|
|||||||
private val nennungen: NennungRepository,
|
private val nennungen: NennungRepository,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
suspend fun create(veranstaltungId: Uuid, oepsNr: String, status: String? = null): Turnier {
|
suspend fun create(
|
||||||
|
veranstaltungId: Uuid,
|
||||||
|
oepsNr: String,
|
||||||
|
turnierNummer: String,
|
||||||
|
einschraenkungen: List<TeilnehmerKreisE>,
|
||||||
|
status: String? = null,
|
||||||
|
): Turnier {
|
||||||
val t = Turnier(
|
val t = Turnier(
|
||||||
id = Uuid.random(),
|
id = Uuid.random(),
|
||||||
veranstaltungId = veranstaltungId,
|
veranstaltungId = veranstaltungId,
|
||||||
oepsTurniernummer = oepsNr,
|
oepsTurniernummer = oepsNr,
|
||||||
|
turnierNummer = turnierNummer,
|
||||||
|
einschraenkungen = einschraenkungen,
|
||||||
status = status ?: "DRAFT",
|
status = status ?: "DRAFT",
|
||||||
publishedAt = null,
|
publishedAt = null,
|
||||||
)
|
)
|
||||||
@@ -27,12 +36,24 @@ class TurnierService(
|
|||||||
|
|
||||||
suspend fun list(status: String?, oepsNr: String?): List<Turnier> = repo.findAll(status, oepsNr)
|
suspend fun list(status: String?, oepsNr: String?): List<Turnier> = repo.findAll(status, oepsNr)
|
||||||
|
|
||||||
suspend fun update(id: Uuid, oepsNr: String): Turnier {
|
suspend fun update(id: Uuid, oepsNr: String, turnierNummer: String, einschraenkungen: List<TeilnehmerKreisE>): Turnier {
|
||||||
val current = get(id)
|
val current = get(id)
|
||||||
if (current.status == "PUBLISHED" && current.oepsTurniernummer != oepsNr) {
|
if (
|
||||||
|
current.status == "PUBLISHED" && (
|
||||||
|
current.oepsTurniernummer != oepsNr ||
|
||||||
|
current.turnierNummer != turnierNummer ||
|
||||||
|
current.einschraenkungen != einschraenkungen
|
||||||
|
)
|
||||||
|
) {
|
||||||
throw LockedException("Turnier ist PUBLISHED – strukturelle Felder nicht änderbar")
|
throw LockedException("Turnier ist PUBLISHED – strukturelle Felder nicht änderbar")
|
||||||
}
|
}
|
||||||
return repo.update(current.copy(oepsTurniernummer = oepsNr))
|
return repo.update(
|
||||||
|
current.copy(
|
||||||
|
oepsTurniernummer = oepsNr,
|
||||||
|
turnierNummer = turnierNummer,
|
||||||
|
einschraenkungen = einschraenkungen,
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun delete(id: Uuid): Boolean {
|
suspend fun delete(id: Uuid): Boolean {
|
||||||
|
|||||||
+18
-2
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
package at.mocode.entries.service.turniere
|
package at.mocode.entries.service.turniere
|
||||||
|
|
||||||
|
import at.mocode.core.domain.model.TeilnehmerKreisE
|
||||||
import at.mocode.entries.service.persistence.VeranstaltungTable
|
import at.mocode.entries.service.persistence.VeranstaltungTable
|
||||||
import at.mocode.entries.service.tenant.tenantTransaction
|
import at.mocode.entries.service.tenant.tenantTransaction
|
||||||
import org.jetbrains.exposed.v1.jdbc.selectAll
|
import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||||
@@ -12,11 +13,15 @@ import kotlin.uuid.toKotlinUuid
|
|||||||
|
|
||||||
data class CreateTurnierRequest(
|
data class CreateTurnierRequest(
|
||||||
val oepsTurniernummer: String,
|
val oepsTurniernummer: String,
|
||||||
|
val turnierNummer: String,
|
||||||
|
val einschraenkungen: List<TeilnehmerKreisE> = emptyList(),
|
||||||
val status: String? = null,
|
val status: String? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class UpdateTurnierRequest(
|
data class UpdateTurnierRequest(
|
||||||
val oepsTurniernummer: String,
|
val oepsTurniernummer: String,
|
||||||
|
val turnierNummer: String,
|
||||||
|
val einschraenkungen: List<TeilnehmerKreisE> = emptyList(),
|
||||||
)
|
)
|
||||||
|
|
||||||
data class UpdateTurnierStatusRequest(
|
data class UpdateTurnierStatusRequest(
|
||||||
@@ -34,7 +39,13 @@ class TurniereController(
|
|||||||
suspend fun create(@RequestBody body: CreateTurnierRequest): Turnier {
|
suspend fun create(@RequestBody body: CreateTurnierRequest): Turnier {
|
||||||
// Veranstaltung pro Tenant auflösen: wir nehmen die einzige vorhandene ID aus dem Schema
|
// Veranstaltung pro Tenant auflösen: wir nehmen die einzige vorhandene ID aus dem Schema
|
||||||
val veranstaltungId = resolveVeranstaltungId()
|
val veranstaltungId = resolveVeranstaltungId()
|
||||||
return service.create(veranstaltungId, body.oepsTurniernummer, body.status)
|
return service.create(
|
||||||
|
veranstaltungId = veranstaltungId,
|
||||||
|
oepsNr = body.oepsTurniernummer,
|
||||||
|
turnierNummer = body.turnierNummer,
|
||||||
|
einschraenkungen = body.einschraenkungen,
|
||||||
|
status = body.status,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/turniere")
|
@GetMapping("/turniere")
|
||||||
@@ -48,7 +59,12 @@ class TurniereController(
|
|||||||
|
|
||||||
@PutMapping("/turniere/{id}")
|
@PutMapping("/turniere/{id}")
|
||||||
suspend fun update(@PathVariable id: String, @RequestBody body: UpdateTurnierRequest): Turnier =
|
suspend fun update(@PathVariable id: String, @RequestBody body: UpdateTurnierRequest): Turnier =
|
||||||
service.update(Uuid.parse(id), body.oepsTurniernummer)
|
service.update(
|
||||||
|
id = Uuid.parse(id),
|
||||||
|
oepsNr = body.oepsTurniernummer,
|
||||||
|
turnierNummer = body.turnierNummer,
|
||||||
|
einschraenkungen = body.einschraenkungen,
|
||||||
|
)
|
||||||
|
|
||||||
@DeleteMapping("/turniere/{id}")
|
@DeleteMapping("/turniere/{id}")
|
||||||
@ResponseStatus(HttpStatus.NO_CONTENT)
|
@ResponseStatus(HttpStatus.NO_CONTENT)
|
||||||
|
|||||||
+23
@@ -0,0 +1,23 @@
|
|||||||
|
-- V4: Add turnier_nummer and einschraenkungen to turniere
|
||||||
|
-- Context: ÖTO § 3 Turniernummer + Teilnehmerkreis-Einschränkungen
|
||||||
|
|
||||||
|
ALTER TABLE IF EXISTS turniere
|
||||||
|
ADD COLUMN IF NOT EXISTS turnier_nummer VARCHAR(5);
|
||||||
|
|
||||||
|
ALTER TABLE IF EXISTS turniere
|
||||||
|
ADD COLUMN IF NOT EXISTS einschraenkungen TEXT;
|
||||||
|
|
||||||
|
UPDATE turniere
|
||||||
|
SET turnier_nummer = oeps_turniernummer
|
||||||
|
WHERE (turnier_nummer IS NULL OR turnier_nummer = '')
|
||||||
|
AND oeps_turniernummer ~ '^\d{5}$';
|
||||||
|
|
||||||
|
UPDATE turniere
|
||||||
|
SET einschraenkungen = ''
|
||||||
|
WHERE einschraenkungen IS NULL;
|
||||||
|
|
||||||
|
ALTER TABLE turniere
|
||||||
|
ALTER COLUMN turnier_nummer SET NOT NULL;
|
||||||
|
|
||||||
|
ALTER TABLE turniere
|
||||||
|
ALTER COLUMN einschraenkungen SET NOT NULL;
|
||||||
+6
@@ -3,6 +3,7 @@
|
|||||||
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.model.TeilnehmerKreisE
|
||||||
import at.mocode.core.domain.model.TurnierkategorieE
|
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
|
||||||
@@ -48,9 +49,11 @@ data class DomTurnier(
|
|||||||
|
|
||||||
// Basis-Informationen
|
// Basis-Informationen
|
||||||
var name: String,
|
var name: String,
|
||||||
|
val turnierNummer: String,
|
||||||
var sparte: SparteE,
|
var sparte: SparteE,
|
||||||
var kategorie: TurnierkategorieE,
|
var kategorie: TurnierkategorieE,
|
||||||
var datum: LocalDate,
|
var datum: LocalDate,
|
||||||
|
var einschraenkungen: List<TeilnehmerKreisE> = emptyList(),
|
||||||
|
|
||||||
// Funktionäre
|
// Funktionäre
|
||||||
@Serializable(with = UuidSerializer::class)
|
@Serializable(with = UuidSerializer::class)
|
||||||
@@ -98,6 +101,9 @@ data class DomTurnier(
|
|||||||
if (name.isBlank()) {
|
if (name.isBlank()) {
|
||||||
warnings.add("Turniername ist erforderlich.")
|
warnings.add("Turniername ist erforderlich.")
|
||||||
}
|
}
|
||||||
|
if (!Regex("^\\d{5}$").matches(turnierNummer)) {
|
||||||
|
warnings.add("Turniernummer muss exakt 5 Ziffern enthalten (z.B. 26128).")
|
||||||
|
}
|
||||||
maxBewerbe?.let { max ->
|
maxBewerbe?.let { max ->
|
||||||
if (max <= 0) {
|
if (max <= 0) {
|
||||||
warnings.add("Maximale Bewerb-Anzahl muss positiv sein.")
|
warnings.add("Maximale Bewerb-Anzahl muss positiv sein.")
|
||||||
|
|||||||
@@ -441,6 +441,36 @@ enum class TurnierStatusE {
|
|||||||
ABGESAGT
|
ABGESAGT
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Einschränkungen des Teilnehmerkreises gemäß ÖTO § 3 (Zusatz-Buchstaben bei Turnieren).
|
||||||
|
*/
|
||||||
|
@Serializable
|
||||||
|
enum class TeilnehmerKreisE {
|
||||||
|
/** Jugend/Junioren/Junge Reiter (J) */
|
||||||
|
JUGEND_JUNIOREN_YR,
|
||||||
|
|
||||||
|
/** Ponys (P) */
|
||||||
|
PONYS,
|
||||||
|
|
||||||
|
/** Noriker (N) */
|
||||||
|
NORIKER,
|
||||||
|
|
||||||
|
/** Haflinger (H) */
|
||||||
|
HAFLINGER,
|
||||||
|
|
||||||
|
/** Ländliche Reiter (L) */
|
||||||
|
LAENDLICHE_REITER,
|
||||||
|
|
||||||
|
/** Vollblutaraber (A) */
|
||||||
|
VOLLBLUTARABER,
|
||||||
|
|
||||||
|
/** Kaltblut (K) */
|
||||||
|
KALTBLUT,
|
||||||
|
|
||||||
|
/** Damensattel (D) */
|
||||||
|
DAMENSATTEL
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Status einer Ausschreibung gemäß ÖTO-Genehmigungsworkflow.
|
* Status einer Ausschreibung gemäß ÖTO-Genehmigungsworkflow.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -155,7 +155,8 @@ Die ÖTO definiert sparten- und klassenabhängige Schwellenwerte, ab wievielen S
|
|||||||
| **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 |
|
||||||
| **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** | Vom OEPS vergebene, eindeutige Kennung eines Turniers (z.B. `25123`). Ohne diese Nummer darf keine offizielle Ausschreibung veröffentlicht werden. | 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 |
|
||||||
|
| **Teilnehmerkreis-Einschränkung** | Optionale Einschränkung des zulässigen Teilnehmerkreises eines Turniers gemäß ÖTO-Gliederung (§ 3), dargestellt über Zusatz-Buchstaben (z.B. `-H`, `-P`, `-J`). | ÖTO § 3 |
|
||||||
| **Turnierkategorie** | Siehe → *Kategorie*. | ÖTO § 3 Abs. 4 |
|
| **Turnierkategorie** | Siehe → *Kategorie*. | ÖTO § 3 Abs. 4 |
|
||||||
| **Turnierkassa** | Kassa auf Ebene des einzelnen → *Turniers*. Hält Belege, Tagesabschlüsse und Barbestände pro Turnier. Wird in der → *Veranstaltungs‑Kassa* konsolidiert. | Billing Context |
|
| **Turnierkassa** | Kassa auf Ebene des einzelnen → *Turniers*. Hält Belege, Tagesabschlüsse und Barbestände pro Turnier. Wird in der → *Veranstaltungs‑Kassa* konsolidiert. | Billing Context |
|
||||||
| **TeilnehmerKonto** | Konto eines Zahlers auf Ebene der → *Veranstaltung* (nicht nur eines Turniers). Aggregiert Saldo, Einzahlungen und Verrechnungen über alle → *Turniere* derselben Veranstaltung hinweg (Multi‑Turnier‑Aggregation). Ermöglicht, dass eine einzelne Zahlung mehrere Rechnungen in verschiedenen Turnieren derselben Veranstaltung ausgleicht. | Billing Context |
|
| **TeilnehmerKonto** | Konto eines Zahlers auf Ebene der → *Veranstaltung* (nicht nur eines Turniers). Aggregiert Saldo, Einzahlungen und Verrechnungen über alle → *Turniere* derselben Veranstaltung hinweg (Multi‑Turnier‑Aggregation). Ermöglicht, dass eine einzelne Zahlung mehrere Rechnungen in verschiedenen Turnieren derselben Veranstaltung ausgleicht. | Billing Context |
|
||||||
|
|||||||
Reference in New Issue
Block a user