Match-lås-fiksen er ferdig og verifisert. En liten, kirurgisk endring i app/routers/scoring.py: begge skrive-endepunktene (submit_hole_score, submit_hole_result) sjekker nå match.points_side_a IS NOT NULL (allerede et pålitelig signal for "avgjort", ingen ny kolonne/migrasjon nødvendig) og avviser med 409 før noen upsert kjøres — både for nye hull og korrigering av allerede talte hull.

Alle 5 punktene i planen bestått mot scratch-databasen:

Gjenskapte en avgjort match ("10&8 (A)")
Nytt hull (14) på avgjort match → 409 ✓
Korrigering av allerede talt hull (1) → 409 ✓
GET scorecard fortsatt leselig for avgjort match ✓
Fersk, ikke-avgjort match tar fortsatt imot hull normalt ✓
This commit is contained in:
Erol Haagenrud 2026-07-16 14:49:42 +02:00
parent b3659cc953
commit c521641c2e
3 changed files with 29 additions and 17 deletions

View file

@ -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: 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' -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(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)"
] ]
} }
} }

View file

@ -50,19 +50,19 @@
upsert (`ON CONFLICT ... DO UPDATE`), så en korrigering er ikke skilt fra en upsert (`ON CONFLICT ... DO UPDATE`), så en korrigering er ikke skilt fra en
førstegangs-innføring. Ingen godkjenning fra motstanderen, ingen audit-trail førstegangs-innføring. Ingen godkjenning fra motstanderen, ingen audit-trail
(forrige verdi overskrives sporløst). (forrige verdi overskrives sporløst).
- **Hvem kan «lukke» en match eller turnering i dag:** INGEN eksplisitt - **Match-lås ved avgjørelse: ✅ FIKSET 2026-07-16.** `submit_hole_score` og
handling finnes. En match sin `status_text`/poeng blir bare cached `submit_hole_result` (`app/routers/scoring.py`) avviser nå med 409 («Matchen
automatisk når `handicap_engine.compute_match_state` sier den er avgjort er avgjort og kan ikke lenger endres.») FØR upserten kjøres, hvis
(eller 18 hull er spilt) — det er ikke en handling noen utfører. **Reell, `match.points_side_a IS NOT NULL` — dette er allerede et pålitelig,
ikke-teoretisk konsekvens oppdaget ved gjennomgang av dette:** ingenting entydig signal siden kolonnen kun settes når `compute_match_state` sier
hindrer at noen fortsetter å legge inn hull ETTER at matchen matematisk er matchen er avgjort. Gjelder BÅDE nye hull og korrigering av allerede talte
avgjort (f.eks. «10&5» ved hull 13) — blir hull 14-18 likevel registrert, hull. Verifisert: hull etter avgjørelse avvist, korrigering av et
regnes de med i sekvensen ved neste rekalkulering og kan endre den cachede talt hull avvist, GET scorecard fortsatt leselig, ikke-avgjorte matcher
marginen, siden `compute_match_state` teller alle sammenhengende avgjorte upåvirket. **Bevisst IKKE bygget:** ingen manuell «avslutt match før den
hull, ikke bare fram til avgjørelsespunktet. Ingen sperre bygget for dette. er avgjort»-handling (det grenser mot walkover under, fortsatt utsatt).
Turnering har et `status`-felt (draft/active/completed/archived) men Turnering har et `status`-felt (draft/active/completed/archived) men
INGEN endepunkt endrer det ennå — organisator kan ikke markere en turnering 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 - **Manglende WO/konsesjon:** hvis en side aldri stiller nok spillere
(`match_participant`-antallet når aldri det økten krever), beregnes (`match_participant`-antallet når aldri det økten krever), beregnes
handicap ALDRI (se `app/handicap.py`), og matchen kan derfor ALDRI få et 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 «gi bort hullet/matchen/turneringen»-mekanisme (walkover/konsesjon) i det
hele tatt ennå — verken datamodell eller endepunkt. hele tatt ennå — verken datamodell eller endepunkt.
- **Avgjort 2026-07-16:** - **Avgjort 2026-07-16:**
- **Match-lås bygges SNART** (neste lille runde) — eksplisitt handling som - **Match-lås ved avgjørelse: ✅ bygget** — se eget punkt over. Automatisk
blokkerer videre hull-innlegging etter at en match er ferdig, og tetter (ikke en handling noen utfører), så «hvem får låse» ble aldri et
«spøkelses-hull etter avgjørelse»-buggen over. Hvem som får lås/åpne den spørsmål som trengte avklaring.
(kaptein? organisator? samme «rostret på laget»-bar som resten?) avgjøres
når den bygges — se punkt (a)/(b) fortsatt åpne under.
- **Walkover/konsesjon VENTER** til brukerroller (kaptein/organisator, - **Walkover/konsesjon VENTER** til brukerroller (kaptein/organisator,
punktet over) er avgjort — å bygge den nå på dagens løse punktet over) er avgjort — å bygge den nå på dagens løse
«rostret på laget»-grense betyr sannsynligvis å bygge den om senere. «rostret på laget»-grense betyr sannsynligvis å bygge den om senere.

View file

@ -218,6 +218,7 @@ async def submit_hole_score(
match = await conn.fetchrow( match = await conn.fetchrow(
""" """
SELECT m.team_a_id::text AS team_a_id, m.team_b_id::text AS team_b_id, 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 s.format, s.scoring_mode, s.hole_config::text AS hole_config
FROM match m JOIN session s ON s.id = m.session_id FROM match m JOIN session s ON s.id = m.session_id
WHERE m.id = $1 WHERE m.id = $1
@ -226,6 +227,11 @@ async def submit_hole_score(
) )
if match is None: if match is None:
raise HTTPException(status.HTTP_404_NOT_FOUND, detail="Matchen finnes ikke.") 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": if match["scoring_mode"] != "stroke":
raise HTTPException( raise HTTPException(
status.HTTP_400_BAD_REQUEST, status.HTTP_400_BAD_REQUEST,
@ -338,6 +344,7 @@ async def submit_hole_result(
match = await conn.fetchrow( match = await conn.fetchrow(
""" """
SELECT m.team_a_id::text AS team_a_id, m.team_b_id::text AS team_b_id, 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 s.scoring_mode, s.hole_config::text AS hole_config
FROM match m JOIN session s ON s.id = m.session_id FROM match m JOIN session s ON s.id = m.session_id
WHERE m.id = $1 WHERE m.id = $1
@ -346,6 +353,11 @@ async def submit_hole_result(
) )
if match is None: if match is None:
raise HTTPException(status.HTTP_404_NOT_FOUND, detail="Matchen finnes ikke.") 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": if match["scoring_mode"] != "hole_result":
raise HTTPException( raise HTTPException(
status.HTTP_400_BAD_REQUEST, status.HTTP_400_BAD_REQUEST,