Æ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:
parent
546900c2d6
commit
1e3ffe634b
6 changed files with 137 additions and 2 deletions
64
CHANGELOG.md
64
CHANGELOG.md
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -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" }),
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Reference in a new issue