Finalize domain model: define event → tournament → class → division hierarchy, multi-tournament account structures, and event-wide cashbox aggregation logic. Add division types and update glossary tasks.

This commit is contained in:
Stefan Mogeritsch 2026-04-02 17:53:46 +02:00
parent 898d249d41
commit bbe5b1a357
5 changed files with 294 additions and 9 deletions

View File

@ -0,0 +1,179 @@
---
type: DOMAIN_SPEC
status: ACTIVE
owner: Lead Architect
last_update: 2026-04-02
---
# DomänenModell: Veranstaltung → Turnier → Bewerb → Abteilung
Ziel: Dieses Dokument fixiert das offizielle KernModell für die EventStruktur sowie Kassa/Konten. Es ist die Single Source of Truth für BackendSchema, FrontendViewModels und Schnittstellen.
Quellen/Verweise:
- Ubiquitous Language: `docs/03_Domain/01_Glossary/Ubiquitous_Language.md`
- ÖTO/FEI Referenz: `docs/03_Domain/02_Reference/` (insb. AbteilungsSchwellenwerte)
- ADR0021 TenantResolution (EventIsolation): `docs/01_Architecture/adr/0021-tenant-resolution-strategy-de.md`
## 1. Struktur und Kardinalitäten
Hierarchie und Identifikatoren (kanonisch):
```
Veranstaltung (event_id)
├─ Turnier (tournament_id) [1:N pro Veranstaltung]
│ └─ Bewerb (class_id) [1:N pro Turnier]
│ └─ Abteilung (division_id) [1:N pro Bewerb]
└─ TeilnehmerKonto (account_id) [1:N pro Veranstaltung, referenziert Teilnehmer]
└─ VeranstaltungsKassa (event_cashbox_id = event_id) [1:1]
```
Leitlinien:
- Jede Veranstaltung ist ein eigener Tenant (SchemaperTenant gemäß ADR0021).
- IDs sind innerhalb des Tenants eindeutig; globale Adressen entstehen durch `{event_id}/{local_id}`.
## 2. Entitäten und Aggregate
### 2.1 Veranstaltung
- Schlüssel: `event_id` (Slug, z. B. `2026-moc-open`)
- AggregateGrenze: umfasst Metadaten der Veranstaltung, Kassa, TeilnehmerKontoKatalog.
- Invarianten:
- `status ∈ {draft, active, archived}`
- Archivierte Veranstaltungen sind readonly.
### 2.2 Turnier
- Schlüssel: `tournament_id` (innerhalb Veranstaltung eindeutig)
- Attribute (Auszug): Titel, Datum(e), Ort, Status.
- Invarianten:
- Ein Turnier gehört genau zu einer Veranstaltung.
- Löschen nur erlaubt, wenn keine Nennungen/Ergebnisse bestätigt sind.
### 2.3 Bewerb
- Schlüssel: `class_id`
- Attribute: Disziplin, Klasse, Lizenzanforderungen, max Starter, Wertungsmodus.
- Invarianten:
- Ein Bewerb gehört genau zu einem Turnier.
- Abteilungsbildung erfolgt gemäß Regelwerk/Schwellenwerten.
### 2.4 Abteilung
- Schlüssel: `division_id`
- Attribute: Lauf/Startzeit, Parcours/Bahn, Typ (siehe unten), Ergebnisstatus.
- Invarianten:
- Eine Abteilung gehört genau zu einem Bewerb.
- Typen steuern UI, Zeitplan und Preisgeld-/Siegerehrungslogik.
### 2.5 TeilnehmerKonto (auf Veranstaltungsebene)
- Zweck: Vereinheitlichte finanzielle Sicht je Teilnehmer über mehrere Turniere derselben Veranstaltung (MultiTurnier).
- Schlüssel: `account_id`
- Beziehungen:
- `Teilnehmer` (z. B. Reiter, Verein, Team) 1:1 ↔ TeilnehmerKonto (pro Veranstaltung)
- Buchungen entstehen aus Nennungen, Startgeldern, Gebühren, Gutschriften, Rückzahlungen turnierübergreifend.
- Invarianten:
- Ein Teilnehmer hat höchstens ein Konto pro Veranstaltung.
- Saldo ist Summe aller bestätigten Buchungen innerhalb des Tenants.
### 2.6 VeranstaltungsKassa (Turnierübergreifender Saldo)
- Zweck: Aggregierte Kasse der gesamten Veranstaltung; spiegelt Einzahlungen/Auszahlungen und Summen über alle Turniere.
- Schlüssel: `event_cashbox_id` = `event_id`
- Komponenten:
- Journal (Belege): Ein/Auszahlungen, Umbuchungen, Korrekturen.
- Summen: aktueller Bestand, Reserven, offene Posten (aggregiert aus TeilnehmerKonten).
- Invarianten:
- Jede Buchung betrifft genau ein Gegenkonto (TeilnehmerKonto oder internes Konto).
- Journal ist unveränderlich; Korrekturen erfolgen als Gegenbuchung.
## 3. AbteilungsTypen
Definiert als `enum DivisionType`:
- `STANDARD`: Normale Abteilung mit regulärer Siegerehrung innerhalb des Bewerbs.
- `SEPARATE_SIEGEREHRUNG`: Abteilung, deren Siegerehrung separat organisiert wird (z. B. zusammengelegt/zeitlich entkoppelt) — STATUS: vorläufig, Detailregeln folgen durch 📜 Rulebook Expert.
- `ORGANISATORISCH`: Rein organisatorische Abteilung (z. B. Aufteilung aus Zeit/PlatzGründen), ohne eigenständige sportliche Wertung/Preisgeldlogik.
Hinweis: Die genaue Ausgestaltung von `SEPARATE_SIEGEREHRUNG` (PreisgeldAggregation, RankingAnzeige, Protokoll) wird im RulebookDokument ergänzt und kann weitere Felder/Beziehungen erfordern (z. B. Verweis auf „gemeinsame Siegerehrung für Bewerbe X/Y“).
## 4. DatenmodellSkizze (relationale Sicht je Tenant)
```sql
-- Veranstaltung (im TenantSchema)
CREATE TABLE event (
event_id TEXT PRIMARY KEY,
title TEXT NOT NULL,
status TEXT NOT NULL CHECK (status IN ('draft','active','archived'))
);
CREATE TABLE tournament (
tournament_id TEXT PRIMARY KEY,
event_id TEXT NOT NULL REFERENCES event(event_id),
title TEXT NOT NULL,
start_date DATE,
end_date DATE,
status TEXT NOT NULL
);
CREATE TABLE class (
class_id TEXT PRIMARY KEY,
tournament_id TEXT NOT NULL REFERENCES tournament(tournament_id),
discipline TEXT NOT NULL,
level TEXT NOT NULL,
max_starters INT,
scoring_mode TEXT NOT NULL
);
CREATE TABLE division (
division_id TEXT PRIMARY KEY,
class_id TEXT NOT NULL REFERENCES class(class_id),
type TEXT NOT NULL CHECK (type IN ('STANDARD','SEPARATE_SIEGEREHRUNG','ORGANISATORISCH')),
scheduled_at TIMESTAMP,
status TEXT NOT NULL
);
-- TeilnehmerKonto (veranstaltungsweit)
CREATE TABLE participant_account (
account_id TEXT PRIMARY KEY,
event_id TEXT NOT NULL REFERENCES event(event_id),
participant_ref TEXT NOT NULL, -- verweist auf TeilnehmerStammdatensatz im Tenant
UNIQUE(event_id, participant_ref)
);
CREATE TABLE participant_ledger_entry (
entry_id TEXT PRIMARY KEY,
account_id TEXT NOT NULL REFERENCES participant_account(account_id),
booking_ts TIMESTAMP NOT NULL,
amount_cents BIGINT NOT NULL,
currency TEXT NOT NULL DEFAULT 'EUR',
source TEXT NOT NULL, -- z. B. Nennung, Startgeld, Rückzahlung
tournament_id TEXT NULL REFERENCES tournament(tournament_id)
);
-- VeranstaltungsKassa
CREATE TABLE event_cashbox (
event_cashbox_id TEXT PRIMARY KEY REFERENCES event(event_id),
created_at TIMESTAMP NOT NULL
);
CREATE TABLE cashbox_journal (
journal_id TEXT PRIMARY KEY,
event_cashbox_id TEXT NOT NULL REFERENCES event_cashbox(event_cashbox_id),
booking_ts TIMESTAMP NOT NULL,
amount_cents BIGINT NOT NULL,
direction TEXT NOT NULL CHECK (direction IN ('IN','OUT')),
counterparty TEXT NOT NULL, -- account_id oder internes Konto
memo TEXT
);
```
## 5. Invarianten und Geschäftsregeln (Auszug)
- AbteilungsTyp `ORGANISATORISCH` darf keine eigenständige Preisgeldlogik auslösen.
- `SEPARATE_SIEGEREHRUNG` kann Ergebnisse bündeln/verschieben; Detailregeln werden im Rulebook spezifiziert. Bis dahin bleiben APIFelder stabil, Verhalten konservativ (keine automatische Zusammenlegung ohne explizite Verknüpfung).
- TeilnehmerKontoSaldo = Summe aller bestätigten `participant_ledger_entry.amount_cents`.
- EventKassaBestand = Summe `IN` Summe `OUT`; regelmäßige Abstimmung mit Summe aller TeilnehmerOffenen Posten.
## 6. API/DTO Richtlinien (HighLevel)
- Alle APIRessourcen werden unterhalb des Tenants adressiert (Header `X-Event-Id`).
- DTOs tragen stabile `*_id` Felder entsprechend diesem Modell; Referenzen sind per ID, keine eingebetteten Aggregate außer ReadViews.
- Enum `DivisionType` wird exakt wie oben benannt; neue Typen erfordern Versionserhöhung des Schemas.
## 7. ToDos und Folgearbeiten
- 📜 Rulebook Expert: DetailSpezifikation `SEPARATE_SIEGEREHRUNG` (Preisgeld, Ranking, UIHinweise) ergänzen.
- 🧹 Curator: `Ubiquitous_Language.md` um obige Begriffe/Definitionen erweitern.
- 👷 Backend: SchemaMigrationen pro Tenant gemäß obiger Tabellen; Repositories/Services entsprechend zuschneiden.
- 🎨 Frontend: ViewModels/Stores entlang dieser Struktur aktualisieren (Navigation: Veranstaltung → Turnier → Bewerb → Abteilung).

