Ærlig feilmelding for ugyldig kilde-data ved baneimport (EXTERNAL_DATA_INVALID)

Rotårsak for "banen er allerede importert"-forvirringen rundt Tjøme-baner:
teeoffs egen hull-data hadde en duplisert hcp-indeks (13 to ganger, 14
manglet). Manglende validering lot dette komme gjennom og feile stille på
en UNIQUE-constraint, oversatt til den misvisende DUPLICATE-meldingen.
Legger til eksplisitt 1-18-validering før lagring, med en ærlig feilkode/
-melding, i både teeoff- og GolfAPI-import-stiene og alle fire frontend-
kallstedene.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Erol Haagenrud 2026-08-21 17:11:41 +02:00
parent 546900c2d6
commit 1e3ffe634b
6 changed files with 137 additions and 2 deletions

View file

@ -14020,3 +14020,67 @@ Neste steg:
(klikk gjennom alle fire med ekte turneringsdata, bekreft faktisk
PDF-nedlasting) gjenstår også -- kun kodenivå-verifisering gjort
denne runden.
151. **Rotårsak funnet for "banen er allerede importert"-forvirringen +
ærlig feilmelding lagt til, 2026-08-21.** Bruker rapporterte at
scorekortet for "Tjøme Invitational 2026" viste feil par på hull
2/5/11/14 (par 6/3/6/3 i stedet for 5/4/5/4). Sporet via en Explore-
agent + direkte spørring mot ekte `teecup_db` (bekreftet, ikke
gjettet): organisasjonen "Erol Haagenruds turneringer" har tre
manuelt håndlagde `custom`-baner ("Tjøme"/"Tjøme G&CC"/"Tjøme
Golfklubb Bane", opprettet 2026-08-18) -- INGEN ekte offisiell
import -- og alle 4 berørte runder (begge "Klubbmesterskap 2026" og
begge "Tjøme Invitational 2026") pekte på den håndlagde varianten
med 4 feiltastede par-verdier.
**Den ekte rotårsaken, ikke bare symptomet:** teeoff sin egen
kildedata for "Tjøme Golfklubb Hovedbanen" hadde en reell datafeil
-- hcp-indeks 13 var oppført på to hull, indeks 14 manglet helt.
Reprodusert direkte (kalte teeoff sitt eget `/api/facilities/
tjome-golfklubb`-endepunkt fra API-containeren) og bekreftet med en
tilbakerullet test-transaksjon mot ekte `teecup_db` (BEGIN → forsøkt
ekte innsetting med reelle data → ROLLBACK, ingen varig endring) at
selve databaseskjemaet var uskyldig -- innsettingen gikk knirkefritt
med KORREKT data. `import_official_course` sin validering sjekket at
hvert hull HADDE en hcp-indeks, men aldri at SETTET av 18 verdier
faktisk var 1-18 uten duplikater -- den ugyldige teeoff-dataen kom
derfor gjennom valideringen og feilet først på `hole`-tabellens
UNIQUE-constraint ved selve lagringen. Denne databasefeilen ble
fanget av den generiske `translate_db_errors()`-oversetteren og vist
til brukeren som "allerede importert til organisasjonen" -- NØYAKTIG
samme melding som et ekte duplikat-forsøk, en totalt misvisende
feilmelding som skjulte den ekte årsaken i en måned. Mest sannsynlige
hendelsesforløp: samme teeoff-datafeil blokkerte trolig importen
allerede 2026-08-18, brukeren fikk samme forvirrende melding, ga opp
og tastet banen inn for hånd i stedet -- og introduserte da sine
egne 4 skrivefeil underveis.
Bruker rettet selv kildedataen direkte hos teeoff (hull 16s indeks
korrigert 13→14). Bekreftet live etterpå (samme direkte kall mot
teeoff sitt API) at rettelsen faktisk slo gjennom.
**Kodefiks (uavhengig av teeoffs egen rettelse, forhindrer samme
misvisende feil for ENHVER fremtidig kilde-datafeil):** ny eksplisitt
validering i `import_official_course` OG `import_international_course`
(`app/routers/courses.py`) -- sjekker at hull-nummer- og hcp-indeks-
settene faktisk er 1..18 uten duplikater/hull FØR noe forsøkes
lagret, med en ny, ærlig feilkode (`EXTERNAL_DATA_INVALID`) og
melding som navngir det faktiske problemet i stedet for å late som
det er et duplikat. Frontend-feilhåndtering oppdatert på alle fire
stedene som kaller disse import-endepunktene: `individual-tournament-
detail.tsx` (individuell turnering), `tournament-program.tsx` (Cup-
format, både teeoff- og GolfAPI-import), og `course-template-
editor.tsx` sin "TeeOff-bane som mal"-flyt.
**Verifisert:** rå teeoff-data hentet på nytt etter brukerens
rettelse -- hcp-indeksene er nå korrekt 1-18. `tsc --noEmit` rent,
`python3 -m py_compile` rent på courses.py.
**Neste steg, avtalt med bruker, IKKE gjort ennå:** full opprydning
av de fire berørte rundene -- importer den nå korrekte offisielle
banen inn i "Erol Haagenruds turneringer", pek rundene om, flytt de
54 allerede innsjekkede spillernes utslagstildelinger til riktige
utslag på den nye banen, slett de tre feilaktige duplikat-banene, og
rett par-tallene i de to lekkede kopiene i det globale bane-
malbiblioteket (`personal_course`, brukt av 2 urelaterte personlige
runder -- rettes i stedet for slettes der).

View file

@ -525,6 +525,30 @@ async def import_official_course(
"Banen mangler par eller HCP-index på ett eller flere hull i teeoff.",
)
# 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.",
)
# 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
@ -828,6 +852,24 @@ async def import_international_course(
raise app_error(
400, "EXTERNAL_DATA_INCOMPLETE", "Ingen av utslagene har en brukbar rating/slope hos GolfAPI."
)
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.",
)
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"]

