2026-07-16 09:16:22 +02:00
|
|
|
"""
|
|
|
|
|
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
|
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).
|
2026-07-16 09:16:22 +02:00
|
|
|
|
|
|
|
|
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.
|
2026-07-16 09:16:22 +02:00
|
|
|
"""
|
|
|
|
|
|
2026-07-16 14:38:42 +02:00
|
|
|
import json
|
2026-07-17 21:40:42 +02:00
|
|
|
from datetime import datetime, timedelta
|
2026-07-16 14:38:42 +02:00
|
|
|
|
2026-07-17 21:40:42 +02:00
|
|
|
from fastapi import APIRouter, Depends
|
2026-07-16 09:16:22 +02:00
|
|
|
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
|
2026-07-16 14:38:42 +02:00
|
|
|
from ..handicap import compute_and_store_side_handicaps, parse_allowance_config
|
Update Todos
Flytte is_participant-logikk til team_authz.py (unngå sirkulær import)
Widen scoring.py: get_scorecard/submit_hole_score/submit_hole_result/concede_match
Widen matches.py: list_matches/add_participant/remove_participant/lock_lineup
Widen tournaments.py: list_sessions/list_teams/concede_tournament
Widen courses.py: list_holes
Widen messaging.py: team chat REST-endepunkter (list/send/delete)
Legge til my_session_id/my_match_id i /auth/me sin my_tournaments
Frontend: legge til lenker til lag-chat/scorekort fra «Mine runder»
Scratch-verifisere hele deltaker-tilgang-runden (positive + negative kontroller)
Ekte typesjekket frontend-produksjonsbuild + backend py_compile på nytt før deploy-forslag
Presentere plan og be om bekreftelse før utrulling mot ekte teecup_db/containere
Bygge sekundær e-postadresse (enkelt tilfelle, ADR-032-mønster)
Bygge HCP-historikk over tid
Deltaker-tilgang til lag-chat/scorekort er bygget og grundig scratch-verifisert (43/43 sjekker, test_isolation.sql fortsatt 12/12). Kort oppsummert hva som er gjort:
Backend — den blanke org-medlemskaps-sperren (get_authorized_org) er fjernet fra ni endepunkter (lag-chat lese/skrive/slette, scorekort-lesing, slag/hull-resultat-innsending, walkover på match- og turneringsnivå, match-/lag-/økt-listing, bane-hull) og erstattet med de faktiske, allerede eksisterende autorisasjonssjekkene (user_is_rostered_on_team, user_is_match_participant, user_is_team_captain — alle uendret) — pluss et nytt eksplisitt org-medlem ELLER turnering-deltaker-OR på de endepunktene som tidligere ikke hadde noen finkornet sjekk i det hele tatt (der en ren fjerning ville åpnet dem for enhver innlogget bruker). /auth/me viser nå også my_session_id/my_match_id per rad i "Mine runder".
Frontend — "Mine runder"-kortet har fått to nye handlingslenker: "Lag-chat" (alltid) og "Scorekort" (når spilleren har en aktiv match).
Verifisert grundig: en rostret spiller UTEN organisasjonsmedlemskap får nå riktig tilgang til alt dette; en helt fremmed bruker avvises fortsatt overalt; org-eier beholder full tilgang som før; lag-chatten er fortsatt ekte privat (org-eier fortsatt avvist der, med vilje); kryss-org-isolasjon holder; og en rostret-men-ikke-kaptein spiller ble korrekt avvist fra walkover (fant samtidig en eksisterende, dokumentert fallback-oppførsel — et lag uten utpekt kaptein godtar enhver rostret spiller — som viste seg å fungere akkurat som tiltenkt).
2026-07-22 05:46:47 +02:00
|
|
|
from ..team_authz import is_org_member, user_is_team_captain, user_is_tournament_participant
|
2026-07-16 09:16:22 +02:00
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
2026-07-16 09:16:22 +02:00
|
|
|
class MatchParticipantOut(BaseModel):
|
|
|
|
|
id: str
|
|
|
|
|
team_side: str
|
|
|
|
|
team_roster_id: str
|
|
|
|
|
player_name: str
|
|
|
|
|
tee_id: str
|
|
|
|
|
tee_name: str
|
2026-07-29 12:44:51 +02:00
|
|
|
playing_handicap: int | None = None
|
All eight new turneringsformater have backend + engine complete and scratch-verified — Chapman, Nassau, Københavner, Bingo Bango Bongo, Flaggturnering, Shamble, Money Ball, and High-low-high. Built and tested one format at a time (engine tests → migration → API wiring → scratch verification) exactly per the approved plan:
Verification totals: handicap_engine.py unit tests went from 63 → 98 (all passing), plus 400+ scratch-API checks across the eight formats' test scripts, covering both frittstående runder and the appropriate org-scoped system (team or individual) for each. test_isolation.sql stayed 12/12 throughout — migrations 041–050 are purely additive.
A few real things surfaced and were fixed during testing:
Nassau can be blocked by the existing "match already decided" lock (ADR-012) if the overall 18-hole match closes early — documented as a known v1 limitation, not fixed (would mean loosening an established safety guard).
Københavner's points depend on all 3 players at once, breaking the usual "recompute one participant" pattern — handled with a dedicated recompute path.
High-low-high doesn't fit the existing win/loss/halved match cache, so it gets its own dedicated read endpoint (like Nassau) — verified digit-for-digit against your own worked example ("1-1 etter hull 1", "2-1 til lag 2").
Two missing-column crashes (caught in scratch, never reached anything resembling "done").
2026-07-30 14:54:11 +02:00
|
|
|
# Money Ball (2026-07-30) -- 0-3, null for alle andre formater.
|
|
|
|
|
lineup_order: int | None = None
|
2026-07-16 09:16:22 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
ackend for the join-code, leading_side, and projected-standings work is fully verified against a fresh scratch database (migrations 001–011, test_isolation.sql 12/12, and live end-to-end checks: join-code generation/uniqueness, code resolution, visibility bypass on both the tournament view and registration — including case-insensitivity and rejection of a wrong code — plus a full hole-by-hole match simulation confirming leading_side/status_text stay in sync through "1 UP" → "AS" → a decided "9&7", with the leaderboard's projected points matching at each stage: 1.0/0.0 while A led, 0.5/0.5 at all-square, and settling to equal actual/projected once decided).
Ready to deploy to the real system:
Migration: 011_join_code_and_leading_side.sql against real teecup_db (adds tournament.join_code — backfills existing tournaments with generated codes — and match.leading_side, plus the public_tournament_by_code() function).
Redeploy: teecup_api only (no frontend changes yet — those come next).
2026-07-19 09:23:35 +02:00
|
|
|
leading_side: str | None
|
2026-07-17 21:40:42 +02:00
|
|
|
tee_time: datetime | None
|
2026-07-16 09:16:22 +02:00
|
|
|
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,
|
2026-07-16 09:16:22 +02:00
|
|
|
)
|
|
|
|
|
if session is None:
|
2026-07-17 21:40:42 +02:00
|
|
|
raise app_error(404, "NOT_FOUND", "Økten finnes ikke.")
|
2026-07-16 09:16:22 +02:00
|
|
|
|
|
|
|
|
# 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.")
|
2026-07-16 09:16:22 +02:00
|
|
|
|
|
|
|
|
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,
|
ackend for the join-code, leading_side, and projected-standings work is fully verified against a fresh scratch database (migrations 001–011, test_isolation.sql 12/12, and live end-to-end checks: join-code generation/uniqueness, code resolution, visibility bypass on both the tournament view and registration — including case-insensitivity and rejection of a wrong code — plus a full hole-by-hole match simulation confirming leading_side/status_text stay in sync through "1 UP" → "AS" → a decided "9&7", with the leaderboard's projected points matching at each stage: 1.0/0.0 while A led, 0.5/0.5 at all-square, and settling to equal actual/projected once decided).
Ready to deploy to the real system:
Migration: 011_join_code_and_leading_side.sql against real teecup_db (adds tournament.join_code — backfills existing tournaments with generated codes — and match.leading_side, plus the public_tournament_by_code() function).
Redeploy: teecup_api only (no frontend changes yet — those come next).
2026-07-19 09:23:35 +02:00
|
|
|
points_side_b::float AS points_side_b, leading_side, tee_time_override
|
2026-07-16 09:16:22 +02:00
|
|
|
""",
|
|
|
|
|
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=[])
|
2026-07-16 09:16:22 +02:00
|
|
|
|
|
|
|
|
|
2026-07-19 22:51:07 +02:00
|
|
|
async def fetch_matches(
|
|
|
|
|
conn, organization_id: str, session_id: str, user_id: str | None
|
2026-07-16 09:16:22 +02:00
|
|
|
) -> list[MatchOut]:
|
2026-07-19 22:51:07 +02:00
|
|
|
"""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)
|
|
|
|
|
)
|
2026-07-16 09:16:22 +02:00
|
|
|
|
2026-07-19 22:51:07 +02:00
|
|
|
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,
|
All eight new turneringsformater have backend + engine complete and scratch-verified — Chapman, Nassau, Københavner, Bingo Bango Bongo, Flaggturnering, Shamble, Money Ball, and High-low-high. Built and tested one format at a time (engine tests → migration → API wiring → scratch verification) exactly per the approved plan:
Verification totals: handicap_engine.py unit tests went from 63 → 98 (all passing), plus 400+ scratch-API checks across the eight formats' test scripts, covering both frittstående runder and the appropriate org-scoped system (team or individual) for each. test_isolation.sql stayed 12/12 throughout — migrations 041–050 are purely additive.
A few real things surfaced and were fixed during testing:
Nassau can be blocked by the existing "match already decided" lock (ADR-012) if the overall 18-hole match closes early — documented as a known v1 limitation, not fixed (would mean loosening an established safety guard).
Københavner's points depend on all 3 players at once, breaking the usual "recompute one participant" pattern — handled with a dedicated recompute path.
High-low-high doesn't fit the existing win/loss/halved match cache, so it gets its own dedicated read endpoint (like Nassau) — verified digit-for-digit against your own worked example ("1-1 etter hull 1", "2-1 til lag 2").
Two missing-column crashes (caught in scratch, never reached anything resembling "done").
2026-07-30 14:54:11 +02:00
|
|
|
mp.tee_id::text, tee.name AS tee_name, mp.playing_handicap, mp.lineup_order
|
2026-07-19 22:51:07 +02:00
|
|
|
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),
|
|
|
|
|
)
|
2026-07-16 09:16:22 +02:00
|
|
|
|
|
|
|
|
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"],
|
2026-07-29 12:44:51 +02:00
|
|
|
playing_handicap=r["playing_handicap"],
|
All eight new turneringsformater have backend + engine complete and scratch-verified — Chapman, Nassau, Københavner, Bingo Bango Bongo, Flaggturnering, Shamble, Money Ball, and High-low-high. Built and tested one format at a time (engine tests → migration → API wiring → scratch verification) exactly per the approved plan:
Verification totals: handicap_engine.py unit tests went from 63 → 98 (all passing), plus 400+ scratch-API checks across the eight formats' test scripts, covering both frittstående runder and the appropriate org-scoped system (team or individual) for each. test_isolation.sql stayed 12/12 throughout — migrations 041–050 are purely additive.
A few real things surfaced and were fixed during testing:
Nassau can be blocked by the existing "match already decided" lock (ADR-012) if the overall 18-hole match closes early — documented as a known v1 limitation, not fixed (would mean loosening an established safety guard).
Københavner's points depend on all 3 players at once, breaking the usual "recompute one participant" pattern — handled with a dedicated recompute path.
High-low-high doesn't fit the existing win/loss/halved match cache, so it gets its own dedicated read endpoint (like Nassau) — verified digit-for-digit against your own worked example ("1-1 etter hull 1", "2-1 til lag 2").
Two missing-column crashes (caught in scratch, never reached anything resembling "done").
2026-07-30 14:54:11 +02:00
|
|
|
lineup_order=r["lineup_order"],
|
2026-07-16 09:16:22 +02:00
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
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"],
|
ackend for the join-code, leading_side, and projected-standings work is fully verified against a fresh scratch database (migrations 001–011, test_isolation.sql 12/12, and live end-to-end checks: join-code generation/uniqueness, code resolution, visibility bypass on both the tournament view and registration — including case-insensitivity and rejection of a wrong code — plus a full hole-by-hole match simulation confirming leading_side/status_text stay in sync through "1 UP" → "AS" → a decided "9&7", with the leaderboard's projected points matching at each stage: 1.0/0.0 while A led, 0.5/0.5 at all-square, and settling to equal actual/projected once decided).
Ready to deploy to the real system:
Migration: 011_join_code_and_leading_side.sql against real teecup_db (adds tournament.join_code — backfills existing tournaments with generated codes — and match.leading_side, plus the public_tournament_by_code() function).
Redeploy: teecup_api only (no frontend changes yet — those come next).
2026-07-19 09:23:35 +02:00
|
|
|
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"]
|
|
|
|
|
),
|
2026-07-16 09:16:22 +02:00
|
|
|
participants=by_match[m["id"]],
|
|
|
|
|
)
|
|
|
|
|
for m in matches
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
2026-07-19 22:51:07 +02:00
|
|
|
@router.get(
|
|
|
|
|
"/orgs/{organization_id}/sessions/{session_id}/matches",
|
|
|
|
|
response_model=list[MatchOut],
|
|
|
|
|
)
|
|
|
|
|
async def list_matches(
|
|
|
|
|
session_id: str,
|
Update Todos
Flytte is_participant-logikk til team_authz.py (unngå sirkulær import)
Widen scoring.py: get_scorecard/submit_hole_score/submit_hole_result/concede_match
Widen matches.py: list_matches/add_participant/remove_participant/lock_lineup
Widen tournaments.py: list_sessions/list_teams/concede_tournament
Widen courses.py: list_holes
Widen messaging.py: team chat REST-endepunkter (list/send/delete)
Legge til my_session_id/my_match_id i /auth/me sin my_tournaments
Frontend: legge til lenker til lag-chat/scorekort fra «Mine runder»
Scratch-verifisere hele deltaker-tilgang-runden (positive + negative kontroller)
Ekte typesjekket frontend-produksjonsbuild + backend py_compile på nytt før deploy-forslag
Presentere plan og be om bekreftelse før utrulling mot ekte teecup_db/containere
Bygge sekundær e-postadresse (enkelt tilfelle, ADR-032-mønster)
Bygge HCP-historikk over tid
Deltaker-tilgang til lag-chat/scorekort er bygget og grundig scratch-verifisert (43/43 sjekker, test_isolation.sql fortsatt 12/12). Kort oppsummert hva som er gjort:
Backend — den blanke org-medlemskaps-sperren (get_authorized_org) er fjernet fra ni endepunkter (lag-chat lese/skrive/slette, scorekort-lesing, slag/hull-resultat-innsending, walkover på match- og turneringsnivå, match-/lag-/økt-listing, bane-hull) og erstattet med de faktiske, allerede eksisterende autorisasjonssjekkene (user_is_rostered_on_team, user_is_match_participant, user_is_team_captain — alle uendret) — pluss et nytt eksplisitt org-medlem ELLER turnering-deltaker-OR på de endepunktene som tidligere ikke hadde noen finkornet sjekk i det hele tatt (der en ren fjerning ville åpnet dem for enhver innlogget bruker). /auth/me viser nå også my_session_id/my_match_id per rad i "Mine runder".
Frontend — "Mine runder"-kortet har fått to nye handlingslenker: "Lag-chat" (alltid) og "Scorekort" (når spilleren har en aktiv match).
Verifisert grundig: en rostret spiller UTEN organisasjonsmedlemskap får nå riktig tilgang til alt dette; en helt fremmed bruker avvises fortsatt overalt; org-eier beholder full tilgang som før; lag-chatten er fortsatt ekte privat (org-eier fortsatt avvist der, med vilje); kryss-org-isolasjon holder; og en rostret-men-ikke-kaptein spiller ble korrekt avvist fra walkover (fant samtidig en eksisterende, dokumentert fallback-oppførsel — et lag uten utpekt kaptein godtar enhver rostret spiller — som viste seg å fungere akkurat som tiltenkt).
2026-07-22 05:46:47 +02:00
|
|
|
organization_id: str,
|
2026-07-19 22:51:07 +02:00
|
|
|
user: CurrentUser = Depends(get_current_user),
|
|
|
|
|
) -> list[MatchOut]:
|
Update Todos
Flytte is_participant-logikk til team_authz.py (unngå sirkulær import)
Widen scoring.py: get_scorecard/submit_hole_score/submit_hole_result/concede_match
Widen matches.py: list_matches/add_participant/remove_participant/lock_lineup
Widen tournaments.py: list_sessions/list_teams/concede_tournament
Widen courses.py: list_holes
Widen messaging.py: team chat REST-endepunkter (list/send/delete)
Legge til my_session_id/my_match_id i /auth/me sin my_tournaments
Frontend: legge til lenker til lag-chat/scorekort fra «Mine runder»
Scratch-verifisere hele deltaker-tilgang-runden (positive + negative kontroller)
Ekte typesjekket frontend-produksjonsbuild + backend py_compile på nytt før deploy-forslag
Presentere plan og be om bekreftelse før utrulling mot ekte teecup_db/containere
Bygge sekundær e-postadresse (enkelt tilfelle, ADR-032-mønster)
Bygge HCP-historikk over tid
Deltaker-tilgang til lag-chat/scorekort er bygget og grundig scratch-verifisert (43/43 sjekker, test_isolation.sql fortsatt 12/12). Kort oppsummert hva som er gjort:
Backend — den blanke org-medlemskaps-sperren (get_authorized_org) er fjernet fra ni endepunkter (lag-chat lese/skrive/slette, scorekort-lesing, slag/hull-resultat-innsending, walkover på match- og turneringsnivå, match-/lag-/økt-listing, bane-hull) og erstattet med de faktiske, allerede eksisterende autorisasjonssjekkene (user_is_rostered_on_team, user_is_match_participant, user_is_team_captain — alle uendret) — pluss et nytt eksplisitt org-medlem ELLER turnering-deltaker-OR på de endepunktene som tidligere ikke hadde noen finkornet sjekk i det hele tatt (der en ren fjerning ville åpnet dem for enhver innlogget bruker). /auth/me viser nå også my_session_id/my_match_id per rad i "Mine runder".
Frontend — "Mine runder"-kortet har fått to nye handlingslenker: "Lag-chat" (alltid) og "Scorekort" (når spilleren har en aktiv match).
Verifisert grundig: en rostret spiller UTEN organisasjonsmedlemskap får nå riktig tilgang til alt dette; en helt fremmed bruker avvises fortsatt overalt; org-eier beholder full tilgang som før; lag-chatten er fortsatt ekte privat (org-eier fortsatt avvist der, med vilje); kryss-org-isolasjon holder; og en rostret-men-ikke-kaptein spiller ble korrekt avvist fra walkover (fant samtidig en eksisterende, dokumentert fallback-oppførsel — et lag uten utpekt kaptein godtar enhver rostret spiller — som viste seg å fungere akkurat som tiltenkt).
2026-07-22 05:46:47 +02:00
|
|
|
"""2026-07-21 (deltaker-tilgang-runden): fjernet get_authorized_org sin
|
|
|
|
|
blanke org-medlemskapssperre -- erstattet med et eksplisitt OR
|
|
|
|
|
(org-medlem ELLER deltaker i turneringen økten hører til), samme
|
|
|
|
|
begrunnelse som scoring.py sin get_scorecard."""
|
2026-07-19 22:51:07 +02:00
|
|
|
async with org_connection(organization_id) as conn:
|
Update Todos
Flytte is_participant-logikk til team_authz.py (unngå sirkulær import)
Widen scoring.py: get_scorecard/submit_hole_score/submit_hole_result/concede_match
Widen matches.py: list_matches/add_participant/remove_participant/lock_lineup
Widen tournaments.py: list_sessions/list_teams/concede_tournament
Widen courses.py: list_holes
Widen messaging.py: team chat REST-endepunkter (list/send/delete)
Legge til my_session_id/my_match_id i /auth/me sin my_tournaments
Frontend: legge til lenker til lag-chat/scorekort fra «Mine runder»
Scratch-verifisere hele deltaker-tilgang-runden (positive + negative kontroller)
Ekte typesjekket frontend-produksjonsbuild + backend py_compile på nytt før deploy-forslag
Presentere plan og be om bekreftelse før utrulling mot ekte teecup_db/containere
Bygge sekundær e-postadresse (enkelt tilfelle, ADR-032-mønster)
Bygge HCP-historikk over tid
Deltaker-tilgang til lag-chat/scorekort er bygget og grundig scratch-verifisert (43/43 sjekker, test_isolation.sql fortsatt 12/12). Kort oppsummert hva som er gjort:
Backend — den blanke org-medlemskaps-sperren (get_authorized_org) er fjernet fra ni endepunkter (lag-chat lese/skrive/slette, scorekort-lesing, slag/hull-resultat-innsending, walkover på match- og turneringsnivå, match-/lag-/økt-listing, bane-hull) og erstattet med de faktiske, allerede eksisterende autorisasjonssjekkene (user_is_rostered_on_team, user_is_match_participant, user_is_team_captain — alle uendret) — pluss et nytt eksplisitt org-medlem ELLER turnering-deltaker-OR på de endepunktene som tidligere ikke hadde noen finkornet sjekk i det hele tatt (der en ren fjerning ville åpnet dem for enhver innlogget bruker). /auth/me viser nå også my_session_id/my_match_id per rad i "Mine runder".
Frontend — "Mine runder"-kortet har fått to nye handlingslenker: "Lag-chat" (alltid) og "Scorekort" (når spilleren har en aktiv match).
Verifisert grundig: en rostret spiller UTEN organisasjonsmedlemskap får nå riktig tilgang til alt dette; en helt fremmed bruker avvises fortsatt overalt; org-eier beholder full tilgang som før; lag-chatten er fortsatt ekte privat (org-eier fortsatt avvist der, med vilje); kryss-org-isolasjon holder; og en rostret-men-ikke-kaptein spiller ble korrekt avvist fra walkover (fant samtidig en eksisterende, dokumentert fallback-oppførsel — et lag uten utpekt kaptein godtar enhver rostret spiller — som viste seg å fungere akkurat som tiltenkt).
2026-07-22 05:46:47 +02:00
|
|
|
tournament_id = await conn.fetchval("SELECT tournament_id::text FROM session WHERE id = $1", session_id)
|
|
|
|
|
if tournament_id is None:
|
|
|
|
|
raise app_error(404, "NOT_FOUND", "Økten finnes ikke.")
|
|
|
|
|
if not await is_org_member(conn, organization_id, user.user_id) and not await user_is_tournament_participant(
|
|
|
|
|
conn, organization_id, tournament_id, user.user_id
|
|
|
|
|
):
|
|
|
|
|
raise app_error(403, "NOT_ORG_MEMBER", "Du har ikke tilgang til denne turneringen.")
|
2026-07-19 22:51:07 +02:00
|
|
|
return await fetch_matches(conn, organization_id, session_id, user.user_id)
|
|
|
|
|
|
|
|
|
|
|
2026-07-16 09:16:22 +02:00
|
|
|
class ParticipantCreate(BaseModel):
|
|
|
|
|
team_side: str = Field(pattern="^[ab]$")
|
|
|
|
|
team_roster_id: str
|
|
|
|
|
tee_id: str
|
All eight new turneringsformater have backend + engine complete and scratch-verified — Chapman, Nassau, Københavner, Bingo Bango Bongo, Flaggturnering, Shamble, Money Ball, and High-low-high. Built and tested one format at a time (engine tests → migration → API wiring → scratch verification) exactly per the approved plan:
Verification totals: handicap_engine.py unit tests went from 63 → 98 (all passing), plus 400+ scratch-API checks across the eight formats' test scripts, covering both frittstående runder and the appropriate org-scoped system (team or individual) for each. test_isolation.sql stayed 12/12 throughout — migrations 041–050 are purely additive.
A few real things surfaced and were fixed during testing:
Nassau can be blocked by the existing "match already decided" lock (ADR-012) if the overall 18-hole match closes early — documented as a known v1 limitation, not fixed (would mean loosening an established safety guard).
Københavner's points depend on all 3 players at once, breaking the usual "recompute one participant" pattern — handled with a dedicated recompute path.
High-low-high doesn't fit the existing win/loss/halved match cache, so it gets its own dedicated read endpoint (like Nassau) — verified digit-for-digit against your own worked example ("1-1 etter hull 1", "2-1 til lag 2").
Two missing-column crashes (caught in scratch, never reached anything resembling "done").
2026-07-30 14:54:11 +02:00
|
|
|
# Money Ball (2026-07-30) -- KUN meningsfullt for session.format=
|
|
|
|
|
# 'money_ball'. 0-3, unik per (match, side).
|
|
|
|
|
lineup_order: int | None = Field(default=None, ge=0, le=3)
|
2026-07-16 09:16:22 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post(
|
|
|
|
|
"/orgs/{organization_id}/matches/{match_id}/participants",
|
|
|
|
|
response_model=MatchParticipantOut,
|
|
|
|
|
status_code=201,
|
|
|
|
|
)
|
|
|
|
|
async def add_participant(
|
|
|
|
|
match_id: str,
|
|
|
|
|
body: ParticipantCreate,
|
Update Todos
Flytte is_participant-logikk til team_authz.py (unngå sirkulær import)
Widen scoring.py: get_scorecard/submit_hole_score/submit_hole_result/concede_match
Widen matches.py: list_matches/add_participant/remove_participant/lock_lineup
Widen tournaments.py: list_sessions/list_teams/concede_tournament
Widen courses.py: list_holes
Widen messaging.py: team chat REST-endepunkter (list/send/delete)
Legge til my_session_id/my_match_id i /auth/me sin my_tournaments
Frontend: legge til lenker til lag-chat/scorekort fra «Mine runder»
Scratch-verifisere hele deltaker-tilgang-runden (positive + negative kontroller)
Ekte typesjekket frontend-produksjonsbuild + backend py_compile på nytt før deploy-forslag
Presentere plan og be om bekreftelse før utrulling mot ekte teecup_db/containere
Bygge sekundær e-postadresse (enkelt tilfelle, ADR-032-mønster)
Bygge HCP-historikk over tid
Deltaker-tilgang til lag-chat/scorekort er bygget og grundig scratch-verifisert (43/43 sjekker, test_isolation.sql fortsatt 12/12). Kort oppsummert hva som er gjort:
Backend — den blanke org-medlemskaps-sperren (get_authorized_org) er fjernet fra ni endepunkter (lag-chat lese/skrive/slette, scorekort-lesing, slag/hull-resultat-innsending, walkover på match- og turneringsnivå, match-/lag-/økt-listing, bane-hull) og erstattet med de faktiske, allerede eksisterende autorisasjonssjekkene (user_is_rostered_on_team, user_is_match_participant, user_is_team_captain — alle uendret) — pluss et nytt eksplisitt org-medlem ELLER turnering-deltaker-OR på de endepunktene som tidligere ikke hadde noen finkornet sjekk i det hele tatt (der en ren fjerning ville åpnet dem for enhver innlogget bruker). /auth/me viser nå også my_session_id/my_match_id per rad i "Mine runder".
Frontend — "Mine runder"-kortet har fått to nye handlingslenker: "Lag-chat" (alltid) og "Scorekort" (når spilleren har en aktiv match).
Verifisert grundig: en rostret spiller UTEN organisasjonsmedlemskap får nå riktig tilgang til alt dette; en helt fremmed bruker avvises fortsatt overalt; org-eier beholder full tilgang som før; lag-chatten er fortsatt ekte privat (org-eier fortsatt avvist der, med vilje); kryss-org-isolasjon holder; og en rostret-men-ikke-kaptein spiller ble korrekt avvist fra walkover (fant samtidig en eksisterende, dokumentert fallback-oppførsel — et lag uten utpekt kaptein godtar enhver rostret spiller — som viste seg å fungere akkurat som tiltenkt).
2026-07-22 05:46:47 +02:00
|
|
|
organization_id: str,
|
2026-07-16 09:16:22 +02:00
|
|
|
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.")
|
2026-07-16 09:16:22 +02:00
|
|
|
|
|
|
|
|
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.")
|
2026-07-16 09:16:22 +02:00
|
|
|
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."
|
2026-07-16 09:16:22 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
session = await conn.fetchrow(
|
2026-07-16 14:38:42 +02:00
|
|
|
"""
|
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,
|
2026-07-16 14:38:42 +02:00
|
|
|
allowance_override::text AS allowance_override
|
|
|
|
|
FROM session WHERE id = $1
|
|
|
|
|
""",
|
|
|
|
|
match["session_id"],
|
2026-07-16 09:16:22 +02:00
|
|
|
)
|
|
|
|
|
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.")
|
2026-07-16 09:16:22 +02:00
|
|
|
|
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."
|
|
|
|
|
)
|
2026-07-16 09:16:22 +02:00
|
|
|
|
All eight new turneringsformater have backend + engine complete and scratch-verified — Chapman, Nassau, Københavner, Bingo Bango Bongo, Flaggturnering, Shamble, Money Ball, and High-low-high. Built and tested one format at a time (engine tests → migration → API wiring → scratch verification) exactly per the approved plan:
Verification totals: handicap_engine.py unit tests went from 63 → 98 (all passing), plus 400+ scratch-API checks across the eight formats' test scripts, covering both frittstående runder and the appropriate org-scoped system (team or individual) for each. test_isolation.sql stayed 12/12 throughout — migrations 041–050 are purely additive.
A few real things surfaced and were fixed during testing:
Nassau can be blocked by the existing "match already decided" lock (ADR-012) if the overall 18-hole match closes early — documented as a known v1 limitation, not fixed (would mean loosening an established safety guard).
Københavner's points depend on all 3 players at once, breaking the usual "recompute one participant" pattern — handled with a dedicated recompute path.
High-low-high doesn't fit the existing win/loss/halved match cache, so it gets its own dedicated read endpoint (like Nassau) — verified digit-for-digit against your own worked example ("1-1 etter hull 1", "2-1 til lag 2").
Two missing-column crashes (caught in scratch, never reached anything resembling "done").
2026-07-30 14:54:11 +02:00
|
|
|
# Shamble (2026-07-30): variabel lagstørrelse (2-4), IKKE en fast
|
|
|
|
|
# FORMAT_UNIT_SIZE -- compute_and_store_side_handicaps sin vanlige
|
|
|
|
|
# SIDE_IS_UNIT-kapasitetssjekk gjelder ikke her (Shamble aliaser til
|
|
|
|
|
# "singles", individuell enhet, telle-agnostisk). Håndhever derfor
|
|
|
|
|
# kun et TAK her (min-2-sjekk finnes bevisst ikke ennå for org-
|
|
|
|
|
# lagturneringer -- ingen tilsvarende "er oppsettet komplett"-gate
|
|
|
|
|
# finnes for individuelle formater der i dag, samme som fourball).
|
|
|
|
|
if session["format"] == "shamble":
|
|
|
|
|
side_count = await conn.fetchval(
|
|
|
|
|
"SELECT count(*) FROM match_participant WHERE match_id = $1 AND team_side = $2::team_side",
|
|
|
|
|
match_id,
|
|
|
|
|
body.team_side,
|
|
|
|
|
)
|
|
|
|
|
if side_count >= 4:
|
|
|
|
|
raise app_error(409, "LIMIT_REACHED", "Et Shamble-lag kan maks ha 4 spillere.")
|
|
|
|
|
|
|
|
|
|
# Money Ball (2026-07-30): FAST 4 -- ikke fleksibel som Shamble.
|
|
|
|
|
# lineup_order (0-3) er PÅKREVD her (avgjør rotasjonen), unik per
|
|
|
|
|
# (match, side) -- håndhevet eksplisitt FØR innsetting, ikke kun
|
|
|
|
|
# via DB-constrainten, for en tydelig feilmelding.
|
|
|
|
|
if session["format"] == "money_ball":
|
|
|
|
|
side_count = await conn.fetchval(
|
|
|
|
|
"SELECT count(*) FROM match_participant WHERE match_id = $1 AND team_side = $2::team_side",
|
|
|
|
|
match_id,
|
|
|
|
|
body.team_side,
|
|
|
|
|
)
|
|
|
|
|
if side_count >= 4:
|
|
|
|
|
raise app_error(409, "LIMIT_REACHED", "Et Money Ball-lag kan maks ha 4 spillere.")
|
|
|
|
|
if body.lineup_order is None:
|
|
|
|
|
raise app_error(400, "VALIDATION_FAILED", "lineup_order er påkrevd for Money Ball.")
|
|
|
|
|
taken = await conn.fetchval(
|
|
|
|
|
"SELECT 1 FROM match_participant WHERE match_id = $1 AND team_side = $2::team_side AND lineup_order = $3",
|
|
|
|
|
match_id,
|
|
|
|
|
body.team_side,
|
|
|
|
|
body.lineup_order,
|
|
|
|
|
)
|
|
|
|
|
if taken:
|
|
|
|
|
raise app_error(409, "DUPLICATE", "En annen spiller på laget har allerede denne plassen i rekkefølgen.")
|
|
|
|
|
|
2026-07-16 09:16:22 +02:00
|
|
|
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."
|
2026-07-16 09:16:22 +02:00
|
|
|
)
|
|
|
|
|
|
2026-07-18 17:15:50 +02:00
|
|
|
# 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,
|
2026-07-18 17:15:50 +02:00
|
|
|
)
|
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:
|
2026-07-18 17:15:50 +02:00
|
|
|
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.",
|
|
|
|
|
)
|
2026-07-18 17:15:50 +02:00
|
|
|
|
2026-07-16 09:16:22 +02:00
|
|
|
row = await conn.fetchrow(
|
|
|
|
|
"""
|
|
|
|
|
WITH inserted AS (
|
|
|
|
|
INSERT INTO match_participant
|
All eight new turneringsformater have backend + engine complete and scratch-verified — Chapman, Nassau, Københavner, Bingo Bango Bongo, Flaggturnering, Shamble, Money Ball, and High-low-high. Built and tested one format at a time (engine tests → migration → API wiring → scratch verification) exactly per the approved plan:
Verification totals: handicap_engine.py unit tests went from 63 → 98 (all passing), plus 400+ scratch-API checks across the eight formats' test scripts, covering both frittstående runder and the appropriate org-scoped system (team or individual) for each. test_isolation.sql stayed 12/12 throughout — migrations 041–050 are purely additive.
A few real things surfaced and were fixed during testing:
Nassau can be blocked by the existing "match already decided" lock (ADR-012) if the overall 18-hole match closes early — documented as a known v1 limitation, not fixed (would mean loosening an established safety guard).
Københavner's points depend on all 3 players at once, breaking the usual "recompute one participant" pattern — handled with a dedicated recompute path.
High-low-high doesn't fit the existing win/loss/halved match cache, so it gets its own dedicated read endpoint (like Nassau) — verified digit-for-digit against your own worked example ("1-1 etter hull 1", "2-1 til lag 2").
Two missing-column crashes (caught in scratch, never reached anything resembling "done").
2026-07-30 14:54:11 +02:00
|
|
|
(organization_id, match_id, team_side, team_roster_id, tee_id, lineup_order)
|
|
|
|
|
VALUES ($1, $2, $3, $4, $5, $6)
|
|
|
|
|
RETURNING id, team_side, team_roster_id, tee_id, lineup_order
|
2026-07-16 09:16:22 +02:00
|
|
|
)
|
|
|
|
|
SELECT inserted.id::text, inserted.team_side::text,
|
|
|
|
|
inserted.team_roster_id::text, p.display_name AS player_name,
|
All eight new turneringsformater have backend + engine complete and scratch-verified — Chapman, Nassau, Københavner, Bingo Bango Bongo, Flaggturnering, Shamble, Money Ball, and High-low-high. Built and tested one format at a time (engine tests → migration → API wiring → scratch verification) exactly per the approved plan:
Verification totals: handicap_engine.py unit tests went from 63 → 98 (all passing), plus 400+ scratch-API checks across the eight formats' test scripts, covering both frittstående runder and the appropriate org-scoped system (team or individual) for each. test_isolation.sql stayed 12/12 throughout — migrations 041–050 are purely additive.
A few real things surfaced and were fixed during testing:
Nassau can be blocked by the existing "match already decided" lock (ADR-012) if the overall 18-hole match closes early — documented as a known v1 limitation, not fixed (would mean loosening an established safety guard).
Københavner's points depend on all 3 players at once, breaking the usual "recompute one participant" pattern — handled with a dedicated recompute path.
High-low-high doesn't fit the existing win/loss/halved match cache, so it gets its own dedicated read endpoint (like Nassau) — verified digit-for-digit against your own worked example ("1-1 etter hull 1", "2-1 til lag 2").
Two missing-column crashes (caught in scratch, never reached anything resembling "done").
2026-07-30 14:54:11 +02:00
|
|
|
inserted.tee_id::text, tee.name AS tee_name, inserted.lineup_order
|
2026-07-16 09:16:22 +02:00
|
|
|
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,
|
All eight new turneringsformater have backend + engine complete and scratch-verified — Chapman, Nassau, Københavner, Bingo Bango Bongo, Flaggturnering, Shamble, Money Ball, and High-low-high. Built and tested one format at a time (engine tests → migration → API wiring → scratch verification) exactly per the approved plan:
Verification totals: handicap_engine.py unit tests went from 63 → 98 (all passing), plus 400+ scratch-API checks across the eight formats' test scripts, covering both frittstående runder and the appropriate org-scoped system (team or individual) for each. test_isolation.sql stayed 12/12 throughout — migrations 041–050 are purely additive.
A few real things surfaced and were fixed during testing:
Nassau can be blocked by the existing "match already decided" lock (ADR-012) if the overall 18-hole match closes early — documented as a known v1 limitation, not fixed (would mean loosening an established safety guard).
Københavner's points depend on all 3 players at once, breaking the usual "recompute one participant" pattern — handled with a dedicated recompute path.
High-low-high doesn't fit the existing win/loss/halved match cache, so it gets its own dedicated read endpoint (like Nassau) — verified digit-for-digit against your own worked example ("1-1 etter hull 1", "2-1 til lag 2").
Two missing-column crashes (caught in scratch, never reached anything resembling "done").
2026-07-30 14:54:11 +02:00
|
|
|
body.lineup_order,
|
2026-07-16 09:16:22 +02:00
|
|
|
)
|
2026-07-16 14:38:42 +02:00
|
|
|
|
|
|
|
|
# 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
|
2026-07-18 17:15:50 +02:00
|
|
|
# komplett) -- se app/handicap.py. `config` allerede parset over.
|
2026-07-16 14:38:42 +02:00
|
|
|
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
|
2026-07-16 14:38:42 +02:00
|
|
|
)
|
2026-07-29 12:44:51 +02:00
|
|
|
# row ble hentet FØR beregningen over -- les playing_handicap på nytt
|
|
|
|
|
# slik at responsen ikke lyver om en verdi som faktisk finnes.
|
|
|
|
|
playing_handicap = await conn.fetchval(
|
|
|
|
|
"SELECT playing_handicap FROM match_participant WHERE id = $1::uuid", row["id"]
|
|
|
|
|
)
|
|
|
|
|
return MatchParticipantOut(**dict(row), playing_handicap=playing_handicap)
|
2026-07-16 09:16:22 +02:00
|
|
|
|
|
|
|
|
|
2026-07-18 17:15:50 +02:00
|
|
|
@router.delete(
|
|
|
|
|
"/orgs/{organization_id}/matches/{match_id}/participants/{participant_id}",
|
|
|
|
|
status_code=204,
|
|
|
|
|
)
|
|
|
|
|
async def remove_participant(
|
|
|
|
|
match_id: str,
|
|
|
|
|
participant_id: str,
|
Update Todos
Flytte is_participant-logikk til team_authz.py (unngå sirkulær import)
Widen scoring.py: get_scorecard/submit_hole_score/submit_hole_result/concede_match
Widen matches.py: list_matches/add_participant/remove_participant/lock_lineup
Widen tournaments.py: list_sessions/list_teams/concede_tournament
Widen courses.py: list_holes
Widen messaging.py: team chat REST-endepunkter (list/send/delete)
Legge til my_session_id/my_match_id i /auth/me sin my_tournaments
Frontend: legge til lenker til lag-chat/scorekort fra «Mine runder»
Scratch-verifisere hele deltaker-tilgang-runden (positive + negative kontroller)
Ekte typesjekket frontend-produksjonsbuild + backend py_compile på nytt før deploy-forslag
Presentere plan og be om bekreftelse før utrulling mot ekte teecup_db/containere
Bygge sekundær e-postadresse (enkelt tilfelle, ADR-032-mønster)
Bygge HCP-historikk over tid
Deltaker-tilgang til lag-chat/scorekort er bygget og grundig scratch-verifisert (43/43 sjekker, test_isolation.sql fortsatt 12/12). Kort oppsummert hva som er gjort:
Backend — den blanke org-medlemskaps-sperren (get_authorized_org) er fjernet fra ni endepunkter (lag-chat lese/skrive/slette, scorekort-lesing, slag/hull-resultat-innsending, walkover på match- og turneringsnivå, match-/lag-/økt-listing, bane-hull) og erstattet med de faktiske, allerede eksisterende autorisasjonssjekkene (user_is_rostered_on_team, user_is_match_participant, user_is_team_captain — alle uendret) — pluss et nytt eksplisitt org-medlem ELLER turnering-deltaker-OR på de endepunktene som tidligere ikke hadde noen finkornet sjekk i det hele tatt (der en ren fjerning ville åpnet dem for enhver innlogget bruker). /auth/me viser nå også my_session_id/my_match_id per rad i "Mine runder".
Frontend — "Mine runder"-kortet har fått to nye handlingslenker: "Lag-chat" (alltid) og "Scorekort" (når spilleren har en aktiv match).
Verifisert grundig: en rostret spiller UTEN organisasjonsmedlemskap får nå riktig tilgang til alt dette; en helt fremmed bruker avvises fortsatt overalt; org-eier beholder full tilgang som før; lag-chatten er fortsatt ekte privat (org-eier fortsatt avvist der, med vilje); kryss-org-isolasjon holder; og en rostret-men-ikke-kaptein spiller ble korrekt avvist fra walkover (fant samtidig en eksisterende, dokumentert fallback-oppførsel — et lag uten utpekt kaptein godtar enhver rostret spiller — som viste seg å fungere akkurat som tiltenkt).
2026-07-22 05:46:47 +02:00
|
|
|
organization_id: str,
|
2026-07-18 17:15:50 +02:00
|
|
|
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 på 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"]
|
|
|
|
|
|
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."
|
|
|
|
|
)
|
2026-07-18 17:15:50 +02:00
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
2026-07-16 09:16:22 +02:00
|
|
|
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,
|
Update Todos
Flytte is_participant-logikk til team_authz.py (unngå sirkulær import)
Widen scoring.py: get_scorecard/submit_hole_score/submit_hole_result/concede_match
Widen matches.py: list_matches/add_participant/remove_participant/lock_lineup
Widen tournaments.py: list_sessions/list_teams/concede_tournament
Widen courses.py: list_holes
Widen messaging.py: team chat REST-endepunkter (list/send/delete)
Legge til my_session_id/my_match_id i /auth/me sin my_tournaments
Frontend: legge til lenker til lag-chat/scorekort fra «Mine runder»
Scratch-verifisere hele deltaker-tilgang-runden (positive + negative kontroller)
Ekte typesjekket frontend-produksjonsbuild + backend py_compile på nytt før deploy-forslag
Presentere plan og be om bekreftelse før utrulling mot ekte teecup_db/containere
Bygge sekundær e-postadresse (enkelt tilfelle, ADR-032-mønster)
Bygge HCP-historikk over tid
Deltaker-tilgang til lag-chat/scorekort er bygget og grundig scratch-verifisert (43/43 sjekker, test_isolation.sql fortsatt 12/12). Kort oppsummert hva som er gjort:
Backend — den blanke org-medlemskaps-sperren (get_authorized_org) er fjernet fra ni endepunkter (lag-chat lese/skrive/slette, scorekort-lesing, slag/hull-resultat-innsending, walkover på match- og turneringsnivå, match-/lag-/økt-listing, bane-hull) og erstattet med de faktiske, allerede eksisterende autorisasjonssjekkene (user_is_rostered_on_team, user_is_match_participant, user_is_team_captain — alle uendret) — pluss et nytt eksplisitt org-medlem ELLER turnering-deltaker-OR på de endepunktene som tidligere ikke hadde noen finkornet sjekk i det hele tatt (der en ren fjerning ville åpnet dem for enhver innlogget bruker). /auth/me viser nå også my_session_id/my_match_id per rad i "Mine runder".
Frontend — "Mine runder"-kortet har fått to nye handlingslenker: "Lag-chat" (alltid) og "Scorekort" (når spilleren har en aktiv match).
Verifisert grundig: en rostret spiller UTEN organisasjonsmedlemskap får nå riktig tilgang til alt dette; en helt fremmed bruker avvises fortsatt overalt; org-eier beholder full tilgang som før; lag-chatten er fortsatt ekte privat (org-eier fortsatt avvist der, med vilje); kryss-org-isolasjon holder; og en rostret-men-ikke-kaptein spiller ble korrekt avvist fra walkover (fant samtidig en eksisterende, dokumentert fallback-oppførsel — et lag uten utpekt kaptein godtar enhver rostret spiller — som viste seg å fungere akkurat som tiltenkt).
2026-07-22 05:46:47 +02:00
|
|
|
organization_id: str,
|
2026-07-16 09:16:22 +02:00
|
|
|
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.")
|
2026-07-16 09:16:22 +02:00
|
|
|
|
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."
|
|
|
|
|
)
|
2026-07-16 09:16:22 +02:00
|
|
|
|
|
|
|
|
# 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))
|