View File

@ -0,0 +1,99 @@
---
type: RULE_SPEC
status: DRAFT
owner: Rulebook Expert
last_update: 2026-04-02
---
# Validierungsregeln (ÖTO/FEI)
Ziel: Dieses Dokument definiert präzise, testbare Validierungsregeln als Grundlage für Frontend (LiveValidation), Backend (serverseitige Validation) und QA (Testfälle). Änderungen erfolgen versioniert und mit Beispielen.
Quellen/Verweise:
- Roadmap: `docs/04_Agents/Roadmaps/Rulebook_Roadmap.md`
- DomänenModell: `docs/03_Domain/01_Core_Model/Domain_Model_Veranstaltung_Turnier_Bewerb_Abteilung.md`
---
## 1. OEPSMitgliedsnummer (Austria)
Status: Draft finale Bestätigung durch OEPS/Verbandsdokumente ausstehend. Regel ist bewusst konservativ und maschinenlesbar formuliert.
### 1.1 Zweck
- Eindeutige Identifikation von Reiter:innen/Vereinsmitgliedern des OEPS in Formularen, Nennungen und SyncSchnittstellen.
### 1.2 Kanonisches Format (Normalform)
- Zeichenraum: Großbuchstaben AZ, Ziffern 09, Bindestrich `-`.
- Erlaubte Schreibweisen (Regex in PCRENotation):
1) Mit Präfix: `^OEPS-[0-9]{6,8}$`
2) Ohne Präfix: `^[0-9]{6,8}$`
Erläuterungen:
- Länge der numerischen Komponente: 6 bis 8 Ziffern (Spielraum für Altdaten/neuere Nummernkreise). Engführen auf exakt 7 Ziffern ist möglich, sobald offiziell bestätigt.
- Präfix ist optional in der Eingabe, wird bei Speicherung normalisiert (siehe 1.5).
### 1.3 Verbote / Nicht erlaubt
- Leerzeichen innerhalb der Nummer (z. B. `123 4567`).
- Punkte, Schrägstriche, Unterstriche oder andere Sonderzeichen (`. / _ + * ? ! …`).
- Alphanumerische Suffixe oder Buchstaben in der Nummer (z. B. `123456A`).
- Falsches oder gemischtes Präfix (z. B. `OEPS:` oder `Oeps-`).
- Führende Nullen sind erlaubt, zählen jedoch zur Gesamtlänge (z. B. `00123456`).
### 1.4 Beispiele
- Gültig:
- `123456`
- `7654321`
- `00123456`
- `OEPS-1234567`
- `OEPS-00123456`
- Ungültig (mit Begründung):
- `12345` — zu kurz (min. 6 Ziffern)
- `123456789` — zu lang (max. 8 Ziffern)
- `12A4567` — Buchstaben in der Nummer nicht erlaubt
- `OEPS1234567` — fehlender Bindestrich nach Präfix
- `OEPS-12 34567` — Leerzeichen nicht erlaubt
- `oeps-1234567` — falsche Groß/Kleinschreibung im Präfix (nur `OEPS-` zulässig)
- `OEPS-1234.567` — Punkt nicht erlaubt
### 1.5 Normalisierung (Speicherformat)
- Interne Normalform: `NNNNNNNN` (nur Ziffern, links mit Nullen auf 8 Zeichen aufgefüllt), sofern Nummernlänge ≤ 8.
- Eingaben mit Präfix werden gespeichert ohne Präfix, jedoch mit Metadatum `source_prefix = OEPS`.
- Beispiel: `OEPS-1234567``01234567` (Normalform), `source_prefix=OEPS`.
Hinweise:
- Falls sich offiziell herausstellt, dass die Länge fix `7` ist, wird die Normalisierung entsprechend angepasst (kein linksseitiges Auffüllen über 7 hinaus). Diese Änderung wäre eine MINOR Schema/ValidationVersion.
### 1.6 PseudocodeValidierung
```kotlin
fun validateOepsId(input: String): Boolean {
val trimmed = input.trim()
val withPrefix = Regex("^OEPS-[0-9]{6,8}$")
val plain = Regex("^[0-9]{6,8}$")
return withPrefix.matches(trimmed) || plain.matches(trimmed)
}
```
### 1.7 Fehlermeldungen (UXTexte)
- Kurz: "Ungültige OEPSMitgliedsnummer. Erlaubt sind 68 Ziffern, optional mit Präfix 'OEPS-'."
- Lang: "Bitte eine gültige OEPSMitgliedsnummer eingeben: 68 Ziffern (z. B. 1234567 oder OEPS-1234567). Keine Leerzeichen oder Sonderzeichen."
### 1.8 Offene Punkte / ToDo
- Verbandsbestätigung: Fixe Länge (6, 7 oder 8) und eventuelle Prüfziffer klären.
- Mapping Altsysteme: Enthalten historische KartenformatVarianten? Falls ja, eigene LegacyRegex aufnehmen.
- QA: Testfälle für Grenzwerte (6/8 Ziffern), PräfixVarianten, WhitespaceTrimmung.
---
## 2. FEIID
ToDo: Wird in A1 (weiterer Unterpunkt) spezifiziert.
---
## 3. Lizenzklassen (R1R4, RD1RD3, LZF)
ToDo: Vollständige Liste und Zuordnung in A1 (weiterer Unterpunkt).
---
## 4. Altersklassen Pferd
ToDo: Mindestalter je Bewerbsklasse / Höhe und Stichtagsregel (1. Jänner) folgt in A1 (weiterer Unterpunkt).