View file

@ -550,7 +550,21 @@ function OfficialCourseList({
credentials: "include",
body: JSON.stringify({ facility_slug: facility.slug, teeoff_course_id: course.teeoff_course_id }),
})
if (!res.ok) throw new Error(`import: ${res.status}`)
if (!res.ok) {
const body = await res.json().catch(() => null)
const code = body?.detail?.code
if (code === "EXTERNAL_DATA_INCOMPLETE") {
setError("Denne banen mangler nok data i teeoff til å importeres ennå.")
} else if (code === "EXTERNAL_DATA_INVALID") {
setError(
body?.detail?.message ??
"Banedataen fra teeoff er ugyldig (f.eks. duplisert hull-indeks) -- dette må rettes hos teeoff før banen kan importeres.",
)
} else {
setError("Klarte ikke å hente banedata fra teeoff. Prøv igjen.")
}
return
}
const imported: { id: string; name: string } = await res.json()
const [holesRes, teesRes] = await Promise.all([
fetch(`/orgs/${organizationId}/courses/${imported.id}/holes`, { credentials: "include" }),

View file

@ -4921,6 +4921,11 @@ function OfficialCourseSearch({
const code = body?.detail?.code
if (code === "EXTERNAL_DATA_INCOMPLETE") {
setError("Denne banen mangler nok data i teeoff til å importeres ennå.")
} else if (code === "EXTERNAL_DATA_INVALID") {
setError(
body?.detail?.message ??
"Banedataen fra teeoff er ugyldig (f.eks. duplisert hull-indeks) -- dette må rettes hos teeoff før banen kan importeres.",
)
} else if (code === "DUPLICATE") {
setError("Denne banen er allerede importert til organisasjonen.")
} else {

View file

@ -1829,6 +1829,11 @@ function OfficialCourseSearch({
const code = body?.detail?.code
if (code === "EXTERNAL_DATA_INCOMPLETE") {
setError("Denne banen mangler nok data i teeoff til å importeres ennå.")
} else if (code === "EXTERNAL_DATA_INVALID") {
setError(
body?.detail?.message ??
"Banedataen fra teeoff er ugyldig (f.eks. duplisert hull-indeks) -- dette må rettes hos teeoff før banen kan importeres.",
)
} else if (code === "DUPLICATE") {
setError("Denne banen er allerede importert til organisasjonen.")
} else {
@ -2033,6 +2038,11 @@ function InternationalCourseSearch({
const code = body?.detail?.code
if (code === "EXTERNAL_DATA_INCOMPLETE") {
setError("Denne banen mangler nok data hos GolfAPI til å importeres ennå.")
} else if (code === "EXTERNAL_DATA_INVALID") {
setError(
body?.detail?.message ??
"Banedataen fra GolfAPI er ugyldig (f.eks. duplisert hull-indeks) -- dette må rettes hos GolfAPI før banen kan importeres.",
)
} else if (code === "NOT_CONFIGURED") {
setError("GolfAPI er ikke konfigurert ennå.")
} else {

File diff suppressed because one or more lines are too long