From 459f3f8e14080199e352486161fc4b766de5c1f4 Mon Sep 17 00:00:00 2001 From: Erol Haagenrud Date: Sat, 18 Jul 2026 22:21:22 +0200 Subject: [PATCH] =?UTF-8?q?Backend=20er=20live=20og=20grundig=20verifisert?= =?UTF-8?q?=20med=20n=C3=B8yaktig=20scenarioet=20du=20beskrev=20(10=20hull?= =?UTF-8?q?=20spilt=20under=20feil=20bane,=20matchen=20avgjort,=20rettet?= =?UTF-8?q?=20til=20riktig=20bane=20=E2=80=94=20tee-er=20byttet,=20handica?= =?UTF-8?q?p=20regnet=20p=C3=A5=20nytt,=20matchstatus=20oppdatert,=20de=20?= =?UTF-8?q?faktiske=20slagene=20ur=C3=B8rt).=20.md-filene=20er=20oppdatert?= =?UTF-8?q?.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ikke bygget ennå: selve rediger-/slett-knappene i program-skjermen — det er ren backend-kapasitet nå. Vil du at jeg skriver en V0-prompt for det, eller er det noe annet du vil ta fatt på? --- .claude/settings.local.json | 7 +- CLAUDE.md | 75 +++++++++++++++ FEATURE_BACKLOG.md | 29 ++++++ app/routers/courses.py | 25 ++++- app/routers/scoring.py | 6 +- app/routers/tournaments.py | 186 ++++++++++++++++++++++++++++++++++++ 6 files changed, 322 insertions(+), 6 deletions(-) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index b97e827..834a4fe 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -265,7 +265,12 @@ "Bash(docker exec teeoff_db psql -U teeoff_admin -d teecup_scratch -c ' *)", "Bash(rm -f /tmp/claude-1000/-opt-teecup/a8bd2fc3-4b9c-4682-a2be-cf36e143de78/scratchpad/scratch.env /tmp/claude-1000/-opt-teecup/a8bd2fc3-4b9c-4682-a2be-cf36e143de78/scratchpad/scratch_app_pw.txt /tmp/claude-1000/-opt-teecup/a8bd2fc3-4b9c-4682-a2be-cf36e143de78/scratchpad/minio_user.txt /tmp/claude-1000/-opt-teecup/a8bd2fc3-4b9c-4682-a2be-cf36e143de78/scratchpad/minio_pass.txt /tmp/lb_cookies.txt)", "Bash(mkdir -p /tmp/claude-1000/-opt-teecup/a8bd2fc3-4b9c-4682-a2be-cf36e143de78/scratchpad/v0-zip-8)", - "Bash(rm -f /tmp/claude-1000/-opt-teecup/a8bd2fc3-4b9c-4682-a2be-cf36e143de78/scratchpad/scratch.env /tmp/claude-1000/-opt-teecup/a8bd2fc3-4b9c-4682-a2be-cf36e143de78/scratchpad/scratch_app_pw.txt /tmp/claude-1000/-opt-teecup/a8bd2fc3-4b9c-4682-a2be-cf36e143de78/scratchpad/minio_user.txt /tmp/claude-1000/-opt-teecup/a8bd2fc3-4b9c-4682-a2be-cf36e143de78/scratchpad/minio_pass.txt /tmp/lb2_cookies.txt)" + "Bash(rm -f /tmp/claude-1000/-opt-teecup/a8bd2fc3-4b9c-4682-a2be-cf36e143de78/scratchpad/scratch.env /tmp/claude-1000/-opt-teecup/a8bd2fc3-4b9c-4682-a2be-cf36e143de78/scratchpad/scratch_app_pw.txt /tmp/claude-1000/-opt-teecup/a8bd2fc3-4b9c-4682-a2be-cf36e143de78/scratchpad/minio_user.txt /tmp/claude-1000/-opt-teecup/a8bd2fc3-4b9c-4682-a2be-cf36e143de78/scratchpad/minio_pass.txt /tmp/lb2_cookies.txt)", + "Bash(rm -f /tmp/claude-1000/-opt-teecup/a8bd2fc3-4b9c-4682-a2be-cf36e143de78/scratchpad/scratch.env /tmp/claude-1000/-opt-teecup/a8bd2fc3-4b9c-4682-a2be-cf36e143de78/scratchpad/scratch_app_pw.txt /tmp/claude-1000/-opt-teecup/a8bd2fc3-4b9c-4682-a2be-cf36e143de78/scratchpad/minio_user.txt /tmp/claude-1000/-opt-teecup/a8bd2fc3-4b9c-4682-a2be-cf36e143de78/scratchpad/minio_pass.txt /tmp/idem_cookies.txt)", + "Bash(docker exec teeoff_db psql -U teeoff_admin -d teecup_db -v ON_ERROR_STOP=1 -c ' *)", + "Bash(python3 -m py_compile /opt/teecup/app/routers/tournaments.py /opt/teecup/app/routers/scoring.py)", + "Bash(rm -f /tmp/claude-1000/-opt-teecup/a8bd2fc3-4b9c-4682-a2be-cf36e143de78/scratchpad/scratch.env /tmp/claude-1000/-opt-teecup/a8bd2fc3-4b9c-4682-a2be-cf36e143de78/scratchpad/scratch_app_pw.txt /tmp/claude-1000/-opt-teecup/a8bd2fc3-4b9c-4682-a2be-cf36e143de78/scratchpad/minio_user.txt /tmp/claude-1000/-opt-teecup/a8bd2fc3-4b9c-4682-a2be-cf36e143de78/scratchpad/minio_pass.txt /tmp/patch_cookies.txt)", + "Bash(awk 'NR==499,/^---$/{print NR\": \"$0}' /opt/teecup/FEATURE_BACKLOG.md)" ], "additionalDirectories": [ "/opt/teeoff/deploy", diff --git a/CLAUDE.md b/CLAUDE.md index 5a0e792..f9a9329 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -965,6 +965,81 @@ Ferdig og verifisert: **Hele "bygg i rekkefølgen ting brukes"-serien for match-play-flyten er dermed komplett:** oppsett (lag/roster) → program → blind draw → scorekort → leaderboard. +- **Offisiell bane-import: idempotent + tydeligere navn, LIVE (2026-07-18), + rapportert av brukeren som faktisk brukte funksjonen på ekte + produksjonsdata:** to reelle problemer, begge funnet ved å lese (kun + lesing) de faktiske dataene for "De Gamle er Eldst" FØR noe ble antatt. + 1. **`POST .../courses/official-import` var IKKE idempotent** — å + importere samme bane på nytt (helt vanlig: flere økter spilles ofte + på samme bane) ga en 409 `DUPLICATE`-feil (migrasjon 010 sin sperre) + i stedet for å bare gi tilbake den allerede importerte banen. Fikset: + sjekker nå `external_course_ref` FØR noe teeoff-kall gjøres — finnes + banen fra før, returneres den eksisterende raden direkte (også + raskere, og robust mot at teeoff er nede akkurat da). + 2. **Lagret navn var kun selve banens navn ("Hovedbanen"), ikke hvilken + klubb** — ubrukelig til å skille baner fra hverandre, siden mange + klubber navngir hovedbanen sin identisk. Fikset: navnet kombineres nå + til "{anlegg} – {bane}" (f.eks. "Tjøme Golfklubb – Hovedbanen") ved + import. + **Reell konsekvens av hull #1 funnet i produksjonsdata:** brukeren hadde, + mens hen forsøkte å søke opp Tjøme via det ØVERSTE banefeltet (som søker + organisasjonens EGNE baner, ikke teeoff), ved et uhell trigget «Opprett + ny bane: «Tj»»-snarveien og fått en tom, søppel `custom`-bane hengende på + Foursome-økten -- en ekte forvekslingsfelle mellom de to adskilte + bane-søkeflatene (eget vs. teeoff), ikke en kodefeil i seg selv. + **Data ryddet opp i EKTE `teecup_db`, bruker bekreftet eksplisitt:** + Foursome-økten pekt om til den allerede importerte, ekte Tjøme-banen + (samme bane som Fourball-økten allerede brukte -- nøyaktig det + organisatoren egentlig ønsket), søppel-«Tj»-banen slettet (bekreftet + ingen matcher/tee-er/hull hang på den FØR sletting), og den ekte banens + navn oppdatert til "Tjøme Golfklubb – Hovedbanen". Verifisert i etterkant + at begge økter nå peker til samme, korrekt navngitte bane. + **Verifisert mot ekte teeoff_api i scratch FØR utrulling:** importerte + Borregaard på nytt to ganger — andre kallet ga nøyaktig samme course-id + (ikke en duplikat-rad), navnet kom ut som "Borregaard Golfklubb – + Hovedbanen". `test_isolation.sql` 12/12. Ingen migrasjon, kun `teecup_api` + redeployet, `teeoff.no` upåvirket. +- **`PATCH`/`DELETE` for økter, LIVE (2026-07-18), samme dag:** brukeren + spurte rett etter opprydningen om det i det hele tatt var mulig å + rette/slette en feiloppsatt økt — det var det ikke (kun `POST`/`GET` + fantes). Ny `PATCH /orgs/{id}/sessions/{id}` (`app/routers/ + tournaments.py`) for enkle felt (navn, klokkeslett/intervall, starthull, + poeng, handicap-brytere) via vanlig `exclude_unset`-mønster, PLUSS en egen + gren for bane-bytte. Ny `DELETE /orgs/{id}/sessions/{id}` — kun tomme + økter (ingen matcher), avviser med 409 ellers (bruk PATCH til å korrigere + i stedet). + **Banebytte-scenarioet brukeren selv reiste** ("5 hull spilt, oppdager + feil bane — slagene er ekte, utregningen er trolig feil") krevde egen + design: `match_participant.tee_id` peker til en tee som HØRER til den + gamle banen. Løst med `_remap_course()` — finner en tee med samme + navn+kjønn på den nye banen for hver allerede tillagte deltaker, flytter + dem dit, og avviser HELE bane-byttet tydelig (400, ingenting skrevet, + bekreftet transaksjonell rollback) hvis den nye banen mangler en + tilsvarende tee. Etter et vellykket bytte: handicap regnes om for alle + berørte deltakere, og matchstatus/poeng regnes om for HVER match i + økten — **bevisst uavhengig av om matchen allerede er avgjort** (brukeren + bekreftet eksplisitt at en bane-korrigering skal kunne endre et allerede + cachet resultat). `recompute_and_cache_match_state` i `app/routers/ + scoring.py` gjort delt (fjernet ledende understrek) for gjenbruk fra + tournaments.py. + **Reelt, urelatert funn underveis i scratch-testingen, IKKE fikset:** + `tee_rating` lages i dag ALLTID kun med `full_18`-omfang (både ved + teeoff-import og manuell tee-opprettelse) — en økt satt til `front_9`/ + `back_9` i `stroke`-modus kan derfor ALDRI få handicap beregnet + (`compute_and_store_side_handicaps` sin `tee_rating`-join finner aldri + noen rad), og dermed aldri avgjøre noen hull. `hole_result`-modus + upåvirket. Flagget til bruker, bevisst latt urørt denne runden. + **Verifisert grundig i scratch:** enkelt feltbytte (kun navn), fullt + banebytte-scenario bygget nøyaktig som brukerens eksempel (10 hull spilt + under feil bane, matchen allerede avgjort 10&8, PATCH til riktig bane → + tee-er ombyttet korrekt, handicap endret fra 7/9 til 10/13 under den nye + banens rating, matchstatus regnet på nytt med UENDREDE rå slagtall), + avvist bane-bytte ved manglende tee-match (bekreftet full rollback, + også av det urelaterte navnefeltet i samme kall), DELETE avvist på økt + med match (409) og godtatt på tom økt (204). `test_isolation.sql` 12/12. + Ingen migrasjon, kun `teecup_api` redeployet, `teeoff.no` upåvirket. + Frontend (rediger-/slett-knapper i program-skjermen) ikke bygget ennå — + kun backend-kapasiteten denne runden. Neste steg: 1. PWA-egenskaper (manifest, service worker, offline-cache) — ikke startet. diff --git a/FEATURE_BACKLOG.md b/FEATURE_BACKLOG.md index 079b6f7..42ce37e 100644 --- a/FEATURE_BACKLOG.md +++ b/FEATURE_BACKLOG.md @@ -564,6 +564,19 @@ andre skjermen når den ble generert i en egen prompt. (`teecup_api`, `teecup_frontend`) bygget og redeployet, live sjekker OK (`/health`, `/dashboard` → 200), `teeoff.no` upåvirket. +**Nytt 2026-07-18, ✅ BACKEND LIVE (frontend gjenstår):** `PATCH`/`DELETE` +for økter (`app/routers/tournaments.py`) — kunne tidligere verken rettes +eller slettes etter opprettelse. `DELETE` kun for tomme økter (409 hvis den +har matcher). `PATCH` dekker enkle felt fritt, pluss en egen, forsiktig +gren for bane-bytte (finner/flytter tilsvarende tee per allerede tillagt +deltaker, regner om handicap+matchstatus for hele økten etterpå — også for +allerede AVGJORTE matcher, bekreftet eksplisitt av bruker). Se +CLAUDE.md-status for det fulle scenarioet (verifisert med et 10-hulls +avgjort-match-eksempel) og et urelatert funn (`front_9`/`back_9` + +`stroke`-modus kan aldri få handicap i dag, siden `tee_rating` alltid kun +lages med `full_18`-omfang). Rediger-/slett-UI i program-skjermen ikke +bygget ennå. + --- ### Offisiell banedata fra teeoff — ✅ BYGGET OG LIVE 2026-07-18 (ADR-019) @@ -610,6 +623,22 @@ vasket bort `?org=...`-parameteren fra URL-en. Fikset ved å fjerne det indre `
`-elementet (vanlig `
` + Enter-tast/knapp-klikk i stedet). Se CLAUDE.md-status for full root cause. +**Nok en reell bug funnet og fikset samme uke, rapportert fra ekte bruk mot +`teecup.teeoff.no`:** import av samme teeoff-bane til flere økter (helt +normalt — flere runder spilles ofte på samme bane) ga en 409-feil i stedet +for å bare gjenbruke banen, og det lagrede navnet ("Hovedbanen" alene) ga +ingen måte å se hvilken klubb det gjaldt. Fikset: `POST .../courses/ +official-import` er nå idempotent (gir tilbake eksisterende rad ved +reimport, sjekket FØR teeoff-kallet), og navnet lagres nå som +"{anlegg} – {bane}". Ekte produksjonsdata for "De Gamle er Eldst" ryddet +opp (en økt hadde ved et uhell fått en tom søppel-`custom`-bane — se +CLAUDE.md-status for full hendelse og rotårsak). +**Åpent, ikke løst i denne runden:** de to bane-søkeflatene (øverste felt += organisasjonens egne baner + "opprett ny"-snarvei, "Hent bane fra +teeoff"-knappen lenger ned = offisielt søk) er lette å forveksle — det var +nettopp dette som forårsaket søppel-banen. Vurder en tydeligere UI- +sammenslåing eller rekkefølge-endring i en senere runde. + --- ## UX / frontend (senere fase) diff --git a/app/routers/courses.py b/app/routers/courses.py index 3bf8979..cb979dd 100644 --- a/app/routers/courses.py +++ b/app/routers/courses.py @@ -326,6 +326,23 @@ async def import_official_course( body: OfficialCourseImport, organization_id: str = Depends(get_authorized_org), ) -> Course: + external_ref = f"{body.facility_slug}:{body.teeoff_course_id}" + + # Idempotent: importerer organisatoren samme bane på nytt (vanlig -- + # flere økter spilles ofte på samme bane), gi tilbake den EKSISTERENDE + # raden i stedet for en 409-feil. Sjekket FØR noe teeoff-kall gjøres, + # både raskere og mer robust mot at teeoff er nede akkurat da. Fant + # og fikset 2026-07-18 etter en reell brukerrapport: duplikat-sperren + # (migrasjon 010) var riktig i prinsippet, men ga ingen vei videre for + # den helt vanlige "bruk samme bane på runde 2" siden av saken. + async with org_connection(organization_id) as conn: + existing = await conn.fetchrow( + f"SELECT {_COURSE_COLUMNS} FROM course WHERE external_course_ref = $1", + external_ref, + ) + if existing is not None: + return Course(**dict(existing)) + try: facility = await teeoff_client.get_facility(body.facility_slug) except teeoff_client.TeeoffNotFoundError: @@ -372,9 +389,13 @@ async def import_official_course( "Banen har ingen tee med registrert rating/slope i teeoff ennå.", ) - external_ref = f"{body.facility_slug}:{body.teeoff_course_id}" par_total = sum(h["par"] for h in holes) + # Navn kombinerer anlegg + bane -- "Hovedbanen" alene er tvetydig (mange + # klubber navngir hovedbanen sin nøyaktig likt). Fant og fikset samme + # runde som idempotent-fiksen over, samme bakenforliggende brukerrapport. + course_name = f"{facility.get('name')} – {course_data.get('name') or 'Bane'}" + async with org_connection(organization_id) as conn, translate_db_errors(): course_row = await conn.fetchrow( f""" @@ -383,7 +404,7 @@ async def import_official_course( RETURNING {_COURSE_COLUMNS} """, organization_id, - course_data.get("name") or facility.get("name"), + course_name, external_ref, ) course_id = course_row["id"] diff --git a/app/routers/scoring.py b/app/routers/scoring.py index a689a27..a60d893 100644 --- a/app/routers/scoring.py +++ b/app/routers/scoring.py @@ -148,7 +148,7 @@ async def _compute_hole_results(conn, match_id: str, match) -> list[HoleResult]: return results -async def _recompute_and_cache_match_state(conn, match_id: str) -> None: +async def recompute_and_cache_match_state(conn, match_id: str) -> None: match = await conn.fetchrow( """ SELECT m.id::text, s.format, s.scoring_mode, @@ -311,7 +311,7 @@ async def submit_hole_score( body.gross_strokes, ) - await _recompute_and_cache_match_state(conn, match_id) + await recompute_and_cache_match_state(conn, match_id) return HoleScoreOut(**dict(row)) @@ -386,7 +386,7 @@ async def submit_hole_result( body.winning_side, ) - await _recompute_and_cache_match_state(conn, match_id) + await recompute_and_cache_match_state(conn, match_id) return HoleResultOut(**dict(row)) diff --git a/app/routers/tournaments.py b/app/routers/tournaments.py index 28e5f41..f3444e3 100644 --- a/app/routers/tournaments.py +++ b/app/routers/tournaments.py @@ -17,6 +17,8 @@ from ..auth import get_authorized_org from ..blind_draw import locked_team_ids from ..db import org_connection from ..errors import app_error, translate_db_errors +from ..handicap import compute_and_store_side_handicaps, parse_allowance_config +from .scoring import recompute_and_cache_match_state from .. import storage router = APIRouter() @@ -542,6 +544,190 @@ async def create_session( return _session_out(row, set()) +class SessionUpdate(BaseModel): + """Bevisst IKKE format/scoring_mode/hole_config ennå -- disse har dypere + strukturelle konsekvenser for allerede registrerte scores (bytter man + scoring_mode havner gamle oppføringer i feil tabell; bytter man format + endres reglene for match_participant) som fortjener egen, senere design. + course_id er den ENESTE feltet som krever spesialhåndtering her (se + _remap_course) -- resten er en vanlig exclude_unset-oppdatering.""" + + name: str | None = None + scheduled_at: datetime | None = None + tee_interval_minutes: int | None = None + start_hole: int | None = Field(default=None, ge=1, le=18) + points_per_match: float | None = None + allowance_override: dict | None = None + course_id: str | None = None + + +async def _remap_course(conn, session_id: str, new_course_id: str) -> None: + """Reist av brukeren (2026-07-18): en organisator oppdager MIDT I RUNDEN + at feil bane var satt opp -- de faktisk registrerte slagene er ekte og + skal IKKE røres, men `match_participant.tee_id` peker til en tee som + hører til DEN GAMLE banen, og må flyttes til en tilsvarende tee på den + nye for at handicap-beregningen skal bli riktig videre. + + Matcher på tee-navn + kjønn (case-insensitive). Finnes ingen tilsvarende + tee på den nye banen for én eller flere spillere, avvises HELE + bane-byttet tydelig FØR noe skrives -- heller stoppe enn å gjette feil + (samme "fail loudly" prinsipp som ADR-019 sin importvalidering). + """ + course_exists = await conn.fetchval("SELECT id FROM course WHERE id = $1", new_course_id) + if course_exists is None: + raise app_error(404, "NOT_FOUND", "Banen finnes ikke.") + + participants = await conn.fetch( + """ + SELECT mp.id::text AS id, t.name AS tee_name, t.gender AS tee_gender + FROM match_participant mp + JOIN match m ON m.id = mp.match_id + JOIN tee t ON t.id = mp.tee_id + WHERE m.session_id = $1 + """, + session_id, + ) + + remap: dict[str, str] = {} + missing: set[str] = set() + for p in participants: + new_tee_id = await conn.fetchval( + """ + SELECT id::text FROM tee + WHERE course_id = $1 AND lower(name) = lower($2) AND gender IS NOT DISTINCT FROM $3 + """, + new_course_id, + p["tee_name"], + p["tee_gender"], + ) + if new_tee_id is None: + missing.add(p["tee_name"]) + else: + remap[p["id"]] = new_tee_id + + if missing: + raise app_error( + 400, + "VALIDATION_FAILED", + "Den nye banen mangler en tee som matcher: " + + ", ".join(sorted(missing)) + + ". Legg til tilsvarende tee på den nye banen før du bytter.", + ) + + for participant_id, new_tee_id in remap.items(): + await conn.execute("UPDATE match_participant SET tee_id = $1 WHERE id = $2", new_tee_id, participant_id) + + await conn.execute("UPDATE session SET course_id = $1 WHERE id = $2", new_course_id, session_id) + + +async def _recompute_session_matches( + conn, session_id: str, format_: str, hole_config: str, allowance_override: dict | None +) -> None: + """Regner om handicap + matchstatus for HVER match i økten -- brukt etter + et bane- eller allowance-bytte. Bevisst UAVHENGIG av om en match allerede + er avgjort (brukeren bekreftet eksplisitt: en bane-korrigering skal kunne + endre et allerede cachet resultat, ikke bare blokkeres av den vanlige + ALREADY_DECIDED-sperren som gjelder NYE slag-innsendinger).""" + config = parse_allowance_config(format_, allowance_override) + matches = await conn.fetch("SELECT id::text AS id FROM match WHERE session_id = $1", session_id) + for m in matches: + for side in ("a", "b"): + await compute_and_store_side_handicaps(conn, m["id"], side, format_, hole_config, config) + await recompute_and_cache_match_state(conn, m["id"]) + + +@router.patch("/orgs/{organization_id}/sessions/{session_id}", response_model=SessionOut) +async def update_session( + session_id: str, + body: SessionUpdate, + organization_id: str = Depends(get_authorized_org), +) -> SessionOut: + updates = body.model_dump(exclude_unset=True) + if not updates: + raise app_error(400, "VALIDATION_FAILED", "Ingen felt å oppdatere.") + + async with org_connection(organization_id) as conn, translate_db_errors(): + session = await conn.fetchrow( + "SELECT format, hole_config::text AS hole_config, course_id::text AS course_id FROM session WHERE id = $1", + session_id, + ) + if session is None: + raise app_error(404, "NOT_FOUND", "Økten finnes ikke.") + + new_course_id = updates.pop("course_id", None) + allowance_or_points_changed = "allowance_override" in updates or "points_per_match" in updates + + if updates: + set_clauses = [] + values: list = [] + for i, (key, value) in enumerate(updates.items(), start=1): + if key == "allowance_override": + set_clauses.append(f"allowance_override = ${i}::jsonb") + values.append(json.dumps(value) if value is not None else None) + else: + set_clauses.append(f"{key} = ${i}") + values.append(value) + values.append(session_id) + await conn.execute( + f"UPDATE session SET {', '.join(set_clauses)} WHERE id = ${len(values)}", + *values, + ) + + if new_course_id is not None and new_course_id != session["course_id"]: + await _remap_course(conn, session_id, new_course_id) + allowance_or_points_changed = True # banebytte krever alltid omregning + + if allowance_or_points_changed: + current = await conn.fetchrow( + "SELECT allowance_override::text AS allowance_override FROM session WHERE id = $1", + session_id, + ) + current_override = json.loads(current["allowance_override"]) if current["allowance_override"] else None + await _recompute_session_matches( + conn, session_id, session["format"], session["hole_config"], current_override + ) + + row = await conn.fetchrow( + """ + SELECT id::text, sequence, name, format, hole_config::text AS hole_config, + course_id::text, points_per_match::float AS points_per_match, + allowance_override::text AS allowance_override, scoring_mode, + scheduled_at, tee_interval_minutes, start_hole + FROM session WHERE id = $1 + """, + session_id, + ) + locked = await locked_team_ids(conn, session_id) + + return _session_out(row, locked) + + +@router.delete("/orgs/{organization_id}/sessions/{session_id}", status_code=204) +async def delete_session( + session_id: str, + organization_id: str = Depends(get_authorized_org), +) -> None: + """Kun tomme økter kan slettes (ingen matcher opprettet ennå) -- dekker + "jeg satte opp feil økt, fjern den" FØR den er tatt i bruk. En økt med + matcher (og dermed potensielt registrerte scores/poeng) må rettes med + PATCH (bane-/felt-korrigering) i stedet for å kunne slettes -- hindrer + at noen ved et uhell sletter ekte kamper/resultater.""" + async with org_connection(organization_id) as conn: + session = await conn.fetchrow("SELECT id FROM session WHERE id = $1", session_id) + if session is None: + raise app_error(404, "NOT_FOUND", "Økten finnes ikke.") + + match_count = await conn.fetchval("SELECT count(*) FROM match WHERE session_id = $1", session_id) + if match_count > 0: + raise app_error( + 409, + "VALIDATION_FAILED", + "Økten har allerede matcher og kan ikke slettes -- bruk PATCH for å korrigere den i stedet.", + ) + + await conn.execute("DELETE FROM session WHERE id = $1", session_id) + + # --------------------------------------------------------------------------- # Sponsorer (ADR-018) -- navn+lenke+logo, alle aktive fra MinIO-runden. # ---------------------------------------------------------------------------