View File

@ -13,12 +13,12 @@
- [x] ADR-0021 in `docs/01_Architecture/adr/` ablegen - [x] ADR-0021 in `docs/01_Architecture/adr/` ablegen
- [x] Backend Developer informieren (A-3 ist Blocker) - [x] Backend Developer informieren (A-3 ist Blocker)
- [ ] **A-2** | Domänen-Modell formal präzisieren - [x] **A-2** | Domänen-Modell formal präzisieren
- [ ] Hierarchie `Veranstaltung → Turnier → Bewerb → Abteilung` als offizielles Modell festschreiben - [x] Hierarchie `Veranstaltung → Turnier → Bewerb → Abteilung` als offizielles Modell festschreiben
- [ ] `TeilnehmerKonto` auf Veranstaltungsebene (Multi-Turnier) ins Modell aufnehmen - [x] `TeilnehmerKonto` auf Veranstaltungsebene (Multi-Turnier) ins Modell aufnehmen
- [ ] Veranstaltungs-Kassa mit Turnier-übergreifendem Saldo modellieren - [x] Veranstaltungs-Kassa mit Turnier-übergreifendem Saldo modellieren
- [ ] Abteilungs-Typen `SEPARATE_SIEGEREHRUNG` und `ORGANISATORISCH` ins Modell aufnehmen - [x] Abteilungs-Typen `SEPARATE_SIEGEREHRUNG` (vorläufig) und `ORGANISATORISCH` ins Modell aufnehmen
- [ ] Curator beauftragen: `Ubiquitous_Language.md` aktualisieren - [x] Curator beauftragen: `Ubiquitous_Language.md` aktualisieren
--- ---

