Program-skjermen er bygget og grundig scratch-verifisert. Kort oppsummert:
Nytt:
app/routers/courses.py — enkel bane-CRUD (GET/POST /orgs/{id}/courses), fant og tettet et reelt hull: SessionCreate.course_id var påkrevd, men ingen vei fantes til å skaffe én
components/tournament-program.tsx + rute /tournaments/[id]/program — tidslinje over økter, opprett-skjema med bane-type-ahead og avanserte handicap-brytere
Fanerad lagt til i både roster- og program-skjermen så du kan bevege deg mellom dem
To reelle feil rettet før integrering:
V0-promptet mitt ba om ett generisk "Scramble"-format, men databasen/motoren krever scramble_2/scramble_4 som atskilte verdier — rettet til to segment-knapper
Verifiserte allowance_override-JSON-formen eksakt mot parse_allowance_config (typet combined/per_player + 0–1-brøk, ikke flat prosent) — bekreftet med en ekte rundtur i scratch, ikke bare lest fra koden
Verifisert: courses opprettet+listet, kryss-org-isolasjon, økt med klokkeslett, økt med scramble_4+full handicap-override-rundtur, gammel "scramble"-verdi korrekt avvist, test_isolation.sql 12/12, ekte typesjekket produksjonsbuild (samme Dockerfile som deployes).
2026-07-18 12:20:45 +02:00
|
|
|
|
"""
|
|
|
|
|
|
Egendefinerte baner på organisasjonsnivå (ADR-001: gjenbrukbar på tvers av
|
|
|
|
|
|
turneringer, samme mønster som players.py).
|
|
|
|
|
|
|
|
|
|
|
|
`course.source = 'custom'` er den eneste veien inn her -- 'official' (ekte
|
|
|
|
|
|
teeoff-baner via ADR-004 sitt planlagte, lesende API) er fortsatt kun vedtatt,
|
|
|
|
|
|
ikke bygget (se FEATURE_BACKLOG.md), og krever uansett `external_course_ref`
|
|
|
|
|
|
satt (skjemaets CHECK-constraint), som ingen klient kan oppgi meningsfullt
|
|
|
|
|
|
ennå. Denne runden dekker KUN det organisatoren trenger for å opprette en
|
|
|
|
|
|
økt: en bane ved navn, ingen hull-/tee-/rating-detaljer ennå (session-motoren
|
|
|
|
|
|
bruker foreløpig kun course_id som fremmednøkkel, ikke banens innhold).
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
Bygget, og verifisert grundig mot ekte infrastruktur — inkludert et ekte kall mot teeoff_api (søkte opp «Borregaard», importerte Borregaard Golfklubb sin 18-hulls hovedbane med alle hull, 4 tee-farger × kjønn, ratinger, og opprettet faktisk en økt med den importerte banen). Duplikat-import ble korrekt avvist (409), kryss-org-isolasjon holder, test_isolation.sql 12/12.
To ting gjenstår, begge mot ekte infrastruktur — vil du bekrefte at jeg går videre?
Migrasjon 010_official_course_unique_ref.sql mot ekte teecup_db — kun én ny partiell unik-indeks (organization_id, external_course_ref), rører ingen eksisterende rader (alle er source='custom' med external_course_ref IS NULL i dag)
docker compose up -d --build teecup_api teecup_frontend — ny backend-kode (courses.py, teeoff_client.py, httpx-avhengighet) + ny frontend-kode (bane-søk mot teeoff i program-skjemaet)
2026-07-18 12:44:05 +02:00
|
|
|
|
from fastapi import APIRouter, Depends, Query
|
Program-skjermen er bygget og grundig scratch-verifisert. Kort oppsummert:
Nytt:
app/routers/courses.py — enkel bane-CRUD (GET/POST /orgs/{id}/courses), fant og tettet et reelt hull: SessionCreate.course_id var påkrevd, men ingen vei fantes til å skaffe én
components/tournament-program.tsx + rute /tournaments/[id]/program — tidslinje over økter, opprett-skjema med bane-type-ahead og avanserte handicap-brytere
Fanerad lagt til i både roster- og program-skjermen så du kan bevege deg mellom dem
To reelle feil rettet før integrering:
V0-promptet mitt ba om ett generisk "Scramble"-format, men databasen/motoren krever scramble_2/scramble_4 som atskilte verdier — rettet til to segment-knapper
Verifiserte allowance_override-JSON-formen eksakt mot parse_allowance_config (typet combined/per_player + 0–1-brøk, ikke flat prosent) — bekreftet med en ekte rundtur i scratch, ikke bare lest fra koden
Verifisert: courses opprettet+listet, kryss-org-isolasjon, økt med klokkeslett, økt med scramble_4+full handicap-override-rundtur, gammel "scramble"-verdi korrekt avvist, test_isolation.sql 12/12, ekte typesjekket produksjonsbuild (samme Dockerfile som deployes).
2026-07-18 12:20:45 +02:00
|
|
|
|
|
2026-08-12 21:39:13 +02:00
|
|
|
|
from .. import golfapi_cache, golfapi_client, teeoff_client
|
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 ..auth import CurrentUser, get_authorized_org, get_current_user
|
2026-08-12 21:39:13 +02:00
|
|
|
|
from ..config import settings
|
Program-skjermen er bygget og grundig scratch-verifisert. Kort oppsummert:
Nytt:
app/routers/courses.py — enkel bane-CRUD (GET/POST /orgs/{id}/courses), fant og tettet et reelt hull: SessionCreate.course_id var påkrevd, men ingen vei fantes til å skaffe én
components/tournament-program.tsx + rute /tournaments/[id]/program — tidslinje over økter, opprett-skjema med bane-type-ahead og avanserte handicap-brytere
Fanerad lagt til i både roster- og program-skjermen så du kan bevege deg mellom dem
To reelle feil rettet før integrering:
V0-promptet mitt ba om ett generisk "Scramble"-format, men databasen/motoren krever scramble_2/scramble_4 som atskilte verdier — rettet til to segment-knapper
Verifiserte allowance_override-JSON-formen eksakt mot parse_allowance_config (typet combined/per_player + 0–1-brøk, ikke flat prosent) — bekreftet med en ekte rundtur i scratch, ikke bare lest fra koden
Verifisert: courses opprettet+listet, kryss-org-isolasjon, økt med klokkeslett, økt med scramble_4+full handicap-override-rundtur, gammel "scramble"-verdi korrekt avvist, test_isolation.sql 12/12, ekte typesjekket produksjonsbuild (samme Dockerfile som deployes).
2026-07-18 12:20:45 +02:00
|
|
|
|
from ..db import org_connection
|
Bygget, og verifisert grundig mot ekte infrastruktur — inkludert et ekte kall mot teeoff_api (søkte opp «Borregaard», importerte Borregaard Golfklubb sin 18-hulls hovedbane med alle hull, 4 tee-farger × kjønn, ratinger, og opprettet faktisk en økt med den importerte banen). Duplikat-import ble korrekt avvist (409), kryss-org-isolasjon holder, test_isolation.sql 12/12.
To ting gjenstår, begge mot ekte infrastruktur — vil du bekrefte at jeg går videre?
Migrasjon 010_official_course_unique_ref.sql mot ekte teecup_db — kun én ny partiell unik-indeks (organization_id, external_course_ref), rører ingen eksisterende rader (alle er source='custom' med external_course_ref IS NULL i dag)
docker compose up -d --build teecup_api teecup_frontend — ny backend-kode (courses.py, teeoff_client.py, httpx-avhengighet) + ny frontend-kode (bane-søk mot teeoff i program-skjemaet)
2026-07-18 12:44:05 +02:00
|
|
|
|
from ..errors import app_error, translate_db_errors
|
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_org_player
|
Program-skjermen er bygget og grundig scratch-verifisert. Kort oppsummert:
Nytt:
app/routers/courses.py — enkel bane-CRUD (GET/POST /orgs/{id}/courses), fant og tettet et reelt hull: SessionCreate.course_id var påkrevd, men ingen vei fantes til å skaffe én
components/tournament-program.tsx + rute /tournaments/[id]/program — tidslinje over økter, opprett-skjema med bane-type-ahead og avanserte handicap-brytere
Fanerad lagt til i både roster- og program-skjermen så du kan bevege deg mellom dem
To reelle feil rettet før integrering:
V0-promptet mitt ba om ett generisk "Scramble"-format, men databasen/motoren krever scramble_2/scramble_4 som atskilte verdier — rettet til to segment-knapper
Verifiserte allowance_override-JSON-formen eksakt mot parse_allowance_config (typet combined/per_player + 0–1-brøk, ikke flat prosent) — bekreftet med en ekte rundtur i scratch, ikke bare lest fra koden
Verifisert: courses opprettet+listet, kryss-org-isolasjon, økt med klokkeslett, økt med scramble_4+full handicap-override-rundtur, gammel "scramble"-verdi korrekt avvist, test_isolation.sql 12/12, ekte typesjekket produksjonsbuild (samme Dockerfile som deployes).
2026-07-18 12:20:45 +02:00
|
|
|
|
from pydantic import BaseModel, Field
|
|
|
|
|
|
|
|
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Course(BaseModel):
|
|
|
|
|
|
id: str
|
|
|
|
|
|
name: str
|
|
|
|
|
|
source: str
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_COURSE_COLUMNS = "id::text, name, source::text AS source"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/orgs/{organization_id}/courses", response_model=list[Course])
|
|
|
|
|
|
async def list_courses(
|
|
|
|
|
|
organization_id: str = Depends(get_authorized_org),
|
|
|
|
|
|
) -> list[Course]:
|
|
|
|
|
|
async with org_connection(organization_id) as conn:
|
|
|
|
|
|
rows = await conn.fetch(f"SELECT {_COURSE_COLUMNS} FROM course ORDER BY name")
|
|
|
|
|
|
return [Course(**dict(r)) for r in rows]
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-18 17:15:50 +02:00
|
|
|
|
# --- Tee-er (ADR-013-relatert: match_participant.tee_id krever en ekte tee) --
|
|
|
|
|
|
#
|
|
|
|
|
|
# Manglet helt frem til nå -- verken en liste-vei (selv offisielt importerte
|
|
|
|
|
|
# baner har tee-rader, men ingenting eksponerte dem) eller en opprett-vei for
|
|
|
|
|
|
# egendefinerte baner (de har ALDRI hatt noen vei til tee-data, uavhengig av
|
|
|
|
|
|
# ADR-019). Reist av brukeren rett før blind draw-skjermen: uten dette kan en
|
|
|
|
|
|
# match aldri få en deltaker (tee_id er påkrevd, ingen gyldig verdi finnes).
|
|
|
|
|
|
# Kun full_18-rating dekket her, samme begrunnelse som ADR-019 Beslutning D.
|
|
|
|
|
|
# Hull-/stroke-index-data for egendefinerte baner (trengs for korrekt
|
|
|
|
|
|
# slagfordeling i SCORING-fasen, ikke i blind draw) er bevisst UTENFOR
|
|
|
|
|
|
# omfang denne runden -- egen, senere sak når scorekort-skjermen bygges.
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
class TeeRatingInput(BaseModel):
|
|
|
|
|
|
gender: str = Field(pattern="^[mf]$")
|
2026-07-18 17:15:50 +02:00
|
|
|
|
course_rating: float = Field(gt=0)
|
|
|
|
|
|
slope_rating: int = Field(ge=55, le=155)
|
|
|
|
|
|
par: int = Field(ge=27, le=90)
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
class TeeCreate(BaseModel):
|
|
|
|
|
|
name: str = Field(min_length=1, max_length=50)
|
|
|
|
|
|
# ADR-029: kjønn hører til RATINGEN, ikke selve utslaget -- ett fysisk
|
|
|
|
|
|
# utslag kan ha én rating per kjønn (aldri to av samme), organisator
|
|
|
|
|
|
# oppgir hvilke som faktisk finnes for banen (ofte begge, noen ganger
|
|
|
|
|
|
# bare én -- klubben har bevisst ikke slopet den andre).
|
|
|
|
|
|
ratings: list[TeeRatingInput] = Field(min_length=1, max_length=2)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TeeRatingOut(BaseModel):
|
|
|
|
|
|
gender: str
|
2026-07-18 17:15:50 +02:00
|
|
|
|
course_rating: float
|
|
|
|
|
|
slope_rating: int
|
|
|
|
|
|
par: int
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
class Tee(BaseModel):
|
|
|
|
|
|
id: str
|
|
|
|
|
|
name: str
|
|
|
|
|
|
ratings: list[TeeRatingOut]
|
2026-07-18 17:15:50 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/orgs/{organization_id}/courses/{course_id}/tees", response_model=list[Tee])
|
|
|
|
|
|
async def list_tees(
|
|
|
|
|
|
course_id: str,
|
|
|
|
|
|
organization_id: str = Depends(get_authorized_org),
|
|
|
|
|
|
) -> list[Tee]:
|
|
|
|
|
|
async with org_connection(organization_id) as conn:
|
|
|
|
|
|
rows = await conn.fetch(
|
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 t.id::text AS id, t.name,
|
|
|
|
|
|
tr.gender, tr.course_rating::float AS course_rating,
|
|
|
|
|
|
tr.slope_rating, tr.par
|
2026-07-18 17:15:50 +02:00
|
|
|
|
FROM tee t
|
|
|
|
|
|
JOIN tee_rating tr ON tr.tee_id = t.id AND tr.scope = 'full_18'
|
|
|
|
|
|
WHERE t.course_id = $1
|
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
|
|
|
|
ORDER BY t.name, tr.gender
|
2026-07-18 17:15:50 +02:00
|
|
|
|
""",
|
|
|
|
|
|
course_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
|
|
|
|
tees: dict[str, Tee] = {}
|
|
|
|
|
|
for r in rows:
|
|
|
|
|
|
tee = tees.setdefault(r["id"], Tee(id=r["id"], name=r["name"], ratings=[]))
|
|
|
|
|
|
tee.ratings.append(
|
|
|
|
|
|
TeeRatingOut(
|
|
|
|
|
|
gender=r["gender"],
|
|
|
|
|
|
course_rating=r["course_rating"],
|
|
|
|
|
|
slope_rating=r["slope_rating"],
|
|
|
|
|
|
par=r["par"],
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
return list(tees.values())
|
2026-07-18 17:15:50 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/orgs/{organization_id}/courses/{course_id}/tees", response_model=Tee, status_code=201)
|
|
|
|
|
|
async def create_tee(
|
|
|
|
|
|
course_id: str,
|
|
|
|
|
|
body: TeeCreate,
|
|
|
|
|
|
organization_id: str = Depends(get_authorized_org),
|
|
|
|
|
|
) -> Tee:
|
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
|
|
|
|
genders = [r.gender for r in body.ratings]
|
|
|
|
|
|
if len(genders) != len(set(genders)):
|
|
|
|
|
|
raise app_error(
|
|
|
|
|
|
400, "VALIDATION_FAILED", "Kan ikke oppgi to ratinger for samme kjønn på ett utslag."
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-07-18 17:15:50 +02:00
|
|
|
|
async with org_connection(organization_id) as conn, translate_db_errors():
|
|
|
|
|
|
source = await conn.fetchval("SELECT source FROM course WHERE id = $1", course_id)
|
|
|
|
|
|
if source is None:
|
|
|
|
|
|
raise app_error(404, "NOT_FOUND", "Banen finnes ikke.")
|
|
|
|
|
|
if source == "official":
|
|
|
|
|
|
raise app_error(
|
|
|
|
|
|
400,
|
|
|
|
|
|
"VALIDATION_FAILED",
|
|
|
|
|
|
"Offisielle baner får tee-ene sine fra teeoff -- kan ikke legges til manuelt.",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
tee_row = 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
|
|
|
|
"INSERT INTO tee (organization_id, course_id, name) VALUES ($1, $2, $3) RETURNING id::text",
|
2026-07-18 17:15:50 +02:00
|
|
|
|
organization_id,
|
|
|
|
|
|
course_id,
|
|
|
|
|
|
body.name,
|
|
|
|
|
|
)
|
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
|
|
|
|
ratings_out: list[TeeRatingOut] = []
|
|
|
|
|
|
for r in body.ratings:
|
|
|
|
|
|
await conn.execute(
|
|
|
|
|
|
"""
|
|
|
|
|
|
INSERT INTO tee_rating
|
|
|
|
|
|
(organization_id, tee_id, scope, gender, course_rating, slope_rating, par)
|
|
|
|
|
|
VALUES ($1, $2, 'full_18', $3, $4, $5, $6)
|
|
|
|
|
|
""",
|
|
|
|
|
|
organization_id,
|
|
|
|
|
|
tee_row["id"],
|
|
|
|
|
|
r.gender,
|
|
|
|
|
|
r.course_rating,
|
|
|
|
|
|
r.slope_rating,
|
|
|
|
|
|
r.par,
|
|
|
|
|
|
)
|
|
|
|
|
|
ratings_out.append(
|
|
|
|
|
|
TeeRatingOut(
|
|
|
|
|
|
gender=r.gender, course_rating=r.course_rating, slope_rating=r.slope_rating, par=r.par
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
return Tee(id=tee_row["id"], name=body.name, ratings=ratings_out)
|
2026-07-18 17:15:50 +02:00
|
|
|
|
|
|
|
|
|
|
|
2026-07-18 17:47:33 +02:00
|
|
|
|
# --- Hull (ADR-008-relatert: stroke_index kreves for slagfordeling i --
|
|
|
|
|
|
# scoring-fasen -- se app/routers/scoring.py sin bruk av `hole`-tabellen) -
|
|
|
|
|
|
#
|
|
|
|
|
|
# Egendefinerte baner har ALDRI hatt noen vei til hull-data, uavhengig av
|
|
|
|
|
|
# ADR-019 (samme klasse hull som tee-mangelen -- flagget FØR scorekort-
|
|
|
|
|
|
# skjermen ble bygget, ikke oppdaget for sent). Kun `stroke`-scoringsmodus
|
|
|
|
|
|
# trenger dette; `hole_result`-modus fungerer allerede uten. Engangs
|
|
|
|
|
|
# oppsett: enten alle 18 hull settes i ÉN innsending, eller ingen -- unngår
|
|
|
|
|
|
# kompleksiteten med delvise/partielle hull-sett.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class HoleCreate(BaseModel):
|
|
|
|
|
|
hole_number: int = Field(ge=1, le=18)
|
|
|
|
|
|
par: int = Field(ge=3, le=6)
|
|
|
|
|
|
stroke_index: int = Field(ge=1, le=18)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Hole(BaseModel):
|
|
|
|
|
|
hole_number: int
|
|
|
|
|
|
par: int
|
|
|
|
|
|
stroke_index: int
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/orgs/{organization_id}/courses/{course_id}/holes", response_model=list[Hole])
|
|
|
|
|
|
async def list_holes(
|
|
|
|
|
|
course_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,
|
|
|
|
|
|
user: CurrentUser = Depends(get_current_user),
|
2026-07-18 17:47:33 +02:00
|
|
|
|
) -> list[Hole]:
|
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): org-medlem ELLER en bruker
|
|
|
|
|
|
koblet til NOEN spillerprofil i organisasjonen (`user_is_org_player`,
|
|
|
|
|
|
bevisst løsere enn en full turnering-deltaker-sjekk -- par/stroke-index
|
|
|
|
|
|
er lavsensitiv banedata, ikke spillerdata, og en bane er ikke bundet til
|
|
|
|
|
|
én bestemt turnering)."""
|
2026-07-18 17:47:33 +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
|
|
|
|
if not await is_org_member(conn, organization_id, user.user_id) and not await user_is_org_player(
|
|
|
|
|
|
conn, organization_id, user.user_id
|
|
|
|
|
|
):
|
|
|
|
|
|
raise app_error(403, "NOT_ORG_MEMBER", "Du har ikke tilgang til denne organisasjonen.")
|
2026-07-18 17:47:33 +02:00
|
|
|
|
rows = await conn.fetch(
|
|
|
|
|
|
"SELECT hole_number, par, stroke_index FROM hole WHERE course_id = $1 ORDER BY hole_number",
|
|
|
|
|
|
course_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
return [Hole(**dict(r)) for r in rows]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/orgs/{organization_id}/courses/{course_id}/holes", response_model=list[Hole], status_code=201)
|
|
|
|
|
|
async def create_holes(
|
|
|
|
|
|
course_id: str,
|
|
|
|
|
|
body: list[HoleCreate],
|
|
|
|
|
|
organization_id: str = Depends(get_authorized_org),
|
|
|
|
|
|
) -> list[Hole]:
|
|
|
|
|
|
if len(body) != 18:
|
|
|
|
|
|
raise app_error(400, "VALIDATION_FAILED", "Må oppgi nøyaktig 18 hull i én innsending.")
|
|
|
|
|
|
numbers = sorted(h.hole_number for h in body)
|
|
|
|
|
|
if numbers != list(range(1, 19)):
|
|
|
|
|
|
raise app_error(400, "VALIDATION_FAILED", "Hullnumrene må dekke nøyaktig 1-18, hver én gang.")
|
|
|
|
|
|
|
|
|
|
|
|
async with org_connection(organization_id) as conn, translate_db_errors():
|
|
|
|
|
|
source = await conn.fetchval("SELECT source FROM course WHERE id = $1", course_id)
|
|
|
|
|
|
if source is None:
|
|
|
|
|
|
raise app_error(404, "NOT_FOUND", "Banen finnes ikke.")
|
|
|
|
|
|
if source == "official":
|
|
|
|
|
|
raise app_error(
|
|
|
|
|
|
400,
|
|
|
|
|
|
"VALIDATION_FAILED",
|
|
|
|
|
|
"Offisielle baner får hullene sine fra teeoff -- kan ikke legges til manuelt.",
|
|
|
|
|
|
)
|
|
|
|
|
|
existing = await conn.fetchval("SELECT count(*) FROM hole WHERE course_id = $1", course_id)
|
|
|
|
|
|
if existing > 0:
|
|
|
|
|
|
raise app_error(
|
|
|
|
|
|
409, "DUPLICATE", "Banen har allerede registrert hull -- kan ikke settes opp på nytt."
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
rows = []
|
|
|
|
|
|
for h in body:
|
|
|
|
|
|
rows.append(
|
|
|
|
|
|
await conn.fetchrow(
|
|
|
|
|
|
"""
|
|
|
|
|
|
INSERT INTO hole (organization_id, course_id, hole_number, par, stroke_index)
|
|
|
|
|
|
VALUES ($1, $2, $3, $4, $5)
|
|
|
|
|
|
RETURNING hole_number, par, stroke_index
|
|
|
|
|
|
""",
|
|
|
|
|
|
organization_id,
|
|
|
|
|
|
course_id,
|
|
|
|
|
|
h.hole_number,
|
|
|
|
|
|
h.par,
|
|
|
|
|
|
h.stroke_index,
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
return sorted((Hole(**dict(r)) for r in rows), key=lambda h: h.hole_number)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-08-04 11:34:28 +02:00
|
|
|
|
# --- Manuell baneoppretting med fullt hull-/utslagoppsett (2026-08-04) ------
|
|
|
|
|
|
#
|
|
|
|
|
|
# `create_course` over tok frem til nå KUN imot et navn -- ingen vei til
|
|
|
|
|
|
# hull/utslag fantes for en manuelt opprettet org-bane (kun de to ubrukte
|
|
|
|
|
|
# sub-ressurs-endepunktene over, `create_tee`/`create_holes`, som ALDRI fikk
|
|
|
|
|
|
# noe frontend-kallsted). Brukeren oppdaget dette hullet: "opprett manuell
|
|
|
|
|
|
# bane" i turneringsmodulen var i praksis ubrukelig uten et sted å taste inn
|
|
|
|
|
|
# hull/par/hcp-indeks/utslag etterpå.
|
|
|
|
|
|
#
|
|
|
|
|
|
# Denne utvidede varianten dekker begge deler i ÉN innsending (speiler
|
|
|
|
|
|
# `PersonalCourseCreate` i rounds.py sin form), OG publiserer samtidig en
|
|
|
|
|
|
# kopi til det delte, plattform-omfattende bane-biblioteket
|
|
|
|
|
|
# (`personal_course`, 020_personal_rounds.sql) -- brukeren bekreftet
|
|
|
|
|
|
# eksplisitt at custom-baner opprettet via den nye mal-baserte flyten skal
|
|
|
|
|
|
# "lagres og være offentlige", slik at samme bane kan brukes som mal av
|
|
|
|
|
|
# andre organisasjoner OG i single-runde-modulen. `personal_course` har
|
|
|
|
|
|
# ingen RLS/organization_id (bevisst globalt siden migrasjon 020) -- å skrive
|
|
|
|
|
|
# til den fra en org-scopet tilkobling er trygt, ingen policy er i veien.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class CourseCreate(BaseModel):
|
|
|
|
|
|
name: str = Field(min_length=1, max_length=200)
|
|
|
|
|
|
# Enten BEGGE er med, eller INGEN -- ufullstendig oppsett (kun holes,
|
|
|
|
|
|
# ikke tees) gir en tydelig 400 heller enn en course-rad som stille
|
|
|
|
|
|
# mangler halve dataene.
|
|
|
|
|
|
holes: list[HoleCreate] | None = None
|
|
|
|
|
|
tees: list[TeeCreate] | None = None
|
|
|
|
|
|
# Satt når banen opprettes med en offentlig custom-bane (personal_course)
|
|
|
|
|
|
# som mal -- ren attribusjon på den NYE personal_course-publiseringen
|
|
|
|
|
|
# under, ingen vedvarende kobling på selve org-bane-raden (samme
|
|
|
|
|
|
# "engangs-kopi"-filosofi som offisiell TeeOff-import, ADR-019).
|
|
|
|
|
|
template_personal_course_id: str | None = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/orgs/{organization_id}/courses", response_model=Course, status_code=201)
|
|
|
|
|
|
async def create_course(
|
|
|
|
|
|
body: CourseCreate,
|
|
|
|
|
|
organization_id: str = Depends(get_authorized_org),
|
|
|
|
|
|
user: CurrentUser = Depends(get_current_user),
|
|
|
|
|
|
) -> Course:
|
|
|
|
|
|
if (body.holes is None) != (body.tees is None):
|
|
|
|
|
|
raise app_error(400, "VALIDATION_FAILED", "Hull og utslag må enten begge være med, eller ingen av dem.")
|
|
|
|
|
|
if body.holes is not None and body.tees is not None:
|
|
|
|
|
|
numbers = sorted(h.hole_number for h in body.holes)
|
|
|
|
|
|
if len(body.holes) != 18 or numbers != list(range(1, 19)):
|
|
|
|
|
|
raise app_error(400, "VALIDATION_FAILED", "Må oppgi nøyaktig 18 hull, nummerert 1-18.")
|
|
|
|
|
|
for t in body.tees:
|
|
|
|
|
|
genders = [r.gender for r in t.ratings]
|
|
|
|
|
|
if len(genders) != len(set(genders)):
|
|
|
|
|
|
raise app_error(
|
|
|
|
|
|
400, "VALIDATION_FAILED", "Kan ikke oppgi to ratinger for samme kjønn på ett utslag."
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
async with org_connection(organization_id) as conn, translate_db_errors():
|
|
|
|
|
|
async with conn.transaction():
|
|
|
|
|
|
row = await conn.fetchrow(
|
|
|
|
|
|
f"""
|
|
|
|
|
|
INSERT INTO course (organization_id, name, source)
|
|
|
|
|
|
VALUES ($1, $2, 'custom')
|
|
|
|
|
|
RETURNING {_COURSE_COLUMNS}
|
|
|
|
|
|
""",
|
|
|
|
|
|
organization_id,
|
|
|
|
|
|
body.name,
|
|
|
|
|
|
)
|
|
|
|
|
|
if body.holes is not None and body.tees is not None:
|
|
|
|
|
|
for h in body.holes:
|
|
|
|
|
|
await conn.execute(
|
|
|
|
|
|
"""
|
|
|
|
|
|
INSERT INTO hole (organization_id, course_id, hole_number, par, stroke_index)
|
|
|
|
|
|
VALUES ($1, $2, $3, $4, $5)
|
|
|
|
|
|
""",
|
|
|
|
|
|
organization_id,
|
|
|
|
|
|
row["id"],
|
|
|
|
|
|
h.hole_number,
|
|
|
|
|
|
h.par,
|
|
|
|
|
|
h.stroke_index,
|
|
|
|
|
|
)
|
|
|
|
|
|
for t in body.tees:
|
|
|
|
|
|
tee_row = await conn.fetchrow(
|
|
|
|
|
|
"INSERT INTO tee (organization_id, course_id, name) VALUES ($1, $2, $3) RETURNING id",
|
|
|
|
|
|
organization_id,
|
|
|
|
|
|
row["id"],
|
|
|
|
|
|
t.name,
|
|
|
|
|
|
)
|
|
|
|
|
|
for r in t.ratings:
|
|
|
|
|
|
await conn.execute(
|
|
|
|
|
|
"""
|
|
|
|
|
|
INSERT INTO tee_rating
|
|
|
|
|
|
(organization_id, tee_id, scope, gender, course_rating, slope_rating, par)
|
|
|
|
|
|
VALUES ($1, $2, 'full_18', $3, $4, $5, $6)
|
|
|
|
|
|
""",
|
|
|
|
|
|
organization_id,
|
|
|
|
|
|
tee_row["id"],
|
|
|
|
|
|
r.gender,
|
|
|
|
|
|
r.course_rating,
|
|
|
|
|
|
r.slope_rating,
|
|
|
|
|
|
r.par,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
pc_row = await conn.fetchrow(
|
|
|
|
|
|
"""
|
|
|
|
|
|
INSERT INTO personal_course (name, created_by_user_id, forked_from_id)
|
|
|
|
|
|
VALUES ($1, $2, $3) RETURNING id
|
|
|
|
|
|
""",
|
|
|
|
|
|
body.name,
|
|
|
|
|
|
user.user_id,
|
|
|
|
|
|
body.template_personal_course_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
for h in body.holes:
|
|
|
|
|
|
await conn.execute(
|
|
|
|
|
|
"""
|
|
|
|
|
|
INSERT INTO personal_course_hole (personal_course_id, hole_number, par, stroke_index)
|
|
|
|
|
|
VALUES ($1, $2, $3, $4)
|
|
|
|
|
|
""",
|
|
|
|
|
|
pc_row["id"],
|
|
|
|
|
|
h.hole_number,
|
|
|
|
|
|
h.par,
|
|
|
|
|
|
h.stroke_index,
|
|
|
|
|
|
)
|
|
|
|
|
|
for t in body.tees:
|
|
|
|
|
|
pc_tee_row = await conn.fetchrow(
|
|
|
|
|
|
"INSERT INTO personal_course_tee (personal_course_id, name) VALUES ($1, $2) RETURNING id",
|
|
|
|
|
|
pc_row["id"],
|
|
|
|
|
|
t.name,
|
|
|
|
|
|
)
|
|
|
|
|
|
for r in t.ratings:
|
|
|
|
|
|
await conn.execute(
|
|
|
|
|
|
"""
|
|
|
|
|
|
INSERT INTO personal_course_tee_rating
|
|
|
|
|
|
(personal_course_tee_id, gender, course_rating, slope_rating, par)
|
|
|
|
|
|
VALUES ($1, $2, $3, $4, $5)
|
|
|
|
|
|
""",
|
|
|
|
|
|
pc_tee_row["id"],
|
|
|
|
|
|
r.gender,
|
|
|
|
|
|
r.course_rating,
|
|
|
|
|
|
r.slope_rating,
|
|
|
|
|
|
r.par,
|
|
|
|
|
|
)
|
|
|
|
|
|
return Course(**dict(row))
|
|
|
|
|
|
|
|
|
|
|
|
|
Bygget, og verifisert grundig mot ekte infrastruktur — inkludert et ekte kall mot teeoff_api (søkte opp «Borregaard», importerte Borregaard Golfklubb sin 18-hulls hovedbane med alle hull, 4 tee-farger × kjønn, ratinger, og opprettet faktisk en økt med den importerte banen). Duplikat-import ble korrekt avvist (409), kryss-org-isolasjon holder, test_isolation.sql 12/12.
To ting gjenstår, begge mot ekte infrastruktur — vil du bekrefte at jeg går videre?
Migrasjon 010_official_course_unique_ref.sql mot ekte teecup_db — kun én ny partiell unik-indeks (organization_id, external_course_ref), rører ingen eksisterende rader (alle er source='custom' med external_course_ref IS NULL i dag)
docker compose up -d --build teecup_api teecup_frontend — ny backend-kode (courses.py, teeoff_client.py, httpx-avhengighet) + ny frontend-kode (bane-søk mot teeoff i program-skjemaet)
2026-07-18 12:44:05 +02:00
|
|
|
|
# --- Offisiell banedata fra teeoff (ADR-019) ---------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class OfficialFacility(BaseModel):
|
|
|
|
|
|
slug: str
|
|
|
|
|
|
name: str
|
|
|
|
|
|
city: str | None = None
|
|
|
|
|
|
county: str | None = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class OfficialCourseOption(BaseModel):
|
|
|
|
|
|
teeoff_course_id: int
|
|
|
|
|
|
name: str
|
|
|
|
|
|
is_main_course: bool
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class OfficialFacilityDetail(BaseModel):
|
|
|
|
|
|
slug: str
|
|
|
|
|
|
name: str
|
|
|
|
|
|
courses: list[OfficialCourseOption]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class OfficialCourseImport(BaseModel):
|
|
|
|
|
|
facility_slug: str
|
|
|
|
|
|
teeoff_course_id: int
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/orgs/{organization_id}/courses/official-search", response_model=list[OfficialFacility])
|
|
|
|
|
|
async def search_official_courses(
|
|
|
|
|
|
q: str = Query(default=""),
|
|
|
|
|
|
organization_id: str = Depends(get_authorized_org),
|
|
|
|
|
|
) -> list[OfficialFacility]:
|
|
|
|
|
|
try:
|
|
|
|
|
|
facilities = await teeoff_client.search_facilities(q)
|
|
|
|
|
|
except teeoff_client.TeeoffUnavailableError:
|
|
|
|
|
|
raise app_error(502, "EXTERNAL_SERVICE_UNAVAILABLE", "Klarte ikke å hente baner fra teeoff akkurat nå.")
|
|
|
|
|
|
return [
|
|
|
|
|
|
OfficialFacility(slug=f["slug"], name=f["name"], city=f.get("city"), county=f.get("county"))
|
|
|
|
|
|
for f in facilities
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get(
|
|
|
|
|
|
"/orgs/{organization_id}/courses/official-search/{slug}",
|
|
|
|
|
|
response_model=OfficialFacilityDetail,
|
|
|
|
|
|
)
|
|
|
|
|
|
async def get_official_facility(
|
|
|
|
|
|
slug: str,
|
|
|
|
|
|
organization_id: str = Depends(get_authorized_org),
|
|
|
|
|
|
) -> OfficialFacilityDetail:
|
|
|
|
|
|
try:
|
|
|
|
|
|
facility = await teeoff_client.get_facility(slug)
|
|
|
|
|
|
except teeoff_client.TeeoffNotFoundError:
|
|
|
|
|
|
raise app_error(404, "NOT_FOUND", "Anlegget finnes ikke i teeoff.")
|
|
|
|
|
|
except teeoff_client.TeeoffUnavailableError:
|
|
|
|
|
|
raise app_error(502, "EXTERNAL_SERVICE_UNAVAILABLE", "Klarte ikke å hente baneinfo fra teeoff akkurat nå.")
|
|
|
|
|
|
|
|
|
|
|
|
courses = [
|
|
|
|
|
|
OfficialCourseOption(
|
|
|
|
|
|
teeoff_course_id=c["id"],
|
|
|
|
|
|
name=c["name"],
|
|
|
|
|
|
is_main_course=bool(c.get("is_main_course")),
|
|
|
|
|
|
)
|
|
|
|
|
|
# Kun 18-hulls baner kan importeres foreløpig (ADR-019) -- filtrert
|
|
|
|
|
|
# her slik at organisator aldri ser et valg som senere feiler.
|
|
|
|
|
|
for c in facility.get("courses", [])
|
|
|
|
|
|
if len(c.get("holes") or []) == 18
|
|
|
|
|
|
]
|
|
|
|
|
|
return OfficialFacilityDetail(slug=facility["slug"], name=facility["name"], courses=courses)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/orgs/{organization_id}/courses/official-import", response_model=Course, status_code=201)
|
|
|
|
|
|
async def import_official_course(
|
|
|
|
|
|
body: OfficialCourseImport,
|
|
|
|
|
|
organization_id: str = Depends(get_authorized_org),
|
|
|
|
|
|
) -> Course:
|
2026-07-18 22:21:22 +02:00
|
|
|
|
external_ref = f"{body.facility_slug}:{body.teeoff_course_id}"
|
|
|
|
|
|
|
|
|
|
|
|
# Idempotent: importerer organisatoren samme bane på nytt (vanlig --
|
|
|
|
|
|
# flere økter spilles ofte på samme bane), gi tilbake den EKSISTERENDE
|
|
|
|
|
|
# raden i stedet for en 409-feil. Sjekket FØR noe teeoff-kall gjøres,
|
|
|
|
|
|
# både raskere og mer robust mot at teeoff er nede akkurat da. Fant
|
|
|
|
|
|
# og fikset 2026-07-18 etter en reell brukerrapport: duplikat-sperren
|
|
|
|
|
|
# (migrasjon 010) var riktig i prinsippet, men ga ingen vei videre for
|
|
|
|
|
|
# den helt vanlige "bruk samme bane på runde 2" siden av saken.
|
|
|
|
|
|
async with org_connection(organization_id) as conn:
|
|
|
|
|
|
existing = await conn.fetchrow(
|
|
|
|
|
|
f"SELECT {_COURSE_COLUMNS} FROM course WHERE external_course_ref = $1",
|
|
|
|
|
|
external_ref,
|
|
|
|
|
|
)
|
|
|
|
|
|
if existing is not None:
|
|
|
|
|
|
return Course(**dict(existing))
|
|
|
|
|
|
|
Bygget, og verifisert grundig mot ekte infrastruktur — inkludert et ekte kall mot teeoff_api (søkte opp «Borregaard», importerte Borregaard Golfklubb sin 18-hulls hovedbane med alle hull, 4 tee-farger × kjønn, ratinger, og opprettet faktisk en økt med den importerte banen). Duplikat-import ble korrekt avvist (409), kryss-org-isolasjon holder, test_isolation.sql 12/12.
To ting gjenstår, begge mot ekte infrastruktur — vil du bekrefte at jeg går videre?
Migrasjon 010_official_course_unique_ref.sql mot ekte teecup_db — kun én ny partiell unik-indeks (organization_id, external_course_ref), rører ingen eksisterende rader (alle er source='custom' med external_course_ref IS NULL i dag)
docker compose up -d --build teecup_api teecup_frontend — ny backend-kode (courses.py, teeoff_client.py, httpx-avhengighet) + ny frontend-kode (bane-søk mot teeoff i program-skjemaet)
2026-07-18 12:44:05 +02:00
|
|
|
|
try:
|
|
|
|
|
|
facility = await teeoff_client.get_facility(body.facility_slug)
|
|
|
|
|
|
except teeoff_client.TeeoffNotFoundError:
|
|
|
|
|
|
raise app_error(404, "NOT_FOUND", "Anlegget finnes ikke i teeoff.")
|
|
|
|
|
|
except teeoff_client.TeeoffUnavailableError:
|
|
|
|
|
|
raise app_error(502, "EXTERNAL_SERVICE_UNAVAILABLE", "Klarte ikke å hente baneinfo fra teeoff akkurat nå.")
|
|
|
|
|
|
|
|
|
|
|
|
course_data = next(
|
|
|
|
|
|
(c for c in facility.get("courses", []) if c.get("id") == body.teeoff_course_id), None
|
|
|
|
|
|
)
|
|
|
|
|
|
if course_data is None:
|
|
|
|
|
|
raise app_error(404, "NOT_FOUND", "Banen finnes ikke på dette anlegget i teeoff.")
|
|
|
|
|
|
|
|
|
|
|
|
holes = course_data.get("holes") or []
|
|
|
|
|
|
tees = course_data.get("tees") or []
|
|
|
|
|
|
|
|
|
|
|
|
# ADR-019 Beslutning C: hele importen feiler tydelig FØR noe skrives,
|
|
|
|
|
|
# aldri en delvis importert bane som senere feiler i handicap-beregning.
|
|
|
|
|
|
if len(holes) != 18:
|
|
|
|
|
|
raise app_error(
|
|
|
|
|
|
400, "EXTERNAL_DATA_INCOMPLETE", "Banen har ikke 18 registrerte hull i teeoff ennå."
|
|
|
|
|
|
)
|
|
|
|
|
|
for h in holes:
|
|
|
|
|
|
if h.get("par") is None or h.get("hcp_index") is None:
|
|
|
|
|
|
raise app_error(
|
|
|
|
|
|
400,
|
|
|
|
|
|
"EXTERNAL_DATA_INCOMPLETE",
|
|
|
|
|
|
"Banen mangler par eller HCP-index på ett eller flere hull i teeoff.",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-08-21 17:11:41 +02:00
|
|
|
|
# Funnet 2026-08-21: teeoff sin egen kildedata for en bane kan ha
|
|
|
|
|
|
# UGYLDIGE hcp-indekser (f.eks. duplisert -- én verdi to ganger, en
|
|
|
|
|
|
# annen mangler helt) uten at feltene over fanger det opp (de sjekker
|
|
|
|
|
|
# bare at HVER rad HAR en verdi, ikke at SETTET av 18 verdier faktisk
|
|
|
|
|
|
# er 1..18). Ubemerket kom dette forbi valideringen og traff i stedet
|
|
|
|
|
|
# `hole`-tabellens UNIQUE-constraint på (course_id, stroke_index) ved
|
|
|
|
|
|
# selve INSERT-en -- fanget opp av den generiske
|
|
|
|
|
|
# translate_db_errors()-oversetteren og vist til brukeren som
|
|
|
|
|
|
# "allerede importert" (samme kode som et EKTE duplikat-forsøk), en
|
|
|
|
|
|
# totalt misvisende feilmelding som skjulte den ekte årsaken. Sjekkes
|
|
|
|
|
|
# derfor eksplisitt her, FØR noe skrives, med sin egen ærlige
|
|
|
|
|
|
# feilmelding -- samme "feil tydelig FØR skriving"-prinsipp som
|
|
|
|
|
|
# sjekken over (ADR-019 Beslutning C).
|
|
|
|
|
|
hole_numbers = [h["hole_number"] for h in holes]
|
|
|
|
|
|
hcp_indexes = [h["hcp_index"] for h in holes]
|
|
|
|
|
|
if sorted(hole_numbers) != list(range(1, 19)) or sorted(hcp_indexes) != list(range(1, 19)):
|
|
|
|
|
|
raise app_error(
|
|
|
|
|
|
400,
|
|
|
|
|
|
"EXTERNAL_DATA_INVALID",
|
|
|
|
|
|
"Banedataen fra teeoff er ugyldig (hull-nummer eller HCP-indeks er duplisert eller "
|
|
|
|
|
|
"mangler for ett eller flere hull) -- dette må rettes hos teeoff før banen kan "
|
|
|
|
|
|
"importeres.",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
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-019 Beslutning D: kun full_18-rating importeres, teeoff har ingen
|
|
|
|
|
|
# egen front9/back9-rating. ADR-029: kjønn hører til RATINGEN, ikke selve
|
|
|
|
|
|
# utslaget -- ett teeoff-utslag blir HER importert som ÉN fysisk
|
|
|
|
|
|
# `tee`-rad med inntil to kjønnsspesifikke ratinger (tidligere ble dette
|
|
|
|
|
|
# feilaktig importert som to separate tee-rader, én per kjønn).
|
|
|
|
|
|
tee_inputs: list[tuple[str, list[tuple[str, float, int]]]] = []
|
Bygget, og verifisert grundig mot ekte infrastruktur — inkludert et ekte kall mot teeoff_api (søkte opp «Borregaard», importerte Borregaard Golfklubb sin 18-hulls hovedbane med alle hull, 4 tee-farger × kjønn, ratinger, og opprettet faktisk en økt med den importerte banen). Duplikat-import ble korrekt avvist (409), kryss-org-isolasjon holder, test_isolation.sql 12/12.
To ting gjenstår, begge mot ekte infrastruktur — vil du bekrefte at jeg går videre?
Migrasjon 010_official_course_unique_ref.sql mot ekte teecup_db — kun én ny partiell unik-indeks (organization_id, external_course_ref), rører ingen eksisterende rader (alle er source='custom' med external_course_ref IS NULL i dag)
docker compose up -d --build teecup_api teecup_frontend — ny backend-kode (courses.py, teeoff_client.py, httpx-avhengighet) + ny frontend-kode (bane-søk mot teeoff i program-skjemaet)
2026-07-18 12:44:05 +02:00
|
|
|
|
for t in tees:
|
|
|
|
|
|
name = t.get("name") or "Tee"
|
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
|
|
|
|
ratings: list[tuple[str, float, int]] = []
|
Bygget, og verifisert grundig mot ekte infrastruktur — inkludert et ekte kall mot teeoff_api (søkte opp «Borregaard», importerte Borregaard Golfklubb sin 18-hulls hovedbane med alle hull, 4 tee-farger × kjønn, ratinger, og opprettet faktisk en økt med den importerte banen). Duplikat-import ble korrekt avvist (409), kryss-org-isolasjon holder, test_isolation.sql 12/12.
To ting gjenstår, begge mot ekte infrastruktur — vil du bekrefte at jeg går videre?
Migrasjon 010_official_course_unique_ref.sql mot ekte teecup_db — kun én ny partiell unik-indeks (organization_id, external_course_ref), rører ingen eksisterende rader (alle er source='custom' med external_course_ref IS NULL i dag)
docker compose up -d --build teecup_api teecup_frontend — ny backend-kode (courses.py, teeoff_client.py, httpx-avhengighet) + ny frontend-kode (bane-søk mot teeoff i program-skjemaet)
2026-07-18 12:44:05 +02:00
|
|
|
|
if t.get("cr_men") is not None and t.get("slope_men") is not None:
|
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
|
|
|
|
ratings.append(("m", float(t["cr_men"]), int(t["slope_men"])))
|
Bygget, og verifisert grundig mot ekte infrastruktur — inkludert et ekte kall mot teeoff_api (søkte opp «Borregaard», importerte Borregaard Golfklubb sin 18-hulls hovedbane med alle hull, 4 tee-farger × kjønn, ratinger, og opprettet faktisk en økt med den importerte banen). Duplikat-import ble korrekt avvist (409), kryss-org-isolasjon holder, test_isolation.sql 12/12.
To ting gjenstår, begge mot ekte infrastruktur — vil du bekrefte at jeg går videre?
Migrasjon 010_official_course_unique_ref.sql mot ekte teecup_db — kun én ny partiell unik-indeks (organization_id, external_course_ref), rører ingen eksisterende rader (alle er source='custom' med external_course_ref IS NULL i dag)
docker compose up -d --build teecup_api teecup_frontend — ny backend-kode (courses.py, teeoff_client.py, httpx-avhengighet) + ny frontend-kode (bane-søk mot teeoff i program-skjemaet)
2026-07-18 12:44:05 +02:00
|
|
|
|
if t.get("cr_women") is not None and t.get("slope_women") is not None:
|
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
|
|
|
|
ratings.append(("f", float(t["cr_women"]), int(t["slope_women"])))
|
|
|
|
|
|
if ratings:
|
|
|
|
|
|
tee_inputs.append((name, ratings))
|
Bygget, og verifisert grundig mot ekte infrastruktur — inkludert et ekte kall mot teeoff_api (søkte opp «Borregaard», importerte Borregaard Golfklubb sin 18-hulls hovedbane med alle hull, 4 tee-farger × kjønn, ratinger, og opprettet faktisk en økt med den importerte banen). Duplikat-import ble korrekt avvist (409), kryss-org-isolasjon holder, test_isolation.sql 12/12.
To ting gjenstår, begge mot ekte infrastruktur — vil du bekrefte at jeg går videre?
Migrasjon 010_official_course_unique_ref.sql mot ekte teecup_db — kun én ny partiell unik-indeks (organization_id, external_course_ref), rører ingen eksisterende rader (alle er source='custom' med external_course_ref IS NULL i dag)
docker compose up -d --build teecup_api teecup_frontend — ny backend-kode (courses.py, teeoff_client.py, httpx-avhengighet) + ny frontend-kode (bane-søk mot teeoff i program-skjemaet)
2026-07-18 12:44:05 +02:00
|
|
|
|
if not tee_inputs:
|
|
|
|
|
|
raise app_error(
|
|
|
|
|
|
400,
|
|
|
|
|
|
"EXTERNAL_DATA_INCOMPLETE",
|
|
|
|
|
|
"Banen har ingen tee med registrert rating/slope i teeoff ennå.",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
par_total = sum(h["par"] for h in holes)
|
|
|
|
|
|
|
2026-07-18 22:21:22 +02:00
|
|
|
|
# Navn kombinerer anlegg + bane -- "Hovedbanen" alene er tvetydig (mange
|
|
|
|
|
|
# klubber navngir hovedbanen sin nøyaktig likt). Fant og fikset samme
|
|
|
|
|
|
# runde som idempotent-fiksen over, samme bakenforliggende brukerrapport.
|
2026-07-25 06:29:09 +02:00
|
|
|
|
# UNNTAK (2026-07-25, brukerrapport om Tjøme Golfklubb): har anlegget
|
|
|
|
|
|
# bare ÉN bane totalt, er banenavnet ("Hovedbanen") overflødig
|
|
|
|
|
|
# informasjon -- kun anleggsnavnet brukes da.
|
|
|
|
|
|
if len(facility.get("courses", [])) == 1:
|
|
|
|
|
|
course_name = facility.get("name") or "Bane"
|
|
|
|
|
|
else:
|
|
|
|
|
|
course_name = f"{facility.get('name')} – {course_data.get('name') or 'Bane'}"
|
2026-07-18 22:21:22 +02:00
|
|
|
|
|
Bygget, og verifisert grundig mot ekte infrastruktur — inkludert et ekte kall mot teeoff_api (søkte opp «Borregaard», importerte Borregaard Golfklubb sin 18-hulls hovedbane med alle hull, 4 tee-farger × kjønn, ratinger, og opprettet faktisk en økt med den importerte banen). Duplikat-import ble korrekt avvist (409), kryss-org-isolasjon holder, test_isolation.sql 12/12.
To ting gjenstår, begge mot ekte infrastruktur — vil du bekrefte at jeg går videre?
Migrasjon 010_official_course_unique_ref.sql mot ekte teecup_db — kun én ny partiell unik-indeks (organization_id, external_course_ref), rører ingen eksisterende rader (alle er source='custom' med external_course_ref IS NULL i dag)
docker compose up -d --build teecup_api teecup_frontend — ny backend-kode (courses.py, teeoff_client.py, httpx-avhengighet) + ny frontend-kode (bane-søk mot teeoff i program-skjemaet)
2026-07-18 12:44:05 +02:00
|
|
|
|
async with org_connection(organization_id) as conn, translate_db_errors():
|
|
|
|
|
|
course_row = await conn.fetchrow(
|
|
|
|
|
|
f"""
|
|
|
|
|
|
INSERT INTO course (organization_id, name, source, external_course_ref)
|
|
|
|
|
|
VALUES ($1, $2, 'official', $3)
|
|
|
|
|
|
RETURNING {_COURSE_COLUMNS}
|
|
|
|
|
|
""",
|
|
|
|
|
|
organization_id,
|
2026-07-18 22:21:22 +02:00
|
|
|
|
course_name,
|
Bygget, og verifisert grundig mot ekte infrastruktur — inkludert et ekte kall mot teeoff_api (søkte opp «Borregaard», importerte Borregaard Golfklubb sin 18-hulls hovedbane med alle hull, 4 tee-farger × kjønn, ratinger, og opprettet faktisk en økt med den importerte banen). Duplikat-import ble korrekt avvist (409), kryss-org-isolasjon holder, test_isolation.sql 12/12.
To ting gjenstår, begge mot ekte infrastruktur — vil du bekrefte at jeg går videre?
Migrasjon 010_official_course_unique_ref.sql mot ekte teecup_db — kun én ny partiell unik-indeks (organization_id, external_course_ref), rører ingen eksisterende rader (alle er source='custom' med external_course_ref IS NULL i dag)
docker compose up -d --build teecup_api teecup_frontend — ny backend-kode (courses.py, teeoff_client.py, httpx-avhengighet) + ny frontend-kode (bane-søk mot teeoff i program-skjemaet)
2026-07-18 12:44:05 +02:00
|
|
|
|
external_ref,
|
|
|
|
|
|
)
|
|
|
|
|
|
course_id = course_row["id"]
|
|
|
|
|
|
|
|
|
|
|
|
for h in holes:
|
|
|
|
|
|
await conn.execute(
|
|
|
|
|
|
"""
|
|
|
|
|
|
INSERT INTO hole (organization_id, course_id, hole_number, par, stroke_index)
|
|
|
|
|
|
VALUES ($1, $2, $3, $4, $5)
|
|
|
|
|
|
""",
|
|
|
|
|
|
organization_id,
|
|
|
|
|
|
course_id,
|
|
|
|
|
|
h["hole_number"],
|
|
|
|
|
|
h["par"],
|
|
|
|
|
|
h["hcp_index"],
|
|
|
|
|
|
)
|
|
|
|
|
|
|
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
|
|
|
|
for name, ratings in tee_inputs:
|
Bygget, og verifisert grundig mot ekte infrastruktur — inkludert et ekte kall mot teeoff_api (søkte opp «Borregaard», importerte Borregaard Golfklubb sin 18-hulls hovedbane med alle hull, 4 tee-farger × kjønn, ratinger, og opprettet faktisk en økt med den importerte banen). Duplikat-import ble korrekt avvist (409), kryss-org-isolasjon holder, test_isolation.sql 12/12.
To ting gjenstår, begge mot ekte infrastruktur — vil du bekrefte at jeg går videre?
Migrasjon 010_official_course_unique_ref.sql mot ekte teecup_db — kun én ny partiell unik-indeks (organization_id, external_course_ref), rører ingen eksisterende rader (alle er source='custom' med external_course_ref IS NULL i dag)
docker compose up -d --build teecup_api teecup_frontend — ny backend-kode (courses.py, teeoff_client.py, httpx-avhengighet) + ny frontend-kode (bane-søk mot teeoff i program-skjemaet)
2026-07-18 12:44:05 +02:00
|
|
|
|
tee_row = 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
|
|
|
|
"INSERT INTO tee (organization_id, course_id, name) VALUES ($1, $2, $3) RETURNING id",
|
Bygget, og verifisert grundig mot ekte infrastruktur — inkludert et ekte kall mot teeoff_api (søkte opp «Borregaard», importerte Borregaard Golfklubb sin 18-hulls hovedbane med alle hull, 4 tee-farger × kjønn, ratinger, og opprettet faktisk en økt med den importerte banen). Duplikat-import ble korrekt avvist (409), kryss-org-isolasjon holder, test_isolation.sql 12/12.
To ting gjenstår, begge mot ekte infrastruktur — vil du bekrefte at jeg går videre?
Migrasjon 010_official_course_unique_ref.sql mot ekte teecup_db — kun én ny partiell unik-indeks (organization_id, external_course_ref), rører ingen eksisterende rader (alle er source='custom' med external_course_ref IS NULL i dag)
docker compose up -d --build teecup_api teecup_frontend — ny backend-kode (courses.py, teeoff_client.py, httpx-avhengighet) + ny frontend-kode (bane-søk mot teeoff i program-skjemaet)
2026-07-18 12:44:05 +02:00
|
|
|
|
organization_id,
|
|
|
|
|
|
course_id,
|
|
|
|
|
|
name,
|
|
|
|
|
|
)
|
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
|
|
|
|
for gender, course_rating, slope_rating in ratings:
|
|
|
|
|
|
await conn.execute(
|
|
|
|
|
|
"""
|
|
|
|
|
|
INSERT INTO tee_rating
|
|
|
|
|
|
(organization_id, tee_id, scope, gender, course_rating, slope_rating, par)
|
|
|
|
|
|
VALUES ($1, $2, 'full_18', $3, $4, $5, $6)
|
|
|
|
|
|
""",
|
|
|
|
|
|
organization_id,
|
|
|
|
|
|
tee_row["id"],
|
|
|
|
|
|
gender,
|
|
|
|
|
|
course_rating,
|
|
|
|
|
|
slope_rating,
|
|
|
|
|
|
par_total,
|
|
|
|
|
|
)
|
Bygget, og verifisert grundig mot ekte infrastruktur — inkludert et ekte kall mot teeoff_api (søkte opp «Borregaard», importerte Borregaard Golfklubb sin 18-hulls hovedbane med alle hull, 4 tee-farger × kjønn, ratinger, og opprettet faktisk en økt med den importerte banen). Duplikat-import ble korrekt avvist (409), kryss-org-isolasjon holder, test_isolation.sql 12/12.
To ting gjenstår, begge mot ekte infrastruktur — vil du bekrefte at jeg går videre?
Migrasjon 010_official_course_unique_ref.sql mot ekte teecup_db — kun én ny partiell unik-indeks (organization_id, external_course_ref), rører ingen eksisterende rader (alle er source='custom' med external_course_ref IS NULL i dag)
docker compose up -d --build teecup_api teecup_frontend — ny backend-kode (courses.py, teeoff_client.py, httpx-avhengighet) + ny frontend-kode (bane-søk mot teeoff i program-skjemaet)
2026-07-18 12:44:05 +02:00
|
|
|
|
|
|
|
|
|
|
return Course(**dict(course_row))
|
2026-08-12 21:39:13 +02:00
|
|
|
|
|
|
|
|
|
|
|
2026-08-17 12:06:47 +02:00
|
|
|
|
# --- Rangefinder-koordinater for offisielle baner (migrasjon 079) -----------
|
|
|
|
|
|
#
|
|
|
|
|
|
# Frem til nå har rangefinder-koordinater (grønn/hindringer, GPS-basert) KUN
|
|
|
|
|
|
# eksistert for GolfAPI-importerte baner. Offisielle (teeoff-koblede) baner
|
|
|
|
|
|
# har ALDRI hatt noen koordinatkilde -- teeoff selv har ingen slik data, og
|
|
|
|
|
|
# det finnes ingen automatisk importvei (i motsetning til GolfAPI). Dette er
|
|
|
|
|
|
# derfor en RENT MANUELL skrive-vei: selve tolkningsarbeidet (fritekst-navn
|
|
|
|
|
|
# på feltbefarte punkter -> poi_type/location/side_fairway) krever
|
|
|
|
|
|
# dømmekraft og skjer FØR kallet hit, ikke automatisert her (bekreftet
|
|
|
|
|
|
# konkret av Tjøme-runden -- "Voll"/"Bjella" kunne ikke vært gjettet av et
|
|
|
|
|
|
# script). Full erstatning (ikke delta) -- speiler hvordan koordinatsett
|
|
|
|
|
|
# faktisk oppstår i praksis (en ny feltbefaring erstatter den forrige, ikke
|
|
|
|
|
|
# et lite tillegg til den). Lagres GLOBALT (teeoff_course_coordinate, nøkkel
|
|
|
|
|
|
# course.external_course_ref) -- IKKE per organisasjon, samme begrunnelse
|
|
|
|
|
|
# som golfapi_course_coordinate: flere organisasjoner kan importere samme
|
|
|
|
|
|
# fysiske bane fra teeoff hver sin course-rad, koordinatene skal deles.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class CoursePointIn(BaseModel):
|
|
|
|
|
|
hole_number: int = Field(ge=1, le=18)
|
|
|
|
|
|
poi_type: str
|
|
|
|
|
|
location: str | None = None
|
|
|
|
|
|
side_fairway: str | None = None
|
|
|
|
|
|
latitude: float = Field(ge=-90, le=90)
|
|
|
|
|
|
longitude: float = Field(ge=-180, le=180)
|
2026-08-17 23:36:28 +02:00
|
|
|
|
# Eksplisitt, menneske-satt paring av forkant/bakkant til SAMME
|
|
|
|
|
|
# fysiske hindring (migrasjon 080/ADR-084) -- se hazard_group-
|
|
|
|
|
|
# kommentaren i migrasjonsfilen. Frivillig, ingen validering utover
|
|
|
|
|
|
# tekst -- kun meningsfullt unikt innenfor ett hull.
|
|
|
|
|
|
hazard_group: str | None = None
|
2026-08-17 12:06:47 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class CoursePointOut(BaseModel):
|
|
|
|
|
|
hole_number: int
|
|
|
|
|
|
poi_type: str
|
|
|
|
|
|
location: str | None
|
|
|
|
|
|
side_fairway: str | None
|
|
|
|
|
|
latitude: float
|
|
|
|
|
|
longitude: float
|
2026-08-17 23:36:28 +02:00
|
|
|
|
hazard_group: str | None
|
2026-08-17 12:06:47 +02:00
|
|
|
|
|
|
|
|
|
|
|
2026-08-17 23:36:28 +02:00
|
|
|
|
_COORDINATE_COLUMNS = "hole_number, poi_type, location, side_fairway, latitude, longitude, hazard_group"
|
2026-08-17 12:06:47 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/orgs/{organization_id}/courses/{course_id}/coordinates", response_model=list[CoursePointOut])
|
|
|
|
|
|
async def list_course_coordinates(
|
|
|
|
|
|
course_id: str,
|
|
|
|
|
|
organization_id: str = Depends(get_authorized_org),
|
|
|
|
|
|
) -> list[CoursePointOut]:
|
|
|
|
|
|
async with org_connection(organization_id) as conn:
|
|
|
|
|
|
course_row = await conn.fetchrow("SELECT source, external_course_ref FROM course WHERE id = $1", course_id)
|
|
|
|
|
|
if course_row is None:
|
|
|
|
|
|
raise app_error(404, "NOT_FOUND", "Banen finnes ikke.")
|
|
|
|
|
|
if course_row["source"] != "official" or course_row["external_course_ref"] is None:
|
|
|
|
|
|
return []
|
|
|
|
|
|
rows = await conn.fetch(
|
|
|
|
|
|
f"""
|
|
|
|
|
|
SELECT {_COORDINATE_COLUMNS} FROM teeoff_course_coordinate
|
|
|
|
|
|
WHERE external_course_ref = $1 ORDER BY hole_number
|
|
|
|
|
|
""",
|
|
|
|
|
|
course_row["external_course_ref"],
|
|
|
|
|
|
)
|
|
|
|
|
|
return [CoursePointOut(**dict(r)) for r in rows]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.put("/orgs/{organization_id}/courses/{course_id}/coordinates", response_model=list[CoursePointOut])
|
|
|
|
|
|
async def replace_course_coordinates(
|
|
|
|
|
|
course_id: str,
|
|
|
|
|
|
body: list[CoursePointIn],
|
|
|
|
|
|
organization_id: str = Depends(get_authorized_org),
|
|
|
|
|
|
) -> list[CoursePointOut]:
|
|
|
|
|
|
async with org_connection(organization_id) as conn, translate_db_errors():
|
|
|
|
|
|
course_row = await conn.fetchrow("SELECT source, external_course_ref FROM course WHERE id = $1", course_id)
|
|
|
|
|
|
if course_row is None:
|
|
|
|
|
|
raise app_error(404, "NOT_FOUND", "Banen finnes ikke.")
|
|
|
|
|
|
if course_row["source"] != "official":
|
|
|
|
|
|
raise app_error(
|
|
|
|
|
|
400,
|
|
|
|
|
|
"VALIDATION_FAILED",
|
|
|
|
|
|
"Kun offisielle (TeeOff-koblede) baner kan få rangefinder-koordinater satt manuelt.",
|
|
|
|
|
|
)
|
|
|
|
|
|
external_ref = course_row["external_course_ref"]
|
|
|
|
|
|
|
|
|
|
|
|
await conn.execute("DELETE FROM teeoff_course_coordinate WHERE external_course_ref = $1", external_ref)
|
|
|
|
|
|
for p in body:
|
|
|
|
|
|
await conn.execute(
|
|
|
|
|
|
"""
|
|
|
|
|
|
INSERT INTO teeoff_course_coordinate
|
2026-08-17 23:36:28 +02:00
|
|
|
|
(external_course_ref, hole_number, poi_type, location, side_fairway, latitude, longitude, hazard_group)
|
|
|
|
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
2026-08-17 12:06:47 +02:00
|
|
|
|
""",
|
2026-08-17 23:36:28 +02:00
|
|
|
|
external_ref, p.hole_number, p.poi_type, p.location, p.side_fairway, p.latitude, p.longitude, p.hazard_group,
|
2026-08-17 12:06:47 +02:00
|
|
|
|
)
|
|
|
|
|
|
rows = await conn.fetch(
|
|
|
|
|
|
f"""
|
|
|
|
|
|
SELECT {_COORDINATE_COLUMNS} FROM teeoff_course_coordinate
|
|
|
|
|
|
WHERE external_course_ref = $1 ORDER BY hole_number
|
|
|
|
|
|
""",
|
|
|
|
|
|
external_ref,
|
|
|
|
|
|
)
|
|
|
|
|
|
return [CoursePointOut(**dict(r)) for r in rows]
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-08-12 21:39:13 +02:00
|
|
|
|
# --- Internasjonal banedata fra GolfAPI.io (ADR-064) -------------------------
|
|
|
|
|
|
#
|
|
|
|
|
|
# Tredje banekilde, for baner utenfor TeeOffs dekning -- Tjøme Golfklubb var
|
|
|
|
|
|
# den konkrete anledningen (bekreftet IKKE i TeeOff 2026-08-12). ULIKT
|
|
|
|
|
|
# TeeOff-importen over: GolfAPI-kall koster ekte, begrenset budsjett, derfor
|
|
|
|
|
|
# går ALL henting via golfapi_cache.get_or_fetch_golfapi_course() -- aldri
|
|
|
|
|
|
# et direkte golfapi_client-kall herfra. Samme "kopier inn, aldri live
|
|
|
|
|
|
# oppslag"-filosofi og samme "fail loudly on ufullstendige data"-prinsipp
|
|
|
|
|
|
# som ADR-019, men fra en annen kilde.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class InternationalCourseOption(BaseModel):
|
|
|
|
|
|
course_id: str
|
|
|
|
|
|
course_name: str
|
|
|
|
|
|
num_holes: int
|
|
|
|
|
|
has_gps: bool
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class InternationalClub(BaseModel):
|
|
|
|
|
|
club_id: str
|
|
|
|
|
|
club_name: str
|
|
|
|
|
|
city: str | None = None
|
|
|
|
|
|
country: str | None = None
|
|
|
|
|
|
courses: list[InternationalCourseOption]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class InternationalCourseImport(BaseModel):
|
|
|
|
|
|
golfapi_course_id: str
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/orgs/{organization_id}/courses/international-search", response_model=list[InternationalClub])
|
|
|
|
|
|
async def search_international_courses(
|
|
|
|
|
|
q: str = Query(default=""),
|
|
|
|
|
|
organization_id: str = Depends(get_authorized_org),
|
|
|
|
|
|
) -> list[InternationalClub]:
|
|
|
|
|
|
if not q.strip():
|
|
|
|
|
|
return []
|
|
|
|
|
|
try:
|
|
|
|
|
|
clubs = await golfapi_client.search_clubs(name=q)
|
|
|
|
|
|
except golfapi_client.GolfApiNotConfiguredError:
|
|
|
|
|
|
raise app_error(400, "NOT_CONFIGURED", "GolfAPI er ikke konfigurert ennå.")
|
|
|
|
|
|
except golfapi_client.GolfApiUnavailableError:
|
|
|
|
|
|
raise app_error(502, "EXTERNAL_SERVICE_UNAVAILABLE", "Klarte ikke å hente baner fra GolfAPI akkurat nå.")
|
|
|
|
|
|
return [
|
|
|
|
|
|
InternationalClub(
|
|
|
|
|
|
club_id=c["clubID"],
|
|
|
|
|
|
club_name=c["clubName"],
|
|
|
|
|
|
city=c.get("city"),
|
|
|
|
|
|
country=c.get("country"),
|
|
|
|
|
|
courses=[
|
|
|
|
|
|
InternationalCourseOption(
|
|
|
|
|
|
course_id=course["courseID"],
|
|
|
|
|
|
course_name=course.get("courseName") or "Bane",
|
|
|
|
|
|
num_holes=int(course["numHoles"]),
|
|
|
|
|
|
has_gps=bool(int(course.get("hasGPS") or 0)),
|
|
|
|
|
|
)
|
|
|
|
|
|
for course in c.get("courses", [])
|
|
|
|
|
|
],
|
|
|
|
|
|
)
|
|
|
|
|
|
for c in clubs
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/orgs/{organization_id}/courses/international-import", response_model=Course, status_code=201)
|
|
|
|
|
|
async def import_international_course(
|
|
|
|
|
|
body: InternationalCourseImport,
|
|
|
|
|
|
organization_id: str = Depends(get_authorized_org),
|
|
|
|
|
|
) -> Course:
|
|
|
|
|
|
if not settings.GOLFAPI_TOKEN:
|
|
|
|
|
|
raise app_error(400, "NOT_CONFIGURED", "GolfAPI er ikke konfigurert ennå.")
|
|
|
|
|
|
|
|
|
|
|
|
# Idempotent, samme begrunnelse som import_official_course over: samme
|
|
|
|
|
|
# organisasjon importerer ofte samme bane til flere økter.
|
|
|
|
|
|
async with org_connection(organization_id) as conn:
|
|
|
|
|
|
existing = await conn.fetchrow(
|
|
|
|
|
|
f"SELECT {_COURSE_COLUMNS} FROM course WHERE external_course_ref = $1 AND source = 'international'",
|
|
|
|
|
|
body.golfapi_course_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
if existing is not None:
|
|
|
|
|
|
return Course(**dict(existing))
|
|
|
|
|
|
|
|
|
|
|
|
async with org_connection(organization_id) as conn, translate_db_errors():
|
|
|
|
|
|
try:
|
|
|
|
|
|
await golfapi_cache.get_or_fetch_golfapi_course(conn, body.golfapi_course_id)
|
|
|
|
|
|
except golfapi_client.GolfApiNotFoundError:
|
|
|
|
|
|
raise app_error(404, "NOT_FOUND", "Banen finnes ikke hos GolfAPI.")
|
|
|
|
|
|
except golfapi_client.GolfApiUnavailableError:
|
|
|
|
|
|
raise app_error(502, "EXTERNAL_SERVICE_UNAVAILABLE", "Klarte ikke å hente baneinfo fra GolfAPI akkurat nå.")
|
|
|
|
|
|
|
|
|
|
|
|
gc = await conn.fetchrow("SELECT club_name, course_name, city FROM golfapi_course WHERE course_id = $1", body.golfapi_course_id)
|
|
|
|
|
|
holes = await conn.fetch(
|
|
|
|
|
|
"SELECT hole_number, par_men, index_men FROM golfapi_course_hole WHERE golfapi_course_id = $1 ORDER BY hole_number",
|
|
|
|
|
|
body.golfapi_course_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
tees = await conn.fetch(
|
|
|
|
|
|
"SELECT name, course_rating_men, slope_men, course_rating_women, slope_women FROM golfapi_course_tee WHERE golfapi_course_id = $1",
|
|
|
|
|
|
body.golfapi_course_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# ADR-064 (speiler ADR-019 Beslutning C): ufullstendige data feiler
|
|
|
|
|
|
# importen tydelig FØR noe skrives til course/hole/tee.
|
|
|
|
|
|
if len(holes) != 18:
|
|
|
|
|
|
raise app_error(400, "EXTERNAL_DATA_INCOMPLETE", "Banen har ikke 18 registrerte hull hos GolfAPI.")
|
|
|
|
|
|
if not tees:
|
|
|
|
|
|
raise app_error(400, "EXTERNAL_DATA_INCOMPLETE", "Banen har ingen registrerte utslag hos GolfAPI.")
|
2026-08-13 21:24:25 +02:00
|
|
|
|
if not any(
|
|
|
|
|
|
(t["course_rating_men"] is not None and t["slope_men"] is not None)
|
|
|
|
|
|
or (t["course_rating_women"] is not None and t["slope_women"] is not None)
|
|
|
|
|
|
for t in tees
|
|
|
|
|
|
):
|
|
|
|
|
|
raise app_error(
|
|
|
|
|
|
400, "EXTERNAL_DATA_INCOMPLETE", "Ingen av utslagene har en brukbar rating/slope hos GolfAPI."
|
|
|
|
|
|
)
|
2026-08-21 17:11:41 +02:00
|
|
|
|
if any(h["par_men"] is None or h["index_men"] is None for h in holes):
|
|
|
|
|
|
raise app_error(
|
|
|
|
|
|
400, "EXTERNAL_DATA_INCOMPLETE", "Banen mangler par eller HCP-index på ett eller flere hull hos GolfAPI."
|
|
|
|
|
|
)
|
|
|
|
|
|
# Samme sjekk, samme begrunnelse som import_official_course (teeoff)
|
|
|
|
|
|
# -- se kommentaren der. En kilde kan ha gyldig-utseende, men
|
|
|
|
|
|
# dupliserte/manglende index-verdier som ellers først feiler stille
|
|
|
|
|
|
# som en misvisende "allerede importert" ved selve INSERT-en.
|
|
|
|
|
|
hole_numbers = [h["hole_number"] for h in holes]
|
|
|
|
|
|
hcp_indexes = [h["index_men"] for h in holes]
|
|
|
|
|
|
if sorted(hole_numbers) != list(range(1, 19)) or sorted(hcp_indexes) != list(range(1, 19)):
|
|
|
|
|
|
raise app_error(
|
|
|
|
|
|
400,
|
|
|
|
|
|
"EXTERNAL_DATA_INVALID",
|
|
|
|
|
|
"Banedataen fra GolfAPI er ugyldig (hull-nummer eller HCP-indeks er duplisert eller "
|
|
|
|
|
|
"mangler for ett eller flere hull) -- dette må rettes hos GolfAPI før banen kan "
|
|
|
|
|
|
"importeres.",
|
|
|
|
|
|
)
|
2026-08-12 21:39:13 +02:00
|
|
|
|
|
|
|
|
|
|
par_total = sum(h["par_men"] for h in holes)
|
|
|
|
|
|
course_name = f"{gc['club_name']} – {gc['course_name']}" if gc["course_name"] else gc["club_name"]
|
|
|
|
|
|
|
|
|
|
|
|
course_row = await conn.fetchrow(
|
|
|
|
|
|
f"""
|
|
|
|
|
|
INSERT INTO course (organization_id, name, source, external_course_ref)
|
|
|
|
|
|
VALUES ($1, $2, 'international', $3)
|
|
|
|
|
|
RETURNING {_COURSE_COLUMNS}
|
|
|
|
|
|
""",
|
|
|
|
|
|
organization_id,
|
|
|
|
|
|
course_name,
|
|
|
|
|
|
body.golfapi_course_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
course_id = course_row["id"]
|
|
|
|
|
|
|
|
|
|
|
|
for h in holes:
|
|
|
|
|
|
await conn.execute(
|
|
|
|
|
|
"INSERT INTO hole (organization_id, course_id, hole_number, par, stroke_index) VALUES ($1, $2, $3, $4, $5)",
|
|
|
|
|
|
organization_id, course_id, h["hole_number"], h["par_men"], h["index_men"],
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
for t in tees:
|
2026-08-13 21:24:25 +02:00
|
|
|
|
# Reelt funn (Nesbyen, 2026-08-13): GolfAPI kan mangle RATING
|
|
|
|
|
|
# FOR BEGGE kjønn på et utslag (opprinnelig antatt at menns alltid
|
|
|
|
|
|
# finnes, speilet fra ADR-019s teeoff-import) -- begge sjekkes nå
|
|
|
|
|
|
# individuelt, samme mønster som teeoff-importen allerede bruker.
|
|
|
|
|
|
# Utslag uten NOEN brukbar rating hoppes over (usable_tees under).
|
|
|
|
|
|
ratings: list[tuple[str, float, int]] = []
|
|
|
|
|
|
if t["course_rating_men"] is not None and t["slope_men"] is not None:
|
|
|
|
|
|
ratings.append(("m", float(t["course_rating_men"]), int(t["slope_men"])))
|
|
|
|
|
|
if t["course_rating_women"] is not None and t["slope_women"] is not None:
|
|
|
|
|
|
ratings.append(("f", float(t["course_rating_women"]), int(t["slope_women"])))
|
|
|
|
|
|
if not ratings:
|
|
|
|
|
|
continue
|
2026-08-12 21:39:13 +02:00
|
|
|
|
tee_row = await conn.fetchrow(
|
|
|
|
|
|
"INSERT INTO tee (organization_id, course_id, name) VALUES ($1, $2, $3) RETURNING id",
|
|
|
|
|
|
organization_id, course_id, t["name"],
|
|
|
|
|
|
)
|
|
|
|
|
|
for gender, course_rating, slope_rating in ratings:
|
|
|
|
|
|
await conn.execute(
|
|
|
|
|
|
"""
|
|
|
|
|
|
INSERT INTO tee_rating (organization_id, tee_id, scope, gender, course_rating, slope_rating, par)
|
|
|
|
|
|
VALUES ($1, $2, 'full_18', $3, $4, $5, $6)
|
|
|
|
|
|
""",
|
|
|
|
|
|
organization_id, tee_row["id"], gender, course_rating, slope_rating, par_total,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
return Course(**dict(course_row))
|