Auf Vorgabe des Auftraggebers umgestellt:
* genau eine Meisterschaft je Saison (unique auf saison_jahr)
* turnier.meisterschaft_id (nullable FK) statt bool zaehlt_meisterschaft;
NULL = "o. M.". Kompat-Property turnier.zaehlt_meisterschaft bleibt.
* Teilwertungen (Erwachsene-Gesamt, Jugend J1-J5) sind (Kategorie, Klasse)-
Sichten auf die eine Meisterschaft, keine eigenen Zeilen mehr
* meisterschaft_stand: kategorie + klasse_id + Unique je Wertung/Fahrer
* berechne_stand(kategorie=, klasse_id=), wertungen_fuer(), berechne_alle_wertungen()
* Read-API: /meisterschaften/{id}/stand[?wertung=], .../endlauf-qualifikation?wertung=
* Ingest: POST /turniere haengt zaehlende Turniere an die Jahres-Meisterschaft
* Initialmigration neu erzeugt (Projekt noch nicht ausgerollt); Seed: eine
"HTH-Meisterschaft <jahr>"
* Tests angepasst/ergaenzt (61 gruen)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
138 lines
5.2 KiB
Python
138 lines
5.2 KiB
Python
"""Ingest-API (Plan §6.1): Auth, dry_run, Idempotenz, Nachrechnung."""
|
|
|
|
from sqlalchemy import select
|
|
|
|
from oamc.models import Ergebnis, Start, Turnier
|
|
from oamc.models.enums import VeranstaltungStatus
|
|
from oamc.seeds import seed_saison_2026
|
|
from tests import factories as f
|
|
|
|
HEAD = {"X-API-Key": "test-key"}
|
|
|
|
|
|
def _turnier(client):
|
|
db = client.db
|
|
seed_saison_2026(db)
|
|
v = f.veranstaltung(db, status=VeranstaltungStatus.LAUFEND)
|
|
t = f.turnier(db, v)
|
|
db.flush()
|
|
return t.id
|
|
|
|
|
|
def test_ohne_api_key_401(client):
|
|
r = client.post("/api/v1/turniere/1/ergebnisse", json={"quelle": "x", "idempotenz_schluessel": "k", "ergebnisse": []})
|
|
assert r.status_code == 401
|
|
|
|
|
|
def test_dry_run_meldet_neue_fahrer_ohne_zu_schreiben(client):
|
|
tid = _turnier(client)
|
|
payload = {
|
|
"quelle": "excel-auswertung-v3",
|
|
"idempotenz_schluessel": "2026-05-31-reinheim-final",
|
|
"ergebnisse": [
|
|
{
|
|
"startnummer": 32,
|
|
"fahrer": {"nachname": "Gál-Szász", "vorname": "Péter", "geburtsjahr": 2015,
|
|
"geschlecht": "M", "verein": "OAMC Reinheim"},
|
|
"klasse": "J 2", "summe_zeit": "159.16", "summe_fehler": 10,
|
|
}
|
|
],
|
|
}
|
|
r = client.post(f"/api/v1/turniere/{tid}/ergebnisse?dry_run=true", json=payload, headers=HEAD)
|
|
assert r.status_code == 200
|
|
body = r.json()
|
|
assert body["dry_run"] is True
|
|
assert body["neue_fahrer"] == 1
|
|
# nichts geschrieben
|
|
assert client.db.scalars(select(Start).where(Start.turnier_id == tid)).first() is None
|
|
|
|
|
|
def test_echter_upload_dann_idempotent(client):
|
|
tid = _turnier(client)
|
|
payload = {
|
|
"quelle": "excel-auswertung-v3",
|
|
"idempotenz_schluessel": "abc-final",
|
|
"ergebnisse": [
|
|
{"startnummer": 1, "fahrer": {"nachname": "Bernius", "vorname": "Meik", "geburtsjahr": 1985},
|
|
"klasse": "S 6", "wertungsklasse": "S 5, S 9, S 3, S 6, S 7",
|
|
"summe_zeit": "105.43", "summe_fehler": 0,
|
|
"gesamt_erwartet": "105.43", "platzierung_erwartet": 1},
|
|
{"startnummer": 2, "fahrer": {"nachname": "Schmunk", "vorname": "Christian", "geburtsjahr": 1988},
|
|
"klasse": "S 9", "wertungsklasse": "S 5, S 9, S 3, S 6, S 7",
|
|
"summe_zeit": "103.26", "summe_fehler": 10,
|
|
"gesamt_erwartet": "113.26", "platzierung_erwartet": 2},
|
|
],
|
|
}
|
|
r1 = client.post(f"/api/v1/turniere/{tid}/ergebnisse", json=payload, headers=HEAD)
|
|
assert r1.status_code == 200, r1.text
|
|
b1 = r1.json()
|
|
assert b1["angelegt"] == 2
|
|
assert b1["abweichungen"] == [] # Nachrechnung deckt sich mit gesamt_erwartet
|
|
|
|
r2 = client.post(f"/api/v1/turniere/{tid}/ergebnisse", json=payload, headers=HEAD)
|
|
assert r2.json()["idempotent_uebersprungen"] is True
|
|
assert client.db.scalars(select(Start).where(Start.turnier_id == tid)).all().__len__() == 2
|
|
|
|
|
|
def test_abweichung_zur_erwartung_wird_gemeldet(client):
|
|
tid = _turnier(client)
|
|
payload = {
|
|
"quelle": "excel", "idempotenz_schluessel": "abw-1",
|
|
"ergebnisse": [
|
|
{"startnummer": 1, "fahrer": {"nachname": "A", "vorname": "A", "geburtsjahr": 1985},
|
|
"klasse": "S 6", "summe_zeit": "100.00", "summe_fehler": 0,
|
|
"gesamt_erwartet": "999.00", "platzierung_erwartet": 1},
|
|
],
|
|
}
|
|
r = client.post(f"/api/v1/turniere/{tid}/ergebnisse", json=payload, headers=HEAD)
|
|
body = r.json()
|
|
assert any(a["feld"] == "gesamt" for a in body["abweichungen"])
|
|
assert body["warnungen"]
|
|
|
|
|
|
def test_finalisieren_setzt_status_final(client):
|
|
tid = _turnier(client)
|
|
client.post(f"/api/v1/turniere/{tid}/ergebnisse", json={
|
|
"quelle": "x", "idempotenz_schluessel": "f1",
|
|
"ergebnisse": [
|
|
{"startnummer": 1, "fahrer": {"nachname": "A", "vorname": "A", "geburtsjahr": 1985},
|
|
"klasse": "S 6", "summe_zeit": "100.00", "summe_fehler": 0},
|
|
],
|
|
}, headers=HEAD)
|
|
r = client.post(f"/api/v1/turniere/{tid}/finalisieren", headers=HEAD)
|
|
assert r.status_code == 200
|
|
assert client.db.get(Turnier, tid).status.value == "final"
|
|
|
|
|
|
def test_turnier_wird_an_jahresmeisterschaft_gehaengt(client):
|
|
db = client.db
|
|
seed_saison_2026(db)
|
|
v = f.veranstaltung(db)
|
|
db.flush()
|
|
r = client.post("/api/v1/turniere", headers=HEAD, json={
|
|
"veranstaltung_id": v.id, "name": "Reinheim", "zaehlt_meisterschaft": True,
|
|
})
|
|
assert r.status_code == 201, r.text
|
|
m = f.meisterschaft(db)
|
|
assert r.json()["meisterschaft_id"] == m.id
|
|
|
|
# o. M. -> keine Verknuepfung
|
|
r2 = client.post("/api/v1/turniere", headers=HEAD, json={
|
|
"veranstaltung_id": v.id, "name": "Pokal o. M.", "zaehlt_meisterschaft": False,
|
|
})
|
|
assert r2.json()["meisterschaft_id"] is None
|
|
|
|
|
|
def test_meisterschaft_stand_liefert_alle_wertungen(client):
|
|
db = client.db
|
|
seed_saison_2026(db)
|
|
m = f.meisterschaft(db)
|
|
r = client.get(f"/api/v1/meisterschaften/{m.id}/stand")
|
|
assert r.status_code == 200
|
|
codes = {w["wertung"] for w in r.json()["wertungen"]}
|
|
assert "erwachsene" in codes
|
|
# eine einzelne Wertung gezielt abrufen
|
|
r2 = client.get(f"/api/v1/meisterschaften/{m.id}/stand", params={"wertung": "J 3"})
|
|
assert r2.status_code == 200
|
|
assert r2.json()["wertung"] == "J 3"
|