diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 32a32bd..ea4776f 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -84,7 +84,9 @@ "Bash(curl -s -w '\\\\n[HTTP %{http_code}]\\\\n' -X POST -H 'X-Debug-User-Id: a1000000-0000-0000-0000-000000000001' -H 'Content-Type: application/json' -d '{\"team_side\":\"a\",\"match_participant_id\":\"c81ed8c5-31f6-468f-8c10-8eba6d637fc3\",\"hole_number\":1,\"gross_strokes\":6}' http://127.0.0.1:8099/orgs/11111111-1111-1111-1111-111111111111/matches/aebcd63e-f452-4326-8a94-89bde1ace11d/hole-scores)", "Bash(curl -s -w '\\\\n[HTTP %{http_code}]\\\\n' -X POST -H 'X-Debug-User-Id: b2000000-0000-0000-0000-000000000002' -H 'Content-Type: application/json' -d '{\"team_side\":\"a\",\"match_participant_id\":\"c81ed8c5-31f6-468f-8c10-8eba6d637fc3\",\"hole_number\":16,\"gross_strokes\":5}' http://127.0.0.1:8099/orgs/11111111-1111-1111-1111-111111111111/matches/aebcd63e-f452-4326-8a94-89bde1ace11d/hole-scores)", "Bash(curl -s -w '\\\\n[HTTP %{http_code}]\\\\n' -H 'X-Debug-User-Id: __TRACKED_VAR__' __TRACKED_VAR__/orgs/__TRACKED_VAR__/matches/__TRACKED_VAR__/scorecard)", - "Bash(rm -f __TRACKED_VAR__/002_scratch.sql __TRACKED_VAR__/002_wrapper.sql __TRACKED_VAR__/hr_ids.env)" + "Bash(rm -f __TRACKED_VAR__/002_scratch.sql __TRACKED_VAR__/002_wrapper.sql __TRACKED_VAR__/hr_ids.env)", + "Bash(python3 -m py_compile /opt/teecup/app/routers/scoring.py)", + "Bash(rm -f __TRACKED_VAR__/002_scratch.sql __TRACKED_VAR__/002_wrapper.sql __TRACKED_VAR__/lock_ids.env)" ] } } diff --git a/FEATURE_BACKLOG.md b/FEATURE_BACKLOG.md index 600f653..cabfafa 100644 --- a/FEATURE_BACKLOG.md +++ b/FEATURE_BACKLOG.md @@ -50,19 +50,19 @@ upsert (`ON CONFLICT ... DO UPDATE`), så en korrigering er ikke skilt fra en førstegangs-innføring. Ingen godkjenning fra motstanderen, ingen audit-trail (forrige verdi overskrives sporløst). -- **Hvem kan «lukke» en match eller turnering i dag:** INGEN eksplisitt - handling finnes. En match sin `status_text`/poeng blir bare cached - automatisk når `handicap_engine.compute_match_state` sier den er avgjort - (eller 18 hull er spilt) — det er ikke en handling noen utfører. **Reell, - ikke-teoretisk konsekvens oppdaget ved gjennomgang av dette:** ingenting - hindrer at noen fortsetter å legge inn hull ETTER at matchen matematisk er - avgjort (f.eks. «10&5» ved hull 13) — blir hull 14-18 likevel registrert, - regnes de med i sekvensen ved neste rekalkulering og kan endre den cachede - marginen, siden `compute_match_state` teller alle sammenhengende avgjorte - hull, ikke bare fram til avgjørelsespunktet. Ingen sperre bygget for dette. +- **Match-lås ved avgjørelse: ✅ FIKSET 2026-07-16.** `submit_hole_score` og + `submit_hole_result` (`app/routers/scoring.py`) avviser nå med 409 («Matchen + er avgjort og kan ikke lenger endres.») FØR upserten kjøres, hvis + `match.points_side_a IS NOT NULL` — dette er allerede et pålitelig, + entydig signal siden kolonnen kun settes når `compute_match_state` sier + matchen er avgjort. Gjelder BÅDE nye hull og korrigering av allerede talte + hull. Verifisert: hull etter avgjørelse avvist, korrigering av et + talt hull avvist, GET scorecard fortsatt leselig, ikke-avgjorte matcher + upåvirket. **Bevisst IKKE bygget:** ingen manuell «avslutt match før den + er avgjort»-handling (det grenser mot walkover under, fortsatt utsatt). Turnering har et `status`-felt (draft/active/completed/archived) men INGEN endepunkt endrer det ennå — organisator kan ikke markere en turnering - ferdig via API-et i dag. + ferdig via API-et i dag (eget, senere punkt). - **Manglende WO/konsesjon:** hvis en side aldri stiller nok spillere (`match_participant`-antallet når aldri det økten krever), beregnes handicap ALDRI (se `app/handicap.py`), og matchen kan derfor ALDRI få et @@ -70,11 +70,9 @@ «gi bort hullet/matchen/turneringen»-mekanisme (walkover/konsesjon) i det hele tatt ennå — verken datamodell eller endepunkt. - **Avgjort 2026-07-16:** - - **Match-lås bygges SNART** (neste lille runde) — eksplisitt handling som - blokkerer videre hull-innlegging etter at en match er ferdig, og tetter - «spøkelses-hull etter avgjørelse»-buggen over. Hvem som får lås/åpne den - (kaptein? organisator? samme «rostret på laget»-bar som resten?) avgjøres - når den bygges — se punkt (a)/(b) fortsatt åpne under. + - **Match-lås ved avgjørelse: ✅ bygget** — se eget punkt over. Automatisk + (ikke en handling noen utfører), så «hvem får låse» ble aldri et + spørsmål som trengte avklaring. - **Walkover/konsesjon VENTER** til brukerroller (kaptein/organisator, punktet over) er avgjort — å bygge den nå på dagens løse «rostret på laget»-grense betyr sannsynligvis å bygge den om senere. diff --git a/app/routers/scoring.py b/app/routers/scoring.py index 5309175..d982f1f 100644 --- a/app/routers/scoring.py +++ b/app/routers/scoring.py @@ -218,6 +218,7 @@ async def submit_hole_score( match = await conn.fetchrow( """ SELECT m.team_a_id::text AS team_a_id, m.team_b_id::text AS team_b_id, + m.points_side_a::float AS points_side_a, s.format, s.scoring_mode, s.hole_config::text AS hole_config FROM match m JOIN session s ON s.id = m.session_id WHERE m.id = $1 @@ -226,6 +227,11 @@ async def submit_hole_score( ) if match is None: raise HTTPException(status.HTTP_404_NOT_FOUND, detail="Matchen finnes ikke.") + if match["points_side_a"] is not None: + raise HTTPException( + status.HTTP_409_CONFLICT, + detail="Matchen er avgjort og kan ikke lenger endres.", + ) if match["scoring_mode"] != "stroke": raise HTTPException( status.HTTP_400_BAD_REQUEST, @@ -338,6 +344,7 @@ async def submit_hole_result( match = await conn.fetchrow( """ SELECT m.team_a_id::text AS team_a_id, m.team_b_id::text AS team_b_id, + m.points_side_a::float AS points_side_a, s.scoring_mode, s.hole_config::text AS hole_config FROM match m JOIN session s ON s.id = m.session_id WHERE m.id = $1 @@ -346,6 +353,11 @@ async def submit_hole_result( ) if match is None: raise HTTPException(status.HTTP_404_NOT_FOUND, detail="Matchen finnes ikke.") + if match["points_side_a"] is not None: + raise HTTPException( + status.HTTP_409_CONFLICT, + detail="Matchen er avgjort og kan ikke lenger endres.", + ) if match["scoring_mode"] != "hole_result": raise HTTPException( status.HTTP_400_BAD_REQUEST,