teecup/app/routers/matches.py

480 lines
18 KiB
Python
Raw Normal View History

"""
Matcher, deltakere og blind draw-lås (ADR-013).
Autorisasjonsgrense (se plan): å opprette en match er organisator-arbeid (kun
org-medlemskap, som tournaments.py). Å legge til en DELTAKER eller LÅSE et lags
Nå dokumentasjonen er oppdatert. Klar for deploy — jeg viser planen først: Update Todos Legge til visuell hint på øktkort ("Sett opp flights") i program-skjermen Avklare designbeslutninger for Brukerroller med bruker Skrive team_authz.py på nytt (kaptein-only + match-deltaker-sjekk) Oppdatere matches.py til å bruke user_is_team_captain Oppdatere scoring.py til å bruke user_is_match_participant Håndheve 'kun én kaptein per lag' i tournaments.py PATCH/POST roster Sjekke ekte teecup_db for lag uten kaptein / med flere kapteiner (kun lesing) Skrive ADR-023 for Brukerroller-beslutningene Sette opp scratch MinIO + engangs API-container Kjøre autorisasjonstester (kaptein/deltaker) mot scratch-API-et Rydde opp scratch-infrastruktur Ekte typesjekket frontend-build (øktkort-hint) Deploy etter eksplisitt bekreftelse + oppdatere .md-filer Alt er scratch-verifisert. Klar for utrulling — planen: Ingen migrasjon (ren autorisasjonslogikk-endring, ingen skjema rørt). docker compose up -d --build teecup_api teecup_frontend mot ekte teeoff_default-nettverk — bygger og bytter ut begge containere. Endringen har reell konsekvens for ekte data akkurat nå: "De Unge" i "De Gamle er Eldst" har 0 kapteiner — de vil fortsette å kunne sette opp/føre score som før (fallback-regelen dekker dette), men fra nå av bør du utpeke en kaptein på det laget for at kaptein-rollen skal bety noe der òg. Etter deploy: sjekke /health + /dashboard fortsatt 200, teeoff.no upåvirket (samme som alle tidligere runder).
2026-07-19 11:41:57 +02:00
oppstilling krever i tillegg at brukeren er KAPTEIN for DET laget (eller
org-eier/admin) -- se app/team_authz.py sin user_is_team_captain for den fulle
begrunnelsen, inkl. fallback for lag uten utpekt kaptein ennå. Byttet fra
"hvem som helst rostret" 2026-07-19 (Brukerroller-runden, ADR-023).
Synlighet (ADR-013): motstanderens deltakere er skjult i app-laget (ikke RLS
begge lag er i samme organisasjon) til BEGGE lag har låst for økten. Filteret
er lagt i SQL (WHERE ... AND ($revealed OR team_id = ANY($egne_lag))), ikke et
etterfølgende Python-filter, slik at lekkasje er strukturelt umulig.
2026-07-17 21:40:42 +02:00
Klokkeslett (se plan): match.tee_time er UTLEDET, ikke lagret -- session sin
scheduled_at + (sequence-1) * tee_interval_minutes, med en valgfri
tee_time_override som vinner hvis satt. Beregnes i Python i både create_match
og list_matches, som allerede henter økten for andre formål.
"""
import json
2026-07-17 21:40:42 +02:00
from datetime import datetime, timedelta
2026-07-17 21:40:42 +02:00
from fastapi import APIRouter, Depends
from pydantic import BaseModel, Field
from ..auth import CurrentUser, get_authorized_org, get_current_user
from ..blind_draw import locked_team_ids, own_team_ids
from ..db import org_connection
2026-07-17 21:40:42 +02:00
from ..errors import app_error, translate_db_errors
from ..handicap import compute_and_store_side_handicaps, parse_allowance_config
Nå dokumentasjonen er oppdatert. Klar for deploy — jeg viser planen først: Update Todos Legge til visuell hint på øktkort ("Sett opp flights") i program-skjermen Avklare designbeslutninger for Brukerroller med bruker Skrive team_authz.py på nytt (kaptein-only + match-deltaker-sjekk) Oppdatere matches.py til å bruke user_is_team_captain Oppdatere scoring.py til å bruke user_is_match_participant Håndheve 'kun én kaptein per lag' i tournaments.py PATCH/POST roster Sjekke ekte teecup_db for lag uten kaptein / med flere kapteiner (kun lesing) Skrive ADR-023 for Brukerroller-beslutningene Sette opp scratch MinIO + engangs API-container Kjøre autorisasjonstester (kaptein/deltaker) mot scratch-API-et Rydde opp scratch-infrastruktur Ekte typesjekket frontend-build (øktkort-hint) Deploy etter eksplisitt bekreftelse + oppdatere .md-filer Alt er scratch-verifisert. Klar for utrulling — planen: Ingen migrasjon (ren autorisasjonslogikk-endring, ingen skjema rørt). docker compose up -d --build teecup_api teecup_frontend mot ekte teeoff_default-nettverk — bygger og bytter ut begge containere. Endringen har reell konsekvens for ekte data akkurat nå: "De Unge" i "De Gamle er Eldst" har 0 kapteiner — de vil fortsette å kunne sette opp/føre score som før (fallback-regelen dekker dette), men fra nå av bør du utpeke en kaptein på det laget for at kaptein-rollen skal bety noe der òg. Etter deploy: sjekke /health + /dashboard fortsatt 200, teeoff.no upåvirket (samme som alle tidligere runder).
2026-07-19 11:41:57 +02:00
from ..team_authz import user_is_team_captain
router = APIRouter()
2026-07-17 21:40:42 +02:00
def _compute_tee_time(
scheduled_at: datetime | None,
tee_interval_minutes: int | None,
sequence: int,
override: datetime | None,
) -> datetime | None:
if override is not None:
return override
if scheduled_at is None or tee_interval_minutes is None:
return None
return scheduled_at + timedelta(minutes=(sequence - 1) * tee_interval_minutes)
class MatchParticipantOut(BaseModel):
id: str
team_side: str
team_roster_id: str
player_name: str
tee_id: str
tee_name: str
class MatchCreate(BaseModel):
sequence: int
team_a_id: str
team_b_id: str
class MatchOut(BaseModel):
id: str
sequence: int
team_a_id: str
team_b_id: str
status_text: str | None
points_side_a: float | None
points_side_b: float | None
leading_side: str | None
2026-07-17 21:40:42 +02:00
tee_time: datetime | None
participants: list[MatchParticipantOut]
@router.post(
"/orgs/{organization_id}/sessions/{session_id}/matches",
response_model=MatchOut,
status_code=201,
)
async def create_match(
session_id: str,
body: MatchCreate,
organization_id: str = Depends(get_authorized_org),
) -> MatchOut:
async with org_connection(organization_id) as conn, translate_db_errors():
session = await conn.fetchrow(
2026-07-17 21:40:42 +02:00
"""
SELECT tournament_id::text AS tournament_id, scheduled_at, tee_interval_minutes
FROM session WHERE id = $1
""",
session_id,
)
if session is None:
2026-07-17 21:40:42 +02:00
raise app_error(404, "NOT_FOUND", "Økten finnes ikke.")
# Matchens FK garanterer bare at lagene er gyldige lag i ORG-en, ikke i
# DENNE turneringen — sjekk det eksplisitt her. Stoler på DB CHECK
# (team_a_id <> team_b_id) for distinkthet, dupliserer den ikke.
team_count = await conn.fetchval(
"SELECT count(*) FROM team WHERE tournament_id = $1 AND id = ANY($2::uuid[])",
session["tournament_id"],
[body.team_a_id, body.team_b_id],
)
if team_count != 2:
2026-07-17 21:40:42 +02:00
raise app_error(400, "VALIDATION_FAILED", "Begge lagene må tilhøre øktens turnering.")
row = await conn.fetchrow(
"""
INSERT INTO match (organization_id, session_id, sequence, team_a_id, team_b_id)
VALUES ($1, $2, $3, $4, $5)
RETURNING id::text, sequence, team_a_id::text, team_b_id::text,
status_text, points_side_a::float AS points_side_a,
points_side_b::float AS points_side_b, leading_side, tee_time_override
""",
organization_id,
session_id,
body.sequence,
body.team_a_id,
body.team_b_id,
)
2026-07-17 21:40:42 +02:00
tee_time = _compute_tee_time(
session["scheduled_at"], session["tee_interval_minutes"], row["sequence"], row["tee_time_override"]
)
return MatchOut(**dict(row), tee_time=tee_time, participants=[])
async def fetch_matches(
conn, organization_id: str, session_id: str, user_id: str | None
) -> list[MatchOut]:
"""Delt mellom den innloggede list_matches under og den offentlige
/public/tournaments/{id}/sessions/{id}/matches (registration.py,
tilskuer-rolle/ADR-026). `user_id=None` for en anonym/ikke-tilknyttet
offentlig leser -- own_team_ids() håndterer det (tom mengde, ser kun
avslørte matcher, se blind_draw.py)."""
session = await conn.fetchrow(
"""
SELECT tournament_id::text AS tournament_id, scheduled_at, tee_interval_minutes
FROM session WHERE id = $1
""",
session_id,
)
if session is None:
raise app_error(404, "NOT_FOUND", "Økten finnes ikke.")
matches = await conn.fetch(
"""
SELECT id::text, sequence, team_a_id::text, team_b_id::text,
status_text, points_side_a::float AS points_side_a,
points_side_b::float AS points_side_b, leading_side, tee_time_override
FROM match
WHERE session_id = $1
ORDER BY sequence
""",
session_id,
)
match_ids = [m["id"] for m in matches]
locked = await locked_team_ids(conn, session_id)
revealed = len(locked) >= 2
# Tomt sett hvis revealed (ubrukt da $2=true gjør ANY-leddet irrelevant).
own = (
set()
if revealed
else await own_team_ids(conn, organization_id, session["tournament_id"], user_id)
)
participant_rows = await conn.fetch(
"""
SELECT mp.match_id::text AS match_id, mp.id::text, mp.team_side::text,
mp.team_roster_id::text, p.display_name AS player_name,
mp.tee_id::text, tee.name AS tee_name
FROM match_participant mp
JOIN team_roster tr ON tr.id = mp.team_roster_id
JOIN player p ON p.id = tr.player_id
JOIN tee ON tee.id = mp.tee_id
WHERE mp.match_id = ANY($1::uuid[])
AND ($2 OR tr.team_id::text = ANY($3::text[]))
""",
match_ids,
revealed,
list(own),
)
by_match: dict[str, list[MatchParticipantOut]] = {mid: [] for mid in match_ids}
for r in participant_rows:
by_match[r["match_id"]].append(
MatchParticipantOut(
id=r["id"],
team_side=r["team_side"],
team_roster_id=r["team_roster_id"],
player_name=r["player_name"],
tee_id=r["tee_id"],
tee_name=r["tee_name"],
)
)
return [
MatchOut(
id=m["id"],
sequence=m["sequence"],
team_a_id=m["team_a_id"],
team_b_id=m["team_b_id"],
status_text=m["status_text"],
points_side_a=m["points_side_a"],
points_side_b=m["points_side_b"],
leading_side=m["leading_side"],
2026-07-17 21:40:42 +02:00
tee_time=_compute_tee_time(
session["scheduled_at"], session["tee_interval_minutes"], m["sequence"], m["tee_time_override"]
),
participants=by_match[m["id"]],
)
for m in matches
]
@router.get(
"/orgs/{organization_id}/sessions/{session_id}/matches",
response_model=list[MatchOut],
)
async def list_matches(
session_id: str,
organization_id: str = Depends(get_authorized_org),
user: CurrentUser = Depends(get_current_user),
) -> list[MatchOut]:
async with org_connection(organization_id) as conn:
return await fetch_matches(conn, organization_id, session_id, user.user_id)
class ParticipantCreate(BaseModel):
team_side: str = Field(pattern="^[ab]$")
team_roster_id: str
tee_id: str
@router.post(
"/orgs/{organization_id}/matches/{match_id}/participants",
response_model=MatchParticipantOut,
status_code=201,
)
async def add_participant(
match_id: str,
body: ParticipantCreate,
organization_id: str = Depends(get_authorized_org),
user: CurrentUser = Depends(get_current_user),
) -> MatchParticipantOut:
async with org_connection(organization_id) as conn, translate_db_errors():
match = await conn.fetchrow(
"""
SELECT session_id::text AS session_id, team_a_id::text AS team_a_id,
team_b_id::text AS team_b_id
FROM match WHERE id = $1
""",
match_id,
)
if match is None:
2026-07-17 21:40:42 +02:00
raise app_error(404, "NOT_FOUND", "Matchen finnes ikke.")
expected_team_id = match["team_a_id"] if body.team_side == "a" else match["team_b_id"]
roster_team_id = await conn.fetchval(
"SELECT team_id::text FROM team_roster WHERE id = $1", body.team_roster_id
)
if roster_team_id is None:
2026-07-17 21:40:42 +02:00
raise app_error(404, "NOT_FOUND", "Roster-oppføringen finnes ikke.")
if roster_team_id != expected_team_id:
2026-07-17 21:40:42 +02:00
raise app_error(
400, "MISMATCHED_SIDE", "team_roster_id tilhører ikke laget på angitt side i denne matchen."
)
session = await conn.fetchrow(
"""
Update Todos Diagnostisere HCP-bug mot ekte data (gjort) Fiks 1: filtrer valgte spillere bort fra listen (blind draw) Fiks 3: tallvelger for slagregistrering (scorekort) Fiks 4: hardkode full_18-scope i handicap-utregning (bekreftet bug) Punkt 2: migrasjon 014 + kode (courses.py, handicap.py, matches.py, tournaments.py, frontend) Scratch-verifisere punkt 2 (fletting, import, validering, remap) Skrive ADR-029 + oppdatere FEATURE_BACKLOG.md Presentere full plan og be om bekreftelse (ekte migrasjon + deploy) Alle fire punkter er nå bygget og grundig scratch-verifisert. Oppsummering før jeg ber om bekreftelse på å rulle ut mot ekte systemer: 1 — Spillerliste: Fikset. Valgte spillere filtreres nå bort i stedet for å bare vises gråtonet. 3 — Tallvelger: Fikset. Ny StrokePicker (1–9, med utvidbar "10+"→10–19). 4 — HCP-bug: Bekreftet ekte kodebug (ikke bare synlighet). Jeg leste ekte produksjonsdata for testøkten din: front_9 + foursome, og alle fire deltakerne hadde NULL i handicap-feltene. Årsak: handicap-utregningen lette etter en tee-rating scopet til øktens hole_config, men slike rader lages i praksis kun med full_18-omfang — så den fant aldri noe for en front_9-økt. Din beskrevne formel (kombinert hcp/2, laveste side til 0 mottatte slag, resten fordelt fra stroke index 1) var allerede riktig implementert — bugen var at den aldri kjørte. Fikset og verifisert: identisk bruttoscore på begge sider ga nå et ikke-delt resultat, som beviser at hcp faktisk brukes. 2 — Tee/kjønn: Du hadde helt rett i presiseringen din — dette var større enn jeg først trodde. Kjønn er nå flyttet fra selve utslaget til ratingen (ny migrasjon 014_tee_gender_to_rating.sql), tee-valget i blind draw er blitt helt automatisk (ingen kjønnsvelger lenger), og manglende kjønn/rating avvises tydelig i stedet for å gjettes. Fikk egen ADR-029. Verifisert i fem separate runder, inkludert mot ekte teeoff-data (Borregaard) og en gjenskaping av akkurat Tjøme-mønsteret. Bifunn, ikke fikset: en liten, urelatert 500-krasj (stroke-registrering på en bane uten registrerte hull) — vil du at jeg tar den også, eller lar den ligge som egen sak? Utrullingsplan — dette er første gang i denne runden noe rører ekte data: Migrasjon 014_tee_gender_to_rating.sql mot ekte teecup_db — slår sammen Tjømes 8 tee-rader til 4 (bevarer alle ratinger, reparerer alle referanser; mekanisk identisk med det jeg nettopp verifiserte i scratch)
2026-07-20 08:53:53 +02:00
SELECT course_id::text AS course_id, format,
allowance_override::text AS allowance_override
FROM session WHERE id = $1
""",
match["session_id"],
)
tee_course_id = await conn.fetchval(
"SELECT course_id::text FROM tee WHERE id = $1", body.tee_id
)
if tee_course_id is None or tee_course_id != session["course_id"]:
2026-07-17 21:40:42 +02:00
raise app_error(400, "OUT_OF_SCOPE", "tee_id tilhører ikke øktens bane.")
Nå dokumentasjonen er oppdatert. Klar for deploy — jeg viser planen først: Update Todos Legge til visuell hint på øktkort ("Sett opp flights") i program-skjermen Avklare designbeslutninger for Brukerroller med bruker Skrive team_authz.py på nytt (kaptein-only + match-deltaker-sjekk) Oppdatere matches.py til å bruke user_is_team_captain Oppdatere scoring.py til å bruke user_is_match_participant Håndheve 'kun én kaptein per lag' i tournaments.py PATCH/POST roster Sjekke ekte teecup_db for lag uten kaptein / med flere kapteiner (kun lesing) Skrive ADR-023 for Brukerroller-beslutningene Sette opp scratch MinIO + engangs API-container Kjøre autorisasjonstester (kaptein/deltaker) mot scratch-API-et Rydde opp scratch-infrastruktur Ekte typesjekket frontend-build (øktkort-hint) Deploy etter eksplisitt bekreftelse + oppdatere .md-filer Alt er scratch-verifisert. Klar for utrulling — planen: Ingen migrasjon (ren autorisasjonslogikk-endring, ingen skjema rørt). docker compose up -d --build teecup_api teecup_frontend mot ekte teeoff_default-nettverk — bygger og bytter ut begge containere. Endringen har reell konsekvens for ekte data akkurat nå: "De Unge" i "De Gamle er Eldst" har 0 kapteiner — de vil fortsette å kunne sette opp/føre score som før (fallback-regelen dekker dette), men fra nå av bør du utpeke en kaptein på det laget for at kaptein-rollen skal bety noe der òg. Etter deploy: sjekke /health + /dashboard fortsatt 200, teeoff.no upåvirket (samme som alle tidligere runder).
2026-07-19 11:41:57 +02:00
if not await user_is_team_captain(conn, organization_id, expected_team_id, user.user_id):
raise app_error(
403, "NOT_TEAM_CAPTAIN", "Du må være kaptein for laget (eller organisasjonsadministrator) for å gjøre dette."
)
locked = await locked_team_ids(conn, match["session_id"])
if expected_team_id in locked:
2026-07-17 21:40:42 +02:00
raise app_error(
409, "ALREADY_LOCKED", "Laget har allerede låst oppstillingen for denne økten."
)
# Beregnet HELT FØR innsetting -- se app/handicap.py sin
# compute_and_store_side_handicaps, som ellers krasjer rått
# (TypeError i handicap_engine.py sin course_handicap_raw) på en
# spiller uten handicap_index når use_handicap er på (standard).
# Fant under scratch-testing av blind draw-skjermen -- reell,
# preeksisterende feil, ikke noe denne runden introduserte.
allowance_override = (
json.loads(session["allowance_override"]) if session["allowance_override"] else None
)
config = parse_allowance_config(session["format"], allowance_override)
if config.use_handicap:
Update Todos Diagnostisere HCP-bug mot ekte data (gjort) Fiks 1: filtrer valgte spillere bort fra listen (blind draw) Fiks 3: tallvelger for slagregistrering (scorekort) Fiks 4: hardkode full_18-scope i handicap-utregning (bekreftet bug) Punkt 2: migrasjon 014 + kode (courses.py, handicap.py, matches.py, tournaments.py, frontend) Scratch-verifisere punkt 2 (fletting, import, validering, remap) Skrive ADR-029 + oppdatere FEATURE_BACKLOG.md Presentere full plan og be om bekreftelse (ekte migrasjon + deploy) Alle fire punkter er nå bygget og grundig scratch-verifisert. Oppsummering før jeg ber om bekreftelse på å rulle ut mot ekte systemer: 1 — Spillerliste: Fikset. Valgte spillere filtreres nå bort i stedet for å bare vises gråtonet. 3 — Tallvelger: Fikset. Ny StrokePicker (1–9, med utvidbar "10+"→10–19). 4 — HCP-bug: Bekreftet ekte kodebug (ikke bare synlighet). Jeg leste ekte produksjonsdata for testøkten din: front_9 + foursome, og alle fire deltakerne hadde NULL i handicap-feltene. Årsak: handicap-utregningen lette etter en tee-rating scopet til øktens hole_config, men slike rader lages i praksis kun med full_18-omfang — så den fant aldri noe for en front_9-økt. Din beskrevne formel (kombinert hcp/2, laveste side til 0 mottatte slag, resten fordelt fra stroke index 1) var allerede riktig implementert — bugen var at den aldri kjørte. Fikset og verifisert: identisk bruttoscore på begge sider ga nå et ikke-delt resultat, som beviser at hcp faktisk brukes. 2 — Tee/kjønn: Du hadde helt rett i presiseringen din — dette var større enn jeg først trodde. Kjønn er nå flyttet fra selve utslaget til ratingen (ny migrasjon 014_tee_gender_to_rating.sql), tee-valget i blind draw er blitt helt automatisk (ingen kjønnsvelger lenger), og manglende kjønn/rating avvises tydelig i stedet for å gjettes. Fikk egen ADR-029. Verifisert i fem separate runder, inkludert mot ekte teeoff-data (Borregaard) og en gjenskaping av akkurat Tjøme-mønsteret. Bifunn, ikke fikset: en liten, urelatert 500-krasj (stroke-registrering på en bane uten registrerte hull) — vil du at jeg tar den også, eller lar den ligge som egen sak? Utrullingsplan — dette er første gang i denne runden noe rører ekte data: Migrasjon 014_tee_gender_to_rating.sql mot ekte teecup_db — slår sammen Tjømes 8 tee-rader til 4 (bevarer alle ratinger, reparerer alle referanser; mekanisk identisk med det jeg nettopp verifiserte i scratch)
2026-07-20 08:53:53 +02:00
roster_row = await conn.fetchrow(
"""
SELECT tr.handicap_index_snapshot, p.gender
FROM team_roster tr
JOIN player p ON p.id = tr.player_id
WHERE tr.id = $1
""",
body.team_roster_id,
)
Update Todos Diagnostisere HCP-bug mot ekte data (gjort) Fiks 1: filtrer valgte spillere bort fra listen (blind draw) Fiks 3: tallvelger for slagregistrering (scorekort) Fiks 4: hardkode full_18-scope i handicap-utregning (bekreftet bug) Punkt 2: migrasjon 014 + kode (courses.py, handicap.py, matches.py, tournaments.py, frontend) Scratch-verifisere punkt 2 (fletting, import, validering, remap) Skrive ADR-029 + oppdatere FEATURE_BACKLOG.md Presentere full plan og be om bekreftelse (ekte migrasjon + deploy) Alle fire punkter er nå bygget og grundig scratch-verifisert. Oppsummering før jeg ber om bekreftelse på å rulle ut mot ekte systemer: 1 — Spillerliste: Fikset. Valgte spillere filtreres nå bort i stedet for å bare vises gråtonet. 3 — Tallvelger: Fikset. Ny StrokePicker (1–9, med utvidbar "10+"→10–19). 4 — HCP-bug: Bekreftet ekte kodebug (ikke bare synlighet). Jeg leste ekte produksjonsdata for testøkten din: front_9 + foursome, og alle fire deltakerne hadde NULL i handicap-feltene. Årsak: handicap-utregningen lette etter en tee-rating scopet til øktens hole_config, men slike rader lages i praksis kun med full_18-omfang — så den fant aldri noe for en front_9-økt. Din beskrevne formel (kombinert hcp/2, laveste side til 0 mottatte slag, resten fordelt fra stroke index 1) var allerede riktig implementert — bugen var at den aldri kjørte. Fikset og verifisert: identisk bruttoscore på begge sider ga nå et ikke-delt resultat, som beviser at hcp faktisk brukes. 2 — Tee/kjønn: Du hadde helt rett i presiseringen din — dette var større enn jeg først trodde. Kjønn er nå flyttet fra selve utslaget til ratingen (ny migrasjon 014_tee_gender_to_rating.sql), tee-valget i blind draw er blitt helt automatisk (ingen kjønnsvelger lenger), og manglende kjønn/rating avvises tydelig i stedet for å gjettes. Fikk egen ADR-029. Verifisert i fem separate runder, inkludert mot ekte teeoff-data (Borregaard) og en gjenskaping av akkurat Tjøme-mønsteret. Bifunn, ikke fikset: en liten, urelatert 500-krasj (stroke-registrering på en bane uten registrerte hull) — vil du at jeg tar den også, eller lar den ligge som egen sak? Utrullingsplan — dette er første gang i denne runden noe rører ekte data: Migrasjon 014_tee_gender_to_rating.sql mot ekte teecup_db — slår sammen Tjømes 8 tee-rader til 4 (bevarer alle ratinger, reparerer alle referanser; mekanisk identisk med det jeg nettopp verifiserte i scratch)
2026-07-20 08:53:53 +02:00
if roster_row["handicap_index_snapshot"] is None:
raise app_error(
400,
"VALIDATION_FAILED",
"Denne spilleren mangler handicap-indeks -- sett en verdi på spilleren "
"før hen kan legges til i en handicap-basert økt.",
)
Update Todos Diagnostisere HCP-bug mot ekte data (gjort) Fiks 1: filtrer valgte spillere bort fra listen (blind draw) Fiks 3: tallvelger for slagregistrering (scorekort) Fiks 4: hardkode full_18-scope i handicap-utregning (bekreftet bug) Punkt 2: migrasjon 014 + kode (courses.py, handicap.py, matches.py, tournaments.py, frontend) Scratch-verifisere punkt 2 (fletting, import, validering, remap) Skrive ADR-029 + oppdatere FEATURE_BACKLOG.md Presentere full plan og be om bekreftelse (ekte migrasjon + deploy) Alle fire punkter er nå bygget og grundig scratch-verifisert. Oppsummering før jeg ber om bekreftelse på å rulle ut mot ekte systemer: 1 — Spillerliste: Fikset. Valgte spillere filtreres nå bort i stedet for å bare vises gråtonet. 3 — Tallvelger: Fikset. Ny StrokePicker (1–9, med utvidbar "10+"→10–19). 4 — HCP-bug: Bekreftet ekte kodebug (ikke bare synlighet). Jeg leste ekte produksjonsdata for testøkten din: front_9 + foursome, og alle fire deltakerne hadde NULL i handicap-feltene. Årsak: handicap-utregningen lette etter en tee-rating scopet til øktens hole_config, men slike rader lages i praksis kun med full_18-omfang — så den fant aldri noe for en front_9-økt. Din beskrevne formel (kombinert hcp/2, laveste side til 0 mottatte slag, resten fordelt fra stroke index 1) var allerede riktig implementert — bugen var at den aldri kjørte. Fikset og verifisert: identisk bruttoscore på begge sider ga nå et ikke-delt resultat, som beviser at hcp faktisk brukes. 2 — Tee/kjønn: Du hadde helt rett i presiseringen din — dette var større enn jeg først trodde. Kjønn er nå flyttet fra selve utslaget til ratingen (ny migrasjon 014_tee_gender_to_rating.sql), tee-valget i blind draw er blitt helt automatisk (ingen kjønnsvelger lenger), og manglende kjønn/rating avvises tydelig i stedet for å gjettes. Fikk egen ADR-029. Verifisert i fem separate runder, inkludert mot ekte teeoff-data (Borregaard) og en gjenskaping av akkurat Tjøme-mønsteret. Bifunn, ikke fikset: en liten, urelatert 500-krasj (stroke-registrering på en bane uten registrerte hull) — vil du at jeg tar den også, eller lar den ligge som egen sak? Utrullingsplan — dette er første gang i denne runden noe rører ekte data: Migrasjon 014_tee_gender_to_rating.sql mot ekte teecup_db — slår sammen Tjømes 8 tee-rader til 4 (bevarer alle ratinger, reparerer alle referanser; mekanisk identisk med det jeg nettopp verifiserte i scratch)
2026-07-20 08:53:53 +02:00
# ADR-029: kjønn hører til RATINGEN (tee_rating), ikke selve
# utslaget -- et utslag kan mangle rating for ett kjønn (klubben
# har bevisst ikke slopet det for det kjønnet). Sjekket her, FØR
# innsetting, av samme "fail loudly"-grunn som sjekken over.
if roster_row["gender"] is None:
raise app_error(
400,
"VALIDATION_FAILED",
"Denne spilleren mangler registrert kjønn -- sett kjønn på spilleren "
"før hen kan legges til i en handicap-basert økt (trengs for å finne "
"riktig tee-rating).",
)
has_rating = await conn.fetchval(
"""
SELECT EXISTS(
SELECT 1 FROM tee_rating
WHERE tee_id = $1 AND scope = 'full_18' AND gender = $2
)
""",
body.tee_id,
roster_row["gender"],
)
if not has_rating:
kjonn_tekst = "dame" if roster_row["gender"] == "f" else "herre"
raise app_error(
400,
"VALIDATION_FAILED",
f"Dette utslaget har ingen {kjonn_tekst}-rating -- velg et annet utslag.",
)
row = await conn.fetchrow(
"""
WITH inserted AS (
INSERT INTO match_participant
(organization_id, match_id, team_side, team_roster_id, tee_id)
VALUES ($1, $2, $3, $4, $5)
RETURNING id, team_side, team_roster_id, tee_id
)
SELECT inserted.id::text, inserted.team_side::text,
inserted.team_roster_id::text, p.display_name AS player_name,
inserted.tee_id::text, tee.name AS tee_name
FROM inserted
JOIN team_roster tr ON tr.id = inserted.team_roster_id
JOIN player p ON p.id = tr.player_id
JOIN tee ON tee.id = inserted.tee_id
""",
organization_id,
match_id,
body.team_side,
body.team_roster_id,
body.tee_id,
)
# Beregn/lagre course_handicap/playing_handicap nå som deltakeren er
# satt inn. Funksjonen avgjør selv om siden er "klar" (singles/
# fourball: alltid; foursome/greensome/scramble: kun når siden er
# komplett) -- se app/handicap.py. `config` allerede parset over.
await compute_and_store_side_handicaps(
Update Todos Diagnostisere HCP-bug mot ekte data (gjort) Fiks 1: filtrer valgte spillere bort fra listen (blind draw) Fiks 3: tallvelger for slagregistrering (scorekort) Fiks 4: hardkode full_18-scope i handicap-utregning (bekreftet bug) Punkt 2: migrasjon 014 + kode (courses.py, handicap.py, matches.py, tournaments.py, frontend) Scratch-verifisere punkt 2 (fletting, import, validering, remap) Skrive ADR-029 + oppdatere FEATURE_BACKLOG.md Presentere full plan og be om bekreftelse (ekte migrasjon + deploy) Alle fire punkter er nå bygget og grundig scratch-verifisert. Oppsummering før jeg ber om bekreftelse på å rulle ut mot ekte systemer: 1 — Spillerliste: Fikset. Valgte spillere filtreres nå bort i stedet for å bare vises gråtonet. 3 — Tallvelger: Fikset. Ny StrokePicker (1–9, med utvidbar "10+"→10–19). 4 — HCP-bug: Bekreftet ekte kodebug (ikke bare synlighet). Jeg leste ekte produksjonsdata for testøkten din: front_9 + foursome, og alle fire deltakerne hadde NULL i handicap-feltene. Årsak: handicap-utregningen lette etter en tee-rating scopet til øktens hole_config, men slike rader lages i praksis kun med full_18-omfang — så den fant aldri noe for en front_9-økt. Din beskrevne formel (kombinert hcp/2, laveste side til 0 mottatte slag, resten fordelt fra stroke index 1) var allerede riktig implementert — bugen var at den aldri kjørte. Fikset og verifisert: identisk bruttoscore på begge sider ga nå et ikke-delt resultat, som beviser at hcp faktisk brukes. 2 — Tee/kjønn: Du hadde helt rett i presiseringen din — dette var større enn jeg først trodde. Kjønn er nå flyttet fra selve utslaget til ratingen (ny migrasjon 014_tee_gender_to_rating.sql), tee-valget i blind draw er blitt helt automatisk (ingen kjønnsvelger lenger), og manglende kjønn/rating avvises tydelig i stedet for å gjettes. Fikk egen ADR-029. Verifisert i fem separate runder, inkludert mot ekte teeoff-data (Borregaard) og en gjenskaping av akkurat Tjøme-mønsteret. Bifunn, ikke fikset: en liten, urelatert 500-krasj (stroke-registrering på en bane uten registrerte hull) — vil du at jeg tar den også, eller lar den ligge som egen sak? Utrullingsplan — dette er første gang i denne runden noe rører ekte data: Migrasjon 014_tee_gender_to_rating.sql mot ekte teecup_db — slår sammen Tjømes 8 tee-rader til 4 (bevarer alle ratinger, reparerer alle referanser; mekanisk identisk med det jeg nettopp verifiserte i scratch)
2026-07-20 08:53:53 +02:00
conn, match_id, body.team_side, session["format"], config
)
return MatchParticipantOut(**dict(row))
@router.delete(
"/orgs/{organization_id}/matches/{match_id}/participants/{participant_id}",
status_code=204,
)
async def remove_participant(
match_id: str,
participant_id: str,
organization_id: str = Depends(get_authorized_org),
user: CurrentUser = Depends(get_current_user),
) -> None:
"""Fjern et deltaker-valg FØR laget har låst -- lar en kaptein endre seg
(velge en annen spiller/tee for samme plass) uten å etterlate en
foreldreløs rad, siden det ikke finnes noen PATCH match_participant.
Reist av brukeren rett før blind draw-skjermen (som lar spiller/tee
endres fritt i UI-et helt frem til låsing)."""
async with org_connection(organization_id) as conn, translate_db_errors():
row = await conn.fetchrow(
"""
SELECT mp.team_side::text AS team_side,
m.session_id::text AS session_id,
m.team_a_id::text AS team_a_id, m.team_b_id::text AS team_b_id
FROM match_participant mp
JOIN match m ON m.id = mp.match_id
WHERE mp.id = $1 AND mp.match_id = $2
""",
participant_id,
match_id,
)
if row is None:
raise app_error(404, "NOT_FOUND", "Deltakeren finnes ikke.")
team_id = row["team_a_id"] if row["team_side"] == "a" else row["team_b_id"]
Nå dokumentasjonen er oppdatert. Klar for deploy — jeg viser planen først: Update Todos Legge til visuell hint på øktkort ("Sett opp flights") i program-skjermen Avklare designbeslutninger for Brukerroller med bruker Skrive team_authz.py på nytt (kaptein-only + match-deltaker-sjekk) Oppdatere matches.py til å bruke user_is_team_captain Oppdatere scoring.py til å bruke user_is_match_participant Håndheve 'kun én kaptein per lag' i tournaments.py PATCH/POST roster Sjekke ekte teecup_db for lag uten kaptein / med flere kapteiner (kun lesing) Skrive ADR-023 for Brukerroller-beslutningene Sette opp scratch MinIO + engangs API-container Kjøre autorisasjonstester (kaptein/deltaker) mot scratch-API-et Rydde opp scratch-infrastruktur Ekte typesjekket frontend-build (øktkort-hint) Deploy etter eksplisitt bekreftelse + oppdatere .md-filer Alt er scratch-verifisert. Klar for utrulling — planen: Ingen migrasjon (ren autorisasjonslogikk-endring, ingen skjema rørt). docker compose up -d --build teecup_api teecup_frontend mot ekte teeoff_default-nettverk — bygger og bytter ut begge containere. Endringen har reell konsekvens for ekte data akkurat nå: "De Unge" i "De Gamle er Eldst" har 0 kapteiner — de vil fortsette å kunne sette opp/føre score som før (fallback-regelen dekker dette), men fra nå av bør du utpeke en kaptein på det laget for at kaptein-rollen skal bety noe der òg. Etter deploy: sjekke /health + /dashboard fortsatt 200, teeoff.no upåvirket (samme som alle tidligere runder).
2026-07-19 11:41:57 +02:00
if not await user_is_team_captain(conn, organization_id, team_id, user.user_id):
raise app_error(
403, "NOT_TEAM_CAPTAIN", "Du må være kaptein for laget (eller organisasjonsadministrator) for å gjøre dette."
)
locked = await locked_team_ids(conn, row["session_id"])
if team_id in locked:
raise app_error(
409, "ALREADY_LOCKED", "Laget har allerede låst oppstillingen for denne økten."
)
await conn.execute("DELETE FROM match_participant WHERE id = $1", participant_id)
class LockCreate(BaseModel):
team_id: str
class LockOut(BaseModel):
team_id: str
locked_at: str
@router.post(
"/orgs/{organization_id}/sessions/{session_id}/lock",
response_model=LockOut,
status_code=201,
)
async def lock_lineup(
session_id: str,
body: LockCreate,
organization_id: str = Depends(get_authorized_org),
user: CurrentUser = Depends(get_current_user),
) -> LockOut:
async with org_connection(organization_id) as conn, translate_db_errors():
session = await conn.fetchrow("SELECT id FROM session WHERE id = $1", session_id)
if session is None:
2026-07-17 21:40:42 +02:00
raise app_error(404, "NOT_FOUND", "Økten finnes ikke.")
Nå dokumentasjonen er oppdatert. Klar for deploy — jeg viser planen først: Update Todos Legge til visuell hint på øktkort ("Sett opp flights") i program-skjermen Avklare designbeslutninger for Brukerroller med bruker Skrive team_authz.py på nytt (kaptein-only + match-deltaker-sjekk) Oppdatere matches.py til å bruke user_is_team_captain Oppdatere scoring.py til å bruke user_is_match_participant Håndheve 'kun én kaptein per lag' i tournaments.py PATCH/POST roster Sjekke ekte teecup_db for lag uten kaptein / med flere kapteiner (kun lesing) Skrive ADR-023 for Brukerroller-beslutningene Sette opp scratch MinIO + engangs API-container Kjøre autorisasjonstester (kaptein/deltaker) mot scratch-API-et Rydde opp scratch-infrastruktur Ekte typesjekket frontend-build (øktkort-hint) Deploy etter eksplisitt bekreftelse + oppdatere .md-filer Alt er scratch-verifisert. Klar for utrulling — planen: Ingen migrasjon (ren autorisasjonslogikk-endring, ingen skjema rørt). docker compose up -d --build teecup_api teecup_frontend mot ekte teeoff_default-nettverk — bygger og bytter ut begge containere. Endringen har reell konsekvens for ekte data akkurat nå: "De Unge" i "De Gamle er Eldst" har 0 kapteiner — de vil fortsette å kunne sette opp/føre score som før (fallback-regelen dekker dette), men fra nå av bør du utpeke en kaptein på det laget for at kaptein-rollen skal bety noe der òg. Etter deploy: sjekke /health + /dashboard fortsatt 200, teeoff.no upåvirket (samme som alle tidligere runder).
2026-07-19 11:41:57 +02:00
if not await user_is_team_captain(conn, organization_id, body.team_id, user.user_id):
raise app_error(
403, "NOT_TEAM_CAPTAIN", "Du må være kaptein for laget (eller organisasjonsadministrator) for å gjøre dette."
)
# UNIQUE(session_id, team_id) gir 409 via translate_db_errors ved
# dobbel-lås — ingen manuell sjekk nødvendig.
row = await conn.fetchrow(
"""
INSERT INTO lineup_lock (organization_id, session_id, team_id, locked_by)
VALUES ($1, $2, $3, $4)
RETURNING team_id::text, locked_at::text
""",
organization_id,
session_id,
body.team_id,
user.user_id,
)
return LockOut(**dict(row))