View File

@ -8,9 +8,10 @@
## 🔴 Sprint A — Sofort (diese Woche) ## 🔴 Sprint A — Sofort (diese Woche)
- [ ] **A-1** | Validierungsregeln schriftlich spezifizieren — Grundlage für alle anderen Teams - [ ] **A-1** | Validierungsregeln schriftlich spezifizieren — Grundlage für alle anderen Teams
- [ ] **OEPS-Mitgliedsnummer** - [x] **OEPS-Mitgliedsnummer**
- [ ] Gültiges Format definieren (Länge, erlaubte Zeichen, Präfixe) - [x] Gültiges Format definieren (Länge, erlaubte Zeichen, Präfixe)
- [ ] Ungültige Beispiele dokumentieren - [x] Ungültige Beispiele dokumentieren
- Ergebnis: siehe `docs/03_Domain/02_Reference/Validierungsregeln.md` Abschnitt „OEPSMitgliedsnummer“
- [ ] **FEI-ID** - [ ] **FEI-ID**
- [ ] Gültiges Format definieren - [ ] Gültiges Format definieren
- [ ] Wann ist FEI-ID Pflicht? (Turnierkategorie-abhängig) - [ ] Wann ist FEI-ID Pflicht? (Turnierkategorie-abhängig)

View File

@ -0,0 +1,6 @@
## ToDos und Folgearbeiten
- 📜 Rulebook Expert: DetailSpezifikation `SEPARATE_SIEGEREHRUNG` (Preisgeld, Ranking, UIHinweise) ergänzen.
- 🧹 Curator: `Ubiquitous_Language.md` um obige Begriffe/Definitionen erweitern.
- 👷 Backend: SchemaMigrationen pro Tenant gemäß obiger Tabellen; Repositories/Services entsprechend zuschneiden.
- 🎨 Frontend: ViewModels/Stores entlang dieser Struktur aktualisieren (Navigation: Veranstaltung → Turnier → Bewerb → Abteilung).