docs: add domain workshop results and event structure diagram

- Documented outcomes of the 2026-03-17 domain workshop under `docs/03_Domain/03_Analysis/Domain_Workshop_Results_2026-03-17.md`.
- Added a structural diagram visualizing events, tournaments, and competitions with their relationships under `docs/03_Domain/01_Core_Model/Entities/Event_Structure_Diagram.md`.

Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
This commit is contained in:
2026-03-17 17:27:27 +01:00
parent b6f4f43122
commit 2538be395a
3 changed files with 289 additions and 70 deletions
@@ -20,7 +20,7 @@ erDiagram
string id PK "UUID"
string event_id FK
string oeps_number "z.B. 21001 (A-Satz)"
string category "z.B. CSN-A, CDN-B"
string category "z.B. CSN-A, CDN-B, CDN-C-NEU"
string ruleset "OETO oder FEI"
}
@@ -31,18 +31,76 @@ erDiagram
string code_official "3-stellig (B-Satz Pos. 61)"
int division_id "Abteilung (0=keine, 1=Abt. 1...)"
string title "z.B. Standardspringprüfung"
string category "Klasse z.B. L, M, S*"
string discipline "S (Springen), D (Dressur), C"
string scoring_method "Richtverfahren (z.B. A0, A2)"
string category "Klasse z.B. A, L, M, S*, lizenzfrei"
string scoring_method "Richtverfahren (z.B. A0, A2, AM5, Stil)"
int start_fee "in Cent"
string status "OPEN, RUNNING, FINISHED"
int planned_duration_per_starter_seconds "Kalkulierte Reitzeit pro Starter"
}
OFFICIALS {
string id PK "UUID"
string tournament_id FK "oder competition_id"
string actor_id FK "Richter / Parcoursbauer"
string role "Hauptrichter, Richter, TD"
string role "Hauptrichter, Richter, TD, Parcoursbauer"
}
ACTOR {
string id PK "UUID"
string oeps_id "z.B. 123456"
string type "PERSON, ORGANIZATION"
string name "Vollständiger Name"
string license_code "z.B. R1"
}
QUALIFICATION {
string id PK "UUID"
string actor_id FK
string discipline "S, D, V"
string level "z.B. L, M, S"
string role "RICHTER, PARCOURSBAUER"
}
ENTRY {
string id PK "UUID"
string competition_id FK
string horse_id FK
string rider_id FK
string status "ACTIVE, WITHDRAWN"
string preferred_start_position "Optional: vorne, hinten, zu"
}
START_LIST_ENTRY {
string id PK "UUID"
string entry_id FK
int start_order "Nummer in der Startreihenfolge"
string status "READY, IN_PROGRESS, FINISHED, DNS"
}
RESULT {
string id PK "UUID"
string start_list_entry_id FK
int rank "Platzierung (kann manuell überschrieben werden)"
string status "OK, EL, RET, DNS"
float points "Fehlerpunkte / Wertnote"
float time_seconds "Benötigte Zeit"
int prize_money "Ausbezahltes Geld in Cent"
}
BILLING_ACCOUNT {
string id PK "UUID"
string event_id FK
string payer_id FK "Akteur (Zahler)"
int current_balance "in Cent"
}
TRANSACTION {
string id PK "UUID"
string account_id FK
string entry_id FK "Optionaler Bezug zur Nennung"
string type "NENNGELD, STARTGELD, TAUSCHGEBÜHR, NACHNENNUNG"
int amount "in Cent"
}
%% Relationships
@@ -50,9 +108,16 @@ erDiagram
TOURNAMENT ||--o{ COMPETITION: "besteht aus (B-Satz)"
TOURNAMENT ||--o{ OFFICIALS: "hat Funktionäre (C-Satz)"
COMPETITION ||--o{ OFFICIALS: "wird gerichtet von"
OFFICIALS }o--|| ACTOR: "ist ein"
ACTOR ||--o{ QUALIFICATION: "hat Berechtigungen"
COMPETITION ||--o{ ENTRY: "hat Nennungen"
ENTRY ||--o| START_LIST_ENTRY: "wird zu Starter"
START_LIST_ENTRY ||--o| RESULT: "hat Ergebnis"
EVENT ||--o{ BILLING_ACCOUNT: "verwaltet Kassa für"
BILLING_ACCOUNT ||--o{ TRANSACTION: "verbucht"
```
## Erläuterung zum Modell (Option B)
## Erläuterung zum Modell
### 1. `EVENT` (Veranstaltung)
@@ -61,17 +126,17 @@ und einen Veranstalter. Es existiert unabhängig von den strikten OEPS-Regularie
Verwaltung (Rechnungsstellung, Anlagenplanung) wichtig.
### 2. `TOURNAMENT` (Turnier)
Ein Event kann mehrere **Turniere** beinhalten (z.B. ein nationales CSN-B und gleichzeitig ein internationales CSI2*).
Das Turnier ist die Instanz, die strikt an das Regelwerk gebunden ist:
* Es korrespondiert 1:1 mit dem **A-Satz** des OEPS-Pflichtenhefts.
* Es hat eine eindeutige 5-stellige `oeps_number`.
* Es legt fest, nach welchem Regelwerk (ÖTO vs. FEI) geritten und ausgewertet wird.
* Es legt fest, nach welchem Regelwerk (ÖTO vs. FEI) geritten und ausgewertet wird. In der ersten Phase konzentrieren
wir uns auf **C-NEU** und **C**.
### 3. `COMPETITION` (Bewerb / Prüfung)
Die kleinste sportliche Einheit und das Herzstück der Ausschreibung.
Die kleinste sportliche Einheit und das Herzstück der Ausschreibung. Wir fokussieren uns initial auf **Dressur (D)** und
**Springen (S)**.
Hier finden wir die direkte Verbindung zum **B-Satz** der Legacy-Spezifikation:
* **Abteilungen (`division_id`):** Laut OEPS-Schnittstelle wird jede Abteilung (z.B. R1-Reiter getrennt von R2-Reitern)
@@ -82,8 +147,31 @@ Hier finden wir die direkte Verbindung zum **B-Satz** der Legacy-Spezifikation:
`code_official` an Stelle 61).
* **Richtverfahren:** Entscheidet darüber, wie Fehler und Zeit (Springen) oder Wertnoten (Dressur) in ein Ranking
übersetzt werden.
* **Zeitplanung:** `planned_duration_per_starter_seconds` ist für die Kalkulation der Startzeiten elementar.
### 4. `OFFICIALS` (Funktionäre)
### 4. `OFFICIALS` & `QUALIFICATION` (Der C-Satz & ZNS-Import)
Dies entspricht dem **C-Satz**. Richter und Parcoursbauer müssen einem Turnier oder spezifisch einem Bewerb zugewiesen
werden. Deren 6-stellige OEPS-Nummer (Satznummer) wird für den Ergebnis-Export benötigt.
werden.
* Neu: Die Entität `QUALIFICATION` bildet die importierten Lizenzstufen aus der ZNS-Datei `RICHT01.DAT` ab. Das Backend
gleicht diese Berechtigungen bei der Zuweisung gegen die Kategorie der `COMPETITION` ab.
### 5. `ENTRY`, `START_LIST_ENTRY` & `RESULT` (Nennung bis Ergebnis)
* **`ENTRY` (Nennung):** Die Nennung verknüpft ein Pferd und einen Reiter mit einem bestimmten Bewerb. Hier werden auch
**Startwünsche** (vorne/hinten) erfasst, die für den Telefon-Workflow kritisch sind.
* **`START_LIST_ENTRY` (Startliste):** Generiert aus den aktiven Nennungen. Definiert die exakte Startreihenfolge.
* **`RESULT` (Ergebnis):** Hält die im "Richterturm-Workflow" erfassten Rohdaten (Punkte/Zeit) und die errechnete
Platzierung. Die Platzierung kann bei Bedarf manuell vom Veranstalter überschrieben werden (z.B. wenn mehr Platziert
werden sollen, als die ÖTO vorschlägt).
### 6. Billing Context (`BILLING_ACCOUNT` & `TRANSACTION`)
Die Abrechnung (Kassa) wird als separater Bereich an das `EVENT` gehängt.
* Es wird ein `BILLING_ACCOUNT` pro "Zahler" (meist ein `ACTOR`) geführt.
* Jede Gebühr (Nenngeld, Startgeld, Nachnenngebühr) wird als atomare `TRANSACTION` verbucht.
* Wenn ein **Nennungstausch** stattfindet, bleibt das Nenngeld als positive Transaktion auf dem Account erhalten,
während für die neue Nennung lediglich eine neue Startgeld- oder Tauschgebühr-Transaktion erzeugt wird. Das ermöglicht
maximale Flexibilität.
@@ -0,0 +1,101 @@
# Prozess: ZNS-Import (Master Data Sync)
**Status:** Draft / Konzept
**Datum:** 17.03.2026
## 1. Ausgangslage & Herausforderungen
Das OEPS stellt die Stammdaten als ZIP-Datei (`zns.zip`) bereit, die in Form von textbasierten ASCII-Dateien (Codepage
850) vorliegen. Die Struktur ist starr, nicht relational und erfahrungsgemäß oft fehlerbehaftet oder unsauber
formatiert (Legacy-Spezifikation V2.4).
Zusätzlich ändern sich Lizenzstände, Sperrlisten oder Registrierungen laufend. Weiters ist die Meldestelle oft
gezwungen, vor Ort manuelle Korrekturen vorzunehmen oder Daten aus anderen Quellen (z.B. Zuchtverbände wie AWÖ) zu
integrieren.
## 2. Architektonische Entscheidung: Event Sourcing & CQRS
Um den Anforderungen (vollständige Historie, turnierspezifischer Datenstand, fehlertoleranter Import, **manuelle
Overrides**) gerecht zu werden, wird der ZNS-Import nach Prinzipien von **Event Sourcing** und **CQRS (Command Query
Responsibility Segregation)** konzipiert.
* Wir überschreiben keine Daten einfach (`UPDATE`), sondern hängen Änderungen als Ereignisse (`EVENTS`) an.
* Dies ermöglicht es uns, den Stand einer Person oder eines Pferdes für die Ewigkeit exakt zu rekonstruieren, selbst
wenn sich die Stammdaten ändern.
### 2.1 Der Import-Ablauf (Die "Command" Seite)
1. **Ingestion:** Der User (Meldestelle) lädt die `zns.zip` hoch oder triggert einen Import aus einer anderen Quelle (
Zuchtverband).
2. **Parsing & Cleansing:** Ein dedizierter Importer-Service entpackt die ZIP, liest die Dateien zeilenweise (Codepage
850!) und konvertiert die starren ASCII-Strings in nutzbare DTOs (Data Transfer Objects). Hier greifen erste
Reinigungs-Routinen.
3. **Event Generation:** Der Service vergleicht die geparsten Daten mit dem aktuellen Stand (der "Read Model"
Datenbank).
* Findet er einen neuen Akteur (Satznummer bisher unbekannt), erzeugt er ein `ActorCreatedEvent`.
* Findet er Änderungen (z.B. Lizenz wurde von R1 auf R2 erhöht, oder Sperre wurde gesetzt), erzeugt er ein
`ActorUpdatedEvent` (bzw. spezifischer `LicenseUpgradedEvent`, `ActorLockedEvent`).
4. **Manuelle Korrekturen (Overrides):** Wenn die Meldestelle vor Ort Daten korrigiert (weil die OEPS-Daten falsch
waren), erzeugt das System ein spezielles Event, z.B. `ManualActorCorrectionEvent`. Dieses Event hat eine **höhere
Priorität** als zukünftige `ActorUpdatedEvents` aus dem ZNS-Import, solange der OEPS die Daten in seinem System nicht
korrigiert hat (Lösung z.B. über einen "Ignorier-Zeitstempel" oder Prioritäts-Flags in der Projektion).
5. **Event Log:** Diese Events werden in einem zentralen Event Log (dem "Event Store") persistiert. Dies ist die
absolute Single Source of Truth.
### 2.2 Die Datenbereitstellung (Die "Query" Seite)
1. **Projection (Projektion):** Kleine "Listener" hören auf das Event Log und bauen daraus die relationale
Lesedatenbank (SQLite / PostgreSQL) auf. Hierbei wird die Logik angewandt, dass manuelle Korrekturen der Meldestelle
Vorrang vor veralteten Verbandsdaten haben.
2. **Turnier-Snapshot:** Wenn ein Turnier konfiguriert wird oder am Vortag aktualisiert wird, zieht sich das System
einen "Snapshot" (Schnappschuss) der aktuellen Stammdaten und verknüpft diese mit der Turnier-ID.
3. **Zuchtverbands-Daten (Fremdformate):** Die Architektur erlaubt es uns leicht, neue Parser (z.B. für AWÖ-Daten) zu
schreiben. Diese lesen die fremden Formate ein und generieren die gleichen Standard-Events (`HorseCreatedEvent`, aber
evtl. mit Lebensnummer statt OEPS-Satznummer), die dann problemlos in die bestehende Projektion einfließen.
## 3. Datenhaltung (Konzeptuelles Modell)
```mermaid
sequenceDiagram
participant User
participant ImporterService as ZNS / AWÖ Importer
participant EventStore as Event Log (Append Only)
participant Projections as DB (Read Models)
User ->> ImporterService: Upload zns.zip (Freitag vor Turnier)
ImporterService ->> ImporterService: Parse & Clean
rect rgb(200, 220, 240)
Note right of ImporterService: Generierung von Import-Events
ImporterService ->> EventStore: Append: ActorUpdatedEvent(Satznummer: 123456, License: R2)
end
User ->> EventStore: Manuelle Korrektur am Turniertag
rect rgb(240, 200, 200)
Note right of User: Generierung von Override-Events
User ->> EventStore: Append: ManualActorCorrectionEvent(Satznummer: 123456, Name: "Neuer Name")
end
EventStore -->> Projections: Update Relational DB (z.B. aktueller Stand)
Note over Projections: Turniersystem liest nur <br/>aus Read Models. Manuelle Korrekturen<br/>gewinnen gegen Import-Daten.
```
## 4. Vorteile dieser Architektur
* **Audit-Sicherheit:** Wir wissen exakt, *wann* sich *was* geändert hat. Nichts geht verloren.
* **Archivierung:** Ein Turnier-Archiv muss nicht mehr mühsam als riesiger PDF/Daten-Dump gesichert werden. Wir können
das Turnier einfach anhand des Timestamps gegen den Event Store abfragen.
* **Fehlertoleranz:** Wenn ein Parsing-Fehler auftritt oder der OEPS kaputte Daten liefert, machen wir einfach ein
Rollback der fehlerhaften Events und projizieren die Datenbank neu. Wir zerschießen nicht die operativen Tabellen.
* **Erweiterbarkeit (Vision):** Wenn später ein "Ergebnis-Analyse-Service" oder ein "Züchter-Portal" angebunden wird,
können diese einfach die historischen Events abonnieren (Kafka/Message Queue).
* **Integration von Drittsystemen (Zuchtverbände):** Fremddaten können durch eigene Parser in unsere Standard-Events
übersetzt und nahtlos integriert werden.
## 5. Zu klärende Details für die Implementierung
* **Sync-Mechanismus:** Wie kommen die Events vom Master-Server auf den Offline-Laptop im Plumpsklo? (Vermutlich eine
robuste Sync-Queue, Kafka könnte für den Offline-Einsatz zu schwergewichtig sein, Alternativen evaluieren).
* **Event-Payload:** Definition der JSON-Struktur für die wichtigsten Events (`zns.actor.updated`,
`zns.horse.registered`).
* **Merge-Logik:** Wie lange bleibt ein `ManualActorCorrectionEvent` gültig, bevor ein zukünftiges Update vom ZNS diesen
Wert wieder überschreiben darf?